diff --git a/apps/web/src/components/usage/UsagePage.tsx b/apps/web/src/components/usage/UsagePage.tsx index b5b762aea73..2101ce06947 100644 --- a/apps/web/src/components/usage/UsagePage.tsx +++ b/apps/web/src/components/usage/UsagePage.tsx @@ -1,8 +1,8 @@ import type { UsageProviderKind } from "@t3tools/contracts"; -import { useCanGoBack, useNavigate, useRouter } from "@tanstack/react-router"; -import { ArrowLeftIcon, CheckIcon, RefreshCwIcon, XIcon } from "lucide-react"; +import { CheckIcon, RefreshCwIcon, XIcon } from "lucide-react"; import { useMemo, useState } from "react"; +import { isElectron } from "../../env"; import { cn } from "../../lib/utils"; import { useUsage, type EnvironmentUsageStatus } from "../../state/usage"; import { @@ -15,6 +15,8 @@ import { makeWindow, } from "@t3tools/shared/usageFormat"; import { ScrollArea } from "../ui/scroll-area"; +import { SidebarInset } from "../ui/sidebar"; +import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar"; import { UsageChartLegend, UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart"; import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders"; @@ -28,9 +30,6 @@ export function UsagePage() { const [windowDays, setWindowDays] = useState(30); const [metric, setMetric] = useState("cost"); const [breakdown, setBreakdown] = useState<"model" | "day">("model"); - const canGoBack = useCanGoBack(); - const navigate = useNavigate(); - const router = useRouter(); // Recomputed only when the window length changes, so a re-render does not // shift the range and refetch every environment. @@ -63,143 +62,207 @@ export function UsagePage() { const cachedShare = observedInput === 0 ? 0 : merged.cachedInputTokens / observedInput; return ( - -
-
-
- -
-

Usage

+ +
+ {!isElectron && ( +
+

Usage

+
+ )} + + {isElectron && ( +
+

Usage

+
+ )} + + +
+

{formatDayShort(window.sinceDay)} to {formatDayShort(window.untilDay)}

-
-
-
-
- {WINDOW_OPTIONS.map((option) => ( +
+
+ {WINDOW_OPTIONS.map((option) => ( + + ))} +
- ))} +
- -
-
- {settling ? ( - <> - {environments.length > 1 ? : null} - - - ) : ( - <> - + {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 + {/* 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_LABEL[provider.provider]} - - - {metric === "cost" - ? formatUsd(provider.costUsd) - : formatTokens(provider.totalTokens)} - -
-
-
-
+
+
+ + {metric === "cost" ? "Raw token cost" : "Processed tokens"} + + + {metric === "cost" + ? `${formatUsd(merged.costUsd)}*` + : formatTokens(merged.totalTokens)} + {metric === "cost" - ? `${formatPercent(share)} of cost · ${formatTokens(provider.totalTokens)} tokens` - : `${formatPercent(share)} of tokens · ${formatUsd(provider.costUsd)}`} + ? "* if billed at full API rate" + : `Input, cache reads and output across ${formatCount(merged.sessions)} sessions.`}
- ); - })} -
-
-
-

- Daily {metric === "tokens" ? "processed tokens" : "cost"} -

-
+ {orderedProviders.map((provider) => { + const share = metric === "cost" ? provider.costShare : provider.tokenShare; + return ( +
+
+ + + {PROVIDER_LABEL[provider.provider]} + + + {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)}`} + +
+ ); + })} +
+ +
+
+

+ 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

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

Breakdown

-
- {(["model", "day"] as const).map((option) => ( - - ))} -
-
- {breakdown === "model" ? ( - - - - - - - - - - - {merged.models.length === 0 ? ( - - - - ) : ( - merged.models.map((model) => ( - - - - - + {breakdown === "model" ? ( +
ModelCostShareTokens
- No activity in this window. -
- - - {model.model} - - - {formatUsd(model.costUsd)} - - {formatPercent(model.costShare)} - - {formatTokens(model.totalTokens)} -
+ + + + + + - )) - )} - -
ModelCostShareTokens
- ) : ( - - - - - {PROVIDER_ORDER.map((provider) => ( - - ))} - - - - - - {recentDays.length === 0 ? ( - - - - ) : ( - recentDays.map((day) => ( - - - {PROVIDER_ORDER.map((provider) => ( - + {merged.models.length === 0 ? ( + + + + ) : ( + merged.models.map((model) => ( + + + + + + + )) + )} + +
Day - {PROVIDER_LABEL[provider]} - TotalTokens
- No activity in this window. -
{formatDayShort(day.day)} - {formatUsd(day.byProvider.get(provider)?.costUsd ?? 0)} + +
+ No activity in this window.
+ + + {model.model} + + + {formatUsd(model.costUsd)} + + {formatPercent(model.costShare)} + + {formatTokens(model.totalTokens)} +
+ ) : ( + + + + + {PROVIDER_ORDER.map((provider) => ( + ))} - - + + - )) - )} - -
Day + {PROVIDER_LABEL[provider]} + - {formatUsd(day.costUsd)} - - {formatTokens(day.totalTokens)} - TotalTokens
- )} -
- - )} + + + {recentDays.length === 0 ? ( + + + No activity in this window. + + + ) : ( + recentDays.map((day) => ( + + {formatDayShort(day.day)} + {PROVIDER_ORDER.map((provider) => ( + + {formatUsd(day.byProvider.get(provider)?.costUsd ?? 0)} + + ))} + + {formatUsd(day.costUsd)} + + + {formatTokens(day.totalTokens)} + + + )) + )} + + + )} + + + )} + +
- + ); }