mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 04:31:27 +08:00
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:
parent
072b52708f
commit
eea9f7a1f6
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
158
src/renderer/src/hooks/useTranslate.ts
Normal file
158
src/renderer/src/hooks/useTranslate.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
26
src/renderer/src/store/translate.ts
Normal file
26
src/renderer/src/store/translate.ts
Normal 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
|
||||
@ -512,6 +512,7 @@ export type GenerateImageResponse = {
|
||||
}
|
||||
|
||||
export type LanguageCode =
|
||||
| 'unknown'
|
||||
| 'en-us'
|
||||
| 'zh-cn'
|
||||
| 'zh-tw'
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user