import React, { useEffect, useState, useRef, useCallback } from 'react'; import { request as invoke } from '../utils/request'; import { useTranslation } from 'react-i18next'; import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts'; import { Clock, Calendar, CalendarDays, Users, Zap, TrendingUp, RefreshCw, Cpu } from 'lucide-react'; interface TokenStatsAggregated { period: string; total_input_tokens: number; total_output_tokens: number; total_tokens: number; request_count: number; } interface AccountTokenStats { account_email: string; total_input_tokens: number; total_output_tokens: number; total_tokens: number; request_count: number; } interface ModelTokenStats { model: string; total_input_tokens: number; total_output_tokens: number; total_tokens: number; request_count: number; } interface ModelTrendPoint { period: string; model_data: Record; } interface AccountTrendPoint { period: string; account_data: Record; } interface TokenStatsSummary { total_input_tokens: number; total_output_tokens: number; total_tokens: number; total_requests: number; unique_accounts: number; } type TimeRange = 'hourly' | 'daily' | 'weekly'; type ViewMode = 'model' | 'account'; const MODEL_COLORS = [ '#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981', '#06b6d4', '#6366f1', '#f43f5e', '#84cc16', '#a855f7', '#14b8a6', '#f97316', '#64748b', '#0ea5e9', '#d946ef' ]; const COLORS = ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981', '#06b6d4', '#6366f1', '#f43f5e']; const formatNumber = (num: number): string => { if (num >= 1000000) return `${(num / 1000000).toFixed(1)}M`; if (num >= 1000) return `${(num / 1000).toFixed(1)}K`; return num.toString(); }; const shortenModelName = (model: string): string => { return model .replace('gemini-', 'g-') .replace('claude-', 'c-') .replace('-preview', '') .replace('-latest', ''); }; const TokenStats: React.FC = () => { const { t } = useTranslation(); const [timeRange, setTimeRange] = useState('daily'); const [viewMode, setViewMode] = useState('model'); const [chartData, setChartData] = useState([]); const [accountData, setAccountData] = useState([]); const [modelData, setModelData] = useState([]); const [modelTrendData, setModelTrendData] = useState([]); const [accountTrendData, setAccountTrendData] = useState([]); const [allModels, setAllModels] = useState([]); const [allAccounts, setAllAccounts] = useState([]); const [summary, setSummary] = useState(null); const [loading, setLoading] = useState(true); const fetchData = async () => { setLoading(true); try { let hours = 24; let data: TokenStatsAggregated[] = []; let modelTrend: ModelTrendPoint[] = []; let accountTrend: AccountTrendPoint[] = []; switch (timeRange) { case 'hourly': hours = 24; data = await invoke('get_token_stats_hourly', { hours: 24 }); modelTrend = await invoke('get_token_stats_model_trend_hourly', { hours: 24 }); accountTrend = await invoke('get_token_stats_account_trend_hourly', { hours: 24 }); break; case 'daily': hours = 168; data = await invoke('get_token_stats_daily', { days: 7 }); modelTrend = await invoke('get_token_stats_model_trend_daily', { days: 7 }); accountTrend = await invoke('get_token_stats_account_trend_daily', { days: 7 }); break; case 'weekly': hours = 720; data = await invoke('get_token_stats_weekly', { weeks: 4 }); modelTrend = await invoke('get_token_stats_model_trend_daily', { days: 30 }); accountTrend = await invoke('get_token_stats_account_trend_daily', { days: 30 }); break; } setChartData(data); const models = new Set(); modelTrend.forEach(point => { Object.keys(point.model_data).forEach(m => models.add(m)); }); const modelList = Array.from(models); setAllModels(modelList); const transformedTrend = modelTrend.map(point => { const row: Record = { period: point.period }; modelList.forEach(model => { row[model] = point.model_data[model] || 0; }); return row; }); setModelTrendData(transformedTrend); // Process Account Trend Data const accountsSet = new Set(); accountTrend.forEach(point => { Object.keys(point.account_data).forEach(acc => accountsSet.add(acc)); }); const accountList = Array.from(accountsSet); setAllAccounts(accountList); const transformedAccountTrend = accountTrend.map(point => { const row: Record = { period: point.period }; accountList.forEach(acc => { row[acc] = point.account_data[acc] || 0; }); return row; }); setAccountTrendData(transformedAccountTrend); const [accounts, models_stats, summaryData] = await Promise.all([ invoke('get_token_stats_by_account', { hours }), invoke('get_token_stats_by_model', { hours }), invoke('get_token_stats_summary', { hours }) ]); setAccountData(accounts); setModelData(models_stats); setSummary(summaryData); } catch (error) { console.error('Failed to fetch token stats:', error); } finally { setLoading(false); } }; useEffect(() => { fetchData(); }, [timeRange]); const pieData = accountData.slice(0, 8).map((account, index) => ({ name: account.account_email.split('@')[0] + '...', value: account.total_tokens, fullEmail: account.account_email, color: COLORS[index % COLORS.length] })); const modelColorMap = new Map(); allModels.forEach((model, index) => { modelColorMap.set(model, MODEL_COLORS[index % MODEL_COLORS.length]); }); const trendChartContainerRef = useRef(null); const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | undefined>(undefined); // Ref and state for pie chart tooltip position const pieChartContainerRef = useRef(null); const [pieTooltipPosition, setPieTooltipPosition] = useState<{ x: number; y: number } | undefined>(undefined); // Handle mouse move to calculate tooltip position const handleTrendChartMouseMove = useCallback((e: any) => { if (!trendChartContainerRef.current || !e?.activeCoordinate) return; const containerRect = trendChartContainerRef.current.getBoundingClientRect(); const tooltipWidth = 200; // Approximate tooltip width const rightEdgeThreshold = containerRect.width - tooltipWidth - 20; // 20px buffer const mouseXInContainer = e.activeCoordinate.x; if (mouseXInContainer > rightEdgeThreshold) { setTooltipPosition({ x: e.activeCoordinate.x - tooltipWidth - 15, y: e.activeCoordinate.y }); } else { setTooltipPosition(undefined); // Use default positioning } }, []); // Handle mouse move for pie chart to calculate tooltip position const handlePieChartMouseMove = useCallback((e: any) => { if (!pieChartContainerRef.current) return; const containerRect = pieChartContainerRef.current.getBoundingClientRect(); const tooltipWidth = 180; // Approximate tooltip width for pie chart // Get mouse position relative to container if (e?.activeCoordinate) { const mouseXInContainer = e.activeCoordinate.x; const rightEdgeThreshold = containerRect.width - tooltipWidth - 20; if (mouseXInContainer > rightEdgeThreshold) { setPieTooltipPosition({ x: e.activeCoordinate.x - tooltipWidth - 15, y: e.activeCoordinate.y }); } else { setPieTooltipPosition(undefined); } } }, []); // Custom Tooltip for Trend Chart const CustomTrendTooltip = ({ active, payload, label }: any) => { if (!active || !payload || !payload.length) return null; // Sort payload by value descending const sortedPayload = [...payload].sort((a: any, b: any) => b.value - a.value); return (

