Spaces:
Running
Running
// 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(); | |
}); | |