render.js 51 KB

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