/** * LLM 论文图书馆 — 前端 JS * 打开时 ping 所有论文源 → 底部状态条 → 点击论文直用缓存结果 */ const API = '/api'; let modules = {}; let moduleData = {}; let currentPdf = null; let pdfTimeout = null; let networkStatus = {}; // { arxiv: bool, huggingface: bool, hk: bool } const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); const TAG_CLASS = { '起点':'tag-start','关键节点':'tag-milestone','前沿':'tag-frontier','前瞻':'tag-forward','支线':'tag-branch' }; // ═══════════════ INIT ═══════════════════════════════════ async function init() { // Build network status bar buildStatusBar(); // Load modules try { const resp = await fetch(`${API}/modules`); const mods = await resp.json(); modules = {}; for (const m of mods) modules[m.id] = m; renderCards(mods); attachGlowTracking(); } catch (e) { console.error(e); } // Ping sources checkSources(); document.addEventListener('keydown', e => { if (e.key === 'Escape') { if ($('#pdfOverlay')?.classList.contains('open')) closePdf(); else if ($('#overlay').classList.contains('open')) closeModal(); } }); } // ═══════════════ STATUS BAR ════════════════════════════ function buildStatusBar() { const bar = document.createElement('div'); bar.id = 'statusBar'; bar.className = 'status-bar'; bar.innerHTML = ` 连通性检测 arXiv HuggingFace `; document.body.appendChild(bar); } function setStatus(id, ok, ms, aborted) { networkStatus[id.replace('status-','')] = ok; const el = document.getElementById(id); if (!el) return; const dot = el.querySelector('.status-dot'); dot.className = 'status-dot ' + (ok ? 'status-ok' : 'status-fail'); const msEl = document.getElementById('ms-' + id.replace('status-','')); if (msEl) { if (aborted) msEl.textContent = '超时 (4s)'; else if (ok) msEl.textContent = ms + 'ms'; else msEl.textContent = '—'; } } async function checkSource(name, url, statusId) { const start = performance.now(); let aborted = false; // Add cache-buster and force no-store to avoid browser caching const probeUrl = url + '?_=' + Date.now(); try { const ctrl = new AbortController(); setTimeout(() => { aborted = true; ctrl.abort(); }, 4000); await fetch(probeUrl, { mode: 'no-cors', signal: ctrl.signal, cache: 'no-store' }); const ms = Math.round(performance.now() - start); setStatus(statusId, true, ms, false); } catch { const ms = Math.round(performance.now() - start); setStatus(statusId, false, ms, aborted); } } async function checkSources() { checkSource('arxiv', 'https://arxiv.org/favicon.ico', 'status-arxiv'); checkSource('hf', 'https://huggingface.co/favicon.ico', 'status-hf'); } // ═══════════════ GLOW ══════════════════════════════════ function attachGlowTracking() { $$('.card').forEach(card => { card.addEventListener('mousemove', e => { const rect = card.getBoundingClientRect(); card.style.setProperty('--mx', ((e.clientX - rect.left) / rect.width * 100) + '%'); card.style.setProperty('--my', ((e.clientY - rect.top) / rect.height * 100) + '%'); }); }); } // ═══════════════ CARDS -> MODAL -> PAPERS ═══════════════ function renderCards(mods) { $('#moduleGrid').innerHTML = mods.map(m => `
${m.icon} ${m.name}
${m.desc}
${m.area_count} 子领域 · ${m.paper_count} 篇
`).join(''); } async function openModule(modId) { let data = moduleData[modId]; if (!data) { try { data = await (await fetch(`${API}/modules/${modId}`)).json(); moduleData[modId] = data; } catch (e) { return; } } const mod = modules[modId]; $('#modalTitle').innerHTML = `${mod?.icon||''} ${data.name}`; $('#modalSubtitle').textContent = data.desc || ''; const areas = data.areas || []; $('#modalTabs').innerHTML = areas.map((a,i) => ``).join(''); if (areas.length > 0) { $('#modalTabs').dataset.areaIdx = '0'; renderPapers(areas[0]); } $('#overlay').classList.add('open'); } function switchArea(idx) { const data = Object.values(moduleData).find(d => d.areas && d.areas[idx]); if (!data) return; $$('#modalTabs .tab').forEach((t,i) => t.classList.toggle('active', i===idx)); renderPapers(data.areas[idx]); } function closeModal() { $('#overlay').classList.remove('open'); } function renderPapers(area) { let h = ''; function s(l, ps, c) { return ps.length ? `
${l}
`+ps.map(renderPaper).join('') : ''; } h += s('📌 主线论文', area.mainline||[], 'mainline'); h += s('🌿 支线论文', area.branches||[], 'branch'); h += s('🔮 前瞻探索', area.forward||[], 'forward'); $('#modalContent').innerHTML = h || '

暂无论文数据

'; } function renderPaper(p) { const pdfUrl = getPdfLink(p); const id = 'p'+Math.random().toString(36).slice(2,8); const tags = (p.tags||[]).map(t => `${t}`).join(' '); const links = []; if (pdfUrl) links.push(``); else if (p.arxiv) links.push(`📋 arXiv`); // Show translation button for ALL papers with arxiv or pdf const paperId = p.arxiv || (p.pdf ? p.pdf.split('/').pop().replace('.pdf','') : null); if (paperId) { const transUrl = `/papers/translated/${paperId}.pdf`; links.push(``); } return `
${p.year||'—'}
${p.title}
${p.authors||''}${p.venue?`${p.venue}`:''}${tags}
`; } function getPdfLink(p) { if (p.pdf) return p.pdf; if (p.arxiv) return `https://arxiv.org/pdf/${p.arxiv}.pdf`; return null; } function openPdfBtn(btn) { openPdf(btn.dataset.pdf, btn.dataset.title); } // ═══════════════ PDF VIEWER (状态条驱动) ═══════════════ function getLocalPdfUrl(extUrl) { const am = extUrl.match(/arxiv\.org\/pdf\/(\d+\.\d+)/); if (am) return `/papers/arxiv/${am[1]}.pdf`; if (extUrl.includes('huggingface.co')) return `/papers/hf/${extUrl.split('/').pop().replace('.pdf','')}.pdf`; return null; } async function openPdf(url, title) { const decodedUrl = decodeURIComponent(url); const decodedTitle = decodeURIComponent(title); currentPdf = decodedUrl; const hkFallback = getLocalPdfUrl(decodedUrl); if (!$('#pdfOverlay')) { const div = document.createElement('div'); div.id='pdfOverlay'; div.className='pdf-overlay'; div.innerHTML = `
PDF
`; document.body.appendChild(div); div.addEventListener('click', e => { if (e.target===div) closePdf(); }); } if (pdfTimeout) { clearTimeout(pdfTimeout); pdfTimeout = null; } $('#pdfTitle').textContent = decodedTitle; $('#pdfStatus').style.display = 'none'; const frame = $('#pdfFrame'); let loaded = false; let abortCtrl = null; frame.src = 'about:blank'; frame.onload = () => { loaded = true; if (pdfTimeout) { clearTimeout(pdfTimeout); pdfTimeout = null; } $('#pdfStatus').style.display = 'none'; }; const isArxiv = decodedUrl.includes('arxiv.org'); const remoteOk = (isArxiv && networkStatus['arxiv'] !== false); // arXiv: iframe 直连(跨域,IDM 只能认了) if (remoteOk) { loaded = false; $('#pdfStatus').textContent = '🌐 arXiv 加载中...'; $('#pdfStatus').style.display = ''; frame.onload = () => { loaded=true; $('#pdfStatus').style.display='none'; }; frame.src = decodedUrl; pdfTimeout = setTimeout(async () => { if (loaded) return; if (!hkFallback) { $('#pdfStatus').textContent='⚠️ arXiv 超时'; return; } // fallback to HK, use fetch→blob to beat IDM $('#pdfStatus').textContent = '⏳ arXiv 超时,走 HK 服务器...'; try { const resp = await fetch(hkFallback, { cache:'default' }); const blobUrl = URL.createObjectURL(await resp.blob()); $('#pdfStatus').textContent = ''; frame.src = blobUrl; } catch(e) { $('#pdfStatus').textContent = '⚠️ HK 加载失败'; } }, 5000); } else { // arXiv down / local / HF → use HK with fetch→blob const src = hkFallback || decodedUrl; $('#pdfStatus').textContent = hkFallback ? '📂 HK 服务器加载中...' : '⏳ 加载中...'; $('#pdfStatus').style.display = ''; try { const resp = await fetch(src, { cache:'default' }); const blobUrl = URL.createObjectURL(await resp.blob()); $('#pdfStatus').textContent = ''; frame.src = blobUrl; } catch(e) { $('#pdfStatus').textContent = '⚠️ 加载失败'; } } $('#pdfOverlay').classList.add('open'); } function closePdf() { if (pdfTimeout) { clearTimeout(pdfTimeout); pdfTimeout = null; } $('#pdfOverlay').classList.remove('open'); $('#pdfFrame').src = ''; currentPdf = null; } // ═══════════════ SEARCH ═══════════════════════════════ function searchPapers(q) { q = (q||'').toLowerCase().trim(); if (q.length<2) { $$('.card').forEach(c=>c.style.outline=''); return; } fetch(`${API}/papers?q=${encodeURIComponent(q)}&limit=10`).then(r=>r.json()).then(ps=>{ const matched = new Set(ps.map(p=>p.module_id)); $$('.card').forEach(c=>{ const m=[...c.classList].find(x=>x.startsWith('card-'))?.replace('card-',''); c.style.outline=matched.has(m)?'2px solid var(--green)':''; }); }); } // ═══════════════ EVENTS ═══════════════════════════════ if (typeof document !== 'undefined') { document.addEventListener('DOMContentLoaded', () => { init(); $('.search-input').addEventListener('input', e => searchPapers(e.target.value)); $('#modalClose').addEventListener('click', closeModal); $('#overlay').addEventListener('click', e => { if (e.target===$('#overlay')) closeModal(); }); }); }