| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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'); |
| const [pricingSubMode, setPricingSubMode] = useState('ratio'); |
| 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); |
| }; |
|
|
| |
| const filteredModels = models.filter((model) => { |
| const keywordMatch = searchText ? model.name.includes(searchText) : 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, |
| ); |
| } |
| }); |
|
|
| |
| 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) => { |
| |
| let newState = { |
| ...(currentModel || {}), |
| tokenPrice: value, |
| ratio: 0, |
| }; |
|
|
| if (!isNaN(value) && value !== '') { |
| const tokenPrice = parseFloat(value); |
| const ratio = calculateRatioFromTokenPrice(tokenPrice); |
| newState.ratio = ratio; |
| } |
|
|
| |
| setCurrentModel(newState); |
| }; |
|
|
| const handleCompletionTokenPriceChange = (value) => { |
| |
| 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; |
| } |
| } |
|
|
| |
| setCurrentModel(newState); |
| }; |
|
|
| const addOrUpdateModel = (values) => { |
| |
| const existingModelIndex = models.findIndex( |
| (model) => model.name === values.name, |
| ); |
|
|
| if (existingModelIndex >= 0) { |
| |
| 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 { |
| |
| |
| 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); |
| |
| let initialPricingMode = 'per-token'; |
| let initialPricingSubMode = 'ratio'; |
|
|
| if (record.price !== '') { |
| initialPricingMode = 'per-request'; |
| } else { |
| initialPricingMode = 'per-token'; |
| |
| } |
|
|
| |
| setPricingMode(initialPricingMode); |
| setPricingSubMode(initialPricingSubMode); |
|
|
| |
| const modelCopy = { ...record }; |
|
|
| |
| if (record.ratio) { |
| modelCopy.tokenPrice = calculateTokenPriceFromRatio( |
| parseFloat(record.ratio), |
| ).toString(); |
|
|
| if (record.completionRatio) { |
| modelCopy.completionTokenPrice = ( |
| parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio) |
| ).toString(); |
| } |
| } |
|
|
| |
| setCurrentModel(modelCopy); |
|
|
| |
| setVisible(true); |
|
|
| |
| setTimeout(() => { |
| if (formRef.current) { |
| |
| 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> |
| </> |
| ); |
| } |
|
|