'use client' import { useState, useEffect, useCallback, useMemo } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Loader } from '@/components/ui/loader' import { useMissionControl } from '@/store' import { createClientLogger } from '@/lib/client-logger' import { detectProvider } from '@/lib/token-utils' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell } from 'recharts' const log = createClientLogger('TokenDashboard') interface UsageStats { summary: { totalTokens: number totalCost: number requestCount: number avgTokensPerRequest: number avgCostPerRequest: number } models: Record sessions: Record timeframe: string recordCount: number } interface TrendData { trends: Array<{ timestamp: string; tokens: number; cost: number; requests: number }> timeframe: string } type DashboardView = 'overview' | 'sessions' interface SessionCostEntry { sessionId: string sessionKey?: string model: string totalTokens: number inputTokens: number outputTokens: number totalCost: number requestCount: number firstSeen: string lastSeen: string } type TimezoneOption = { label: string; offset: number } const TIMEZONE_OPTIONS: TimezoneOption[] = [ { label: 'Local', offset: NaN }, { label: 'UTC', offset: 0 }, { label: 'UTC-8 (PST)', offset: -8 }, { label: 'UTC-7 (MST)', offset: -7 }, { label: 'UTC-6 (CST)', offset: -6 }, { label: 'UTC-5 (EST)', offset: -5 }, { label: 'UTC+1 (CET)', offset: 1 }, { label: 'UTC+5:30 (IST)', offset: 5.5 }, { label: 'UTC+8 (CST)', offset: 8 }, { label: 'UTC+9 (JST)', offset: 9 }, ] const deriveProvider = detectProvider export function TokenDashboardPanel() { const { sessions } = useMissionControl() const t = useTranslations('tokenDashboard') const [selectedTimeframe, setSelectedTimeframe] = useState<'hour' | 'day' | 'week' | 'month'>('day') const [usageStats, setUsageStats] = useState(null) const [trendData, setTrendData] = useState(null) const [isLoading, setIsLoading] = useState(false) const [isExporting, setIsExporting] = useState(false) const [view, setView] = useState('overview') const [sessionCosts, setSessionCosts] = useState([]) const [sessionSort, setSessionSort] = useState<'cost' | 'tokens' | 'requests' | 'recent'>('cost') const [chartMode, setChartMode] = useState<'incremental' | 'cumulative'>('incremental') // Filter state const [modelFilters, setModelFilters] = useState>(new Set()) const [sessionFilters, setSessionFilters] = useState>(new Set()) // Timezone state const [selectedTimezone, setSelectedTimezone] = useState(TIMEZONE_OPTIONS[0]) const loadUsageStats = useCallback(async () => { setIsLoading(true) try { const response = await fetch(`/api/tokens?action=stats&timeframe=${selectedTimeframe}`) const data = await response.json() setUsageStats(data) } catch (error) { log.error('Failed to load usage stats:', error) } finally { setIsLoading(false) } }, [selectedTimeframe]) const loadTrendData = useCallback(async () => { try { const response = await fetch(`/api/tokens?action=trends&timeframe=${selectedTimeframe}`) const data = await response.json() setTrendData(data) } catch (error) { log.error('Failed to load trend data:', error) } }, [selectedTimeframe]) const loadSessionCosts = useCallback(async () => { try { const response = await fetch(`/api/tokens?action=session-costs&timeframe=${selectedTimeframe}`) const data = await response.json() if (Array.isArray(data?.sessions)) { setSessionCosts(data.sessions) } else if (usageStats?.sessions) { const entries: SessionCostEntry[] = Object.entries(usageStats.sessions).map(([sessionId, stats]) => { const info = sessions.find(s => s.id === sessionId) return { sessionId, sessionKey: info?.key, model: '', totalTokens: stats.totalTokens, inputTokens: 0, outputTokens: 0, totalCost: stats.totalCost, requestCount: stats.requestCount, firstSeen: '', lastSeen: '', } }) setSessionCosts(entries) } } catch { if (usageStats?.sessions) { const entries: SessionCostEntry[] = Object.entries(usageStats.sessions).map(([sessionId, stats]) => { const info = sessions.find(s => s.id === sessionId) return { sessionId, sessionKey: info?.key, model: '', totalTokens: stats.totalTokens, inputTokens: 0, outputTokens: 0, totalCost: stats.totalCost, requestCount: stats.requestCount, firstSeen: '', lastSeen: '', } }) setSessionCosts(entries) } } }, [selectedTimeframe, usageStats, sessions]) useEffect(() => { loadUsageStats() loadTrendData() }, [loadUsageStats, loadTrendData]) useEffect(() => { if (view === 'sessions') loadSessionCosts() }, [view, loadSessionCosts]) // Filtered stats based on active filter chips const filteredUsageStats = useMemo((): UsageStats | null => { if (!usageStats) return null if (modelFilters.size === 0 && sessionFilters.size === 0) return usageStats const filteredModels: typeof usageStats.models = {} const filteredSessions: typeof usageStats.sessions = {} // Filter models for (const [model, stats] of Object.entries(usageStats.models)) { if (modelFilters.size > 0 && !modelFilters.has(model)) continue filteredModels[model] = stats } // Filter sessions for (const [sessionId, stats] of Object.entries(usageStats.sessions)) { if (sessionFilters.size > 0 && !sessionFilters.has(sessionId)) continue filteredSessions[sessionId] = stats } // Recalculate summary from filtered models const sourceEntries = Object.values(modelFilters.size > 0 ? filteredModels : usageStats.models) const totalTokens = sourceEntries.reduce((sum, s) => sum + s.totalTokens, 0) const totalCost = sourceEntries.reduce((sum, s) => sum + s.totalCost, 0) const requestCount = sourceEntries.reduce((sum, s) => sum + s.requestCount, 0) return { ...usageStats, summary: { totalTokens, totalCost, requestCount, avgTokensPerRequest: requestCount > 0 ? Math.round(totalTokens / requestCount) : 0, avgCostPerRequest: requestCount > 0 ? totalCost / requestCount : 0, }, models: filteredModels, sessions: filteredSessions, } }, [usageStats, modelFilters, sessionFilters]) // Client-side CSV export from currently displayed data const exportClientCsv = useCallback(() => { if (!filteredUsageStats) return setIsExporting(true) try { const headers = ['timestamp', 'model', 'session', 'inputTokens', 'outputTokens', 'totalTokens', 'cost'] const rows: string[] = [headers.join(',')] // Export model-level rows for (const [model, stats] of Object.entries(filteredUsageStats.models)) { rows.push([ new Date().toISOString(), `"${model}"`, '', '', '', stats.totalTokens, stats.totalCost.toFixed(4), ].join(',')) } // Export session-level rows for (const [sessionId, stats] of Object.entries(filteredUsageStats.sessions)) { rows.push([ new Date().toISOString(), '', `"${sessionId}"`, '', '', stats.totalTokens, stats.totalCost.toFixed(4), ].join(',')) } // Export session cost detail rows if available for (const entry of sessionCosts) { rows.push([ entry.lastSeen || new Date().toISOString(), `"${entry.model}"`, `"${entry.sessionId}"`, entry.inputTokens, entry.outputTokens, entry.totalTokens, entry.totalCost.toFixed(4), ].join(',')) } const csv = rows.join('\n') const blob = new Blob([csv], { type: 'text/csv' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.style.display = 'none' a.href = url a.download = `usage-${selectedTimeframe}-${new Date().toISOString().split('T')[0]}.csv` document.body.appendChild(a) a.click() URL.revokeObjectURL(url) document.body.removeChild(a) } catch (error) { log.error('Client CSV export failed:', error) } finally { setIsExporting(false) } }, [filteredUsageStats, sessionCosts, selectedTimeframe]) const exportData = async (format: 'json' | 'csv') => { setIsExporting(true) try { const response = await fetch(`/api/tokens?action=export&timeframe=${selectedTimeframe}&format=${format}`) if (!response.ok) { throw new Error('Export failed') } const blob = await response.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.style.display = 'none' a.href = url a.download = `token-usage-${selectedTimeframe}-${new Date().toISOString().split('T')[0]}.${format}` document.body.appendChild(a) a.click() window.URL.revokeObjectURL(url) document.body.removeChild(a) } catch (error) { log.error('Export failed:', error) alert('Export failed: ' + error) } finally { setIsExporting(false) } } const formatNumber = (num: number) => { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M' } if (num >= 1000) { return (num / 1000).toFixed(1) + 'K' } return num.toString() } const formatCost = (cost: number) => { return '$' + cost.toFixed(4) } const getModelDisplayName = (modelName: string) => { const parts = modelName.split('/') return parts[parts.length - 1] || modelName } const formatTimestamp = useCallback((isoString: string) => { const date = new Date(isoString) if (isNaN(selectedTimezone.offset)) { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) } const utcMs = date.getTime() + date.getTimezoneOffset() * 60000 const adjusted = new Date(utcMs + selectedTimezone.offset * 3600000) return adjusted.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }, [selectedTimezone]) const toggleModelFilter = (model: string) => { setModelFilters(prev => { const next = new Set(prev) if (next.has(model)) next.delete(model) else next.add(model) return next }) } const toggleSessionFilter = (sessionId: string) => { setSessionFilters(prev => { const next = new Set(prev) if (next.has(sessionId)) next.delete(sessionId) else next.add(sessionId) return next }) } const clearAllFilters = () => { setModelFilters(new Set()) setSessionFilters(new Set()) } const hasActiveFilters = modelFilters.size > 0 || sessionFilters.size > 0 const prepareModelChartData = () => { if (!filteredUsageStats?.models) return [] return Object.entries(filteredUsageStats.models) .map(([model, stats]) => ({ name: getModelDisplayName(model), tokens: stats.totalTokens, cost: stats.totalCost, requests: stats.requestCount })) .sort((a, b) => b.cost - a.cost) } const preparePieChartData = () => { if (!filteredUsageStats?.models) return [] const data = Object.entries(filteredUsageStats.models) .map(([model, stats]) => ({ name: getModelDisplayName(model), value: stats.totalCost, tokens: stats.totalTokens })) .sort((a, b) => b.value - a.value) .slice(0, 6) return data } const prepareProviderPieData = () => { if (!filteredUsageStats?.models) return [] const providerMap: Record = {} for (const [model, stats] of Object.entries(filteredUsageStats.models)) { const provider = deriveProvider(model) if (!providerMap[provider]) providerMap[provider] = { cost: 0, tokens: 0 } providerMap[provider].cost += stats.totalCost providerMap[provider].tokens += stats.totalTokens } return Object.entries(providerMap) .map(([name, data]) => ({ name, value: data.cost, tokens: data.tokens })) .sort((a, b) => b.value - a.value) } const prepareTrendChartData = () => { if (!trendData?.trends) return [] const raw = trendData.trends.map(trend => ({ time: formatTimestamp(trend.timestamp), tokens: trend.tokens, cost: trend.cost, requests: trend.requests })) if (chartMode === 'cumulative') { let cumTokens = 0 let cumCost = 0 let cumRequests = 0 return raw.map(d => { cumTokens += d.tokens cumCost += d.cost cumRequests += d.requests return { ...d, tokens: cumTokens, cost: cumCost, requests: cumRequests } }) } return raw } // Find peak error/request hour for trend highlighting const peakTrendHour = useMemo(() => { if (!trendData?.trends || trendData.trends.length === 0) return null let peak = trendData.trends[0] for (const t of trendData.trends) { if (t.requests > peak.requests) peak = t } return formatTimestamp(peak.timestamp) }, [trendData, formatTimestamp]) const sortedSessionCosts = [...sessionCosts].sort((a, b) => { switch (sessionSort) { case 'cost': return b.totalCost - a.totalCost case 'tokens': return b.totalTokens - a.totalTokens case 'requests': return b.requestCount - a.requestCount case 'recent': return (b.lastSeen || '').localeCompare(a.lastSeen || '') default: return 0 } }) const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'] const PROVIDER_COLORS: Record = { Anthropic: '#d97706', OpenAI: '#10b981', Google: '#3b82f6', Mistral: '#f97316', Meta: '#6366f1', DeepSeek: '#06b6d4', Cohere: '#ec4899', Other: '#6b7280', } // Enhanced performance metrics const getPerformanceMetrics = () => { if (!filteredUsageStats?.models) return null const models = Object.entries(filteredUsageStats.models) if (models.length === 0) return null let mostEfficient = { model: models[0][0], stats: models[0][1] } for (const [model, stats] of models) { const costPerToken = stats.totalCost / Math.max(1, stats.totalTokens) const bestCostPerToken = mostEfficient.stats.totalCost / Math.max(1, mostEfficient.stats.totalTokens) if (costPerToken < bestCostPerToken) { mostEfficient = { model, stats } } } let mostUsed = { model: models[0][0], stats: models[0][1] } for (const [model, stats] of models) { if (stats.requestCount > mostUsed.stats.requestCount) { mostUsed = { model, stats } } } let mostExpensive = { model: models[0][0], stats: models[0][1] } for (const [model, stats] of models) { const costPerToken = stats.totalCost / Math.max(1, stats.totalTokens) const bestCostPerToken = mostExpensive.stats.totalCost / Math.max(1, mostExpensive.stats.totalTokens) if (costPerToken > bestCostPerToken) { mostExpensive = { model, stats } } } const totalTokens = filteredUsageStats.summary.totalTokens const currentCost = filteredUsageStats.summary.totalCost const efficientCostPerToken = mostEfficient.stats.totalCost / Math.max(1, mostEfficient.stats.totalTokens) const potentialCost = totalTokens * efficientCostPerToken const potentialSavings = Math.max(0, currentCost - potentialCost) return { mostEfficient, mostUsed, mostExpensive, potentialSavings, savingsPercentage: currentCost > 0 ? (potentialSavings / currentCost) * 100 : 0 } } const performanceMetrics = getPerformanceMetrics() const getAlerts = () => { const alerts = [] if (filteredUsageStats && filteredUsageStats.summary.totalCost !== undefined && filteredUsageStats.summary.totalCost > 100) { alerts.push({ type: 'warning', title: 'High Usage Cost', message: `Total cost of ${formatCost(filteredUsageStats.summary.totalCost)} exceeds $100 threshold`, suggestion: 'Consider using more cost-effective models for routine tasks' }) } if (performanceMetrics && performanceMetrics.savingsPercentage !== undefined && performanceMetrics.savingsPercentage > 20) { alerts.push({ type: 'info', title: 'Optimization Opportunity', message: `Using ${getModelDisplayName(performanceMetrics.mostEfficient.model)} could save ${formatCost(performanceMetrics.potentialSavings)} (${performanceMetrics.savingsPercentage.toFixed(1)}%)`, suggestion: 'Consider switching routine tasks to more efficient models' }) } if (filteredUsageStats && filteredUsageStats.summary.requestCount !== undefined && filteredUsageStats.summary.requestCount > 1000) { alerts.push({ type: 'info', title: 'High Request Volume', message: `${filteredUsageStats.summary.requestCount} requests in selected timeframe`, suggestion: 'Consider implementing request batching or caching for efficiency' }) } return alerts } const alerts = getAlerts() // Available models and sessions for filter chips const availableModels = useMemo(() => { if (!usageStats?.models) return [] return Object.keys(usageStats.models).sort() }, [usageStats]) const availableSessions = useMemo(() => { if (!usageStats?.sessions) return [] return Object.keys(usageStats.sessions).sort() }, [usageStats]) // Cache token stats from session costs (if available in the data) const cacheStats = useMemo(() => { // Aggregate from session cost entries if they have cache token info // For now show zeroes; real data flows once backend provides cacheReadTokens/cacheWriteTokens let cacheRead = 0 let cacheWrite = 0 for (const entry of sessionCosts) { const e = entry as unknown as Record if (typeof e.cacheReadTokens === 'number') cacheRead += e.cacheReadTokens if (typeof e.cacheWriteTokens === 'number') cacheWrite += e.cacheWriteTokens } return cacheRead > 0 || cacheWrite > 0 ? { cacheRead, cacheWrite } : null }, [sessionCosts]) return (

{t('title')}

{t('subtitle')}

{(['hour', 'day', 'week', 'month'] as const).map((timeframe) => ( ))}
{/* Filter Chips Bar */} {view === 'overview' && usageStats && (availableModels.length > 0 || availableSessions.length > 0) && (
{t('filtersLabel')} {availableModels.map(model => ( ))} {availableSessions.length > 0 && availableModels.length > 0 && ( | )} {availableSessions.slice(0, 8).map(sessionId => { const info = sessions.find(s => s.id === sessionId) const label = info?.key || sessionId.split(':')[0] || sessionId return ( ) })} {hasActiveFilters && ( )}
)} {/* Timezone Selector */} {view === 'overview' && (
{t('timezoneLabel')}
)} {view === 'sessions' ? (
{t('sortByLabel')} {(['cost', 'tokens', 'requests', 'recent'] as const).map(s => ( ))}
{sortedSessionCosts.length === 0 ? (

{t('noSessionCostData')}

{t('noSessionCostSubtitle')}

) : (
{sortedSessionCosts.map((entry) => { const sessionInfo = sessions.find(s => s.id === entry.sessionId) return (
{entry.sessionKey || sessionInfo?.key || entry.sessionId}
{sessionInfo?.active && } {sessionInfo?.active ? t('sessionActive') : t('sessionInactive')} {entry.model && | {getModelDisplayName(entry.model)}} {sessionInfo?.kind && | {sessionInfo.kind}}
{formatCost(entry.totalCost)}
{formatNumber(entry.totalTokens)} tokens
{entry.requestCount} {t('requests')}
{formatNumber(entry.inputTokens || 0)} {t('inSuffix')}
{formatNumber(entry.outputTokens || 0)} {t('outSuffix')}
{entry.totalTokens > 0 ? {formatCost(entry.totalCost / entry.requestCount)} : '-' }{' '}{t('avgPerRequest')}
) })}
)}
) : isLoading ? ( ) : filteredUsageStats ? (
{/* Overview Stats */}
{formatNumber(filteredUsageStats.summary.totalTokens)}
{t('totalTokens', { timeframe: selectedTimeframe })}
{formatCost(filteredUsageStats.summary.totalCost)}
{t('totalCost', { timeframe: selectedTimeframe })}
{formatNumber(filteredUsageStats.summary.requestCount)}
{t('apiRequests')}
{formatNumber(filteredUsageStats.summary.avgTokensPerRequest)}
{t('avgTokensPerRequest')}
{cacheStats && ( <>
{formatNumber(cacheStats.cacheRead)}
{t('cacheReadTokens')}
{formatNumber(cacheStats.cacheWrite)}
{t('cacheWriteTokens')}
)}
{/* Charts Section */}
{/* Usage Trends Chart */}

{t('usageTrends', { timeframe: selectedTimeframe })}

{peakTrendHour && ( {t('peakLabel')} {peakTrendHour} )}
{prepareTrendChartData().length === 0 ? (
{t('noTrendData')}
) : ( )}
{/* Model Usage Bar Chart */}

{t('tokenUsageByModel')}

{prepareModelChartData().length === 0 ? (
{t('noModelUsageData')}
) : ( [formatNumber(Number(value)), name]} /> )}
{/* Cost Distribution Pie Chart */}

{t('costDistributionByModel')}

{preparePieChartData().length === 0 ? (
{t('noCostData')}
) : ( {preparePieChartData().map((_, index) => ( ))} formatCost(Number(value))} /> )}
{/* Cost by Provider Pie Chart */}

{t('costByProvider')}

{prepareProviderPieData().length === 0 ? (
{t('noProviderData')}
) : (
{prepareProviderPieData().map((entry) => ( ))} formatCost(Number(value))} />
{prepareProviderPieData().map(entry => (
{entry.name}
{formatCost(entry.value)}
))}
)}
{/* Export Section */}