{label}

{sortedPayload.map((entry: any, index: number) => { const name = entry.name; const displayName = viewMode === 'model' ? shortenModelName(name) : name.split('@')[0]; return (
{displayName}
{formatNumber(entry.value)}
); })}
); }; // Custom Tooltip for Bar/Pie Charts const SimpleCustomTooltip = ({ active, payload, label }: any) => { if (!active || !payload || !payload.length) return null; return (
{label &&

{label}

}
{payload.map((entry: any, index: number) => (
{entry.name}: {formatNumber(entry.value)}
))}
); }; // Custom Tooltip for Pie Chart const CustomPieTooltip = ({ active, payload }: any) => { if (!active || !payload || !payload.length) return null; const entry = payload[0]; return (
{entry.payload.fullEmail || entry.name}: {formatNumber(entry.value)}
); }; return (

{t('token_stats.title', 'Token 消费统计')}

{summary && (
{t('token_stats.total_tokens', '总 Token')}
{formatNumber(summary.total_tokens)}
{t('token_stats.input_tokens', '输入 Token')}
{formatNumber(summary.total_input_tokens)}
{t('token_stats.output_tokens', '输出 Token')}
{formatNumber(summary.total_output_tokens)}
{t('token_stats.accounts_used', '活跃账号')}
{summary.unique_accounts}
{t('token_stats.models_used', '使用模型')}
{modelData.length}
)}

{viewMode === 'model' ? ( ) : ( )} {viewMode === 'model' ? t('token_stats.model_trend', '分模型使用趋势') : t('token_stats.account_trend', '分账号使用趋势') }

{modelTrendData.length > 0 && allModels.length > 0 ? ( setTooltipPosition(undefined)} > { if (timeRange === 'hourly') return val.split(' ')[1] || val; if (timeRange === 'daily') return val.split('-').slice(1).join('/'); return val; }} axisLine={false} tickLine={false} dy={10} /> formatNumber(val)} axisLine={false} tickLine={false} /> } cursor={{ stroke: '#6b7280', strokeWidth: 1, strokeDasharray: '4 4', fill: 'transparent' }} allowEscapeViewBox={{ x: true, y: true }} position={tooltipPosition} wrapperStyle={{ zIndex: 100 }} /> viewMode === 'model' ? shortenModelName(value) : value.split('@')[0]} wrapperStyle={{ fontSize: '11px', paddingTop: '10px', maxHeight: '60px', overflowY: 'auto', zIndex: 0 }} /> {(viewMode === 'model' ? allModels : allAccounts).map((item, index) => ( ))} ) : (
{loading ? t('common.loading', '加载中...') : t('token_stats.no_data', '暂无数据')}
)}

