| author | |
| committer | |
| log | f0ebc628c6dd83fd0c7963078ad7778ce6028d0c |
| tree | a257c98ea9e3c745c69b75bb279bdd0c310abde8 |
| parent | 6ae9662d8ed215476e697adc12403ce035500828 |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 42 insertions(+), 40 deletions(-)
apps/web/src/components/usage/UsagePage.tsx+6-6| ... | ... | @@ -25,7 +25,7 @@ import { SidebarInset } from "../ui/sidebar"; |
| 25 | 25 | import { WorkspaceBreadcrumb, WorkspaceBreadcrumbItem } from "../WorkspaceBreadcrumb"; |
| 26 | 26 | import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "../../workspaceTitlebar"; |
| 27 | 27 | import { UsageChartLegend, UsageProviderChart, type UsageChartMetric } from "./UsageProviderChart"; |
| 28 | import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders"; | |
| 28 | import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders"; | |
| 29 | 29 | |
| 30 | 30 | const WINDOW_OPTIONS = [ |
| 31 | 31 | { days: 1, label: "Past 24h" }, |
| ... | ... | @@ -209,7 +209,7 @@ export function UsagePage() { |
| 209 | 209 | <div className="flex items-baseline justify-between"> |
| 210 | 210 | <span className="flex items-center gap-2 text-sm text-foreground"> |
| 211 | 211 | <ProviderMark provider={provider.provider} className="size-4" /> |
| 212 | {PROVIDER_LABEL[provider.provider]} | |
| 212 | {PROVIDER_PRESENTATION[provider.provider].label} | |
| 213 | 213 | </span> |
| 214 | 214 | <span className="text-sm text-foreground tabular-nums"> |
| 215 | 215 | {metric === "cost" |
| ... | ... | @@ -222,7 +222,7 @@ export function UsagePage() { |
| 222 | 222 | className="h-full" |
| 223 | 223 | style={{ |
| 224 | 224 | width: `${(share * 100).toFixed(1)}%`, |
| 225 | backgroundColor: PROVIDER_COLOR[provider.provider], | |
| 225 | backgroundColor: PROVIDER_PRESENTATION[provider.provider].color, | |
| 226 | 226 | }} |
| 227 | 227 | /> |
| 228 | 228 | </div> |
| ... | ... | @@ -385,7 +385,7 @@ export function UsagePage() { |
| 385 | 385 | <th className="py-2 font-normal">{isPast24Hours ? "Hour" : "Day"}</th> |
| 386 | 386 | {PROVIDER_ORDER.map((provider) => ( |
| 387 | 387 | <th key={provider} className="py-2 text-right font-normal"> |
| 388 | {PROVIDER_LABEL[provider]} | |
| 388 | {PROVIDER_PRESENTATION[provider].label} | |
| 389 | 389 | </th> |
| 390 | 390 | ))} |
| 391 | 391 | <th className="py-2 text-right font-normal">Total</th> |
| ... | ... | @@ -448,7 +448,7 @@ function ProviderMark({ |
| 448 | 448 | readonly provider: UsageProviderKind; |
| 449 | 449 | readonly className: string; |
| 450 | 450 | }) { |
| 451 | const Mark = PROVIDER_MARK[provider]; | |
| 451 | const Mark = PROVIDER_PRESENTATION[provider].mark; | |
| 452 | 452 | return <Mark className={cn("shrink-0", className)} aria-hidden />; |
| 453 | 453 | } |
| 454 | 454 | |
| ... | ... | @@ -595,7 +595,7 @@ function UsageSkeleton({ resolution }: { readonly resolution: "day" | "hour" }) |
| 595 | 595 | <div className="flex items-center justify-between"> |
| 596 | 596 | <span className="flex items-center gap-2 text-sm text-foreground"> |
| 597 | 597 | <ProviderMark provider={provider} className="size-4" /> |
| 598 | {PROVIDER_LABEL[provider]} | |
| 598 | {PROVIDER_PRESENTATION[provider].label} | |
| 599 | 599 | </span> |
| 600 | 600 | <div className="h-3.5 w-14 rounded-sm bg-muted" /> |
| 601 | 601 | </div> |
apps/web/src/components/usage/UsageProviderChart.tsx+14-9| ... | ... | @@ -9,7 +9,7 @@ import { |
| 9 | 9 | formatTokens, |
| 10 | 10 | formatUsd, |
| 11 | 11 | } from "@t3tools/shared/usageFormat"; |
| 12 | import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders"; | |
| 12 | import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders"; | |
| 13 | 13 | |
| 14 | 14 | const VIEW_WIDTH = 960; |
| 15 | 15 | const VIEW_HEIGHT = 260; |
| ... | ... | @@ -339,14 +339,19 @@ export function UsageProviderChart({ |
| 339 | 339 | |
| 340 | 340 | {/* Fills first, then every stroke, so no series covers another's line. */} |
| 341 | 341 | {paths.map(({ provider, area }) => ( |
| 342 | <path key={provider} d={area} fill={PROVIDER_COLOR[provider]} fillOpacity={0.12} /> | |
| 342 | <path | |
| 343 | key={provider} | |
| 344 | d={area} | |
| 345 | fill={PROVIDER_PRESENTATION[provider].color} | |
| 346 | fillOpacity={0.12} | |
| 347 | /> | |
| 343 | 348 | ))} |
| 344 | 349 | {paths.map(({ provider, line }) => ( |
| 345 | 350 | <path |
| 346 | 351 | key={provider} |
| 347 | 352 | d={line} |
| 348 | 353 | fill="none" |
| 349 | stroke={PROVIDER_COLOR[provider]} | |
| 354 | stroke={PROVIDER_PRESENTATION[provider].color} | |
| 350 | 355 | strokeWidth={2} |
| 351 | 356 | vectorEffect="non-scaling-stroke" |
| 352 | 357 | /> |
| ... | ... | @@ -376,12 +381,12 @@ export function UsageProviderChart({ |
| 376 | 381 | > |
| 377 | 382 | <div className="mb-1 text-muted-foreground">{formatTooltipPeriod(hoveredPeriod)}</div> |
| 378 | 383 | {PROVIDER_ORDER.map((provider) => { |
| 379 | const Mark = PROVIDER_MARK[provider]; | |
| 384 | const { label, mark: Mark } = PROVIDER_PRESENTATION[provider]; | |
| 380 | 385 | return ( |
| 381 | 386 | <div key={provider} className="flex items-center justify-between gap-3"> |
| 382 | 387 | <span className="flex items-center gap-1.5 text-muted-foreground"> |
| 383 | 388 | <Mark className="size-3 shrink-0" aria-hidden /> |
| 384 | {PROVIDER_LABEL[provider]} | |
| 389 | {label} | |
| 385 | 390 | </span> |
| 386 | 391 | <span className="text-foreground tabular-nums"> |
| 387 | 392 | {format( |
| ... | ... | @@ -423,13 +428,13 @@ export function UsageChartLegend() { |
| 423 | 428 | return ( |
| 424 | 429 | <div className="flex items-center gap-4"> |
| 425 | 430 | {PROVIDER_ORDER.map((provider) => { |
| 426 | // The marks carry the same fills as the bands, so they key the chart | |
| 427 | // just as a colour swatch would. | |
| 428 | const Mark = PROVIDER_MARK[provider]; | |
| 431 | // Brand marks keep monochrome providers identifiable even when their | |
| 432 | // chart series use distinct colors. | |
| 433 | const { label, mark: Mark } = PROVIDER_PRESENTATION[provider]; | |
| 429 | 434 | return ( |
| 430 | 435 | <span key={provider} className="flex items-center gap-1.5 text-xs text-muted-foreground"> |
| 431 | 436 | <Mark className="size-3.5 shrink-0" aria-hidden /> |
| 432 | {PROVIDER_LABEL[provider]} | |
| 437 | {label} | |
| 433 | 438 | </span> |
| 434 | 439 | ); |
| 435 | 440 | })} |
apps/web/src/components/usage/usageProviders.ts+22-25| ... | ... | @@ -2,32 +2,29 @@ import type { UsageProviderKind } from "@t3tools/contracts"; |
| 2 | 2 | |
| 3 | 3 | import { ClaudeAI, type Icon, OpenAI } from "../Icons"; |
| 4 | 4 | |
| 5 | /** | |
| 6 | * Series and table order. The chart layers both providers from a shared zero | |
| 7 | * baseline, so this only fixes the reading order of legends, tables and hover | |
| 8 | * rows; it does not decide which series sits above the other. | |
| 9 | */ | |
| 10 | export const PROVIDER_ORDER: readonly UsageProviderKind[] = ["codex", "claude"]; | |
| 11 | ||
| 12 | export const PROVIDER_LABEL: Record<UsageProviderKind, string> = { | |
| 13 | claude: "Claude Code", | |
| 14 | codex: "Codex", | |
| 15 | }; | |
| 16 | ||
| 17 | /** Claude's brand orange against a neutral white for Codex. */ | |
| 18 | export const PROVIDER_COLOR: Record<UsageProviderKind, string> = { | |
| 19 | claude: "#d97757", | |
| 20 | codex: "#e6e6e6", | |
| 5 | type UsageProviderPresentation = { | |
| 6 | readonly label: string; | |
| 7 | readonly color: string; | |
| 8 | readonly mark: Icon; | |
| 21 | 9 | }; |
| 22 | 10 | |
| 23 | 11 | /** |
| 24 | * Brand marks, reused from the provider picker. | |
| 25 | * | |
| 26 | * These ship their own fills (`#d97757` for Claude, white on dark for OpenAI), | |
| 27 | * which are the same colours as the chart bands, so swapping a colour dot for a | |
| 28 | * mark keeps the series association intact rather than trading it away. | |
| 12 | * Exhaustive presentation for providers supported by the usage contract. | |
| 13 | * Declaration order is reused by every chart, table, legend, and skeleton, so | |
| 14 | * adding a provider only requires its contract support and one entry here. | |
| 29 | 15 | */ |
| 30 | export const PROVIDER_MARK: Record<UsageProviderKind, Icon> = { | |
| 31 | claude: ClaudeAI, | |
| 32 | codex: OpenAI, | |
| 33 | }; | |
| 16 | export const PROVIDER_PRESENTATION = { | |
| 17 | codex: { | |
| 18 | label: "Codex", | |
| 19 | color: "var(--foreground)", | |
| 20 | mark: OpenAI, | |
| 21 | }, | |
| 22 | claude: { | |
| 23 | label: "Claude Code", | |
| 24 | color: "#d97757", | |
| 25 | mark: ClaudeAI, | |
| 26 | }, | |
| 27 | } satisfies Record<UsageProviderKind, UsageProviderPresentation>; | |
| 28 | ||
| 29 | /** The chart layers every series from zero, so order only controls how it is read. */ | |
| 30 | export const PROVIDER_ORDER = Object.keys(PROVIDER_PRESENTATION) as UsageProviderKind[]; |