/* 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, { useEffect, useState, useRef } from 'react'; import { Table, Button, Input, Modal, Form, Space, RadioGroup, Radio, Checkbox, Tag, } from '@douyinfe/semi-ui'; import { IconDelete, IconPlus, IconSearch, IconSave, IconEdit, } from '@douyinfe/semi-icons'; import { API, showError, showSuccess, getQuotaPerUnit } from '../../../helpers'; import { useTranslation } from 'react-i18next'; export default function ModelSettingsVisualEditor(props) { const { t } = useTranslation(); const [models, setModels] = useState([]); const [visible, setVisible] = useState(false); const [isEditMode, setIsEditMode] = useState(false); const [currentModel, setCurrentModel] = useState(null); const [searchText, setSearchText] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [loading, setLoading] = useState(false); const [pricingMode, setPricingMode] = useState('per-token'); // 'per-token' or 'per-request' const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price' const [conflictOnly, setConflictOnly] = useState(false); const formRef = useRef(null); const pageSize = 10; const quotaPerUnit = getQuotaPerUnit(); useEffect(() => { try { const modelPrice = JSON.parse(props.options.ModelPrice || '{}'); const modelRatio = JSON.parse(props.options.ModelRatio || '{}'); const completionRatio = JSON.parse(props.options.CompletionRatio || '{}'); // 合并所有模型名称 const modelNames = new Set([ ...Object.keys(modelPrice), ...Object.keys(modelRatio), ...Object.keys(completionRatio), ]); const modelData = Array.from(modelNames).map((name) => { const price = modelPrice[name] === undefined ? '' : modelPrice[name]; const ratio = modelRatio[name] === undefined ? '' : modelRatio[name]; const comp = completionRatio[name] === undefined ? '' : completionRatio[name]; return { name, price, ratio, completionRatio: comp, hasConflict: price !== '' && (ratio !== '' || comp !== ''), }; }); setModels(modelData); } catch (error) { console.error('JSON解析错误:', error); } }, [props.options]); // 首先声明分页相关的工具函数 const getPagedData = (data, currentPage, pageSize) => { const start = (currentPage - 1) * pageSize; const end = start + pageSize; return data.slice(start, end); }; // 在 return 语句之前,先处理过滤和分页逻辑 const filteredModels = models.filter((model) => { const keywordMatch = searchText ? model.name.includes(searchText) : true; const conflictMatch = conflictOnly ? model.hasConflict : true; return keywordMatch && conflictMatch; }); // 然后基于过滤后的数据计算分页数据 const pagedData = getPagedData(filteredModels, currentPage, pageSize); const SubmitData = async () => { setLoading(true); const output = { ModelPrice: {}, ModelRatio: {}, CompletionRatio: {}, }; let currentConvertModelName = ''; try { // 数据转换 models.forEach((model) => { currentConvertModelName = model.name; if (model.price !== '') { // 如果价格不为空,则转换为浮点数,忽略倍率参数 output.ModelPrice[model.name] = parseFloat(model.price); } else { if (model.ratio !== '') output.ModelRatio[model.name] = parseFloat(model.ratio); if (model.completionRatio !== '') output.CompletionRatio[model.name] = parseFloat( model.completionRatio, ); } }); // 准备API请求数组 const finalOutput = { ModelPrice: JSON.stringify(output.ModelPrice, null, 2), ModelRatio: JSON.stringify(output.ModelRatio, null, 2), CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2), }; const requestQueue = Object.entries(finalOutput).map(([key, value]) => { return API.put('/api/option/', { key, value, }); }); // 批量处理请求 const results = await Promise.all(requestQueue); // 验证结果 if (requestQueue.length === 1) { if (results.includes(undefined)) return; } else if (requestQueue.length > 1) { if (results.includes(undefined)) { return showError('部分保存失败,请重试'); } } // 检查每个请求的结果 for (const res of results) { if (!res.data.success) { return showError(res.data.message); } } showSuccess('保存成功'); props.refresh(); } catch (error) { console.error('保存失败:', error); showError('保存失败,请重试'); } finally { setLoading(false); } }; const columns = [ { title: t('模型名称'), dataIndex: 'name', key: 'name', render: (text, record) => ( {text} {record.hasConflict && ( {t('矛盾')} )} ), }, { title: t('模型固定价格'), dataIndex: 'price', key: 'price', render: (text, record) => ( updateModel(record.name, 'price', value)} /> ), }, { title: t('模型倍率'), dataIndex: 'ratio', key: 'ratio', render: (text, record) => ( updateModel(record.name, 'ratio', value)} /> ), }, { title: t('补全倍率'), dataIndex: 'completionRatio', key: 'completionRatio', render: (text, record) => ( updateModel(record.name, 'completionRatio', value) } /> ), }, { title: t('操作'), key: 'action', render: (_, record) => ( } placeholder={t('搜索模型名称')} value={searchText} onChange={(value) => { setSearchText(value); setCurrentPage(1); }} style={{ width: 200 }} showClear /> { setConflictOnly(e.target.checked); setCurrentPage(1); }} > {t('仅显示矛盾倍率')} setCurrentPage(page), showTotal: true, showSizeChanger: false, }} /> { resetModalState(); setVisible(false); }} onOk={() => { if (currentModel) { // If we're in token price mode, make sure ratio values are properly set const valuesToSave = { ...currentModel }; if ( pricingMode === 'per-token' && pricingSubMode === 'token-price' && currentModel.tokenPrice ) { // Calculate and set ratio from token price const tokenPrice = parseFloat(currentModel.tokenPrice); valuesToSave.ratio = (tokenPrice / 2).toString(); // Calculate and set completion ratio if both token prices are available if ( currentModel.completionTokenPrice && currentModel.tokenPrice ) { const completionPrice = parseFloat( currentModel.completionTokenPrice, ); const modelPrice = parseFloat(currentModel.tokenPrice); if (modelPrice > 0) { valuesToSave.completionRatio = ( completionPrice / modelPrice ).toString(); } } } // Clear price if we're in per-token mode if (pricingMode === 'per-token') { valuesToSave.price = ''; } else { // Clear ratios if we're in per-request mode valuesToSave.ratio = ''; valuesToSave.completionRatio = ''; } addOrUpdateModel(valuesToSave); } }} >
(formRef.current = api)}> setCurrentModel((prev) => ({ ...prev, name: value })) } />
{ const newMode = e.target.value; const oldMode = pricingMode; setPricingMode(newMode); // Instead of resetting all values, convert between modes if (currentModel) { const updatedModel = { ...currentModel }; // Update formRef with converted values if (formRef.current) { const formValues = { name: updatedModel.name, }; if (newMode === 'per-request') { formValues.priceInput = updatedModel.price || ''; } else if (newMode === 'per-token') { formValues.ratioInput = updatedModel.ratio || ''; formValues.completionRatioInput = updatedModel.completionRatio || ''; formValues.modelTokenPrice = updatedModel.tokenPrice || ''; formValues.completionTokenPrice = updatedModel.completionTokenPrice || ''; } formRef.current.setValues(formValues); } // Update the model state setCurrentModel(updatedModel); } }} > {t('按量计费')} {t('按次计费')}
{pricingMode === 'per-token' && ( <>
{ const newSubMode = e.target.value; const oldSubMode = pricingSubMode; setPricingSubMode(newSubMode); // Handle conversion between submodes if (currentModel) { const updatedModel = { ...currentModel }; // Convert between ratio and token price if ( oldSubMode === 'ratio' && newSubMode === 'token-price' ) { if (updatedModel.ratio) { updatedModel.tokenPrice = calculateTokenPriceFromRatio( parseFloat(updatedModel.ratio), ).toString(); if (updatedModel.completionRatio) { updatedModel.completionTokenPrice = ( parseFloat(updatedModel.tokenPrice) * parseFloat(updatedModel.completionRatio) ).toString(); } } } else if ( oldSubMode === 'token-price' && newSubMode === 'ratio' ) { // Ratio values should already be calculated by the handlers } // Update the form values if (formRef.current) { const formValues = {}; if (newSubMode === 'ratio') { formValues.ratioInput = updatedModel.ratio || ''; formValues.completionRatioInput = updatedModel.completionRatio || ''; } else if (newSubMode === 'token-price') { formValues.modelTokenPrice = updatedModel.tokenPrice || ''; formValues.completionTokenPrice = updatedModel.completionTokenPrice || ''; } formRef.current.setValues(formValues); } setCurrentModel(updatedModel); } }} > {t('按倍率设置')} {t('按价格设置')}
{pricingSubMode === 'ratio' && ( <> setCurrentModel((prev) => ({ ...(prev || {}), ratio: value, })) } initValue={currentModel?.ratio || ''} /> setCurrentModel((prev) => ({ ...(prev || {}), completionRatio: value, })) } initValue={currentModel?.completionRatio || ''} /> )} {pricingSubMode === 'token-price' && ( <> { handleTokenPriceChange(value); }} initValue={currentModel?.tokenPrice || ''} suffix={t('$/1M tokens')} /> { handleCompletionTokenPriceChange(value); }} initValue={currentModel?.completionTokenPrice || ''} suffix={t('$/1M tokens')} /> )} )} {pricingMode === 'per-request' && ( setCurrentModel((prev) => ({ ...(prev || {}), price: value, })) } initValue={currentModel?.price || ''} /> )}
); }