diff --git a/internal/plugins/webui/dashboard.html b/internal/plugins/webui/dashboard.html index 5199fc5..e6a748c 100644 --- a/internal/plugins/webui/dashboard.html +++ b/internal/plugins/webui/dashboard.html @@ -3164,12 +3164,24 @@ background: badge.style.display = "none"; } - // 流式渲染防抖:SSE 高频推送时合并为一次重建,避免每个 chunk 全量 innerHTML。 - // 聊天内容变化 → 短超时后只重渲聊天(跳过星图/终端/命令历史等无关重建)。 + // 流式渲染分发:流式增量(只更新最后一条正文)vs 全量重建 var _rerenderTimer = null; function rerenderChat(full) { + // 流式中且非强制全量:走增量路径(防抖合并 chunk,只更新最后一条消息节点) + if (!full && state.chatLoading) { + var msgs = state.messages; + var last = msgs.length ? msgs[msgs.length - 1] : null; + if (last && last.role === "assistant" && !last._final) { + if (_rerenderTimer) return; // 已有排程的增量更新 + _rerenderTimer = setTimeout(function () { + _rerenderTimer = null; + renderChatStreamChunk(); + }, 90); + return; + } + } + // 非流式(完成/工具/历史变化):全量重渲(含防抖合并) if (_rerenderTimer) clearTimeout(_rerenderTimer); - // 流式增量(非全量)走短防抖,仅重渲聊天;工具调用/历史变化等需全量重渲 _rerenderTimer = setTimeout(function () { _rerenderTimer = null; renderChat(); @@ -3180,6 +3192,47 @@ background: } }, 90); } + + // 流式增量渲染(移植自 GUI):仅更新最后一条 assistant 消息的正文与思考预览, + // 不重建 DOM。正文节流 parse(>200 字符或 >300ms 才 renderMd),小增量纯文本追加。 + function renderChatStreamChunk() { + var msgsEl = document.getElementById("chat-msgs"); + var last = state.messages.length ? state.messages[state.messages.length - 1] : null; + if (!msgsEl || !last) return; + var el = msgsEl.lastElementChild; + if (!el) { renderChat(); return; } + var textEl = el.querySelector(".msg-bubble .text"); + var c = last.content || ""; + if (textEl && c) { + var now = Date.now(); + var lastParse = el.__lastParse || 0; + var lastLen = el.__lastLen || 0; + if (c.length - lastLen > 200 || now - lastParse > 300) { + textEl.innerHTML = renderMd(c); + el.__lastParse = now; + el.__lastLen = c.length; + } else { + var tail = c.slice(lastLen); + if (tail) textEl.appendChild(document.createTextNode(tail)); + el.__lastLen = c.length; + } + if (state.chatStick !== false) { + try { msgsEl.scrollTop = msgsEl.scrollHeight; } catch (e) {} + } + return; + } + // 思考预览更新(流式中折叠,只刷 preview 文本) + var rcPrev = el.querySelector(".reasoning-preview"); + if (rcPrev && last.reasoning_content) { + rcPrev.textContent = last.reasoning_content.replace(/[\s\n]+/g, " ").slice(0, 60); + if (state.chatStick !== false) { + try { msgsEl.scrollTop = msgsEl.scrollHeight; } catch (e) {} + } + return; + } + // 结构变化兜底:全量 + renderChat(); + } function toggleToolCall(el) { var d = el.querySelector(".tc-detail"); if (!d) return;