import { useState, useMemo, useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler } from 'chart.js'; import { Button } from '@/components/ui/Button'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { Select } from '@/components/ui/Select'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { useThemeStore, useConfigStore } from '@/stores'; import { StatCards, UsageChart, ChartLineSelector, ApiDetailsCard, ModelStatsCard, PriceSettingsCard, CredentialStatsCard, TokenBreakdownChart, CostTrendChart, ServiceHealthCard, useUsageData, useSparklines, useChartData } from '@/components/usage'; import { getModelNamesFromUsage, getApiStats, getModelStats, filterUsageByTimeRange, type UsageTimeRange } from '@/utils/usage'; import styles from './UsagePage.module.scss'; // Register Chart.js components ChartJS.register( CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, Filler ); const CHART_LINES_STORAGE_KEY = 'cli-proxy-usage-chart-lines-v1'; const TIME_RANGE_STORAGE_KEY = 'cli-proxy-usage-time-range-v1'; const DEFAULT_CHART_LINES = ['all']; const DEFAULT_TIME_RANGE: UsageTimeRange = '24h'; const MAX_CHART_LINES = 9; const TIME_RANGE_OPTIONS: ReadonlyArray<{ value: UsageTimeRange; labelKey: string }> = [ { value: 'all', labelKey: 'usage_stats.range_all' }, { value: '7h', labelKey: 'usage_stats.range_7h' }, { value: '24h', labelKey: 'usage_stats.range_24h' }, { value: '7d', labelKey: 'usage_stats.range_7d' }, ]; const HOUR_WINDOW_BY_TIME_RANGE: Record, number> = { '7h': 7, '24h': 24, '7d': 7 * 24 }; const isUsageTimeRange = (value: unknown): value is UsageTimeRange => value === '7h' || value === '24h' || value === '7d' || value === 'all'; const normalizeChartLines = (value: unknown, maxLines = MAX_CHART_LINES): string[] => { if (!Array.isArray(value)) { return DEFAULT_CHART_LINES; } const filtered = value .filter((item): item is string => typeof item === 'string') .map((item) => item.trim()) .filter(Boolean) .slice(0, maxLines); return filtered.length ? filtered : DEFAULT_CHART_LINES; }; const loadChartLines = (): string[] => { try { if (typeof localStorage === 'undefined') { return DEFAULT_CHART_LINES; } const raw = localStorage.getItem(CHART_LINES_STORAGE_KEY); if (!raw) { return DEFAULT_CHART_LINES; } return normalizeChartLines(JSON.parse(raw)); } catch { return DEFAULT_CHART_LINES; } }; const loadTimeRange = (): UsageTimeRange => { try { if (typeof localStorage === 'undefined') { return DEFAULT_TIME_RANGE; } const raw = localStorage.getItem(TIME_RANGE_STORAGE_KEY); return isUsageTimeRange(raw) ? raw : DEFAULT_TIME_RANGE; } catch { return DEFAULT_TIME_RANGE; } }; export function UsagePage() { const { t } = useTranslation(); const isMobile = useMediaQuery('(max-width: 768px)'); const resolvedTheme = useThemeStore((state) => state.resolvedTheme); const isDark = resolvedTheme === 'dark'; const config = useConfigStore((state) => state.config); // Data hook const { usage, loading, error, lastRefreshedAt, modelPrices, setModelPrices, loadUsage, handleExport, handleImport, handleImportChange, importInputRef, exporting, importing } = useUsageData(); useHeaderRefresh(loadUsage); // Chart lines state const [chartLines, setChartLines] = useState(loadChartLines); const [timeRange, setTimeRange] = useState(loadTimeRange); const timeRangeOptions = useMemo( () => TIME_RANGE_OPTIONS.map((opt) => ({ value: opt.value, label: t(opt.labelKey) })), [t] ); const filteredUsage = useMemo( () => (usage ? filterUsageByTimeRange(usage, timeRange) : null), [usage, timeRange] ); const hourWindowHours = timeRange === 'all' ? undefined : HOUR_WINDOW_BY_TIME_RANGE[timeRange]; const handleChartLinesChange = useCallback((lines: string[]) => { setChartLines(normalizeChartLines(lines)); }, []); useEffect(() => { try { if (typeof localStorage === 'undefined') { return; } localStorage.setItem(CHART_LINES_STORAGE_KEY, JSON.stringify(chartLines)); } catch { // Ignore storage errors. } }, [chartLines]); useEffect(() => { try { if (typeof localStorage === 'undefined') { return; } localStorage.setItem(TIME_RANGE_STORAGE_KEY, timeRange); } catch { // Ignore storage errors. } }, [timeRange]); // Sparklines hook const { requestsSparkline, tokensSparkline, rpmSparkline, tpmSparkline, costSparkline } = useSparklines({ usage: filteredUsage, loading }); // Chart data hook const { requestsPeriod, setRequestsPeriod, tokensPeriod, setTokensPeriod, requestsChartData, tokensChartData, requestsChartOptions, tokensChartOptions } = useChartData({ usage: filteredUsage, chartLines, isDark, isMobile, hourWindowHours }); // Derived data const modelNames = useMemo(() => getModelNamesFromUsage(usage), [usage]); const apiStats = useMemo( () => getApiStats(filteredUsage, modelPrices), [filteredUsage, modelPrices] ); const modelStats = useMemo( () => getModelStats(filteredUsage, modelPrices), [filteredUsage, modelPrices] ); const hasPrices = Object.keys(modelPrices).length > 0; return (
{loading && !usage && (
{t('common.loading')}
)}

{t('usage_stats.title')}

{t('usage_stats.range_filter')} {lastRefreshedAt && ( {t('usage_stats.last_updated')}: {lastRefreshedAt.toLocaleTimeString()} )}
{error &&
{error}
} {/* Stats Overview Cards */} {/* Chart Line Selection */} {/* Service Health */} {/* Charts Grid */}
{/* Token Breakdown Chart */} {/* Cost Trend Chart */} {/* Details Grid */}
{/* Credential Stats */} {/* Price Settings */}
); }