ModelRationNotSetEditor.jsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useEffect, useState } from 'react';
  16. import {
  17. Table,
  18. Button,
  19. Input,
  20. Modal,
  21. Form,
  22. Space,
  23. Typography,
  24. Radio,
  25. Notification,
  26. } from '@douyinfe/semi-ui';
  27. import {
  28. IconDelete,
  29. IconPlus,
  30. IconSearch,
  31. IconSave,
  32. IconBolt,
  33. } from '@douyinfe/semi-icons';
  34. import { API, showError, showSuccess } from '../../../helpers';
  35. import { useTranslation } from 'react-i18next';
  36. export default function ModelRatioNotSetEditor(props) {
  37. const { t } = useTranslation();
  38. const [models, setModels] = useState([]);
  39. const [visible, setVisible] = useState(false);
  40. const [batchVisible, setBatchVisible] = useState(false);
  41. const [currentModel, setCurrentModel] = useState(null);
  42. const [searchText, setSearchText] = useState('');
  43. const [currentPage, setCurrentPage] = useState(1);
  44. const [pageSize, setPageSize] = useState(10);
  45. const [loading, setLoading] = useState(false);
  46. const [enabledModels, setEnabledModels] = useState([]);
  47. const [selectedRowKeys, setSelectedRowKeys] = useState([]);
  48. const [batchFillType, setBatchFillType] = useState('ratio');
  49. const [batchFillValue, setBatchFillValue] = useState('');
  50. const [batchRatioValue, setBatchRatioValue] = useState('');
  51. const [batchCompletionRatioValue, setBatchCompletionRatioValue] =
  52. useState('');
  53. const { Text } = Typography;
  54. // 定义可选的每页显示条数
  55. const pageSizeOptions = [10, 20, 50, 100];
  56. const getAllEnabledModels = async () => {
  57. try {
  58. const res = await API.get('/api/channel/models_enabled');
  59. const { success, message, data } = res.data;
  60. if (success) {
  61. setEnabledModels(data);
  62. } else {
  63. showError(message);
  64. }
  65. } catch (error) {
  66. console.error(t('获取启用模型失败:'), error);
  67. showError(t('获取启用模型失败'));
  68. }
  69. };
  70. useEffect(() => {
  71. // 获取所有启用的模型
  72. getAllEnabledModels();
  73. }, []);
  74. useEffect(() => {
  75. try {
  76. const modelPrice = JSON.parse(props.options.ModelPrice || '{}');
  77. const modelRatio = JSON.parse(props.options.ModelRatio || '{}');
  78. const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
  79. // 找出所有未设置价格和倍率的模型
  80. const unsetModels = enabledModels.filter((modelName) => {
  81. const hasPrice = modelPrice[modelName] !== undefined;
  82. const hasRatio = modelRatio[modelName] !== undefined;
  83. // 如果模型没有价格或者没有倍率设置,则显示
  84. return !hasPrice && !hasRatio;
  85. });
  86. // 创建模型数据
  87. const modelData = unsetModels.map((name) => ({
  88. name,
  89. price: modelPrice[name] || '',
  90. ratio: modelRatio[name] || '',
  91. completionRatio: completionRatio[name] || '',
  92. }));
  93. setModels(modelData);
  94. // 清空选择
  95. setSelectedRowKeys([]);
  96. } catch (error) {
  97. console.error(t('JSON解析错误:'), error);
  98. }
  99. }, [props.options, enabledModels]);
  100. // 首先声明分页相关的工具函数
  101. const getPagedData = (data, currentPage, pageSize) => {
  102. const start = (currentPage - 1) * pageSize;
  103. const end = start + pageSize;
  104. return data.slice(start, end);
  105. };
  106. // 处理页面大小变化
  107. const handlePageSizeChange = (size) => {
  108. setPageSize(size);
  109. // 重新计算当前页,避免数据丢失
  110. const totalPages = Math.ceil(filteredModels.length / size);
  111. if (currentPage > totalPages) {
  112. setCurrentPage(totalPages || 1);
  113. }
  114. };
  115. // 在 return 语句之前,先处理过滤和分页逻辑
  116. const filteredModels = models.filter((model) =>
  117. searchText
  118. ? model.name.includes(searchText)
  119. : true,
  120. );
  121. // 然后基于过滤后的数据计算分页数据
  122. const pagedData = getPagedData(filteredModels, currentPage, pageSize);
  123. const SubmitData = async () => {
  124. setLoading(true);
  125. const output = {
  126. ModelPrice: JSON.parse(props.options.ModelPrice || '{}'),
  127. ModelRatio: JSON.parse(props.options.ModelRatio || '{}'),
  128. CompletionRatio: JSON.parse(props.options.CompletionRatio || '{}'),
  129. };
  130. try {
  131. // 数据转换 - 只处理已修改的模型
  132. models.forEach((model) => {
  133. // 只有当用户设置了值时才更新
  134. if (model.price !== '') {
  135. // 如果价格不为空,则转换为浮点数,忽略倍率参数
  136. output.ModelPrice[model.name] = parseFloat(model.price);
  137. } else {
  138. if (model.ratio !== '')
  139. output.ModelRatio[model.name] = parseFloat(model.ratio);
  140. if (model.completionRatio !== '')
  141. output.CompletionRatio[model.name] = parseFloat(
  142. model.completionRatio,
  143. );
  144. }
  145. });
  146. // 准备API请求数组
  147. const finalOutput = {
  148. ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
  149. ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
  150. CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
  151. };
  152. const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
  153. return API.put('/api/option/', {
  154. key,
  155. value,
  156. });
  157. });
  158. // 批量处理请求
  159. const results = await Promise.all(requestQueue);
  160. // 验证结果
  161. if (requestQueue.length === 1) {
  162. if (results.includes(undefined)) return;
  163. } else if (requestQueue.length > 1) {
  164. if (results.includes(undefined)) {
  165. return showError(t('部分保存失败,请重试'));
  166. }
  167. }
  168. // 检查每个请求的结果
  169. for (const res of results) {
  170. if (!res.data.success) {
  171. return showError(res.data.message);
  172. }
  173. }
  174. showSuccess(t('保存成功'));
  175. props.refresh();
  176. // 重新获取未设置的模型
  177. getAllEnabledModels();
  178. } catch (error) {
  179. console.error(t('保存失败:'), error);
  180. showError(t('保存失败,请重试'));
  181. } finally {
  182. setLoading(false);
  183. }
  184. };
  185. const columns = [
  186. {
  187. title: t('模型名称'),
  188. dataIndex: 'name',
  189. key: 'name',
  190. },
  191. {
  192. title: t('模型固定价格'),
  193. dataIndex: 'price',
  194. key: 'price',
  195. render: (text, record) => (
  196. <Input
  197. value={text}
  198. placeholder={t('按量计费')}
  199. onChange={(value) => updateModel(record.name, 'price', value)}
  200. />
  201. ),
  202. },
  203. {
  204. title: t('模型倍率'),
  205. dataIndex: 'ratio',
  206. key: 'ratio',
  207. render: (text, record) => (
  208. <Input
  209. value={text}
  210. placeholder={record.price !== '' ? t('模型倍率') : t('输入模型倍率')}
  211. disabled={record.price !== ''}
  212. onChange={(value) => updateModel(record.name, 'ratio', value)}
  213. />
  214. ),
  215. },
  216. {
  217. title: t('补全倍率'),
  218. dataIndex: 'completionRatio',
  219. key: 'completionRatio',
  220. render: (text, record) => (
  221. <Input
  222. value={text}
  223. placeholder={record.price !== '' ? t('补全倍率') : t('输入补全倍率')}
  224. disabled={record.price !== ''}
  225. onChange={(value) =>
  226. updateModel(record.name, 'completionRatio', value)
  227. }
  228. />
  229. ),
  230. },
  231. ];
  232. const updateModel = (name, field, value) => {
  233. if (value !== '' && isNaN(value)) {
  234. showError(t('请输入数字'));
  235. return;
  236. }
  237. setModels((prev) =>
  238. prev.map((model) =>
  239. model.name === name ? { ...model, [field]: value } : model,
  240. ),
  241. );
  242. };
  243. const addModel = (values) => {
  244. // 检查模型名称是否存在, 如果存在则拒绝添加
  245. if (models.some((model) => model.name === values.name)) {
  246. showError(t('模型名称已存在'));
  247. return;
  248. }
  249. setModels((prev) => [
  250. {
  251. name: values.name,
  252. price: values.price || '',
  253. ratio: values.ratio || '',
  254. completionRatio: values.completionRatio || '',
  255. },
  256. ...prev,
  257. ]);
  258. setVisible(false);
  259. showSuccess(t('添加成功'));
  260. };
  261. // 批量填充功能
  262. const handleBatchFill = () => {
  263. if (selectedRowKeys.length === 0) {
  264. showError(t('请先选择需要批量设置的模型'));
  265. return;
  266. }
  267. if (batchFillType === 'bothRatio') {
  268. if (batchRatioValue === '' || batchCompletionRatioValue === '') {
  269. showError(t('请输入模型倍率和补全倍率'));
  270. return;
  271. }
  272. if (isNaN(batchRatioValue) || isNaN(batchCompletionRatioValue)) {
  273. showError(t('请输入有效的数字'));
  274. return;
  275. }
  276. } else {
  277. if (batchFillValue === '') {
  278. showError(t('请输入填充值'));
  279. return;
  280. }
  281. if (isNaN(batchFillValue)) {
  282. showError(t('请输入有效的数字'));
  283. return;
  284. }
  285. }
  286. // 根据选择的类型批量更新模型
  287. setModels((prev) =>
  288. prev.map((model) => {
  289. if (selectedRowKeys.includes(model.name)) {
  290. if (batchFillType === 'price') {
  291. return {
  292. ...model,
  293. price: batchFillValue,
  294. ratio: '',
  295. completionRatio: '',
  296. };
  297. } else if (batchFillType === 'ratio') {
  298. return {
  299. ...model,
  300. price: '',
  301. ratio: batchFillValue,
  302. };
  303. } else if (batchFillType === 'completionRatio') {
  304. return {
  305. ...model,
  306. price: '',
  307. completionRatio: batchFillValue,
  308. };
  309. } else if (batchFillType === 'bothRatio') {
  310. return {
  311. ...model,
  312. price: '',
  313. ratio: batchRatioValue,
  314. completionRatio: batchCompletionRatioValue,
  315. };
  316. }
  317. }
  318. return model;
  319. }),
  320. );
  321. setBatchVisible(false);
  322. Notification.success({
  323. title: t('批量设置成功'),
  324. content: t('已为 {{count}} 个模型设置{{type}}', {
  325. count: selectedRowKeys.length,
  326. type:
  327. batchFillType === 'price'
  328. ? t('固定价格')
  329. : batchFillType === 'ratio'
  330. ? t('模型倍率')
  331. : batchFillType === 'completionRatio'
  332. ? t('补全倍率')
  333. : t('模型倍率和补全倍率'),
  334. }),
  335. duration: 3,
  336. });
  337. };
  338. const handleBatchTypeChange = (value) => {
  339. console.log(t('Changing batch type to:'), value);
  340. setBatchFillType(value);
  341. // 切换类型时清空对应的值
  342. if (value !== 'bothRatio') {
  343. setBatchFillValue('');
  344. } else {
  345. setBatchRatioValue('');
  346. setBatchCompletionRatioValue('');
  347. }
  348. };
  349. const rowSelection = {
  350. selectedRowKeys,
  351. onChange: (selectedKeys) => {
  352. setSelectedRowKeys(selectedKeys);
  353. },
  354. };
  355. return (
  356. <>
  357. <Space vertical align='start' style={{ width: '100%' }}>
  358. <Space className='mt-2'>
  359. <Button icon={<IconPlus />} onClick={() => setVisible(true)}>
  360. {t('添加模型')}
  361. </Button>
  362. <Button
  363. icon={<IconBolt />}
  364. type='secondary'
  365. onClick={() => setBatchVisible(true)}
  366. disabled={selectedRowKeys.length === 0}
  367. >
  368. {t('批量设置')} ({selectedRowKeys.length})
  369. </Button>
  370. <Button
  371. type='primary'
  372. icon={<IconSave />}
  373. onClick={SubmitData}
  374. loading={loading}
  375. >
  376. {t('应用更改')}
  377. </Button>
  378. <Input
  379. prefix={<IconSearch />}
  380. placeholder={t('搜索模型名称')}
  381. value={searchText}
  382. onChange={(value) => {
  383. setSearchText(value);
  384. setCurrentPage(1);
  385. }}
  386. style={{ width: 200 }}
  387. />
  388. </Space>
  389. <Text>
  390. {t('此页面仅显示未设置价格或倍率的模型,设置后将自动从列表中移除')}
  391. </Text>
  392. <Table
  393. columns={columns}
  394. dataSource={pagedData}
  395. rowSelection={rowSelection}
  396. rowKey='name'
  397. pagination={{
  398. currentPage: currentPage,
  399. pageSize: pageSize,
  400. total: filteredModels.length,
  401. onPageChange: (page) => setCurrentPage(page),
  402. onPageSizeChange: handlePageSizeChange,
  403. pageSizeOptions: pageSizeOptions,
  404. showTotal: true,
  405. showSizeChanger: true,
  406. }}
  407. empty={
  408. <div style={{ textAlign: 'center', padding: '20px' }}>
  409. {t('没有未设置的模型')}
  410. </div>
  411. }
  412. />
  413. </Space>
  414. {/* 添加模型弹窗 */}
  415. <Modal
  416. title={t('添加模型')}
  417. visible={visible}
  418. onCancel={() => setVisible(false)}
  419. onOk={() => {
  420. currentModel && addModel(currentModel);
  421. }}
  422. >
  423. <Form>
  424. <Form.Input
  425. field='name'
  426. label={t('模型名称')}
  427. placeholder='strawberry'
  428. required
  429. onChange={(value) =>
  430. setCurrentModel((prev) => ({ ...prev, name: value }))
  431. }
  432. />
  433. <Form.Switch
  434. field='priceMode'
  435. label={
  436. <>
  437. {t('定价模式')}:
  438. {currentModel?.priceMode ? t('固定价格') : t('倍率模式')}
  439. </>
  440. }
  441. onChange={(checked) => {
  442. setCurrentModel((prev) => ({
  443. ...prev,
  444. price: '',
  445. ratio: '',
  446. completionRatio: '',
  447. priceMode: checked,
  448. }));
  449. }}
  450. />
  451. {currentModel?.priceMode ? (
  452. <Form.Input
  453. field='price'
  454. label={t('固定价格(每次)')}
  455. placeholder={t('输入每次价格')}
  456. onChange={(value) =>
  457. setCurrentModel((prev) => ({ ...prev, price: value }))
  458. }
  459. />
  460. ) : (
  461. <>
  462. <Form.Input
  463. field='ratio'
  464. label={t('模型倍率')}
  465. placeholder={t('输入模型倍率')}
  466. onChange={(value) =>
  467. setCurrentModel((prev) => ({ ...prev, ratio: value }))
  468. }
  469. />
  470. <Form.Input
  471. field='completionRatio'
  472. label={t('补全倍率')}
  473. placeholder={t('输入补全价格')}
  474. onChange={(value) =>
  475. setCurrentModel((prev) => ({
  476. ...prev,
  477. completionRatio: value,
  478. }))
  479. }
  480. />
  481. </>
  482. )}
  483. </Form>
  484. </Modal>
  485. {/* 批量设置弹窗 */}
  486. <Modal
  487. title={t('批量设置模型参数')}
  488. visible={batchVisible}
  489. onCancel={() => setBatchVisible(false)}
  490. onOk={handleBatchFill}
  491. width={500}
  492. >
  493. <Form>
  494. <Form.Section text={t('设置类型')}>
  495. <div style={{ marginBottom: '16px' }}>
  496. <Space>
  497. <Radio
  498. checked={batchFillType === 'price'}
  499. onChange={() => handleBatchTypeChange('price')}
  500. >
  501. {t('固定价格')}
  502. </Radio>
  503. <Radio
  504. checked={batchFillType === 'ratio'}
  505. onChange={() => handleBatchTypeChange('ratio')}
  506. >
  507. {t('模型倍率')}
  508. </Radio>
  509. <Radio
  510. checked={batchFillType === 'completionRatio'}
  511. onChange={() => handleBatchTypeChange('completionRatio')}
  512. >
  513. {t('补全倍率')}
  514. </Radio>
  515. <Radio
  516. checked={batchFillType === 'bothRatio'}
  517. onChange={() => handleBatchTypeChange('bothRatio')}
  518. >
  519. {t('模型倍率和补全倍率同时设置')}
  520. </Radio>
  521. </Space>
  522. </div>
  523. </Form.Section>
  524. {batchFillType === 'bothRatio' ? (
  525. <>
  526. <Form.Input
  527. field='batchRatioValue'
  528. label={t('模型倍率值')}
  529. placeholder={t('请输入模型倍率')}
  530. value={batchRatioValue}
  531. onChange={(value) => setBatchRatioValue(value)}
  532. />
  533. <Form.Input
  534. field='batchCompletionRatioValue'
  535. label={t('补全倍率值')}
  536. placeholder={t('请输入补全倍率')}
  537. value={batchCompletionRatioValue}
  538. onChange={(value) => setBatchCompletionRatioValue(value)}
  539. />
  540. </>
  541. ) : (
  542. <Form.Input
  543. field='batchFillValue'
  544. label={
  545. batchFillType === 'price'
  546. ? t('固定价格值')
  547. : batchFillType === 'ratio'
  548. ? t('模型倍率值')
  549. : t('补全倍率值')
  550. }
  551. placeholder={t('请输入数值')}
  552. value={batchFillValue}
  553. onChange={(value) => setBatchFillValue(value)}
  554. />
  555. )}
  556. <Text type='tertiary'>
  557. {t('将为选中的 ')} <Text strong>{selectedRowKeys.length}</Text>{' '}
  558. {t(' 个模型设置相同的值')}
  559. </Text>
  560. <div style={{ marginTop: '8px' }}>
  561. <Text type='tertiary'>
  562. {t('当前设置类型: ')}{' '}
  563. <Text strong>
  564. {batchFillType === 'price'
  565. ? t('固定价格')
  566. : batchFillType === 'ratio'
  567. ? t('模型倍率')
  568. : batchFillType === 'completionRatio'
  569. ? t('补全倍率')
  570. : t('模型倍率和补全倍率')}
  571. </Text>
  572. </Text>
  573. </div>
  574. </Form>
  575. </Modal>
  576. </>
  577. );
  578. }