/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useState, useCallback, useEffect } from 'react'; import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import { modelColorMap, renderNumber, renderQuota, modelToColor, getQuotaWithUnit, } from '../../helpers'; import { processRawData, calculateTrendData, aggregateDataByTimeAndModel, generateChartTimePoints, updateChartSpec, updateMapValue, initializeMaps, } from '../../helpers/dashboard'; export const useDashboardCharts = ( dataExportDefaultTime, setTrendData, setConsumeQuota, setTimes, setConsumeTokens, setPieData, setLineData, setModelColors, t, ) => { // ========== 图表规格状态 ========== const [spec_pie, setSpecPie] = useState({ type: 'pie', data: [ { id: 'id0', values: [{ type: 'null', value: '0' }], }, ], outerRadius: 0.8, innerRadius: 0.5, padAngle: 0.6, valueField: 'value', categoryField: 'type', pie: { style: { cornerRadius: 10, }, state: { hover: { outerRadius: 0.85, stroke: '#000', lineWidth: 1, }, selected: { outerRadius: 0.85, stroke: '#000', lineWidth: 1, }, }, }, title: { visible: true, text: t('模型调用次数占比'), subtext: `${t('总计')}:${renderNumber(0)}`, }, legends: { visible: true, orient: 'left', }, label: { visible: true, }, tooltip: { mark: { content: [ { key: (datum) => datum['type'], value: (datum) => renderNumber(datum['value']), }, ], }, }, color: { specified: modelColorMap, }, }); const [spec_line, setSpecLine] = useState({ type: 'bar', data: [ { id: 'barData', values: [], }, ], xField: 'Time', yField: 'Usage', seriesField: 'Model', stack: true, legends: { visible: true, selectMode: 'single', }, title: { visible: true, text: t('模型消耗分布'), subtext: `${t('总计')}:${renderQuota(0, 2)}`, }, bar: { state: { hover: { stroke: '#000', lineWidth: 1, }, }, }, tooltip: { mark: { content: [ { key: (datum) => datum['Model'], value: (datum) => renderQuota(datum['rawQuota'] || 0, 4), }, ], }, dimension: { content: [ { key: (datum) => datum['Model'], value: (datum) => datum['rawQuota'] || 0, }, ], updateContent: (array) => { array.sort((a, b) => b.value - a.value); let sum = 0; for (let i = 0; i < array.length; i++) { if (array[i].key == '其他') { continue; } let value = parseFloat(array[i].value); if (isNaN(value)) { value = 0; } if (array[i].datum && array[i].datum.TimeSum) { sum = array[i].datum.TimeSum; } array[i].value = renderQuota(value, 4); } array.unshift({ key: t('总计'), value: renderQuota(sum, 4), }); return array; }, }, }, color: { specified: modelColorMap, }, }); // 模型消耗趋势折线图 const [spec_model_line, setSpecModelLine] = useState({ type: 'line', data: [ { id: 'lineData', values: [], }, ], xField: 'Time', yField: 'Count', seriesField: 'Model', legends: { visible: true, selectMode: 'single', }, title: { visible: true, text: t('模型消耗趋势'), subtext: '', }, tooltip: { mark: { content: [ { key: (datum) => datum['Model'], value: (datum) => renderNumber(datum['Count']), }, ], }, }, color: { specified: modelColorMap, }, }); // 模型调用次数排行柱状图 const [spec_rank_bar, setSpecRankBar] = useState({ type: 'bar', data: [ { id: 'rankData', values: [], }, ], xField: 'Model', yField: 'Count', seriesField: 'Model', legends: { visible: true, selectMode: 'single', }, title: { visible: true, text: t('模型调用次数排行'), subtext: '', }, bar: { state: { hover: { stroke: '#000', lineWidth: 1, }, }, }, tooltip: { mark: { content: [ { key: (datum) => datum['Model'], value: (datum) => renderNumber(datum['Count']), }, ], }, }, color: { specified: modelColorMap, }, }); // ========== 数据处理函数 ========== const generateModelColors = useCallback((uniqueModels, modelColors) => { const newModelColors = {}; Array.from(uniqueModels).forEach((modelName) => { newModelColors[modelName] = modelColorMap[modelName] || modelColors[modelName] || modelToColor(modelName); }); return newModelColors; }, []); const updateChartData = useCallback( (data) => { const processedData = processRawData( data, dataExportDefaultTime, initializeMaps, updateMapValue, ); const { totalQuota, totalTimes, totalTokens, uniqueModels, timePoints, timeQuotaMap, timeTokensMap, timeCountMap, } = processedData; const trendDataResult = calculateTrendData( timePoints, timeQuotaMap, timeTokensMap, timeCountMap, dataExportDefaultTime, ); setTrendData(trendDataResult); const newModelColors = generateModelColors(uniqueModels, {}); setModelColors(newModelColors); const aggregatedData = aggregateDataByTimeAndModel( data, dataExportDefaultTime, ); const modelTotals = new Map(); for (let [_, value] of aggregatedData) { updateMapValue(modelTotals, value.model, value.count); } const newPieData = Array.from(modelTotals) .map(([model, count]) => ({ type: model, value: count, })) .sort((a, b) => b.value - a.value); const chartTimePoints = generateChartTimePoints( aggregatedData, data, dataExportDefaultTime, ); let newLineData = []; chartTimePoints.forEach((time) => { let timeData = Array.from(uniqueModels).map((model) => { const key = `${time}-${model}`; const aggregated = aggregatedData.get(key); return { Time: time, Model: model, rawQuota: aggregated?.quota || 0, Usage: aggregated?.quota ? getQuotaWithUnit(aggregated.quota, 4) : 0, }; }); const timeSum = timeData.reduce((sum, item) => sum + item.rawQuota, 0); timeData.sort((a, b) => b.rawQuota - a.rawQuota); timeData = timeData.map((item) => ({ ...item, TimeSum: timeSum })); newLineData.push(...timeData); }); newLineData.sort((a, b) => a.Time.localeCompare(b.Time)); updateChartSpec( setSpecPie, newPieData, `${t('总计')}:${renderNumber(totalTimes)}`, newModelColors, 'id0', ); updateChartSpec( setSpecLine, newLineData, `${t('总计')}:${renderQuota(totalQuota, 2)}`, newModelColors, 'barData', ); // ===== 模型调用次数折线图 ===== let modelLineData = []; chartTimePoints.forEach((time) => { const timeData = Array.from(uniqueModels).map((model) => { const key = `${time}-${model}`; const aggregated = aggregatedData.get(key); return { Time: time, Model: model, Count: aggregated?.count || 0, }; }); modelLineData.push(...timeData); }); modelLineData.sort((a, b) => a.Time.localeCompare(b.Time)); // ===== 模型调用次数排行柱状图 ===== const rankData = Array.from(modelTotals) .map(([model, count]) => ({ Model: model, Count: count, })) .sort((a, b) => b.Count - a.Count); updateChartSpec( setSpecModelLine, modelLineData, `${t('总计')}:${renderNumber(totalTimes)}`, newModelColors, 'lineData', ); updateChartSpec( setSpecRankBar, rankData, `${t('总计')}:${renderNumber(totalTimes)}`, newModelColors, 'rankData', ); setPieData(newPieData); setLineData(newLineData); setConsumeQuota(totalQuota); setTimes(totalTimes); setConsumeTokens(totalTokens); }, [ dataExportDefaultTime, setTrendData, generateModelColors, setModelColors, setPieData, setLineData, setConsumeQuota, setTimes, setConsumeTokens, t, ], ); // ========== 初始化图表主题 ========== useEffect(() => { initVChartSemiTheme({ isWatchingThemeSwitch: true, }); }, []); return { // 图表规格 spec_pie, spec_line, spec_model_line, spec_rank_bar, // 函数 updateChartData, generateModelColors, }; };