|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const filteredModels = models.filter((model) => |
|
|
searchText ? model.name.includes(searchText) : 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, |
|
|
); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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 className='mt-2'> |
|
|
<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, |
|
|
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> |
|
|
</> |
|
|
); |
|
|
} |
|
|
|