feat: implement update intervals for text accumulation in PopupAssist, PopupDict, and PopupTrans components

This commit is contained in:
troyeguo
2026-05-02 15:57:28 +08:00
parent d99b127c52
commit 96b1e781ee
4 changed files with 120 additions and 93 deletions

View File

@@ -18,6 +18,8 @@ class PopupAssist extends React.Component<PopupAssistProps, PopupAssistState> {
private chatBoxRef: React.RefObject<HTMLDivElement>;
private textareaRef: React.RefObject<HTMLTextAreaElement>;
private singleLineScrollHeight: number = 0;
private answerTextAccumulator: string = "";
private updateInterval: ReturnType<typeof setInterval> | null = null;
constructor(props: PopupAssistProps) {
super(props);
@@ -35,6 +37,30 @@ class PopupAssist extends React.Component<PopupAssistProps, PopupAssistState> {
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<PopupAssistProps, PopupAssistState> {
if (!plugin) {
return;
}
let isFirst = true;
let systemPrompt =
ConfigService.getReaderConfig("aiAssistancePrompt") ||
KookitConfig.DefaultPrompts.aiAssistance;
@@ -153,6 +178,8 @@ class PopupAssist extends React.Component<PopupAssistProps, PopupAssistState> {
if (!currentQuestion) {
return;
}
this.answerTextAccumulator = "";
this.startUpdateInterval();
await chatStream(
config.endpoint,
config.providerId,
@@ -165,25 +192,21 @@ class PopupAssist extends React.Component<PopupAssistProps, PopupAssistState> {
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<PopupAssistProps, PopupAssistState> {
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<PopupAssistProps, PopupAssistState> {
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<PopupAssistProps, PopupAssistState> {
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<PopupAssistProps, PopupAssistState> {
...this.state.askHistory,
{
role: "assistant",
content: this.state.answer,
content: finalAnswer,
},
],
answer: "",
@@ -261,7 +278,7 @@ class PopupAssist extends React.Component<PopupAssistProps, PopupAssistState> {
...this.state.chatHistory,
{
role: "assistant",
content: this.state.answer,
content: finalAnswer,
},
],
answer: "",
@@ -270,13 +287,6 @@ class PopupAssist extends React.Component<PopupAssistProps, PopupAssistState> {
});
}
}
// if (res.code === 20006) {
// this.setState({
// isWaiting: false,
// answer: "",
// question: "",
// });
// }
if (ConfigService.getReaderConfig("isManualScroll") !== "yes") {
this.scrollToBottom();
}

View File

@@ -22,6 +22,9 @@ import { marked } from "marked";
import { getIframeDoc } from "../../../utils/reader/docUtil";
declare var window: any;
class PopupDict extends React.Component<PopupDictProps, PopupDictState> {
private aiTextAccumulator: string = "";
private updateInterval: ReturnType<typeof setInterval> | null = null;
constructor(props: PopupDictProps) {
super(props);
this.state = {
@@ -37,6 +40,27 @@ class PopupDict extends React.Component<PopupDictProps, PopupDictState> {
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<PopupDictProps, PopupDictState> {
(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<PopupDictProps, PopupDictState> {
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<PopupDictProps, PopupDictState> {
[],
(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<PopupDictProps, PopupDictState> {
};
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<PopupDictProps, PopupDictState> {
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);
}

View File

@@ -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<PopupTransProps, PopupTransState> {
private textAccumulator: string = "";
private updateInterval: ReturnType<typeof setInterval> | null = null;
constructor(props: PopupTransProps) {
super(props);
this.state = {
@@ -26,6 +29,27 @@ class PopupTrans extends React.Component<PopupTransProps, PopupTransState> {
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<PopupTransProps, PopupTransState> {
if (!plugin) {
return;
}
let isFirst = true;
let targetLang =
ConfigService.getReaderConfig("transTarget") ||
getDefaultTransTarget(plugin.langList);
@@ -124,6 +147,8 @@ class PopupTrans extends React.Component<PopupTransProps, PopupTransState> {
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<PopupTransProps, PopupTransState> {
[],
(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<PopupTransProps, PopupTransState> {
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<PopupTransProps, PopupTransState> {
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 });
}
};

View File

@@ -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,