diff --git a/ui/panel.js b/ui/panel.js index b464e03..79afe96 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1533,6 +1533,24 @@ function _refreshTaskTimeline() { // ---------- Memory Browser (Master-Detail) ---------- +function _getMemoryNodeTypeClass(type) { + switch (type) { + case "pov_memory": + case "character": + return "type-character"; + case "event": + return "type-event"; + case "location": + return "type-location"; + case "rule": + return "type-rule"; + case "thread": + return "type-thread"; + default: + return "type-default"; + } +} + function _refreshTaskMemoryBrowser() { const el = document.getElementById("bme-task-memory"); if (!el) return; @@ -1580,17 +1598,6 @@ function _refreshTaskMemoryBrowser() { currentSelectedMemoryNodeId = sorted[0]?.id || ""; } - const typeClass = (type) => { - switch (type) { - case "pov_memory": case "character": return "type-character"; - case "event": return "type-event"; - case "location": return "type-location"; - case "rule": return "type-rule"; - case "thread": return "type-thread"; - default: return "type-default"; - } - }; - const listItems = sorted.map((node) => { const sel = node.id === currentSelectedMemoryNodeId ? "selected" : ""; const preview = _getNodeSnippet(node); @@ -1600,7 +1607,7 @@ function _refreshTaskMemoryBrowser() { return `
- ${_escHtml(_typeLabel(node.type))} + ${_escHtml(_typeLabel(node.type))} IMP: ${typeof node.importance === "number" ? node.importance.toFixed(1) : "—"}
${_escHtml(displayName)}
@@ -1613,8 +1620,6 @@ function _refreshTaskMemoryBrowser() {
`; }).join(""); - const detailHtml = _renderMemoryDetailPanel(sorted.find((n) => n.id === currentSelectedMemoryNodeId) || null, graph); - el.innerHTML = `
@@ -1636,12 +1641,11 @@ function _refreshTaskMemoryBrowser() { ${listItems || '
无节点
'}
-
- ${detailHtml} -
+
`; + _renderTaskMemoryDetailSelection(graph); _bindTaskMemoryListClick(); const searchInput = document.getElementById("bme-task-memory-search"); @@ -1665,14 +1669,25 @@ function _bindTaskMemoryListClick() { currentSelectedMemoryNodeId = item.dataset.nodeId || ""; list.querySelectorAll(".bme-memory-node-item").forEach((n) => n.classList.toggle("selected", n.dataset.nodeId === currentSelectedMemoryNodeId)); const graph = _getGraph?.(); - const node = (graph?.nodes || []).find((n) => n.id === currentSelectedMemoryNodeId) || null; - const detailEl = document.getElementById("bme-task-memory-detail"); - if (detailEl) detailEl.innerHTML = _renderMemoryDetailPanel(node, graph); + _renderTaskMemoryDetailSelection(graph); }); } -function _renderMemoryDetailPanel(node, graph) { - if (!node) return '
选择左侧节点查看详情
'; +function _renderTaskMemoryDetailSelection(graph = _getGraph?.()) { + const detailEl = document.getElementById("bme-task-memory-detail"); + if (!detailEl) return; + + const node = (graph?.nodes || []).find((candidate) => candidate.id === currentSelectedMemoryNodeId) || null; + if (!node) { + detailEl.innerHTML = '
选择左侧节点查看详情
'; + return; + } + + _renderTaskMemoryDetailPanel(detailEl, node, graph); +} + +function _renderTaskMemoryDetailPanel(detailEl, node, graph) { + if (!detailEl) return; const edges = (graph?.edges || []).filter( (e) => @@ -1680,57 +1695,79 @@ function _renderMemoryDetailPanel(node, graph) { !e?.expiredAt && (e?.fromId === node.id || e?.toId === node.id), ); - const fields = node?.fields || {}; const detailSummary = _getNodeSnippet(node); - const scopeMeta = _buildScopeMetaText(node); const scopeBadge = buildScopeBadgeText(node.scope); const displayName = getNodeDisplayName(node); + const writeBlocked = _isGraphWriteBlocked(); + const disabledAttr = writeBlocked ? " disabled" : ""; const badges = [ - node.type ? `${_escHtml(_typeLabel(node.type))}` : "", + node.type ? `${_escHtml(_typeLabel(node.type))}` : "", scopeBadge ? `${_escHtml(scopeBadge)}` : "", node.archived ? 'ARCHIVED' : "", ].filter(Boolean).join(""); - const detailFields = [ - ["ID", node.id], - ["名称", displayName], - ["范围", scopeMeta || scopeBadge || "—"], - ["重要度", _formatMemoryMetricNumber(node.importance, { fallback: 5, maxFrac: 2 })], - ["访问次数", _formatMemoryInt(node.accessCount, 0)], - ["最后序列", _formatMemoryInt(node.seqRange?.[1] ?? node.seq, 0)], - ["创建时间", node.createdAt || "—"], - ["更新时间", node.updatedAt || "—"], - ]; - - const extraFieldRows = Object.entries(fields) - .filter(([key]) => !["embedding", "name", "title", "summary"].includes(key)) - .slice(0, 6) - .map(([key, value]) => { - let text = "—"; - if (typeof value === "string") { - text = value; - } else if (value !== undefined && value !== null) { - try { - text = JSON.stringify(value); - } catch { - text = String(value); - } - } - return [key, text]; - }); - - return ` + detailEl.innerHTML = `
${_escHtml(displayName)}
${badges}
${_escHtml(detailSummary || "无补充字段")}
-
- ${detailFields.concat(extraFieldRows).map(([k, v]) => `
${_escHtml(k)}${_escHtml(String(v))}
`).join("")} -
${edges.length} 条连接 访问 ${_formatMemoryInt(node.accessCount, 0)}
+
+ + +
+
`; + + const editorBody = detailEl.querySelector("#bme-task-memory-editor-body"); + if (editorBody) { + editorBody.replaceChildren( + _buildNodeDetailEditorFragment(node, { idPrefix: "bme-task-detail" }), + ); + } + + detailEl + .querySelector('[data-task-memory-action="save"]') + ?.addEventListener("click", () => _saveTaskMemoryDetail()); + detailEl + .querySelector('[data-task-memory-action="delete"]') + ?.addEventListener("click", () => _deleteTaskMemoryDetail()); +} + +function _saveTaskMemoryDetail() { + const detailEl = document.getElementById("bme-task-memory-detail"); + const bodyEl = detailEl?.querySelector("#bme-task-memory-editor-body"); + const nodeId = currentSelectedMemoryNodeId; + if (!nodeId || !bodyEl) return; + + const collected = _collectNodeDetailEditorUpdates(bodyEl, { + idPrefix: "bme-task-detail", + }); + if (!collected.ok) { + toastr.error(collected.errorMessage || "保存失败", "ST-BME"); + return; + } + + _persistNodeDetailEdits(nodeId, collected.updates); +} + +function _deleteTaskMemoryDetail() { + const nodeId = currentSelectedMemoryNodeId; + if (!nodeId) return; + + _deleteGraphNodeById(nodeId, { + afterSuccess: () => { + currentSelectedMemoryNodeId = ""; + }, + }); } // ---------- Injection Preview ---------- @@ -3843,6 +3880,278 @@ function _appendNodeDetailTextareaField( container.appendChild(row); } +function _buildNodeDetailEditorFragment(raw, { idPrefix = "bme-detail" } = {}) { + const fields = raw.fields || {}; + const scope = normalizeMemoryScope(raw.scope); + const fragment = document.createDocumentFragment(); + const inputId = (suffix) => `${idPrefix}-${suffix}`; + + _appendNodeDetailReadOnly(fragment, "类型", _typeLabel(raw.type)); + _appendNodeDetailReadOnly( + fragment, + "作用域", + buildScopeBadgeText(raw.scope), + ); + _appendNodeDetailReadOnly(fragment, "ID", raw.id || "—"); + _appendNodeDetailReadOnly( + fragment, + "序列号", + raw.seqRange?.[1] ?? raw.seq ?? 0, + ); + + if (scope.layer === "pov") { + _appendNodeDetailReadOnly( + fragment, + "POV 归属", + `${scope.ownerType || "unknown"} / ${scope.ownerName || scope.ownerId || "—"}`, + ); + } + const regionLine = buildRegionLine(scope); + if (regionLine) { + _appendNodeDetailReadOnly(fragment, "地区", regionLine); + } + _appendNodeDetailTextInput( + fragment, + "主地区", + inputId("scope-region-primary"), + scope.regionPrimary || "", + ); + _appendNodeDetailTextInput( + fragment, + "地区路径 (用 / 分隔)", + inputId("scope-region-path"), + Array.isArray(scope.regionPath) ? scope.regionPath.join(" / ") : "", + ); + _appendNodeDetailTextInput( + fragment, + "次级地区 (用逗号或 / 分隔)", + inputId("scope-region-secondary"), + Array.isArray(scope.regionSecondary) + ? scope.regionSecondary.join(", ") + : "", + ); + if (Array.isArray(raw.seqRange)) { + _appendNodeDetailReadOnly( + fragment, + "序列范围", + `${raw.seqRange[0]} ~ ${raw.seqRange[1]}`, + ); + } + _appendNodeDetailTextareaField( + fragment, + "剧情时间", + "__storyTime", + "json", + JSON.stringify(raw.storyTime || {}, null, 2), + ); + _appendNodeDetailTextareaField( + fragment, + "剧情时间范围", + "__storyTimeSpan", + "json", + JSON.stringify(raw.storyTimeSpan || {}, null, 2), + ); + + _appendNodeDetailNumberInput( + fragment, + "重要度 (0–10)", + inputId("importance"), + raw.importance ?? 5, + { min: 0, max: 10, step: 0.1 }, + ); + _appendNodeDetailNumberInput( + fragment, + "访问次数", + inputId("accesscount"), + raw.accessCount ?? 0, + { min: 0, step: 1 }, + ); + + const clustersStr = Array.isArray(raw.clusters) + ? raw.clusters.join(", ") + : ""; + _appendNodeDetailTextInput( + fragment, + "聚类标签 (逗号分隔)", + inputId("clusters"), + clustersStr, + ); + + const section = document.createElement("div"); + section.className = "bme-node-detail-section"; + section.textContent = "记忆字段"; + fragment.appendChild(section); + + for (const [key, value] of Object.entries(fields)) { + const isJson = typeof value === "object" && value !== null; + const displayVal = isJson + ? JSON.stringify(value, null, 2) + : String(value ?? ""); + _appendNodeDetailTextareaField( + fragment, + key, + key, + isJson ? "json" : "string", + displayVal, + ); + } + + return fragment; +} + +function _collectNodeDetailEditorUpdates(bodyEl, { idPrefix = "bme-detail" } = {}) { + if (!bodyEl) { + return { ok: false, errorMessage: "未找到可编辑表单" }; + } + + const findInput = (suffix) => + bodyEl.querySelector(`#${idPrefix}-${suffix}`); + const updates = { fields: {} }; + const impEl = findInput("importance"); + if (impEl && impEl.value !== "") { + const imp = Number.parseFloat(impEl.value); + if (Number.isFinite(imp)) { + updates.importance = Math.max(0, Math.min(10, imp)); + } + } + const accessEl = findInput("accesscount"); + if (accessEl && accessEl.value !== "") { + const ac = Number.parseInt(accessEl.value, 10); + if (Number.isFinite(ac)) { + updates.accessCount = Math.max(0, ac); + } + } + const clustersEl = findInput("clusters"); + if (clustersEl) { + updates.clusters = clustersEl.value + .split(/[,,]/) + .map((s) => s.trim()) + .filter(Boolean); + } + const regionPrimaryEl = findInput("scope-region-primary"); + const regionPathEl = findInput("scope-region-path"); + const regionSecondaryEl = findInput("scope-region-secondary"); + if (regionPrimaryEl || regionPathEl || regionSecondaryEl) { + updates.scope = { + regionPrimary: String(regionPrimaryEl?.value || "").trim(), + regionPath: _parseNodeDetailScopeList(regionPathEl?.value, { + allowSlash: true, + }), + regionSecondary: _parseNodeDetailScopeList(regionSecondaryEl?.value, { + allowSlash: true, + }), + }; + } + + const fieldEls = bodyEl.querySelectorAll("[data-bme-field-key]"); + for (const el of fieldEls) { + const key = el.dataset.bmeFieldKey; + const type = el.dataset.bmeFieldType || "string"; + const rawVal = el.value; + if (key === "__storyTime" || key === "__storyTimeSpan") { + try { + updates[key === "__storyTime" ? "storyTime" : "storyTimeSpan"] = JSON.parse( + rawVal || "{}", + ); + } catch { + return { + ok: false, + errorMessage: `字段「${key === "__storyTime" ? "剧情时间" : "剧情时间范围"}」须为合法 JSON`, + }; + } + continue; + } + if (type === "json") { + try { + updates.fields[key] = JSON.parse(rawVal || "null"); + } catch { + return { + ok: false, + errorMessage: `字段「${key}」须为合法 JSON`, + }; + } + } else { + updates.fields[key] = rawVal; + } + } + + return { ok: true, updates }; +} + +function _persistNodeDetailEdits(nodeId, updates, { afterSuccess } = {}) { + if (!nodeId) return false; + if (_isGraphWriteBlocked()) { + toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); + return false; + } + + const result = _actionHandlers.saveGraphNode?.({ + nodeId, + updates, + }); + if (!result?.ok) { + toastr.error( + result?.error === "node-not-found" + ? "节点已不存在,请关闭后重试" + : "保存失败", + "ST-BME", + ); + return false; + } + if (result.persistBlocked) { + toastr.warning( + "内容已更新,但写回聊天元数据可能被拦截,请查看图谱状态", + "ST-BME", + ); + } else { + toastr.success("节点已保存", "ST-BME"); + } + + afterSuccess?.(); + refreshLiveState(); + return true; +} + +function _deleteGraphNodeById(nodeId, { afterSuccess } = {}) { + if (!nodeId) return false; + if (_isGraphWriteBlocked()) { + toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); + return false; + } + + const g = _getGraph?.(); + const node = g?.nodes?.find((n) => n.id === nodeId); + const label = node ? getNodeDisplayName(node) : nodeId; + if ( + !confirm( + `确定删除节点「${label}」?\n\n若该节点有层级子节点,将一并删除。此操作不可在本面板内撤销。`, + ) + ) { + return false; + } + + const result = _actionHandlers.deleteGraphNode?.({ nodeId }); + if (!result?.ok) { + toastr.error( + result?.error === "node-not-found" ? "节点已不存在" : "删除失败", + "ST-BME", + ); + return false; + } + if (result.persistBlocked) { + toastr.warning( + "节点已从图中移除,但写回可能被拦截,请查看图谱状态", + "ST-BME", + ); + } else { + toastr.success("节点已删除", "ST-BME"); + } + + afterSuccess?.(); + refreshLiveState(); + return true; +} + function _useMobileGraphNodeDetail() { return _isMobile() && currentTabId === "graph"; } @@ -3884,123 +4193,9 @@ function _showNodeDetail(node) { } const raw = node.raw || node; - const fields = raw.fields || {}; titleEl.textContent = getNodeDisplayName(raw); detailEl.dataset.editNodeId = raw.id || ""; - - const fragment = document.createDocumentFragment(); - - _appendNodeDetailReadOnly(fragment, "类型", _typeLabel(raw.type)); - _appendNodeDetailReadOnly( - fragment, - "作用域", - buildScopeBadgeText(raw.scope), - ); - _appendNodeDetailReadOnly(fragment, "ID", raw.id || "—"); - _appendNodeDetailReadOnly( - fragment, - "序列号", - raw.seqRange?.[1] ?? raw.seq ?? 0, - ); - - const scope = normalizeMemoryScope(raw.scope); - if (scope.layer === "pov") { - _appendNodeDetailReadOnly( - fragment, - "POV 归属", - `${scope.ownerType || "unknown"} / ${scope.ownerName || scope.ownerId || "—"}`, - ); - } - const regionLine = buildRegionLine(scope); - if (regionLine) { - _appendNodeDetailReadOnly(fragment, "地区", regionLine); - } - _appendNodeDetailTextInput( - fragment, - "主地区", - "bme-detail-scope-region-primary", - scope.regionPrimary || "", - ); - _appendNodeDetailTextInput( - fragment, - "地区路径 (用 / 分隔)", - "bme-detail-scope-region-path", - Array.isArray(scope.regionPath) ? scope.regionPath.join(" / ") : "", - ); - _appendNodeDetailTextInput( - fragment, - "次级地区 (用逗号或 / 分隔)", - "bme-detail-scope-region-secondary", - Array.isArray(scope.regionSecondary) - ? scope.regionSecondary.join(", ") - : "", - ); - if (Array.isArray(raw.seqRange)) { - _appendNodeDetailReadOnly( - fragment, - "序列范围", - `${raw.seqRange[0]} ~ ${raw.seqRange[1]}`, - ); - } - _appendNodeDetailTextareaField( - fragment, - "剧情时间", - "__storyTime", - "json", - JSON.stringify(raw.storyTime || {}, null, 2), - ); - _appendNodeDetailTextareaField( - fragment, - "剧情时间范围", - "__storyTimeSpan", - "json", - JSON.stringify(raw.storyTimeSpan || {}, null, 2), - ); - - _appendNodeDetailNumberInput( - fragment, - "重要度 (0–10)", - "bme-detail-importance", - raw.importance ?? 5, - { min: 0, max: 10, step: 0.1 }, - ); - _appendNodeDetailNumberInput( - fragment, - "访问次数", - "bme-detail-accesscount", - raw.accessCount ?? 0, - { min: 0, step: 1 }, - ); - - const clustersStr = Array.isArray(raw.clusters) - ? raw.clusters.join(", ") - : ""; - _appendNodeDetailTextInput( - fragment, - "聚类标签 (逗号分隔)", - "bme-detail-clusters", - clustersStr, - ); - - const section = document.createElement("div"); - section.className = "bme-node-detail-section"; - section.textContent = "记忆字段"; - fragment.appendChild(section); - - for (const [key, value] of Object.entries(fields)) { - const isJson = typeof value === "object" && value !== null; - const displayVal = isJson - ? JSON.stringify(value, null, 2) - : String(value ?? ""); - _appendNodeDetailTextareaField( - fragment, - key, - key, - isJson ? "json" : "string", - displayVal, - ); - } - bodyEl.replaceChildren(fragment); + bodyEl.replaceChildren(_buildNodeDetailEditorFragment(raw)); if (mobile) { scrimEl?.removeAttribute("hidden"); @@ -4014,110 +4209,27 @@ function _saveNodeDetail() { const bodyEl = els?.bodyEl; const nodeId = detailEl?.dataset?.editNodeId; if (!nodeId || !bodyEl) return; - if (_isGraphWriteBlocked()) { - toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); + const collected = _collectNodeDetailEditorUpdates(bodyEl); + if (!collected.ok) { + toastr.error(collected.errorMessage || "保存失败", "ST-BME"); return; } - const updates = { fields: {} }; - const impEl = document.getElementById("bme-detail-importance"); - if (impEl && impEl.value !== "") { - const imp = Number.parseFloat(impEl.value); - if (Number.isFinite(imp)) { - updates.importance = Math.max(0, Math.min(10, imp)); - } - } - const accessEl = document.getElementById("bme-detail-accesscount"); - if (accessEl && accessEl.value !== "") { - const ac = Number.parseInt(accessEl.value, 10); - if (Number.isFinite(ac)) { - updates.accessCount = Math.max(0, ac); - } - } - const clustersEl = document.getElementById("bme-detail-clusters"); - if (clustersEl) { - updates.clusters = clustersEl.value - .split(/[,,]/) - .map((s) => s.trim()) - .filter(Boolean); - } - const regionPrimaryEl = document.getElementById("bme-detail-scope-region-primary"); - const regionPathEl = document.getElementById("bme-detail-scope-region-path"); - const regionSecondaryEl = document.getElementById("bme-detail-scope-region-secondary"); - if (regionPrimaryEl || regionPathEl || regionSecondaryEl) { - updates.scope = { - regionPrimary: String(regionPrimaryEl?.value || "").trim(), - regionPath: _parseNodeDetailScopeList(regionPathEl?.value, { - allowSlash: true, - }), - regionSecondary: _parseNodeDetailScopeList(regionSecondaryEl?.value, { - allowSlash: true, - }), - }; - } - - const fieldEls = bodyEl.querySelectorAll("[data-bme-field-key]"); - for (const el of fieldEls) { - const key = el.dataset.bmeFieldKey; - const type = el.dataset.bmeFieldType || "string"; - const rawVal = el.value; - if (key === "__storyTime" || key === "__storyTimeSpan") { - try { - updates[key === "__storyTime" ? "storyTime" : "storyTimeSpan"] = JSON.parse( - rawVal || "{}", - ); - } catch { - toastr.error(`字段「${key === "__storyTime" ? "剧情时间" : "剧情时间范围"}」须为合法 JSON`, "ST-BME"); - return; + _persistNodeDetailEdits(nodeId, collected.updates, { + afterSuccess: () => { + const r = _getActiveGraphRenderer(); + const sel = r?.selectedNode; + if (sel?.id === nodeId && sel.raw) { + _showNodeDetail(sel); + } else { + const g = _getGraph?.(); + const rawN = g?.nodes?.find((n) => n.id === nodeId); + if (rawN) { + _showNodeDetail({ raw: rawN, id: rawN.id }); + } } - continue; - } - if (type === "json") { - try { - updates.fields[key] = JSON.parse(rawVal || "null"); - } catch { - toastr.error(`字段「${key}」须为合法 JSON`, "ST-BME"); - return; - } - } else { - updates.fields[key] = rawVal; - } - } - - const result = _actionHandlers.saveGraphNode?.({ - nodeId, - updates, + }, }); - if (!result?.ok) { - toastr.error( - result?.error === "node-not-found" - ? "节点已不存在,请关闭后重试" - : "保存失败", - "ST-BME", - ); - return; - } - if (result.persistBlocked) { - toastr.warning( - "内容已更新,但写回聊天元数据可能被拦截,请查看图谱状态", - "ST-BME", - ); - } else { - toastr.success("节点已保存", "ST-BME"); - } - - const r = _getActiveGraphRenderer(); - const sel = r?.selectedNode; - if (sel?.id === nodeId && sel.raw) { - _showNodeDetail(sel); - } else { - const g = _getGraph?.(); - const rawN = g?.nodes?.find((n) => n.id === nodeId); - if (rawN) { - _showNodeDetail({ raw: rawN, id: rawN.id }); - } - } - refreshLiveState(); } function _bindNodeDetailPanel() { @@ -4148,44 +4260,18 @@ function _deleteNodeDetail() { const detailEl = els?.detailEl; const nodeId = detailEl?.dataset?.editNodeId; if (!nodeId) return; - if (_isGraphWriteBlocked()) { - toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); - return; - } - const g = _getGraph?.(); - const node = g?.nodes?.find((n) => n.id === nodeId); - const label = node ? getNodeDisplayName(node) : nodeId; - if ( - !confirm( - `确定删除节点「${label}」?\n\n若该节点有层级子节点,将一并删除。此操作不可在本面板内撤销。`, - ) - ) { - return; - } - const result = _actionHandlers.deleteGraphNode?.({ nodeId }); - if (!result?.ok) { - toastr.error( - result?.error === "node-not-found" ? "节点已不存在" : "删除失败", - "ST-BME", - ); - return; - } - if (result.persistBlocked) { - toastr.warning( - "节点已从图中移除,但写回可能被拦截,请查看图谱状态", - "ST-BME", - ); - } else { - toastr.success("节点已删除", "ST-BME"); - } - _closeNodeDetailUi(); - const dDesk = document.getElementById("bme-node-detail"); - const dMob = document.getElementById("bme-mobile-node-detail"); - if (dDesk) delete dDesk.dataset.editNodeId; - if (dMob) delete dMob.dataset.editNodeId; - graphRenderer?.highlightNode?.("__cleared__"); - mobileGraphRenderer?.highlightNode?.("__cleared__"); - refreshLiveState(); + + _deleteGraphNodeById(nodeId, { + afterSuccess: () => { + _closeNodeDetailUi(); + const dDesk = document.getElementById("bme-node-detail"); + const dMob = document.getElementById("bme-mobile-node-detail"); + if (dDesk) delete dDesk.dataset.editNodeId; + if (dMob) delete dMob.dataset.editNodeId; + graphRenderer?.highlightNode?.("__cleared__"); + mobileGraphRenderer?.highlightNode?.("__cleared__"); + }, + }); } function _bindClose() {