| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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 { showError, showSuccess } from '../../../helpers'; |
| import { API } 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.toLowerCase().includes(searchText.toLowerCase()) |
| : 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 [loadingMixed, setLoadingMixed] = useState(false); |
| const handleFillFromMixed = async () => { |
| try { |
| setLoadingMixed(true); |
| |
| const promptUrl = 'https://public-assets.veloera.org/defaults/model-ratios/mixed/prompt.json'; |
| const completionUrl = 'https://public-assets.veloera.org/defaults/model-ratios/mixed/completion.json'; |
|
|
| const [promptRatios, completionRatios] = await Promise.all([ |
| fetch(promptUrl).then((res) => (res.ok ? res.json() : {})).catch(() => ({})), |
| fetch(completionUrl).then((res) => (res.ok ? res.json() : {})).catch(() => ({})), |
| ]); |
|
|
| let filledCount = 0; |
|
|
| setModels((prev) => |
| prev.map((m) => { |
| |
| |
| if (m.price !== '') return m; |
|
|
| const next = { ...m }; |
| if ((next.ratio === '' || next.ratio === undefined) && promptRatios && promptRatios[next.name] !== undefined) { |
| next.ratio = String(promptRatios[next.name]); |
| filledCount += 1; |
| } |
| if ((next.completionRatio === '' || next.completionRatio === undefined) && completionRatios && completionRatios[next.name] !== undefined) { |
| next.completionRatio = String(completionRatios[next.name]); |
| filledCount += 1; |
| } |
| return next; |
| }), |
| ); |
|
|
| if (filledCount > 0) { |
| Notification.success({ |
| title: t('已填入官网倍率'), |
| content: t('成功填充 {{count}} 项缺失倍率', { count: filledCount }), |
| duration: 3, |
| }); |
| } else { |
| Notification.info({ |
| title: t('无可填充项'), |
| content: t('未找到可从官网填充的缺失倍率或均已设置'), |
| duration: 3, |
| }); |
| } |
| } catch (e) { |
| console.error('填入官网倍率失败:', e); |
| showError(t('填入官网倍率失败,请稍后重试')); |
| } finally { |
| setLoadingMixed(false); |
| } |
| }; |
|
|
| 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> |
| <Button icon={<IconPlus />} onClick={() => setVisible(true)}> |
| {t('添加模型')} |
| </Button> |
| <Button |
| icon={<IconBolt />} |
| type='secondary' |
| onClick={() => setBatchVisible(true)} |
| disabled={selectedRowKeys.length === 0} |
| > |
| {t('批量设置')} ({selectedRowKeys.length}) |
| </Button> |
| {/* 新增:填入官网倍率按钮,仅填充缺失项,来源 mixed */} |
| <Button |
| type='tertiary' |
| onClick={handleFillFromMixed} |
| loading={loadingMixed} |
| disabled={models.length === 0} |
| > |
| {t('填入官网倍率')} |
| </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, |
| formatPageText: (page) => |
| t('第 {{start}} - {{end}} 条,共 {{total}} 条', { |
| start: page.currentStart, |
| end: page.currentEnd, |
| total: filteredModels.length, |
| }), |
| 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> |
| </> |
| ); |
| } |
|
|