{t('exportData')}

Export token usage data for analysis. "Filtered" exports only the currently displayed data; "Full" exports all records from the server.

{/* Performance Insights */} {performanceMetrics && (

{t('performanceInsights')}

{/* Alerts */} {alerts.length > 0 && (
{alerts.map((alert, index) => (
{alert.type === 'warning' ? '!!' : 'i'}

{alert.title}

{alert.message}

{alert.suggestion}

))}
)} {/* Performance Metrics Grid */}

{t('mostEfficientModel')}

{getModelDisplayName(performanceMetrics.mostEfficient.model)}
${(performanceMetrics.mostEfficient.stats.totalCost / Math.max(1, performanceMetrics.mostEfficient.stats.totalTokens) * 1000).toFixed(4)}/1K tokens

{t('mostUsedModel')}

{getModelDisplayName(performanceMetrics.mostUsed.model)}
{performanceMetrics.mostUsed.stats.requestCount} requests

{t('optimizationPotential')}

{formatCost(performanceMetrics.potentialSavings)}
{t('savingsPossible', { pct: performanceMetrics.savingsPercentage.toFixed(1) })}
{/* Model Efficiency Comparison */}

{t('modelEfficiencyComparison')}

{Object.entries(filteredUsageStats?.models || {}) .map(([model, stats]) => { const costPerToken = stats.totalCost / Math.max(1, stats.totalTokens) * 1000 const efficiency = 1 / costPerToken const maxEfficiency = Math.max(...Object.values(filteredUsageStats?.models || {}).map(s => 1 / (s.totalCost / Math.max(1, s.totalTokens) * 1000))) const barWidth = (efficiency / maxEfficiency) * 100 return (
{getModelDisplayName(model)}
${costPerToken.toFixed(4)}/1K
) })}
)} {/* Detailed Statistics */}
{/* Model Statistics */}

