101 lines
4.1 KiB
JavaScript
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);
|
|
});
|