1189 lines
55 KiB
JavaScript
1189 lines
55 KiB
JavaScript
/* ==================================================================
|
||
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 { "&": "&", "<": "<", ">": ">", '"': """ }[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(/("[^&]*")/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);
|
||
}
|
||
}
|
||
})();
|