feat(web): update model management

This commit is contained in:
zhaoying
2026-01-27 20:07:53 +08:00
parent 74f0018962
commit 75b3ea1f05
24 changed files with 1768 additions and 360 deletions

View File

@@ -0,0 +1,97 @@
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import clsx from 'clsx'
import { Button } from 'antd'
import { useTranslation } from 'react-i18next';
import type { ProviderModelItem, ModelListItem, DescriptionItem, BaseRef } from './types'
import RbCard from '@/components/RbCard/Card'
import { getModelNewList } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty';
import { formatDateTime } from '@/utils/format';
const Group = forwardRef <BaseRef,{ query: any; handleEdit: (data: ModelListItem) => void; }>(({ query, handleEdit }, ref) => {
const { t } = useTranslation();
const [list, setList] = useState<ModelListItem[]>([])
useEffect(() => {
getList()
}, [query])
const getList = () => {
getModelNewList({
...query,
is_composite: true,
is_active: true,
})
.then(res => {
const response = res as ProviderModelItem[]
setList(response[0]?.models || [])
})
}
const formatData = (data: ModelListItem) => {
return [
{
key: 'type',
label: t(`modelNew.type`),
children: data.type || '-',
},
{
key: 'provider',
label: t(`modelNew.provider`),
children: data.provider || '-',
},
{
key: 'is_active',
label: t(`modelNew.status`),
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
},
{
key: 'created_at',
label: t(`modelNew.created_at`),
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
},
]
}
useImperativeHandle(ref, () => ({
getList,
}));
return (
<>
{list.length === 0
? <PageEmpty />
:(
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
{list.map(item => (
<RbCard
key={item.id}
title={item.name}
avatarUrl={item.logo}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
>
{formatData(item)?.map((description: DescriptionItem) => (
<div
key={description.key}
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
>
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
<span className={clsx({
"rb:text-[#212332]": description.key !== 'is_active',
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
})}>{(description.children as string)}</span>
</div>
))}
<Button className="rb:mt-2" type="primary" ghost block onClick={() => handleEdit(item)}>{t('modelNew.configureBtn')}</Button>
</RbCard>
))}
</div>
)
}
</>
)
})
export default Group

View File

@@ -0,0 +1,83 @@
import { useRef, useState, useEffect, type FC } from 'react';
import { Button, Space, Row, Col } from 'antd'
import { useTranslation } from 'react-i18next';
import type { ProviderModelItem, KeyConfigModalRef, ModelListDetailRef } from './types'
import RbCard from '@/components/RbCard/Card'
import { getModelNewList } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty';
import Tag from '@/components/Tag';
import KeyConfigModal from './components/KeyConfigModal'
import ModelListDetail from './components/ModelListDetail'
const ModelList: FC<{ query: any }> = ({ query }) => {
const { t } = useTranslation();
const keyConfigModalRef = useRef<KeyConfigModalRef>(null)
const modelListDetailRef = useRef<ModelListDetailRef>(null)
const [list, setList] = useState<ProviderModelItem[]>([])
useEffect(() => {
getList()
}, [query])
const getList = () => {
getModelNewList({
...query,
is_composite: false,
is_active: true,
})
.then(res => {
setList((res || []) as ProviderModelItem[])
})
}
const handleShowModel = (vo: ProviderModelItem) => {
modelListDetailRef.current?.handleOpen(vo)
}
const handleKeyConfig = (vo: ProviderModelItem) => {
keyConfigModalRef.current?.handleOpen(vo)
}
return (
<>
{list.length === 0
? <PageEmpty />
:(
<div className="rb:grid rb:grid-cols-4 rb:gap-4">
{list.map(item => (
<RbCard
key={item.provider}
title={item.provider}
avatarUrl={item.logo}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.provider[0]}
</div>
}
>
<Space>{item.tags.map(tag => <Tag key={tag}>{t(`modelNew.${tag}`)}</Tag>)}</Space>
<Row gutter={12} className="rb:mt-4">
<Col span={12}>
<Button block onClick={() => handleShowModel(item)}>{t('modelNew.showModel')}</Button>
</Col>
<Col span={12}>
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
</Col>
</Row>
</RbCard>
))}
</div>
)
}
<KeyConfigModal
ref={keyConfigModalRef}
refresh={getList}
/>
<ModelListDetail
ref={modelListDetailRef}
refresh={getList}
/>
</>
)
}
export default ModelList

View File

