From a4c43bb83b997eeefae3f7083bc221b9c35b361b Mon Sep 17 00:00:00 2001 From: CalciumIon <1808837298@qq.com> Date: Sat, 14 Dec 2024 22:13:31 +0800 Subject: [PATCH] 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. --- web/src/components/OperationSetting.js | 22 ++- web/src/i18n/locales/en.json | 18 +- .../Setting/Operation/GroupRatioSettings.js | 131 +++++++++++++ .../Setting/Operation/ModelRatioSettings.js | 178 ++++++++++++++++++ .../Operation/ModelSettingsVisualEditor.js | 93 +++++---- 5 files changed, 400 insertions(+), 42 deletions(-) create mode 100644 web/src/pages/Setting/Operation/GroupRatioSettings.js create mode 100644 web/src/pages/Setting/Operation/ModelRatioSettings.js diff --git a/web/src/components/OperationSetting.js b/web/src/components/OperationSetting.js index d7d69f71..eebff04f 100644 --- a/web/src/components/OperationSetting.js +++ b/web/src/components/OperationSetting.js @@ -1,5 +1,5 @@ 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 SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.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 SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.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 SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; +import { useTranslation } from 'react-i18next'; const OperationSetting = () => { + const { t } = useTranslation(); let [inputs, setInputs] = useState({ QuotaForNewUser: 0, QuotaForInviter: 0, @@ -138,13 +143,20 @@ const OperationSetting = () => { - {/* 倍率设置 */} + {/* 分组倍率设置 */} - + - {/*可视化倍率设置*/} + {/* 合并模型倍率设置和可视化倍率设置 */} - + + + + + + + + diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 32da3670..51b2c510 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1161,7 +1161,7 @@ "默认折叠侧边栏": "Default collapse sidebar", "聊天链接功能已经弃用,请使用下方聊天设置功能": "Chat link function has been deprecated, please use the chat settings below", "你似乎并没有修改什么": "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", "链接中的{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", @@ -1217,5 +1217,19 @@ "确定要修改所有子渠道权重为 ": "Confirm to modify all sub-channel weights to ", " 吗?": "?", "修改子渠道优先级": "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" } \ No newline at end of file diff --git a/web/src/pages/Setting/Operation/GroupRatioSettings.js b/web/src/pages/Setting/Operation/GroupRatioSettings.js new file mode 100644 index 00000000..b3258b28 --- /dev/null +++ b/web/src/pages/Setting/Operation/GroupRatioSettings.js @@ -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 ( + +
(refForm.current = formAPI)} + style={{ marginBottom: 15 }} + > + + + + verifyJSON(value), + message: t('不是合法的 JSON 字符串') + } + ]} + onChange={(value) => setInputs({ ...inputs, GroupRatio: value })} + /> + + + + + verifyJSON(value), + message: t('不是合法的 JSON 字符串') + } + ]} + onChange={(value) => setInputs({ ...inputs, UserUsableGroups: value })} + /> + + + +
+ +
+ ); +} \ No newline at end of file diff --git a/web/src/pages/Setting/Operation/ModelRatioSettings.js b/web/src/pages/Setting/Operation/ModelRatioSettings.js new file mode 100644 index 00000000..48981d6a --- /dev/null +++ b/web/src/pages/Setting/Operation/ModelRatioSettings.js @@ -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 ( + +
(refForm.current = formAPI)} + style={{ marginBottom: 15 }} + > + + + + verifyJSON(value), + message: '不是合法的 JSON 字符串' + } + ]} + onChange={(value) => setInputs({ ...inputs, ModelPrice: value })} + /> + + + + + verifyJSON(value), + message: '不是合法的 JSON 字符串' + } + ]} + onChange={(value) => setInputs({ ...inputs, ModelRatio: value })} + /> + + + + + verifyJSON(value), + message: '不是合法的 JSON 字符串' + } + ]} + onChange={(value) => setInputs({ ...inputs, CompletionRatio: value })} + /> + + + +
+ + + + + + +
+ ); +} \ No newline at end of file diff --git a/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js b/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js index 6158b17e..c5ad9737 100644 --- a/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js +++ b/web/src/pages/Setting/Operation/ModelSettingsVisualEditor.js @@ -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 { showError, showSuccess } from '../../../helpers'; import { API } 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); @@ -122,51 +125,50 @@ export default function ModelSettingsVisualEditor(props) { const columns = [ { - title: '模型名称', + title: t('模型名称'), dataIndex: 'name', key: 'name', }, { - title: '固定价格', + title: t('模型固定价格'), dataIndex: 'price', key: 'price', render: (text, record) => ( updateModel(record.name, 'price', value)} /> ) }, { - title: '模型倍率', + title: t('模型倍率'), dataIndex: 'ratio', key: 'ratio', render: (text, record) => ( updateModel(record.name, 'ratio', value)} /> ) }, { - title: '补全倍率', + title: t('补全倍率'), dataIndex: 'completionRatio', key: 'completionRatio', render: (text, record) => ( updateModel(record.name, 'completionRatio', value)} /> ) }, { - title: '操作', + title: t('操作'), key: 'action', render: (_, record) => ( } - placeholder="搜索模型名称" + placeholder={t('搜索模型名称')} value={searchText} onChange={value => { setSearchText(value) - // 搜索时重置页码 setCurrentPage(1); }} style={{ width: 200 }} @@ -242,12 +242,18 @@ export default function ModelSettingsVisualEditor(props) { setCurrentPage(page), + formatPageText: (page) => + t('第 {{start}} - {{end}} 条,共 {{total}} 条', { + start: page.currentStart, + end: page.currentEnd, + total: filteredModels.length + }), showTotal: true, showSizeChanger: false }} @@ -255,7 +261,7 @@ export default function ModelSettingsVisualEditor(props) { setVisible(false)} onOk={() => { @@ -263,32 +269,49 @@ export default function ModelSettingsVisualEditor(props) { }} >
-

请输入固定价格或者模型倍率+补全倍率

setCurrentModel(prev => ({ ...prev, name: value }))} /> - setCurrentModel(prev => ({ ...prev, price: value }))} - /> - setCurrentModel(prev => ({ ...prev, ratio: value }))} - /> - setCurrentModel(prev => ({ ...prev, completionRatio: value }))} + {t('定价模式')}:{currentModel?.priceMode ? t("固定价格") : t("倍率模式")}} + onChange={checked => { + setCurrentModel(prev => ({ + ...prev, + price: '', + ratio: '', + completionRatio: '', + priceMode: checked + })); + }} /> + {currentModel?.priceMode ? ( + setCurrentModel(prev => ({ ...prev, price: value }))} + /> + ) : ( + <> + setCurrentModel(prev => ({ ...prev, ratio: value }))} + /> + setCurrentModel(prev => ({ ...prev, completionRatio: value }))} + /> + + )}