614 lines · 25.4 KB · TypeScript
RawDownload
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 { Button } from "../ui/button";import { ScrollArea } from "../ui/scroll-area";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";import { SidebarInset } from "../ui/sidebar";import { Toggle, ToggleGroup } from "../ui/toggle-group";import {  WorkspaceBreadcrumb,  WorkspaceBreadcrumbItem,  WorkspaceBreadcrumbSeparator,} from "../WorkspaceBreadcrumb";import { WorkspacePageContainer } from "../WorkspacePageContainer";import { WorkspacePageHeader } from "../WorkspacePageHeader";import { 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<UsageChartMetric>("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<readonly (DailyTotals | HourlyTotals)[]>(    () => (isPast24Hours ? merged.hourly : merged.daily).toReversed(),    [isPast24Hours, merged.daily, merged.hourly],  );   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 });    }  };  const windowLabel =    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)}`;  const topbarContent = (    <div className="flex w-full min-w-0 items-center gap-3">      <WorkspaceBreadcrumb ariaLabel="Usage breadcrumb" className="min-w-0">        <WorkspaceBreadcrumbItem current>          <h1>Usage</h1>        </WorkspaceBreadcrumbItem>        <WorkspaceBreadcrumbSeparator className="hidden md:flex" />        <WorkspaceBreadcrumbItem className="hidden min-w-0 shrink md:flex">          <span className="truncate">{windowLabel}</span>        </WorkspaceBreadcrumbItem>      </WorkspaceBreadcrumb>      <div className="ms-auto hidden min-w-0 items-center justify-end gap-2 lg:flex">        <ToggleGroup          aria-label="Usage metric"          variant="segmented"          value={[metric]}          onValueChange={(next) => {            const value = next[0];            if (value === "cost" || value === "tokens") setMetric(value);          }}        >          {(["cost", "tokens"] as const).map((option) => (            <Toggle key={option} value={option}>              {option === "cost" ? "Cost" : "Tokens"}            </Toggle>          ))}        </ToggleGroup>        <ToggleGroup          aria-label="Usage period"          variant="segmented"          value={[String(windowDays)]}          onValueChange={(next) => {            const value = next[0];            if (value) selectWindow(Number(value));          }}        >          {WINDOW_OPTIONS.map((option) => (            <Toggle key={option.days} value={String(option.days)}>              {option.label}            </Toggle>          ))}        </ToggleGroup>        <Button onClick={refreshWindow} aria-label="Refresh usage" size="icon-sm" variant="ghost">          <RefreshCwIcon className="size-3.5" />        </Button>      </div>      <div className="ms-auto flex min-w-0 items-center justify-end gap-1 lg:hidden">        <Select          value={metric}          onValueChange={(value) => {            if (value === "cost" || value === "tokens") setMetric(value);          }}        >          <SelectTrigger            aria-label="Usage metric"            size="compact"            variant="ghost"            className="w-auto min-w-0"          >            <SelectValue>{metric === "cost" ? "Cost" : "Tokens"}</SelectValue>          </SelectTrigger>          <SelectPopup align="end" alignItemWithTrigger={false}>            <SelectItem value="cost">Cost</SelectItem>            <SelectItem value="tokens">Tokens</SelectItem>          </SelectPopup>        </Select>        <Select value={String(windowDays)} onValueChange={(value) => selectWindow(Number(value))}>          <SelectTrigger            aria-label="Usage period"            size="compact"            variant="ghost"            className="w-auto min-w-0"          >            <SelectValue>              {WINDOW_OPTIONS.find((option) => option.days === windowDays)?.label}            </SelectValue>          </SelectTrigger>          <SelectPopup align="end" alignItemWithTrigger={false}>            {WINDOW_OPTIONS.map((option) => (              <SelectItem key={option.days} value={String(option.days)}>                {option.label}              </SelectItem>            ))}          </SelectPopup>        </Select>        <Button onClick={refreshWindow} aria-label="Refresh usage" size="icon-sm" variant="ghost">          <RefreshCwIcon className="size-3.5" />        </Button>      </div>    </div>  );   return (    <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">      <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground">        <WorkspacePageHeader electron={isElectron}>{topbarContent}</WorkspacePageHeader>         <ScrollArea className="min-h-0 flex-1">          <WorkspacePageContainer width="wide">            {settling ? (              <>                {environments.length > 1 ? <UsageDeviceStrip environments={environments} /> : null}                <UsageSkeleton />              </>            ) : (              <>                <UsageCoverageNotice                  environments={environments}                  duplicateSources={merged.duplicateSources}                  staleEnvironments={merged.staleEnvironments}                />                 <section className="grid gap-6 lg:grid-cols-[minmax(0,16rem)_minmax(0,1fr)]">                  <div className="flex min-w-0 flex-col gap-5">                    <div className="flex flex-col gap-1">                      <span className="text-4xl font-semibold text-foreground tabular-nums">                        {metric === "cost"                          ? formatUsd(merged.costUsd)                          : formatTokens(merged.totalTokens)}                      </span>                      <span className="text-xs text-muted-foreground">                        {metric === "cost"                          ? `${formatCount(merged.sessions)} sessions · API estimate`                          : `${formatCount(merged.sessions)} sessions`}                      </span>                    </div>                     {PROVIDER_ORDER.map((provider) => {                      const totals = merged.providers.find((entry) => entry.provider === provider);                      const share =                        metric === "cost" ? (totals?.costShare ?? 0) : (totals?.tokenShare ?? 0);                      const providerSessions = totals?.sessions ?? 0;                      const sessionLabel = `${formatCount(providerSessions)} ${                        providerSessions === 1 ? "session" : "sessions"                      }`;                      return (                        <div key={provider} className="flex flex-col gap-1">                          <div className="flex items-baseline justify-between gap-4">                            <span className="flex min-w-0 items-center gap-2 text-sm text-foreground">                              <span                                aria-hidden                                className="size-2 shrink-0 rounded-full"                                style={{                                  backgroundColor: PROVIDER_PRESENTATION[provider].color,                                }}                              />                              <ProviderMark provider={provider} className="size-4" />                              <span className="flex min-w-0 items-baseline gap-1.5">                                <span className="truncate">                                  {PROVIDER_PRESENTATION[provider].label}                                </span>                                <span className="shrink-0 whitespace-nowrap text-[11px] text-muted-foreground tabular-nums">                                  {sessionLabel}                                </span>                              </span>                            </span>                            <span className="shrink-0 text-sm font-medium text-foreground tabular-nums">                              {metric === "cost"                                ? formatUsd(totals?.costUsd ?? 0)                                : formatTokens(totals?.totalTokens ?? 0)}                            </span>                          </div>                          <span className="text-xs text-muted-foreground">                            {metric === "cost"                              ? `${formatPercent(share)} of cost · ${formatTokens(totals?.totalTokens ?? 0)} tokens`                              : `${formatPercent(share)} of tokens · ${formatUsd(totals?.costUsd ?? 0)}`}                          </span>                        </div>                      );                    })}                  </div>                   <div className="flex min-w-0 flex-col gap-3">                    <h2 className="text-sm font-medium text-foreground">                      {isPast24Hours ? "Hourly" : "Daily"}{" "}                      {metric === "tokens" ? "processed tokens" : "cost"}                    </h2>                    <UsageProviderChart                      days={days}                      daily={merged.daily}                      hours={hours}                      hourly={merged.hourly}                      metric={metric}                      referenceTime={window.untilTime}                      resolution={isPast24Hours ? "hour" : "day"}                      timeZone={window.timeZone}                    />                  </div>                </section>                 <section className="flex flex-col gap-2">                  <h2 className="text-sm font-medium text-foreground">Totals</h2>                  <div className="grid grid-cols-2 gap-x-6 gap-y-4 py-1 md:grid-cols-5">                    <Metric label="Processed tokens" value={formatTokens(merged.totalTokens)} />                    <Metric label="Cached input" value={formatTokens(merged.cachedInputTokens)} />                    <Metric                      label="Uncached input"                      value={formatTokens(merged.uncachedInputTokens)}                    />                    <Metric label="Output" value={formatTokens(merged.outputTokens)} />                    <Metric                      label="Cache savings"                      value={formatUsd(merged.costQuality.cacheSavingsUsd)}                    />                  </div>                </section>                 <section className="flex flex-col gap-3">                  <div className="flex items-center justify-between gap-3">                    <h2 className="text-sm font-medium text-foreground">Breakdown</h2>                    <ToggleGroup                      aria-label="Usage breakdown"                      variant="segmented"                      value={[breakdown]}                      onValueChange={(next) => {                        const value = next[0];                        if (value === "model" || value === "time") setBreakdown(value);                      }}                    >                      {(                        [                          { value: "model", label: "Model" },                          { value: "time", label: isPast24Hours ? "Hour" : "Day" },                        ] as const                      ).map((option) => (                        <Toggle key={option.value} value={option.value}>                          {option.label}                        </Toggle>                      ))}                    </ToggleGroup>                  </div>                   {breakdown === "model" ? (                    <table className="w-full text-sm">                      <thead>                        <tr className="border-b border-border text-left text-xs text-muted-foreground">                          <th className="py-2 font-normal">Model</th>                          <th className="py-2 text-right font-normal">Cost</th>                          <th className="py-2 text-right font-normal">Share</th>                          <th className="py-2 text-right font-normal">Tokens</th>                        </tr>                      </thead>                      <tbody>                        {merged.models.length === 0 ? (                          <tr>                            <td colSpan={4} className="py-6 text-center text-muted-foreground">                              No activity in this window.                            </td>                          </tr>                        ) : (                          merged.models.map((model) => (                            <tr                              key={`${model.provider}:${model.model}`}                              className="border-b border-border/50 transition-colors hover:bg-muted/50"                            >                              <td className="py-2 text-foreground">                                <span className="flex items-center gap-2">                                  <ProviderMark provider={model.provider} className="size-3.5" />                                  {model.model}                                </span>                              </td>                              <td className="py-2 text-right text-foreground tabular-nums">                                {formatUsd(model.costUsd)}                              </td>                              <td className="py-2 text-right text-muted-foreground tabular-nums">                                {formatPercent(model.costShare)}                              </td>                              <td className="py-2 text-right text-muted-foreground tabular-nums">                                {formatTokens(model.totalTokens)}                              </td>                            </tr>                          ))                        )}                      </tbody>                    </table>                  ) : (                    <table className="w-full text-sm">                      <thead>                        <tr className="border-b border-border text-left text-xs text-muted-foreground">                          <th className="py-2 font-normal">{isPast24Hours ? "Hour" : "Day"}</th>                          {PROVIDER_ORDER.map((provider) => (                            <th key={provider} className="py-2 text-right font-normal">                              {PROVIDER_PRESENTATION[provider].label}                            </th>                          ))}                          <th className="py-2 text-right font-normal">Total</th>                          <th className="py-2 text-right font-normal">Tokens</th>                        </tr>                      </thead>                      <tbody>                        {breakdownPeriods.length === 0 ? (                          <tr>                            <td colSpan={5} className="py-6 text-center text-muted-foreground">                              No activity in this window.                            </td>                          </tr>                        ) : (                          breakdownPeriods.map((period) => (                            <tr                              key={"hourStart" in period ? period.hourStart : period.day}                              className="border-b border-border/50 transition-colors hover:bg-muted/50"                            >                              <td className="py-2 text-foreground">                                {"hourStart" in period                                  ? formatHourShort(period.hourStart, window.timeZone)                                  : formatDayShort(period.day)}                              </td>                              {PROVIDER_ORDER.map((provider) => (                                <td                                  key={provider}                                  className="py-2 text-right text-muted-foreground tabular-nums"                                >                                  {formatUsd(period.byProvider.get(provider)?.costUsd ?? 0)}                                </td>                              ))}                              <td className="py-2 text-right text-foreground tabular-nums">                                {formatUsd(period.costUsd)}                              </td>                              <td className="py-2 text-right text-muted-foreground tabular-nums">                                {formatTokens(period.totalTokens)}                              </td>                            </tr>                          ))                        )}                      </tbody>                    </table>                  )}                </section>              </>            )}          </WorkspacePageContainer>        </ScrollArea>      </div>    </SidebarInset>  );} /** 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 <Mark className={cn("shrink-0", className)} aria-hidden />;} function Metric({ label, value }: { readonly label: string; readonly value: string }) {  return (    <div className="flex min-w-0 flex-col gap-0.5">      <span className="text-xs text-muted-foreground">{label}</span>      <span className="text-base font-medium text-foreground tabular-nums">{value}</span>    </div>  );} /** * 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 (    <div className="flex flex-col gap-1 border border-border px-3 py-2 text-xs text-muted-foreground">      {failed.map((environment) => (        <span key={environment.label}>{environment.label} could not report usage.</span>      ))}      {stale.map((environment) => (        <span key={environment.label}>          {environment.label} runs an older server version and is excluded from totals.        </span>      ))}      {duplicateSources.length > 0 ? (        <span>          Counted once across environments sharing a transcript directory:{" "}          {duplicateSources.join(", ")}        </span>      ) : null}    </div>  );}

Lines 1–500 of 614.