mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
parent
192ccaaf02
commit
cd63c7e5a7
2 changed files with 62 additions and 31 deletions
|
|
@ -1,5 +1,5 @@
|
|||
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 { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
|
|
@ -151,15 +151,18 @@ describe("AutoRouterBenchmarksTab", () => {
|
|||
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" })]) });
|
||||
renderTab();
|
||||
|
||||
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);
|
||||
expect(labels).toEqual([
|
||||
"Total estimated savings",
|
||||
"Avg saved per session",
|
||||
"Avg turns per session",
|
||||
"Avg session length",
|
||||
"Avg tokens per session",
|
||||
|
|
@ -181,13 +184,35 @@ describe("AutoRouterBenchmarksTab", () => {
|
|||
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" })]) });
|
||||
renderTab();
|
||||
|
||||
expect(screen.getByText("Avg saved per session")).toBeInTheDocument();
|
||||
expect(screen.getByText("$23.13")).toBeInTheDocument();
|
||||
expect(screen.getByText("across 94 sessions")).toBeInTheDocument();
|
||||
const tile = screen.getByText("Avg saved per session").closest('[data-slot="card"]');
|
||||
if (!tile) throw new Error("expected avg saved per session to render as a metric tile");
|
||||
|
||||
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", () => {
|
||||
|
|
@ -315,7 +340,7 @@ describe("AutoRouterBenchmarksTab", () => {
|
|||
|
||||
expect(screen.getByText("Total estimated savings")).toBeInTheDocument();
|
||||
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(/turns measured/)).toBeInTheDocument();
|
||||
expect(screen.getAllByText("0.0%").length).toBeGreaterThan(0);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import AdvancedDatePicker from "@/components/shared/advanced_date_picker";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
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>
|
||||
);
|
||||
|
||||
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">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm font-normal text-muted-foreground">{label}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardContent className="flex flex-wrap items-baseline gap-2">
|
||||
<p className="text-3xl font-semibold text-foreground">{value}</p>
|
||||
{hint && <p className="text-sm text-muted-foreground">{hint}</p>}
|
||||
</CardContent>
|
||||
</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 stats = view.stats;
|
||||
const cheaper = stats.saved_spend >= 0;
|
||||
return (
|
||||
<Card className="overflow-hidden py-0">
|
||||
<div className="grid md:grid-cols-[1fr_1fr]">
|
||||
<div className="flex flex-col justify-center gap-3 p-6">
|
||||
<p className="text-sm text-muted-foreground">Total estimated savings</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<p className="text-5xl font-semibold tracking-tight text-foreground">{usd(stats.saved_spend)}</p>
|
||||
<div className="grid md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<div className="flex flex-col items-center justify-center gap-2 p-6">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
Total estimated savings
|
||||
</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
|
||||
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 ? "-" : "+")}
|
||||
{Math.abs(stats.saved_pct).toFixed(0)}%
|
||||
</Badge>
|
||||
</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 className="flex flex-col items-center justify-center gap-2 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>
|
||||
<p className="text-5xl font-semibold tracking-tight text-foreground">{usd(stats.saved_per_session)}</p>
|
||||
<p className="text-sm text-muted-foreground">across {stats.sessions.toLocaleString()} sessions</p>
|
||||
<div className="flex flex-col justify-center border-t p-6 md:border-t-0 md:border-l">
|
||||
<SpendRow label="Actual auto-router spend" value={usd(stats.spend)} />
|
||||
<Separator />
|
||||
<SpendRow label="Estimated spend at highest-tier model" value={usd(stats.baseline_spend)} />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -239,7 +240,12 @@ const BenchmarksBody: React.FC<BenchmarksBodyProps> = ({ isPending, error, data,
|
|||
|
||||
<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 session length" value={durationLabel(stats.avg_session_seconds)} />
|
||||
<Metric label="Avg tokens per session" value={formatNumberWithCommas(stats.avg_tokens_per_session, 1, true)} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue