feat: Add model editing functionality to provider settings (#2243)
This commit is contained in:
parent
c0117c25ac
commit
a7a82be083
@ -5,6 +5,7 @@ import {
|
|||||||
addProvider,
|
addProvider,
|
||||||
removeModel,
|
removeModel,
|
||||||
removeProvider,
|
removeProvider,
|
||||||
|
updateModel,
|
||||||
updateProvider,
|
updateProvider,
|
||||||
updateProviders
|
updateProviders
|
||||||
} from '@renderer/store/llm'
|
} from '@renderer/store/llm'
|
||||||
@ -51,7 +52,8 @@ export function useProvider(id: string) {
|
|||||||
models: provider?.models || [],
|
models: provider?.models || [],
|
||||||
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
|
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
|
||||||
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
|
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
|
||||||
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))
|
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model })),
|
||||||
|
updateModel: (model: Model) => dispatch(updateModel({ providerId: id, model }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -472,7 +472,8 @@
|
|||||||
"vision": "Vision"
|
"vision": "Vision"
|
||||||
},
|
},
|
||||||
"vision": "Vision",
|
"vision": "Vision",
|
||||||
"websearch": "WebSearch"
|
"websearch": "WebSearch",
|
||||||
|
"edit": "Edit Model"
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||||
|
|||||||
@ -472,7 +472,8 @@
|
|||||||
"vision": "画像"
|
"vision": "画像"
|
||||||
},
|
},
|
||||||
"vision": "画像",
|
"vision": "画像",
|
||||||
"websearch": "ウェブ検索"
|
"websearch": "ウェブ検索",
|
||||||
|
"edit": "モデルを編集"
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
||||||
|
|||||||
@ -472,7 +472,8 @@
|
|||||||
"vision": "Изображение"
|
"vision": "Изображение"
|
||||||
},
|
},
|
||||||
"vision": "Визуальные",
|
"vision": "Визуальные",
|
||||||
"websearch": "Веб-поисковые"
|
"websearch": "Веб-поисковые",
|
||||||
|
"edit": "Редактировать модель"
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||||
|
|||||||
@ -472,7 +472,8 @@
|
|||||||
"vision": "图像"
|
"vision": "图像"
|
||||||
},
|
},
|
||||||
"vision": "视觉",
|
"vision": "视觉",
|
||||||
"websearch": "联网"
|
"websearch": "联网",
|
||||||
|
"edit": "编辑模型"
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)",
|
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)",
|
||||||
|
|||||||
@ -472,7 +472,8 @@
|
|||||||
"vision": "圖像"
|
"vision": "圖像"
|
||||||
},
|
},
|
||||||
"vision": "視覺",
|
"vision": "視覺",
|
||||||
"websearch": "網路搜索"
|
"websearch": "網路搜索",
|
||||||
|
"edit": "編輯模型"
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
|
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
|
||||||
|
|||||||
@ -22,9 +22,10 @@ import { isProviderSupportAuth, isProviderSupportCharge } from '@renderer/servic
|
|||||||
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, ModelType, 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, Input, Popover, Space, Switch } from 'antd'
|
import { Avatar, Button, Card, Checkbox, Divider, Flex, Form, Input, Modal, 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'
|
||||||
@ -51,6 +52,129 @@ 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 onFinish = (values: any) => {
|
||||||
|
const updatedModel = {
|
||||||
|
...model,
|
||||||
|
id: values.id || model.id,
|
||||||
|
name: values.name || model.name,
|
||||||
|
group: values.group || model.group
|
||||||
|
}
|
||||||
|
onUpdateModel(updatedModel)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('models.edit')}
|
||||||
|
open={open}
|
||||||
|
onCancel={onClose}
|
||||||
|
footer={null}
|
||||||
|
maskClosable={false}
|
||||||
|
centered
|
||||||
|
afterOpenChange={(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
form.getFieldInstance('id')?.focus()
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<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' }}>
|
||||||
|
<Button type="primary" htmlType="submit" size="middle">
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
<Divider style={{ margin: '0 0 15px 0' }} />
|
||||||
|
<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])]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox.Group
|
||||||
|
value={selectedTypes}
|
||||||
|
onChange={(types) => onUpdateModel({ ...model, type: types as ModelType[] })}
|
||||||
|
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)
|
||||||
@ -76,6 +200,8 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
const modelsWebsite = providerConfig?.websites?.models
|
const modelsWebsite = providerConfig?.websites?.models
|
||||||
const configedApiHost = providerConfig?.api?.url
|
const configedApiHost = providerConfig?.api?.url
|
||||||
|
|
||||||
|
const [editingModel, setEditingModel] = useState<Model | null>(null)
|
||||||
|
|
||||||
const onUpdateApiKey = () => {
|
const onUpdateApiKey = () => {
|
||||||
if (apiKey !== provider.apiKey) {
|
if (apiKey !== provider.apiKey) {
|
||||||
updateProvider({ ...provider, apiKey })
|
updateProvider({ ...provider, apiKey })
|
||||||
@ -164,67 +290,42 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
return formatApiHost(apiHost) + 'chat/completions'
|
return formatApiHost(apiHost) + 'chat/completions'
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpdateModelTypes = (model: Model, types: ModelType[]) => {
|
const onUpdateModel = (updatedModel: Model) => {
|
||||||
const updatedModels = models.map((m) => {
|
const updatedModels = models.map((m) => {
|
||||||
if (m.id === model.id) {
|
if (m.id === updatedModel.id) {
|
||||||
return { ...m, type: types }
|
return updatedModel
|
||||||
}
|
}
|
||||||
return m
|
return m
|
||||||
})
|
})
|
||||||
|
|
||||||
updateProvider({ ...provider, models: updatedModels })
|
updateProvider({ ...provider, models: updatedModels })
|
||||||
|
|
||||||
|
// Update assistants using this model
|
||||||
assistants.forEach((assistant) => {
|
assistants.forEach((assistant) => {
|
||||||
if (assistant?.model?.id === model.id && assistant.model.provider === provider.id) {
|
if (assistant?.model?.id === updatedModel.id && assistant.model.provider === provider.id) {
|
||||||
dispatch(
|
dispatch(
|
||||||
setModel({
|
setModel({
|
||||||
assistantId: assistant.id,
|
assistantId: assistant.id,
|
||||||
model: { ...model, type: types }
|
model: updatedModel
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (defaultModel?.id === model.id && defaultModel?.provider === provider.id) {
|
// Update default model if needed
|
||||||
setDefaultModel({ ...defaultModel, type: types })
|
if (defaultModel?.id === updatedModel.id && defaultModel?.provider === provider.id) {
|
||||||
|
setDefaultModel(updatedModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelTypeContent = (model: Model) => {
|
const modelTypeContent = (model: Model) => {
|
||||||
// 获取默认选中的类型
|
|
||||||
const defaultTypes = [
|
|
||||||
...(isVisionModel(model) ? ['vision'] : []),
|
|
||||||
...(isEmbeddingModel(model) ? ['embedding'] : []),
|
|
||||||
...(isReasoningModel(model) ? ['reasoning'] : [])
|
|
||||||
] as ModelType[]
|
|
||||||
|
|
||||||
// 合并现有选择和默认类型
|
|
||||||
const selectedTypes = [...new Set([...(model.type || []), ...defaultTypes])]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<ModelEditContent
|
||||||
<Checkbox.Group
|
model={model}
|
||||||
value={selectedTypes}
|
onUpdateModel={onUpdateModel}
|
||||||
onChange={(types) => onUpdateModelTypes(model, types as ModelType[])}
|
open={editingModel?.id === model.id}
|
||||||
options={[
|
onClose={() => setEditingModel(null)}
|
||||||
{
|
/>
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,9 +456,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
<span>{model?.name}</span>
|
<span>{model?.name}</span>
|
||||||
<ModelTags model={model} />
|
<ModelTags model={model} />
|
||||||
</ModelNameRow>
|
</ModelNameRow>
|
||||||
<Popover content={modelTypeContent(model)} title={t('models.type.select')} trigger="click">
|
<SettingIcon onClick={() => setEditingModel(model)} />
|
||||||
<SettingIcon />
|
|
||||||
</Popover>
|
|
||||||
</ModelListHeader>
|
</ModelListHeader>
|
||||||
<RemoveIcon onClick={() => removeModel(model)} />
|
<RemoveIcon onClick={() => removeModel(model)} />
|
||||||
</ModelListItem>
|
</ModelListItem>
|
||||||
@ -386,6 +485,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
{t('button.add')}
|
{t('button.add')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{models.map((model) => modelTypeContent(model))}
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -434,4 +534,10 @@ const ProviderName = styled.span`
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const TypeTitle = styled.div`
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
`
|
||||||
|
|
||||||
export default ProviderSetting
|
export default ProviderSetting
|
||||||
|
|||||||
@ -502,6 +502,21 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setLMStudioKeepAliveTime: (state, action: PayloadAction<number>) => {
|
setLMStudioKeepAliveTime: (state, action: PayloadAction<number>) => {
|
||||||
state.settings.lmstudio.keepAliveTime = action.payload
|
state.settings.lmstudio.keepAliveTime = action.payload
|
||||||
|
},
|
||||||
|
updateModel: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{
|
||||||
|
providerId: string
|
||||||
|
model: Model
|
||||||
|
}>
|
||||||
|
) => {
|
||||||
|
const provider = state.providers.find((p) => p.id === action.payload.providerId)
|
||||||
|
if (provider) {
|
||||||
|
const modelIndex = provider.models.findIndex((m) => m.id === action.payload.model.id)
|
||||||
|
if (modelIndex !== -1) {
|
||||||
|
provider.models[modelIndex] = action.payload.model
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -517,7 +532,8 @@ export const {
|
|||||||
setTopicNamingModel,
|
setTopicNamingModel,
|
||||||
setTranslateModel,
|
setTranslateModel,
|
||||||
setOllamaKeepAliveTime,
|
setOllamaKeepAliveTime,
|
||||||
setLMStudioKeepAliveTime
|
setLMStudioKeepAliveTime,
|
||||||
|
updateModel
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user