mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-01 09:49:03 +08:00
feat(translate): 支持后台执行翻译任务
- 新增translate store模块管理翻译状态 - 实现useTranslate hook封装翻译逻辑 - 重构TranslatePage组件使用新的翻译逻辑
This commit is contained in:
parent
ed9ecd4667
commit
6d929c322b
70
src/renderer/src/hooks/useTranslate.ts
Normal file
70
src/renderer/src/hooks/useTranslate.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import db from '@renderer/databases'
|
||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
setTranslatedContent as _setTranslatedContent,
|
||||
setTranslating as _setTranslating
|
||||
} from '@renderer/store/translate'
|
||||
import { Assistant, TranslateHistory } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
|
||||
export default function useTranslate() {
|
||||
const translatedContent = useAppSelector((state) => state.translate.translatedContent)
|
||||
const translating = useAppSelector((state) => state.translate.translating)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const setTranslatedContent = (content: string) => {
|
||||
dispatch(_setTranslatedContent(content))
|
||||
}
|
||||
|
||||
const setTranslating = (translating: boolean) => {
|
||||
dispatch(_setTranslating(translating))
|
||||
}
|
||||
|
||||
const translate = async (
|
||||
text: string,
|
||||
assistant: Assistant,
|
||||
actualSourceLanguage: string,
|
||||
actualTargetLanguage: string
|
||||
) => {
|
||||
setTranslating(true)
|
||||
await fetchTranslate({
|
||||
content: text,
|
||||
assistant,
|
||||
onResponse: (text) => {
|
||||
setTranslatedContent(text)
|
||||
}
|
||||
})
|
||||
const translatedContent = store.getState().translate.translatedContent
|
||||
await saveTranslateHistory(text, translatedContent, actualSourceLanguage, actualTargetLanguage)
|
||||
|
||||
setTranslating(false)
|
||||
}
|
||||
|
||||
const saveTranslateHistory = async (
|
||||
sourceText: string,
|
||||
targetText: string,
|
||||
sourceLanguage: string,
|
||||
targetLanguage: string
|
||||
) => {
|
||||
const history: TranslateHistory = {
|
||||
id: uuid(),
|
||||
sourceText,
|
||||
targetText,
|
||||
sourceLanguage,
|
||||
targetLanguage,
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
await db.translate_history.add(history)
|
||||
}
|
||||
|
||||
return {
|
||||
translatedContent,
|
||||
translating,
|
||||
setTranslatedContent,
|
||||
setTranslating,
|
||||
translate,
|
||||
saveTranslateHistory
|
||||
}
|
||||
}
|
||||
@ -10,13 +10,13 @@ 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 useTranslate from '@renderer/hooks/useTranslate'
|
||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setTranslateModelPrompt } from '@renderer/store/settings'
|
||||
import type { Model, TranslateHistory } from '@renderer/types'
|
||||
import { runAsyncFunction, uuid } from '@renderer/utils'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import {
|
||||
createInputScrollHandler,
|
||||
createOutputScrollHandler,
|
||||
@ -34,7 +34,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
let _text = ''
|
||||
let _result = ''
|
||||
let _targetLanguage = 'english'
|
||||
|
||||
const TranslateSettings: FC<{
|
||||
@ -277,10 +276,8 @@ const TranslatePage: FC = () => {
|
||||
const { shikiMarkdownIt } = useCodeStyle()
|
||||
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
|
||||
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)
|
||||
@ -299,9 +296,9 @@ const TranslatePage: FC = () => {
|
||||
const allModels = useMemo(() => providers.map((p) => p.models).flat(), [providers])
|
||||
|
||||
const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
|
||||
const { translatedContent, translating, translate, setTranslatedContent, setTranslating } = useTranslate()
|
||||
|
||||
_text = text
|
||||
_result = result
|
||||
_targetLanguage = targetLanguage
|
||||
|
||||
const selectOptions = useMemo(
|
||||
@ -326,23 +323,6 @@ const TranslatePage: FC = () => {
|
||||
db.settings.put({ id: 'translate:model', value: model.id })
|
||||
}
|
||||
|
||||
const saveTranslateHistory = async (
|
||||
sourceText: string,
|
||||
targetText: string,
|
||||
sourceLanguage: string,
|
||||
targetLanguage: string
|
||||
) => {
|
||||
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)
|
||||
}
|
||||
@ -361,7 +341,7 @@ const TranslatePage: FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setTranslating(true)
|
||||
try {
|
||||
// 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
|
||||
let actualSourceLanguage: string
|
||||
@ -385,7 +365,7 @@ const TranslatePage: FC = () => {
|
||||
content: errorMessage,
|
||||
key: 'translate-message'
|
||||
})
|
||||
setLoading(false)
|
||||
setTranslating(false)
|
||||
return
|
||||
}
|
||||
|
||||
@ -395,25 +375,15 @@ const TranslatePage: FC = () => {
|
||||
}
|
||||
|
||||
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, actualTargetLanguage)
|
||||
setLoading(false)
|
||||
await translate(text, assistant, actualSourceLanguage, actualTargetLanguage)
|
||||
} catch (error) {
|
||||
console.error('Translation error:', error)
|
||||
window.message.error({
|
||||
content: String(error),
|
||||
key: 'translate-message'
|
||||
})
|
||||
setLoading(false)
|
||||
setTranslating(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -424,26 +394,26 @@ const TranslatePage: FC = () => {
|
||||
}
|
||||
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(result)
|
||||
navigator.clipboard.writeText(translatedContent)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const onHistoryItemClick = (history: TranslateHistory) => {
|
||||
setText(history.sourceText)
|
||||
setResult(history.targetText)
|
||||
setTranslatedContent(history.targetText)
|
||||
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)
|
||||
}
|
||||
@ -455,7 +425,7 @@ const TranslatePage: FC = () => {
|
||||
setRenderedMarkdown('')
|
||||
return undefined
|
||||
}
|
||||
}, [result, enableMarkdown, shikiMarkdownIt])
|
||||
}, [enableMarkdown, shikiMarkdownIt, translatedContent])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
@ -650,7 +620,7 @@ const TranslatePage: FC = () => {
|
||||
}>
|
||||
<TranslateButton
|
||||
type="primary"
|
||||
loading={loading}
|
||||
loading={translating}
|
||||
onClick={onTranslate}
|
||||
disabled={!text.trim()}
|
||||
icon={<SendOutlined />}>
|
||||
@ -667,7 +637,7 @@ const TranslatePage: FC = () => {
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
onScroll={handleInputScroll}
|
||||
disabled={loading}
|
||||
disabled={translating}
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
@ -680,18 +650,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>
|
||||
|
||||
@ -24,6 +24,7 @@ import runtime from './runtime'
|
||||
import selectionStore from './selectionStore'
|
||||
import settings from './settings'
|
||||
import shortcuts from './shortcuts'
|
||||
import translate from './translate'
|
||||
import websearch from './websearch'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
@ -47,7 +48,8 @@ const rootReducer = combineReducers({
|
||||
preprocess,
|
||||
messages: newMessagesReducer,
|
||||
messageBlocks: messageBlocksReducer,
|
||||
inputTools: inputToolsReducer
|
||||
inputTools: inputToolsReducer,
|
||||
translate
|
||||
})
|
||||
|
||||
const persistedReducer = persistReducer(
|
||||
|
||||
34
src/renderer/src/store/translate.ts
Normal file
34
src/renderer/src/store/translate.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface TranslateState {
|
||||
translating: boolean
|
||||
translatedContent: string
|
||||
}
|
||||
|
||||
const initialState: TranslateState = {
|
||||
translating: false,
|
||||
translatedContent: ''
|
||||
}
|
||||
|
||||
const translateSlice = createSlice({
|
||||
name: 'translate',
|
||||
initialState,
|
||||
reducers: {
|
||||
setTranslating: (state, action: PayloadAction<boolean>) => {
|
||||
return {
|
||||
...state,
|
||||
translating: action.payload
|
||||
}
|
||||
},
|
||||
setTranslatedContent: (state, action: PayloadAction<string>) => {
|
||||
return {
|
||||
...state,
|
||||
translatedContent: action.payload
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const { setTranslating, setTranslatedContent } = translateSlice.actions
|
||||
|
||||
export default translateSlice.reducer
|
||||
Loading…
Reference in New Issue
Block a user