'use client' import { useState, useEffect, useCallback, useRef } 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 { PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, } from 'recharts' const log = createClientLogger('CostTracker') // ── Types ────────────────────────────────────────── interface TokenStats { totalTokens: number; totalCost: number; requestCount: number avgTokensPerRequest: number; avgCostPerRequest: number } interface UsageStats { summary: TokenStats models: Record sessions: Record timeframe: string recordCount: number } interface TrendData { trends: Array<{ timestamp: string; tokens: number; cost: number; requests: number }> timeframe: string } interface ByAgentModelBreakdown { model: string; input_tokens: number; output_tokens: number; request_count: number; cost: number } interface ByAgentEntry { agent: string; total_input_tokens: number; total_output_tokens: number total_tokens: number; total_cost: number; session_count: number request_count: number; last_active: string; models: ByAgentModelBreakdown[] } interface ByAgentResponse { agents: ByAgentEntry[] summary: { total_cost: number; total_tokens: number; agent_count: number; days: number } } interface TaskCostEntry { taskId: number; title: string; status: string; priority: string assignedTo?: string | null project: { id?: number | null; name?: string | null; slug?: string | null; ticketRef?: string | null } stats: TokenStats models: Record } interface TaskCostsResponse { summary: TokenStats tasks: TaskCostEntry[] agents: Record unattributed: TokenStats timeframe: string } interface SessionCostEntry { sessionId: string; sessionKey?: string; model: string totalTokens: number; inputTokens: number; outputTokens: number totalCost: number; requestCount: number; firstSeen: string; lastSeen: string } // ── Helpers ────────────────────────────────────────── const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d', '#ffc658', '#ff6b6b'] const formatNumber = (num: number) => { if (num >= 1_000_000) return (num / 1_000_000).toFixed(1) + 'M' if (num >= 1_000) return (num / 1_000).toFixed(1) + 'K' return num.toString() } const formatCost = (cost: number) => '$' + cost.toFixed(4) const getModelDisplayName = (name: string) => name.split('/').pop() || name type View = 'overview' | 'agents' | 'sessions' | 'tasks' type Timeframe = 'hour' | 'day' | 'week' | 'month' // ── Main Component ────────────────────────────────── export function CostTrackerPanel() { const t = useTranslations('costTracker') const { sessions } = useMissionControl() const [view, setView] = useState('overview') const [timeframe, setTimeframe] = useState('day') const [chartMode, setChartMode] = useState<'incremental' | 'cumulative'>('incremental') const [isLoading, setIsLoading] = useState(false) const [isExporting, setIsExporting] = useState(false) // Data const [usageStats, setUsageStats] = useState(null) const [trendData, setTrendData] = useState(null) const [byAgentData, setByAgentData] = useState(null) const [taskData, setTaskData] = useState(null) const [sessionCosts, setSessionCosts] = useState([]) const [sessionSort, setSessionSort] = useState<'cost' | 'tokens' | 'requests' | 'recent'>('cost') const [expandedAgent, setExpandedAgent] = useState(null) const refreshTimer = useRef | null>(null) const timeframeToDays = (tf: Timeframe): number => { switch (tf) { case 'hour': case 'day': return 1; case 'week': return 7; case 'month': return 30 } } const loadData = useCallback(async () => { setIsLoading(true) try { const [statsRes, trendRes, byAgentRes, taskRes] = await Promise.all([ fetch(`/api/tokens?action=stats&timeframe=${timeframe}`), fetch(`/api/tokens?action=trends&timeframe=${timeframe}`), fetch(`/api/tokens/by-agent?days=${timeframeToDays(timeframe)}`), fetch(`/api/tokens?action=task-costs&timeframe=${timeframe}`), ]) const [statsJson, trendJson, byAgentJson, taskJson] = await Promise.all([ statsRes.json(), trendRes.json(), byAgentRes.json(), taskRes.json(), ]) setUsageStats(statsJson) setTrendData(trendJson) setByAgentData(byAgentJson) setTaskData(taskJson) } catch (err) { log.error('Failed to load cost data:', err) } finally { setIsLoading(false) } }, [timeframe]) const loadSessionCosts = useCallback(async () => { try { const res = await fetch(`/api/tokens?action=session-costs&timeframe=${timeframe}`) const data = await res.json() if (Array.isArray(data?.sessions)) { setSessionCosts(data.sessions) } else if (usageStats?.sessions) { setSessionCosts(Object.entries(usageStats.sessions).map(([id, stats]) => ({ sessionId: id, model: '', totalTokens: stats.totalTokens, inputTokens: 0, outputTokens: 0, totalCost: stats.totalCost, requestCount: stats.requestCount, firstSeen: '', lastSeen: '', }))) } } catch { if (usageStats?.sessions) { setSessionCosts(Object.entries(usageStats.sessions).map(([id, stats]) => ({ sessionId: id, model: '', totalTokens: stats.totalTokens, inputTokens: 0, outputTokens: 0, totalCost: stats.totalCost, requestCount: stats.requestCount, firstSeen: '', lastSeen: '', }))) } } }, [timeframe, usageStats]) useEffect(() => { loadData() }, [loadData]) useEffect(() => { refreshTimer.current = setInterval(loadData, 30_000) return () => { if (refreshTimer.current) clearInterval(refreshTimer.current) } }, [loadData]) useEffect(() => { if (view === 'sessions') loadSessionCosts() }, [view, loadSessionCosts]) const exportData = async (format: 'json' | 'csv') => { setIsExporting(true) try { const res = await fetch(`/api/tokens?action=export&timeframe=${timeframe}&format=${format}`) if (!res.ok) throw new Error('Export failed') const blob = await res.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.style.display = 'none'; a.href = url a.download = `cost-tracker-${timeframe}-${new Date().toISOString().split('T')[0]}.${format}` document.body.appendChild(a); a.click() window.URL.revokeObjectURL(url); document.body.removeChild(a) } catch (err) { log.error('Export failed:', err) } finally { setIsExporting(false) } } // Derived data const summary = usageStats?.summary const agentSummary = byAgentData?.summary const agentList = byAgentData?.agents || [] const maxAgentCost = Math.max(...agentList.map(a => a.total_cost), 0.0001) const getAgentTasks = (agentName: string): TaskCostEntry[] => { if (!taskData) return [] const entry = taskData.agents[agentName] if (!entry) return [] return taskData.tasks.filter(t => entry.taskIds.includes(t.taskId)) } return (
{/* Header */}