{t('modelPerformance')}

{Object.entries(filteredUsageStats.models) .sort(([,a], [,b]) => b.totalCost - a.totalCost) .map(([model, stats]) => { const avgCostPerRequest = stats.totalCost / Math.max(1, stats.requestCount) const avgTokensPerRequest = stats.totalTokens / Math.max(1, stats.requestCount) return (
{getModelDisplayName(model)}
{formatCost(stats.totalCost)}
{formatNumber(stats.totalTokens)} tokens
{stats.requestCount}
{t('requestsLabel')}
{formatCost(avgCostPerRequest)}
{t('avgCost')}
{formatNumber(avgTokensPerRequest)}
{t('avgTokens')}
) })}
{/* Session Statistics */}

{t('topSessionsByCost')}

{Object.entries(filteredUsageStats.sessions) .sort(([,a], [,b]) => b.totalCost - a.totalCost) .slice(0, 10) .map(([sessionId, stats]) => { const sessionInfo = sessions.find(s => s.id === sessionId) const avgCostPerRequest = stats.totalCost / Math.max(1, stats.requestCount) return (
{sessionInfo?.key || sessionId}
{sessionInfo?.active ? t('sessionActive') : t('sessionInactive')}
{formatCost(stats.totalCost)}
{formatNumber(stats.totalTokens)} tokens
{stats.requestCount}
{t('requestsLabel')}
{formatCost(avgCostPerRequest)}
{t('avgCost')}
) })}
) : (
{t('noUsageData')}
{t('noUsageDataSubtitle')}
)}
) }