ModelPricing.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  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, { useContext, useEffect, useRef, useMemo, useState } from 'react';
  16. import { API, copy, showError, showInfo, showSuccess, getModelCategories, renderModelTag, stringToColor } from '../../helpers';
  17. import { useTranslation } from 'react-i18next';
  18. import {
  19. Input,
  20. Layout,
  21. Modal,
  22. Space,
  23. Table,
  24. Tag,
  25. Tooltip,
  26. Popover,
  27. ImagePreview,
  28. Button,
  29. Card,
  30. Tabs,
  31. TabPane,
  32. Empty,
  33. Switch,
  34. Select
  35. } from '@douyinfe/semi-ui';
  36. import {
  37. IllustrationNoResult,
  38. IllustrationNoResultDark
  39. } from '@douyinfe/semi-illustrations';
  40. import {
  41. IconVerify,
  42. IconHelpCircle,
  43. IconSearch,
  44. IconCopy,
  45. IconInfoCircle,
  46. IconLayers
  47. } from '@douyinfe/semi-icons';
  48. import { UserContext } from '../../context/User/index.js';
  49. import { AlertCircle } from 'lucide-react';
  50. import { StatusContext } from '../../context/Status/index.js';
  51. const ModelPricing = () => {
  52. const { t } = useTranslation();
  53. const [filteredValue, setFilteredValue] = useState([]);
  54. const compositionRef = useRef({ isComposition: false });
  55. const [selectedRowKeys, setSelectedRowKeys] = useState([]);
  56. const [modalImageUrl, setModalImageUrl] = useState('');
  57. const [isModalOpenurl, setIsModalOpenurl] = useState(false);
  58. const [selectedGroup, setSelectedGroup] = useState('default');
  59. const [activeKey, setActiveKey] = useState('all');
  60. const [pageSize, setPageSize] = useState(10);
  61. const [currency, setCurrency] = useState('USD');
  62. const [showWithRecharge, setShowWithRecharge] = useState(false);
  63. const [tokenUnit, setTokenUnit] = useState('M');
  64. const [statusState] = useContext(StatusContext);
  65. // 充值汇率(price)与美元兑人民币汇率(usd_exchange_rate)
  66. const priceRate = useMemo(() => statusState?.status?.price ?? 1, [statusState]);
  67. const usdExchangeRate = useMemo(() => statusState?.status?.usd_exchange_rate ?? priceRate, [statusState, priceRate]);
  68. const rowSelection = useMemo(
  69. () => ({
  70. onChange: (selectedRowKeys, selectedRows) => {
  71. setSelectedRowKeys(selectedRowKeys);
  72. },
  73. }),
  74. [],
  75. );
  76. const handleChange = (value) => {
  77. if (compositionRef.current.isComposition) {
  78. return;
  79. }
  80. const newFilteredValue = value ? [value] : [];
  81. setFilteredValue(newFilteredValue);
  82. };
  83. const handleCompositionStart = () => {
  84. compositionRef.current.isComposition = true;
  85. };
  86. const handleCompositionEnd = (event) => {
  87. compositionRef.current.isComposition = false;
  88. const value = event.target.value;
  89. const newFilteredValue = value ? [value] : [];
  90. setFilteredValue(newFilteredValue);
  91. };
  92. function renderQuotaType(type) {
  93. switch (type) {
  94. case 1:
  95. return (
  96. <Tag color='teal' shape='circle'>
  97. {t('按次计费')}
  98. </Tag>
  99. );
  100. case 0:
  101. return (
  102. <Tag color='violet' shape='circle'>
  103. {t('按量计费')}
  104. </Tag>
  105. );
  106. default:
  107. return t('未知');
  108. }
  109. }
  110. function renderAvailable(available) {
  111. return available ? (
  112. <Popover
  113. content={
  114. <div style={{ padding: 8 }}>{t('您的分组可以使用该模型')}</div>
  115. }
  116. position='top'
  117. key={available}
  118. className="bg-green-50"
  119. >
  120. <IconVerify style={{ color: 'rgb(22 163 74)' }} size='large' />
  121. </Popover>
  122. ) : null;
  123. }
  124. function renderSupportedEndpoints(endpoints) {
  125. if (!endpoints || endpoints.length === 0) {
  126. return null;
  127. }
  128. return (
  129. <Space wrap>
  130. {endpoints.map((endpoint, idx) => (
  131. <Tag
  132. key={endpoint}
  133. color={stringToColor(endpoint)}
  134. shape='circle'
  135. >
  136. {endpoint}
  137. </Tag>
  138. ))}
  139. </Space>
  140. );
  141. }
  142. const displayPrice = (usdPrice) => {
  143. let priceInUSD = usdPrice;
  144. if (showWithRecharge) {
  145. priceInUSD = usdPrice * priceRate / usdExchangeRate;
  146. }
  147. if (currency === 'CNY') {
  148. return `¥${(priceInUSD * usdExchangeRate).toFixed(3)}`;
  149. }
  150. return `$${priceInUSD.toFixed(3)}`;
  151. };
  152. const columns = [
  153. {
  154. title: t('可用性'),
  155. dataIndex: 'available',
  156. render: (text, record, index) => {
  157. return renderAvailable(record.enable_groups.includes(selectedGroup));
  158. },
  159. sorter: (a, b) => {
  160. const aAvailable = a.enable_groups.includes(selectedGroup);
  161. const bAvailable = b.enable_groups.includes(selectedGroup);
  162. return Number(aAvailable) - Number(bAvailable);
  163. },
  164. defaultSortOrder: 'descend',
  165. },
  166. {
  167. title: t('可用端点类型'),
  168. dataIndex: 'supported_endpoint_types',
  169. render: (text, record, index) => {
  170. return renderSupportedEndpoints(text);
  171. },
  172. },
  173. {
  174. title: t('模型名称'),
  175. dataIndex: 'model_name',
  176. render: (text, record, index) => {
  177. return renderModelTag(text, {
  178. onClick: () => {
  179. copyText(text);
  180. }
  181. });
  182. },
  183. onFilter: (value, record) =>
  184. record.model_name.toLowerCase().includes(value.toLowerCase()),
  185. filteredValue,
  186. },
  187. {
  188. title: t('计费类型'),
  189. dataIndex: 'quota_type',
  190. render: (text, record, index) => {
  191. return renderQuotaType(parseInt(text));
  192. },
  193. sorter: (a, b) => a.quota_type - b.quota_type,
  194. },
  195. {
  196. title: t('可用分组'),
  197. dataIndex: 'enable_groups',
  198. render: (text, record, index) => {
  199. return (
  200. <Space wrap>
  201. {text.map((group) => {
  202. if (usableGroup[group]) {
  203. if (group === selectedGroup) {
  204. return (
  205. <Tag color='blue' shape='circle' prefixIcon={<IconVerify />}>
  206. {group}
  207. </Tag>
  208. );
  209. } else {
  210. return (
  211. <Tag
  212. color='blue'
  213. shape='circle'
  214. onClick={() => {
  215. setSelectedGroup(group);
  216. showInfo(
  217. t('当前查看的分组为:{{group}},倍率为:{{ratio}}', {
  218. group: group,
  219. ratio: groupRatio[group],
  220. }),
  221. );
  222. }}
  223. className="cursor-pointer hover:opacity-80 transition-opacity"
  224. >
  225. {group}
  226. </Tag>
  227. );
  228. }
  229. }
  230. })}
  231. </Space>
  232. );
  233. },
  234. },
  235. {
  236. title: () => (
  237. <div className="flex items-center space-x-1">
  238. <span>{t('倍率')}</span>
  239. <Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
  240. <IconHelpCircle
  241. className="text-blue-500 cursor-pointer"
  242. onClick={() => {
  243. setModalImageUrl('/ratio.png');
  244. setIsModalOpenurl(true);
  245. }}
  246. />
  247. </Tooltip>
  248. </div>
  249. ),
  250. dataIndex: 'model_ratio',
  251. render: (text, record, index) => {
  252. let content = text;
  253. let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
  254. content = (
  255. <div className="space-y-1">
  256. <div className="text-gray-700">
  257. {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
  258. </div>
  259. <div className="text-gray-700">
  260. {t('补全倍率')}:
  261. {record.quota_type === 0 ? completionRatio : t('无')}
  262. </div>
  263. <div className="text-gray-700">
  264. {t('分组倍率')}:{groupRatio[selectedGroup]}
  265. </div>
  266. </div>
  267. );
  268. return content;
  269. },
  270. },
  271. {
  272. title: (
  273. <div className="flex items-center space-x-2">
  274. <span>{t('模型价格')}</span>
  275. {/* 计费单位切换 */}
  276. <Switch
  277. checked={tokenUnit === 'K'}
  278. onChange={(checked) => setTokenUnit(checked ? 'K' : 'M')}
  279. checkedText="K"
  280. uncheckedText="M"
  281. />
  282. </div>
  283. ),
  284. dataIndex: 'model_price',
  285. render: (text, record, index) => {
  286. let content = text;
  287. if (record.quota_type === 0) {
  288. let inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup];
  289. let completionRatioPriceUSD =
  290. record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup];
  291. const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
  292. const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
  293. let displayInput = displayPrice(inputRatioPriceUSD);
  294. let displayCompletion = displayPrice(completionRatioPriceUSD);
  295. const divisor = unitDivisor;
  296. const numInput = parseFloat(displayInput.replace(/[^0-9.]/g, '')) / divisor;
  297. const numCompletion = parseFloat(displayCompletion.replace(/[^0-9.]/g, '')) / divisor;
  298. displayInput = `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(3)}`;
  299. displayCompletion = `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(3)}`;
  300. content = (
  301. <div className="space-y-1">
  302. <div className="text-gray-700">
  303. {t('提示')} {displayInput} / 1{unitLabel} tokens
  304. </div>
  305. <div className="text-gray-700">
  306. {t('补全')} {displayCompletion} / 1{unitLabel} tokens
  307. </div>
  308. </div>
  309. );
  310. } else {
  311. let priceUSD = parseFloat(text) * groupRatio[selectedGroup];
  312. let displayVal = displayPrice(priceUSD);
  313. content = (
  314. <div className="text-gray-700">
  315. {t('模型价格')}:{displayVal}
  316. </div>
  317. );
  318. }
  319. return content;
  320. },
  321. },
  322. ];
  323. const [models, setModels] = useState([]);
  324. const [loading, setLoading] = useState(true);
  325. const [userState] = useContext(UserContext);
  326. const [groupRatio, setGroupRatio] = useState({});
  327. const [usableGroup, setUsableGroup] = useState({});
  328. const setModelsFormat = (models, groupRatio) => {
  329. for (let i = 0; i < models.length; i++) {
  330. models[i].key = models[i].model_name;
  331. models[i].group_ratio = groupRatio[models[i].model_name];
  332. }
  333. models.sort((a, b) => {
  334. return a.quota_type - b.quota_type;
  335. });
  336. models.sort((a, b) => {
  337. if (a.model_name.startsWith('gpt') && !b.model_name.startsWith('gpt')) {
  338. return -1;
  339. } else if (
  340. !a.model_name.startsWith('gpt') &&
  341. b.model_name.startsWith('gpt')
  342. ) {
  343. return 1;
  344. } else {
  345. return a.model_name.localeCompare(b.model_name);
  346. }
  347. });
  348. setModels(models);
  349. };
  350. const loadPricing = async () => {
  351. setLoading(true);
  352. let url = '/api/pricing';
  353. const res = await API.get(url);
  354. const { success, message, data, group_ratio, usable_group } = res.data;
  355. if (success) {
  356. setGroupRatio(group_ratio);
  357. setUsableGroup(usable_group);
  358. setSelectedGroup(userState.user ? userState.user.group : 'default');
  359. setModelsFormat(data, group_ratio);
  360. } else {
  361. showError(message);
  362. }
  363. setLoading(false);
  364. };
  365. const refresh = async () => {
  366. await loadPricing();
  367. };
  368. const copyText = async (text) => {
  369. if (await copy(text)) {
  370. showSuccess(t('已复制:') + text);
  371. } else {
  372. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  373. }
  374. };
  375. useEffect(() => {
  376. refresh().then();
  377. }, []);
  378. const modelCategories = getModelCategories(t);
  379. const categoryCounts = useMemo(() => {
  380. const counts = {};
  381. if (models.length > 0) {
  382. counts['all'] = models.length;
  383. Object.entries(modelCategories).forEach(([key, category]) => {
  384. if (key !== 'all') {
  385. counts[key] = models.filter(model => category.filter(model)).length;
  386. }
  387. });
  388. }
  389. return counts;
  390. }, [models, modelCategories]);
  391. const availableCategories = useMemo(() => {
  392. if (!models.length) return ['all'];
  393. return Object.entries(modelCategories).filter(([key, category]) => {
  394. if (key === 'all') return true;
  395. return models.some(model => category.filter(model));
  396. }).map(([key]) => key);
  397. }, [models]);
  398. const renderTabs = () => {
  399. return (
  400. <Tabs
  401. activeKey={activeKey}
  402. type="card"
  403. collapsible
  404. onChange={key => setActiveKey(key)}
  405. className="mt-2"
  406. >
  407. {Object.entries(modelCategories)
  408. .filter(([key]) => availableCategories.includes(key))
  409. .map(([key, category]) => {
  410. const modelCount = categoryCounts[key] || 0;
  411. return (
  412. <TabPane
  413. tab={
  414. <span className="flex items-center gap-2">
  415. {category.icon && <span className="w-4 h-4">{category.icon}</span>}
  416. {category.label}
  417. <Tag
  418. color={activeKey === key ? 'red' : 'grey'}
  419. shape='circle'
  420. >
  421. {modelCount}
  422. </Tag>
  423. </span>
  424. }
  425. itemKey={key}
  426. key={key}
  427. />
  428. );
  429. })}
  430. </Tabs>
  431. );
  432. };
  433. const filteredModels = useMemo(() => {
  434. let result = models;
  435. if (activeKey !== 'all') {
  436. result = result.filter(model => modelCategories[activeKey].filter(model));
  437. }
  438. if (filteredValue.length > 0) {
  439. const searchTerm = filteredValue[0].toLowerCase();
  440. result = result.filter(model =>
  441. model.model_name.toLowerCase().includes(searchTerm)
  442. );
  443. }
  444. return result;
  445. }, [activeKey, models, filteredValue]);
  446. const SearchAndActions = useMemo(() => (
  447. <Card className="!rounded-xl mb-6" bordered={false}>
  448. <div className="flex flex-wrap items-center gap-4">
  449. <div className="flex-1 min-w-[200px]">
  450. <Input
  451. prefix={<IconSearch />}
  452. placeholder={t('模糊搜索模型名称')}
  453. onCompositionStart={handleCompositionStart}
  454. onCompositionEnd={handleCompositionEnd}
  455. onChange={handleChange}
  456. showClear
  457. />
  458. </div>
  459. <Button
  460. theme='light'
  461. type='primary'
  462. icon={<IconCopy />}
  463. onClick={() => copyText(selectedRowKeys)}
  464. disabled={selectedRowKeys.length === 0}
  465. className="!bg-blue-500 hover:!bg-blue-600 text-white"
  466. >
  467. {t('复制选中模型')}
  468. </Button>
  469. {/* 充值价格显示开关 */}
  470. <Space align="center">
  471. <span>{t('以充值价格显示')}</span>
  472. <Switch
  473. checked={showWithRecharge}
  474. onChange={setShowWithRecharge}
  475. size="small"
  476. />
  477. {showWithRecharge && (
  478. <Select
  479. value={currency}
  480. onChange={setCurrency}
  481. size="small"
  482. style={{ width: 100 }}
  483. >
  484. <Select.Option value="USD">USD ($)</Select.Option>
  485. <Select.Option value="CNY">CNY (¥)</Select.Option>
  486. </Select>
  487. )}
  488. </Space>
  489. </div>
  490. </Card>
  491. ), [selectedRowKeys, t, showWithRecharge, currency]);
  492. const ModelTable = useMemo(() => (
  493. <Card className="!rounded-xl overflow-hidden" bordered={false}>
  494. <Table
  495. columns={columns}
  496. dataSource={filteredModels}
  497. loading={loading}
  498. rowSelection={rowSelection}
  499. className="custom-table"
  500. empty={
  501. <Empty
  502. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  503. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  504. description={t('搜索无结果')}
  505. style={{ padding: 30 }}
  506. />
  507. }
  508. pagination={{
  509. defaultPageSize: 10,
  510. pageSize: pageSize,
  511. showSizeChanger: true,
  512. pageSizeOptions: [10, 20, 50, 100],
  513. onPageSizeChange: (size) => setPageSize(size),
  514. }}
  515. />
  516. </Card>
  517. ), [filteredModels, loading, columns, rowSelection, pageSize, t]);
  518. return (
  519. <div className="bg-gray-50">
  520. <Layout>
  521. <Layout.Content>
  522. <div className="flex justify-center">
  523. <div className="w-full">
  524. {/* 主卡片容器 */}
  525. <Card bordered={false} className="!rounded-2xl shadow-lg border-0">
  526. {/* 顶部状态卡片 */}
  527. <Card
  528. className="!rounded-2xl !border-0 !shadow-md overflow-hidden mb-6"
  529. style={{
  530. background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 25%, #a855f7 50%, #c084fc 75%, #d8b4fe 100%)',
  531. position: 'relative'
  532. }}
  533. bodyStyle={{ padding: 0 }}
  534. >
  535. <div className="relative p-6 sm:p-8" style={{ color: 'white' }}>
  536. <div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 lg:gap-6">
  537. <div className="flex items-start">
  538. <div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-white/10 flex items-center justify-center mr-3 sm:mr-4">
  539. <IconLayers size="extra-large" className="text-white" />
  540. </div>
  541. <div className="flex-1 min-w-0">
  542. <div className="text-base sm:text-lg font-semibold mb-1 sm:mb-2">
  543. {t('模型定价')}
  544. </div>
  545. <div className="text-sm text-white/80">
  546. {userState.user ? (
  547. <div className="flex items-center">
  548. <IconVerify className="mr-1.5 flex-shrink-0" size="small" />
  549. <span className="truncate">
  550. {t('当前分组')}: {userState.user.group},{t('倍率')}: {groupRatio[userState.user.group]}
  551. </span>
  552. </div>
  553. ) : (
  554. <div className="flex items-center">
  555. <AlertCircle size={14} className="mr-1.5 flex-shrink-0" />
  556. <span className="truncate">
  557. {t('未登录,使用默认分组倍率:')}{groupRatio['default']}
  558. </span>
  559. </div>
  560. )}
  561. </div>
  562. </div>
  563. </div>
  564. <div className="grid grid-cols-3 gap-2 sm:gap-3 mt-2 lg:mt-0">
  565. <div
  566. className="text-center px-2 py-2 sm:px-3 sm:py-2.5 bg-white/10 rounded-lg backdrop-blur-sm hover:bg-white/20 transition-colors duration-200"
  567. style={{ backdropFilter: 'blur(10px)' }}
  568. >
  569. <div className="text-xs text-white/70 mb-0.5">{t('分组倍率')}</div>
  570. <div className="text-sm sm:text-base font-semibold">{groupRatio[selectedGroup] || '1.0'}x</div>
  571. </div>
  572. <div
  573. className="text-center px-2 py-2 sm:px-3 sm:py-2.5 bg-white/10 rounded-lg backdrop-blur-sm hover:bg-white/20 transition-colors duration-200"
  574. style={{ backdropFilter: 'blur(10px)' }}
  575. >
  576. <div className="text-xs text-white/70 mb-0.5">{t('可用模型')}</div>
  577. <div className="text-sm sm:text-base font-semibold">
  578. {models.filter(m => m.enable_groups.includes(selectedGroup)).length}
  579. </div>
  580. </div>
  581. <div
  582. className="text-center px-2 py-2 sm:px-3 sm:py-2.5 bg-white/10 rounded-lg backdrop-blur-sm hover:bg-white/20 transition-colors duration-200"
  583. style={{ backdropFilter: 'blur(10px)' }}
  584. >
  585. <div className="text-xs text-white/70 mb-0.5">{t('计费类型')}</div>
  586. <div className="text-sm sm:text-base font-semibold">2</div>
  587. </div>
  588. </div>
  589. </div>
  590. {/* 计费说明 */}
  591. <div className="mt-4 sm:mt-5">
  592. <div className="flex items-start">
  593. <div
  594. className="w-full flex items-start space-x-2 px-3 py-2 sm:px-4 sm:py-2.5 rounded-lg text-xs sm:text-sm"
  595. style={{
  596. backgroundColor: 'rgba(255, 255, 255, 0.2)',
  597. color: 'white',
  598. backdropFilter: 'blur(10px)'
  599. }}
  600. >
  601. <IconInfoCircle className="flex-shrink-0 mt-0.5" size="small" />
  602. <span>
  603. {t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}
  604. </span>
  605. </div>
  606. </div>
  607. </div>
  608. <div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-yellow-400 via-orange-400 to-red-400" style={{ opacity: 0.6 }}></div>
  609. </div>
  610. </Card>
  611. {/* 模型分类 Tabs */}
  612. <div className="mb-6">
  613. {renderTabs()}
  614. {/* 搜索和表格区域 */}
  615. {SearchAndActions}
  616. {ModelTable}
  617. </div>
  618. {/* 倍率说明图预览 */}
  619. <ImagePreview
  620. src={modalImageUrl}
  621. visible={isModalOpenurl}
  622. onVisibleChange={(visible) => setIsModalOpenurl(visible)}
  623. />
  624. </Card>
  625. </div>
  626. </div>
  627. </Layout.Content>
  628. </Layout>
  629. </div>
  630. );
  631. };
  632. export default ModelPricing;