PricingTableColumns.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  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 from 'react';
  16. import { Tag, Space, Tooltip, Switch } from '@douyinfe/semi-ui';
  17. import { IconVerify, IconHelpCircle } from '@douyinfe/semi-icons';
  18. import { Popover } from '@douyinfe/semi-ui';
  19. import { renderModelTag, stringToColor } from '../../../helpers';
  20. function renderQuotaType(type, t) {
  21. switch (type) {
  22. case 1:
  23. return (
  24. <Tag color='teal' shape='circle'>
  25. {t('按次计费')}
  26. </Tag>
  27. );
  28. case 0:
  29. return (
  30. <Tag color='violet' shape='circle'>
  31. {t('按量计费')}
  32. </Tag>
  33. );
  34. default:
  35. return t('未知');
  36. }
  37. }
  38. function renderAvailable(available, t) {
  39. return available ? (
  40. <Popover
  41. content={
  42. <div style={{ padding: 8 }}>{t('您的分组可以使用该模型')}</div>
  43. }
  44. position='top'
  45. key={available}
  46. className="bg-green-50"
  47. >
  48. <IconVerify style={{ color: 'rgb(22 163 74)' }} size='large' />
  49. </Popover>
  50. ) : null;
  51. }
  52. function renderSupportedEndpoints(endpoints) {
  53. if (!endpoints || endpoints.length === 0) {
  54. return null;
  55. }
  56. return (
  57. <Space wrap>
  58. {endpoints.map((endpoint, idx) => (
  59. <Tag
  60. key={endpoint}
  61. color={stringToColor(endpoint)}
  62. shape='circle'
  63. >
  64. {endpoint}
  65. </Tag>
  66. ))}
  67. </Space>
  68. );
  69. }
  70. export const getPricingTableColumns = ({
  71. t,
  72. selectedGroup,
  73. usableGroup,
  74. groupRatio,
  75. copyText,
  76. setModalImageUrl,
  77. setIsModalOpenurl,
  78. currency,
  79. showWithRecharge,
  80. tokenUnit,
  81. setTokenUnit,
  82. displayPrice,
  83. handleGroupClick,
  84. showRatio,
  85. }) => {
  86. const baseColumns = [
  87. {
  88. title: t('可用性'),
  89. dataIndex: 'available',
  90. render: (text, record, index) => {
  91. return renderAvailable(record.enable_groups.includes(selectedGroup), t);
  92. },
  93. sorter: (a, b) => {
  94. const aAvailable = a.enable_groups.includes(selectedGroup);
  95. const bAvailable = b.enable_groups.includes(selectedGroup);
  96. return Number(aAvailable) - Number(bAvailable);
  97. },
  98. defaultSortOrder: 'descend',
  99. },
  100. {
  101. title: t('可用端点类型'),
  102. dataIndex: 'supported_endpoint_types',
  103. render: (text, record, index) => {
  104. return renderSupportedEndpoints(text);
  105. },
  106. },
  107. {
  108. title: t('模型名称'),
  109. dataIndex: 'model_name',
  110. render: (text, record, index) => {
  111. return renderModelTag(text, {
  112. onClick: () => {
  113. copyText(text);
  114. }
  115. });
  116. },
  117. onFilter: (value, record) =>
  118. record.model_name.toLowerCase().includes(value.toLowerCase()),
  119. },
  120. {
  121. title: t('计费类型'),
  122. dataIndex: 'quota_type',
  123. render: (text, record, index) => {
  124. return renderQuotaType(parseInt(text), t);
  125. },
  126. sorter: (a, b) => a.quota_type - b.quota_type,
  127. },
  128. {
  129. title: t('可用分组'),
  130. dataIndex: 'enable_groups',
  131. render: (text, record, index) => {
  132. return (
  133. <Space wrap>
  134. {text.map((group) => {
  135. if (usableGroup[group]) {
  136. if (group === selectedGroup) {
  137. return (
  138. <Tag key={group} color='blue' shape='circle' prefixIcon={<IconVerify />}>
  139. {group}
  140. </Tag>
  141. );
  142. } else {
  143. return (
  144. <Tag
  145. key={group}
  146. color='blue'
  147. shape='circle'
  148. onClick={() => handleGroupClick(group)}
  149. className="cursor-pointer hover:opacity-80 transition-opacity"
  150. >
  151. {group}
  152. </Tag>
  153. );
  154. }
  155. }
  156. })}
  157. </Space>
  158. );
  159. },
  160. },
  161. ];
  162. // 倍率列 - 只有在showRatio为true时才包含
  163. const ratioColumn = {
  164. title: () => (
  165. <div className="flex items-center space-x-1">
  166. <span>{t('倍率')}</span>
  167. <Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
  168. <IconHelpCircle
  169. className="text-blue-500 cursor-pointer"
  170. onClick={() => {
  171. setModalImageUrl('/ratio.png');
  172. setIsModalOpenurl(true);
  173. }}
  174. />
  175. </Tooltip>
  176. </div>
  177. ),
  178. dataIndex: 'model_ratio',
  179. render: (text, record, index) => {
  180. let content = text;
  181. let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
  182. content = (
  183. <div className="space-y-1">
  184. <div className="text-gray-700">
  185. {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
  186. </div>
  187. <div className="text-gray-700">
  188. {t('补全倍率')}:
  189. {record.quota_type === 0 ? completionRatio : t('无')}
  190. </div>
  191. <div className="text-gray-700">
  192. {t('分组倍率')}:{groupRatio[selectedGroup]}
  193. </div>
  194. </div>
  195. );
  196. return content;
  197. },
  198. };
  199. // 价格列
  200. const priceColumn = {
  201. title: (
  202. <div className="flex items-center space-x-2">
  203. <span>{t('模型价格')}</span>
  204. {/* 计费单位切换 */}
  205. <Switch
  206. checked={tokenUnit === 'K'}
  207. onChange={(checked) => setTokenUnit(checked ? 'K' : 'M')}
  208. checkedText="K"
  209. uncheckedText="M"
  210. />
  211. </div>
  212. ),
  213. dataIndex: 'model_price',
  214. render: (text, record, index) => {
  215. let content = text;
  216. if (record.quota_type === 0) {
  217. let inputRatioPriceUSD = record.model_ratio * 2 * groupRatio[selectedGroup];
  218. let completionRatioPriceUSD =
  219. record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup];
  220. const unitDivisor = tokenUnit === 'K' ? 1000 : 1;
  221. const unitLabel = tokenUnit === 'K' ? 'K' : 'M';
  222. let displayInput = displayPrice(inputRatioPriceUSD);
  223. let displayCompletion = displayPrice(completionRatioPriceUSD);
  224. const divisor = unitDivisor;
  225. const numInput = parseFloat(displayInput.replace(/[^0-9.]/g, '')) / divisor;
  226. const numCompletion = parseFloat(displayCompletion.replace(/[^0-9.]/g, '')) / divisor;
  227. displayInput = `${currency === 'CNY' ? '¥' : '$'}${numInput.toFixed(3)}`;
  228. displayCompletion = `${currency === 'CNY' ? '¥' : '$'}${numCompletion.toFixed(3)}`;
  229. content = (
  230. <div className="space-y-1">
  231. <div className="text-gray-700">
  232. {t('提示')} {displayInput} / 1{unitLabel} tokens
  233. </div>
  234. <div className="text-gray-700">
  235. {t('补全')} {displayCompletion} / 1{unitLabel} tokens
  236. </div>
  237. </div>
  238. );
  239. } else {
  240. let priceUSD = parseFloat(text) * groupRatio[selectedGroup];
  241. let displayVal = displayPrice(priceUSD);
  242. content = (
  243. <div className="text-gray-700">
  244. {t('模型价格')}:{displayVal}
  245. </div>
  246. );
  247. }
  248. return content;
  249. },
  250. };
  251. // 根据showRatio决定是否包含倍率列
  252. const columns = [...baseColumns];
  253. if (showRatio) {
  254. columns.push(ratioColumn);
  255. }
  256. columns.push(priceColumn);
  257. return columns;
  258. };