'use client'; import { ChevronDown, ChevronUp } from 'lucide-react'; import { type ReactNode, useEffect, useState } from 'react'; import { Sparkline, StepChart, stepPoints, type Series } from '@/components/charts'; import { clientIntel } from '@/lib/client-api'; import { cn } from '@/lib/cn'; import { fmtDate, fmtUsdPerM, num } from '@/lib/format'; import type { Price } from '@/lib/types'; /* Expandable offer row: the server renders the cells (children); the last cell holds a toggle. On first expansion the browser fetches `/prices/history?model=&provider=` and renders input/output sparklines (offers table) or a step chart (provider page). Rows are keyed by model × provider; the fetch is lazy and cached in component state. */ export function ExpandableOfferRow({ model, provider, children, colSpan, mode = 'spark', className }: { model: string; provider: string; children: ReactNode; colSpan: number; mode?: 'spark' | 'step'; className?: string }) { const [open, setOpen] = useState(false); const [rows, setRows] = useState(null); const [error, setError] = useState(null); useEffect(() => { if (!open || rows || error) return; const ctrl = new AbortController(); clientIntel .priceHistory(model, provider, ctrl.signal) .then((r) => setRows(r.items.slice().sort((a, b) => a.valid_from.localeCompare(b.valid_from)))) .catch((e) => { if (!ctrl.signal.aborted) setError(e instanceof Error ? e.message : 'unavailable'); }); return () => ctrl.abort(); }, [open, rows, error, model, provider]); return ( <> {children} {open && ( {error ? (

Price history unavailable ({error}).

) : !rows ? (

Loading history…

) : ( )} )} ); } function History({ rows, mode }: { rows: Price[]; mode: 'spark' | 'step' }) { const ins = rows.map((r) => num(r.input_per_mtok)).filter((v): v is number => v !== null); const outs = rows.map((r) => num(r.output_per_mtok)).filter((v): v is number => v !== null); if (!rows.length) return

No history rows for this model × provider.

; const first = rows[0] as Price; const last = rows[rows.length - 1] as Price; const distinct = new Set(rows.map((r) => `${r.input_per_mtok}|${r.output_per_mtok}`)).size; if (mode === 'step' && distinct >= 2) { const now = new Date().toISOString(); const build = (field: 'input_per_mtok' | 'output_per_mtok', name: string, color: string): Series => ({ name, color, points: stepPoints([...rows.map((r) => ({ at: r.valid_from, value: num(r[field]) })), ...(last.valid_to ? [] : [{ at: now, value: num(last[field]) }])]) }); const series = [build('input_per_mtok', 'Input', 'var(--series-1)'), build('output_per_mtok', 'Output', 'var(--series-2)')].filter((s) => s.points.length > 1); return (
fmtUsdPerM(v)} yLabel="USD per 1M tokens" />
); } return (
Input fmtUsdPerM(v)} title="Input price history" /> Output fmtUsdPerM(v)} title="Output price history" />
); } function Facts({ rows, first, last, distinct, inline = false }: { rows: Price[]; first: Price; last: Price; distinct: number; inline?: boolean }) { return (

{rows.length} observation{rows.length === 1 ? '' : 's'} · {distinct} distinct price{distinct === 1 ? '' : 's'} · first {fmtDate(first.valid_from)} · latest {fmtDate(last.observed_at)} {distinct < 2 && No change recorded yet — a sparkline needs two distinct prices.}

); }