DVampire
update website
78f6650
// Theme Management
class ThemeManager {
constructor() {
this.theme = localStorage.getItem('theme') || 'light';
this.init();
}
init() {
document.documentElement.setAttribute('data-theme', this.theme);
this.updateThemeIcon();
}
toggle() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', this.theme);
localStorage.setItem('theme', this.theme);
this.updateThemeIcon();
}
updateThemeIcon() {
const lightIcon = document.querySelector('.light-icon');
const darkIcon = document.querySelector('.dark-icon');
if (this.theme === 'light') {
lightIcon.style.display = 'block';
darkIcon.style.display = 'none';
} else {
lightIcon.style.display = 'none';
darkIcon.style.display = 'block';
}
}
}
// Date Management
class DateManager {
constructor() {
// Start with today's date, but it will be updated when we get the actual available date
this.currentDate = new Date();
this.app = null; // Reference to the main app
this.init();
}
init() {
this.updateDateDisplay();
this.bindEvents();
}
setApp(app) {
this.app = app;
}
formatDate(date) {
const options = {
year: 'numeric',
month: 'short',
day: 'numeric'
};
return date.toLocaleDateString('en-US', options);
}
async updateDateDisplay() {
const dateDisplay = document.getElementById('dateDisplay');
if (dateDisplay) {
dateDisplay.textContent = this.formatDate(this.currentDate);
}
// Update button states based on available dates
await this.updateButtonStates();
}
async updateButtonStates() {
try {
// Check if current date is in the future
const today = new Date();
today.setHours(23, 59, 59, 999);
if (this.currentDate > today) {
this.setButtonState('nextDate', false);
this.setButtonState('prevDate', true);
return;
}
// For previous button, always allow going back (unless it's too far in the past)
const minDate = new Date('2020-01-01'); // Reasonable minimum date
this.setButtonState('prevDate', this.currentDate > minDate);
// For next button, only disable if it's today or in the future
this.setButtonState('nextDate', this.currentDate < today);
} catch (error) {
console.error('Error updating button states:', error);
}
}
setButtonState(buttonId, enabled) {
const button = document.getElementById(buttonId);
if (button) {
button.disabled = !enabled;
button.style.opacity = enabled ? '1' : '0.5';
button.style.cursor = enabled ? 'pointer' : 'not-allowed';
}
}
async navigateDate(direction) {
try {
// Calculate target date first
const newDate = new Date(this.currentDate);
newDate.setDate(newDate.getDate() + direction);
// Check if the new date is in the future
const today = new Date();
today.setHours(23, 59, 59, 999); // End of today
if (newDate > today) {
this.showDateLimitNotification('Cannot navigate to future dates');
return;
}
// Update current date
this.currentDate = newDate;
this.updateDateDisplay();
// Show loading animation
const dateStr = this.formatDate(this.currentDate);
const direction_str = direction > 0 ? "next" : "prev";
this.showLoading(`Loading papers for ${dateStr}...`, `Navigating ${direction_str} from Hugging Face`);
// Try to load the target date with direction
if (this.app && this.app.loadDaily) {
await this.app.loadDaily(direction_str);
}
} catch (error) {
console.error('Error navigating date:', error);
this.showDateLimitNotification('Error loading date');
}
}
// Removed old notification functions - now using unified notification system
showLoading(message = 'Loading papers...', submessage = 'Fetching data from Hugging Face') {
const loadingOverlay = document.getElementById('loadingOverlay');
if (loadingOverlay) {
const loadingText = loadingOverlay.querySelector('.loading-text');
const loadingSubtext = loadingOverlay.querySelector('.loading-subtext');
if (loadingText) loadingText.textContent = message;
if (loadingSubtext) loadingSubtext.textContent = submessage;
loadingOverlay.classList.add('show');
}
}
hideLoading() {
const loadingOverlay = document.getElementById('loadingOverlay');
if (loadingOverlay) {
loadingOverlay.classList.remove('show');
}
}
bindEvents() {
const prevBtn = document.getElementById('prevDate');
const nextBtn = document.getElementById('nextDate');
if (prevBtn) {
prevBtn.addEventListener('click', async () => {
await this.navigateDate(-1);
});
}
if (nextBtn) {
nextBtn.addEventListener('click', async () => {
await this.navigateDate(1);
});
}
}
getDateString() {
const pad = (n) => String(n).padStart(2, '0');
return `${this.currentDate.getFullYear()}-${pad(this.currentDate.getMonth()+1)}-${pad(this.currentDate.getDate())}`;
}
}
// Search Management
class SearchManager {
constructor() {
this.init();
}
init() {
this.bindEvents();
}
bindEvents() {
const searchInput = document.querySelector('.search-input');
const aiSearchInput = document.querySelector('.ai-search-input');
searchInput.addEventListener('input', (e) => {
this.handleSearch(e.target.value);
});
aiSearchInput.addEventListener('input', (e) => {
this.handleAISearch(e.target.value);
});
}
handleSearch(query) {
// Implement search functionality
console.log('Search query:', query);
}
handleAISearch(query) {
// Implement AI search functionality
console.log('AI search query:', query);
}
}
// Paper Card Renderer
class PaperCardRenderer {
constructor() {
this.cardsContainer = document.getElementById('cards');
}
generateThumbnail(title) {
// Generate a simple thumbnail based on title
const canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 120;
const ctx = canvas.getContext('2d');
// Create gradient background
const gradient = ctx.createLinearGradient(0, 0, 400, 120);
gradient.addColorStop(0, '#3b82f6');
gradient.addColorStop(1, '#06b6d4');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 400, 120);
// Add text
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.font = 'bold 16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const words = title.split(' ');
const lines = [];
let currentLine = '';
for (const word of words) {
const testLine = currentLine + word + ' ';
const metrics = ctx.measureText(testLine);
if (metrics.width > 350 && currentLine !== '') {
lines.push(currentLine);
currentLine = word + ' ';
} else {
currentLine = testLine;
}
}
lines.push(currentLine);
const yStart = 60 - (lines.length * 20) / 2;
lines.forEach((line, index) => {
ctx.fillText(line.trim(), 200, yStart + index * 20);
});
return canvas.toDataURL();
}
generateAuthorAvatars(authorCount) {
const avatars = [];
const count = Math.min(authorCount, 5);
for (let i = 0; i < count; i++) {
avatars.push(`<li title="Author ${i + 1}"></li>`);
}
return avatars.join('');
}
renderCard(paper) {
const title = paper.title || 'Untitled Paper';
const abstract = paper.abstract || 'No abstract available';
const authors = paper.authors || [];
const authorCount = paper.author_count || authors.length || 0;
const upvotes = paper.upvotes || 0;
const githubStars = paper.github_stars || 0;
const comments = paper.comments || 0;
const submitter = paper.submitter || 'Anonymous';
// Generate thumbnail URL - try to use HF thumbnail if available
const arxivId = paper.arxiv_id;
const thumbnailUrl = arxivId ?
`https://cdn-thumbnails.huggingface.co/social-thumbnails/papers/${arxivId}.png` :
this.generateThumbnail(title);
const authorAvatars = this.generateAuthorAvatars(authorCount);
const card = document.createElement('article');
card.className = 'hf-paper-card';
card.innerHTML = `
<a href="${paper.huggingface_url || '#'}" class="paper-thumbnail-link" target="_blank" rel="noreferrer">
<img src="${thumbnailUrl}" loading="lazy" decoding="async" alt="" class="paper-thumbnail-img">
</a>
<div class="submitted-by-badge">
<span>Submitted by</span>
<div class="submitter-avatar-placeholder">
<i class="fas fa-user"></i>
</div>
${submitter}
</div>
<div class="card-content">
<div class="upvote-section">
<label class="upvote-button">
<input type="checkbox" class="upvote-checkbox">
<svg class="upvote-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 12 12">
<path fill="currentColor" d="M5.19 2.67a.94.94 0 0 1 1.62 0l3.31 5.72a.94.94 0 0 1-.82 1.4H2.7a.94.94 0 0 1-.82-1.4l3.31-5.7v-.02Z"></path>
</svg>
<div class="upvote-count">${upvotes}</div>
</label>
</div>
<div class="paper-info">
<h3 class="paper-title">
<a href="${paper.huggingface_url || '#'}" class="title-link">
${title}
</a>
</h3>
<div class="paper-meta">
<div class="authors-section">
<a href="${paper.huggingface_url || '#'}" class="authors-link">
<ul class="author-avatars-list">
${authorAvatars}
</ul>
<div class="author-count">· ${authorCount} authors</div>
</a>
</div>
<div class="engagement-metrics">
<a href="${paper.huggingface_url || '#'}" class="metric-link">
<svg class="github-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" width="1.03em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 250">
<path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46c6.397 1.185 8.746-2.777 8.746-6.158c0-3.052-.12-13.135-.174-23.83c-35.61 7.742-43.124-15.103-43.124-15.103c-5.823-14.795-14.213-18.73-14.213-18.73c-11.613-7.944.876-7.78.876-7.78c12.853.902 19.621 13.19 19.621 13.19c11.417 19.568 29.945 13.911 37.249 10.64c1.149-8.272 4.466-13.92 8.127-17.116c-28.431-3.236-58.318-14.212-58.318-63.258c0-13.975 5-25.394 13.188-34.358c-1.329-3.224-5.71-16.242 1.24-33.874c0 0 10.749-3.44 35.21 13.121c10.21-2.836 21.16-4.258 32.038-4.307c10.878.049 21.837 1.47 32.066 4.307c24.431-16.56 35.165-13.12 35.165-13.12c6.967 17.63 2.584 30.65 1.255 33.873c8.207 8.964 13.173 20.383 13.173 34.358c0 49.163-29.944 59.988-58.447 63.157c4.591 3.972 8.682 11.762 8.682 23.704c0 17.126-.148 30.91-.148 35.126c0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002C256 57.307 198.691 0 128.001 0zm-80.06 182.34c-.282.636-1.283.827-2.194.39c-.929-.417-1.45-1.284-1.15-1.922c.276-.655 1.279-.838 2.205-.399c.93.418 1.46 1.293 1.139 1.931zm6.296 5.618c-.61.566-1.804.303-2.614-.591c-.837-.892-.994-2.086-.375-2.66c.63-.566 1.787-.301 2.626.591c.838.903 1 2.088.363 2.66zm4.32 7.188c-.785.545-2.067.034-2.86-1.104c-.784-1.138-.784-2.503.017-3.05c.795-.547 2.058-.055 2.861 1.075c.782 1.157.782 2.522-.019 3.08zm7.304 8.325c-.701.774-2.196.566-3.29-.49c-1.119-1.032-1.43-2.496-.726-3.27c.71-.776 2.213-.558 3.315.49c1.11 1.03 1.45 2.505.701 3.27zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033c-1.448-.439-2.395-1.613-2.103-2.626c.301-1.01 1.747-1.484 3.207-1.028c1.446.436 2.396 1.602 2.095 2.622zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95c-1.53.034-2.769-.82-2.786-1.86c0-1.065 1.202-1.932 2.733-1.958c1.522-.03 2.768.818 2.768 1.868zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37c-1.485.271-2.861-.365-3.05-1.386c-.184-1.056.893-2.114 2.376-2.387c1.514-.263 2.868.356 3.061 1.403z" fill="currentColor"></path>
</svg>
<span>${githubStars}</span>
</a>
<a href="${paper.huggingface_url || '#'}" class="metric-link">
<svg class="comment-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
<span>${comments}</span>
</a>
</div>
</div>
</div>
</div>
${paper.arxiv_id ? `
<div class="card-actions">
<button class="eval-button" data-arxiv-id="${paper.arxiv_id}" data-paper-title="${encodeURIComponent(title)}">
<i class="fas fa-spinner fa-spin" style="display: none;"></i>
<i class="fas fa-chart-line eval-icon"></i>
<span class="eval-text">Checking...</span>
</button>
</div>
` : ''}
`;
// Check evaluation status for this paper
if (paper.arxiv_id) {
this.checkEvaluationStatus(card, paper.arxiv_id);
// Store paper data in card for score checking
card.setAttribute('data-paper-data', JSON.stringify(paper));
this.checkPaperScore(card, paper.arxiv_id);
}
return card;
}
renderCards(papers) {
this.cardsContainer.innerHTML = '';
if (!papers || papers.length === 0) {
this.cardsContainer.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 48px; color: var(--text-muted);">
<i class="fas fa-search" style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;"></i>
<h3>No papers found</h3>
<p>Try selecting a different date or check back later.</p>
</div>
`;
return;
}
papers.forEach(paper => {
const card = this.renderCard(paper);
this.cardsContainer.appendChild(card);
});
}
async checkEvaluationStatus(card, arxivId) {
const button = card.querySelector('.eval-button');
const spinner = button.querySelector('.fa-spinner');
const evalIcon = button.querySelector('.eval-icon');
const evalText = button.querySelector('.eval-text');
try {
const response = await fetch(`/api/has-eval/${encodeURIComponent(arxivId)}`);
const data = await response.json();
if (data.exists) {
// Paper has evaluation - show evaluation button
evalIcon.className = 'fas fa-chart-line eval-icon';
evalText.textContent = 'Evaluation';
button.className = 'eval-button evaluation-state';
button.onclick = () => {
window.location.href = `/paper.html?id=${encodeURIComponent(arxivId)}`;
};
// Add re-evaluate button for already evaluated papers
this.addReevaluateButton(card, arxivId);
} else {
// Paper doesn't have evaluation - show evaluate button
evalIcon.className = 'fas fa-play eval-icon';
evalText.textContent = 'Evaluate';
button.className = 'eval-button evaluate-state';
button.onclick = () => {
this.evaluatePaper(button, arxivId);
};
}
} catch (error) {
console.error('Error checking evaluation status:', error);
evalIcon.className = 'fas fa-exclamation-triangle eval-icon';
evalText.textContent = 'Error';
button.className = 'eval-button error-state';
}
}
addReevaluateButton(card, arxivId) {
// Check if re-evaluate button already exists
if (card.querySelector('.reevaluate-button')) {
return;
}
const cardActions = card.querySelector('.card-actions');
if (cardActions) {
const reevaluateButton = document.createElement('button');
reevaluateButton.className = 'reevaluate-button';
reevaluateButton.innerHTML = `
<i class="fas fa-redo"></i>
<span>Re-evaluate</span>
`;
reevaluateButton.onclick = () => {
this.reevaluatePaper(reevaluateButton, arxivId);
};
cardActions.appendChild(reevaluateButton);
}
}
async reevaluatePaper(button, arxivId) {
const icon = button.querySelector('i');
const text = button.querySelector('span');
const originalText = text.textContent;
const originalIcon = icon.className;
// Show loading state
icon.className = 'fas fa-spinner fa-spin';
text.textContent = 'Re-evaluating...';
button.disabled = true;
// Show log message
this.showLogMessage(`Started re-evaluation for paper ${arxivId}`, 'info');
try {
const response = await fetch(`/api/papers/reevaluate/${encodeURIComponent(arxivId)}`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
if (result.status === 'already_running') {
text.textContent = 'Already running';
this.showLogMessage(`Re-evaluation already running for paper ${arxivId}`, 'warning');
setTimeout(() => {
icon.className = originalIcon;
text.textContent = originalText;
button.disabled = false;
}, 2000);
} else {
// Start polling for status
this.pollReevaluationStatus(button, arxivId, originalText, originalIcon);
}
} else {
throw new Error('Failed to start re-evaluation');
}
} catch (error) {
console.error('Error re-evaluating paper:', error);
icon.className = 'fas fa-exclamation-triangle';
text.textContent = 'Error';
this.showLogMessage(`Re-evaluation failed for paper ${arxivId}: ${error.message}`, 'error');
setTimeout(() => {
icon.className = originalIcon;
text.textContent = originalText;
button.disabled = false;
}, 2000);
}
}
async pollReevaluationStatus(button, arxivId, originalText, originalIcon) {
const icon = button.querySelector('i');
const text = button.querySelector('span');
let pollCount = 0;
const maxPolls = 60; // Poll for up to 5 minutes (5s intervals)
const poll = async () => {
try {
const response = await fetch(`/api/papers/evaluate/${encodeURIComponent(arxivId)}/status`);
if (response.ok) {
const status = await response.json();
switch (status.status) {
case 'evaluating':
text.textContent = `Re-evaluating... (${pollCount * 5}s)`;
icon.className = 'fas fa-spinner fa-spin';
this.showLogMessage(`Re-evaluating paper ${arxivId}... (${pollCount * 5}s)`, 'info');
break;
case 'completed':
icon.className = 'fas fa-check';
text.textContent = 'Re-evaluated';
button.disabled = false;
this.showLogMessage(`Re-evaluation completed for paper ${arxivId}`, 'success');
// Refresh the page to show updated results
setTimeout(() => {
window.location.reload();
}, 1000);
return;
case 'failed':
icon.className = 'fas fa-exclamation-triangle';
text.textContent = 'Failed';
button.disabled = false;
this.showLogMessage(`Re-evaluation failed for paper ${arxivId}`, 'error');
return;
default:
text.textContent = `Status: ${status.status}`;
}
pollCount++;
if (pollCount < maxPolls) {
setTimeout(poll, 5000);
} else {
icon.className = 'fas fa-clock';
text.textContent = 'Timeout';
button.disabled = false;
this.showLogMessage(`Re-evaluation timeout for paper ${arxivId}`, 'warning');
}
} else {
throw new Error('Failed to get status');
}
} catch (error) {
console.error('Error polling re-evaluation status:', error);
icon.className = 'fas fa-exclamation-triangle';
text.textContent = 'Error';
button.disabled = false;
}
};
poll();
}
async checkPaperScore(card, arxivId) {
try {
// First check if the card already has score data from the API response
const cardData = card.getAttribute('data-paper-data');
if (cardData) {
const paperData = JSON.parse(cardData);
if (paperData.overall_score !== null && paperData.overall_score !== undefined) {
this.displayScoreBadge(card, paperData.overall_score, arxivId);
return;
}
}
// Fallback to API call if no score data in card
const response = await fetch(`/api/paper-score/${encodeURIComponent(arxivId)}`);
const data = await response.json();
console.log(`Paper score data for ${arxivId}:`, data);
if (data.has_score && data.score !== null) {
this.displayScoreBadge(card, data.score, arxivId);
}
} catch (error) {
console.error('Error checking paper score:', error);
}
}
displayScoreBadge(card, score, arxivId) {
// Create score badge
const scoreBadge = document.createElement('div');
scoreBadge.className = 'score-badge';
const formattedScore = parseFloat(score).toFixed(1);
// Determine score color based on value (0-4 scale)
const scoreValue = parseFloat(score);
let scoreColor = 'var(--accent-primary)';
if (scoreValue >= 3.0) {
scoreColor = 'var(--accent-success)';
} else if (scoreValue >= 2.0) {
scoreColor = 'var(--accent-warning)';
} else if (scoreValue < 1.0) {
scoreColor = 'var(--accent-danger)';
}
scoreBadge.style.background = `linear-gradient(135deg, ${scoreColor}, ${scoreColor}dd)`;
scoreBadge.innerHTML = `
<span class="score-number">${formattedScore}</span>
<span class="score-label">Overall</span>
`;
// Add click handler to navigate to evaluation page
scoreBadge.onclick = () => {
window.location.href = `/paper.html?id=${encodeURIComponent(arxivId)}`;
};
// Add to card with animation
card.appendChild(scoreBadge);
scoreBadge.style.opacity = '0';
scoreBadge.style.transform = 'scale(0.8) translateY(10px)';
// Animate in
setTimeout(() => {
scoreBadge.style.transition = 'all 0.3s ease';
scoreBadge.style.opacity = '1';
scoreBadge.style.transform = 'scale(1) translateY(0)';
}, 100);
}
async evaluatePaper(button, arxivId, isReevaluate = false) {
const spinner = button.querySelector('.fa-spinner');
const evalIcon = button.querySelector('.eval-icon');
const evalText = button.querySelector('.eval-text');
const paperTitle = button.getAttribute('data-paper-title');
// Clear any existing state classes and show loading state
button.className = 'eval-button started-state';
spinner.style.display = 'inline-block';
evalIcon.style.display = 'none';
evalText.textContent = isReevaluate ? 'Re-starting...' : 'Starting...';
button.disabled = true;
try {
// First, check if paper exists in database, if not, insert it
const paperData = {
arxiv_id: arxivId,
title: decodeURIComponent(paperTitle),
authors: "Unknown Authors", // We don't have authors in the card data
abstract: "No abstract available",
categories: "Unknown",
published_date: new Date().toISOString().split('T')[0]
};
// Try to insert the paper (this will work even if it already exists)
await fetch('/api/papers/insert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(paperData)
});
// Start evaluation
const url = isReevaluate ?
`/api/papers/reevaluate/${encodeURIComponent(arxivId)}` :
`/api/papers/evaluate/${encodeURIComponent(arxivId)}`;
const response = await fetch(url, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
if (result.status === 'already_evaluated' && !isReevaluate) {
// Paper was already evaluated, redirect to evaluation page
window.location.href = `/paper.html?id=${encodeURIComponent(arxivId)}`;
} else {
// Evaluation started, show progress and poll for status
evalText.textContent = isReevaluate ? 'Re-started...' : 'Started...';
button.className = 'eval-button started-state';
// Start polling for status
this.pollEvaluationStatus(button, arxivId, isReevaluate);
}
} else {
throw new Error('Failed to start evaluation');
}
} catch (error) {
console.error('Error evaluating paper:', error);
evalIcon.className = 'fas fa-exclamation-triangle eval-icon';
evalText.textContent = 'Error';
button.className = 'eval-button error-state';
button.disabled = false;
} finally {
spinner.style.display = 'none';
evalIcon.style.display = 'inline-block';
}
}
async pollEvaluationStatus(button, arxivId, isReevaluate = false) {
const evalIcon = button.querySelector('.eval-icon');
const evalText = button.querySelector('.eval-text');
let pollCount = 0;
const maxPolls = 60; // Poll for up to 5 minutes (5s intervals)
// Show log message
const action = isReevaluate ? 're-evaluation' : 'evaluation';
this.showLogMessage(`Started ${action} for paper ${arxivId}`, 'info');
const poll = async () => {
try {
const response = await fetch(`/api/papers/evaluate/${encodeURIComponent(arxivId)}/status`);
if (response.ok) {
const status = await response.json();
switch (status.status) {
case 'evaluating':
evalText.textContent = isReevaluate ? `Re-evaluating... (${pollCount * 5}s)` : `Evaluating... (${pollCount * 5}s)`;
evalIcon.className = 'fas fa-spinner fa-spin eval-icon';
button.className = 'eval-button evaluating-state';
const evaluatingAction = isReevaluate ? 'Re-evaluating' : 'Evaluating';
this.showLogMessage(`${evaluatingAction} paper ${arxivId}... (${pollCount * 5}s)`, 'info');
break;
case 'completed':
evalIcon.className = 'fas fa-check eval-icon';
evalText.textContent = isReevaluate ? 'Re-evaluated' : 'Completed';
button.className = 'eval-button evaluation-state';
button.onclick = () => {
window.location.href = `/paper.html?id=${encodeURIComponent(arxivId)}`;
};
const completedAction = isReevaluate ? 'Re-evaluation' : 'Evaluation';
this.showLogMessage(`${completedAction} completed for paper ${arxivId}`, 'success');
// Add score badge after completion
this.checkPaperScore(button.closest('.hf-paper-card'), arxivId);
// Add re-evaluate button if not already re-evaluating
if (!isReevaluate) {
this.addReevaluateButton(button.closest('.hf-paper-card'), arxivId);
}
return; // Stop polling
case 'failed':
evalIcon.className = 'fas fa-exclamation-triangle eval-icon';
evalText.textContent = 'Failed';
button.className = 'eval-button error-state';
button.disabled = false;
this.showLogMessage(`Evaluation failed for paper ${arxivId}`, 'error');
return; // Stop polling
default:
evalText.textContent = `Processing... (${pollCount * 5}s)`;
button.className = 'eval-button processing-state';
}
}
} catch (error) {
console.error('Error polling status:', error);
this.showLogMessage(`Error checking status for paper ${arxivId}`, 'error');
}
pollCount++;
if (pollCount < maxPolls) {
setTimeout(poll, 5000); // Poll every 5 seconds
} else {
// Timeout
evalIcon.className = 'fas fa-clock eval-icon';
evalText.textContent = 'Timeout';
button.className = 'eval-button error-state';
button.disabled = false;
this.showLogMessage(`Evaluation timeout for paper ${arxivId}`, 'warning');
}
};
// Start polling
setTimeout(poll, 5000); // First poll after 5 seconds
}
showLogMessage(message, type = 'info') {
// Create or get log container
let logContainer = document.getElementById('evaluation-log');
if (!logContainer) {
logContainer = document.createElement('div');
logContainer.id = 'evaluation-log';
logContainer.className = 'evaluation-log';
logContainer.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
max-width: 400px;
max-height: 300px;
overflow-y: auto;
background: var(--bg-primary);
border: 1px solid var(--border-medium);
border-radius: 8px;
padding: 12px;
box-shadow: var(--shadow-lg);
z-index: 1000;
font-size: 12px;
`;
document.body.appendChild(logContainer);
}
// Create log entry
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${type}`;
logEntry.style.cssText = `
margin-bottom: 8px;
padding: 8px;
border-radius: 4px;
border-left: 3px solid;
`;
// Set color based on type
switch (type) {
case 'success':
logEntry.style.borderLeftColor = 'var(--accent-success)';
logEntry.style.backgroundColor = 'rgba(16, 185, 129, 0.1)';
break;
case 'error':
logEntry.style.borderLeftColor = 'var(--accent-danger)';
logEntry.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
break;
case 'warning':
logEntry.style.borderLeftColor = 'var(--accent-warning)';
logEntry.style.backgroundColor = 'rgba(245, 158, 11, 0.1)';
break;
default:
logEntry.style.borderLeftColor = 'var(--accent-primary)';
logEntry.style.backgroundColor = 'rgba(59, 130, 246, 0.1)';
}
const timestamp = new Date().toLocaleTimeString();
logEntry.innerHTML = `
<div style="font-weight: 500; margin-bottom: 2px;">${timestamp}</div>
<div>${message}</div>
`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
// Auto-remove old entries (keep last 10)
const entries = logContainer.querySelectorAll('.log-entry');
if (entries.length > 10) {
entries[0].remove();
}
// Auto-hide success messages after 5 seconds
if (type === 'success') {
setTimeout(() => {
if (logEntry.parentNode) {
logEntry.style.opacity = '0.5';
}
}, 5000);
}
}
}
// Main Application
class PaperIndexApp {
constructor() {
this.themeManager = new ThemeManager();
this.dateManager = new DateManager();
this.dateManager.setApp(this); // Pass app reference to date manager
this.searchManager = new SearchManager();
this.cardRenderer = new PaperCardRenderer();
this.init();
}
init() {
this.bindEvents();
this.dateManager.showLoading('Loading papers...', 'Initializing application');
this.loadDaily();
}
bindEvents() {
// Theme toggle
document.getElementById('themeToggle').addEventListener('click', () => {
this.themeManager.toggle();
});
// Filter buttons
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
});
});
// Batch evaluate button
const batchEvaluateBtn = document.getElementById('batchEvaluateBtn');
console.log('Looking for batchEvaluateBtn:', batchEvaluateBtn);
if (batchEvaluateBtn) {
console.log('Adding click listener to batchEvaluateBtn');
batchEvaluateBtn.addEventListener('click', () => {
console.log('Batch evaluate button clicked');
this.startBatchEvaluation();
});
} else {
console.error('batchEvaluateBtn not found during initialization');
}
}
async loadDaily(direction = null) {
const dateStr = this.dateManager.getDateString();
try {
// Build URL with direction parameter if provided
let url = `/api/daily?date_str=${encodeURIComponent(dateStr)}`;
if (direction) {
url += `&direction=${direction}`;
}
const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to load daily papers');
}
const data = await response.json();
console.log('API Response:', {
requested_date: data.requested_date,
actual_date: data.date,
fallback_used: data.fallback_used,
cards_count: data.cards?.length,
direction: direction
});
// Handle fallback cases - if we got redirected to a different date
if (data.date && data.requested_date && data.date !== data.requested_date) {
console.log('Redirected from', data.requested_date, 'to', data.date);
// Update to the actual date that was found
const actualDate = new Date(data.date);
this.dateManager.currentDate = actualDate;
this.dateManager.updateDateDisplay();
// Show a notification about the redirect
this.showRedirectNotification(data.requested_date, data.date);
} else if (data.cards && data.cards.length === 0) {
// No papers found for the requested date
this.showNoPapersNotification(data.requested_date);
}
// Show cache status if available
if (data.cached) {
this.showCacheNotification(data.cached_at);
}
this.cardRenderer.renderCards(data.cards || []);
} catch (error) {
console.error('Error loading papers:', error);
this.cardRenderer.renderCards([]);
// Show fallback message
this.cardRenderer.cardsContainer.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 48px; color: var(--text-muted);">
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;"></i>
<h3>Unable to load papers</h3>
<p>Backend unavailable on static hosting. Try opening the daily page on Hugging Face:</p>
<a class="action-btn primary" href="https://huggingface.co/papers/date/${encodeURIComponent(dateStr)}" target="_blank" rel="noreferrer">
<i class="fas fa-external-link-alt"></i>Open on Hugging Face
</a>
</div>
`;
} finally {
// Hide loading animation and update button states
this.dateManager.hideLoading();
await this.dateManager.updateDateDisplay();
}
}
async startBatchEvaluation() {
console.log('startBatchEvaluation called');
const button = document.getElementById('batchEvaluateBtn');
if (!button) {
console.error('batchEvaluateBtn not found');
return;
}
console.log('Found batchEvaluateBtn:', button);
// Disable button and show loading state
button.disabled = true;
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>Starting...</span>';
try {
// Find all unevaluated evaluate buttons
const unevaluatedButtons = document.querySelectorAll('.eval-button');
console.log('Found eval buttons:', unevaluatedButtons.length);
const buttonsToClick = [];
unevaluatedButtons.forEach((evalButton, index) => {
const evalText = evalButton.querySelector('.eval-text');
console.log(`Button ${index}:`, evalText ? evalText.textContent : 'no text');
if (evalText && (evalText.textContent === 'Evaluate' || evalText.textContent === 'Check')) {
buttonsToClick.push(evalButton);
}
});
console.log('Buttons to click:', buttonsToClick.length);
if (buttonsToClick.length === 0) {
console.log('No buttons to click');
this.cardRenderer.showLogMessage('All papers have already been evaluated.', 'info');
return;
}
this.cardRenderer.showLogMessage(`Starting batch evaluation of ${buttonsToClick.length} papers...`, 'info');
// Click each evaluate button with delay
for (let i = 0; i < buttonsToClick.length; i++) {
const evalButton = buttonsToClick[i];
// Update button text to show progress
button.innerHTML = `<i class="fas fa-spinner fa-spin"></i><span>Starting ${i + 1} of ${buttonsToClick.length}</span>`;
console.log(`Clicking button ${i + 1}:`, evalButton);
// Simulate click on the evaluate button
evalButton.click();
// Add delay between clicks to avoid API overload
await new Promise(resolve => setTimeout(resolve, 1000));
}
this.cardRenderer.showLogMessage(`Started evaluation for ${buttonsToClick.length} papers. They will complete in the background.`, 'success');
} catch (error) {
console.error('Batch evaluation error:', error);
this.cardRenderer.showLogMessage(`Batch evaluation failed: ${error.message}`, 'error');
} finally {
// Restore button state
button.disabled = false;
button.innerHTML = originalContent;
}
}
// Unified notification system
showNotification(options) {
const {
type = 'info', // 'info', 'success', 'warning', 'error'
title = '',
message = '',
duration = 4000,
icon = null
} = options;
// Remove existing notifications
const existingNotifications = document.querySelectorAll('.notification');
existingNotifications.forEach(notification => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
});
// Create notification element
const notification = document.createElement('div');
notification.className = 'notification';
// Set icon based on type if not provided
let iconClass = icon;
if (!iconClass) {
switch (type) {
case 'success':
iconClass = 'fas fa-check-circle';
break;
case 'warning':
iconClass = 'fas fa-exclamation-triangle';
break;
case 'error':
iconClass = 'fas fa-times-circle';
break;
case 'info':
default:
iconClass = 'fas fa-info-circle';
break;
}
}
// Set colors based on type
let borderColor = 'var(--accent-info)';
let iconColor = 'var(--accent-info)';
switch (type) {
case 'success':
borderColor = 'var(--accent-success)';
iconColor = 'var(--accent-success)';
break;
case 'warning':
borderColor = 'var(--accent-warning)';
iconColor = 'var(--accent-warning)';
break;
case 'error':
borderColor = 'var(--accent-danger)';
iconColor = 'var(--accent-danger)';
break;
}
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: var(--bg-primary);
border: 1px solid ${borderColor};
border-radius: 8px;
padding: 16px;
box-shadow: var(--shadow-lg);
z-index: 1000;
max-width: 350px;
color: var(--text-primary);
animation: slideInRight 0.3s ease;
`;
notification.innerHTML = `
<div style="display: flex; align-items: flex-start; gap: 12px;">
<i class="${iconClass}" style="color: ${iconColor}; font-size: 18px; margin-top: 2px; flex-shrink: 0;"></i>
<div style="flex: 1; min-width: 0;">
${title ? `<div style="font-weight: 600; margin-bottom: 4px; color: var(--text-primary);">${title}</div>` : ''}
${message ? `<div style="font-size: 14px; color: var(--text-secondary); line-height: 1.4;">${message}</div>` : ''}
</div>
</div>
`;
// Add CSS animation
const style = document.createElement('style');
style.textContent = `
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
`;
document.head.appendChild(style);
document.body.appendChild(notification);
// Remove notification after duration
if (duration > 0) {
setTimeout(() => {
if (notification.parentNode) {
notification.style.animation = 'slideInRight 0.3s ease reverse';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 300);
}
}, duration);
}
return notification;
}
// Convenience methods for different notification types
showDateLimitNotification(message) {
this.showNotification({
type: 'warning',
title: 'Date Limit',
message: message,
icon: 'fas fa-calendar-times'
});
}
showNoPapersNotification(date) {
this.showNotification({
type: 'info',
title: 'No Papers Found',
message: `No papers available for ${date}. Try a different date.`,
icon: 'fas fa-search'
});
}
showRedirectNotification(requestedDate, actualDate) {
this.showNotification({
type: 'info',
title: 'Date Redirected',
message: `Papers for ${requestedDate} not available. Showing papers for ${actualDate}.`,
icon: 'fas fa-arrow-right'
});
}
showCacheNotification(cachedAt) {
const cacheTime = new Date(cachedAt).toLocaleTimeString();
this.showNotification({
type: 'info',
title: 'Cached Data',
message: `Showing cached data from ${cacheTime}`,
icon: 'fas fa-database',
duration: 3000
});
}
}
// Initialize the application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new PaperIndexApp();
});