/** * 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
{message}
; }