useDashboardCharts.jsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  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 { useState, useCallback, useEffect } from 'react';
  16. import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
  17. import {
  18. modelColorMap,
  19. renderNumber,
  20. renderQuota,
  21. modelToColor,
  22. getQuotaWithUnit,
  23. } from '../../helpers';
  24. import {
  25. processRawData,
  26. calculateTrendData,
  27. aggregateDataByTimeAndModel,
  28. generateChartTimePoints,
  29. updateChartSpec,
  30. updateMapValue,
  31. initializeMaps,
  32. } from '../../helpers/dashboard';
  33. export const useDashboardCharts = (
  34. dataExportDefaultTime,
  35. setTrendData,
  36. setConsumeQuota,
  37. setTimes,
  38. setConsumeTokens,
  39. setPieData,
  40. setLineData,
  41. setModelColors,
  42. t,
  43. ) => {
  44. // ========== 图表规格状态 ==========
  45. const [spec_pie, setSpecPie] = useState({
  46. type: 'pie',
  47. data: [
  48. {
  49. id: 'id0',
  50. values: [{ type: 'null', value: '0' }],
  51. },
  52. ],
  53. outerRadius: 0.8,
  54. innerRadius: 0.5,
  55. padAngle: 0.6,
  56. valueField: 'value',
  57. categoryField: 'type',
  58. pie: {
  59. style: {
  60. cornerRadius: 10,
  61. },
  62. state: {
  63. hover: {
  64. outerRadius: 0.85,
  65. stroke: '#000',
  66. lineWidth: 1,
  67. },
  68. selected: {
  69. outerRadius: 0.85,
  70. stroke: '#000',
  71. lineWidth: 1,
  72. },
  73. },
  74. },
  75. title: {
  76. visible: true,
  77. text: t('模型调用次数占比'),
  78. subtext: `${t('总计')}:${renderNumber(0)}`,
  79. },
  80. legends: {
  81. visible: true,
  82. orient: 'left',
  83. },
  84. label: {
  85. visible: true,
  86. },
  87. tooltip: {
  88. mark: {
  89. content: [
  90. {
  91. key: (datum) => datum['type'],
  92. value: (datum) => renderNumber(datum['value']),
  93. },
  94. ],
  95. },
  96. },
  97. color: {
  98. specified: modelColorMap,
  99. },
  100. });
  101. const [spec_line, setSpecLine] = useState({
  102. type: 'bar',
  103. data: [
  104. {
  105. id: 'barData',
  106. values: [],
  107. },
  108. ],
  109. xField: 'Time',
  110. yField: 'Usage',
  111. seriesField: 'Model',
  112. stack: true,
  113. legends: {
  114. visible: true,
  115. selectMode: 'single',
  116. },
  117. title: {
  118. visible: true,
  119. text: t('模型消耗分布'),
  120. subtext: `${t('总计')}:${renderQuota(0, 2)}`,
  121. },
  122. bar: {
  123. state: {
  124. hover: {
  125. stroke: '#000',
  126. lineWidth: 1,
  127. },
  128. },
  129. },
  130. tooltip: {
  131. mark: {
  132. content: [
  133. {
  134. key: (datum) => datum['Model'],
  135. value: (datum) => renderQuota(datum['rawQuota'] || 0, 4),
  136. },
  137. ],
  138. },
  139. dimension: {
  140. content: [
  141. {
  142. key: (datum) => datum['Model'],
  143. value: (datum) => datum['rawQuota'] || 0,
  144. },
  145. ],
  146. updateContent: (array) => {
  147. array.sort((a, b) => b.value - a.value);
  148. let sum = 0;
  149. for (let i = 0; i < array.length; i++) {
  150. if (array[i].key == '其他') {
  151. continue;
  152. }
  153. let value = parseFloat(array[i].value);
  154. if (isNaN(value)) {
  155. value = 0;
  156. }
  157. if (array[i].datum && array[i].datum.TimeSum) {
  158. sum = array[i].datum.TimeSum;
  159. }
  160. array[i].value = renderQuota(value, 4);
  161. }
  162. array.unshift({
  163. key: t('总计'),
  164. value: renderQuota(sum, 4),
  165. });
  166. return array;
  167. },
  168. },
  169. },
  170. color: {
  171. specified: modelColorMap,
  172. },
  173. });
  174. // 模型消耗趋势折线图
  175. const [spec_model_line, setSpecModelLine] = useState({
  176. type: 'line',
  177. data: [
  178. {
  179. id: 'lineData',
  180. values: [],
  181. },
  182. ],
  183. xField: 'Time',
  184. yField: 'Count',
  185. seriesField: 'Model',
  186. legends: {
  187. visible: true,
  188. selectMode: 'single',
  189. },
  190. title: {
  191. visible: true,
  192. text: t('模型消耗趋势'),
  193. subtext: '',
  194. },
  195. tooltip: {
  196. mark: {
  197. content: [
  198. {
  199. key: (datum) => datum['Model'],
  200. value: (datum) => renderNumber(datum['Count']),
  201. },
  202. ],
  203. },
  204. },
  205. color: {
  206. specified: modelColorMap,
  207. },
  208. });
  209. // 模型调用次数排行柱状图
  210. const [spec_rank_bar, setSpecRankBar] = useState({
  211. type: 'bar',
  212. data: [
  213. {
  214. id: 'rankData',
  215. values: [],
  216. },
  217. ],
  218. xField: 'Model',
  219. yField: 'Count',
  220. seriesField: 'Model',
  221. legends: {
  222. visible: true,
  223. selectMode: 'single',
  224. },
  225. title: {
  226. visible: true,
  227. text: t('模型调用次数排行'),
  228. subtext: '',
  229. },
  230. bar: {
  231. state: {
  232. hover: {
  233. stroke: '#000',
  234. lineWidth: 1,
  235. },
  236. },
  237. },
  238. tooltip: {
  239. mark: {
  240. content: [
  241. {
  242. key: (datum) => datum['Model'],
  243. value: (datum) => renderNumber(datum['Count']),
  244. },
  245. ],
  246. },
  247. },
  248. color: {
  249. specified: modelColorMap,
  250. },
  251. });
  252. // ========== 数据处理函数 ==========
  253. const generateModelColors = useCallback((uniqueModels, modelColors) => {
  254. const newModelColors = {};
  255. Array.from(uniqueModels).forEach((modelName) => {
  256. newModelColors[modelName] =
  257. modelColorMap[modelName] ||
  258. modelColors[modelName] ||
  259. modelToColor(modelName);
  260. });
  261. return newModelColors;
  262. }, []);
  263. const updateChartData = useCallback(
  264. (data) => {
  265. const processedData = processRawData(
  266. data,
  267. dataExportDefaultTime,
  268. initializeMaps,
  269. updateMapValue,
  270. );
  271. const {
  272. totalQuota,
  273. totalTimes,
  274. totalTokens,
  275. uniqueModels,
  276. timePoints,
  277. timeQuotaMap,
  278. timeTokensMap,
  279. timeCountMap,
  280. } = processedData;
  281. const trendDataResult = calculateTrendData(
  282. timePoints,
  283. timeQuotaMap,
  284. timeTokensMap,
  285. timeCountMap,
  286. dataExportDefaultTime,
  287. );
  288. setTrendData(trendDataResult);
  289. const newModelColors = generateModelColors(uniqueModels, {});
  290. setModelColors(newModelColors);
  291. const aggregatedData = aggregateDataByTimeAndModel(
  292. data,
  293. dataExportDefaultTime,
  294. );
  295. const modelTotals = new Map();
  296. for (let [_, value] of aggregatedData) {
  297. updateMapValue(modelTotals, value.model, value.count);
  298. }
  299. const newPieData = Array.from(modelTotals)
  300. .map(([model, count]) => ({
  301. type: model,
  302. value: count,
  303. }))
  304. .sort((a, b) => b.value - a.value);
  305. const chartTimePoints = generateChartTimePoints(
  306. aggregatedData,
  307. data,
  308. dataExportDefaultTime,
  309. );
  310. let newLineData = [];
  311. chartTimePoints.forEach((time) => {
  312. let timeData = Array.from(uniqueModels).map((model) => {
  313. const key = `${time}-${model}`;
  314. const aggregated = aggregatedData.get(key);
  315. return {
  316. Time: time,
  317. Model: model,
  318. rawQuota: aggregated?.quota || 0,
  319. Usage: aggregated?.quota
  320. ? getQuotaWithUnit(aggregated.quota, 4)
  321. : 0,
  322. };
  323. });
  324. const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0);
  325. timeData.sort((a, b) => b.rawQuota - a.rawQuota);
  326. timeData = timeData.map((item) => ({ ...item, TimeSum: timeSum }));
  327. newLineData.push(...timeData);
  328. });
  329. newLineData.sort((a, b) => a.Time.localeCompare(b.Time));
  330. updateChartSpec(
  331. setSpecPie,
  332. newPieData,
  333. `${t('总计')}:${renderNumber(totalTimes)}`,
  334. newModelColors,
  335. 'id0',
  336. );
  337. updateChartSpec(
  338. setSpecLine,
  339. newLineData,
  340. `${t('总计')}:${renderQuota(totalQuota, 2)}`,
  341. newModelColors,
  342. 'barData',
  343. );
  344. // ===== 模型调用次数折线图 =====
  345. let modelLineData = [];
  346. chartTimePoints.forEach((time) => {
  347. const timeData = Array.from(uniqueModels).map((model) => {
  348. const key = `${time}-${model}`;
  349. const aggregated = aggregatedData.get(key);
  350. return {
  351. Time: time,
  352. Model: model,
  353. Count: aggregated?.count || 0,
  354. };
  355. });
  356. modelLineData.push(...timeData);
  357. });
  358. modelLineData.sort((a, b) => a.Time.localeCompare(b.Time));
  359. // ===== 模型调用次数排行柱状图 =====
  360. const rankData = Array.from(modelTotals)
  361. .map(([model, count]) => ({
  362. Model: model,
  363. Count: count,
  364. }))
  365. .sort((a, b) => b.Count - a.Count);
  366. updateChartSpec(
  367. setSpecModelLine,
  368. modelLineData,
  369. `${t('总计')}:${renderNumber(totalTimes)}`,
  370. newModelColors,
  371. 'lineData',
  372. );
  373. updateChartSpec(
  374. setSpecRankBar,
  375. rankData,
  376. `${t('总计')}:${renderNumber(totalTimes)}`,
  377. newModelColors,
  378. 'rankData',
  379. );
  380. setPieData(newPieData);
  381. setLineData(newLineData);
  382. setConsumeQuota(totalQuota);
  383. setTimes(totalTimes);
  384. setConsumeTokens(totalTokens);
  385. },
  386. [
  387. dataExportDefaultTime,
  388. setTrendData,
  389. generateModelColors,
  390. setModelColors,
  391. setPieData,
  392. setLineData,
  393. setConsumeQuota,
  394. setTimes,
  395. setConsumeTokens,
  396. t,
  397. ],
  398. );
  399. // ========== 初始化图表主题 ==========
  400. useEffect(() => {
  401. initVChartSemiTheme({
  402. isWatchingThemeSwitch: true,
  403. });
  404. }, []);
  405. return {
  406. // 图表规格
  407. spec_pie,
  408. spec_line,
  409. spec_model_line,
  410. spec_rank_bar,
  411. // 函数
  412. updateChartData,
  413. generateModelColors,
  414. };
  415. };