Download web/src/pages/Setting/Model/SettingModelMapping.js from Jack698/velo: direct link, hf CLI and curl.
- Browser
- Download file 17.1 kB
-
https://huggingface.co/spaces/Jack698/velo/resolve/main/web/src/pages/Setting/Model/SettingModelMapping.js
- Command line
-
hf download hf://spaces/Jack698/velo/web/src/pages/Setting/Model/SettingModelMapping.js
-
curl -L -o SettingModelMapping.js https://huggingface.co/spaces/Jack698/velo/resolve/main/web/src/pages/Setting/Model/SettingModelMapping.js
17.1 kB
| /* | |
| Copyright (c) 2025 Tethys Plex | |
| This file is part of Veloera. | |
| This program is free software: you can redistribute it and/or modify | |
| it under the terms of the GNU 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 General Public License for more details. | |
| You should have received a copy of the GNU General Public License | |
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |
| */ | |
| import React, { useEffect, useState, useRef } from 'react'; | |
| import { | |
| Button, | |
| Table, | |
| Modal, | |
| Form, | |
| Input, | |
| InputNumber, | |
| Space, | |
| Popconfirm, | |
| Typography, | |
| Banner, | |
| Card, | |
| Spin, | |
| Tag, | |
| } from '@douyinfe/semi-ui'; | |
| import { API, showError, showSuccess, showWarning } from '../../../helpers'; | |
| import { useTranslation } from 'react-i18next'; | |
| import { | |
| IconPlus, | |
| IconEdit, | |
| IconDelete, | |
| IconRefresh, | |
| } from '@douyinfe/semi-icons'; | |
| const { Title, Text } = Typography; | |
| export default function SettingModelMapping() { | |
| const { t } = useTranslation(); | |
| const [loading, setLoading] = useState(false); | |
| const [mappings, setMappings] = useState({}); | |
| const [modalVisible, setModalVisible] = useState(false); | |
| const [editingMapping, setEditingMapping] = useState(null); | |
| const [models, setModels] = useState([{ model: '', priorities: 0 }]); | |
| const [virtualModelError, setVirtualModelError] = useState(''); | |
| const [modelErrors, setModelErrors] = useState([]); | |
| const formApiRef = useRef(); | |
| // 获取模型映射配置 | |
| const fetchMappings = async () => { | |
| try { | |
| setLoading(true); | |
| const res = await API.get('/api/model_mapping/'); | |
| if (res.data.success) { | |
| setMappings(res.data.data.mapping || {}); | |
| } else { | |
| showError(res.data.message); | |
| } | |
| } catch (error) { | |
| showError('获取模型映射配置失败:' + error.message); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchMappings(); | |
| }, []); | |
| // 当models变化时自动验证 | |
| useEffect(() => { | |
| validateModels(); | |
| }, [models]); | |
| // 实时验证虚拟模型名 | |
| const validateVirtualModel = (value) => { | |
| const trimmedValue = value.trim(); | |
| if (!trimmedValue) { | |
| setVirtualModelError(''); | |
| return; | |
| } | |
| if (!editingMapping && mappings[trimmedValue]) { | |
| setVirtualModelError(`虚拟模型名 '${trimmedValue}' 已存在`); | |
| } else if (editingMapping && editingMapping !== trimmedValue && mappings[trimmedValue]) { | |
| setVirtualModelError(`虚拟模型名 '${trimmedValue}' 已存在`); | |
| } else { | |
| setVirtualModelError(''); | |
| } | |
| }; | |
| // 实时验证实际模型名 | |
| const validateModels = () => { | |
| const errors = []; | |
| const modelNames = new Set(); | |
| const duplicates = new Set(); | |
| models.forEach((model, index) => { | |
| const trimmedModel = model.model.trim(); | |
| if (trimmedModel) { | |
| if (modelNames.has(trimmedModel)) { | |
| duplicates.add(trimmedModel); | |
| errors[index] = `模型名重复: ${trimmedModel}`; | |
| } else { | |
| modelNames.add(trimmedModel); | |
| errors[index] = ''; | |
| } | |
| } else { | |
| errors[index] = ''; | |
| } | |
| }); | |
| // 为重复的模型设置错误信息 | |
| models.forEach((model, index) => { | |
| const trimmedModel = model.model.trim(); | |
| if (duplicates.has(trimmedModel) && !errors[index]) { | |
| errors[index] = `模型名重复: ${trimmedModel}`; | |
| } | |
| }); | |
| setModelErrors(errors); | |
| }; | |
| // 转换数据为表格格式 | |
| const getTableData = () => { | |
| return Object.entries(mappings).map(([virtualModel, items]) => ({ | |
| key: virtualModel, | |
| virtualModel, | |
| models: items || [], | |
| })); | |
| }; | |
| // 添加/编辑映射 | |
| const handleSaveMapping = async () => { | |
| try { | |
| const values = await formApiRef.current.validate(); | |
| const { virtualModel } = values; | |
| // 处理模型列表数据 | |
| const processedModels = models | |
| .map((model) => ({ | |
| model: model.model.trim(), | |
| priorities: parseInt(model.priorities) || 0, | |
| })) | |
| .filter((model) => model.model); // 过滤空模型 | |
| // 前端验证:检查虚拟模型名是否重复 | |
| const trimmedVirtualModel = virtualModel.trim(); | |
| if (!editingMapping && mappings[trimmedVirtualModel]) { | |
| showError(`虚拟模型名 '${trimmedVirtualModel}' 已存在,请使用不同的名称`); | |
| return; | |
| } | |
| // 如果是编辑模式但虚拟模型名已存在且不是当前编辑的项目 | |
| if (editingMapping && editingMapping !== trimmedVirtualModel && mappings[trimmedVirtualModel]) { | |
| showError(`虚拟模型名 '${trimmedVirtualModel}' 已存在,请使用不同的名称`); | |
| return; | |
| } | |
| // 前端验证:检查同一虚拟模型内实际模型名是否重复 | |
| const modelNames = new Set(); | |
| const duplicateModels = []; | |
| for (let i = 0; i < processedModels.length; i++) { | |
| const modelName = processedModels[i].model; | |
| if (modelNames.has(modelName)) { | |
| duplicateModels.push(modelName); | |
| } else { | |
| modelNames.add(modelName); | |
| } | |
| } | |
| if (duplicateModels.length > 0) { | |
| showError(`实际模型名重复: ${duplicateModels.join(', ')}`); | |
| return; | |
| } | |
| // 检查是否有空的实际模型名 | |
| if (processedModels.length === 0) { | |
| showError('至少需要配置一个实际模型'); | |
| return; | |
| } | |
| const newMappings = { ...mappings }; | |
| // 如果是编辑模式且虚拟模型名发生了变化,需要删除原来的映射 | |
| if (editingMapping && editingMapping !== trimmedVirtualModel) { | |
| delete newMappings[editingMapping]; | |
| } | |
| newMappings[trimmedVirtualModel] = processedModels; | |
| const res = await API.put('/api/model_mapping/', { | |
| mapping: newMappings, | |
| }); | |
| if (res.data.success) { | |
| showSuccess(res.data.message || '保存成功'); | |
| setMappings(newMappings); | |
| setModalVisible(false); | |
| if (formApiRef.current) { | |
| formApiRef.current.reset(); | |
| } | |
| setEditingMapping(null); | |
| setModels([{ model: '', priorities: 0 }]); | |
| setVirtualModelError(''); | |
| setModelErrors([]); | |
| } else { | |
| showError(res.data.message); | |
| } | |
| } catch (error) { | |
| if (error.errorFields) { | |
| showWarning('请检查表单输入'); | |
| } else { | |
| showError('保存失败:' + error.message); | |
| } | |
| } | |
| }; | |
| // 删除映射 | |
| const handleDeleteMapping = async (virtualModel) => { | |
| try { | |
| const newMappings = { ...mappings }; | |
| delete newMappings[virtualModel]; | |
| const res = await API.put('/api/model_mapping/', { | |
| mapping: newMappings, | |
| }); | |
| if (res.data.success) { | |
| showSuccess('删除成功'); | |
| setMappings(newMappings); | |
| } else { | |
| showError(res.data.message); | |
| } | |
| } catch (error) { | |
| showError('删除失败:' + error.message); | |
| } | |
| }; | |
| // 重新加载配置 | |
| const handleReload = async () => { | |
| try { | |
| const res = await API.post('/api/model_mapping/reload'); | |
| if (res.data.success) { | |
| showSuccess(res.data.message); | |
| await fetchMappings(); | |
| } else { | |
| showError(res.data.message); | |
| } | |
| } catch (error) { | |
| showError('重新加载失败:' + error.message); | |
| } | |
| }; | |
| // 打开编辑模态框 | |
| const openEditModal = (virtualModel = null) => { | |
| setEditingMapping(virtualModel); | |
| setModalVisible(true); | |
| setVirtualModelError(''); | |
| setModelErrors([]); | |
| // 延迟设置表单值,确保模态框已渲染 | |
| setTimeout(() => { | |
| if (formApiRef.current) { | |
| if (virtualModel && mappings[virtualModel]) { | |
| formApiRef.current.setValues({ | |
| virtualModel, | |
| }); | |
| setModels(mappings[virtualModel] || [{ model: '', priorities: 0 }]); | |
| } else { | |
| formApiRef.current.reset(); | |
| setModels([{ model: '', priorities: 0 }]); | |
| } | |
| } | |
| }, 100); | |
| }; | |
| // 添加模型 | |
| const addModel = () => { | |
| setModels([...models, { model: '', priorities: 0 }]); | |
| }; | |
| // 删除模型 | |
| const removeModel = (index) => { | |
| if (models.length > 1) { | |
| const newModels = models.filter((_, i) => i !== index); | |
| setModels(newModels); | |
| } | |
| }; | |
| // 更新模型 | |
| const updateModel = (index, field, value) => { | |
| const newModels = [...models]; | |
| newModels[index][field] = value; | |
| setModels(newModels); | |
| }; | |
| // 复制虚拟模型名 | |
| const copyVirtualModel = (virtualModel) => { | |
| navigator.clipboard | |
| .writeText(virtualModel) | |
| .then(() => { | |
| showSuccess(`已复制: ${virtualModel}`); | |
| }) | |
| .catch(() => { | |
| // 备用方法 | |
| const textarea = document.createElement('textarea'); | |
| textarea.value = virtualModel; | |
| document.body.appendChild(textarea); | |
| textarea.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(textarea); | |
| showSuccess(`已复制: ${virtualModel}`); | |
| }); | |
| }; | |
| const columns = [ | |
| { | |
| title: '虚拟模型名', | |
| dataIndex: 'virtualModel', | |
| key: 'virtualModel', | |
| width: 200, | |
| render: (virtualModel) => ( | |
| <div style={{ display: 'flex', alignItems: 'center' }}> | |
| <Tag | |
| color='violet' | |
| size='large' | |
| style={{ | |
| fontSize: '13px', | |
| fontWeight: '500', | |
| padding: '4px 12px', | |
| borderRadius: '6px', | |
| cursor: 'pointer', | |
| userSelect: 'none', | |
| transition: 'all 0.2s', | |
| }} | |
| onClick={() => copyVirtualModel(virtualModel)} | |
| onMouseEnter={(e) => { | |
| e.target.style.transform = 'scale(1.02)'; | |
| e.target.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)'; | |
| }} | |
| onMouseLeave={(e) => { | |
| e.target.style.transform = 'scale(1)'; | |
| e.target.style.boxShadow = 'none'; | |
| }} | |
| title='点击复制模型名' | |
| > | |
| {virtualModel} | |
| </Tag> | |
| </div> | |
| ), | |
| }, | |
| { | |
| title: '实际模型映射', | |
| dataIndex: 'models', | |
| key: 'models', | |
| render: (models) => { | |
| // 找到最高优先级的值 | |
| const maxPriority = Math.max(...models.map(model => model.priorities)); | |
| return ( | |
| <Space wrap> | |
| {models.map((model, index) => { | |
| // 判断是否为最高优先级 | |
| const isHighestPriority = model.priorities === maxPriority; | |
| return ( | |
| <Tag | |
| key={index} | |
| color={ | |
| isHighestPriority | |
| ? 'green' | |
| : model.priorities >= 8 | |
| ? 'blue' | |
| : model.priorities >= 5 | |
| ? 'orange' | |
| : 'grey' | |
| } | |
| style={ | |
| isHighestPriority | |
| ? { fontWeight: 'bold' } | |
| : {} | |
| } | |
| > | |
| {model.model} (优先级: {model.priorities}) | |
| </Tag> | |
| ); | |
| })} | |
| </Space> | |
| ); | |
| }, | |
| }, | |
| { | |
| title: '操作', | |
| key: 'action', | |
| width: 150, | |
| render: (_, record) => ( | |
| <Space> | |
| <Button | |
| icon={<IconEdit />} | |
| size='small' | |
| onClick={() => openEditModal(record.virtualModel)} | |
| > | |
| 编辑 | |
| </Button> | |
| <Popconfirm | |
| title='确定要删除这个模型映射吗?' | |
| onConfirm={() => handleDeleteMapping(record.virtualModel)} | |
| position='leftTop' | |
| > | |
| <Button icon={<IconDelete />} type='danger' size='small'> | |
| 删除 | |
| </Button> | |
| </Popconfirm> | |
| </Space> | |
| ), | |
| }, | |
| ]; | |
| return ( | |
| <> | |
| <Spin spinning={loading}> | |
| <Form style={{ marginBottom: 15 }}> | |
| <Form.Section text='模型映射配置'> | |
| <Banner | |
| type='info' | |
| description='配置虚拟模型名到实际模型的映射关系。支持多个实际模型,按优先级和轮询策略选择。' | |
| style={{ marginBottom: 16 }} | |
| /> | |
| <Space style={{ marginBottom: 16 }}> | |
| <Button | |
| type='primary' | |
| icon={<IconPlus />} | |
| onClick={() => openEditModal()} | |
| > | |
| 添加映射 | |
| </Button> | |
| <Button icon={<IconRefresh />} onClick={handleReload}> | |
| 重新加载 | |
| </Button> | |
| </Space> | |
| <div style={{ position: 'relative', overflow: 'visible' }}> | |
| <Table | |
| columns={columns} | |
| dataSource={getTableData()} | |
| pagination={false} | |
| size='small' | |
| empty={ | |
| <div style={{ textAlign: 'center', padding: '20px' }}> | |
| <Text type='secondary'>暂无模型映射配置</Text> | |
| </div> | |
| } | |
| /> | |
| </div> | |
| </Form.Section> | |
| </Form> | |
| </Spin> | |
| {/* 添加/编辑模态框 */} | |
| <Modal | |
| title={editingMapping ? '编辑模型映射' : '添加模型映射'} | |
| visible={modalVisible} | |
| onOk={handleSaveMapping} | |
| onCancel={() => { | |
| setModalVisible(false); | |
| setEditingMapping(null); | |
| setModels([{ model: '', priorities: 0 }]); | |
| setVirtualModelError(''); | |
| setModelErrors([]); | |
| if (formApiRef.current) { | |
| formApiRef.current.reset(); | |
| } | |
| }} | |
| width={600} | |
| > | |
| <Form | |
| getFormApi={(formAPI) => (formApiRef.current = formAPI)} | |
| labelPosition='left' | |
| labelWidth={120} | |
| > | |
| <Form.Input | |
| field='virtualModel' | |
| label='虚拟模型名' | |
| placeholder='输入虚拟模型名,如: gpt-4' | |
| rules={[ | |
| { required: true, message: '请输入虚拟模型名' }, | |
| { type: 'string', message: '虚拟模型名必须是字符串' }, | |
| ]} | |
| disabled={false} | |
| onChange={validateVirtualModel} | |
| validateStatus={virtualModelError ? 'error' : ''} | |
| helpText={virtualModelError} | |
| /> | |
| <div style={{ marginBottom: 16 }}> | |
| <Text strong>实际模型列表:</Text> | |
| {models.map((model, index) => ( | |
| <div | |
| key={index} | |
| style={{ | |
| display: 'flex', | |
| alignItems: 'center', | |
| marginTop: 8, | |
| marginBottom: 8, | |
| }} | |
| > | |
| <div style={{ display: 'flex', flexDirection: 'column', marginRight: 8 }}> | |
| <Input | |
| value={model.model} | |
| placeholder='实际模型名' | |
| style={{ | |
| width: 300, | |
| borderColor: modelErrors[index] ? '#ff4d4f' : undefined | |
| }} | |
| onChange={(value) => updateModel(index, 'model', value)} | |
| /> | |
| {modelErrors[index] && ( | |
| <Text | |
| type="danger" | |
| size="small" | |
| style={{ fontSize: '12px', marginTop: '2px' }} | |
| > | |
| {modelErrors[index]} | |
| </Text> | |
| )} | |
| </div> | |
| <InputNumber | |
| value={model.priorities} | |
| placeholder='优先级' | |
| min={0} | |
| style={{ width: 100, marginRight: 8, alignSelf: 'flex-start' }} | |
| onChange={(value) => updateModel(index, 'priorities', value)} | |
| /> | |
| <Button | |
| type='danger' | |
| size='small' | |
| onClick={() => removeModel(index)} | |
| disabled={models.length === 1} | |
| style={{ alignSelf: 'flex-start' }} | |
| > | |
| 删除 | |
| </Button> | |
| </div> | |
| ))} | |
| <Button | |
| onClick={addModel} | |
| icon={<IconPlus />} | |
| size='small' | |
| style={{ marginTop: 8 }} | |
| > | |
| 添加模型 | |
| </Button> | |
| </div> | |
| </Form> | |
| </Modal> | |
| </> | |
| ); | |
| } | |