From 37bada37b6911b1a1a65a2053244861df13a3555 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Mon, 20 Apr 2026 16:39:02 +0800 Subject: [PATCH] feat: add recall card editing and ENA preview UI --- index.js | 114 +++++++++++++++++++++ style.css | 212 +++++++++++++++++++++++++++++++++++++++ tests/p0-regressions.mjs | 173 ++++++++++++++++++++++++++++++++ ui/recall-message-ui.js | 201 ++++++++++++++++++++++++++++++++++++- 4 files changed, 697 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 8c912bd..684b59c 100644 --- a/index.js +++ b/index.js @@ -3063,6 +3063,102 @@ function editMessageRecallRecord(messageIndex, nextInjectionText) { return edited; } +function syncEditedUserMessageDom(messageIndex, nextText) { + const chatRoot = document?.getElementById?.("chat"); + if (!chatRoot?.querySelectorAll) return false; + + for (const messageElement of Array.from(chatRoot.querySelectorAll(".mes") || [])) { + if (resolveMessageIndexFromElement(messageElement) !== messageIndex) continue; + const userTextElement = messageElement.querySelector?.(".mes_text"); + if (!userTextElement) return false; + userTextElement.textContent = String(nextText || ""); + return true; + } + return false; +} + +function persistEditedUserMessage(context = getContext()) { + const candidates = [ + ["saveChatConditional", context?.saveChatConditional], + ["saveChat", context?.saveChat], + ]; + + for (const [label, handler] of candidates) { + if (typeof handler !== "function") continue; + try { + const result = handler.call(context); + if (result && typeof result.catch === "function") { + result.catch((error) => { + console.error(`[ST-BME] 保存用户输入编辑失败 (${label}):`, error); + }); + } + return label; + } catch (error) { + console.error(`[ST-BME] 调用 ${label} 保存用户输入编辑失败:`, error); + } + } + + return triggerChatMetadataSave(context, { immediate: true }); +} + +function editMessageUserInputText(messageIndex, nextUserInputText) { + const context = getContext(); + const chat = context?.chat; + if (!Array.isArray(chat)) { + return { ok: false, error: "missing-chat" }; + } + + const message = chat[messageIndex]; + if (!message?.is_user) { + return { ok: false, error: "not-user-message" }; + } + + const normalizedText = normalizeRecallInputText(nextUserInputText); + if (!normalizedText) { + return { ok: false, error: "empty-user-input" }; + } + + const previousText = normalizeRecallInputText(message.mes || ""); + const currentRecord = readPersistedRecallFromUserMessage(chat, messageIndex); + const recallBoundText = normalizeRecallInputText( + currentRecord?.boundUserFloorText || previousText, + ); + const recallMayBeStale = Boolean(currentRecord) && recallBoundText !== normalizedText; + + message.mes = normalizedText; + const swipeIndex = Number.isFinite(Number(message?.swipe_id)) + ? Math.max(0, Math.floor(Number(message.swipe_id))) + : null; + if ( + Array.isArray(message?.swipes) && + swipeIndex !== null && + swipeIndex < message.swipes.length + ) { + message.swipes[swipeIndex] = normalizedText; + } + + if (message.extra && typeof message.extra === "object") { + if (typeof message.extra.display_text === "string") { + message.extra.display_text = normalizedText; + } + if (typeof message.extra.current_display_text === "string") { + message.extra.current_display_text = normalizedText; + } + } + + const saveMode = persistEditedUserMessage(context); + const domSynced = syncEditedUserMessageDom(messageIndex, normalizedText); + + return { + ok: true, + nextText: normalizedText, + recallMayBeStale, + unchanged: previousText === normalizedText, + saveMode, + domSynced, + }; +} + function rewriteRecallPayloadWithInjection( promptData = null, injectionText = "", @@ -4056,6 +4152,24 @@ function getRecallCardCallbacks() { }, }); }, + onEditUserInput: (messageIndex, nextUserInputText) => { + const result = editMessageUserInputText(messageIndex, nextUserInputText); + if (!result?.ok) { + toastr.warning("编辑失败:内容不能为空或此楼层非用户消息"); + return result; + } + + if (result.unchanged) { + toastr.info("用户输入未变化"); + } else { + toastr.success("已更新本轮用户输入"); + } + if (result.recallMayBeStale) { + toastr.info("输入已改,当前召回结果可能需要重新召回"); + } + schedulePersistedRecallMessageUiRefresh(); + return result; + }, onDelete: (messageIndex) => { if (removeMessageRecallRecord(messageIndex)) { toastr.success("已删除持久召回注入"); diff --git a/style.css b/style.css index 8ae9e8f..d44c53e 100644 --- a/style.css +++ b/style.css @@ -5895,6 +5895,7 @@ .bme-recall-user-label { display: flex; align-items: center; + justify-content: space-between; gap: 6px; padding: 10px 14px 4px; font-size: 12px; @@ -5902,6 +5903,48 @@ font-weight: 500; } +.bme-recall-user-label-text { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; +} + +.bme-recall-user-label-actions { + display: inline-flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.bme-recall-user-edit-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08)); + border-radius: 999px; + background: transparent; + color: var(--bme-on-surface-dim, rgba(228, 225, 230, 0.6)); + font-size: 11px; + cursor: pointer; + transition: + background 0.15s, + border-color 0.15s, + color 0.15s; +} + +.bme-recall-user-edit-btn:hover:not(:disabled) { + background: var(--bme-surface-high, #2a2a2d); + border-color: var(--bme-border-active, rgba(233, 69, 96, 0.4)); + color: var(--bme-on-surface, #e4e1e6); +} + +.bme-recall-user-edit-btn:disabled { + opacity: 0.5; + cursor: default; +} + .bme-recall-user-text { padding: 4px 14px 10px; font-size: 14px; @@ -5912,6 +5955,63 @@ word-break: break-word; } +.bme-recall-user-edit-wrap { + display: flex; + flex-direction: column; + gap: 8px; + padding: 4px 14px 10px; +} + +.bme-recall-user-edit-textarea { + width: 100%; + min-height: 72px; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08)); + background: var(--bme-surface-low, #1b1b1e); + color: var(--bme-on-surface, #e4e1e6); + font-size: 13px; + line-height: 1.6; + resize: vertical; + transition: border-color 0.15s; +} + +.bme-recall-user-edit-textarea:focus { + outline: none; + border-color: var(--bme-border-active, rgba(233, 69, 96, 0.4)); +} + +.bme-recall-user-edit-actions { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.bme-recall-user-edit-action { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 68px; + padding: 6px 10px; + border-radius: 6px; + border: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08)); + background: transparent; + color: var(--bme-on-surface-dim, rgba(228, 225, 230, 0.6)); + font-size: 11px; + cursor: pointer; +} + +.bme-recall-user-edit-action.primary { + background: var(--bme-primary-dim, rgba(233, 69, 96, 0.15)); + border-color: var(--bme-primary, #e94560); + color: var(--bme-primary-text, #ffb2b7); +} + +.bme-recall-user-edit-action.secondary:hover, +.bme-recall-user-edit-action.primary:hover { + filter: brightness(1.06); +} + .bme-recall-card.bme-recall-hide-user-input .bme-recall-user-label, .bme-recall-card.bme-recall-hide-user-input .bme-recall-user-text { display: none; @@ -6051,6 +6151,103 @@ color: #000; } +.bme-recall-meta-tag.is-ena { + background: rgba(111, 194, 255, 0.16); + color: #9dd6ff; + border: 1px solid rgba(111, 194, 255, 0.28); +} + +.bme-recall-meta-text { + min-width: 0; +} + +.bme-recall-injection-preview { + margin: 0 14px 8px; + border: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08)); + border-radius: 8px; + overflow: hidden; + background: var(--bme-surface-low, #1b1b1e); +} + +.bme-recall-injection-preview.is-ena { + border-left: 3px solid #6fc2ff; +} + +.bme-recall-injection-toggle { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 10px; + border: none; + background: transparent; + color: var(--bme-on-surface, #e4e1e6); + font-size: 12px; + font-weight: 600; + cursor: pointer; +} + +.bme-recall-injection-toggle-arrow { + color: var(--bme-on-surface-dim, rgba(228, 225, 230, 0.6)); + transition: transform 0.2s ease; +} + +.bme-recall-injection-preview.expanded .bme-recall-injection-toggle-arrow { + transform: rotate(90deg); +} + +.bme-recall-injection-content { + display: none; + padding: 0 10px 10px; + border-top: 1px solid var(--bme-border, rgba(255, 255, 255, 0.08)); +} + +.bme-recall-injection-preview.expanded .bme-recall-injection-content { + display: block; +} + +.bme-recall-injection-note { + padding-top: 10px; + font-size: 11px; + color: #9dd6ff; +} + +.bme-recall-injection-section-title { + margin-top: 10px; + font-size: 11px; + font-weight: 700; + color: var(--bme-primary-text, #ffb2b7); +} + +.bme-recall-injection-subsection { + margin-top: 8px; + font-size: 11px; + font-weight: 600; + color: var(--bme-on-surface, #e4e1e6); +} + +.bme-recall-injection-line, +.bme-recall-injection-table { + margin-top: 6px; + font-size: 12px; + line-height: 1.55; + color: var(--bme-on-surface-dim, rgba(228, 225, 230, 0.78)); + white-space: pre-wrap; + word-break: break-word; +} + +.bme-recall-injection-table { + padding: 8px; + border-radius: 6px; + background: var(--bme-surface-lowest, #0e0e11); + overflow-x: auto; +} + +.bme-recall-injection-spacer { + height: 6px; +} + /* --- Recall Actions --- */ .bme-recall-actions { @@ -6312,6 +6509,21 @@ height: 180px; } + .bme-recall-user-label { + flex-wrap: wrap; + align-items: flex-start; + } + + .bme-recall-user-label-actions { + width: 100%; + justify-content: flex-end; + } + + .bme-recall-injection-preview { + margin-left: 10px; + margin-right: 10px; + } + .bme-recall-sidebar { width: 100vw; } diff --git a/tests/p0-regressions.mjs b/tests/p0-regressions.mjs index a9877ea..b4b804b 100644 --- a/tests/p0-regressions.mjs +++ b/tests/p0-regressions.mjs @@ -1867,6 +1867,176 @@ async function testRecallCardDisplayModeToggleRestoresOriginalUserText() { } } +async function testRecallCardSupportsManagedUserInputEditing() { + const chat = [ + { + is_user: true, + mes: "原始用户输入", + extra: { + bme_recall: buildPersistedRecallRecord({ + injectionText: "[Memory - Recalled]\nline-1", + selectedNodeIds: ["n1"], + boundUserFloorText: "原始用户输入", + nowIso: "2026-01-01T00:00:00.000Z", + }), + }, + }, + ]; + const harness = await createRecallUiHarness({ chat }); + const messageElement = createMessageElement(harness.document, 0, { + stableId: true, + withMesBlock: true, + isUser: true, + }); + const userTextElement = messageElement.querySelector(".mes_text"); + userTextElement.textContent = chat[0].mes; + harness.chatRoot.appendChild(messageElement); + + try { + const saveResults = []; + const cardModule = await import("../ui/recall-message-ui.js"); + const mountedCard = cardModule.createRecallCardElement({ + messageIndex: 0, + record: chat[0].extra.bme_recall, + userMessageText: chat[0].mes, + callbacks: { + onEditUserInput: async (_messageIndex, nextText) => { + saveResults.push(nextText); + return { + ok: true, + nextText, + recallMayBeStale: true, + }; + }, + }, + }); + messageElement.querySelector(".mes_block")?.appendChild(mountedCard); + + const card = harness.chatRoot.querySelector(".bme-recall-card"); + const editBtn = card.querySelector(".bme-recall-user-edit-btn"); + assert.equal(Boolean(editBtn), true); + + editBtn.click(); + const textarea = card.querySelector(".bme-recall-user-edit-textarea"); + assert.equal(Boolean(textarea), true); + assert.equal(card.classList.contains("bme-recall-user-input-editing"), true); + + textarea.value = "新的用户输入"; + const saveBtn = card.querySelector(".bme-recall-user-edit-action.primary"); + await saveBtn.dispatchEvent({ type: "click", stopPropagation() {} }); + + assert.deepEqual(saveResults, ["新的用户输入"]); + assert.equal(chat[0].mes, "原始用户输入"); + assert.equal( + card.querySelector(".bme-recall-user-text")?.textContent, + "新的用户输入", + ); + assert.equal(card.classList.contains("bme-recall-user-input-editing"), false); + } finally { + harness.restoreGlobals(); + } +} + +async function testRecallCardShowsEnaSourceChipAndExpandedPreview() { + const chat = [ + { + is_user: true, + mes: "planner user input", + extra: { + bme_recall: buildPersistedRecallRecord({ + injectionText: "[Memory - Recalled]\n### 当前状态记忆\nline-1\n|a|b|", + selectedNodeIds: ["n1"], + recallSource: "planner-handoff", + hookName: "ena-planner", + nowIso: "2026-01-01T00:00:00.000Z", + }), + }, + }, + ]; + const harness = await createRecallUiHarness({ chat }); + const messageElement = createMessageElement(harness.document, 0, { + stableId: true, + withMesBlock: true, + isUser: true, + }); + harness.chatRoot.appendChild(messageElement); + + try { + harness.api.refreshPersistedRecallMessageUi(); + const card = harness.chatRoot.querySelector(".bme-recall-card"); + card.querySelector(".bme-recall-bar")?.click(); + + const enaTag = card.querySelector(".bme-recall-meta-tag.is-ena"); + assert.equal(Boolean(enaTag), true); + assert.equal(enaTag?.textContent, "🧭 ENA Planner"); + + const preview = card.querySelector(".bme-recall-injection-preview.is-ena"); + assert.equal(Boolean(preview), true); + assert.equal(preview.classList.contains("expanded"), true); + assert.equal( + preview.querySelector(".bme-recall-injection-note")?.textContent, + "由 Ena Planner 触发的本轮记忆块", + ); + } finally { + harness.restoreGlobals(); + } +} + +async function testRecallCardBeautifiesInjectionPreviewSections() { + const chat = [ + { + is_user: true, + mes: "normal user input", + extra: { + bme_recall: buildPersistedRecallRecord({ + injectionText: "[Memory - Objective / Global]\n#### 子标题\n普通行\n\n|col1|col2|", + selectedNodeIds: ["n1"], + recallSource: "history", + nowIso: "2026-01-01T00:00:00.000Z", + }), + }, + }, + ]; + const harness = await createRecallUiHarness({ chat }); + const messageElement = createMessageElement(harness.document, 0, { + stableId: true, + withMesBlock: true, + isUser: true, + }); + harness.chatRoot.appendChild(messageElement); + + try { + harness.api.refreshPersistedRecallMessageUi(); + const card = harness.chatRoot.querySelector(".bme-recall-card"); + card.querySelector(".bme-recall-bar")?.click(); + + const preview = card.querySelector(".bme-recall-injection-preview"); + assert.equal(Boolean(preview), true); + assert.equal(preview.classList.contains("expanded"), false); + + preview.querySelector(".bme-recall-injection-toggle")?.click(); + assert.equal(preview.classList.contains("expanded"), true); + assert.equal( + preview.querySelector(".bme-recall-injection-section-title")?.textContent, + "[Memory - Objective / Global]", + ); + assert.equal( + preview.querySelector(".bme-recall-injection-subsection")?.textContent, + "子标题", + ); + assert.equal( + preview.querySelector(".bme-recall-injection-line")?.textContent, + "普通行", + ); + assert.equal( + preview.querySelector(".bme-recall-injection-table")?.textContent, + "|col1|col2|", + ); + } finally { + harness.restoreGlobals(); + } +} + function makeEvent(seq, title) { return createNode({ type: "event", @@ -7114,6 +7284,9 @@ await testRecallCardRefreshCleansLegacyBadgeAndAvoidsDuplicates(); await testRecallCardExpandedContentRerendersAfterRecordUpdate(); await testRecallCardUserTextRefreshesWithoutCardRecreate(); await testRecallCardDisplayModeToggleRestoresOriginalUserText(); +await testRecallCardSupportsManagedUserInputEditing(); +await testRecallCardShowsEnaSourceChipAndExpandedPreview(); +await testRecallCardBeautifiesInjectionPreviewSections(); await testRecallSubGraphAndDataLayerEntryPoints(); await testRerollUsesBatchBoundaryRollbackAndPersistsState(); await testNotifyHistoryDirtyUsesStageNoticeWithoutGenericWarningToast(); diff --git a/ui/recall-message-ui.js b/ui/recall-message-ui.js index 4c101e1..5ecfca7 100644 --- a/ui/recall-message-ui.js +++ b/ui/recall-message-ui.js @@ -81,7 +81,9 @@ function formatTokenHint(tokenEstimate) { function formatMetaLine(record) { const parts = []; - if (record.recallSource) parts.push(`来源: ${record.recallSource}`); + if (record.recallSource && !buildRecallSourceLabel(record)) { + parts.push(`来源: ${record.recallSource}`); + } if (record.authoritativeInputUsed) parts.push("权威输入"); if (record.tokenEstimate > 0) parts.push(`~${record.tokenEstimate} tokens`); if (Number.isFinite(record.generationCount) && record.generationCount > 0) { @@ -172,6 +174,109 @@ function summarizeSubGraphForSignature(subGraph) { return { nodes, edges }; } +function isPlannerRecallSource(record = {}) { + const recallSource = String(record?.recallSource || "").trim().toLowerCase(); + const hookName = String(record?.hookName || "").trim().toLowerCase(); + return recallSource.startsWith("planner") || hookName.includes("planner") || hookName.includes("ena"); +} + +function buildRecallSourceLabel(record = {}) { + if (isPlannerRecallSource(record)) return "ENA Planner"; + return ""; +} + +function classifyInjectionLine(line = "") { + const text = String(line || ""); + const trimmed = text.trim(); + if (!trimmed) return { kind: "blank", text }; + if (/^\[[^\]]+\]$/.test(trimmed)) return { kind: "section", text: trimmed }; + if (/^#{2,6}\s+/.test(trimmed)) { + return { kind: "subsection", text: trimmed.replace(/^#{2,6}\s+/, "") }; + } + if (/^\|.*\|$/.test(trimmed)) return { kind: "table", text }; + return { kind: "line", text }; +} + +function appendInjectionPreviewContent(container, injectionText = "") { + const lines = String(injectionText || "").replace(/\r\n/g, "\n").split("\n"); + let tableBuffer = []; + + const flushTable = () => { + if (tableBuffer.length === 0) return; + const pre = el("pre", "bme-recall-injection-table", tableBuffer.join("\n")); + container.appendChild(pre); + tableBuffer = []; + }; + + for (const rawLine of lines) { + const classified = classifyInjectionLine(rawLine); + if (classified.kind === "table") { + tableBuffer.push(classified.text); + continue; + } + + flushTable(); + + if (classified.kind === "blank") { + container.appendChild(el("div", "bme-recall-injection-spacer")); + continue; + } + if (classified.kind === "section") { + container.appendChild( + el("div", "bme-recall-injection-section-title", classified.text), + ); + continue; + } + if (classified.kind === "subsection") { + container.appendChild( + el("div", "bme-recall-injection-subsection", classified.text), + ); + continue; + } + container.appendChild(el("div", "bme-recall-injection-line", classified.text)); + } + + flushTable(); +} + +function buildInjectionPreviewBlock(record = {}) { + const injectionText = String(record?.injectionText || "").trim(); + if (!injectionText) return null; + + const isEna = isPlannerRecallSource(record); + const wrap = el( + "div", + `bme-recall-injection-preview${isEna ? " is-ena" : ""}`, + ); + const header = el("button", "bme-recall-injection-toggle"); + header.type = "button"; + const defaultExpanded = isEna; + header.setAttribute("aria-expanded", defaultExpanded ? "true" : "false"); + header.innerHTML = ` + ${isEna ? "ENA 注入预览" : "注入预览"} + + `; + wrap.appendChild(header); + + const content = el("div", "bme-recall-injection-content"); + if (isEna) { + content.appendChild( + el("div", "bme-recall-injection-note", "由 Ena Planner 触发的本轮记忆块"), + ); + } + appendInjectionPreviewContent(content, injectionText); + wrap.appendChild(content); + wrap.classList.toggle("expanded", defaultExpanded); + + header.addEventListener("click", (event) => { + event.stopPropagation(); + const expanded = wrap.classList.toggle("expanded"); + header.setAttribute("aria-expanded", expanded ? "true" : "false"); + }); + + return wrap; +} + function buildExpandedRenderSignature({ record, userMessageText, @@ -231,15 +336,39 @@ export function createRecallCardElement({ userInputDisplayMode, ); let expandedRenderSignature = ""; + let isEditingUserInput = false; // -- 用户消息区 -- const userLabel = el("div", "bme-recall-user-label"); - userLabel.innerHTML = "💬 本轮用户输入"; + const userLabelText = el("div", "bme-recall-user-label-text"); + userLabelText.innerHTML = "💬 本轮用户输入"; + userLabel.appendChild(userLabelText); + + const userLabelActions = el("div", "bme-recall-user-label-actions"); + const editUserInputBtn = el("button", "bme-recall-user-edit-btn"); + editUserInputBtn.type = "button"; + editUserInputBtn.innerHTML = '✏️编辑'; + userLabelActions.appendChild(editUserInputBtn); + userLabel.appendChild(userLabelActions); card.appendChild(userLabel); const userText = el("div", "bme-recall-user-text", activeUserMessageText || "(empty)"); card.appendChild(userText); + const userEditWrap = el("div", "bme-recall-user-edit-wrap"); + const userEditTextarea = document.createElement("textarea"); + userEditTextarea.className = "bme-recall-user-edit-textarea"; + userEditWrap.appendChild(userEditTextarea); + const userEditActions = el("div", "bme-recall-user-edit-actions"); + const userEditSaveBtn = el("button", "bme-recall-user-edit-action primary", "保存"); + userEditSaveBtn.type = "button"; + const userEditCancelBtn = el("button", "bme-recall-user-edit-action secondary", "取消"); + userEditCancelBtn.type = "button"; + userEditActions.appendChild(userEditSaveBtn); + userEditActions.appendChild(userEditCancelBtn); + userEditWrap.appendChild(userEditActions); + card.appendChild(userEditWrap); + // -- 召回条 -- const initialNodeCount = Array.isArray(activeRecord?.selectedNodeIds) ? activeRecord.selectedNodeIds.length @@ -279,6 +408,22 @@ export function createRecallCardElement({ // renderer 实例管理 let renderer = null; + function setUserInputEditMode(editing = false) { + isEditingUserInput = Boolean(editing); + card.classList.toggle("bme-recall-user-input-editing", isEditingUserInput); + userText.hidden = isEditingUserInput; + userEditWrap.hidden = !isEditingUserInput; + editUserInputBtn.disabled = isEditingUserInput; + if (!isEditingUserInput) return; + + userEditTextarea.value = activeUserMessageText || ""; + const lineCount = Math.max(3, String(activeUserMessageText || "").split(/\n/).length); + if (userEditTextarea.style && typeof userEditTextarea.style === "object") { + userEditTextarea.style.minHeight = `${Math.min(12, lineCount) * 22}px`; + } + userEditTextarea.focus?.(); + } + function destroyRenderer() { if (renderer) { renderer.stopAnimation(); @@ -332,13 +477,34 @@ export function createRecallCardElement({ } // 元信息行 - const meta = el("div", "bme-recall-meta", formatMetaLine(activeRecord || {})); + const meta = el("div", "bme-recall-meta"); + const sourceLabel = buildRecallSourceLabel(activeRecord || {}); + const metaText = formatMetaLine(activeRecord || {}); + if (typeof HTMLElement === "undefined" || !(meta instanceof HTMLElement)) { + meta.textContent = metaText; + } + if (sourceLabel) { + const sourceTag = el( + "span", + `bme-recall-meta-tag${isPlannerRecallSource(activeRecord) ? " is-ena" : ""}`, + isPlannerRecallSource(activeRecord) ? `🧭 ${sourceLabel}` : sourceLabel, + ); + meta.appendChild(sourceTag); + } + if (metaText) { + meta.appendChild(el("span", "bme-recall-meta-text", metaText)); + } if (activeRecord?.manuallyEdited) { const tag = el("span", "bme-recall-meta-tag", "✍ 手动编辑"); meta.appendChild(tag); } body.appendChild(meta); + const injectionPreviewBlock = buildInjectionPreviewBlock(activeRecord || {}); + if (injectionPreviewBlock) { + body.appendChild(injectionPreviewBlock); + } + // 操作按钮行 const actions = el("div", "bme-recall-actions"); @@ -413,6 +579,9 @@ export function createRecallCardElement({ activeUserInputDisplayMode === "off", ); userText.textContent = activeUserMessageText || "(empty)"; + if (isEditingUserInput) { + userEditTextarea.value = activeUserMessageText || ""; + } const nodeCount = Array.isArray(activeRecord?.selectedNodeIds) ? activeRecord.selectedNodeIds.length @@ -439,6 +608,31 @@ export function createRecallCardElement({ card._bmeUpdateRecallCard = applyCardRuntimeData; + editUserInputBtn.addEventListener("click", (event) => { + event.stopPropagation(); + setUserInputEditMode(true); + }); + + userEditCancelBtn.addEventListener("click", (event) => { + event.stopPropagation(); + setUserInputEditMode(false); + }); + + userEditSaveBtn.addEventListener("click", async (event) => { + event.stopPropagation(); + const result = await activeCallbacks.onEditUserInput?.( + messageIndex, + userEditTextarea.value, + ); + if (result?.ok) { + if (Object.prototype.hasOwnProperty.call(result, "nextText")) { + activeUserMessageText = String(result.nextText || ""); + userText.textContent = activeUserMessageText || "(empty)"; + } + setUserInputEditMode(false); + } + }); + // 点击召回条 toggle 展开/折叠 bar.addEventListener("click", (e) => { e.stopPropagation(); @@ -455,6 +649,7 @@ export function createRecallCardElement({ }); applyCardRuntimeData({}, { skipExpandedRerender: true }); + setUserInputEditMode(false); // 暴露清理方法 card._bmeDestroyRenderer = () => {