Files
llm-library/static/app.js
LaoWang f0ff62e082 feat: LLM 论文图书馆 — 初始提交
- FastAPI 后端: REST API + Bearer Token 鉴权 + PDF 代理
- 180 篇论文数据 (data/papers.json): 9 模块、32 子领域
- 前端: 数据驱动、卡片径向渐变光效、PDF 页面内阅读
- 底部状态栏: arXiv/HF 连通性检测
- PDF 加载: arXiv 优先(5s超时) → HK 本地兜底
- Docker 化部署 (Dockerfile + start.sh + nginx.conf)
- arXiv + HF 批量下载器 (api/downloader.py)
2026-06-02 10:25:14 +00:00

253 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* LLM 论文图书馆 — 前端 JS
* 页面加载检测 arXiv/HF 连通性 → 底部状态条
* 点击论文arXiv 连通? → iframe 直连 arXiv → 5s 超时 → HK 兜底
* IDM 拦就拦,不额外对抗
*/
const API = '/api';
let modules = {};
let moduleData = {};
let pdfTimeout = null;
let networkStatus = {};
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() {
buildStatusBar();
try {
const resp = await fetch(`${API}/modules`);
const mods = await resp.json();
for (const m of mods) modules[m.id] = m;
renderCards(mods);
attachGlowTracking();
} catch (e) { console.error(e); }
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 = `<span class="status-label">连通性检测</span>
<span class="status-item" id="status-arxiv"><span class="status-dot"></span> arXiv <span class="status-ms" id="ms-arxiv">—</span></span>
<span class="status-item" id="status-hf"><span class="status-dot"></span> HuggingFace <span class="status-ms" id="ms-hf">—</span></span>`;
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-',''));
if (msEl) {
if (aborted) msEl.textContent = '超时';
else if (ok) msEl.textContent = ms+'ms';
else msEl.textContent = '—';
}
}
async function checkSource(name, url, statusId) {
const start = performance.now();
let aborted = false;
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);
}
}
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 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+'%');
});
});
}
// ══════════════════ CARDS → MODAL → PAPERS ═══════════
function renderCards(mods) {
$('#moduleGrid').innerHTML = mods.map(m =>
`<div class="card card-${m.id}" onclick="openModule('${m.id}')">
<div class="card-header"><span class="card-icon">${m.icon}</span> ${m.name}</div>
<div class="card-desc">${m.desc}</div>
<div class="card-badge">${m.area_count} 子领域 · ${m.paper_count} 篇</div>
</div>`).join('');
}
async function openModule(modId) {
let data = moduleData[modId];
if (!data) {
try { data = await (await fetch(`${API}/modules/${modId}`)).json(); moduleData[modId]=data; }
catch { return; }
}
const mod = modules[modId];
$('#modalTitle').innerHTML = `<span class="card-icon">${mod?.icon||''}</span> ${data.name}`;
$('#modalSubtitle').textContent = data.desc||'';
const areas = data.areas||[];
$('#modalTabs').innerHTML = areas.map((a,i)=>`<button class="tab ${i?'':'active'}" onclick="switchArea(${i})">${a.name}</button>`).join('');
if (areas.length) { $('#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) {
const s = (l, ps, c) => ps.length ? `<div class="section-label ${c}">${l}</div>`+ps.map(renderPaper).join('') : '';
$('#modalContent').innerHTML =
s('📌 主线论文', area.mainline||[],'mainline') +
s('🌿 支线论文', area.branches||[],'branch') +
s('🔮 前瞻探索', area.forward||[],'forward') ||
'<p style="color:var(--text-dim);padding:20px;">暂无论文数据</p>';
}
function renderPaper(p) {
const pdfUrl = getPdfLink(p);
const tags = (p.tags||[]).map(t=>`<span class="paper-tag ${TAG_CLASS[t]||'tag-branch'}">${t}</span>`).join(' ');
const links = [];
if (pdfUrl) links.push(`<button class="paper-link" data-pdf="${encodeURIComponent(pdfUrl)}" data-title="${encodeURIComponent(p.title)}" onclick="openPdfBtn(this)">📄 阅读</button>`);
else if (p.arxiv) links.push(`<a class="paper-link" href="https://arxiv.org/abs/${p.arxiv}" target="_blank">📋 arXiv</a>`);
return `<div class="paper-item"><div class="paper-year">${p.year||'—'}</div><div class="paper-body">
<div class="paper-title">${p.title}</div>
<div class="paper-meta"><span>${p.authors||''}</span>${p.venue?`<span class="paper-venue">${p.venue}</span>`:''}${tags}</div>
<div class="paper-links">${links.join('')}</div>
</div></div>`;
}
function getPdfLink(p) {
// 有 pdf 字段 → 返回外部源 URLarxiv 直连 / 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
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`;
}
return null;
}
function openPdf(url, title) {
const decodedUrl = decodeURIComponent(url);
const decodedTitle = decodeURIComponent(title);
// 构建 overlay
if (!$('#pdfOverlay')) {
const div = document.createElement('div'); div.id='pdfOverlay'; div.className='pdf-overlay';
div.innerHTML = `<div class="pdf-container" id="pdfContainer">
<div class="pdf-toolbar"><span id="pdfTitle">PDF</span><span id="pdfStatus" style="color:var(--orange);font-size:0.8em;margin-left:8px;"></span>
<button onclick="window.open($('#pdfFrame').src || currentPdf,'_blank')">🔗 新窗口</button>
<button class="pdf-close" onclick="closePdf()">&times;</button></div>
<iframe class="pdf-frame" id="pdfFrame" src=""></iframe></div>`;
document.body.appendChild(div);
div.addEventListener('click', e => { if (e.target===div) closePdf(); });
}
if (pdfTimeout) { clearTimeout(pdfTimeout); pdfTimeout=null; }
$('#pdfTitle').textContent = decodedTitle;
$('#pdfStatus').textContent = '';
const frame = $('#pdfFrame');
let loaded = false;
frame.src = 'about:blank';
frame.onload = ()=>{ loaded=true; if(pdfTimeout){clearTimeout(pdfTimeout);pdfTimeout=null;} $('#pdfStatus').textContent=''; };
const hkLocalUrl = getLocalUrl(decodedUrl);
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;
// 弹框先出
$('#pdfOverlay').classList.add('open');
if (isRemote && sourceOk) {
// 远程源 iframe 直连
loaded = false;
$('#pdfStatus').textContent = isArxiv ? '🌐 arXiv 加载中...' : '🤗 HuggingFace 加载中...';
frame.src = decodedUrl;
pdfTimeout = setTimeout(() => {
if (loaded) return;
if (!hkLocalUrl) { $('#pdfStatus').textContent='⚠️ 超时'; return; }
$('#pdfStatus').textContent = '⏳ 超时,走 HK 服务器...';
frame.src = hkLocalUrl;
}, 5000);
} else {
// 直接走 HK
$('#pdfStatus').textContent = hkLocalUrl ? '📂 HK 服务器加载中...' : '⏳ 加载中...';
frame.src = hkLocalUrl || decodedUrl;
}
}
function closePdf() {
if (pdfTimeout) { clearTimeout(pdfTimeout); pdfTimeout=null; }
$('#pdfOverlay').classList.remove('open');
$('#pdfFrame').src = '';
}
// ══════════════════ 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(); });
});
}