From 96b1e781ee97d2f52fa4eea65f3bc6d22a09f3f8 Mon Sep 17 00:00:00 2001 From: troyeguo <13820674+troyeguo@users.noreply.github.com> Date: Sat, 2 May 2026 15:57:28 +0800 Subject: [PATCH] feat: implement update intervals for text accumulation in PopupAssist, PopupDict, and PopupTrans components --- .../popups/popupAssist/component.tsx | 82 +++++++++++-------- src/components/popups/popupDict/component.tsx | 58 +++++++++---- .../popups/popupTrans/component.tsx | 58 +++++++------ src/utils/request/reader.ts | 15 ---- 4 files changed, 120 insertions(+), 93 deletions(-) diff --git a/src/components/popups/popupAssist/component.tsx b/src/components/popups/popupAssist/component.tsx index 84020ae9..8b283585 100644 --- a/src/components/popups/popupAssist/component.tsx +++ b/src/components/popups/popupAssist/component.tsx @@ -18,6 +18,8 @@ class PopupAssist extends React.Component { private chatBoxRef: React.RefObject; private textareaRef: React.RefObject; private singleLineScrollHeight: number = 0; + private answerTextAccumulator: string = ""; + private updateInterval: ReturnType | null = null; constructor(props: PopupAssistProps) { super(props); @@ -35,6 +37,30 @@ class PopupAssist extends React.Component { this.chatBoxRef = React.createRef(); this.textareaRef = React.createRef(); } + + private startUpdateInterval() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + this.updateInterval = setInterval(() => { + if (this.answerTextAccumulator) { + this.setState({ answer: this.answerTextAccumulator }); + if (ConfigService.getReaderConfig("isManualScroll") !== "yes") { + this.scrollToBottom(); + } + } + }, 150); + } + + private stopUpdateInterval(finalAnswer?: string) { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + if (finalAnswer !== undefined) { + this.setState({ answer: finalAnswer }); + } + } componentDidMount(): void { if (this.props.quoteText) { this.setState({ inputQuestion: this.props.quoteText + "\n" }, () => { @@ -132,7 +158,6 @@ class PopupAssist extends React.Component { if (!plugin) { return; } - let isFirst = true; let systemPrompt = ConfigService.getReaderConfig("aiAssistancePrompt") || KookitConfig.DefaultPrompts.aiAssistance; @@ -153,6 +178,8 @@ class PopupAssist extends React.Component { if (!currentQuestion) { return; } + this.answerTextAccumulator = ""; + this.startUpdateInterval(); await chatStream( config.endpoint, config.providerId, @@ -165,25 +192,21 @@ class PopupAssist extends React.Component { return; } if (result && result.text) { - if (isFirst) { - this.setState({ answer: result.text, isWaiting: false }); - isFirst = false; - } else { - this.setState({ - answer: this.state.answer + result.text, - }); + if (!this.answerTextAccumulator) { + this.setState({ isWaiting: false }); } - } - if (ConfigService.getReaderConfig("isManualScroll") !== "yes") { - this.scrollToBottom(); + this.answerTextAccumulator += result.text; } } ); + this.stopUpdateInterval(this.answerTextAccumulator); + const finalAnswer = this.answerTextAccumulator; + this.answerTextAccumulator = ""; if (this.state.mode === "ask") { this.setState({ askHistory: [ ...this.state.askHistory, - { role: "assistant", content: this.state.answer }, + { role: "assistant", content: finalAnswer }, ], answer: "", question: "", @@ -193,7 +216,7 @@ class PopupAssist extends React.Component { this.setState({ chatHistory: [ ...this.state.chatHistory, - { role: "assistant", content: this.state.answer }, + { role: "assistant", content: finalAnswer }, ], answer: "", question: "", @@ -214,7 +237,8 @@ class PopupAssist extends React.Component { if (!plugin) { return; } - let isFirst = true; + this.answerTextAccumulator = ""; + this.startUpdateInterval(); let res = await getAnswerStream( text, this.state.question, @@ -224,23 +248,16 @@ class PopupAssist extends React.Component { this.state.mode, (result) => { if (result && result.text) { - if (isFirst) { - this.setState({ - answer: result.text, - isWaiting: false, - }); - isFirst = false; - } else { - this.setState({ - answer: this.state.answer + result.text, - }); + if (!this.answerTextAccumulator) { + this.setState({ isWaiting: false }); } - } - if (ConfigService.getReaderConfig("isManualScroll") !== "yes") { - this.scrollToBottom(); + this.answerTextAccumulator += result.text; } } ); + this.stopUpdateInterval(this.answerTextAccumulator); + const finalAnswer = this.answerTextAccumulator; + this.answerTextAccumulator = ""; if (res.data && res.done) { if (this.state.mode === "ask") { this.setState({ @@ -248,7 +265,7 @@ class PopupAssist extends React.Component { ...this.state.askHistory, { role: "assistant", - content: this.state.answer, + content: finalAnswer, }, ], answer: "", @@ -261,7 +278,7 @@ class PopupAssist extends React.Component { ...this.state.chatHistory, { role: "assistant", - content: this.state.answer, + content: finalAnswer, }, ], answer: "", @@ -270,13 +287,6 @@ class PopupAssist extends React.Component { }); } } - // if (res.code === 20006) { - // this.setState({ - // isWaiting: false, - // answer: "", - // question: "", - // }); - // } if (ConfigService.getReaderConfig("isManualScroll") !== "yes") { this.scrollToBottom(); } diff --git a/src/components/popups/popupDict/component.tsx b/src/components/popups/popupDict/component.tsx index ca2642e0..1786d047 100644 --- a/src/components/popups/popupDict/component.tsx +++ b/src/components/popups/popupDict/component.tsx @@ -22,6 +22,9 @@ import { marked } from "marked"; import { getIframeDoc } from "../../../utils/reader/docUtil"; declare var window: any; class PopupDict extends React.Component { + private aiTextAccumulator: string = ""; + private updateInterval: ReturnType | null = null; + constructor(props: PopupDictProps) { super(props); this.state = { @@ -37,6 +40,27 @@ class PopupDict extends React.Component { isAiWaiting: false, }; } + + private startUpdateInterval() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + this.updateInterval = setInterval(() => { + if (this.aiTextAccumulator) { + this.setState({ aiAnswer: this.aiTextAccumulator }); + } + }, 150); + } + + private stopUpdateInterval() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + if (this.aiTextAccumulator) { + this.setState({ aiAnswer: this.aiTextAccumulator }); + } + } componentDidMount() { this.handleLookUp(); } @@ -89,7 +113,6 @@ class PopupDict extends React.Component { (item) => item.key === "custom-ai-dict-plugin" ); if (!plugin) return; - let isFirst = true; let targetLang = this.state.dictTarget || ConfigService.getReaderConfig("dictTarget") || @@ -102,7 +125,9 @@ class PopupDict extends React.Component { systemPrompt = systemPrompt.replace("{word}", text); systemPrompt = systemPrompt.replace("{to}", targetLang); let config: any = plugin.config || {}; + this.aiTextAccumulator = ""; this.setState({ aiAnswer: "", isAiWaiting: true }); + this.startUpdateInterval(); await chatStream( config.endpoint, config.providerId, @@ -112,24 +137,18 @@ class PopupDict extends React.Component { [], (result) => { if (result && result.done) { - this.setState({ isAiWaiting: false }); return; } if (result && result.text) { - if (isFirst) { - this.setState({ - aiAnswer: result.text, - isAiWaiting: false, - }); - isFirst = false; - } else { - this.setState({ - aiAnswer: this.state.aiAnswer + result.text, - }); + if (!this.aiTextAccumulator) { + this.setState({ isAiWaiting: false }); } + this.aiTextAccumulator += result.text; } } ); + this.stopUpdateInterval(); + this.aiTextAccumulator = ""; this.setState({ isAiWaiting: false, dictText: " " }); return; } else if ( @@ -216,8 +235,9 @@ class PopupDict extends React.Component { }; handleDictionaryStream = async (text: string, isFullAnalysis: boolean) => { try { + this.aiTextAccumulator = ""; this.setState({ aiAnswer: "", isAiWaiting: true }); - let isFirst = true; + this.startUpdateInterval(); let res = await getDictionaryStream( text, "auto", @@ -226,19 +246,21 @@ class PopupDict extends React.Component { isFullAnalysis, (result) => { if (result && result.text) { - if (isFirst) { - this.setState({ aiAnswer: result.text, isAiWaiting: false }); - isFirst = false; - } else { - this.setState({ aiAnswer: this.state.aiAnswer + result.text }); + if (!this.aiTextAccumulator) { + this.setState({ isAiWaiting: false }); } + this.aiTextAccumulator += result.text; } } ); + this.stopUpdateInterval(); + this.aiTextAccumulator = ""; if (res && res.done) { this.setState({ isAiWaiting: false }); } } catch (error) { + this.stopUpdateInterval(); + this.aiTextAccumulator = ""; this.setState({ isAiWaiting: false }); console.error(error); } diff --git a/src/components/popups/popupTrans/component.tsx b/src/components/popups/popupTrans/component.tsx index ea106f82..6363ee03 100644 --- a/src/components/popups/popupTrans/component.tsx +++ b/src/components/popups/popupTrans/component.tsx @@ -14,6 +14,9 @@ import { chatStream } from "../../../utils/request/common"; import { getIframeDoc } from "../../../utils/reader/docUtil"; declare var window: any; class PopupTrans extends React.Component { + private textAccumulator: string = ""; + private updateInterval: ReturnType | null = null; + constructor(props: PopupTransProps) { super(props); this.state = { @@ -26,6 +29,27 @@ class PopupTrans extends React.Component { isFinishOutput: false, }; } + + private startUpdateInterval() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + } + this.updateInterval = setInterval(() => { + if (this.textAccumulator) { + this.setState({ translatedText: this.textAccumulator }); + } + }, 150); + } + + private stopUpdateInterval() { + if (this.updateInterval) { + clearInterval(this.updateInterval); + this.updateInterval = null; + } + if (this.textAccumulator) { + this.setState({ translatedText: this.textAccumulator }); + } + } async componentDidMount() { let originalText = this.props.originalText.replace(/(\r\n|\n|\r)/gm, ""); this.setState({ originalText: originalText }); @@ -107,7 +131,6 @@ class PopupTrans extends React.Component { if (!plugin) { return; } - let isFirst = true; let targetLang = ConfigService.getReaderConfig("transTarget") || getDefaultTransTarget(plugin.langList); @@ -124,6 +147,8 @@ class PopupTrans extends React.Component { systemPrompt = systemPrompt.replace("{to}", targetLang); systemPrompt = systemPrompt.replace("{text}", text); let config: any = plugin.config || {}; + this.textAccumulator = ""; + this.startUpdateInterval(); await chatStream( config.endpoint, config.providerId, @@ -133,23 +158,15 @@ class PopupTrans extends React.Component { [], (result) => { if (result && result.done) { - this.setState({ isFinishOutput: true }); return; } if (result && result.text) { - if (isFirst) { - this.setState({ - translatedText: result.text, - }); - isFirst = false; - } else { - this.setState({ - translatedText: this.state.translatedText + result.text, - }); - } + this.textAccumulator += result.text; } } ); + this.stopUpdateInterval(); + this.textAccumulator = ""; this.setState({ isFinishOutput: true }); } else if ( this.props.isAuthed && @@ -165,13 +182,14 @@ class PopupTrans extends React.Component { if (!plugin) { return; } - let isFirst = true; let targetLang = ConfigService.getReaderConfig("transTarget") || getDefaultTransTarget(plugin.langList); if (targetLang === "Traditional Chinese") { targetLang = "繁体中文"; } + this.textAccumulator = ""; + this.startUpdateInterval(); await getTransStream( text, ConfigService.getReaderConfig("transSource") || "Automatic", @@ -179,23 +197,15 @@ class PopupTrans extends React.Component { getDefaultTransTarget(plugin.langList), (result) => { if (result && result.done) { - this.setState({ isFinishOutput: true }); return; } if (result && result.text) { - if (isFirst) { - this.setState({ - translatedText: result.text, - }); - isFirst = false; - } else { - this.setState({ - translatedText: this.state.translatedText + result.text, - }); - } + this.textAccumulator += result.text; } } ); + this.stopUpdateInterval(); + this.textAccumulator = ""; this.setState({ isFinishOutput: true }); } }; diff --git a/src/utils/request/reader.ts b/src/utils/request/reader.ts index 92ea6c2b..1e0eba9e 100644 --- a/src/utils/request/reader.ts +++ b/src/utils/request/reader.ts @@ -34,21 +34,6 @@ export const getTransStream = async ( ); return result; }; -export const getSummaryStream = async ( - text: string, - to: string, - onMessage: (result) => void -) => { - let readerRequest = await getReaderRequest(); - let result = await readerRequest.getSummaryFetch( - { - text, - to, - }, - onMessage - ); - return result; -}; export const getAnswerStream = async ( text: string, question: string,