feat: auto translate input text
This commit is contained in:
parent
3717ff25bf
commit
2e9041c891
@ -1,27 +1,41 @@
|
|||||||
import { TranslationOutlined } from '@ant-design/icons'
|
import { LoadingOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||||
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||||
import { getUserMessage } from '@renderer/services/MessagesService'
|
import { getUserMessage } from '@renderer/services/MessagesService'
|
||||||
import { Button } from 'antd'
|
import { Button, Tooltip } from 'antd'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text?: string
|
text?: string
|
||||||
onTranslated: (translatedText: string) => void
|
onTranslated: (translatedText: string) => void
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
|
isLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style }) => {
|
const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoading }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { translateModel } = useDefaultModel()
|
const { translateModel } = useDefaultModel()
|
||||||
const [isTranslating, setIsTranslating] = useState(false)
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
|
|
||||||
|
const translateConfirm = () => {
|
||||||
|
return window?.modal?.confirm({
|
||||||
|
title: t('translate.confirm.title'),
|
||||||
|
content: t('translate.confirm.content'),
|
||||||
|
centered: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleTranslate = async () => {
|
const handleTranslate = async () => {
|
||||||
if (!text?.trim()) return
|
if (!text?.trim()) return
|
||||||
|
|
||||||
|
if (!(await translateConfirm())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!translateModel) {
|
if (!translateModel) {
|
||||||
window.message.error({
|
window.message.error({
|
||||||
content: t('translate.error.not_configured'),
|
content: t('translate.error.not_configured'),
|
||||||
@ -55,16 +69,53 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style }) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsTranslating(isLoading ?? false)
|
||||||
|
}, [isLoading])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Tooltip placement="top" title={t('chat.input.translate')} arrow>
|
||||||
icon={<TranslationOutlined style={{ fontSize: 14 }} />}
|
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
|
||||||
onClick={handleTranslate}
|
{isTranslating ? <LoadingOutlined spin /> : <TranslationOutlined />}
|
||||||
disabled={disabled || isTranslating}
|
</ToolbarButton>
|
||||||
loading={isTranslating}
|
</Tooltip>
|
||||||
style={style}
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ToolbarButton = styled(Button)`
|
||||||
|
min-width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
font-size: 17px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: var(--color-icon);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
&.anticon,
|
||||||
|
&.iconfont {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: var(--color-icon);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
.anticon,
|
||||||
|
.iconfont {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-primary) !important;
|
||||||
|
.anticon,
|
||||||
|
.iconfont {
|
||||||
|
color: var(--color-white-soft);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default TranslateButton
|
export default TranslateButton
|
||||||
|
|||||||
@ -104,6 +104,7 @@
|
|||||||
"input.upload": "Upload image or document file",
|
"input.upload": "Upload image or document file",
|
||||||
"input.context_count.tip": "Context Count",
|
"input.context_count.tip": "Context Count",
|
||||||
"input.estimated_tokens.tip": "Estimated tokens",
|
"input.estimated_tokens.tip": "Estimated tokens",
|
||||||
|
"input.translate": "Translate to English",
|
||||||
"settings.temperature": "Temperature",
|
"settings.temperature": "Temperature",
|
||||||
"settings.temperature.tip": "Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.",
|
"settings.temperature.tip": "Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.",
|
||||||
"settings.context_count": "Context",
|
"settings.context_count": "Context",
|
||||||
@ -350,6 +351,7 @@
|
|||||||
"topic.position.right": "Right",
|
"topic.position.right": "Right",
|
||||||
"topic.show.time": "Show Topic Time",
|
"topic.show.time": "Show Topic Time",
|
||||||
"display.title": "Display Settings",
|
"display.title": "Display Settings",
|
||||||
|
"input.auto_translate_with_space": "Quickly translate with 3 spaces",
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Keyboard Shortcuts",
|
"title": "Keyboard Shortcuts",
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
@ -425,7 +427,10 @@
|
|||||||
"error.not_configured": "Translation model is not configured",
|
"error.not_configured": "Translation model is not configured",
|
||||||
"input.placeholder": "Enter text to translate",
|
"input.placeholder": "Enter text to translate",
|
||||||
"output.placeholder": "Translation",
|
"output.placeholder": "Translation",
|
||||||
"confirm": "Original text has been copied to clipboard. Do you want to replace it with the translated text?"
|
"confirm": {
|
||||||
|
"title": "Translation Confirmation",
|
||||||
|
"content": "Translation will replace the original text, continue?"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"english": "English",
|
"english": "English",
|
||||||
|
|||||||
@ -104,6 +104,7 @@
|
|||||||
"input.upload": "Загрузить изображение или документ",
|
"input.upload": "Загрузить изображение или документ",
|
||||||
"input.context_count.tip": "Количество контекстов",
|
"input.context_count.tip": "Количество контекстов",
|
||||||
"input.estimated_tokens.tip": "Затраты токенов",
|
"input.estimated_tokens.tip": "Затраты токенов",
|
||||||
|
"input.translate": "Перевести на английский",
|
||||||
"settings.temperature": "Температура",
|
"settings.temperature": "Температура",
|
||||||
"settings.temperature.tip": "Меньшие значения делают модель более креативной и непредсказуемой, в то время как большие значения делают её более детерминированной и точной.",
|
"settings.temperature.tip": "Меньшие значения делают модель более креативной и непредсказуемой, в то время как большие значения делают её более детерминированной и точной.",
|
||||||
"settings.context_count": "Контекст",
|
"settings.context_count": "Контекст",
|
||||||
@ -350,6 +351,7 @@
|
|||||||
"topic.position.right": "Справа",
|
"topic.position.right": "Справа",
|
||||||
"topic.show.time": "Показывать время топика",
|
"topic.show.time": "Показывать время топика",
|
||||||
"display.title": "Настройки отображения",
|
"display.title": "Настройки отображения",
|
||||||
|
"input.auto_translate_with_space": "Быстрый перевод с помощью 3-х пробелов",
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "Горячие клавиши",
|
"title": "Горячие клавиши",
|
||||||
"action": "Действие",
|
"action": "Действие",
|
||||||
@ -425,7 +427,10 @@
|
|||||||
"error.not_configured": "Модель перевода не настроена",
|
"error.not_configured": "Модель перевода не настроена",
|
||||||
"input.placeholder": "Введите текст для перевода",
|
"input.placeholder": "Введите текст для перевода",
|
||||||
"output.placeholder": "Перевод",
|
"output.placeholder": "Перевод",
|
||||||
"confirm": "Исходный текст скопирован в буфер обмена. Хотите заменить его переведенным текстом?"
|
"confirm": {
|
||||||
|
"title": "Перевод подтверждение",
|
||||||
|
"content": "Перевод заменит исходный текст, продолжить?"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"english": "Английский",
|
"english": "Английский",
|
||||||
|
|||||||
@ -104,6 +104,7 @@
|
|||||||
"input.upload": "上传图片或文档",
|
"input.upload": "上传图片或文档",
|
||||||
"input.context_count.tip": "上下文数",
|
"input.context_count.tip": "上下文数",
|
||||||
"input.estimated_tokens.tip": "预估 token 数",
|
"input.estimated_tokens.tip": "预估 token 数",
|
||||||
|
"input.translate": "翻译成英文",
|
||||||
"settings.temperature": "模型温度",
|
"settings.temperature": "模型温度",
|
||||||
"settings.temperature.tip": "模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7",
|
"settings.temperature.tip": "模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7",
|
||||||
"settings.context_count": "上下文数",
|
"settings.context_count": "上下文数",
|
||||||
@ -338,6 +339,7 @@
|
|||||||
"topic.position.right": "右侧",
|
"topic.position.right": "右侧",
|
||||||
"topic.show.time": "显示话题时间",
|
"topic.show.time": "显示话题时间",
|
||||||
"display.title": "显示设置",
|
"display.title": "显示设置",
|
||||||
|
"input.auto_translate_with_space": "快速敲击3次空格翻译",
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "快捷方式",
|
"title": "快捷方式",
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
@ -413,7 +415,10 @@
|
|||||||
"error.not_configured": "翻译模型未配置",
|
"error.not_configured": "翻译模型未配置",
|
||||||
"input.placeholder": "输入文本进行翻译",
|
"input.placeholder": "输入文本进行翻译",
|
||||||
"output.placeholder": "翻译",
|
"output.placeholder": "翻译",
|
||||||
"confirm": "原文已复制到剪贴板,是否用翻译后的文本替换?"
|
"confirm": {
|
||||||
|
"title": "翻译确认",
|
||||||
|
"content": "翻译后将覆盖原文,是否继续?"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"english": "英文",
|
"english": "英文",
|
||||||
|
|||||||
@ -104,6 +104,7 @@
|
|||||||
"input.upload": "上傳圖片或文檔",
|
"input.upload": "上傳圖片或文檔",
|
||||||
"input.context_count.tip": "上下文數量",
|
"input.context_count.tip": "上下文數量",
|
||||||
"input.estimated_tokens.tip": "預估 Token 數",
|
"input.estimated_tokens.tip": "預估 Token 數",
|
||||||
|
"input.translate": "翻譯成英文",
|
||||||
"settings.temperature": "溫度",
|
"settings.temperature": "溫度",
|
||||||
"settings.temperature.tip": "較低的值使模型更具創造性和不可預測性,較高的值則使其更具確定性和精確性。",
|
"settings.temperature.tip": "較低的值使模型更具創造性和不可預測性,較高的值則使其更具確定性和精確性。",
|
||||||
"settings.context_count": "上下文",
|
"settings.context_count": "上下文",
|
||||||
@ -338,6 +339,7 @@
|
|||||||
"topic.position.right": "右側",
|
"topic.position.right": "右側",
|
||||||
"topic.show.time": "顯示話題時間",
|
"topic.show.time": "顯示話題時間",
|
||||||
"display.title": "顯示設定",
|
"display.title": "顯示設定",
|
||||||
|
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"title": "快速方式",
|
"title": "快速方式",
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
@ -413,7 +415,10 @@
|
|||||||
"error.not_configured": "翻譯模型未配置",
|
"error.not_configured": "翻譯模型未配置",
|
||||||
"input.placeholder": "輸入文字進行翻譯",
|
"input.placeholder": "輸入文字進行翻譯",
|
||||||
"output.placeholder": "翻譯",
|
"output.placeholder": "翻譯",
|
||||||
"confirm": "原文已複製到剪貼簿,是否用翻譯後的文字替換?"
|
"confirm": {
|
||||||
|
"title": "翻譯確認",
|
||||||
|
"content": "翻譯後將覆蓋原文,是否繼續?"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"languages": {
|
"languages": {
|
||||||
"english": "英文",
|
"english": "英文",
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
QuestionCircleOutlined
|
QuestionCircleOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { PicCenterOutlined } from '@ant-design/icons'
|
import { PicCenterOutlined } from '@ant-design/icons'
|
||||||
|
import TranslateButton from '@renderer/components/TranslateButton'
|
||||||
import { documentExts, imageExts, isMac, textExts } from '@renderer/config/constant'
|
import { documentExts, imageExts, isMac, textExts } from '@renderer/config/constant'
|
||||||
import { isVisionModel } from '@renderer/config/models'
|
import { isVisionModel } from '@renderer/config/models'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
@ -19,6 +20,7 @@ import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services
|
|||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService'
|
import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/TokenService'
|
||||||
|
import { translateText } from '@renderer/services/TranslateService'
|
||||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||||
import { Assistant, FileType, Message, Topic } from '@renderer/types'
|
import { Assistant, FileType, Message, Topic } from '@renderer/types'
|
||||||
@ -48,8 +50,15 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const [text, setText] = useState(_text)
|
const [text, setText] = useState(_text)
|
||||||
const [inputFocus, setInputFocus] = useState(false)
|
const [inputFocus, setInputFocus] = useState(false)
|
||||||
const { addTopic, model, setModel } = useAssistant(assistant.id)
|
const { addTopic, model, setModel } = useAssistant(assistant.id)
|
||||||
const { sendMessageShortcut, fontSize, pasteLongTextAsFile, showInputEstimatedTokens, clickAssistantToShowTopic } =
|
const {
|
||||||
useSettings()
|
sendMessageShortcut,
|
||||||
|
fontSize,
|
||||||
|
pasteLongTextAsFile,
|
||||||
|
showInputEstimatedTokens,
|
||||||
|
clickAssistantToShowTopic,
|
||||||
|
language,
|
||||||
|
autoTranslateWithSpace
|
||||||
|
} = useSettings()
|
||||||
const [expended, setExpend] = useState(false)
|
const [expended, setExpend] = useState(false)
|
||||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||||
const [contextCount, setContextCount] = useState(0)
|
const [contextCount, setContextCount] = useState(0)
|
||||||
@ -62,6 +71,9 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const { searching } = useRuntime()
|
const { searching } = useRuntime()
|
||||||
const { isBubbleStyle } = useMessageStyle()
|
const { isBubbleStyle } = useMessageStyle()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||||
|
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||||
|
const [isTranslating, setIsTranslating] = useState(false)
|
||||||
|
|
||||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
@ -109,9 +121,47 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
setExpend(false)
|
setExpend(false)
|
||||||
}, [assistant.id, assistant.topics, generating, files, text])
|
}, [assistant.id, assistant.topics, generating, files, text])
|
||||||
|
|
||||||
|
const translate = async () => {
|
||||||
|
if (isTranslating) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsTranslating(true)
|
||||||
|
setText(await translateText(text, 'english'))
|
||||||
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Translation failed:', error)
|
||||||
|
} finally {
|
||||||
|
setIsTranslating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
const isEnterPressed = event.keyCode == 13
|
const isEnterPressed = event.keyCode == 13
|
||||||
|
|
||||||
|
if (autoTranslateWithSpace) {
|
||||||
|
if (event.key === ' ') {
|
||||||
|
setSpaceClickCount((prev) => prev + 1)
|
||||||
|
|
||||||
|
if (spaceClickTimer.current) {
|
||||||
|
clearTimeout(spaceClickTimer.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
spaceClickTimer.current = setTimeout(() => {
|
||||||
|
setSpaceClickCount(0)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
if (spaceClickCount === 2) {
|
||||||
|
console.log('Triple space detected - trigger translation')
|
||||||
|
setSpaceClickCount(0)
|
||||||
|
setIsTranslating(true)
|
||||||
|
translate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (expended) {
|
if (expended) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
return setExpend(false)
|
return setExpend(false)
|
||||||
@ -261,6 +311,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onTranslated = (translatedText: string) => {
|
||||||
|
setText(translatedText)
|
||||||
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
|
}
|
||||||
|
|
||||||
// Command or Ctrl + N create new topic
|
// Command or Ctrl + N create new topic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeydown = (e) => {
|
const onKeydown = (e) => {
|
||||||
@ -297,6 +352,14 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
textareaRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
}, [assistant])
|
}, [assistant])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (spaceClickTimer.current) {
|
||||||
|
clearTimeout(spaceClickTimer.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
|
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
|
||||||
<AttachmentPreview files={files} setFiles={setFiles} />
|
<AttachmentPreview files={files} setFiles={setFiles} />
|
||||||
@ -305,7 +368,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
value={text}
|
value={text}
|
||||||
onChange={(e) => setText(e.target.value)}
|
onChange={(e) => setText(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={t('chat.input.placeholder')}
|
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
|
||||||
autoFocus
|
autoFocus
|
||||||
contextMenu="true"
|
contextMenu="true"
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
@ -370,6 +433,9 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
/>
|
/>
|
||||||
</ToolbarMenu>
|
</ToolbarMenu>
|
||||||
<ToolbarMenu>
|
<ToolbarMenu>
|
||||||
|
{!language.startsWith('en') && (
|
||||||
|
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||||
|
)}
|
||||||
{generating && (
|
{generating && (
|
||||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
||||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { useSettings } from '@renderer/hooks/useSettings'
|
|||||||
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings'
|
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
|
setAutoTranslateWithSpace,
|
||||||
setClickAssistantToShowTopic,
|
setClickAssistantToShowTopic,
|
||||||
setCodeCollapsible,
|
setCodeCollapsible,
|
||||||
setCodeShowLineNumbers,
|
setCodeShowLineNumbers,
|
||||||
@ -60,6 +61,7 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
topicPosition,
|
topicPosition,
|
||||||
showTopicTime,
|
showTopicTime,
|
||||||
clickAssistantToShowTopic,
|
clickAssistantToShowTopic,
|
||||||
|
autoTranslateWithSpace,
|
||||||
setTopicPosition
|
setTopicPosition
|
||||||
} = useSettings()
|
} = useSettings()
|
||||||
|
|
||||||
@ -328,6 +330,15 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={autoTranslateWithSpace}
|
||||||
|
onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
|
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
|
||||||
<Select
|
<Select
|
||||||
@ -342,8 +353,9 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
style={{ width: 135 }}
|
style={{ width: 135 }}
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
</SettingGroup>
|
||||||
<SettingSubtitle>{t('settings.display.title')}</SettingSubtitle>
|
<SettingGroup>
|
||||||
|
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.display.title')}</SettingSubtitle>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
||||||
|
|||||||
@ -232,23 +232,6 @@ const PaintingsPage: FC = () => {
|
|||||||
setCurrentImageIndex(0)
|
setCurrentImageIndex(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTranslation = async (translatedText: string) => {
|
|
||||||
const currentText = textareaRef.current?.resizableTextArea?.textArea?.value
|
|
||||||
|
|
||||||
if (currentText) {
|
|
||||||
await navigator.clipboard.writeText(currentText)
|
|
||||||
|
|
||||||
const confirmed = await window.modal.confirm({
|
|
||||||
content: t('translate.confirm'),
|
|
||||||
centered: true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (confirmed) {
|
|
||||||
updatePaintingState({ prompt: translatedText })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Navbar>
|
<Navbar>
|
||||||
@ -385,7 +368,7 @@ const PaintingsPage: FC = () => {
|
|||||||
<ToolbarMenu>
|
<ToolbarMenu>
|
||||||
<TranslateButton
|
<TranslateButton
|
||||||
text={textareaRef.current?.resizableTextArea?.textArea?.value}
|
text={textareaRef.current?.resizableTextArea?.textArea?.value}
|
||||||
onTranslated={handleTranslation}
|
onTranslated={(translatedText) => updatePaintingState({ prompt: translatedText })}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
style={{ marginRight: 6, borderRadius: '50%' }}
|
style={{ marginRight: 6, borderRadius: '50%' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
15
src/renderer/src/services/TranslateService.ts
Normal file
15
src/renderer/src/services/TranslateService.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { fetchTranslate } from './ApiService'
|
||||||
|
import { getDefaultTopic } from './AssistantService'
|
||||||
|
import { getDefaultTranslateAssistant } from './AssistantService'
|
||||||
|
import { getUserMessage } from './MessagesService'
|
||||||
|
|
||||||
|
export const translateText = async (text: string, targetLanguage: string) => {
|
||||||
|
const assistant = getDefaultTranslateAssistant(targetLanguage, text)
|
||||||
|
const message = getUserMessage({
|
||||||
|
assistant,
|
||||||
|
topic: getDefaultTopic('default'),
|
||||||
|
type: 'text'
|
||||||
|
})
|
||||||
|
const translatedText = await fetchTranslate({ message, assistant })
|
||||||
|
return translatedText
|
||||||
|
}
|
||||||
@ -36,6 +36,7 @@ export interface SettingsState {
|
|||||||
webdavPass: string
|
webdavPass: string
|
||||||
webdavPath: string
|
webdavPath: string
|
||||||
translateModelPrompt: string
|
translateModelPrompt: string
|
||||||
|
autoTranslateWithSpace: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SettingsState = {
|
const initialState: SettingsState = {
|
||||||
@ -68,7 +69,8 @@ const initialState: SettingsState = {
|
|||||||
webdavUser: '',
|
webdavUser: '',
|
||||||
webdavPass: '',
|
webdavPass: '',
|
||||||
webdavPath: '/cherry-studio',
|
webdavPath: '/cherry-studio',
|
||||||
translateModelPrompt: TRANSLATE_PROMPT
|
translateModelPrompt: TRANSLATE_PROMPT,
|
||||||
|
autoTranslateWithSpace: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@ -171,6 +173,9 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setTranslateModelPrompt: (state, action: PayloadAction<string>) => {
|
setTranslateModelPrompt: (state, action: PayloadAction<string>) => {
|
||||||
state.translateModelPrompt = action.payload
|
state.translateModelPrompt = action.payload
|
||||||
|
},
|
||||||
|
setAutoTranslateWithSpace: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.autoTranslateWithSpace = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -207,7 +212,8 @@ export const {
|
|||||||
setMathEngine,
|
setMathEngine,
|
||||||
setMessageStyle,
|
setMessageStyle,
|
||||||
setCodeStyle,
|
setCodeStyle,
|
||||||
setTranslateModelPrompt
|
setTranslateModelPrompt,
|
||||||
|
setAutoTranslateWithSpace
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user