feat: Extract ModelEditContent into separate component and refine UI styling

- Create new ModelEditContent component in ProviderSettings
- Separate model editing logic from ProviderSetting
- Adjust styling for AssistantItem and TopicsTab list items
- Refine Segmented component styling with transparent background
- Improve modal layout and interaction for model type configuration
This commit is contained in:
kangfenmao 2025-03-01 00:40:55 +08:00
parent 8e1207c2a2
commit 1d4916c516
5 changed files with 226 additions and 222 deletions

View File

@ -133,10 +133,9 @@ const Container = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
padding: 7px 12px; padding: 7px 10px;
position: relative; position: relative;
margin: 0 10px; margin: 0 10px;
padding-right: 35px;
font-family: Ubuntu; font-family: Ubuntu;
border-radius: var(--list-item-border-radius); border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent; border: 0.5px solid transparent;
@ -152,7 +151,6 @@ const Container = styled.div`
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
.name { .name {
font-weight: 500;
} }
} }
` `

View File

@ -388,7 +388,6 @@ const TopicListItem = styled.div`
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);
.name { .name {
font-weight: 500;
} }
.menu { .menu {
opacity: 1; opacity: 1;

View File

@ -159,6 +159,12 @@ const TabContent = styled.div`
` `
const Segmented = styled(AntSegmented)` const Segmented = styled(AntSegmented)`
&.ant-segmented {
background-color: transparent;
border-radius: 0 !important;
border-bottom: 0.5px solid var(--color-border);
padding-bottom: 10px;
}
.ant-segmented-item { .ant-segmented-item {
overflow: hidden; overflow: hidden;
transition: none !important; transition: none !important;
@ -202,14 +208,8 @@ const Segmented = styled(AntSegmented)`
border-radius: var(--list-item-border-radius); border-radius: var(--list-item-border-radius);
box-shadow: none; box-shadow: none;
} }
/* Added styles from AntdProvider */
&.ant-segmented {
background-color: transparent;
}
/* These styles ensure the same appearance as before */ /* These styles ensure the same appearance as before */
border-radius: 16px; border-radius: 0;
box-shadow: none; box-shadow: none;
` `

View File

