diff --git a/src/renderer/src/config/models.ts b/src/renderer/src/config/models.ts index e97d31f45a..8a8b293373 100644 --- a/src/renderer/src/config/models.ts +++ b/src/renderer/src/config/models.ts @@ -2816,7 +2816,7 @@ export function isNotSupportTemperatureAndTopP(model: Model): boolean { return true } - if (isOpenAIReasoningModel(model) || isOpenAIChatCompletionOnlyModel(model)) { + if (isOpenAIReasoningModel(model) || isOpenAIChatCompletionOnlyModel(model) || isQwenMTModel(model)) { return true } diff --git a/src/renderer/src/config/translate.ts b/src/renderer/src/config/translate.ts index d707fa0431..704565fed5 100644 --- a/src/renderer/src/config/translate.ts +++ b/src/renderer/src/config/translate.ts @@ -1,6 +1,13 @@ import i18n from '@renderer/i18n' import { Language } from '@renderer/types' +export const UNKNOWN: Language = { + value: 'Unknown', + langCode: 'unknown', + label: () => i18n.t('languages.unknown'), + emoji: '🏳️' +} + export const ENGLISH: Language = { value: 'English', langCode: 'en-us', diff --git a/src/renderer/src/hooks/useTranslate.ts b/src/renderer/src/hooks/useTranslate.ts new file mode 100644 index 0000000000..177fe2fa20 --- /dev/null +++ b/src/renderer/src/hooks/useTranslate.ts @@ -0,0 +1,158 @@ +import db from '@renderer/databases' +import { fetchTranslate } from '@renderer/services/ApiService' +import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' +import { loggerService } from '@renderer/services/LoggerService' +import store, { useAppDispatch, useAppSelector } from '@renderer/store' +import { setTranslating as _setTranslating } from '@renderer/store/runtime' +import { setTranslatedContent as _setTranslatedContent } from '@renderer/store/translate' +import { Language, LanguageCode, TranslateAssistant, TranslateHistory } from '@renderer/types' +import { uuid } from '@renderer/utils' +import { t } from 'i18next' +import { throttle } from 'lodash' + +/** + * 翻译页面的核心钩子函数 + * @returns 返回翻译相关的状态和方法 + * - translatedContent: 翻译后的内容 + * - translating: 是否正在翻译 + * - setTranslatedContent: 设置翻译后的内容 + * - setTranslating: 设置翻译状态 + * - translate: 执行翻译操作 + * - saveTranslateHistory: 保存翻译历史 + * - deleteHistory: 删除指定翻译历史 + * - clearHistory: 清空所有翻译历史 + */ +export default function useTranslate() { + const translatedContent = useAppSelector((state) => state.translate.translatedContent) + const translating = useAppSelector((state) => state.runtime.translating) + + const dispatch = useAppDispatch() + const logger = loggerService.withContext('useTranslate') + + const setTranslatedContent = (content: string) => { + dispatch(_setTranslatedContent(content)) + } + + const setTranslating = (translating: boolean) => { + dispatch(_setTranslating(translating)) + } + + /** + * 翻译文本并保存历史记录,包含完整的异常处理,不会抛出异常 + * @param text - 需要翻译的文本 + * @param actualSourceLanguage - 源语言 + * @param actualTargetLanguage - 目标语言 + */ + const translate = async ( + text: string, + actualSourceLanguage: Language, + actualTargetLanguage: Language + ): Promise => { + try { + if (translating) { + return + } + + setTranslating(true) + + let assistant: TranslateAssistant + try { + assistant = getDefaultTranslateAssistant(actualTargetLanguage, text) + } catch (e) { + if (e instanceof Error) { + window.message.error(e.message) + return + } else { + throw e + } + } + + try { + await fetchTranslate({ + content: text, + assistant, + onResponse: throttle(setTranslatedContent, 100) + }) + } catch (e) { + logger.error('Failed to translate text', e as Error) + window.message.error(t('translate.error.failed')) + setTranslating(false) + return + } + + window.message.success(t('translate.complete')) + + try { + const translatedContent = store.getState().translate.translatedContent + await saveTranslateHistory( + text, + translatedContent, + actualSourceLanguage.langCode, + actualTargetLanguage.langCode + ) + } catch (e) { + logger.error('Failed to save translate history', e as Error) + window.message.error(t('translate.history.error.save')) + } + + setTranslating(false) + } catch (e) { + logger.error('Failed to translate', e as Error) + window.message.error(t('translate.error.unknown')) + setTranslating(false) + } + } + + /** + * 保存翻译历史记录到数据库 + * @param sourceText - 原文内容 + * @param targetText - 翻译后的内容 + * @param sourceLanguage - 源语言代码 + * @param targetLanguage - 目标语言代码 + * @returns Promise + */ + const saveTranslateHistory = async ( + sourceText: string, + targetText: string, + sourceLanguage: LanguageCode, + targetLanguage: LanguageCode + ) => { + const history: TranslateHistory = { + id: uuid(), + sourceText, + targetText, + sourceLanguage, + targetLanguage, + createdAt: new Date().toISOString() + } + await db.translate_history.add(history) + } + + /** + * 删除指定的翻译历史记录 + * @param id - 要删除的翻译历史记录ID + * @returns Promise + */ + const deleteHistory = async (id: string) => { + db.translate_history.delete(id) + } + + /** + * 清空所有翻译历史记录 + * @returns Promise + */ + const clearHistory = async () => { + db.translate_history.clear() + } + + return { + translatedContent, + translating, + setTranslatedContent, + setTranslating, + translate, + saveTranslateHistory, + deleteHistory, + clearHistory + } +} diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 99d6a7c421..0fbfa9fcd8 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -913,6 +913,7 @@ "thai": "Thai", "turkish": "Turkish", "ukrainian": "Ukrainian", + "unknown": "unknown", "urdu": "Urdu", "vietnamese": "Vietnamese" }, @@ -3394,6 +3395,7 @@ }, "close": "Close", "closed": "Translation closed", + "complete": "Translation completed", "confirm": { "content": "Translation will replace the original text, continue?", "title": "Translation Confirmation" @@ -3405,13 +3407,17 @@ "empty": "Translation content is empty", "error": { "failed": "Translation failed", - "not_configured": "Translation model is not configured" + "not_configured": "Translation model is not configured", + "unknown": "An unknown error occurred during translation" }, "history": { "clear": "Clear History", "clear_description": "Clear history will delete all translation history, continue?", "delete": "Delete", "empty": "No translation history", + "error": { + "save": "Failed to save translation history" + }, "title": "Translation History" }, "input": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 14a0838b6a..a5918fabde 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -913,6 +913,7 @@ "thai": "タイ語", "turkish": "トルコ語", "ukrainian": "ウクライナ語", + "unknown": "未知", "urdu": "ウルドゥー語", "vietnamese": "ベトナム語" }, @@ -3394,6 +3395,7 @@ }, "close": "閉じる", "closed": "翻訳は閉じられました", + "complete": "翻訳完了", "confirm": { "content": "翻訳すると元のテキストが上書きされます。続行しますか?", "title": "翻訳確認" @@ -3405,13 +3407,17 @@ "empty": "翻訳内容が空です", "error": { "failed": "翻訳に失敗しました", - "not_configured": "翻訳モデルが設定されていません" + "not_configured": "翻訳モデルが設定されていません", + "unknown": "翻訳中に不明なエラーが発生しました" }, "history": { "clear": "履歴をクリア", "clear_description": "履歴をクリアすると、すべての翻訳履歴が削除されます。続行しますか?", "delete": "削除", "empty": "翻訳履歴がありません", + "error": { + "save": "保存翻訳履歴に失敗しました" + }, "title": "翻訳履歴" }, "input": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 09ea99543b..ea2368d3db 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -913,6 +913,7 @@ "thai": "Тайский", "turkish": "Туркменский", "ukrainian": "украинский язык", + "unknown": "неизвестно", "urdu": "Урду", "vietnamese": "Вьетнамский" }, @@ -3394,6 +3395,7 @@ }, "close": "Закрыть", "closed": "Перевод закрыт", + "complete": "перевод завершен", "confirm": { "content": "Перевод заменит исходный текст, продолжить?", "title": "Перевод подтверждение" @@ -3405,13 +3407,17 @@ "empty": "Содержимое перевода пусто", "error": { "failed": "Перевод не удалось", - "not_configured": "Модель перевода не настроена" + "not_configured": "Модель перевода не настроена", + "unknown": "Во время перевода возникла неизвестная ошибка" }, "history": { "clear": "Очистить историю", "clear_description": "Очистка истории удалит все записи переводов. Продолжить?", "delete": "Удалить", "empty": "История переводов отсутствует", + "error": { + "save": "Не удалось сохранить историю переводов" + }, "title": "История переводов" }, "input": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 34c5cc4360..46ed5b2df5 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -913,6 +913,7 @@ "thai": "泰文", "turkish": "土耳其文", "ukrainian": "乌克兰语", + "unknown": "未知", "urdu": "乌尔都文", "vietnamese": "越南文" }, @@ -3394,6 +3395,7 @@ }, "close": "关闭", "closed": "翻译已关闭", + "complete": "翻译完成", "confirm": { "content": "翻译后将覆盖原文,是否继续?", "title": "翻译确认" @@ -3405,13 +3407,17 @@ "empty": "翻译内容为空", "error": { "failed": "翻译失败", - "not_configured": "翻译模型未配置" + "not_configured": "翻译模型未配置", + "unknown": "翻译过程中遇到未知错误" }, "history": { "clear": "清空历史", "clear_description": "清空历史将删除所有翻译历史记录,是否继续?", "delete": "删除", "empty": "暂无翻译历史", + "error": { + "save": "保存翻译历史失败" + }, "title": "翻译历史" }, "input": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index b46e485bdb..11c63797a6 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -913,6 +913,7 @@ "thai": "泰文", "turkish": "土耳其文", "ukrainian": "烏克蘭語", + "unknown": "未知", "urdu": "烏爾都文", "vietnamese": "越南文" }, @@ -3394,6 +3395,7 @@ }, "close": "關閉", "closed": "翻譯已關閉", + "complete": "翻譯完成", "confirm": { "content": "翻譯後將覆蓋原文,是否繼續?", "title": "翻譯確認" @@ -3405,13 +3407,17 @@ "empty": "翻譯內容為空", "error": { "failed": "翻譯失敗", - "not_configured": "翻譯模型未設定" + "not_configured": "翻譯模型未設定", + "unknown": "翻譯過程中遇到未知錯誤" }, "history": { "clear": "清空歷史", "clear_description": "清空歷史將刪除所有翻譯歷史記錄,是否繼續?", "delete": "刪除", "empty": "翻譯歷史為空", + "error": { + "save": "保存翻譯歷史失敗" + }, "title": "翻譯歷史" }, "input": { diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index b69c6406be..da29b4ac57 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -913,6 +913,7 @@ "thai": "Ταϊλανδικά", "turkish": "Τουρκικά", "ukrainian": "ουκρανικά", + "unknown": "Άγνωστο", "urdu": "Ουρντού", "vietnamese": "Βιετναμέζικα" }, @@ -3394,6 +3395,7 @@ }, "close": "Κλείσιμο", "closed": "Η μετάφραση έχει απενεργοποιηθεί", + "complete": "Η μετάφραση ολοκληρώθηκε", "confirm": { "content": "Μετάφραση θα επικαλύψει το αρχικό κείμενο, συνεχίζει;", "title": "Επιβεβαίωση μετάφρασης" @@ -3405,13 +3407,17 @@ "empty": "Το μεταφρασμένο κείμενο είναι κενό", "error": { "failed": "Η μετάφραση απέτυχε", - "not_configured": "Το μοντέλο μετάφρασης δεν είναι ρυθμισμένο" + "not_configured": "Το μοντέλο μετάφρασης δεν είναι ρυθμισμένο", + "unknown": "κατά τη μετάφραση παρουσιάστηκε άγνωστο σφάλμα" }, "history": { "clear": "Καθαρισμός ιστορικού", "clear_description": "Η διαγραφή του ιστορικού θα διαγράψει όλα τα απομνημονεύματα μετάφρασης. Θέλετε να συνεχίσετε;", "delete": "Διαγραφή", "empty": "δεν υπάρχουν απομνημονεύματα μετάφρασης", + "error": { + "save": "Αποτυχία αποθήκευσης του ιστορικού μεταφράσεων" + }, "title": "Ιστορικό μετάφρασης" }, "input": { diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index d45fa94b48..a8d4e2fff1 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -913,6 +913,7 @@ "thai": "tailandés", "turkish": "turco", "ukrainian": "ucraniano", + "unknown": "desconocido", "urdu": "urdu", "vietnamese": "vietnamita" }, @@ -2891,7 +2892,7 @@ }, "supported_text_delta": { "label": "salida de texto incremental", - "tooltip": "Cuando el modelo no lo admita, desactive este botón" + "tooltip": "Cuando el modelo no sea compatible, desactive este botón." } }, "api_key": "Clave API", @@ -3394,6 +3395,7 @@ }, "close": "Cerrar", "closed": "La traducción ha sido desactivada", + "complete": "traducción completada", "confirm": { "content": "La traducción reemplazará el texto original, ¿desea continuar?", "title": "Confirmación de traducción" @@ -3405,13 +3407,17 @@ "empty": "El contenido de traducción está vacío", "error": { "failed": "Fallo en la traducción", - "not_configured": "El modelo de traducción no está configurado" + "not_configured": "El modelo de traducción no está configurado", + "unknown": "Se produjo un error desconocido durante la traducción" }, "history": { "clear": "Borrar historial", "clear_description": "Borrar el historial eliminará todos los registros de traducciones, ¿desea continuar?", "delete": "Eliminar", "empty": "Sin historial de traducciones por el momento", + "error": { + "save": "Error al guardar el historial de traducciones" + }, "title": "Historial de traducciones" }, "input": { diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index c28726eb88..ae4b31e6c5 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -913,6 +913,7 @@ "thai": "Thaï", "turkish": "Turc", "ukrainian": "ukrainien", + "unknown": "inconnu", "urdu": "Ourdou", "vietnamese": "Vietnamien" }, @@ -2891,7 +2892,7 @@ }, "supported_text_delta": { "label": "sortie de texte incrémentielle", - "tooltip": "Lorsque le modèle n'est pas pris en charge, désactivez ce bouton" + "tooltip": "Désactivez ce bouton lorsque le modèle n'est pas pris en charge" } }, "api_key": "Clé API", @@ -3394,6 +3395,7 @@ }, "close": "fermer", "closed": "La traduction est désactivée", + "complete": "La traduction est terminée", "confirm": { "content": "La traduction remplacera le texte original, voulez-vous continuer ?", "title": "Confirmation de traduction" @@ -3405,13 +3407,17 @@ "empty": "Le contenu à traduire est vide", "error": { "failed": "échec de la traduction", - "not_configured": "le modèle de traduction n'est pas configuré" + "not_configured": "le modèle de traduction n'est pas configuré", + "unknown": "Une erreur inconnue s'est produite lors de la traduction" }, "history": { "clear": "Effacer l'historique", "clear_description": "L'effacement de l'historique supprimera toutes les entrées d'historique de traduction, voulez-vous continuer ?", "delete": "Supprimer", "empty": "Aucun historique de traduction pour le moment", + "error": { + "save": "Échec de la sauvegarde de l'historique des traductions" + }, "title": "Historique des traductions" }, "input": { diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 27e05b32bf..dab84639e6 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -913,6 +913,7 @@ "thai": "Tailandês", "turkish": "Turco", "ukrainian": "ucraniano", + "unknown": "desconhecido", "urdu": "Urdu", "vietnamese": "Vietnamita" }, @@ -3394,6 +3395,7 @@ }, "close": "Fechar", "closed": "A tradução foi desativada", + "complete": "Tradução concluída", "confirm": { "content": "A tradução substituirá o texto original, deseja continuar?", "title": "Confirmação de Tradução" @@ -3405,13 +3407,17 @@ "empty": "O conteúdo de tradução está vazio", "error": { "failed": "Tradução falhou", - "not_configured": "Modelo de tradução não configurado" + "not_configured": "Modelo de tradução não configurado", + "unknown": "Ocorreu um erro desconhecido durante a tradução" }, "history": { "clear": "Limpar Histórico", "clear_description": "Limpar histórico irá deletar todos os registros de tradução. Deseja continuar?", "delete": "Excluir", "empty": "Nenhum histórico de tradução disponível", + "error": { + "save": "Falha ao guardar o histórico de traduções" + }, "title": "Histórico de Tradução" }, "input": { diff --git a/src/renderer/src/pages/translate/TranslatePage.tsx b/src/renderer/src/pages/translate/TranslatePage.tsx index fb4fdfeb9a..dcdb506cc8 100644 --- a/src/renderer/src/pages/translate/TranslatePage.tsx +++ b/src/renderer/src/pages/translate/TranslatePage.tsx @@ -12,13 +12,12 @@ import db from '@renderer/databases' import { useDefaultModel } from '@renderer/hooks/useAssistant' import { useProviders } from '@renderer/hooks/useProvider' import { useSettings } from '@renderer/hooks/useSettings' -import { fetchTranslate } from '@renderer/services/ApiService' -import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' +import useTranslate from '@renderer/hooks/useTranslate' import { getModelUniqId, hasModel } from '@renderer/services/ModelService' import { useAppDispatch } from '@renderer/store' import { setTranslateModelPrompt } from '@renderer/store/settings' import type { Language, LanguageCode, Model, TranslateHistory } from '@renderer/types' -import { runAsyncFunction, uuid } from '@renderer/utils' +import { runAsyncFunction } from '@renderer/utils' import { createInputScrollHandler, createOutputScrollHandler, @@ -39,7 +38,6 @@ import styled from 'styled-components' const logger = loggerService.withContext('TranslatePage') let _text = '' -let _result = '' let _targetLanguage = LanguagesEnum.enUS const TranslateSettings: FC<{ @@ -285,10 +283,8 @@ const TranslatePage: FC = () => { const { t } = useTranslation() const { shikiMarkdownIt } = useCodeStyle() const [text, setText] = useState(_text) - const [result, setResult] = useState(_result) const [renderedMarkdown, setRenderedMarkdown] = useState('') const { translateModel, setTranslateModel } = useDefaultModel() - const [loading, setLoading] = useState(false) const [copied, setCopied] = useState(false) const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false) const [isScrollSyncEnabled, setIsScrollSyncEnabled] = useState(false) @@ -306,6 +302,8 @@ const TranslatePage: FC = () => { const textAreaRef = useRef(null) const outputTextRef = useRef(null) const isProgrammaticScroll = useRef(false) + const { translatedContent, translating, translate, setTranslatedContent, clearHistory, deleteHistory } = + useTranslate() const _translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), []) @@ -318,7 +316,6 @@ const TranslatePage: FC = () => { }, [_translateHistory]) _text = text - _result = result _targetLanguage = targetLanguage const handleModelChange = (model: Model) => { @@ -326,31 +323,6 @@ const TranslatePage: FC = () => { db.settings.put({ id: 'translate:model', value: model.id }) } - const saveTranslateHistory = async ( - sourceText: string, - targetText: string, - sourceLanguage: LanguageCode, - targetLanguage: LanguageCode - ) => { - const history: TranslateHistory = { - id: uuid(), - sourceText, - targetText, - sourceLanguage, - targetLanguage, - createdAt: new Date().toISOString() - } - await db.translate_history.add(history) - } - - const deleteHistory = async (id: string) => { - db.translate_history.delete(id) - } - - const clearHistory = async () => { - db.translate_history.clear() - } - const onTranslate = async () => { if (!text.trim()) return if (!translateModel) { @@ -361,7 +333,6 @@ const TranslatePage: FC = () => { return } - setLoading(true) try { // 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测 let actualSourceLanguage: Language @@ -385,7 +356,6 @@ const TranslatePage: FC = () => { content: errorMessage, key: 'translate-message' }) - setLoading(false) return } @@ -394,26 +364,13 @@ const TranslatePage: FC = () => { setTargetLanguage(actualTargetLanguage) } - const assistant = getDefaultTranslateAssistant(actualTargetLanguage, text) - let translatedText = '' - await fetchTranslate({ - content: text, - assistant, - onResponse: (text) => { - translatedText = text.replace(/^\s*\n+/g, '') - setResult(translatedText) - } - }) - - await saveTranslateHistory(text, translatedText, actualSourceLanguage.langCode, actualTargetLanguage.langCode) - setLoading(false) + await translate(text, actualSourceLanguage, actualTargetLanguage) } catch (error) { logger.error('Translation error:', error as Error) window.message.error({ content: String(error), key: 'translate-message' }) - setLoading(false) return } } @@ -424,27 +381,27 @@ const TranslatePage: FC = () => { } const onCopy = () => { - navigator.clipboard.writeText(result) + navigator.clipboard.writeText(translatedContent) setCopied(true) setTimeout(() => setCopied(false), 2000) } const onHistoryItemClick = (history: TranslateHistory & { _sourceLanguage: Language; _targetLanguage: Language }) => { setText(history.sourceText) - setResult(history.targetText) + setTranslatedContent(history.targetText) setSourceLanguage(history._sourceLanguage) setTargetLanguage(history._targetLanguage) } useEffect(() => { - isEmpty(text) && setResult('') - }, [text]) + isEmpty(text) && setTranslatedContent('') + }, [setTranslatedContent, text]) // Render markdown content when result or enableMarkdown changes useEffect(() => { - if (enableMarkdown && result) { + if (enableMarkdown && translatedContent) { let isMounted = true - shikiMarkdownIt(result).then((rendered) => { + shikiMarkdownIt(translatedContent).then((rendered) => { if (isMounted) { setRenderedMarkdown(rendered) } @@ -456,7 +413,7 @@ const TranslatePage: FC = () => { setRenderedMarkdown('') return undefined } - }, [result, enableMarkdown, shikiMarkdownIt]) + }, [enableMarkdown, shikiMarkdownIt, translatedContent]) useEffect(() => { runAsyncFunction(async () => { @@ -675,7 +632,7 @@ const TranslatePage: FC = () => { }> }> @@ -692,7 +649,7 @@ const TranslatePage: FC = () => { onChange={(e) => setText(e.target.value)} onKeyDown={onKeyDown} onScroll={handleInputScroll} - disabled={loading} + disabled={translating} spellCheck={false} allowClear /> @@ -705,18 +662,18 @@ const TranslatePage: FC = () => { : } /> - {!result ? ( + {!translatedContent ? ( t('translate.output.placeholder') ) : enableMarkdown ? (
) : ( -
{result}
+
{translatedContent}
)} diff --git a/src/renderer/src/services/AssistantService.ts b/src/renderer/src/services/AssistantService.ts index 654b7e7bde..82ddc94598 100644 --- a/src/renderer/src/services/AssistantService.ts +++ b/src/renderer/src/services/AssistantService.ts @@ -1,3 +1,4 @@ +import { loggerService } from '@logger' import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, @@ -20,6 +21,8 @@ import type { } from '@renderer/types' import { uuid } from '@renderer/utils' +const logger = loggerService.withContext('AssistantService') + export function getDefaultAssistant(): Assistant { return { id: 'default', @@ -50,6 +53,11 @@ export function getDefaultTranslateAssistant(targetLanguage: Language, text: str const assistant: Assistant = getDefaultAssistant() assistant.model = translateModel + if (!assistant.model) { + logger.error('No translate model') + throw new Error(i18n.t('translate.error.not_configured')) + } + assistant.settings = { temperature: 0.7 } diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts index 5860871235..28a3c27a98 100644 --- a/src/renderer/src/store/index.ts +++ b/src/renderer/src/store/index.ts @@ -26,6 +26,7 @@ import selectionStore from './selectionStore' import settings from './settings' import shortcuts from './shortcuts' import tabs from './tabs' +import translate from './translate' import websearch from './websearch' const rootReducer = combineReducers({ @@ -51,7 +52,8 @@ const rootReducer = combineReducers({ preprocess, messages: newMessagesReducer, messageBlocks: messageBlocksReducer, - inputTools: inputToolsReducer + inputTools: inputToolsReducer, + translate }) const persistedReducer = persistReducer( diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts index d1e3752d10..643d2636c2 100644 --- a/src/renderer/src/store/runtime.ts +++ b/src/renderer/src/store/runtime.ts @@ -29,6 +29,7 @@ export interface UpdateState { export interface RuntimeState { avatar: string generating: boolean + translating: boolean /** whether the minapp popup is shown */ minappShow: boolean /** the minapps that are opened and should be keep alive */ @@ -53,6 +54,7 @@ export interface ExportState { const initialState: RuntimeState = { avatar: UserAvatar, generating: false, + translating: false, minappShow: false, openedKeepAliveMinapps: [], openedOneOffMinapp: null, @@ -93,6 +95,9 @@ const runtimeSlice = createSlice({ setGenerating: (state, action: PayloadAction) => { state.generating = action.payload }, + setTranslating: (state, action: PayloadAction) => { + state.translating = action.payload + }, setMinappShow: (state, action: PayloadAction) => { state.minappShow = action.payload }, @@ -156,6 +161,7 @@ const runtimeSlice = createSlice({ export const { setAvatar, setGenerating, + setTranslating, setMinappShow, setOpenedKeepAliveMinapps, setOpenedOneOffMinapp, diff --git a/src/renderer/src/store/translate.ts b/src/renderer/src/store/translate.ts new file mode 100644 index 0000000000..b5cda7a666 --- /dev/null +++ b/src/renderer/src/store/translate.ts @@ -0,0 +1,26 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +export interface TranslateState { + translatedContent: string +} + +const initialState: TranslateState = { + translatedContent: '' +} + +const translateSlice = createSlice({ + name: 'translate', + initialState, + reducers: { + setTranslatedContent: (state, action: PayloadAction) => { + return { + ...state, + translatedContent: action.payload + } + } + } +}) + +export const { setTranslatedContent } = translateSlice.actions + +export default translateSlice.reducer diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 32a0080607..70a55c867a 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -512,6 +512,7 @@ export type GenerateImageResponse = { } export type LanguageCode = + | 'unknown' | 'en-us' | 'zh-cn' | 'zh-tw' diff --git a/src/renderer/src/utils/translate.ts b/src/renderer/src/utils/translate.ts index b0edfddc23..44464daedc 100644 --- a/src/renderer/src/utils/translate.ts +++ b/src/renderer/src/utils/translate.ts @@ -1,5 +1,5 @@ import { loggerService } from '@logger' -import { LanguagesEnum } from '@renderer/config/translate' +import { LanguagesEnum, UNKNOWN } from '@renderer/config/translate' import { Language, LanguageCode } from '@renderer/types' import { franc } from 'franc-min' import React, { MutableRefObject } from 'react' @@ -245,7 +245,7 @@ export const getLanguageByLangcode = (langcode: LanguageCode): Language => { const result = Object.values(LanguagesEnum).find((item) => item.langCode === langcode) if (!result) { logger.error(`Language not found for langcode: ${langcode}`) - return LanguagesEnum.enUS + return UNKNOWN } return result }