render.js 50 KB

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