@@ -0,0 +1,94 @@
import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Button, Space, App, Divider, Flex } from 'antd'
import { UsergroupAddOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef, BaseRef } from './types'
import RbCard from '@/components/RbCard/Card'
import { getModelPlaza, addModelPlaza } from '@/api/models'
import PageEmpty from '@/components/Empty/PageEmpty';
import Tag from '@/components/Tag';
import ModelSquareDetail from './components/ModelSquareDetail'
const ModelSquare = forwardRef <BaseRef, { query: any; handleEdit: (vo?: ModelPlazaItem) => void; }>(({ query, handleEdit }, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const modelSquareDetailRef = useRef<ModelSquareDetailRef>(null)
const [list, setList] = useState<ModelPlaza[]>([])
useEffect(() => {
getList()
}, [query])
const getList = () => {
getModelPlaza(query)
.then(res => {
setList((res as ModelPlaza[]) || [])
})
}
const handleMore = (vo: ModelPlaza) => {
modelSquareDetailRef.current?.handleOpen(vo)
}
const handleAdd = (item: ModelPlazaItem) => {
addModelPlaza(item.id)
.then(() => {
message.success(`${item.name}${t('modelNew.addSuccess')}`)
getList()
})
}
useImperativeHandle(ref, () => ({
getList,
}));
return (
<>
{list.length === 0
? <PageEmpty />
: list.map(vo => (
<div key={vo.provider}>
<div className="rb:flex rb:justify-between rb:items-center rb:bg-[rgba(21,94,239,0.12)] rb:px-4 rb:py-2.5 rb:leading-5 rb:mb-4 rb:mt-6 rb:rounded-md">
<div className="rb:font-medium">{vo.provider}</div>
<Button type="link" onClick={() => handleMore(vo)}>{t('modelNew.viewAll')}({t(`modelNew.modelCount`, { count: vo.models.length })})&gt;</Button>
</div>
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
{vo.models.slice(0, 6).map(item => (
<RbCard
key={item.id}
title={item.name}
avatarUrl={item.logo}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
>
<Tag>{t(`modelNew.${item.type}`)}</Tag>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mt-3 rb:h-9">{item.description}</div>
<Space size={8} className="rb:mt-3">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Space>
<Divider size="middle" />
<Flex justify="space-between">
<Space size={8}><UsergroupAddOutlined /> {item.add_count}</Space>
<Space>
{!item.is_official && <Button type="primary" disabled={item.is_deprecated} onClick={() => handleEdit(item)}>{t('modelNew.edit')}</Button>}
{item.is_added
? <Button type="primary" disabled>{t('modelNew.added')}</Button>
: <Button type="primary" ghost disabled={item.is_deprecated} onClick={() => handleAdd(item)}>+ {t('common.add')}</Button>
}
</Space>
</Flex>
</RbCard>
))}
</div>
</div>
))
}
<ModelSquareDetail
ref={modelSquareDetailRef}
refresh={getList}
/>
</>
)
})
export default ModelSquare

View File

@@ -1,171 +0,0 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ModelFormData, Model, ConfigModalRef, ConfigModalProps } from '../types';
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import { updateModel, addModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
const ConfigModal = forwardRef<ConfigModalRef, ConfigModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<Model>({} as Model);
const [isEdit, setIsEdit] = useState(false);
const [form] = Form.useForm<ModelFormData>();
const [loading, setLoading] = useState(false)
const values = Form.useWatch<ModelFormData>([], form);
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setModel({} as Model);
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (model?: Model) => {
if (model) {
setIsEdit(true);
setModel(model);
// 设置表单值
const apiKeyInfo = model.api_keys[0]
form.setFieldsValue({
provider: apiKeyInfo.provider,
model_name: apiKeyInfo.model_name,
api_key: apiKeyInfo.api_key,
api_base: apiKeyInfo.api_base
});
} else {
setIsEdit(false);
form.resetFields();
}
setVisible(true);
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
form
.validateFields()
.then(() => {
const data = {
name: values.name,
type: values.type,
api_keys: {
provider: values.provider,
model_name: values.model_name,
api_key: values.api_key,
api_base: values.api_base
},
}
setLoading(true)
const res = isEdit
? updateModel(model.api_keys[0].id, {
provider: values.provider,
model_name: values.model_name,
api_key: values.api_key,
api_base: values.api_base
} as ModelFormData)
: addModel(data as ModelFormData)
res.then(() => {
if (refresh) {
refresh();
}
handleClose()
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
})
.catch(() => {
setLoading(false)
});
})
.catch((err) => {
console.log('err', err)
});
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={isEdit ? `${model.name} - ${t('model.modelConfiguration')}` : t('model.createModel')}
open={visible}
onCancel={handleClose}
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
initialValues={{}}
>
{!isEdit && (
<>
<Form.Item
name="name"
label={t('model.displayName')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.displayName') }) }]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="type"
label={t('model.type')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.type') }) }]}
>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
/>
</Form.Item>
</>
)}
<Form.Item
name="provider"
label={t('model.provider')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('model.provider') }) }]}
>
<CustomSelect
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`model.${item}`), value: item }))}
/>
</Form.Item>
<Form.Item
name="model_name"
label={t('model.modelName')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.modelName') }) }]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="api_key"
label={t('model.apiKey')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('model.apiKey') }) }]}
>
<Input.Password placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="api_base"
label={t('model.apiEndpoint')}
>
<Input placeholder="https://api.example.com/v1" />
</Form.Item>
</Form>
</RbModal>
);
});
export default ConfigModal;

View File

