feat: new settings ui

This commit is contained in:
kangfenmao 2024-11-19 19:54:18 +08:00
parent 335ce4963b
commit 0ec61e1c47
13 changed files with 305 additions and 258 deletions

View File

@ -51,42 +51,42 @@ const Sidebar: FC = () => {
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} /> <AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus> <MainMenus>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('assistants.title')} mouseEnterDelay={0.5} placement="right"> <Tooltip title={t('assistants.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/')}> <StyledLink onClick={() => to('/')}>
<Icon className={isRoute('/')}> <Icon className={isRoute('/')}>
<i className="iconfont icon-chat" /> <i className="iconfont icon-chat" />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip> </Tooltip>
<Tooltip title={t('agents.title')} mouseEnterDelay={0.5} placement="right"> <Tooltip title={t('agents.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/agents')}> <StyledLink onClick={() => to('/agents')}>
<Icon className={isRoutes('/agents')}> <Icon className={isRoutes('/agents')}>
<i className="iconfont icon-business-smart-assistant" /> <i className="iconfont icon-business-smart-assistant" />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip> </Tooltip>
<Tooltip title={t('paintings.title')} mouseEnterDelay={0.5} placement="right"> <Tooltip title={t('paintings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/paintings')}> <StyledLink onClick={() => to('/paintings')}>
<Icon className={isRoute('/paintings')}> <Icon className={isRoute('/paintings')}>
<PictureOutlined style={{ fontSize: 16 }} /> <PictureOutlined style={{ fontSize: 16 }} />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip> </Tooltip>
<Tooltip title={t('translate.title')} mouseEnterDelay={0.5} placement="right"> <Tooltip title={t('translate.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/translate')}> <StyledLink onClick={() => to('/translate')}>
<Icon className={isRoute('/translate')}> <Icon className={isRoute('/translate')}>
<TranslationOutlined /> <TranslationOutlined />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip> </Tooltip>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.5} placement="right"> <Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/apps')}> <StyledLink onClick={() => to('/apps')}>
<Icon className={isRoute('/apps')}> <Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore" /> <i className="iconfont icon-appstore" />
</Icon> </Icon>
</StyledLink> </StyledLink>
</Tooltip> </Tooltip>
<Tooltip title={t('files.title')} mouseEnterDelay={0.5} placement="right"> <Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/files')}> <StyledLink onClick={() => to('/files')}>
<Icon className={isRoute('/files')}> <Icon className={isRoute('/files')}>
<FolderOutlined /> <FolderOutlined />
@ -96,7 +96,7 @@ const Sidebar: FC = () => {
</Menus> </Menus>
</MainMenus> </MainMenus>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.5} placement="right"> <Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
<Icon onClick={() => toggleTheme()}> <Icon onClick={() => toggleTheme()}>
{theme === 'dark' ? ( {theme === 'dark' ? (
<i className="iconfont icon-theme icon-dark1" /> <i className="iconfont icon-theme icon-dark1" />
@ -105,7 +105,7 @@ const Sidebar: FC = () => {
)} )}
</Icon> </Icon>
</Tooltip> </Tooltip>
<Tooltip title={t('settings.title')} mouseEnterDelay={0.5} placement="right"> <Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}> <StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}> <Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
<i className="iconfont icon-setting" /> <i className="iconfont icon-setting" />
@ -183,7 +183,8 @@ const Icon = styled.div`
background-color: var(--color-active); background-color: var(--color-active);
.iconfont, .iconfont,
.anticon { .anticon {
color: var(--color-icon-white); color: var(--color-primary);
font-weight: bold;
} }
} }
` `

View File

@ -302,8 +302,11 @@
"provider.api.url.preview": "Preview: {{url}}", "provider.api.url.preview": "Preview: {{url}}",
"provider.api.url.tip": "Ending with / ignores v1, ending with # forces use of input address", "provider.api.url.tip": "Ending with / ignores v1, ending with # forces use of input address",
"models.default_assistant_model": "Default Assistant Model", "models.default_assistant_model": "Default Assistant Model",
"models.default_assistant_model_description": "Model used when creating a new assistant, if the assistant is not set, this model will be used",
"models.topic_naming_model": "Topic Naming Model", "models.topic_naming_model": "Topic Naming Model",
"models.topic_naming_model_description": "Model used when automatically naming a new topic",
"models.translate_model": "Translate Model", "models.translate_model": "Translate Model",
"models.translate_model_description": "Model used for translation service",
"models.add.add_model": "Add Model", "models.add.add_model": "Add Model",
"models.add.model_id.placeholder": "Required e.g. gpt-3.5-turbo", "models.add.model_id.placeholder": "Required e.g. gpt-3.5-turbo",
"models.add.model_id": "Model ID", "models.add.model_id": "Model ID",

View File

@ -302,8 +302,11 @@
"provider.api.url.preview": "Предпросмотр: {{url}}", "provider.api.url.preview": "Предпросмотр: {{url}}",
"provider.api.url.tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес", "provider.api.url.tip": "Заканчивая на / игнорирует v1, заканчивая на # принудительно использует введенный адрес",
"models.default_assistant_model": "Модель ассистента по умолчанию", "models.default_assistant_model": "Модель ассистента по умолчанию",
"models.default_assistant_model_description": "Модель, используемая при создании нового ассистента, если ассистент не имеет настроенной модели, будет использоваться эта модель",
"models.topic_naming_model": "Модель именования топика", "models.topic_naming_model": "Модель именования топика",
"models.topic_naming_model_description": "Модель, используемая при автоматическом именовании нового топика",
"models.translate_model": "Модель перевода", "models.translate_model": "Модель перевода",
"models.translate_model_description": "Модель, используемая для сервиса перевода",
"models.add.add_model": "Добавить модель", "models.add.add_model": "Добавить модель",
"models.add.model_id.placeholder": "Обязательно, например, gpt-3.5-turbo", "models.add.model_id.placeholder": "Обязательно, например, gpt-3.5-turbo",
"models.add.model_id": "ID модели", "models.add.model_id": "ID модели",

View File

@ -290,8 +290,11 @@
"advanced.title": "高级设置", "advanced.title": "高级设置",
"advanced.auto_switch_to_topics": "自动切换到话题", "advanced.auto_switch_to_topics": "自动切换到话题",
"models.default_assistant_model": "默认助手模型", "models.default_assistant_model": "默认助手模型",
"models.default_assistant_model_description": "创建新助手时使用的模型,如果助手未设置模型,则使用此模型",
"models.topic_naming_model": "话题命名模型", "models.topic_naming_model": "话题命名模型",
"models.topic_naming_model_description": "自动命名新话题时使用的模型",
"models.translate_model": "翻译模型", "models.translate_model": "翻译模型",
"models.translate_model_description": "翻译服务使用的模型",
"models.add.add_model": "添加模型", "models.add.add_model": "添加模型",
"models.add.model_id.placeholder": "必填 例如 gpt-3.5-turbo", "models.add.model_id.placeholder": "必填 例如 gpt-3.5-turbo",
"models.add.model_id": "模型 ID", "models.add.model_id": "模型 ID",

View File

@ -290,8 +290,11 @@
"advanced.title": "進階設定", "advanced.title": "進階設定",
"advanced.auto_switch_to_topics": "自動切換到話題", "advanced.auto_switch_to_topics": "自動切換到話題",
"models.default_assistant_model": "預設助手模型", "models.default_assistant_model": "預設助手模型",
"models.default_assistant_model_description": "創建新助手時使用的模型,如果助手未設置模型,則使用此模型",
"models.topic_naming_model": "話題命名模型", "models.topic_naming_model": "話題命名模型",
"models.topic_naming_model_description": "自動命名新話題時使用的模型",
"models.translate_model": "翻譯模型", "models.translate_model": "翻譯模型",
"models.translate_model_description": "翻譯服務使用的模型",
"models.add.add_model": "添加模型", "models.add.add_model": "添加模型",
"models.add.model_id.placeholder": "必填,例如 gpt-3.5-turbo", "models.add.model_id.placeholder": "必填,例如 gpt-3.5-turbo",
"models.add.model_id": "模型 ID", "models.add.model_id": "模型 ID",

View File

@ -3,6 +3,7 @@ import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp' import MinApp from '@renderer/components/MinApp'
import { APP_NAME, AppLogo } from '@renderer/config/env' import { APP_NAME, AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setManualUpdateCheck } from '@renderer/store/settings' import { setManualUpdateCheck } from '@renderer/store/settings'
@ -15,7 +16,7 @@ import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingRow, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitle } from '.'
const AboutSettings: FC = () => { const AboutSettings: FC = () => {
const [version, setVersion] = useState('') const [version, setVersion] = useState('')
@ -24,6 +25,7 @@ const AboutSettings: FC = () => {
const [checkUpdateLoading, setCheckUpdateLoading] = useState(false) const [checkUpdateLoading, setCheckUpdateLoading] = useState(false)
const [downloading, setDownloading] = useState(false) const [downloading, setDownloading] = useState(false)
const { manualUpdateCheck } = useSettings() const { manualUpdateCheck } = useSettings()
const { theme } = useTheme()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const onCheckUpdate = debounce( const onCheckUpdate = debounce(
@ -104,102 +106,104 @@ const AboutSettings: FC = () => {
}, [t]) }, [t])
return ( return (
<SettingContainer> <SettingContainer theme={theme}>
<SettingTitle> <SettingGroup theme={theme}>
{t('settings.about.title')} <SettingTitle>
<HStack alignItems="center"> {t('settings.about.title')}
<Link to="https://github.com/kangfenmao/cherry-studio"> <HStack alignItems="center">
<GithubOutlined style={{ marginRight: 4, color: 'var(--color-text)', fontSize: 20 }} /> <Link to="https://github.com/kangfenmao/cherry-studio">
</Link> <GithubOutlined style={{ marginRight: 4, color: 'var(--color-text)', fontSize: 20 }} />
</HStack> </Link>
</SettingTitle> </HStack>
<SettingDivider /> </SettingTitle>
<AboutHeader> <SettingDivider />
<Row align="middle"> <AboutHeader>
<AvatarWrapper onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio')}> <Row align="middle">
{percent > 0 && ( <AvatarWrapper onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio')}>
<ProgressCircle {percent > 0 && (
type="circle" <ProgressCircle
size={84} type="circle"
percent={percent} size={84}
showInfo={false} percent={percent}
strokeLinecap="butt" showInfo={false}
strokeColor="#67ad5b" strokeLinecap="butt"
/> strokeColor="#67ad5b"
)} />
<Avatar src={AppLogo} size={80} style={{ minHeight: 80 }} /> )}
</AvatarWrapper> <Avatar src={AppLogo} size={80} style={{ minHeight: 80 }} />
<VersionWrapper> </AvatarWrapper>
<Title>{APP_NAME}</Title> <VersionWrapper>
<Description>{t('settings.about.description')}</Description> <Title>{APP_NAME}</Title>
<Tag <Description>{t('settings.about.description')}</Description>
onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')} <Tag
color="cyan" onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}
style={{ marginTop: 8, cursor: 'pointer' }}> color="cyan"
v{version} style={{ marginTop: 8, cursor: 'pointer' }}>
</Tag> v{version}
</VersionWrapper> </Tag>
</Row> </VersionWrapper>
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}> </Row>
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')} <CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}>
</CheckUpdateButton> {downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
</AboutHeader> </CheckUpdateButton>
<SettingDivider /> </AboutHeader>
<SettingRow> <SettingDivider />
<SettingRowTitle>{t('settings.general.manually_check_update.title')}</SettingRowTitle> <SettingRow>
<Switch value={manualUpdateCheck} onChange={(v) => dispatch(setManualUpdateCheck(v))} /> <SettingRowTitle>{t('settings.general.manually_check_update.title')}</SettingRowTitle>
</SettingRow> <Switch value={manualUpdateCheck} onChange={(v) => dispatch(setManualUpdateCheck(v))} />
<SettingDivider /> </SettingRow>
<SettingRow> </SettingGroup>
<SettingRowTitle> <SettingGroup theme={theme}>
<SoundOutlined /> <SettingRow>
{t('settings.about.releases.title')} <SettingRowTitle>
</SettingRowTitle> <SoundOutlined />
<Button {t('settings.about.releases.title')}
onClick={() => </SettingRowTitle>
MinApp.start({ <Button
name: t('settings.about.releases.title'), onClick={() =>
url: 'https://github.com/kangfenmao/cherry-studio/releases', MinApp.start({
logo: AppLogo name: t('settings.about.releases.title'),
}) url: 'https://github.com/kangfenmao/cherry-studio/releases',
}> logo: AppLogo
{t('settings.about.releases.button')} })
</Button> }>
</SettingRow> {t('settings.about.releases.button')}
<SettingDivider /> </Button>
<SettingRow> </SettingRow>
<SettingRowTitle> <SettingDivider />
<GlobalOutlined /> <SettingRow>
{t('settings.about.website.title')} <SettingRowTitle>
</SettingRowTitle> <GlobalOutlined />
<Button onClick={() => onOpenWebsite('https://cherry-ai.com')}>{t('settings.about.website.button')}</Button> {t('settings.about.website.title')}
</SettingRow> </SettingRowTitle>
<SettingDivider /> <Button onClick={() => onOpenWebsite('https://cherry-ai.com')}>{t('settings.about.website.button')}</Button>
<SettingRow> </SettingRow>
<SettingRowTitle> <SettingDivider />
<GithubOutlined /> <SettingRow>
{t('settings.about.feedback.title')} <SettingRowTitle>
</SettingRowTitle> <GithubOutlined />
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/issues/new')}> {t('settings.about.feedback.title')}
{t('settings.about.feedback.button')} </SettingRowTitle>
</Button> <Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/issues/new')}>
</SettingRow> {t('settings.about.feedback.button')}
<SettingDivider /> </Button>
<SettingRow> </SettingRow>
<SettingRowTitle> <SettingDivider />
<FileProtectOutlined /> <SettingRow>
{t('settings.about.license.title')} <SettingRowTitle>
</SettingRowTitle> <FileProtectOutlined />
<Button onClick={showLicense}>{t('settings.about.license.button')}</Button> {t('settings.about.license.title')}
</SettingRow> </SettingRowTitle>
<SettingDivider /> <Button onClick={showLicense}>{t('settings.about.license.button')}</Button>
<SettingRow> </SettingRow>
<SettingRowTitle> <SettingDivider />
<MailOutlined /> {t('settings.about.contact.title')} <SettingRow>
</SettingRowTitle> <SettingRowTitle>
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button> <MailOutlined /> {t('settings.about.contact.title')}
</SettingRow> </SettingRowTitle>
<SettingDivider /> <Button onClick={mailto}>{t('settings.about.contact.button')}</Button>
</SettingRow>
</SettingGroup>
</SettingContainer> </SettingContainer>
) )
} }

View File

@ -2,6 +2,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant' import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultAssistant } from '@renderer/hooks/useAssistant' import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { AssistantSettings as AssistantSettingsType } from '@renderer/types' import { AssistantSettings as AssistantSettingsType } from '@renderer/types'
import { Button, Col, Input, InputNumber, Modal, Row, Slider, Switch, Tooltip } from 'antd' import { Button, Col, Input, InputNumber, Modal, Row, Slider, Switch, Tooltip } from 'antd'
@ -18,6 +19,7 @@ const AssistantSettings: FC = () => {
const [contextCount, setContextCount] = useState(defaultAssistant.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT) const [contextCount, setContextCount] = useState(defaultAssistant.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(defaultAssistant?.settings?.enableMaxTokens ?? false) const [enableMaxTokens, setEnableMaxTokens] = useState(defaultAssistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(defaultAssistant?.settings?.maxTokens ?? 0) const [maxTokens, setMaxTokens] = useState(defaultAssistant?.settings?.maxTokens ?? 0)
const { theme } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
@ -67,7 +69,7 @@ const AssistantSettings: FC = () => {
} }
return ( return (
<SettingContainer style={{ height: 'auto' }}> <SettingContainer style={{ height: 'auto', background: 'transparent', padding: 0 }} theme={theme}>
<SettingSubtitle style={{ marginTop: 0 }}>{t('common.name')}</SettingSubtitle> <SettingSubtitle style={{ marginTop: 0 }}>{t('common.name')}</SettingSubtitle>
<Input <Input
placeholder={t('common.assistant') + t('common.name')} placeholder={t('common.assistant') + t('common.name')}

View File

@ -1,5 +1,6 @@
import { FileSearchOutlined, FolderOpenOutlined, SaveOutlined } from '@ant-design/icons' import { FileSearchOutlined, FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
import { HStack, VStack } from '@renderer/components/Layout' import { HStack, VStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider'
import { backup, reset, restore } from '@renderer/services/BackupService' import { backup, reset, restore } from '@renderer/services/BackupService'
import { AppInfo } from '@renderer/types' import { AppInfo } from '@renderer/types'
import { Button, Typography } from 'antd' import { Button, Typography } from 'antd'
@ -14,12 +15,14 @@ import WebDavSettings from './WebDavSettings'
const DataSettings: FC = () => { const DataSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [appInfo, setAppInfo] = useState<AppInfo>() const [appInfo, setAppInfo] = useState<AppInfo>()
const { theme } = useTheme()
useEffect(() => { useEffect(() => {
window.api.getAppInfo().then(setAppInfo) window.api.getAppInfo().then(setAppInfo)
}, []) }, [])
const handleOpenPath = (path: string) => { const handleOpenPath = (path?: string) => {
if (!path) return
if (path?.endsWith('log')) { if (path?.endsWith('log')) {
const dirPath = path.split(/[/\\]/).slice(0, -1).join('/') const dirPath = path.split(/[/\\]/).slice(0, -1).join('/')
window.api.openPath(dirPath) window.api.openPath(dirPath)
@ -29,8 +32,8 @@ const DataSettings: FC = () => {
} }
return ( return (
<SettingContainer> <SettingContainer theme={theme}>
<SettingGroup> <SettingGroup theme={theme}>
<SettingTitle>{t('settings.data')}</SettingTitle> <SettingTitle>{t('settings.data')}</SettingTitle>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
@ -63,10 +66,10 @@ const DataSettings: FC = () => {
</HStack> </HStack>
</SettingRow> </SettingRow>
</SettingGroup> </SettingGroup>
<SettingGroup> <SettingGroup theme={theme}>
<WebDavSettings /> <WebDavSettings />
</SettingGroup> </SettingGroup>
<SettingGroup> <SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.title')}</SettingTitle> <SettingTitle>{t('settings.data.title')}</SettingTitle>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>

View File

@ -1,4 +1,5 @@
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
@ -10,7 +11,7 @@ import { Input, Select, Space, Switch } from 'antd'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.'
const GeneralSettings: FC = () => { const GeneralSettings: FC = () => {
const { const {
@ -25,6 +26,7 @@ const GeneralSettings: FC = () => {
proxyMode: storeProxyMode proxyMode: storeProxyMode
} = useSettings() } = useSettings()
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl) const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
const { theme: themeMode } = useTheme()
const updateTray = (value: boolean) => { const updateTray = (value: boolean) => {
setTray(value) setTray(value)
@ -76,82 +78,88 @@ const GeneralSettings: FC = () => {
] ]
return ( return (
<SettingContainer> <SettingContainer theme={themeMode}>
<SettingTitle>{t('settings.general.title')}</SettingTitle> <SettingGroup theme={theme}>
<SettingDivider /> <SettingTitle>{t('settings.general.title')}</SettingTitle>
<SettingRow> <SettingDivider />
<SettingRowTitle>{t('common.language')}</SettingRowTitle> <SettingRow>
<Select defaultValue={language || 'en-US'} style={{ width: 180 }} onChange={onSelectLanguage}> <SettingRowTitle>{t('common.language')}</SettingRowTitle>
{languagesOptions.map((lang) => ( <Select defaultValue={language || 'en-US'} style={{ width: 180 }} onChange={onSelectLanguage}>
<Select.Option key={lang.value} value={lang.value}> {languagesOptions.map((lang) => (
<Space.Compact direction="horizontal" block> <Select.Option key={lang.value} value={lang.value}>
<Space.Compact block>{lang.label}</Space.Compact> <Space.Compact direction="horizontal" block>
<span role="img" aria-label={lang.flag}> <Space.Compact block>{lang.label}</Space.Compact>
{lang.flag} <span role="img" aria-label={lang.flag}>
</span> {lang.flag}
</Space.Compact> </span>
</Select.Option> </Space.Compact>
))} </Select.Option>
</Select> ))}
</SettingRow> </Select>
<SettingDivider /> </SettingRow>
<SettingRow> <SettingDivider />
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle> <SettingRow>
<Select <SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
defaultValue={theme} <Select
style={{ width: 180 }} defaultValue={theme}
onChange={setTheme} style={{ width: 180 }}
options={[ onChange={setTheme}
{ value: ThemeMode.light, label: t('settings.theme.light') }, options={[
{ value: ThemeMode.dark, label: t('settings.theme.dark') }, { value: ThemeMode.light, label: t('settings.theme.light') },
{ value: ThemeMode.auto, label: t('settings.theme.auto') } { value: ThemeMode.dark, label: t('settings.theme.dark') },
]} { value: ThemeMode.auto, label: t('settings.theme.auto') }
/> ]}
</SettingRow> />
{isMac && ( </SettingRow>
<> {isMac && (
<SettingDivider /> <>
<SettingRow> <SettingDivider />
<SettingRowTitle>{t('settings.theme.window.style.title')}</SettingRowTitle> <SettingRow>
<Select <SettingRowTitle>{t('settings.theme.window.style.title')}</SettingRowTitle>
defaultValue={windowStyle || 'opaque'} <Select
style={{ width: 180 }} defaultValue={windowStyle || 'opaque'}
onChange={setWindowStyle} style={{ width: 180 }}
options={[ onChange={setWindowStyle}
{ value: 'transparent', label: t('settings.theme.window.style.transparent') }, options={[
{ value: 'opaque', label: t('settings.theme.window.style.opaque') } { value: 'transparent', label: t('settings.theme.window.style.transparent') },
]} { value: 'opaque', label: t('settings.theme.window.style.opaque') }
/> ]}
</SettingRow> />
</> </SettingRow>
)} </>
<SettingDivider /> )}
<SettingRow> <SettingDivider />
<SettingRowTitle>{t('settings.proxy.mode.title')}</SettingRowTitle> <SettingRow>
<Select value={storeProxyMode} style={{ width: 180 }} onChange={onProxyModeChange} options={proxyModeOptions} /> <SettingRowTitle>{t('settings.proxy.mode.title')}</SettingRowTitle>
</SettingRow> <Select
{storeProxyMode === 'custom' && ( value={storeProxyMode}
<> style={{ width: 180 }}
<SettingDivider /> onChange={onProxyModeChange}
<SettingRow> options={proxyModeOptions}
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle> />
<Input </SettingRow>
placeholder="socks5://127.0.0.1:6153" {storeProxyMode === 'custom' && (
value={proxyUrl} <>
onChange={(e) => setProxyUrl(e.target.value)} <SettingDivider />
style={{ width: 180 }} <SettingRow>
onBlur={() => onSetProxyUrl()} <SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
type="url" <Input
/> placeholder="socks5://127.0.0.1:6153"
</SettingRow> value={proxyUrl}
</> onChange={(e) => setProxyUrl(e.target.value)}
)} style={{ width: 180 }}
<SettingDivider /> onBlur={() => onSetProxyUrl()}
<SettingRow> type="url"
<SettingRowTitle>{t('settings.tray.title')}</SettingRowTitle> />
<Switch checked={tray} onChange={(checked) => updateTray(checked)} /> </SettingRow>
</SettingRow> </>
<SettingDivider /> )}
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.tray.title')}</SettingRowTitle>
<Switch checked={tray} onChange={(checked) => updateTray(checked)} />
</SettingRow>
</SettingGroup>
</SettingContainer> </SettingContainer>
) )
} }

View File

@ -1,5 +1,6 @@
import { EditOutlined, MessageOutlined, SettingOutlined, TranslationOutlined } from '@ant-design/icons' import { EditOutlined, MessageOutlined, SettingOutlined, TranslationOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useDefaultModel } from '@renderer/hooks/useAssistant' import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId, hasModel } from '@renderer/services/ModelService' import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
@ -9,7 +10,7 @@ import { find, sortBy } from 'lodash'
import { FC, useMemo } from 'react' import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDivider, SettingTitle } from '.' import { SettingContainer, SettingDescription, SettingDivider, SettingGroup, SettingTitle } from '.'
import AssistantSettingsPopup from './AssistantSettings' import AssistantSettingsPopup from './AssistantSettings'
const ModelSettings: FC = () => { const ModelSettings: FC = () => {
@ -17,6 +18,7 @@ const ModelSettings: FC = () => {
useDefaultModel() useDefaultModel()
const { providers } = useProviders() const { providers } = useProviders()
const allModels = providers.map((p) => p.models).flat() const allModels = providers.map((p) => p.models).flat()
const { theme } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
const selectOptions = providers const selectOptions = providers
@ -46,57 +48,64 @@ const ModelSettings: FC = () => {
) )
return ( return (
<SettingContainer> <SettingContainer theme={theme}>
<SettingTitle> <SettingGroup theme={theme}>
<div> <SettingTitle>
<MessageOutlined style={iconStyle} /> <div>
{t('settings.models.default_assistant_model')} <MessageOutlined style={iconStyle} />
</div> {t('settings.models.default_assistant_model')}
</SettingTitle> </div>
<SettingDivider /> </SettingTitle>
<HStack alignItems="center"> <SettingDivider />
<HStack alignItems="center">
<Select
value={defaultModelValue}
defaultValue={defaultModelValue}
style={{ width: 360 }}
onChange={(value) => setDefaultModel(find(allModels, JSON.parse(value)) as Model)}
options={selectOptions}
placeholder={t('settings.models.empty')}
/>
<Button icon={<SettingOutlined />} style={{ marginLeft: 8 }} onClick={() => AssistantSettingsPopup.show()} />
</HStack>
<SettingDescription>{t('settings.models.default_assistant_model_description')}</SettingDescription>
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>
<div>
<EditOutlined style={iconStyle} />
{t('settings.models.topic_naming_model')}
</div>
</SettingTitle>
<SettingDivider />
<Select <Select
value={defaultModelValue} value={defaultTopicNamingModel}
defaultValue={defaultModelValue} defaultValue={defaultTopicNamingModel}
style={{ width: 360 }} style={{ width: 360 }}
onChange={(value) => setDefaultModel(find(allModels, JSON.parse(value)) as Model)} onChange={(value) => setTopicNamingModel(find(allModels, JSON.parse(value)) as Model)}
options={selectOptions} options={selectOptions}
placeholder={t('settings.models.empty')} placeholder={t('settings.models.empty')}
/> />
<Button icon={<SettingOutlined />} style={{ marginLeft: 8 }} onClick={() => AssistantSettingsPopup.show()} /> <SettingDescription>{t('settings.models.topic_naming_model_description')}</SettingDescription>
</HStack> </SettingGroup>
<div style={{ height: 30 }} /> <SettingGroup theme={theme}>
<SettingTitle> <SettingTitle>
<div> <div>
<EditOutlined style={iconStyle} /> <TranslationOutlined style={iconStyle} />
{t('settings.models.topic_naming_model')} {t('settings.models.translate_model')}
</div> </div>
</SettingTitle> </SettingTitle>
<SettingDivider /> <SettingDivider />
<Select <Select
value={defaultTopicNamingModel} value={defaultTranslateModel}
defaultValue={defaultTopicNamingModel} defaultValue={defaultTranslateModel}
style={{ width: 360 }} style={{ width: 360 }}
onChange={(value) => setTopicNamingModel(find(allModels, JSON.parse(value)) as Model)} onChange={(value) => setTranslateModel(find(allModels, JSON.parse(value)) as Model)}
options={selectOptions} options={selectOptions}
placeholder={t('settings.models.empty')} placeholder={t('settings.models.empty')}
/> />
<div style={{ height: 30 }} /> <SettingDescription>{t('settings.models.translate_model_description')}</SettingDescription>
<SettingTitle> </SettingGroup>
<div>
<TranslationOutlined style={iconStyle} />
{t('settings.models.translate_model')}
</div>
</SettingTitle>
<SettingDivider />
<Select
value={defaultTranslateModel}
defaultValue={defaultTranslateModel}
style={{ width: 360 }}
onChange={(value) => setTranslateModel(find(allModels, JSON.parse(value)) as Model)}
options={selectOptions}
placeholder={t('settings.models.empty')}
/>
</SettingContainer> </SettingContainer>
) )
} }

View File

@ -126,12 +126,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
} }
return ( return (
<SettingContainer <SettingContainer theme={theme}>
style={
theme === 'dark'
? { backgroundColor: 'var(--color-background)' }
: { backgroundColor: 'var(--color-background-mute)' }
}>
<SettingTitle> <SettingTitle>
<Flex align="center"> <Flex align="center">
<span>{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}</span> <span>{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}</span>

View File

@ -1,11 +1,12 @@
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useTheme } from '@renderer/context/ThemeProvider'
import { Switch, Table as AntTable, Tag } from 'antd' import { Switch, Table as AntTable, Tag } from 'antd'
import type { ColumnsType } from 'antd/es/table' import type { ColumnsType } from 'antd/es/table'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '.'
interface ShortcutItem { interface ShortcutItem {
key: string key: string
@ -16,6 +17,7 @@ interface ShortcutItem {
const ShortcutSettings: FC = () => { const ShortcutSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme()
const commandKey = isMac ? '⌘' : 'Ctrl' const commandKey = isMac ? '⌘' : 'Ctrl'
@ -81,16 +83,18 @@ const ShortcutSettings: FC = () => {
] ]
return ( return (
<SettingContainer> <SettingContainer theme={theme}>
<SettingTitle>{t('settings.shortcuts.title')}</SettingTitle> <SettingGroup theme={theme}>
<SettingDivider style={{ marginBottom: 0 }} /> <SettingTitle>{t('settings.shortcuts.title')}</SettingTitle>
<Table <SettingDivider style={{ marginBottom: 0 }} />
columns={columns as ColumnsType<unknown>} <Table
dataSource={shortcuts} columns={columns as ColumnsType<unknown>}
pagination={false} dataSource={shortcuts}
size="middle" pagination={false}
showHeader={false} size="middle"
/> showHeader={false}
/>
</SettingGroup>
</SettingContainer> </SettingContainer>
) )
} }

View File

@ -1,15 +1,17 @@
import { ThemeMode } from '@renderer/types'
import { Divider } from 'antd' import { Divider } from 'antd'
import Link from 'antd/es/typography/Link' import Link from 'antd/es/typography/Link'
import styled from 'styled-components' import styled from 'styled-components'
export const SettingContainer = styled.div` export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
padding: 15px; padding: 20px;
overflow-y: scroll; overflow-y: scroll;
font-family: Ubuntu; font-family: Ubuntu;
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
@ -22,7 +24,7 @@ export const SettingTitle = styled.div`
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
user-select: none; user-select: none;
font-size: 16px; font-size: 14px;
font-weight: bold; font-weight: bold;
` `
@ -34,6 +36,12 @@ export const SettingSubtitle = styled.div`
font-weight: bold; font-weight: bold;
` `
export const SettingDescription = styled.div`
font-size: 12px;
color: var(--color-text-3);
margin-top: 10px;
`
export const SettingDivider = styled(Divider)` export const SettingDivider = styled(Divider)`
margin: 10px 0; margin: 10px 0;
` `
@ -70,9 +78,10 @@ export const SettingHelpLink = styled(Link)`
padding: 0 5px; padding: 0 5px;
` `
export const SettingGroup = styled.div` export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
margin-bottom: 16px; margin-bottom: 20px;
border-radius: 8px; border-radius: 8px;
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
padding: 16px; padding: 16px;
background: ${(props) => (props.theme === 'dark' ? 'var(--color-background-soft)' : 'var(--color-background)')};
` `