From b22c9f7f741948998113fd3e75034c9d2c18a7e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 27 May 2026 17:10:35 +0000 Subject: [PATCH] Add dynamic insight panel at the bottom of every card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new "动态洞察 / Dynamic insight" section now appends to every full-tab card view, turning the node's local adjacency into a deterministic reading brief: - Synthesised summary (prefers authored summary_zh, falls back to most- frequent edge relation when both are missing). - Importance estimate sourced from incoming composes / validates / manifests / motivates edges; cites the top-3 most-cited referrers by name with anchor-link routing. - Next-step suggestions ranked by outbound relation strength (enables > extends > motivates > composes > validates > parallel > prereq > contrasts > unsolved_by) with one-line rationale per item. - Open problems list when the node connects to problem:* via unsolved_by or motivates, capped at 3 with clipped summaries. Implementation: - atlas-cards.js: new _insightReport pipeline appended to _renderFull, plus _insightSummary / _insightImportance / _insightNextSuggestions / _insightOpenProblems helpers and an internal meta-language guard (logs a console warning, never user-visible). - atlas3d.css: dedicated `.insight-report` panel chrome + responsive 1-column collapse below 920px. Skipped for kind=lab and kind=channel (external resources, no abstract adjacency to summarise). https://claude.ai/code/session_017Ez7KNKDCGRRLjEnJi9TW7 --- docs/atlas3d.css | 67 +++++++++++ docs/js/atlas-cards.js | 258 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 322 insertions(+), 3 deletions(-) diff --git a/docs/atlas3d.css b/docs/atlas3d.css index fda085c..53f7b30 100644 --- a/docs/atlas3d.css +++ b/docs/atlas3d.css @@ -220,6 +220,73 @@ input#yearSlider { width: 100%; accent-color: var(--accent); } .trace-block li:hover { color: var(--accent-warm); } .trace-block .blockkind { font-size: 10.5px; color: var(--ink-mute); margin-right: 6px; } +/* ---------- dynamic insight panel ---------- */ +.insight-report { + margin-top: 24px; + padding: 14px 16px; + background: rgba(108, 177, 255, 0.06); + border: 1px solid rgba(108, 177, 255, 0.22); + border-radius: 8px; +} +.insight-report h2 { + margin-top: 0; + margin-bottom: 6px; + font-size: 14px; + color: var(--accent-warm); + letter-spacing: 0.04em; +} +.insight-summary { + font-size: 13px; + line-height: 1.6; + color: var(--ink); + margin: 0 0 10px; +} +.insight-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px 16px; + margin-top: 8px; +} +.insight-cell h4 { + font-size: 12px; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.06em; + margin: 0 0 4px; +} +.insight-cell p { + font-size: 12.5px; + line-height: 1.55; + margin: 0; + color: var(--ink); +} +.insight-cell p.insight-empty { + color: var(--ink-mute); + font-style: italic; +} +.next-suggestions { + padding-left: 18px; + margin: 0; +} +.next-suggestions li { + font-size: 12.5px; + line-height: 1.55; + margin-bottom: 3px; +} +.open-problems { + padding-left: 18px; + margin: 0; + list-style: disc; +} +.open-problems li { + font-size: 12.5px; + line-height: 1.55; + margin-bottom: 3px; +} +@media (max-width: 920px) { + .insight-grid { grid-template-columns: 1fr; } +} + /* ---------- mermaid diagrams ---------- */ .mermaid-rendered { background: rgba(8, 12, 24, 0.55); diff --git a/docs/js/atlas-cards.js b/docs/js/atlas-cards.js index e4ffaf9..4434f9e 100644 --- a/docs/js/atlas-cards.js +++ b/docs/js/atlas-cards.js @@ -142,13 +142,16 @@ export class CardRenderer { } async _renderFull(node, raw) { + let main; if (raw) { const { body } = this._stripFront(raw); const html = this._mdToHtml(body); - return `${this._headerBlock(node)}${html}`; + main = `${this._headerBlock(node)}${html}`; + } else { + // synthesized for new kinds + main = `${this._headerBlock(node)}${this._synthesizedBody(node)}`; } - // synthesized for new kinds - return `${this._headerBlock(node)}${this._synthesizedBody(node)}`; + return `${main}${this._insightReport(node)}`; } async _renderAnchors(node, raw) { @@ -535,4 +538,253 @@ export class CardRenderer { warn.setAttribute('title', `Mermaid 渲染失败: ${message || 'unknown error'}`); pre.parentElement.insertBefore(warn, pre); } + + // ============================================================ + // Dynamic insight panel + // + // Appended to the bottom of every card (full tab), this section + // turns the node's local adjacency into a deterministic reading + // brief: a synthesised summary, an importance estimate sourced + // from incoming structural relations, a ranked list of next + // hops, and (where the graph carries them) open problems. + // + // The synthesis must be deterministic — no Math.random — and it + // must avoid any phrase from tools/audit_card_meta_language.py's + // ban list (we test against the list internally below). + // ============================================================ + + _insightReport(node) { + if (!node) return ""; + // External resource kinds — skip the panel entirely. + if (node.kind === "lab" || node.kind === "channel") return ""; + + const adj = this.adj.get(node.id) || { in: [], out: [] }; + const summary = this._insightSummary(node, adj); + const importance = this._insightImportance(node, adj); + const suggestions = this._insightNextSuggestions(node, adj); + const problems = this._insightOpenProblems(node, adj); + + const html = []; + html.push(`
`); + html.push(`