@@ -0,0 +1,162 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Select } from 'antd';
import { useTranslation } from 'react-i18next';
import type { CustomModelForm, ModelPlazaItem, CustomModelModalRef, CustomModelModalProps } from '../types';
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import UploadImages from '@/components/Upload/UploadImages'
import { updateCustomModel, addCustomModel, modelTypeUrl, modelProviderUrl } from '@/api/models'
import { getFileLink } from '@/api/fileStorage'
const CustomModelModal = forwardRef<CustomModelModalRef, CustomModelModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<ModelPlazaItem>({} as ModelPlazaItem);
const [isEdit, setIsEdit] = useState(false);
const [form] = Form.useForm<CustomModelForm>();
const [loading, setLoading] = useState(false)
const formValues = Form.useWatch([], form)
const handleClose = () => {
setModel({} as ModelPlazaItem);
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (model?: ModelPlazaItem) => {
if (model) {
setIsEdit(true);
setModel(model);
form.setFieldsValue({
...model,
});
} else {
setIsEdit(false);
form.resetFields();
}
setVisible(true);
};
const handleSave = () => {
form
.validateFields()
.then((values) => {
setLoading(true)
values.is_official = false;
const logo = values.logo as any;
if (typeof logo === 'object') {
getFileLink(logo?.response?.data.file_id).then(res => {
const logoRes = res as { url: string }
values.logo = logoRes.url.replace('http://127.0.0.1:8000', 'https://devmemorybear.redbearai.com')
addCustomModel(values).then(() => {
if (refresh) {
refresh();
}
handleClose()
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
})
.catch(() => {
setLoading(false)
});
})
} else {
updateCustomModel(model.id, values).then(() => {
if (refresh) {
refresh();
}
handleClose()
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
})
.catch(() => {
setLoading(false)
});
}
})
.catch((err) => {
console.log('err', err)
});
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
console.log('formValues', formValues)
return (
<RbModal
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createCustomModel')}
open={visible}
onCancel={handleClose}
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
{!isEdit && <Form.Item
name="logo"
label={t('modelNew.logo')}
valuePropName="fileList"
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<UploadImages />
</Form.Item>}
<Form.Item
name="name"
label={t('modelNew.model_name')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.displayName') }) }]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="type"
label={t('modelNew.type')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
/>
</Form.Item>
<Form.Item
name="provider"
label={t('modelNew.provider')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
>
<CustomSelect
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
/>
</Form.Item>
<Form.Item
name="description"
label={t('modelNew.description')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.description') }) }]}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="tags"
label={t('modelNew.tags')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.tags') }) }]}
>
<Select mode="tags" placeholder={t('common.pleaseEnter')} />
</Form.Item>
</Form>
</RbModal>
);
});
export default CustomModelModal;

View File

@@ -0,0 +1,155 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ModelListItem, CompositeModelForm, GroupModelModalRef, GroupModelModalProps, ModelApiKey } from '../types';
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import { updateCompositeModel, modelTypeUrl, addCompositeModel } from '@/api/models'
import UploadImages from '@/components/Upload/UploadImages'
import ModelImplement from './ModelImplement'
import { getFileLink } from '@/api/fileStorage'
const GroupModelModal = forwardRef<GroupModelModalRef, GroupModelModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
const [isEdit, setIsEdit] = useState(false);
const [form] = Form.useForm<CompositeModelForm>();
const [loading, setLoading] = useState(false)
const type = Form.useWatch(['type'], form)
const handleClose = () => {
setModel({} as ModelListItem);
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (model?: ModelListItem) => {
if (model) {
setIsEdit(true);
setModel(model);
form.setFieldsValue({
...model,
api_key_ids: model.api_keys,
logo: model.logo ? [{url: model.logo, uid: model.logo, status: 'done', name: 'logo'}] : undefined
})
} else {
setIsEdit(false);
form.resetFields();
}
setVisible(true);
};
const handleSave = () => {
form
.validateFields()
.then((values) => {
const { api_key_ids, logo, ...rest } = values
const formData: CompositeModelForm = {
...rest,
api_key_ids: api_key_ids.map(vo => (vo as ModelApiKey).id)
}
if (logo?.response?.data.file_id) {
getFileLink(logo?.response?.data.file_id).then(res => {
const logoRes = res as { url: string }
formData.logo = logoRes.url.replace('http://127.0.0.1:8000', 'https://devmemorybear.redbearai.com')
handleUpdate(formData)
})
} else {
handleUpdate(formData)
}
})
.catch((err) => {
console.log('err', err)
});
}
const handleUpdate = (data: CompositeModelForm) => {
setLoading(true)
const res = isEdit
? updateCompositeModel(model.id, data)
: addCompositeModel(data)
res.then(() => {
refresh?.();
handleClose()
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
})
.catch(() => {
setLoading(false)
});
}
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={isEdit ? `${model.name} - ${t('modelNew.modelConfiguration')}` : t('modelNew.createGroupModel')}
open={visible}
onCancel={handleClose}
okText={t(`common.${isEdit ? 'save' : 'create'}`)}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="logo"
label={t('modelNew.logo')}
valuePropName="fileList"
// rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<UploadImages />
</Form.Item>
<Form.Item
name="name"
label={t('modelNew.name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
>
<Input placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="type"
label={t('modelNew.type')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.type') }) }]}
>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
value: typeof item === 'object' ? item.value : item
}))}
disabled={isEdit}
/>
</Form.Item>
<Form.Item
name="description"
label={t('modelNew.description')}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item name="api_key_ids">
<ModelImplement type={type} />
</Form.Item>
</Form>
</RbModal>
);
});
export default GroupModelModal;

View File

@@ -0,0 +1,92 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { KeyConfigModalForm, ProviderModelItem, KeyConfigModalRef, KeyConfigModalProps } from '../types';
import RbModal from '@/components/RbModal'
import { updateProviderApiKeys } from '@/api/models'
const KeyConfigModal = forwardRef<KeyConfigModalRef, KeyConfigModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<ProviderModelItem>({} as ProviderModelItem);
const [form] = Form.useForm<KeyConfigModalForm>();
const [loading, setLoading] = useState(false)
const handleClose = () => {
setModel({} as ProviderModelItem);
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (vo: ProviderModelItem) => {
setVisible(true);
setModel(vo);
};
const handleSave = () => {
form
.validateFields()
.then((values) => {
setLoading(true)
updateProviderApiKeys({
...values,
provider: model.provider
}).then(() => {
if (refresh) {
refresh();
}
handleClose()
message.success(t('common.updateSuccess'))
})
.catch(() => {
setLoading(false)
});
})
.catch((err) => {
console.log('err', err)
});
}
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={`${model.provider} - ${t('modelNew.keyConfig')}`}
open={visible}
onCancel={handleClose}
okText={t(`common.save`)}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="api_key"
label={t('modelNew.api_key')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.apiKey') }) }]}
>
<Input.Password placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="api_base"
label={t('modelNew.api_base')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
>
<Input placeholder="https://api.example.com/v1" />
</Form.Item>
</Form>
</RbModal>
);
});
export default KeyConfigModal;

View File

@@ -0,0 +1,173 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Cascader, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { SubModelModalForm, SubModelModalRef, SubModelModalProps, ModelList } from './types';
import RbModal from '@/components/RbModal'
import CustomSelect from '@/components/CustomSelect'
import { modelProviderUrl, getModelNewList } from '@/api/models'
import type { ProviderModelItem } from '../../types'
const { SHOW_CHILD } = Cascader;
interface Option {
value: string | number;
label: string;
children?: Option[];
[key: string]: any;
}
const SubModelModal = forwardRef<SubModelModalRef, SubModelModalProps>(({
refresh,
type
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<SubModelModalForm>();
const [loading, setLoading] = useState(false)
const [selecteds, setSelecteds] = useState<any[]>([])
const [modelList, setModelList] = useState<Option[]>([])
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
form.resetFields();
setLoading(false)
setVisible(false);
setSelecteds([])
};
const handleOpen = (list?: ModelList[], provider?: string) => {
if (list?.length && provider) {
const initialValue: SubModelModalForm = {
provider,
api_key_ids: list.map(vo => {
return [vo.model_config_ids[0], vo.id]
})
}
form.setFieldsValue(initialValue);
handleChangeProvider(provider, initialValue.api_key_ids)
} else {
form.resetFields()
}
setVisible(true);
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
form
.validateFields()
.then((values) => {
console.log('SubModelModal values', values, selecteds, selecteds.map(vo => ({
...vo[0],
model_name: vo[0].name,
model_config_ids: [vo[0].id],
id: vo[1].value
})))
refresh?.(selecteds.map(vo => ({
...vo[0],
model_name: vo[0].name,
model_config_ids: [vo[0].id],
id: vo[1].value
})))
handleClose()
})
}
const handleChange = (value: (string | number)[][], selectedOptions: Option[][]) => {
const filterList = selectedOptions.filter(vo => vo.length === 1).map(item => item[0])
const lastFilterLit = value.filter(vo => vo.length !== 1)
console.log('onchange', value, lastFilterLit, selectedOptions, filterList)
if (filterList.length) {
message.warning(`${filterList.map(vo => vo.label)}${t('modelNew.selectOneTip')}`)
form.setFieldValue('api_key_ids', lastFilterLit)
}
setSelecteds(selectedOptions)
}
const handleChangeProvider = (provider: string, api_key_ids?: any[]) => {
form.setFieldValue('api_key_ids', undefined)
getModelNewList({
provider: provider,
is_composite: false,
is_active: true,
type
})
.then(res => {
const response = res as ProviderModelItem[]
const list = response[0]?.models || []
setModelList(list.map(vo => {
const children = vo.api_keys.map(item => ({
label: item.api_key,
value: item.id,
}))
return {
...vo,
label: vo.name,
value: vo.id,
children: children
}
}))
if (api_key_ids?.length) {
form.setFieldsValue({
api_key_ids: api_key_ids
})
}
})
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={t('modelNew.implementConfig')}
open={visible}
onCancel={handleClose}
okText={t('common.save')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="provider"
label={t('modelNew.provider')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.provider') }) }]}
>
<CustomSelect
placeholder={t('common.pleaseSelect')}
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({
label: t(`modelNew.${typeof item === 'object' ? item.value : item}`),
value: typeof item === 'object' ? item.value : item
}))}
onChange={(value) => handleChangeProvider(value)}
/>
</Form.Item>
<Form.Item
name="api_key_ids"
label={t('modelNew.api_key_ids')}
rules={[{ required: true, message: t('common.selectPlaceholder', { title: t('modelNew.api_key_ids') }) }]}
>
<Cascader
placeholder={t('common.pleaseSelect')}
options={modelList}
onChange={handleChange}
multiple
autoClearSearchValue
className="rb:w-full!"
showCheckedStrategy={SHOW_CHILD}
changeOnSelect
/>
</Form.Item>
</Form>
</RbModal>
);
});
export default SubModelModal;

View File

@@ -0,0 +1,106 @@
import { type FC, useRef } from "react";
import { useTranslation } from 'react-i18next';
import { Flex, Button, Space, App } from 'antd'
import type { SubModelModalRef, ModelList } from './types'
import SubModelModal from './SubModelModal'
import Empty from '@/components/Empty'
import Tag from '@/components/Tag'
interface ModelImplementProps {
type?: string;
value?: any;
onChange?: (value: any) => void;
}
const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
const { t } = useTranslation();
const { modal, message } = App.useApp();
const subModelModalRef = useRef<SubModelModalRef>(null)
const handleAdd = () => {
if (!type || type.trim() === '') {
message.warning(t('common.selectPlaceholder', { title: t('modelNew.type') }))
return
}
subModelModalRef.current?.handleOpen()
}
const handleEdit = (list: ModelList[], provider: string ) => {
subModelModalRef.current?.handleOpen(list, provider)
}
const handleDelete = (provider: string) => {
modal.confirm({
title: t('common.confirmDeleteDesc', { name: provider }),
content: t('application.apiKeyDeleteContent'),
okText: t('common.delete'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
onChange?.(value?.filter((item: any) => item.provider !== provider))
}
})
}
const handleRefresh = (list: ModelList[]) => {
const existingModels = value || [];
let updatedModels = [...existingModels];
const provider = list[0].provider
updatedModels = updatedModels.filter(item => item.provider !== provider)
updatedModels = [...updatedModels, ...list]
onChange?.([...updatedModels]);
}
const groupedByProvider: Record<string, ModelList[]> = (value || []).reduce((acc: Record<string, ModelList[]>, item: ModelList) => {
const provider = item.provider || 'unknown';
if (!acc[provider]) acc[provider] = [];
acc[provider].push(item);
return acc;
}, {} as Record<string, ModelList[]>);
return (
<div>
<Flex justify="space-between" align="center">
{t('modelNew.modelImplement')}
<Space>
<Button type="primary" onClick={handleAdd} className="rb:px-2! rb:h-6!">+ {t('modelNew.addImplement')}</Button>
<Button size="small" className="rb:px-2! rb:h-6!">{t('modelNew.noAuth')}</Button>
</Space>
</Flex>
<div className="rb:bg-[#F5F6F7] rb:rounded-lg rb:p-3 rb:mt-2">
{!value || value.length === 0
? <Empty size={88} />
: Object.entries(groupedByProvider).map(([provider, items]: [string, ModelList[]]) => {
return (
<div key={provider} className="rb:mb-4 last:rb:mb-0">
<Flex justify="space-between" align="center" className="rb:mb-2 last:rb:mb-0">
<div className="rb:font-medium">{[...new Set(items?.map((vo) => vo.model_name))].join(', ')}</div>
<Space>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleEdit(items, provider)}
></div>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDelete(provider)}
></div>
</Space>
</Flex>
<Tag className="rb:mb-2">{provider}</Tag>
</div>
)
})}
</div>
<SubModelModal
ref={subModelModalRef}
refresh={handleRefresh}
type={type}
/>
</div>
)
}
export default ModelImplement

