This commit is contained in:
kangfenmao 2025-11-01 12:12:01 +08:00
parent 4186e9c990
commit 1bf5104f97
13 changed files with 151 additions and 159 deletions

View File

@ -7,7 +7,6 @@ This file provides guidance to AI coding assistants when working with code in th
- **Keep it clear**: Write code that is easy to read, maintain, and explain.
- **Match the house style**: Reuse existing patterns, naming, and conventions.
- **Search smart**: Prefer `ast-grep` for semantic queries; fall back to `rg`/`grep` when needed.
- **Build with HeroUI**: Use HeroUI for every new UI component; never add `antd` or `styled-components`.
- **Log centrally**: Route all logging through `loggerService` with the right context—no `console.log`.
- **Research via subagent**: Lean on `subagent` for external docs, APIs, news, and references.
- **Always propose before executing**: Before making any changes, clearly explain your planned approach and wait for explicit user approval to ensure alignment and prevent unwanted modifications.
@ -41,7 +40,6 @@ This file provides guidance to AI coding assistants when working with code in th
- **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
### Logging
```typescript

View File

@ -1,4 +1,4 @@
import { Button } from '@heroui/button'
import { Button } from 'antd'
import { formatErrorMessage } from '@renderer/utils/error'
import { Alert, Space } from 'antd'
import type { ComponentType, ReactNode } from 'react'
@ -24,10 +24,10 @@ const DefaultFallback: ComponentType<FallbackProps> = (props: FallbackProps): Re
type="error"
action={
<Space>
<Button size="sm" onPress={debug}>
<Button size="small" onClick={debug}>
{t('error.boundary.default.devtools')}
</Button>
<Button size="sm" onPress={reload}>
<Button size="small" onClick={reload}>
{t('error.boundary.default.reload')}
</Button>
</Space>

View File

@ -1,4 +1,4 @@
import { Select, SelectItem } from '@heroui/react'
import { Select } from 'antd'
import { ProviderAvatarPrimitive } from '@renderer/components/ProviderAvatar'
import { getProviderLogo } from '@renderer/config/providers'
import ImageStorage from '@renderer/services/ImageStorage'
@ -54,46 +54,46 @@ const ProviderSelect: FC<ProviderSelectProps> = ({ provider, options, onChange,
return (
<Select
selectedKeys={[provider.id]}
onSelectionChange={(keys) => {
const selectedKey = Array.from(keys)[0] as string
onChange(selectedKey)
}}
style={style}
className={`w-full ${className || ''}`}
renderValue={(items) => {
return items.map((item) => (
<div key={item.key} className="flex items-center gap-2">
value={provider.id}
onChange={onChange}
style={{ width: '100%', ...style }}
className={className}
options={providerOptions}
labelRender={(props) => {
const providerId = props.value as string
const providerName = providerOptions.find((opt) => opt.value === providerId)?.label || ''
return (
<div className="flex items-center gap-2">
<div className="flex h-4 w-4 items-center justify-center">
<ProviderAvatarPrimitive
providerId={item.key as string}
providerName={item.textValue || ''}
logoSrc={getProviderLogoSrc(item.key as string)}
providerId={providerId}
providerName={providerName}
logoSrc={getProviderLogoSrc(providerId)}
size={16}
/>
</div>
<span>{item.textValue}</span>
<span>{providerName}</span>
</div>
))
}}>
{providerOptions.map((providerOption) => (
<SelectItem
key={providerOption.value}
textValue={providerOption.label}
startContent={
)
}}
optionRender={(option) => {
const providerId = option.value as string
const providerName = option.label as string
return (
<div className="flex items-center gap-2">
<div className="flex h-4 w-4 items-center justify-center">
<ProviderAvatarPrimitive
providerId={providerOption.value}
providerName={providerOption.label}
logoSrc={getProviderLogoSrc(providerOption.value)}
providerId={providerId}
providerName={providerName}
logoSrc={getProviderLogoSrc(providerId)}
size={16}
/>
</div>
}>
{providerOption.label}
</SelectItem>
))}
</Select>
<span>{providerName}</span>
</div>
)
}}
/>
)
}

View File

@ -1,4 +1,4 @@
import { Button, Tooltip } from '@heroui/react'
import { Button, Tooltip } from 'antd'
import { loggerService } from '@logger'
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
import { Plus } from 'lucide-react'
@ -65,21 +65,21 @@ export const AccessibleDirsSetting = ({ base, update }: AccessibleDirsSettingPro
<SettingsItem>
<SettingsTitle
actions={
<Tooltip content={t('agent.session.accessible_paths.add')}>
<Button variant="light" size="sm" startContent={<Plus />} isIconOnly onPress={addAccessiblePath} />
<Tooltip title={t('agent.session.accessible_paths.add')}>
<Button type="text" icon={<Plus size={16} />} shape="circle" onClick={addAccessiblePath} />
</Tooltip>
}>
{t('agent.session.accessible_paths.label')}
</SettingsTitle>
<ul className="flex flex-col gap-2">
<ul className="flex flex-col">
{base.accessible_paths.map((path) => (
<li
key={path}
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-2 py-1">
<span className="w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm" title={path}>
<li key={path} className="flex items-center justify-between gap-2 py-1">
<span
className="w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[var(--color-text-2)] text-sm"
title={path}>
{path}
</span>
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}>
<Button size="small" type="text" danger onClick={() => removeAccessiblePath(path)}>
{t('common.delete')}
</Button>
</li>

View File

@ -1,4 +1,4 @@
import { Input, Tooltip } from '@heroui/react'
import { InputNumber, Tooltip } from 'antd'
import type { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import type {
@ -31,34 +31,33 @@ const defaultConfiguration: AgentConfigurationState = AgentConfigurationSchema.p
export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ agentBase, update }) => {
const { t } = useTranslation()
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration)
const [maxTurnsInput, setMaxTurnsInput] = useState<string>(String(defaultConfiguration.max_turns))
const [maxTurnsInput, setMaxTurnsInput] = useState<number>(defaultConfiguration.max_turns)
useEffect(() => {
if (!agentBase) {
setConfiguration(defaultConfiguration)
setMaxTurnsInput(String(defaultConfiguration.max_turns))
setMaxTurnsInput(defaultConfiguration.max_turns)
return
}
const parsed: AgentConfigurationState = AgentConfigurationSchema.parse(agentBase.configuration ?? {})
setConfiguration(parsed)
setMaxTurnsInput(String(parsed.max_turns))
setMaxTurnsInput(parsed.max_turns)
}, [agentBase])
const commitMaxTurns = useCallback(() => {
if (!agentBase) return
const parsedValue = Number.parseInt(maxTurnsInput, 10)
if (!Number.isFinite(parsedValue)) {
setMaxTurnsInput(String(configuration.max_turns))
if (!Number.isFinite(maxTurnsInput)) {
setMaxTurnsInput(configuration.max_turns)
return
}
const sanitized = Math.max(1, parsedValue)
const sanitized = Math.max(1, maxTurnsInput)
if (sanitized === configuration.max_turns) {
setMaxTurnsInput(String(configuration.max_turns))
setMaxTurnsInput(configuration.max_turns)
return
}
const next: AgentConfigurationState = { ...configuration, max_turns: sanitized }
setConfiguration(next)
setMaxTurnsInput(String(sanitized))
setMaxTurnsInput(sanitized)
update({ id: agentBase.id, configuration: next } satisfies UpdateAgentBaseForm)
}, [agentBase, configuration, maxTurnsInput, update])
@ -71,27 +70,23 @@ export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ agentBase, u
<SettingsItem divider={false}>
<SettingsTitle
actions={
<Tooltip content={t('agent.settings.advance.maxTurns.description')} placement="right">
<Tooltip title={t('agent.settings.advance.maxTurns.description')} placement="left">
<Info size={16} className="text-foreground-400" />
</Tooltip>
}>
{t('agent.settings.advance.maxTurns.label')}
</SettingsTitle>
<div className="flex w-full flex-col gap-2">
<Input
type="number"
<div className="my-2 flex w-full flex-col gap-2">
<InputNumber
min={1}
value={maxTurnsInput}
onValueChange={setMaxTurnsInput}
onChange={(value) => setMaxTurnsInput(value ?? 1)}
onBlur={commitMaxTurns}
onKeyDown={(event) => {
if (event.key === 'Enter') {
commitMaxTurns()
}
}}
onPressEnter={commitMaxTurns}
aria-label={t('agent.settings.advance.maxTurns.label')}
style={{ width: '100%' }}
/>
<span className="text-foreground-500 text-xs">{t('agent.settings.advance.maxTurns.helper')}</span>
<span className="mt-1 text-foreground-500 text-xs">{t('agent.settings.advance.maxTurns.helper')}</span>
</div>
</SettingsItem>
</SettingsContainer>

View File

@ -1,4 +1,3 @@
import { Alert, Spinner } from '@heroui/react'
import { TopView } from '@renderer/components/TopView'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
@ -11,6 +10,8 @@ import PluginSettings from './PluginSettings'
import PromptSettings from './PromptSettings'
import { AgentLabel, LeftMenu, Settings, StyledMenu, StyledModal } from './shared'
import ToolingSettings from './ToolingSettings'
import { Center } from '@renderer/components/Layout'
import { Alert, Spin } from 'antd'
interface AgentSettingPopupShowParams {
agentId: string
@ -71,18 +72,25 @@ const AgentSettingPopupContainer: React.FC<AgentSettingPopupParams> = ({ tab, ag
const ModalContent = () => {
if (isLoading) {
// TODO: use skeleton for better ux
return <Spinner />
}
if (error) {
return (
<div>
<Alert color="danger" title={t('agent.get.error.failed')} />
</div>
<Center flex={1}>
<Spin />
</Center>
)
}
if (error) {
return (
<Center flex={1}>
<Alert type="error" message={t('agent.get.error.failed')} />
</Center>
)
}
if (!agent) {
return null
}
return (
<div className="flex w-full flex-1">
<LeftMenu>

View File

@ -1,9 +1,9 @@
import { Textarea } from '@heroui/react'
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingsItem, SettingsTitle } from './shared'
import TextArea from 'antd/es/input/TextArea'
export interface DescriptionSettingProps {
base: AgentBaseWithId | undefined | null
@ -24,11 +24,12 @@ export const DescriptionSetting = ({ base, update }: DescriptionSettingProps) =>
if (!base) return null
return (
<SettingsItem>
<SettingsItem divider={false}>
<SettingsTitle>{t('common.description')}</SettingsTitle>
<Textarea
<TextArea
value={description}
onValueChange={setDescription}
onChange={(e) => setDescription(e.target.value)}
rows={4}
onBlur={() => {
if (description !== base.description) {
updateDesc(description)

View File

@ -1,4 +1,4 @@
import { Input } from '@heroui/react'
import { Input } from 'antd'
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -25,14 +25,13 @@ export const NameSetting = ({ base, update }: NameSettingsProps) => {
<Input
placeholder={t('common.agent_one') + t('common.name')}
value={name}
size="sm"
onValueChange={(value) => setName(value)}
onChange={(e) => setName(e.target.value)}
onBlur={() => {
if (name !== base.name) {
updateName(name)
}
}}
className="max-w-80 flex-1"
className="max-w-70 flex-1"
/>
</SettingsItem>
)

View File

@ -1,4 +1,5 @@
import { Alert, Spinner } from '@heroui/react'
import { Alert, Spin } from 'antd'
import { Center } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView'
import { useSession } from '@renderer/hooks/agents/useSession'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
@ -68,15 +69,21 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
const ModalContent = () => {
if (isLoading) {
// TODO: use skeleton for better ux
return <Spinner />
}
if (error) {
return (
<div>
<Alert color="danger" title={t('agent.get.error.failed')} />
</div>
<Center flex={1}>
<Spin />
</Center>
)
}
if (error) {
return (
<Center flex={1}>
<Alert type="error" message={t('agent.get.error.failed')} />
</Center>
)
}
return (
<div className="flex w-full flex-1">
<LeftMenu>

View File

@ -6,8 +6,6 @@ import {
WifiOutlined,
YuqueOutlined
} from '@ant-design/icons'
import { Button } from '@heroui/button'
import { Switch } from '@heroui/switch'
import DividerWithText from '@renderer/components/DividerWithText'
import { NutstoreIcon } from '@renderer/components/Icons/NutstoreIcons'
import { HStack } from '@renderer/components/Layout'
@ -24,7 +22,7 @@ import { setSkipBackupFile as _setSkipBackupFile } from '@renderer/store/setting
import type { AppInfo } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { occupiedDirs } from '@shared/config/constant'
import { Progress, Typography } from 'antd'
import { Button, Progress, Switch, Typography } from 'antd'
import { FileText, FolderCog, FolderInput, FolderOpen, SaveIcon, Sparkle } from 'lucide-react'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
@ -295,16 +293,11 @@ const DataSettings: FC = () => {
<div>
<MigrationPathRow style={{ marginTop: '20px', flexDirection: 'row', alignItems: 'center' }}>
<Switch
defaultSelected={shouldCopyData}
onValueChange={(checked) => {
shouldCopyData = checked
}}
size="sm">
<span style={{ fontWeight: 'normal', fontSize: '14px' }}>
{t('settings.data.app_data.copy_data_option')}
</span>
</Switch>
defaultChecked={shouldCopyData}
onChange={(checked) => (shouldCopyData = checked)}
style={{ marginRight: '8px' }}
title={t('settings.data.app_data.copy_data_option')}
/>
<MigrationPathLabel style={{ fontWeight: 'normal', fontSize: '14px' }}>
{t('settings.data.app_data.copy_data_option')}
</MigrationPathLabel>
@ -614,10 +607,10 @@ const DataSettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
<HStack gap="5px" justifyContent="space-between">
<Button variant="ghost" size="sm" onPress={BackupPopup.show} startContent={<SaveIcon size={14} />}>
<Button onClick={BackupPopup.show} icon={<SaveIcon size={14} />}>
{t('settings.general.backup.button')}
</Button>
<Button variant="ghost" size="sm" onPress={RestorePopup.show} startContent={<FolderOpen size={14} />}>
<Button onClick={RestorePopup.show} icon={<FolderOpen size={14} />}>
{t('settings.general.restore.button')}
</Button>
</HStack>
@ -625,7 +618,7 @@ const DataSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch isSelected={skipBackupFile} onValueChange={onSkipBackupFilesChange} size="sm" />
<Switch checked={skipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
@ -634,11 +627,7 @@ const DataSettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.data.export_to_phone.title')}</SettingRowTitle>
<HStack gap="5px" justifyContent="space-between">
<Button
variant="ghost"
size="sm"
onPress={ExportToPhoneLanPopup.show}
startContent={<WifiOutlined />}>
<Button onClick={ExportToPhoneLanPopup.show} icon={<WifiOutlined size={14} />}>
{t('settings.data.export_to_phone.lan.title')}
</Button>
</HStack>
@ -657,9 +646,7 @@ const DataSettings: FC = () => {
</PathText>
<StyledIcon onClick={() => handleOpenPath(appInfo?.appDataPath)} style={{ flexShrink: 0 }} />
<HStack gap="5px" style={{ marginLeft: '8px' }}>
<Button variant="ghost" size="sm" onClick={handleSelectAppDataPath}>
{t('settings.data.app_data.select')}
</Button>
<Button onClick={handleSelectAppDataPath}>{t('settings.data.app_data.select')}</Button>
</HStack>
</PathRow>
</SettingRow>
@ -672,7 +659,7 @@ const DataSettings: FC = () => {
</PathText>
<StyledIcon onClick={() => handleOpenPath(appInfo?.logsPath)} style={{ flexShrink: 0 }} />
<HStack gap="5px" style={{ marginLeft: '8px' }}>
<Button variant="ghost" size="sm" onClick={() => handleOpenPath(appInfo?.logsPath)}>
<Button onClick={() => handleOpenPath(appInfo?.logsPath)}>
{t('settings.data.app_logs.button')}
</Button>
</HStack>
@ -682,9 +669,7 @@ const DataSettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.data.app_knowledge.label')}</SettingRowTitle>
<HStack alignItems="center" gap="5px">
<Button variant="ghost" size="sm" onClick={handleRemoveAllFiles}>
{t('settings.data.app_knowledge.button.delete')}
</Button>
<Button onClick={handleRemoveAllFiles}>{t('settings.data.app_knowledge.button.delete')}</Button>
</HStack>
</SettingRow>
<SettingDivider />
@ -694,16 +679,14 @@ const DataSettings: FC = () => {
{cacheSize && <CacheText>({cacheSize}MB)</CacheText>}
</SettingRowTitle>
<HStack gap="5px">
<Button variant="ghost" size="sm" onClick={handleClearCache}>
{t('settings.data.clear_cache.button')}
</Button>
<Button onClick={handleClearCache}>{t('settings.data.clear_cache.button')}</Button>
</HStack>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
<HStack gap="5px">
<Button variant="ghost" size="sm" onPress={reset} color="danger">
<Button onClick={reset} danger>
{t('settings.general.reset.title')}
</Button>
</HStack>

View File

@ -1,4 +1,3 @@
import { Alert, Skeleton } from '@heroui/react'
import { loggerService } from '@logger'
import { ErrorTag } from '@renderer/components/Tags/ErrorTag'
import { isMac, isWin } from '@renderer/config/constant'
@ -6,7 +5,7 @@ import { useOcrProviders } from '@renderer/hooks/useOcrProvider'
import type { ImageOcrProvider, OcrProvider } from '@renderer/types'
import { BuiltinOcrProviderIds, isImageOcrProvider } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils'
import { Select } from 'antd'
import { Alert, Select, Skeleton } from 'antd'
import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import useSWRImmutable from 'swr/immutable'
@ -70,27 +69,39 @@ const OcrImageSettings = ({ setProvider }: Props) => {
<SettingRowTitle>{t('settings.tool.ocr.image_provider')}</SettingRowTitle>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{!platformSupport && isSystem && <ErrorTag message={t('settings.tool.ocr.error.not_system')} />}
<Skeleton isLoaded={!isLoading}>
{!error && (
<Select
value={imageProvider.id}
style={{ width: '200px' }}
onChange={(id: string) => setImageProvider(id)}
options={options}
/>
)}
{error && (
<Alert
color="danger"
title={t('ocr.error.provider.get_providers')}
description={getErrorMessage(error)}
/>
)}
</Skeleton>
<OcrProviderSelector
isLoading={isLoading}
error={error}
value={imageProvider.id}
options={options}
onChange={setImageProvider}
/>
</div>
</SettingRow>
</>
)
}
type OcrProviderSelectorProps = {
isLoading: boolean
error: any
value: string
options: Array<{ value: string; label: string }>
onChange: (id: string) => void
}
const OcrProviderSelector = ({ isLoading, error, value, options, onChange }: OcrProviderSelectorProps) => {
const { t } = useTranslation()
if (isLoading) {
return <Skeleton.Input active style={{ width: '200px', height: '32px' }} />
}
if (error) {
return <Alert type="error" message={t('ocr.error.provider.get_providers')} description={getErrorMessage(error)} />
}
return <Select value={value} style={{ width: '200px' }} onChange={onChange} options={options} />
}
export default OcrImageSettings

View File

@ -1,4 +1,3 @@
// TODO: Refactor this component to use HeroUI
import { useTheme } from '@renderer/context/ThemeProvider'
import { useApiServer } from '@renderer/hooks/useApiServer'
import type { RootState } from '@renderer/store'

View File

@ -1,10 +1,9 @@
import { Switch } from '@heroui/react'
import LanguageSelect from '@renderer/components/LanguageSelect'
import { HStack } from '@renderer/components/Layout'
import db from '@renderer/databases'
import useTranslate from '@renderer/hooks/useTranslate'
import type { AutoDetectionMethod, Model, TranslateLanguage } from '@renderer/types'
import { Button, Flex, Modal, Radio, Space, Tooltip } from 'antd'
import { Button, Flex, Modal, Radio, Space, Switch, Tooltip } from 'antd'
import { HelpCircle } from 'lucide-react'
import type { FC } from 'react'
import { memo, useEffect, useState } from 'react'
@ -69,8 +68,8 @@ const TranslateSettings: FC<{
<Flex align="center" justify="space-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.preview')}</div>
<Switch
isSelected={enableMarkdown}
onValueChange={(checked) => {
checked={enableMarkdown}
onChange={(checked) => {
setEnableMarkdown(checked)
db.settings.put({ id: 'translate:markdown:enabled', value: checked })
}}
@ -81,13 +80,7 @@ const TranslateSettings: FC<{
<div>
<HStack alignItems="center" justifyContent="space-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.autoCopy')}</div>
<Switch
isSelected={autoCopy}
color="primary"
onValueChange={(isSelected) => {
updateSettings({ autoCopy: isSelected })
}}
/>
<Switch checked={autoCopy} onChange={(checked) => updateSettings({ autoCopy: checked })} />
</HStack>
</div>
@ -95,11 +88,10 @@ const TranslateSettings: FC<{
<Flex align="center" justify="space-between">
<div style={{ fontWeight: 500 }}>{t('translate.settings.scroll_sync')}</div>
<Switch
isSelected={isScrollSyncEnabled}
color="primary"
onValueChange={(isSelected) => {
setIsScrollSyncEnabled(isSelected)
db.settings.put({ id: 'translate:scroll:sync', value: isSelected })
checked={isScrollSyncEnabled}
onChange={(checked) => {
setIsScrollSyncEnabled(checked)
db.settings.put({ id: 'translate:scroll:sync', value: checked })
}}
/>
</Flex>
@ -149,10 +141,9 @@ const TranslateSettings: FC<{
</HStack>
</div>
<Switch
isSelected={isBidirectional}
color="primary"
onValueChange={(isSelected) => {
setIsBidirectional(isSelected)
checked={isBidirectional}
onChange={(checked) => {
setIsBidirectional(checked)
// 双向翻译设置不需要持久化,它只是界面状态
}}
/>