zcbot/web/static/js/memory.js

101 lines
4.1 KiB
JavaScript

// 记忆 modal:只读两栏 master-detail。左栏列 Core(常驻)+ Extended 各条(带 description),
// 右栏渲染选中项原文(markdown)。左侧 rail 底部「记忆」按钮触发。
// **只读** —— 改记忆全走对话(agent 自管,见 core/memory.py 的 _CONTRACT)。
// GUI 当"眼睛"不当"手":看全貌靠直接读 FS(便宜、是地面真相),改靠模型(DESIGN §3.7)。
// 后端:GET /v1/memory(全貌)、GET /v1/memory/extended/{filename}(单篇原文)。
import { $ } from "./dom.js";
import { api } from "./api.js";
import { escapeHtml } from "./format.js";
import { renderMd, highlightIn } from "./markdown.js";
const PLACEHOLDER = '<div class="sk-empty">← 选 Core 或某条专题查看</div>';
let _cache = null; // 本次打开的 {core, extended} 快照;渲染右栏 Core 复用,免二次请求
function openMemoryModal() {
$("memory-modal").classList.add("show");
$("mem-detail").innerHTML = PLACEHOLDER;
renderList();
}
export function closeMemoryModal() {
$("memory-modal").classList.remove("show");
}
async function renderList() {
const list = $("mem-list");
list.innerHTML = '<div class="muted" style="padding:8px;">加载中…</div>';
let data;
try {
data = await api("GET", "/v1/memory");
} catch (e) {
list.innerHTML = `<div class="err" style="padding:8px;">加载失败: ${escapeHtml(e.message)}</div>`;
return;
}
_cache = data;
const ext = data.extended || [];
const coreEmpty = !data.core || !data.core.trim();
let html = '<div class="sk-group-title">常驻 (Core)</div>';
html += `<div class="sk-item" data-kind="core">
<div class="sk-name">core.md${coreEmpty ? ' <span class="sk-badge">空</span>' : ""}</div>
<div class="sk-desc">每轮注入,跨任务高频事实</div>
</div>`;
html += `<div class="sk-group-title" style="margin-top:12px;">专题 (Extended ${ext.length})</div>`;
html += ext.length
? ext
.map(
(e) => `<div class="sk-item" data-kind="ext" data-file="${escapeHtml(e.filename)}">
<div class="sk-name">${escapeHtml(e.filename)}</div>
<div class="sk-desc">${escapeHtml(e.description || "")}</div>
</div>`
)
.join("")
: '<div class="muted" style="padding:4px 8px;font-size:12px;">还没有。在对话里让我「记住某专题」即可。</div>';
list.innerHTML = html;
}
function highlightSel(itemEl) {
$("mem-list").querySelectorAll(".sk-item.active").forEach((el) => el.classList.remove("active"));
if (itemEl) itemEl.classList.add("active");
}
function showCore(itemEl) {
highlightSel(itemEl);
const detail = $("mem-detail");
const core = (_cache && _cache.core) || "";
detail.innerHTML = core.trim()
? '<div class="sk-d-head"><span class="sk-d-name">core.md</span><span class="sk-badge">常驻</span></div>' +
`<div class="sk-detail-md">${renderMd(core)}</div>`
: '<div class="sk-empty">core.md 还是空的。在对话里跟我说你的偏好 / 项目约定,我会记进来。</div>';
highlightIn(detail);
}
async function showExt(filename, itemEl) {
highlightSel(itemEl);
const detail = $("mem-detail");
detail.innerHTML = '<div class="muted" style="padding:8px;">加载中…</div>';
let data;
try {
data = await api("GET", "/v1/memory/extended/" + encodeURIComponent(filename));
} catch (e) {
detail.innerHTML = `<div class="err" style="padding:8px;">加载失败: ${escapeHtml(e.message)}</div>`;
return;
}
detail.innerHTML =
`<div class="sk-d-head"><span class="sk-d-name">${escapeHtml(filename)}</span><span class="sk-badge">按需</span></div>` +
`<div class="sk-detail-md">${renderMd(data.content)}</div>`;
highlightIn(detail);
}
// ───── 顶层绑定 ─────
$("hd-memory").onclick = openMemoryModal;
$("mem-close").onclick = closeMemoryModal;
$("memory-modal").addEventListener("click", (e) => {
if (e.target.id === "memory-modal") closeMemoryModal(); // 点遮罩关闭
});
$("mem-list").addEventListener("click", (e) => {
const item = e.target.closest(".sk-item");
if (!item) return;
if (item.getAttribute("data-kind") === "core") showCore(item);
else showExt(item.getAttribute("data-file"), item);
});