View File

@@ -0,0 +1,16 @@
import type { ModelListItem } from '../../types'
export interface ModelList extends ModelListItem {
api_key_id: string;
}
export interface SubModelModalForm {
provider: string;
api_key_ids: string[][];
}
export interface SubModelModalRef {
handleOpen: (list?: ModelList[], provider?: string) => void;
}
export interface SubModelModalProps {
type?: string;
refresh?: (vo: ModelList[]) => void;
}

View File

@@ -0,0 +1,111 @@
import { useState, useImperativeHandle, forwardRef, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Switch, Row, Col, Space } from 'antd'
import type { ProviderModelItem, ModelListItem, ModelListDetailRef, MultiKeyConfigModalRef } from '../types';
import RbDrawer from '@/components/RbDrawer';
import RbCard from '@/components/RbCard/Card'
import Tag from '@/components/Tag';
import PageEmpty from '@/components/Empty/PageEmpty';
import MultiKeyConfigModal from './MultiKeyConfigModal'
import { getModelNewList, updateModelStatus } from '@/api/models'
interface ModelListDetailProps {
refresh?: () => void;
}
const ModelListDetail = forwardRef<ModelListDetailRef, ModelListDetailProps>(({ refresh }, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [data, setData] = useState<ProviderModelItem>({} as ProviderModelItem)
const [list, setList] = useState<ModelListItem[]>([])
const multiKeyConfigModalRef = useRef<MultiKeyConfigModalRef>(null)
const [loading, setLoading] = useState(false)
const handleOpen = (vo: ProviderModelItem) => {
setOpen(true)
getData(vo)
}
const getData = (vo: ProviderModelItem) => {
if (!vo.provider) return
getModelNewList({
provider: vo.provider
})
.then(res => {
const response = res as ProviderModelItem[]
setData(response[0])
setList(response[0].models)
})
}
const handleKeyConfig = (vo: ModelListItem) => {
multiKeyConfigModalRef.current?.handleOpen(vo, data.provider)
}
const handleChange = (vo: ModelListItem) => {
setLoading(true)
updateModelStatus(vo.id, { is_active: !vo.is_active })
.finally(() => {
getData(data)
setLoading(false)
})
}
const handleClose = () => {
setOpen(false)
refresh?.()
}
const handleRefresh = () => {
getData(data)
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbDrawer
title={<>{data.provider} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
open={open}
onClose={handleClose}
>
{list.length === 0
? <PageEmpty />
: <div className="rb:grid rb:grid-cols-2 rb:gap-4">
{list.map(item => (
<RbCard
key={item.id}
title={item.name}
subTitle={<Space>
<Tag>{t(`modelNew.${item.type}`)}</Tag>
<Tag color="warning">{item.api_keys.length}{t('modelNew.apiKeyNum')}</Tag>
</Space>}
avatarUrl={item.logo}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
extra={<Switch defaultChecked={item.is_active} disabled={loading} onChange={() => handleChange(item)} />}
>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mt-3">{item.description}</div>
<Row gutter={12} className="rb:mt-4">
<Col span={24}>
<Button type="primary" ghost block onClick={() => handleKeyConfig(item)}>{t('modelNew.keyConfig')}</Button>
</Col>
</Row>
</RbCard>
))}
</div>
}
<MultiKeyConfigModal
ref={multiKeyConfigModalRef}
refresh={handleRefresh}
/>
</RbDrawer>
);
});
export default ModelListDetail;

View File

@@ -0,0 +1,86 @@
import { useState, useImperativeHandle, forwardRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Space, App } from 'antd'
import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef } from '../types';
import RbDrawer from '@/components/RbDrawer';
import { getModelPlaza, addModelPlaza } from '@/api/models'
import RbCard from '@/components/RbCard/Card'
import Tag from '@/components/Tag';
import PageEmpty from '@/components/Empty/PageEmpty';
interface ModelSquareDetailProps {
refresh: () => void;
}
const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProps>(({ refresh }, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const [model, setModel] = useState<ModelPlaza>({} as ModelPlaza)
const [open, setOpen] = useState(false);
const [list, setList] = useState<ModelPlazaItem[]>([])
const handleOpen = (vo: ModelPlaza) => {
setModel(vo)
setOpen(true)
getList(vo)
}
const handleClose = () => {
setOpen(false)
refresh()
}
const getList = (vo: ModelPlaza) => {
getModelPlaza({ provider: vo.provider })
.then(res => {
const response = res as ModelPlaza[]
setList(response.length > 0 ? response[0].models : [])
})
}
const handleAdd = (item: ModelPlazaItem) => {
addModelPlaza(item.id)
.then(() => {
message.success(`${item.name}${t('modelNew.addSuccess')}`)
getList(model)
})
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbDrawer
title={<>{model.provider} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})</>}
open={open}
onClose={handleClose}
>
{list.length === 0
? <PageEmpty />
: <div className="rb:grid rb:grid-cols-2 rb:gap-4">
{list.map(item => (
<RbCard
key={item.id}
title={item.name}
avatarUrl={item.logo}
avatar={
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
>
<Tag>{t(`modelNew.${item.type}`)}</Tag>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.5 rb:mt-3 rb:h-9">{item.description}</div>
<Space size={8} className="rb:mt-3">{item.tags.map((tag, tagIndex) => <Tag key={tagIndex}>{tag}</Tag>)}</Space>
{item.is_added
? <Button className="rb:mt-3" type="primary" disabled block>{t('modelNew.added')}</Button>
: <Button className="rb:mt-3" type="primary" ghost block onClick={() => handleAdd(item)}>+ {t('common.add')}</Button>
}
</RbCard>
))}
</div>
}
</RbDrawer>
);
});
export default ModelSquareDetail;

