render.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032
  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. ) {
  283. if (modelPrice !== -1) {
  284. return i18next.t(
  285. '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
  286. {
  287. price: modelPrice,
  288. ratio: groupRatio,
  289. total: modelPrice * groupRatio,
  290. },
  291. );
  292. } else {
  293. if (completionRatio === undefined) {
  294. completionRatio = 0;
  295. }
  296. let inputRatioPrice = modelRatio * 2.0;
  297. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  298. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  299. let imageRatioPrice = modelRatio * 2.0 * imageRatio;
  300. // Calculate effective input tokens (non-cached + cached with ratio applied)
  301. let effectiveInputTokens =
  302. inputTokens - cacheTokens + cacheTokens * cacheRatio;
  303. // Handle image tokens if present
  304. if (image && imageOutputTokens > 0) {
  305. effectiveInputTokens = inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
  306. }
  307. let price =
  308. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  309. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  310. return (
  311. <>
  312. <article>
  313. <p>
  314. {i18next.t('输入价格:${{price}} / 1M tokens', {
  315. price: inputRatioPrice,
  316. })}
  317. </p>
  318. <p>
  319. {i18next.t(
  320. '输出价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
  321. {
  322. price: inputRatioPrice,
  323. total: completionRatioPrice,
  324. completionRatio: completionRatio,
  325. },
  326. )}
  327. </p>
  328. {cacheTokens > 0 && (
  329. <p>
  330. {i18next.t(
  331. '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  332. {
  333. price: inputRatioPrice,
  334. total: inputRatioPrice * cacheRatio,
  335. cacheRatio: cacheRatio,
  336. },
  337. )}
  338. </p>
  339. )}
  340. {image && imageOutputTokens > 0 && (
  341. <p>
  342. {i18next.t(
  343. '图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})',
  344. {
  345. price: imageRatioPrice,
  346. ratio: groupRatio,
  347. total: imageRatioPrice * groupRatio,
  348. imageRatio: imageRatio,
  349. },
  350. )}
  351. </p>
  352. )}
  353. <p></p>
  354. <p>
  355. {cacheTokens > 0 && !image
  356. ? i18next.t(
  357. '输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  358. {
  359. nonCacheInput: inputTokens - cacheTokens,
  360. cacheInput: cacheTokens,
  361. cachePrice: inputRatioPrice * cacheRatio,
  362. price: inputRatioPrice,
  363. completion: completionTokens,
  364. compPrice: completionRatioPrice,
  365. ratio: groupRatio,
  366. total: price.toFixed(6),
  367. },
  368. )
  369. : image && imageOutputTokens > 0
  370. ? i18next.t(
  371. '输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  372. {
  373. nonImageInput: inputTokens - imageOutputTokens,
  374. imageInput: imageOutputTokens,
  375. imageRatio: imageRatio,
  376. price: inputRatioPrice,
  377. completion: completionTokens,
  378. compPrice: completionRatioPrice,
  379. ratio: groupRatio,
  380. total: price.toFixed(6),
  381. },
  382. )
  383. : i18next.t(
  384. '输入 {{input}} tokens / 1M tokens * ${{price}} + 输出 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  385. {
  386. input: inputTokens,
  387. price: inputRatioPrice,
  388. completion: completionTokens,
  389. compPrice: completionRatioPrice,
  390. ratio: groupRatio,
  391. total: price.toFixed(6),
  392. },
  393. )}
  394. </p>
  395. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  396. </article>
  397. </>
  398. );
  399. }
  400. }
  401. export function renderLogContent(
  402. modelRatio,
  403. completionRatio,
  404. modelPrice = -1,
  405. groupRatio,
  406. user_group_ratio,
  407. image = false,
  408. imageRatio = 1.0,
  409. useUserGroupRatio = undefined
  410. ) {
  411. const ratioLabel = useUserGroupRatio ? i18next.t('专属倍率') : i18next.t('分组倍率');
  412. const ratio = useUserGroupRatio ? user_group_ratio : groupRatio;
  413. if (modelPrice !== -1) {
  414. return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
  415. price: modelPrice,
  416. ratioType: ratioLabel,
  417. ratio
  418. });
  419. } else {
  420. if (image) {
  421. return i18next.t('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}', {
  422. modelRatio: modelRatio,
  423. completionRatio: completionRatio,
  424. imageRatio: imageRatio,
  425. ratioType: ratioLabel,
  426. ratio
  427. });
  428. } else {
  429. return i18next.t('模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}', {
  430. modelRatio: modelRatio,
  431. completionRatio: completionRatio,
  432. ratioType: ratioLabel,
  433. ratio
  434. });
  435. }
  436. }
  437. }
  438. export function renderModelPriceSimple(
  439. modelRatio,
  440. modelPrice = -1,
  441. groupRatio,
  442. cacheTokens = 0,
  443. cacheRatio = 1.0,
  444. image = false,
  445. imageRatio = 1.0,
  446. ) {
  447. if (modelPrice !== -1) {
  448. return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
  449. price: modelPrice,
  450. ratio: groupRatio,
  451. });
  452. } else {
  453. if (image && cacheTokens !== 0) {
  454. return i18next.t(
  455. '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存倍率: {{cacheRatio}} * 图片输入倍率: {{imageRatio}}',
  456. {
  457. ratio: modelRatio,
  458. ratioType: ratioLabel,
  459. groupRatio: groupRatio,
  460. cacheRatio: cacheRatio,
  461. imageRatio: imageRatio,
  462. },
  463. );
  464. } else if (image) {
  465. return i18next.t(
  466. '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 图片输入倍率: {{imageRatio}}',
  467. {
  468. ratio: modelRatio,
  469. ratioType: ratioLabel,
  470. groupRatio: groupRatio,
  471. imageRatio: imageRatio,
  472. },
  473. );
  474. } else if (cacheTokens !== 0) {
  475. return i18next.t(
  476. '模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}',
  477. {
  478. ratio: modelRatio,
  479. groupRatio: groupRatio,
  480. cacheRatio: cacheRatio,
  481. },
  482. );
  483. } else {
  484. return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
  485. ratio: modelRatio,
  486. groupRatio: groupRatio,
  487. });
  488. }
  489. }
  490. }
  491. export function renderAudioModelPrice(
  492. inputTokens,
  493. completionTokens,
  494. modelRatio,
  495. modelPrice = -1,
  496. completionRatio,
  497. audioInputTokens,
  498. audioCompletionTokens,
  499. audioRatio,
  500. audioCompletionRatio,
  501. groupRatio,
  502. cacheTokens = 0,
  503. cacheRatio = 1.0,
  504. ) {
  505. // 1 ratio = $0.002 / 1K tokens
  506. if (modelPrice !== -1) {
  507. return i18next.t(
  508. '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
  509. {
  510. price: modelPrice,
  511. ratio: groupRatio,
  512. total: modelPrice * groupRatio,
  513. },
  514. );
  515. } else {
  516. if (completionRatio === undefined) {
  517. completionRatio = 0;
  518. }
  519. // try toFixed audioRatio
  520. audioRatio = parseFloat(audioRatio).toFixed(6);
  521. // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
  522. let inputRatioPrice = modelRatio * 2.0;
  523. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  524. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  525. // Calculate effective input tokens (non-cached + cached with ratio applied)
  526. const effectiveInputTokens =
  527. inputTokens - cacheTokens + cacheTokens * cacheRatio;
  528. let textPrice =
  529. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  530. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  531. let audioPrice =
  532. (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
  533. (audioCompletionTokens / 1000000) *
  534. inputRatioPrice *
  535. audioRatio *
  536. audioCompletionRatio *
  537. groupRatio;
  538. let price = textPrice + audioPrice;
  539. return (
  540. <>
  541. <article>
  542. <p>
  543. {i18next.t('提示价格:${{price}} / 1M tokens', {
  544. price: inputRatioPrice,
  545. })}
  546. </p>
  547. <p>
  548. {i18next.t(
  549. '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
  550. {
  551. price: inputRatioPrice,
  552. total: completionRatioPrice,
  553. completionRatio: completionRatio,
  554. },
  555. )}
  556. </p>
  557. {cacheTokens > 0 && (
  558. <p>
  559. {i18next.t(
  560. '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  561. {
  562. price: inputRatioPrice,
  563. total: inputRatioPrice * cacheRatio,
  564. cacheRatio: cacheRatio,
  565. },
  566. )}
  567. </p>
  568. )}
  569. <p>
  570. {i18next.t(
  571. '音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})',
  572. {
  573. price: inputRatioPrice,
  574. total: inputRatioPrice * audioRatio,
  575. audioRatio: audioRatio,
  576. },
  577. )}
  578. </p>
  579. <p>
  580. {i18next.t(
  581. '音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})',
  582. {
  583. price: inputRatioPrice,
  584. total: inputRatioPrice * audioRatio * audioCompletionRatio,
  585. audioRatio: audioRatio,
  586. audioCompRatio: audioCompletionRatio,
  587. },
  588. )}
  589. </p>
  590. <p>
  591. {cacheTokens > 0
  592. ? i18next.t(
  593. '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
  594. {
  595. nonCacheInput: inputTokens - cacheTokens,
  596. cacheInput: cacheTokens,
  597. cachePrice: inputRatioPrice * cacheRatio,
  598. price: inputRatioPrice,
  599. completion: completionTokens,
  600. compPrice: completionRatioPrice,
  601. total: textPrice.toFixed(6),
  602. },
  603. )
  604. : i18next.t(
  605. '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
  606. {
  607. input: inputTokens,
  608. price: inputRatioPrice,
  609. completion: completionTokens,
  610. compPrice: completionRatioPrice,
  611. total: textPrice.toFixed(6),
  612. },
  613. )}
  614. </p>
  615. <p>
  616. {i18next.t(
  617. '音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}',
  618. {
  619. input: audioInputTokens,
  620. completion: audioCompletionTokens,
  621. audioInputPrice: audioRatio * inputRatioPrice,
  622. audioCompPrice:
  623. audioRatio * audioCompletionRatio * inputRatioPrice,
  624. total: audioPrice.toFixed(6),
  625. },
  626. )}
  627. </p>
  628. <p>
  629. {i18next.t(
  630. '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}',
  631. {
  632. total: price.toFixed(6),
  633. textPrice: textPrice.toFixed(6),
  634. audioPrice: audioPrice.toFixed(6),
  635. },
  636. )}
  637. </p>
  638. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  639. </article>
  640. </>
  641. );
  642. }
  643. }
  644. export function renderQuotaWithPrompt(quota, digits) {
  645. let displayInCurrency = localStorage.getItem('display_in_currency');
  646. displayInCurrency = displayInCurrency === 'true';
  647. if (displayInCurrency) {
  648. return (
  649. ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + ''
  650. );
  651. }
  652. return '';
  653. }
  654. const colors = [
  655. 'amber',
  656. 'blue',
  657. 'cyan',
  658. 'green',
  659. 'grey',
  660. 'indigo',
  661. 'light-blue',
  662. 'lime',
  663. 'orange',
  664. 'pink',
  665. 'purple',
  666. 'red',
  667. 'teal',
  668. 'violet',
  669. 'yellow',
  670. ];
  671. // 基础10色色板 (N ≤ 10)
  672. const baseColors = [
  673. '#1664FF', // 主色
  674. '#1AC6FF',
  675. '#FF8A00',
  676. '#3CC780',
  677. '#7442D4',
  678. '#FFC400',
  679. '#304D77',
  680. '#B48DEB',
  681. '#009488',
  682. '#FF7DDA',
  683. ];
  684. // 扩展20色色板 (10 < N ≤ 20)
  685. const extendedColors = [
  686. '#1664FF',
  687. '#B2CFFF',
  688. '#1AC6FF',
  689. '#94EFFF',
  690. '#FF8A00',
  691. '#FFCE7A',
  692. '#3CC780',
  693. '#B9EDCD',
  694. '#7442D4',
  695. '#DDC5FA',
  696. '#FFC400',
  697. '#FAE878',
  698. '#304D77',
  699. '#8B959E',
  700. '#B48DEB',
  701. '#EFE3FF',
  702. '#009488',
  703. '#59BAA8',
  704. '#FF7DDA',
  705. '#FFCFEE',
  706. ];
  707. export const modelColorMap = {
  708. 'dall-e': 'rgb(147,112,219)', // 深紫色
  709. // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
  710. 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
  711. 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
  712. // 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
  713. 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
  714. 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
  715. 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
  716. 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
  717. 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
  718. 'gpt-4': 'rgb(135,206,235)', // 天蓝色
  719. // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
  720. 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
  721. 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
  722. 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
  723. 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
  724. 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
  725. // 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
  726. 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
  727. 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
  728. 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
  729. 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
  730. 'text-ada-001': 'rgb(255,192,203)', // 粉红色
  731. 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
  732. 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
  733. // 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
  734. 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
  735. 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
  736. 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
  737. 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
  738. 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
  739. 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
  740. 'tts-1': 'rgb(255,140,0)', // 深橙色
  741. 'tts-1-1106': 'rgb(255,165,0)', // 橙色
  742. 'tts-1-hd': 'rgb(255,215,0)', // 金色
  743. 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
  744. 'whisper-1': 'rgb(245,245,220)', // 米色
  745. 'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
  746. 'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
  747. 'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
  748. 'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
  749. };
  750. export function modelToColor(modelName) {
  751. // 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
  752. if (modelColorMap[modelName]) {
  753. return modelColorMap[modelName];
  754. }
  755. // 2. 生成一个稳定的数字作为索引
  756. let hash = 0;
  757. for (let i = 0; i < modelName.length; i++) {
  758. hash = (hash << 5) - hash + modelName.charCodeAt(i);
  759. hash = hash & hash; // Convert to 32-bit integer
  760. }
  761. hash = Math.abs(hash);
  762. // 3. 根据模型名称长度选择不同的色板
  763. const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
  764. // 4. 使用hash值选择颜色
  765. const index = hash % colorPalette.length;
  766. return colorPalette[index];
  767. }
  768. export function stringToColor(str) {
  769. let sum = 0;
  770. for (let i = 0; i < str.length; i++) {
  771. sum += str.charCodeAt(i);
  772. }
  773. let i = sum % colors.length;
  774. return colors[i];
  775. }
  776. export function renderClaudeModelPrice(
  777. inputTokens,
  778. completionTokens,
  779. modelRatio,
  780. modelPrice = -1,
  781. completionRatio,
  782. groupRatio,
  783. cacheTokens = 0,
  784. cacheRatio = 1.0,
  785. cacheCreationTokens = 0,
  786. cacheCreationRatio = 1.0,
  787. ) {
  788. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
  789. if (modelPrice !== -1) {
  790. return i18next.t(
  791. '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
  792. {
  793. price: modelPrice,
  794. ratioType: ratioLabel,
  795. ratio: groupRatio,
  796. total: modelPrice * groupRatio,
  797. },
  798. );
  799. } else {
  800. if (completionRatio === undefined) {
  801. completionRatio = 0;
  802. }
  803. const completionRatioValue = completionRatio || 0;
  804. const inputRatioPrice = modelRatio * 2.0;
  805. const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
  806. let cacheRatioPrice = (modelRatio * 2.0 * cacheRatio).toFixed(2);
  807. let cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
  808. // Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
  809. const nonCachedTokens = inputTokens;
  810. const effectiveInputTokens =
  811. nonCachedTokens +
  812. cacheTokens * cacheRatio +
  813. cacheCreationTokens * cacheCreationRatio;
  814. let price =
  815. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  816. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  817. return (
  818. <>
  819. <article>
  820. <p>
  821. {i18next.t('提示价格:${{price}} / 1M tokens', {
  822. price: inputRatioPrice,
  823. })}
  824. </p>
  825. <p>
  826. {i18next.t(
  827. '补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens',
  828. {
  829. price: inputRatioPrice,
  830. ratio: completionRatio,
  831. total: completionRatioPrice,
  832. },
  833. )}
  834. </p>
  835. {cacheTokens > 0 && (
  836. <p>
  837. {i18next.t(
  838. '缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  839. {
  840. price: inputRatioPrice,
  841. ratio: cacheRatio,
  842. total: cacheRatioPrice,
  843. cacheRatio: cacheRatio,
  844. },
  845. )}
  846. </p>
  847. )}
  848. {cacheCreationTokens > 0 && (
  849. <p>
  850. {i18next.t(
  851. '缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
  852. {
  853. price: inputRatioPrice,
  854. ratio: cacheCreationRatio,
  855. total: cacheCreationRatioPrice,
  856. cacheCreationRatio: cacheCreationRatio,
  857. },
  858. )}
  859. </p>
  860. )}
  861. <p></p>
  862. <p>
  863. {cacheTokens > 0 || cacheCreationTokens > 0
  864. ? i18next.t(
  865. '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  866. {
  867. nonCacheInput: nonCachedTokens,
  868. cacheInput: cacheTokens,
  869. cacheRatio: cacheRatio,
  870. cacheCreationInput: cacheCreationTokens,
  871. cacheCreationRatio: cacheCreationRatio,
  872. cachePrice: cacheRatioPrice,
  873. cacheCreationPrice: cacheCreationRatioPrice,
  874. price: inputRatioPrice,
  875. completion: completionTokens,
  876. compPrice: completionRatioPrice,
  877. ratio: groupRatio,
  878. total: price.toFixed(6),
  879. },
  880. )
  881. : i18next.t(
  882. '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  883. {
  884. input: inputTokens,
  885. price: inputRatioPrice,
  886. completion: completionTokens,
  887. compPrice: completionRatioPrice,
  888. ratio: groupRatio,
  889. total: price.toFixed(6),
  890. },
  891. )}
  892. </p>
  893. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  894. </article>
  895. </>
  896. );
  897. }
  898. }
  899. export function renderClaudeLogContent(
  900. modelRatio,
  901. completionRatio,
  902. modelPrice = -1,
  903. groupRatio,
  904. cacheRatio = 1.0,
  905. cacheCreationRatio = 1.0,
  906. ) {
  907. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
  908. if (modelPrice !== -1) {
  909. return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
  910. price: modelPrice,
  911. ratioType: ratioLabel,
  912. ratio: groupRatio,
  913. });
  914. } else {
  915. return i18next.t(
  916. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}',
  917. {
  918. modelRatio: modelRatio,
  919. completionRatio: completionRatio,
  920. cacheRatio: cacheRatio,
  921. cacheCreationRatio: cacheCreationRatio,
  922. ratioType: ratioLabel,
  923. ratio: groupRatio,
  924. },
  925. );
  926. }
  927. }
  928. export function renderClaudeModelPriceSimple(
  929. modelRatio,
  930. modelPrice = -1,
  931. groupRatio,
  932. cacheTokens = 0,
  933. cacheRatio = 1.0,
  934. cacheCreationTokens = 0,
  935. cacheCreationRatio = 1.0,
  936. ) {
  937. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组');
  938. if (modelPrice !== -1) {
  939. return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {
  940. price: modelPrice,
  941. ratioType: ratioLabel,
  942. ratio: groupRatio,
  943. });
  944. } else {
  945. if (cacheTokens !== 0 || cacheCreationTokens !== 0) {
  946. return i18next.t(
  947. '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}',
  948. {
  949. ratio: modelRatio,
  950. ratioType: ratioLabel,
  951. groupRatio: groupRatio,
  952. cacheRatio: cacheRatio,
  953. cacheCreationRatio: cacheCreationRatio,
  954. },
  955. );
  956. } else {
  957. return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', {
  958. ratio: modelRatio,
  959. ratioType: ratioLabel,
  960. groupRatio: groupRatio,
  961. });
  962. }
  963. }
  964. }