SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
5.2 KB · 98 lines tsx
Raw Blame History
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