new-api / web /src /pages /Setting /Ratio /ModelRationNotSetEditor.jsx
liuzhao521
Deploy New API v0.9.25+ (commit b47cf4ef) to HuggingFace Spaces
4674012
/*
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 <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { useEffect, useState } from 'react';
import {
Table,
Button,
Input,
Modal,
Form,
Space,
Typography,
Radio,
Notification,
} from '@douyinfe/semi-ui';
import {
IconDelete,
IconPlus,
IconSearch,
IconSave,
IconBolt,
} from '@douyinfe/semi-icons';
import { API, showError, showSuccess } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function ModelRatioNotSetEditor(props) {
const { t } = useTranslation();
const [models, setModels] = useState([]);
const [visible, setVisible] = useState(false);
const [batchVisible, setBatchVisible] = useState(false);
const [currentModel, setCurrentModel] = useState(null);
const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [loading, setLoading] = useState(false);
const [enabledModels, setEnabledModels] = useState([]);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [batchFillType, setBatchFillType] = useState('ratio');
const [batchFillValue, setBatchFillValue] = useState('');
const [batchRatioValue, setBatchRatioValue] = useState('');
const [batchCompletionRatioValue, setBatchCompletionRatioValue] =
useState('');
const { Text } = Typography;
// 定义可选的每页显示条数
const pageSizeOptions = [10, 20, 50, 100];
const getAllEnabledModels = async () => {
try {
const res = await API.get('/api/channel/models_enabled');
const { success, message, data } = res.data;
if (success) {
setEnabledModels(data);
} else {
showError(message);
}
} catch (error) {
console.error(t('获取启用模型失败:'), error);
showError(t('获取启用模型失败'));
}
};
useEffect(() => {
// 获取所有启用的模型
getAllEnabledModels();
}, []);
useEffect(() => {
try {
const modelPrice = JSON.parse(props.options.ModelPrice || '{}');
const modelRatio = JSON.parse(props.options.ModelRatio || '{}');
const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
// 找出所有未设置价格和倍率的模型
const unsetModels = enabledModels.filter((modelName) => {
const hasPrice = modelPrice[modelName] !== undefined;
const hasRatio = modelRatio[modelName] !== undefined;
// 如果模型没有价格或者没有倍率设置,则显示
return !hasPrice && !hasRatio;
});
// 创建模型数据
const modelData = unsetModels.map((name) => ({
name,
price: modelPrice[name] || '',
ratio: modelRatio[name] || '',
completionRatio: completionRatio[name] || '',
}));
setModels(modelData);
// 清空选择
setSelectedRowKeys([]);
} catch (error) {
console.error(t('JSON解析错误:'), error);
}
}, [props.options, enabledModels]);
// 首先声明分页相关的工具函数
const getPagedData = (data, currentPage, pageSize) => {
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
return data.slice(start, end);
};
// 处理页面大小变化
const handlePageSizeChange = (size) => {
setPageSize(size);
// 重新计算当前页,避免数据丢失
const totalPages = Math.ceil(filteredModels.length / size);
if (currentPage > totalPages) {
setCurrentPage(totalPages || 1);
}
};
// 在 return 语句之前,先处理过滤和分页逻辑
const filteredModels = models.filter((model) =>
searchText ? model.name.includes(searchText) : true,
);
// 然后基于过滤后的数据计算分页数据
const pagedData = getPagedData(filteredModels, currentPage, pageSize);
const SubmitData = async () => {
setLoading(true);
const output = {
ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
};
try {
// 数据转换 - 只处理已修改的模型
models.forEach((model) => {
// 只有当用户设置了值时才更新
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(t('部分保存失败,请重试'));
}
}
// 检查每个请求的结果
for (const res of results) {
if (!res.data.success) {
return showError(res.data.message);
}
}
showSuccess(t('保存成功'));
props.refresh();
// 重新获取未设置的模型
getAllEnabledModels();
} catch (error) {
console.error(t('保存失败:'), error);
showError(t('保存失败,请重试'));
} finally {
setLoading(false);
}
};
const columns = [
{
title: t('模型名称'),
dataIndex: 'name',
key: 'name',
},
{
title: t('模型固定价格'),
dataIndex: 'price',
key: 'price',
render: (text, record) => (
<Input
value={text}
placeholder={t('按量计费')}
onChange={(value) => updateModel(record.name, 'price', value)}
/>
),
},
{
title: t('模型倍率'),
dataIndex: 'ratio',
key: 'ratio',
render: (text, record) => (
<Input
value={text}
placeholder={record.price !== '' ? t('模型倍率') : t('输入模型倍率')}
disabled={record.price !== ''}
onChange={(value) => updateModel(record.name, 'ratio', value)}
/>
),
},
{
title: t('补全倍率'),
dataIndex: 'completionRatio',
key: 'completionRatio',
render: (text, record) => (
<Input
value={text}
placeholder={record.price !== '' ? t('补全倍率') : t('输入补全倍率')}
disabled={record.price !== ''}
onChange={(value) =>
updateModel(record.name, 'completionRatio', value)
}
/>
),
},
];
const updateModel = (name, field, value) => {
if (value !== '' && isNaN(value)) {
showError(t('请输入数字'));
return;
}
setModels((prev) =>
prev.map((model) =>
model.name === name ? { ...model, [field]: value } : model,
),
);
};
const addModel = (values) => {
// 检查模型名称是否存在, 如果存在则拒绝添加
if (models.some((model) => model.name === values.name)) {
showError(t('模型名称已存在'));
return;
}
setModels((prev) => [
{
name: values.name,
price: values.price || '',
ratio: values.ratio || '',
completionRatio: values.completionRatio || '',
},
...prev,
]);
setVisible(false);
showSuccess(t('添加成功'));
};
// 批量填充功能
const handleBatchFill = () => {
if (selectedRowKeys.length === 0) {
showError(t('请先选择需要批量设置的模型'));
return;
}
if (batchFillType === 'bothRatio') {
if (batchRatioValue === '' || batchCompletionRatioValue === '') {
showError(t('请输入模型倍率和补全倍率'));
return;
}
if (isNaN(batchRatioValue) || isNaN(batchCompletionRatioValue)) {
showError(t('请输入有效的数字'));
return;
}
} else {
if (batchFillValue === '') {
showError(t('请输入填充值'));
return;
}
if (isNaN(batchFillValue)) {
showError(t('请输入有效的数字'));
return;
}
}
// 根据选择的类型批量更新模型
setModels((prev) =>
prev.map((model) => {
if (selectedRowKeys.includes(model.name)) {
if (batchFillType === 'price') {
return {
...model,
price: batchFillValue,
ratio: '',
completionRatio: '',
};
} else if (batchFillType === 'ratio') {
return {
...model,
price: '',
ratio: batchFillValue,
};
} else if (batchFillType === 'completionRatio') {
return {
...model,
price: '',
completionRatio: batchFillValue,
};
} else if (batchFillType === 'bothRatio') {
return {
...model,
price: '',
ratio: batchRatioValue,
completionRatio: batchCompletionRatioValue,
};
}
}
return model;
}),
);
setBatchVisible(false);
Notification.success({
title: t('批量设置成功'),
content: t('已为 {{count}} 个模型设置{{type}}', {
count: selectedRowKeys.length,
type:
batchFillType === 'price'
? t('固定价格')
: batchFillType === 'ratio'
? t('模型倍率')
: batchFillType === 'completionRatio'
? t('补全倍率')
: t('模型倍率和补全倍率'),
}),
duration: 3,
});
};
const handleBatchTypeChange = (value) => {
console.log(t('Changing batch type to:'), value);
setBatchFillType(value);
// 切换类型时清空对应的值
if (value !== 'bothRatio') {
setBatchFillValue('');
} else {
setBatchRatioValue('');
setBatchCompletionRatioValue('');
}
};
const rowSelection = {
selectedRowKeys,
onChange: (selectedKeys) => {
setSelectedRowKeys(selectedKeys);
},
};
return (
<>
<Space vertical align='start' style={{ width: '100%' }}>
<Space className='mt-2'>
<Button icon={<IconPlus />} onClick={() => setVisible(true)}>
{t('添加模型')}
</Button>
<Button
icon={<IconBolt />}
type='secondary'
onClick={() => setBatchVisible(true)}
disabled={selectedRowKeys.length === 0}
>
{t('批量设置')} ({selectedRowKeys.length})
</Button>
<Button
type='primary'
icon={<IconSave />}
onClick={SubmitData}
loading={loading}
>
{t('应用更改')}
</Button>
<Input
prefix={<IconSearch />}
placeholder={t('搜索模型名称')}
value={searchText}
onChange={(value) => {
setSearchText(value);
setCurrentPage(1);
}}
style={{ width: 200 }}
/>
</Space>
<Text>
{t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}
</Text>
<Table
columns={columns}
dataSource={pagedData}
rowSelection={rowSelection}
rowKey='name'
pagination={{
currentPage: currentPage,
pageSize: pageSize,
total: filteredModels.length,
onPageChange: (page) => setCurrentPage(page),
onPageSizeChange: handlePageSizeChange,
pageSizeOptions: pageSizeOptions,
showTotal: true,
showSizeChanger: true,
}}
empty={
<div style={{ textAlign: 'center', padding: '20px' }}>
{t('没有未设置的模型')}
</div>
}
/>
</Space>
{/* 添加模型弹窗 */}
<Modal
title={t('添加模型')}
visible={visible}
onCancel={() => setVisible(false)}
onOk={() => {
currentModel && addModel(currentModel);
}}
>
<Form>
<Form.Input
field='name'
label={t('模型名称')}
placeholder='strawberry'
required
onChange={(value) =>
setCurrentModel((prev) => ({ ...prev, name: value }))
}
/>
<Form.Switch
field='priceMode'
label={
<>
{t('定价模式')}:
{currentModel?.priceMode ? t('固定价格') : t('倍率模式')}
</>
}
onChange={(checked) => {
setCurrentModel((prev) => ({
...prev,
price: '',
ratio: '',
completionRatio: '',
priceMode: checked,
}));
}}
/>
{currentModel?.priceMode ? (
<Form.Input
field='price'
label={t('固定价格(每次)')}
placeholder={t('输入每次价格')}
onChange={(value) =>
setCurrentModel((prev) => ({ ...prev, price: value }))
}
/>
) : (
<>
<Form.Input
field='ratio'
label={t('模型倍率')}
placeholder={t('输入模型倍率')}
onChange={(value) =>
setCurrentModel((prev) => ({ ...prev, ratio: value }))
}
/>
<Form.Input
field='completionRatio'
label={t('补全倍率')}
placeholder={t('输入补全价格')}
onChange={(value) =>
setCurrentModel((prev) => ({
...prev,
completionRatio: value,
}))
}
/>
</>
)}
</Form>
</Modal>
{/* 批量设置弹窗 */}
<Modal
title={t('批量设置模型参数')}
visible={batchVisible}
onCancel={() => setBatchVisible(false)}
onOk={handleBatchFill}
width={500}
>
<Form>
<Form.Section text={t('设置类型')}>
<div style={{ marginBottom: '16px' }}>
<Space>
<Radio
checked={batchFillType === 'price'}
onChange={() => handleBatchTypeChange('price')}
>
{t('固定价格')}
</Radio>
<Radio
checked={batchFillType === 'ratio'}
onChange={() => handleBatchTypeChange('ratio')}
>
{t('模型倍率')}
</Radio>
<Radio
checked={batchFillType === 'completionRatio'}
onChange={() => handleBatchTypeChange('completionRatio')}
>
{t('补全倍率')}
</Radio>
<Radio
checked={batchFillType === 'bothRatio'}
onChange={() => handleBatchTypeChange('bothRatio')}
>
{t('模型倍率和补全倍率同时设置')}
</Radio>
</Space>
</div>
</Form.Section>
{batchFillType === 'bothRatio' ? (
<>
<Form.Input
field='batchRatioValue'
label={t('模型倍率值')}
placeholder={t('请输入模型倍率')}
value={batchRatioValue}
onChange={(value) => setBatchRatioValue(value)}
/>
<Form.Input
field='batchCompletionRatioValue'
label={t('补全倍率值')}
placeholder={t('请输入补全倍率')}
value={batchCompletionRatioValue}
onChange={(value) => setBatchCompletionRatioValue(value)}
/>
</>
) : (
<Form.Input
field='batchFillValue'
label={
batchFillType === 'price'
? t('固定价格值')
: batchFillType === 'ratio'
? t('模型倍率值')
: t('补全倍率值')
}
placeholder={t('请输入数值')}
value={batchFillValue}
onChange={(value) => setBatchFillValue(value)}
/>
)}
<Text type='tertiary'>
{t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text>{' '}
{t(' 个模型设置相同的值')}
</Text>
<div style={{ marginTop: '8px' }}>
<Text type='tertiary'>
{t('当前设置类型: ')}{' '}
<Text strong>
{batchFillType === 'price'
? t('固定价格')
: batchFillType === 'ratio'
? t('模型倍率')
: batchFillType === 'completionRatio'
? t('补全倍率')
: t('模型倍率和补全倍率')}
</Text>
</Text>
</div>
</Form>
</Modal>
</>
);
}