{t('title')}

{t('subtitle')}

{/* View tabs */}
{(['overview', 'agents', 'sessions', 'tasks'] as const).map(v => ( ))}
{/* Timeframe */}
{(['hour', 'day', 'week', 'month'] as const).map(tf => ( ))}
{isLoading && !usageStats ? ( ) : view === 'overview' ? ( ) : view === 'agents' ? ( ) : view === 'sessions' ? ( ) : ( )}
) } // ── Overview View ────────────────────────────────── function OverviewView({ stats, trendData, agentSummary, taskData, timeframe, chartMode, setChartMode, exportData, isExporting, onRefresh, }: { stats: UsageStats | null; trendData: TrendData | null agentSummary: ByAgentResponse['summary'] | undefined; taskData: TaskCostsResponse | null timeframe: Timeframe; chartMode: 'incremental' | 'cumulative' setChartMode: (m: 'incremental' | 'cumulative') => void exportData: (f: 'json' | 'csv') => void; isExporting: boolean onRefresh: () => void }) { const t = useTranslations('costTracker') if (!stats) { return (
{t('noUsageData')}
{t('noUsageDataDesc')}
) } const modelData = Object.entries(stats.models) .map(([model, s]) => ({ name: getModelDisplayName(model), fullName: model, tokens: s.totalTokens, cost: s.totalCost, requests: s.requestCount })) .sort((a, b) => b.cost - a.cost) const pieData = modelData.slice(0, 6).map(m => ({ name: m.name, value: m.cost })) const trendChartData = (() => { if (!trendData?.trends) return [] const raw = trendData.trends.map(t => ({ time: new Date(t.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), tokens: t.tokens, cost: t.cost, requests: t.requests, })) if (chartMode === 'cumulative') { let ct = 0, cc = 0, cr = 0 return raw.map(d => { ct += d.tokens; cc += d.cost; cr += d.requests; return { ...d, tokens: ct, cost: cc, requests: cr } }) } return raw })() // Performance metrics const models = Object.entries(stats.models) const mostEfficient = models.length > 0 ? models.reduce((best, curr) => { const c = curr[1].totalCost / Math.max(1, curr[1].totalTokens) const b = best[1].totalCost / Math.max(1, best[1].totalTokens) return c < b ? curr : best }) : null const efficientCostPerToken = mostEfficient ? mostEfficient[1].totalCost / Math.max(1, mostEfficient[1].totalTokens) : 0 const potentialSavings = Math.max(0, stats.summary.totalCost - stats.summary.totalTokens * efficientCostPerToken) return (
{/* Summary cards */}
{formatCost(stats.summary.totalCost)}
{t('totalCost', { timeframe })}
{formatNumber(stats.summary.totalTokens)}
{t('totalTokens')}
{formatNumber(stats.summary.requestCount)}
{t('apiRequests')}
{agentSummary?.agent_count ?? '-'}
{t('activeAgents')}
{taskData ? `${((1 - taskData.unattributed.totalCost / Math.max(stats.summary.totalCost, 0.0001)) * 100).toFixed(0)}%` : '-'}
{t('taskAttributed')}
{/* Charts */}
{/* Trend chart */}

{t('usageTrends')}

{(['incremental', 'cumulative'] as const).map(m => ( ))}
{trendChartData.length === 0 ? (
{t('noTrendData')}
) : ( )}
{/* Model bar chart */}

{t('tokenUsageByModel')}

{modelData.length === 0 ? (
{t('noModelData')}
) : ( [formatNumber(Number(v)), n]} /> )}
{/* Cost pie */}

{t('costDistributionByModel')}

{pieData.length === 0 ? (
{t('noCostData')}
) : ( {pieData.map((_, i) => )} formatCost(Number(v))} /> )}
{/* Performance insights */} {models.length > 0 && (

{t('performanceInsights')}

{t('mostEfficientModel')}
{mostEfficient ? getModelDisplayName(mostEfficient[0]) : '-'}
{mostEfficient &&
${(efficientCostPerToken * 1000).toFixed(4)}/1K tokens
}
{t('avgTokensPerRequest')}
{formatNumber(stats.summary.avgTokensPerRequest)}
{t('optimizationPotential')}
{formatCost(potentialSavings)}
{stats.summary.totalCost > 0 ? ((potentialSavings / stats.summary.totalCost) * 100).toFixed(1) : '0'}% {t('savingsPossible')}
{/* Model efficiency bars */}
{modelData.map(m => { const costPer1k = m.cost / Math.max(1, m.tokens) * 1000 const maxCostPer1k = Math.max(...modelData.map(d => d.cost / Math.max(1, d.tokens) * 1000), 0.0001) return (
{m.name}
${costPer1k.toFixed(4)}/1K
) })}
)} {/* Export */}

{t('exportData')}

{t('exportDataDesc')}

) } // ── Agents View ────────────────────────────────── function AgentsView({ agents, summary, maxCost, expandedAgent, setExpandedAgent, getAgentTasks, onRefresh, }: { agents: ByAgentEntry[]; summary: ByAgentResponse['summary'] | undefined maxCost: number; expandedAgent: string | null setExpandedAgent: (a: string | null) => void getAgentTasks: (name: string) => TaskCostEntry[]; onRefresh: () => void }) { const t = useTranslations('costTracker') const [expandedSection, setExpandedSection] = useState<'models' | 'tasks'>('tasks') if (!summary || agents.length === 0) { return (
{t('noAgentData')}
{t('noAgentDataDesc')}
) } return (
{/* Summary row */}
{summary.agent_count}
{t('agents')}
{formatCost(summary.total_cost)}
{t('totalCostDays', { days: summary.days })}
{formatNumber(summary.total_tokens)}
{t('totalTokens')}
{summary.total_tokens > 0 ? `$${(summary.total_cost / summary.total_tokens * 1000).toFixed(4)}` : '-'}
{t('avgPer1kTokens')}
{/* Cost bar chart */}

{t('perAgentCost')}

({ name: a.agent.length > 12 ? a.agent.slice(0, 11) + '\u2026' : a.agent, cost: Number(a.total_cost.toFixed(4)), }))}> formatCost(Number(v))} />
{/* Agent detail rows */}

{t('agentBreakdown')}

{agents.map(agent => { const costShare = (agent.total_cost / Math.max(summary.total_cost, 0.0001)) * 100 const isExpanded = expandedAgent === agent.agent const agentTasks = getAgentTasks(agent.agent) return (
{isExpanded && (
{t('inputTokens')}
{formatNumber(agent.total_input_tokens)}
{t('outputTokens')}
{formatNumber(agent.total_output_tokens)}
{t('ioRatio')}
{agent.total_output_tokens > 0 ? (agent.total_input_tokens / agent.total_output_tokens).toFixed(2) : '-'}
{t('lastActive')}
{new Date(agent.last_active).toLocaleDateString()}
{expandedSection === 'tasks' && (
{agentTasks.length === 0 ? (
{t('noTaskCosts')}
) : (
{agentTasks.map(task => (
{task.priority} {task.project.ticketRef && {task.project.ticketRef}} {task.title}
{formatCost(task.stats.totalCost)}
))}
)}
)} {expandedSection === 'models' && agent.models.length > 0 && (
{agent.models.map(m => (
{getModelDisplayName(m.model)}
{formatNumber(m.input_tokens)} in {formatNumber(m.output_tokens)} out {m.request_count} reqs {formatCost(m.cost)}
))}
)}
)}
) })}
) } // ── Sessions View ────────────────────────────────── function SessionsView({ sessionCosts, sessions, sessionSort, setSessionSort, }: { sessionCosts: SessionCostEntry[]; sessions: any[] sessionSort: 'cost' | 'tokens' | 'requests' | 'recent' setSessionSort: (s: 'cost' | 'tokens' | 'requests' | 'recent') => void }) { const t = useTranslations('costTracker') const sorted = [...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 } }) return (
{t('sortBy')}: {(['cost', 'tokens', 'requests', 'recent'] as const).map(s => ( ))}
{sorted.length === 0 ? (

{t('noSessionCostData')}

{t('noSessionCostDataDesc')}

) : (
{sorted.map(entry => { const sessionInfo = sessions.find((s: any) => s.id === entry.sessionId) return (
{entry.sessionKey || sessionInfo?.key || entry.sessionId}
{sessionInfo?.active && } {sessionInfo?.active ? t('activeStatus') : t('inactiveStatus')} {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('inShort')}
{formatNumber(entry.outputTokens || 0)} {t('outShort')}
{entry.totalTokens > 0 ? {formatCost(entry.totalCost / entry.requestCount)} : '-'} {t('avgPerReq')}
) })}
)}
) } // ── Tasks View ────────────────────────────────── function TasksView({ taskData, onRefresh }: { taskData: TaskCostsResponse | null; onRefresh: () => void }) { const t = useTranslations('costTracker') if (!taskData || taskData.tasks.length === 0) { return (
{t('noTaskCostData')}
{t('noTaskCostDataDesc')}
) } return (
{/* Summary */}
{taskData.tasks.length}
{t('tasksWithCosts')}
{formatCost(taskData.summary.totalCost)}
{t('attributedCost')}
{formatNumber(taskData.summary.totalTokens)}
{t('attributedTokens')}
{formatCost(taskData.unattributed.totalCost)}
{t('unattributed')}
{/* Task list */}

{t('tasksByCost')}

{taskData.tasks.map(task => (
{task.priority} {task.project.ticketRef && {task.project.ticketRef}} {task.title} {task.status}
{formatCost(task.stats.totalCost)}
{formatNumber(task.stats.totalTokens)} {t('tokens')} | {task.stats.requestCount} {t('reqs')}
))}
) }