Frontend: site chrome, demo shell, pages, and the contract gates
Five parallel lanes plus an integration pass. The header, gallery, router and sitemap are all generated from the demo registry, so adding src/demos/<slug>/ puts a demo everywhere with zero edits to shared files — which is the whole reason demo nine cannot break demo one. check-demos enforces the twelve contract rules: 142 checks over one live demo. Two worth naming. The shell may not mention a specific slug, because an 'if (slug === wordle)' in src/components/demo/ is a contract bug wearing a patch. And a spec-status demo must ship a real specification — task, actions, grader, counterweight, eval command — since a coming-soon card reads worse than an honest empty gallery. Bundle budget holds: entry 108.79 kB gzipped against a 160 kB ceiling, the demo chunk 21.15 kB against 90 kB. recharts is 108 kB gzipped and lives behind a lazy import so it never touches the entry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
+242
-335
@@ -1,14 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import * as Tabs from '@radix-ui/react-tabs';
|
||||
import { useUrlState } from '@/lib/url-state';
|
||||
import type {
|
||||
DemoEpisode,
|
||||
DemoModule,
|
||||
DemoStep,
|
||||
RunRef,
|
||||
StoryBeat,
|
||||
} from '@/lib/demo-kit/types';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { listRuns, loadEpisode, rewardTotal } from '@/lib/demo-kit/episode';
|
||||
import { usePlayer } from '@/lib/demo-kit/player';
|
||||
import { loadDemoModule } from '@/lib/demo-kit/registry';
|
||||
import type { AnyDemoModule } from '@/lib/demo-kit/registry';
|
||||
import type { DemoEpisode, DemoStep, RunRef, StoryBeat } from '@/lib/demo-kit/types';
|
||||
import { useRunParam, useSpeedParam, useStepParam, useTabParam } from '@/lib/url-state';
|
||||
import * as st from '@/content/styles';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { BeatSection } from './BeatSection';
|
||||
import { BlindCompare } from './BlindCompare';
|
||||
@@ -24,142 +25,71 @@ import { ReasoningPanel } from './ReasoningPanel';
|
||||
import { RewardBreakdown } from './RewardBreakdown';
|
||||
import { RewardEditor } from './RewardEditor';
|
||||
import type { RewardArm } from './RewardEditor';
|
||||
import { SegmentedControl } from './SegmentedControl';
|
||||
import { SlotRegion } from './SlotRegion';
|
||||
import { StatStrip } from './StatStrip';
|
||||
import type { Stat } from './StatStrip';
|
||||
import { StepTimeline } from './StepTimeline';
|
||||
import { RecordedBadge, TracePlayer, useTracePlayback } from './TracePlayer';
|
||||
import { RecordedBadge, TracePlayer } from './TracePlayer';
|
||||
import { VerifyBadge } from './VerifyBadge';
|
||||
import { clampIndex, formatOrDash, useIsDesktop } from './format';
|
||||
import { scoreReward } from './reward-math';
|
||||
import { mockDemo, mockEpisodes, mockRuns } from './mock';
|
||||
import { formatOrDash, useIsDesktop } from './format';
|
||||
|
||||
const REPO_BLOB = 'https://github.com/karti-ai/PIG-Demo/blob/main/';
|
||||
const MANIFEST_URL = '/traces/manifest.json';
|
||||
|
||||
/**
|
||||
* Every demo module in the repo, as an unresolved import each.
|
||||
*
|
||||
* `import.meta.glob` rather than a generated registry import on purpose: this
|
||||
* file must compile and render before any demo directory exists, and a glob
|
||||
* that matches nothing is an empty object rather than a build error.
|
||||
*/
|
||||
const DEMO_MODULES = import.meta.glob<Record<string, unknown>>('/src/demos/*/index.{ts,tsx}');
|
||||
/** The tab the step-detail strip opens on. Kept out of the URL when it is this. */
|
||||
const DEFAULT_DETAIL_TAB = 'reasoning';
|
||||
|
||||
export interface DemoBundle<T = unknown> {
|
||||
demo: DemoModule<T>;
|
||||
/** Reserved slug for the shell's own hand-written demo. Dev builds only. */
|
||||
const MOCK_SLUG = '__mock';
|
||||
|
||||
export interface DemoBundle {
|
||||
demo: AnyDemoModule;
|
||||
runs: RunRef[];
|
||||
episodes: Record<string, DemoEpisode>;
|
||||
}
|
||||
|
||||
type LoadState<T> =
|
||||
type LoadState =
|
||||
| { status: 'loading' }
|
||||
| { status: 'ready'; bundle: DemoBundle<T> }
|
||||
| { status: 'ready'; bundle: DemoBundle }
|
||||
| { status: 'error'; message: string };
|
||||
|
||||
function isRunRef(value: unknown): value is RunRef {
|
||||
if (typeof value !== 'object' || value === null) return false;
|
||||
const run = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof run['id'] === 'string' &&
|
||||
typeof run['label'] === 'string' &&
|
||||
typeof run['path'] === 'string' &&
|
||||
typeof run['model'] === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
function isEpisode(value: unknown): value is DemoEpisode {
|
||||
if (typeof value !== 'object' || value === null) return false;
|
||||
const episode = value as Record<string, unknown>;
|
||||
return (
|
||||
typeof episode['runId'] === 'string' &&
|
||||
Array.isArray(episode['turns']) &&
|
||||
typeof episode['rewards'] === 'object' &&
|
||||
episode['rewards'] !== null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The manifest is data on disk, not a typed import, so it is validated rather
|
||||
* than trusted — and three plausible shapes are accepted because the file is
|
||||
* written by a script in another lane and a keyed map, a nested map and a flat
|
||||
* list are all reasonable things for that script to have produced.
|
||||
* A demo's module plus every recorded run it has.
|
||||
*
|
||||
* `loadDemoModule` and `loadEpisode` both cache their promises, so the route
|
||||
* loader having already fetched the module makes this resolve without a second
|
||||
* request. Runs are loaded with `allSettled` on purpose: one unreadable trace
|
||||
* drops that arm rather than blanking the page.
|
||||
*/
|
||||
export function extractRuns(json: unknown, slug: string): RunRef[] {
|
||||
if (typeof json !== 'object' || json === null) return [];
|
||||
const root = json as Record<string, unknown>;
|
||||
const nested = root['demos'];
|
||||
const keyed =
|
||||
(Array.isArray(root[slug]) ? root[slug] : undefined) ??
|
||||
(typeof nested === 'object' && nested !== null
|
||||
? (nested as Record<string, unknown>)[slug]
|
||||
: undefined);
|
||||
if (Array.isArray(keyed)) return keyed.filter(isRunRef);
|
||||
|
||||
const flat = Array.isArray(root['runs']) ? root['runs'] : Array.isArray(json) ? json : null;
|
||||
if (flat) {
|
||||
return flat.filter(isRunRef).filter((run) => {
|
||||
const owner = (run as unknown as Record<string, unknown>)['demo'];
|
||||
return owner === undefined || owner === slug;
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function pickModule(mod: Record<string, unknown>): DemoModule | null {
|
||||
const candidate = mod['default'] ?? mod['demo'];
|
||||
if (typeof candidate !== 'object' || candidate === null) return null;
|
||||
const shape = candidate as Record<string, unknown>;
|
||||
return typeof shape['adapt'] === 'function' && typeof shape['Surface'] === 'function'
|
||||
? (candidate as DemoModule)
|
||||
: null;
|
||||
}
|
||||
|
||||
async function loadBundle(slug: string): Promise<DemoBundle> {
|
||||
if (slug === '__mock') {
|
||||
return { demo: mockDemo as unknown as DemoModule, runs: mockRuns, episodes: mockEpisodes };
|
||||
if (slug === MOCK_SLUG) {
|
||||
// Dynamic, so the mock lands in its own chunk and production never fetches
|
||||
// it. A static import would ship several hundred lines of fake trace to
|
||||
// every visitor of every real demo.
|
||||
const mock = await import('./mock');
|
||||
return { demo: mock.mockDemo, runs: mock.mockRuns, episodes: mock.mockEpisodes };
|
||||
}
|
||||
|
||||
const entry = Object.entries(DEMO_MODULES).find(([path]) =>
|
||||
path.startsWith(`/src/demos/${slug}/index.`),
|
||||
);
|
||||
if (!entry) throw new Error(`No demo is registered under the slug "${slug}".`);
|
||||
const demo = pickModule(await entry[1]());
|
||||
if (!demo) {
|
||||
throw new Error(`The module for "${slug}" does not export a demo that satisfies the contract.`);
|
||||
}
|
||||
const demo = await loadDemoModule(slug);
|
||||
const runs = await listRuns(slug).catch(() => [] as RunRef[]);
|
||||
const settled = await Promise.allSettled(runs.map((run) => loadEpisode(run)));
|
||||
|
||||
const manifest = await fetch(MANIFEST_URL, { cache: 'no-cache' })
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.catch(() => null);
|
||||
const runs = extractRuns(manifest, slug);
|
||||
|
||||
// One unreadable trace must not blank the page: fetch them all, keep the
|
||||
// ones that parse, and let the shell report the shortfall.
|
||||
const loaded = await Promise.all(
|
||||
runs.map(async (run) => {
|
||||
try {
|
||||
const response = await fetch(run.path, { cache: 'no-cache' });
|
||||
if (!response.ok) return null;
|
||||
const json: unknown = await response.json();
|
||||
return isEpisode(json) ? ([run.id, json] as const) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const episodes: Record<string, DemoEpisode> = {};
|
||||
for (const item of loaded) {
|
||||
if (item) episodes[item[0]] = item[1];
|
||||
}
|
||||
settled.forEach((outcome, index) => {
|
||||
const run = runs[index];
|
||||
if (!run) return;
|
||||
if (outcome.status === 'fulfilled') episodes[run.id] = outcome.value;
|
||||
else console.error(`[pig-demo] dropped run "${run.id}":`, outcome.reason);
|
||||
});
|
||||
|
||||
return { demo, runs: runs.filter((run) => episodes[run.id] !== undefined), episodes };
|
||||
}
|
||||
|
||||
export interface DemoShellProps<T = unknown> {
|
||||
export interface DemoShellProps {
|
||||
/** Overrides the route param. Useful for previews and tests. */
|
||||
slug?: string;
|
||||
/** Skips loading entirely when the caller already has the bundle. */
|
||||
bundle?: DemoBundle<T>;
|
||||
bundle?: DemoBundle;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,13 +98,12 @@ export interface DemoShellProps<T = unknown> {
|
||||
* It owns four things and no more: loading, the narrative beats, the URL state,
|
||||
* and the page's single polite live region. Everything visual is delegated to
|
||||
* the surfaces in this directory, and the demo module is never reached into —
|
||||
* the shell is generic over the demo's board type and only ever calls `adapt`
|
||||
* and renders `Surface`.
|
||||
* the shell only ever calls `adapt` and renders `Surface`.
|
||||
*/
|
||||
export function DemoShell<T = unknown>({ slug: slugProp, bundle }: DemoShellProps<T>) {
|
||||
export function DemoShell({ slug: slugProp, bundle }: DemoShellProps) {
|
||||
const params = useParams();
|
||||
const slug = slugProp ?? params['slug'] ?? '';
|
||||
const [state, setState] = useState<LoadState<T>>(
|
||||
const [state, setState] = useState<LoadState>(
|
||||
bundle ? { status: 'ready', bundle } : { status: 'loading' },
|
||||
);
|
||||
|
||||
@@ -187,7 +116,7 @@ export function DemoShell<T = unknown>({ slug: slugProp, bundle }: DemoShellProp
|
||||
setState({ status: 'loading' });
|
||||
loadBundle(slug)
|
||||
.then((loaded) => {
|
||||
if (live) setState({ status: 'ready', bundle: loaded as DemoBundle<T> });
|
||||
if (live) setState({ status: 'ready', bundle: loaded });
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (!live) return;
|
||||
@@ -204,36 +133,36 @@ export function DemoShell<T = unknown>({ slug: slugProp, bundle }: DemoShellProp
|
||||
if (state.status === 'loading') return <ShellSkeleton />;
|
||||
if (state.status === 'error') {
|
||||
return (
|
||||
<div role="alert" className="card mx-auto my-16 max-w-xl p-6">
|
||||
<h1 className="text-lg font-semibold">That demo is not here</h1>
|
||||
<p className="mt-2 text-sm leading-relaxed text-muted">{state.message}</p>
|
||||
<a
|
||||
href="/"
|
||||
className="tap mt-4 inline-flex items-center rounded-lg border border-border px-4 text-sm font-medium hover:bg-surface-2"
|
||||
>
|
||||
Back to the gallery
|
||||
</a>
|
||||
</div>
|
||||
<main className={cn(st.shell, 'py-16')}>
|
||||
<div role="alert" className="card max-w-xl p-6">
|
||||
<h1 className={st.h2}>That demo is not here</h1>
|
||||
<p className={cn(st.prose, 'mt-3')}>{state.message}</p>
|
||||
<a href="/gallery" className={cn(st.btnSecondary, 'mt-6')}>
|
||||
Back to the demos
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// A second boundary inside the route's own: this one is keyed to the demo
|
||||
// so a crash names it, and resetting re-renders the surfaces rather than
|
||||
// re-navigating.
|
||||
<DemoErrorBoundary demoTitle={state.bundle.demo.meta.title}>
|
||||
<DemoBody bundle={state.bundle} />
|
||||
</DemoErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
function DemoBody({ bundle }: { bundle: DemoBundle }) {
|
||||
const { demo, runs, episodes } = bundle;
|
||||
const isDesktop = useIsDesktop();
|
||||
|
||||
// The shell writes `?step=` on every advance, including during playback. It
|
||||
// is `@/lib/url-state`'s job to REPLACE rather than push for these — pushing
|
||||
// would turn a six-step run into six back-button presses.
|
||||
const [runParam, setRunParam] = useUrlState('run', '');
|
||||
const [stepParam, setStepParam] = useUrlState('step', '0');
|
||||
const [tabParam, setTabParam] = useUrlState('tab', '');
|
||||
const [runParam, setRunParam] = useRunParam();
|
||||
const [stepParam, setStepParam] = useStepParam();
|
||||
const [tabParam, setTabParam] = useTabParam(DEFAULT_DETAIL_TAB);
|
||||
const [speedParam, setSpeedParam] = useSpeedParam();
|
||||
|
||||
const run = useMemo(
|
||||
() => runs.find((candidate) => candidate.id === runParam) ?? runs[0],
|
||||
@@ -241,34 +170,34 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
);
|
||||
const episode = run ? episodes[run.id] : undefined;
|
||||
|
||||
const steps: DemoStep<T>[] = useMemo(
|
||||
() => (episode ? (demo.adapt(episode) as DemoStep<T>[]) : []),
|
||||
const steps = useMemo<DemoStep<unknown>[]>(
|
||||
() => (episode ? demo.adapt(episode) : []),
|
||||
[demo, episode],
|
||||
);
|
||||
|
||||
const step = clampIndex(Number(stepParam), steps.length);
|
||||
const setStep = useCallback(
|
||||
(next: number) => setStepParam(String(clampIndex(next, steps.length))),
|
||||
[setStepParam, steps.length],
|
||||
);
|
||||
const player = usePlayer(steps, {
|
||||
initialIndex: stepParam,
|
||||
initialSpeed: speedParam,
|
||||
onIndexChange: setStepParam,
|
||||
});
|
||||
|
||||
const playback = useTracePlayback({ stepCount: steps.length, step, onStepChange: setStep });
|
||||
const current = steps[step];
|
||||
// The URL is the other writer of this state — Back, a pasted permalink, the
|
||||
// run switcher. The player is the source of truth while it is running, so it
|
||||
// only follows the URL when the two have actually diverged.
|
||||
const { seek } = player;
|
||||
useEffect(() => {
|
||||
if (stepParam !== player.index) seek(stepParam);
|
||||
// Intentionally keyed on the URL only: including `player.index` here would
|
||||
// re-run the effect on the player's own advance and fight it.
|
||||
}, [stepParam, seek]);
|
||||
|
||||
const hasBeat = (surface: StoryBeat['surface']) =>
|
||||
demo.narrative.beats.some((beat) => beat.surface === surface);
|
||||
const timelineInSplit = !hasBeat('scrubber');
|
||||
const extras = demo.tabs ?? [];
|
||||
const extrasInCustom = hasBeat('custom');
|
||||
const extrasInCustomBeat = hasBeat('custom');
|
||||
|
||||
const tabIds = useMemo(() => {
|
||||
const ids = isDesktop ? ['reasoning', 'call'] : ['call'];
|
||||
if (!extrasInCustom) ids.push(...extras.map((tab) => tab.id));
|
||||
return ids;
|
||||
}, [isDesktop, extras, extrasInCustom]);
|
||||
const activeTab = tabIds.includes(tabParam) ? tabParam : (tabIds[0] ?? 'call');
|
||||
|
||||
const arms: RewardArm[] = useMemo(
|
||||
const arms = useMemo<RewardArm[]>(
|
||||
() =>
|
||||
runs.map((candidate) => {
|
||||
const armEpisode = episodes[candidate.id];
|
||||
@@ -283,15 +212,6 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
[runs, episodes],
|
||||
);
|
||||
|
||||
const totals = useMemo(
|
||||
() =>
|
||||
arms.map((arm) => ({
|
||||
label: arm.label,
|
||||
total: scoreReward(demo.reward, arm.values).total,
|
||||
})),
|
||||
[arms, demo.reward],
|
||||
);
|
||||
|
||||
const blindPair = useMemo(() => {
|
||||
for (let i = 0; i < runs.length; i += 1) {
|
||||
for (let j = i + 1; j < runs.length; j += 1) {
|
||||
@@ -304,75 +224,113 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
return { left, right, leftEpisode, rightEpisode };
|
||||
}
|
||||
}
|
||||
// Two runs on different seeds are two different puzzles; showing them side
|
||||
// by side would be a comparison of luck.
|
||||
return null;
|
||||
}, [runs, episodes]);
|
||||
|
||||
if (!run || !episode || steps.length === 0) {
|
||||
return (
|
||||
<div className="mx-auto max-w-canvas px-4 py-16">
|
||||
<h1 className="text-lg font-semibold">{demo.meta.title}</h1>
|
||||
<p className="mt-2 max-w-prose text-sm leading-relaxed text-muted">
|
||||
<main className={cn(st.shell, 'py-16')}>
|
||||
<h1 className={st.h2}>{demo.meta.title}</h1>
|
||||
<p className={cn(st.prose, 'mt-3 max-w-prose')}>
|
||||
No recorded run is available for this demo yet. The environment and its grader are in
|
||||
the repository; the traces are produced by the eval command on the demo's provenance
|
||||
card.
|
||||
the repository; the traces come from the eval command on the provenance card.
|
||||
</p>
|
||||
<EnvAnatomy anatomy={demo.anatomy} rewardLine={demo.meta.rewardLine} className="mt-8" />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const Surface = demo.Surface as unknown as React.ComponentType<{
|
||||
state: T;
|
||||
compact?: boolean;
|
||||
}>;
|
||||
const Surface = demo.Surface as ComponentType<{ state: unknown; compact?: boolean }>;
|
||||
const current = steps[player.index];
|
||||
const lastStep = steps[steps.length - 1];
|
||||
|
||||
const heroStats: Stat[] = [
|
||||
{
|
||||
label: 'Outcome',
|
||||
value: episode.outcome,
|
||||
tone: episode.outcome === 'solved' ? 'positive' : 'warning',
|
||||
hint: episode.truncated ? 'Truncated before a terminal state' : undefined,
|
||||
...(episode.truncated ? { hint: 'Truncated before a terminal state' } : {}),
|
||||
},
|
||||
{
|
||||
label: 'Total reward',
|
||||
value: formatOrDash(scoreReward(demo.reward, episode.rewards).total),
|
||||
value: formatOrDash(rewardTotal(episode.rewards, demo.reward.components)),
|
||||
tone: 'brand',
|
||||
hint: 'Shipped weights',
|
||||
},
|
||||
{ label: 'Steps', value: steps.length, hint: 'Model calls in this run' },
|
||||
{ label: 'Seed', value: episode.seed, hint: 'Same seed reproduces this board' },
|
||||
{ label: 'Seed', value: episode.seed, hint: 'The same seed reproduces this board' },
|
||||
];
|
||||
|
||||
const reasoningPanel = (
|
||||
<ReasoningPanel
|
||||
reasoning={current?.reasoning ?? null}
|
||||
durationMs={current?.call?.durationMs ?? null}
|
||||
playing={playback.playing}
|
||||
speed={playback.speed}
|
||||
stepIndex={step}
|
||||
/>
|
||||
);
|
||||
|
||||
const timeline = (
|
||||
<StepTimeline
|
||||
steps={steps}
|
||||
current={step}
|
||||
current={player.index}
|
||||
onSelect={(next) => {
|
||||
playback.setPlaying(false);
|
||||
setStep(next);
|
||||
player.pause();
|
||||
player.seek(next);
|
||||
}}
|
||||
Surface={Surface}
|
||||
onTogglePlay={playback.toggle}
|
||||
onTogglePlay={player.toggle}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderSurface = (beat: StoryBeat) => {
|
||||
const detailTabs: { id: string; label: string; content: ReactNode }[] = [
|
||||
{
|
||||
id: 'reasoning',
|
||||
label: 'Reasoning',
|
||||
content: isDesktop ? (
|
||||
<ReasoningPanel
|
||||
reasoning={current?.reasoning ?? null}
|
||||
durationMs={current?.call?.durationMs ?? null}
|
||||
playing={player.isPlaying}
|
||||
speed={player.speed}
|
||||
stepIndex={player.index}
|
||||
/>
|
||||
) : (
|
||||
// Under `lg` there is no column for this, and putting it below the
|
||||
// board means watching the run with the thinking off-screen. The sheet
|
||||
// is mounted only here, so vaul never locks body scroll on desktop.
|
||||
<ReasoningDrawer
|
||||
reasoning={current?.reasoning ?? null}
|
||||
durationMs={current?.call?.durationMs ?? null}
|
||||
playing={player.isPlaying}
|
||||
speed={player.speed}
|
||||
stepIndex={player.index}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'call',
|
||||
label: 'Model call',
|
||||
content: (
|
||||
<div className="space-y-3">
|
||||
<ModelCallPanel call={current?.call ?? null} />
|
||||
{current?.reply ? (
|
||||
<div className="card p-3">
|
||||
<h3 className="text-sm font-semibold">Reply</h3>
|
||||
<p className="mt-1 whitespace-pre-wrap font-mono text-xs leading-relaxed">
|
||||
{current.reply}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...(extrasInCustomBeat
|
||||
? []
|
||||
: extras.map((tab) => ({ id: tab.id, label: tab.label, content: <tab.Component /> }))),
|
||||
];
|
||||
const activeTab = detailTabs.some((tab) => tab.id === tabParam) ? tabParam : DEFAULT_DETAIL_TAB;
|
||||
|
||||
const renderSurface = (beat: StoryBeat): ReactNode => {
|
||||
switch (beat.surface) {
|
||||
case 'hero':
|
||||
return (
|
||||
<div className="grid gap-4 lg:grid-cols-[auto_minmax(0,1fr)] lg:items-start">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-[auto_minmax(0,1fr)] lg:items-start">
|
||||
<div className="card w-fit p-4">
|
||||
<Surface state={(steps[steps.length - 1] as DemoStep<T>).state} />
|
||||
{lastStep ? <Surface state={lastStep.state} /> : null}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<StatStrip stats={heroStats} />
|
||||
@@ -382,9 +340,7 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
{...(run.intervention ? { intervention: run.intervention } : {})}
|
||||
className="ml-0 w-fit"
|
||||
/>
|
||||
<p className="max-w-prose text-sm leading-relaxed text-muted">
|
||||
{demo.narrative.thesis}
|
||||
</p>
|
||||
<p className={cn(st.prose, 'max-w-prose')}>{demo.narrative.thesis}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -400,90 +356,61 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
runs={runs}
|
||||
activeId={run.id}
|
||||
onSelect={(id) => {
|
||||
playback.setPlaying(false);
|
||||
player.pause();
|
||||
// The run param setter also zeroes `step`: step 6 of a
|
||||
// nine-turn rollout is not step 6 of a three-turn one.
|
||||
setRunParam(id);
|
||||
setStepParam('0');
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TracePlayer
|
||||
playing={playback.playing}
|
||||
onPlayingChange={playback.setPlaying}
|
||||
speed={playback.speed}
|
||||
onSpeedChange={playback.setSpeed}
|
||||
onRestart={playback.restart}
|
||||
step={step}
|
||||
playing={player.isPlaying}
|
||||
onPlayingChange={(next) => (next ? player.play() : player.pause())}
|
||||
speed={player.speed}
|
||||
onSpeedChange={(next) => {
|
||||
player.setSpeed(next);
|
||||
setSpeedParam(next);
|
||||
// `instant` is a destination, not a rate. The player only
|
||||
// consumes it while running, so choosing it from a paused
|
||||
// transport has to start the run — otherwise the button
|
||||
// visibly does nothing, which reads as broken.
|
||||
if (next === 'instant') player.play();
|
||||
}}
|
||||
onRestart={player.restart}
|
||||
step={player.index}
|
||||
stepCount={steps.length}
|
||||
onStepChange={(next) => {
|
||||
playback.setPlaying(false);
|
||||
setStep(next);
|
||||
player.pause();
|
||||
player.seek(next);
|
||||
}}
|
||||
progress={player.progress}
|
||||
timingIsReal={player.timingIsReal}
|
||||
model={run.model}
|
||||
capturedAt={run.capturedAt}
|
||||
{...(run.intervention ? { intervention: run.intervention } : {})}
|
||||
/>
|
||||
|
||||
<div className="grid gap-3 lg:grid-cols-[auto_minmax(0,1fr)] lg:items-start">
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-[auto_minmax(0,1fr)] lg:items-start">
|
||||
<div className="card w-fit p-4">
|
||||
{current ? <Surface state={current.state} /> : null}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 space-y-3">
|
||||
{!isDesktop ? (
|
||||
<ReasoningDrawer
|
||||
reasoning={current?.reasoning ?? null}
|
||||
durationMs={current?.call?.durationMs ?? null}
|
||||
playing={playback.playing}
|
||||
speed={playback.speed}
|
||||
stepIndex={step}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Tabs.Root value={activeTab} onValueChange={setTabParam}>
|
||||
<Tabs.List
|
||||
aria-label="Details for this step"
|
||||
className="flex gap-1 overflow-x-auto rounded-lg bg-surface-2 p-1"
|
||||
>
|
||||
{isDesktop ? <TabTrigger value="reasoning">Reasoning</TabTrigger> : null}
|
||||
<TabTrigger value="call">Model call</TabTrigger>
|
||||
{!extrasInCustom
|
||||
? extras.map((tab) => (
|
||||
<TabTrigger key={tab.id} value={tab.id}>
|
||||
{tab.label}
|
||||
</TabTrigger>
|
||||
))
|
||||
: null}
|
||||
</Tabs.List>
|
||||
|
||||
{isDesktop ? (
|
||||
<Tabs.Content value="reasoning" className="mt-3 focus-visible:outline-none">
|
||||
{reasoningPanel}
|
||||
</Tabs.Content>
|
||||
) : null}
|
||||
<Tabs.Content value="call" className="mt-3 focus-visible:outline-none">
|
||||
<ModelCallPanel call={current?.call ?? null} />
|
||||
{current?.reply ? (
|
||||
<div className="card mt-3 p-3">
|
||||
<h3 className="text-sm font-semibold">Reply</h3>
|
||||
<p className="mt-1 whitespace-pre-wrap font-mono text-xs leading-relaxed">
|
||||
{current.reply}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</Tabs.Content>
|
||||
{!extrasInCustom
|
||||
? extras.map((tab) => (
|
||||
<Tabs.Content
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className="mt-3 focus-visible:outline-none"
|
||||
>
|
||||
<tab.Component />
|
||||
</Tabs.Content>
|
||||
))
|
||||
: null}
|
||||
</Tabs.Root>
|
||||
<div className="min-w-0">
|
||||
<Tabs value={activeTab} onValueChange={setTabParam}>
|
||||
<TabsList aria-label="Details for this step" className="w-full overflow-x-auto">
|
||||
{detailTabs.map((tab) => (
|
||||
<TabsTrigger key={tab.id} value={tab.id}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
{detailTabs.map((tab) => (
|
||||
<TabsContent key={tab.id} value={tab.id}>
|
||||
{tab.content}
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -496,6 +423,12 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{timeline}
|
||||
{player.timingIsReal ? null : (
|
||||
<p className="text-xs text-muted">
|
||||
Some steps in this run carried no recorded latency, so their dwell on the timeline
|
||||
is the player's fallback rather than a measurement.
|
||||
</p>
|
||||
)}
|
||||
<SlotRegion id="below-timeline" />
|
||||
</div>
|
||||
);
|
||||
@@ -514,23 +447,24 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
);
|
||||
|
||||
case 'metric': {
|
||||
const currentTotal = scoreReward(demo.reward, episode.rewards).total;
|
||||
const first = totals[0];
|
||||
const series = totals
|
||||
.filter((entry): entry is { label: string; total: number } => entry.total !== null)
|
||||
.map((entry) => ({ x: entry.label, y: entry.total }));
|
||||
const currentTotal = rewardTotal(episode.rewards, demo.reward.components);
|
||||
const points = arms
|
||||
.map((arm) => ({ x: arm.label, y: rewardTotal(arm.values, demo.reward.components) }))
|
||||
.filter((point): point is { x: string; y: number } => point.y !== null);
|
||||
const baselineArm = arms[0];
|
||||
const baselineValue = baselineArm
|
||||
? rewardTotal(baselineArm.values, demo.reward.components)
|
||||
: null;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<MetricMover
|
||||
label={`Total reward — ${run.label}`}
|
||||
value={currentTotal ?? 0}
|
||||
{...(first && first.total !== null && first.label !== run.label
|
||||
? { baseline: { value: first.total, label: first.label } }
|
||||
{...(baselineArm && baselineValue !== null && arms.length > 1
|
||||
? { baseline: { value: baselineValue, label: baselineArm.label } }
|
||||
: {})}
|
||||
series={series}
|
||||
caption={
|
||||
'Every point is a recorded run scored by the same grader. Nothing here is a projection.'
|
||||
}
|
||||
series={points}
|
||||
caption="Every point is a recorded run scored by the same grader. Nothing here is a projection."
|
||||
/>
|
||||
{blindPair ? (
|
||||
<BlindCompare
|
||||
@@ -543,8 +477,8 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
...(blindPair.left.intervention
|
||||
? { intervention: blindPair.left.intervention }
|
||||
: {}),
|
||||
steps: demo.adapt(blindPair.leftEpisode) as DemoStep<T>[],
|
||||
total: scoreReward(demo.reward, blindPair.leftEpisode.rewards).total,
|
||||
steps: demo.adapt(blindPair.leftEpisode),
|
||||
total: rewardTotal(blindPair.leftEpisode.rewards, demo.reward.components),
|
||||
}}
|
||||
b={{
|
||||
runId: blindPair.right.id,
|
||||
@@ -553,8 +487,8 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
...(blindPair.right.intervention
|
||||
? { intervention: blindPair.right.intervention }
|
||||
: {}),
|
||||
steps: demo.adapt(blindPair.rightEpisode) as DemoStep<T>[],
|
||||
total: scoreReward(demo.reward, blindPair.rightEpisode.rewards).total,
|
||||
steps: demo.adapt(blindPair.rightEpisode),
|
||||
total: rewardTotal(blindPair.rightEpisode.rewards, demo.reward.components),
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
@@ -564,7 +498,7 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
|
||||
case 'receipt':
|
||||
return (
|
||||
<div className="grid gap-4 lg:grid-cols-2 lg:items-start">
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:items-start">
|
||||
<ProvenanceCard provenance={demo.provenance} run={run} />
|
||||
<CodeReceipt
|
||||
code={demo.reward.source.code}
|
||||
@@ -596,10 +530,10 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-canvas px-4 pb-16" style={{ paddingBottom: 'var(--safe-bottom)' }}>
|
||||
<main className={cn(st.shell, 'pb-16')}>
|
||||
{/*
|
||||
The page's ONE live region. Every step change lands here and nowhere
|
||||
else: with reduced motion the tile animation is gone, so this sentence
|
||||
else: with reduced motion the board animation is gone, so this sentence
|
||||
is the only thing that tells a screen-reader user what just happened.
|
||||
*/}
|
||||
<div aria-live="polite" aria-atomic="true" className="sr-only">
|
||||
@@ -607,13 +541,9 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
</div>
|
||||
|
||||
<header className="pt-8">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-accent-fg">
|
||||
{demo.meta.vertical.replace(/-/g, ' ')} · for {demo.meta.persona}
|
||||
</p>
|
||||
<h1 className="mt-1 text-2xl font-semibold tracking-tight lg:text-3xl">
|
||||
{demo.meta.title}
|
||||
</h1>
|
||||
<p className="mt-2 max-w-prose text-base text-muted">{demo.meta.tagline}</p>
|
||||
<p className={cn(st.eyebrow, 'text-accent-fg')}>For {demo.meta.persona}</p>
|
||||
<h1 className={cn(st.h2, 'mt-1')}>{demo.meta.title}</h1>
|
||||
<p className={cn(st.lede, 'mt-2 max-w-prose')}>{demo.meta.tagline}</p>
|
||||
<p className="mt-4 max-w-prose border-l-2 border-brand pl-3 text-sm italic leading-relaxed text-fg">
|
||||
{demo.narrative.anxiety}
|
||||
</p>
|
||||
@@ -626,18 +556,7 @@ function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
</BeatSection>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabTrigger({ value, children }: { value: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<Tabs.Trigger
|
||||
value={value}
|
||||
className="tap flex-1 whitespace-nowrap rounded-md px-3 text-sm font-medium text-muted transition-colors duration-2 ease-enter data-[state=active]:bg-surface data-[state=active]:text-fg data-[state=active]:shadow-sm"
|
||||
>
|
||||
{children}
|
||||
</Tabs.Trigger>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -651,50 +570,38 @@ function RunSwitcher({
|
||||
onSelect: (id: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Recorded run"
|
||||
className="flex flex-wrap gap-1 rounded-lg bg-surface-2 p-1"
|
||||
>
|
||||
{runs.map((run) => {
|
||||
const active = run.id === activeId;
|
||||
return (
|
||||
<button
|
||||
key={run.id}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={active}
|
||||
onClick={() => onSelect(run.id)}
|
||||
className={cn(
|
||||
'tap rounded-md px-3 text-sm font-medium transition-colors duration-2 ease-enter',
|
||||
active ? 'bg-surface text-fg shadow-sm' : 'text-muted hover:text-fg',
|
||||
)}
|
||||
>
|
||||
{run.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<SegmentedControl
|
||||
label="Recorded run"
|
||||
options={runs.map((run) => ({
|
||||
value: run.id,
|
||||
label: run.label,
|
||||
...(run.intervention ? { title: run.intervention } : {}),
|
||||
}))}
|
||||
value={activeId}
|
||||
onChange={onSelect}
|
||||
className="border border-border p-1"
|
||||
optionClassName="tap px-3 text-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The loading state. Deliberately shaped like the page it becomes, and with no
|
||||
* spinner: a spinner on this site would imply a live model call, which is the
|
||||
* one thing the whole page is at pains to say is not happening.
|
||||
* The loading state. Shaped like the page it becomes, and with no spinner: a
|
||||
* spinner here would imply a live model call, which is the one thing the whole
|
||||
* page is at pains to say is not happening.
|
||||
*/
|
||||
function ShellSkeleton() {
|
||||
return (
|
||||
<div className="mx-auto max-w-canvas px-4 py-10" aria-busy="true">
|
||||
<div className={cn(st.shell, 'py-10')} aria-busy="true">
|
||||
<p className="sr-only">Loading the recorded run.</p>
|
||||
<div className="h-8 w-64 rounded-lg bg-surface-2" />
|
||||
<div className="mt-3 h-4 w-96 max-w-full rounded-lg bg-surface-2" />
|
||||
<div className="mt-10 grid gap-3 lg:grid-cols-4">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="mt-3 h-4 w-full max-w-md" />
|
||||
<div className="mt-10 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{[0, 1, 2, 3].map((index) => (
|
||||
<div key={index} className="h-28 rounded-xl bg-surface-2" />
|
||||
<Skeleton key={index} className="h-28" />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 h-64 rounded-xl bg-surface-2" />
|
||||
<Skeleton className="mt-6 h-64" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user