From ac788daa03401f5c1db6b8e84bedf3b6944075cf Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 26 Apr 2026 16:52:22 +0800 Subject: [PATCH] fix(show_html): freeze completed card during streaming --- static/src/app/bootstrap.ts | 38 ++++++++++++-- static/src/stores/chat.ts | 50 ++++++++++++++++--- .../styles/components/chat/_chat-area.scss | 3 +- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/static/src/app/bootstrap.ts b/static/src/app/bootstrap.ts index 304e733..ed4c077 100644 --- a/static/src/app/bootstrap.ts +++ b/static/src/app/bootstrap.ts @@ -565,6 +565,23 @@ function renderShowImages(root: ParentNode | null = document) { }); let persistent = showHtmlPersistentRenderByPath.get(pathKey); + if (!persistent && effectiveEncoded && inStreaming) { + // 精确匹配 + let ep = showHtmlPersistentByEncoded.get(effectiveEncoded); + // 前缀匹配:多条渲染源可能输出不同阶段的 encoded,用最长前缀匹配找到 COMPLETE 的 wrapper + if (!ep) { + let bestLen = 0; + for (const [key, val] of showHtmlPersistentByEncoded) { + if (key.startsWith(effectiveEncoded) && key.length > bestLen) { + ep = val; bestLen = key.length; + } + } + } + if (ep) { + persistent = { encoded: ep.encoded, ratioKey: ep.ratioKey, widthPx: ep.widthPx, heightPx: ep.heightPx, wrapper: ep.wrapper, host: ep.host, root: ep.root }; + showHtmlPersistentRenderByPath.set(pathKey, persistent); + } + } if (!persistent) { const wrapper = document.createElement('div'); wrapper.className = 'chat-inline-html'; @@ -615,17 +632,23 @@ function renderShowImages(root: ParentNode | null = document) { persistent.wrapper.style.width = `${widthPx}px`; persistent.host.style.height = `${heightPx}px`; - if ( + const needsUpdate = persistent.encoded !== effectiveEncoded || persistent.widthPx !== widthPx || persistent.heightPx !== heightPx || - persistent.ratioKey !== ratioKey - ) { + persistent.ratioKey !== ratioKey; + if (needsUpdate) { persistent.root.innerHTML = safeHtml; persistent.encoded = effectiveEncoded; persistent.widthPx = widthPx; persistent.heightPx = heightPx; persistent.ratioKey = ratioKey; + if (effectiveEncoded && inStreaming) { + showHtmlPersistentByEncoded.set(effectiveEncoded, { + wrapper: persistent.wrapper, host: persistent.host, root: persistent.root, + encoded: effectiveEncoded, ratioKey, widthPx, heightPx + }); + } debugShowHtmlLog('render:node-persistent-update', { renderId, pathKey, @@ -642,7 +665,9 @@ function renderShowImages(root: ParentNode | null = document) { }); } - node.replaceChildren(persistent.wrapper); + if (!inStreaming || isPartial || needsUpdate || !node.hasAttribute('data-rendered')) { + node.replaceChildren(persistent.wrapper); + } node.setAttribute('data-rendered', '1'); const rect = persistent.wrapper.getBoundingClientRect(); debugShowHtmlLog('render:node-done', { @@ -712,6 +737,10 @@ const showHtmlPersistentRenderByPath = new Map< root: HTMLElement } >(); +const showHtmlPersistentByEncoded = new Map(); let layoutDebugObserver: MutationObserver | null = null; let layoutDebugResizeObserver: ResizeObserver | null = null; let layoutDebugStarted = false; @@ -1011,6 +1040,7 @@ export function teardownShowImageObserver() { showHtmlCompletedSnapshotByPath.clear(); showHtmlStreamingSizeLockByPath.clear(); showHtmlPersistentRenderByPath.clear(); + showHtmlPersistentByEncoded.clear(); showTagObservedContainer = null; } diff --git a/static/src/stores/chat.ts b/static/src/stores/chat.ts index e39c72b..9e7b2b5 100644 --- a/static/src/stores/chat.ts +++ b/static/src/stores/chat.ts @@ -32,6 +32,13 @@ const GENERATING_LABELS = [ '快敲完了,别急' ]; +const SHOW_HTML_COMPLETE_BLOCK_RE = //i; + +function hasCompletedShowHtmlBlock(content: string | null | undefined) { + if (!content) return false; + return SHOW_HTML_COMPLETE_BLOCK_RE.test(content); +} + function randomGeneratingLabel() { if (!GENERATING_LABELS.length) { return ''; @@ -249,6 +256,7 @@ export const useChatStore = defineStore('chat', { clearAwaitingFirstContent(msg); msg.streamingText = ''; msg.currentStreamingType = 'text'; + (msg as any).__splitByShowHtml = false; const action = { id: randomId('text'), type: 'text', @@ -269,28 +277,58 @@ export const useChatStore = defineStore('chat', { msg.streamingText = ''; } msg.streamingText += content; - const lastAction = msg.actions[msg.actions.length - 1]; - if (lastAction && lastAction.type === 'text') { - lastAction.content += content; - return lastAction; + let lastAction = msg.actions[msg.actions.length - 1]; + if (!(lastAction && lastAction.type === 'text' && lastAction.streaming)) { + lastAction = { + id: randomId('text'), + type: 'text', + content: '', + streaming: true, + timestamp: Date.now(), + continuation: true + }; + msg.actions.push(lastAction); } - return null; + lastAction.content += content; + + // show_html 一旦闭合,当前卡片应“定格”,后续 chunk 进入新的 text action, + // 避免 streaming 阶段每个新 chunk 都重建同一 show_html 卡片。 + if (lastAction.streaming && hasCompletedShowHtmlBlock(lastAction.content)) { + lastAction.streaming = false; + lastAction.frozenByShowHtml = true; + (msg as any).__splitByShowHtml = true; + } + + return lastAction; }, completeText(fullContent: string) { if (this.currentMessageIndex < 0) return; const msg = this.messages[this.currentMessageIndex]; + const splitByShowHtml = !!(msg as any).__splitByShowHtml; + let completedStreamingAction = false; for (let i = msg.actions.length - 1; i >= 0; i--) { const action = msg.actions[i]; if (action.type === 'text' && action.streaming) { action.streaming = false; - if (typeof fullContent === 'string' && fullContent.length) { + completedStreamingAction = true; + if (!splitByShowHtml && typeof fullContent === 'string' && fullContent.length) { action.content = fullContent; } break; } } + if (!completedStreamingAction && !splitByShowHtml && typeof fullContent === 'string') { + for (let i = msg.actions.length - 1; i >= 0; i--) { + const action = msg.actions[i]; + if (action.type === 'text') { + action.content = fullContent; + break; + } + } + } msg.streamingText = ''; msg.currentStreamingType = null; + delete (msg as any).__splitByShowHtml; }, addSystemMessage(content: string, meta: any = null) { // 与历史重建保持一致:子智能体/后台完成通知作为独立 assistant 消息渲染, diff --git a/static/src/styles/components/chat/_chat-area.scss b/static/src/styles/components/chat/_chat-area.scss index 3cfccfa..2211149 100644 --- a/static/src/styles/components/chat/_chat-area.scss +++ b/static/src/styles/components/chat/_chat-area.scss @@ -937,7 +937,7 @@ body[data-theme='dark'] .more-icon { margin: 14px auto; border: 1px solid var(--claude-border); border-radius: 12px; - overflow: auto; + overflow: hidden; background: transparent; box-shadow: var(--claude-shadow); } @@ -945,6 +945,7 @@ body[data-theme='dark'] .more-icon { .chat-inline-html__host { display: block; width: 100%; + overflow: hidden; border: 0; background: transparent; }