Merge pull request #213 from SuanmoSuanyangTechnology/feature/model_zy

Feature/model zy
This commit is contained in:
yingzhao
2026-01-28 14:46:09 +08:00
committed by GitHub
41 changed files with 1862 additions and 386 deletions

View File

@@ -0,0 +1,25 @@
import { request, API_PREFIX } from '@/utils/request'
// Upload filefile storage has expiration period
export const fileUploadUrl = `${API_PREFIX}/storage/files`
export const fileUpload = (formData?: unknown) => {
return request.uploadFile('/storage/files', formData)
}
// Get file access URL (no token required)
export const getFileUrl = (file_id: string) => `/storage/files/${file_id}/url`
export const getFileLink = (fileId: string, data: { permanent?: boolean } = { permanent: true }) => {
return request.get(getFileUrl(fileId), data)
}
// Get file internally
export const getInternalFileUrl = (file_id: string) => `/storage/files/${file_id}`
export const getInternalFile = (fileId: string) => {
return request.get(getInternalFileUrl(fileId))
}
// Delete file
export const deleteFileUrl = (file_id: string) => `/storage/files/${file_id}`
export const deleteFile = (fileId: string) => {
return request.delete(deleteFileUrl(fileId))
}

View File

@@ -65,7 +65,7 @@ export const getModelTypeList = async () => {
};
// 获取模型列表
export const getModelList = async (pageInfo: PageRequest) => {
const response = await request.get(`${apiPrefix}/models`, pageInfo);
const response = await request.get(`${apiPrefix}/models`, { ...pageInfo, is_active: true });
return response as any;
};
//获取模型提供者

View File

