import React, { useState, useEffect } from "react"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Badge } from "@/components/ui/badge"; import { api, type UsageStats, type ProjectUsage } from "@/lib/api"; import { ArrowLeft, TrendingUp, Calendar, Filter, Loader2, DollarSign, Activity, FileText, Briefcase } from "lucide-react"; import { cn } from "@/lib/utils"; import { useTranslation } from "@/hooks/useTranslation"; interface UsageDashboardProps { /** * Callback when back button is clicked */ onBack: () => void; } /** * UsageDashboard component - Displays Claude API usage statistics and costs * * @example * setView('welcome')} /> */ export const UsageDashboard: React.FC = ({ onBack }) => { const { t } = useTranslation(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [stats, setStats] = useState(null); const [sessionStats, setSessionStats] = useState(null); const [selectedDateRange, setSelectedDateRange] = useState<"all" | "7d" | "30d">("all"); const [activeTab, setActiveTab] = useState("overview"); useEffect(() => { loadUsageStats(); }, [selectedDateRange]); const loadUsageStats = async () => { try { setLoading(true); setError(null); let statsData: UsageStats; let sessionData: ProjectUsage[]; if (selectedDateRange === "all") { statsData = await api.getUsageStats(); sessionData = await api.getSessionStats(); } else { const endDate = new Date(); const startDate = new Date(); const days = selectedDateRange === "7d" ? 7 : 30; startDate.setDate(startDate.getDate() - days); const formatDateForApi = (date: Date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); return `${year}${month}${day}`; } statsData = await api.getUsageByDateRange( startDate.toISOString(), endDate.toISOString() ); sessionData = await api.getSessionStats( formatDateForApi(startDate), formatDateForApi(endDate), 'desc' ); } setStats(statsData); setSessionStats(sessionData); } catch (err) { console.error("Failed to load usage stats:", err); setError(t('usage.failedToLoadUsageStats')); } finally { setLoading(false); } }; const formatCurrency = (amount: number): string => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 4 }).format(amount); }; const formatNumber = (num: number): string => { return new Intl.NumberFormat('en-US').format(num); }; const formatTokens = (num: number): string => { if (num >= 1_000_000) { return `${(num / 1_000_000).toFixed(2)}M`; } else if (num >= 1_000) { return `${(num / 1_000).toFixed(1)}K`; } return formatNumber(num); }; const getModelDisplayName = (model: string): string => { const modelMap: Record = { "claude-4-opus": "Opus 4", "claude-4-sonnet": "Sonnet 4", "claude-3.5-sonnet": "Sonnet 3.5", "claude-3-opus": "Opus 3", }; return modelMap[model] || model; }; const getModelColor = (model: string): string => { if (model.includes("opus")) return "text-purple-500"; if (model.includes("sonnet")) return "text-blue-500"; return "text-gray-500"; }; return (
{/* Header */}

{t('usage.usageDashboardTitle')}

{t('usage.trackUsageAndCosts')}

{/* Date Range Filter */}
{(["all", "30d", "7d"] as const).map((range) => ( ))}
{/* Main Content */}
{loading ? (

{t('usage.loadingUsageStats')}

) : error ? (

{error}

) : stats ? ( {/* Summary Cards */}
{/* Total Cost Card */}

{t('usage.totalCost')}

{formatCurrency(stats.total_cost)}

{/* Total Sessions Card */}

{t('usage.totalSessions')}

{formatNumber(stats.total_sessions)}

{/* Total Tokens Card */}

{t('usage.totalTokens')}

{formatTokens(stats.total_tokens)}

{/* Average Cost per Session Card */}

{t('usage.avgCostPerSession')}

{formatCurrency( stats.total_sessions > 0 ? stats.total_cost / stats.total_sessions : 0 )}

{/* Tabs for different views */} {t('usage.overview')} {t('usage.byModel')} {t('usage.byProject')} {t('usage.byDate')} {t('usage.timeline')} {/* Overview Tab */}

{t('usage.tokenBreakdown')}

{t('usage.inputTokens')}

{formatTokens(stats.total_input_tokens)}

{t('usage.outputTokens')}

{formatTokens(stats.total_output_tokens)}

{t('usage.cacheWrite')}

{formatTokens(stats.total_cache_creation_tokens)}

{t('usage.cacheRead')}

{formatTokens(stats.total_cache_read_tokens)}

{/* Quick Stats */}

{t('usage.mostUsedModels')}

{stats.by_model.slice(0, 3).map((model) => (
{getModelDisplayName(model.model)} {model.session_count} {t('usage.sessions')}
{formatCurrency(model.total_cost)}
))}

{t('usage.topProjects')}

{stats.by_project.slice(0, 3).map((project) => (
{project.project_path} {project.session_count} {t('usage.sessions')}
{formatCurrency(project.total_cost)}
))}
{/* Models Tab */}

{t('usage.usageByModel')}

{stats.by_model.map((model) => (
{getModelDisplayName(model.model)} {model.session_count} {t('usage.sessions')}
{formatCurrency(model.total_cost)}
{t('usage.input')}: {formatTokens(model.input_tokens)}
{t('usage.output')}: {formatTokens(model.output_tokens)}
Cache W: {formatTokens(model.cache_creation_tokens)}
Cache R: {formatTokens(model.cache_read_tokens)}
))}
{/* Projects Tab */}

{t('usage.usageByProject')}

{stats.by_project.map((project) => (
{project.project_path}
{project.session_count} {t('usage.sessions')} {formatTokens(project.total_tokens)} {t('usage.tokens')}

{formatCurrency(project.total_cost)}

{formatCurrency(project.total_cost / project.session_count)}/{t('usage.session')}

))}
{/* Sessions Tab */}

{t('usage.usageBySession')}

{sessionStats?.map((session) => (
{session.project_path.split('/').slice(-2).join('/')}
{session.project_name}

{formatCurrency(session.total_cost)}

{new Date(session.last_used).toLocaleDateString()}

))}
{/* Timeline Tab */}

{t('usage.dailyUsage')}

{stats.by_date.length > 0 ? (() => { const maxCost = Math.max(...stats.by_date.map(d => d.total_cost), 0); const halfMaxCost = maxCost / 2; return (
{/* Y-axis labels */}
{formatCurrency(maxCost)} {formatCurrency(halfMaxCost)} {formatCurrency(0)}
{/* Chart container */}
{stats.by_date.slice().reverse().map((day) => { const heightPercent = maxCost > 0 ? (day.total_cost / maxCost) * 100 : 0; const date = new Date(day.date.replace(/-/g, '/')); const formattedDate = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); return (
{/* Tooltip */}

{formattedDate}

{t('usage.cost')}: {formatCurrency(day.total_cost)}

{formatTokens(day.total_tokens)} {t('usage.tokens')}

{day.models_used.length} {t('usage.models')}{day.models_used.length !== 1 ? 's' : ''}

{/* Bar */}
{/* X-axis label – absolutely positioned below the bar so it doesn't affect bar height */}
{date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
); })}
{/* X-axis label */}
{t('usage.dailyUsageOverTime')}
) })() : (
{t('usage.noUsageData')}
)} ) : null}
); };