render.js 45 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558
  1. import i18next from 'i18next';
  2. import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
  3. import { copy, isMobile, showSuccess } from './utils';
  4. import { visit } from 'unist-util-visit';
  5. import {
  6. OpenAI,
  7. Claude,
  8. Gemini,
  9. Moonshot,
  10. Zhipu,
  11. Qwen,
  12. DeepSeek,
  13. Minimax,
  14. Wenxin,
  15. Spark,
  16. Midjourney,
  17. Hunyuan,
  18. Cohere,
  19. Cloudflare,
  20. Ai360,
  21. Yi,
  22. Jina,
  23. Mistral,
  24. XAI,
  25. Ollama,
  26. Doubao,
  27. } from '@lobehub/icons';
  28. import {
  29. LayoutDashboard,
  30. TerminalSquare,
  31. MessageSquare,
  32. Key,
  33. BarChart3,
  34. Image as ImageIcon,
  35. CheckSquare,
  36. CreditCard,
  37. Layers,
  38. Gift,
  39. User,
  40. Settings,
  41. CircleUser,
  42. } from 'lucide-react';
  43. // 侧边栏图标颜色映射
  44. export const sidebarIconColors = {
  45. dashboard: '#4F46E5', // 紫蓝色
  46. terminal: '#10B981', // 绿色
  47. message: '#06B6D4', // 青色
  48. key: '#3B82F6', // 蓝色
  49. chart: '#8B5CF6', // 紫色
  50. image: '#EC4899', // 粉色
  51. check: '#F59E0B', // 琥珀色
  52. credit: '#F97316', // 橙色
  53. layers: '#EF4444', // 红色
  54. gift: '#F43F5E', // 玫红色
  55. user: '#6366F1', // 靛蓝色
  56. settings: '#6B7280', // 灰色
  57. };
  58. // 获取侧边栏Lucide图标组件
  59. export function getLucideIcon(key, selected = false) {
  60. const size = 16;
  61. const strokeWidth = 2;
  62. const commonProps = {
  63. size,
  64. strokeWidth,
  65. className: `transition-colors duration-200 ${selected ? 'transition-transform duration-200 scale-105' : ''}`,
  66. };
  67. // 根据不同的key返回不同的图标
  68. switch (key) {
  69. case 'detail':
  70. return (
  71. <LayoutDashboard
  72. {...commonProps}
  73. color={selected ? sidebarIconColors.dashboard : 'currentColor'}
  74. />
  75. );
  76. case 'playground':
  77. return (
  78. <TerminalSquare
  79. {...commonProps}
  80. color={selected ? sidebarIconColors.terminal : 'currentColor'}
  81. />
  82. );
  83. case 'chat':
  84. return (
  85. <MessageSquare
  86. {...commonProps}
  87. color={selected ? sidebarIconColors.message : 'currentColor'}
  88. />
  89. );
  90. case 'token':
  91. return (
  92. <Key
  93. {...commonProps}
  94. color={selected ? sidebarIconColors.key : 'currentColor'}
  95. />
  96. );
  97. case 'log':
  98. return (
  99. <BarChart3
  100. {...commonProps}
  101. color={selected ? sidebarIconColors.chart : 'currentColor'}
  102. />
  103. );
  104. case 'midjourney':
  105. return (
  106. <ImageIcon
  107. {...commonProps}
  108. color={selected ? sidebarIconColors.image : 'currentColor'}
  109. />
  110. );
  111. case 'task':
  112. return (
  113. <CheckSquare
  114. {...commonProps}
  115. color={selected ? sidebarIconColors.check : 'currentColor'}
  116. />
  117. );
  118. case 'topup':
  119. return (
  120. <CreditCard
  121. {...commonProps}
  122. color={selected ? sidebarIconColors.credit : 'currentColor'}
  123. />
  124. );
  125. case 'channel':
  126. return (
  127. <Layers
  128. {...commonProps}
  129. color={selected ? sidebarIconColors.layers : 'currentColor'}
  130. />
  131. );
  132. case 'redemption':
  133. return (
  134. <Gift
  135. {...commonProps}
  136. color={selected ? sidebarIconColors.gift : 'currentColor'}
  137. />
  138. );
  139. case 'user':
  140. case 'personal':
  141. return (
  142. <User
  143. {...commonProps}
  144. color={selected ? sidebarIconColors.user : 'currentColor'}
  145. />
  146. );
  147. case 'setting':
  148. return (
  149. <Settings
  150. {...commonProps}
  151. color={selected ? sidebarIconColors.settings : 'currentColor'}
  152. />
  153. );
  154. default:
  155. return (
  156. <CircleUser
  157. {...commonProps}
  158. color={selected ? sidebarIconColors.user : 'currentColor'}
  159. />
  160. );
  161. }
  162. }
  163. // 获取模型分类
  164. export const getModelCategories = (() => {
  165. let categoriesCache = null;
  166. let lastLocale = null;
  167. return (t) => {
  168. const currentLocale = i18next.language;
  169. if (categoriesCache && lastLocale === currentLocale) {
  170. return categoriesCache;
  171. }
  172. categoriesCache = {
  173. all: {
  174. label: t('全部模型'),
  175. icon: null,
  176. filter: () => true,
  177. },
  178. openai: {
  179. label: 'OpenAI',
  180. icon: <OpenAI />,
  181. filter: (model) =>
  182. model.model_name.toLowerCase().includes('gpt') ||
  183. model.model_name.toLowerCase().includes('dall-e') ||
  184. model.model_name.toLowerCase().includes('whisper') ||
  185. model.model_name.toLowerCase().includes('tts') ||
  186. model.model_name.toLowerCase().includes('text-') ||
  187. model.model_name.toLowerCase().includes('babbage') ||
  188. model.model_name.toLowerCase().includes('davinci') ||
  189. model.model_name.toLowerCase().includes('curie') ||
  190. model.model_name.toLowerCase().includes('ada') ||
  191. model.model_name.toLowerCase().includes('o1') ||
  192. model.model_name.toLowerCase().includes('o3') ||
  193. model.model_name.toLowerCase().includes('o4'),
  194. },
  195. anthropic: {
  196. label: 'Anthropic',
  197. icon: <Claude.Color />,
  198. filter: (model) => model.model_name.toLowerCase().includes('claude'),
  199. },
  200. gemini: {
  201. label: 'Gemini',
  202. icon: <Gemini.Color />,
  203. filter: (model) => model.model_name.toLowerCase().includes('gemini'),
  204. },
  205. moonshot: {
  206. label: 'Moonshot',
  207. icon: <Moonshot />,
  208. filter: (model) => model.model_name.toLowerCase().includes('moonshot'),
  209. },
  210. zhipu: {
  211. label: t('智谱'),
  212. icon: <Zhipu.Color />,
  213. filter: (model) =>
  214. model.model_name.toLowerCase().includes('chatglm') ||
  215. model.model_name.toLowerCase().includes('glm-'),
  216. },
  217. qwen: {
  218. label: t('通义千问'),
  219. icon: <Qwen.Color />,
  220. filter: (model) => model.model_name.toLowerCase().includes('qwen'),
  221. },
  222. deepseek: {
  223. label: 'DeepSeek',
  224. icon: <DeepSeek.Color />,
  225. filter: (model) => model.model_name.toLowerCase().includes('deepseek'),
  226. },
  227. minimax: {
  228. label: 'MiniMax',
  229. icon: <Minimax.Color />,
  230. filter: (model) => model.model_name.toLowerCase().includes('abab'),
  231. },
  232. baidu: {
  233. label: t('文心一言'),
  234. icon: <Wenxin.Color />,
  235. filter: (model) => model.model_name.toLowerCase().includes('ernie'),
  236. },
  237. xunfei: {
  238. label: t('讯飞星火'),
  239. icon: <Spark.Color />,
  240. filter: (model) => model.model_name.toLowerCase().includes('spark'),
  241. },
  242. midjourney: {
  243. label: 'Midjourney',
  244. icon: <Midjourney />,
  245. filter: (model) => model.model_name.toLowerCase().includes('mj_'),
  246. },
  247. tencent: {
  248. label: t('腾讯混元'),
  249. icon: <Hunyuan.Color />,
  250. filter: (model) => model.model_name.toLowerCase().includes('hunyuan'),
  251. },
  252. cohere: {
  253. label: 'Cohere',
  254. icon: <Cohere.Color />,
  255. filter: (model) => model.model_name.toLowerCase().includes('command'),
  256. },
  257. cloudflare: {
  258. label: 'Cloudflare',
  259. icon: <Cloudflare.Color />,
  260. filter: (model) => model.model_name.toLowerCase().includes('@cf/'),
  261. },
  262. ai360: {
  263. label: t('360智脑'),
  264. icon: <Ai360.Color />,
  265. filter: (model) => model.model_name.toLowerCase().includes('360'),
  266. },
  267. yi: {
  268. label: t('零一万物'),
  269. icon: <Yi.Color />,
  270. filter: (model) => model.model_name.toLowerCase().includes('yi'),
  271. },
  272. jina: {
  273. label: 'Jina',
  274. icon: <Jina />,
  275. filter: (model) => model.model_name.toLowerCase().includes('jina'),
  276. },
  277. mistral: {
  278. label: 'Mistral AI',
  279. icon: <Mistral.Color />,
  280. filter: (model) => model.model_name.toLowerCase().includes('mistral'),
  281. },
  282. xai: {
  283. label: 'xAI',
  284. icon: <XAI />,
  285. filter: (model) => model.model_name.toLowerCase().includes('grok'),
  286. },
  287. llama: {
  288. label: 'Llama',
  289. icon: <Ollama />,
  290. filter: (model) => model.model_name.toLowerCase().includes('llama'),
  291. },
  292. doubao: {
  293. label: t('豆包'),
  294. icon: <Doubao.Color />,
  295. filter: (model) => model.model_name.toLowerCase().includes('doubao'),
  296. },
  297. };
  298. lastLocale = currentLocale;
  299. return categoriesCache;
  300. };
  301. })();
  302. // 颜色列表
  303. const colors = [
  304. 'amber',
  305. 'blue',
  306. 'cyan',
  307. 'green',
  308. 'grey',
  309. 'indigo',
  310. 'light-blue',
  311. 'lime',
  312. 'orange',
  313. 'pink',
  314. 'purple',
  315. 'red',
  316. 'teal',
  317. 'violet',
  318. 'yellow',
  319. ];
  320. // 基础10色色板 (N ≤ 10)
  321. const baseColors = [
  322. '#1664FF', // 主色
  323. '#1AC6FF',
  324. '#FF8A00',
  325. '#3CC780',
  326. '#7442D4',
  327. '#FFC400',
  328. '#304D77',
  329. '#B48DEB',
  330. '#009488',
  331. '#FF7DDA',
  332. ];
  333. // 扩展20色色板 (10 < N ≤ 20)
  334. const extendedColors = [
  335. '#1664FF',
  336. '#B2CFFF',
  337. '#1AC6FF',
  338. '#94EFFF',
  339. '#FF8A00',
  340. '#FFCE7A',
  341. '#3CC780',
  342. '#B9EDCD',
  343. '#7442D4',
  344. '#DDC5FA',
  345. '#FFC400',
  346. '#FAE878',
  347. '#304D77',
  348. '#8B959E',
  349. '#B48DEB',
  350. '#EFE3FF',
  351. '#009488',
  352. '#59BAA8',
  353. '#FF7DDA',
  354. '#FFCFEE',
  355. ];
  356. // 模型颜色映射
  357. export const modelColorMap = {
  358. 'dall-e': 'rgb(147,112,219)', // 深紫色
  359. // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
  360. 'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
  361. 'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
  362. // 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
  363. 'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
  364. 'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
  365. 'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
  366. 'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
  367. 'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
  368. 'gpt-4': 'rgb(135,206,235)', // 天蓝色
  369. // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
  370. 'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
  371. 'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
  372. 'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
  373. 'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
  374. 'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
  375. // 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
  376. 'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
  377. 'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
  378. 'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
  379. 'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
  380. 'text-ada-001': 'rgb(255,192,203)', // 粉红色
  381. 'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
  382. 'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
  383. // 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
  384. 'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
  385. 'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
  386. 'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
  387. 'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
  388. 'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
  389. 'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
  390. 'tts-1': 'rgb(255,140,0)', // 深橙色
  391. 'tts-1-1106': 'rgb(255,165,0)', // 橙色
  392. 'tts-1-hd': 'rgb(255,215,0)', // 金色
  393. 'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
  394. 'whisper-1': 'rgb(245,245,220)', // 米色
  395. 'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
  396. 'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
  397. 'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
  398. 'claude-2.1': 'rgb(255,209,190)', // 浅橙色(略有区别)
  399. };
  400. export function modelToColor(modelName) {
  401. // 1. 如果模型在预定义的 modelColorMap 中,使用预定义颜色
  402. if (modelColorMap[modelName]) {
  403. return modelColorMap[modelName];
  404. }
  405. // 2. 生成一个稳定的数字作为索引
  406. let hash = 0;
  407. for (let i = 0; i < modelName.length; i++) {
  408. hash = (hash << 5) - hash + modelName.charCodeAt(i);
  409. hash = hash & hash; // Convert to 32-bit integer
  410. }
  411. hash = Math.abs(hash);
  412. // 3. 根据模型名称长度选择不同的色板
  413. const colorPalette = modelName.length > 10 ? extendedColors : baseColors;
  414. // 4. 使用hash值选择颜色
  415. const index = hash % colorPalette.length;
  416. return colorPalette[index];
  417. }
  418. export function stringToColor(str) {
  419. let sum = 0;
  420. for (let i = 0; i < str.length; i++) {
  421. sum += str.charCodeAt(i);
  422. }
  423. let i = sum % colors.length;
  424. return colors[i];
  425. }
  426. // 渲染带有模型图标的标签
  427. export function renderModelTag(modelName, options = {}) {
  428. const {
  429. color,
  430. size = 'large',
  431. shape = 'circle',
  432. onClick,
  433. suffixIcon,
  434. } = options;
  435. const categories = getModelCategories(i18next.t);
  436. let icon = null;
  437. for (const [key, category] of Object.entries(categories)) {
  438. if (key !== 'all' && category.filter({ model_name: modelName })) {
  439. icon = category.icon;
  440. break;
  441. }
  442. }
  443. return (
  444. <Tag
  445. color={color || stringToColor(modelName)}
  446. prefixIcon={icon}
  447. suffixIcon={suffixIcon}
  448. size={size}
  449. shape={shape}
  450. onClick={onClick}
  451. >
  452. {modelName}
  453. </Tag>
  454. );
  455. }
  456. export function renderText(text, limit) {
  457. if (text.length > limit) {
  458. return text.slice(0, limit - 3) + '...';
  459. }
  460. return text;
  461. }
  462. /**
  463. * Render group tags based on the input group string
  464. * @param {string} group - The input group string
  465. * @returns {JSX.Element} - The rendered group tags
  466. */
  467. export function renderGroup(group) {
  468. if (group === '') {
  469. return (
  470. <Tag size='large' key='default' color='orange' shape='circle'>
  471. {i18next.t('用户分组')}
  472. </Tag>
  473. );
  474. }
  475. const tagColors = {
  476. vip: 'yellow',
  477. pro: 'yellow',
  478. svip: 'red',
  479. premium: 'red',
  480. };
  481. const groups = group.split(',').sort();
  482. return (
  483. <span key={group}>
  484. {groups.map((group) => (
  485. <Tag
  486. size='large'
  487. color={tagColors[group] || stringToColor(group)}
  488. key={group}
  489. shape='circle'
  490. onClick={async (event) => {
  491. event.stopPropagation();
  492. if (await copy(group)) {
  493. showSuccess(i18next.t('已复制:') + group);
  494. } else {
  495. Modal.error({
  496. title: t('无法复制到剪贴板,请手动复制'),
  497. content: group,
  498. });
  499. }
  500. }}
  501. >
  502. {group}
  503. </Tag>
  504. ))}
  505. </span>
  506. );
  507. }
  508. export function renderRatio(ratio) {
  509. let color = 'green';
  510. if (ratio > 5) {
  511. color = 'red';
  512. } else if (ratio > 3) {
  513. color = 'orange';
  514. } else if (ratio > 1) {
  515. color = 'blue';
  516. }
  517. return (
  518. <Tag color={color}>
  519. {ratio}x {i18next.t('倍率')}
  520. </Tag>
  521. );
  522. }
  523. const measureTextWidth = (
  524. text,
  525. style = {
  526. fontSize: '14px',
  527. fontFamily:
  528. '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
  529. },
  530. containerWidth,
  531. ) => {
  532. const span = document.createElement('span');
  533. span.style.visibility = 'hidden';
  534. span.style.position = 'absolute';
  535. span.style.whiteSpace = 'nowrap';
  536. span.style.fontSize = style.fontSize;
  537. span.style.fontFamily = style.fontFamily;
  538. span.textContent = text;
  539. document.body.appendChild(span);
  540. const width = span.offsetWidth;
  541. document.body.removeChild(span);
  542. return width;
  543. };
  544. export function truncateText(text, maxWidth = 200) {
  545. if (!isMobile()) {
  546. return text;
  547. }
  548. if (!text) return text;
  549. try {
  550. // Handle percentage-based maxWidth
  551. let actualMaxWidth = maxWidth;
  552. if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) {
  553. const percentage = parseFloat(maxWidth) / 100;
  554. // Use window width as fallback container width
  555. actualMaxWidth = window.innerWidth * percentage;
  556. }
  557. const width = measureTextWidth(text);
  558. if (width <= actualMaxWidth) return text;
  559. let left = 0;
  560. let right = text.length;
  561. let result = text;
  562. while (left <= right) {
  563. const mid = Math.floor((left + right) / 2);
  564. const truncated = text.slice(0, mid) + '...';
  565. const currentWidth = measureTextWidth(truncated);
  566. if (currentWidth <= actualMaxWidth) {
  567. result = truncated;
  568. left = mid + 1;
  569. } else {
  570. right = mid - 1;
  571. }
  572. }
  573. return result;
  574. } catch (error) {
  575. console.warn(
  576. 'Text measurement failed, falling back to character count',
  577. error,
  578. );
  579. if (text.length > 20) {
  580. return text.slice(0, 17) + '...';
  581. }
  582. return text;
  583. }
  584. }
  585. export const renderGroupOption = (item) => {
  586. const {
  587. disabled,
  588. selected,
  589. label,
  590. value,
  591. focused,
  592. className,
  593. style,
  594. onMouseEnter,
  595. onClick,
  596. empty,
  597. emptyContent,
  598. ...rest
  599. } = item;
  600. const baseStyle = {
  601. display: 'flex',
  602. justifyContent: 'space-between',
  603. alignItems: 'center',
  604. padding: '8px 16px',
  605. cursor: disabled ? 'not-allowed' : 'pointer',
  606. backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
  607. opacity: disabled ? 0.5 : 1,
  608. ...(selected && {
  609. backgroundColor: 'var(--semi-color-primary-light-default)',
  610. }),
  611. '&:hover': {
  612. backgroundColor: !disabled && 'var(--semi-color-fill-1)',
  613. },
  614. };
  615. const handleClick = () => {
  616. if (!disabled && onClick) {
  617. onClick();
  618. }
  619. };
  620. const handleMouseEnter = (e) => {
  621. if (!disabled && onMouseEnter) {
  622. onMouseEnter(e);
  623. }
  624. };
  625. return (
  626. <div
  627. style={baseStyle}
  628. onClick={handleClick}
  629. onMouseEnter={handleMouseEnter}
  630. >
  631. <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
  632. <Typography.Text strong type={disabled ? 'tertiary' : undefined}>
  633. {value}
  634. </Typography.Text>
  635. <Typography.Text type='secondary' size='small'>
  636. {label}
  637. </Typography.Text>
  638. </div>
  639. {item.ratio && renderRatio(item.ratio)}
  640. </div>
  641. );
  642. };
  643. export function renderNumber(num) {
  644. if (num >= 1000000000) {
  645. return (num / 1000000000).toFixed(1) + 'B';
  646. } else if (num >= 1000000) {
  647. return (num / 1000000).toFixed(1) + 'M';
  648. } else if (num >= 10000) {
  649. return (num / 1000).toFixed(1) + 'k';
  650. } else {
  651. return num;
  652. }
  653. }
  654. export function renderQuotaNumberWithDigit(num, digits = 2) {
  655. if (typeof num !== 'number' || isNaN(num)) {
  656. return 0;
  657. }
  658. let displayInCurrency = localStorage.getItem('display_in_currency');
  659. num = num.toFixed(digits);
  660. if (displayInCurrency) {
  661. return '$' + num;
  662. }
  663. return num;
  664. }
  665. export function renderNumberWithPoint(num) {
  666. if (num === undefined) return '';
  667. num = num.toFixed(2);
  668. if (num >= 100000) {
  669. // Convert number to string to manipulate it
  670. let numStr = num.toString();
  671. // Find the position of the decimal point
  672. let decimalPointIndex = numStr.indexOf('.');
  673. let wholePart = numStr;
  674. let decimalPart = '';
  675. // If there is a decimal point, split the number into whole and decimal parts
  676. if (decimalPointIndex !== -1) {
  677. wholePart = numStr.slice(0, decimalPointIndex);
  678. decimalPart = numStr.slice(decimalPointIndex);
  679. }
  680. // Take the first two and last two digits of the whole number part
  681. let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
  682. // Return the formatted number
  683. return shortenedWholePart + decimalPart;
  684. }
  685. // If the number is less than 100,000, return it unmodified
  686. return num;
  687. }
  688. export function getQuotaPerUnit() {
  689. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  690. quotaPerUnit = parseFloat(quotaPerUnit);
  691. return quotaPerUnit;
  692. }
  693. export function renderUnitWithQuota(quota) {
  694. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  695. quotaPerUnit = parseFloat(quotaPerUnit);
  696. quota = parseFloat(quota);
  697. return quotaPerUnit * quota;
  698. }
  699. export function getQuotaWithUnit(quota, digits = 6) {
  700. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  701. quotaPerUnit = parseFloat(quotaPerUnit);
  702. return (quota / quotaPerUnit).toFixed(digits);
  703. }
  704. export function renderQuotaWithAmount(amount) {
  705. let displayInCurrency = localStorage.getItem('display_in_currency');
  706. displayInCurrency = displayInCurrency === 'true';
  707. if (displayInCurrency) {
  708. return '$' + amount;
  709. } else {
  710. return renderUnitWithQuota(amount);
  711. }
  712. }
  713. export function renderQuota(quota, digits = 2) {
  714. let quotaPerUnit = localStorage.getItem('quota_per_unit');
  715. let displayInCurrency = localStorage.getItem('display_in_currency');
  716. quotaPerUnit = parseFloat(quotaPerUnit);
  717. displayInCurrency = displayInCurrency === 'true';
  718. if (displayInCurrency) {
  719. return '$' + (quota / quotaPerUnit).toFixed(digits);
  720. }
  721. return renderNumber(quota);
  722. }
  723. export function renderModelPrice(
  724. inputTokens,
  725. completionTokens,
  726. modelRatio,
  727. modelPrice = -1,
  728. completionRatio,
  729. groupRatio,
  730. cacheTokens = 0,
  731. cacheRatio = 1.0,
  732. image = false,
  733. imageRatio = 1.0,
  734. imageOutputTokens = 0,
  735. webSearch = false,
  736. webSearchCallCount = 0,
  737. webSearchPrice = 0,
  738. fileSearch = false,
  739. fileSearchCallCount = 0,
  740. fileSearchPrice = 0,
  741. audioInputSeperatePrice = false,
  742. audioInputTokens = 0,
  743. audioInputPrice = 0,
  744. ) {
  745. if (modelPrice !== -1) {
  746. return i18next.t(
  747. '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
  748. {
  749. price: modelPrice,
  750. ratio: groupRatio,
  751. total: modelPrice * groupRatio,
  752. },
  753. );
  754. } else {
  755. if (completionRatio === undefined) {
  756. completionRatio = 0;
  757. }
  758. let inputRatioPrice = modelRatio * 2.0;
  759. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  760. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  761. let imageRatioPrice = modelRatio * 2.0 * imageRatio;
  762. // Calculate effective input tokens (non-cached + cached with ratio applied)
  763. let effectiveInputTokens =
  764. inputTokens - cacheTokens + cacheTokens * cacheRatio;
  765. // Handle image tokens if present
  766. if (image && imageOutputTokens > 0) {
  767. effectiveInputTokens =
  768. inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
  769. }
  770. if (audioInputTokens > 0) {
  771. effectiveInputTokens -= audioInputTokens;
  772. }
  773. let price =
  774. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  775. (audioInputTokens / 1000000) * audioInputPrice * groupRatio +
  776. (completionTokens / 1000000) * completionRatioPrice * groupRatio +
  777. (webSearchCallCount / 1000) * webSearchPrice * groupRatio +
  778. (fileSearchCallCount / 1000) * fileSearchPrice * groupRatio;
  779. return (
  780. <>
  781. <article>
  782. <p>
  783. {i18next.t('输入价格:${{price}} / 1M tokens{{audioPrice}}', {
  784. price: inputRatioPrice,
  785. audioPrice: audioInputSeperatePrice
  786. ? `,音频 $${audioInputPrice} / 1M tokens`
  787. : '',
  788. })}
  789. </p>
  790. <p>
  791. {i18next.t(
  792. '输出价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
  793. {
  794. price: inputRatioPrice,
  795. total: completionRatioPrice,
  796. completionRatio: completionRatio,
  797. },
  798. )}
  799. </p>
  800. {cacheTokens > 0 && (
  801. <p>
  802. {i18next.t(
  803. '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  804. {
  805. price: inputRatioPrice,
  806. total: inputRatioPrice * cacheRatio,
  807. cacheRatio: cacheRatio,
  808. },
  809. )}
  810. </p>
  811. )}
  812. {image && imageOutputTokens > 0 && (
  813. <p>
  814. {i18next.t(
  815. '图片输入价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (图片倍率: {{imageRatio}})',
  816. {
  817. price: imageRatioPrice,
  818. ratio: groupRatio,
  819. total: imageRatioPrice * groupRatio,
  820. imageRatio: imageRatio,
  821. },
  822. )}
  823. </p>
  824. )}
  825. {webSearch && webSearchCallCount > 0 && (
  826. <p>
  827. {i18next.t('Web搜索价格:${{price}} / 1K 次', {
  828. price: webSearchPrice,
  829. })}
  830. </p>
  831. )}
  832. {fileSearch && fileSearchCallCount > 0 && (
  833. <p>
  834. {i18next.t('文件搜索价格:${{price}} / 1K 次', {
  835. price: fileSearchPrice,
  836. })}
  837. </p>
  838. )}
  839. <p></p>
  840. <p>
  841. {(() => {
  842. // 构建输入部分描述
  843. let inputDesc = '';
  844. if (image && imageOutputTokens > 0) {
  845. inputDesc = i18next.t(
  846. '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens * {{imageRatio}} / 1M tokens * ${{price}}',
  847. {
  848. nonImageInput: inputTokens - imageOutputTokens,
  849. imageInput: imageOutputTokens,
  850. imageRatio: imageRatio,
  851. price: inputRatioPrice,
  852. },
  853. );
  854. } else if (cacheTokens > 0) {
  855. inputDesc = i18next.t(
  856. '(输入 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}}',
  857. {
  858. nonCacheInput: inputTokens - cacheTokens,
  859. cacheInput: cacheTokens,
  860. price: inputRatioPrice,
  861. cachePrice: cacheRatioPrice,
  862. },
  863. );
  864. } else if (audioInputSeperatePrice && audioInputTokens > 0) {
  865. inputDesc = i18next.t(
  866. '(输入 {{nonAudioInput}} tokens / 1M tokens * ${{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * ${{audioPrice}}',
  867. {
  868. nonAudioInput: inputTokens - audioInputTokens,
  869. audioInput: audioInputTokens,
  870. price: inputRatioPrice,
  871. audioPrice: audioInputPrice,
  872. },
  873. );
  874. } else {
  875. inputDesc = i18next.t(
  876. '(输入 {{input}} tokens / 1M tokens * ${{price}}',
  877. {
  878. input: inputTokens,
  879. price: inputRatioPrice,
  880. },
  881. );
  882. }
  883. // 构建输出部分描述
  884. const outputDesc = i18next.t(
  885. '输出 {{completion}} tokens / 1M tokens * ${{compPrice}}) * 分组倍率 {{ratio}}',
  886. {
  887. completion: completionTokens,
  888. compPrice: completionRatioPrice,
  889. ratio: groupRatio,
  890. },
  891. );
  892. // 构建额外服务描述
  893. const extraServices = [
  894. webSearch && webSearchCallCount > 0
  895. ? i18next.t(
  896. ' + Web搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
  897. {
  898. count: webSearchCallCount,
  899. price: webSearchPrice,
  900. ratio: groupRatio,
  901. },
  902. )
  903. : '',
  904. fileSearch && fileSearchCallCount > 0
  905. ? i18next.t(
  906. ' + 文件搜索 {{count}}次 / 1K 次 * ${{price}} * 分组倍率 {{ratio}}',
  907. {
  908. count: fileSearchCallCount,
  909. price: fileSearchPrice,
  910. ratio: groupRatio,
  911. },
  912. )
  913. : '',
  914. ].join('');
  915. return i18next.t(
  916. '{{inputDesc}} + {{outputDesc}}{{extraServices}} = ${{total}}',
  917. {
  918. inputDesc,
  919. outputDesc,
  920. extraServices,
  921. total: price.toFixed(6),
  922. },
  923. );
  924. })()}
  925. </p>
  926. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  927. </article>
  928. </>
  929. );
  930. }
  931. }
  932. export function renderLogContent(
  933. modelRatio,
  934. completionRatio,
  935. modelPrice = -1,
  936. groupRatio,
  937. user_group_ratio,
  938. image = false,
  939. imageRatio = 1.0,
  940. useUserGroupRatio = undefined,
  941. webSearch = false,
  942. webSearchCallCount = 0,
  943. fileSearch = false,
  944. fileSearchCallCount = 0,
  945. ) {
  946. const ratioLabel = useUserGroupRatio
  947. ? i18next.t('专属倍率')
  948. : i18next.t('分组倍率');
  949. const ratio = useUserGroupRatio ? user_group_ratio : groupRatio;
  950. if (modelPrice !== -1) {
  951. return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
  952. price: modelPrice,
  953. ratioType: ratioLabel,
  954. ratio,
  955. });
  956. } else {
  957. if (image) {
  958. return i18next.t(
  959. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},图片输入倍率 {{imageRatio}},{{ratioType}} {{ratio}}',
  960. {
  961. modelRatio: modelRatio,
  962. completionRatio: completionRatio,
  963. imageRatio: imageRatio,
  964. ratioType: ratioLabel,
  965. ratio,
  966. },
  967. );
  968. } else if (webSearch) {
  969. return i18next.t(
  970. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}},Web 搜索调用 {{webSearchCallCount}} 次',
  971. {
  972. modelRatio: modelRatio,
  973. completionRatio: completionRatio,
  974. ratioType: ratioLabel,
  975. ratio,
  976. webSearchCallCount,
  977. },
  978. );
  979. } else {
  980. return i18next.t(
  981. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},{{ratioType}} {{ratio}}',
  982. {
  983. modelRatio: modelRatio,
  984. completionRatio: completionRatio,
  985. ratioType: ratioLabel,
  986. ratio,
  987. },
  988. );
  989. }
  990. }
  991. }
  992. export function renderModelPriceSimple(
  993. modelRatio,
  994. modelPrice = -1,
  995. groupRatio,
  996. cacheTokens = 0,
  997. cacheRatio = 1.0,
  998. image = false,
  999. imageRatio = 1.0,
  1000. ) {
  1001. if (modelPrice !== -1) {
  1002. return i18next.t('价格:${{price}} * 分组:{{ratio}}', {
  1003. price: modelPrice,
  1004. ratio: groupRatio,
  1005. });
  1006. } else {
  1007. if (image && cacheTokens !== 0) {
  1008. return i18next.t(
  1009. '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存倍率: {{cacheRatio}} * 图片输入倍率: {{imageRatio}}',
  1010. {
  1011. ratio: modelRatio,
  1012. ratioType: ratioLabel,
  1013. groupRatio: groupRatio,
  1014. cacheRatio: cacheRatio,
  1015. imageRatio: imageRatio,
  1016. },
  1017. );
  1018. } else if (image) {
  1019. return i18next.t(
  1020. '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 图片输入倍率: {{imageRatio}}',
  1021. {
  1022. ratio: modelRatio,
  1023. ratioType: ratioLabel,
  1024. groupRatio: groupRatio,
  1025. imageRatio: imageRatio,
  1026. },
  1027. );
  1028. } else if (cacheTokens !== 0) {
  1029. return i18next.t(
  1030. '模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}',
  1031. {
  1032. ratio: modelRatio,
  1033. groupRatio: groupRatio,
  1034. cacheRatio: cacheRatio,
  1035. },
  1036. );
  1037. } else {
  1038. return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', {
  1039. ratio: modelRatio,
  1040. groupRatio: groupRatio,
  1041. });
  1042. }
  1043. }
  1044. }
  1045. export function renderAudioModelPrice(
  1046. inputTokens,
  1047. completionTokens,
  1048. modelRatio,
  1049. modelPrice = -1,
  1050. completionRatio,
  1051. audioInputTokens,
  1052. audioCompletionTokens,
  1053. audioRatio,
  1054. audioCompletionRatio,
  1055. groupRatio,
  1056. cacheTokens = 0,
  1057. cacheRatio = 1.0,
  1058. ) {
  1059. // 1 ratio = $0.002 / 1K tokens
  1060. if (modelPrice !== -1) {
  1061. return i18next.t(
  1062. '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}',
  1063. {
  1064. price: modelPrice,
  1065. ratio: groupRatio,
  1066. total: modelPrice * groupRatio,
  1067. },
  1068. );
  1069. } else {
  1070. if (completionRatio === undefined) {
  1071. completionRatio = 0;
  1072. }
  1073. // try toFixed audioRatio
  1074. audioRatio = parseFloat(audioRatio).toFixed(6);
  1075. // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
  1076. let inputRatioPrice = modelRatio * 2.0;
  1077. let completionRatioPrice = modelRatio * 2.0 * completionRatio;
  1078. let cacheRatioPrice = modelRatio * 2.0 * cacheRatio;
  1079. // Calculate effective input tokens (non-cached + cached with ratio applied)
  1080. const effectiveInputTokens =
  1081. inputTokens - cacheTokens + cacheTokens * cacheRatio;
  1082. let textPrice =
  1083. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  1084. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  1085. let audioPrice =
  1086. (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
  1087. (audioCompletionTokens / 1000000) *
  1088. inputRatioPrice *
  1089. audioRatio *
  1090. audioCompletionRatio *
  1091. groupRatio;
  1092. let price = textPrice + audioPrice;
  1093. return (
  1094. <>
  1095. <article>
  1096. <p>
  1097. {i18next.t('提示价格:${{price}} / 1M tokens', {
  1098. price: inputRatioPrice,
  1099. })}
  1100. </p>
  1101. <p>
  1102. {i18next.t(
  1103. '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})',
  1104. {
  1105. price: inputRatioPrice,
  1106. total: completionRatioPrice,
  1107. completionRatio: completionRatio,
  1108. },
  1109. )}
  1110. </p>
  1111. {cacheTokens > 0 && (
  1112. <p>
  1113. {i18next.t(
  1114. '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  1115. {
  1116. price: inputRatioPrice,
  1117. total: inputRatioPrice * cacheRatio,
  1118. cacheRatio: cacheRatio,
  1119. },
  1120. )}
  1121. </p>
  1122. )}
  1123. <p>
  1124. {i18next.t(
  1125. '音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})',
  1126. {
  1127. price: inputRatioPrice,
  1128. total: inputRatioPrice * audioRatio,
  1129. audioRatio: audioRatio,
  1130. },
  1131. )}
  1132. </p>
  1133. <p>
  1134. {i18next.t(
  1135. '音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})',
  1136. {
  1137. price: inputRatioPrice,
  1138. total: inputRatioPrice * audioRatio * audioCompletionRatio,
  1139. audioRatio: audioRatio,
  1140. audioCompRatio: audioCompletionRatio,
  1141. },
  1142. )}
  1143. </p>
  1144. <p>
  1145. {cacheTokens > 0
  1146. ? i18next.t(
  1147. '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
  1148. {
  1149. nonCacheInput: inputTokens - cacheTokens,
  1150. cacheInput: cacheTokens,
  1151. cachePrice: inputRatioPrice * cacheRatio,
  1152. price: inputRatioPrice,
  1153. completion: completionTokens,
  1154. compPrice: completionRatioPrice,
  1155. total: textPrice.toFixed(6),
  1156. },
  1157. )
  1158. : i18next.t(
  1159. '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}',
  1160. {
  1161. input: inputTokens,
  1162. price: inputRatioPrice,
  1163. completion: completionTokens,
  1164. compPrice: completionRatioPrice,
  1165. total: textPrice.toFixed(6),
  1166. },
  1167. )}
  1168. </p>
  1169. <p>
  1170. {i18next.t(
  1171. '音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}',
  1172. {
  1173. input: audioInputTokens,
  1174. completion: audioCompletionTokens,
  1175. audioInputPrice: audioRatio * inputRatioPrice,
  1176. audioCompPrice:
  1177. audioRatio * audioCompletionRatio * inputRatioPrice,
  1178. total: audioPrice.toFixed(6),
  1179. },
  1180. )}
  1181. </p>
  1182. <p>
  1183. {i18next.t(
  1184. '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}',
  1185. {
  1186. total: price.toFixed(6),
  1187. textPrice: textPrice.toFixed(6),
  1188. audioPrice: audioPrice.toFixed(6),
  1189. },
  1190. )}
  1191. </p>
  1192. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  1193. </article>
  1194. </>
  1195. );
  1196. }
  1197. }
  1198. export function renderQuotaWithPrompt(quota, digits) {
  1199. let displayInCurrency = localStorage.getItem('display_in_currency');
  1200. displayInCurrency = displayInCurrency === 'true';
  1201. if (displayInCurrency) {
  1202. return (
  1203. ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + ''
  1204. );
  1205. }
  1206. return '';
  1207. }
  1208. export function renderClaudeModelPrice(
  1209. inputTokens,
  1210. completionTokens,
  1211. modelRatio,
  1212. modelPrice = -1,
  1213. completionRatio,
  1214. groupRatio,
  1215. cacheTokens = 0,
  1216. cacheRatio = 1.0,
  1217. cacheCreationTokens = 0,
  1218. cacheCreationRatio = 1.0,
  1219. ) {
  1220. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
  1221. if (modelPrice !== -1) {
  1222. return i18next.t(
  1223. '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}',
  1224. {
  1225. price: modelPrice,
  1226. ratioType: ratioLabel,
  1227. ratio: groupRatio,
  1228. total: modelPrice * groupRatio,
  1229. },
  1230. );
  1231. } else {
  1232. if (completionRatio === undefined) {
  1233. completionRatio = 0;
  1234. }
  1235. const completionRatioValue = completionRatio || 0;
  1236. const inputRatioPrice = modelRatio * 2.0;
  1237. const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;
  1238. let cacheRatioPrice = (modelRatio * 2.0 * cacheRatio).toFixed(2);
  1239. let cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio;
  1240. // Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied)
  1241. const nonCachedTokens = inputTokens;
  1242. const effectiveInputTokens =
  1243. nonCachedTokens +
  1244. cacheTokens * cacheRatio +
  1245. cacheCreationTokens * cacheCreationRatio;
  1246. let price =
  1247. (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
  1248. (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  1249. return (
  1250. <>
  1251. <article>
  1252. <p>
  1253. {i18next.t('提示价格:${{price}} / 1M tokens', {
  1254. price: inputRatioPrice,
  1255. })}
  1256. </p>
  1257. <p>
  1258. {i18next.t(
  1259. '补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens',
  1260. {
  1261. price: inputRatioPrice,
  1262. ratio: completionRatio,
  1263. total: completionRatioPrice,
  1264. },
  1265. )}
  1266. </p>
  1267. {cacheTokens > 0 && (
  1268. <p>
  1269. {i18next.t(
  1270. '缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})',
  1271. {
  1272. price: inputRatioPrice,
  1273. ratio: cacheRatio,
  1274. total: cacheRatioPrice,
  1275. cacheRatio: cacheRatio,
  1276. },
  1277. )}
  1278. </p>
  1279. )}
  1280. {cacheCreationTokens > 0 && (
  1281. <p>
  1282. {i18next.t(
  1283. '缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})',
  1284. {
  1285. price: inputRatioPrice,
  1286. ratio: cacheCreationRatio,
  1287. total: cacheCreationRatioPrice,
  1288. cacheCreationRatio: cacheCreationRatio,
  1289. },
  1290. )}
  1291. </p>
  1292. )}
  1293. <p></p>
  1294. <p>
  1295. {cacheTokens > 0 || cacheCreationTokens > 0
  1296. ? i18next.t(
  1297. '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  1298. {
  1299. nonCacheInput: nonCachedTokens,
  1300. cacheInput: cacheTokens,
  1301. cacheRatio: cacheRatio,
  1302. cacheCreationInput: cacheCreationTokens,
  1303. cacheCreationRatio: cacheCreationRatio,
  1304. cachePrice: cacheRatioPrice,
  1305. cacheCreationPrice: cacheCreationRatioPrice,
  1306. price: inputRatioPrice,
  1307. completion: completionTokens,
  1308. compPrice: completionRatioPrice,
  1309. ratio: groupRatio,
  1310. total: price.toFixed(6),
  1311. },
  1312. )
  1313. : i18next.t(
  1314. '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}',
  1315. {
  1316. input: inputTokens,
  1317. price: inputRatioPrice,
  1318. completion: completionTokens,
  1319. compPrice: completionRatioPrice,
  1320. ratio: groupRatio,
  1321. total: price.toFixed(6),
  1322. },
  1323. )}
  1324. </p>
  1325. <p>{i18next.t('仅供参考,以实际扣费为准')}</p>
  1326. </article>
  1327. </>
  1328. );
  1329. }
  1330. }
  1331. export function renderClaudeLogContent(
  1332. modelRatio,
  1333. completionRatio,
  1334. modelPrice = -1,
  1335. groupRatio,
  1336. cacheRatio = 1.0,
  1337. cacheCreationRatio = 1.0,
  1338. ) {
  1339. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率');
  1340. if (modelPrice !== -1) {
  1341. return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', {
  1342. price: modelPrice,
  1343. ratioType: ratioLabel,
  1344. ratio: groupRatio,
  1345. });
  1346. } else {
  1347. return i18next.t(
  1348. '模型倍率 {{modelRatio}},输出倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}',
  1349. {
  1350. modelRatio: modelRatio,
  1351. completionRatio: completionRatio,
  1352. cacheRatio: cacheRatio,
  1353. cacheCreationRatio: cacheCreationRatio,
  1354. ratioType: ratioLabel,
  1355. ratio: groupRatio,
  1356. },
  1357. );
  1358. }
  1359. }
  1360. export function renderClaudeModelPriceSimple(
  1361. modelRatio,
  1362. modelPrice = -1,
  1363. groupRatio,
  1364. cacheTokens = 0,
  1365. cacheRatio = 1.0,
  1366. cacheCreationTokens = 0,
  1367. cacheCreationRatio = 1.0,
  1368. ) {
  1369. const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组');
  1370. if (modelPrice !== -1) {
  1371. return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', {
  1372. price: modelPrice,
  1373. ratioType: ratioLabel,
  1374. ratio: groupRatio,
  1375. });
  1376. } else {
  1377. if (cacheTokens !== 0 || cacheCreationTokens !== 0) {
  1378. return i18next.t(
  1379. '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}',
  1380. {
  1381. ratio: modelRatio,
  1382. ratioType: ratioLabel,
  1383. groupRatio: groupRatio,
  1384. cacheRatio: cacheRatio,
  1385. cacheCreationRatio: cacheCreationRatio,
  1386. },
  1387. );
  1388. } else {
  1389. return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', {
  1390. ratio: modelRatio,
  1391. ratioType: ratioLabel,
  1392. groupRatio: groupRatio,
  1393. });
  1394. }
  1395. }
  1396. }
  1397. /**
  1398. * rehype 插件:将段落等文本节点拆分为逐词 <span>,并添加淡入动画 class。
  1399. * 仅在流式渲染阶段使用,避免已渲染文字重复动画。
  1400. */
  1401. export function rehypeSplitWordsIntoSpans(options = {}) {
  1402. const { previousContentLength = 0 } = options;
  1403. return (tree) => {
  1404. let currentCharCount = 0; // 当前已处理的字符数
  1405. visit(tree, 'element', (node) => {
  1406. if (
  1407. ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(
  1408. node.tagName,
  1409. ) &&
  1410. node.children
  1411. ) {
  1412. const newChildren = [];
  1413. node.children.forEach((child) => {
  1414. if (child.type === 'text') {
  1415. try {
  1416. // 使用 Intl.Segmenter 精准拆分中英文及标点
  1417. const segmenter = new Intl.Segmenter('zh', {
  1418. granularity: 'word',
  1419. });
  1420. const segments = segmenter.segment(child.value);
  1421. Array.from(segments)
  1422. .map((seg) => seg.segment)
  1423. .filter(Boolean)
  1424. .forEach((word) => {
  1425. const wordStartPos = currentCharCount;
  1426. const wordEndPos = currentCharCount + word.length;
  1427. // 判断这个词是否是新增的(在 previousContentLength 之后)
  1428. const isNewContent = wordStartPos >= previousContentLength;
  1429. newChildren.push({
  1430. type: 'element',
  1431. tagName: 'span',
  1432. properties: {
  1433. className: isNewContent ? ['animate-fade-in'] : [],
  1434. },
  1435. children: [{ type: 'text', value: word }],
  1436. });
  1437. currentCharCount = wordEndPos;
  1438. });
  1439. } catch (_) {
  1440. // Fallback:如果浏览器不支持 Segmenter
  1441. const textStartPos = currentCharCount;
  1442. const isNewContent = textStartPos >= previousContentLength;
  1443. if (isNewContent) {
  1444. // 新内容,添加动画
  1445. newChildren.push({
  1446. type: 'element',
  1447. tagName: 'span',
  1448. properties: {
  1449. className: ['animate-fade-in'],
  1450. },
  1451. children: [{ type: 'text', value: child.value }],
  1452. });
  1453. } else {
  1454. // 旧内容,不添加动画
  1455. newChildren.push(child);
  1456. }
  1457. currentCharCount += child.value.length;
  1458. }
  1459. } else {
  1460. newChildren.push(child);
  1461. }
  1462. });
  1463. node.children = newChildren;
  1464. }
  1465. });
  1466. };
  1467. }