@@ -1,23 +1,68 @@
import { request } from '@/utils/request'
import type { ModelFormData } from '@/views/ModelManagement/types'
import type { MultiKeyForm, Query, KeyConfigModalForm, CompositeModelForm, CustomModelForm } from '@/views/ModelManagement/types'
// 模型列表
// Model list
export const getModelListUrl = '/models'
export const getModelList = (data: { type: string; pagesize: number; page: number; }) => {
export const getModelList = (data: Query) => {
return request.get(getModelListUrl, data)
}
// 创建模型
export const addModel = (data: ModelFormData) => {
return request.post('/models', data)
}
// 更新模型
export const updateModel = (apiKeyId: string, data: ModelFormData) => {
return request.put(`/models/apikeys/${apiKeyId}`, data)
}
// 模型类型列表
// Model type list
export const modelTypeUrl = '/models/type'
// 模型供应商列表
// Model provider list
export const modelProviderUrl = '/models/provider'
export const getModelProviderList = () => {
return request.get(modelProviderUrl)
}
// New model list
export const getModelNewListUrl = '/models/new'
export const getModelNewList = (data: Query) => {
return request.get(getModelNewListUrl, data)
}
// Get model information
export const getModelInfo = (model_id: string) => {
return request.get(`/models/${model_id}`)
}
// Create composite model
export const addCompositeModel = (data: CompositeModelForm) => {
return request.post('/models/composite', data)
}
// Update composite model
export const updateCompositeModel = (model_id: string, data: CompositeModelForm) => {
return request.put(`/models/composite/${model_id}`, data)
}
// Delete composite model
export const deleteCompositeModel = (model_id: string) => {
return request.delete(`/models/composite/${model_id}`)
}
// Create API keys for all matching models by provider
export const updateProviderApiKeys = (data: KeyConfigModalForm) => {
return request.post('/models/provider/apikeys', data)
}
// Create model API key
export const addModelApiKey = (model_id: string, data: MultiKeyForm) => {
return request.post(`/models/${model_id}/apikeys`, data)
}
// Delete model API key
export const deleteModelApiKey = (api_key_id: string) => {
return request.delete(`/models/apikeys/${api_key_id}`)
}
// Update model status
export const updateModelStatus = (model_id: string, data: { is_active: boolean; }) => {
return request.put(`/models/${model_id}`, data)
}
// Model plaza list
export const getModelPlaza = (data: { search?: string; provider?: string; }) => {
return request.get('/models/model_plaza', data)
}
// Add model to plaza
export const addModelPlaza = (model_base_id: string) => {
return request.post(`/models/model_plaza/${model_base_id}/add`)
}
// Create custom model
export const addCustomModel = (data: CustomModelForm) => {
return request.post('/models/model_plaza', data)
}
// Update custom model
export const updateCustomModel = (model_base_id: string, data: CustomModelForm) => {
return request.put(`/models/model_plaza/${model_base_id}`, data)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -0,0 +1,16 @@
import { useTranslation } from 'react-i18next'
import pageEmptyIcon from '@/assets/images/empty/pageEmpty.png'
import Empty from './index'
const PageEmpty = ({ size = [240, 210] }: { size?: number | number[] }) => {
const { t } = useTranslation()
return (
<Empty
url={pageEmptyIcon}
title={t('empty.pageEmpty')}
subTitle={t('empty.pageEmptyDesc')}
size={size}
className="rb:h-full"
/>
)
}
export default PageEmpty;

View File

@@ -0,0 +1,13 @@
.page-tabs:global(.ant-segmented) {
background-color: rgba(91, 97, 103, 0.08);
padding: 4px;
}
.page-tabs:global(.ant-segmented .ant-segmented-item-label) {
line-height: 24px;
min-height: 24px;
padding: 0 12px;
}
.page-tabs:global(.ant-segmented .ant-segmented-item-selected) {
box-shadow: 0px 2px 4px 0px rgba(33, 35, 50, 0.16);
}

View File

@@ -0,0 +1,18 @@
import { type FC } from 'react';
import { Segmented, type SegmentedProps } from 'antd';
import styles from './index.module.css';
const PageTabs: FC<SegmentedProps> = ({
value,
options,
onChange
}) => {
return <Segmented
value={value}
options={options}
onChange={onChange}
className={styles.pageTabs}
/>;
};
export default PageTabs;

View File

@@ -1,5 +1,5 @@
import { type FC, type ReactNode } from 'react'
import { Card } from 'antd';
import { Card, Tooltip } from 'antd';
import clsx from 'clsx';
interface RbCardProps {
@@ -9,7 +9,7 @@ interface RbCardProps {
extra?: ReactNode;
children?: ReactNode;
avatar?: ReactNode;
avatarUrl?: string;
avatarUrl?: string | null;
bodyPadding?: string;
bodyClassName?: string;
headerType?: 'border' | 'borderless' | 'borderBL' | 'borderL';
@@ -63,7 +63,7 @@ const RbCard: FC<RbCardProps> = ({
}
)
}>
<div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div>
<Tooltip title={title}><div className="rb:w-full rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{title}</div></Tooltip>
{subTitle && <div className="rb:text-[#5B6167] rb:text-[12px]">{subTitle}</div>}
</div>
</div> : null

View File

@@ -1,23 +1,23 @@
import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
import { Upload, Modal, Image, App } from 'antd';
import { Upload, Image, App } from 'antd';
import type { GetProp, UploadFile, UploadProps } from 'antd';
// import { UploadOutlined, } from '@ant-design/icons';
import type { UploadProps as RcUploadProps } from 'antd/es/upload/interface';
import { useTranslation } from 'react-i18next';
import PlusIcon from '@/assets/images/plus.svg'
import { cookieUtils } from '@/utils/request'
import { fileUploadUrl } from '@/api/fileStorage'
import styles from './index.module.less'
const { confirm } = Modal;
interface UploadImagesProps extends Omit<UploadProps, 'onChange'> {
interface UploadImagesProps extends Omit<UploadProps, 'onChange' | 'fileList'> {
/** 上传接口地址 */
action?: string;
/** 是否支持多选 */
multiple?: boolean;
/** 已上传的文件列表 */
fileList?: UploadFile[];
fileList?: UploadFile[] | UploadFile;
/** 文件列表变化回调 */
onChange?: (fileList: UploadFile[]) => void;
onChange?: (fileList?: UploadFile[] | UploadFile) => void;
/** 禁用上传 */
disabled?: boolean;
/** 文件大小限制MB */
@@ -28,6 +28,7 @@ interface UploadImagesProps extends Omit<UploadProps, 'onChange'> {
isAutoUpload?: boolean;
/** 最大上传文件数 */
maxCount?: number;
className?: string;
}
const ALL_FILE_TYPE: {
[key: string]: string;
@@ -59,7 +60,7 @@ const getBase64 = (file: FileType): Promise<string> => {
* 支持单文件/多文件上传、拖拽上传、文件验证、预览等功能
*/
const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
action = '/api/upload',
action = fileUploadUrl,
multiple = false,
fileList: propFileList = [],
onChange,
@@ -68,27 +69,42 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
fileType = ['png', 'jpg', 'gif'],
isAutoUpload = true,
maxCount = 1,
className = 'rb:size-24! rb:leading-1!',
...props
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp()
const [fileList, setFileList] = useState<UploadFile[]>(propFileList);
const { message, modal } = App.useApp()
const [fileList, setFileList] = useState<UploadFile[]>([]);
const [accept, setAccept] = useState<string | undefined>();
// const [loading, setLoading] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState('');
useEffect(() => {
if (!Array.isArray(propFileList) && typeof propFileList === 'object') {
setFileList([propFileList]);
}
}, [propFileList])
const updateValue = (list: UploadFile[]) => {
if (maxCount === 1) {
onChange?.(list[0])
} else {
onChange?.(list)
}
}
// 处理文件移除
const handleRemove = (file: UploadFile) => {
confirm({
title: '确定要删除此文件吗?',
okText: '确定',
modal.confirm({
title: t('common.confirmRemoveFile'),
okText: `${t('common.confirm')}`,
okType: 'danger',
cancelText: '取消',
cancelText: `${t('common.cancel')}`,
onOk: () => {
const newFileList = fileList.filter((item) => item.uid !== file.uid);
setFileList(newFileList);
onChange?.(newFileList);
updateValue(newFileList)
},
});
return false; // 阻止默认删除行为由confirm控制
@@ -100,7 +116,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
if (fileSize && file.size) {
const isLtMaxSize = (file.size / 1024 / 1024) < fileSize;
if (!isLtMaxSize) {
message.error(`文件大小不能超过 ${fileSize}MB`);
message.error(t('common.fileSizeTip', { size: fileSize }));
return Upload.LIST_IGNORE;
}
}
@@ -108,7 +124,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
if (accept && accept.length > 0 && file.type) {
const isAccept = accept.includes(file.type);
if (!isAccept) {
message.error(`不支持的文件类型: ${file.type}`);
message.error(`${t('common.fileAcceptTip')}${file.type}`);
return Upload.LIST_IGNORE;
}
}
@@ -119,7 +135,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
}
const newFileList = [...fileList, file];
setFileList(newFileList);
onChange?.(newFileList);
updateValue(newFileList);
return Upload.LIST_IGNORE; // 阻止自动上传
}
@@ -129,17 +145,13 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
// 处理上传状态变化
const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
setFileList(newFileList);
if (onChange) {
onChange(newFileList);
}
updateValue(newFileList);
};
// 清空已上传文件
const clearFiles = () => {
setFileList([]);
if (onChange) {
onChange([]);
}
updateValue([]);
}
const handlePreview = async (file: UploadFile) => {
@@ -167,7 +179,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
fileList,
beforeUpload,
headers: {
authorization: cookieUtils.get('authToken') || '',
authorization: `Bearer ${cookieUtils.get('authToken') }`,
},
onPreview: handlePreview,
onRemove: handleRemove,
@@ -180,6 +192,7 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
showRemoveIcon: true,
showDownloadIcon: false,
},
className: `${styles.imageUpload} ${className}`,
...props,
};
@@ -193,16 +206,9 @@ const UploadImages = forwardRef<UploadImagesRef, UploadImagesProps>(({
<>
<Upload
{...uploadProps}
style={{
width: '136px',
height: '136px',
}}
>
{fileList.length < maxCount && (
<div className="rb:flex rb:flex-wrap rb:items-center rb:justify-center">
<img src={PlusIcon} className="rb:w-[32px] rb:h-[32px]" />
<div className="rb:mt-[12px] rb:text-[12px] rb:text-[#5B6167] rb:leading-[16px]">{t('common.clickUploadIcon')}</div>
</div>
<img src={PlusIcon} className="rb:size-7" />
)}
</Upload>
{previewImage && (

View File

@@ -0,0 +1,7 @@
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container),
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-card .ant-upload-list-item-container),
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-card-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container),
.image-upload:global(.ant-upload-wrapper.ant-upload-picture-circle-wrapper .ant-upload-list.ant-upload-list-picture-circle .ant-upload-list-item-container) {
width: 96px;
height: 96px;
}

View File

@@ -419,6 +419,9 @@ export const en = {
statusEnabled: 'Available',
statusDisabled: 'Unavailable',
remove: 'Remove',
fileSizeTip: 'File size cannot exceed {{size}}MB',
fileAcceptTip: 'Unsupported file type:'
},
model: {
searchPlaceholder: 'search model…',
@@ -510,6 +513,59 @@ export const en = {
gpustack: "Gpustack",
bedrock: "Bedrock"
},
modelNew: {
group: 'Model Group',
list: 'Model List',
square: 'Model Plaza',
createGroupModel: 'Create Model Group',
groupSearchPlaceholder: 'Search model groups',
listSearchPlaceholder: 'Search available models',
squareSearchPlaceholder: 'Search platform models',
status: 'Model Status',
created_at: 'Created At',
configureBtn: 'Click to Configure',
showModel: 'Show Model',
keyConfig: 'Configure KEY',
modelConfiguration: 'Model Configuration',
logo: 'Model LOGO',
name: 'Model Name',
type: 'Model Type',
modelImplement: 'Model Implementation',
addImplement: 'Add Implementation',
noAuth: 'Unauthorized (Limited to 1 implementation)',
implementConfig: 'Configure Model Implementation',
provider: 'Model Provider',
api_key_ids: 'Select Model',
viewAll: 'More',
modelCount: 'Total {{count}} models',
modelList: 'Model List',
added: ' Added',
addSuccess: 'Added successfully',
model_name: 'Model Name',
tags: 'Tags',
createCustomModel: 'Add Custom Model',
edit: 'Edit',
selectOneTip: 'Model API KEY not configured, please configure in Model Plaza first',
api_key: 'API KEY',
api_base: 'API Base URL',
description: 'Description',
add: 'Add',
item: 'item',
apiKeyNum: ' API Keys',
llm: 'LLM',
chat: 'Chat',
embedding: 'Embedding',
rerank: 'Rerank',
openai: "Openai",
dashscope: "Dashscope",
ollama: "Ollama",
xinference: "Xinference",
gpustack: "Gpustack",
bedrock: "Bedrock"
},
knowledgeBase: {
pleaseUploadFileFirst: 'Please upload file first',
shareSuccess: 'Share successfully',
@@ -1534,7 +1590,9 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
noPermissionDesc: ' Please contact the administrator to grant permission',
tableEmpty: 'No data available.',
loadingEmpty: 'The content is loading…',
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen'
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen',
pageEmpty: 'Oops No search results available at the moment',
pageEmptyDesc: "Red Bear tilts its head and waits for you to change a new keyword, let's explore together.",
},
apiKey: {
name: 'Project Name',

View File

@@ -967,6 +967,9 @@ export const zh = {
statusEnabled: '可用',
statusDisabled: '不可用',
remove: '删除',
fileSizeTip: '文件大小不能超过 {{size}}MB',
fileAcceptTip: '不支持的文件类型:'
},
product: {
applicationManagement: '应用管理',
@@ -1076,6 +1079,59 @@ export const zh = {
gpustack: "Gpustack",
bedrock: "Bedrock"
},
modelNew: {
group: '模型组合',
list: '模型列表',
square: '模型广场',
createGroupModel: '创建模型组合',
groupSearchPlaceholder: '搜索模型组合',
listSearchPlaceholder: '搜索可用模型',
squareSearchPlaceholder: '搜索平台模型',
status: '模型状态',
created_at: '创建时间',
configureBtn: '点击配置',
showModel: '显示模型',
keyConfig: '配置 KEY',
modelConfiguration: '模型配置',
logo: '模型LOGO',
name: '模型名称',
type: '模型类型',
modelImplement: '模型实现',
addImplement: '添加实现',
noAuth: '未授权(限1个实现)',
implementConfig: '配置模型实现',
provider: '模型供应商',
api_key_ids: '选择模型',
viewAll: '更多',
modelCount: '共 {{count}} 个模型',
modelList: '模型列表',
added: ' 已添加',
addSuccess: '添加成功',
model_name: '模型名称',
tags: '标签',
createCustomModel: '添加自定义模型',
edit: '编辑',
selectOneTip: '模型未配置API KEY请先在模型广场配置',
api_key: 'API KEY',
api_base: 'API Base URL',
description: '描述',
add: '添加',
item: '个',
apiKeyNum: '个 API Key',
llm: 'LLM',
chat: 'Chat',
embedding: 'Embedding',
rerank: 'Rerank',
openai: "Openai",
dashscope: "Dashscope",
ollama: "Ollama",
xinference: "Xinference",
gpustack: "Gpustack",
bedrock: "Bedrock"
},
timezones: {
'Asia/Shanghai': '中国标准时间 (UTC+8)',
'Asia/Kolkata': '印度标准时间 (UTC+5:30)',
@@ -1607,7 +1663,9 @@ export const zh = {
noPermissionDesc: '请联系管理员授予权限',
tableEmpty: '目前没有数据',
loadingEmpty: '内容正在加载中…',
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上'
loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上',
pageEmpty: '哎呀!暂无搜索结果',
pageEmptyDesc: '红熊歪着头等待您更换新的关键词,让我们一起探索吧。',
},
home: {

View File

@@ -22,7 +22,7 @@ export const lightTheme: ThemeConfig = {
// colorBgContainer: '#FBFDFF',
colorError: '#FF5D34',
sizeSM: 12,
fontSizeSM: 12,
fontSizeSM: 12,
},
components: {
Layout: {
@@ -105,6 +105,9 @@ export const lightTheme: ThemeConfig = {
},
Select: {
lineHeightSM: 26
},
Upload: {
pictureCardSize: 96,
}
}
};

View File

@@ -23,9 +23,10 @@ interface data {
}
export const API_PREFIX = '/api'
// 创建axios实例
const service = axios.create({
baseURL: '/api', // 与vite.config.ts中的代理配置对应
baseURL: API_PREFIX, // 与vite.config.ts中的代理配置对应
// timeout: 10000, // 请求超时时间
withCredentials: false,
headers: {
@@ -126,7 +127,7 @@ service.interceptors.response.use(
if (axios.isCancel(error) || error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
return Promise.reject(error);
}
// 处理网络错误、超时等
let msg = error.response?.data?.error || error.response?.error;
const status = error?.response ? error.response.status : error;

View File

@@ -20,7 +20,7 @@ import type {
} from './types'
import type { Variable } from './components/VariableList/types'
import type { KnowledgeConfig } from './components/Knowledge/types'
import type { Model } from '@/views/ModelManagement/types'
import type { ModelListItem } from '@/views/ModelManagement/types'
import { getModelList } from '@/api/models';
import { saveAgentConfig } from '@/api/application'
import Knowledge from './components/Knowledge/Knowledge'
@@ -96,8 +96,8 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
const [loading, setLoading] = useState(false)
const [data, setData] = useState<Config | null>(null);
const modelConfigModalRef = useRef<ModelConfigModalRef>(null)
const [modelList, setModelList] = useState<Model[]>([])
const [defaultModel, setDefaultModel] = useState<Model | null>(null)
const [modelList, setModelList] = useState<ModelListItem[]>([])
const [defaultModel, setDefaultModel] = useState<ModelListItem | null>(null)
const [chatList, setChatList] = useState<ChatData[]>([])
const values = Form.useWatch<Config>([], form)
const [isSave, setIsSave] = useState(false)
@@ -239,9 +239,9 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
})
}
const getModels = () => {
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
.then(res => {
const response = res as { items: Model[] }
const response = res as { items: ModelListItem[] }
setModelList(response.items)
})
}
@@ -251,7 +251,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
useEffect(() => {
if (values?.default_model_config_id && modelList.length > 0) {
const filterValue = modelList.find(item => item.id === values.default_model_config_id)
setDefaultModel(filterValue as Model | null)
setDefaultModel(filterValue as ModelListItem | null)
setChatList([{
label: filterValue?.name || '',
model_config_id: filterValue?.id || '',

View File

@@ -225,7 +225,7 @@ const Cluster = forwardRef<ClusterRef>((_props, ref) => {
<Form.Item name="default_model_config_id" noStyle>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm,chat', pagesize: 100 }}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -181,7 +181,7 @@ const AiPromptModal = forwardRef<AiPromptModalRef, AiPromptModalProps>(({
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm,chat', pagesize: 100 }}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -97,7 +97,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -20,7 +20,7 @@ const configList = [
key: 'emotion_model_id',
type: 'customSelect',
url: getModelListUrl,
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
},
{
key: 'emotion_min_intensity',

View File

@@ -39,7 +39,7 @@ const MemberManagement: React.FC = () => {
onOk: () => {
deleteMember(member.id)
.then(() => {
message.success(t('member.deleteSuccess'));
message.success(t('common.deleteSuccess'));
refreshTable();
})
}
@@ -93,7 +93,7 @@ const MemberManagement: React.FC = () => {
return (
<>
<div className="rb:flex rb:justify-end rb:mb-[12px]">
<div className="rb:flex rb:justify-end rb:mb-3">
<Button type="primary" onClick={() => handleEdit()}>
{t('member.createMember')}
</Button>

View File

@@ -1,14 +1,14 @@
import { type FC, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
import { Row, Col, Space, Switch, Select, InputNumber, Slider, App, Form } from 'antd'
import { Row, Col, Space, Select, InputNumber, Slider, App, Form } from 'antd'
import clsx from 'clsx'
import Card from './components/Card'
import type { ConfigForm, Variable } from './types'
import { getMemoryExtractionConfig, updateMemoryExtractionConfig } from '@/api/memory'
import Markdown from '@/components/Markdown'
import { getModelList } from '@/api/models';
import type { Model } from '@/views/ModelManagement/types'
import type { ModelListItem } from '@/views/ModelManagement/types'
import { configList } from './constant'
import Result from './components/Result'
import SwitchFormItem from '@/components/FormItem/SwitchFormItem'
@@ -43,7 +43,7 @@ const MemoryExtractionEngine: FC = () => {
const values = Form.useWatch<ConfigForm>([], form)
const [loading, setLoading] = useState(false)
const [iterationPeriodDisabled, setIterationPeriodDisabled] = useState(false)
const [modelList, setModelList] = useState<Model[]>([])
const [modelList, setModelList] = useState<ModelListItem[]>([])
useEffect(() => {
if (values?.reflexion_range === 'database') {
@@ -55,9 +55,9 @@ const MemoryExtractionEngine: FC = () => {
}, [values])
const getModels = () => {
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
.then(res => {
const response = res as { items: Model[] }
const response = res as { items: ModelListItem[] }
setModelList(response.items)
})
}

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,95 @@
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}
handleEdit={handleEdit}
/>
</>
)
})
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,165 @@
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,
logo: model.logo ? { url: model.logo, uid: model.logo, status: 'done', name: 'logo' } : undefined
});
} else {
setIsEdit(false);
form.resetFields();
}
setVisible(true);
};
const handleUpdate = (data: CustomModelForm) => {
setLoading(true)
const res = isEdit ? updateCustomModel(model.id, data) : addCustomModel(data)
res.then(() => {
refresh && refresh()
handleClose()
message.success(isEdit ? t('common.updateSuccess') : t('common.createSuccess'))
})
.catch(() => {
setLoading(false)
});
}
const handleSave = () => {
form
.validateFields()
.then((values) => {
setLoading(true)
const { logo, ...rest } = values;
let formData: CustomModelForm = {
...rest
}
formData.is_official = false;
if (typeof logo === 'object' && logo?.response?.data.file_id) {
getFileLink(logo?.response?.data.file_id)
.then(res => {
const logoRes = res as { url: string }
formData.logo = logoRes.url
handleUpdate(formData)
})
.catch(() => {
handleUpdate(formData)
})
} else {
formData.logo = typeof logo === 'string' ? logo : logo.url
handleUpdate(formData)
}
})
.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"
>
<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.inputPlaceholder', { title: t('modelNew.name') }) }]}
>
<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')}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</Form.Item>
<Form.Item
name="tags"
label={t('modelNew.tags')}
>
<Select mode="tags" placeholder={t('common.pleaseEnter')} />
</Form.Item>
</Form>
</RbModal>
);
});
export default CustomModelModal;

View File

@@ -0,0 +1,158 @@
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
handleUpdate(formData)
}).catch(() => {
handleUpdate(formData)
})
} else {
formData.logo = typeof logo === 'string' ? logo : logo.url
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,164 @@
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 [selecteds, setSelecteds] = useState<any[]>([])
const [modelList, setModelList] = useState<Option[]>([])
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
form.resetFields();
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(() => {
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}
>
<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,95 @@
import { useState, useImperativeHandle, forwardRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Space, App, Flex } from 'antd'
import { UsergroupAddOutlined } from '@ant-design/icons';
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;
handleEdit: (vo: ModelPlazaItem) => void;
}
const ModelSquareDetail = forwardRef<ModelSquareDetailRef, ModelSquareDetailProps>(({ refresh, handleEdit }, 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>
<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>
}
</RbDrawer>
);
});
export default ModelSquareDetail;

View File

@@ -0,0 +1,122 @@
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, deleteModelApiKey, 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) => {
setLoading(true)
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) => {
deleteModelApiKey(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,139 @@
// 模型表单数据类型
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;
pagesize?: number;
page?: number;
}
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 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?: any;
description: string;
is_official: boolean;
tags: string[];
}
export interface CustomModelModalRef {
handleOpen: (vo?: ModelPlazaItem) => void;
}
export interface CustomModelModalProps {
refresh?: () => void;
}
export interface BaseRef {
getList: () => void;
}

View File

@@ -24,7 +24,7 @@ const configList = [
key: 'reflection_model_id',
type: 'customSelect',
url: getModelListUrl,
params: { type: 'chat,llm', page: 1, pagesize: 100 }, // chat,llm
params: { type: 'chat,llm', page: 1, pagesize: 100, is_active: true }, // chat,llm
},
// 迭代周期
{

View File

@@ -66,7 +66,7 @@ const SpaceConfig: FC = () => {
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'llm', pagesize: 100 }}
params={{ type: 'llm', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
@@ -80,7 +80,7 @@ const SpaceConfig: FC = () => {
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'embedding', pagesize: 100 }}
params={{ type: 'embedding', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
@@ -94,7 +94,7 @@ const SpaceConfig: FC = () => {
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -8,7 +8,7 @@ import { createWorkspace } from '@/api/workspaces'
import RadioGroupCard from '@/components/RadioGroupCard'
import { getModelListUrl, getModelList } from '@/api/models'
import CustomSelect from '@/components/CustomSelect'
import type { Model } from '@/views/ModelManagement/types'
import type { ModelListItem } from '@/views/ModelManagement/types'
const FormItem = Form.Item;
@@ -29,7 +29,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
const [form] = Form.useForm<SpaceModalData>();
const [loading, setLoading] = useState(false)
const [editVo, setEditVo] = useState<Space | null>(null)
const [modelList, setModelList] = useState<Model[]>([])
const [modelList, setModelList] = useState<ModelListItem[]>([])
const values = Form.useWatch([], form);
@@ -80,9 +80,9 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
}, [])
const getModels = () => {
getModelList({ type: 'llm,chat', pagesize: 100, page: 1 })
getModelList({ type: 'llm,chat', pagesize: 100, page: 1, is_active: true })
.then(res => {
const response = res as { items: Model[] }
const response = res as { items: ModelListItem[] }
setModelList(response.items)
})
}
@@ -134,7 +134,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'embedding', pagesize: 100 }}
params={{ type: 'embedding', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}
@@ -148,7 +148,7 @@ const SpaceModal = forwardRef<SpaceModalRef, SpaceModalProps>(({
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -98,7 +98,7 @@ const KnowledgeGlobalConfigModal = forwardRef<KnowledgeGlobalConfigModalRef, Kno
>
<CustomSelect
url={getModelListUrl}
params={{ type: 'rerank', pagesize: 100 }}
params={{ type: 'rerank', pagesize: 100, is_active: true }}
valueKey="id"
labelKey="name"
hasAll={false}

View File

@@ -105,7 +105,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat' }, // llm/chat
params: { type: 'llm,chat', is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},
@@ -166,7 +166,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat' }, // llm/chat
params: { type: 'llm,chat', is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},
@@ -259,7 +259,7 @@ export const nodeLibrary: NodeLibrary[] = [
model_id: {
type: 'customSelect',
url: getModelListUrl,
params: { type: 'llm,chat' }, // llm/chat
params: { type: 'llm,chat', is_active: true }, // llm/chat
valueKey: 'id',
labelKey: 'name',
},