diff --git a/electron-builder.yml b/electron-builder.yml index bb3d930e95..197839d412 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -63,8 +63,11 @@ electronDownload: afterSign: scripts/notarize.js releaseInfo: releaseNotes: | - 支持清除应用缓存 - 支持编辑翻译模型提示词 - 支持使用搜索内容快速创建助手 - 支持编辑模型是否为视觉模型 - 界面样式优化和错误修复 + 输入内容支持快速翻译成英文 + 输出内容支持翻译成其他语言 + 快速敲击3次空格翻译 + 支持自定义快捷键 + 支持关闭对话自动重命名 + 修复 Gemini 自定义域名不生效问题 + 画图支持生成 Seed 种子词 + 修复 Markdown 渲染错误导致应用崩溃 diff --git a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx index 72c7827f42..755e165950 100644 --- a/src/renderer/src/pages/home/Messages/MessageMenubar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageMenubar.tsx @@ -69,27 +69,30 @@ const MessageMenubar: FC = (props) => { editedText && onEditMessage?.({ ...message, content: editedText }) }, [message, onEditMessage]) - const handleTranslate = async (language: string) => { - if (isTranslating) return + const handleTranslate = useCallback( + async (language: string) => { + if (isTranslating) return - onEditMessage?.({ ...message, translatedContent: t('translate.processing') }) + onEditMessage?.({ ...message, translatedContent: t('translate.processing') }) - setIsTranslating(true) + setIsTranslating(true) - try { - const translatedText = await translateText(message.content, language) - onEditMessage?.({ ...message, translatedContent: translatedText }) - } catch (error) { - console.error('Translation failed:', error) - window.message.error({ - content: t('translate.error.failed'), - key: 'translate-message' - }) - onEditMessage?.({ ...message, translatedContent: undefined }) - } finally { - setIsTranslating(false) - } - } + try { + const translatedText = await translateText(message.content, language) + onEditMessage?.({ ...message, translatedContent: translatedText }) + } catch (error) { + console.error('Translation failed:', error) + window.message.error({ + content: t('translate.error.failed'), + key: 'translate-message' + }) + onEditMessage?.({ ...message, translatedContent: undefined }) + } finally { + setIsTranslating(false) + } + }, + [isTranslating, message, onEditMessage, t] + ) const dropdownItems = useMemo( () => [ diff --git a/src/renderer/src/pages/paintings/PaintingsPage.tsx b/src/renderer/src/pages/paintings/PaintingsPage.tsx index d25905ff02..a6ee6366b0 100644 --- a/src/renderer/src/pages/paintings/PaintingsPage.tsx +++ b/src/renderer/src/pages/paintings/PaintingsPage.tsx @@ -15,9 +15,11 @@ import { useTheme } from '@renderer/context/ThemeProvider' import { usePaintings } from '@renderer/hooks/usePaintings' import { useAllProviders } from '@renderer/hooks/useProvider' import { useRuntime } from '@renderer/hooks/useRuntime' +import { useSettings } from '@renderer/hooks/useSettings' import AiProvider from '@renderer/providers/AiProvider' import { getProviderByModel } from '@renderer/services/AssistantService' import FileManager from '@renderer/services/FileManager' +import { translateText } from '@renderer/services/TranslateService' import { useAppDispatch } from '@renderer/store' import { DEFAULT_PAINTING } from '@renderer/store/paintings' import { setGenerating } from '@renderer/store/runtime' @@ -25,7 +27,7 @@ import { FileType, Painting } from '@renderer/types' import { getErrorMessage } from '@renderer/utils' import { Button, Input, InputNumber, Radio, Select, Slider, Tooltip } from 'antd' 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 styled from 'styled-components' @@ -232,6 +234,59 @@ const PaintingsPage: FC = () => { setCurrentImageIndex(0) } + const { autoTranslateWithSpace } = useSettings() + const [spaceClickCount, setSpaceClickCount] = useState(0) + const [isTranslating, setIsTranslating] = useState(false) + const spaceClickTimer = useRef() + + 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) => { + 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 ( @@ -362,14 +417,16 @@ const PaintingsPage: FC = () => { disabled={isLoading} value={painting.prompt} onChange={(e) => updatePaintingState({ prompt: e.target.value })} - placeholder={t('paintings.prompt_placeholder')} + placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder')} + onKeyDown={handleKeyDown} /> updatePaintingState({ prompt: translatedText })} - disabled={isLoading} + disabled={isLoading || isTranslating} + isLoading={isTranslating} style={{ marginRight: 6, borderRadius: '50%' }} />