render.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import i18next from 'i18next';
  2. import { Tag } from '@douyinfe/semi-ui';
  3. export function renderText(text, limit) {
  4. if (text.length > limit) {
  5. return text.slice(0, limit - 3) + '...';
  6. }
  7. return text;
  8. }
  9. /**
  10. * Render group tags based on the input group string
  11. * @param {string} group - The input group string
  12. * @returns {JSX.Element} - The rendered group tags
  13. */
  14. export function renderGroup(group) {
  15. if (group === '') {
  16. return (
  17. <Tag size='large' key='default' color='orange'>
  18. {i18next.t('用户分组')}
  19. </Tag>
  20. );
  21. }
  22. const tagColors = {
  23. vip: 'yellow',
  24. pro: 'yellow',
  25. svip: 'red',
  26. premium: 'red',
  27. };
  28. const groups = group.split(',').sort();
  29. return (
  30. <span key={group}>
  31. {groups.map((group) => (
  32. <Tag
  33. size='large'
  34. color={tagColors[group] || stringToColor(group)}
  35. key={group}
  36. >
  37. {group}
  38. </Tag>
  39. ))}
  40. </span>
  41. );
  42. }
  43. export function renderNumber(num) {
  44. if (num >= 1000000000) {
  45. return (num / 1000000000).toFixed(1) + 'B';
  46. } else if (num >= 1000000) {
  47. return (num / 1000000).toFixed(1) + 'M';
  48. } else if (num >= 10000) {
  49. return (num / 1000).toFixed(1) + 'k';
  50. } else {
  51. return num;
  52. }
  53. }
  54. export function renderQuotaNumberWithDigit(num, digits = 2) {
  55. let displayInCurrency = localStorage.getItem('display_in_currency');
  56. num = num.toFixed(digits);
  57. if (displayInCurrency) {
  58. return '$' + num;
  59. }
  60. return num;
  61. }
  62. export function renderNumberWithPoint(num) {
  63. if (num === undefined)
  64. return '';
  65. num = num.toFixed(2);
  66. if (num >= 100000) {
  67. // Convert number to string to manipulate it
  68. let numStr = num.toString();
  69. // Find the position of the decimal point
  70. let decimalPointIndex = numStr.indexOf('.');
  71. let wholePart = numStr;
  72. let decimalPart = '';
  73. // If there is a decimal point, split the number into whole and decimal parts
  74. if (decimalPointIndex !== -1) {
  75. wholePart = numStr.slice(0, decimalPointIndex);
  76. decimalPart = numStr.slice(decimalPointIndex);
  77. }
  78. // Take the first two and last two digits of the whole number part
  79. let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
  80. // Return the formatted number
  81. return shortenedWholePart + decimalPart;
  82. }
  83. // If the number is less than 100,000, return it unmodified
  84. return num;
  85. }
  86. export function getQuotaPerUnit() {
  87. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  88. quotaPerUnit = parseFloat(quotaPerUnit);
  89. return quotaPerUnit;
  90. }
  91. export function renderUnitWithQuota(quota) {
  92. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  93. quotaPerUnit = parseFloat(quotaPerUnit);
  94. quota = parseFloat(quota);
  95. return quotaPerUnit * quota;
  96. }
  97. export function getQuotaWithUnit(quota, digits = 6) {
  98. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  99. quotaPerUnit = parseFloat(quotaPerUnit);
  100. return (quota / quotaPerUnit).toFixed(digits);
  101. }
  102. export function renderQuotaWithAmount(amount) {
  103. let displayInCurrency = localStorage.getItem('display_in_currency');
  104. displayInCurrency = displayInCurrency === 'true';
  105. if (displayInCurrency) {
  106. return '$' + amount;
  107. } else {
  108. return renderUnitWithQuota(amount);
  109. }
  110. }
  111. export function renderQuota(quota, digits = 2) {
  112. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  113. let displayInCurrency = localStorage.getItem('display_in_currency');
  114. quotaPerUnit = parseFloat(quotaPerUnit);
  115. displayInCurrency = displayInCurrency === 'true';
  116. if (displayInCurrency) {
  117. return '$' + (quota / quotaPerUnit).toFixed(digits);
  118. }
  119. return renderNumber(quota);
  120. }
  121. export function renderModelPrice(
  122. inputTokens,
  123. completionTokens,
  124. modelRatio,
  125. modelPrice = -1,
  126. completionRatio,
  127. groupRatio,
  128. ) {
  129. if (modelPrice !== -1) {
  130. return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', {
  131. price: modelPrice,
  132. ratio: groupRatio,
  133. total: modelPrice * groupRatio
  134. });
  135. } else {
  136. if (completionRatio === undefined) {
  137. completionRatio = 0;
  138. }
  139. let inputRatioPrice = modelRatio * 2.0;
  140. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  141. let price =
  142. (inputTokens / 1000000) * inputRatioPrice * groupRatio +
  143. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  144. return (
  145. <>
  146. <article>
  147. <p>{i18next.t('提示:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
  148. price: inputRatioPrice,
  149. ratio: groupRatio,
  150. total: inputRatioPrice * groupRatio
  151. })}</p>
  152. <p>{i18next.t('补全:${{price}} * {{ratio}} = ${{total}} / 1M tokens', {
  153. price: completionRatioPrice,
  154. ratio: groupRatio,
  155. total: completionRatioPrice * groupRatio
  156. })}</p>
  157. <p></p>
  158. <p>
  159. {i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', {
  160. input: inputTokens,
  161. price: inputRatioPrice,
  162. completion: completionTokens,
  163. compPrice: completionRatioPrice,
  164. ratio: groupRatio,
  165. total: price.toFixed(6)
  166. })}
  167. </p>
  168. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  169. </article>
  170. </>
  171. );
  172. }
  173. }
  174. export function renderModelPriceSimple(
  175. modelRatio,
  176. modelPrice = -1,
  177. groupRatio,
  178. ) {
  179. if (modelPrice !== -1) {
  180. return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
  181. price: modelPrice,
  182. ratio: groupRatio
  183. });
  184. } else {
  185. return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
  186. ratio: modelRatio,
  187. groupRatio: groupRatio
  188. });
  189. }
  190. }
  191. export function renderAudioModelPrice(
  192. inputTokens,
  193. completionTokens,
  194. modelRatio,
  195. modelPrice = -1,
  196. completionRatio,
  197. audioInputTokens,
  198. audioCompletionTokens,
  199. audioRatio,
  200. audioCompletionRatio,
  201. groupRatio,
  202. ) {
  203. // 1 ratio = $0.002 / 1K tokens
  204. if (modelPrice !== -1) {
  205. return '模型价格:$' + modelPrice + ' * 分组倍率:' + groupRatio + ' = $' + modelPrice * groupRatio;
  206. } else {
  207. if (completionRatio === undefined) {
  208. completionRatio = 0;
  209. }
  210. // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
  211. let inputRatioPrice = modelRatio * 2.0;
  212. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  213. let price =
  214. (inputTokens / 1000000) * inputRatioPrice * groupRatio +
  215. (completionTokens / 1000000) * completionRatioPrice * groupRatio +
  216. (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
  217. (audioCompletionTokens / 1000000) * inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio;
  218. return (
  219. <>
  220. <article>
  221. <p>提示:${inputRatioPrice} * {groupRatio} = ${inputRatioPrice * groupRatio} / 1M tokens</p>
  222. <p>补全:${completionRatioPrice} * {groupRatio} = ${completionRatioPrice * groupRatio} / 1M tokens</p>
  223. <p>音频提示:${inputRatioPrice} * {groupRatio} * {audioRatio} = ${inputRatioPrice * audioRatio * groupRatio} / 1M tokens</p>
  224. <p>音频补全:${inputRatioPrice} * {groupRatio} * {audioRatio} * {audioCompletionRatio} = ${inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio} / 1M tokens</p>
  225. <p></p>
  226. <p>
  227. {i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} +', {
  228. input: inputTokens,
  229. price: inputRatioPrice,
  230. completion: completionTokens,
  231. compPrice: completionRatioPrice
  232. })}
  233. </p>
  234. <p>
  235. 音频提示 {audioInputTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} + 音频补全 {audioCompletionTokens} tokens / 1M tokens * ${inputRatioPrice} * {audioRatio} * {audioCompletionRatio}
  236. </p>
  237. <p>
  238. (文字 + 音频) * 分组 {groupRatio} =
  239. ${price.toFixed(6)}
  240. </p>
  241. <p>仅供参考,以实际扣费为准</p>
  242. </article>
  243. </>
  244. );
  245. }
  246. }
  247. export function renderQuotaWithPrompt(quota, digits) {
  248. let displayInCurrency = localStorage.getItem('display_in_currency');
  249. displayInCurrency = displayInCurrency === 'true';
  250. if (displayInCurrency) {
  251. return '|' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '';
  252. }
  253. return '';
  254. }
  255. const colors = [
  256. 'amber',
  257. 'blue',
  258. 'cyan',
  259. 'green',
  260. 'grey',
  261. 'indigo',
  262. 'light-blue',
  263. 'lime',
  264. 'orange',
  265. 'pink',
  266. 'purple',
  267. 'red',
  268. 'teal',
  269. 'violet',
  270. 'yellow',
  271. ];
  272. // 基础10色色板 (N ≤ 10)
  273. const baseColors = [
  274. '#1664FF', // 主色
  275. '#1AC6FF',
  276. '#FF8A00',
  277. '#3CC780',
  278. '#7442D4',
  279. '#FFC400',
  280. '#304D77',
  281. '#B48DEB',
  282. '#009488',
  283. '#FF7DDA'
  284. ];
  285. // 扩展20色色板 (10 < N ≤ 20)
  286. const extendedColors = [
  287. '#1664FF',
  288. '#B2CFFF',
  289. '#1AC6FF',
  290. '#94EFFF',
  291. '#FF8A00',
  292. '#FFCE7A',
  293. '#3CC780',
  294. '#B9EDCD',
  295. '#7442D4',
  296. '#DDC5FA',
  297. '#FFC400',
  298. '#FAE878',
  299. '#304D77',
  300. '#8B959E',
  301. '#B48DEB',
  302. '#EFE3FF',
  303. '#009488',
  304. '#59BAA8',
  305. '#FF7DDA',
  306. '#FFCFEE'
  307. ];
  308. export const modelColorMap = {
  309. 'dall-e': 'rgb(147,112,219)', // 深紫色
  310. // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
  311. 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
  312. 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
  313. // 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
  314. 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
  315. 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
  316. 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
  317. 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃���
  318. 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
  319. 'gpt-4': 'rgb(135,206,235)', // 天蓝色
  320. // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
  321. 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
  322. 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
  323. 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
  324. 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
  325. 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
  326. // 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
  327. 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
  328. 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
  329. 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
  330. 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
  331. 'text-ada-001': 'rgb(255,192,203)', // 粉红色
  332. 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
  333. 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
  334. // 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
  335. 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
  336. 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
  337. 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
  338. 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
  339. 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
  340. 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(���Babbage相同,表示同一类功能)
  341. 'tts-1': 'rgb(255,140,0)', // 深橙色
  342. 'tts-1-1106': 'rgb(255,165,0)', // 橙色
  343. 'tts-1-hd': 'rgb(255,215,0)', // 金色
  344. 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
  345. 'whisper-1': 'rgb(245,245,220)', // 米色
  346. 'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
  347. 'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
  348. 'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
  349. 'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
  350. };
  351. export function modelToColor(modelName) {
  352. // 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
  353. if (modelColorMap[modelName]) {
  354. return modelColorMap[modelName];
  355. }
  356. // 2. 生成一个稳定的数字作为索引
  357. let hash = 0;
  358. for (let i = 0; i < modelName.length; i++) {
  359. hash = ((hash << 5) - hash) + modelName.charCodeAt(i);
  360. hash = hash & hash; // Convert to 32-bit integer
  361. }
  362. hash = Math.abs(hash);
  363. // 3. 根据模型名称长度选择不同的色板
  364. const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
  365. // 4. 使用hash值选择颜色
  366. const index = hash % colorPalette.length;
  367. return colorPalette[index];
  368. }
  369. export function stringToColor(str) {
  370. let sum = 0;
  371. for (let i = 0; i < str.length; i++) {
  372. sum += str.charCodeAt(i);
  373. }
  374. let i = sum % colors.length;
  375. return colors[i];
  376. }