@ -0,0 +1,204 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons'
import { isEmbeddingModel, isReasoningModel, isVisionModel } from '@renderer/config/models'
import { Model, ModelType } from '@renderer/types'
import { getDefaultGroupName } from '@renderer/utils'
import { Button, Checkbox, Divider, Flex, Form, Input, Modal } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface ModelEditContentProps {
model: Model
onUpdateModel: (model: Model) => void
open: boolean
onClose: () => void
}
const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, open, onClose }) => {
const [form] = Form.useForm()
const { t } = useTranslation()
const [showModelTypes, setShowModelTypes] = useState(false)
const onFinish = (values: any) => {
const updatedModel = {
...model,
id: values.id || model.id,
name: values.name || model.name,
group: values.group || model.group
}
onUpdateModel(updatedModel)
setShowModelTypes(false)
onClose()
}
const handleClose = () => {
setShowModelTypes(false)
onClose()
}
return (
<Modal
title={t('models.edit')}
open={open}
onCancel={handleClose}
footer={null}
maskClosable={false}
centered
afterOpenChange={(visible) => {
if (visible) {
form.getFieldInstance('id')?.focus()
} else {
setShowModelTypes(false)
}
}}>
<Form
form={form}
labelCol={{ flex: '110px' }}
labelAlign="left"
colon={false}
style={{ marginTop: 15 }}
initialValues={{
id: model.id,
name: model.name,
group: model.group
}}
onFinish={onFinish}>
<Form.Item
name="id"
label={t('settings.models.add.model_id')}
tooltip={t('settings.models.add.model_id.tooltip')}
rules={[{ required: true }]}>
<Input
placeholder={t('settings.models.add.model_id.placeholder')}
spellCheck={false}
maxLength={200}
disabled={true}
onChange={(e) => {
const value = e.target.value
form.setFieldValue('name', value)
form.setFieldValue('group', getDefaultGroupName(value))
}}
/>
</Form.Item>
<Form.Item
name="name"
label={t('settings.models.add.model_name')}
tooltip={t('settings.models.add.model_name.tooltip')}>
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item
name="group"
label={t('settings.models.add.group_name')}
tooltip={t('settings.models.add.group_name.tooltip')}>
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
<Flex justify="center" align="center" style={{ position: 'relative' }}>
<div>
<Button type="primary" htmlType="submit" size="middle">
{t('common.save')}
</Button>
</div>
<MoreSettingsRow
onClick={() => setShowModelTypes(!showModelTypes)}
style={{ position: 'absolute', right: 0 }}>
{t('settings.moresetting')}
<ExpandIcon>{showModelTypes ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
</MoreSettingsRow>
</Flex>
</Form.Item>
{showModelTypes && (
<div>
<Divider style={{ margin: '0 0 15px 0' }} />
<TypeTitle>{t('models.type.select')}:</TypeTitle>
{(() => {
const defaultTypes = [
...(isVisionModel(model) ? ['vision'] : []),
...(isEmbeddingModel(model) ? ['embedding'] : []),
...(isReasoningModel(model) ? ['reasoning'] : [])
] as ModelType[]
// 合并现有选择和默认类型
const selectedTypes = [...new Set([...(model.type || []), ...defaultTypes])]
const showTypeConfirmModal = (type: string) => {
Modal.confirm({
title: t('settings.moresetting.warn'),
content: t('settings.moresetting.check.warn'),
okText: t('settings.moresetting.check.confirm'),
cancelText: t('common.cancel'),
okButtonProps: { danger: true },
cancelButtonProps: { type: 'primary' },
onOk: () => onUpdateModel({ ...model, type: [...selectedTypes, type] as ModelType[] }),
onCancel: () => {},
centered: true
})
}
const handleTypeChange = (types: string[]) => {
const newType = types.find((type) => !selectedTypes.includes(type as ModelType))
if (newType) {
showTypeConfirmModal(newType)
} else {
onUpdateModel({ ...model, type: types as ModelType[] })
}
}
return (
<Checkbox.Group
value={selectedTypes}
onChange={handleTypeChange}
options={[
{
label: t('models.type.vision'),
value: 'vision',
disabled: isVisionModel(model) && !selectedTypes.includes('vision')
},
{
label: t('models.type.embedding'),
value: 'embedding',
disabled: isEmbeddingModel(model) && !selectedTypes.includes('embedding')
},
{
label: t('models.type.reasoning'),
value: 'reasoning',
disabled: isReasoningModel(model) && !selectedTypes.includes('reasoning')
}
]}
/>
)
})()}
</div>
)}
</Form>
</Modal>
)
}
const TypeTitle = styled.div`
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
`
const ExpandIcon = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MoreSettingsRow = styled.div`
display: flex;
align-items: center;
gap: 8px;
color: var(--color-text-3);
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: var(--color-background-soft);
}
`
export default ModelEditContent

View File

@ -1,18 +1,16 @@
import { import {
CheckOutlined, CheckOutlined,
DownOutlined,
EditOutlined, EditOutlined,
ExportOutlined, ExportOutlined,
LoadingOutlined, LoadingOutlined,
MinusCircleOutlined, MinusCircleOutlined,
PlusOutlined, PlusOutlined,
SettingOutlined, SettingOutlined
UpOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import ModelTags from '@renderer/components/ModelTags' import ModelTags from '@renderer/components/ModelTags'
import OAuthButton from '@renderer/components/OAuth/OAuthButton' import OAuthButton from '@renderer/components/OAuth/OAuthButton'
import { getModelLogo, isEmbeddingModel, isReasoningModel, isVisionModel } from '@renderer/config/models' import { getModelLogo } from '@renderer/config/models'
import { PROVIDER_CONFIG } from '@renderer/config/providers' import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
@ -23,11 +21,10 @@ import { checkApi } from '@renderer/services/ApiService'
import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/services/ProviderService' import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/services/ProviderService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setModel } from '@renderer/store/assistants' import { setModel } from '@renderer/store/assistants'
import { Model, ModelType, Provider } from '@renderer/types' import { Model, Provider } from '@renderer/types'
import { getDefaultGroupName } from '@renderer/utils'
import { formatApiHost } from '@renderer/utils/api' import { formatApiHost } from '@renderer/utils/api'
import { providerCharge } from '@renderer/utils/oauth' import { providerCharge } from '@renderer/utils/oauth'
import { Avatar, Button, Card, Checkbox, Divider, Flex, Form, Input, Modal, Space, Switch } from 'antd' import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd'
import Link from 'antd/es/typography/Link' import Link from 'antd/es/typography/Link'
import { groupBy, isEmpty } from 'lodash' import { groupBy, isEmpty } from 'lodash'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
@ -47,6 +44,7 @@ import ApiCheckPopup from './ApiCheckPopup'
import EditModelsPopup from './EditModelsPopup' import EditModelsPopup from './EditModelsPopup'
import GraphRAGSettings from './GraphRAGSettings' import GraphRAGSettings from './GraphRAGSettings'
import LMStudioSettings from './LMStudioSettings' import LMStudioSettings from './LMStudioSettings'
import ModelEditContent from './ModelEditContent'
import OllamSettings from './OllamaSettings' import OllamSettings from './OllamaSettings'
import SelectProviderModelPopup from './SelectProviderModelPopup' import SelectProviderModelPopup from './SelectProviderModelPopup'
@ -54,170 +52,6 @@ interface Props {
provider: Provider provider: Provider
} }
interface ModelEditContentProps {
model: Model
onUpdateModel: (model: Model) => void
open: boolean
onClose: () => void
}
const ModelEditContent: FC<ModelEditContentProps> = ({ model, onUpdateModel, open, onClose }) => {
const [form] = Form.useForm()
const { t } = useTranslation()
const [showModelTypes, setShowModelTypes] = useState(false)
const onFinish = (values: any) => {
const updatedModel = {
...model,
id: values.id || model.id,
name: values.name || model.name,
group: values.group || model.group
}
onUpdateModel(updatedModel)
setShowModelTypes(false)
onClose()
}
const handleClose = () => {
setShowModelTypes(false)
onClose()
}
return (
<Modal
title={t('models.edit')}
open={open}
onCancel={handleClose}
footer={null}
maskClosable={false}
centered
afterOpenChange={(visible) => {
if (visible) {
form.getFieldInstance('id')?.focus()
} else {
setShowModelTypes(false)
}
}}>
<Form
form={form}
labelCol={{ flex: '110px' }}
labelAlign="left"
colon={false}
style={{ marginTop: 15 }}
initialValues={{
id: model.id,
name: model.name,
group: model.group
}}
onFinish={onFinish}>
<Form.Item
name="id"
label={t('settings.models.add.model_id')}
tooltip={t('settings.models.add.model_id.tooltip')}
rules={[{ required: true }]}>
<Input
placeholder={t('settings.models.add.model_id.placeholder')}
spellCheck={false}
maxLength={200}
onChange={(e) => {
const value = e.target.value
form.setFieldValue('name', value)
form.setFieldValue('group', getDefaultGroupName(value))
}}
/>
</Form.Item>
<Form.Item
name="name"
label={t('settings.models.add.model_name')}
tooltip={t('settings.models.add.model_name.tooltip')}>
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item
name="group"
label={t('settings.models.add.group_name')}
tooltip={t('settings.models.add.group_name.tooltip')}>
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
</Form.Item>
<Form.Item style={{ marginBottom: 15, textAlign: 'center' }}>
<Flex justify="center" align="center" style={{ position: 'relative' }}>
<div>
<Button type="primary" htmlType="submit" size="middle">
{t('common.save')}
</Button>
</div>
<MoreSettingsRow
onClick={() => setShowModelTypes(!showModelTypes)}
style={{ position: 'absolute', right: 0 }}>
{t('settings.moresetting')}
<ExpandIcon>{showModelTypes ? <UpOutlined /> : <DownOutlined />}</ExpandIcon>
</MoreSettingsRow>
</Flex>
</Form.Item>
<Divider style={{ margin: '0 0 15px 0' }} />
{showModelTypes && (
<div>
<TypeTitle>{t('models.type.select')}:</TypeTitle>
{(() => {
const defaultTypes = [
...(isVisionModel(model) ? ['vision'] : []),
...(isEmbeddingModel(model) ? ['embedding'] : []),
...(isReasoningModel(model) ? ['reasoning'] : [])
] as ModelType[]
// 合并现有选择和默认类型
const selectedTypes = [...new Set([...(model.type || []), ...defaultTypes])]
const showTypeConfirmModal = (type: string) => {
Modal.confirm({
title: t('settings.moresetting.warn'),
content: t('settings.moresetting.check.warn'),
okText: t('settings.moresetting.check.confirm'),
cancelText: t('common.cancel'),
okButtonProps: { danger: true },
cancelButtonProps: { type: 'primary' },
onOk: () => onUpdateModel({ ...model, type: [...selectedTypes, type] as ModelType[] }),
onCancel: () => {},
centered: true
})
}
const handleTypeChange = (types: string[]) => {
const newType = types.find((type) => !selectedTypes.includes(type as ModelType))
if (newType) {
showTypeConfirmModal(newType)
} else {
onUpdateModel({ ...model, type: types as ModelType[] })
}
}
return (
<Checkbox.Group
value={selectedTypes}
onChange={handleTypeChange}
options={[
{
label: t('models.type.vision'),
value: 'vision',
disabled: isVisionModel(model) && !selectedTypes.includes('vision')
},
{
label: t('models.type.embedding'),
value: 'embedding',
disabled: isEmbeddingModel(model) && !selectedTypes.includes('embedding')
},
{
label: t('models.type.reasoning'),
value: 'reasoning',
disabled: isReasoningModel(model) && !selectedTypes.includes('reasoning')
}
]}
/>
)
})()}
</div>
)}
</Form>
</Modal>
)
}
const ProviderSetting: FC<Props> = ({ provider: _provider }) => { const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
const { provider } = useProvider(_provider.id) const { provider } = useProvider(_provider.id)
const [apiKey, setApiKey] = useState(provider.apiKey) const [apiKey, setApiKey] = useState(provider.apiKey)
@ -361,17 +195,6 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
} }
} }
const modelTypeContent = (model: Model) => {
return (
<ModelEditContent
model={model}
onUpdateModel={onUpdateModel}
open={editingModel?.id === model.id}
onClose={() => setEditingModel(null)}
/>
)
}
const formatApiKeys = (value: string) => { const formatApiKeys = (value: string) => {
return value.replaceAll('', ',').replaceAll(' ', ',').replaceAll(' ', '').replaceAll('\n', ',') return value.replaceAll('', ',').replaceAll(' ', ',').replaceAll(' ', '').replaceAll('\n', ',')
} }
@ -528,7 +351,15 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
{t('button.add')} {t('button.add')}
</Button> </Button>
</Flex> </Flex>
{models.map((model) => modelTypeContent(model))} {models.map((model) => (
<ModelEditContent
model={model}
onUpdateModel={onUpdateModel}
open={editingModel?.id === model.id}
onClose={() => setEditingModel(null)}
key={model.id}
/>
))}
</SettingContainer> </SettingContainer>
) )
} }
@ -577,32 +408,4 @@ const ProviderName = styled.span`
font-weight: 500; font-weight: 500;
` `
const TypeTitle = styled.div`
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
`
const ExpandIcon = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MoreSettingsRow = styled.div`
display: flex;
align-items: center;
gap: 8px;
color: var(--color-text-3);
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: var(--color-background-soft);
}
`
export default ProviderSetting export default ProviderSetting