// ST-BME: 操控面板交互逻辑 import { renderExtensionTemplateAsync } from "../../../extensions.js"; import { GraphRenderer } from "./graph-renderer.js"; import { getNodeColors } from "./themes.js"; let panelEl = null; let overlayEl = null; let graphRenderer = null; let mobileGraphRenderer = null; // 由 index.js 注入的引用 let _getGraph = null; let _getSettings = null; let _getLastExtract = null; let _getLastRecall = null; let _getLastInjection = null; let _actionHandlers = {}; /** * 初始化面板(由 index.js 调用一次) */ export async function initPanel({ getGraph, getSettings, getLastExtract, getLastRecall, getLastInjection, actions, }) { _getGraph = getGraph; _getSettings = getSettings; _getLastExtract = getLastExtract; _getLastRecall = getLastRecall; _getLastInjection = getLastInjection; _actionHandlers = actions || {}; overlayEl = document.getElementById("st-bme-panel-overlay"); panelEl = document.getElementById("st-bme-panel"); if (!overlayEl || !panelEl) { const html = await renderExtensionTemplateAsync("third-party/ST-BME", "panel"); $("body").append(html); overlayEl = document.getElementById("st-bme-panel-overlay"); panelEl = document.getElementById("st-bme-panel"); } _bindTabs(); _bindClose(); _bindGraphControls(); _bindActions(); } /** * 打开面板 */ export function openPanel() { if (!overlayEl) return; overlayEl.classList.add("active"); const isMobile = _isMobile(); const settings = _getSettings?.() || {}; const themeName = settings.panelTheme || "crimson"; const canvas = document.getElementById("bme-graph-canvas"); if (canvas && !graphRenderer && !isMobile) { graphRenderer = new GraphRenderer(canvas, themeName); graphRenderer.onNodeSelect = (node) => _showNodeDetail(node); } const mobileCanvas = document.getElementById("bme-mobile-graph-canvas"); if (mobileCanvas && !mobileGraphRenderer && isMobile) { mobileGraphRenderer = new GraphRenderer(mobileCanvas, themeName); mobileGraphRenderer.onNodeSelect = (node) => _showNodeDetail(node); } _refreshDashboard(); _refreshGraph(); _buildLegend(); } /** * 关闭面板 */ export function closePanel() { if (!overlayEl) return; overlayEl.classList.remove("active"); } /** * 更新主题 */ export function updatePanelTheme(themeName) { graphRenderer?.setTheme(themeName); mobileGraphRenderer?.setTheme(themeName); _buildLegend(); } // ==================== Tab 切换 ==================== function _bindTabs() { panelEl?.querySelectorAll(".bme-tab-btn").forEach((btn) => { btn.addEventListener("click", () => { const tabId = btn.dataset.tab; _switchTab(tabId); }); }); } function _switchTab(tabId) { panelEl?.querySelectorAll(".bme-tab-btn").forEach((btn) => { btn.classList.toggle("active", btn.dataset.tab === tabId); }); panelEl?.querySelectorAll(".bme-tab-pane").forEach((pane) => { pane.classList.toggle("active", pane.id === `bme-pane-${tabId}`); }); switch (tabId) { case "dashboard": _refreshDashboard(); break; case "memory": _refreshMemoryBrowser(); break; case "injection": void _refreshInjectionPreview(); break; default: break; } } // ==================== 总览 Tab ==================== function _refreshDashboard() { const graph = _getGraph?.(); if (!graph) return; const activeNodes = graph.nodes.filter((node) => !node.archived); const archivedCount = graph.nodes.filter((node) => node.archived).length; const totalNodes = graph.nodes.length; const fragRate = totalNodes > 0 ? Math.round((archivedCount / totalNodes) * 100) : 0; _setText("bme-stat-nodes", activeNodes.length); _setText("bme-stat-edges", graph.edges.length); _setText("bme-stat-archived", archivedCount); _setText("bme-stat-frag", `${fragRate}%`); _setText( "bme-status-meta", `NODES: ${activeNodes.length} | EDGES: ${graph.edges.length}`, ); _renderRecentList("bme-recent-extract", _getLastExtract?.() || []); _renderRecentList("bme-recent-recall", _getLastRecall?.() || []); } function _renderRecentList(elementId, items) { const listEl = document.getElementById(elementId); if (!listEl) return; if (!items.length) { listEl.innerHTML = '
  • 暂无数据
  • '; return; } listEl.innerHTML = items .map((item) => { const secondary = item.meta || item.time || ""; return `
  • ${_typeLabel(item.type)}
    ${_escHtml(item.name || "—")}
    ${_escHtml(secondary)}
  • `; }) .join(""); } // ==================== 记忆浏览器 ==================== function _refreshMemoryBrowser() { const graph = _getGraph?.(); if (!graph) return; const searchInput = document.getElementById("bme-memory-search"); const filterSelect = document.getElementById("bme-memory-filter"); const listEl = document.getElementById("bme-memory-list"); if (!listEl) return; const query = String(searchInput?.value || "").trim().toLowerCase(); const filter = filterSelect?.value || "all"; let nodes = graph.nodes.filter((node) => !node.archived); if (filter !== "all") { nodes = nodes.filter((node) => node.type === filter); } if (query) { nodes = nodes.filter((node) => { const name = getNodeDisplayName(node).toLowerCase(); const text = JSON.stringify(node.fields || {}).toLowerCase(); return name.includes(query) || text.includes(query); }); } nodes.sort((a, b) => { const importanceDiff = (b.importance || 5) - (a.importance || 5); if (importanceDiff !== 0) return importanceDiff; return (b.seqRange?.[1] ?? b.seq ?? 0) - (a.seqRange?.[1] ?? a.seq ?? 0); }); listEl.innerHTML = nodes .slice(0, 100) .map((node) => { const name = getNodeDisplayName(node); const snippet = _getNodeSnippet(node); return `
  • ${_typeLabel(node.type)}
    ${_escHtml(name)}
    ${_escHtml(snippet)}
    imp: ${node.importance || 5} acc: ${node.accessCount || 0} seq: ${node.seqRange?.[1] ?? node.seq ?? 0}
  • `; }) .join(""); listEl.querySelectorAll(".bme-memory-item").forEach((el) => { el.addEventListener("click", () => { const nodeId = el.dataset.nodeId; graphRenderer?.highlightNode(nodeId); mobileGraphRenderer?.highlightNode(nodeId); const node = graph.nodes.find((candidate) => candidate.id === nodeId); if (node) _showNodeDetail(node); }); }); if (searchInput && !searchInput._bmeBound) { let timer = null; searchInput.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(() => _refreshMemoryBrowser(), 200); }); filterSelect?.addEventListener("change", () => _refreshMemoryBrowser()); searchInput._bmeBound = true; } } // ==================== 注入预览 ==================== async function _refreshInjectionPreview() { const container = document.getElementById("bme-injection-content"); const tokenEl = document.getElementById("bme-injection-tokens"); if (!container) return; const injection = String(_getLastInjection?.() || "").trim(); if (!injection) { container.innerHTML = '
    暂无注入内容。先完成一次召回或正常生成后再查看。
    '; if (tokenEl) tokenEl.textContent = ""; return; } try { const { estimateTokens } = await import("./injector.js"); const totalTokens = estimateTokens(injection); container.innerHTML = `
    ${_escHtml(injection)}
    `; if (tokenEl) tokenEl.textContent = `≈ ${totalTokens} tokens`; } catch (error) { container.innerHTML = `
    预览生成失败: ${_escHtml(error.message)}
    `; if (tokenEl) tokenEl.textContent = ""; } } // ==================== 图谱 ==================== function _refreshGraph() { const graph = _getGraph?.(); if (!graph) return; graphRenderer?.loadGraph(graph); mobileGraphRenderer?.loadGraph(graph); } function _buildLegend() { const legendEl = document.getElementById("bme-graph-legend"); if (!legendEl) return; const settings = _getSettings?.() || {}; const colors = getNodeColors(settings.panelTheme || "crimson"); const types = [ { key: "character", label: "角色" }, { key: "event", label: "事件" }, { key: "location", label: "地点" }, { key: "thread", label: "主线" }, { key: "rule", label: "规则" }, { key: "synopsis", label: "概要" }, { key: "reflection", label: "反思" }, ]; legendEl.innerHTML = types .map( (type) => ` ${type.label} `, ) .join(""); } function _bindGraphControls() { document .getElementById("bme-graph-zoom-in") ?.addEventListener("click", () => graphRenderer?.zoomIn()); document .getElementById("bme-graph-zoom-out") ?.addEventListener("click", () => graphRenderer?.zoomOut()); document .getElementById("bme-graph-reset") ?.addEventListener("click", () => graphRenderer?.resetView()); } // ==================== 节点详情 ==================== function _showNodeDetail(node) { const detailEl = document.getElementById("bme-node-detail"); const titleEl = document.getElementById("bme-detail-title"); const bodyEl = document.getElementById("bme-detail-body"); if (!detailEl || !titleEl || !bodyEl) return; const raw = node.raw || node; const fields = raw.fields || {}; titleEl.textContent = getNodeDisplayName(raw); const items = [ { label: "类型", value: _typeLabel(raw.type) }, { label: "ID", value: raw.id || "—" }, { label: "重要度", value: raw.importance || 5 }, { label: "访问次数", value: raw.accessCount || 0 }, { label: "序列号", value: raw.seqRange?.[1] ?? raw.seq ?? 0 }, ]; if (Array.isArray(raw.seqRange)) { items.push({ label: "序列范围", value: `${raw.seqRange[0]} ~ ${raw.seqRange[1]}` }); } if (Array.isArray(raw.clusters) && raw.clusters.length > 0) { items.push({ label: "聚类标签", value: raw.clusters.join(", ") }); } for (const [key, value] of Object.entries(fields)) { items.push({ label: key, value: typeof value === "object" ? JSON.stringify(value, null, 2) : value, }); } bodyEl.innerHTML = items .map( (item) => `
    ${_escHtml(String(item.value ?? "—"))}
    `, ) .join(""); detailEl.classList.add("open"); } function _bindClose() { document.getElementById("bme-panel-close")?.addEventListener("click", closePanel); document.getElementById("bme-detail-close")?.addEventListener("click", () => { document.getElementById("bme-node-detail")?.classList.remove("open"); }); overlayEl?.addEventListener("click", (event) => { if (event.target === overlayEl) closePanel(); }); } // ==================== 操作绑定 ==================== function _bindActions() { const bindings = { "bme-act-extract": "extract", "bme-act-compress": "compress", "bme-act-sleep": "sleep", "bme-act-synopsis": "synopsis", "bme-act-export": "export", "bme-act-import": "import", "bme-act-rebuild": "rebuild", "bme-act-evolve": "evolve", }; for (const [elementId, actionKey] of Object.entries(bindings)) { document.getElementById(elementId)?.addEventListener("click", async () => { const handler = _actionHandlers[actionKey]; if (!handler) return; try { await handler(); _refreshDashboard(); _refreshGraph(); if (document.getElementById("bme-pane-memory")?.classList.contains("active")) { _refreshMemoryBrowser(); } if (document.getElementById("bme-pane-injection")?.classList.contains("active")) { await _refreshInjectionPreview(); } } catch (error) { console.error(`[ST-BME] Action ${actionKey} failed:`, error); } }); } } // ==================== 工具函数 ==================== function _setText(id, text) { const el = document.getElementById(id); if (el) el.textContent = String(text); } function _escHtml(str) { const div = document.createElement("div"); div.textContent = String(str ?? ""); return div.innerHTML; } function _typeLabel(type) { const map = { character: "角色", event: "事件", location: "地点", thread: "主线", rule: "规则", synopsis: "概要", reflection: "反思", }; return map[type] || type || "—"; } function _getNodeSnippet(node) { const fields = node.fields || {}; if (fields.summary) return fields.summary; if (fields.state) return fields.state; if (fields.constraint) return fields.constraint; if (fields.insight) return fields.insight; if (fields.traits) return fields.traits; const entries = Object.entries(fields).filter( ([key]) => !["name", "title", "summary", "embedding"].includes(key), ); if (entries.length > 0) { return entries .slice(0, 2) .map(([key, value]) => `${key}: ${value}`) .join("; "); } return "无补充字段"; } function getNodeDisplayName(node) { return ( node?.fields?.name || node?.fields?.title || node?.fields?.summary || node?.fields?.insight || node?.id?.slice(0, 8) || "—" ); } function _isMobile() { return window.innerWidth <= 768; }