Wordle module, cross-language tests, and the deploy path
The browser engine is a port of the Python one and CI proves it: all 21.2M (guess, answer) pairs hashed on both sides to the same SHA-256. Six TS tests, including the duplicate-letter table and the twelve pinned seed vectors that keep ?seed= permalinks pointing at the same word the recording used. Word lists are split by how they are used. answers.json is inlined because the board needs it before first paint to turn a seed into a word, and a fetch there means a visibly empty board on a cold cache. guesses.json is fetched, because it is three times larger and only needed the first time somebody presses Enter; until it lands, validation falls back to the answer list, which accepts strictly fewer words. The failure mode is 'your real word was briefly rejected', not 'a non-word was accepted' — the right way round. The solver runs in a worker constructed from a same-origin module URL, never Vite's ?worker&inline: that yields a blob:, and production CSP has no worker-src, so it falls back to default-src 'self' and the worker is blocked with no console error. It would fail in production only. deploy.sh smoke-tests the real public hostname from the deploying machine and fails on a body under 1 kB, because the bind bug's signature is a valid certificate over an empty 200 and a local --resolve check passes anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
@@ -0,0 +1,700 @@
|
||||
import { useCallback, useEffect, useMemo, useState } 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 { cn } from '@/lib/utils';
|
||||
import { BeatSection } from './BeatSection';
|
||||
import { BlindCompare } from './BlindCompare';
|
||||
import { CodeReceipt } from './CodeReceipt';
|
||||
import { DemoErrorBoundary } from './DemoErrorBoundary';
|
||||
import { EnvAnatomy } from './EnvAnatomy';
|
||||
import { LimitsCallout } from './LimitsCallout';
|
||||
import { MetricMover } from './MetricMover';
|
||||
import { ModelCallPanel } from './ModelCallPanel';
|
||||
import { ProvenanceCard } from './ProvenanceCard';
|
||||
import { ReasoningDrawer } from './ReasoningDrawer';
|
||||
import { ReasoningPanel } from './ReasoningPanel';
|
||||
import { RewardBreakdown } from './RewardBreakdown';
|
||||
import { RewardEditor } from './RewardEditor';
|
||||
import type { RewardArm } from './RewardEditor';
|
||||
import { SlotRegion } from './SlotRegion';
|
||||
import { StatStrip } from './StatStrip';
|
||||
import type { Stat } from './StatStrip';
|
||||
import { StepTimeline } from './StepTimeline';
|
||||
import { RecordedBadge, TracePlayer, useTracePlayback } from './TracePlayer';
|
||||
import { VerifyBadge } from './VerifyBadge';
|
||||
import { clampIndex, formatOrDash, useIsDesktop } from './format';
|
||||
import { scoreReward } from './reward-math';
|
||||
import { mockDemo, mockEpisodes, mockRuns } from './mock';
|
||||
|
||||
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}');
|
||||
|
||||
export interface DemoBundle<T = unknown> {
|
||||
demo: DemoModule<T>;
|
||||
runs: RunRef[];
|
||||
episodes: Record<string, DemoEpisode>;
|
||||
}
|
||||
|
||||
type LoadState<T> =
|
||||
| { status: 'loading' }
|
||||
| { status: 'ready'; bundle: DemoBundle<T> }
|
||||
| { 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.
|
||||
*/
|
||||
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 };
|
||||
}
|
||||
|
||||
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 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];
|
||||
}
|
||||
return { demo, runs: runs.filter((run) => episodes[run.id] !== undefined), episodes };
|
||||
}
|
||||
|
||||
export interface DemoShellProps<T = unknown> {
|
||||
/** Overrides the route param. Useful for previews and tests. */
|
||||
slug?: string;
|
||||
/** Skips loading entirely when the caller already has the bundle. */
|
||||
bundle?: DemoBundle<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The route component every demo is rendered through.
|
||||
*
|
||||
* 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`.
|
||||
*/
|
||||
export function DemoShell<T = unknown>({ slug: slugProp, bundle }: DemoShellProps<T>) {
|
||||
const params = useParams();
|
||||
const slug = slugProp ?? params['slug'] ?? '';
|
||||
const [state, setState] = useState<LoadState<T>>(
|
||||
bundle ? { status: 'ready', bundle } : { status: 'loading' },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (bundle) {
|
||||
setState({ status: 'ready', bundle });
|
||||
return;
|
||||
}
|
||||
let live = true;
|
||||
setState({ status: 'loading' });
|
||||
loadBundle(slug)
|
||||
.then((loaded) => {
|
||||
if (live) setState({ status: 'ready', bundle: loaded as DemoBundle<T> });
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (!live) return;
|
||||
setState({
|
||||
status: 'error',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, [slug, bundle]);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DemoErrorBoundary demoTitle={state.bundle.demo.meta.title}>
|
||||
<DemoBody bundle={state.bundle} />
|
||||
</DemoErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
function DemoBody<T>({ bundle }: { bundle: DemoBundle<T> }) {
|
||||
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 run = useMemo(
|
||||
() => runs.find((candidate) => candidate.id === runParam) ?? runs[0],
|
||||
[runs, runParam],
|
||||
);
|
||||
const episode = run ? episodes[run.id] : undefined;
|
||||
|
||||
const steps: DemoStep<T>[] = useMemo(
|
||||
() => (episode ? (demo.adapt(episode) as DemoStep<T>[]) : []),
|
||||
[demo, episode],
|
||||
);
|
||||
|
||||
const step = clampIndex(Number(stepParam), steps.length);
|
||||
const setStep = useCallback(
|
||||
(next: number) => setStepParam(String(clampIndex(next, steps.length))),
|
||||
[setStepParam, steps.length],
|
||||
);
|
||||
|
||||
const playback = useTracePlayback({ stepCount: steps.length, step, onStepChange: setStep });
|
||||
const current = steps[step];
|
||||
|
||||
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 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(
|
||||
() =>
|
||||
runs.map((candidate) => {
|
||||
const armEpisode = episodes[candidate.id];
|
||||
const arm: RewardArm = {
|
||||
id: candidate.id,
|
||||
label: candidate.label,
|
||||
values: armEpisode?.rewards ?? {},
|
||||
};
|
||||
if (candidate.intervention) arm.note = candidate.intervention;
|
||||
return arm;
|
||||
}),
|
||||
[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) {
|
||||
const left = runs[i];
|
||||
const right = runs[j];
|
||||
if (!left || !right || left.seed !== right.seed) continue;
|
||||
const leftEpisode = episodes[left.id];
|
||||
const rightEpisode = episodes[right.id];
|
||||
if (!leftEpisode || !rightEpisode) continue;
|
||||
return { left, right, leftEpisode, rightEpisode };
|
||||
}
|
||||
}
|
||||
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">
|
||||
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.
|
||||
</p>
|
||||
<EnvAnatomy anatomy={demo.anatomy} rewardLine={demo.meta.rewardLine} className="mt-8" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Surface = demo.Surface as unknown as React.ComponentType<{
|
||||
state: T;
|
||||
compact?: boolean;
|
||||
}>;
|
||||
|
||||
const heroStats: Stat[] = [
|
||||
{
|
||||
label: 'Outcome',
|
||||
value: episode.outcome,
|
||||
tone: episode.outcome === 'solved' ? 'positive' : 'warning',
|
||||
hint: episode.truncated ? 'Truncated before a terminal state' : undefined,
|
||||
},
|
||||
{
|
||||
label: 'Total reward',
|
||||
value: formatOrDash(scoreReward(demo.reward, episode.rewards).total),
|
||||
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' },
|
||||
];
|
||||
|
||||
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}
|
||||
onSelect={(next) => {
|
||||
playback.setPlaying(false);
|
||||
setStep(next);
|
||||
}}
|
||||
Surface={Surface}
|
||||
onTogglePlay={playback.toggle}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderSurface = (beat: StoryBeat) => {
|
||||
switch (beat.surface) {
|
||||
case 'hero':
|
||||
return (
|
||||
<div className="grid 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} />
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<StatStrip stats={heroStats} />
|
||||
<RecordedBadge
|
||||
model={run.model}
|
||||
capturedAt={run.capturedAt}
|
||||
{...(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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'anatomy':
|
||||
return <EnvAnatomy anatomy={demo.anatomy} rewardLine={demo.meta.rewardLine} />;
|
||||
|
||||
case 'split-play':
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{runs.length > 1 ? (
|
||||
<RunSwitcher
|
||||
runs={runs}
|
||||
activeId={run.id}
|
||||
onSelect={(id) => {
|
||||
playback.setPlaying(false);
|
||||
setRunParam(id);
|
||||
setStepParam('0');
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TracePlayer
|
||||
playing={playback.playing}
|
||||
onPlayingChange={playback.setPlaying}
|
||||
speed={playback.speed}
|
||||
onSpeedChange={playback.setSpeed}
|
||||
onRestart={playback.restart}
|
||||
step={step}
|
||||
stepCount={steps.length}
|
||||
onStepChange={(next) => {
|
||||
playback.setPlaying(false);
|
||||
setStep(next);
|
||||
}}
|
||||
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="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>
|
||||
</div>
|
||||
|
||||
{timelineInSplit ? timeline : null}
|
||||
<SlotRegion id="below-board" />
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'scrubber':
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{timeline}
|
||||
<SlotRegion id="below-timeline" />
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'reward-editor':
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<RewardBreakdown
|
||||
spec={demo.reward}
|
||||
values={episode.rewards}
|
||||
{...(episode.metrics ? { metrics: episode.metrics } : {})}
|
||||
/>
|
||||
<VerifyBadge demo={demo} episode={episode} />
|
||||
{arms.length > 1 ? <RewardEditor spec={demo.reward} arms={arms} /> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
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 }));
|
||||
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 } }
|
||||
: {})}
|
||||
series={series}
|
||||
caption={
|
||||
'Every point is a recorded run scored by the same grader. Nothing here is a projection.'
|
||||
}
|
||||
/>
|
||||
{blindPair ? (
|
||||
<BlindCompare
|
||||
seed={blindPair.left.seed}
|
||||
Surface={Surface}
|
||||
a={{
|
||||
runId: blindPair.left.id,
|
||||
label: blindPair.left.label,
|
||||
model: blindPair.left.model,
|
||||
...(blindPair.left.intervention
|
||||
? { intervention: blindPair.left.intervention }
|
||||
: {}),
|
||||
steps: demo.adapt(blindPair.leftEpisode) as DemoStep<T>[],
|
||||
total: scoreReward(demo.reward, blindPair.leftEpisode.rewards).total,
|
||||
}}
|
||||
b={{
|
||||
runId: blindPair.right.id,
|
||||
label: blindPair.right.label,
|
||||
model: blindPair.right.model,
|
||||
...(blindPair.right.intervention
|
||||
? { intervention: blindPair.right.intervention }
|
||||
: {}),
|
||||
steps: demo.adapt(blindPair.rightEpisode) as DemoStep<T>[],
|
||||
total: scoreReward(demo.reward, blindPair.rightEpisode.rewards).total,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'receipt':
|
||||
return (
|
||||
<div className="grid gap-4 lg:grid-cols-2 lg:items-start">
|
||||
<ProvenanceCard provenance={demo.provenance} run={run} />
|
||||
<CodeReceipt
|
||||
code={demo.reward.source.code}
|
||||
path={demo.reward.source.path}
|
||||
{...(demo.reward.source.marker ? { marker: demo.reward.source.marker } : {})}
|
||||
href={`${REPO_BLOB}${demo.reward.source.path}`}
|
||||
/>
|
||||
<SlotRegion id="after-receipts" className="lg:col-span-2" />
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'limits':
|
||||
return <LimitsCallout limits={demo.narrative.limits} />;
|
||||
|
||||
case 'custom':
|
||||
return extras.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{extras.map((tab) => (
|
||||
<tab.Component key={tab.id} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<SlotRegion id="before-limits" />
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-canvas px-4 pb-16" style={{ paddingBottom: 'var(--safe-bottom)' }}>
|
||||
{/*
|
||||
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
|
||||
is the only thing that tells a screen-reader user what just happened.
|
||||
*/}
|
||||
<div aria-live="polite" aria-atomic="true" className="sr-only">
|
||||
{current?.announce ?? ''}
|
||||
</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="mt-4 max-w-prose border-l-2 border-brand pl-3 text-sm italic leading-relaxed text-fg">
|
||||
{demo.narrative.anxiety}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="divide-y divide-border">
|
||||
{demo.narrative.beats.map((beat, index) => (
|
||||
<BeatSection key={beat.id} beat={beat} number={index + 1}>
|
||||
{renderSurface(beat)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
function RunSwitcher({
|
||||
runs,
|
||||
activeId,
|
||||
onSelect,
|
||||
}: {
|
||||
runs: RunRef[];
|
||||
activeId: string;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function ShellSkeleton() {
|
||||
return (
|
||||
<div className="mx-auto max-w-canvas px-4 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">
|
||||
{[0, 1, 2, 3].map((index) => (
|
||||
<div key={index} className="h-28 rounded-xl bg-surface-2" />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 h-64 rounded-xl bg-surface-2" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user