render.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  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({ title: t('无法复制到剪贴板,请手动复制'), content: group });
  43. }
  44. }}
  45. >
  46. {group}
  47. </Tag>
  48. ))}
  49. </span>
  50. );
  51. }
  52. export function renderRatio(ratio) {
  53. let color = 'green';
  54. if (ratio > 5) {
  55. color = 'red';
  56. } else if (ratio > 3) {
  57. color = 'orange';
  58. } else if (ratio > 1) {
  59. color = 'blue';
  60. }
  61. return <Tag color={color}>{ratio}x {i18next.t('倍率')}</Tag>;
  62. }
  63. const measureTextWidth = (text, style = {
  64. fontSize: '14px',
  65. fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
  66. }, containerWidth) => {
  67. const span = document.createElement('span');
  68. span.style.visibility = 'hidden';
  69. span.style.position = 'absolute';
  70. span.style.whiteSpace = 'nowrap';
  71. span.style.fontSize = style.fontSize;
  72. span.style.fontFamily = style.fontFamily;
  73. span.textContent = text;
  74. document.body.appendChild(span);
  75. const width = span.offsetWidth;
  76. document.body.removeChild(span);
  77. return width;
  78. };
  79. export function truncateText(text, maxWidth = 200) {
  80. if (!isMobile()) {
  81. return text;
  82. }
  83. if (!text) return text;
  84. try {
  85. // Handle percentage-based maxWidth
  86. let actualMaxWidth = maxWidth;
  87. if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) {
  88. const percentage = parseFloat(maxWidth) / 100;
  89. // Use window width as fallback container width
  90. actualMaxWidth = window.innerWidth * percentage;
  91. }
  92. const width = measureTextWidth(text);
  93. if (width <= actualMaxWidth) return text;
  94. let left = 0;
  95. let right = text.length;
  96. let result = text;
  97. while (left <= right) {
  98. const mid = Math.floor((left + right) / 2);
  99. const truncated = text.slice(0, mid) + '...';
  100. const currentWidth = measureTextWidth(truncated);
  101. if (currentWidth <= actualMaxWidth) {
  102. result = truncated;
  103. left = mid + 1;
  104. } else {
  105. right = mid - 1;
  106. }
  107. }
  108. return result;
  109. } catch (error) {
  110. console.warn('Text measurement failed, falling back to character count', error);
  111. if (text.length > 20) {
  112. return text.slice(0, 17) + '...';
  113. }
  114. return text;
  115. }
  116. }
  117. export const renderGroupOption = (item) => {
  118. const {
  119. disabled,
  120. selected,
  121. label,
  122. value,
  123. focused,
  124. className,
  125. style,
  126. onMouseEnter,
  127. onClick,
  128. empty,
  129. emptyContent,
  130. ...rest
  131. } = item;
  132. const baseStyle = {
  133. display: 'flex',
  134. justifyContent: 'space-between',
  135. alignItems: 'center',
  136. padding: '8px 16px',
  137. cursor: disabled ? 'not-allowed' : 'pointer',
  138. backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
  139. opacity: disabled ? 0.5 : 1,
  140. ...(selected && {
  141. backgroundColor: 'var(--semi-color-primary-light-default)',
  142. }),
  143. '&:hover': {
  144. backgroundColor: !disabled && 'var(--semi-color-fill-1)'
  145. }
  146. };
  147. const handleClick = () => {
  148. if (!disabled && onClick) {
  149. onClick();
  150. }
  151. };
  152. const handleMouseEnter = (e) => {
  153. if (!disabled && onMouseEnter) {
  154. onMouseEnter(e);
  155. }
  156. };
  157. return (
  158. <div
  159. style={baseStyle}
  160. onClick={handleClick}
  161. onMouseEnter={handleMouseEnter}
  162. >
  163. <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
  164. <Typography.Text strong type={disabled ? 'tertiary' : undefined}>
  165. {value}
  166. </Typography.Text>
  167. <Typography.Text type="secondary" size="small">
  168. {label}
  169. </Typography.Text>
  170. </div>
  171. {item.ratio && renderRatio(item.ratio)}
  172. </div>
  173. );
  174. };
  175. export function renderNumber(num) {
  176. if (num >= 1000000000) {
  177. return (num / 1000000000).toFixed(1) + 'B';
  178. } else if (num >= 1000000) {
  179. return (num / 1000000).toFixed(1) + 'M';
  180. } else if (num >= 10000) {
  181. return (num / 1000).toFixed(1) + 'k';
  182. } else {
  183. return num;
  184. }
  185. }
  186. export function renderQuotaNumberWithDigit(num, digits = 2) {
  187. if (typeof num !== 'number' || isNaN(num)) {
  188. return 0;
  189. }
  190. let displayInCurrency = localStorage.getItem('display_in_currency');
  191. num = num.toFixed(digits);
  192. if (displayInCurrency) {
  193. return '$' + num;
  194. }
  195. return num;
  196. }
  197. export function renderNumberWithPoint(num) {
  198. if (num === undefined)
  199. return '';
  200. num = num.toFixed(2);
  201. if (num >= 100000) {
  202. // Convert number to string to manipulate it
  203. let numStr = num.toString();
  204. // Find the position of the decimal point
  205. let decimalPointIndex = numStr.indexOf('.');
  206. let wholePart = numStr;
  207. let decimalPart = '';
  208. // If there is a decimal point, split the number into whole and decimal parts
  209. if (decimalPointIndex !== -1) {
  210. wholePart = numStr.slice(0, decimalPointIndex);
  211. decimalPart = numStr.slice(decimalPointIndex);
  212. }
  213. // Take the first two and last two digits of the whole number part
  214. let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
  215. // Return the formatted number
  216. return shortenedWholePart + decimalPart;
  217. }
  218. // If the number is less than 100,000, return it unmodified
  219. return num;
  220. }
  221. export function getQuotaPerUnit() {
  222. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  223. quotaPerUnit = parseFloat(quotaPerUnit);
  224. return quotaPerUnit;
  225. }
  226. export function renderUnitWithQuota(quota) {
  227. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  228. quotaPerUnit = parseFloat(quotaPerUnit);
  229. quota = parseFloat(quota);
  230. return quotaPerUnit * quota;
  231. }
  232. export function getQuotaWithUnit(quota, digits = 6) {
  233. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  234. quotaPerUnit = parseFloat(quotaPerUnit);
  235. return (quota / quotaPerUnit).toFixed(digits);
  236. }
  237. export function renderQuotaWithAmount(amount) {
  238. let displayInCurrency = localStorage.getItem('display_in_currency');
  239. displayInCurrency = displayInCurrency === 'true';
  240. if (displayInCurrency) {
  241. return '$' + amount;
  242. } else {
  243. return renderUnitWithQuota(amount);
  244. }
  245. }
  246. export function renderQuota(quota, digits = 2) {
  247. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  248. let displayInCurrency = localStorage.getItem('display_in_currency');
  249. quotaPerUnit = parseFloat(quotaPerUnit);
  250. displayInCurrency = displayInCurrency === 'true';
  251. if (displayInCurrency) {
  252. return '$' + (quota / quotaPerUnit).toFixed(digits);
  253. }
  254. return renderNumber(quota);
  255. }
  256. export function renderModelPrice(
  257. inputTokens,
  258. completionTokens,
  259. modelRatio,
  260. modelPrice = -1,
  261. completionRatio,
  262. groupRatio,
  263. cacheTokens = 0,
  264. cacheRatio = 1.0,
  265. ) {
  266. if (modelPrice !== -1) {
  267. return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
  268. price: modelPrice,
  269. ratio: groupRatio,
  270. total: modelPrice * groupRatio
  271. });
  272. } else {
  273. if (completionRatio === undefined) {
  274. completionRatio = 0;
  275. }
  276. let inputRatioPrice = modelRatio * 2.0;
  277. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  278. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  279. // Calculate effective input tokens (non-cached + cached with ratio applied)
  280. const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio);
  281. let price =
  282. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  283. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  284. return (
  285. <>
  286. <article>
  287. <p>{i18next.t('提示价格:${{price}} / 1M tokens', {
  288. price: inputRatioPrice,
  289. })}</p>
  290. <p>{i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', {
  291. price: inputRatioPrice,
  292. total: completionRatioPrice,
  293. completionRatio: completionRatio
  294. })}</p>
  295. {cacheTokens > 0 && (
  296. <p>{i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
  297. price: inputRatioPrice,
  298. total: inputRatioPrice * cacheRatio,
  299. cacheRatio: cacheRatio
  300. })}</p>
  301. )}
  302. <p></p>
  303. <p>
  304. {cacheTokens > 0 ?
  305. i18next.t('提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
  306. nonCacheInput: inputTokens - cacheTokens,
  307. cacheInput: cacheTokens,
  308. cachePrice: inputRatioPrice * cacheRatio,
  309. price: inputRatioPrice,
  310. completion: completionTokens,
  311. compPrice: completionRatioPrice,
  312. ratio: groupRatio,
  313. total: price.toFixed(6)
  314. }) :
  315. i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
  316. input: inputTokens,
  317. price: inputRatioPrice,
  318. completion: completionTokens,
  319. compPrice: completionRatioPrice,
  320. ratio: groupRatio,
  321. total: price.toFixed(6)
  322. })
  323. }
  324. </p>
  325. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  326. </article>
  327. </>
  328. );
  329. }
  330. }
  331. export function renderModelPriceSimple(
  332. modelRatio,
  333. modelPrice = -1,
  334. groupRatio,
  335. cacheTokens = 0,
  336. cacheRatio = 1.0,
  337. ) {
  338. if (modelPrice !== -1) {
  339. return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
  340. price: modelPrice,
  341. ratio: groupRatio
  342. });
  343. } else {
  344. if (cacheTokens !== 0) {
  345. return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}', {
  346. ratio: modelRatio,
  347. groupRatio: groupRatio,
  348. cacheRatio: cacheRatio
  349. });
  350. } else {
  351. return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
  352. ratio: modelRatio,
  353. groupRatio: groupRatio
  354. });
  355. }
  356. }
  357. }
  358. export function renderAudioModelPrice(
  359. inputTokens,
  360. completionTokens,
  361. modelRatio,
  362. modelPrice = -1,
  363. completionRatio,
  364. audioInputTokens,
  365. audioCompletionTokens,
  366. audioRatio,
  367. audioCompletionRatio,
  368. groupRatio,
  369. cacheTokens = 0,
  370. cacheRatio = 1.0,
  371. ) {
  372. // 1 ratio = $0.002 / 1K tokens
  373. if (modelPrice !== -1) {
  374. return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
  375. price: modelPrice,
  376. ratio: groupRatio,
  377. total: modelPrice * groupRatio
  378. });
  379. } else {
  380. if (completionRatio === undefined) {
  381. completionRatio = 0;
  382. }
  383. // try toFixed audioRatio
  384. audioRatio = parseFloat(audioRatio).toFixed(6);
  385. // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
  386. let inputRatioPrice = modelRatio * 2.0;
  387. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  388. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  389. // Calculate effective input tokens (non-cached + cached with ratio applied)
  390. const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio);
  391. let textPrice =
  392. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  393. (completionTokens / 1000000) * completionRatioPrice * groupRatio
  394. let audioPrice =
  395. (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
  396. (audioCompletionTokens / 1000000) * inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio;
  397. let price = textPrice + audioPrice;
  398. return (
  399. <>
  400. <article>
  401. <p>{i18next.t('提示价格:${{price}} / 1M tokens', {
  402. price: inputRatioPrice,
  403. })}</p>
  404. <p>{i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', {
  405. price: inputRatioPrice,
  406. total: completionRatioPrice,
  407. completionRatio: completionRatio
  408. })}</p>
  409. {cacheTokens > 0 && (
  410. <p>{i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
  411. price: inputRatioPrice,
  412. total: inputRatioPrice * cacheRatio,
  413. cacheRatio: cacheRatio
  414. })}</p>
  415. )}
  416. <p>{i18next.t('音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})', {
  417. price: inputRatioPrice,
  418. total: inputRatioPrice * audioRatio,
  419. audioRatio: audioRatio
  420. })}</p>
  421. <p>{i18next.t('音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', {
  422. price: inputRatioPrice,
  423. total: inputRatioPrice * audioRatio * audioCompletionRatio,
  424. audioRatio: audioRatio,
  425. audioCompRatio: audioCompletionRatio
  426. })}</p>
  427. <p>
  428. {cacheTokens > 0 ?
  429. i18next.t('文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', {
  430. nonCacheInput: inputTokens - cacheTokens,
  431. cacheInput: cacheTokens,
  432. cachePrice: inputRatioPrice * cacheRatio,
  433. price: inputRatioPrice,
  434. completion: completionTokens,
  435. compPrice: completionRatioPrice,
  436. total: textPrice.toFixed(6)
  437. }) :
  438. i18next.t('文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', {
  439. input: inputTokens,
  440. price: inputRatioPrice,
  441. completion: completionTokens,
  442. compPrice: completionRatioPrice,
  443. total: textPrice.toFixed(6)
  444. })
  445. }
  446. </p>
  447. <p>
  448. {i18next.t('音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}', {
  449. input: audioInputTokens,
  450. completion: audioCompletionTokens,
  451. audioInputPrice: audioRatio * inputRatioPrice,
  452. audioCompPrice: audioRatio * audioCompletionRatio * inputRatioPrice,
  453. total: audioPrice.toFixed(6)
  454. })}
  455. </p>
  456. <p>
  457. {i18next.t('总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}', {
  458. total: price.toFixed(6),
  459. textPrice: textPrice.toFixed(6),
  460. audioPrice: audioPrice.toFixed(6)
  461. })}
  462. </p>
  463. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  464. </article>
  465. </>
  466. );
  467. }
  468. }
  469. export function renderQuotaWithPrompt(quota, digits) {
  470. let displayInCurrency = localStorage.getItem('display_in_currency');
  471. displayInCurrency = displayInCurrency === 'true';
  472. if (displayInCurrency) {
  473. return ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
  474. }
  475. return '';
  476. }
  477. const colors = [
  478. 'amber',
  479. 'blue',
  480. 'cyan',
  481. 'green',
  482. 'grey',
  483. 'indigo',
  484. 'light-blue',
  485. 'lime',
  486. 'orange',
  487. 'pink',
  488. 'purple',
  489. 'red',
  490. 'teal',
  491. 'violet',
  492. 'yellow'
  493. ];
  494. // 基础10色色板 (N ≤ 10)
  495. const baseColors = [
  496. '#1664FF', // 主色
  497. '#1AC6FF',
  498. '#FF8A00',
  499. '#3CC780',
  500. '#7442D4',
  501. '#FFC400',
  502. '#304D77',
  503. '#B48DEB',
  504. '#009488',
  505. '#FF7DDA'
  506. ];
  507. // 扩展20色色板 (10 < N ≤ 20)
  508. const extendedColors = [
  509. '#1664FF',
  510. '#B2CFFF',
  511. '#1AC6FF',
  512. '#94EFFF',
  513. '#FF8A00',
  514. '#FFCE7A',
  515. '#3CC780',
  516. '#B9EDCD',
  517. '#7442D4',
  518. '#DDC5FA',
  519. '#FFC400',
  520. '#FAE878',
  521. '#304D77',
  522. '#8B959E',
  523. '#B48DEB',
  524. '#EFE3FF',
  525. '#009488',
  526. '#59BAA8',
  527. '#FF7DDA',
  528. '#FFCFEE'
  529. ];
  530. export const modelColorMap = {
  531. 'dall-e': 'rgb(147,112,219)', // 深紫色
  532. // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
  533. 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
  534. 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
  535. // 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
  536. 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
  537. 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
  538. 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
  539. 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
  540. 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
  541. 'gpt-4': 'rgb(135,206,235)', // 天蓝色
  542. // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
  543. 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
  544. 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
  545. 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
  546. 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
  547. 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
  548. // 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
  549. 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
  550. 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
  551. 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
  552. 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
  553. 'text-ada-001': 'rgb(255,192,203)', // 粉红色
  554. 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
  555. 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
  556. // 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
  557. 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
  558. 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
  559. 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
  560. 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
  561. 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
  562. 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
  563. 'tts-1': 'rgb(255,140,0)', // 深橙色
  564. 'tts-1-1106': 'rgb(255,165,0)', // 橙色
  565. 'tts-1-hd': 'rgb(255,215,0)', // 金色
  566. 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
  567. 'whisper-1': 'rgb(245,245,220)', // 米色
  568. 'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
  569. 'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
  570. 'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
  571. 'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
  572. };
  573. export function modelToColor(modelName) {
  574. // 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
  575. if (modelColorMap[modelName]) {
  576. return modelColorMap[modelName];
  577. }
  578. // 2. 生成一个稳定的数字作为索引
  579. let hash = 0;
  580. for (let i = 0; i < modelName.length; i++) {
  581. hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
  582. hash = hash & hash; // Convert to 32-bit integer
  583. }
  584. hash = Math.abs(hash);
  585. // 3. 根据模型名称长度选择不同的色板
  586. const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
  587. // 4. 使用hash值选择颜色
  588. const index = hash % colorPalette.length;
  589. return colorPalette[index];
  590. }
  591. export function stringToColor(str) {
  592. let sum = 0;
  593. for (let i = 0; i < str.length; i++) {
  594. sum += str.charCodeAt(i);
  595. }
  596. let i = sum % colors.length;
  597. return colors[i];
  598. }
  599. export function renderClaudeModelPrice(
  600. inputTokens,
  601. completionTokens,
  602. modelRatio,
  603. modelPrice = -1,
  604. completionRatio,
  605. groupRatio,
  606. cacheTokens = 0,
  607. cacheRatio = 1.0,
  608. cacheCreationTokens = 0,
  609. cacheCreationRatio = 1.0,
  610. ) {
  611. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
  612. if (modelPrice !== -1) {
  613. return i18next.t('模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', {
  614. price: modelPrice,
  615. ratioType: ratioLabel,
  616. ratio: groupRatio,
  617. total: modelPrice * groupRatio
  618. });
  619. } else {
  620. if (completionRatio === undefined) {
  621. completionRatio = 0;
  622. }
  623. const completionRatioValue = completionRatio || 0;
  624. const inputRatioPrice = modelRatio * 2.0;
  625. const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
  626. let cacheRatioPrice = (modelRatio * 2.0 * cacheRatio).toFixed(2);
  627. let cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
  628. // Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
  629. const nonCachedTokens = inputTokens;
  630. const effectiveInputTokens = nonCachedTokens +
  631. (cacheTokens * cacheRatio) +
  632. (cacheCreationTokens * cacheCreationRatio);
  633. let price =
  634. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  635. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  636. return (
  637. <>
  638. <article>
  639. <p>{i18next.t('提示价格:${{price}} / 1M tokens', {
  640. price: inputRatioPrice,
  641. })}</p>
  642. <p>{i18next.t('补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
  643. price: inputRatioPrice,
  644. ratio: completionRatio,
  645. total: completionRatioPrice
  646. })}</p>
  647. {cacheTokens > 0 && (
  648. <p>{i18next.t('缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', {
  649. price: inputRatioPrice,
  650. ratio: cacheRatio,
  651. total: cacheRatioPrice,
  652. cacheRatio: cacheRatio
  653. })}</p>
  654. )}
  655. {cacheCreationTokens > 0 && (
  656. <p>{i18next.t('缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})', {
  657. price: inputRatioPrice,
  658. ratio: cacheCreationRatio,
  659. total: cacheCreationRatioPrice,
  660. cacheCreationRatio: cacheCreationRatio
  661. })}</p>
  662. )}
  663. <p></p>
  664. <p>
  665. {(cacheTokens > 0 || cacheCreationTokens > 0) ?
  666. i18next.t('提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
  667. nonCacheInput: nonCachedTokens,
  668. cacheInput: cacheTokens,
  669. cacheRatio: cacheRatio,
  670. cacheCreationInput: cacheCreationTokens,
  671. cacheCreationRatio: cacheCreationRatio,
  672. cachePrice: cacheRatioPrice,
  673. cacheCreationPrice: cacheCreationRatioPrice,
  674. price: inputRatioPrice,
  675. completion: completionTokens,
  676. compPrice: completionRatioPrice,
  677. ratio: groupRatio,
  678. total: price.toFixed(6)
  679. }) :
  680. i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
  681. input: inputTokens,
  682. price: inputRatioPrice,
  683. completion: completionTokens,
  684. compPrice: completionRatioPrice,
  685. ratio: groupRatio,
  686. total: price.toFixed(6)
  687. })
  688. }
  689. </p>
  690. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  691. </article>
  692. </>
  693. );
  694. }
  695. }
  696. export function renderClaudeLogContent(
  697. modelRatio,
  698. completionRatio,
  699. modelPrice = -1,
  700. groupRatio,
  701. cacheRatio = 1.0,
  702. cacheCreationRatio = 1.0,
  703. ) {
  704. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
  705. if (modelPrice !== -1) {
  706. return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
  707. price: modelPrice,
  708. ratioType: ratioLabel,
  709. ratio: groupRatio
  710. });
  711. } else {
  712. return i18next.t('模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}', {
  713. modelRatio: modelRatio,
  714. completionRatio: completionRatio,
  715. cacheRatio: cacheRatio,
  716. cacheCreationRatio: cacheCreationRatio,
  717. ratioType: ratioLabel,
  718. ratio: groupRatio
  719. });
  720. }
  721. }
  722. export function renderClaudeModelPriceSimple(
  723. modelRatio,
  724. modelPrice = -1,
  725. groupRatio,
  726. cacheTokens = 0,
  727. cacheRatio = 1.0,
  728. cacheCreationTokens = 0,
  729. cacheCreationRatio = 1.0,
  730. ) {
  731. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组');
  732. if (modelPrice !== -1) {
  733. return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {
  734. price: modelPrice,
  735. ratioType: ratioLabel,
  736. ratio: groupRatio
  737. });
  738. } else {
  739. if (cacheTokens !== 0 || cacheCreationTokens !== 0) {
  740. return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}', {
  741. ratio: modelRatio,
  742. ratioType: ratioLabel,
  743. groupRatio: groupRatio,
  744. cacheRatio: cacheRatio,
  745. cacheCreationRatio: cacheCreationRatio
  746. });
  747. } else {
  748. return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', {
  749. ratio: modelRatio,
  750. ratioType: ratioLabel,
  751. groupRatio: groupRatio
  752. });
  753. }
  754. }
  755. }
  756. export function renderLogContent(
  757. modelRatio,
  758. completionRatio,
  759. modelPrice = -1,
  760. groupRatio
  761. ) {
  762. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
  763. if (modelPrice !== -1) {
  764. return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
  765. price: modelPrice,
  766. ratioType: ratioLabel,
  767. ratio: groupRatio
  768. });
  769. } else {
  770. return i18next.t('模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},{{ratioType}} {{ratio}}', {
  771. modelRatio: modelRatio,
  772. completionRatio: completionRatio,
  773. ratioType: ratioLabel,
  774. ratio: groupRatio
  775. });
  776. }
  777. }