mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-06 21:35:52 +08:00
feat: implement automatic text translation functionality
This commit is contained in:
parent
b73fa5a44f
commit
97052f825f
@ -63,8 +63,11 @@ electronDownload:
|
|||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
支持清除应用缓存
|
输入内容支持快速翻译成英文
|
||||||
支持编辑翻译模型提示词
|
输出内容支持翻译成其他语言
|
||||||
支持使用搜索内容快速创建助手
|
快速敲击3次空格翻译
|
||||||
支持编辑模型是否为视觉模型
|
支持自定义快捷键
|
||||||
界面样式优化和错误修复
|
支持关闭对话自动重命名
|
||||||
|
修复 Gemini 自定义域名不生效问题
|
||||||
|
画图支持生成 Seed 种子词
|
||||||
|
修复 Markdown 渲染错误导致应用崩溃
|
||||||
|
|||||||
@ -69,27 +69,30 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
editedText && onEditMessage?.({ ...message, content: editedText })
|
editedText && onEditMessage?.({ ...message, content: editedText })
|
||||||
}, [message, onEditMessage])
|
}, [message, onEditMessage])
|
||||||
|
|
||||||
const handleTranslate = async (language: string) => {
|
const handleTranslate = useCallback(
|
||||||
if (isTranslating) return
|
async (language: string) => {
|
||||||
|
if (isTranslating) return
|
||||||
|
|
||||||
onEditMessage?.({ ...message, translatedContent: t('translate.processing') })
|
onEditMessage?.({ ...message, translatedContent: t('translate.processing') })
|
||||||
|
|
||||||
setIsTranslating(true)
|
setIsTranslating(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const translatedText = await translateText(message.content, language)
|
const translatedText = await translateText(message.content, language)
|
||||||
onEditMessage?.({ ...message, translatedContent: translatedText })
|
onEditMessage?.({ ...message, translatedContent: translatedText })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Translation failed:', error)
|
console.error('Translation failed:', error)
|
||||||
window.message.error({
|
window.message.error({
|
||||||
content: t('translate.error.failed'),
|
content: t('translate.error.failed'),
|
||||||
key: 'translate-message'
|
key: 'translate-message'
|
||||||
})
|
})
|
||||||
onEditMessage?.({ ...message, translatedContent: undefined })
|
onEditMessage?.({ ...message, translatedContent: undefined })
|
||||||
} finally {
|
} finally {
|
||||||
setIsTranslating(false)
|
setIsTranslating(false)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[isTranslating, message, onEditMessage, t]
|
||||||
|
)
|
||||||
|
|
||||||
const dropdownItems = useMemo(
|
const dropdownItems = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
|||||||
@ -15,9 +15,11 @@ import { useTheme } from '@renderer/context/ThemeProvider'
|
|||||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
import { useAllProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders } from '@renderer/hooks/useProvider'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import AiProvider from '@renderer/providers/AiProvider'
|
import AiProvider from '@renderer/providers/AiProvider'
|
||||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { DEFAULT_PAINTING } from '@renderer/store/paintings'
|
import { DEFAULT_PAINTING } from '@renderer/store/paintings'
|
||||||
import { setGenerating } from '@renderer/store/runtime'
|
import { setGenerating } from '@renderer/store/runtime'
|
||||||
@ -25,7 +27,7 @@ import { FileType, Painting } from '@renderer/types'
|
|||||||
import { getErrorMessage } from '@renderer/utils'
|
import { getErrorMessage } from '@renderer/utils'
|
||||||
import { Button, Input, InputNumber, Radio, Select, Slider, Tooltip } from 'antd'
|
import { Button, Input, InputNumber, Radio, Select, Slider, Tooltip } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
import { FC, useRef, useState } from 'react'
|
import { FC, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -232,6 +234,59 @@ const PaintingsPage: FC = () => {
|
|||||||
setCurrentImageIndex(0)
|
setCurrentImageIndex(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { autoTranslateWithSpace } = useSettings()
|
||||||
|
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||||
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
|
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||||
|
|
||||||
|
const translate = async () => {
|
||||||
|
if (isTranslating) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!painting.prompt) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsTranslating(true)
|
||||||
|
const translatedText = await translateText(painting.prompt, 'english')
|
||||||
|
updatePaintingState({ prompt: translatedText })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Translation failed:', error)
|
||||||
|
} finally {
|
||||||
|
setIsTranslating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (autoTranslateWithSpace && event.key === ' ') {
|
||||||
|
setSpaceClickCount((prev) => prev + 1)
|
||||||
|
|
||||||
|
if (spaceClickTimer.current) {
|
||||||
|
clearTimeout(spaceClickTimer.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
spaceClickTimer.current = setTimeout(() => {
|
||||||
|
setSpaceClickCount(0)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
if (spaceClickCount === 2) {
|
||||||
|
setSpaceClickCount(0)
|
||||||
|
setIsTranslating(true)
|
||||||
|
translate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (spaceClickTimer.current) {
|
||||||
|
clearTimeout(spaceClickTimer.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Navbar>
|
<Navbar>
|
||||||
@ -362,14 +417,16 @@ const PaintingsPage: FC = () => {
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
value={painting.prompt}
|
value={painting.prompt}
|
||||||
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
|
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
|
||||||
placeholder={t('paintings.prompt_placeholder')}
|
placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder')}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<ToolbarMenu>
|
<ToolbarMenu>
|
||||||
<TranslateButton
|
<TranslateButton
|
||||||
text={textareaRef.current?.resizableTextArea?.textArea?.value}
|
text={textareaRef.current?.resizableTextArea?.textArea?.value}
|
||||||
onTranslated={(translatedText) => updatePaintingState({ prompt: translatedText })}
|
onTranslated={(translatedText) => updatePaintingState({ prompt: translatedText })}
|
||||||
disabled={isLoading}
|
disabled={isLoading || isTranslating}
|
||||||
|
isLoading={isTranslating}
|
||||||
style={{ marginRight: 6, borderRadius: '50%' }}
|
style={{ marginRight: 6, borderRadius: '50%' }}
|
||||||
/>
|
/>
|
||||||
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
|
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user