🚀 feat(ui): isolate ratio configurations into dedicated “Ratio” tab and refactor settings components
Summary • Added new Ratio tab in Settings for managing all ratio-related configurations (group & model multipliers). • Created `RatioSetting` component to host GroupRatio, ModelRatio, Visual Editor and Unset-Models panels. • Moved ratio components to `web/src/pages/Setting/Ratio/` directory: – `GroupRatioSettings.js` – `ModelRatioSettings.js` – `ModelSettingsVisualEditor.js` – `ModelRationNotSetEditor.js` • Updated imports in `RatioSetting.js` to use the new path. • Updated main Settings router (`web/src/pages/Setting/index.js`) to include the new “Ratio Settings” tab. • Pruned `OperationSetting.js`: – Removed ratio-specific cards, tabs and unused imports. – Reduced state to only the keys required by its child components. – Deleted obsolete fields (`StreamCacheQueueLength`, `CheckSensitiveOnCompletionEnabled`, `StopOnSensitiveEnabled`). • Added boolean handling simplification in `OperationSetting.js`. • Adjusted helper import list and removed unused translation hook. Why Separating ratio-related settings improves UX clarity, reduces cognitive load in the Operation Settings panel and keeps the codebase modular and easier to maintain. BREAKING CHANGE The file paths for ratio components have changed. Any external imports referencing the old `Operation` directory must update to the new `Ratio` path.
This commit is contained in:
@@ -1,230 +0,0 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { Button, Col, Form, Row, Spin } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function GroupRatioSettings(props) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
GroupRatio: '',
|
||||
UserUsableGroups: '',
|
||||
GroupGroupRatio: '',
|
||||
AutoGroups: '',
|
||||
DefaultUseAutoGroup: false,
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
await refForm.current
|
||||
.validate()
|
||||
.then(() => {
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length)
|
||||
return showWarning(t('你似乎并没有修改什么'));
|
||||
|
||||
const requestQueue = updateArray.map((item) => {
|
||||
const value =
|
||||
typeof inputs[item.key] === 'boolean'
|
||||
? String(inputs[item.key])
|
||||
: inputs[item.key];
|
||||
return API.put('/api/option/', { key: item.key, value });
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
Promise.all(requestQueue)
|
||||
.then((res) => {
|
||||
if (res.includes(undefined)) {
|
||||
return showError(
|
||||
requestQueue.length > 1
|
||||
? t('部分保存失败,请重试')
|
||||
: t('保存失败'),
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Unexpected error:', error);
|
||||
showError(t('保存失败,请重试'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
showError(t('请检查输入'));
|
||||
});
|
||||
} catch (error) {
|
||||
showError(t('请检查输入'));
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const currentInputs = {};
|
||||
for (let key in props.options) {
|
||||
if (Object.keys(inputs).includes(key)) {
|
||||
currentInputs[key] = props.options[key];
|
||||
}
|
||||
}
|
||||
setInputs(currentInputs);
|
||||
setInputsRow(structuredClone(currentInputs));
|
||||
refForm.current.setValues(currentInputs);
|
||||
}, [props.options]);
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
values={inputs}
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
style={{ marginBottom: 15 }}
|
||||
>
|
||||
<Form.Section text={t('分组设置')}>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('分组倍率')}
|
||||
placeholder={t('为一个 JSON 文本,键为分组名称,值为倍率')}
|
||||
extraText={t(
|
||||
'分组倍率设置,可以在此处新增分组或修改现有分组的倍率,格式为 JSON 字符串,例如:{"vip": 0.5, "test": 1},表示 vip 分组的倍率为 0.5,test 分组的倍率为 1',
|
||||
)}
|
||||
field={'GroupRatio'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, GroupRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('用户可选分组')}
|
||||
placeholder={t('为一个 JSON 文本,键为分组名称,值为分组描述')}
|
||||
extraText={t(
|
||||
'用户新建令牌时可选的分组,格式为 JSON 字符串,例如:{"vip": "VIP 用户", "test": "测试"},表示用户可以选择 vip 分组和 test 分组',
|
||||
)}
|
||||
field={'UserUsableGroups'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, UserUsableGroups: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('分组特殊倍率')}
|
||||
placeholder={t('为一个 JSON 文本')}
|
||||
extraText={t(
|
||||
'键为分组名称,值为另一个 JSON 对象,键为分组名称,值为该分组的用户的特殊分组倍率,例如:{"vip": {"default": 0.5, "test": 1}},表示 vip 分组的用户在使用default分组的令牌时倍率为0.5,使用test分组时倍率为1',
|
||||
)}
|
||||
field={'GroupGroupRatio'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: t('不是合法的 JSON 字符串'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, GroupGroupRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('自动分组auto,从第一个开始选择')}
|
||||
placeholder={t('为一个 JSON 文本')}
|
||||
field={'AutoGroups'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (!value || value.trim() === '') {
|
||||
return true; // Allow empty values
|
||||
}
|
||||
|
||||
// First check if it's valid JSON
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
|
||||
// Check if it's an array
|
||||
if (!Array.isArray(parsed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if every element is a string
|
||||
return parsed.every(item => typeof item === 'string');
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
message: t('必须是有效的 JSON 字符串数组,例如:["g1","g2"]'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, AutoGroups: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col span={16}>
|
||||
<Form.Switch
|
||||
label={t(
|
||||
'创建令牌默认选择auto分组,初始令牌也将设为auto(否则留空,为用户默认分组)',
|
||||
)}
|
||||
field={'DefaultUseAutoGroup'}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, DefaultUseAutoGroup: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
<Button onClick={onSubmit}>{t('保存分组倍率设置')}</Button>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
compareObjects,
|
||||
API,
|
||||
showError,
|
||||
showSuccess,
|
||||
showWarning,
|
||||
verifyJSON,
|
||||
} from '../../../helpers';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ModelRatioSettings(props) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [inputs, setInputs] = useState({
|
||||
ModelPrice: '',
|
||||
ModelRatio: '',
|
||||
CacheRatio: '',
|
||||
CompletionRatio: '',
|
||||
});
|
||||
const refForm = useRef();
|
||||
const [inputsRow, setInputsRow] = useState(inputs);
|
||||
const { t } = useTranslation();
|
||||
|
||||
async function onSubmit() {
|
||||
try {
|
||||
await refForm.current
|
||||
.validate()
|
||||
.then(() => {
|
||||
const updateArray = compareObjects(inputs, inputsRow);
|
||||
if (!updateArray.length)
|
||||
return showWarning(t('你似乎并没有修改什么'));
|
||||
|
||||
const requestQueue = updateArray.map((item) => {
|
||||
const value =
|
||||
typeof inputs[item.key] === 'boolean'
|
||||
? String(inputs[item.key])
|
||||
: inputs[item.key];
|
||||
return API.put('/api/option/', { key: item.key, value });
|
||||
});
|
||||
|
||||
setLoading(true);
|
||||
Promise.all(requestQueue)
|
||||
.then((res) => {
|
||||
if (res.includes(undefined)) {
|
||||
return showError(
|
||||
requestQueue.length > 1
|
||||
? t('部分保存失败,请重试')
|
||||
: t('保存失败'),
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < res.length; i++) {
|
||||
if (!res[i].data.success) {
|
||||
return showError(res[i].data.message);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccess(t('保存成功'));
|
||||
props.refresh();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Unexpected error:', error);
|
||||
showError(t('保存失败,请重试'));
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
showError(t('请检查输入'));
|
||||
});
|
||||
} catch (error) {
|
||||
showError(t('请检查输入'));
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function resetModelRatio() {
|
||||
try {
|
||||
let res = await API.post(`/api/option/rest_model_ratio`);
|
||||
if (res.data.success) {
|
||||
showSuccess(res.data.message);
|
||||
props.refresh();
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const currentInputs = {};
|
||||
for (let key in props.options) {
|
||||
if (Object.keys(inputs).includes(key)) {
|
||||
currentInputs[key] = props.options[key];
|
||||
}
|
||||
}
|
||||
setInputs(currentInputs);
|
||||
setInputsRow(structuredClone(currentInputs));
|
||||
refForm.current.setValues(currentInputs);
|
||||
}, [props.options]);
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
values={inputs}
|
||||
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||
style={{ marginBottom: 15 }}
|
||||
>
|
||||
<Form.Section>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('模型固定价格')}
|
||||
extraText={t('一次调用消耗多少刀,优先级大于模型倍率')}
|
||||
placeholder={t(
|
||||
'为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀',
|
||||
)}
|
||||
field={'ModelPrice'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, ModelPrice: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('模型倍率')}
|
||||
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
|
||||
field={'ModelRatio'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, ModelRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('提示缓存倍率')}
|
||||
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
|
||||
field={'CacheRatio'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, CacheRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} sm={16}>
|
||||
<Form.TextArea
|
||||
label={t('模型补全倍率(仅对自定义模型有效)')}
|
||||
extraText={t('仅对自定义模型有效')}
|
||||
placeholder={t('为一个 JSON 文本,键为模型名称,值为倍率')}
|
||||
field={'CompletionRatio'}
|
||||
autosize={{ minRows: 6, maxRows: 12 }}
|
||||
trigger='blur'
|
||||
stopValidateWithError
|
||||
rules={[
|
||||
{
|
||||
validator: (rule, value) => verifyJSON(value),
|
||||
message: '不是合法的 JSON 字符串',
|
||||
},
|
||||
]}
|
||||
onChange={(value) =>
|
||||
setInputs({ ...inputs, CompletionRatio: value })
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Section>
|
||||
</Form>
|
||||
<Space>
|
||||
<Button onClick={onSubmit}>{t('保存模型倍率设置')}</Button>
|
||||
<Popconfirm
|
||||
title={t('确定重置模型倍率吗?')}
|
||||
content={t('此修改将不可逆')}
|
||||
okType={'danger'}
|
||||
position={'top'}
|
||||
onConfirm={resetModelRatio}
|
||||
>
|
||||
<Button type={'danger'}>{t('重置模型倍率')}</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</Spin>
|
||||
);
|
||||
}
|
||||
@@ -1,607 +0,0 @@
|
||||
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.toLowerCase().includes(searchText.toLowerCase())
|
||||
: 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>
|
||||
<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,
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: filteredModels.length,
|
||||
}),
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,711 +0,0 @@
|
||||
// ModelSettingsVisualEditor.js
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
Form,
|
||||
Space,
|
||||
RadioGroup,
|
||||
Radio
|
||||
} 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 [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 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) => ({
|
||||
name,
|
||||
price: modelPrice[name] === undefined ? '' : modelPrice[name],
|
||||
ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
|
||||
completionRatio:
|
||||
completionRatio[name] === undefined ? '' : completionRatio[name],
|
||||
}));
|
||||
|
||||
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) =>
|
||||
searchText
|
||||
? model.name.toLowerCase().includes(searchText.toLowerCase())
|
||||
: true,
|
||||
);
|
||||
|
||||
// 然后基于过滤后的数据计算分页数据
|
||||
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',
|
||||
},
|
||||
{
|
||||
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)
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
key: 'action',
|
||||
render: (_, record) => (
|
||||
<Space>
|
||||
<Button
|
||||
type='primary'
|
||||
icon={<IconEdit />}
|
||||
onClick={() => editModel(record)}
|
||||
></Button>
|
||||
<Button
|
||||
icon={<IconDelete />}
|
||||
type='danger'
|
||||
onClick={() => deleteModel(record.name)}
|
||||
/>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const updateModel = (name, field, value) => {
|
||||
if (isNaN(value)) {
|
||||
showError('请输入数字');
|
||||
return;
|
||||
}
|
||||
setModels((prev) =>
|
||||
prev.map((model) =>
|
||||
model.name === name ? { ...model, [field]: value } : model,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const deleteModel = (name) => {
|
||||
setModels((prev) => prev.filter((model) => model.name !== name));
|
||||
};
|
||||
|
||||
const calculateRatioFromTokenPrice = (tokenPrice) => {
|
||||
return tokenPrice / 2;
|
||||
};
|
||||
|
||||
const calculateCompletionRatioFromPrices = (
|
||||
modelTokenPrice,
|
||||
completionTokenPrice,
|
||||
) => {
|
||||
if (!modelTokenPrice || modelTokenPrice === '0') {
|
||||
showError('模型价格不能为0');
|
||||
return '';
|
||||
}
|
||||
return completionTokenPrice / modelTokenPrice;
|
||||
};
|
||||
|
||||
const handleTokenPriceChange = (value) => {
|
||||
// Use a temporary variable to hold the new state
|
||||
let newState = {
|
||||
...(currentModel || {}),
|
||||
tokenPrice: value,
|
||||
ratio: 0,
|
||||
};
|
||||
|
||||
if (!isNaN(value) && value !== '') {
|
||||
const tokenPrice = parseFloat(value);
|
||||
const ratio = calculateRatioFromTokenPrice(tokenPrice);
|
||||
newState.ratio = ratio;
|
||||
}
|
||||
|
||||
// Set the state with the complete updated object
|
||||
setCurrentModel(newState);
|
||||
};
|
||||
|
||||
const handleCompletionTokenPriceChange = (value) => {
|
||||
// Use a temporary variable to hold the new state
|
||||
let newState = {
|
||||
...(currentModel || {}),
|
||||
completionTokenPrice: value,
|
||||
completionRatio: 0,
|
||||
};
|
||||
|
||||
if (!isNaN(value) && value !== '' && currentModel?.tokenPrice) {
|
||||
const completionTokenPrice = parseFloat(value);
|
||||
const modelTokenPrice = parseFloat(currentModel.tokenPrice);
|
||||
|
||||
if (modelTokenPrice > 0) {
|
||||
const completionRatio = calculateCompletionRatioFromPrices(
|
||||
modelTokenPrice,
|
||||
completionTokenPrice,
|
||||
);
|
||||
newState.completionRatio = completionRatio;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the state with the complete updated object
|
||||
setCurrentModel(newState);
|
||||
};
|
||||
|
||||
const addOrUpdateModel = (values) => {
|
||||
// Check if we're editing an existing model or adding a new one
|
||||
const existingModelIndex = models.findIndex(
|
||||
(model) => model.name === values.name,
|
||||
);
|
||||
|
||||
if (existingModelIndex >= 0) {
|
||||
// Update existing model
|
||||
setModels((prev) =>
|
||||
prev.map((model, index) =>
|
||||
index === existingModelIndex
|
||||
? {
|
||||
name: values.name,
|
||||
price: values.price || '',
|
||||
ratio: values.ratio || '',
|
||||
completionRatio: values.completionRatio || '',
|
||||
}
|
||||
: model,
|
||||
),
|
||||
);
|
||||
setVisible(false);
|
||||
showSuccess(t('更新成功'));
|
||||
} else {
|
||||
// Add new model
|
||||
// Check if model name already exists
|
||||
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 calculateTokenPriceFromRatio = (ratio) => {
|
||||
return ratio * 2;
|
||||
};
|
||||
|
||||
const resetModalState = () => {
|
||||
setCurrentModel(null);
|
||||
setPricingMode('per-token');
|
||||
setPricingSubMode('ratio');
|
||||
};
|
||||
|
||||
const editModel = (record) => {
|
||||
// Determine which pricing mode to use based on the model's current configuration
|
||||
let initialPricingMode = 'per-token';
|
||||
let initialPricingSubMode = 'ratio';
|
||||
|
||||
if (record.price !== '') {
|
||||
initialPricingMode = 'per-request';
|
||||
} else {
|
||||
initialPricingMode = 'per-token';
|
||||
// We default to ratio mode, but could set to token-price if needed
|
||||
}
|
||||
|
||||
// Set the pricing modes for the form
|
||||
setPricingMode(initialPricingMode);
|
||||
setPricingSubMode(initialPricingSubMode);
|
||||
|
||||
// Create a copy of the model data to avoid modifying the original
|
||||
const modelCopy = { ...record };
|
||||
|
||||
// If the model has ratio data and we want to populate token price fields
|
||||
if (record.ratio) {
|
||||
modelCopy.tokenPrice = calculateTokenPriceFromRatio(
|
||||
parseFloat(record.ratio),
|
||||
).toString();
|
||||
|
||||
if (record.completionRatio) {
|
||||
modelCopy.completionTokenPrice = (
|
||||
parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)
|
||||
).toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Set the current model
|
||||
setCurrentModel(modelCopy);
|
||||
|
||||
// Open the modal
|
||||
setVisible(true);
|
||||
|
||||
// Use setTimeout to ensure the form is rendered before setting values
|
||||
setTimeout(() => {
|
||||
if (formRef.current) {
|
||||
// Update the form fields based on pricing mode
|
||||
const formValues = {
|
||||
name: modelCopy.name,
|
||||
};
|
||||
|
||||
if (initialPricingMode === 'per-request') {
|
||||
formValues.priceInput = modelCopy.price;
|
||||
} else if (initialPricingMode === 'per-token') {
|
||||
formValues.ratioInput = modelCopy.ratio;
|
||||
formValues.completionRatioInput = modelCopy.completionRatio;
|
||||
formValues.modelTokenPrice = modelCopy.tokenPrice;
|
||||
formValues.completionTokenPrice = modelCopy.completionTokenPrice;
|
||||
}
|
||||
|
||||
formRef.current.setValues(formValues);
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space vertical align='start' style={{ width: '100%' }}>
|
||||
<Space>
|
||||
<Button
|
||||
icon={<IconPlus />}
|
||||
onClick={() => {
|
||||
resetModalState();
|
||||
setVisible(true);
|
||||
}}
|
||||
>
|
||||
{t('添加模型')}
|
||||
</Button>
|
||||
<Button type='primary' icon={<IconSave />} onClick={SubmitData}>
|
||||
{t('应用更改')}
|
||||
</Button>
|
||||
<Input
|
||||
prefix={<IconSearch />}
|
||||
placeholder={t('搜索模型名称')}
|
||||
value={searchText}
|
||||
onChange={(value) => {
|
||||
setSearchText(value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={pagedData}
|
||||
pagination={{
|
||||
currentPage: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: filteredModels.length,
|
||||
onPageChange: (page) => setCurrentPage(page),
|
||||
formatPageText: (page) =>
|
||||
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
|
||||
start: page.currentStart,
|
||||
end: page.currentEnd,
|
||||
total: filteredModels.length,
|
||||
}),
|
||||
showTotal: true,
|
||||
showSizeChanger: false,
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
currentModel &&
|
||||
currentModel.name &&
|
||||
models.some((model) => model.name === currentModel.name)
|
||||
? t('编辑模型')
|
||||
: t('添加模型')
|
||||
}
|
||||
visible={visible}
|
||||
onCancel={() => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form getFormApi={(api) => (formRef.current = api)}>
|
||||
<Form.Input
|
||||
field='name'
|
||||
label={t('模型名称')}
|
||||
placeholder='strawberry'
|
||||
required
|
||||
disabled={
|
||||
currentModel &&
|
||||
currentModel.name &&
|
||||
models.some((model) => model.name === currentModel.name)
|
||||
}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({ ...prev, name: value }))
|
||||
}
|
||||
/>
|
||||
|
||||
<Form.Section text={t('定价模式')}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<RadioGroup
|
||||
type='button'
|
||||
value={pricingMode}
|
||||
onChange={(e) => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Radio value='per-token'>{t('按量计费')}</Radio>
|
||||
<Radio value='per-request'>{t('按次计费')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Form.Section>
|
||||
|
||||
{pricingMode === 'per-token' && (
|
||||
<>
|
||||
<Form.Section text={t('价格设置方式')}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<RadioGroup
|
||||
type='button'
|
||||
value={pricingSubMode}
|
||||
onChange={(e) => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Radio value='ratio'>{t('按倍率设置')}</Radio>
|
||||
<Radio value='token-price'>{t('按价格设置')}</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Form.Section>
|
||||
|
||||
{pricingSubMode === 'ratio' && (
|
||||
<>
|
||||
<Form.Input
|
||||
field='ratioInput'
|
||||
label={t('模型倍率')}
|
||||
placeholder={t('输入模型倍率')}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({
|
||||
...(prev || {}),
|
||||
ratio: value,
|
||||
}))
|
||||
}
|
||||
initValue={currentModel?.ratio || ''}
|
||||
/>
|
||||
<Form.Input
|
||||
field='completionRatioInput'
|
||||
label={t('补全倍率')}
|
||||
placeholder={t('输入补全倍率')}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({
|
||||
...(prev || {}),
|
||||
completionRatio: value,
|
||||
}))
|
||||
}
|
||||
initValue={currentModel?.completionRatio || ''}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{pricingSubMode === 'token-price' && (
|
||||
<>
|
||||
<Form.Input
|
||||
field='modelTokenPrice'
|
||||
label={t('输入价格')}
|
||||
onChange={(value) => {
|
||||
handleTokenPriceChange(value);
|
||||
}}
|
||||
initValue={currentModel?.tokenPrice || ''}
|
||||
suffix={t('$/1M tokens')}
|
||||
/>
|
||||
<Form.Input
|
||||
field='completionTokenPrice'
|
||||
label={t('输出价格')}
|
||||
onChange={(value) => {
|
||||
handleCompletionTokenPriceChange(value);
|
||||
}}
|
||||
initValue={currentModel?.completionTokenPrice || ''}
|
||||
suffix={t('$/1M tokens')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{pricingMode === 'per-request' && (
|
||||
<Form.Input
|
||||
field='priceInput'
|
||||
label={t('固定价格(每次)')}
|
||||
placeholder={t('输入每次价格')}
|
||||
onChange={(value) =>
|
||||
setCurrentModel((prev) => ({
|
||||
...(prev || {}),
|
||||
price: value,
|
||||
}))
|
||||
}
|
||||
initValue={currentModel?.price || ''}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user