View File

@@ -0,0 +1,121 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Button } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ModelListItem, MultiKeyForm, MultiKeyConfigModalRef, MultiKeyConfigModalProps } from '../types';
import RbModal from '@/components/RbModal'
import { addModelApiKey, delteModelApiKey, getModelInfo } from '@/api/models'
const MultiKeyConfigModal = forwardRef<MultiKeyConfigModalRef, MultiKeyConfigModalProps>(({ refresh }, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [model, setModel] = useState<ModelListItem>({} as ModelListItem);
const [form] = Form.useForm<MultiKeyForm>();
const [loading, setLoading] = useState(false)
const handleClose = () => {
setModel({} as ModelListItem);
refresh?.()
form.resetFields();
setLoading(false)
setVisible(false);
};
const handleOpen = (vo: ModelListItem) => {
setVisible(true);
getData(vo)
};
const getData = (vo: ModelListItem) => {
if (!vo.id) return
getModelInfo(vo?.id)
.then(res => {
setModel(res as ModelListItem)
})
}
const handleSave = () => {
form
.validateFields()
.then((values) => {
addModelApiKey(model.id, {
...values,
model_config_id: model.id,
model_name: model.name,
provider: model.provider,
}).then(() => {
message.success(t('common.saveSuccess'))
form.resetFields();
getData(model)
})
.catch(() => {
setLoading(false)
});
})
.catch((err) => {
console.log('err', err)
});
}
const handleDelete = (api_key_id: string) => {
delteModelApiKey(api_key_id)
.then(() => {
message.success(t('common.deleteSuccess'))
getData(model)
})
}
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={`${model.name} - ${t('modelNew.keyConfig')}`}
open={visible}
onCancel={handleClose}
footer={null}
confirmLoading={loading}
>
{model.api_keys && model.api_keys.length > 0 && (
<div className="rb:mb-4">
{model.api_keys.map((key) => (
<div key={key.id} className="rb:flex rb:items-center rb:justify-between rb:p-3 rb:bg-[#F5F6F7] rb:rounded-lg rb:mb-2">
<div>
<div className="rb:text-[#1D2129] rb:text-[14px] rb:font-medium">{key.api_key}</div>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mt-1">{key.api_base}</div>
</div>
<Button type="primary" danger ghost onClick={() => handleDelete(key.id)}>{t('common.remove')}</Button>
</div>
))}
</div>
)}
<Form
form={form}
layout="vertical"
>
<Form.Item
name="api_key"
label={t('modelNew.api_key')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_key') }) }]}
>
<Input.Password placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="api_base"
label={t('modelNew.api_base')}
rules={[{ required: true, message: t('common.inputPlaceholder', { title: t('modelNew.api_base') }) }]}
>
<Input placeholder="https://api.example.com/v1" />
</Form.Item>
<Form.Item>
<Button type="primary" block onClick={handleSave} loading={loading}>+ {t('modelNew.add')}</Button>
</Form.Item>
</Form>
</RbModal>
);
});
export default MultiKeyConfigModal;

View File

@@ -1,99 +1,123 @@
import { useState, useRef, type FC } from 'react';
import { Row, Col, Button } from 'antd'
import { Button, Flex, Space, type SegmentedProps } from 'antd'
import { useTranslation } from 'react-i18next';
import clsx from 'clsx';
import ConfigModal from './components/ConfigModal'
import type { Model, DescriptionItem, ConfigModalRef } from './types'
import RbCard from '@/components/RbCard/Card'
import GroupModelModal from './components/GroupModelModal'
import type { ModelListItem, GroupModelModalRef, CustomModelModalRef, ModelPlazaItem, BaseRef } from './types'
import SearchInput from '@/components/SearchInput'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { getModelListUrl } from '@/api/models'
import { formatDateTime } from '@/utils/format';
import PageTabs from '@/components/PageTabs'
import GroupModel from './Group'
import ModelList from './List'
import ModelSquare from './Square'
import CustomModelModal from './components/CustomModelModal'
import CustomSelect from '@/components/CustomSelect'
import { modelTypeUrl, modelProviderUrl } from '@/api/models'
const tabKeys = ['group', 'list', 'square']
const ModelManagement: FC = () => {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('group');
const [query, setQuery] = useState({})
const configModalRef = useRef<ConfigModalRef>(null)
const scrollListRef = useRef<PageScrollListRef>(null)
const configModalRef = useRef<GroupModelModalRef>(null)
const customModelModalRef = useRef<CustomModelModalRef>(null)
const groupRef = useRef<BaseRef>(null)
const squareRef = useRef<BaseRef>(null)
const formatData = (data: Model) => {
return [
{
key: 'type',
label: t(`model.type`),
children: data.type || '-',
},
{
key: 'provider',
label: t(`model.provider`),
children: data.api_keys[0].provider || '-',
},
{
key: 'is_active',
label: t(`model.status`),
children: data.is_active ? t(`common.statusEnabled`) : t(`common.statusDisabled`),
},
{
key: 'created',
label: t(`model.created`),
children: data.created_at ? formatDateTime(data.created_at, 'YYYY-MM-DD HH:mm:ss') : '-',
},
]
const formatTabItems = () => {
return tabKeys.map(value => ({
value,
label: t(`modelNew.${value}`),
}))
}
const handleChangeTab = (value: SegmentedProps['value']) => {
setActiveTab(value as string);
setQuery({})
}
const handleEdit = (model?: Model) => {
configModalRef?.current?.handleOpen(model)
const handleEdit = (vo?: ModelListItem | ModelPlazaItem) => {
switch(activeTab) {
case 'group':
configModalRef?.current?.handleOpen(vo as ModelListItem)
break
case 'square':
customModelModalRef?.current?.handleOpen(vo as ModelPlazaItem)
break
}
}
const handleRefresh = () => {
switch (activeTab) {
case 'group':
groupRef.current?.getList()
break
case 'square':
squareRef.current?.getList()
break
}
}
const handleSearch = (value?: string) => {
setQuery({ search: value })
}
const handleTypeChange = (value: string) => {
setQuery(pre => ({ ...pre, type: value }))
}
const handleProviderChange = (value: string) => {
setQuery(pre => ({ ...pre, provider: value }))
}
return (
<div className="rb:w-full">
<Row className='rb:mb-[16px] rb:w-full'>
<Col span={6}>
<SearchInput
placeholder={t('model.searchPlaceholder')}
<>
<Flex justify="space-between" align="center">
<PageTabs
value={activeTab}
options={formatTabItems()}
onChange={handleChangeTab}
/>
<Space size={12}>
{activeTab === 'list' ? <>
<CustomSelect
url={modelTypeUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
onChange={handleTypeChange}
className="rb:w-30"
allowClear={true}
placeholder={t('modelNew.type')}
/>
<CustomSelect
url={modelProviderUrl}
hasAll={false}
format={(items) => items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))}
onChange={handleProviderChange}
className="rb:w-30"
allowClear={true}
placeholder={t('modelNew.provider')}
/>
</>
: <SearchInput
placeholder={t(`modelNew.${activeTab}SearchPlaceholder`)}
onSearch={handleSearch}
style={{width: '100%'}}
/>
</Col>
<Col span={18} className="rb:text-right">
<Button type="primary" onClick={() => handleEdit()}>{t('model.createModel')}</Button>
</Col>
</Row>
className="rb:w-70!"
/>}
{activeTab === 'group' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createGroupModel')}</Button>}
{activeTab === 'square' && <Button type="primary" onClick={() => handleEdit()}>+ {t('modelNew.createCustomModel')}</Button>}
</Space>
</Flex>
<PageScrollList
ref={scrollListRef}
url={getModelListUrl}
query={query}
renderItem={(item: Model) => (
<RbCard
title={item.name}
>
{formatData(item)?.map((description: DescriptionItem) => (
<div
key={description.key}
className="rb:flex rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-[20px] rb:mb-[12px]"
>
<span className="rb:whitespace-nowrap">{(description.label as string)}</span>
<span className={clsx({
"rb:text-[#212332]": description.key !== 'is_active',
"rb:text-[#369F21] rb:font-medium": description.key === 'is_active' && item.is_active,
})}>{(description.children as string)}</span>
</div>
))}
<Button className="rb:mt-[8px]" type="primary" ghost block onClick={() => handleEdit(item)}>{t('model.configureBtn')}</Button>
</RbCard>
)}
/>
<ConfigModal
<div className="rb:w-full rb:h-[calc(100%-48px)] rb:my-4">
{activeTab === 'group' && <GroupModel ref={groupRef} query={query} handleEdit={handleEdit} />}
{activeTab === 'list' && <ModelList query={query} />}
{activeTab === 'square' && <ModelSquare ref={squareRef} query={query} handleEdit={handleEdit} />}
</div>
<GroupModelModal
ref={configModalRef}
refresh={() => scrollListRef?.current?.refresh()}
refresh={handleRefresh}
/>
</div>
<CustomModelModal
ref={customModelModalRef}
refresh={handleRefresh}
/>
</>
)
}

View File

@@ -1,70 +1,146 @@
// 模型表单数据类型
export interface ModelFormData extends ApiKey {
name: string;
type: string;
api_keys: ApiKey;
export interface Query {
type?: string;
provider?: string;
is_active?: boolean;
is_public?: boolean;
is_composite?: boolean;
search?: string;
}
export interface DescriptionItem {
key: string;
label: string;
children: string;
}
export interface CompositeModelForm {
logo?: any;
name: string;
type: string;
description: string;
api_key_ids: ModelApiKey[] | string[];
}
export interface GroupModelModalRef {
handleOpen: (model?: ModelListItem) => void;
}
export interface GroupModelModalProps {
refresh?: () => void;
}
export interface SubModelModalForm {
provider: string;
model_ids: string[]
}
export interface SubModelModalRef {
handleOpen: (model?: SubModelModalForm) => void;
}
export interface SubModelModalProps {
refresh?: () => void;
}
export interface ModelListDetailRef {
handleOpen: (vo: ProviderModelItem) => void;
}
// 模型类型定义
export interface Model {
export interface ModelApiKey {
model_name: string;
description: string | null;
provider: string;
api_key: string;
api_base: string;
config: any;
is_active: boolean;
priority: string;
id: string;
usage_count: string;
last_used_at: number;
created_at: number;
updated_at: number;
model_config_ids: string[];
}
export interface ModelListItem {
model_name?: string;
model_config_ids: string[];
name: string;
type: string;
logo: string;
description: string;
provider: string;
config: any;
is_active: boolean;
is_public: boolean;
id: string;
created_at: number;
updated_at: number;
api_keys: ModelApiKey[]
}
export interface ProviderModelItem {
provider: string;
logo?: string;
tags: string[];
models: ModelListItem[];
}
export interface KeyConfigModalForm {
provider: string;
api_key: string;
api_base: string;
}
export interface KeyConfigModalRef {
handleOpen: (vo: ProviderModelItem) => void;
}
export interface KeyConfigModalProps {
refresh?: () => void;
}
export interface MultiKeyForm {
model_config_id?: string;
model_name: string;
provider: string;
api_key: string;
api_base: string;
}
export interface MultiKeyConfigModalRef {
handleOpen: (vo: ModelListItem, provider?: string) => void;
}
export interface MultiKeyConfigModalProps {
refresh?: () => void;
}
export interface ModelPlaza {
provider: string;
models: ModelPlazaItem[];
}
export interface ModelPlazaItem {
id: string;
name: string;
type: string;
description?: string;
config: Record<string, unknown>;
is_active: boolean;
is_public: boolean;
created_at: string | number;
updated_at: string | number;
api_keys: ApiKey[];
// provider: string;
// temperature: number,
// topP: number,
// status: string;
// vectorDimension: number;
// batchSize: number;
// truncateStrategy: string;
// created: string;
// updatedAt: string;
// descriptionItems?: Record<string, unknown>[];
// basicParameters?: string;
// normalization?: string;
// maxInputLength?: number;
// encodingFormat?: string;
// enablePooling?: boolean;
// poolingStrategy?: string;
// apiKey?: string;
// apiEndpoint?: string;
// timeout?: number;
// autoRetry?: boolean;
// retryCount?: number;
}
interface ApiKey {
model_name?: string;
provider: string;
api_key?: string;
api_base?: string;
config?: Record<string, unknown>;
is_active?: boolean;
priority?: string;
id: string;
model_config_id?: string;
usage_count?: string;
last_used_at?: string | null;
created_at?: string;
updated_at?: string;
logo: string;
description: string;
is_deprecated: boolean;
is_official: boolean;
tags: string[];
add_count: number;
is_added: boolean;
}
// 定义组件暴露的方法接口
export interface ConfigModalRef {
handleOpen: (model?: Model) => void;
export interface ModelSquareDetailRef {
handleOpen: (vo: ModelPlaza) => void;
}
export interface ConfigModalProps {
export interface CustomModelForm {
name: string;
type: string;
provider: string;
logo: string;
description: string;
is_official: boolean;
tags: string[];
}
export interface CustomModelModalRef {
handleOpen: (vo?: ModelPlazaItem) => void;
}
export interface CustomModelModalProps {
refresh?: () => void;
}
export interface BaseRef {
getList: () => void;
}