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. - **Keep it clear**: Write code that is easy to read, maintain, and explain.
- **Match the house style**: Reuse existing patterns, naming, and conventions. - **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. - **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`. - **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. - **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. - **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. - **Services** (`src/main/services/`): MCPService, KnowledgeService, WindowService, etc.
- **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces. - **Build System**: Electron-Vite with experimental rolldown-vite, yarn workspaces.
- **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state. - **State Management**: Redux Toolkit (`src/renderer/src/store/`) for predictable state.
- **UI Components**: HeroUI (`@heroui/*`) for all new UI elements.
### Logging ### Logging
```typescript ```typescript

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { Button, Tooltip } from '@heroui/react' import { Button, Tooltip } from 'antd'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types' import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
import { Plus } from 'lucide-react' import { Plus } from 'lucide-react'
@ -65,21 +65,21 @@ export const AccessibleDirsSetting = ({ base, update }: AccessibleDirsSettingPro
<SettingsItem> <SettingsItem>
<SettingsTitle <SettingsTitle
actions={ actions={
<Tooltip content={t('agent.session.accessible_paths.add')}> <Tooltip title={t('agent.session.accessible_paths.add')}>
<Button variant="light" size="sm" startContent={<Plus />} isIconOnly onPress={addAccessiblePath} /> <Button type="text" icon={<Plus size={16} />} shape="circle" onClick={addAccessiblePath} />
</Tooltip> </Tooltip>
}> }>
{t('agent.session.accessible_paths.label')} {t('agent.session.accessible_paths.label')}
</SettingsTitle> </SettingsTitle>
<ul className="flex flex-col gap-2"> <ul className="flex flex-col">
{base.accessible_paths.map((path) => ( {base.accessible_paths.map((path) => (
<li <li key={path} className="flex items-center justify-between gap-2 py-1">
key={path} <span
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-2 py-1"> className="w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[var(--color-text-2)] text-sm"
<span className="w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm" title={path}> title={path}>
{path} {path}
</span> </span>
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}> <Button size="small" type="text" danger onClick={() => removeAccessiblePath(path)}>
{t('common.delete')} {t('common.delete')}
</Button> </Button>
</li> </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 { useUpdateAgent } from '@renderer/hooks/agents/useUpdateAgent'
import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession' import type { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
import type { import type {
@ -31,34 +31,33 @@ const defaultConfiguration: AgentConfigurationState = AgentConfigurationSchema.p
export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ agentBase, update }) => { export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ agentBase, update }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration) const [configuration, setConfiguration] = useState<AgentConfigurationState>(defaultConfiguration)
const [maxTurnsInput, setMaxTurnsInput] = useState<string>(String(defaultConfiguration.max_turns)) const [maxTurnsInput, setMaxTurnsInput] = useState<number>(defaultConfiguration.max_turns)
useEffect(() => { useEffect(() => {
if (!agentBase) { if (!agentBase) {
setConfiguration(defaultConfiguration) setConfiguration(defaultConfiguration)
setMaxTurnsInput(String(defaultConfiguration.max_turns)) setMaxTurnsInput(defaultConfiguration.max_turns)
return return
} }
const parsed: AgentConfigurationState = AgentConfigurationSchema.parse(agentBase.configuration ?? {}) const parsed: AgentConfigurationState = AgentConfigurationSchema.parse(agentBase.configuration ?? {})
setConfiguration(parsed) setConfiguration(parsed)
setMaxTurnsInput(String(parsed.max_turns)) setMaxTurnsInput(parsed.max_turns)
}, [agentBase]) }, [agentBase])
const commitMaxTurns = useCallback(() => { const commitMaxTurns = useCallback(() => {
if (!agentBase) return if (!agentBase) return
const parsedValue = Number.parseInt(maxTurnsInput, 10) if (!Number.isFinite(maxTurnsInput)) {
if (!Number.isFinite(parsedValue)) { setMaxTurnsInput(configuration.max_turns)
setMaxTurnsInput(String(configuration.max_turns))
return return
} }
const sanitized = Math.max(1, parsedValue) const sanitized = Math.max(1, maxTurnsInput)
if (sanitized === configuration.max_turns) { if (sanitized === configuration.max_turns) {
setMaxTurnsInput(String(configuration.max_turns)) setMaxTurnsInput(configuration.max_turns)
return return
} }
const next: AgentConfigurationState = { ...configuration, max_turns: sanitized } const next: AgentConfigurationState = { ...configuration, max_turns: sanitized }
setConfiguration(next) setConfiguration(next)
setMaxTurnsInput(String(sanitized)) setMaxTurnsInput(sanitized)
update({ id: agentBase.id, configuration: next } satisfies UpdateAgentBaseForm) update({ id: agentBase.id, configuration: next } satisfies UpdateAgentBaseForm)
}, [agentBase, configuration, maxTurnsInput, update]) }, [agentBase, configuration, maxTurnsInput, update])
@ -71,27 +70,23 @@ export const AdvancedSettings: React.FC<AdvancedSettingsProps> = ({ agentBase, u
<SettingsItem divider={false}> <SettingsItem divider={false}>
<SettingsTitle <SettingsTitle
actions={ 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" /> <Info size={16} className="text-foreground-400" />
</Tooltip> </Tooltip>
}> }>
{t('agent.settings.advance.maxTurns.label')} {t('agent.settings.advance.maxTurns.label')}
</SettingsTitle> </SettingsTitle>
<div className="flex w-full flex-col gap-2"> <div className="my-2 flex w-full flex-col gap-2">
<Input <InputNumber
type="number"
min={1} min={1}
value={maxTurnsInput} value={maxTurnsInput}
onValueChange={setMaxTurnsInput} onChange={(value) => setMaxTurnsInput(value ?? 1)}
onBlur={commitMaxTurns} onBlur={commitMaxTurns}
onKeyDown={(event) => { onPressEnter={commitMaxTurns}
if (event.key === 'Enter') {
commitMaxTurns()
}
}}
aria-label={t('agent.settings.advance.maxTurns.label')} 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> </div>
</SettingsItem> </SettingsItem>
</SettingsContainer> </SettingsContainer>

View File

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

View File

@ -1,9 +1,9 @@
import { Textarea } from '@heroui/react'
import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types' import type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingsItem, SettingsTitle } from './shared' import { SettingsItem, SettingsTitle } from './shared'
import TextArea from 'antd/es/input/TextArea'
export interface DescriptionSettingProps { export interface DescriptionSettingProps {
base: AgentBaseWithId | undefined | null base: AgentBaseWithId | undefined | null
@ -24,11 +24,12 @@ export const DescriptionSetting = ({ base, update }: DescriptionSettingProps) =>
if (!base) return null if (!base) return null
return ( return (
<SettingsItem> <SettingsItem divider={false}>
<SettingsTitle>{t('common.description')}</SettingsTitle> <SettingsTitle>{t('common.description')}</SettingsTitle>
<Textarea <TextArea
value={description} value={description}
onValueChange={setDescription} onChange={(e) => setDescription(e.target.value)}
rows={4}
onBlur={() => { onBlur={() => {
if (description !== base.description) { if (description !== base.description) {
updateDesc(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 type { AgentBaseWithId, UpdateAgentBaseForm, UpdateAgentFunctionUnion } from '@renderer/types'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -25,14 +25,13 @@ export const NameSetting = ({ base, update }: NameSettingsProps) => {
<Input <Input
placeholder={t('common.agent_one') + t('common.name')} placeholder={t('common.agent_one') + t('common.name')}
value={name} value={name}
size="sm" onChange={(e) => setName(e.target.value)}
onValueChange={(value) => setName(value)}
onBlur={() => { onBlur={() => {
if (name !== base.name) { if (name !== base.name) {
updateName(name) updateName(name)
} }
}} }}
className="max-w-80 flex-1" className="max-w-70 flex-1"
/> />
</SettingsItem> </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 { TopView } from '@renderer/components/TopView'
import { useSession } from '@renderer/hooks/agents/useSession' import { useSession } from '@renderer/hooks/agents/useSession'
import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession' import { useUpdateSession } from '@renderer/hooks/agents/useUpdateSession'
@ -68,15 +69,21 @@ const SessionSettingPopupContainer: React.FC<SessionSettingPopupParams> = ({ tab
const ModalContent = () => { const ModalContent = () => {
if (isLoading) { if (isLoading) {
// TODO: use skeleton for better ux // TODO: use skeleton for better ux
return <Spinner />
}
if (error) {
return ( return (
<div> <Center flex={1}>
<Alert color="danger" title={t('agent.get.error.failed')} /> <Spin />
</div> </Center>
) )
} }
if (error) {
return (
<Center flex={1}>
<Alert type="error" message={t('agent.get.error.failed')} />
</Center>
)
}
return ( return (
<div className="flex w-full flex-1"> <div className="flex w-full flex-1">
<LeftMenu> <LeftMenu>

View File

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

View File

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

View File

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

View File

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