diff --git a/addon/locale/en-US/addon.ftl b/addon/locale/en-US/addon.ftl index c435515e..2ad80bd3 100644 --- a/addon/locale/en-US/addon.ftl +++ b/addon/locale/en-US/addon.ftl @@ -105,3 +105,5 @@ itemmenu-retranslateAbstract-label=Retranslate Abstract field-titleTranslation=Title Translation field-abstractTranslation=Abstract Translation + +status-translating=Translating... diff --git a/addon/locale/it-IT/addon.ftl b/addon/locale/it-IT/addon.ftl index 102ca352..3f9f68d1 100644 --- a/addon/locale/it-IT/addon.ftl +++ b/addon/locale/it-IT/addon.ftl @@ -104,3 +104,5 @@ itemmenu-retranslateAbstract-label=Ritraduci Abstract field-titleTranslation=Traduzione del titolo field-abstractTranslation=Traduzione dell'Abstract + +status-translating=Traduzione in corso... diff --git a/addon/locale/zh-CN/addon.ftl b/addon/locale/zh-CN/addon.ftl index 4d5fff12..ffdf95ec 100644 --- a/addon/locale/zh-CN/addon.ftl +++ b/addon/locale/zh-CN/addon.ftl @@ -104,3 +104,5 @@ itemmenu-retranslateAbstract-label=重新翻译摘要 field-titleTranslation=标题翻译 field-abstractTranslation=摘要翻译 + +status-translating=正在翻译... diff --git a/addon/prefs.js b/addon/prefs.js index cd9970ec..bd83cd64 100644 --- a/addon/prefs.js +++ b/addon/prefs.js @@ -52,6 +52,7 @@ pref( "__prefsPrefix__.chatGPT.prompt", "As an academic expert with specialized knowledge in various fields, please provide a proficient and precise translation translation from ${langFrom} to ${langTo} of the academic text enclosed in 🔤. It is crucial to maintaining the original phrase or sentence and ensure accuracy while utilizing the appropriate language. The text is as follows: 🔤 ${sourceText} 🔤 Please provide the translated result without any additional explanation and remove 🔤.", ); +pref("__prefsPrefix__.chatGPT.stream", true); pref("__prefsPrefix__.azureGPT.endPoint", ""); pref("__prefsPrefix__.azureGPT.model", ""); pref("__prefsPrefix__.azureGPT.apiVersion", "2023-05-15"); @@ -60,6 +61,7 @@ pref( "__prefsPrefix__.azureGPT.prompt", "As an academic expert with specialized knowledge in various fields, please provide a proficient and precise translation translation from ${langFrom} to ${langTo} of the academic text enclosed in 🔤. It is crucial to maintaining the original phrase or sentence and ensure accuracy while utilizing the appropriate language. The text is as follows: 🔤 ${sourceText} 🔤 Please provide the translated result without any additional explanation and remove 🔤.", ); +pref("__prefsPrefix__.azureGPT.stream", true); pref( "__prefsPrefix__.gemini.endPoint", "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest", diff --git a/src/modules/services/gpt.ts b/src/modules/services/gpt.ts index eb027bda..c82c2ee1 100644 --- a/src/modules/services/gpt.ts +++ b/src/modules/services/gpt.ts @@ -1,5 +1,6 @@ import { TranslateTask, TranslateTaskProcessor } from "../../utils/task"; import { getPref } from "../../utils/prefs"; +import { getString } from "../../utils/locale"; const gptTranslate = async function ( apiURL: string, @@ -7,6 +8,7 @@ const gptTranslate = async function ( temperature: number, prefix: string, data: Required, + stream?: boolean ) { function transformContent( langFrom: string, @@ -19,6 +21,79 @@ const gptTranslate = async function ( .replaceAll("${sourceText}", sourceText); } + const streamMode = stream ?? true; + + //It takes some time to translate, so set the text to "Translating" before the request + if (streamMode === false) { + data.result = getString('status-translating'); + addon.hooks.onReaderPopupRefresh(); + addon.hooks.onReaderTabPanelRefresh(); + } + + /** + * The requestObserver callback, under streaming mode + * @param xmlhttp + */ + const streamCallback = (xmlhttp: XMLHttpRequest) => { + let preLength = 0; + let result = ""; + xmlhttp.onprogress = (e: any) => { + // Only concatenate the new strings + const newResponse = e.target.response.slice(preLength); + const dataArray = newResponse.split("data: "); + + for (const data of dataArray) { + try { + const obj = JSON.parse(data); + const choice = obj.choices[0]; + if (choice.finish_reason) { + break; + } + result += choice.delta.content || ""; + } catch { + continue; + } + } + + // Clear timeouts caused by stream transfers + if (e.target.timeout) { + e.target.timeout = 0; + } + + // Remove \n\n from the beginning of the data + data.result = result.replace(/^\n\n/, ""); + preLength = e.target.response.length; + + if (data.type === "text") { + addon.hooks.onReaderPopupRefresh(); + addon.hooks.onReaderTabPanelRefresh(); + } + }; + } + + /** + * The requestObserver callback, under non-streaming mode + * @param xmlhttp + */ + const nonStreamCallback = (xmlhttp: XMLHttpRequest) => { + // Non-streaming logic: Handle the complete response at once + xmlhttp.onload = () => { + // console.debug("GPT response received"); + try { + const responseObj = JSON.parse(xmlhttp.responseText); + const resultContent = responseObj.choices[0].message.content; + data.result = resultContent.replace(/^\n\n/, ""); + } catch (error) { + // throw `Failed to parse response: ${error}`; + return; + } + + // Trigger UI updates after receiving the full response + addon.hooks.onReaderPopupRefresh(); + addon.hooks.onReaderTabPanelRefresh(); + }; + } + const xhr = await Zotero.HTTP.request("POST", apiURL, { headers: { "Content-Type": "application/json", @@ -34,44 +109,15 @@ const gptTranslate = async function ( }, ], temperature: temperature, - stream: true, + stream: streamMode, }), responseType: "text", requestObserver: (xmlhttp: XMLHttpRequest) => { - let preLength = 0; - let result = ""; - xmlhttp.onprogress = (e: any) => { - // Only concatenate the new strings - const newResponse = e.target.response.slice(preLength); - const dataArray = newResponse.split("data: "); - - for (const data of dataArray) { - try { - const obj = JSON.parse(data); - const choice = obj.choices[0]; - if (choice.finish_reason) { - break; - } - result += choice.delta.content || ""; - } catch { - continue; - } - } - - // Clear timeouts caused by stream transfers - if (e.target.timeout) { - e.target.timeout = 0; - } - - // Remove \n\n from the beginning of the data - data.result = result.replace(/^\n\n/, ""); - preLength = e.target.response.length; - - if (data.type === "text") { - addon.hooks.onReaderPopupRefresh(); - addon.hooks.onReaderTabPanelRefresh(); - } - }; + if (streamMode) { + streamCallback(xmlhttp); + } else { + nonStreamCallback(xmlhttp); + } }, }); if (xhr?.status !== 200) { @@ -84,8 +130,9 @@ export const chatGPT = async function (data) { const apiURL = getPref("chatGPT.endPoint") as string; const model = getPref("chatGPT.model") as string; const temperature = parseFloat(getPref("chatGPT.temperature") as string); + const stream = getPref("chatGPT.stream") as boolean; - return await gptTranslate(apiURL, model, temperature, "chatGPT", data); + return await gptTranslate(apiURL, model, temperature, "chatGPT", data, stream); }; export const azureGPT = async function (data) { @@ -93,9 +140,11 @@ export const azureGPT = async function (data) { const apiVersion = getPref("azureGPT.apiVersion"); const model = getPref("azureGPT.model") as string; const temperature = parseFloat(getPref("azureGPT.temperature") as string); + const stream = getPref("azureGPT.stream") as boolean; + const apiURL = new URL(endPoint); apiURL.pathname = `/openai/deployments/${model}/chat/completions`; apiURL.search = `api-version=${apiVersion}`; - return await gptTranslate(apiURL.href, model, temperature, "azureGPT", data); + return await gptTranslate(apiURL.href, model, temperature, "azureGPT", data, stream); }; diff --git a/src/modules/settings/gpt.ts b/src/modules/settings/gpt.ts index 4a922d34..891c7f06 100644 --- a/src/modules/settings/gpt.ts +++ b/src/modules/settings/gpt.ts @@ -13,6 +13,7 @@ async function gptStatusCallback( temperature: parseFloat(getPref(`${prefix}.temperature`) as string), prompt: getPref(`${prefix}.prompt`), apiVersion: getPref("azureGPT.apiVersion"), + stream: getPref(`${prefix}.stream`), }; dialog @@ -133,6 +134,25 @@ async function gptStatusCallback( "data-prop": "value", }, }, + { + tag: "label", + namespace: "html", + attributes: { + for: "stream", + }, + properties: { + innerHTML: 'Stream', + }, + }, + { + tag: "input", + id: "stream", + attributes: { + type: "checkbox", + "data-bind": "stream", + "data-prop": "checked", + }, + }, ], }, false, @@ -156,6 +176,7 @@ async function gptStatusCallback( setPref(`${prefix}.endPoint`, dialogData.endPoint); setPref(`${prefix}.model`, dialogData.model); setPref(`${prefix}.prompt`, dialogData.prompt); + setPref(`${prefix}.stream`, dialogData.stream); setPref("azureGPT.apiVersion", dialogData.apiVersion); } break;