feat(translate): support background execution of translation tasks (#7892)

* feat(translate): 支持后台执行翻译任务

- 新增translate store模块管理翻译状态
- 实现useTranslate hook封装翻译逻辑
- 重构TranslatePage组件使用新的翻译逻辑

* feat(翻译): 添加翻译成功提醒并跟踪当前路由

在翻译完成后添加成功提示,但仅在非翻译页面显示
添加activeRoute状态以跟踪当前路由路径

* refactor(useTranslate): 移除调试用的console.log语句

* fix: update dependencies in effect hooks for improved reactivity

* Revert "fix: update dependencies in effect hooks for improved reactivity"

This reverts commit bb6734c628.

* feat(i18n): 为翻译状态添加"完成"的本地化文本

* refactor(store): 将translating状态从translate迁移到runtime模块

简化translate模块状态管理,将translating状态移至更合适的runtime模块

* feat(i18n): 添加未知语言支持

为语言类型和翻译配置添加未知语言选项,当检测到未知语言代码时返回默认未知语言配置

* reafactor(translate): 添加翻译历史记录管理功能并优化翻译流程

- 在useTranslate钩子中新增deleteHistory和clearHistory方法
- 将翻译历史管理逻辑从页面组件移至useTranslate钩子
- 优化翻译流程,自动获取默认翻译助手
- 调整语言参数类型为Language接口

* refactor(translate): 移除翻译完成时的路由检查逻辑

删除不再使用的activeRoute状态及相关代码,简化翻译完成时的通知逻辑

* feat(翻译): 增强翻译钩子函数的错误处理和日志记录

添加完整的异常处理逻辑和日志记录服务到翻译钩子函数
为翻译操作和保存历史记录添加详细的错误处理
增加JSDoc注释以提升代码可读性和维护性

* feat(i18n): 添加翻译历史保存失败的错误提示

* feat(i18n): 添加翻译未知错误的提示信息

为翻译功能添加未知错误的提示信息,并在捕获异常时显示该提示

* fix(translate): 添加翻译模型检查并处理错误情况

当翻译模型未配置时,添加错误日志记录并抛出异常。在useTranslate中捕获异常并显示错误信息给用户。

* perf(useTranslate): 使用 throttle 优化翻译响应性能

避免频繁触发翻译内容更新,通过 lodash 的 throttle 函数限制更新频率为 100ms

* fix: 修复不支持温度和top_p参数的模型判断逻辑

添加对QwenMT模型的判断,确保其被正确识别为不支持温度和top_p参数

---------

Co-authored-by: 自由的世界人 <3196812536@qq.com>
This commit is contained in:
Phantom 2025-07-29 09:29:49 +08:00 committed by GitHub
parent 072b52708f
commit eea9f7a1f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 294 additions and 75 deletions

View File

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

View File

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

View File

@ -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<void> => {
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<void>
*/
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<void>
*/
const deleteHistory = async (id: string) => {
db.translate_history.delete(id)
}
/**
*
* @returns Promise<void>
*/
const clearHistory = async () => {
db.translate_history.clear()
}
return {
translatedContent,
translating,
setTranslatedContent,
setTranslating,
translate,
saveTranslateHistory,
deleteHistory,
clearHistory
}
}

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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": {

View File

@ -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<string>('')
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<TextAreaRef>(null)
const outputTextRef = useRef<HTMLDivElement>(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 = () => {
}>
<TranslateButton
type="primary"
loading={loading}
loading={translating}
onClick={onTranslate}
disabled={!text.trim()}
icon={<SendOutlined />}>
@ -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 = () => {
</HStack>
<CopyButton
onClick={onCopy}
disabled={!result}
disabled={!translatedContent}
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon />}
/>
</OperationBar>
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className={'selectable'}>
{!result ? (
{!translatedContent ? (
t('translate.output.placeholder')
) : enableMarkdown ? (
<div className="markdown" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
) : (
<div className="plain">{result}</div>
<div className="plain">{translatedContent}</div>
)}
</OutputText>
</OutputContainer>

View File

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

View File

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

View File

@ -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<boolean>) => {
state.generating = action.payload
},
setTranslating: (state, action: PayloadAction<boolean>) => {
state.translating = action.payload
},
setMinappShow: (state, action: PayloadAction<boolean>) => {
state.minappShow = action.payload
},
@ -156,6 +161,7 @@ const runtimeSlice = createSlice({
export const {
setAvatar,
setGenerating,
setTranslating,
setMinappShow,
setOpenedKeepAliveMinapps,
setOpenedOneOffMinapp,

View File

@ -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<string>) => {
return {
...state,
translatedContent: action.payload
}
}
}
})
export const { setTranslatedContent } = translateSlice.actions
export default translateSlice.reducer

View File

@ -512,6 +512,7 @@ export type GenerateImageResponse = {
}
export type LanguageCode =
| 'unknown'
| 'en-us'
| 'zh-cn'
| 'zh-tw'

View File

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