diff --git a/static/app.js b/static/app.js index 62483d8..a9f420e 100644 --- a/static/app.js +++ b/static/app.js @@ -1,32 +1,37 @@ /** * LLM 论文图书馆 — 前端 JS - * 页面加载检测 arXiv/HF 连通性 → 底部状态条 - * 点击论文:arXiv 连通? → iframe 直连 arXiv → 5s 超时 → HK 兜底 - * IDM 拦就拦,不额外对抗 + * 打开时 ping 所有论文源 → 底部状态条 → 点击论文直用缓存结果 */ const API = '/api'; let modules = {}; let moduleData = {}; +let currentPdf = null; let pdfTimeout = null; -let networkStatus = {}; +let networkStatus = {}; // { arxiv: bool, huggingface: bool, hk: bool } -const $ = (sel) => document.querySelector(sel); +const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); const TAG_CLASS = { '起点':'tag-start','关键节点':'tag-milestone','前沿':'tag-frontier','前瞻':'tag-forward','支线':'tag-branch' }; -// ══════════════════ INIT ═══════════════════════════════ +// ═══════════════ 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(); @@ -35,31 +40,37 @@ async function init() { }); } -// ══════════════════ STATUS BAR ════════════════════════ +// ═══════════════ STATUS BAR ════════════════════════════ function buildStatusBar() { const bar = document.createElement('div'); - bar.id = 'statusBar'; bar.className = 'status-bar'; - bar.innerHTML = `连通性检测 + bar.id = 'statusBar'; + bar.className = 'status-bar'; + bar.innerHTML = ` + 连通性检测 arXiv - HuggingFace `; + HuggingFace + `; document.body.appendChild(bar); } function setStatus(id, ok, ms, aborted) { networkStatus[id.replace('status-','')] = ok; - const el = document.getElementById(id); if (!el) return; - el.querySelector('.status-dot').className = 'status-dot ' + (ok ? 'status-ok' : 'status-fail'); - const msEl = document.getElementById('ms-'+id.replace('status-','')); + 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 = '超时'; - else if (ok) msEl.textContent = ms+'ms'; - else msEl.textContent = '—'; + 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(); @@ -73,26 +84,26 @@ async function checkSource(name, url, statusId) { } } -function checkSources() { +async function checkSources() { checkSource('arxiv', 'https://arxiv.org/favicon.ico', 'status-arxiv'); - checkSource('hf', 'https://huggingface.co/favicon.ico', 'status-hf'); + checkSource('hf', 'https://huggingface.co/favicon.ico', 'status-hf'); } -// ══════════════════ GLOW ══════════════════════════════ +// ═══════════════ GLOW ══════════════════════════════════ function attachGlowTracking() { $$('.card').forEach(card => { card.addEventListener('mousemove', e => { - const r = card.getBoundingClientRect(); - card.style.setProperty('--mx', (e.clientX-r.left)/r.width*100+'%'); - card.style.setProperty('--my', (e.clientY-r.top)/r.height*100+'%'); + 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 ═══════════ +// ═══════════════ CARDS -> MODAL -> PAPERS ═══════════════ function renderCards(mods) { - $('#moduleGrid').innerHTML = mods.map(m => - `
+ $('#moduleGrid').innerHTML = mods.map(m => ` +
${m.icon} ${m.name}
${m.desc}
${m.area_count} 子领域 · ${m.paper_count} 篇
@@ -102,43 +113,45 @@ function renderCards(mods) { async function openModule(modId) { let data = moduleData[modId]; if (!data) { - try { data = await (await fetch(`${API}/modules/${modId}`)).json(); moduleData[modId]=data; } - catch { return; } + 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) { $('#modalTabs').dataset.areaIdx='0'; renderPapers(areas[0]); } + $('#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)); + $$('#modalTabs .tab').forEach((t,i) => t.classList.toggle('active', i===idx)); renderPapers(data.areas[idx]); } function closeModal() { $('#overlay').classList.remove('open'); } function renderPapers(area) { - const s = (l, ps, c) => ps.length ? ``+ps.map(renderPaper).join('') : ''; - $('#modalContent').innerHTML = - s('📌 主线论文', area.mainline||[],'mainline') + - s('🌿 支线论文', area.branches||[],'branch') + - s('🔮 前瞻探索', area.forward||[],'forward') || - '

暂无论文数据

'; + let h = ''; + function s(l, ps, c) { return ps.length ? ``+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 tags = (p.tags||[]).map(t=>`${t}`).join(' '); + 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`); - return `
${p.year||'—'}
+ return `
${p.year||'—'}
${p.title}
${p.authors||''}${p.venue?`${p.venue}`:''}${tags}
@@ -146,91 +159,101 @@ function renderPaper(p) { } function getPdfLink(p) { - // 有 pdf 字段 → 返回外部源 URL(arxiv 直连 / HF 直连) if (p.pdf) return p.pdf; - // 只有 arxiv id → arXiv 直连 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 getLocalUrl(extUrl) { - // arXiv +// ═══════════════ PDF VIEWER (状态条驱动) ═══════════════ +function getLocalPdfUrl(extUrl) { const am = extUrl.match(/arxiv\.org\/pdf\/(\d+\.\d+)/); if (am) return `/papers/arxiv/${am[1]}.pdf`; - // HuggingFace - if (extUrl.includes('huggingface.co')) { - const name = decodeURIComponent(extUrl).split('/').pop().replace('.pdf',''); - return `/papers/hf/${name}.pdf`; - } + if (extUrl.includes('huggingface.co')) return `/papers/hf/${extUrl.split('/').pop().replace('.pdf','')}.pdf`; return null; } function openPdf(url, title) { const decodedUrl = decodeURIComponent(url); const decodedTitle = decodeURIComponent(title); + currentPdf = decodedUrl; + const hkFallback = getLocalPdfUrl(decodedUrl); - // 构建 overlay if (!$('#pdfOverlay')) { const div = document.createElement('div'); div.id='pdfOverlay'; div.className='pdf-overlay'; div.innerHTML = `
-
PDF - +
PDF +
`; document.body.appendChild(div); div.addEventListener('click', e => { if (e.target===div) closePdf(); }); } - if (pdfTimeout) { clearTimeout(pdfTimeout); pdfTimeout=null; } + if (pdfTimeout) { clearTimeout(pdfTimeout); pdfTimeout = null; } $('#pdfTitle').textContent = decodedTitle; - $('#pdfStatus').textContent = ''; + $('#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').textContent=''; }; - const hkLocalUrl = getLocalUrl(decodedUrl); + frame.onload = () => { + loaded = true; + if (pdfTimeout) { clearTimeout(pdfTimeout); pdfTimeout = null; } + $('#pdfStatus').style.display = 'none'; + }; + const isArxiv = decodedUrl.includes('arxiv.org'); - const isHF = decodedUrl.includes('huggingface.co'); - const isRemote = isArxiv || isHF; - const sourceOk = isArxiv ? networkStatus['arxiv'] !== false - : isHF ? networkStatus['hf'] !== false - : false; + const remoteOk = (isArxiv && networkStatus['arxiv'] !== false); - // 弹框先出 - $('#pdfOverlay').classList.add('open'); - - if (isRemote && sourceOk) { - // 远程源 iframe 直连 + // arXiv: iframe 直连(跨域,IDM 只能认了) + if (remoteOk) { loaded = false; - $('#pdfStatus').textContent = isArxiv ? '🌐 arXiv 加载中...' : '🤗 HuggingFace 加载中...'; + $('#pdfStatus').textContent = '🌐 arXiv 加载中...'; + $('#pdfStatus').style.display = ''; + frame.onload = () => { loaded=true; $('#pdfStatus').style.display='none'; }; frame.src = decodedUrl; - pdfTimeout = setTimeout(() => { + pdfTimeout = setTimeout(async () => { if (loaded) return; - if (!hkLocalUrl) { $('#pdfStatus').textContent='⚠️ 超时'; return; } - $('#pdfStatus').textContent = '⏳ 超时,走 HK 服务器...'; - frame.src = hkLocalUrl; + 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 { - // 直接走 HK - $('#pdfStatus').textContent = hkLocalUrl ? '📂 HK 服务器加载中...' : '⏳ 加载中...'; - frame.src = hkLocalUrl || decodedUrl; + // 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; } + if (pdfTimeout) { clearTimeout(pdfTimeout); pdfTimeout = null; } $('#pdfOverlay').classList.remove('open'); $('#pdfFrame').src = ''; + currentPdf = null; } -// ══════════════════ SEARCH ════════════════════════════ +// ═══════════════ SEARCH ═══════════════════════════════ function searchPapers(q) { - q=(q||'').toLowerCase().trim(); + 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)); @@ -241,7 +264,7 @@ function searchPapers(q) { }); } -// ══════════════════ EVENTS ════════════════════════════ +// ═══════════════ EVENTS ═══════════════════════════════ if (typeof document !== 'undefined') { document.addEventListener('DOMContentLoaded', () => { init(); diff --git a/static/index.html b/static/index.html index e7132a2..364bafe 100644 --- a/static/index.html +++ b/static/index.html @@ -29,6 +29,283 @@
- +