diff --git a/src/renderer/src/assets/styles/color.scss b/src/renderer/src/assets/styles/color.scss index 897715b2b6..278d732a4e 100644 --- a/src/renderer/src/assets/styles/color.scss +++ b/src/renderer/src/assets/styles/color.scss @@ -26,6 +26,7 @@ --color-primary-mute: #00b96b33; --color-text: var(--color-text-1); + --color-text-secondary: rgba(235, 235, 245, 0.7); --color-icon: #ffffff99; --color-icon-white: #ffffff; --color-border: #ffffff19; @@ -98,6 +99,7 @@ --color-primary-mute: #00b96b33; --color-text: var(--color-text-1); + --color-text-secondary: rgba(0, 0, 0, 0.75); --color-icon: #00000099; --color-icon-white: #000000; --color-border: #00000019; diff --git a/src/renderer/src/config/translate.ts b/src/renderer/src/config/translate.ts index fa35acea1a..b8e5cd0b4e 100644 --- a/src/renderer/src/config/translate.ts +++ b/src/renderer/src/config/translate.ts @@ -1,65 +1,127 @@ import i18n from '@renderer/i18n' -export const TranslateLanguageOptions = [ +export interface TranslateLanguageOption { + value: string + langCode?: string + label: string + emoji: string +} + +export const TranslateLanguageOptions: TranslateLanguageOption[] = [ { value: 'english', + langCode: 'en-us', label: i18n.t('languages.english'), emoji: '🇬🇧' }, { value: 'chinese', + langCode: 'zh-cn', label: i18n.t('languages.chinese'), emoji: '🇨🇳' }, { value: 'chinese-traditional', + langCode: 'zh-tw', label: i18n.t('languages.chinese-traditional'), emoji: '🇭🇰' }, { value: 'japanese', + langCode: 'ja-jp', label: i18n.t('languages.japanese'), emoji: '🇯🇵' }, { value: 'korean', + langCode: 'ko-kr', label: i18n.t('languages.korean'), emoji: '🇰🇷' }, - { - value: 'russian', - label: i18n.t('languages.russian'), - emoji: '🇷🇺' - }, - { - value: 'spanish', - label: i18n.t('languages.spanish'), - emoji: '🇪🇸' - }, + { value: 'french', + langCode: 'fr-fr', label: i18n.t('languages.french'), emoji: '🇫🇷' }, + { + value: 'german', + langCode: 'de-de', + label: i18n.t('languages.german'), + emoji: '🇩🇪' + }, { value: 'italian', + langCode: 'it-it', label: i18n.t('languages.italian'), emoji: '🇮🇹' }, + { + value: 'spanish', + langCode: 'es-es', + label: i18n.t('languages.spanish'), + emoji: '🇪🇸' + }, { value: 'portuguese', + langCode: 'pt-pt', label: i18n.t('languages.portuguese'), emoji: '🇵🇹' }, + { + value: 'russian', + langCode: 'ru-ru', + label: i18n.t('languages.russian'), + emoji: '🇷🇺' + }, + { + value: 'polish', + langCode: 'pl-pl', + label: i18n.t('languages.polish'), + emoji: '🇵🇱' + }, { value: 'arabic', + langCode: 'ar-ar', label: i18n.t('languages.arabic'), emoji: '🇸🇦' }, { - value: 'german', - label: i18n.t('languages.german'), - emoji: '🇩🇪' + value: 'turkish', + langCode: 'tr-tr', + label: i18n.t('languages.turkish'), + emoji: '🇹🇷' + }, + { + value: 'thai', + langCode: 'th-th', + label: i18n.t('languages.thai'), + emoji: '🇹🇭' + }, + { + value: 'vietnamese', + langCode: 'vi-vn', + label: i18n.t('languages.vietnamese'), + emoji: '🇻🇳' + }, + { + value: 'indonesian', + langCode: 'id-id', + label: i18n.t('languages.indonesian'), + emoji: '🇮🇩' + }, + { + value: 'urdu', + langCode: 'ur-pk', + label: i18n.t('languages.urdu'), + emoji: '🇵🇰' + }, + { + value: 'malay', + langCode: 'ms-my', + label: i18n.t('languages.malay'), + emoji: '🇲🇾' } ] diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 7bc1eef756..3fd4886f97 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -586,7 +586,14 @@ "korean": "Korean", "portuguese": "Portuguese", "russian": "Russian", - "spanish": "Spanish" + "spanish": "Spanish", + "polish": "Polish", + "turkish": "Turkish", + "thai": "Thai", + "vietnamese": "Vietnamese", + "indonesian": "Indonesian", + "urdu": "Urdu", + "malay": "Malay" }, "lmstudio": { "keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.", @@ -1794,6 +1801,8 @@ }, "translate": { "any.language": "Any language", + "target_language": "Target Language", + "alter_language": "Alternative Language", "button.translate": "Translate", "close": "Close", "closed": "Translation closed", @@ -1873,6 +1882,9 @@ "esc_stop": "Esc: Stop", "c_copy": "C: Copy", "r_regenerate": "R: Regenerate" + }, + "translate": { + "smart_translate_tips": "Smart Translation: Content will be translated to the target language first; content already in the target language will be translated to the alternative language" } }, "settings": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index f3658e81da..92488e5bc0 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -586,7 +586,14 @@ "korean": "韓国語", "portuguese": "ポルトガル語", "russian": "ロシア語", - "spanish": "スペイン語" + "spanish": "スペイン語", + "polish": "ポーランド語", + "turkish": "トルコ語", + "thai": "タイ語", + "vietnamese": "ベトナム語", + "indonesian": "インドネシア語", + "urdu": "ウルドゥー語", + "malay": "マレー語" }, "lmstudio": { "keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)", @@ -1794,6 +1801,8 @@ }, "translate": { "any.language": "任意の言語", + "target_language": "目標言語", + "alter_language": "備用言語", "button.translate": "翻訳", "close": "閉じる", "closed": "翻訳は閉じられました", @@ -1873,6 +1882,9 @@ "esc_stop": "Escで停止", "c_copy": "Cでコピー", "r_regenerate": "Rで再生成" + }, + "translate": { + "smart_translate_tips": "スマート翻訳:内容は優先的に目標言語に翻訳されます。すでに目標言語の場合は、備用言語に翻訳されます。" } }, "settings": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index e03383ccd4..942fc2f2dc 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -586,7 +586,14 @@ "korean": "Корейский", "portuguese": "Португальский", "russian": "Русский", - "spanish": "Испанский" + "spanish": "Испанский", + "polish": "Польский", + "turkish": "Туркменский", + "thai": "Тайский", + "vietnamese": "Вьетнамский", + "indonesian": "Индонезийский", + "urdu": "Урду", + "malay": "Малайзийский" }, "lmstudio": { "keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.", @@ -1794,6 +1801,8 @@ }, "translate": { "any.language": "Любой язык", + "target_language": "Целевой язык", + "alter_language": "Альтернативный язык", "button.translate": "Перевести", "close": "Закрыть", "closed": "Перевод закрыт", @@ -1873,6 +1882,9 @@ "esc_stop": "Esc - остановить", "c_copy": "C - копировать", "r_regenerate": "R - перегенерировать" + }, + "translate": { + "smart_translate_tips": "Смарт-перевод: содержимое будет переведено на целевой язык; содержимое уже на целевом языке будет переведено на альтернативный язык" } }, "settings": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 85ec5109ad..437d8db25c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -586,7 +586,14 @@ "korean": "韩文", "portuguese": "葡萄牙文", "russian": "俄文", - "spanish": "西班牙文" + "spanish": "西班牙文", + "polish": "波兰文", + "turkish": "土耳其文", + "thai": "泰文", + "vietnamese": "越南文", + "indonesian": "印尼文", + "urdu": "乌尔都文", + "malay": "马来文" }, "lmstudio": { "keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)", @@ -1794,6 +1801,8 @@ }, "translate": { "any.language": "任意语言", + "target_language": "目标语言", + "alter_language": "备用语言", "button.translate": "翻译", "close": "关闭", "closed": "翻译已关闭", @@ -1873,6 +1882,9 @@ "esc_stop": "Esc 停止", "c_copy": "C 复制", "r_regenerate": "R 重新生成" + }, + "translate": { + "smart_translate_tips": "智能翻译:内容将优先翻译为目标语言;内容已是目标语言的,将翻译为备选语言" } }, "settings": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 91c3c970a6..3546889bed 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -586,7 +586,14 @@ "korean": "韓文", "portuguese": "葡萄牙文", "russian": "俄文", - "spanish": "西班牙文" + "spanish": "西班牙文", + "polish": "波蘭文", + "turkish": "土耳其文", + "thai": "泰文", + "vietnamese": "越南文", + "indonesian": "印尼文", + "urdu": "烏爾都文", + "malay": "馬來文" }, "lmstudio": { "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)", @@ -1794,6 +1801,8 @@ }, "translate": { "any.language": "任意語言", + "target_language": "目標語言", + "alter_language": "備用語言", "button.translate": "翻譯", "close": "關閉", "closed": "翻譯已關閉", @@ -1873,6 +1882,9 @@ "esc_stop": "Esc 停止", "c_copy": "C 複製", "r_regenerate": "R 重新生成" + }, + "translate": { + "smart_translate_tips": "智能翻譯:內容將優先翻譯為目標語言;內容已是目標語言的,將翻譯為備用語言" } }, "settings": { diff --git a/src/renderer/src/utils/translate.ts b/src/renderer/src/utils/translate.ts index b40c8f2ae7..bd50733482 100644 --- a/src/renderer/src/utils/translate.ts +++ b/src/renderer/src/utils/translate.ts @@ -84,7 +84,14 @@ export const detectLanguage = async (inputText: string): Promise => { deu: 'de', ita: 'it', por: 'pt', - eng: 'en' + eng: 'en', + pol: 'pl', + tur: 'tr', + tha: 'th', + vie: 'vi', + ind: 'id', + urd: 'ur', + zsm: 'ms' } code = isoMap[iso3] || 'en' } @@ -101,7 +108,14 @@ export const detectLanguage = async (inputText: string): Promise => { it: 'italian', pt: 'portuguese', ar: 'arabic', - en: 'english' + en: 'english', + pl: 'polish', + tr: 'turkish', + th: 'thai', + vi: 'vietnamese', + id: 'indonesian', + ur: 'urdu', + ms: 'malay' } return languageMap[code] || 'english' diff --git a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx index 086d46a68e..3366c76b58 100644 --- a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx +++ b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx @@ -7,7 +7,7 @@ import { IpcChannel } from '@shared/IpcChannel' import { Button, Slider, Tooltip } from 'antd' import { Droplet, Minus, Pin, X } from 'lucide-react' import { DynamicIcon } from 'lucide-react/dynamic' -import { FC, useEffect, useRef, useState } from 'react' +import { FC, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -140,14 +140,14 @@ const SelectionActionApp: FC = () => { setOpacity(value) } - const handleScrollToBottom = () => { + const handleScrollToBottom = useCallback(() => { if (contentElementRef.current && isAutoScrollEnabled.current) { contentElementRef.current.scrollTo({ top: contentElementRef.current.scrollHeight, behavior: 'smooth' }) } - } + }, []) const handleUserScroll = () => { if (!contentElementRef.current) return diff --git a/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx b/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx index 90cce65f14..8a10740b68 100644 --- a/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx +++ b/src/renderer/src/windows/selection/action/components/ActionGeneral.tsx @@ -3,29 +3,21 @@ import CopyButton from '@renderer/components/CopyButton' import { useTopicMessages } from '@renderer/hooks/useMessageOperations' import { useSettings } from '@renderer/hooks/useSettings' import MessageContent from '@renderer/pages/home/Messages/MessageContent' -import { fetchChatCompletion } from '@renderer/services/ApiService' import { getAssistantById, getDefaultAssistant, getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantService' -import { getAssistantMessage, getUserMessage } from '@renderer/services/MessagesService' -import store from '@renderer/store' -import { updateOneBlock, upsertManyBlocks, upsertOneBlock } from '@renderer/store/messageBlock' -import { newMessagesActions } from '@renderer/store/newMessage' import { Assistant, Topic } from '@renderer/types' -import { Chunk, ChunkType } from '@renderer/types/chunk' -import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage' import type { ActionItem } from '@renderer/types/selectionTypes' import { abortCompletion } from '@renderer/utils/abortController' -import { isAbortError } from '@renderer/utils/error' -import { createMainTextBlock } from '@renderer/utils/messageUtils/create' import { ChevronDown } from 'lucide-react' import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import { processMessages } from './ActionUtils' import WindowFooter from './WindowFooter' interface Props { @@ -101,111 +93,41 @@ const ActionGeneral: FC = React.memo(({ action, scrollToBottom }) => { promptContentRef.current = userContent }, [action, language]) - const allMessages = useTopicMessages(topicRef.current?.id || '') - - const fetchResult = useCallback(async () => { - if (!assistantRef.current || !topicRef.current) return - - try { - const { message: userMessage, blocks: userBlocks } = getUserMessage({ - assistant: assistantRef.current, - topic: topicRef.current, - content: promptContentRef.current - }) - - askId.current = userMessage.id - - store.dispatch(newMessagesActions.addMessage({ topicId: topicRef.current.id, message: userMessage })) - store.dispatch(upsertManyBlocks(userBlocks)) - - let blockId: string | null = null - let blockContent: string = '' - - const assistantMessage = getAssistantMessage({ - assistant: assistantRef.current, - topic: topicRef.current - }) - store.dispatch( - newMessagesActions.addMessage({ - topicId: topicRef.current.id, - message: assistantMessage - }) - ) - - await fetchChatCompletion({ - messages: [userMessage], - assistant: assistantRef.current, - onChunkReceived: (chunk: Chunk) => { - switch (chunk.type) { - case ChunkType.THINKING_DELTA: - case ChunkType.THINKING_COMPLETE: - //TODO - break - case ChunkType.TEXT_DELTA: - { - setIsContented(true) - blockContent += chunk.text - if (!blockId) { - const block = createMainTextBlock(assistantMessage.id, chunk.text, { - status: MessageBlockStatus.STREAMING - }) - blockId = block.id - store.dispatch( - newMessagesActions.updateMessage({ - topicId: topicRef.current!.id, - messageId: assistantMessage.id, - updates: { blockInstruction: { id: block.id } } - }) - ) - store.dispatch(upsertOneBlock(block)) - } else { - store.dispatch(updateOneBlock({ id: blockId, changes: { content: blockContent } })) - } - - scrollToBottom?.() - } - break - case ChunkType.TEXT_COMPLETE: - { - blockId && - store.dispatch( - updateOneBlock({ - id: blockId, - changes: { status: MessageBlockStatus.SUCCESS } - }) - ) - store.dispatch( - newMessagesActions.updateMessage({ - topicId: topicRef.current!.id, - messageId: assistantMessage.id, - updates: { status: AssistantMessageStatus.SUCCESS } - }) - ) - setContentToCopy(chunk.text) - } - break - case ChunkType.BLOCK_COMPLETE: - case ChunkType.ERROR: - setIsLoading(false) - break - } - } - }) - } catch (err) { - if (isAbortError(err)) return - setIsLoading(false) - setError(err instanceof Error ? err.message : 'An error occurred') - console.error('Error fetching result:', err) + const fetchResult = useCallback(() => { + const setAskId = (id: string) => { + askId.current = id } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + const onStream = () => { + setIsContented(true) + scrollToBottom?.() + } + const onFinish = (content: string) => { + setContentToCopy(content) + setIsLoading(false) + } + const onError = (error: Error) => { + setIsLoading(false) + setError(error.message) + } + + if (!assistantRef.current || !topicRef.current) return + processMessages( + assistantRef.current, + topicRef.current, + promptContentRef.current, + setAskId, + onStream, + onFinish, + onError + ) + }, [scrollToBottom]) useEffect(() => { - if (assistantRef.current && topicRef.current) { - fetchResult() - } + fetchResult() }, [fetchResult]) + const allMessages = useTopicMessages(topicRef.current?.id || '') + // Memoize the messages to prevent unnecessary re-renders const messageContent = useMemo(() => { const assistantMessages = allMessages.filter((message) => message.role === 'assistant') diff --git a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx index 1d36c18768..106b316668 100644 --- a/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx +++ b/src/renderer/src/windows/selection/action/components/ActionTranslate.tsx @@ -1,119 +1,229 @@ import { LoadingOutlined } from '@ant-design/icons' import CopyButton from '@renderer/components/CopyButton' -import { TranslateLanguageOptions } from '@renderer/config/translate' +import { TranslateLanguageOptions, translateLanguageOptions } from '@renderer/config/translate' import db from '@renderer/databases' -import { useDefaultModel } from '@renderer/hooks/useAssistant' -import { fetchTranslate } from '@renderer/services/ApiService' -import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' -import { Assistant } from '@renderer/types' +import { useTopicMessages } from '@renderer/hooks/useMessageOperations' +import { useSettings } from '@renderer/hooks/useSettings' +import MessageContent from '@renderer/pages/home/Messages/MessageContent' +import { + getDefaultAssistant, + getDefaultModel, + getDefaultTopic, + getTranslateModel +} from '@renderer/services/AssistantService' +import { Assistant, Topic } from '@renderer/types' import type { ActionItem } from '@renderer/types/selectionTypes' import { runAsyncFunction } from '@renderer/utils' -import { Select, Space } from 'antd' -import { isEmpty } from 'lodash' -import { ChevronDown } from 'lucide-react' -import { FC, useCallback, useEffect, useRef, useState } from 'react' +import { abortCompletion } from '@renderer/utils/abortController' +import { detectLanguage } from '@renderer/utils/translate' +import { Select, Space, Tooltip } from 'antd' +import { ArrowRightFromLine, ArrowRightToLine, ChevronDown, CircleHelp, Globe } from 'lucide-react' +import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import { processMessages } from './ActionUtils' import WindowFooter from './WindowFooter' - interface Props { action: ActionItem scrollToBottom: () => void } -let _targetLanguage = 'chinese' - const ActionTranslate: FC = ({ action, scrollToBottom }) => { const { t } = useTranslation() + const { translateModelPrompt, language } = useSettings() - const [targetLanguage, setTargetLanguage] = useState(_targetLanguage) - const { translateModel } = useDefaultModel() + const [targetLanguage, setTargetLanguage] = useState('') + const [alterLanguage, setAlterLanguage] = useState('') - const [isLangSelectDisabled, setIsLangSelectDisabled] = useState(false) - const [showOriginal, setShowOriginal] = useState(false) - - const [result, setResult] = useState('') - const [contentToCopy, setContentToCopy] = useState('') const [error, setError] = useState('') + const [showOriginal, setShowOriginal] = useState(false) + const [isContented, setIsContented] = useState(false) + const [isLoading, setIsLoading] = useState(true) + const [contentToCopy, setContentToCopy] = useState('') - const translatingRef = useRef(false) - - _targetLanguage = targetLanguage - - const translate = useCallback(async () => { - if (!action.selectedText || !action.selectedText.trim() || !translateModel) return - - if (translatingRef.current) return - - try { - translatingRef.current = true - setError('') - - const targetLang = await db.settings.get({ id: 'translate:target:language' }) - const assistant: Assistant = getDefaultTranslateAssistant( - targetLang?.value || targetLanguage, - action.selectedText - ) - - const onResult = (text: string, isComplete: boolean) => { - setResult(text) - scrollToBottom() - - if (isComplete) { - setContentToCopy(text) - setIsLangSelectDisabled(false) - } - } - - setIsLangSelectDisabled(true) - await fetchTranslate({ content: action.selectedText || '', assistant, onResponse: onResult }) - - translatingRef.current = false - } catch (error: any) { - setError(error?.message || t('error.unknown')) - console.error(error) - } finally { - translatingRef.current = false - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [action, targetLanguage, translateModel]) + // Use useRef for values that shouldn't trigger re-renders + const initialized = useRef(false) + const assistantRef = useRef(null) + const topicRef = useRef(null) + const askId = useRef('') useEffect(() => { runAsyncFunction(async () => { - const targetLang = await db.settings.get({ id: 'translate:target:language' }) - targetLang && setTargetLanguage(targetLang.value) + const biDirectionLangPair = await db.settings.get({ id: 'translate:bidirectional:pair' }) + + let targetLang = '' + let alterLang = '' + + if (!biDirectionLangPair || !biDirectionLangPair.value[0]) { + const lang = TranslateLanguageOptions.find((lang) => lang.langCode?.toLowerCase() === language.toLowerCase()) + if (lang) { + targetLang = lang.value + } else { + targetLang = 'chinese' + } + } else { + targetLang = biDirectionLangPair.value[0] + } + + if (!biDirectionLangPair || !biDirectionLangPair.value[1]) { + alterLang = 'english' + } else { + alterLang = biDirectionLangPair.value[1] + } + + setTargetLanguage(targetLang) + setAlterLanguage(alterLang) }) - }, []) + }, [language]) + + // Initialize values only once when action changes + useEffect(() => { + if (initialized.current || !action.selectedText) return + initialized.current = true + + // Initialize assistant + const currentAssistant = getDefaultAssistant() + const translateModel = getTranslateModel() || getDefaultModel() + + currentAssistant.model = translateModel + currentAssistant.settings = { + temperature: 0.7 + } + + assistantRef.current = currentAssistant + + // Initialize topic + topicRef.current = getDefaultTopic(currentAssistant.id) + }, [action, targetLanguage, translateModelPrompt]) + + const fetchResult = useCallback(async () => { + if (!assistantRef.current || !topicRef.current || !action.selectedText) return + + const setAskId = (id: string) => { + askId.current = id + } + const onStream = () => { + setIsContented(true) + scrollToBottom?.() + } + const onFinish = (content: string) => { + setContentToCopy(content) + setIsLoading(false) + } + const onError = (error: Error) => { + setIsLoading(false) + setError(error.message) + } + + setIsLoading(true) + + const sourceLanguage = await detectLanguage(action.selectedText) + + let translateLang = '' + if (sourceLanguage === targetLanguage) { + translateLang = alterLanguage + } else { + translateLang = targetLanguage + } + + // Initialize prompt content + const userContent = translateModelPrompt + .replaceAll('{{target_language}}', translateLang) + .replaceAll('{{text}}', action.selectedText) + + processMessages(assistantRef.current, topicRef.current, userContent, setAskId, onStream, onFinish, onError) + }, [action, targetLanguage, alterLanguage, translateModelPrompt, scrollToBottom]) useEffect(() => { - translate() - }, [translate]) + fetchResult() + }, [fetchResult]) + + const allMessages = useTopicMessages(topicRef.current?.id || '') + + const messageContent = useMemo(() => { + const assistantMessages = allMessages.filter((message) => message.role === 'assistant') + const lastAssistantMessage = assistantMessages[assistantMessages.length - 1] + return lastAssistantMessage ? : null + }, [allMessages]) + + const handleChangeLanguage = (targetLanguage: string, alterLanguage: string) => { + setTargetLanguage(targetLanguage) + setAlterLanguage(alterLanguage) + + db.settings.put({ id: 'translate:bidirectional:pair', value: [targetLanguage, alterLanguage] }) + } + + const handlePause = () => { + if (askId.current) { + abortCompletion(askId.current) + setIsLoading(false) + } + } + + const handleRegenerate = () => { + setContentToCopy('') + setIsLoading(true) + fetchResult() + } return ( <> - ({ + value: lang.value, + label: ( + + + {lang.emoji} + + {lang.label} + + ) + }))} + onChange={(value) => handleChangeLanguage(value, alterLanguage)} + disabled={isLoading} + /> + + + +