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:
@@ -9,7 +9,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.32.1",
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
||||||
"@moduletrace/bun-sqlite": "^1.3.0",
|
"@moduletrace/bun-sqlite": "^1.3.0",
|
||||||
"@moduletrace/bunext": "^1.3.3",
|
"@moduletrace/bunext": "^1.3.3",
|
||||||
"@moduletrace/twui": "^0.1.6",
|
"@moduletrace/twui": "^0.1.7",
|
||||||
"@tiptap/extension-bubble-menu": "3.32.0",
|
"@tiptap/extension-bubble-menu": "3.32.0",
|
||||||
"@tiptap/extension-mention": "3.32.0",
|
"@tiptap/extension-mention": "3.32.0",
|
||||||
"@tiptap/extension-placeholder": "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/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=="],
|
"@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
@@ -45,7 +45,7 @@
|
|||||||
"@modelcontextprotocol/sdk": "^1.32.1",
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
||||||
"@moduletrace/bun-sqlite": "^1.3.0",
|
"@moduletrace/bun-sqlite": "^1.3.0",
|
||||||
"@moduletrace/bunext": "^1.3.3",
|
"@moduletrace/bunext": "^1.3.3",
|
||||||
"@moduletrace/twui": "^0.1.6",
|
"@moduletrace/twui": "^0.1.7",
|
||||||
"@tiptap/extension-bubble-menu": "3.32.0",
|
"@tiptap/extension-bubble-menu": "3.32.0",
|
||||||
"@tiptap/extension-mention": "3.32.0",
|
"@tiptap/extension-mention": "3.32.0",
|
||||||
"@tiptap/extension-placeholder": "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 Stack from "@moduletrace/twui/layout/Stack";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import type { UsageOverview } from "@/src/types";
|
import type { UsageOverview } from "@/src/types";
|
||||||
import formatTokens from "@/src/utils/format-tokens";
|
|
||||||
import { usageBarHeight } from "../(data)/usage-bar-heights";
|
import { usageBarHeight } from "../(data)/usage-bar-heights";
|
||||||
|
import UsageDaySummary from "./usage-day-summary";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
day: UsageOverview["days"][number];
|
day: UsageOverview["days"][number];
|
||||||
max: 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,
|
* One day's bar in the chart. Hovering it (or tapping it) opens a small
|
||||||
* and the title spells the day out with what it spent. Hovering it shows
|
* card above it summing the day up.
|
||||||
* that title.
|
|
||||||
*/
|
*/
|
||||||
export default function UsageDayBar({ day, max }: Props) {
|
export default function UsageDayBar({ day, max, total_tokens }: Props) {
|
||||||
const title = `${dayjs(day.at).format("ddd, MMM D")}: ${formatTokens({ tokens: day.total_tokens })} tokens, ${daysCalls({ calls: day.calls })}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Popover
|
||||||
title={title}
|
trigger="hover"
|
||||||
className="grow basis-[3px] min-w-[3px] max-w-10 h-24 justify-end items-stretch gap-0!"
|
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
|
<Stack
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-full rounded-t-[2px]",
|
"w-full gap-0! rounded-t-[2px] transition-opacity",
|
||||||
day.total_tokens ? "bg-ink" : "bg-hairline",
|
day.total_tokens ? "bg-ink group-hover:opacity-70" : "bg-hairline group-hover:bg-hairline-strong",
|
||||||
usageBarHeight({ value: day.total_tokens, max }),
|
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
|
* 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) {
|
export default function UsageDayChart({ days }: Props) {
|
||||||
const max = days.reduce((highest, day) => Math.max(highest, day.total_tokens), 0);
|
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 first = days[0];
|
||||||
const last = days[days.length - 1];
|
const last = days[days.length - 1];
|
||||||
|
|
||||||
@@ -30,9 +32,9 @@ export default function UsageDayChart({ days }: Props) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Stack className="w-full gap-2!">
|
<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) => (
|
{days.map((day) => (
|
||||||
<UsageDayBar day={day} max={max} key={day.at} />
|
<UsageDayBar day={day} max={max} total_tokens={total_tokens} key={day.at} />
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
{first && last ? (
|
{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 Span from "@moduletrace/twui/layout/Span";
|
||||||
import Stack from "@moduletrace/twui/layout/Stack";
|
import Stack from "@moduletrace/twui/layout/Stack";
|
||||||
import type { UsageOverview } from "@/src/types";
|
import type { UsageOverview } from "@/src/types";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import SettingsGroup from "@/src/pages/admin/settings/(partials)/settings-group";
|
import SettingsGroup from "@/src/pages/admin/settings/(partials)/settings-group";
|
||||||
|
import { UsageHarnessColumns } from "../(data)/usage-table-columns";
|
||||||
import sumTokens from "../(functions)/sum-tokens";
|
import sumTokens from "../(functions)/sum-tokens";
|
||||||
import UsageHarnessRow from "./usage-harness-row";
|
import UsageHarnessRow from "./usage-harness-row";
|
||||||
|
|
||||||
@@ -32,17 +34,22 @@ export default function UsageHarnessBreakdown({ rows }: Props) {
|
|||||||
return (
|
return (
|
||||||
<SettingsGroup title="By kind of call" description="Where the period's tokens went.">
|
<SettingsGroup title="By kind of call" description="Where the period's tokens went.">
|
||||||
<Stack className="w-full gap-0!">
|
<Stack className="w-full gap-0!">
|
||||||
<Row className="w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline">
|
<Row
|
||||||
<Span className="text-[11px] uppercase tracking-wide text-ink-secondary grow">
|
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
|
Kind
|
||||||
</Span>
|
</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
|
Share
|
||||||
</Span>
|
</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
|
Tokens
|
||||||
</Span>
|
</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
|
Cost
|
||||||
</Span>
|
</Span>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import Row from "@moduletrace/twui/layout/Row";
|
import Row from "@moduletrace/twui/layout/Row";
|
||||||
import Span from "@moduletrace/twui/layout/Span";
|
import Span from "@moduletrace/twui/layout/Span";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import type { UsageOverview } from "@/src/types";
|
import type { UsageOverview } from "@/src/types";
|
||||||
import formatTokens from "@/src/utils/format-tokens";
|
import formatTokens from "@/src/utils/format-tokens";
|
||||||
import { UsageHarnessTitles } from "../(data)/usage-harness-titles";
|
import { UsageHarnessTitles } from "../(data)/usage-harness-titles";
|
||||||
|
import { UsageHarnessColumns } from "../(data)/usage-table-columns";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
row: UsageOverview["by_harness"][number];
|
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;
|
const share = total_tokens ? Math.round((row.total_tokens / total_tokens) * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="w-full py-2 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0">
|
<Row
|
||||||
<Span className="text-[13px] leading-5 text-ink grow min-w-0 truncate">
|
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}
|
{UsageHarnessTitles[row.harness] || row.harness}
|
||||||
</Span>
|
</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}%
|
{share}%
|
||||||
</Span>
|
</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 })}
|
{formatTokens({ tokens: row.total_tokens })}
|
||||||
</Span>
|
</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)}` : "-"}
|
{row.cost ? `$${row.cost.toFixed(2)}` : "-"}
|
||||||
</Span>
|
</Span>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function UsageModelHeadCell({ title, className }: Props) {
|
|||||||
return (
|
return (
|
||||||
<Span
|
<Span
|
||||||
className={twMerge(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import Row from "@moduletrace/twui/layout/Row";
|
import Row from "@moduletrace/twui/layout/Row";
|
||||||
import Span from "@moduletrace/twui/layout/Span";
|
import Span from "@moduletrace/twui/layout/Span";
|
||||||
import Stack from "@moduletrace/twui/layout/Stack";
|
import Stack from "@moduletrace/twui/layout/Stack";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import type { UsageOverview } from "@/src/types";
|
import type { UsageOverview } from "@/src/types";
|
||||||
import formatTokens from "@/src/utils/format-tokens";
|
import formatTokens from "@/src/utils/format-tokens";
|
||||||
import { UsageHarnessTitles } from "../(data)/usage-harness-titles";
|
import { UsageHarnessTitles } from "../(data)/usage-harness-titles";
|
||||||
|
import { UsageModelColumns } from "../(data)/usage-table-columns";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
row: UsageOverview["models"][number];
|
row: UsageOverview["models"][number];
|
||||||
@@ -20,8 +22,13 @@ export default function UsageModelRow({ row }: Props) {
|
|||||||
: "-";
|
: "-";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row className="w-full py-2.5 gap-3! items-center border-0 border-b border-solid border-hairline last:border-b-0">
|
<Row
|
||||||
<Stack className="grow min-w-0 gap-0!">
|
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}>
|
<Span className="text-[13px] leading-5 text-ink truncate" title={row.model_slug}>
|
||||||
{row.model_name || row.model_slug}
|
{row.model_name || row.model_slug}
|
||||||
</Span>
|
</Span>
|
||||||
@@ -29,22 +36,22 @@ export default function UsageModelRow({ row }: Props) {
|
|||||||
{row.provider_name} - {UsageHarnessTitles[row.harness] || row.harness}
|
{row.provider_name} - {UsageHarnessTitles[row.harness] || row.harness}
|
||||||
</Span>
|
</Span>
|
||||||
</Stack>
|
</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 })}
|
{formatTokens({ tokens: row.calls })}
|
||||||
</Span>
|
</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 })}
|
{formatTokens({ tokens: row.prompt_tokens })}
|
||||||
</Span>
|
</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 })}
|
{formatTokens({ tokens: row.completion_tokens })}
|
||||||
</Span>
|
</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 })}
|
{formatTokens({ tokens: row.total_tokens })}
|
||||||
</Span>
|
</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}
|
{per_thousand}
|
||||||
</Span>
|
</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)}` : "-"}
|
{row.cost ? `$${row.cost.toFixed(2)}` : "-"}
|
||||||
</Span>
|
</Span>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import Row from "@moduletrace/twui/layout/Row";
|
|||||||
import Span from "@moduletrace/twui/layout/Span";
|
import Span from "@moduletrace/twui/layout/Span";
|
||||||
import Stack from "@moduletrace/twui/layout/Stack";
|
import Stack from "@moduletrace/twui/layout/Stack";
|
||||||
import type { UsageOverview } from "@/src/types";
|
import type { UsageOverview } from "@/src/types";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
import SettingsGroup from "@/src/pages/admin/settings/(partials)/settings-group";
|
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 UsageModelHeadCell from "./usage-model-head-cell";
|
||||||
import UsageModelRow from "./usage-model-row";
|
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.`}
|
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">
|
<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]">
|
<Row
|
||||||
<UsageModelHeadCell className="grow text-left" title="Model" />
|
className={twMerge(
|
||||||
<UsageModelHeadCell className="w-14" title="Calls" />
|
UsageModelColumns,
|
||||||
<UsageModelHeadCell className="w-16" title="In" />
|
"w-full pb-2 gap-3! items-center border-0 border-b border-solid border-hairline",
|
||||||
<UsageModelHeadCell className="w-16" title="Out" />
|
)}
|
||||||
<UsageModelHeadCell className="w-16" title="Tokens" />
|
>
|
||||||
<UsageModelHeadCell className="w-16" title="Per 1K" />
|
<UsageModelHeadCell className="text-left" title="Model" />
|
||||||
<UsageModelHeadCell className="w-14" title="Cost" />
|
<UsageModelHeadCell title="Calls" />
|
||||||
|
<UsageModelHeadCell title="In" />
|
||||||
|
<UsageModelHeadCell title="Out" />
|
||||||
|
<UsageModelHeadCell title="Tokens" />
|
||||||
|
<UsageModelHeadCell title="Per 1K" />
|
||||||
|
<UsageModelHeadCell title="Cost" />
|
||||||
</Row>
|
</Row>
|
||||||
{rows.map((row) => (
|
{rows.map((row) => (
|
||||||
<UsageModelRow
|
<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 Row from "@moduletrace/twui/layout/Row";
|
||||||
import { UsageRanges } from "../(data)/usage-ranges";
|
import { UsageRanges } from "../(data)/usage-ranges";
|
||||||
|
import UsageRangeButton from "./usage-range-button";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
days: number;
|
days: number;
|
||||||
@@ -9,26 +9,20 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The periods the page totals, one button each. The one showing is the
|
* The periods the page totals, one segment each in a hairline track. The
|
||||||
* one that reads as pressed.
|
* one showing is filled in ink, so it reads as pressed.
|
||||||
*/
|
*/
|
||||||
export default function UsageRangePicker({ days, loading, onChange }: Props) {
|
export default function UsageRangePicker({ days, loading, onChange }: Props) {
|
||||||
return (
|
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) => (
|
{UsageRanges.map((range) => (
|
||||||
<Button
|
<UsageRangeButton
|
||||||
key={range.days}
|
key={range.days}
|
||||||
title={range.title}
|
range={range}
|
||||||
type="button"
|
selected={range.days == days}
|
||||||
size="small"
|
loading={loading}
|
||||||
variant={range.days == days ? "outlined" : "ghost"}
|
onChange={onChange}
|
||||||
color="text"
|
/>
|
||||||
disabled={loading}
|
|
||||||
onClick={() => onChange(range.days)}
|
|
||||||
className="px-2.5! py-1! text-[12px]! rounded-default-xs"
|
|
||||||
>
|
|
||||||
{range.title}
|
|
||||||
</Button>
|
|
||||||
))}
|
))}
|
||||||
</Row>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user