usage-list.tsx
1 'use client' 2 3 import { useEffect, useState } from 'react' 4 import { api } from '@/lib/app/api-client' 5 6 interface UsageData { 7 totalTokens: number 8 totalCost: number 9 byProvider: Record<string, { cost: number; tokens: number; requests: number }> 10 bySession: Record<string, { cost: number; tokens: number; requests: number }> 11 raw: unknown[] 12 } 13 14 export function UsageList() { 15 const [data, setData] = useState<UsageData | null>(null) 16 const [loading, setLoading] = useState(true) 17 18 useEffect(() => { 19 api<UsageData>('GET', '/usage') 20 .then(setData) 21 .catch(() => {}) 22 .finally(() => setLoading(false)) 23 }, []) 24 25 if (loading) { 26 return ( 27 <div className="flex-1 flex items-center justify-center text-text-3 text-[13px]"> 28 Loading usage... 29 </div> 30 ) 31 } 32 33 if (!data) { 34 return ( 35 <div className="flex-1 flex items-center justify-center text-text-3 text-[13px]"> 36 Failed to load usage data 37 </div> 38 ) 39 } 40 41 const providers = Object.entries(data.byProvider || {}).sort( 42 ([, a], [, b]) => b.cost - a.cost 43 ) 44 45 const formatCost = (cost: number) => 46 cost < 0.01 ? `$${cost.toFixed(4)}` : `$${cost.toFixed(2)}` 47 48 const formatTokens = (tokens: number) => { 49 if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M` 50 if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}K` 51 return String(tokens) 52 } 53 54 return ( 55 <div className="flex-1 overflow-y-auto px-5 pb-8"> 56 {/* Summary */} 57 <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4 mt-1"> 58 <div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]"> 59 <div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Total Cost</div> 60 <div className="text-[18px] font-700 text-text tracking-tight">{formatCost(data.totalCost)}</div> 61 </div> 62 <div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]"> 63 <div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Total Tokens</div> 64 <div className="text-[18px] font-700 text-text tracking-tight">{formatTokens(data.totalTokens)}</div> 65 </div> 66 <div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]"> 67 <div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Total Requests</div> 68 <div className="text-[18px] font-700 text-text tracking-tight">{providers.reduce((sum, [, s]) => sum + s.requests, 0)}</div> 69 </div> 70 <div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]"> 71 <div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Providers</div> 72 <div className="text-[18px] font-700 text-text tracking-tight">{providers.length}</div> 73 </div> 74 </div> 75 76 {/* Provider breakdown */} 77 <div className="mb-2"> 78 <h3 className="text-[11px] font-600 text-text-3 uppercase tracking-wider mb-2">By Provider</h3> 79 {providers.length === 0 ? ( 80 <div className="text-center py-6 text-[12px] text-text-3/60">No usage data yet</div> 81 ) : ( 82 <div className="grid grid-cols-1 md:grid-cols-2 gap-3"> 83 {providers.map(([provider, stats]) => { 84 const pct = data.totalCost > 0 ? (stats.cost / data.totalCost) * 100 : 0 85 return ( 86 <div 87 key={provider} 88 className="p-3 rounded-[10px] bg-surface border border-white/[0.06] hover:bg-surface-2 transition-colors" 89 > 90 <div className="flex items-center justify-between mb-1.5"> 91 <span className="text-[13px] font-600 text-text capitalize">{provider}</span> 92 <span className="text-[13px] font-700 text-text tabular-nums">{formatCost(stats.cost)}</span> 93 </div> 94 <div className="flex items-center gap-3 text-[11px] text-text-3"> 95 <span>{formatTokens(stats.tokens)} tokens</span> 96 <span>{stats.requests} requests</span> 97 <span className="ml-auto">{pct.toFixed(1)}%</span> 98 </div> 99 <div className="mt-2 h-1 rounded-full bg-white/[0.04] overflow-hidden"> 100 <div 101 className="h-full rounded-full bg-accent-bright/60" 102 style={{ width: `${Math.max(pct, 1)}%` }} 103 /> 104 </div> 105 </div> 106 ) 107 })} 108 </div> 109 )} 110 </div> 111 </div> 112 ) 113 }