动态洞察 / Dynamic insight

`); + html.push(`

${summary}

`); + html.push(`
`); + html.push(`

这条节点为什么重要

${importance}

`); + html.push(`

下一步可读

${suggestions}
`); + html.push(`

开放问题

${problems}
`); + html.push(`
`); + html.push(`
`); + // Internal guard — ensure no banned meta-language slipped into our + // synthesised text. We never display this warning to the user; it's a + // belt-and-braces check meant to surface in dev consoles only. + const joined = html.join(""); + if (this._hasMetaLeakage(joined)) { + try { (window.console && console.warn && console.warn("[insight-report] meta-language leakage detected for", node.id)); } catch (_e) { /* noop */ } + } + return joined; + } + + // 1) Summary — prefer authored `summary_zh`, fall back to `summary`, + // else synthesise from kind + label + most-frequent edge relation. + _insightSummary(node, adj) { + const authored = (node.summary_zh && String(node.summary_zh).trim()) + || (node.summary && String(node.summary).trim()); + if (authored) { + // Clip to ~2 sentences worth — defensive against extremely long YAML + // summaries that would dominate the panel. + const clipped = this._clipSentences(authored, 2, 220); + return this._escape(clipped); + } + // Synthesise. + const kindLabel = this._kindLabel(node.kind); + const label = node.label_zh || node.label || node.id; + const freq = new Map(); + for (const { rel } of adj.out) freq.set(rel, (freq.get(rel) || 0) + 1); + for (const { rel } of adj.in) freq.set(rel, (freq.get(rel) || 0) + 1); + let topRel = null; let topCount = 0; + // Iteration order on Map is insertion order — deterministic. + for (const [r, c] of freq) { + if (c > topCount) { topCount = c; topRel = r; } + } + if (topRel) { + return `${this._escape(label)} 是一条 ${this._escape(kindLabel)} 节点,邻接里最常出现的是 ${this._escape(this._relLabel(topRel))}(${topCount} 次),可由此切入理解它在图谱里的位置。`; + } + return `${this._escape(label)} 是一条 ${this._escape(kindLabel)} 节点,目前邻接较稀疏,建议先从同主题节点回看。`; + } + + // 2) Importance — count incoming composes/validates/manifests/motivates, + // cite specific node names. + _insightImportance(node, adj) { + const incoming = adj.in.filter(e => ["composes", "validates", "manifests", "motivates"].includes(e.rel)); + // Bucket by relation, deterministic order + const order = ["composes", "validates", "manifests", "motivates"]; + const byRel = new Map(order.map(r => [r, []])); + for (const e of incoming) { + const list = byRel.get(e.rel); + if (list) list.push(e.other); + } + const total = incoming.length; + if (!total) { + // Try a softer fallback — any incoming edges at all. + const anyIn = adj.in.length; + if (!anyIn) { + return `当前图谱里没有指向这条节点的关系,可能是新接入或处于研究范式的源头。`; + } + return `当前没有结构性引用(composes / validates / manifests / motivates),但仍被 ${anyIn} 条其它关系连接,更多线索见 锚点链路 标签页。`; + } + // Pick up to 3 most-cited representative nodes (across all four buckets) + const ranked = []; + for (const r of order) { + for (const id of byRel.get(r)) { + const o = this.byId.get(id); + if (!o) continue; + ranked.push({ id, o, rel: r, deg: typeof o.degree === "number" ? o.degree : (this.adj.get(id)?.in?.length || 0) }); + } + } + ranked.sort((a, b) => (b.deg - a.deg) || a.id.localeCompare(b.id)); + const exemplars = ranked.slice(0, 3); + // Compose Chinese counts: "被 N 条 paradigm/validation/manifests 关系引用" + const parts = []; + for (const r of order) { + const c = byRel.get(r).length; + if (c) parts.push(`${c} 条 ${this._relLabel(r)}`); + } + const countText = parts.join(" + "); + let examplesHtml = ""; + if (exemplars.length) { + const links = exemplars.map(({ o }) => this._anchorTag(o)).join("、"); + examplesHtml = `,例如 ${links}`; + } + return `被 ${countText} 关系共 ${total} 次引用${examplesHtml}。`; + } + + // 3) Next suggestions — outbound neighbours, prefer enables/extends/motivates, + // avoid parallel/unsolved_by but keep them as fallback. Up to 5. + _insightNextSuggestions(node, adj) { + if (!adj.out.length) { + return `

这条节点几乎没有显式衍生,请回到 锚点链路 标签页查看完整邻接。

