884 lines · 36.1 KB · TypeScript
RawDownload
import { RefreshIcon } from "~/components/ui/refresh-icon";import { useAtomValue } from "@effect/atom-react";import {  USAGE_CONTRACT_VERSION,  type EnvironmentId,  type UsageProviderKind,} from "@t3tools/contracts";import {  CircleAlertIcon,  ChevronDownIcon,  CircleDashedIcon,  SlidersHorizontalIcon,} from "lucide-react";import { useMemo, useRef, useState } from "react"; import {  isCompatibleUsageContractVersion,  isModelCostUnknown,  type DailyTotals,  type HourlyTotals,} from "@t3tools/shared/usageMerge"; import { isElectron } from "../../env";import { cn } from "../../lib/utils";import { environmentPresentations } from "../../state/presentation";import { serverEnvironment } from "../../state/server";import { useUsage, type EnvironmentUsageStatus } from "../../state/usage";import { useAtomCommand } from "../../state/use-atom-command";import {  enumerateDays,  enumerateHourStarts,  formatCount,  formatDateTimeShort,  formatDayShort,  formatHourShort,  formatPercent,  formatTokens,  formatUsd,  makeWindow,} from "@t3tools/shared/usageFormat";import { Button } from "../ui/button";import {  Menu,  MenuCheckboxItem,  MenuItem,  MenuPopup,  MenuSeparator,  MenuTrigger,} from "../ui/menu";import { ScrollArea } from "../ui/scroll-area";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";import { SidebarInset } from "../ui/sidebar";import { Skeleton } from "../ui/skeleton";import { Toggle, ToggleGroup } from "../ui/toggle-group";import {  WorkspaceBreadcrumb,  WorkspaceBreadcrumbItem,  WorkspaceBreadcrumbSeparator,} from "../WorkspaceBreadcrumb";import { WorkspacePageContainer } from "../WorkspacePageContainer";import { WorkspacePageHeader } from "../WorkspacePageHeader";import { UsageLimitsSection } from "./UsageLimits";import { UsagePriceOverrides } from "./UsagePriceOverrides";import { UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart";import { PROVIDER_ORDER, PROVIDER_PRESENTATION, providersWithUsage } from "./usageProviders";import {  readUsagePagePreferences,  saveUsagePagePreferences,  type UsagePagePreferences,} from "./usagePagePreferences"; type UsageMetric = UsageChartMetric | "limits";const METRIC_OPTIONS = [  { value: "cost", label: "Cost" },  { value: "tokens", label: "Tokens" },  { value: "limits", label: "Limits" },] as const satisfies readonly { value: UsageMetric; label: string }[]; function isUsageMetric(value: string | null | undefined): value is UsageMetric {  return METRIC_OPTIONS.some((option) => option.value === value);} 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; function isUsageWindowDays(value: number): value is UsagePagePreferences["windowDays"] {  return WINDOW_OPTIONS.some((option) => option.days === value);} export function UsagePage() {  const [preferences, setPreferences] = useState(readUsagePagePreferences);  const [windowSelection, setWindowSelection] = useState(() => ({    days: preferences.windowDays,    window: makeWindow(      preferences.windowDays,      undefined,      preferences.windowDays === 1 ? "hour" : "day",    ),  }));  const metric = preferences.metric;  const showingLimits = metric === "limits";  const [isRefreshing, setIsRefreshing] = useState(false);  const refreshingRef = useRef(false);  const [breakdown, setBreakdown] = useState<"model" | "time">("model");  const [selectedEnvironmentIds, setSelectedEnvironmentIds] =    useState<ReadonlySet<EnvironmentId> | null>(null);  const { days: windowDays, window } = windowSelection;  const isPast24Hours = windowDays === 1;  const { merged, environments, selectedEnvironments, isPending, isPartial, refresh } = useUsage(    window,    selectedEnvironmentIds,  );  const presentations = useAtomValue(environmentPresentations.presentationsAtom);  const refreshProviders = useAtomCommand(serverEnvironment.refreshProviders, {    reportFailure: false,  });   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 breakdownModels = useMemo(    () =>      breakdown === "model" && metric === "tokens"        ? merged.models.toSorted(            (left, right) => right.totalTokens - left.totalTokens || right.costUsd - left.costUsd,          )        : merged.models,    [breakdown, merged.models, metric],  );  const activeProviders = useMemo(() => providersWithUsage(merged.providers), [merged.providers]);  const timeValueColumnWidth = `${60 / (activeProviders.length + 2)}%`;   const selectWindow = (days: number) => {    if (!isUsageWindowDays(days)) return;    const nextPreferences = { metric, windowDays: days };    setPreferences(nextPreferences);    saveUsagePagePreferences(nextPreferences);    setWindowSelection({      days,      window: makeWindow(days, undefined, days === 1 ? "hour" : "day"),    });  };  const selectMetric = (nextMetric: UsageMetric) => {    const nextPreferences = { metric: nextMetric, windowDays };    setPreferences(nextPreferences);    saveUsagePagePreferences(nextPreferences);  };  const refreshWindow = () => {    if (refreshingRef.current) return;     if (showingLimits) {      refreshingRef.current = true;      setIsRefreshing(true);      void Promise.all(        Array.from(presentations, ([environmentId, presentation]) => {          if (selectedEnvironmentIds !== null && !selectedEnvironmentIds.has(environmentId)) return;          if (presentation.connection.phase === "connected" && presentation.serverConfig !== null) {            return refreshProviders({ environmentId, input: {} });          }        }),      ).finally(() => {        refreshingRef.current = false;        setIsRefreshing(false);      });      return;    }    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    ) {      setWindowSelection({ days: windowDays, window: nextWindow });    }    refreshingRef.current = true;    setIsRefreshing(true);    void refresh(nextWindow).finally(() => {      refreshingRef.current = false;      setIsRefreshing(false);    });  };  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="grid w-full min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-2 py-2 xl:flex">      <WorkspaceBreadcrumb ariaLabel="Usage breadcrumb" className="col-span-2 min-w-0">        <WorkspaceBreadcrumbItem>          <h1>Usage</h1>        </WorkspaceBreadcrumbItem>        <WorkspaceBreadcrumbSeparator />        <WorkspaceBreadcrumbItem current className="min-w-10">          <UsageEnvironmentFilter            environments={environments}            selectedEnvironments={selectedEnvironments}            selectedEnvironmentIds={selectedEnvironmentIds}            onSelectionChange={setSelectedEnvironmentIds}            showUsageStatus={!showingLimits}            isPartial={isPartial}            duplicateSources={merged.duplicateSources}            staleEnvironments={merged.staleEnvironments}          />        </WorkspaceBreadcrumbItem>      </WorkspaceBreadcrumb>      {!showingLimits ? (        <span className="hidden min-w-0 truncate text-xs text-muted-foreground 2xl:block">          {windowLabel}        </span>      ) : null}      <div className="ms-auto hidden min-w-0 items-center justify-end gap-2 xl:flex">        <ToggleGroup          aria-label="Usage metric"          variant="segmented"          value={[metric]}          onValueChange={(next) => {            const value = next[0];            if (isUsageMetric(value)) selectMetric(value);          }}        >          {METRIC_OPTIONS.map((option) => (            <Toggle key={option.value} value={option.value}>              {option.label}            </Toggle>          ))}        </ToggleGroup>        {/* The period does not apply to Limits, so it stays in place but            disabled; unmounting it shifted the metric toggle ~300px. */}        <ToggleGroup          aria-label="Usage period"          variant="segmented"          value={[String(windowDays)]}          disabled={showingLimits}          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={showingLimits ? "Refresh limits" : "Refresh usage"}          aria-busy={isRefreshing}          disabled={isRefreshing}          size="icon-sm"          variant="ghost"        >          <RefreshIcon className="size-3.5" refreshing={isRefreshing} />        </Button>      </div>      <div className="col-span-2 ms-auto flex min-w-0 items-center justify-end gap-1 xl:hidden">        <Select          value={metric}          onValueChange={(value) => {            if (isUsageMetric(value)) selectMetric(value);          }}        >          <SelectTrigger            aria-label="Usage metric"            size="compact"            variant="ghost"            className="w-auto min-w-0"          >            <SelectValue>              {METRIC_OPTIONS.find((option) => option.value === metric)?.label}            </SelectValue>          </SelectTrigger>          <SelectPopup align="end" alignItemWithTrigger={false}>            {METRIC_OPTIONS.map((option) => (              <SelectItem key={option.value} value={option.value}>                {option.label}              </SelectItem>            ))}          </SelectPopup>        </Select>        <Select          value={String(windowDays)}          disabled={showingLimits}          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={showingLimits ? "Refresh limits" : "Refresh usage"}          aria-busy={isRefreshing}          disabled={isRefreshing}          size="icon-sm"          variant="ghost"        >          <RefreshIcon className="size-3.5" refreshing={isRefreshing} />        </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} className="h-auto">          {topbarContent}        </WorkspacePageHeader>         <ScrollArea className="min-h-0 flex-1">          <WorkspacePageContainer width="wide">            {selectedEnvironments.length === 0 ? (              <p className="text-sm text-muted-foreground">                {environments.length === 0                  ? `Connect an environment to see ${showingLimits ? "limits" : "usage"}.`                  : `Select an environment to see ${showingLimits ? "limits" : "usage"}.`}              </p>            ) : showingLimits ? (              <UsageLimitsSection selectedEnvironmentIds={selectedEnvironmentIds} />            ) : isPending ? (              <UsageSkeleton />            ) : (              <>                <section className="grid gap-6 lg:grid-cols-[minmax(0,18rem)_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`                          : merged.costQuality.unpricedShare > 0                            ? `${formatCount(merged.sessions)} sessions · API estimate excludes ${formatPercent(                                merged.costQuality.unpricedShare,                              )} unpriced records`                            : `${formatCount(merged.sessions)} sessions · API estimate`}                      </span>                    </div>                     {activeProviders.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                      providers={activeProviders}                      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 table-fixed text-sm">                      <colgroup>                        <col className="w-2/5" />                        <col className="w-1/5" />                        <col className="w-1/5" />                        <col className="w-1/5" />                      </colgroup>                      <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>                        {breakdownModels.length === 0 ? (

Lines 1–500 of 884.