/**
* Shared primitives for the per-model breakdown tables (ModelsTable,
* BehaviorModelsTable). Each table still owns its column definitions, sort
* order, sidebar contents and chart type — this module owns the surface
* chrome, expand-row plumbing, theme palette, and the mini-sparkline plus
* the shared plugin/scale config consumed by multi-line detail charts.
*/
import { format } from "date-fns";
import { ChevronDown, ChevronUp } from "lucide-react";
import { Line } from "react-chartjs-2";
export { MODEL_COLORS } from "./chart-shared";
export const TABLE_CHART_THEMES = {
dark: {
legendLabel: "#cbd5e1",
tooltipBackground: "#16161e",
tooltipTitle: "#f8fafc",
tooltipBody: "#94a3b8",
tooltipBorder: "rgba(255, 255, 255, 0.1)",
grid: "rgba(255, 255, 255, 0.06)",
tick: "#94a3b8",
},
light: {
legendLabel: "#334155",
tooltipBackground: "#ffffff",
tooltipTitle: "#0f172a",
tooltipBody: "#334155",
tooltipBorder: "rgba(15, 23, 42, 0.18)",
grid: "rgba(15, 23, 42, 0.08)",
tick: "#475569",
},
} as const;
export type TableChartTheme = (typeof TABLE_CHART_THEMES)[keyof typeof TABLE_CHART_THEMES];
/** Style defaults for one line in a non-stacked detail chart. */
export function lineSeriesStyle(color: string) {
return {
borderColor: color,
backgroundColor: "transparent",
tension: 0.4,
pointRadius: 0,
borderWidth: 2,
};
}
/**
* No-axis, no-legend single-series sparkline used in the trend cell of every
* model row. Caller supplies the already-extracted numeric series so this
* stays agnostic of the row's underlying data shape.
*/
export function MiniSparkline({
timestamps,
values,
color,
}: {
timestamps: number[];
values: number[];
color: string;
}) {
const chartData = {
labels: timestamps.map(ts => format(new Date(ts), "MMM d")),
datasets: [{ data: values, ...lineSeriesStyle(color) }],
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false }, tooltip: { enabled: false } },
scales: {
x: { display: false },
y: { display: false, min: 0 },
},
};
return ;
}
/**
* Plugin block (legend + tooltip) shared by every multi-series detail chart
* in the table expanded views.
*/
export function detailChartPlugins(chartTheme: TableChartTheme) {
return {
legend: {
display: true,
position: "top" as const,
labels: {
color: chartTheme.legendLabel,
usePointStyle: true,
padding: 16,
font: { size: 12 },
},
},
tooltip: {
backgroundColor: chartTheme.tooltipBackground,
titleColor: chartTheme.tooltipTitle,
bodyColor: chartTheme.tooltipBody,
borderColor: chartTheme.tooltipBorder,
borderWidth: 1,
cornerRadius: 8,
},
};
}
/**
* Single-Y-axis scales for a detail chart (used when every series shares a
* unit, e.g. behavior counts). Min anchored at 0.
*/
export function detailChartScalesSingleAxis(chartTheme: TableChartTheme) {
return {
x: {
grid: { color: chartTheme.grid },
ticks: { color: chartTheme.tick, font: { size: 11 } },
},
y: {
grid: { color: chartTheme.grid },
ticks: { color: chartTheme.tick, font: { size: 11 } },
min: 0,
},
};
}
/**
* Dual-Y-axis scales for a detail chart with mixed units (e.g. TTFT seconds
* on left, tokens/s on right). Right-axis grid is suppressed so it doesn't
* collide with the left.
*/
export function detailChartScalesDualAxis(chartTheme: TableChartTheme) {
return {
x: {
grid: { color: chartTheme.grid },
ticks: { color: chartTheme.tick, font: { size: 11 } },
},
y: {
type: "linear" as const,
display: true,
position: "left" as const,
grid: { color: chartTheme.grid },
ticks: { color: chartTheme.tick, font: { size: 11 } },
},
y1: {
type: "linear" as const,
display: true,
position: "right" as const,
grid: { drawOnChartArea: false },
ticks: { color: chartTheme.tick, font: { size: 11 } },
},
};
}
export interface TableColumn {
label: string;
align?: "left" | "right" | "center";
}
/** Outer card + section title used by every model table. */
export function ModelTableShell({
title,
subtitle,
children,
}: {
title: string;
subtitle?: string;
children: React.ReactNode;
}) {
return (
{title}
{subtitle ?
{subtitle}
: null}
{children}
);
}
function alignClass(align: TableColumn["align"]): string {
if (align === "right") return "text-right";
if (align === "center") return "text-center";
return "";
}
/** Sticky column-header row for a model table. */
export function ModelTableHeader({ columns, gridTemplate }: { columns: TableColumn[]; gridTemplate: string }) {
return (
{columns.map(col => (
{col.label}
))}
{/* trailing chevron column has no header label */}
);
}
/** Scroll wrapper for the row stack — capped to fit the dashboard viewport. */
export function ModelTableBody({ children }: { children: React.ReactNode }) {
return
{children}
;
}
/**
* Two-line model identity cell (model name + provider) shared by every
* per-model table. Kept as a stable named contract so callers don't restate
* the same two divs and font-utility classes.
*/
export function ModelNameCell({ model, provider }: { model: string; provider: string }) {
return (
{model}
{provider}
);
}
/**
* One expandable model row. `cells` matches the column order from
* `ModelTableHeader` plus the trend cell at the end (caller controls the
* sparkline / placeholder). `expandedContent` is the panel revealed on toggle.
*/
export function ExpandableModelRow({
gridTemplate,
cells,
trendCell,
isExpanded,
onToggle,
expandedContent,
}: {
gridTemplate: string;
cells: React.ReactNode[];
trendCell: React.ReactNode;
isExpanded: boolean;
onToggle: () => void;
expandedContent: React.ReactNode;
}) {
return (
{isExpanded ? (
{expandedContent}
) : null}
);
}
/** Placeholder shown in the trend cell when a model has no time-series data. */
export function TrendEmpty() {
return
-
;
}
/** Placeholder shown in the expanded detail-chart slot when data is missing. */
export function DetailChartEmpty({ message = "No data available" }: { message?: string }) {
return