Spaces:
Sleeping
Sleeping
| // 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.init(); | |
| } | |
| init() { | |
| this.updateDateDisplay(); | |
| this.bindEvents(); | |
| } | |
| formatDate(date) { | |
| const options = { | |
| year: 'numeric', | |
| month: 'short', | |
| day: 'numeric' | |
| }; | |
| return date.toLocaleDateString('en-US', options); | |
| } | |
| updateDateDisplay() { | |
| const dateDisplay = document.getElementById('dateDisplay'); | |
| dateDisplay.textContent = this.formatDate(this.currentDate); | |
| } | |
| navigateDate(direction) { | |
| const newDate = new Date(this.currentDate); | |
| newDate.setDate(newDate.getDate() + direction); | |
| this.currentDate = newDate; | |
| this.updateDateDisplay(); | |
| this.loadDaily(); | |
| } | |
| bindEvents() { | |
| document.getElementById('prevDate').addEventListener('click', () => { | |
| this.navigateDate(-1); | |
| }); | |
| document.getElementById('nextDate').addEventListener('click', () => { | |
| 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"> | |
| <a href="/paper.html?id=${encodeURIComponent(paper.arxiv_id)}" class="eval-button"> | |
| <i class="fas fa-chart-line"></i>Evaluation | |
| </a> | |
| </div> | |
| ` : ''} | |
| `; | |
| 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); | |
| }); | |
| } | |
| } | |
| // Main Application | |
| class PaperIndexApp { | |
| constructor() { | |
| this.themeManager = new ThemeManager(); | |
| this.dateManager = new DateManager(); | |
| this.searchManager = new SearchManager(); | |
| this.cardRenderer = new PaperCardRenderer(); | |
| this.init(); | |
| } | |
| init() { | |
| this.bindEvents(); | |
| 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'); | |
| }); | |
| }); | |
| } | |
| async loadDaily() { | |
| const dateStr = this.dateManager.getDateString(); | |
| try { | |
| const response = await fetch(`/api/daily?date_str=${encodeURIComponent(dateStr)}`); | |
| 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 | |
| }); | |
| // Update the date display if a fallback was used or if we got a different date than requested | |
| if (data.date && data.requested_date && data.date !== data.requested_date) { | |
| console.log('Updating date display from', data.requested_date, 'to', data.date); | |
| const fallbackDate = new Date(data.date); | |
| this.dateManager.currentDate = fallbackDate; | |
| this.dateManager.updateDateDisplay(); | |
| // Show a notification about the fallback | |
| this.showFallbackNotification(data.requested_date, data.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> | |
| `; | |
| } | |
| } | |
| showFallbackNotification(requestedDate, actualDate) { | |
| // Create a temporary notification | |
| const notification = document.createElement('div'); | |
| notification.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-medium); | |
| border-radius: 8px; | |
| padding: 16px; | |
| box-shadow: var(--shadow-lg); | |
| z-index: 1000; | |
| max-width: 300px; | |
| color: var(--text-primary); | |
| `; | |
| notification.innerHTML = ` | |
| <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;"> | |
| <i class="fas fa-info-circle" style="color: var(--accent-primary);"></i> | |
| <strong>Date Updated</strong> | |
| </div> | |
| <p style="margin: 0; font-size: 14px; color: var(--text-secondary);"> | |
| Papers for ${requestedDate} not available. Showing latest available: ${actualDate} | |
| </p> | |
| `; | |
| document.body.appendChild(notification); | |
| // Remove notification after 5 seconds | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 5000); | |
| } | |
| showCacheNotification(cachedAt) { | |
| // Create a temporary notification | |
| const notification = document.createElement('div'); | |
| notification.style.cssText = ` | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border-medium); | |
| border-radius: 8px; | |
| padding: 16px; | |
| box-shadow: var(--shadow-lg); | |
| z-index: 1000; | |
| max-width: 300px; | |
| color: var(--text-primary); | |
| `; | |
| const cacheTime = new Date(cachedAt).toLocaleString(); | |
| notification.innerHTML = ` | |
| <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;"> | |
| <i class="fas fa-database" style="color: var(--accent-success);"></i> | |
| <strong>Cached Data</strong> | |
| </div> | |
| <p style="margin: 0; font-size: 14px; color: var(--text-secondary);"> | |
| Showing cached data from ${cacheTime} | |
| </p> | |
| `; | |
| document.body.appendChild(notification); | |
| // Remove notification after 3 seconds | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 3000); | |
| } | |
| } | |
| // Initialize the application when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new PaperIndexApp(); | |
| }); | |