Files

1189 lines
55 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/* ==================================================================
Natural Memory v2 — site.js(多页版)
共享外壳(顶栏读数、页脚、入场动效)+ 按 body[data-page] 分派的页面渲染。
所有内容来自 window.NM_DATA / window.NM_EXAMPLES,保证与数据层单一来源。
================================================================== */
(function () {
"use strict";
var D = window.NM_DATA;
if (!D) return;
/* ---------- 工具 ---------- */
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
var el = function (tag, cls, html) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (html != null) n.innerHTML = html;
return n;
};
var esc = function (s) {
return String(s == null ? "" : s).replace(/[&<>"]/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c];
});
};
var fmt = function (n, d) { return Number(n).toFixed(d == null ? 2 : d); };
var num = function (s) { var p = String(s).split("/"); return { c: Number(p[0]), n: Number(p[1]) }; };
var arrow = '<span class="arrow">→</span>';
var setText = function (sel, v) { var n = $(sel); if (n) n.textContent = v; };
var copyText = function (btn, text) {
var done = function () {
btn.textContent = "已复制";
btn.classList.add("is-done");
setTimeout(function () { btn.textContent = "复制"; btn.classList.remove("is-done"); }, 1900);
};
var fallback = function () {
var ta = document.createElement("textarea");
ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); done(); } catch (e) { btn.textContent = "复制失败"; }
document.body.removeChild(ta);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, fallback);
} else { fallback(); }
};
/* ---------- 入场与数值动画 ----------
注意:这里**不把 IntersectionObserver 当唯一触发源**。
IO 在无头虚拟时间、被节流的 iframe、以及某些低端 WebView 里都不保证投递,
而「内容卡在隐藏态」是绝不能出现的故障 —— 尤其是 clip-path 擦除,
一次不触发就是一个永远看不见的标题。所以 IO 之外还有一条按滚动位置判定的兜底。 */
var pending = [];
var revealNow = function (node) {
if (!node || (node.classList && node.classList.contains("is-in"))) return;
node.classList.add("is-in");
$$("[data-fill]", node).forEach(function (f) { f.style.width = f.getAttribute("data-fill") + "%"; });
if (node.hasAttribute && node.hasAttribute("data-fill")) {
node.style.width = node.getAttribute("data-fill") + "%";
}
};
var io = ("IntersectionObserver" in window)
? new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
revealNow(e.target);
io.unobserve(e.target);
var k = pending.indexOf(e.target);
if (k > -1) pending.splice(k, 1);
});
}, { rootMargin: "0px 0px -8% 0px", threshold: 0.06 })
: null;
/* 兜底:每帧/每次滚动再按位置判一次,进视口就放出来 */
var sweepPending = function () {
if (!pending.length) return;
var vh = window.innerHeight || document.documentElement.clientHeight;
pending = pending.filter(function (n) {
var r = n.getBoundingClientRect();
if (r.top < vh * 0.94 && r.bottom > -60) {
revealNow(n);
if (io) io.unobserve(n);
return false;
}
return true;
});
};
window.addEventListener("scroll", sweepPending, { passive: true });
window.addEventListener("resize", sweepPending);
var watch = function (node) {
if (!node) return;
if (io) { pending.push(node); io.observe(node); }
else { revealNow(node); return; }
sweepPending(); /* 已经在视口里的立刻放出来 */
};
var bar = function (cls, pct, label, valueHtml) {
return '<div class="bar ' + cls + '">' +
'<span class="bar__l">' + esc(label) + "</span>" +
'<span class="bar__track"><span class="bar__fill" data-fill="' + pct + '"></span></span>' +
'<span class="bar__v">' + valueHtml + "</span>" +
"</div>";
};
/* ================= 共享外壳 ================= */
(function shell() {
var header = $("#site"), progress = $("#progress");
var onScroll = function () {
var y = window.scrollY || document.documentElement.scrollTop;
if (header) header.classList.toggle("is-stuck", y > 8);
var h = document.documentElement.scrollHeight - window.innerHeight;
if (progress) progress.style.width = (h > 0 ? Math.min(100, (y / h) * 100) : 0) + "%";
};
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
// 键盘导航:← / → 切换上一节、下一节(线性风格里的快捷键触感)
document.addEventListener("keydown", function (e) {
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
var t = e.target;
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
var sel = e.key === "ArrowLeft" ? ".pn__a--prev" : (e.key === "ArrowRight" ? ".pn__a--next" : null);
if (!sel) return;
var a = $(sel);
if (a && a.getAttribute("href")) { e.preventDefault(); location.href = a.getAttribute("href"); }
});
// 页脚:来源文件 + 免责声明
var ff = $("#footFiles");
if (ff) {
D.reproduce.files.forEach(function (x) {
ff.appendChild(el("span", "src", esc(x.v.split("\\").pop())));
});
}
setText("#disclaimer", D.meta.disclaimer);
// 服务端运行时读数
var chip = $("#serverChipText"), foot = $("#footServer");
fetch("api/status", { cache: "no-store" })
.then(function (r) { if (!r.ok) throw new Error("bad"); return r.json(); })
.then(function (s) {
if (chip) {
chip.textContent = "LIVE · UP " + s.uptime;
var c = $("#serverChip");
if (c) c.title = "本站服务端运行时读数 · bind " + s.bind;
}
if (foot) {
foot.innerHTML =
"<span>host " + esc(s.host) + "</span>" +
"<span>bind " + esc(s.bind) + "</span>" +
"<span>python " + esc(s.python) + "</span>" +
"<span>uptime " + esc(s.uptime) + "</span>" +
"<span>requests " + esc(s.requests) + "</span>";
}
})
.catch(function () {
if (chip) chip.textContent = "STATIC";
if (foot) foot.innerHTML = "<span>运行时读数不可用(/api/status 无响应)</span>";
});
/* ---------- 全站检索(命令面板) ----------
索引由 build.py 生成到 assets/js/search.js,纯客户端,不发任何网络请求。
快捷键:/ 或 Ctrl/Cmd+K 打开,Esc 关闭,↑↓ 选择,↵ 打开。 */
(function palette() {
var pal = $("#pal"), input = $("#palInput"), list = $("#palList"), n = $("#palN");
if (!pal || !input || !list) return;
var IDX = window.NM_SEARCH || [];
var HERE = document.body.getAttribute("data-page") || "";
var hits = [], cur = 0, open = false;
var low = function (s) { return String(s == null ? "" : s).toLowerCase(); };
function mark(text, q) {
if (!q) return esc(text);
var i = low(text).indexOf(q);
if (i < 0) return esc(text);
return esc(text.slice(0, i)) + "<mark>" + esc(text.slice(i, i + q.length)) + "</mark>" +
esc(text.slice(i + q.length));
}
function score(e, q) {
var h = low(e.h), t = low(e.t), p = low(e.pg), s = 0;
if (h === q) s += 100;
else if (h.indexOf(q) === 0) s += 60;
else if (h.indexOf(q) > -1) s += 40;
if (p.indexOf(q) > -1) s += 25;
if (t.indexOf(q) > -1) s += 14;
return s;
}
function query(raw) {
var q = low(raw).trim();
if (!q) return IDX.filter(function (e) { return e.d; }).map(function (e) { return { e: e, q: "" }; });
var out = [];
IDX.forEach(function (e) { var s = score(e, q); if (s > 0) out.push({ e: e, q: q, s: s }); });
out.sort(function (a, b) { return b.s - a.s || String(a.e.n).localeCompare(String(b.e.n)); });
return out.slice(0, 40);
}
function slugOf(href) { return href === "./" ? "index" : String(href).replace(/^\.\//, ""); }
function findHeading(text) {
var want = String(text).replace(/\s+/g, "");
var found = null;
$$("#doc h2, #doc h3").forEach(function (h) {
if (!found && h.textContent.replace(/\s+/g, "") === want) found = h;
});
return found;
}
function jumpTo(node) {
var reduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
var y = node.getBoundingClientRect().top + (window.scrollY || 0) - 84;
window.scrollTo({ top: y < 0 ? 0 : y, behavior: reduce ? "auto" : "smooth" });
node.classList.add("is-flash");
setTimeout(function () { node.classList.remove("is-flash"); }, 1700);
}
function render(q) {
hits = query(q);
cur = 0;
list.innerHTML = hits.length ? hits.map(function (h, i) {
var e = h.e, title = e.d ? e.pg : (e.h || e.pg);
var sub = e.h ? e.t : (e.d ? e.t : "");
return '<a class="pal__i' + (i === 0 ? " is-cur" : "") + '" role="option" aria-selected="' +
(i === 0 ? "true" : "false") + '" data-i="' + i + '" href="' + esc(e.p) + '"' +
' data-h="' + esc(e.d ? "" : (e.h || "")) + '">' +
'<span class="pal__i-n mono">' + esc(e.n) + "</span>" +
'<span class="pal__i-b"><span class="pal__i-h">' + mark(title, h.q) + "</span>" +
(sub ? '<span class="pal__i-t">' + mark(sub, h.q) + "</span>" : "") + "</span>" +
'<span class="pal__i-g">' + esc(e.g) + "</span></a>";
}).join("") : '<p class="pal__none">没有匹配的内容。试试「拒答」「写入路径」「召回」「SHA256」这类词。</p>';
if (n) n.textContent = hits.length ? hits.length + " 条" : "";
list.scrollTop = 0;
}
function select(i) {
var items = $$(".pal__i", list);
if (!items.length) return;
cur = Math.max(0, Math.min(items.length - 1, i));
items.forEach(function (el2, k) {
el2.classList.toggle("is-cur", k === cur);
el2.setAttribute("aria-selected", k === cur ? "true" : "false");
});
var it = items[cur];
if (it.offsetTop < list.scrollTop) list.scrollTop = it.offsetTop - 6;
else if (it.offsetTop + it.offsetHeight > list.scrollTop + list.clientHeight)
list.scrollTop = it.offsetTop + it.offsetHeight - list.clientHeight + 6;
}
function activate(a) {
var heading = a.getAttribute("data-h") || "";
var href = a.getAttribute("href") || "./";
var target = slugOf(href);
// 同页结果一律就地处理,绝不重新加载
if (target === HERE) {
var node = heading ? findHeading(heading) : null;
close();
if (node) { jumpTo(node); return; }
window.scrollTo({ top: 0, behavior: "smooth" });
var hd = $("#doc .doc__hd") || $("#doc h1");
if (hd) {
hd.classList.add("is-flash");
setTimeout(function () { hd.classList.remove("is-flash"); }, 1700);
}
return;
}
try { sessionStorage.setItem("nm.pal", JSON.stringify({ s: target, h: heading })); }
catch (e) { /* 无存储时退化为普通跳转 */ }
// 键盘回车不会触发 <a> 的默认激活,所以这里显式跳转
location.href = href;
}
function show() {
open = true;
pal.hidden = false;
document.documentElement.classList.add("pal-open");
input.value = "";
render("");
input.focus();
}
function close() {
open = false;
pal.hidden = true;
document.documentElement.classList.remove("pal-open");
}
var btn = $("#palBtn"), mask = $("#palMask");
if (btn) btn.addEventListener("click", show);
if (mask) mask.addEventListener("click", close);
input.addEventListener("input", function () { render(input.value); });
list.addEventListener("mousemove", function (e) {
var a = e.target && e.target.closest ? e.target.closest(".pal__i") : null;
if (a) select(Number(a.getAttribute("data-i")));
});
list.addEventListener("click", function (e) {
var a = e.target && e.target.closest ? e.target.closest(".pal__i") : null;
if (!a) return;
e.preventDefault();
activate(a);
});
input.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown") { e.preventDefault(); select(cur + 1); }
else if (e.key === "ArrowUp") { e.preventDefault(); select(cur - 1); }
else if (e.key === "Enter") {
var a = $$(".pal__i", list)[cur];
if (a) { e.preventDefault(); activate(a); }
}
});
document.addEventListener("keydown", function (e) {
var t = e.target;
var typing = !!(t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable));
if (e.key === "Escape" && open) { close(); if (t === input) input.blur(); return; }
if (open) return;
if ((e.ctrlKey || e.metaKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); show(); return; }
if (e.key === "/" && !typing && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); show(); }
});
/* 从检索跨页跳进来时,落到对应小节并闪一下 */
try {
var saved = JSON.parse(sessionStorage.getItem("nm.pal") || "null");
sessionStorage.removeItem("nm.pal");
if (saved && saved.h && saved.s === HERE) {
var node2 = findHeading(saved.h);
if (node2) setTimeout(function () { jumpTo(node2); }, 60);
}
} catch (e) { /* 忽略 */ }
})();
})();
/* ================= 各页面渲染 ================= */
var PAGES = {};
/* ================= 重启实验装置 =================
数据全部来自 window.NM_RESTART(逐字取自工程原始 JSON)。
装置只做一件事:把「写入 → 重启 → 召回 → 作答」四个节拍的真实字段
按顺序放出来。修复前 / 修复后 两个版本可以随时对照。
================================================= */
function restartInstrument() {
var R = window.NM_RESTART, host = $("#rst");
if (!R || !host) return;
var beatsBox = $("#rstBeats"), playBtn = $("#rstPlay"), out = $("#rstOut");
var reduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
var vi = 0, timer = null, items = [];
/* 默认停在哪一版:由数据决定,不靠 HTML 里手写的 is-on(两者不一致过一次) */
R.variants.forEach(function (v, k) { if (v.key === "after") vi = k; });
/* --- 四个节拍的骨架 --- */
R.beats.forEach(function (b, i) {
var li = el("li", "rst__b");
li.dataset.i = i;
li.innerHTML =
'<span class="rst__i">' + esc(b.i) + "</span>" +
"<div>" +
'<p class="rst__bt">' + esc(b.t) + "</p>" +
'<p class="rst__bd">' + esc(b.s) + "</p>" +
'<div class="rst__data" id="rstData' + i + '"></div>' +
"</div>";
li.addEventListener("click", function () { stop(); show(i); });
beatsBox.appendChild(li);
items.push(li);
});
/* --- 每个节拍要露出的真实字段 --- */
function beatData(v, i) {
var L = R.limits, after = v.key === "after";
if (i === 0) return [
["automatic_write", "true"],
["write_changed", String(v.writeChanged) + (v.writeChanged ? " → 记录已落库" : " → 记录没有落库")],
["records", "0 → " + v.store.records],
["coarse_index_buckets", "0 → " + v.store.coarse_index_buckets]
];
if (i === 1) return [
["history_passed_to_restart", "false → 重启不携带任何对话历史"],
["resident_records", v.store.resident_records + " 条留在进程内存"],
["gpu_cache records / tokens", v.store.gpu_cache_records + " / " + v.store.gpu_cache_tokens],
["gpu_cache_bytes", v.store.gpu_cache_bytes + " B"],
["容量上限", L.capacityRecords.toLocaleString("en-US") + " 条 · " + L.maxPages.toLocaleString("en-US") + " 页 × " + L.pageCapacity + " 条"]
];
if (i === 2) {
if (!after) return [
["need_memory", "false"],
["stop_reason", v.stopReason],
["confidence", fmt(v.confidence, 4)],
["page_ids / record_ids", "(空)"]
];
return [
["need_memory", "true"],
["page_ids", v.pageIds.join(", ") + " score " + fmt(v.pageScores[0], 4)],
["record_ids", v.recordIds.join(", ") + " score " + fmt(v.recordScores[0], 4)],
["hop_count", String(v.hopCount)],
["confidence", fmt(v.confidence, 4)],
["stop_reason", v.stopReason]
];
}
return [
["prefix_length", v.prefixLength + " tok (KV 预算 " + L.kvBudgetTokens.toLocaleString("en-US") + " tok)"],
["prefix_used", String(v.prefixUsed)],
["response", v.response],
["router_recalled_after_restart", String(v.recalled)],
["generated_contains_expected", String(v.correct)]
];
}
function variant() { return R.variants[vi]; }
function fillData() {
var v = variant();
items.forEach(function (li, i) {
var d = $("#rstData" + i, li);
if (!d) return;
d.innerHTML = beatData(v, i).map(function (r) {
return '<span class="rst__d"><i>' + esc(r[0]) + "</i><span>" + esc(r[1]) + "</span></span>";
}).join("");
});
setText("#rstFile", (D.meta.repo || "") + "\\dynamic_memory_lab\\" + v.file);
}
function applyGauges(i) {
var v = variant(), L = R.limits;
var restarted = i >= 1, answered = i >= 3;
var rec = i >= 0 ? v.store.records : 0;
setText("#rstRec", String(rec));
$("#rstRecBar").style.width = (rec > 0 ? 100 : 0) + "%";
setText("#rstHist", restarted ? "false" : "true(本轮对话)");
$("#rstHistBar").style.width = (restarted ? 0 : 100) + "%";
var pf = answered ? v.prefixLength : 0;
$("#rstPf").innerHTML = pf + "<small> tok</small>";
var pct = pf ? (pf / L.kvBudgetTokens) * 100 : 0;
$("#rstPfBar").style.width = pf ? Math.max(0.8, pct) + "%" : "0%";
setText("#rstPfHint", pf
? "占 KV 预算 " + pct.toFixed(2) + "% —— 记忆走的是前缀,不是把历史塞进上下文"
: "尚未注入证据前缀");
out.classList.toggle("is-idle", !answered);
var vd = $("#rstVerdict");
vd.className = "rst__verdict mono " + (answered ? (v.correct ? "is-ok" : "is-bad") : "");
vd.textContent = answered ? (v.correct ? "✓ 命中 " + R.expected : "✗ 未命中 " + R.expected) : "";
setText("#rstResp", answered ? v.response : "…");
var kv = $("#rstKv");
kv.innerHTML = answered ? [
["stop_reason", v.stopReason, false],
["prefix_used", String(v.prefixUsed), v.prefixUsed],
["confidence", fmt(v.confidence, 4), v.confidence >= 0.7],
["recalled_after_restart", String(v.recalled), v.recalled],
["contains_expected", String(v.correct), v.correct]
].map(function (r) {
return '<span class="' + (r[2] ? "is-hot" : "") + '">' + esc(r[0]) + " <b>" + esc(r[1]) + "</b></span>";
}).join("") : "";
host.classList.toggle("is-payoff", answered && v.correct);
}
function show(i) {
items.forEach(function (li, j) {
li.classList.toggle("is-on", j === i);
li.classList.toggle("is-past", j < i);
li.setAttribute("aria-current", j === i ? "step" : "false");
});
applyGauges(i);
}
function stop() {
if (timer) { clearTimeout(timer); timer = null; }
playBtn.classList.remove("is-playing");
}
function play() {
stop();
show(-1);
if (reduce) { show(R.beats.length - 1); return; }
playBtn.classList.add("is-playing");
var i = 0;
var step = function () {
show(i);
i++;
if (i < R.beats.length) timer = setTimeout(step, 1050);
else { timer = null; playBtn.classList.remove("is-playing"); }
};
timer = setTimeout(step, 260);
}
playBtn.addEventListener("click", play);
$$(".seg__b", host).forEach(function (b, k) {
b.classList.toggle("is-on", k === vi);
b.addEventListener("click", function () {
vi = k;
$$(".seg__b", host).forEach(function (x) { x.classList.toggle("is-on", x === b); });
fillData();
play();
});
});
fillData();
show(-1);
// 进入视口后自动放一次。
// 用滚动位置判定而不是 IntersectionObserver 的可见比例:装置比视口高时
// 比例阈值可能永远达不到,这是很常见的坑。触发一次后即摘掉监听。
var played = false;
function autoPlay() {
if (played) return;
var r = host.getBoundingClientRect();
var vh = window.innerHeight || document.documentElement.clientHeight;
if (r.top < vh * 0.72 && r.bottom > vh * 0.08) {
played = true;
window.removeEventListener("scroll", autoPlay);
window.removeEventListener("resize", autoPlay);
play();
}
}
window.addEventListener("scroll", autoPlay, { passive: true });
window.addEventListener("resize", autoPlay);
autoPlay();
}
/* ---- 00 总览 ---- */
PAGES.index = function () {
setText("#heroLede", D.positioning.body);
setText("#heroSrc", D.meta.repo + "\\dynamic_memory_lab");
setText("#heroDisclaimer", D.meta.disclaimer);
var H = {};
D.dirty.headline.forEach(function (h) { H[h.metric] = h; });
var ans = H["可回答正确率"], rec = H["目标记忆召回率"];
var A = D.dirty.batches[0], B = D.dirty.batches[1];
var chain = function (h) {
return fmt(h.base) + "%" + arrow + '<span class="mid">' + fmt(h.a) + "%</span>" + arrow +
'<span class="to">' + fmt(h.b) + "%</span>";
};
var items = [
{ k: "可回答正确率 · 基线 → A → B", v: chain(ans),
d: "同一 264 题题集的两个独立批次。基线 " + ans.baseRaw + ",批次 A " + ans.aRaw + ",批次 B " + ans.bRaw + ";两批次配置不同,不可合并。" },
{ k: "目标记忆召回率 · 基线 → A → B", v: chain(rec),
d: "基线没有记忆机制,召回恒为 0。批次 B 把该查的记忆查回来的比例达 " + rec.bRaw + "。" },
{ k: "读取路径额外耗时 A → B", v: fmt(A.reader) + arrow + '<span class="to">' + fmt(B.reader) + "</span> ms",
d: "记忆读取相对无记忆基线新增的耗时;批次 A " + fmt(A.reader) + " ms,批次 B 降到 " + fmt(B.reader) + " ms。" }
];
var box = $("#heroKpis");
if (box) {
items.forEach(function (it) {
box.appendChild(el("div", "kpi",
'<span class="kpi__k">' + esc(it.k) + "</span>" +
'<span class="kpi__v kpi__v--long">' + it.v + "</span>" +
'<span class="kpi__d">' + esc(it.d) + "</span>"));
});
watch(box);
}
// 发布页:结果条、读取路径图绘制、三种抵达编排、重启装置
watch($("#releaseResult"));
watch($(".teaser"));
$$(".arr-wipe, .arr-num, .arr-cards").forEach(watch);
restartInstrument();
var cite = $("#citeCopy");
if (cite) {
var text = ($("#citeBlock") && $("#citeBlock").textContent) || "";
cite.addEventListener("click", function () {
var done = function () {
cite.textContent = "已复制"; cite.classList.add("is-done");
setTimeout(function () { cite.textContent = "复制"; cite.classList.remove("is-done"); }, 1900);
};
var fallback = function () {
var ta = document.createElement("textarea");
ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); done(); } catch (e) { cite.textContent = "复制失败"; }
document.body.removeChild(ta);
};
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, fallback);
else fallback();
});
}
};
/* ---- 02 机制 ---- */
PAGES.mechanism = function () {
// 读取路径步进器
(function flow() {
var steps = D.readPath, box = $("#steps");
if (!box) return;
steps.forEach(function (s, i) {
var b = el("button", "step",
'<span class="step__i">' + String(i + 1).padStart(2, "0") + "</span>" +
'<span class="step__l">' + esc(s.label) + "</span>");
b.type = "button";
b.dataset.i = i;
b.setAttribute("aria-selected", i === 0 ? "true" : "false");
b.addEventListener("click", function () { show(i); });
b.addEventListener("mouseenter", function () { show(i); });
box.appendChild(b);
});
function show(i) {
$$(".step", box).forEach(function (b) {
var j = Number(b.dataset.i);
b.classList.toggle("is-on", j === i);
b.classList.toggle("is-past", j < i);
b.setAttribute("aria-selected", j === i ? "true" : "false");
});
setText("#flowIdx", "STAGE " + String(i + 1).padStart(2, "0") + " / " + String(steps.length).padStart(2, "0"));
setText("#flowTitle", steps[i].label);
setText("#flowBody", steps[i].detail);
setText("#flowIo", steps[i].io || "");
}
show(0);
watch(box);
})();
// 记录字段
var rf = $("#recFields");
if (rf) {
D.recordFields.forEach(function (f) {
rf.appendChild(el("div", "rec__f",
'<span class="rec__k">' + esc(f.f) + "</span>" +
'<span class="rec__d">' + esc(f.d) + "</span>"));
});
watch(rf);
}
// 真实记录示例(取自 examples.js,逐字)
var rr = $("#realRecord");
var E = window.NM_EXAMPLES || [];
if (rr && E.length && E[0].nm.records && E[0].nm.records.length) {
var r = E[0].nm.records[0];
var rows = [
["record_id", r.record_id], ["entity", r.entity], ["attribute", r.attribute],
["value", r.value], ["status", r.status], ["version", r.version],
["source", r.source], ["text_preview", r.text_preview]
].filter(function (x) { return x[1] !== undefined && x[1] !== null && x[1] !== ""; });
rr.innerHTML = '<p class="eyebrow" style="margin-bottom:16px">一条真实记录 · 逐字取自评测结果</p>' +
'<div class="recjson">' + rows.map(function (x) {
return '<div class="recjson__r"><span class="recjson__k">' + esc(x[0]) + '</span>' +
'<span class="recjson__v">' + esc(x[1]) + "</span></div>";
}).join("") + "</div>" +
'<p class="tiny" style="margin-top:16px">这条记录在评测中命中了问题 <code>user-004</code>,' +
'内容是用户给当时模型定的名字。注意「Natural Memory v1」是写入时的项目名,记录按原样保留、未做改写。</p>';
} else if (rr) {
rr.textContent = "真实记录示例不可用(examples.js 未加载)。";
}
// 状态机
var sl = $("#stateList");
if (sl) {
D.states.forEach(function (s) {
sl.appendChild(el("div", "state state--" + s.tone,
'<span class="state__s">' + esc(s.s) + "</span>" +
'<span class="state__d">' + esc(s.d) + "</span>"));
});
watch(sl);
}
};
/* ---- 03 测量结果 ---- */
PAGES.results = function () {
var tabs = $("#resultTabs"), panels = $("#resultPanels");
if (!tabs || !panels) return;
var DT = D.dirty;
var defs = [
{ id: "dirty", label: "批次 A / B 对照" },
{ id: "stage5", label: "Stage 5 结构化基准" },
{ id: "router", label: "记忆路由器工程线" }
];
var want = (new URLSearchParams(location.search).get("tab") || "").toLowerCase();
var startId = defs.some(function (d) { return d.id === want; }) ? want : defs[0].id;
var activate = function (id, push) {
$$(".tab", tabs).forEach(function (x) {
var on = x.dataset.id === id;
x.classList.toggle("is-on", on);
x.setAttribute("aria-selected", on ? "true" : "false");
});
$$(".panel", panels).forEach(function (p) {
var on = p.dataset.id === id;
p.classList.toggle("is-on", on);
p.setAttribute("aria-hidden", on ? "false" : "true");
if (on) $$("[data-fill]", p).forEach(function (f) { f.style.width = f.getAttribute("data-fill") + "%"; });
});
if (push) {
try {
var u = new URL(location.href);
u.searchParams.set("tab", id);
history.replaceState(null, "", u.pathname + u.search + u.hash);
} catch (e) { /* 仅影响地址栏 */ }
}
};
defs.forEach(function (d) {
var t = el("button", "tab" + (d.id === startId ? " is-on" : ""), esc(d.label));
t.type = "button"; t.setAttribute("role", "tab"); t.dataset.id = d.id;
t.setAttribute("aria-selected", d.id === startId ? "true" : "false");
t.addEventListener("click", function () { activate(d.id, true); });
tabs.appendChild(t);
});
/* --- 面板 1 --- */
var p1 = el("div", "panel"); p1.dataset.id = "dirty";
var h = '<p class="small" style="max-width:88ch;margin:0 0 12px">' +
"<b style='color:var(--ink)'>" + esc(DT.title) + "</b> · " + esc(DT.subtitle) + "</p>" +
'<p class="small" style="max-width:88ch;margin:0 0 12px;color:var(--ink-3)">' + esc(DT.protocol) + "</p>" +
'<p class="small" style="max-width:88ch;margin:0 0 30px;padding-left:14px;border-left:1px solid var(--acc-line)">' +
'<span class="src">出处 ' + esc(DT.src) + "</span><br>" + esc(DT.batchNote) + "</p>";
h += '<div class="legend" style="margin-bottom:20px">' +
'<span><i class="l-base"></i>原版 Qwen3.5-4B</span>' +
'<span><i class="l-a"></i>批次 A</span>' +
'<span><i class="l-nm2"></i>批次 B</span></div>';
h += '<div class="grid g-2-1"><div class="bars">';
DT.headline.forEach(function (m) {
h += '<div class="barset">' +
'<div class="barset__top"><span class="barset__t">' + esc(m.metric) + "</span>" +
'<span class="barset__n">批次 A ' + fmt(m.a) + "% → 批次 B " + fmt(m.b) + "%</span></div>" +
bar("bar--base", m.base, "原版 Qwen", fmt(m.base) + "%<small>" + esc(m.baseRaw) + "</small>") +
bar("bar--a", m.a, "批次 A", fmt(m.a) + "%<small>" + esc(m.aRaw) + "</small>") +
bar("bar--nm2", m.b, "批次 B", fmt(m.b) + "%<small>" + esc(m.bRaw) + "</small>") +
"</div>";
});
h += "</div><div>";
h += '<div class="kv">' + DT.corpus.map(function (c) {
return '<div class="kv__r"><span class="kv__k">' + esc(c.k) + '</span><span class="kv__v">' + esc(c.v) + "</span></div>";
}).join("") + "</div></div></div>";
h += '<div style="margin-top:clamp(40px,5vw,64px)"><p class="eyebrow" style="margin-bottom:18px">两个批次的运行明细</p>' +
'<div class="tbl-wrap"><table class="dtable"><thead><tr><th>指标</th>' +
DT.batches.map(function (b) {
return "<th>" + esc(b.label) + '<br><span class="src" style="text-transform:none">' + esc(b.date) + " · " + esc(b.tag) + "</span></th>";
}).join("") + "</tr></thead><tbody>";
[
{ k: "读取路径额外耗时", v: function (b) { return b.reader + " ms"; } },
{ k: "平均输入前缀 token", v: function (b) { return fmt(b.promptTokens); } },
{ k: "前缀命中率 / 平均前缀 token", v: function (b) { return fmt(b.prefixRate) + "% / " + fmt(b.prefixTokens); } },
{ k: "平均总延迟(基线 → 记忆)", v: function (b) { return b.latencyBase + " → " + b.latencyNm + " ms"; } },
{ k: "平均解码速度(基线 → 记忆)", v: function (b) { return b.decodeBase + " → " + b.decodeNm + " tok/s"; } },
{ k: "NM 显存快照峰值 allocated / reserved", v: function (b) { return fmt(b.allocNm, 3) + " / " + fmt(b.resvNm, 3) + " GiB"; } },
{ k: "active records / pages", v: function (b) { return b.records + " / " + b.pages; } },
{ k: "GPU cache token", v: function (b) { return b.cacheTokens.toLocaleString("en-US"); } }
].forEach(function (r) {
h += "<tr><td>" + esc(r.k) + "</td>" + DT.batches.map(function (b) {
return '<td class="n">' + esc(r.v(b)) + "</td>";
}).join("") + "</tr>";
});
h += "</tbody></table></div></div>";
h += '<div style="margin-top:clamp(40px,5vw,64px)"><p class="eyebrow" style="margin-bottom:20px">按数据域拆分(可回答正确率)</p><div class="grid g2">';
DT.domains.forEach(function (d) {
h += '<div class="barset"><div class="barset__top"><span class="barset__t">' + esc(d.domain) +
'</span><span class="barset__n">' + d.n + " 题</span></div>" +
bar("bar--base", d.base, "原版 Qwen", fmt(d.base) + "%<small>" + esc(d.baseRaw) + "</small>") +
bar("bar--a", d.a, "批次 A", fmt(d.a) + "%<small>" + esc(d.aRaw) + "</small>") +
bar("bar--nm2", d.b, "批次 B", fmt(d.b) + "%<small>" + esc(d.bRaw) + "</small>") + "</div>";
});
h += "</div></div>";
h += '<div style="margin-top:clamp(40px,5vw,64px)"><p class="eyebrow" style="margin-bottom:18px">按类别拆分(正确 / 题量)</p>' +
'<div class="tbl-wrap"><table class="dtable"><thead><tr><th>类别</th><th>题量</th><th>原版 Qwen</th><th>批次 A</th><th>批次 B</th><th>B 相对 A</th></tr></thead><tbody>';
DT.categories.forEach(function (c) {
var na = num(c.a), nb = num(c.b), diff = nb.c - na.c;
h += "<tr><td>" + esc(c.label) + '<br><span class="src" style="text-transform:none">' + esc(c.k) + "</span></td>" +
'<td class="n">' + c.n + '</td><td class="n">' + esc(c.base) + '</td><td class="n">' + esc(c.a) + "</td>" +
'<td class="n ' + (diff >= 0 ? "acc" : "") + '">' + esc(c.b) + "</td>" +
'<td class="n ' + (diff > 0 ? "acc" : (diff < 0 ? "bad" : "")) + '">' + (diff > 0 ? "+" : "") + diff + " 题</td></tr>";
});
h += "</tbody></table></div><p class=\"small\" style=\"margin-top:16px;max-width:88ch\">" + esc(DT.categoryNote) + "</p></div>";
h += '<div style="margin-top:clamp(40px,5vw,64px)"><p class="eyebrow" style="margin-bottom:20px">路由与生成的差距</p>' +
'<div class="gap">' +
'<div class="gap__c gap__c--recall"><span class="gap__k">批次 A · 目标记忆召回</span><span class="gap__v">' + fmt(DT.gap.recallA) + '%</span><span class="gap__n">' + esc(DT.gap.recallARaw) + "</span></div>" +
'<div class="gap__c gap__c--recall"><span class="gap__k">批次 B · 目标记忆召回</span><span class="gap__v">' + fmt(DT.gap.recallB) + '%</span><span class="gap__n">' + esc(DT.gap.recallBRaw) + "</span></div>" +
'<div class="gap__c gap__c--answer"><span class="gap__k">批次 A · 最终回答正确</span><span class="gap__v">' + fmt(DT.gap.answerA) + '%</span><span class="gap__n">' + esc(DT.gap.answerARaw) + "</span></div>" +
'<div class="gap__c gap__c--answer"><span class="gap__k">批次 B · 最终回答正确</span><span class="gap__v">' + fmt(DT.gap.answerB) + '%</span><span class="gap__n">' + esc(DT.gap.answerBRaw) + "</span></div>" +
'</div><p class="small" style="margin-top:18px;max-width:88ch">' + esc(DT.gap.note) + "</p></div>";
p1.innerHTML = h;
/* --- 面板 2:Stage 5 --- */
var p2 = el("div", "panel"); p2.dataset.id = "stage5";
var h2 = '<p class="small" style="max-width:88ch;margin:0 0 30px"><b style="color:var(--ink)">' +
esc(D.stage5.title) + "</b> · " + esc(D.stage5.subtitle) +
'<br><span class="src">出处 ' + esc(D.stage5.src) + "</span></p>" +
'<div class="legend" style="margin-bottom:20px">' +
'<span><i class="l-base"></i>原版 Qwen3.5-4B</span><span><i class="l-rag"></i>强 RAG 基线</span><span><i class="l-nm2"></i>Natural Memory v2</span></div>' +
'<div class="grid g2">';
D.stage5.sets.forEach(function (set) {
h2 += '<div><p class="eyebrow" style="margin-bottom:20px">' + esc(set.name) + " · " + set.n + " 题</p><div class=\"bars\">";
set.rows.forEach(function (r) {
var cls = /Natural Memory/.test(r.sys) ? "bar--nm2" : (/RAG/.test(r.sys) ? "bar--rag" : "bar--base");
h2 += '<div class="barset"><div class="barset__top"><span class="barset__t">' + esc(r.sys) +
'</span><span class="barset__n">' + r.lat + " ms · " + r.tok + " tok/s</span></div>" +
bar(cls, r.acc, "正确率", fmt(r.acc) + "%<small>" + esc(r.raw) + "</small>") + "</div>";
});
h2 += "</div></div>";
});
h2 += "</div>";
h2 += '<div class="tbl-wrap" style="margin-top:clamp(28px,3.4vw,44px)"><table class="dtable"><thead><tr>' +
"<th>题集</th><th>系统</th><th>正确率</th><th>原始</th><th>平均延迟</th><th>解码速度</th><th>未知拒答</th></tr></thead><tbody>";
D.stage5.sets.forEach(function (set) {
set.rows.forEach(function (r, i) {
h2 += "<tr>" + (i === 0 ? '<td rowspan="' + set.rows.length + '">' + esc(set.name) + " · " + set.n + "</td>" : "") +
"<td>" + esc(r.sys) + '</td><td class="n' + (/Natural Memory/.test(r.sys) ? " acc" : "") + '">' + fmt(r.acc) + "%</td>" +
'<td class="n">' + esc(r.raw) + '</td><td class="n">' + r.lat + ' ms</td><td class="n">' + r.tok + " tok/s</td>" +
'<td class="n">' + (r.abstain ? esc(r.abstain) : "—") + "</td></tr>";
});
});
h2 += "</tbody></table></div><p class=\"small\" style=\"margin-top:18px;max-width:88ch\">" + esc(D.stage5.note) + "</p>";
p2.innerHTML = h2;
/* --- 面板 3:路由器工程线 --- */
var p3 = el("div", "panel"); p3.dataset.id = "router";
var h3 = '<p class="small" style="max-width:88ch;margin:0 0 26px"><b style="color:var(--ink)">' +
esc(D.router.title) + "</b> · " + esc(D.router.subtitle) +
'<br><span class="src">出处 ' + esc(D.router.src) + "</span></p>" +
'<div class="grid g-2-1"><div class="tbl-wrap"><table class="dtable"><thead><tr>' +
"<th>指标</th><th>原 512 维 / 基线</th><th>128 维路由器</th><th>备注</th></tr></thead><tbody>" +
D.router.metrics.map(function (m) {
return "<tr><td>" + esc(m.k) + '</td><td class="n">' + esc(m.base) + '</td><td class="n acc">' +
esc(m.nm2) + '</td><td class="small" style="color:var(--ink-4)">' + esc(m.note || "—") + "</td></tr>";
}).join("") + "</tbody></table></div>" +
'<div class="kv">' + D.router.spec.map(function (s) {
return '<div class="kv__r"><span class="kv__k">' + esc(s.k) + '</span><span class="kv__v">' + esc(s.v) + "</span></div>";
}).join("") + "</div></div>" +
'<div class="card card--warn" style="margin-top:clamp(28px,3.4vw,44px)">' +
'<p class="eyebrow" style="margin-bottom:18px;color:var(--warn)">这张表必须连这些一起读</p><div class="lims">' +
D.router.caveats.map(function (c, i) {
return '<div class="lim"><span class="lim__i">' + String(i + 1).padStart(2, "0") +
'</span><p class="lim__d">' + esc(c) + "</p></div>";
}).join("") + "</div></div>";
p3.innerHTML = h3;
panels.appendChild(p1); panels.appendChild(p2); panels.appendChild(p3);
[p1, p2, p3].forEach(function (p) { p.setAttribute("role", "tabpanel"); p.setAttribute("aria-hidden", "true"); });
watch(p1);
activate(startId, false);
};
/* ---- 04 实测样例 ---- */
PAGES.examples = function () {
var box = $("#exampleList");
if (!box) return;
var E = window.NM_EXAMPLES || [];
setText("#exampleIntro", D.examples.subtitle);
setText("#exampleNote", D.examples.note);
if (!E.length) { box.appendChild(el("p", "small", "样例数据未加载。")); return; }
var DOMAIN = { user_dialogue: "用户对话事实", real_repository: "真实源码/文档" };
var CAT = {
symbol_navigation: "符号定位", unknown_refusal: "未知拒答", paraphrase: "自然改写",
constraint: "约束类问题", cross_session: "跨会话", temporal_conflict: "时间冲突"
};
var TOKMAX = 64;
var tok = function (t) {
return t >= TOKMAX ? '<span style="color:var(--warn)">' + t + " token(达上限截断)</span>" : t + " token";
};
var verdict = function (ok) {
return '<span class="v v--' + (ok ? "ok" : "no") + '">' + (ok ? "命中" : "未命中") + "</span>";
};
var col = function (title, cls, r, meta, extra) {
return '<div class="ex__col ex__col--' + cls + '">' +
'<p class="ex__lab">' + esc(title) + "</p>" +
'<p class="ex__ans">' + esc(r.response || "(空)") + "</p>" +
'<p class="ex__meta">' + meta + "<br>" + tok(r.tokens || 0) + "</p>" + (extra || "") + "</div>";
};
E.forEach(function (e, i) {
var identical = (e.nm.response || "") === (e.base.response || "");
var ev = "";
if (e.nm.records && e.nm.records.length) {
ev = '<div class="ex__ev"><p class="ex__evt">命中的记忆记录</p>' +
e.nm.records.map(function (r) {
return '<div class="ex__evr">' +
'<span class="ex__evk">' + esc(r.attribute || r.entity || r.record_id) + "</span>" +
'<span class="ex__evv">' + esc(r.text_preview || r.value || "") + "</span>" +
'<span class="ex__evs">' + esc(r.record_id) + " · " + esc(r.source || "") +
(r.status ? " · " + esc(r.status) : "") + "</span></div>";
}).join("") + "</div>";
} else if (!e.answerable) {
ev = '<div class="ex__ev"><p class="ex__evt">命中的记忆记录</p>' +
'<div class="ex__evr"><span class="ex__evv">无 —— 该题目标本身不在库里,考察的是模型会不会硬答。</span></div></div>';
}
box.appendChild(el("article", "ex reveal",
'<div class="ex__hd">' +
'<span class="ex__no">' + String(i + 1).padStart(2, "0") + "</span>" +
'<span class="chip">' + esc(DOMAIN[e.domain] || e.domain) + " · " + esc(CAT[e.category] || e.category) + "</span>" +
(e.answerable ? "" : '<span class="chip">未知题</span>') +
(identical ? '<span class="chip">两系统回答逐字相同</span>' : "") +
'<span class="src" style="margin-left:auto">id=' + esc(e.id) + "</span></div>" +
'<p class="ex__q">' + esc(e.query) + "</p>" +
'<p class="ex__exp"><span>期望锚点</span>' + (e.expected
? esc(e.expected) + (e.expected_anchors && e.expected_anchors.length > 1
? " · 其余:" + esc(e.expected_anchors.slice(1).join("、")) : "")
: "无(未知题,考察拒答)") + "</p>" +
'<div class="ex__grid">' +
col("原版 Qwen3.5-4B", "base", e.base, "判分 " + verdict(e.base.correct)) +
col("Natural Memory v2", "nm", e.nm,
"判分 " + verdict(e.nm.correct) + (e.answerable ? " · 目标召回 " + (e.nm.recall ? "命中" : "未命中") : ""), ev) +
"</div>"));
});
$$(".ex", box).forEach(watch);
};
/* ---- 05 代价 ---- */
PAGES.cost = function () {
setText("#costIntro", D.cost.subtitle);
setText("#costQuote", D.cost.quote);
var box = $("#ledger");
if (box) {
box.appendChild(el("div", "ledger__h", "<span>指标</span><span>原版 Qwen</span><span>NM v2</span><span>变化</span>"));
D.cost.rows.forEach(function (r) {
box.appendChild(el("div", "ledger__r",
'<span class="ledger__k">' + esc(r.k) + "</span>" +
'<span class="ledger__b">' + esc(r.base) + "</span>" +
'<span class="ledger__n">' + esc(r.nm2) + "</span>" +
'<span class="ledger__d t-' + r.tone + '">' + esc(r.delta) + "</span>"));
});
watch(box);
}
var v = D.cost.vram, vb = $("#vram");
if (vb) {
[
{ k: "allocated · 无记忆基线", val: v.allocBase, cls: "base" },
{ k: "allocated · NM v2", val: v.allocNm2, cls: "nm2" },
{ k: "reserved · 无记忆基线", val: v.resvBase, cls: "base" },
{ k: "reserved · NM v2", val: v.resvNm2, cls: "nm2" }
].forEach(function (r) {
vb.innerHTML += '<div class="vram__row"><div class="vram__top"><span>' + esc(r.k) + "</span><b>" +
fmt(r.val, 3) + " " + v.unit + "</b></div>" +
'<div class="vram__track"><span class="vram__fill vram__fill--' + r.cls + '" data-fill="' +
((r.val / v.card) * 100).toFixed(1) + '"></span><span class="vram__mark" style="left:calc(100% / 3)"></span></div></div>';
});
vb.innerHTML += '<div class="vram__card"><span>0</span><span>12 GiB 显卡</span></div>';
watch(vb);
}
};
/* ---- 06 容量 ---- */
PAGES.capacity = function () {
var box = $("#capList");
if (box) {
D.capacity.stats.forEach(function (s) {
var hi = /设计容量|GPU cache 上限/.test(s.k);
box.appendChild(el("div", "cap__r" + (hi ? " cap__r--hi" : ""),
'<span class="cap__k">' + esc(s.k) + (s.d ? "<small>" + esc(s.d) + "</small>" : "") + "</span>" +
'<span class="cap__v">' + esc(s.v) + "</span>"));
});
watch(box);
}
var g = $("#pageMap"), total = 512, used = D.capacity.grid.usedPages;
if (g) {
var step = Math.max(1, Math.floor(total / used)), out = "";
for (var i = 0; i < total; i++) {
var on = i % step === 0 && (i / step) < used;
out += "<i" + (on ? ' class="on"' : "") + "></i>";
}
g.innerHTML = out;
}
setText("#storageMode", D.capacity.storage.mode);
setText("#storageText", D.capacity.storage.text);
setText("#storageTradeoff", D.capacity.storage.tradeoff);
};
/* ---- 07 边界 ---- */
PAGES.limits = function () {
setText("#limitsIntro", D.limits.subtitle);
var c = $("#claims");
if (c) {
D.limits.claims.forEach(function (t, i) {
c.appendChild(el("div", "claim",
'<span class="claim__x">✕</span><span class="claim__t">' + esc(t) + "</span>" +
'<span class="claim__s">UNSUPPORTED ' + String(i + 1).padStart(2, "0") + "</span>"));
});
watch(c);
}
var l = $("#lims");
if (l) {
D.limits.limitations.forEach(function (x, i) {
l.appendChild(el("div", "lim",
'<span class="lim__i">' + String(i + 1).padStart(2, "0") + "</span>" +
"<div><h3 class=\"lim__t\">" + esc(x.t) + '</h3><p class="lim__d">' + esc(x.d) + "</p></div>"));
});
watch(l);
}
var q = $("#quotes");
if (q) {
D.limits.quotes.forEach(function (x) {
q.appendChild(el("div", "quote", "<q>" + esc(x.q) + "</q><cite>" + esc(x.s) + "</cite>"));
});
watch(q);
}
};
/* ---- 08 路线图 ---- */
PAGES.roadmap = function () {
var s = $("#solved");
if (s) {
D.solved.forEach(function (t, i) {
s.appendChild(el("div", "check",
'<span class="check__i">✓ ' + String(i + 1).padStart(2, "0") + "</span>" +
'<span class="check__t">' + esc(t) + "</span>"));
});
watch(s);
}
var r = $("#roadmap-list");
if (r) {
D.roadmap.forEach(function (t, i) {
r.appendChild(el("div", "road__r",
'<span class="road__i">→ ' + String(i + 1).padStart(2, "0") + "</span>" +
'<span class="road__t">' + esc(t) + "</span>"));
});
watch(r);
}
};
/* ---- 09 复现 ---- */
PAGES.reproduce = function () {
var R = D.reproduce;
setText("#cwd", R.cwd);
setText("#pBase", D.meta.base);
var cmd = R.cmd.join("\n"), block = $("#cmdBlock");
if (block) {
block.innerHTML = esc(cmd)
.replace(/(--[a-z-]+)/g, '<span class="k">$1</span>')
.replace(/(&quot;[^&]*&quot;)/g, '<span class="c">$1</span>');
}
var f = $("#files");
if (f) {
R.files.forEach(function (x) {
f.appendChild(el("div", "kv__r",
'<span class="kv__k">' + esc(x.k) + '</span><span class="kv__v">' + esc(x.v) + "</span>"));
});
watch(f);
}
var btn = $("#copyBtn");
if (btn) {
btn.addEventListener("click", function () {
var done = function () {
btn.textContent = "已复制";
btn.classList.add("is-done");
setTimeout(function () { btn.textContent = "复制命令"; btn.classList.remove("is-done"); }, 1900);
};
var fallback = function () {
var ta = document.createElement("textarea");
ta.value = cmd; ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta); ta.select();
try { document.execCommand("copy"); done(); } catch (e) { btn.textContent = "复制失败"; }
document.body.removeChild(ta);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(cmd).then(done, fallback);
} else { fallback(); }
});
}
};
/* ---- 04 NM2.1 ---- */
function doseChart(host, d) {
if (!host || !d) return;
var head = '<div class="dose__hd"><span class="mono">' + esc(d.axis) + "</span>" +
'<span class="tiny">' + esc(d.xLabel) + "</span></div>";
var axis = '<div class="dose__x"><span class="dose__x0"></span>' +
d.x.map(function (v) {
return '<span class="' + (v === d.pick ? "is-pick" : "") + '">' + esc(String(v)) + "</span>";
}).join("") + "</div>";
var rows = d.metrics.map(function (m) {
var max = Math.max.apply(null, m.v) || 1;
return '<div class="dose__r"><span class="dose__l">' + esc(m.label) +
"<small>" + (m.higherBetter ? "越高越好" : "越低越好") + "</small></span>" +
'<span class="dose__cells">' + m.v.map(function (v, i) {
var pick = d.x[i] === d.pick;
return '<span class="dose__c' + (pick ? " is-pick" : "") + '">' +
'<span class="dose__bar"><span class="dose__fill" style="width:' +
((v / max) * 100).toFixed(1) + '%"></span></span>' +
"<b>" + v.toFixed(2) + esc(m.unit) + "</b></span>";
}).join("") + "</span></div>";
}).join("");
host.innerHTML = head + axis + rows +
'<p class="dose__note tiny">' + esc(d.note) + "</p>" +
'<p class="dose__src tiny">' + esc(window.NM21 ? window.NM21.srcLabel : "") + "</p>";
}
PAGES.nm21 = function () {
var M = window.NM21;
if (M) {
doseChart($("#nmBlend"), M.blend);
doseChart($("#nmPrior"), M.prior);
}
var b = $("#nmCopy"), cmd = $("#nmCmd");
if (b && cmd) b.addEventListener("click", function () { copyText(b, cmd.textContent || ""); });
};
/* ---- 06 真实评测台账 ---- */
PAGES.ledger = function () {
var L = window.NM_LEDGER, box = $("#ledgerList");
if (!box || !L) return;
var total = 0, all = [];
L.forEach(function (s) {
total += s.n;
s.items.forEach(function (it) { all.push({ s: s, it: it }); });
});
setText("#ledgerIntro", "共 " + total + " 条真实 episode,来自 " + L.length +
" 次正式运行(24 条同句式候选、值为随机码,随机猜中概率 4.17%)。");
var verdictOf = function (it) {
if (!it.ans) return it.leak ? "leak" : "abstain";
if (it.ok) return "ok";
return it.wrong ? "wrong" : "miss";
};
var V = {
ok: { label: "答对", tone: "ok" },
wrong: { label: "答成别的属性", tone: "no" },
miss: { label: "答错", tone: "no" },
leak: { label: "未知泄漏", tone: "no" },
abstain: { label: "未知已拒答", tone: "ok" }
};
var fSrc = "all", fV = "all";
var srcDefs = [{ k: "all", label: "全部来源" }].concat(L.map(function (s) {
return { k: s.key, label: s.label + " · " + s.n };
}));
var vDefs = [{ k: "all", label: "全部判定" }].concat(["ok", "wrong", "miss", "leak", "abstain"].map(function (k) {
return { k: k, label: V[k].label };
}));
function buildSeg(host, defs, get, set) {
host.innerHTML = "";
defs.forEach(function (d) {
var b = el("button", "seg__b" + (get() === d.k ? " is-on" : ""), esc(d.label));
b.type = "button";
b.addEventListener("click", function () {
set(d.k);
$$(".seg__b", host).forEach(function (x, i) { x.classList.toggle("is-on", defs[i].k === d.k); });
paint();
});
host.appendChild(b);
});
}
function paint() {
var rows = all.filter(function (r) {
if (fSrc !== "all" && r.s.key !== fSrc) return false;
if (fV !== "all" && verdictOf(r.it) !== fV) return false;
return true;
});
var tally = { ok: 0, wrong: 0, miss: 0, leak: 0, abstain: 0 };
rows.forEach(function (r) { tally[verdictOf(r.it)]++; });
setText("#ledgerCount", "显示 " + rows.length + " / " + total + " 条");
setText("#ledgerBreak", Object.keys(V).map(function (k) {
return V[k].label + " " + tally[k];
}).join(" · "));
box.innerHTML = rows.length ? rows.map(function (r, i) {
var it = r.it, v = V[verdictOf(it)];
var src = r.s.note;
return '<article class="lg">' +
'<div class="lg__hd">' +
'<span class="lg__i mono">' + String(i + 1).padStart(3, "0") + "</span>" +
'<span class="v v--' + v.tone + '">' + esc(v.label) + "</span>" +
'<span class="chip">属性 · ' + esc(it.attr || "—") + "</span>" +
(it.ans ? "" : '<span class="chip">未知题</span>') +
'<span class="src lg__id">' + esc(it.id) + " · " + esc(src) + "</span>" +
"</div>" +
'<p class="lg__exp"><span>期望锚点</span>' +
(it.ans ? "<code>" + esc(it.exp) + "</code>" : "无(未知题,考察拒答)") + "</p>" +
'<p class="lg__rep mono">' + esc(it.reply || "(空)") + "</p>" +
'<p class="lg__meta mono">stop_reason=<b>' + esc(it.stop || "—") + "</b>" +
" · records_selected=<b>" + esc(String(it.sel)) + "</b>" +
" · top_score=<b>" + esc(Number(it.score).toFixed(4)) + "</b>" +
" · records_active=<b>" + esc(String(it.active)) + "</b>" +
" · need_memory=<b>" + esc(String(it.need)) + "</b></p>" +
"</article>";
}).join("") : '<p class="small">当前筛选下没有记录。</p>';
}
buildSeg($("#ledgerSrc"), srcDefs, function () { return fSrc; }, function (k) { fSrc = k; });
buildSeg($("#ledgerVerdict"), vDefs, function () { return fV; }, function (k) { fV = k; });
paint();
};
/* ---------- 分派 ---------- */
var page = document.body.getAttribute("data-page") || "";
if (PAGES[page]) {
try { PAGES[page](); }
catch (err) {
if (window.console) console.error("[NM] 页面渲染失败: " + page, err);
}
}
})();