| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- /* views/insights.js —— 归纳区:给判断,不是统计报表。
- * 每条归纳展示三件事:多少人独立提到 / 几条带原文 / 是哪几条记录支撑(可点开追溯)。
- * 可按交叉人数、原文证据数排序,并可叠加证据/品类/成员筛选。 */
- (function () {
- "use strict";
- const T = window.KF_TAXONOMY;
- const U = window.KF_UTIL;
- const Q = window.KF_QUERY;
- const UI = window.KF_UI;
- const esc = U.esc;
- const SORTS = {
- owners: { label: "交叉人数", fn: (a, b) => b.ownerCount - a.ownerCount || b.quoteCount - a.quoteCount },
- quotes: { label: "原文证据", fn: (a, b) => b.quoteCount - a.quoteCount || b.ownerCount - a.ownerCount },
- newest: { label: "最新", fn: (a, b) => new Date(b.createdAt) - new Date(a.createdAt) }
- };
- /* 归纳 → 它归纳自哪些「报告」:支撑成果里带完整报告的,就是这层结论的另一条来路。
- * 报告要讲清楚:这份报告里哪几条结论引用了本归纳的支撑成果、每条结论有几条原文。
- * 归纳可以用 citedInsightIds 指明"对应报告里的哪几条结论"(没有就用交集推断)。 */
- function sourceReports(i) {
- return i.artifacts.filter((a) => a.report).map((a) => {
- const ev = a.report.evidence || {};
- const preferred = Array.isArray(i._raw.citedInsightIds) && i._raw.citedInsightIds.length
- ? new Set(i._raw.citedInsightIds) : null;
- const hits = Object.keys(ev)
- .filter((k) => (ev[k].derivedFrom || []).some((id) => i.artifactIds.includes(id)))
- .map((k) => ({ id: k, claim: ev[k].claim, quotes: (ev[k].quotes || []).length, refs: (ev[k].derivedFrom || []).filter(Boolean).length,
- preferred: preferred ? preferred.has(k) : false }))
- .sort((x, y) => (y.preferred ? 1 : 0) - (x.preferred ? 1 : 0));
- return { report: a, hits };
- });
- }
- function rowOf(i) {
- const arts = i.artifacts;
- const cats = [...new Set(arts.map((a) => (a._raw && a._raw.category) || "").filter(Boolean))];
- return {
- id: i.id,
- priority: "",
- stageObj: null,
- category: i.category || cats[0] || "",
- layer: "sediment",
- ownerName: "",
- evidenceKey: i.unaligned ? "speculative" : "verified",
- title: i.title,
- summary: i.body,
- members: new Set(arts.map((a) => a.ownerName)),
- _i: i
- };
- }
- /* 归纳的筛选:证据(是否有原文)/ 成员(谁提到过)/ 品类 */
- function matchInsight(r, c) {
- if (c.evidence.length) {
- const ok = c.evidence.some((v) => (v === "verified" ? !r._i.unaligned : v === "speculative" ? r._i.unaligned : true));
- if (!ok) return false;
- }
- if (c.member.length && !c.member.some((m) => r.members.has(m))) return false;
- if (c.category.length && !c.category.includes(r.category)) return false;
- if (c.stage.length) return false; /* 归纳不绑定阶段;有阶段条件时结果为空 */
- if (c.q) {
- const hay = (r._i.title + " " + r._i.body + " " + r.category).toLowerCase();
- if (!hay.includes(c.q)) return false;
- }
- return true;
- }
- function facet(rows, c, key, testFn, keyFn) {
- const m = new Map();
- for (const r of rows) {
- if (!testFn(r, { ...c, [key]: [] })) continue;
- const k = keyFn(r);
- (Array.isArray(k) ? k : [k]).forEach((x) => x && m.set(x, (m.get(x) || 0) + 1));
- }
- return m;
- }
- function filterBar(db, rows, c) {
- const ec = facet(rows, c, "evidence", matchInsight, (r) => r.evidenceKey);
- const cc = facet(rows, c, "category", matchInsight, (r) => r.category);
- const mc = facet(rows, c, "member", matchInsight, (r) => [...r.members]);
- const memberOpts = [...mc.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], "zh"));
- const dims = [
- { key: "evidence", label: "证据", options: [
- { value: "verified", label: "有原文支撑", count: ec.get("verified") || 0 },
- { value: "speculative", label: "无原文(推测)", count: ec.get("speculative") || 0 }
- ] },
- { key: "category", label: "品类", options: [...cc.entries()].map(([v, n]) => ({ value: v, label: v, count: n })) },
- { key: "member", label: "提到过的人", multi: true, options: memberOpts.map(([v, n]) => ({ value: v, label: v, count: n })) }
- ];
- dims.forEach((d) => { d.active = c[d.key] || []; });
- return UI.filterBar(dims, c, rows.length, "条归纳");
- }
- function judgement(i) {
- const n = i.ownerCount;
- const a = i.artifacts.length;
- const q = i.verifiedArts;
- if (a === 0) {
- return `<b>${n} 人</b>提到,但目前<b class="warn-txt">没有可追溯的支撑记录</b>——结论还不能拿去回灌。`;
- }
- if (i.originBroken) {
- return `<b>${n} 人</b>提到,${a} 条支撑记录里<b class="warn-txt">有 ${i.noOriginCount} 条缺少出身</b>(不知道是谁、在哪个会话交的),按规定不能进沉淀层。`;
- }
- if (i.unaligned) {
- return `<b>${n} 人</b>提到,有 ${a} 条记录支撑,但其中<b class="warn-txt">没有一条带原文</b>,目前是观点而非事实。`;
- }
- if (q === a) {
- return `<b>${n} 人</b>独立提到,<b>${a} 条</b>支撑记录全部带原文,可追溯。`;
- }
- return `<b>${n} 人</b>独立提到,<b>${q} 条</b>带原文证据,其余 ${a - q} 条为佐证或模型推测。`;
- }
- function gauge(n) {
- const max = Math.max(5, ...window.__kf_all_insights.map((x) => x.ownerCount));
- const lit = Math.max(1, Math.round((n / max) * 5));
- return `<span class="gauge" aria-hidden="true">${Array.from({ length: 5 }).map((_, k) => `<i class="${k < lit ? "on" : ""}"></i>`).join("")}</span>`;
- }
- function item(i) {
- const reps = sourceReports(i);
- const totalHits = reps.reduce((n, x) => n + x.hits.length, 0);
- return `<article class="insight" data-insight="${esc(i.id)}">
- <div class="insight-top">
- <div style="min-width:0">
- <h4 data-act="open-insight" data-id="${esc(i.id)}">${esc(i.title)}</h4>
- ${i.body ? `<p class="lead">${esc(i.body)}</p>` : ""}
- </div>
- </div>
- <div class="strength">
- <span class="kpi-inline">
- <span><b>${i.ownerCount}</b>人提到</span>
- <span class="${i.unaligned ? "warn" : (i.quoteCount === 0 ? "zero" : "")}"><b>${i.quoteCount}</b>条原文</span>
- </span>
- ${gauge(i.ownerCount)}
- <span class="gauge-l">交叉强度</span>
- </div>
- <div class="judge">${judgement(i)}</div>
- <div class="meta">
- ${UI.layerBadge("sediment")}
- ${i.category ? `<span>品类 ${esc(i.category)}</span>` : ""}
- ${i.owners.length ? `<span>${esc(i.owners.slice(0, 4).join("、"))}${i.owners.length > 4 ? ` 等 ${i.owners.length} 人` : ""}</span>` : ""}
- ${i.sources.length ? `<span>原材料出处 ${i.sources.length} 条</span>` : ""}
- </div>
- <div class="insight-foot">
- <button class="trace-btn" data-act="open-insight" data-id="${esc(i.id)}">看 ${i.artifacts.length} 条支撑记录 ${UI.icons.arrow}</button>
- ${reps.length ? `<button class="trace-btn src-toggle" data-act="toggle-sources" data-id="${esc(i.id)}" aria-expanded="false" aria-controls="src-${esc(i.id)}">归纳自 ${reps.length} 份报告(${totalHits} 条结论)${UI.icons.chevron}</button>` : ""}
- ${i.originBroken ? `<span class="una">${i.noOriginCount} 条支撑记录缺少出身</span>` : ""}
- ${i.unaligned ? `<span class="una">证据级别与原文不一致</span>` : ""}
- </div>
- ${reps.length ? `<div class="insight-sources" id="src-${esc(i.id)}" hidden>
- ${reps.map(({ report: a, hits }) => `
- <div class="isrc">
- <div class="isrc-h">
- <a href="#/report/${encodeURIComponent(a.id)}" data-act="open-report" data-id="${esc(a.id)}">${UI.icons.doc}${esc(a.report.title || a.shortTitle)}</a>
- <span class="tiny muted">${esc(a.ownerName)} · ${esc(a.stageObj ? a.stageObj.code : a.stage)} · ${(a.report.chapters || []).length} 章</span>
- </div>
- ${hits.length ? hits.map((h) => `<div class="isrc-q">
- <div>${esc(h.claim)}</div>
- <div class="tiny muted">${h.refs} 条支撑成果 · ${h.quotes} 条原文 · <a href="#" data-act="open-evidence" data-id="${esc(h.id)}">看这条证据链</a></div>
- </div>`).join("") : `<div class="tiny muted">这份报告里有支撑成果,但没有把本结论列进证据链。</div>`}
- </div>`).join("")}
- </div>` : ""}
- </article>`;
- }
- function render(db, c) {
- const rows = db.insights.map(rowOf);
- window.__kf_all_insights = db.insights;
- const hit = rows.filter((r) => matchInsight(r, c));
- const sortKey = SORTS[c.sort] ? c.sort : "owners";
- const sorted = hit.slice().sort(SORTS[sortKey].fn).map((r) => r._i);
- const focus = c.ins || null;
- let body;
- if (!db.insights.length) {
- body = UI.empty({ title: "还没有归纳结论", body: "当过程层成果积累后,这里的每一条都会写清:多少人独立提到、几条有原文、具体靠哪几条记录撑着。点开任何一条都能看到原文。" });
- } else if (!sorted.length) {
- body = UI.empty({ title: "当前条件下没有归纳", body: `库里有 ${db.insights.length} 条归纳,但没有同时满足这些条件的。`, actions: [{ label: "清除全部筛选", act: "clear-filters" }] });
- } else {
- body = `<div class="panel">${sorted.map(item).join("")}</div>`;
- }
- return `<div class="view-head"><div>
- <h1>归纳</h1>
- <p class="lede">每条结论必须说得清三件事:<b>多少人独立提到</b>、<b>几条带原文</b>、<b>是哪几条记录撑起来的</b>。数字之外要能直接给出判断。</p>
- </div>
- <div class="aside"><div class="seg" role="group" aria-label="排序">
- ${Object.entries(SORTS).map(([k, s]) => `<button class="${k === sortKey ? "on" : ""}" data-act="sort" data-sort="${k}">${esc(s.label)}</button>`).join("")}
- </div></div>
- </div>
- <div data-filterbar>${filterBar(db, rows, c)}</div>
- <div data-active>${UI.activeLine(c)}</div>
- <div data-list>${body}</div>
- ${focus ? `<div data-focus-insight="${esc(focus)}"></div>` : ""}`;
- }
- window.KF_VIEW_INSIGHTS = { render, SORTS };
- })();
|