feat(ui): put the auto-router savings hero on a spend rail and a four-tile row (#38470)

The savings card carried four numbers in two stacked halves: the headline
saving with its delta on the left over the two spend rows, and avg saved per
session on the right. Give the headline the whole left half, move the two
spend rows into a rail on the right, and drop avg saved per session into the
metric row below as its first tile, with the session count as an inline hint.

Each spend row stays a description list so assistive tech keeps the label to
value association, with the shadcn Separator between the two rows. Both hero
columns are minmax(0,1fr) so a large total wraps instead of overflowing the
card, which also fixes the clipping the old 1fr columns already had. Metric
grows one optional hint slot so the new tile reuses the same presenter as its
three siblings.
This commit is contained in:
tin-berri 2026-08-27 00:22:38 -07:00 committed by GitHub
parent 192ccaaf02
commit cd63c7e5a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 31 deletions

View file

@ -1,5 +1,5 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen } from "@testing-library/react"; import { fireEvent, render, screen, within } from "@testing-library/react";
import React from "react"; import React from "react";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
@ -151,15 +151,18 @@ describe("AutoRouterBenchmarksTab", () => {
mockAutoRouters(); mockAutoRouters();
}); });
it("leads with total estimated savings, before the three session-shape metrics", () => { it("leads with total estimated savings, before the four session-shape metrics", () => {
mockHook({ data: response([group(), group({ router_name: "gpt-auto" })]) }); mockHook({ data: response([group(), group({ router_name: "gpt-auto" })]) });
renderTab(); renderTab();
const labels = screen const labels = screen
.getAllByText(/Total estimated savings|Avg turns per session|Avg session length|Avg tokens per session/) .getAllByText(
/Total estimated savings|Avg saved per session|Avg turns per session|Avg session length|Avg tokens per session/,
)
.map((node) => node.textContent); .map((node) => node.textContent);
expect(labels).toEqual([ expect(labels).toEqual([
"Total estimated savings", "Total estimated savings",
"Avg saved per session",
"Avg turns per session", "Avg turns per session",
"Avg session length", "Avg session length",
"Avg tokens per session", "Avg tokens per session",
@ -181,13 +184,35 @@ describe("AutoRouterBenchmarksTab", () => {
expect(screen.getByText("5.3M")).toBeInTheDocument(); expect(screen.getByText("5.3M")).toBeInTheDocument();
}); });
it("pairs the savings with the session count it was earned over", () => { it("pairs the savings with the session count it was earned over, in its own tile", () => {
mockHook({ data: response([group(), group({ router_name: "gpt-auto" })]) }); mockHook({ data: response([group(), group({ router_name: "gpt-auto" })]) });
renderTab(); renderTab();
expect(screen.getByText("Avg saved per session")).toBeInTheDocument(); const tile = screen.getByText("Avg saved per session").closest('[data-slot="card"]');
expect(screen.getByText("$23.13")).toBeInTheDocument(); if (!tile) throw new Error("expected avg saved per session to render as a metric tile");
expect(screen.getByText("across 94 sessions")).toBeInTheDocument();
expect(within(tile).getByText("$23.13")).toBeInTheDocument();
expect(within(tile).getByText("· 94 sessions")).toBeInTheDocument();
});
it("exposes each spend row as a term and its value, not as loose text", () => {
mockHook({ data: response([group()]) });
renderTab();
const terms = screen.getAllByRole("term").map((node) => node.textContent);
const values = screen.getAllByRole("definition").map((node) => node.textContent);
expect(terms).toEqual(["Actual auto-router spend", "Estimated spend at highest-tier model"]);
expect(values).toEqual(["$359.86", "$2,534.45"]);
});
it("lets both hero columns shrink below their content so a large total cannot clip", () => {
const huge = totals({ saved_spend: 123_456_789_012.34 });
mockHook({ data: response([group(huge)], huge) });
renderTab();
const figure = screen.getByText("$123,456,789,012.34");
const grid = figure.closest('[data-slot="card"]')?.firstElementChild;
expect(grid).toHaveClass("md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]");
}); });
it("shows a cost increase as a positive delta rather than a saving", () => { it("shows a cost increase as a positive delta rather than a saving", () => {
@ -315,7 +340,7 @@ describe("AutoRouterBenchmarksTab", () => {
expect(screen.getByText("Total estimated savings")).toBeInTheDocument(); expect(screen.getByText("Total estimated savings")).toBeInTheDocument();
expect(screen.getAllByText("$0.00")).toHaveLength(4); expect(screen.getAllByText("$0.00")).toHaveLength(4);
expect(screen.getByText("across 0 sessions")).toBeInTheDocument(); expect(screen.getByText("· 0 sessions")).toBeInTheDocument();
expect(screen.getByText("0s")).toBeInTheDocument(); expect(screen.getByText("0s")).toBeInTheDocument();
expect(screen.getByText(/turns measured/)).toBeInTheDocument(); expect(screen.getByText(/turns measured/)).toBeInTheDocument();
expect(screen.getAllByText("0.0%").length).toBeGreaterThan(0); expect(screen.getAllByText("0.0%").length).toBeGreaterThan(0);

View file

@ -8,6 +8,7 @@ import AdvancedDatePicker from "@/components/shared/advanced_date_picker";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
@ -39,51 +40,51 @@ const Message: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<p className="py-8 text-center text-sm text-muted-foreground">{children}</p> <p className="py-8 text-center text-sm text-muted-foreground">{children}</p>
); );
const Metric: React.FC<{ label: string; value: string }> = ({ label, value }) => ( const Metric: React.FC<{ label: string; value: string; hint?: string }> = ({ label, value, hint }) => (
<Card size="sm"> <Card size="sm">
<CardHeader> <CardHeader>
<CardTitle className="text-sm font-normal text-muted-foreground">{label}</CardTitle> <CardTitle className="text-sm font-normal text-muted-foreground">{label}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="flex flex-wrap items-baseline gap-2">
<p className="text-3xl font-semibold text-foreground">{value}</p> <p className="text-3xl font-semibold text-foreground">{value}</p>
{hint && <p className="text-sm text-muted-foreground">{hint}</p>}
</CardContent> </CardContent>
</Card> </Card>
); );
const SpendRow: React.FC<{ label: string; value: string }> = ({ label, value }) => (
<dl className="flex items-baseline justify-between gap-6 py-3">
<dt className="text-sm text-muted-foreground">{label}</dt>
<dd className="text-base font-semibold tabular-nums text-foreground">{value}</dd>
</dl>
);
const HeroCard: React.FC<{ view: BenchmarkView }> = ({ view }) => { const HeroCard: React.FC<{ view: BenchmarkView }> = ({ view }) => {
const stats = view.stats; const stats = view.stats;
const cheaper = stats.saved_spend >= 0; const cheaper = stats.saved_spend >= 0;
return ( return (
<Card className="overflow-hidden py-0"> <Card className="overflow-hidden py-0">
<div className="grid md:grid-cols-[1fr_1fr]"> <div className="grid md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<div className="flex flex-col justify-center gap-3 p-6"> <div className="flex flex-col items-center justify-center gap-2 p-6">
<p className="text-sm text-muted-foreground">Total estimated savings</p> <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
<div className="flex flex-wrap items-center gap-3"> Total estimated savings
<p className="text-5xl font-semibold tracking-tight text-foreground">{usd(stats.saved_spend)}</p> </p>
<div className="flex flex-wrap items-center justify-center gap-3">
<p className="text-6xl font-semibold tracking-tight text-foreground">{usd(stats.saved_spend)}</p>
<Badge <Badge
variant="secondary" variant="secondary"
className={cheaper ? "bg-success/10 text-success" : "bg-destructive/10 text-destructive"} className={`h-6 px-2.5 text-sm ${cheaper ? "bg-success/10 text-success" : "bg-destructive/10 text-destructive"}`}
> >
{stats.saved_spend !== 0 && (cheaper ? "-" : "+")} {stats.saved_spend !== 0 && (cheaper ? "-" : "+")}
{Math.abs(stats.saved_pct).toFixed(0)}% {Math.abs(stats.saved_pct).toFixed(0)}%
</Badge> </Badge>
</div> </div>
<dl className="divide-y text-sm">
<div className="flex items-baseline justify-between gap-6 py-3">
<dt className="text-muted-foreground">Actual auto-router spend</dt>
<dd className="font-medium tabular-nums text-foreground">{usd(stats.spend)}</dd>
</div>
<div className="flex items-baseline justify-between gap-6 py-3">
<dt className="text-muted-foreground">Estimated spend at highest-tier model</dt>
<dd className="font-medium tabular-nums text-foreground">{usd(stats.baseline_spend)}</dd>
</div>
</dl>
</div> </div>
<div className="flex flex-col items-center justify-center gap-2 border-t p-6 md:border-t-0 md:border-l"> <div className="flex flex-col justify-center border-t p-6 md:border-t-0 md:border-l">
<p className="text-[11px] uppercase tracking-wide text-muted-foreground">Avg saved per session</p> <SpendRow label="Actual auto-router spend" value={usd(stats.spend)} />
<p className="text-5xl font-semibold tracking-tight text-foreground">{usd(stats.saved_per_session)}</p> <Separator />
<p className="text-sm text-muted-foreground">across {stats.sessions.toLocaleString()} sessions</p> <SpendRow label="Estimated spend at highest-tier model" value={usd(stats.baseline_spend)} />
</div> </div>
</div> </div>
</Card> </Card>
@ -239,7 +240,12 @@ const BenchmarksBody: React.FC<BenchmarksBodyProps> = ({ isPending, error, data,
<TierTurnsChart view={view} autoRouters={autoRouters} /> <TierTurnsChart view={view} autoRouters={autoRouters} />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Metric
label="Avg saved per session"
value={usd(stats.saved_per_session)}
hint={`· ${stats.sessions.toLocaleString()} sessions`}
/>
<Metric label="Avg turns per session" value={stats.avg_turns_per_session.toFixed(1)} /> <Metric label="Avg turns per session" value={stats.avg_turns_per_session.toFixed(1)} />
<Metric label="Avg session length" value={durationLabel(stats.avg_session_seconds)} /> <Metric label="Avg session length" value={durationLabel(stats.avg_session_seconds)} />
<Metric label="Avg tokens per session" value={formatNumberWithCommas(stats.avg_tokens_per_session, 1, true)} /> <Metric label="Avg tokens per session" value={formatNumberWithCommas(stats.avg_tokens_per_session, 1, true)} />