| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755 |
- /*
- 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, useRef } from 'react';
- import {
- Table,
- Button,
- Input,
- Modal,
- Form,
- Space,
- RadioGroup,
- Radio,
- Checkbox,
- Tag
- } from '@douyinfe/semi-ui';
- import {
- IconDelete,
- IconPlus,
- IconSearch,
- IconSave,
- IconEdit,
- } from '@douyinfe/semi-icons';
- import { API, showError, showSuccess, getQuotaPerUnit } from '../../../helpers';
- import { useTranslation } from 'react-i18next';
- export default function ModelSettingsVisualEditor(props) {
- const { t } = useTranslation();
- const [models, setModels] = useState([]);
- const [visible, setVisible] = useState(false);
- const [isEditMode, setIsEditMode] = useState(false);
- const [currentModel, setCurrentModel] = useState(null);
- const [searchText, setSearchText] = useState('');
- const [currentPage, setCurrentPage] = useState(1);
- const [loading, setLoading] = useState(false);
- const [pricingMode, setPricingMode] = useState('per-token'); // 'per-token' or 'per-request'
- const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price'
- const [conflictOnly, setConflictOnly] = useState(false);
- const formRef = useRef(null);
- const pageSize = 10;
- const quotaPerUnit = getQuotaPerUnit();
- useEffect(() => {
- try {
- const modelPrice = JSON.parse(props.options.ModelPrice || '{}');
- const modelRatio = JSON.parse(props.options.ModelRatio || '{}');
- const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
- // 合并所有模型名称
- const modelNames = new Set([
- ...Object.keys(modelPrice),
- ...Object.keys(modelRatio),
- ...Object.keys(completionRatio),
- ]);
- const modelData = Array.from(modelNames).map((name) => {
- const price = modelPrice[name] === undefined ? '' : modelPrice[name];
- const ratio = modelRatio[name] === undefined ? '' : modelRatio[name];
- const comp = completionRatio[name] === undefined ? '' : completionRatio[name];
- return {
- name,
- price,
- ratio,
- completionRatio: comp,
- hasConflict: price !== '' && (ratio !== '' || comp !== ''),
- };
- });
- setModels(modelData);
- } catch (error) {
- console.error('JSON解析错误:', error);
- }
- }, [props.options]);
- // 首先声明分页相关的工具函数
- const getPagedData = (data, currentPage, pageSize) => {
- const start = (currentPage - 1) * pageSize;
- const end = start + pageSize;
- return data.slice(start, end);
- };
- // 在 return 语句之前,先处理过滤和分页逻辑
- const filteredModels = models.filter((model) => {
- const keywordMatch = searchText
- ? model.name.toLowerCase().includes(searchText.toLowerCase())
- : true;
- const conflictMatch = conflictOnly ? model.hasConflict : true;
- return keywordMatch && conflictMatch;
- });
- // 然后基于过滤后的数据计算分页数据
- const pagedData = getPagedData(filteredModels, currentPage, pageSize);
- const SubmitData = async () => {
- setLoading(true);
- const output = {
- ModelPrice: {},
- ModelRatio: {},
- CompletionRatio: {},
- };
- let currentConvertModelName = '';
- try {
- // 数据转换
- models.forEach((model) => {
- currentConvertModelName = model.name;
- if (model.price !== '') {
- // 如果价格不为空,则转换为浮点数,忽略倍率参数
- output.ModelPrice[model.name] = parseFloat(model.price);
- } else {
- if (model.ratio !== '')
- output.ModelRatio[model.name] = parseFloat(model.ratio);
- if (model.completionRatio !== '')
- output.CompletionRatio[model.name] = parseFloat(
- model.completionRatio,
- );
- }
- });
- // 准备API请求数组
- const finalOutput = {
- ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
- ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
- CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
- };
- const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
- return API.put('/api/option/', {
- key,
- value,
- });
- });
- // 批量处理请求
- const results = await Promise.all(requestQueue);
- // 验证结果
- if (requestQueue.length === 1) {
- if (results.includes(undefined)) return;
- } else if (requestQueue.length > 1) {
- if (results.includes(undefined)) {
- return showError('部分保存失败,请重试');
- }
- }
- // 检查每个请求的结果
- for (const res of results) {
- if (!res.data.success) {
- return showError(res.data.message);
- }
- }
- showSuccess('保存成功');
- props.refresh();
- } catch (error) {
- console.error('保存失败:', error);
- showError('保存失败,请重试');
- } finally {
- setLoading(false);
- }
- };
- const columns = [
- {
- title: t('模型名称'),
- dataIndex: 'name',
- key: 'name',
- render: (text, record) => (
- <span>
- {text}
- {record.hasConflict && (
- <Tag color='red' shape='circle' className='ml-2'>
- {t('矛盾')}
- </Tag>
- )}
- </span>
- ),
- },
- {
- 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) => {
- if (model.name !== name) return model;
- const updated = { ...model, [field]: value };
- updated.hasConflict =
- updated.price !== '' && (updated.ratio !== '' || updated.completionRatio !== '');
- return updated;
- }),
- );
- };
- 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) => {
- if (index !== existingModelIndex) return model;
- const updated = {
- name: values.name,
- price: values.price || '',
- ratio: values.ratio || '',
- completionRatio: values.completionRatio || '',
- };
- updated.hasConflict =
- updated.price !== '' && (updated.ratio !== '' || updated.completionRatio !== '');
- return updated;
- }),
- );
- 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) => {
- const newModel = {
- name: values.name,
- price: values.price || '',
- ratio: values.ratio || '',
- completionRatio: values.completionRatio || '',
- };
- newModel.hasConflict =
- newModel.price !== '' && (newModel.ratio !== '' || newModel.completionRatio !== '');
- return [newModel, ...prev];
- });
- setVisible(false);
- showSuccess(t('添加成功'));
- }
- };
- const calculateTokenPriceFromRatio = (ratio) => {
- return ratio * 2;
- };
- const resetModalState = () => {
- setCurrentModel(null);
- setPricingMode('per-token');
- setPricingSubMode('ratio');
- setIsEditMode(false);
- };
- const editModel = (record) => {
- setIsEditMode(true);
- // 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 className='mt-2'>
- <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 }}
- showClear
- />
- <Checkbox
- checked={conflictOnly}
- onChange={(e) => {
- setConflictOnly(e.target.checked);
- setCurrentPage(1);
- }}
- >
- {t('仅显示矛盾倍率')}
- </Checkbox>
- </Space>
- <Table
- columns={columns}
- dataSource={pagedData}
- pagination={{
- currentPage: currentPage,
- pageSize: pageSize,
- total: filteredModels.length,
- onPageChange: (page) => setCurrentPage(page),
- showTotal: true,
- showSizeChanger: false,
- }}
- />
- </Space>
- <Modal
- title={isEditMode ? 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={isEditMode}
- 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>
- </>
- );
- }
|