feat: Enhance Operation Settings with Group and Model Ratio Management

- Added new components for GroupRatioSettings and ModelRatioSettings to manage group and model ratios.
- Integrated tabs in OperationSetting to switch between model and visual ratio settings.
- Updated translations for new settings and improved existing ones in the English locale file.
- Refactored ModelSettingsVisualEditor to support dynamic pricing and ratio configurations.

This update improves the user interface for managing operational settings, enhancing usability and localization support.
This commit is contained in:
CalciumIon
2024-12-14 22:13:31 +08:00
parent 6ca68651ff
commit a4c43bb83b
5 changed files with 400 additions and 42 deletions

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui'; import { Card, Spin, Tabs } from '@douyinfe/semi-ui';
import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js'; import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js';
import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js'; import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js';
import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js'; import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js';
@@ -9,11 +9,16 @@ import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js
import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js'; import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js';
import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js'; import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js';
import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js'; import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js';
import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js';
import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js';
import { API, showError, showSuccess } from '../helpers'; import { API, showError, showSuccess } from '../helpers';
import SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; import SettingsChats from '../pages/Setting/Operation/SettingsChats.js';
import { useTranslation } from 'react-i18next';
const OperationSetting = () => { const OperationSetting = () => {
const { t } = useTranslation();
let [inputs, setInputs] = useState({ let [inputs, setInputs] = useState({
QuotaForNewUser: 0, QuotaForNewUser: 0,
QuotaForInviter: 0, QuotaForInviter: 0,
@@ -138,13 +143,20 @@ const OperationSetting = () => {
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<SettingsChats options={inputs} refresh={onRefresh} /> <SettingsChats options={inputs} refresh={onRefresh} />
</Card> </Card>
{/* 倍率设置 */} {/* 分组倍率设置 */}
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<SettingsMagnification options={inputs} refresh={onRefresh} /> <GroupRatioSettings options={inputs} refresh={onRefresh} />
</Card> </Card>
{/*可视化倍率设置*/} {/* 合并模型倍率设置和可视化倍率设置 */}
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<ModelSettingsVisualEditor options={inputs} refresh={onRefresh} /> <Tabs type="line">
<Tabs.TabPane tab={t('模型倍率设置')} itemKey="model">
<ModelRatioSettings options={inputs} refresh={onRefresh} />
</Tabs.TabPane>
<Tabs.TabPane tab={t('可视化倍率设置')} itemKey="visual">
<ModelSettingsVisualEditor options={inputs} refresh={onRefresh} />
</Tabs.TabPane>
</Tabs>
</Card> </Card>
</Spin> </Spin>
</> </>

View File

@@ -1161,7 +1161,7 @@
"默认折叠侧边栏": "Default collapse sidebar", "默认折叠侧边栏": "Default collapse sidebar",
"聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below", "聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below",
"你似乎并没有修改什么": "You seem to have not modified anything", "你似乎并没有修改什么": "You seem to have not modified anything",
"令牌聊天设置": "Token chat settings", "令牌聊天设置": "Chat settings",
"必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能": "Must set all chat links above to empty to use the chat settings below", "必须将上方聊天链接全部设置为空,才能使用下方聊天设置功能": "Must set all chat links above to empty to use the chat settings below",
"链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "The {key} in the link will be automatically replaced with sk-xxxx, the {address} will be automatically replaced with the server address in system settings, and the end will not have / and /v1", "链接中的{key}将自动替换为sk-xxxx{address}将自动替换为系统设置的服务器地址,末尾不带/和/v1": "The {key} in the link will be automatically replaced with sk-xxxx, the {address} will be automatically replaced with the server address in system settings, and the end will not have / and /v1",
"聊天配置": "Chat configuration", "聊天配置": "Chat configuration",
@@ -1217,5 +1217,19 @@
"确定要修改所有子渠道权重为 ": "Confirm to modify all sub-channel weights to ", "确定要修改所有子渠道权重为 ": "Confirm to modify all sub-channel weights to ",
" 吗?": "?", " 吗?": "?",
"修改子渠道优先级": "Modify sub-channel priority", "修改子渠道优先级": "Modify sub-channel priority",
"确定要修改所有子渠道优先级为 ": "Confirm to modify all sub-channel priorities to " "确定要修改所有子渠道优先级为 ": "Confirm to modify all sub-channel priorities to ",
"分组设置": "Group settings",
"用户可选分组": "User selectable groups",
"保存分组倍率设置": "Save group ratio settings",
"模型倍率设置": "Model ratio settings",
"可视化倍率设置": "Visual model ratio settings",
"确定重置模型倍率吗?": "Confirm to reset model ratio?",
"模型固定价格": "Model price per call",
"模型补全倍率(仅对自定义模型有效)": "Model completion ratio (only effective for custom models)",
"保存模型倍率设置": "Save model ratio settings",
"重置模型倍率": "Reset model ratio",
"一次调用消耗多少刀,优先级大于模型倍率": "How much USD one call costs, priority over model ratio",
"仅对自定义模型有效": "Only effective for custom models",
"添加模型": "Add model",
"应用更改": "Apply changes"
} }

View File

@@ -0,0 +1,131 @@
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: ''
});
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 span={16}>
<Form.TextArea
label={t('分组倍率')}
placeholder={t('为一个 JSON 文本,键为分组名称,值为倍率')}
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 span={16}>
<Form.TextArea
label={t('用户可选分组')}
placeholder={t('为一个 JSON 文本,键为分组名称,值为分组描述')}
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>
</Form.Section>
</Form>
<Button onClick={onSubmit}>{t('保存分组倍率设置')}</Button>
</Spin>
);
}

View File

@@ -0,0 +1,178 @@
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: '',
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 span={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 span={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 span={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>
);
}

View File

@@ -4,7 +4,10 @@ import { Table, Button, Input, Modal, Form, Space } from '@douyinfe/semi-ui';
import { IconDelete, IconPlus, IconSearch, IconSave } from '@douyinfe/semi-icons'; import { IconDelete, IconPlus, IconSearch, IconSave } from '@douyinfe/semi-icons';
import { showError, showSuccess } from '../../../helpers'; import { showError, showSuccess } from '../../../helpers';
import { API } from '../../../helpers'; import { API } from '../../../helpers';
import { useTranslation } from 'react-i18next';
export default function ModelSettingsVisualEditor(props) { export default function ModelSettingsVisualEditor(props) {
const { t } = useTranslation();
const [models, setModels] = useState([]); const [models, setModels] = useState([]);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [currentModel, setCurrentModel] = useState(null); const [currentModel, setCurrentModel] = useState(null);
@@ -122,51 +125,50 @@ export default function ModelSettingsVisualEditor(props) {
const columns = [ const columns = [
{ {
title: '模型名称', title: t('模型名称'),
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
}, },
{ {
title: '固定价格', title: t('模型固定价格'),
dataIndex: 'price', dataIndex: 'price',
key: 'price', key: 'price',
render: (text, record) => ( render: (text, record) => (
<Input <Input
value={text} value={text}
placeholder="按量计价" placeholder={t('按量计费')}
onChange={value => updateModel(record.name, 'price', value)} onChange={value => updateModel(record.name, 'price', value)}
/> />
) )
}, },
{ {
title: '模型倍率', title: t('模型倍率'),
dataIndex: 'ratio', dataIndex: 'ratio',
key: 'ratio', key: 'ratio',
render: (text, record) => ( render: (text, record) => (
<Input <Input
value={text} value={text}
placeholder={record.price !== '' ? t('模型倍率') : t('默认补全倍率')}
placeholder={record.price !== '' ? '固定价格' : '默认补全倍率'}
disabled={record.price !== ''} disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'ratio', value)} onChange={value => updateModel(record.name, 'ratio', value)}
/> />
) )
}, },
{ {
title: '补全倍率', title: t('补全倍率'),
dataIndex: 'completionRatio', dataIndex: 'completionRatio',
key: 'completionRatio', key: 'completionRatio',
render: (text, record) => ( render: (text, record) => (
<Input <Input
value={text} value={text}
placeholder={record.price !== '' ? '固定价格' : '默认补全倍率'} placeholder={record.price !== '' ? t('补全倍率') : t('默认补全倍率')}
disabled={record.price !== ''} disabled={record.price !== ''}
onChange={value => updateModel(record.name, 'completionRatio', value)} onChange={value => updateModel(record.name, 'completionRatio', value)}
/> />
) )
}, },
{ {
title: '操作', title: t('操作'),
key: 'action', key: 'action',
render: (_, record) => ( render: (_, record) => (
<Button <Button
@@ -219,22 +221,20 @@ export default function ModelSettingsVisualEditor(props) {
return ( return (
<> <>
<h3>模型价格</h3>
<Space vertical align="start" style={{ width: '100%' }}> <Space vertical align="start" style={{ width: '100%' }}>
<Space> <Space>
<Button icon={<IconPlus />} onClick={() => setVisible(true)}> <Button icon={<IconPlus />} onClick={() => setVisible(true)}>
添加模型 {t('添加模型')}
</Button> </Button>
<Button type="primary" icon={<IconSave />} onClick={SubmitData}> <Button type="primary" icon={<IconSave />} onClick={SubmitData}>
应用更改 {t('应用更改')}
</Button> </Button>
<Input <Input
prefix={<IconSearch />} prefix={<IconSearch />}
placeholder="搜索模型名称" placeholder={t('搜索模型名称')}
value={searchText} value={searchText}
onChange={value => { onChange={value => {
setSearchText(value) setSearchText(value)
// 搜索时重置页码
setCurrentPage(1); setCurrentPage(1);
}} }}
style={{ width: 200 }} style={{ width: 200 }}
@@ -242,12 +242,18 @@ export default function ModelSettingsVisualEditor(props) {
</Space> </Space>
<Table <Table
columns={columns} columns={columns}
dataSource={pagedData} // 使用分页后的数据 dataSource={pagedData}
pagination={{ pagination={{
currentPage: currentPage, currentPage: currentPage,
pageSize: pageSize, pageSize: pageSize,
total: filteredModels.length, total: filteredModels.length,
onPageChange: page => setCurrentPage(page), onPageChange: page => setCurrentPage(page),
formatPageText: (page) =>
t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
start: page.currentStart,
end: page.currentEnd,
total: filteredModels.length
}),
showTotal: true, showTotal: true,
showSizeChanger: false showSizeChanger: false
}} }}
@@ -255,7 +261,7 @@ export default function ModelSettingsVisualEditor(props) {
</Space> </Space>
<Modal <Modal
title="添加模型" title={t('添加模型')}
visible={visible} visible={visible}
onCancel={() => setVisible(false)} onCancel={() => setVisible(false)}
onOk={() => { onOk={() => {
@@ -263,32 +269,49 @@ export default function ModelSettingsVisualEditor(props) {
}} }}
> >
<Form> <Form>
<p>请输入固定价格或者模型倍率+补全倍率</p>
<Form.Input <Form.Input
field="name" field="name"
label="模型名称" label={t('模型名称')}
placeholder="strawberry" placeholder="strawberry"
required required
onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))} onChange={value => setCurrentModel(prev => ({ ...prev, name: value }))}
/> />
<Form.Input <Form.Switch
field="price" field="priceMode"
label="固定价格(每次)" label={<>{t('定价模式')}{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}</>}
placeholder="输入每次价格" onChange={checked => {
onChange={value => setCurrentModel(prev => ({ ...prev, price: value }))} setCurrentModel(prev => ({
/> ...prev,
<Form.Input price: '',
field="ratio" ratio: '',
label="模型倍率" completionRatio: '',
placeholder="输入模型倍率" priceMode: checked
onChange={value => setCurrentModel(prev => ({ ...prev, ratio: value }))} }));
/> }}
<Form.Input
field="completionRatio"
label="补全倍率"
placeholder="输入补全价格"
onChange={value => setCurrentModel(prev => ({ ...prev, completionRatio: value }))}
/> />
{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> </Form>
</Modal> </Modal>
</> </>