refactor(translate): reorganize translation settings and remove deprecated components

- Removed TranslateSettings and TranslateModelSettings components to streamline the translation settings interface.
- Introduced CustomLanguageSettings and TranslatePromptSettings components for better management of custom languages and prompt settings.
- Updated ModelSettings to utilize the new TranslateSettingsPopup for handling translation-related configurations.
- Enhanced the overall structure and readability of the translation settings page.
This commit is contained in:
kangfenmao 2025-08-11 18:10:56 +08:00
parent c666361611
commit 809a532a6c
9 changed files with 169 additions and 249 deletions

View File

@ -1,7 +1,6 @@
import { RedoOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
import ModelSelector from '@renderer/components/ModelSelector'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import { useTheme } from '@renderer/context/ThemeProvider'
@ -19,6 +18,7 @@ import { FC, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDescription, SettingGroup, SettingTitle } from '..'
import TranslateSettingsPopup from '../TranslateSettingsPopup/TranslateSettingsPopup'
import DefaultAssistantSettings from './DefaultAssistantSettings'
import TopicNamingModalPopup from './TopicNamingModalPopup'
@ -53,21 +53,6 @@ const ModelSettings: FC = () => {
[translateModel]
)
const onUpdateTranslateModel = async () => {
const prompt = await PromptPopup.show({
title: t('settings.models.translate_model_prompt_title'),
message: t('settings.models.translate_model_prompt_message'),
defaultValue: translateModelPrompt,
inputProps: {
rows: 10,
onPressEnter: () => {}
}
})
if (prompt) {
dispatch(setTranslateModelPrompt(prompt))
}
}
const onResetTranslatePrompt = () => {
dispatch(setTranslateModelPrompt(TRANSLATE_PROMPT))
}
@ -133,7 +118,11 @@ const ModelSettings: FC = () => {
onChange={(value) => setTranslateModel(find(allModels, JSON.parse(value)) as Model)}
placeholder={t('settings.models.empty')}
/>
<Button icon={<Settings2 size={16} />} style={{ marginLeft: 8 }} onClick={onUpdateTranslateModel} />
<Button
icon={<Settings2 size={16} />}
style={{ marginLeft: 8 }}
onClick={() => TranslateSettingsPopup.show()}
/>
{translateModelPrompt !== TRANSLATE_PROMPT && (
<Tooltip title={t('common.reset')}>
<Button icon={<RedoOutlined />} style={{ marginLeft: 8 }} onClick={onResetTranslatePrompt}></Button>

View File

@ -7,7 +7,6 @@ import {
FolderCog,
HardDrive,
Info,
Languages,
MonitorCog,
Package,
PictureInPicture2,
@ -31,7 +30,6 @@ import QuickAssistantSettings from './QuickAssistantSettings'
import SelectionAssistantSettings from './SelectionAssistantSettings/SelectionAssistantSettings'
import ShortcutSettings from './ShortcutSettings'
import ToolSettings from './ToolSettings'
import TranslateSettings from './TranslateSettings/TranslateSettings'
const SettingsPage: FC = () => {
const { pathname } = useLocation()
@ -82,12 +80,6 @@ const SettingsPage: FC = () => {
{t('settings.mcp.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/translate">
<MenuItem className={isRoute('/settings/translate')}>
<Languages size={18} />
{t('settings.translate.title')}
</MenuItem>
</MenuItemLink>
<MenuItemLink to="/settings/memory">
<MenuItem className={isRoute('/settings/memory')}>
<Brain size={18} />
@ -131,7 +123,6 @@ const SettingsPage: FC = () => {
<Route path="model" element={<ModelSettings />} />
<Route path="tool/*" element={<ToolSettings />} />
<Route path="mcp/*" element={<MCPSettings />} />
<Route path="translate" element={<TranslateSettings />} />
<Route path="memory" element={<MemorySettings />} />
<Route path="general/*" element={<GeneralSettings />} />
<Route path="display" element={<DisplaySettings />} />

View File

@ -1,56 +0,0 @@
import { HStack } from '@renderer/components/Layout'
import ModelSelector from '@renderer/components/ModelSelector'
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
import { Model } from '@renderer/types'
import { find } from 'lodash'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingDescription, SettingGroup, SettingTitle } from '..'
const TranslateModelSettings = () => {
const { t } = useTranslation()
const { theme } = useTheme()
const { providers } = useProviders()
const { translateModel, setTranslateModel } = useDefaultModel()
const allModels = useMemo(() => providers.map((p) => p.models).flat(), [providers])
const modelPredicate = useCallback(
(m: Model) => !isEmbeddingModel(m) && !isRerankModel(m) && !isTextToImageModel(m),
[]
)
const defaultTranslateModel = useMemo(
() => (hasModel(translateModel) ? getModelUniqId(translateModel) : undefined),
[translateModel]
)
return (
<SettingGroup theme={theme}>
<SettingTitle style={{ marginBottom: 12 }}>
<HStack alignItems="center" gap={10}>
{t('settings.models.translate_model')}
</HStack>
</SettingTitle>
<HStack alignItems="center">
<ModelSelector
providers={providers}
predicate={modelPredicate}
value={defaultTranslateModel}
defaultValue={defaultTranslateModel}
style={{ width: 360 }}
onChange={(value) => setTranslateModel(find(allModels, JSON.parse(value)) as Model)}
placeholder={t('settings.models.empty')}
/>
</HStack>
<SettingDescription>{t('settings.models.translate_model_description')}</SettingDescription>
</SettingGroup>
)
}
export default TranslateModelSettings

View File

@ -1,51 +0,0 @@
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
import { useTheme } from '@renderer/context/ThemeProvider'
import CustomLanguageSettings from '@renderer/pages/settings/TranslateSettings/CustomLanguageSettings'
import { getAllCustomLanguages } from '@renderer/services/TranslateService'
import { CustomTranslateLanguage } from '@renderer/types'
import { Suspense, useEffect, useState } from 'react'
import { SettingContainer, SettingGroup } from '..'
import TranslateModelSettings from './TranslateModelSettings'
import TranslatePromptSettings from './TranslatePromptSettings'
const TranslateSettings = () => {
const { theme } = useTheme()
const [dataPromise, setDataPromise] = useState<Promise<CustomTranslateLanguage[]>>(Promise.resolve([]))
useEffect(() => {
setDataPromise(getAllCustomLanguages())
}, [])
return (
<>
<SettingContainer theme={theme}>
<TranslateModelSettings />
<TranslatePromptSettings />
<SettingGroup theme={theme} style={{ flex: 1 }}>
<Suspense fallback={<CustomLanguagesSettingsFallback />}>
<CustomLanguageSettings dataPromise={dataPromise} />
</Suspense>
</SettingGroup>
</SettingContainer>
</>
)
}
const CustomLanguagesSettingsFallback = () => {
return (
<div
style={{
width: '100%',
height: 250,
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<SvgSpinners180Ring />
</div>
)
}
export default TranslateSettings

View File

@ -98,7 +98,9 @@ const CustomLanguageModal = ({ isOpen, editingCustomLanguage, onAdd, onEdit, onC
footer={footer}
onCancel={onCancel}
maskClosable={false}
transitionName="animation-move-down"
forceRender
centered
styles={{
body: {
padding: '20px'

View File

@ -1,20 +1,19 @@
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { HStack } from '@renderer/components/Layout'
import { deleteCustomLanguage } from '@renderer/services/TranslateService'
import { deleteCustomLanguage, getAllCustomLanguages } from '@renderer/services/TranslateService'
import { CustomTranslateLanguage } from '@renderer/types'
import { Button, Popconfirm, Space, Table, TableProps } from 'antd'
import { memo, startTransition, use, useCallback, useEffect, useMemo, useState } from 'react'
import { memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingRowTitle } from '..'
import CustomLanguageModal from './CustomLanguageModal'
type Props = {
dataPromise: Promise<CustomTranslateLanguage[]>
}
const logger = loggerService.withContext('CustomLanguageSettings')
const CustomLanguageSettings = ({ dataPromise }: Props) => {
const CustomLanguageSettings = () => {
const { t } = useTranslation()
const [displayedItems, setDisplayedItems] = useState<CustomTranslateLanguage[]>([])
const [isModalOpen, setIsModalOpen] = useState(false)
@ -104,18 +103,28 @@ const CustomLanguageSettings = ({ dataPromise }: Props) => {
[onDelete, t]
)
const data = use(dataPromise)
useEffect(() => {
setDisplayedItems(data)
}, [data])
const loadData = async () => {
try {
const data = await getAllCustomLanguages()
setDisplayedItems(data)
} catch (error) {
logger.error('Failed to load custom languages:', error as Error)
}
}
loadData()
}, [])
return (
<>
<CustomLanguageSettingsContainer>
<HStack justifyContent="space-between" style={{ padding: '4px 0' }}>
<SettingRowTitle>{t('translate.custom.label')}</SettingRowTitle>
<Button type="primary" icon={<PlusOutlined size={16} />} onClick={onClickAdd}>
<Button
type="primary"
icon={<PlusOutlined size={16} />}
onClick={onClickAdd}
style={{ marginBottom: 5, marginTop: -5 }}>
{t('common.add')}
</Button>
</HStack>

View File

@ -45,7 +45,8 @@ const TranslatePromptSettings = () => {
onChange={(e) => setLocalPrompt(e.target.value)}
onBlur={(e) => dispatch(setTranslateModelPrompt(e.target.value))}
autoSize={{ minRows: 4, maxRows: 10 }}
placeholder={t('settings.models.translate_model_prompt_message')}></Input.TextArea>
placeholder={t('settings.models.translate_model_prompt_message')}
/>
</SettingGroup>
)
}

View File

@ -0,0 +1,74 @@
import { TopView } from '@renderer/components/TopView'
import { useTheme } from '@renderer/context/ThemeProvider'
import { Modal } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingGroup } from '..'
import CustomLanguageSettings from './CustomLanguageSettings'
import TranslatePromptSettings from './TranslatePromptSettings'
interface Props {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const { theme } = useTheme()
const { t } = useTranslation()
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
TranslateSettingsPopup.hide = onCancel
return (
<Modal
title={t('settings.translate.title')}
open={open}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="animation-move-down"
width="80vw"
centered>
<SettingContainer theme={theme} style={{ padding: '10px 0' }}>
<TranslatePromptSettings />
<SettingGroup theme={theme} style={{ flex: 1 }}>
<CustomLanguageSettings />
</SettingGroup>
</SettingContainer>
</Modal>
)
}
const TopViewKey = 'TranslateSettingsPopup'
export default class TranslateSettingsPopup {
static topviewId = 0
static hide() {
TopView.hide(TopViewKey)
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
TopView.hide(TopViewKey)
}}
/>,
TopViewKey
)
})
}
}

View File

@ -1,18 +1,14 @@
import { RedoOutlined } from '@ant-design/icons'
import LanguageSelect from '@renderer/components/LanguageSelect'
import { HStack } from '@renderer/components/Layout'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import db from '@renderer/databases'
import { useSettings } from '@renderer/hooks/useSettings'
import useTranslate from '@renderer/hooks/useTranslate'
import { useAppDispatch } from '@renderer/store'
import { setTranslateModelPrompt } from '@renderer/store/settings'
import { Model, TranslateLanguage } from '@renderer/types'
import { Button, Flex, Input, Modal, Space, Switch, Tooltip } from 'antd'
import { ChevronDown, HelpCircle } from 'lucide-react'
import { Button, Flex, Modal, Space, Switch, Tooltip } from 'antd'
import { HelpCircle } from 'lucide-react'
import { FC, memo, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import TranslateSettingsPopup from '../settings/TranslateSettingsPopup/TranslateSettingsPopup'
const TranslateSettings: FC<{
visible: boolean
@ -39,37 +35,16 @@ const TranslateSettings: FC<{
setBidirectionalPair
}) => {
const { t } = useTranslation()
const { translateModelPrompt } = useSettings()
const dispatch = useAppDispatch()
const [localPair, setLocalPair] = useState<[TranslateLanguage, TranslateLanguage]>(bidirectionalPair)
const [showPrompt, setShowPrompt] = useState(false)
const [localPrompt, setLocalPrompt] = useState(translateModelPrompt)
const { getLanguageByLangcode } = useTranslate()
useEffect(() => {
setLocalPair(bidirectionalPair)
setLocalPrompt(translateModelPrompt)
}, [bidirectionalPair, translateModelPrompt, visible])
}, [bidirectionalPair, visible])
const handleSave = () => {
if (localPair[0] === localPair[1]) {
window.message.warning({
content: t('translate.language.same'),
key: 'translate-message'
})
return
}
setBidirectionalPair(localPair)
db.settings.put({ id: 'translate:bidirectional:pair', value: [localPair[0].langCode, localPair[1].langCode] })
db.settings.put({ id: 'translate:scroll:sync', value: isScrollSyncEnabled })
db.settings.put({ id: 'translate:markdown:enabled', value: enableMarkdown })
db.settings.put({ id: 'translate:model:prompt', value: localPrompt })
dispatch(setTranslateModelPrompt(localPrompt))
window.message.success({
content: t('message.save.success.title'),
key: 'translate-settings-save'
})
const onMoreSetting = () => {
onClose()
TranslateSettingsPopup.show()
}
return (
@ -78,27 +53,33 @@ const TranslateSettings: FC<{
open={visible}
onCancel={onClose}
centered={true}
footer={[
<Button key="cancel" onClick={onClose}>
{t('common.cancel')}
</Button>,
<Button key="save" type="primary" onClick={handleSave}>
{t('common.save')}
</Button>
]}
width={420}>
<Flex vertical gap={16} style={{ marginTop: 16 }}>
footer={null}
width={420}
transitionName="animation-move-down">
<Flex vertical gap={16} style={{ marginTop: 16, paddingBottom: 20 }}>
<div>
<Flex align="center" justify="space-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.preview')}</div>
<Switch checked={enableMarkdown} onChange={setEnableMarkdown} />
<Switch
checked={enableMarkdown}
onChange={(checked) => {
setEnableMarkdown(checked)
db.settings.put({ id: 'translate:markdown:enabled', value: checked })
}}
/>
</Flex>
</div>
<div>
<Flex align="center" justify="space-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.scroll_sync')}</div>
<Switch checked={isScrollSyncEnabled} onChange={setIsScrollSyncEnabled} />
<Switch
checked={isScrollSyncEnabled}
onChange={(checked) => {
setIsScrollSyncEnabled(checked)
db.settings.put({ id: 'translate:scroll:sync', value: checked })
}}
/>
</Flex>
</div>
@ -114,7 +95,13 @@ const TranslateSettings: FC<{
</Tooltip>
</HStack>
</div>
<Switch checked={isBidirectional} onChange={setIsBidirectional} />
<Switch
checked={isBidirectional}
onChange={(checked) => {
setIsBidirectional(checked)
// 双向翻译设置不需要持久化,它只是界面状态
}}
/>
</Flex>
{isBidirectional && (
<Space direction="vertical" style={{ width: '100%', marginTop: 8 }}>
@ -122,78 +109,52 @@ const TranslateSettings: FC<{
<LanguageSelect
style={{ flex: 1 }}
value={localPair[0].langCode}
onChange={(value) => setLocalPair([getLanguageByLangcode(value), localPair[1]])}
onChange={(value) => {
const newPair: [TranslateLanguage, TranslateLanguage] = [getLanguageByLangcode(value), localPair[1]]
if (newPair[0] === newPair[1]) {
window.message.warning({
content: t('translate.language.same'),
key: 'translate-message'
})
return
}
setLocalPair(newPair)
setBidirectionalPair(newPair)
db.settings.put({
id: 'translate:bidirectional:pair',
value: [newPair[0].langCode, newPair[1].langCode]
})
}}
/>
<span></span>
<LanguageSelect
style={{ flex: 1 }}
value={localPair[1].langCode}
onChange={(value) => setLocalPair([localPair[0], getLanguageByLangcode(value)])}
onChange={(value) => {
const newPair: [TranslateLanguage, TranslateLanguage] = [localPair[0], getLanguageByLangcode(value)]
if (newPair[0] === newPair[1]) {
window.message.warning({
content: t('translate.language.same'),
key: 'translate-message'
})
return
}
setLocalPair(newPair)
setBidirectionalPair(newPair)
db.settings.put({
id: 'translate:bidirectional:pair',
value: [newPair[0].langCode, newPair[1].langCode]
})
}}
/>
</Flex>
</Space>
)}
</div>
<div>
<Flex align="center" justify="space-between">
<div
style={{
fontWeight: 500,
display: 'flex',
alignItems: 'center',
cursor: 'pointer'
}}
onClick={() => setShowPrompt(!showPrompt)}>
{t('settings.models.translate_model_prompt_title')}
<ChevronDown
size={16}
style={{
transform: showPrompt ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.3s',
marginLeft: 5
}}
/>
</div>
{localPrompt !== TRANSLATE_PROMPT && (
<Tooltip title={t('common.reset')}>
<Button
icon={<RedoOutlined />}
size="small"
type="text"
onClick={() => setLocalPrompt(TRANSLATE_PROMPT)}
/>
</Tooltip>
)}
</Flex>
</div>
<div style={{ display: showPrompt ? 'block' : 'none' }}>
<Textarea
rows={8}
value={localPrompt}
onChange={(e) => setLocalPrompt(e.target.value)}
placeholder={t('settings.models.translate_model_prompt_message')}
style={{ borderRadius: '8px' }}
/>
</div>
<Button onClick={onMoreSetting}>{t('settings.moresetting.label')}</Button>
</Flex>
</Modal>
)
}
export default memo(TranslateSettings)
const Textarea = styled(Input.TextArea)`
display: flex;
flex: 1;
font-size: 16px;
border-radius: 0;
.ant-input {
resize: none;
padding: 5px 16px;
}
.ant-input-clear-icon {
font-size: 16px;
}
`