{t('token_stats.usage_trend', 'Token 使用趋势')}

{chartData.length > 0 ? ( { if (timeRange === 'hourly') return val.split(' ')[1] || val; if (timeRange === 'daily') return val.split('-').slice(1).join('/'); return val; }} axisLine={false} tickLine={false} dy={10} /> formatNumber(val)} axisLine={false} tickLine={false} /> } cursor={{ fill: 'transparent' }} allowEscapeViewBox={{ x: true, y: true }} wrapperStyle={{ zIndex: 100 }} /> ) : (
{loading ? t('common.loading', '加载中...') : t('token_stats.no_data', '暂无数据')}
)}

{t('token_stats.by_account', '分账号统计')}

{pieData.length > 0 ? ( setPieTooltipPosition(undefined)} > {pieData.map((entry, index) => ( ))} } allowEscapeViewBox={{ x: true, y: true }} position={pieTooltipPosition} wrapperStyle={{ zIndex: 100 }} /> ) : (
{loading ? t('common.loading', '加载中...') : t('token_stats.no_data', '暂无数据')}
)}
{accountData.slice(0, 5).map((account, index) => (
{account.account_email.split('@')[0]}
{formatNumber(account.total_tokens)}
))}
{ modelData.length > 0 && viewMode === 'model' && (

{t('token_stats.model_details', '分模型详细统计')}

{modelData.map((model, index) => { const percentage = summary ? ((model.total_tokens / summary.total_tokens) * 100).toFixed(1) : '0'; return ( ); })}
{t('token_stats.model', '模型')} {t('token_stats.requests', '请求数')} {t('token_stats.input', '输入')} {t('token_stats.output', '输出')} {t('token_stats.total', '合计')} {t('token_stats.percentage', '占比')}
{model.model}
{model.request_count.toLocaleString()} {formatNumber(model.total_input_tokens)} {formatNumber(model.total_output_tokens)} {formatNumber(model.total_tokens)}
{percentage}%
) } { accountData.length > 0 && viewMode === 'account' && (

{t('token_stats.account_details', '账号详细统计')}

{accountData.map((account) => ( ))}
{t('token_stats.account', '账号')} {t('token_stats.requests', '请求数')} {t('token_stats.input', '输入')} {t('token_stats.output', '输出')} {t('token_stats.total', '合计')}
{account.account_email} {account.request_count.toLocaleString()} {formatNumber(account.total_input_tokens)} {formatNumber(account.total_output_tokens)} {formatNumber(account.total_tokens)}
) }
); }; export default TokenStats;