arXiv / app.js
taesiri's picture
Claude Fable 5
feat: AI-summary badge links to the portal summary page; drop Export
24eb3d0
Raw History Blame Contribute Delete
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);