Spaces:
Running
Running
Download app.js from taesiri/arXiv: direct link, hf CLI and curl.
- Browser
- Download file 24.4 kB
-
https://huggingface.co/spaces/taesiri/arXiv/resolve/main/app.js
- Command line
-
hf download hf://spaces/taesiri/arXiv/app.js
-
curl -L -o app.js https://huggingface.co/spaces/taesiri/arXiv/resolve/main/app.js
24.4 kB
| // ---- Configuration ---- | |
| // Single data source: the ArXivSignals public daily feed — the latest arXiv | |
| // announce day, ranked by signal score, refreshed as the pipeline publishes. | |
| const FEED_URL = "https://arxivsignals.io/daily.json"; | |
| // ---- Helpers ---- | |
| function $(id) { return document.getElementById(id); } | |
| async function fetchJSON(url) { | |
| try { | |
| const res = await fetch(url); | |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); | |
| return await res.json(); | |
| } catch (error) { | |
| console.error('Fetch error:', error); | |
| console.error('Failed URL:', url); | |
| if (error.message.includes('Failed to fetch') || error.name === 'TypeError') { | |
| throw new Error(`CORS or network error - ${error.message}`); | |
| } | |
| throw error; | |
| } | |
| } | |
| function formatAuthors(authors) { | |
| if (!Array.isArray(authors) || authors.length === 0) return 'No authors'; | |
| if (authors.length === 1) return authors[0]; | |
| return `${authors[0]} et al. · ${authors.length} authors`; | |
| } | |
| // Feed tag slugs are kebab/snake case ("multi-agent-systems", "computer_vision"). | |
| function formatTag(tag) { | |
| if (!tag) return tag; | |
| return tag.replace(/[_-]/g, ' ').replace(/^\w/, c => c.toUpperCase()); | |
| } | |
| // ---- State Management ---- | |
| const state = { | |
| feed: null, // the whole /daily.json body | |
| papers: [], // feed.papers (already signal-ranked) | |
| searchTerm: '', | |
| selectedTags: new Set(), | |
| selectedCategories: new Set(), | |
| currentPage: 1, | |
| itemsPerPage: 99, | |
| filteredItems: [], | |
| theme: 'dark' | |
| }; | |
| // ---- Data Loading ---- | |
| async function loadFeed() { | |
| showStatus('Loading latest papers…'); | |
| try { | |
| const feed = await fetchJSON(FEED_URL); | |
| state.feed = feed; | |
| state.papers = Array.isArray(feed.papers) ? feed.papers : []; | |
| state.currentPage = 1; | |
| updateHeader(); | |
| filterItems(); | |
| renderCards(); | |
| updateTagButton(); | |
| updateCategoryButton(); | |
| const cats = new Set(); | |
| state.papers.forEach(p => { if (p.categories && p.categories[0]) cats.add(p.categories[0]); }); | |
| showStatus(`${state.filteredItems.length} papers across ${cats.size} categories · ${feed.date}`, 'success'); | |
| return feed; | |
| } catch (error) { | |
| console.error('Failed to load the daily feed:', error); | |
| showStatus('Failed to load the daily feed. Check the browser console for details.', 'error'); | |
| return null; | |
| } | |
| } | |
| function updateHeader() { | |
| const dateLabel = $('currentDate'); | |
| const countLabel = $('dayCount'); | |
| if (dateLabel) dateLabel.textContent = state.feed?.date || '—'; | |
| if (countLabel) { | |
| const n = state.papers.length; | |
| let updated = ''; | |
| if (state.feed?.catalog_updated_at) { | |
| const t = new Date(state.feed.catalog_updated_at.replace(' ', 'T')); | |
| if (!Number.isNaN(t.getTime())) { | |
| updated = ` · updated ${t.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`; | |
| } | |
| } | |
| countLabel.textContent = `${n} papers${updated}`; | |
| } | |
| } | |
| function showStatus(message, type = 'info') { | |
| const statusEl = $('status'); | |
| if (!statusEl) return; | |
| statusEl.textContent = message; | |
| statusEl.className = `status ${type}`; | |
| statusEl.style.display = 'block'; | |
| if (type !== 'error') { | |
| setTimeout(() => { | |
| statusEl.style.display = 'none'; | |
| }, 5000); | |
| } | |
| } | |
| // ---- Rendering ---- | |
| function renderCard(paper) { | |
| const el = document.createElement('article'); | |
| el.className = 'card'; | |
| // Thumbnail wrapper (for positioning action buttons) | |
| const thumbWrapper = document.createElement('div'); | |
| thumbWrapper.className = 'thumb-wrapper'; | |
| const thumb = document.createElement('img'); | |
| thumb.className = 'thumb'; | |
| thumb.src = '/placeholder.svg'; | |
| thumb.alt = 'thumbnail'; | |
| thumb.title = 'Open arXiv abstract'; | |
| if (paper.thumbnail_url) { | |
| thumb.dataset.src = paper.thumbnail_url; | |
| thumb.onerror = () => { thumb.src = '/placeholder.svg'; }; | |
| } | |
| thumb.addEventListener('click', () => { | |
| if (paper.arxiv_url) window.open(paper.arxiv_url, '_blank', 'noopener'); | |
| }); | |
| thumbWrapper.appendChild(thumb); | |
| // "AI summary" chip on the thumbnail, like the portal's explore cards — | |
| // links straight to the summary on the public site (the thumb itself opens arXiv) | |
| if (paper.has_summary && paper.portal_url) { | |
| const chip = document.createElement('a'); | |
| chip.className = 'summary-chip'; | |
| chip.href = paper.portal_url; | |
| chip.target = '_blank'; | |
| chip.rel = 'noopener'; | |
| chip.title = 'Read the AI summary on ArXivSignals'; | |
| chip.textContent = '+ AI summary'; | |
| thumbWrapper.appendChild(chip); | |
| } | |
| // Action buttons (on thumbnail) | |
| const actions = document.createElement('div'); | |
| actions.className = 'card-actions'; | |
| const icons = { | |
| id: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>', | |
| link: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>', | |
| copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>' | |
| }; | |
| if (paper.id) { | |
| const copyIdBtn = createActionButton(icons.id, 'Copy arXiv ID: ' + paper.id, async () => { | |
| await copyToClipboard(paper.id, copyIdBtn, icons.id); | |
| }); | |
| actions.appendChild(copyIdBtn); | |
| } | |
| if (paper.arxiv_url) { | |
| const copyLinkBtn = createActionButton(icons.link, 'Copy arXiv link', async () => { | |
| await copyToClipboard(paper.arxiv_url, copyLinkBtn, icons.link); | |
| }); | |
| actions.appendChild(copyLinkBtn); | |
| } | |
| const summaryText = [paper.one_liner, paper.abstract_snippet].filter(Boolean).join('\n\n'); | |
| if (summaryText) { | |
| const copySummaryBtn = createActionButton(icons.copy, 'Copy summary', async () => { | |
| await copyToClipboard(summaryText, copySummaryBtn, icons.copy); | |
| }); | |
| actions.appendChild(copySummaryBtn); | |
| } | |
| thumbWrapper.appendChild(actions); | |
| el.appendChild(thumbWrapper); | |
| const body = document.createElement('div'); | |
| body.className = 'body'; | |
| // Meta header row: mono arXiv id · contribution type · signal score | |
| const metadata = document.createElement('div'); | |
| metadata.className = 'metadata'; | |
| const idEl = document.createElement('span'); | |
| idEl.className = 'paper-id'; | |
| idEl.textContent = paper.id || ''; | |
| metadata.appendChild(idEl); | |
| const spacer = document.createElement('span'); | |
| spacer.className = 'spacer'; | |
| metadata.appendChild(spacer); | |
| if (paper.contribution_type) { | |
| const contribEl = document.createElement('span'); | |
| contribEl.className = 'contrib-pill'; | |
| contribEl.textContent = paper.contribution_type; | |
| metadata.appendChild(contribEl); | |
| } | |
| if (typeof paper.signal_score === 'number') { | |
| const signalEl = document.createElement('span'); | |
| signalEl.className = 'signal-badge'; | |
| signalEl.title = 'ArXivSignals signal score (0-100)'; | |
| signalEl.textContent = `sig ${paper.signal_score}`; | |
| metadata.appendChild(signalEl); | |
| } | |
| body.appendChild(metadata); | |
| // Title with hyperlink to arXiv | |
| const titleLink = document.createElement('a'); | |
| titleLink.href = paper.arxiv_url; | |
| titleLink.target = '_blank'; | |
| titleLink.rel = 'noopener'; | |
| const title = document.createElement('h3'); | |
| title.className = 'title'; | |
| title.textContent = paper.title || 'No title'; | |
| titleLink.appendChild(title); | |
| body.appendChild(titleLink); | |
| // Authors (immediately below title) | |
| const authors = document.createElement('div'); | |
| authors.className = 'authors'; | |
| authors.textContent = formatAuthors(paper.authors); | |
| body.appendChild(authors); | |
| // One-liner takeaway + abstract snippet | |
| if (paper.one_liner) { | |
| const oneLiner = document.createElement('p'); | |
| oneLiner.className = 'summary one-liner'; | |
| oneLiner.textContent = paper.one_liner; | |
| body.appendChild(oneLiner); | |
| } | |
| if (paper.abstract_snippet) { | |
| const snippet = document.createElement('p'); | |
| snippet.className = 'summary snippet'; | |
| snippet.textContent = paper.abstract_snippet; | |
| body.appendChild(snippet); | |
| } | |
| // Chips: primary category first (mono, blue-tinted), then topic tags | |
| const catStr = (paper.categories && paper.categories[0]) || null; | |
| if (catStr || (paper.tags && paper.tags.length > 0)) { | |
| const tags = document.createElement('div'); | |
| tags.className = 'tags'; | |
| if (catStr) { | |
| const categoryEl = document.createElement('span'); | |
| categoryEl.className = 'category'; | |
| categoryEl.textContent = catStr; | |
| tags.appendChild(categoryEl); | |
| } | |
| (paper.tags || []).forEach(tag => { | |
| const tagEl = document.createElement('span'); | |
| tagEl.className = 'tag'; | |
| tagEl.textContent = formatTag(tag); | |
| tags.appendChild(tagEl); | |
| }); | |
| body.appendChild(tags); | |
| } | |
| // Footer: mono microlinks — arXiv abs + the ArXivSignals paper page | |
| const footer = document.createElement('div'); | |
| footer.className = 'card-footer'; | |
| if (paper.arxiv_url) { | |
| const arxivLink = document.createElement('a'); | |
| arxivLink.href = paper.arxiv_url; | |
| arxivLink.target = '_blank'; | |
| arxivLink.rel = 'noopener'; | |
| arxivLink.textContent = 'arXiv ↗'; | |
| footer.appendChild(arxivLink); | |
| } | |
| if (paper.portal_url) { | |
| const signalsLink = document.createElement('a'); | |
| signalsLink.className = 'signals-link'; | |
| signalsLink.href = paper.portal_url; | |
| signalsLink.target = '_blank'; | |
| signalsLink.rel = 'noopener'; | |
| signalsLink.textContent = paper.has_summary ? 'AI summary →' : 'Signals →'; | |
| footer.appendChild(signalsLink); | |
| } | |
| body.appendChild(footer); | |
| el.appendChild(body); | |
| return el; | |
| } | |
| // Helper function to create action buttons (icon-only) | |
| function createActionButton(iconSvg, title, onClick) { | |
| const btn = document.createElement('button'); | |
| btn.className = 'action-btn'; | |
| btn.innerHTML = iconSvg; | |
| btn.title = title; | |
| btn.addEventListener('click', onClick); | |
| return btn; | |
| } | |
| // SVG icons for feedback | |
| const feedbackIcons = { | |
| check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>', | |
| error: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' | |
| }; | |
| // Helper function to copy text to clipboard | |
| async function copyToClipboard(text, btn, originalIcon) { | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| showCopySuccess(btn, originalIcon); | |
| } catch (err) { | |
| console.error('Failed to copy:', err); | |
| showCopyError(btn, originalIcon); | |
| } | |
| } | |
| function showCopySuccess(btn, originalIcon) { | |
| btn.classList.add('success'); | |
| btn.innerHTML = feedbackIcons.check; | |
| setTimeout(() => { | |
| btn.classList.remove('success'); | |
| btn.innerHTML = originalIcon; | |
| }, 1500); | |
| } | |
| function showCopyError(btn, originalIcon) { | |
| btn.innerHTML = feedbackIcons.error; | |
| setTimeout(() => { | |
| btn.innerHTML = originalIcon; | |
| }, 1500); | |
| } | |
| function renderCards() { | |
| const cardsEl = $('cards'); | |
| if (!cardsEl) return; | |
| cardsEl.innerHTML = ''; | |
| if (!state.filteredItems || state.filteredItems.length === 0) { | |
| cardsEl.innerHTML = '<div class="no-results">No papers found</div>'; | |
| return; | |
| } | |
| const startIndex = (state.currentPage - 1) * state.itemsPerPage; | |
| const endIndex = startIndex + state.itemsPerPage; | |
| const pageItems = state.filteredItems.slice(startIndex, endIndex); | |
| pageItems.forEach((paper) => { | |
| cardsEl.appendChild(renderCard(paper)); | |
| }); | |
| setupLazyLoading(); | |
| updatePagination(); | |
| } | |
| function setupLazyLoading() { | |
| const images = document.querySelectorAll('img[data-src]'); | |
| const imageObserver = new IntersectionObserver((entries, observer) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| const img = entry.target; | |
| img.src = img.dataset.src; | |
| img.classList.add('loaded'); | |
| observer.unobserve(img); | |
| } | |
| }); | |
| }); | |
| images.forEach(img => imageObserver.observe(img)); | |
| } | |
| function updatePagination() { | |
| const pageInfoEl = $('pageInfo'); | |
| const prevBtn = $('prevPage'); | |
| const nextBtn = $('nextPage'); | |
| if (!pageInfoEl || !prevBtn || !nextBtn) return; | |
| const totalPages = Math.ceil(state.filteredItems.length / state.itemsPerPage); | |
| const currentPage = state.currentPage; | |
| pageInfoEl.textContent = `Page ${currentPage} of ${totalPages}`; | |
| prevBtn.disabled = currentPage <= 1; | |
| nextBtn.disabled = currentPage >= totalPages; | |
| } | |
| function filterItems() { | |
| let items = state.papers; | |
| // Apply search filter (title, one-liner, snippet, authors) | |
| if (state.searchTerm) { | |
| const term = state.searchTerm.toLowerCase(); | |
| items = items.filter((p) => { | |
| const title = (p.title || '').toLowerCase(); | |
| const oneLiner = (p.one_liner || '').toLowerCase(); | |
| const snippet = (p.abstract_snippet || '').toLowerCase(); | |
| const authors = (p.authors || []).join(' ').toLowerCase(); | |
| return title.includes(term) || oneLiner.includes(term) || snippet.includes(term) || authors.includes(term); | |
| }); | |
| } | |
| // Apply tag filter | |
| if (state.selectedTags.size > 0) { | |
| items = items.filter((p) => { | |
| if (!p.tags || !Array.isArray(p.tags)) return false; | |
| return p.tags.some(tag => state.selectedTags.has(tag)); | |
| }); | |
| } | |
| // Apply category filter (primary category = categories[0]) | |
| if (state.selectedCategories.size > 0) { | |
| items = items.filter((p) => { | |
| const primary = (p.categories && p.categories[0]) || 'Unknown category'; | |
| return state.selectedCategories.has(primary); | |
| }); | |
| } | |
| state.filteredItems = items; | |
| } | |
| // ---- Event Handlers ---- | |
| function setupEventListeners() { | |
| // Search | |
| const searchInput = $('searchInput'); | |
| if (searchInput) { | |
| searchInput.addEventListener('input', (e) => { | |
| state.searchTerm = e.target.value.trim(); | |
| state.currentPage = 1; | |
| filterItems(); | |
| renderCards(); | |
| }); | |
| } | |
| // Keyboard shortcuts (only when not in input fields) | |
| document.addEventListener('keydown', (e) => { | |
| if (document.activeElement.matches('input, textarea, [contenteditable]')) { | |
| return; | |
| } | |
| const categoryPanel = $('categoryPanel'); | |
| const tagPanel = $('tagPanel'); | |
| const hasOpenModal = (categoryPanel && !categoryPanel.classList.contains('hidden')) || | |
| (tagPanel && !tagPanel.classList.contains('hidden')); | |
| if (hasOpenModal && e.key !== 'Escape') { | |
| return; | |
| } | |
| // Focus search with Ctrl+K or Cmd+K | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| e.preventDefault(); | |
| if (searchInput) { | |
| searchInput.focus(); | |
| searchInput.select(); | |
| } | |
| } | |
| // Reload with R key | |
| if (e.key === 'r' && !e.ctrlKey && !e.metaKey) { | |
| e.preventDefault(); | |
| loadFeed(); | |
| } | |
| // Close modals with Escape | |
| if (e.key === 'Escape') { | |
| if (categoryPanel && !categoryPanel.classList.contains('hidden')) { | |
| categoryPanel.classList.add('hidden'); | |
| } else if (tagPanel && !tagPanel.classList.contains('hidden')) { | |
| tagPanel.classList.add('hidden'); | |
| } | |
| } | |
| }); | |
| // Reload button | |
| const reloadBtn = $('reloadBtn'); | |
| if (reloadBtn) { | |
| reloadBtn.addEventListener('click', () => loadFeed()); | |
| } | |
| // Pagination | |
| const prevPageBtn = $('prevPage'); | |
| const nextPageBtn = $('nextPage'); | |
| if (prevPageBtn) { | |
| prevPageBtn.addEventListener('click', () => { | |
| if (state.currentPage > 1) { | |
| state.currentPage--; | |
| renderCards(); | |
| } | |
| }); | |
| } | |
| if (nextPageBtn) { | |
| nextPageBtn.addEventListener('click', () => { | |
| const totalPages = Math.ceil(state.filteredItems.length / state.itemsPerPage); | |
| if (state.currentPage < totalPages) { | |
| state.currentPage++; | |
| renderCards(); | |
| } | |
| }); | |
| } | |
| // Tag filtering | |
| const tagFilterBtn = $('tagFilterBtn'); | |
| const tagPanel = $('tagPanel'); | |
| const tagSearch = $('tagSearch'); | |
| const closeTagPanel = $('closeTagPanel'); | |
| const clearTags = $('clearTags'); | |
| const applyTags = $('applyTags'); | |
| if (tagFilterBtn && tagPanel) { | |
| tagFilterBtn.addEventListener('click', () => { | |
| if (!tagPanel.classList.contains('hidden')) { | |
| tagPanel.classList.add('hidden'); | |
| } else { | |
| populateTagList(); | |
| tagPanel.classList.remove('hidden'); | |
| } | |
| }); | |
| } | |
| if (closeTagPanel) { | |
| closeTagPanel.addEventListener('click', () => { | |
| tagPanel.classList.add('hidden'); | |
| }); | |
| } | |
| if (tagSearch) { | |
| tagSearch.addEventListener('input', (e) => { | |
| filterTagList(e.target.value); | |
| }); | |
| } | |
| if (clearTags) { | |
| clearTags.addEventListener('click', () => { | |
| state.selectedTags.clear(); | |
| updateTagSelectionUI(); | |
| }); | |
| } | |
| if (applyTags) { | |
| applyTags.addEventListener('click', () => { | |
| tagPanel.classList.add('hidden'); | |
| state.currentPage = 1; | |
| filterItems(); | |
| renderCards(); | |
| }); | |
| } | |
| // Category filtering | |
| const categoryFilterBtn = $('categoryFilterBtn'); | |
| const categoryPanel = $('categoryPanel'); | |
| const categorySearch = $('categorySearch'); | |
| const closeCategoryPanel = $('closeCategoryPanel'); | |
| const clearCategories = $('clearCategories'); | |
| const applyCategories = $('applyCategories'); | |
| if (categoryFilterBtn && categoryPanel) { | |
| categoryFilterBtn.addEventListener('click', () => { | |
| if (!categoryPanel.classList.contains('hidden')) { | |
| categoryPanel.classList.add('hidden'); | |
| } else { | |
| populateCategoryList(); | |
| categoryPanel.classList.remove('hidden'); | |
| } | |
| }); | |
| } | |
| if (closeCategoryPanel) { | |
| closeCategoryPanel.addEventListener('click', () => { | |
| categoryPanel.classList.add('hidden'); | |
| }); | |
| } | |
| if (categorySearch) { | |
| categorySearch.addEventListener('input', (e) => { | |
| filterCategoryList(e.target.value); | |
| }); | |
| } | |
| if (clearCategories) { | |
| clearCategories.addEventListener('click', () => { | |
| state.selectedCategories.clear(); | |
| updateCategorySelectionUI(); | |
| }); | |
| } | |
| if (applyCategories) { | |
| applyCategories.addEventListener('click', () => { | |
| categoryPanel.classList.add('hidden'); | |
| state.currentPage = 1; | |
| filterItems(); | |
| renderCards(); | |
| }); | |
| } | |
| // Theme toggle — dark by default, matching arxivsignals.io | |
| const themeBtn = $('themeBtn'); | |
| if (themeBtn) { | |
| themeBtn.addEventListener('click', toggleTheme); | |
| const savedTheme = localStorage.getItem('arxiv-theme') || 'dark'; | |
| setTheme(savedTheme, { silent: true }); | |
| } | |
| } | |
| // ---- Theme Management ---- | |
| function toggleTheme() { | |
| const newTheme = state.theme === 'light' ? 'dark' : 'light'; | |
| setTheme(newTheme); | |
| } | |
| function setTheme(theme, { silent = false } = {}) { | |
| state.theme = theme; | |
| document.documentElement.setAttribute('data-theme', theme); | |
| localStorage.setItem('arxiv-theme', theme); | |
| const themeBtn = $('themeBtn'); | |
| if (themeBtn) { | |
| themeBtn.classList.toggle('dark', theme === 'dark'); | |
| themeBtn.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'; | |
| } | |
| if (!silent) showStatus(`Switched to ${theme} mode`, 'success'); | |
| } | |
| // ---- Tag Management ---- | |
| function populateTagList() { | |
| const tagList = $('tagList'); | |
| if (!tagList) return; | |
| const tagCounts = new Map(); | |
| state.papers.forEach((p) => { | |
| if (p.tags && Array.isArray(p.tags)) { | |
| p.tags.forEach(tag => { | |
| tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1); | |
| }); | |
| } | |
| }); | |
| const sortedTags = Array.from(tagCounts.entries()).sort((a, b) => { | |
| const tagA = formatTag(a[0]).toLowerCase(); | |
| const tagB = formatTag(b[0]).toLowerCase(); | |
| return tagA.localeCompare(tagB); | |
| }); | |
| tagList.innerHTML = ''; | |
| sortedTags.forEach(([tag, count]) => { | |
| const tagEl = document.createElement('div'); | |
| tagEl.className = 'tag-item'; | |
| tagEl.innerHTML = ` | |
| <label class="tag-checkbox"> | |
| <input type="checkbox" value="${tag}" ${state.selectedTags.has(tag) ? 'checked' : ''}> | |
| <span class="tag-name">${formatTag(tag)}</span> | |
| <span class="tag-count">(${count})</span> | |
| </label> | |
| `; | |
| const checkbox = tagEl.querySelector('input'); | |
| checkbox.addEventListener('change', (e) => { | |
| if (e.target.checked) { | |
| state.selectedTags.add(tag); | |
| } else { | |
| state.selectedTags.delete(tag); | |
| } | |
| updateTagButton(); | |
| }); | |
| tagList.appendChild(tagEl); | |
| }); | |
| } | |
| function filterTagList(searchTerm) { | |
| const tagItems = document.querySelectorAll('.tag-item'); | |
| const term = searchTerm.toLowerCase(); | |
| tagItems.forEach(item => { | |
| const tagName = item.querySelector('.tag-name').textContent.toLowerCase(); | |
| const checkbox = item.querySelector('input'); | |
| const originalTag = checkbox ? checkbox.value.toLowerCase() : ''; | |
| if (tagName.includes(term) || originalTag.includes(term)) { | |
| item.style.display = 'block'; | |
| } else { | |
| item.style.display = 'none'; | |
| } | |
| }); | |
| } | |
| function updateTagSelectionUI() { | |
| document.querySelectorAll('.tag-item input').forEach(checkbox => { | |
| checkbox.checked = state.selectedTags.has(checkbox.value); | |
| }); | |
| updateTagButton(); | |
| } | |
| function updateTagButton() { | |
| const tagFilterBtn = $('tagFilterBtn'); | |
| if (!tagFilterBtn) return; | |
| if (state.selectedTags.size > 0) { | |
| tagFilterBtn.textContent = `Tags (${state.selectedTags.size})`; | |
| tagFilterBtn.classList.add('active'); | |
| } else { | |
| tagFilterBtn.textContent = 'Tags'; | |
| tagFilterBtn.classList.remove('active'); | |
| } | |
| } | |
| // ---- Category Management ---- | |
| function populateCategoryList() { | |
| const categoryList = $('categoryList'); | |
| if (!categoryList) return; | |
| const categoryCounts = new Map(); | |
| state.papers.forEach((p) => { | |
| const category = (p.categories && p.categories[0]) || 'Unknown category'; | |
| categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1); | |
| }); | |
| const sortedCategories = Array.from(categoryCounts.entries()).sort((a, b) => b[1] - a[1]); | |
| categoryList.innerHTML = ''; | |
| sortedCategories.forEach(([category, count]) => { | |
| const categoryEl = document.createElement('div'); | |
| categoryEl.className = 'tag-item'; | |
| categoryEl.innerHTML = ` | |
| <label class="tag-checkbox"> | |
| <input type="checkbox" value="${category}" ${state.selectedCategories.has(category) ? 'checked' : ''}> | |
| <span class="tag-name">${category}</span> | |
| <span class="tag-count">(${count})</span> | |
| </label> | |
| `; | |
| const checkbox = categoryEl.querySelector('input'); | |
| checkbox.addEventListener('change', (e) => { | |
| if (e.target.checked) { | |
| state.selectedCategories.add(category); | |
| } else { | |
| state.selectedCategories.delete(category); | |
| } | |
| updateCategoryButton(); | |
| }); | |
| categoryList.appendChild(categoryEl); | |
| }); | |
| } | |
| function filterCategoryList(searchTerm) { | |
| const categoryItems = document.querySelectorAll('#categoryList .tag-item'); | |
| const term = searchTerm.toLowerCase(); | |
| categoryItems.forEach(item => { | |
| const categoryName = item.querySelector('.tag-name').textContent.toLowerCase(); | |
| if (categoryName.includes(term)) { | |
| item.style.display = 'block'; | |
| } else { | |
| item.style.display = 'none'; | |
| } | |
| }); | |
| } | |
| function updateCategorySelectionUI() { | |
| document.querySelectorAll('#categoryList .tag-item input').forEach(checkbox => { | |
| checkbox.checked = state.selectedCategories.has(checkbox.value); | |
| }); | |
| updateCategoryButton(); | |
| } | |
| function updateCategoryButton() { | |
| const categoryFilterBtn = $('categoryFilterBtn'); | |
| if (!categoryFilterBtn) return; | |
| if (state.selectedCategories.size > 0) { | |
| categoryFilterBtn.textContent = `Categories (${state.selectedCategories.size})`; | |
| categoryFilterBtn.classList.add('active'); | |
| } else { | |
| categoryFilterBtn.textContent = 'Categories'; | |
| categoryFilterBtn.classList.remove('active'); | |
| } | |
| } | |
| // ---- Initialization ---- | |
| async function init() { | |
| setupEventListeners(); | |
| await loadFeed(); | |
| } | |
| // Start the application | |
| document.addEventListener('DOMContentLoaded', init); | |