Fix the usage page's tables, range tabs and day bars

- Both tables lay their heading and rows on one shared grid, so every
  figure sits under its own heading (twui's Row wrapped the cells).
- The selected range reads as pressed: ink-filled in a hairline track.
- Each day's bar opens a small summary card on hover or tap.
- twui 0.1.7 places popovers before paint, so opening one no longer
  makes the layout flicker.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-25 05:01:24 +01:00
co-authored by Claude Opus 5.5
parent b0fe619ae8
commit 3a9e57f682
14 changed files with 221 additions and 72 deletions
+2 -2
View File
@@ -9,7 +9,7 @@
"@modelcontextprotocol/sdk": "^1.32.1",
"@moduletrace/bun-sqlite": "^1.3.0",
"@moduletrace/bunext": "^1.3.3",
"@moduletrace/twui": "^0.1.6",
"@moduletrace/twui": "^0.1.7",
"@tiptap/extension-bubble-menu": "3.32.0",
"@tiptap/extension-mention": "3.32.0",
"@tiptap/extension-placeholder": "3.32.0",
@@ -255,7 +255,7 @@
"@moduletrace/bunext": ["@moduletrace/[email protected]", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Fbunext/-/1.3.3/bunext-1.3.3.tgz", { "dependencies": { "@tailwindcss/postcss": "^4.3.3", "@types/bun": "1.3.14", "@types/node": "^24.10.0", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", "bun-plugin-tailwind": "^0.1.2", "chalk": "^5.6.2", "chokidar": "^5.0.0", "commander": "^14.0.2", "esbuild": "^0.28.2", "lightningcss-wasm": "^1.32.0", "lodash": "^4.18.1", "ora": "^9.0.0", "postcss": "^8.5.30", "react": "^19.3.0", "react-dom": "^19.3.0", "tailwindcss": "^4.3.3", "typescript": "^5.0.0", "uuid": "^14.0.2" }, "bin": { "bunext": "dist/commands/index.js" } }, "sha512-bbUtQaigsDPUQf6vtDOSCo8GYR7Gnw2ny1Fi3JtUnKsYsYQEcEadzmqntAgf7rLcKc12dadDUgFLxru+jiA92g=="],
"@moduletrace/twui": ["@moduletrace/[email protected].6", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Ftwui/-/0.1.6/twui-0.1.6.tgz", { "dependencies": { "lodash": "^4.18.1", "tailwind-merge": "^3.7.0" }, "peerDependencies": { "ace-builds": "^1.40.0", "lucide-react": ">=0.577.0", "next-mdx-remote": "^6.0.0", "openai": ">=6.0.0", "react": ">=18", "react-dom": ">=18", "react-markdown": ">=9.0.0", "rehype-prism-plus": "^2.0.0", "remark-gfm": "^4.0.0", "tailwindcss": "^4" }, "optionalPeers": ["ace-builds", "next-mdx-remote", "openai", "react-markdown", "rehype-prism-plus", "remark-gfm"] }, "sha512-BFNIV1itkIYXmhq1rBJA8E8MjwN9AP7MESbOnzM0yUb6xbsOFIBbmlKfUu6kTr7QCl1TWNxWTGFPVmQ2S54cDA=="],
"@moduletrace/twui": ["@moduletrace/[email protected].7", "https://git.tben.me/api/packages/Moduletrace/npm/%40moduletrace%2Ftwui/-/0.1.7/twui-0.1.7.tgz", { "dependencies": { "lodash": "^4.18.1", "tailwind-merge": "^3.7.0" }, "peerDependencies": { "ace-builds": "^1.40.0", "lucide-react": ">=0.577.0", "next-mdx-remote": "^6.0.0", "openai": ">=6.0.0", "react": ">=18", "react-dom": ">=18", "react-markdown": ">=9.0.0", "rehype-prism-plus": "^2.0.0", "remark-gfm": "^4.0.0", "tailwindcss": "^4" }, "optionalPeers": ["ace-builds", "next-mdx-remote", "openai", "react-markdown", "rehype-prism-plus", "remark-gfm"] }, "sha512-/Y9QyhtQsoEAJO+ahlGYQaYMx3gUWS+DlSkaBVjAIY5V9OQ6i7qhhqMNcmnfOi9m3eyAze1uW00jox+RDUqLpA=="],
"@next/mdx": ["@next/[email protected]", "", { "dependencies": { "source-map": "^0.7.0" }, "peerDependencies": { "@mdx-js/loader": ">=0.15.0", "@mdx-js/react": ">=0.15.0" }, "optionalPeers": ["@mdx-js/loader", "@mdx-js/react"] }, "sha512-Vpfuf928ACvx4AOdDXWx/cj13Ak6xlvMn3QCkjvfQMmLnQWCesQTeY01CPDbIyZvjY+NOwP7bvbvJ2ghkj/3GA=="],
+1 -1
View File
@@ -45,7 +45,7 @@
"@modelcontextprotocol/sdk": "^1.32.1",
"@moduletrace/bun-sqlite": "^1.3.0",
"@moduletrace/bunext": "^1.3.3",
"@moduletrace/twui": "^0.1.6",
"@moduletrace/twui": "^0.1.7",
"@tiptap/extension-bubble-menu": "3.32.0",
"@tiptap/extension-mention": "3.32.0",
"@tiptap/extension-placeholder": "3.32.0",
@@ -0,0 +1,12 @@
/**
* The column tracks of the models table, shared by its heading and every
* row, so a figure always sits under its own heading whatever its width.
* The table never gets narrower than its columns: it scrolls instead.
*/
export const UsageModelColumns =
"grid grid-cols-[minmax(160px,1fr)_48px_56px_56px_64px_64px_64px] min-w-[620px]";
/**
* The column tracks of the breakdown by kind of call, the same way.
*/
export const UsageHarnessColumns = "grid grid-cols-[minmax(0,1fr)_40px_72px_64px]";
@@ -1,39 +1,44 @@
import Popover from "@moduletrace/twui/elements/Popover";
import Stack from "@moduletrace/twui/layout/Stack";
import { twMerge } from "tailwind-merge";
import dayjs from "dayjs";
import type { UsageOverview } from "@/src/types";
import formatTokens from "@/src/utils/format-tokens";
import { usageBarHeight } from "../(data)/usage-bar-heights";
import UsageDaySummary from "./usage-day-summary";
type Props = {
day: UsageOverview["days"][number];
max: number;
/**
* Every day's tokens in the period, for the day's share.
*/
total_tokens: number;
};
/**
* One day's bar in the chart: its tokens above nothing, the bar itself,
* and the title spells the day out with what it spent. Hovering it shows
* that title.
* One day's bar in the chart. Hovering it (or tapping it) opens a small
* card above it summing the day up.
*/
export default function UsageDayBar({ day, max }: Props) {
const title = `${dayjs(day.at).format("ddd, MMM D")}: ${formatTokens({ tokens: day.total_tokens })} tokens, ${daysCalls({ calls: day.calls })}`;
export default function UsageDayBar({ day, max, total_tokens }: Props) {
return (
<Stack
title={title}
className="grow basis-[3px] min-w-[3px] max-w-10 h-24 justify-end items-stretch gap-0!"
>
<Popover
trigger="hover"
position="top"
debounce={80}
targetWrapperProps={{
className: "grow basis-[3px] min-w-[3px] max-w-10 h-24 flex flex-col justify-end cursor-pointer group",
}}
target={
<Stack
className={twMerge(
"w-full rounded-t-[2px]",
day.total_tokens ? "bg-ink" : "bg-hairline",
"w-full gap-0! rounded-t-[2px] transition-opacity",
day.total_tokens ? "bg-ink group-hover:opacity-70" : "bg-hairline group-hover:bg-hairline-strong",
usageBarHeight({ value: day.total_tokens, max }),
)}
/>
</Stack>
}
className="w-56 p-3 gap-2! bg-surface dark:bg-surface border-hairline dark:border-hairline rounded-default shadow-popover"
>
<UsageDaySummary day={day} total_tokens={total_tokens} />
</Popover>
);
}
function daysCalls({ calls }: { calls: number }) {
return `${calls} ${calls == 1 ? "call" : "calls"}`;
}
@@ -13,10 +13,12 @@ type Props = {
/**
* The period day by day, one bar a day, so a spike or a quiet stretch is
* read at a glance. The busiest day's total labels the top.
* read at a glance. The busiest day's total labels the top, and each bar
* opens a small summary of its day.
*/
export default function UsageDayChart({ days }: Props) {
const max = days.reduce((highest, day) => Math.max(highest, day.total_tokens), 0);
const total_tokens = days.reduce((total, day) => total + day.total_tokens, 0);
const first = days[0];
const last = days[days.length - 1];
@@ -30,9 +32,9 @@ export default function UsageDayChart({ days }: Props) {
}
>
<Stack className="w-full gap-2!">
<Row className="w-full gap-1! items-end">
<Row className="w-full gap-1! flex-nowrap items-end">
{days.map((day) => (
<UsageDayBar day={day} max={max} key={day.at} />
<UsageDayBar day={day} max={max} total_tokens={total_tokens} key={day.at} />
))}
</Row>
{first && last ? (
@@ -0,0 +1,49 @@
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import dayjs from "dayjs";
import type { UsageOverview } from "@/src/types";
import formatTokens from "@/src/utils/format-tokens";
import UsageSummaryLine from "./usage-summary-line";
type Props = {
day: UsageOverview["days"][number];
/**
* Every day's tokens in the period, for the day's share.
*/
total_tokens: number;
};
/**
* What one day spent, in the card its bar opens: the day spelled out, then
* its tokens, how they split, its calls, its cost and its share of the
* period.
*/
export default function UsageDaySummary({ day, total_tokens }: Props) {
const share = total_tokens ? Math.round((day.total_tokens / total_tokens) * 100) : 0;
const lines = [
{ label: "Tokens", value: formatTokens({ tokens: day.total_tokens }) },
{ label: "In", value: formatTokens({ tokens: day.prompt_tokens }) },
{ label: "Out", value: formatTokens({ tokens: day.completion_tokens }) },
{ label: "Model calls", value: formatTokens({ tokens: day.calls }) },
{ label: "Cost", value: day.cost ? `$${day.cost.toFixed(2)}` : "-" },
{ label: "Share of period", value: `${share}%` },
];
return (
<Stack className="w-full gap-2! items-stretch">
<Span className="text-[13px] leading-5 font-bold text-ink">
{dayjs(day.at).format("dddd, MMM D")}
</Span>
{day.calls ? (
<Stack className="w-full gap-1! items-stretch">
{lines.map((line) => (
<UsageSummaryLine {...line} key={line.label} />
))}
</Stack>
) : (
<Span className="text-[12px] leading-4 text-ink-secondary">No model calls.</Span>
)}
</Stack>
);
}
@@ -3,7 +3,9 @@ import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import type { UsageOverview } from "@/src/types";
import { twMerge } from "tailwind-merge";
import SettingsGroup from "@/src/pages/admin/settings/(partials)/settings-group";
import { UsageHarnessColumns } from "../(data)/usage-table-columns";
import sumTokens from "../(functions)/sum-tokens";
import UsageHarnessRow from "./usage-harness-row";
@@ -32,17 +34,22 @@ export default function UsageHarnessBreakdown({ rows }: Props) {
return (
<SettingsGroup title="By kind of call" description="Where the period's tokens went.">
<Stack className="w-full gap-0!">
<Row className="w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline">
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary grow">
<Row
className={twMerge(
UsageHarnessColumns,
"w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline",
)}
>
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary">
Kind
</Span>
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary w-10 text-right">
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary text-right">
Share
</Span>
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary w-20 text-right">
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary text-right">
Tokens
</Span>
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary w-16 text-right">
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary text-right">
Cost
</Span>
</Row>
@@ -1,8 +1,10 @@
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import { twMerge } from "tailwind-merge";
import type { UsageOverview } from "@/src/types";
import formatTokens from "@/src/utils/format-tokens";
import { UsageHarnessTitles } from "../(data)/usage-harness-titles";
import { UsageHarnessColumns } from "../(data)/usage-table-columns";
type Props = {
row: UsageOverview["by_harness"][number];
@@ -20,17 +22,22 @@ export default function UsageHarnessRow({ row, total_tokens }: Props) {
const share = total_tokens ? Math.round((row.total_tokens / total_tokens) * 100) : 0;
return (
<Row className="w-full py-2 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0">
<Span className="text-[13px] leading-5 text-ink grow min-w-0 truncate">
<Row
className={twMerge(
UsageHarnessColumns,
"w-full py-2 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0",
)}
>
<Span className="text-[13px] leading-5 text-ink min-w-0 truncate">
{UsageHarnessTitles[row.harness] || row.harness}
</Span>
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-10 text-right">
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
{share}%
</Span>
<Span className="text-[13px] leading-5 text-ink tabular-nums w-20 text-right">
<Span className="text-[13px] leading-5 text-ink tabular-nums text-right truncate">
{formatTokens({ tokens: row.total_tokens })}
</Span>
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-16 text-right">
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
{row.cost ? `$${row.cost.toFixed(2)}` : "-"}
</Span>
</Row>
@@ -13,7 +13,7 @@ export default function UsageModelHeadCell({ title, className }: Props) {
return (
<Span
className={twMerge(
"text-[11px] uppercase tracking-wide text-ink-secondary text-right shrink-0",
"text-[11px] uppercase tracking-wide text-ink-secondary text-right truncate",
className,
)}
>
@@ -1,9 +1,11 @@
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import { twMerge } from "tailwind-merge";
import type { UsageOverview } from "@/src/types";
import formatTokens from "@/src/utils/format-tokens";
import { UsageHarnessTitles } from "../(data)/usage-harness-titles";
import { UsageModelColumns } from "../(data)/usage-table-columns";
type Props = {
row: UsageOverview["models"][number];
@@ -20,8 +22,13 @@ export default function UsageModelRow({ row }: Props) {
: "-";
return (
<Row className="w-full py-2.5 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0">
<Stack className="grow min-w-0 gap-0!">
<Row
className={twMerge(
UsageModelColumns,
"w-full py-2.5 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0",
)}
>
<Stack className="min-w-0 gap-0! items-stretch">
<Span className="text-[13px] leading-5 text-ink truncate" title={row.model_slug}>
{row.model_name || row.model_slug}
</Span>
@@ -29,22 +36,22 @@ export default function UsageModelRow({ row }: Props) {
{row.provider_name} - {UsageHarnessTitles[row.harness] || row.harness}
</Span>
</Stack>
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-14 text-right">
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
{formatTokens({ tokens: row.calls })}
</Span>
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-16 text-right">
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
{formatTokens({ tokens: row.prompt_tokens })}
</Span>
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-16 text-right">
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
{formatTokens({ tokens: row.completion_tokens })}
</Span>
<Span className="text-[13px] leading-5 text-ink tabular-nums w-16 text-right">
<Span className="text-[13px] leading-5 text-ink tabular-nums text-right truncate">
{formatTokens({ tokens: row.total_tokens })}
</Span>
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-16 text-right">
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
{per_thousand}
</Span>
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums w-14 text-right">
<Span className="text-[12px] leading-4 text-ink-secondary tabular-nums text-right truncate">
{row.cost ? `$${row.cost.toFixed(2)}` : "-"}
</Span>
</Row>
@@ -3,7 +3,9 @@ import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
import Stack from "@moduletrace/twui/layout/Stack";
import type { UsageOverview } from "@/src/types";
import { twMerge } from "tailwind-merge";
import SettingsGroup from "@/src/pages/admin/settings/(partials)/settings-group";
import { UsageModelColumns } from "../(data)/usage-table-columns";
import UsageModelHeadCell from "./usage-model-head-cell";
import UsageModelRow from "./usage-model-row";
@@ -34,14 +36,19 @@ export default function UsageModelsTable({ rows, days }: Props) {
description={`The models this install ran in the last ${days} days, busiest first. The price per thousand tokens is read from what the provider reported.`}
>
<Stack className="w-full gap-0! overflow-x-auto">
<Row className="w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline min-w-[640px]">
<UsageModelHeadCell className="grow text-left" title="Model" />
<UsageModelHeadCell className="w-14" title="Calls" />
<UsageModelHeadCell className="w-16" title="In" />
<UsageModelHeadCell className="w-16" title="Out" />
<UsageModelHeadCell className="w-16" title="Tokens" />
<UsageModelHeadCell className="w-16" title="Per 1K" />
<UsageModelHeadCell className="w-14" title="Cost" />
<Row
className={twMerge(
UsageModelColumns,
"w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline",
)}
>
<UsageModelHeadCell className="text-left" title="Model" />
<UsageModelHeadCell title="Calls" />
<UsageModelHeadCell title="In" />
<UsageModelHeadCell title="Out" />
<UsageModelHeadCell title="Tokens" />
<UsageModelHeadCell title="Per 1K" />
<UsageModelHeadCell title="Cost" />
</Row>
{rows.map((row) => (
<UsageModelRow
@@ -0,0 +1,39 @@
import Button from "@moduletrace/twui/layout/Button";
import { twMerge } from "tailwind-merge";
import type { UsageRange } from "../(data)/usage-ranges";
type Props = {
range: UsageRange;
selected: boolean;
loading?: boolean;
onChange: (days: number) => void;
};
/**
* One period of the range picker: ink-filled while it is the one showing,
* quiet otherwise.
*/
export default function UsageRangeButton({ range, selected, loading, onChange }: Props) {
return (
<Button
title={range.title}
type="button"
size="small"
variant="ghost"
color="text"
aria-pressed={selected}
disabled={loading && !selected}
onClick={() => {
if (!selected) onChange(range.days);
}}
className={twMerge(
"px-2.5! py-1! text-[12px]! rounded-default-xs",
selected
? "bg-ink dark:bg-ink hover:bg-ink dark:hover:bg-ink text-on-ink dark:text-on-ink cursor-default"
: "text-ink-secondary dark:text-ink-secondary hover:bg-ink/5 dark:hover:bg-ink/5 hover:text-ink dark:hover:text-ink",
)}
>
{range.title}
</Button>
);
}
@@ -1,6 +1,6 @@
import Button from "@moduletrace/twui/layout/Button";
import Row from "@moduletrace/twui/layout/Row";
import { UsageRanges } from "../(data)/usage-ranges";
import UsageRangeButton from "./usage-range-button";
type Props = {
days: number;
@@ -9,26 +9,20 @@ type Props = {
};
/**
* The periods the page totals, one button each. The one showing is the
* one that reads as pressed.
* The periods the page totals, one segment each in a hairline track. The
* one showing is filled in ink, so it reads as pressed.
*/
export default function UsageRangePicker({ days, loading, onChange }: Props) {
return (
<Row className="gap-1! items-center">
<Row className="gap-0.5! p-0.5 flex-nowrap items-center rounded-default border border-solid border-hairline">
{UsageRanges.map((range) => (
<Button
<UsageRangeButton
key={range.days}
title={range.title}
type="button"
size="small"
variant={range.days == days ? "outlined" : "ghost"}
color="text"
disabled={loading}
onClick={() => onChange(range.days)}
className="px-2.5! py-1! text-[12px]! rounded-default-xs"
>
{range.title}
</Button>
range={range}
selected={range.days == days}
loading={loading}
onChange={onChange}
/>
))}
</Row>
);
@@ -0,0 +1,20 @@
import Row from "@moduletrace/twui/layout/Row";
import Span from "@moduletrace/twui/layout/Span";
type Props = {
label: string;
value: string;
};
/**
* One figure of a day's summary card: what it is on the left, the figure
* on the right.
*/
export default function UsageSummaryLine({ label, value }: Props) {
return (
<Row className="w-full gap-3! flex-nowrap items-center justify-between">
<Span className="text-[12px] leading-4 text-ink-secondary">{label}</Span>
<Span className="text-[12px] leading-4 text-ink tabular-nums">{value}</Span>
</Row>
);
}