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

@@ -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;