// 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(`
  • `); } 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 = `
    Submitted by
    ${submitter}

    ${title}

    ${paper.arxiv_id ? `
    ` : ''} `; // 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:

    Open on Hugging Face
    `; } 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 = 'Starting...'; 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 = `Starting ${i + 1} of ${buttonsToClick.length}`; 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 = `
    ${title ? `
    ${title}
    ` : ''} ${message ? `
    ${message}
    ` : ''}
    `; // 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(); });