const state = { atlas: null, health: null, view: "atlas", activeTopic: "", topicFocus: null, selectedPaper: null, selectedMarkdown: "", searchItems: [], searchTotal: 0, aiBusy: false, }; const $ = (selector) => document.querySelector(selector); const elements = { healthLine: $("#healthLine"), searchInput: $("#searchInput"), searchBtn: $("#searchBtn"), mapTitle: $("#mapTitle"), atlasSvg: $("#atlasSvg"), routeList: $("#routeList"), timeline: $("#timeline"), explainAtlasBtn: $("#explainAtlasBtn"), readingPathBtn: $("#readingPathBtn"), lensEyebrow: $("#lensEyebrow"), lensTitle: $("#lensTitle"), lensBody: $("#lensBody"), closePaperBtn: $("#closePaperBtn"), }; function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function debounce(fn, delay = 260) { let timer = null; return (...args) => { window.clearTimeout(timer); timer = window.setTimeout(() => fn(...args), delay); }; } async function api(path, options = {}) { const response = await fetch(path, { ...options, headers: { "content-type": "application/json", ...(options.headers || {}), }, }); const payload = await response.json(); if (!response.ok) { throw new Error(payload.error || `HTTP ${response.status}`); } return payload; } function topicLabel(topic) { const found = state.atlas?.topics?.find((item) => item.id === topic); return found?.label || topic.replaceAll("-", " "); } function colorFor(index) { const colors = ["#0d766f", "#ba5644", "#aa7419", "#527b46", "#665d9f", "#236092", "#8a5a25"]; return colors[index % colors.length]; } async function loadHealth() { try { const health = await api("/api/health"); state.health = health; elements.healthLine.textContent = `${health.paper_count} papers · ${health.ollama_ok ? "Ollama online" : "Ollama offline"}`; } catch (error) { elements.healthLine.textContent = error.message; } } async function loadAtlas() { state.atlas = await api("/api/atlas"); elements.mapTitle.textContent = `${state.atlas.paper_count} 篇论文构成的 Agent 版图`; renderAtlasMap(); renderRoutes(); renderTimeline(); renderOverview(); } function renderAtlasMap() { const topics = state.atlas.topics.filter((topic) => topic.count >= 30).slice(0, 13); const topicMap = new Map(topics.map((topic, index) => [topic.id, { ...topic, index }])); const maxCount = Math.max(...topics.map((topic) => topic.count), 1); const isMobile = window.matchMedia("(max-width: 760px)").matches; elements.atlasSvg.setAttribute("viewBox", isMobile ? "16 -4 78 86" : "0 0 170 100"); const point = (topic) => ({ x: isMobile ? topic.x : 14 + topic.x * 1.58, y: -18 + topic.y * 0.93, }); const edges = state.atlas.edges .filter((edge) => topicMap.has(edge.source) && topicMap.has(edge.target)) .map((edge) => { const source = topicMap.get(edge.source); const target = topicMap.get(edge.target); const sourcePoint = point(source); const targetPoint = point(target); return ` ${escapeHtml(topicLabel(edge.source))} ↔ ${escapeHtml(topicLabel(edge.target))}: ${edge.count} `; }) .join(""); const nodes = topics .map((topic, index) => { const active = topic.id === state.activeTopic ? " active" : ""; const r = 3.6 + 5.6 * (topic.count / maxCount); const ring = r + 1.6 + topic.heat * 1.7; const color = colorFor(index); const p = point(topic); return ` ${escapeHtml(topic.label)} ${topic.count} · +${topic.recent_count} `; }) .join(""); elements.atlasSvg.innerHTML = `${edges}${nodes}`; } function renderRoutes() { elements.routeList.innerHTML = state.atlas.routes .map( (route) => ` `, ) .join(""); } function renderTimeline() { const rows = state.atlas.timeline.slice(-13); const maxTotal = Math.max(...rows.map((row) => row.total), 1); elements.timeline.innerHTML = rows .map((row) => { const height = 8 + 70 * (row.total / maxTotal); return `
${escapeHtml(row.month.slice(5))}
`; }) .join(""); } function statGrid(items) { return `
${items .map( (item) => `
${escapeHtml(item.value)} ${escapeHtml(item.label)}
`, ) .join("")}
`; } function paperList(items, limit = items.length) { if (!items?.length) { return `
暂无证据
`; } return `
${items .slice(0, limit) .map( (paper) => ` `, ) .join("")}
`; } function routeButtons(routes) { return `
${routes .map( (route) => ` `, ) .join("")}
`; } function renderOverview(aiHtml = "") { state.view = "atlas"; state.selectedPaper = null; elements.lensEyebrow.textContent = "Atlas"; elements.lensTitle.textContent = "整体脉络"; setModeButtons("atlas"); const topTopics = state.atlas.topics.slice(0, 7); elements.lensBody.innerHTML = `
${statGrid([ { value: state.atlas.paper_count, label: "papers" }, { value: state.atlas.topic_count, label: "topics" }, { value: state.atlas.recent_months.join(" / "), label: "recent window" }, ])}

这版 Atlas 把论文当作证据,把主题、路线和趋势当作入口。当前最强的主轴是 evaluation、tool use、RAG、reasoning、planning、safety、memory。

核心问题:Agent 如何从“能回答”走向“能长期、可观察、可治理地行动”?

主题入口

${topTopics .map( (topic) => ` `, ) .join("")}

路线

${routeButtons(state.atlas.routes)}

关键论文候选

${paperList(state.atlas.key_papers, 8)}

AI 研究编辑

${aiHtml}
`; } async function selectTopic(topic) { state.activeTopic = topic; state.selectedPaper = null; renderAtlasMap(); const focus = await api(`/api/topic?topic=${encodeURIComponent(topic)}`); state.topicFocus = focus; renderTopic(focus); } function renderTopic(focus, aiHtml = "") { state.view = "atlas"; elements.lensEyebrow.textContent = "Topic"; elements.lensTitle.textContent = focus.label; setModeButtons("atlas"); const recentTotal = Object.values(focus.months).slice(-3).reduce((sum, value) => sum + Number(value || 0), 0); elements.lensBody.innerHTML = `
${statGrid([ { value: focus.count, label: "papers" }, { value: recentTotal, label: "recent" }, { value: focus.neighbors.length, label: "neighbors" }, ])}

${escapeHtml(focus.stance)}

${escapeHtml(focus.question)}

相邻主题

${focus.neighbors .slice(0, 8) .map( (item) => ` `, ) .join("")}

所在路线

${routeButtons(focus.routes)}

AI 研究编辑

${ focus.neighbors[0] ? `` : "" }
${aiHtml}

关键论文

${paperList(focus.key_papers, 10)}

最近新增

${paperList(focus.recent_papers, 8)}
`; } function renderRoute(routeId) { const route = state.atlas.routes.find((item) => item.id === routeId); if (!route) return; state.selectedPaper = null; elements.lensEyebrow.textContent = "Route"; elements.lensTitle.textContent = route.title; elements.lensBody.innerHTML = `

${escapeHtml(route.summary)}

主题链路

${route.topics .map((topic) => ``) .join("")}

路线证据

${paperList( state.atlas.key_papers.filter((paper) => paper.topics.some((topic) => route.topics.includes(topic))), 12, )}
`; } async function runSearch() { const q = elements.searchInput.value.trim(); if (!q) { renderOverview(); return; } state.view = "search"; setModeButtons("search"); elements.lensEyebrow.textContent = "Search"; elements.lensTitle.textContent = q; elements.lensBody.innerHTML = `
searching
`; const params = new URLSearchParams({ q, limit: "60", sort: "score" }); if (state.activeTopic) params.set("topic", state.activeTopic); const payload = await api(`/api/papers?${params.toString()}`); state.searchItems = payload.items; state.searchTotal = payload.total; elements.lensBody.innerHTML = `
${statGrid([ { value: payload.total, label: "matches" }, { value: state.activeTopic ? topicLabel(state.activeTopic) : "all", label: "scope" }, { value: "score", label: "sort" }, ])}
${paperList(payload.items, payload.items.length)}
`; } function renderReading() { state.view = "reading"; setModeButtons("reading"); elements.lensEyebrow.textContent = "Reading"; elements.lensTitle.textContent = "阅读流"; elements.lensBody.innerHTML = `

先从路线理解问题,再进入主题,最后把论文作为证据读。当前版本把高分论文和最近新增放在这里,后续可以接入个人阅读队列。

高优先级

${paperList(state.atlas.key_papers, 12)}

最近新增

${paperList(state.atlas.recent_papers, 12)}
`; } function stripFrontmatter(markdown) { const text = String(markdown || ""); const match = text.match(/^# .+?\n\n---[\s\S]*?\n---\n\n?/); return match ? text.slice(match[0].length) : text; } function renderMarkdown(markdown) { return escapeHtml(stripFrontmatter(markdown)) .replace(/^### (.+)$/gm, "\n$1\n") .replace(/^## (.+)$/gm, "\n$1\n") .replace(/^# (.+)$/gm, "$1\n") .replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, "$1 ($2)"); } async function selectPaper(id) { const payload = await api(`/api/paper?id=${encodeURIComponent(id)}`); state.selectedPaper = payload.paper; state.selectedMarkdown = payload.markdown; renderPaperReader(); } function renderPaperReader(aiHtml = "", abstractHtml = "") { const paper = state.selectedPaper; if (!paper) return; elements.lensEyebrow.textContent = "Paper"; elements.lensTitle.textContent = "研读"; elements.lensBody.innerHTML = `

${escapeHtml(paper.title)}

${escapeHtml(paper.published_at || paper.year)} ${escapeHtml(paper.venue || paper.source)} score ${escapeHtml(paper.collection_score)}
${(paper.topics || []).map((topic) => ``).join("")}
arXiv
${abstractHtml}
${aiHtml}

本地卡片

${renderMarkdown(state.selectedMarkdown)}
`; } async function loadAbstract() { if (!state.selectedPaper) return; setAiBusy(true, "摘要获取中"); try { const payload = await api(`/api/paper/abstract?id=${encodeURIComponent(state.selectedPaper.id)}`); const abstract = payload.abstract; const html = abstract ? `

arXiv Abstract

${escapeHtml(abstract.abstract)}
` : `
${escapeHtml(payload.message || "No abstract")}
`; renderPaperReader("", html); } catch (error) { renderPaperReader("", `
${escapeHtml(error.message)}
`); } finally { setAiBusy(false); } } function setAiBusy(isBusy, label = "") { state.aiBusy = isBusy; document.querySelectorAll(".tool-button, .quiet-button").forEach((button) => { button.disabled = isBusy; }); if (isBusy) { const box = $("#aiResult") || $("#paperAi"); if (box) { box.style.display = ""; box.innerHTML = `
${escapeHtml(label)}
`; } } } async function runPaperAi(mode) { if (!state.selectedPaper) return; const model = state.health?.default_models?.[mode] || (mode === "translate" ? "ChatGPT-5.6:light" : "ChatGPT-5.6:fast"); if (model.includes(":large") && !window.confirm("将调用 large 模型,占用更多 GPU。继续?")) return; setAiBusy(true, `${mode} · ${model}`); try { const payload = await api("/api/ai", { method: "POST", body: JSON.stringify({ id: state.selectedPaper.id, mode, model }), }); const cache = payload.cached ? "cached" : `${payload.duration_seconds}s`; const html = `

${escapeHtml(payload.model)} · ${escapeHtml(cache)}

${escapeHtml(payload.response)}
`; renderPaperReader(html, $("#paperAbstract")?.innerHTML || ""); } catch (error) { renderPaperReader(`
${escapeHtml(error.message)}
`, $("#paperAbstract")?.innerHTML || ""); } finally { setAiBusy(false); } } async function runAtlasAi(mode, topic = "", topicB = "") { const model = state.health?.default_models?.[mode] || "ChatGPT-5.6:fast"; if (model.includes(":large") && !window.confirm("将调用 large 模型做高维综合,占用更多 GPU。继续?")) return; setAiBusy(true, `${mode} · ${model}`); try { const payload = await api("/api/atlas/ai", { method: "POST", body: JSON.stringify({ mode, topic, topic_b: topicB, model }), }); const cache = payload.cached ? "cached" : `${payload.duration_seconds}s`; const html = `

${escapeHtml(payload.model)} · ${escapeHtml(cache)}

${escapeHtml(payload.response)}
`; if (mode === "topic" || (mode === "path" && state.topicFocus)) { renderTopic(state.topicFocus, html); } else { renderOverview(html); } } catch (error) { const html = `
${escapeHtml(error.message)}
`; if (state.topicFocus) renderTopic(state.topicFocus, html); else renderOverview(html); } finally { setAiBusy(false); } } function setModeButtons(view) { document.querySelectorAll("[data-view]").forEach((button) => { button.classList.toggle("active", button.dataset.view === view); }); window.addEventListener( "resize", debounce(() => { if (state.atlas) renderAtlasMap(); }, 150), ); } function scrollLensIntoView() { if (window.matchMedia("(max-width: 760px)").matches) { document.querySelector(".lens-panel").scrollIntoView({ behavior: "smooth", block: "start" }); } } function bindEvents() { elements.atlasSvg.addEventListener("click", async (event) => { const node = event.target.closest("[data-topic]"); if (!node) return; await selectTopic(node.dataset.topic); scrollLensIntoView(); }); elements.routeList.addEventListener("click", (event) => { const button = event.target.closest("[data-route]"); if (button) { renderRoute(button.dataset.route); scrollLensIntoView(); } }); elements.lensBody.addEventListener("click", async (event) => { const topicButton = event.target.closest("[data-topic]"); const routeButton = event.target.closest("[data-route]"); const paperButton = event.target.closest("[data-paper]"); const atlasAi = event.target.closest("[data-atlas-ai]"); const topicAi = event.target.closest("[data-topic-ai]"); const compareButton = event.target.closest("[data-compare-topic]"); const paperAction = event.target.closest("[data-paper-action]"); if (topicButton) { await selectTopic(topicButton.dataset.topic); scrollLensIntoView(); } else if (routeButton) { renderRoute(routeButton.dataset.route); } else if (paperButton) { await selectPaper(paperButton.dataset.paper); scrollLensIntoView(); } else if (atlasAi) { await runAtlasAi(atlasAi.dataset.atlasAi, state.activeTopic); } else if (topicAi) { await runAtlasAi(topicAi.dataset.topicAi, state.activeTopic); } else if (compareButton) { await runAtlasAi("compare", state.activeTopic, compareButton.dataset.compareTopic); } else if (paperAction) { const action = paperAction.dataset.paperAction; if (action === "abstract") await loadAbstract(); else await runPaperAi(action); } }); elements.searchBtn.addEventListener("click", runSearch); elements.searchInput.addEventListener( "input", debounce(() => { if (elements.searchInput.value.trim()) runSearch(); }), ); elements.explainAtlasBtn.addEventListener("click", () => runAtlasAi("atlas", state.activeTopic)); elements.readingPathBtn.addEventListener("click", () => runAtlasAi("path", state.activeTopic)); elements.closePaperBtn.addEventListener("click", () => { if (state.topicFocus) renderTopic(state.topicFocus); else renderOverview(); }); document.querySelectorAll("[data-view]").forEach((button) => { button.addEventListener("click", async () => { const view = button.dataset.view; if (view === "atlas") { state.activeTopic ? await selectTopic(state.activeTopic) : renderOverview(); } else if (view === "search") { await runSearch(); } else { renderReading(); } scrollLensIntoView(); }); }); } async function boot() { bindEvents(); await loadHealth(); await loadAtlas(); } boot();