"use client";
import { useState, useEffect, useCallback } from "react";
import type { ReactElement } from "react";
import {
LineChart,
Line,
BarChart,
Bar,
PieChart,
Pie,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from "recharts";
import { Card, CardHeader, CardContent, CardTitle, CardDescription } from "@/components/ui/card";
import {
fetchUsageSummary,
fetchTokenUsage,
fetchCostBreakdown,
fetchTaskOutcomes,
} from "@/lib/api/telemetry";
import type {
TimeRange,
UsageSummary,
TokenUsagePoint,
CostBreakdownItem,
TaskOutcomeItem,
} from "@/lib/api/telemetry";
// ─── Constants ───────────────────────────────────────────────────────
const TIME_RANGES: { value: TimeRange; label: string }[] = [
{ value: "7d", label: "7 Days" },
{ value: "30d", label: "30 Days" },
{ value: "90d", label: "90 Days" },
];
// Calm, PDA-friendly chart colors (no aggressive reds)
const CHART_COLORS = {
inputTokens: "#6366F1", // Indigo
outputTokens: "#38BDF8", // Sky blue
grid: "#E2E8F0", // Slate 200
barFill: "#818CF8", // Indigo 400
};
// ─── Helpers ─────────────────────────────────────────────────────────
function formatNumber(value: number): string {
if (value >= 1_000_000) {
return `${(value / 1_000_000).toFixed(1)}M`;
}
if (value >= 1_000) {
return `${(value / 1_000).toFixed(1)}K`;
}
return value.toFixed(0);
}
function formatCurrency(value: number): string {
return `$${value.toFixed(2)}`;
}
function formatPercent(value: number): string {
return `${(value * 100).toFixed(1)}%`;
}
function formatDateLabel(dateStr: string): string {
const date = new Date(dateStr + "T00:00:00");
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
}
/**
* Map TaskOutcomeItem[] to recharts-compatible data with `fill` property.
* This replaces deprecated Cell component (removed in Recharts 4.0).
*/
function toFillData(
outcomes: TaskOutcomeItem[]
): { outcome: string; count: number; fill: string }[] {
return outcomes.map((item) => ({
outcome: item.outcome,
count: item.count,
fill: item.color,
}));
}
// ─── Sub-components ──────────────────────────────────────────────────
function SummaryCard({
title,
value,
subtitle,
}: {
title: string;
value: string;
subtitle?: string;
}): ReactElement {
return (
{title}
{value}
{subtitle ? {subtitle}
: null}
);
}
function LoadingSkeleton(): ReactElement {
return (
{/* Summary cards skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
))}
{/* Chart skeletons */}
{Array.from({ length: 3 }).map((_, i) => (
))}
);
}
function EmptyState(): ReactElement {
return (
📊
No usage data yet
Once you start using AI-powered features, your token usage and cost data will appear here.
);
}
// ─── Main Page Component ─────────────────────────────────────────────
export default function UsagePage(): ReactElement {
const [timeRange, setTimeRange] = useState("30d");
const [isLoading, setIsLoading] = useState(true);
const [isEmpty, setIsEmpty] = useState(false);
const [error, setError] = useState(null);
const [summary, setSummary] = useState(null);
const [tokenUsage, setTokenUsage] = useState([]);
const [costBreakdown, setCostBreakdown] = useState([]);
const [taskOutcomes, setTaskOutcomes] = useState([]);
const loadData = useCallback(async (range: TimeRange): Promise => {
setIsLoading(true);
setError(null);
try {
const [summaryData, tokenData, costData, outcomeData] = await Promise.all([
fetchUsageSummary(range),
fetchTokenUsage(range),
fetchCostBreakdown(range),
fetchTaskOutcomes(range),
]);
setSummary(summaryData);
setTokenUsage(tokenData);
setCostBreakdown(costData);
setTaskOutcomes(outcomeData);
// Check if there's any meaningful data
setIsEmpty(summaryData.taskCount === 0);
} catch (err) {
setError(
err instanceof Error
? err.message
: "We had trouble loading usage data. Please try again when you're ready."
);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
void loadData(timeRange);
}, [timeRange, loadData]);
function handleTimeRangeChange(range: TimeRange): void {
setTimeRange(range);
}
return (
{/* Header */}
Usage
Token usage and cost overview
{/* Time range selector */}
{TIME_RANGES.map(({ value, label }) => (
))}
{/* Error state */}
{error !== null ? (
{error}
) : isLoading ? (
) : isEmpty ? (
) : (
{/* Summary Cards */}
{/* Charts */}
{/* Token Usage Over Time — Full width */}
Token Usage Over Time
Input and output tokens by day
[
formatNumber(value),
name === "inputTokens" ? "Input Tokens" : "Output Tokens",
]) as never
}
labelFormatter={((label: string) => formatDateLabel(label)) as never}
contentStyle={{
borderRadius: "8px",
border: "1px solid #E2E8F0",
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
}}
/>
{/* Cost Breakdown by Model */}
Cost by Model
Estimated cost breakdown
formatCurrency(v)}
tick={{ fontSize: 12, fill: "#64748B" }}
/>
[formatCurrency(value), "Cost"]) as never}
contentStyle={{
borderRadius: "8px",
border: "1px solid #E2E8F0",
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
}}
/>
{/* Task Outcomes */}
Task Outcomes
Distribution of task completion results
`${props.outcome ?? ""}: ${String(props.count ?? 0)}`) as never
}
/>
[value, name]) as never}
contentStyle={{
borderRadius: "8px",
border: "1px solid #E2E8F0",
boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
}}
/>
)}
);
}