Files
llm-library/static/index.html

330 lines
13 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LLM 论文图书馆</title>
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="header">
<h1>🧠 LLM 论文图书馆</h1>
</div>
<div class="search-wrap">
<span class="search-icon">🔍</span>
<input class="search-input" placeholder="搜索论文标题、作者、关键词..." autocomplete="off">
</div>
<div class="grid" id="moduleGrid"></div>
<div class="overlay" id="overlay">
<div class="modal" id="modal">
<button class="modal-close" id="modalClose">&times;</button>
<div class="modal-title" id="modalTitle"></div>
<div class="modal-subtitle" id="modalSubtitle"></div>
<div class="tabs" id="modalTabs"></div>
<div id="modalContent"></div>
</div>
</div>
<script>
/**
* 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 = `
<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>
<span style="flex:1"></span>
<a href="https://spdis.space" style="color:var(--blue);text-decoration:none;font-size:0.85em;margin-right:12px">spdis.space</a>
<span style="color:var(--text-dim);font-size:0.75em">由 <strong style="color:var(--cyan)">DeepSeek-V4-Pro</strong> 每周维护</span>
`;
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 => `
<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 (e) { 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===0?'active':''}" onclick="switchArea(${i})">${a.name}</button>`).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 ? `<div class="section-label ${c}">${l}</div>`+ps.map(renderPaper).join('') : ''; }
h += s('📌 主线论文', area.mainline||[], 'mainline');
h += s('🌿 支线论文', area.branches||[], 'branch');
h += s('🔮 前瞻探索', area.forward||[], 'forward');
$('#modalContent').innerHTML = h || '<p style="color:var(--text-dim);padding:20px;">暂无论文数据</p>';
}
function renderPaper(p) {
const pdfUrl = getPdfLink(p);
const id = 'p'+Math.random().toString(36).slice(2,8);
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>`);
// 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) {
links.push('<button class="paper-link trans-btn" data-pdf="/papers/translated/' + paperId + '.pdf" data-title="' + p.title + ' [译文]" onclick="openTrans(this)">📖 译文</button>');
}
return `<div class="paper-item"><div class="paper-year">${p.year||'—'}</div><div class="paper-body" id="${id}">
<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) {
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); }
async function openTrans(btn) {
const url = btn.dataset.pdf;
try {
const resp = await fetch(url, { method: 'HEAD' });
if (resp.ok) { openPdf(url, btn.dataset.title); return; }
} catch(e) {}
alert('该论文译文尚未生成,请稍后再试');
}
// ═══════════════ 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 = `<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;display:none"></span>
<button onclick="window.open(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').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(); });
});
}
</script>
</body>
</html>