From 7ce2857affc533009c17e168344739b2e84d211b Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 2 Mar 2026 22:29:41 +0800 Subject: [PATCH] fix(chat): run assistant typewriter only once per message --- components/message.tsx | 66 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/components/message.tsx b/components/message.tsx index 846546443..4254c5370 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -177,6 +177,7 @@ function filterAgentRoutingNoise(text: string): string { const IMAGE_DONE_SUMMARY = "已完成本次生成,如需微调可继续告诉我。"; const ASSISTANT_TYPEWRITER_SECTION_REGEX = /(?:###\s*需求理解|【\s*需求理解\s*】|###\s*图片分析|【\s*图片分析\s*】|###\s*执行计划|【\s*执行计划\s*】|###\s*输出设置|【\s*输出设置\s*】)/; +const ASSISTANT_TYPEWRITER_STORAGE_KEY = "assistant-typewriter-completed-keys-v1"; const MODEL_BADGE_LINE_REGEX = /^\s*[◈◇◆🔹⬡✦•·]\s*([A-Za-z][A-Za-z0-9 .:+\-_/()]{2,})\s*$/im; const MODEL_BADGE_LINE_GLOBAL_REGEX = @@ -221,6 +222,60 @@ function shouldUseAssistantTypewriter(text: string): boolean { } const completedTypewriterKeys = new Set(); +let completedTypewriterKeysLoaded = false; + +function loadCompletedTypewriterKeys() { + if (completedTypewriterKeysLoaded) return; + completedTypewriterKeysLoaded = true; + if (typeof window === "undefined") return; + + try { + const raw = window.localStorage.getItem(ASSISTANT_TYPEWRITER_STORAGE_KEY); + if (!raw) return; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return; + parsed.forEach((key) => { + if (typeof key === "string" && key.trim()) { + completedTypewriterKeys.add(key); + } + }); + } catch { + // ignore + } +} + +function persistCompletedTypewriterKeys() { + if (typeof window === "undefined") return; + try { + const keep = Array.from(completedTypewriterKeys).slice(-400); + window.localStorage.setItem( + ASSISTANT_TYPEWRITER_STORAGE_KEY, + JSON.stringify(keep) + ); + } catch { + // ignore + } +} + +function hasCompletedTypewriter(typingKey: string) { + loadCompletedTypewriterKeys(); + return completedTypewriterKeys.has(typingKey); +} + +function markTypewriterCompleted(typingKey: string) { + loadCompletedTypewriterKeys(); + completedTypewriterKeys.add(typingKey); + if (completedTypewriterKeys.size > 450) { + const overflow = completedTypewriterKeys.size - 400; + const iterator = completedTypewriterKeys.values(); + for (let i = 0; i < overflow; i += 1) { + const next = iterator.next(); + if (next.done) break; + completedTypewriterKeys.delete(next.value); + } + } + persistCompletedTypewriterKeys(); +} function AssistantTypewriter({ typingKey, @@ -232,9 +287,10 @@ function AssistantTypewriter({ children: (displayText: string) => ReactNode; }) { const content = String(text || ""); - const [displayText, setDisplayText] = useState(() => - completedTypewriterKeys.has(typingKey) ? content : "" - ); + const [displayText, setDisplayText] = useState(() => { + if (typeof window === "undefined") return content; + return hasCompletedTypewriter(typingKey) ? content : ""; + }); useEffect(() => { if (!content) { @@ -242,7 +298,7 @@ function AssistantTypewriter({ return; } - if (completedTypewriterKeys.has(typingKey)) { + if (hasCompletedTypewriter(typingKey)) { setDisplayText(content); return; } @@ -259,7 +315,7 @@ function AssistantTypewriter({ setDisplayText(content.slice(0, cursor)); if (cursor >= content.length) { window.clearInterval(timer); - completedTypewriterKeys.add(typingKey); + markTypewriterCompleted(typingKey); } }, tickMs);