import { useMemo, useState } from 'react'; import { createFileRoute } from '@tanstack/react-router'; import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { ResponsiveBar } from '@nivo/bar'; import { ResponsivePie } from '@nivo/pie'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { ipc } from '@/ipc/manager'; import { useCloudAccounts } from '@/hooks/useCloudAccounts'; import { StatCard } from '@/components/usage/StatCard'; import { ChartCard } from '@/components/usage/ChartCard'; import { COLOR_PROMPT, COLOR_COMPLETION, COLOR_TOTAL, PIE_COLORS, BarTooltip, PieTooltip, } from '@/components/usage/NivoTooltip'; import { nivoTheme } from '@/lib/nivo-theme'; import { Activity, ArrowUpRight, ArrowDownRight, Layers, Loader2, MessageSquare, Zap, } from 'lucide-react'; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ type TimeRange = '24h' | '7d' | '30d'; interface UsageBucket { bucket: string; promptTokens: number; completionTokens: number; totalTokens: number; requests: number; } interface ModelBucket { model: string; promptTokens: number; completionTokens: number; totalTokens: number; requests: number; } /* ------------------------------------------------------------------ */ /* Helpers */ /* ------------------------------------------------------------------ */ function getTimeRange(r: TimeRange): { start: number; end: number } { const now = Date.now(); if (r === '24h') { return { start: now - 24 * 60 * 60 * 1000, end: now }; } if (r === '7d') { return { start: now - 7 * 24 * 60 * 60 * 1000, end: now }; } return { start: now - 30 * 24 * 60 * 60 * 1000, end: now }; } function formatBucketLabel(bucket: string, range: TimeRange): string { if (range === '24h') { // bucket is "YYYY-MM-DD HH:00" const parts = bucket.split(' '); if (parts.length === 2) { return parts[1]; // "HH:00" } } if (range === '7d') { // bucket is "YYYY-MM-DD" const d = new Date(bucket + 'T00:00:00'); return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } // 30d const d = new Date(bucket + 'T00:00:00'); return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } function barColor(bar: any): string { if (bar.id === 'Prompt') return COLOR_PROMPT; if (bar.id === 'Completion') return COLOR_COMPLETION; if (bar.id === 'Total') return COLOR_TOTAL; return '#a5a5a5'; } /* ------------------------------------------------------------------ */ /* Main Component */ /* ------------------------------------------------------------------ */ function UsagePage() { const { t } = useTranslation(); const [range, setRange] = useState('7d'); const ALL_ACCOUNTS = '__all__'; const [selectedAccountId, setSelectedAccountId] = useState(ALL_ACCOUNTS); const { data: accounts, isLoading: accountsLoading } = useCloudAccounts(); const { data: dailyData, isLoading: dailyLoading, error: dailyError, refetch: refetchDaily, } = useQuery({ queryKey: ['usage', 'day', range, selectedAccountId], queryFn: () => { const { start, end } = getTimeRange(range); return ipc.client.usage.getUsageByDay({ accountId: selectedAccountId === ALL_ACCOUNTS ? undefined : selectedAccountId, start, end }); }, refetchInterval: 30000, refetchIntervalInBackground: false, staleTime: 60_000, }); const { data: modelData, isLoading: modelLoading, error: modelError, refetch: refetchModel, } = useQuery({ queryKey: ['usage', 'model', range, selectedAccountId], queryFn: () => { const { start, end } = getTimeRange(range); return ipc.client.usage.getUsageByModel({ accountId: selectedAccountId === ALL_ACCOUNTS ? undefined : selectedAccountId, start, end }); }, refetchInterval: 30000, refetchIntervalInBackground: false, staleTime: 60_000, }); const { data: hourlyData, isLoading: hourlyLoading, error: hourlyError, refetch: refetchHourly, } = useQuery({ queryKey: ['usage', 'hour', range, selectedAccountId], queryFn: () => { const { start, end } = getTimeRange(range); return ipc.client.usage.getUsageByHour({ accountId: selectedAccountId === ALL_ACCOUNTS ? undefined : selectedAccountId, start, end }); }, enabled: range === '24h', refetchInterval: 30000, refetchIntervalInBackground: false, staleTime: 60_000, }); /* -------------------------- Derived data -------------------------- */ const totals = useMemo(() => { const source = dailyData || []; return source.reduce( (acc, row) => ({ promptTokens: acc.promptTokens + row.promptTokens, completionTokens: acc.completionTokens + row.completionTokens, totalTokens: acc.totalTokens + row.totalTokens, requests: acc.requests + row.requests, }), { promptTokens: 0, completionTokens: 0, totalTokens: 0, requests: 0 }, ); }, [dailyData]); const promptRatio = useMemo(() => { if (totals.totalTokens === 0) return 0; return Math.round((totals.promptTokens / totals.totalTokens) * 100); }, [totals]); const completionRatio = useMemo(() => { if (totals.totalTokens === 0) return 0; return Math.round((totals.completionTokens / totals.totalTokens) * 100); }, [totals]); const chartData = useMemo(() => { return (dailyData || []).map((d) => ({ name: formatBucketLabel(d.bucket, range), Prompt: d.promptTokens, Completion: d.completionTokens, })); }, [dailyData, range]); const pieData = useMemo(() => { return (modelData || []).map((d) => ({ id: d.model, value: d.totalTokens, })); }, [modelData]); const hourlyChartData = useMemo(() => { return (hourlyData || []).map((d) => ({ name: formatBucketLabel(d.bucket, '24h'), Prompt: d.promptTokens, Completion: d.completionTokens, Total: d.totalTokens, })); }, [hourlyData]); const hourlyTickValues = useMemo(() => { return hourlyChartData.filter((_, i) => i % 3 === 0).map((d) => d.name); }, [hourlyChartData]); const isDailyEmpty = !dailyLoading && !dailyError && (!dailyData || dailyData.length === 0); const isModelEmpty = !modelLoading && !modelError && (!modelData || modelData.length === 0); const isHourlyEmpty = !hourlyLoading && !hourlyError && (!hourlyData || hourlyData.length === 0); /* ----------------------------- Render ----------------------------- */ return (
{/* Header */}

{t('usage.title')}

{t('usage.description')}

{/* Account filter */} {/* Time range tabs */} setRange(v as TimeRange)}> {t('usage.range24h')} {t('usage.range7d')} {t('usage.range30d')}
{/* Summary Stats */}
{/* Trend indicators row */} {!dailyLoading && totals.totalTokens > 0 && (
{t('usage.promptLabel')}: {promptRatio}%
{t('usage.completionLabel')}: {completionRatio}%
{t('usage.requestsLabel')}:{' '} {totals.requests.toLocaleString()}
)} {/* Charts */}
{/* Daily Usage Bar Chart */} refetchDaily()} emptyTitle={t('usage.emptyStateTitle')} emptyDescription={t('usage.emptyStateDescription')} errorMessage={t('usage.loadFailed')} retryLabel={t('usage.retry')} >
(v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${v}`), }} tooltip={BarTooltip} legends={[ { dataFrom: 'keys', anchor: 'bottom', direction: 'row', justify: false, translateX: 0, translateY: 30, itemsSpacing: 16, itemWidth: 80, itemHeight: 16, itemDirection: 'left-to-right', symbolSize: 8, symbolShape: 'circle', }, ]} animate={true} />
{/* Model Usage Donut Chart */} refetchModel()} emptyTitle={t('usage.emptyStateTitle')} emptyDescription={t('usage.emptyStateDescription')} errorMessage={t('usage.loadFailed')} retryLabel={t('usage.retry')} >
{/* Hourly Usage — 24h only */} {range === '24h' && ( refetchHourly()} emptyTitle={t('usage.emptyStateTitle')} emptyDescription={t('usage.emptyStateDescription')} errorMessage={t('usage.loadFailed')} retryLabel={t('usage.retry')} >
(v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${v}`), }} tooltip={BarTooltip} legends={[ { dataFrom: 'keys', anchor: 'bottom', direction: 'row', justify: false, translateX: 0, translateY: 30, itemsSpacing: 16, itemWidth: 80, itemHeight: 16, itemDirection: 'left-to-right', symbolSize: 8, symbolShape: 'circle', }, ]} animate={true} />
)}
); } export const Route = createFileRoute('/usage')({ component: UsagePage, });