Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions apps/web/src/components/usage/UsagePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ 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_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders";
import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders";

const WINDOW_OPTIONS = [
{ days: 1, label: "Past 24h" },
Expand Down Expand Up @@ -209,7 +209,7 @@ export function UsagePage() {
<div className="flex items-baseline justify-between">
<span className="flex items-center gap-2 text-sm text-foreground">
<ProviderMark provider={provider.provider} className="size-4" />
{PROVIDER_LABEL[provider.provider]}
{PROVIDER_PRESENTATION[provider.provider].label}
</span>
<span className="text-sm text-foreground tabular-nums">
{metric === "cost"
Expand All @@ -222,7 +222,7 @@ export function UsagePage() {
className="h-full"
style={{
width: `${(share * 100).toFixed(1)}%`,
backgroundColor: PROVIDER_COLOR[provider.provider],
backgroundColor: PROVIDER_PRESENTATION[provider.provider].color,
}}
/>
</div>
Expand Down Expand Up @@ -385,7 +385,7 @@ export function UsagePage() {
<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_LABEL[provider]}
{PROVIDER_PRESENTATION[provider].label}
</th>
))}
<th className="py-2 text-right font-normal">Total</th>
Expand Down Expand Up @@ -448,7 +448,7 @@ function ProviderMark({
readonly provider: UsageProviderKind;
readonly className: string;
}) {
const Mark = PROVIDER_MARK[provider];
const Mark = PROVIDER_PRESENTATION[provider].mark;
return <Mark className={cn("shrink-0", className)} aria-hidden />;
}

Expand Down Expand Up @@ -595,7 +595,7 @@ function UsageSkeleton({ resolution }: { readonly resolution: "day" | "hour" })
<div className="flex items-center justify-between">
<span className="flex items-center gap-2 text-sm text-foreground">
<ProviderMark provider={provider} className="size-4" />
{PROVIDER_LABEL[provider]}
{PROVIDER_PRESENTATION[provider].label}
</span>
<div className="h-3.5 w-14 rounded-sm bg-muted" />
</div>
Expand Down
23 changes: 14 additions & 9 deletions apps/web/src/components/usage/UsageProviderChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
formatTokens,
formatUsd,
} from "@t3tools/shared/usageFormat";
import { PROVIDER_COLOR, PROVIDER_LABEL, PROVIDER_MARK, PROVIDER_ORDER } from "./usageProviders";
import { PROVIDER_ORDER, PROVIDER_PRESENTATION } from "./usageProviders";

const VIEW_WIDTH = 960;
const VIEW_HEIGHT = 260;
Expand Down Expand Up @@ -339,14 +339,19 @@ export function UsageProviderChart({

{/* Fills first, then every stroke, so no series covers another's line. */}
{paths.map(({ provider, area }) => (
<path key={provider} d={area} fill={PROVIDER_COLOR[provider]} fillOpacity={0.12} />
<path
key={provider}
d={area}
fill={PROVIDER_PRESENTATION[provider].color}
fillOpacity={0.12}
/>
))}
{paths.map(({ provider, line }) => (
<path
key={provider}
d={line}
fill="none"
stroke={PROVIDER_COLOR[provider]}
stroke={PROVIDER_PRESENTATION[provider].color}
strokeWidth={2}
vectorEffect="non-scaling-stroke"
/>
Expand Down Expand Up @@ -376,12 +381,12 @@ export function UsageProviderChart({
>
<div className="mb-1 text-muted-foreground">{formatTooltipPeriod(hoveredPeriod)}</div>
{PROVIDER_ORDER.map((provider) => {
const Mark = PROVIDER_MARK[provider];
const { label, mark: Mark } = PROVIDER_PRESENTATION[provider];
return (
<div key={provider} className="flex items-center justify-between gap-3">
<span className="flex items-center gap-1.5 text-muted-foreground">
<Mark className="size-3 shrink-0" aria-hidden />
{PROVIDER_LABEL[provider]}
{label}
</span>
<span className="text-foreground tabular-nums">
{format(
Expand Down Expand Up @@ -423,13 +428,13 @@ export function UsageChartLegend() {
return (
<div className="flex items-center gap-4">
{PROVIDER_ORDER.map((provider) => {
// The marks carry the same fills as the bands, so they key the chart
// just as a colour swatch would.
const Mark = PROVIDER_MARK[provider];
// Brand marks keep monochrome providers identifiable even when their
// chart series use distinct colors.
const { label, mark: Mark } = PROVIDER_PRESENTATION[provider];
return (
<span key={provider} className="flex items-center gap-1.5 text-xs text-muted-foreground">
<Mark className="size-3.5 shrink-0" aria-hidden />
{PROVIDER_LABEL[provider]}
{label}
</span>
);
})}
Expand Down
55 changes: 34 additions & 21 deletions apps/web/src/components/usage/usageProviders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import type { UsageProviderKind } from "@t3tools/contracts";

import { ClaudeAI, type Icon, OpenAI } from "../Icons";
import { ClaudeAI, CursorIcon, GrokIcon, type Icon, OpenAI } from "../Icons";

type UsageProviderPresentationKind = UsageProviderKind | "cursor" | "grok";

type UsageProviderPresentation = {
readonly label: string;
readonly color: string;
readonly mark: Icon;
};

/**
* Series and table order. The chart layers both providers from a shared zero
Expand All @@ -9,25 +17,30 @@ import { ClaudeAI, type Icon, OpenAI } from "../Icons";
*/
export const PROVIDER_ORDER: readonly UsageProviderKind[] = ["codex", "claude"];

export const PROVIDER_LABEL: Record<UsageProviderKind, string> = {
claude: "Claude Code",
codex: "Codex",
};

/** Claude's brand orange against a neutral white for Codex. */
export const PROVIDER_COLOR: Record<UsageProviderKind, string> = {
claude: "#d97757",
codex: "#e6e6e6",
};

/**
* Brand marks, reused from the provider picker.
*
* These ship their own fills (`#d97757` for Claude, white on dark for OpenAI),
* which are the same colours as the chart bands, so swapping a colour dot for a
* mark keeps the series association intact rather than trading it away.
* Usage-series presentation, including planned Cursor and Grok support. Their
* monochrome marks stay on-brand while distinct mode-aware series colors keep
* a future four-provider chart readable.
*/
export const PROVIDER_MARK: Record<UsageProviderKind, Icon> = {
claude: ClaudeAI,
codex: OpenAI,
};
export const PROVIDER_PRESENTATION = {
claude: {
label: "Claude Code",
color: "#d97757",
mark: ClaudeAI,
},
codex: {
label: "Codex",
color: "var(--foreground)",
mark: OpenAI,
},
cursor: {
label: "Cursor",
color: "light-dark(var(--color-blue-700), var(--color-blue-300))",
mark: CursorIcon,
},
grok: {
label: "Grok",
color: "light-dark(var(--color-violet-700), var(--color-violet-300))",
mark: GrokIcon,
},
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
} satisfies Record<UsageProviderPresentationKind, UsageProviderPresentation>;
Loading