mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 11:49:02 +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 { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
import { useProviders } from '@renderer/hooks/useProvider'
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
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 { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||||
import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
|
import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setTranslateModelPrompt } from '@renderer/store/settings'
|
import { setTranslateModelPrompt } from '@renderer/store/settings'
|
||||||
import type { Model, TranslateHistory } from '@renderer/types'
|
import type { Model, TranslateHistory } from '@renderer/types'
|
||||||
import { runAsyncFunction, uuid } from '@renderer/utils'
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
import {
|
import {
|
||||||
createInputScrollHandler,
|
createInputScrollHandler,
|
||||||
createOutputScrollHandler,
|
createOutputScrollHandler,
|
||||||
@ -34,7 +34,6 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
let _text = ''
|
let _text = ''
|
||||||
let _result = ''
|
|
||||||
let _targetLanguage = 'english'
|
let _targetLanguage = 'english'
|
||||||
|
|
||||||
const TranslateSettings: FC<{
|
const TranslateSettings: FC<{
|
||||||
@ -277,10 +276,8 @@ const TranslatePage: FC = () => {
|
|||||||
const { shikiMarkdownIt } = useCodeStyle()
|
const { shikiMarkdownIt } = useCodeStyle()
|
||||||
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
|
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
|
||||||
const [text, setText] = useState(_text)
|
const [text, setText] = useState(_text)
|
||||||
const [result, setResult] = useState(_result)
|
|
||||||
const [renderedMarkdown, setRenderedMarkdown] = useState<string>('')
|
const [renderedMarkdown, setRenderedMarkdown] = useState<string>('')
|
||||||
const { translateModel, setTranslateModel } = useDefaultModel()
|
const { translateModel, setTranslateModel } = useDefaultModel()
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
|
const [historyDrawerVisible, setHistoryDrawerVisible] = useState(false)
|
||||||
const [isScrollSyncEnabled, setIsScrollSyncEnabled] = 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 allModels = useMemo(() => providers.map((p) => p.models).flat(), [providers])
|
||||||
|
|
||||||
const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
|
const translateHistory = useLiveQuery(() => db.translate_history.orderBy('createdAt').reverse().toArray(), [])
|
||||||
|
const { translatedContent, translating, translate, setTranslatedContent, setTranslating } = useTranslate()
|
||||||
|
|
||||||
_text = text
|
_text = text
|
||||||
_result = result
|
|
||||||
_targetLanguage = targetLanguage
|
_targetLanguage = targetLanguage
|
||||||
|
|
||||||
const selectOptions = useMemo(
|
const selectOptions = useMemo(
|
||||||
@ -326,23 +323,6 @@ const TranslatePage: FC = () => {
|
|||||||
db.settings.put({ id: 'translate:model', value: model.id })
|
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) => {
|
const deleteHistory = async (id: string) => {
|
||||||
db.translate_history.delete(id)
|
db.translate_history.delete(id)
|
||||||
}
|
}
|
||||||
@ -361,7 +341,7 @@ const TranslatePage: FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true)
|
setTranslating(true)
|
||||||
try {
|
try {
|
||||||
// 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
|
// 确定源语言:如果用户选择了特定语言,使用用户选择的;如果选择'auto',则自动检测
|
||||||
let actualSourceLanguage: string
|
let actualSourceLanguage: string
|
||||||
@ -385,7 +365,7 @@ const TranslatePage: FC = () => {
|
|||||||
content: errorMessage,
|
content: errorMessage,
|
||||||
key: 'translate-message'
|
key: 'translate-message'
|
||||||
})
|
})
|
||||||
setLoading(false)
|
setTranslating(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,25 +375,15 @@ const TranslatePage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const assistant = getDefaultTranslateAssistant(actualTargetLanguage, text)
|
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)
|
await translate(text, assistant, actualSourceLanguage, actualTargetLanguage)
|
||||||
setLoading(false)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Translation error:', error)
|
console.error('Translation error:', error)
|
||||||
window.message.error({
|
window.message.error({
|
||||||
content: String(error),
|
content: String(error),
|
||||||
key: 'translate-message'
|
key: 'translate-message'
|
||||||
})
|
})
|
||||||
setLoading(false)
|
setTranslating(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -424,26 +394,26 @@ const TranslatePage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onCopy = () => {
|
const onCopy = () => {
|
||||||
navigator.clipboard.writeText(result)
|
navigator.clipboard.writeText(translatedContent)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onHistoryItemClick = (history: TranslateHistory) => {
|
const onHistoryItemClick = (history: TranslateHistory) => {
|
||||||
setText(history.sourceText)
|
setText(history.sourceText)
|
||||||
setResult(history.targetText)
|
setTranslatedContent(history.targetText)
|
||||||
setTargetLanguage(history.targetLanguage)
|
setTargetLanguage(history.targetLanguage)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
isEmpty(text) && setResult('')
|
isEmpty(text) && setTranslatedContent('')
|
||||||
}, [text])
|
}, [setTranslatedContent, text])
|
||||||
|
|
||||||
// Render markdown content when result or enableMarkdown changes
|
// Render markdown content when result or enableMarkdown changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enableMarkdown && result) {
|
if (enableMarkdown && translatedContent) {
|
||||||
let isMounted = true
|
let isMounted = true
|
||||||
shikiMarkdownIt(result).then((rendered) => {
|
shikiMarkdownIt(translatedContent).then((rendered) => {
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setRenderedMarkdown(rendered)
|
setRenderedMarkdown(rendered)
|
||||||
}
|
}
|
||||||
@ -455,7 +425,7 @@ const TranslatePage: FC = () => {
|
|||||||
setRenderedMarkdown('')
|
setRenderedMarkdown('')
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}, [result, enableMarkdown, shikiMarkdownIt])
|
}, [enableMarkdown, shikiMarkdownIt, translatedContent])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
@ -650,7 +620,7 @@ const TranslatePage: FC = () => {
|
|||||||
}>
|
}>
|
||||||
<TranslateButton
|
<TranslateButton
|
||||||
type="primary"
|
type="primary"
|
||||||
loading={loading}
|
loading={translating}
|
||||||
onClick={onTranslate}
|
onClick={onTranslate}
|
||||||
disabled={!text.trim()}
|
disabled={!text.trim()}
|
||||||
icon={<SendOutlined />}>
|
icon={<SendOutlined />}>
|
||||||
@ -667,7 +637,7 @@ const TranslatePage: FC = () => {
|
|||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
onScroll={handleInputScroll}
|
onScroll={handleInputScroll}
|
||||||
disabled={loading}
|
disabled={translating}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
@ -680,18 +650,18 @@ const TranslatePage: FC = () => {
|
|||||||
</HStack>
|
</HStack>
|
||||||
<CopyButton
|
<CopyButton
|
||||||
onClick={onCopy}
|
onClick={onCopy}
|
||||||
disabled={!result}
|
disabled={!translatedContent}
|
||||||
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon />}
|
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon />}
|
||||||
/>
|
/>
|
||||||
</OperationBar>
|
</OperationBar>
|
||||||
|
|
||||||
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className={'selectable'}>
|
<OutputText ref={outputTextRef} onScroll={handleOutputScroll} className={'selectable'}>
|
||||||
{!result ? (
|
{!translatedContent ? (
|
||||||
t('translate.output.placeholder')
|
t('translate.output.placeholder')
|
||||||
) : enableMarkdown ? (
|
) : enableMarkdown ? (
|
||||||
<div className="markdown" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
|
<div className="markdown" dangerouslySetInnerHTML={{ __html: renderedMarkdown }} />
|
||||||
) : (
|
) : (
|
||||||
<div className="plain">{result}</div>
|
<div className="plain">{translatedContent}</div>
|
||||||
)}
|
)}
|
||||||
</OutputText>
|
</OutputText>
|
||||||
</OutputContainer>
|
</OutputContainer>
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import runtime from './runtime'
|
|||||||
import selectionStore from './selectionStore'
|
import selectionStore from './selectionStore'
|
||||||
import settings from './settings'
|
import settings from './settings'
|
||||||
import shortcuts from './shortcuts'
|
import shortcuts from './shortcuts'
|
||||||
|
import translate from './translate'
|
||||||
import websearch from './websearch'
|
import websearch from './websearch'
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
@ -47,7 +48,8 @@ const rootReducer = combineReducers({
|
|||||||
preprocess,
|
preprocess,
|
||||||
messages: newMessagesReducer,
|
messages: newMessagesReducer,
|
||||||
messageBlocks: messageBlocksReducer,
|
messageBlocks: messageBlocksReducer,
|
||||||
inputTools: inputToolsReducer
|
inputTools: inputToolsReducer,
|
||||||
|
translate
|
||||||
})
|
})
|
||||||
|
|
||||||
const persistedReducer = persistReducer(
|
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