diff --git a/web/src/api/fileStorage.ts b/web/src/api/fileStorage.ts new file mode 100644 index 00000000..e7b476a3 --- /dev/null +++ b/web/src/api/fileStorage.ts @@ -0,0 +1,25 @@ +import { request, API_PREFIX } from '@/utils/request' + +// Upload file,file 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)) +} diff --git a/web/src/api/models.ts b/web/src/api/models.ts index 20fdf91a..f8619e43 100644 --- a/web/src/api/models.ts +++ b/web/src/api/models.ts @@ -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 delteModelApiKey = (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) } \ No newline at end of file diff --git a/web/src/components/RbCard/Card.tsx b/web/src/components/RbCard/Card.tsx index f86b1c60..eadd2916 100644 --- a/web/src/components/RbCard/Card.tsx +++ b/web/src/components/RbCard/Card.tsx @@ -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 = ({ } ) }> -
{title}
+
{title}
{subTitle &&
{subTitle}
} : null diff --git a/web/src/components/Upload/UploadImages.tsx b/web/src/components/Upload/UploadImages.tsx index 2006ea09..77291e92 100644 --- a/web/src/components/Upload/UploadImages.tsx +++ b/web/src/components/Upload/UploadImages.tsx @@ -1,13 +1,12 @@ 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' - -const { confirm } = Modal; +import { fileUploadUrl } from '@/api/fileStorage' interface UploadImagesProps extends Omit { /** 上传接口地址 */ @@ -17,7 +16,7 @@ interface UploadImagesProps extends Omit { /** 已上传的文件列表 */ fileList?: UploadFile[]; /** 文件列表变化回调 */ - onChange?: (fileList: UploadFile[]) => void; + onChange?: (fileList?: UploadFile[] | UploadFile) => void; /** 禁用上传 */ disabled?: boolean; /** 文件大小限制(MB) */ @@ -28,6 +27,7 @@ interface UploadImagesProps extends Omit { isAutoUpload?: boolean; /** 最大上传文件数 */ maxCount?: number; + className?: string; } const ALL_FILE_TYPE: { [key: string]: string; @@ -59,7 +59,7 @@ const getBase64 = (file: FileType): Promise => { * 支持单文件/多文件上传、拖拽上传、文件验证、预览等功能 */ const UploadImages = forwardRef(({ - action = '/api/upload', + action = fileUploadUrl, multiple = false, fileList: propFileList = [], onChange, @@ -68,27 +68,36 @@ const UploadImages = forwardRef(({ 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 { message, modal } = App.useApp() const [fileList, setFileList] = useState(propFileList); const [accept, setAccept] = useState(); // const [loading, setLoading] = useState(false); const [previewOpen, setPreviewOpen] = useState(false); const [previewImage, setPreviewImage] = useState(''); + 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 +109,7 @@ const UploadImages = forwardRef(({ 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 +117,7 @@ const UploadImages = forwardRef(({ 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 +128,7 @@ const UploadImages = forwardRef(({ } const newFileList = [...fileList, file]; setFileList(newFileList); - onChange?.(newFileList); + updateValue(newFileList); return Upload.LIST_IGNORE; // 阻止自动上传 } @@ -129,17 +138,13 @@ const UploadImages = forwardRef(({ // 处理上传状态变化 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 +172,7 @@ const UploadImages = forwardRef(({ fileList, beforeUpload, headers: { - authorization: cookieUtils.get('authToken') || '', + authorization: `Bearer ${cookieUtils.get('authToken') }`, }, onPreview: handlePreview, onRemove: handleRemove, @@ -193,16 +198,9 @@ const UploadImages = forwardRef(({ <> {fileList.length < maxCount && ( -
- -
{t('common.clickUploadIcon')}
-
+ )}
{previewImage && ( diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 1df2eb6d..aee68114 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -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', @@ -866,7 +922,7 @@ export const en = { minimumRetention: 'Minimum retention (λ_time)', minimumRetentionDesc: 'Controls the minimum retention threshold of memory retention', - forgettingRate: 'Forgetting rate (λ_mem)', + forgettingRate: 'Forgetting rate (λ_mem)', forgettingRateDesc: 'Control the speed of memory forgetting, the higher the value, the faster the forgetting', offset: 'Offset (offset)', offsetDesc: 'The offset of the minimum preservation degree', @@ -934,7 +990,7 @@ export const en = { number: 'Number', checkbox: 'Checkbox', apiVariable: 'API Variable', - + displayName: 'Display Name', maxLength: 'Max Length', required: 'Required', @@ -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', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 39908757..78fe948a 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -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,13 +1663,10 @@ export const zh = { noPermissionDesc: '请联系管理员授予权限', tableEmpty: '目前没有数据', loadingEmpty: '内容正在加载中…', - loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上' + loadingEmptyDesc: '您的内容正在火箭运输中!很快就会降落在您的屏幕上', + pageEmpty: '哎呀!暂无搜索结果', + pageEmptyDesc: '红熊歪着头等待您更换新的关键词,让我们一起探索吧。', }, - count: '计数: {{count}}', - increment: '增加', - decrement: '减少', - reset: '重置', - switchLanguage: '切换语言', home: { title: '首页', diff --git a/web/src/styles/antdThemeConfig.ts b/web/src/styles/antdThemeConfig.ts index db1166fb..1d281730 100644 --- a/web/src/styles/antdThemeConfig.ts +++ b/web/src/styles/antdThemeConfig.ts @@ -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, } } }; \ No newline at end of file diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 479fc1f3..e7112ded 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -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; diff --git a/web/src/views/MemberManagement/index.tsx b/web/src/views/MemberManagement/index.tsx index 8ce2fc62..68c90410 100644 --- a/web/src/views/MemberManagement/index.tsx +++ b/web/src/views/MemberManagement/index.tsx @@ -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 ( <> -
+
diff --git a/web/src/views/ModelManagement/Group.tsx b/web/src/views/ModelManagement/Group.tsx new file mode 100644 index 00000000..311455b4 --- /dev/null +++ b/web/src/views/ModelManagement/Group.tsx @@ -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 void; }>(({ query, handleEdit }, ref) => { + const { t } = useTranslation(); + const [list, setList] = useState([]) + 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 + ? + :( +
+ {list.map(item => ( + + {item.name[0]} +
+ } + > + {formatData(item)?.map((description: DescriptionItem) => ( +
+ {(description.label as string)} + {(description.children as string)} +
+ ))} + + + ))} +
+ ) + } + + ) +}) + +export default Group \ No newline at end of file diff --git a/web/src/views/ModelManagement/List.tsx b/web/src/views/ModelManagement/List.tsx new file mode 100644 index 00000000..f1127623 --- /dev/null +++ b/web/src/views/ModelManagement/List.tsx @@ -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(null) + const modelListDetailRef = useRef(null) + const [list, setList] = useState([]) + 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 + ? + :( +
+ {list.map(item => ( + + {item.provider[0]} +
+ } + > + {item.tags.map(tag => {t(`modelNew.${tag}`)})} + + + + + + + + + + ))} +
+ ) + } + + + + + ) +} + +export default ModelList \ No newline at end of file diff --git a/web/src/views/ModelManagement/Square.tsx b/web/src/views/ModelManagement/Square.tsx new file mode 100644 index 00000000..8b69140b --- /dev/null +++ b/web/src/views/ModelManagement/Square.tsx @@ -0,0 +1,94 @@ +import { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react'; +import { Button, Space, App, Divider, Flex } from 'antd' +import { UsergroupAddOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; + +import type { ModelPlaza, ModelPlazaItem, ModelSquareDetailRef, BaseRef } from './types' +import RbCard from '@/components/RbCard/Card' +import { getModelPlaza, addModelPlaza } from '@/api/models' +import PageEmpty from '@/components/Empty/PageEmpty'; +import Tag from '@/components/Tag'; +import ModelSquareDetail from './components/ModelSquareDetail' + +const ModelSquare = forwardRef void; }>(({ query, handleEdit }, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp() + const modelSquareDetailRef = useRef(null) + const [list, setList] = useState([]) + 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 + ? + : list.map(vo => ( +
+
+
{vo.provider}
+ +
+ +
+ {vo.models.slice(0, 6).map(item => ( + + {item.name[0]} +
+ } + > + {t(`modelNew.${item.type}`)} +
{item.description}
+ {item.tags.map((tag, tagIndex) => {tag})} + + + {item.add_count} + + {!item.is_official && } + {item.is_added + ? + : + } + + + + ))} +
+ + )) + } + + + + ) +}) + +export default ModelSquare \ No newline at end of file diff --git a/web/src/views/ModelManagement/components/ConfigModal.tsx b/web/src/views/ModelManagement/components/ConfigModal.tsx deleted file mode 100644 index e4bdf84c..00000000 --- a/web/src/views/ModelManagement/components/ConfigModal.tsx +++ /dev/null @@ -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(({ - refresh -}, ref) => { - const { t } = useTranslation(); - const { message } = App.useApp(); - const [visible, setVisible] = useState(false); - const [model, setModel] = useState({} as Model); - const [isEdit, setIsEdit] = useState(false); - const [form] = Form.useForm(); - const [loading, setLoading] = useState(false) - - const values = Form.useWatch([], 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 ( - -
- {!isEdit && ( - <> - - - - - items.map((item) => ({ label: t(`model.${item}`), value: item }))} - /> - - - )} - - - - items.map((item) => ({ label: t(`model.${item}`), value: item }))} - /> - - - - - - - - - - - - -
-
- ); -}); - -export default ConfigModal; \ No newline at end of file diff --git a/web/src/views/ModelManagement/components/CustomModelModal.tsx b/web/src/views/ModelManagement/components/CustomModelModal.tsx new file mode 100644 index 00000000..df6693f7 --- /dev/null +++ b/web/src/views/ModelManagement/components/CustomModelModal.tsx @@ -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(({ + refresh +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [model, setModel] = useState({} as ModelPlazaItem); + const [isEdit, setIsEdit] = useState(false); + const [form] = Form.useForm(); + 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 ( + +
+ {!isEdit && + + } + + + + + + items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} + /> + + + + items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} + /> + + + + + + + + + + + items.map((item) => ({ + label: t(`modelNew.${typeof item === 'object' ? item.value : item}`), + value: typeof item === 'object' ? item.value : item + }))} + disabled={isEdit} + /> + + + + + + + + + +
+
+ ); +}); + +export default GroupModelModal; \ No newline at end of file diff --git a/web/src/views/ModelManagement/components/KeyConfigModal.tsx b/web/src/views/ModelManagement/components/KeyConfigModal.tsx new file mode 100644 index 00000000..d157dde7 --- /dev/null +++ b/web/src/views/ModelManagement/components/KeyConfigModal.tsx @@ -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(({ + refresh +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [model, setModel] = useState({} as ProviderModelItem); + const [form] = Form.useForm(); + 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 ( + +
+ + + + + + + +
+
+ ); +}); + +export default KeyConfigModal; \ No newline at end of file diff --git a/web/src/views/ModelManagement/components/ModelImplement/SubModelModal.tsx b/web/src/views/ModelManagement/components/ModelImplement/SubModelModal.tsx new file mode 100644 index 00000000..cfe3f090 --- /dev/null +++ b/web/src/views/ModelManagement/components/ModelImplement/SubModelModal.tsx @@ -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(({ + refresh, + type +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp() + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + const [selecteds, setSelecteds] = useState([]) + const [modelList, setModelList] = useState([]) + + // 封装取消方法,添加关闭弹窗逻辑 + 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 ( + +
+ + items.map((item) => ({ + label: t(`modelNew.${typeof item === 'object' ? item.value : item}`), + value: typeof item === 'object' ? item.value : item + }))} + onChange={(value) => handleChangeProvider(value)} + /> + + + + +
+
+ ); +}); + +export default SubModelModal; \ No newline at end of file diff --git a/web/src/views/ModelManagement/components/ModelImplement/index.tsx b/web/src/views/ModelManagement/components/ModelImplement/index.tsx new file mode 100644 index 00000000..a876587d --- /dev/null +++ b/web/src/views/ModelManagement/components/ModelImplement/index.tsx @@ -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 = ({ type, value, onChange }) => { + const { t } = useTranslation(); + const { modal, message } = App.useApp(); + const subModelModalRef = useRef(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 = (value || []).reduce((acc: Record, item: ModelList) => { + const provider = item.provider || 'unknown'; + if (!acc[provider]) acc[provider] = []; + acc[provider].push(item); + return acc; + }, {} as Record); + + return ( +
+ + {t('modelNew.modelImplement')} + + + + + + + + +
+ {!value || value.length === 0 + ? + : Object.entries(groupedByProvider).map(([provider, items]: [string, ModelList[]]) => { + return ( +
+ +
{[...new Set(items?.map((vo) => vo.model_name))].join(', ')}
+ +
handleEdit(items, provider)} + >
+
handleDelete(provider)} + >
+
+
+ {provider} +
+ ) + })} +
+ +
+ ) +} + +export default ModelImplement \ No newline at end of file diff --git a/web/src/views/ModelManagement/components/ModelImplement/types.ts b/web/src/views/ModelManagement/components/ModelImplement/types.ts new file mode 100644 index 00000000..c6d2f6d6 --- /dev/null +++ b/web/src/views/ModelManagement/components/ModelImplement/types.ts @@ -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; +} \ No newline at end of file diff --git a/web/src/views/ModelManagement/components/ModelListDetail.tsx b/web/src/views/ModelManagement/components/ModelListDetail.tsx new file mode 100644 index 00000000..48abd953 --- /dev/null +++ b/web/src/views/ModelManagement/components/ModelListDetail.tsx @@ -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(({ refresh }, ref) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + const [data, setData] = useState({} as ProviderModelItem) + const [list, setList] = useState([]) + const multiKeyConfigModalRef = useRef(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 ( + {data.provider} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})} + open={open} + onClose={handleClose} + > + {list.length === 0 + ? + :
+ {list.map(item => ( + + {t(`modelNew.${item.type}`)} + {item.api_keys.length}{t('modelNew.apiKeyNum')} + } + avatarUrl={item.logo} + avatar={ +
+ {item.name[0]} +
+ } + extra={ handleChange(item)} />} + > + +
{item.description}
+ + + + + +
+ ))} +
+ } + + +
+ ); +}); + +export default ModelListDetail; \ No newline at end of file diff --git a/web/src/views/ModelManagement/components/ModelSquareDetail.tsx b/web/src/views/ModelManagement/components/ModelSquareDetail.tsx new file mode 100644 index 00000000..6d437729 --- /dev/null +++ b/web/src/views/ModelManagement/components/ModelSquareDetail.tsx @@ -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(({ refresh }, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp() + const [model, setModel] = useState({} as ModelPlaza) + const [open, setOpen] = useState(false); + + const [list, setList] = useState([]) + + 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 ( + {model.provider} {t('modelNew.modelList')} ({list.length}{t('modelNew.item')})} + open={open} + onClose={handleClose} + > + {list.length === 0 + ? + :
+ {list.map(item => ( + + {item.name[0]} +
+ } + > + {t(`modelNew.${item.type}`)} +
{item.description}
+ {item.tags.map((tag, tagIndex) => {tag})} + {item.is_added + ? + : + } + + ))} + + } +
+ ); +}); + +export default ModelSquareDetail; \ No newline at end of file diff --git a/web/src/views/ModelManagement/components/MultiKeyConfigModal.tsx b/web/src/views/ModelManagement/components/MultiKeyConfigModal.tsx new file mode 100644 index 00000000..860efc3f --- /dev/null +++ b/web/src/views/ModelManagement/components/MultiKeyConfigModal.tsx @@ -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(({ refresh }, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [model, setModel] = useState({} as ModelListItem); + const [form] = Form.useForm(); + 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 ( + + {model.api_keys && model.api_keys.length > 0 && ( +
+ {model.api_keys.map((key) => ( +
+
+
{key.api_key}
+
{key.api_base}
+
+ +
+ ))} +
+ )} +
+ + + + + + + + + + + +
+
+ ); +}); + +export default MultiKeyConfigModal; \ No newline at end of file diff --git a/web/src/views/ModelManagement/index.tsx b/web/src/views/ModelManagement/index.tsx index 930a18e6..35f4c887 100644 --- a/web/src/views/ModelManagement/index.tsx +++ b/web/src/views/ModelManagement/index.tsx @@ -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(null) - const scrollListRef = useRef(null) + const configModalRef = useRef(null) + const customModelModalRef = useRef(null) + const groupRef = useRef(null) + const squareRef = useRef(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 ( -
- - - + + + + + {activeTab === 'list' ? <> + items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} + onChange={handleTypeChange} + className="rb:w-30" + allowClear={true} + placeholder={t('modelNew.type')} + /> + items.map((item) => ({ label: t(`modelNew.${item}`), value: String(item) }))} + onChange={handleProviderChange} + className="rb:w-30" + allowClear={true} + placeholder={t('modelNew.provider')} + /> + + : - - - - - + className="rb:w-70!" + />} + {activeTab === 'group' && } + {activeTab === 'square' && } + + - ( - - {formatData(item)?.map((description: DescriptionItem) => ( -
- {(description.label as string)} - {(description.children as string)} -
- ))} - -
- )} - /> - - + {activeTab === 'group' && } + {activeTab === 'list' && } + {activeTab === 'square' && } +
+ scrollListRef?.current?.refresh()} + refresh={handleRefresh} /> - + + ) } diff --git a/web/src/views/ModelManagement/types.ts b/web/src/views/ModelManagement/types.ts index 215e0d9f..b8d830cc 100644 --- a/web/src/views/ModelManagement/types.ts +++ b/web/src/views/ModelManagement/types.ts @@ -1,70 +1,146 @@ -// 模型表单数据类型 -export interface ModelFormData extends ApiKey { - name: string; - type: string; - api_keys: ApiKey; +export interface Query { + type?: string; + provider?: string; + is_active?: boolean; + is_public?: boolean; + is_composite?: boolean; + search?: string; } - export interface DescriptionItem { key: string; label: string; children: string; } +export interface CompositeModelForm { + logo?: any; + name: string; + type: string; + description: string; + api_key_ids: ModelApiKey[] | string[]; +} +export interface GroupModelModalRef { + handleOpen: (model?: ModelListItem) => void; +} +export interface GroupModelModalProps { + refresh?: () => void; +} +export interface SubModelModalForm { + provider: string; + model_ids: string[] +} +export interface SubModelModalRef { + handleOpen: (model?: SubModelModalForm) => void; +} +export interface SubModelModalProps { + refresh?: () => void; +} +export interface ModelListDetailRef { + handleOpen: (vo: ProviderModelItem) => void; +} -// 模型类型定义 -export interface Model { + +export interface ModelApiKey { + model_name: string; + description: string | null; + provider: string; + api_key: string; + api_base: string; + config: any; + is_active: boolean; + priority: string; + id: string; + usage_count: string; + last_used_at: number; + created_at: number; + updated_at: number; + model_config_ids: string[]; +} +export interface ModelListItem { + model_name?: string; + model_config_ids: string[]; + name: string; + type: string; + logo: string; + description: string; + provider: string; + config: any; + is_active: boolean; + is_public: boolean; + id: string; + created_at: number; + updated_at: number; + api_keys: ModelApiKey[] +} +export interface ProviderModelItem { + provider: string; + logo?: string; + tags: string[]; + models: ModelListItem[]; +} +export interface KeyConfigModalForm { + provider: string; + api_key: string; + api_base: string; +} +export interface KeyConfigModalRef { + handleOpen: (vo: ProviderModelItem) => void; +} +export interface KeyConfigModalProps { + refresh?: () => void; +} +export interface MultiKeyForm { + model_config_id?: string; + model_name: string; + provider: string; + api_key: string; + api_base: string; +} + +export interface MultiKeyConfigModalRef { + handleOpen: (vo: ModelListItem, provider?: string) => void; +} +export interface MultiKeyConfigModalProps { + refresh?: () => void; +} + + +export interface ModelPlaza { + provider: string; + models: ModelPlazaItem[]; +} +export interface ModelPlazaItem { id: string; name: string; type: string; - description?: string; - config: Record; - 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[]; - // 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; - is_active?: boolean; - priority?: string; - id: string; - model_config_id?: string; - usage_count?: string; - last_used_at?: string | null; - created_at?: string; - updated_at?: string; + logo: string; + description: string; + is_deprecated: boolean; + is_official: boolean; + tags: string[]; + add_count: number; + is_added: boolean; } -// 定义组件暴露的方法接口 -export interface ConfigModalRef { - handleOpen: (model?: Model) => void; +export interface ModelSquareDetailRef { + handleOpen: (vo: ModelPlaza) => void; } -export interface ConfigModalProps { +export interface CustomModelForm { + name: string; + type: string; + provider: string; + logo: string; + description: string; + is_official: boolean; + tags: string[]; +} +export interface CustomModelModalRef { + handleOpen: (vo?: ModelPlazaItem) => void; +} +export interface CustomModelModalProps { refresh?: () => void; +} + + +export interface BaseRef { + getList: () => void; } \ No newline at end of file