import type { UsageProviderKind } from "@t3tools/contracts"; import { CheckIcon, RefreshCwIcon, XIcon } from "lucide-react"; import { useMemo, useState } from "react"; import type { DailyTotals, HourlyTotals } from "@t3tools/shared/usageMerge"; import { isElectron } from "../../env"; import { cn } from "../../lib/utils"; import { useUsage, type EnvironmentUsageStatus } from "../../state/usage"; import { enumerateDays, enumerateHourStarts, formatCount, formatDateTimeShort, formatDayShort, formatHourShort, formatPercent, formatTokens, formatUsd, makeWindow, } from "@t3tools/shared/usageFormat"; import { ScrollArea } from "../ui/scroll-area"; import { Button } from "../ui/button"; import { SidebarInset } from "../ui/sidebar"; import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb"; import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar"; import { UsageChartLegend, UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart"; import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders"; const WINDOW_OPTIONS = [ { days: 1, label: "Past 24h" }, { days: 7, label: "7 days" }, { days: 30, label: "30 days" }, { days: 90, label: "90 days" }, ] as const; export function UsagePage() { const [windowSelection, setWindowSelection] = useState(() => ({ days: 30, window: makeWindow(30), })); const [metric, setMetric] = useState("cost"); const [breakdown, setBreakdown] = useState<"model" | "time">("model"); const { days: windowDays, window } = windowSelection; const isPast24Hours = windowDays === 1; const { merged, environments, isPending, isPartial, refresh } = useUsage(window); // Hold the content until every environment is terminal. Rendering merged // totals while devices are still answering makes every number on the page // jump as each one lands. const settling = isPending || isPartial; const days = useMemo( () => enumerateDays(window.sinceDay, window.untilDay), [window.sinceDay, window.untilDay], ); const hours = useMemo( () => window.sinceTime === undefined || window.untilTime === undefined ? [] : enumerateHourStarts(window.sinceTime, window.untilTime), [window.sinceTime, window.untilTime], ); // Newest first: the window can run 90 periods, so the interesting end // belongs at the top of the table. const breakdownPeriods = useMemo( () => (isPast24Hours ? merged.hourly : merged.daily).toReversed(), [isPast24Hours, merged.daily, merged.hourly], ); // Ranked by whatever the toggle is showing, so the bars always descend. const orderedProviders = useMemo( () => merged.providers.toSorted((a, b) => metric === "cost" ? b.costUsd - a.costUsd : b.totalTokens - a.totalTokens, ), [merged.providers, metric], ); const activePeriods = (isPast24Hours ? merged.hourly : merged.daily).filter( (period) => period.totalTokens > 0, ).length; const periodAverage = activePeriods === 0 ? 0 : merged.totalTokens / activePeriods; const observedInput = merged.uncachedInputTokens + merged.cachedInputTokens; const cachedShare = observedInput === 0 ? 0 : merged.cachedInputTokens / observedInput; const selectWindow = (days: number) => { setWindowSelection({ days, window: makeWindow(days, undefined, days === 1 ? "hour" : "day"), }); }; const refreshWindow = () => { const nextWindow = makeWindow(windowDays, undefined, isPast24Hours ? "hour" : "day"); if ( nextWindow.sinceDay === window.sinceDay && nextWindow.untilDay === window.untilDay && nextWindow.sinceTime === window.sinceTime && nextWindow.untilTime === window.untilTime ) { refresh(); } else { setWindowSelection({ days: windowDays, window: nextWindow }); } }; return (
{!isElectron && (
Usage
)} {isElectron && (
Usage
)}

{isPast24Hours && window.sinceTime !== undefined && window.untilTime !== undefined ? `${formatDateTimeShort(window.sinceTime, window.timeZone)} to ${formatDateTimeShort(window.untilTime, window.timeZone)}` : `${formatDayShort(window.sinceDay)} to ${formatDayShort(window.untilDay)}`}

{WINDOW_OPTIONS.map((option) => ( ))}
{settling ? ( <> {environments.length > 1 ? : null} ) : ( <> {/* Cost first: the financial answer, then the provider split. */}
{/* The summary follows the chart toggle, so the headline and the series are always reading the same units. */}
{metric === "cost" ? "Raw token cost" : "Processed tokens"} {metric === "cost" ? `${formatUsd(merged.costUsd)}*` : formatTokens(merged.totalTokens)} {metric === "cost" ? "* if billed at full API rate" : `Input, cache reads and output across ${formatCount(merged.sessions)} sessions.`}
{orderedProviders.map((provider) => { const share = metric === "cost" ? provider.costShare : provider.tokenShare; return (
{PROVIDER_PRESENTATION[provider.provider].label} {metric === "cost" ? formatUsd(provider.costUsd) : formatTokens(provider.totalTokens)}
{metric === "cost" ? `${formatPercent(share)} of cost · ${formatTokens(provider.totalTokens)} tokens` : `${formatPercent(share)} of tokens · ${formatUsd(provider.costUsd)}`}
); })}

{isPast24Hours ? "Hourly" : "Daily"}{" "} {metric === "tokens" ? "processed tokens" : "cost"}

{(["cost", "tokens"] as const).map((option) => ( ))}
0 ? `${(merged.costQuality.cacheSavingsUsd / merged.costUsd).toFixed(1)}x the raw token cost` : "vs full input rates" } />

Breakdown

{( [ { value: "model", label: "model" }, { value: "time", label: isPast24Hours ? "hour" : "day" }, ] as const ).map((option) => ( ))}
{breakdown === "model" ? ( {merged.models.length === 0 ? ( ) : ( merged.models.map((model) => ( )) )}
Model Cost Share Tokens
No activity in this window.
{model.model} {formatUsd(model.costUsd)} {formatPercent(model.costShare)} {formatTokens(model.totalTokens)}
) : ( {PROVIDER_ORDER.map((provider) => ( ))} {breakdownPeriods.length === 0 ? ( ) : ( breakdownPeriods.map((period) => ( {PROVIDER_ORDER.map((provider) => ( ))} )) )}
{isPast24Hours ? "Hour" : "Day"} {PROVIDER_PRESENTATION[provider].label} Total Tokens
No activity in this window.
{"hourStart" in period ? formatHourShort(period.hourStart, window.timeZone) : formatDayShort(period.day)} {formatUsd(period.byProvider.get(provider)?.costUsd ?? 0)} {formatUsd(period.costUsd)} {formatTokens(period.totalTokens)}
)}
)}
); } /** Brand mark for the harness a row belongs to. */ function ProviderMark({ provider, className, }: { readonly provider: UsageProviderKind; readonly className: string; }) { const Mark = PROVIDER_PRESENTATION[provider].mark; return ; } function Metric({ label, value, detail, }: { readonly label: string; readonly value: string; readonly detail: string; }) { return (
{label} {value} {detail}
); } /** * Says plainly when the totals are incomplete: an environment that failed, or * one whose transcripts another environment already reported. Environments * that are still answering never reach this notice; the page shows the * loading skeleton until every one is terminal. */ function UsageCoverageNotice({ environments, duplicateSources, staleEnvironments, }: { readonly environments: readonly EnvironmentUsageStatus[]; readonly duplicateSources: readonly string[]; readonly staleEnvironments: readonly string[]; }) { const failed = environments.filter((environment) => environment.error !== null); const stale = environments.filter((environment) => staleEnvironments.includes(environment.environmentId), ); if (failed.length === 0 && stale.length === 0 && duplicateSources.length === 0) { return null; } return (
{failed.map((environment) => ( {environment.label} could not report usage. ))} {stale.map((environment) => ( {environment.label} runs an older server version and is excluded from totals. ))} {duplicateSources.length > 0 ? ( Counted once across environments sharing a transcript directory:{" "} {duplicateSources.join(", ")} ) : null}
); } /** * Per-device progress while the page waits for every environment to answer. * Only rendered with two or more devices; a lone device has nothing to * enumerate. */ function UsageDeviceStrip({ environments, }: { readonly environments: readonly EnvironmentUsageStatus[]; }) { const scanning = environments.filter( (environment) => environment.summary === null && environment.error === null, ); return (
{environments.map((environment) => { if (environment.summary !== null) { return ( {environment.label} ); } if (environment.error !== null) { return ( {environment.label} ); } return ( {environment.label}… ); })} {scanning.length === 1 ? "1 device still scanning" : `${scanning.length} devices still scanning`}
); } /** Deterministic bar heights (each unique: they double as keys). */ const SKELETON_BAR_HEIGHTS = [34, 58, 41, 72, 22, 12, 49, 63, 80, 38, 55, 26, 44, 67]; /** * Static stand-in with the loaded page's shape: headline, provider split, * chart and metrics strip. No shimmer; blocks fill in exactly once when the * last device answers. */ function UsageSkeleton({ resolution }: { readonly resolution: "day" | "hour" }) { return ( <>
Raw token cost
{PROVIDER_ORDER.map((provider) => (
{PROVIDER_PRESENTATION[provider].label}
))}

{resolution === "hour" ? "Hourly" : "Daily"} cost

{/* Mirrors the chart's h-56 body and w-14 axis gutter to avoid a relayout when the real chart swaps in. */}
{SKELETON_BAR_HEIGHTS.map((height) => (
))}
{["Processed tokens", "Cached input", "Uncached input", "Output", "Cache savings"].map( (label) => (
{label}
), )}
); }