ModelSettingsVisualEditor.js 24 KB

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