` : ''}
`;
// 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 = `
No papers found
Try selecting a different date or check back later.
`;
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 = `
Re-evaluate
`;
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 = `
${formattedScore}Overall
`;
// 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 = `
${timestamp}
${message}
`;
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 = `
Unable to load papers
Backend unavailable on static hosting. Try opening the daily page on Hugging Face: