render.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129
  1. import i18next from 'i18next';
  2. import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
  3. import { copy, isMobile, showSuccess } from './utils.js';
  4. export function renderText(text, limit) {
  5. if (text.length > limit) {
  6. return text.slice(0, limit - 3) + '...';
  7. }
  8. return text;
  9. }
  10. /**
  11. * Render group tags based on the input group string
  12. * @param {string} group - The input group string
  13. * @returns {JSX.Element} - The rendered group tags
  14. */
  15. export function renderGroup(group) {
  16. if (group === '') {
  17. return (
  18. <Tag size='large' key='default' color='orange'>
  19. {i18next.t('用户分组')}
  20. </Tag>
  21. );
  22. }
  23. const tagColors = {
  24. vip: 'yellow',
  25. pro: 'yellow',
  26. svip: 'red',
  27. premium: 'red',
  28. };
  29. const groups = group.split(',').sort();
  30. return (
  31. <span key={group}>
  32. {groups.map((group) => (
  33. <Tag
  34. size='large'
  35. color={tagColors[group] || stringToColor(group)}
  36. key={group}
  37. onClick={async (event) => {
  38. event.stopPropagation();
  39. if (await copy(group)) {
  40. showSuccess(i18next.t('已复制:') + group);
  41. } else {
  42. Modal.error({
  43. title: t('无法复制到剪贴板,请手动复制'),
  44. content: group,
  45. });
  46. }
  47. }}
  48. >
  49. {group}
  50. </Tag>
  51. ))}
  52. </span>
  53. );
  54. }
  55. export function renderRatio(ratio) {
  56. let color = 'green';
  57. if (ratio > 5) {
  58. color = 'red';
  59. } else if (ratio > 3) {
  60. color = 'orange';
  61. } else if (ratio > 1) {
  62. color = 'blue';
  63. }
  64. return (
  65. <Tag color={color}>
  66. {ratio}x {i18next.t('倍率')}
  67. </Tag>
  68. );
  69. }
  70. const measureTextWidth = (
  71. text,
  72. style = {
  73. fontSize: '14px',
  74. fontFamily:
  75. '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
  76. },
  77. containerWidth,
  78. ) => {
  79. const span = document.createElement('span');
  80. span.style.visibility = 'hidden';
  81. span.style.position = 'absolute';
  82. span.style.whiteSpace = 'nowrap';
  83. span.style.fontSize = style.fontSize;
  84. span.style.fontFamily = style.fontFamily;
  85. span.textContent = text;
  86. document.body.appendChild(span);
  87. const width = span.offsetWidth;
  88. document.body.removeChild(span);
  89. return width;
  90. };
  91. export function truncateText(text, maxWidth = 200) {
  92. if (!isMobile()) {
  93. return text;
  94. }
  95. if (!text) return text;
  96. try {
  97. // Handle percentage-based maxWidth
  98. let actualMaxWidth = maxWidth;
  99. if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) {
  100. const percentage = parseFloat(maxWidth) / 100;
  101. // Use window width as fallback container width
  102. actualMaxWidth = window.innerWidth * percentage;
  103. }
  104. const width = measureTextWidth(text);
  105. if (width <= actualMaxWidth) return text;
  106. let left = 0;
  107. let right = text.length;
  108. let result = text;
  109. while (left <= right) {
  110. const mid = Math.floor((left + right) / 2);
  111. const truncated = text.slice(0, mid) + '...';
  112. const currentWidth = measureTextWidth(truncated);
  113. if (currentWidth <= actualMaxWidth) {
  114. result = truncated;
  115. left = mid + 1;
  116. } else {
  117. right = mid - 1;
  118. }
  119. }
  120. return result;
  121. } catch (error) {
  122. console.warn(
  123. 'Text measurement failed, falling back to character count',
  124. error,
  125. );
  126. if (text.length > 20) {
  127. return text.slice(0, 17) + '...';
  128. }
  129. return text;
  130. }
  131. }
  132. export const renderGroupOption = (item) => {
  133. const {
  134. disabled,
  135. selected,
  136. label,
  137. value,
  138. focused,
  139. className,
  140. style,
  141. onMouseEnter,
  142. onClick,
  143. empty,
  144. emptyContent,
  145. ...rest
  146. } = item;
  147. const baseStyle = {
  148. display: 'flex',
  149. justifyContent: 'space-between',
  150. alignItems: 'center',
  151. padding: '8px 16px',
  152. cursor: disabled ? 'not-allowed' : 'pointer',
  153. backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
  154. opacity: disabled ? 0.5 : 1,
  155. ...(selected && {
  156. backgroundColor: 'var(--semi-color-primary-light-default)',
  157. }),
  158. '&:hover': {
  159. backgroundColor: !disabled && 'var(--semi-color-fill-1)',
  160. },
  161. };
  162. const handleClick = () => {
  163. if (!disabled && onClick) {
  164. onClick();
  165. }
  166. };
  167. const handleMouseEnter = (e) => {
  168. if (!disabled && onMouseEnter) {
  169. onMouseEnter(e);
  170. }
  171. };
  172. return (
  173. <div
  174. style={baseStyle}
  175. onClick={handleClick}
  176. onMouseEnter={handleMouseEnter}
  177. >
  178. <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
  179. <Typography.Text strong type={disabled ? 'tertiary' : undefined}>
  180. {value}
  181. </Typography.Text>
  182. <Typography.Text type='secondary' size='small'>
  183. {label}
  184. </Typography.Text>
  185. </div>
  186. {item.ratio && renderRatio(item.ratio)}
  187. </div>
  188. );
  189. };
  190. export function renderNumber(num) {
  191. if (num >= 1000000000) {
  192. return (num / 1000000000).toFixed(1) + 'B';
  193. } else if (num >= 1000000) {
  194. return (num / 1000000).toFixed(1) + 'M';
  195. } else if (num >= 10000) {
  196. return (num / 1000).toFixed(1) + 'k';
  197. } else {
  198. return num;
  199. }
  200. }
  201. export function renderQuotaNumberWithDigit(num, digits = 2) {
  202. if (typeof num !== 'number' || isNaN(num)) {
  203. return 0;
  204. }
  205. let displayInCurrency = localStorage.getItem('display_in_currency');
  206. num = num.toFixed(digits);
  207. if (displayInCurrency) {
  208. return '$' + num;
  209. }
  210. return num;
  211. }
  212. export function renderNumberWithPoint(num) {
  213. if (num === undefined) return '';
  214. num = num.toFixed(2);
  215. if (num >= 100000) {
  216. // Convert number to string to manipulate it
  217. let numStr = num.toString();
  218. // Find the position of the decimal point
  219. let decimalPointIndex = numStr.indexOf('.');
  220. let wholePart = numStr;
  221. let decimalPart = '';
  222. // If there is a decimal point, split the number into whole and decimal parts
  223. if (decimalPointIndex !== -1) {
  224. wholePart = numStr.slice(0, decimalPointIndex);
  225. decimalPart = numStr.slice(decimalPointIndex);
  226. }
  227. // Take the first two and last two digits of the whole number part
  228. let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
  229. // Return the formatted number
  230. return shortenedWholePart + decimalPart;
  231. }
  232. // If the number is less than 100,000, return it unmodified
  233. return num;
  234. }
  235. export function getQuotaPerUnit() {
  236. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  237. quotaPerUnit = parseFloat(quotaPerUnit);
  238. return quotaPerUnit;
  239. }
  240. export function renderUnitWithQuota(quota) {
  241. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  242. quotaPerUnit = parseFloat(quotaPerUnit);
  243. quota = parseFloat(quota);
  244. return quotaPerUnit * quota;
  245. }
  246. export function getQuotaWithUnit(quota, digits = 6) {
  247. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  248. quotaPerUnit = parseFloat(quotaPerUnit);
  249. return (quota / quotaPerUnit).toFixed(digits);
  250. }
  251. export function renderQuotaWithAmount(amount) {
  252. let displayInCurrency = localStorage.getItem('display_in_currency');
  253. displayInCurrency = displayInCurrency === 'true';
  254. if (displayInCurrency) {
  255. return '$' + amount;
  256. } else {
  257. return renderUnitWithQuota(amount);
  258. }
  259. }
  260. export function renderQuota(quota, digits = 2) {
  261. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  262. let displayInCurrency = localStorage.getItem('display_in_currency');
  263. quotaPerUnit = parseFloat(quotaPerUnit);
  264. displayInCurrency = displayInCurrency === 'true';
  265. if (displayInCurrency) {
  266. return '$' + (quota / quotaPerUnit).toFixed(digits);
  267. }
  268. return renderNumber(quota);
  269. }
  270. export function renderModelPrice(
  271. inputTokens,
  272. completionTokens,
  273. modelRatio,
  274. modelPrice = -1,
  275. completionRatio,
  276. groupRatio,
  277. cacheTokens = 0,
  278. cacheRatio = 1.0,
  279. image = false,
  280. imageRatio = 1.0,
  281. imageOutputTokens = 0,
  282. webSearch = false,
  283. webSearchCallCount = 0,
  284. webSearchPrice = 0,
  285. fileSearch = false,
  286. fileSearchCallCount = 0,
  287. fileSearchPrice = 0,
  288. ) {
  289. if (modelPrice !== -1) {
  290. return i18next.t(
  291. '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
  292. {
  293. price: modelPrice,
  294. ratio: groupRatio,
  295. total: modelPrice * groupRatio,
  296. },
  297. );
  298. } else {
  299. if (completionRatio === undefined) {
  300. completionRatio = 0;
  301. }
  302. let inputRatioPrice = modelRatio * 2.0;
  303. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  304. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  305. let imageRatioPrice = modelRatio * 2.0 * imageRatio;
  306. // Calculate effective input tokens (non-cached + cached with ratio applied)
  307. let effectiveInputTokens =
  308. inputTokens - cacheTokens + cacheTokens * cacheRatio;
  309. // Handle image tokens if present
  310. if (image && imageOutputTokens > 0) {
  311. effectiveInputTokens =
  312. inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
  313. }
  314. let price =
  315. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  316. (completionTokens / 1000000) * completionRatioPrice * groupRatio +
  317. (webSearchCallCount / 1000) * webSearchPrice * groupRatio +
  318. (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
  319. return (
  320. <>
  321. <article>
  322. <p>
  323. {i18next.t('输入价格:${{price}} / 1M tokens', {
  324. price: inputRatioPrice,
  325. })}
  326. </p>
  327. <p>
  328. {i18next.t(
  329. '输出价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
  330. {
  331. price: inputRatioPrice,
  332. total: completionRatioPrice,
  333. completionRatio: completionRatio,
  334. },
  335. )}
  336. </p>
  337. {cacheTokens > 0 && (
  338. <p>
  339. {i18next.t(
  340. '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  341. {
  342. price: inputRatioPrice,
  343. total: inputRatioPrice * cacheRatio,
  344. cacheRatio: cacheRatio,
  345. },
  346. )}
  347. </p>
  348. )}
  349. {image && imageOutputTokens > 0 && (
  350. <p>
  351. {i18next.t(
  352. '图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})',
  353. {
  354. price: imageRatioPrice,
  355. ratio: groupRatio,
  356. total: imageRatioPrice * groupRatio,
  357. imageRatio: imageRatio,
  358. },
  359. )}
  360. </p>
  361. )}
  362. {webSearch && webSearchCallCount > 0 && (
  363. <p>
  364. {i18next.t('Web搜索价格:${{price}} / 1K 次', {
  365. price: webSearchPrice,
  366. })}
  367. </p>
  368. )}
  369. {fileSearch && fileSearchCallCount > 0 && (
  370. <p>
  371. {i18next.t('文件搜索价格:${{price}} / 1K 次', {
  372. price: fileSearchPrice,
  373. })}
  374. </p>
  375. )}
  376. <p></p>
  377. <p>
  378. {cacheTokens > 0 && !image && !webSearch && !fileSearch
  379. ? i18next.t(
  380. '输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  381. {
  382. nonCacheInput: inputTokens - cacheTokens,
  383. cacheInput: cacheTokens,
  384. cachePrice: inputRatioPrice * cacheRatio,
  385. price: inputRatioPrice,
  386. completion: completionTokens,
  387. compPrice: completionRatioPrice,
  388. ratio: groupRatio,
  389. total: price.toFixed(6),
  390. },
  391. )
  392. : image && imageOutputTokens > 0 && !webSearch && !fileSearch
  393. ? i18next.t(
  394. '输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  395. {
  396. nonImageInput: inputTokens - imageOutputTokens,
  397. imageInput: imageOutputTokens,
  398. imageRatio: imageRatio,
  399. price: inputRatioPrice,
  400. completion: completionTokens,
  401. compPrice: completionRatioPrice,
  402. ratio: groupRatio,
  403. total: price.toFixed(6),
  404. },
  405. )
  406. : webSearch && webSearchCallCount > 0 && !image && !fileSearch
  407. ? i18next.t(
  408. '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}} = ${{total}}',
  409. {
  410. input: inputTokens,
  411. price: inputRatioPrice,
  412. completion: completionTokens,
  413. compPrice: completionRatioPrice,
  414. ratio: groupRatio,
  415. webSearchCallCount,
  416. webSearchPrice,
  417. total: price.toFixed(6),
  418. },
  419. )
  420. : fileSearch &&
  421. fileSearchCallCount > 0 &&
  422. !image &&
  423. !webSearch
  424. ? i18next.t(
  425. '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
  426. {
  427. input: inputTokens,
  428. price: inputRatioPrice,
  429. completion: completionTokens,
  430. compPrice: completionRatioPrice,
  431. ratio: groupRatio,
  432. fileSearchCallCount,
  433. fileSearchPrice,
  434. total: price.toFixed(6),
  435. },
  436. )
  437. : webSearch &&
  438. webSearchCallCount > 0 &&
  439. fileSearch &&
  440. fileSearchCallCount > 0 &&
  441. !image
  442. ? i18next.t(
  443. '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} + Web搜索 {{webSearchCallCount}}次 / 1K 次 * ${{webSearchPrice}} * {{ratio}}+ 文件搜索 {{fileSearchCallCount}}次 / 1K 次 * ${{fileSearchPrice}} * {{ratio}}= ${{total}}',
  444. {
  445. input: inputTokens,
  446. price: inputRatioPrice,
  447. completion: completionTokens,
  448. compPrice: completionRatioPrice,
  449. ratio: groupRatio,
  450. webSearchCallCount,
  451. webSearchPrice,
  452. fileSearchCallCount,
  453. fileSearchPrice,
  454. total: price.toFixed(6),
  455. },
  456. )
  457. : i18next.t(
  458. '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  459. {
  460. input: inputTokens,
  461. price: inputRatioPrice,
  462. completion: completionTokens,
  463. compPrice: completionRatioPrice,
  464. ratio: groupRatio,
  465. total: price.toFixed(6),
  466. },
  467. )}
  468. </p>
  469. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  470. </article>
  471. </>
  472. );
  473. }
  474. }
  475. export function renderLogContent(
  476. modelRatio,
  477. completionRatio,
  478. modelPrice = -1,
  479. groupRatio,
  480. user_group_ratio,
  481. image = false,
  482. imageRatio = 1.0,
  483. useUserGroupRatio = undefined,
  484. webSearch = false,
  485. webSearchCallCount = 0,
  486. fileSearch = false,
  487. fileSearchCallCount = 0,
  488. ) {
  489. const ratioLabel = useUserGroupRatio
  490. ? i18next.t('专属倍率')
  491. : i18next.t('分组倍率');
  492. const ratio = useUserGroupRatio ? user_group_ratio : groupRatio;
  493. if (modelPrice !== -1) {
  494. return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
  495. price: modelPrice,
  496. ratioType: ratioLabel,
  497. ratio,
  498. });
  499. } else {
  500. if (image) {
  501. return i18next.t(
  502. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}',
  503. {
  504. modelRatio: modelRatio,
  505. completionRatio: completionRatio,
  506. imageRatio: imageRatio,
  507. ratioType: ratioLabel,
  508. ratio,
  509. },
  510. );
  511. } else if (webSearch) {
  512. return i18next.t(
  513. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次',
  514. {
  515. modelRatio: modelRatio,
  516. completionRatio: completionRatio,
  517. ratioType: ratioLabel,
  518. ratio,
  519. webSearchCallCount,
  520. },
  521. );
  522. } else {
  523. return i18next.t(
  524. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}',
  525. {
  526. modelRatio: modelRatio,
  527. completionRatio: completionRatio,
  528. ratioType: ratioLabel,
  529. ratio,
  530. },
  531. );
  532. }
  533. }
  534. }
  535. export function renderModelPriceSimple(
  536. modelRatio,
  537. modelPrice = -1,
  538. groupRatio,
  539. cacheTokens = 0,
  540. cacheRatio = 1.0,
  541. image = false,
  542. imageRatio = 1.0,
  543. ) {
  544. if (modelPrice !== -1) {
  545. return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
  546. price: modelPrice,
  547. ratio: groupRatio,
  548. });
  549. } else {
  550. if (image && cacheTokens !== 0) {
  551. return i18next.t(
  552. '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存倍率: {{cacheRatio}} * 图片输入倍率: {{imageRatio}}',
  553. {
  554. ratio: modelRatio,
  555. ratioType: ratioLabel,
  556. groupRatio: groupRatio,
  557. cacheRatio: cacheRatio,
  558. imageRatio: imageRatio,
  559. },
  560. );
  561. } else if (image) {
  562. return i18next.t(
  563. '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 图片输入倍率: {{imageRatio}}',
  564. {
  565. ratio: modelRatio,
  566. ratioType: ratioLabel,
  567. groupRatio: groupRatio,
  568. imageRatio: imageRatio,
  569. },
  570. );
  571. } else if (cacheTokens !== 0) {
  572. return i18next.t(
  573. '模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}',
  574. {
  575. ratio: modelRatio,
  576. groupRatio: groupRatio,
  577. cacheRatio: cacheRatio,
  578. },
  579. );
  580. } else {
  581. return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
  582. ratio: modelRatio,
  583. groupRatio: groupRatio,
  584. });
  585. }
  586. }
  587. }
  588. export function renderAudioModelPrice(
  589. inputTokens,
  590. completionTokens,
  591. modelRatio,
  592. modelPrice = -1,
  593. completionRatio,
  594. audioInputTokens,
  595. audioCompletionTokens,
  596. audioRatio,
  597. audioCompletionRatio,
  598. groupRatio,
  599. cacheTokens = 0,
  600. cacheRatio = 1.0,
  601. ) {
  602. // 1 ratio = $0.002 / 1K tokens
  603. if (modelPrice !== -1) {
  604. return i18next.t(
  605. '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
  606. {
  607. price: modelPrice,
  608. ratio: groupRatio,
  609. total: modelPrice * groupRatio,
  610. },
  611. );
  612. } else {
  613. if (completionRatio === undefined) {
  614. completionRatio = 0;
  615. }
  616. // try toFixed audioRatio
  617. audioRatio = parseFloat(audioRatio).toFixed(6);
  618. // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
  619. let inputRatioPrice = modelRatio * 2.0;
  620. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  621. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  622. // Calculate effective input tokens (non-cached + cached with ratio applied)
  623. const effectiveInputTokens =
  624. inputTokens - cacheTokens + cacheTokens * cacheRatio;
  625. let textPrice =
  626. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  627. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  628. let audioPrice =
  629. (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
  630. (audioCompletionTokens / 1000000) *
  631. inputRatioPrice *
  632. audioRatio *
  633. audioCompletionRatio *
  634. groupRatio;
  635. let price = textPrice + audioPrice;
  636. return (
  637. <>
  638. <article>
  639. <p>
  640. {i18next.t('提示价格:${{price}} / 1M tokens', {
  641. price: inputRatioPrice,
  642. })}
  643. </p>
  644. <p>
  645. {i18next.t(
  646. '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
  647. {
  648. price: inputRatioPrice,
  649. total: completionRatioPrice,
  650. completionRatio: completionRatio,
  651. },
  652. )}
  653. </p>
  654. {cacheTokens > 0 && (
  655. <p>
  656. {i18next.t(
  657. '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  658. {
  659. price: inputRatioPrice,
  660. total: inputRatioPrice * cacheRatio,
  661. cacheRatio: cacheRatio,
  662. },
  663. )}
  664. </p>
  665. )}
  666. <p>
  667. {i18next.t(
  668. '音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})',
  669. {
  670. price: inputRatioPrice,
  671. total: inputRatioPrice * audioRatio,
  672. audioRatio: audioRatio,
  673. },
  674. )}
  675. </p>
  676. <p>
  677. {i18next.t(
  678. '音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})',
  679. {
  680. price: inputRatioPrice,
  681. total: inputRatioPrice * audioRatio * audioCompletionRatio,
  682. audioRatio: audioRatio,
  683. audioCompRatio: audioCompletionRatio,
  684. },
  685. )}
  686. </p>
  687. <p>
  688. {cacheTokens > 0
  689. ? i18next.t(
  690. '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
  691. {
  692. nonCacheInput: inputTokens - cacheTokens,
  693. cacheInput: cacheTokens,
  694. cachePrice: inputRatioPrice * cacheRatio,
  695. price: inputRatioPrice,
  696. completion: completionTokens,
  697. compPrice: completionRatioPrice,
  698. total: textPrice.toFixed(6),
  699. },
  700. )
  701. : i18next.t(
  702. '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
  703. {
  704. input: inputTokens,
  705. price: inputRatioPrice,
  706. completion: completionTokens,
  707. compPrice: completionRatioPrice,
  708. total: textPrice.toFixed(6),
  709. },
  710. )}
  711. </p>
  712. <p>
  713. {i18next.t(
  714. '音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}',
  715. {
  716. input: audioInputTokens,
  717. completion: audioCompletionTokens,
  718. audioInputPrice: audioRatio * inputRatioPrice,
  719. audioCompPrice:
  720. audioRatio * audioCompletionRatio * inputRatioPrice,
  721. total: audioPrice.toFixed(6),
  722. },
  723. )}
  724. </p>
  725. <p>
  726. {i18next.t(
  727. '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}',
  728. {
  729. total: price.toFixed(6),
  730. textPrice: textPrice.toFixed(6),
  731. audioPrice: audioPrice.toFixed(6),
  732. },
  733. )}
  734. </p>
  735. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  736. </article>
  737. </>
  738. );
  739. }
  740. }
  741. export function renderQuotaWithPrompt(quota, digits) {
  742. let displayInCurrency = localStorage.getItem('display_in_currency');
  743. displayInCurrency = displayInCurrency === 'true';
  744. if (displayInCurrency) {
  745. return (
  746. ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + ''
  747. );
  748. }
  749. return '';
  750. }
  751. const colors = [
  752. 'amber',
  753. 'blue',
  754. 'cyan',
  755. 'green',
  756. 'grey',
  757. 'indigo',
  758. 'light-blue',
  759. 'lime',
  760. 'orange',
  761. 'pink',
  762. 'purple',
  763. 'red',
  764. 'teal',
  765. 'violet',
  766. 'yellow',
  767. ];
  768. // 基础10色色板 (N ≤ 10)
  769. const baseColors = [
  770. '#1664FF', // 主色
  771. '#1AC6FF',
  772. '#FF8A00',
  773. '#3CC780',
  774. '#7442D4',
  775. '#FFC400',
  776. '#304D77',
  777. '#B48DEB',
  778. '#009488',
  779. '#FF7DDA',
  780. ];
  781. // 扩展20色色板 (10 < N ≤ 20)
  782. const extendedColors = [
  783. '#1664FF',
  784. '#B2CFFF',
  785. '#1AC6FF',
  786. '#94EFFF',
  787. '#FF8A00',
  788. '#FFCE7A',
  789. '#3CC780',
  790. '#B9EDCD',
  791. '#7442D4',
  792. '#DDC5FA',
  793. '#FFC400',
  794. '#FAE878',
  795. '#304D77',
  796. '#8B959E',
  797. '#B48DEB',
  798. '#EFE3FF',
  799. '#009488',
  800. '#59BAA8',
  801. '#FF7DDA',
  802. '#FFCFEE',
  803. ];
  804. export const modelColorMap = {
  805. 'dall-e': 'rgb(147,112,219)', // 深紫色
  806. // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
  807. 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
  808. 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
  809. // 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
  810. 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
  811. 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
  812. 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
  813. 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
  814. 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
  815. 'gpt-4': 'rgb(135,206,235)', // 天蓝色
  816. // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
  817. 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
  818. 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
  819. 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
  820. 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
  821. 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
  822. // 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
  823. 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
  824. 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
  825. 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
  826. 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
  827. 'text-ada-001': 'rgb(255,192,203)', // 粉红色
  828. 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
  829. 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
  830. // 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
  831. 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
  832. 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
  833. 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
  834. 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
  835. 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
  836. 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
  837. 'tts-1': 'rgb(255,140,0)', // 深橙色
  838. 'tts-1-1106': 'rgb(255,165,0)', // 橙色
  839. 'tts-1-hd': 'rgb(255,215,0)', // 金色
  840. 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
  841. 'whisper-1': 'rgb(245,245,220)', // 米色
  842. 'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
  843. 'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
  844. 'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
  845. 'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
  846. };
  847. export function modelToColor(modelName) {
  848. // 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
  849. if (modelColorMap[modelName]) {
  850. return modelColorMap[modelName];
  851. }
  852. // 2. 生成一个稳定的数字作为索引
  853. let hash = 0;
  854. for (let i = 0; i < modelName.length; i++) {
  855. hash = (hash << 5) - hash + modelName.charCodeAt(i);
  856. hash = hash & hash; // Convert to 32-bit integer
  857. }
  858. hash = Math.abs(hash);
  859. // 3. 根据模型名称长度选择不同的色板
  860. const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
  861. // 4. 使用hash值选择颜色
  862. const index = hash % colorPalette.length;
  863. return colorPalette[index];
  864. }
  865. export function stringToColor(str) {
  866. let sum = 0;
  867. for (let i = 0; i < str.length; i++) {
  868. sum += str.charCodeAt(i);
  869. }
  870. let i = sum % colors.length;
  871. return colors[i];
  872. }
  873. export function renderClaudeModelPrice(
  874. inputTokens,
  875. completionTokens,
  876. modelRatio,
  877. modelPrice = -1,
  878. completionRatio,
  879. groupRatio,
  880. cacheTokens = 0,
  881. cacheRatio = 1.0,
  882. cacheCreationTokens = 0,
  883. cacheCreationRatio = 1.0,
  884. ) {
  885. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
  886. if (modelPrice !== -1) {
  887. return i18next.t(
  888. '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
  889. {
  890. price: modelPrice,
  891. ratioType: ratioLabel,
  892. ratio: groupRatio,
  893. total: modelPrice * groupRatio,
  894. },
  895. );
  896. } else {
  897. if (completionRatio === undefined) {
  898. completionRatio = 0;
  899. }
  900. const completionRatioValue = completionRatio || 0;
  901. const inputRatioPrice = modelRatio * 2.0;
  902. const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
  903. let cacheRatioPrice = (modelRatio * 2.0 * cacheRatio).toFixed(2);
  904. let cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
  905. // Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
  906. const nonCachedTokens = inputTokens;
  907. const effectiveInputTokens =
  908. nonCachedTokens +
  909. cacheTokens * cacheRatio +
  910. cacheCreationTokens * cacheCreationRatio;
  911. let price =
  912. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  913. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  914. return (
  915. <>
  916. <article>
  917. <p>
  918. {i18next.t('提示价格:${{price}} / 1M tokens', {
  919. price: inputRatioPrice,
  920. })}
  921. </p>
  922. <p>
  923. {i18next.t(
  924. '补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens',
  925. {
  926. price: inputRatioPrice,
  927. ratio: completionRatio,
  928. total: completionRatioPrice,
  929. },
  930. )}
  931. </p>
  932. {cacheTokens > 0 && (
  933. <p>
  934. {i18next.t(
  935. '缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  936. {
  937. price: inputRatioPrice,
  938. ratio: cacheRatio,
  939. total: cacheRatioPrice,
  940. cacheRatio: cacheRatio,
  941. },
  942. )}
  943. </p>
  944. )}
  945. {cacheCreationTokens > 0 && (
  946. <p>
  947. {i18next.t(
  948. '缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
  949. {
  950. price: inputRatioPrice,
  951. ratio: cacheCreationRatio,
  952. total: cacheCreationRatioPrice,
  953. cacheCreationRatio: cacheCreationRatio,
  954. },
  955. )}
  956. </p>
  957. )}
  958. <p></p>
  959. <p>
  960. {cacheTokens > 0 || cacheCreationTokens > 0
  961. ? i18next.t(
  962. '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  963. {
  964. nonCacheInput: nonCachedTokens,
  965. cacheInput: cacheTokens,
  966. cacheRatio: cacheRatio,
  967. cacheCreationInput: cacheCreationTokens,
  968. cacheCreationRatio: cacheCreationRatio,
  969. cachePrice: cacheRatioPrice,
  970. cacheCreationPrice: cacheCreationRatioPrice,
  971. price: inputRatioPrice,
  972. completion: completionTokens,
  973. compPrice: completionRatioPrice,
  974. ratio: groupRatio,
  975. total: price.toFixed(6),
  976. },
  977. )
  978. : i18next.t(
  979. '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  980. {
  981. input: inputTokens,
  982. price: inputRatioPrice,
  983. completion: completionTokens,
  984. compPrice: completionRatioPrice,
  985. ratio: groupRatio,
  986. total: price.toFixed(6),
  987. },
  988. )}
  989. </p>
  990. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  991. </article>
  992. </>
  993. );
  994. }
  995. }
  996. export function renderClaudeLogContent(
  997. modelRatio,
  998. completionRatio,
  999. modelPrice = -1,
  1000. groupRatio,
  1001. cacheRatio = 1.0,
  1002. cacheCreationRatio = 1.0,
  1003. ) {
  1004. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
  1005. if (modelPrice !== -1) {
  1006. return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
  1007. price: modelPrice,
  1008. ratioType: ratioLabel,
  1009. ratio: groupRatio,
  1010. });
  1011. } else {
  1012. return i18next.t(
  1013. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}',
  1014. {
  1015. modelRatio: modelRatio,
  1016. completionRatio: completionRatio,
  1017. cacheRatio: cacheRatio,
  1018. cacheCreationRatio: cacheCreationRatio,
  1019. ratioType: ratioLabel,
  1020. ratio: groupRatio,
  1021. },
  1022. );
  1023. }
  1024. }
  1025. export function renderClaudeModelPriceSimple(
  1026. modelRatio,
  1027. modelPrice = -1,
  1028. groupRatio,
  1029. cacheTokens = 0,
  1030. cacheRatio = 1.0,
  1031. cacheCreationTokens = 0,
  1032. cacheCreationRatio = 1.0,
  1033. ) {
  1034. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组');
  1035. if (modelPrice !== -1) {
  1036. return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {
  1037. price: modelPrice,
  1038. ratioType: ratioLabel,
  1039. ratio: groupRatio,
  1040. });
  1041. } else {
  1042. if (cacheTokens !== 0 || cacheCreationTokens !== 0) {
  1043. return i18next.t(
  1044. '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}',
  1045. {
  1046. ratio: modelRatio,
  1047. ratioType: ratioLabel,
  1048. groupRatio: groupRatio,
  1049. cacheRatio: cacheRatio,
  1050. cacheCreationRatio: cacheCreationRatio,
  1051. },
  1052. );
  1053. } else {
  1054. return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', {
  1055. ratio: modelRatio,
  1056. ratioType: ratioLabel,
  1057. groupRatio: groupRatio,
  1058. });
  1059. }
  1060. }
  1061. }