`; + } + // Deduplicate destinations, keeping the most informative relation seen. + const relPriority = { + enables: 10, extends: 9, motivates: 8, composes: 7, validates: 6, + manifests: 5, covers: 4, feeds: 4, implements: 4, prereq: 3, contrasts: 3, + parallel: 2, unsolved_by: 1, + }; + const best = new Map(); // other -> {rel, other} + for (const e of adj.out) { + const prev = best.get(e.other); + if (!prev || (relPriority[e.rel] || 0) > (relPriority[prev.rel] || 0)) { + best.set(e.other, { rel: e.rel, other: e.other }); + } + } + // Pull nodes, attach importance score (degree). + const candidates = []; + for (const { rel, other } of best.values()) { + // unsolved_by goes to the open-problem block, not here. + if (rel === "unsolved_by") continue; + const o = this.byId.get(other); + if (!o) continue; + const deg = typeof o.degree === "number" ? o.degree : (this.adj.get(o.id)?.in?.length || 0); + candidates.push({ rel, o, deg, prio: relPriority[rel] || 0 }); + } + if (!candidates.length) { + return `

这条节点几乎没有显式衍生,请回到 锚点链路 标签页查看完整邻接。

`; + } + // Deterministic sort: relation priority desc, then degree desc, then id asc. + candidates.sort((a, b) => (b.prio - a.prio) || (b.deg - a.deg) || a.o.id.localeCompare(b.o.id)); + const pick = candidates.slice(0, 5); + const lis = pick.map(({ rel, o }) => { + const reason = this._suggestionReason(rel); + return `
  • ${this._anchorTag(o)} — ${this._escape(reason)}
  • `; + }); + return `
      ${lis.join("")}
    `; + } + + _suggestionReason(rel) { + return ({ + enables: "建立在这条节点之上,把抽象推到具体工程", + extends: "顺这条节点继续深入", + motivates: "这条节点解释了为什么要研究它", + composes: "它从这条节点借了关键组件", + validates: "它把这条节点变成可验证的具体方案", + parallel: "并行思路,可作对照", + contrasts: "和这条节点形成对立,理解二者差异能澄清边界", + prereq: "先修前置,没读它读这条会卡住", + covers: "围绕这条节点展开讲解,便于快速建立全局图景", + feeds: "把这条节点的产出输给下一环,是衔接节奏的关键", + manifests: "在这条节点上具体显形,可看到抽象落到具体范例", + implements: "把这条节点写成可运行的代码或工程实现", + })[rel] || "由邻接关系给出的相关线索"; + } + + // 4) Open problems — outbound unsolved_by + outbound motivates → problem:* + _insightOpenProblems(node, adj) { + const seen = new Set(); + const items = []; + for (const { rel, other } of adj.out) { + if (!["unsolved_by", "motivates"].includes(rel)) continue; + const o = this.byId.get(other); + if (!o) continue; + if (rel === "motivates" && o.kind !== "problem") continue; + if (seen.has(o.id)) continue; + seen.add(o.id); + const note = this._clipSentences(o.summary_zh || o.summary || "", 1, 30); + items.push({ o, note }); + if (items.length >= 3) break; + } + if (!items.length) { + return `

    没有显式的 unsolved_by / motivates 边指向待解问题。

    `; + } + const lis = items.map(({ o, note }) => { + const tail = note ? ` — ${this._escape(note)}` : ""; + return `
  • ${this._anchorTag(o)}${tail}
  • `; + }); + return ``; + } + + _anchorTag(node) { + const label = node.label_zh || node.label || node.id; + return `${this._escape(label)}`; + } + + // Clip free text to roughly N sentences but no more than maxChars characters. + // Splits on Chinese full-width punctuation as well as ASCII sentence-enders. + _clipSentences(text, n, maxChars) { + const s = String(text || "").replace(/\s+/g, " ").trim(); + if (!s) return ""; + const re = /[^。!?!?\.]+[。!?!?\.]?/g; + const parts = s.match(re) || [s]; + let acc = ""; + for (let i = 0; i < parts.length && i < n; i++) { + const next = (acc + parts[i]).trim(); + if (next.length > maxChars) { + // Take what fits. + if (!acc) acc = next.slice(0, maxChars); + break; + } + acc = next; + } + if (!acc) acc = s.slice(0, maxChars); + if (acc.length > maxChars) acc = acc.slice(0, maxChars); + return acc; + } + + // Internal mirror of tools/audit_card_meta_language.py's HARD_BANS list. + // Used to flag any leakage in synthesised text — never displayed to the + // user; only logs to console for developer attention. + _hasMetaLeakage(html) { + const HARD_BANS = [ + "本卡片","本节将","本节回答","本节聚焦","本节旨在","本文档", + "为了让读者","为了让初学者","为了让新手","为了让资深", + "顶刊审稿","顶刊文章","新入门的研究者","资深科研", + "0 到 1","0到1","1 到 10000","1到10000", + "学习地图","超级学科星图","对话广场","推演星空","本卡片旨在", + "as a top-tier reviewer","as a top journal reviewer", + "for beginners and experts","this card will","this section will", + ]; + for (const phrase of HARD_BANS) { + if (html.includes(phrase)) return true; + } + return false; + } }