/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { SideSheet, Form, Button, Space, Spin, Typography, Card, Tag, Avatar, Col, Row, } from '@douyinfe/semi-ui'; import { IconSave, IconClose, IconLayers, } from '@douyinfe/semi-icons'; import { API, showError, showSuccess } from '../../../../helpers'; import { useTranslation } from 'react-i18next'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; const nameRuleOptions = [ { label: '精确名称匹配', value: 0 }, { label: '前缀名称匹配', value: 1 }, { label: '包含名称匹配', value: 2 }, { label: '后缀名称匹配', value: 3 }, ]; const endpointOptions = [ { label: 'OpenAI', value: 'openai' }, { label: 'Anthropic', value: 'anthropic' }, { label: 'Gemini', value: 'gemini' }, { label: 'Image Generation', value: 'image-generation' }, { label: 'Jina Rerank', value: 'jina-rerank' }, ]; const { Text, Title } = Typography; const EditModelModal = (props) => { const { t } = useTranslation(); const [loading, setLoading] = useState(false); const isMobile = useIsMobile(); const formApiRef = useRef(null); const isEdit = props.editingModel && props.editingModel.id !== undefined; const placement = useMemo(() => (isEdit ? 'right' : 'left'), [isEdit]); // 供应商列表 const [vendors, setVendors] = useState([]); // 预填组(标签、端点) const [tagGroups, setTagGroups] = useState([]); const [endpointGroups, setEndpointGroups] = useState([]); // 获取供应商列表 const fetchVendors = async () => { try { const res = await API.get('/api/vendors/?page_size=1000'); // 获取全部供应商 if (res.data.success) { const items = res.data.data.items || res.data.data || []; setVendors(Array.isArray(items) ? items : []); } } catch (error) { // ignore } }; // 获取预填组(标签、端点) const fetchPrefillGroups = async () => { try { const [tagRes, endpointRes] = await Promise.all([ API.get('/api/prefill_group?type=tag'), API.get('/api/prefill_group?type=endpoint'), ]); if (tagRes?.data?.success) { setTagGroups(tagRes.data.data || []); } if (endpointRes?.data?.success) { setEndpointGroups(endpointRes.data.data || []); } } catch (error) { // ignore } }; useEffect(() => { if (props.visiable) { fetchVendors(); fetchPrefillGroups(); } }, [props.visiable]); const getInitValues = () => ({ model_name: props.editingModel?.model_name || '', description: '', tags: [], vendor_id: undefined, vendor: '', vendor_icon: '', endpoints: [], name_rule: props.editingModel?.model_name ? 0 : undefined, // 通过未配置模型过来的固定为精确匹配 status: true, }); const handleCancel = () => { props.handleClose(); }; const loadModel = async () => { if (!isEdit || !props.editingModel.id) return; setLoading(true); try { const res = await API.get(`/api/models/${props.editingModel.id}`); const { success, message, data } = res.data; if (success) { // 处理tags if (data.tags) { data.tags = data.tags.split(',').filter(Boolean); } else { data.tags = []; } // 处理endpoints if (data.endpoints) { try { data.endpoints = JSON.parse(data.endpoints); } catch (e) { data.endpoints = []; } } else { data.endpoints = []; } // 处理status,将数字转为布尔值 data.status = data.status === 1; if (formApiRef.current) { formApiRef.current.setValues({ ...getInitValues(), ...data }); } } else { showError(message); } } catch (error) { showError(t('加载模型信息失败')); } setLoading(false); }; useEffect(() => { if (formApiRef.current) { if (!isEdit) { formApiRef.current.setValues({ ...getInitValues(), model_name: props.editingModel?.model_name || '', }); } } }, [props.editingModel?.id, props.editingModel?.model_name]); useEffect(() => { if (props.visiable) { if (isEdit) { loadModel(); } else { formApiRef.current?.setValues({ ...getInitValues(), model_name: props.editingModel?.model_name || '', }); } } else { formApiRef.current?.reset(); } }, [props.visiable, props.editingModel?.id, props.editingModel?.model_name]); const submit = async (values) => { setLoading(true); try { const submitData = { ...values, tags: Array.isArray(values.tags) ? values.tags.join(',') : values.tags, endpoints: JSON.stringify(values.endpoints || []), status: values.status ? 1 : 0, }; if (isEdit) { submitData.id = props.editingModel.id; const res = await API.put('/api/models/', submitData); const { success, message } = res.data; if (success) { showSuccess(t('模型更新成功!')); props.refresh(); props.handleClose(); } else { showError(t(message)); } } else { const res = await API.post('/api/models/', submitData); const { success, message } = res.data; if (success) { showSuccess(t('模型创建成功!')); props.refresh(); props.handleClose(); } else { showError(t(message)); } } } catch (error) { showError(error.response?.data?.message || t('操作失败')); } setLoading(false); formApiRef.current?.setValues(getInitValues()); }; return ( {isEdit ? ( {t('更新')} ) : ( {t('新建')} )} {isEdit ? t('更新模型信息') : t('创建新的模型')} } bodyStyle={{ padding: '0' }} visible={props.visiable} width={isMobile ? '100%' : 600} footer={
} closeIcon={null} onCancel={() => handleCancel()} >
(formApiRef.current = api)} onSubmit={submit} > {({ values }) => (
{/* 基本信息 */}
{t('基本信息')}
{t('设置模型的基本信息')}
({ label: t(o.label), value: o.value }))} rules={[{ required: true, message: t('请选择名称匹配类型') }]} disabled={!!props.editingModel?.model_name} // 通过未配置模型过来的禁用选择 style={{ width: '100%' }} extraText={t('根据模型名称和匹配规则查找模型元数据,优先级:精确 > 前缀 > 后缀 > 包含')} /> ({ label: g.name, value: g.id }))} showClear style={{ width: '100%' }} onChange={(value) => { const g = tagGroups.find(item => item.id === value); if (g && formApiRef.current) { formApiRef.current.setValue('tags', g.items || []); } }} /> { if (!formApiRef.current) return; const normalize = (tags) => { if (!Array.isArray(tags)) return []; return [...new Set(tags.flatMap(tag => tag.split(',').map(t => t.trim()).filter(Boolean)))]; }; const normalized = normalize(newTags); formApiRef.current.setValue('tags', normalized); }} />
{/* 供应商信息 */}
{t('供应商信息')}
{t('设置模型的供应商相关信息')}
({ label: v.name, value: v.id }))} filter showClear style={{ width: '100%' }} onChange={(value) => { const vendorInfo = vendors.find(v => v.id === value); if (vendorInfo && formApiRef.current) { formApiRef.current.setValue('vendor', vendorInfo.name); } }} />
{/* 功能配置 */}
{t('功能配置')}
{t('设置模型的功能和状态')}
({ label: g.name, value: g.id }))} showClear style={{ width: '100%' }} onChange={(value) => { const g = endpointGroups.find(item => item.id === value); if (g && formApiRef.current) { formApiRef.current.setValue('endpoints', g.items || []); } }} />
)}
); }; export default EditModelModal;