"use client"; import * as React from "react"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { BarChart3, Download, Info, Menu, RefreshCw } from "lucide-react"; import { PageHeader, EmptyState, Skeleton } from "@/components/ui/misc"; import { Button } from "@/components/ui/button"; import { Tooltip } from "@/components/ui/tooltip"; import { ProviderIcon } from "@/components/brand/provider-icon"; import { useApp } from "@/components/app/store"; import { useApi } from "@/lib/client/api"; import { providerName } from "@/lib/client/providers"; import { formatUsd, formatNumber, cn } from "@/lib/utils"; import { errorMessage } from "@/lib/client/humanize"; import { isProviderId } from "@/lib/ai/core/types"; import type { ProviderId } from "@/lib/client/types"; import { ChartCard, Legend, providerColor, type ModelRow } from "./charts"; import { CostOverTime, RequestsOverTime, TokensOverTime, ProviderDonut, ModelBars, LatencyByModel } from "./lazy-charts"; import { KpiGrid, KpiSkeleton } from "./kpi-tiles"; import { RangePicker, FilterChips, ActiveFilters, DEFAULT_QUERY, rangeLabel, type UsageQueryState } from "./usage-filters"; import { ProjectionCard, SavingsCard } from "./insight-cards"; import { RecentActivity } from "./recent-activity"; import type { UsageSummary, UsageRangeKey } from "./types"; const RANGE_KEYS: UsageRangeKey[] = ["today", "7d", "30d", "90d", "custom", "all"]; function readQuery(sp: URLSearchParams | null): UsageQueryState { if (!sp) return DEFAULT_QUERY; const r = sp.get("range"); const provider = sp.get("provider") ?? ""; return { range: r && (RANGE_KEYS as string[]).includes(r) ? (r as UsageRangeKey) : DEFAULT_QUERY.range, from: sp.get("from") ?? "", to: sp.get("to") ?? "", provider: isProviderId(provider) ? provider : "", modelKey: sp.get("modelKey") ?? "", projectId: sp.get("projectId") ?? "", }; } function toSearch(q: UsageQueryState, tz: string): string { const sp = new URLSearchParams(); sp.set("range", q.range); if (q.range === "custom") { if (q.from) sp.set("from", q.from); if (q.to) sp.set("to", q.to); } if (q.provider) sp.set("provider", q.provider); if (q.modelKey) sp.set("modelKey", q.modelKey); if (q.projectId) sp.set("projectId", q.projectId); sp.set("tz", tz); return sp.toString(); } export function UsageDashboard() { const router = useRouter(); const searchParams = useSearchParams(); const { modelsByKey, labels, setSidebarOpen } = useApp(); const tz = React.useMemo(() => Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC", []); const [query, setQueryState] = React.useState(() => readQuery(searchParams)); const setQuery = React.useCallback( (patch: Partial) => { const next = { ...query, ...patch }; setQueryState(next); // Keep the URL shareable (deep links from model pages / palette); tz is derived client-side. const sp = new URLSearchParams(toSearch(next, tz)); sp.delete("tz"); router.replace(`/app/usage?${sp.toString()}`, { scroll: false }); }, [query, router, tz], ); const search = toSearch(query, tz); const q = useApi(`/api/usage?${search}`, { keepPreviousData: true, revalidateOnFocus: true }); const data = q.data; const hourly = data?.range.bucket === "hour"; const modelLabel = React.useCallback( (key: string) => { const m = modelsByKey.get(key); return labels[key] ?? m?.displayName ?? (key.startsWith("custom/") ? key.split(":").slice(1).join(":") : key.split("/").slice(1).join("/") || key); }, [modelsByKey, labels], ); const modelRows = React.useMemo( () => (data?.byModel ?? []).map((m) => ({ modelKey: m.modelKey, provider: m.provider, label: modelLabel(m.modelKey), requests: m.requests, costUsd: m.costUsd, inputTokens: m.inputTokens, outputTokens: m.outputTokens, tokensPerSec: m.tokensPerSec, avgLatencyMs: m.avgLatencyMs, avgTtftMs: m.avgTtftMs, failures: m.failures, })), [data?.byModel, modelLabel], ); const providersUsed = React.useMemo(() => (data?.byProvider ?? []).filter((p) => p.requests > 0).map((p) => p.provider), [data?.byProvider]); const empty = data ? data.kpis.requests === 0 : false; const hasFilters = Boolean(query.provider || query.modelKey || query.projectId); const exportHref = `/api/usage/export.csv?${search}`; const header = ( <> {/* Phone: compact 48 px bar */}

Usage

{/* Desktop */}
Requests, tokens, latency and estimated spend across your providers. Costs are estimates } actions={ <> } />
); return (
{header} {/* One filter row scopes every chart below it */}
{data ? (

{rangeLabel(query)} · {hourly ? "hourly" : "daily"} · {data.range.tz}

) : null}
{q.error ? (
void q.mutate()}>Retry} />
) : !data ? (
) : empty ? (
} title={hasFilters ? "No requests match these filters" : query.range === "all" ? "No usage yet" : `No usage in ${rangeLabel(query).toLowerCase()}`} description={hasFilters ? "Try clearing a filter or widening the time range." : "Every chat and Arena request is recorded here with tokens, latency and an estimated cost."} action={
{hasFilters ? ( ) : null} {query.range !== "all" ? ( ) : null}
} />
) : (
{/* Time series */}
) : undefined } >
} > 1 ? ({ label: providerName(p), color: providerColor(p), icon: }))} /> : undefined}>
{/* Insights */}
{/* Per model */}
formatUsd(v)} emptyText="No priced requests in this period." /> formatNumber(v)} />

All costs on this page are estimates from public list prices at request time; billing happens on your provider accounts. {data.kpis.unpricedRequests ? ` ${formatNumber(data.kpis.unpricedRequests)} request${data.kpis.unpricedRequests === 1 ? "" : "s"} had no known price (custom endpoints or unpriced models) and count as $0.` : ""}

)}
); }