fix(chat): run assistant typewriter only once per message
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-02 22:29:41 +08:00
parent ec63e28887
commit 7ce2857aff

View File

@ -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<string>();
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);