HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1'use client';2import { ChevronDown, ChevronUp } from 'lucide-react';3import { type ReactNode, useEffect, useState } from 'react';4import { Sparkline, StepChart, stepPoints, type Series } from '@/components/charts';5import { clientIntel } from '@/lib/client-api';6import { cn } from '@/lib/cn';7import { fmtDate, fmtUsdPerM, num } from '@/lib/format';8import type { Price } from '@/lib/types';910/*11 Expandable offer row: the server renders the cells (children); the last cell holds a toggle. On first expansion the browser12 fetches `/prices/history?model=&provider=` and renders input/output sparklines (offers table) or a step chart (provider page).13 Rows are keyed by model × provider; the fetch is lazy and cached in component state.14*/15export function ExpandableOfferRow({ model, provider, children, colSpan, mode = 'spark', className }: { model: string; provider: string; children: ReactNode; colSpan: number; mode?: 'spark' | 'step'; className?: string }) {16 const [open, setOpen] = useState(false);17 const [rows, setRows] = useState<Price[] | null>(null);18 const [error, setError] = useState<string | null>(null);19 useEffect(() => {20 if (!open || rows || error) return;21 const ctrl = new AbortController();22 clientIntel23 .priceHistory(model, provider, ctrl.signal)24 .then((r) => setRows(r.items.slice().sort((a, b) => a.valid_from.localeCompare(b.valid_from))))25 .catch((e) => {26 if (!ctrl.signal.aborted) setError(e instanceof Error ? e.message : 'unavailable');27 });28 return () => ctrl.abort();29 }, [open, rows, error, model, provider]);30 return (31 <>32 <tr className={className} data-offer-row>33 {children}34 <td className="text-right">35 <button type="button" onClick={() => setOpen((o) => !o)} aria-expanded={open} aria-label={open ? 'Hide price history' : 'Show price history'} className="inline-flex h-7 items-center gap-1 border border-rule px-1.5 text-xs text-ink-2 hover:border-rule-strong hover:text-ink" data-expand-history>36 {open ? <ChevronUp className="size-3" aria-hidden /> : <ChevronDown className="size-3" aria-hidden />} History37 </button>38 </td>39 </tr>40 {open && (41 <tr className="bg-surface-2/40" data-offer-history>42 <td colSpan={colSpan + 1} className="wide !py-3">43 {error ? (44 <p className="text-xs text-ink-3">Price history unavailable ({error}).</p>45 ) : !rows ? (46 <p className="text-xs text-ink-3" aria-busy="true">47 Loading history…48 </p>49 ) : (50 <History rows={rows} mode={mode} />51 )}52 </td>53 </tr>54 )}55 </>56 );57}5859function History({ rows, mode }: { rows: Price[]; mode: 'spark' | 'step' }) {60 const ins = rows.map((r) => num(r.input_per_mtok)).filter((v): v is number => v !== null);61 const outs = rows.map((r) => num(r.output_per_mtok)).filter((v): v is number => v !== null);62 if (!rows.length) return <p className="text-xs text-ink-3">No history rows for this model × provider.</p>;63 const first = rows[0] as Price;64 const last = rows[rows.length - 1] as Price;65 const distinct = new Set(rows.map((r) => `${r.input_per_mtok}|${r.output_per_mtok}`)).size;66 if (mode === 'step' && distinct >= 2) {67 const now = new Date().toISOString();68 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]) }])]) });69 const series = [build('input_per_mtok', 'Input', 'var(--series-1)'), build('output_per_mtok', 'Output', 'var(--series-2)')].filter((s) => s.points.length > 1);70 return (71 <div className="grid gap-3 md:grid-cols-[minmax(0,1fr)_16rem] md:items-start">72 <StepChart series={series} height={160} yFormat={(v) => fmtUsdPerM(v)} yLabel="USD per 1M tokens" />73 <Facts rows={rows} first={first} last={last} distinct={distinct} />74 </div>75 );76 }77 return (78 <div className="flex flex-wrap items-center gap-x-6 gap-y-2">79 <span className="flex items-center gap-2 text-xs text-ink-3">80 Input <Sparkline values={ins} width={120} height={26} stroke="var(--series-1)" variant="trend" invert format={(v) => fmtUsdPerM(v)} title="Input price history" />81 </span>82 <span className="flex items-center gap-2 text-xs text-ink-3">83 Output <Sparkline values={outs} width={120} height={26} stroke="var(--series-2)" variant="trend" invert format={(v) => fmtUsdPerM(v)} title="Output price history" />84 </span>85 <Facts rows={rows} first={first} last={last} distinct={distinct} inline />86 </div>87 );88}8990function Facts({ rows, first, last, distinct, inline = false }: { rows: Price[]; first: Price; last: Price; distinct: number; inline?: boolean }) {91 return (92 <p className={cn('tnum text-xs text-ink-3', !inline && 'leading-relaxed')}>93 {rows.length} observation{rows.length === 1 ? '' : 's'} · {distinct} distinct price{distinct === 1 ? '' : 's'} · first {fmtDate(first.valid_from)} · latest {fmtDate(last.observed_at)}94 {distinct < 2 && <span className="block">No change recorded yet — a sparkline needs two distinct prices.</span>}95 </p>96 );97}98