ModelSettingsVisualEditor.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. // ModelSettingsVisualEditor.js
  2. import React, { useContext, useEffect, useState, useRef } from 'react';
  3. import {
  4. Table,
  5. Button,
  6. Input,
  7. Modal,
  8. Form,
  9. Space,
  10. RadioGroup,
  11. Radio,
  12. Tabs,
  13. TabPane,
  14. } from '@douyinfe/semi-ui';
  15. import {
  16. IconDelete,
  17. IconPlus,
  18. IconSearch,
  19. IconSave,
  20. IconEdit,
  21. } from '@douyinfe/semi-icons';
  22. import { showError, showSuccess } from '../../../helpers';
  23. import { API } from '../../../helpers';
  24. import { useTranslation } from 'react-i18next';
  25. import { StatusContext } from '../../../context/Status/index.js';
  26. import { getQuotaPerUnit } from '../../../helpers/render.js';
  27. export default function ModelSettingsVisualEditor(props) {
  28. const { t } = useTranslation();
  29. const [models, setModels] = useState([]);
  30. const [visible, setVisible] = useState(false);
  31. const [currentModel, setCurrentModel] = useState(null);
  32. const [searchText, setSearchText] = useState('');
  33. const [currentPage, setCurrentPage] = useState(1);
  34. const [loading, setLoading] = useState(false);
  35. const [pricingMode, setPricingMode] = useState('per-token'); // 'per-token' or 'per-request'
  36. const [pricingSubMode, setPricingSubMode] = useState('ratio'); // 'ratio' or 'token-price'
  37. const formRef = useRef(null);
  38. const pageSize = 10;
  39. const quotaPerUnit = getQuotaPerUnit();
  40. useEffect(() => {
  41. try {
  42. const modelPrice = JSON.parse(props.options.ModelPrice || '{}');
  43. const modelRatio = JSON.parse(props.options.ModelRatio || '{}');
  44. const completionRatio = JSON.parse(props.options.CompletionRatio || '{}');
  45. // 合并所有模型名称
  46. const modelNames = new Set([
  47. ...Object.keys(modelPrice),
  48. ...Object.keys(modelRatio),
  49. ...Object.keys(completionRatio),
  50. ]);
  51. const modelData = Array.from(modelNames).map((name) => ({
  52. name,
  53. price: modelPrice[name] === undefined ? '' : modelPrice[name],
  54. ratio: modelRatio[name] === undefined ? '' : modelRatio[name],
  55. completionRatio:
  56. completionRatio[name] === undefined ? '' : completionRatio[name],
  57. }));
  58. setModels(modelData);
  59. } catch (error) {
  60. console.error('JSON解析错误:', error);
  61. }
  62. }, [props.options]);
  63. // 首先声明分页相关的工具函数
  64. const getPagedData = (data, currentPage, pageSize) => {
  65. const start = (currentPage - 1) * pageSize;
  66. const end = start + pageSize;
  67. return data.slice(start, end);
  68. };
  69. // 在 return 语句之前,先处理过滤和分页逻辑
  70. const filteredModels = models.filter((model) =>
  71. searchText
  72. ? model.name.toLowerCase().includes(searchText.toLowerCase())
  73. : true,
  74. );
  75. // 然后基于过滤后的数据计算分页数据
  76. const pagedData = getPagedData(filteredModels, currentPage, pageSize);
  77. const SubmitData = async () => {
  78. setLoading(true);
  79. const output = {
  80. ModelPrice: {},
  81. ModelRatio: {},
  82. CompletionRatio: {},
  83. };
  84. let currentConvertModelName = '';
  85. try {
  86. // 数据转换
  87. models.forEach((model) => {
  88. currentConvertModelName = model.name;
  89. if (model.price !== '') {
  90. // 如果价格不为空,则转换为浮点数,忽略倍率参数
  91. output.ModelPrice[model.name] = parseFloat(model.price);
  92. } else {
  93. if (model.ratio !== '')
  94. output.ModelRatio[model.name] = parseFloat(model.ratio);
  95. if (model.completionRatio !== '')
  96. output.CompletionRatio[model.name] = parseFloat(
  97. model.completionRatio,
  98. );
  99. }
  100. });
  101. // 准备API请求数组
  102. const finalOutput = {
  103. ModelPrice: JSON.stringify(output.ModelPrice, null, 2),
  104. ModelRatio: JSON.stringify(output.ModelRatio, null, 2),
  105. CompletionRatio: JSON.stringify(output.CompletionRatio, null, 2),
  106. };
  107. const requestQueue = Object.entries(finalOutput).map(([key, value]) => {
  108. return API.put('/api/option/', {
  109. key,
  110. value,
  111. });
  112. });
  113. // 批量处理请求
  114. const results = await Promise.all(requestQueue);
  115. // 验证结果
  116. if (requestQueue.length === 1) {
  117. if (results.includes(undefined)) return;
  118. } else if (requestQueue.length > 1) {
  119. if (results.includes(undefined)) {
  120. return showError('部分保存失败,请重试');
  121. }
  122. }
  123. // 检查每个请求的结果
  124. for (const res of results) {
  125. if (!res.data.success) {
  126. return showError(res.data.message);
  127. }
  128. }
  129. showSuccess('保存成功');
  130. props.refresh();
  131. } catch (error) {
  132. console.error('保存失败:', error);
  133. showError('保存失败,请重试');
  134. } finally {
  135. setLoading(false);
  136. }
  137. };
  138. const columns = [
  139. {
  140. title: t('模型名称'),
  141. dataIndex: 'name',
  142. key: 'name',
  143. },
  144. {
  145. title: t('模型固定价格'),
  146. dataIndex: 'price',
  147. key: 'price',
  148. render: (text, record) => (
  149. <Input
  150. value={text}
  151. placeholder={t('按量计费')}
  152. onChange={(value) => updateModel(record.name, 'price', value)}
  153. />
  154. ),
  155. },
  156. {
  157. title: t('模型倍率'),
  158. dataIndex: 'ratio',
  159. key: 'ratio',
  160. render: (text, record) => (
  161. <Input
  162. value={text}
  163. placeholder={record.price !== '' ? t('模型倍率') : t('默认补全倍率')}
  164. disabled={record.price !== ''}
  165. onChange={(value) => updateModel(record.name, 'ratio', value)}
  166. />
  167. ),
  168. },
  169. {
  170. title: t('补全倍率'),
  171. dataIndex: 'completionRatio',
  172. key: 'completionRatio',
  173. render: (text, record) => (
  174. <Input
  175. value={text}
  176. placeholder={record.price !== '' ? t('补全倍率') : t('默认补全倍率')}
  177. disabled={record.price !== ''}
  178. onChange={(value) =>
  179. updateModel(record.name, 'completionRatio', value)
  180. }
  181. />
  182. ),
  183. },
  184. {
  185. title: t('操作'),
  186. key: 'action',
  187. render: (_, record) => (
  188. <Space>
  189. <Button
  190. type='primary'
  191. icon={<IconEdit />}
  192. onClick={() => editModel(record)}
  193. ></Button>
  194. <Button
  195. icon={<IconDelete />}
  196. type='danger'
  197. onClick={() => deleteModel(record.name)}
  198. />
  199. </Space>
  200. ),
  201. },
  202. ];
  203. const updateModel = (name, field, value) => {
  204. if (isNaN(value)) {
  205. showError('请输入数字');
  206. return;
  207. }
  208. setModels((prev) =>
  209. prev.map((model) =>
  210. model.name === name ? { ...model, [field]: value } : model,
  211. ),
  212. );
  213. };
  214. const deleteModel = (name) => {
  215. setModels((prev) => prev.filter((model) => model.name !== name));
  216. };
  217. const calculateRatioFromTokenPrice = (tokenPrice) => {
  218. return tokenPrice / 2;
  219. };
  220. const calculateCompletionRatioFromPrices = (
  221. modelTokenPrice,
  222. completionTokenPrice,
  223. ) => {
  224. if (!modelTokenPrice || modelTokenPrice === '0') {
  225. showError('模型价格不能为0');
  226. return '';
  227. }
  228. return completionTokenPrice / modelTokenPrice;
  229. };
  230. const handleTokenPriceChange = (value) => {
  231. // Use a temporary variable to hold the new state
  232. let newState = {
  233. ...(currentModel || {}),
  234. tokenPrice: value,
  235. ratio: 0,
  236. };
  237. if (!isNaN(value) && value !== '') {
  238. const tokenPrice = parseFloat(value);
  239. const ratio = calculateRatioFromTokenPrice(tokenPrice);
  240. newState.ratio = ratio;
  241. }
  242. // Set the state with the complete updated object
  243. setCurrentModel(newState);
  244. };
  245. const handleCompletionTokenPriceChange = (value) => {
  246. // Use a temporary variable to hold the new state
  247. let newState = {
  248. ...(currentModel || {}),
  249. completionTokenPrice: value,
  250. completionRatio: 0,
  251. };
  252. if (!isNaN(value) && value !== '' && currentModel?.tokenPrice) {
  253. const completionTokenPrice = parseFloat(value);
  254. const modelTokenPrice = parseFloat(currentModel.tokenPrice);
  255. if (modelTokenPrice > 0) {
  256. const completionRatio = calculateCompletionRatioFromPrices(
  257. modelTokenPrice,
  258. completionTokenPrice,
  259. );
  260. newState.completionRatio = completionRatio;
  261. }
  262. }
  263. // Set the state with the complete updated object
  264. setCurrentModel(newState);
  265. };
  266. const addOrUpdateModel = (values) => {
  267. // Check if we're editing an existing model or adding a new one
  268. const existingModelIndex = models.findIndex(
  269. (model) => model.name === values.name,
  270. );
  271. if (existingModelIndex >= 0) {
  272. // Update existing model
  273. setModels((prev) =>
  274. prev.map((model, index) =>
  275. index === existingModelIndex
  276. ? {
  277. name: values.name,
  278. price: values.price || '',
  279. ratio: values.ratio || '',
  280. completionRatio: values.completionRatio || '',
  281. }
  282. : model,
  283. ),
  284. );
  285. setVisible(false);
  286. showSuccess(t('更新成功'));
  287. } else {
  288. // Add new model
  289. // Check if model name already exists
  290. if (models.some((model) => model.name === values.name)) {
  291. showError(t('模型名称已存在'));
  292. return;
  293. }
  294. setModels((prev) => [
  295. {
  296. name: values.name,
  297. price: values.price || '',
  298. ratio: values.ratio || '',
  299. completionRatio: values.completionRatio || '',
  300. },
  301. ...prev,
  302. ]);
  303. setVisible(false);
  304. showSuccess(t('添加成功'));
  305. }
  306. };
  307. const calculateTokenPriceFromRatio = (ratio) => {
  308. return ratio * 2;
  309. };
  310. const resetModalState = () => {
  311. setCurrentModel(null);
  312. setPricingMode('per-token');
  313. setPricingSubMode('ratio');
  314. };
  315. const editModel = (record) => {
  316. // Determine which pricing mode to use based on the model's current configuration
  317. let initialPricingMode = 'per-token';
  318. let initialPricingSubMode = 'ratio';
  319. if (record.price !== '') {
  320. initialPricingMode = 'per-request';
  321. } else {
  322. initialPricingMode = 'per-token';
  323. // We default to ratio mode, but could set to token-price if needed
  324. }
  325. // Set the pricing modes for the form
  326. setPricingMode(initialPricingMode);
  327. setPricingSubMode(initialPricingSubMode);
  328. // Create a copy of the model data to avoid modifying the original
  329. const modelCopy = { ...record };
  330. // If the model has ratio data and we want to populate token price fields
  331. if (record.ratio) {
  332. modelCopy.tokenPrice = calculateTokenPriceFromRatio(
  333. parseFloat(record.ratio),
  334. ).toString();
  335. if (record.completionRatio) {
  336. modelCopy.completionTokenPrice = (
  337. parseFloat(modelCopy.tokenPrice) * parseFloat(record.completionRatio)
  338. ).toString();
  339. }
  340. }
  341. // Set the current model
  342. setCurrentModel(modelCopy);
  343. // Open the modal
  344. setVisible(true);
  345. // Use setTimeout to ensure the form is rendered before setting values
  346. setTimeout(() => {
  347. if (formRef.current) {
  348. // Update the form fields based on pricing mode
  349. const formValues = {
  350. name: modelCopy.name,
  351. };
  352. if (initialPricingMode === 'per-request') {
  353. formValues.priceInput = modelCopy.price;
  354. } else if (initialPricingMode === 'per-token') {
  355. formValues.ratioInput = modelCopy.ratio;
  356. formValues.completionRatioInput = modelCopy.completionRatio;
  357. formValues.modelTokenPrice = modelCopy.tokenPrice;
  358. formValues.completionTokenPrice = modelCopy.completionTokenPrice;
  359. }
  360. formRef.current.setValues(formValues);
  361. }
  362. }, 0);
  363. };
  364. return (
  365. <>
  366. <Space vertical align='start' style={{ width: '100%' }}>
  367. <Space>
  368. <Button
  369. icon={<IconPlus />}
  370. onClick={() => {
  371. resetModalState();
  372. setVisible(true);
  373. }}
  374. >
  375. {t('添加模型')}
  376. </Button>
  377. <Button type='primary' icon={<IconSave />} onClick={SubmitData}>
  378. {t('应用更改')}
  379. </Button>
  380. <Input
  381. prefix={<IconSearch />}
  382. placeholder={t('搜索模型名称')}
  383. value={searchText}
  384. onChange={(value) => {
  385. setSearchText(value);
  386. setCurrentPage(1);
  387. }}
  388. style={{ width: 200 }}
  389. />
  390. </Space>
  391. <Table
  392. columns={columns}
  393. dataSource={pagedData}
  394. pagination={{
  395. currentPage: currentPage,
  396. pageSize: pageSize,
  397. total: filteredModels.length,
  398. onPageChange: (page) => setCurrentPage(page),
  399. formatPageText: (page) =>
  400. t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
  401. start: page.currentStart,
  402. end: page.currentEnd,
  403. total: filteredModels.length,
  404. }),
  405. showTotal: true,
  406. showSizeChanger: false,
  407. }}
  408. />
  409. </Space>
  410. <Modal
  411. title={
  412. currentModel &&
  413. currentModel.name &&
  414. models.some((model) => model.name === currentModel.name)
  415. ? t('编辑模型')
  416. : t('添加模型')
  417. }
  418. visible={visible}
  419. onCancel={() => {
  420. resetModalState();
  421. setVisible(false);
  422. }}
  423. onOk={() => {
  424. if (currentModel) {
  425. // If we're in token price mode, make sure ratio values are properly set
  426. const valuesToSave = { ...currentModel };
  427. if (
  428. pricingMode === 'per-token' &&
  429. pricingSubMode === 'token-price' &&
  430. currentModel.tokenPrice
  431. ) {
  432. // Calculate and set ratio from token price
  433. const tokenPrice = parseFloat(currentModel.tokenPrice);
  434. valuesToSave.ratio = (tokenPrice / 2).toString();
  435. // Calculate and set completion ratio if both token prices are available
  436. if (
  437. currentModel.completionTokenPrice &&
  438. currentModel.tokenPrice
  439. ) {
  440. const completionPrice = parseFloat(
  441. currentModel.completionTokenPrice,
  442. );
  443. const modelPrice = parseFloat(currentModel.tokenPrice);
  444. if (modelPrice > 0) {
  445. valuesToSave.completionRatio = (
  446. completionPrice / modelPrice
  447. ).toString();
  448. }
  449. }
  450. }
  451. // Clear price if we're in per-token mode
  452. if (pricingMode === 'per-token') {
  453. valuesToSave.price = '';
  454. } else {
  455. // Clear ratios if we're in per-request mode
  456. valuesToSave.ratio = '';
  457. valuesToSave.completionRatio = '';
  458. }
  459. addOrUpdateModel(valuesToSave);
  460. }
  461. }}
  462. >
  463. <Form getFormApi={(api) => (formRef.current = api)}>
  464. <Form.Input
  465. field='name'
  466. label={t('模型名称')}
  467. placeholder='strawberry'
  468. required
  469. disabled={
  470. currentModel &&
  471. currentModel.name &&
  472. models.some((model) => model.name === currentModel.name)
  473. }
  474. onChange={(value) =>
  475. setCurrentModel((prev) => ({ ...prev, name: value }))
  476. }
  477. />
  478. <Form.Section text={t('定价模式')}>
  479. <div style={{ marginBottom: '16px' }}>
  480. <RadioGroup
  481. type='button'
  482. value={pricingMode}
  483. onChange={(e) => {
  484. const newMode = e.target.value;
  485. const oldMode = pricingMode;
  486. setPricingMode(newMode);
  487. // Instead of resetting all values, convert between modes
  488. if (currentModel) {
  489. const updatedModel = { ...currentModel };
  490. // Update formRef with converted values
  491. if (formRef.current) {
  492. const formValues = {
  493. name: updatedModel.name,
  494. };
  495. if (newMode === 'per-request') {
  496. formValues.priceInput = updatedModel.price || '';
  497. } else if (newMode === 'per-token') {
  498. formValues.ratioInput = updatedModel.ratio || '';
  499. formValues.completionRatioInput =
  500. updatedModel.completionRatio || '';
  501. formValues.modelTokenPrice =
  502. updatedModel.tokenPrice || '';
  503. formValues.completionTokenPrice =
  504. updatedModel.completionTokenPrice || '';
  505. }
  506. formRef.current.setValues(formValues);
  507. }
  508. // Update the model state
  509. setCurrentModel(updatedModel);
  510. }
  511. }}
  512. >
  513. <Radio value='per-token'>{t('按量计费')}</Radio>
  514. <Radio value='per-request'>{t('按次计费')}</Radio>
  515. </RadioGroup>
  516. </div>
  517. </Form.Section>
  518. {pricingMode === 'per-token' && (
  519. <>
  520. <Form.Section text={t('价格设置方式')}>
  521. <div style={{ marginBottom: '16px' }}>
  522. <RadioGroup
  523. type='button'
  524. value={pricingSubMode}
  525. onChange={(e) => {
  526. const newSubMode = e.target.value;
  527. const oldSubMode = pricingSubMode;
  528. setPricingSubMode(newSubMode);
  529. // Handle conversion between submodes
  530. if (currentModel) {
  531. const updatedModel = { ...currentModel };
  532. // Convert between ratio and token price
  533. if (
  534. oldSubMode === 'ratio' &&
  535. newSubMode === 'token-price'
  536. ) {
  537. if (updatedModel.ratio) {
  538. updatedModel.tokenPrice =
  539. calculateTokenPriceFromRatio(
  540. parseFloat(updatedModel.ratio),
  541. ).toString();
  542. if (updatedModel.completionRatio) {
  543. updatedModel.completionTokenPrice = (
  544. parseFloat(updatedModel.tokenPrice) *
  545. parseFloat(updatedModel.completionRatio)
  546. ).toString();
  547. }
  548. }
  549. } else if (
  550. oldSubMode === 'token-price' &&
  551. newSubMode === 'ratio'
  552. ) {
  553. // Ratio values should already be calculated by the handlers
  554. }
  555. // Update the form values
  556. if (formRef.current) {
  557. const formValues = {};
  558. if (newSubMode === 'ratio') {
  559. formValues.ratioInput = updatedModel.ratio || '';
  560. formValues.completionRatioInput =
  561. updatedModel.completionRatio || '';
  562. } else if (newSubMode === 'token-price') {
  563. formValues.modelTokenPrice =
  564. updatedModel.tokenPrice || '';
  565. formValues.completionTokenPrice =
  566. updatedModel.completionTokenPrice || '';
  567. }
  568. formRef.current.setValues(formValues);
  569. }
  570. setCurrentModel(updatedModel);
  571. }
  572. }}
  573. >
  574. <Radio value='ratio'>{t('按倍率设置')}</Radio>
  575. <Radio value='token-price'>{t('按价格设置')}</Radio>
  576. </RadioGroup>
  577. </div>
  578. </Form.Section>
  579. {pricingSubMode === 'ratio' && (
  580. <>
  581. <Form.Input
  582. field='ratioInput'
  583. label={t('模型倍率')}
  584. placeholder={t('输入模型倍率')}
  585. onChange={(value) =>
  586. setCurrentModel((prev) => ({
  587. ...(prev || {}),
  588. ratio: value,
  589. }))
  590. }
  591. initValue={currentModel?.ratio || ''}
  592. />
  593. <Form.Input
  594. field='completionRatioInput'
  595. label={t('补全倍率')}
  596. placeholder={t('输入补全倍率')}
  597. onChange={(value) =>
  598. setCurrentModel((prev) => ({
  599. ...(prev || {}),
  600. completionRatio: value,
  601. }))
  602. }
  603. initValue={currentModel?.completionRatio || ''}
  604. />
  605. </>
  606. )}
  607. {pricingSubMode === 'token-price' && (
  608. <>
  609. <Form.Input
  610. field='modelTokenPrice'
  611. label={t('输入价格')}
  612. onChange={(value) => {
  613. handleTokenPriceChange(value);
  614. }}
  615. initValue={currentModel?.tokenPrice || ''}
  616. suffix={t('$/1M tokens')}
  617. />
  618. <Form.Input
  619. field='completionTokenPrice'
  620. label={t('输出价格')}
  621. onChange={(value) => {
  622. handleCompletionTokenPriceChange(value);
  623. }}
  624. initValue={currentModel?.completionTokenPrice || ''}
  625. suffix={t('$/1M tokens')}
  626. />
  627. </>
  628. )}
  629. </>
  630. )}
  631. {pricingMode === 'per-request' && (
  632. <Form.Input
  633. field='priceInput'
  634. label={t('固定价格(每次)')}
  635. placeholder={t('输入每次价格')}
  636. onChange={(value) =>
  637. setCurrentModel((prev) => ({
  638. ...(prev || {}),
  639. price: value,
  640. }))
  641. }
  642. initValue={currentModel?.price || ''}
  643. />
  644. )}
  645. </Form>
  646. </Modal>
  647. </>
  648. );
  649. }