Capture harness, fixture verification, CI, and the public README
The site does no live inference. Rollouts are captured once against spark-1 and replayed at their recorded wall-clock — a public demo with no auth cannot hold an API key, and a recorded run can be scrubbed, permalinked, blind-compared and verified in ways a live one cannot. What stops it being a video is that the browser re-derives every number from the recorded moves. verify_fixtures.py is the Python half of that: it replays every committed fixture through the engine and reproduces its own rewards. All 16 land at delta 0.0. A fixture that cannot be regenerated is a claim with no receipt. First real measurement, thinking off, 8 seeds: solved 0/8. The model repeats guesses it has already played, invents words (trape, slith, postt, boomy), and contradicts its own feedback — consistency 0.09 to 0.17. That is the published failure taxonomy showing up in our own data on the first run, and it is why `consistency` is a reward component rather than a footnote. A capture failure is recorded as a turn with a null reply, never dropped. A capture that silently discarded failed turns would be reporting a better model than the one that ran. CI gates both halves and four things that fail silently in production: the word lists must rebuild byte-identically, the prerendered routes must carry their own baked og tags (crawlers do not run JS, so without them every shared link previews as the homepage), no blob: URL may reach the bundle (the site's CSP has no worker-src, so it falls back to default-src 'self' and a blob worker is blocked with no error), and the conformance digest must match across languages. 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,192 @@
|
||||
import { useState } from 'react';
|
||||
import type { ComponentType } from 'react';
|
||||
import { Eye, Trophy } from 'lucide-react';
|
||||
import type { DemoStep } from '@/lib/demo-kit/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatOrDash } from './format';
|
||||
|
||||
export interface BlindCompareRun<T> {
|
||||
runId: string;
|
||||
/** The identity, revealed only after the visitor commits. */
|
||||
label: string;
|
||||
model: string;
|
||||
/** Required by the contract on an `intervened` run; shown at reveal. */
|
||||
intervention?: string;
|
||||
steps: DemoStep<T>[];
|
||||
total: number | null;
|
||||
}
|
||||
|
||||
export type BlindVote = 'A' | 'B' | 'tie';
|
||||
|
||||
export interface BlindCompareProps<T> {
|
||||
/** Both runs must be the same seed or the comparison is meaningless. */
|
||||
seed: number;
|
||||
a: BlindCompareRun<T>;
|
||||
b: BlindCompareRun<T>;
|
||||
Surface: ComponentType<{ state: T; compact?: boolean }>;
|
||||
question?: string;
|
||||
onVote?: (vote: BlindVote) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Two runs on the same seed, unlabelled, until you commit.
|
||||
*
|
||||
* The point is not the vote. The point is that the visitor forms an opinion
|
||||
* from the behaviour BEFORE they learn which one had the better prompt, the
|
||||
* bigger model or the training run — because once they know, they cannot
|
||||
* unknow it, and every "obviously the trained one looks better" is worthless
|
||||
* after the fact.
|
||||
*
|
||||
* There is a "reveal without voting" escape on purpose: a visitor who does not
|
||||
* want to play should not be held hostage by a modal-shaped page.
|
||||
*/
|
||||
export function BlindCompare<T>({
|
||||
seed,
|
||||
a,
|
||||
b,
|
||||
Surface,
|
||||
question = 'Which agent would you rather have running this?',
|
||||
onVote,
|
||||
className,
|
||||
}: BlindCompareProps<T>) {
|
||||
const [vote, setVote] = useState<BlindVote | null>(null);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
|
||||
const commit = (choice: BlindVote) => {
|
||||
setVote(choice);
|
||||
setRevealed(true);
|
||||
onVote?.(choice);
|
||||
};
|
||||
|
||||
const winner: 'A' | 'B' | 'tie' =
|
||||
a.total === null || b.total === null
|
||||
? 'tie'
|
||||
: a.total > b.total
|
||||
? 'A'
|
||||
: b.total > a.total
|
||||
? 'B'
|
||||
: 'tie';
|
||||
|
||||
const sides: { id: 'A' | 'B'; run: BlindCompareRun<T> }[] = [
|
||||
{ id: 'A', run: a },
|
||||
{ id: 'B', run: b },
|
||||
];
|
||||
|
||||
return (
|
||||
<section aria-label="Blind comparison" className={cn('space-y-3', className)}>
|
||||
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<h3 className="text-sm font-semibold">{question}</h3>
|
||||
<p className="nums text-xs text-muted">Same puzzle, same seed ({seed}).</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{sides.map(({ id, run }) => {
|
||||
const last = run.steps[run.steps.length - 1];
|
||||
const picked = vote === id;
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={cn(
|
||||
'card flex flex-col gap-3 p-3 transition-colors duration-2 ease-enter',
|
||||
picked && 'border-brand',
|
||||
revealed && winner === id && 'border-positive',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4 className="text-sm font-semibold">Agent {id}</h4>
|
||||
<span className="nums text-xs text-muted">{run.steps.length} steps</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-surface-2 p-3">
|
||||
{last ? (
|
||||
<Surface state={last.state} />
|
||||
) : (
|
||||
<p className="text-sm text-muted">This run recorded no steps.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!revealed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => commit(id)}
|
||||
className="tap w-full rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors duration-2 ease-enter hover:bg-primary/90"
|
||||
>
|
||||
Agent {id} is better
|
||||
</button>
|
||||
) : (
|
||||
<dl className="space-y-1 text-sm">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<dt className="text-muted">Identity</dt>
|
||||
<dd className="text-right font-medium">{run.label}</dd>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<dt className="text-muted">Model</dt>
|
||||
<dd className="nums text-right font-mono text-xs">{run.model}</dd>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<dt className="text-muted">Intervention</dt>
|
||||
<dd className="text-right text-xs">
|
||||
{run.intervention ?? (
|
||||
<span className="text-muted">none — plain rollout</span>
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="flex items-baseline justify-between gap-2 border-t border-border pt-1">
|
||||
<dt className="text-muted">Reward</dt>
|
||||
<dd className="nums flex items-center gap-1 text-right font-mono font-semibold">
|
||||
{revealed && winner === id ? (
|
||||
<Trophy className="h-3.5 w-3.5 text-positive" aria-hidden="true" />
|
||||
) : null}
|
||||
{formatOrDash(run.total)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{!revealed ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => commit('tie')}
|
||||
className="tap rounded-lg border border-border px-3 text-sm font-medium transition-colors duration-2 ease-enter hover:bg-surface-2"
|
||||
>
|
||||
Too close to call
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRevealed(true)}
|
||||
className="tap inline-flex items-center gap-1.5 rounded-lg px-3 text-sm font-medium text-muted transition-colors duration-2 ease-enter hover:text-fg"
|
||||
>
|
||||
<Eye className="h-4 w-4" aria-hidden="true" />
|
||||
Just show me
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p role="status" className="card bg-surface-2 p-3 text-sm leading-relaxed">
|
||||
{vote === null ? (
|
||||
<>Revealed without a vote. </>
|
||||
) : vote === winner ? (
|
||||
<>
|
||||
<span className="font-semibold text-positive">You picked the higher-scoring run.</span>{' '}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-semibold text-warning">
|
||||
You picked the lower-scoring run.
|
||||
</span>{' '}
|
||||
</>
|
||||
)}
|
||||
The environment scored these two with the same grader, on the same seed. The difference
|
||||
between them is stated above — and if it is a prompt change rather than a training run,
|
||||
it says so, because a prompt change presented as a training result is the oldest trick
|
||||
in this business.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ExternalLink } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface CodeReceiptProps {
|
||||
/** The file's text, imported with `?raw` so it cannot drift from the source. */
|
||||
code: string;
|
||||
/** Repo-relative path, shown as the receipt's header. */
|
||||
path: string;
|
||||
/**
|
||||
* A literal string that appears in the source and marks the interesting part.
|
||||
* See `resolveMarkedRange` for the two conventions it supports.
|
||||
*/
|
||||
marker?: string;
|
||||
/** Link to the whole file — GitHub, usually. */
|
||||
href?: string;
|
||||
/** Lines of context kept around the marked range while collapsed. */
|
||||
context?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface MarkedRange {
|
||||
/** 0-based, inclusive. */
|
||||
start: number;
|
||||
/** 0-based, inclusive. */
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Two marker conventions, because both exist in real repos:
|
||||
*
|
||||
* TWICE — the marker brackets a region (`# --8<-- reward` … `# --8<--`).
|
||||
* The marked range is the lines BETWEEN them; the fences themselves
|
||||
* are not interesting code.
|
||||
*
|
||||
* ONCE — the marker sits on a definition line (`def compute_reward`). The
|
||||
* marked range is that line plus its indented body, which is what
|
||||
* you actually meant. Blank lines inside the body are kept; trailing
|
||||
* blank lines are not, or the highlight runs on past the function.
|
||||
*
|
||||
* A marker that matches nothing returns null and the whole file renders. That
|
||||
* is the right failure: a stale marker must not hide the source.
|
||||
*/
|
||||
export function resolveMarkedRange(lines: string[], marker?: string): MarkedRange | null {
|
||||
if (!marker) return null;
|
||||
const hits: number[] = [];
|
||||
lines.forEach((line, index) => {
|
||||
if (line.includes(marker)) hits.push(index);
|
||||
});
|
||||
|
||||
const first = hits[0];
|
||||
if (first === undefined) return null;
|
||||
|
||||
if (hits.length >= 2) {
|
||||
const last = hits[hits.length - 1] as number;
|
||||
return last - first > 1 ? { start: first + 1, end: last - 1 } : { start: first, end: last };
|
||||
}
|
||||
|
||||
const anchor = lines[first] ?? '';
|
||||
const indent = anchor.length - anchor.trimStart().length;
|
||||
let end = first;
|
||||
for (let i = first + 1; i < lines.length; i += 1) {
|
||||
const line = lines[i] ?? '';
|
||||
if (line.trim() === '') continue;
|
||||
const lineIndent = line.length - line.trimStart().length;
|
||||
if (lineIndent <= indent) break;
|
||||
end = i;
|
||||
}
|
||||
return { start: first, end };
|
||||
}
|
||||
|
||||
/**
|
||||
* Source, with the part that matters marked.
|
||||
*
|
||||
* Syntax highlighting is deliberately absent: it costs a highlighter in the
|
||||
* bundle and buys nothing an exec can use, while the marked range — the thing
|
||||
* that says "this, right here, is the grader" — costs nothing and is the whole
|
||||
* reason the panel exists.
|
||||
*
|
||||
* The block scrolls horizontally inside itself. Long Python lines must never
|
||||
* make the PAGE scroll sideways; on a phone that turns every vertical swipe
|
||||
* into a fight.
|
||||
*/
|
||||
export function CodeReceipt({
|
||||
code,
|
||||
path,
|
||||
marker,
|
||||
href,
|
||||
context = 3,
|
||||
className,
|
||||
}: CodeReceiptProps) {
|
||||
const lines = useMemo(() => code.replace(/\n$/, '').split('\n'), [code]);
|
||||
const range = useMemo(() => resolveMarkedRange(lines, marker), [lines, marker]);
|
||||
const [expanded, setExpanded] = useState(range === null);
|
||||
|
||||
const shown = useMemo(() => {
|
||||
if (range === null || expanded) return { from: 0, to: lines.length - 1 };
|
||||
return {
|
||||
from: Math.max(range.start - context, 0),
|
||||
to: Math.min(range.end + context, lines.length - 1),
|
||||
};
|
||||
}, [range, expanded, context, lines.length]);
|
||||
|
||||
const visible = lines.slice(shown.from, shown.to + 1);
|
||||
const gutterWidth = `${String(lines.length).length + 1}ch`;
|
||||
|
||||
return (
|
||||
<section aria-label={`Source: ${path}`} className={cn('card overflow-hidden', className)}>
|
||||
<header className="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-border px-3 py-2">
|
||||
<h3 className="nums min-w-0 flex-1 truncate font-mono text-xs text-muted" title={path}>
|
||||
{path}
|
||||
</h3>
|
||||
{range && !expanded ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(true)}
|
||||
className="tap rounded-lg border border-border px-2.5 text-xs font-medium transition-colors duration-2 ease-enter hover:bg-surface-2"
|
||||
>
|
||||
Show all {lines.length} lines
|
||||
</button>
|
||||
) : null}
|
||||
{range && expanded ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(false)}
|
||||
className="tap rounded-lg border border-border px-2.5 text-xs font-medium transition-colors duration-2 ease-enter hover:bg-surface-2"
|
||||
>
|
||||
Collapse to the marked part
|
||||
</button>
|
||||
) : null}
|
||||
{href ? (
|
||||
<a
|
||||
href={href}
|
||||
rel="noreferrer"
|
||||
className="tap inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-2 hover:underline"
|
||||
>
|
||||
Read the whole file
|
||||
<ExternalLink className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<div className="max-h-96 overflow-auto">
|
||||
<pre className="w-max min-w-full py-2 font-mono text-xs leading-relaxed">
|
||||
<code>
|
||||
{shown.from > 0 ? (
|
||||
<span className="block px-3 text-muted" aria-hidden="true">
|
||||
…
|
||||
</span>
|
||||
) : null}
|
||||
{visible.map((line, offset) => {
|
||||
const number = shown.from + offset;
|
||||
const marked = range !== null && number >= range.start && number <= range.end;
|
||||
return (
|
||||
<span
|
||||
key={number}
|
||||
className={cn(
|
||||
'block border-l-2 pr-4',
|
||||
marked
|
||||
? 'border-brand bg-accent-subtle/60 text-fg'
|
||||
: 'border-transparent text-muted',
|
||||
)}
|
||||
>
|
||||
{/* Sticky so the line numbers survive a horizontal scroll —
|
||||
without it they slide out of view exactly when a long
|
||||
line makes you want them. */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="sticky left-0 inline-block select-none bg-surface pr-3 text-right text-muted"
|
||||
style={{ width: gutterWidth }}
|
||||
>
|
||||
{number + 1}
|
||||
</span>
|
||||
{line === '' ? ' ' : line}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{shown.to < lines.length - 1 ? (
|
||||
<span className="block px-3 text-muted" aria-hidden="true">
|
||||
…
|
||||
</span>
|
||||
) : null}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import type { DemoMeta } from '@/lib/demo-kit/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { DemoIcon } from './icons';
|
||||
|
||||
export interface DemoCardProps<T> {
|
||||
meta: DemoMeta;
|
||||
/** Defaults to the canonical demo route. */
|
||||
href?: string;
|
||||
/**
|
||||
* The demo's OWN board, drawn compact, as the thumbnail. A screenshot would
|
||||
* go stale the first time the board changes and nobody would notice; this
|
||||
* cannot, because it is the same component the demo page renders.
|
||||
*/
|
||||
Surface?: ComponentType<{ state: T; compact?: boolean }>;
|
||||
/** A representative state for the thumbnail — usually a solved board. */
|
||||
thumbnailState?: T;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DemoCard<T>({
|
||||
meta,
|
||||
href,
|
||||
Surface,
|
||||
thumbnailState,
|
||||
className,
|
||||
}: DemoCardProps<T>) {
|
||||
const to = href ?? `/demo/${meta.slug}`;
|
||||
const isSpec = meta.status === 'spec';
|
||||
const showSurface = Surface !== undefined && thumbnailState !== undefined;
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cn(
|
||||
'card group relative flex flex-col overflow-hidden transition-colors duration-2 ease-enter hover:border-brand/50',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3 p-4 pb-3">
|
||||
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-accent-subtle text-accent-fg">
|
||||
<DemoIcon name={meta.icon} className="h-5 w-5" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-base font-semibold leading-tight">
|
||||
{/* Stretched link: the whole card is the hit target, but there is
|
||||
still exactly ONE link in the accessibility tree for it. */}
|
||||
<Link
|
||||
to={to}
|
||||
className="after:absolute after:inset-0 after:content-[''] focus-visible:outline-none"
|
||||
>
|
||||
{meta.title}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="mt-0.5 text-sm leading-snug text-muted">{meta.tagline}</p>
|
||||
</div>
|
||||
{isSpec ? (
|
||||
<span className="shrink-0 rounded-md border border-border px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted">
|
||||
Spec
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{showSurface ? (
|
||||
<div className="mx-4 overflow-hidden rounded-lg bg-surface-2 p-3">
|
||||
{/* Decorative here: the title and tagline already name the demo, and
|
||||
a screen reader has no use for a board with no run behind it. */}
|
||||
<div aria-hidden="true">
|
||||
<Surface state={thumbnailState as T} compact />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<dl className="mt-3 flex flex-wrap gap-x-4 gap-y-1 px-4 text-xs">
|
||||
<div className="flex gap-1">
|
||||
<dt className="text-muted">For</dt>
|
||||
<dd className="font-medium">{meta.persona}</dd>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<dt className="text-muted">Vertical</dt>
|
||||
<dd className="font-medium capitalize">{meta.vertical.replace(/-/g, ' ')}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<p className="mt-2 px-4 pb-4 text-xs leading-relaxed text-muted">
|
||||
<span className="font-medium text-fg">Reward: </span>
|
||||
{meta.rewardLine}
|
||||
</p>
|
||||
|
||||
<p className="mt-auto flex items-center gap-1 border-t border-border px-4 py-2.5 text-sm font-medium text-accent-fg">
|
||||
{isSpec ? 'Read the specification' : 'Open the demo'}
|
||||
<ArrowRight
|
||||
className="h-4 w-4 transition-transform duration-2 ease-enter group-hover:translate-x-0.5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</p>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ArrowRight, ShieldQuestion } from 'lucide-react';
|
||||
import type { Limit } from '@/lib/demo-kit/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface LimitsCalloutProps {
|
||||
limits: Limit[];
|
||||
/**
|
||||
* Turns a demo slug into something linkable. The shell has no registry
|
||||
* dependency of its own, so the page that knows the routes supplies this.
|
||||
*/
|
||||
resolveDemo?: (slug: string) => { title: string; href: string } | undefined;
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function defaultResolve(slug: string) {
|
||||
return { title: slug, href: `/demo/${slug}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* What this demo does not teach.
|
||||
*
|
||||
* Styled as confidence, not apology. A vendor who tells you the limits of their
|
||||
* own demo before you find them is a vendor you believe about everything else
|
||||
* on the page — and every limit here names the demo that closes it, so the list
|
||||
* reads as a roadmap rather than a disclaimer.
|
||||
*/
|
||||
export function LimitsCallout({
|
||||
limits,
|
||||
resolveDemo = defaultResolve,
|
||||
title = 'What this demo does not teach',
|
||||
className,
|
||||
}: LimitsCalloutProps) {
|
||||
if (limits.length === 0) return null;
|
||||
return (
|
||||
<section aria-label={title} className={cn('card p-4', className)}>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldQuestion className="h-4 w-4 text-muted" aria-hidden="true" />
|
||||
<h3 className="text-sm font-semibold">{title}</h3>
|
||||
</div>
|
||||
<ul className="mt-3 space-y-3">
|
||||
{limits.map((limit) => {
|
||||
const target = limit.answeredBy ? resolveDemo(limit.answeredBy) : undefined;
|
||||
return (
|
||||
<li key={limit.text} className="border-l-2 border-border pl-3">
|
||||
<p className="text-pretty text-sm leading-relaxed text-fg">{limit.text}</p>
|
||||
{target ? (
|
||||
<a
|
||||
href={target.href}
|
||||
className="mt-1 inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-2 hover:underline"
|
||||
>
|
||||
Answered by {target.title}
|
||||
<ArrowRight className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
) : (
|
||||
<p className="mt-1 text-xs text-muted">
|
||||
No demo answers this one yet. It is a real gap, not a rhetorical one.
|
||||
</p>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
CartesianGrid,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceLine,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts';
|
||||
import type { TooltipProps } from 'recharts';
|
||||
import { formatNumber } from './format';
|
||||
|
||||
export interface MetricPoint {
|
||||
/** Whatever the x axis is counting: checkpoint, step, arm name. */
|
||||
x: string | number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface MetricChartProps {
|
||||
data: MetricPoint[];
|
||||
/** The rule the headline number is being compared against. */
|
||||
baseline?: { value: number; label: string };
|
||||
height: number;
|
||||
/** Off under `prefers-reduced-motion`; recharts animates on mount by default. */
|
||||
animate: boolean;
|
||||
digits?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The chart, in its own module so `React.lazy` can hold recharts out of the
|
||||
* entry chunk. Nothing else in the shell may import this file directly — an
|
||||
* ordinary import here defeats the whole arrangement and the entry chunk grows
|
||||
* by ~100 kB without anyone noticing.
|
||||
*
|
||||
* Every colour is read from the token layer at paint time rather than passed in
|
||||
* as a literal, so the chart follows the theme toggle without a re-render.
|
||||
*/
|
||||
export default function MetricChart({
|
||||
data,
|
||||
baseline,
|
||||
height,
|
||||
animate,
|
||||
digits = 3,
|
||||
}: MetricChartProps) {
|
||||
return (
|
||||
<div style={{ height }} className="w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: 4 }}>
|
||||
<CartesianGrid stroke="hsl(var(--border))" strokeDasharray="2 4" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="x"
|
||||
tick={{ fill: 'hsl(var(--muted))', fontSize: 11 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: 'hsl(var(--border))' }}
|
||||
minTickGap={12}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fill: 'hsl(var(--muted))', fontSize: 11 }}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
width={40}
|
||||
tickFormatter={(value: number) => formatNumber(value, digits > 2 ? 2 : digits)}
|
||||
/>
|
||||
{baseline ? (
|
||||
<ReferenceLine
|
||||
y={baseline.value}
|
||||
stroke="hsl(var(--muted))"
|
||||
strokeDasharray="5 4"
|
||||
label={{
|
||||
value: baseline.label,
|
||||
position: 'insideTopLeft',
|
||||
fill: 'hsl(var(--muted))',
|
||||
fontSize: 11,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<Tooltip
|
||||
cursor={{ stroke: 'hsl(var(--border))' }}
|
||||
content={(props: TooltipProps<number, string>) => (
|
||||
<ChartTooltip {...props} digits={digits} />
|
||||
)}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="y"
|
||||
stroke="hsl(var(--accent))"
|
||||
strokeWidth={2}
|
||||
dot={{ r: 2.5, fill: 'hsl(var(--accent))', strokeWidth: 0 }}
|
||||
activeDot={{ r: 4 }}
|
||||
isAnimationActive={animate}
|
||||
animationDuration={400}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChartTooltip({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
digits,
|
||||
}: TooltipProps<number, string> & { digits: number }) {
|
||||
if (!active || !payload || payload.length === 0) return null;
|
||||
const value = payload[0]?.value;
|
||||
return (
|
||||
<div className="card px-2.5 py-1.5 text-xs shadow-md">
|
||||
<p className="nums text-muted">{String(label)}</p>
|
||||
<p className="nums font-mono font-semibold">
|
||||
{typeof value === 'number' ? formatNumber(value, digits) : '—'}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Suspense, lazy, useMemo } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatNumber, formatSigned, usePrefersReducedMotion } from './format';
|
||||
import type { MetricPoint } from './MetricChart';
|
||||
import { EditedChip } from './StatStrip';
|
||||
|
||||
export type { MetricPoint } from './MetricChart';
|
||||
|
||||
// Lazy, and deliberately not a static import: recharts is ~100 kB gzipped and
|
||||
// exactly one surface on the site uses it. `vite.config.ts` also names it as
|
||||
// its own manual chunk, so this stays out of the entry bundle in both dev and
|
||||
// production builds.
|
||||
const MetricChart = lazy(() => import('./MetricChart'));
|
||||
|
||||
const CHART_HEIGHT = 168;
|
||||
|
||||
export interface MetricMoverProps {
|
||||
/** The one number a reader would repeat in a meeting. */
|
||||
label: string;
|
||||
value: number;
|
||||
digits?: number;
|
||||
unit?: string;
|
||||
/** The dashed rule: what the number was before, or what counts as par. */
|
||||
baseline?: { value: number; label: string };
|
||||
series?: MetricPoint[];
|
||||
/** One sentence on what moved it. Not a caveat — the mechanism. */
|
||||
caption?: string;
|
||||
edited?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The headline number, with the line that shows it moving.
|
||||
*
|
||||
* The reference rule is not decoration: a number on its own is a claim, and a
|
||||
* number against the line it used to sit on is evidence. If a demo has no
|
||||
* baseline to draw, it should not be using this component.
|
||||
*/
|
||||
export function MetricMover({
|
||||
label,
|
||||
value,
|
||||
digits = 3,
|
||||
unit,
|
||||
baseline,
|
||||
series,
|
||||
caption,
|
||||
edited = false,
|
||||
className,
|
||||
}: MetricMoverProps) {
|
||||
const reducedMotion = usePrefersReducedMotion();
|
||||
const delta = baseline ? value - baseline.value : null;
|
||||
const points = useMemo(() => series ?? [], [series]);
|
||||
|
||||
return (
|
||||
<section aria-label={label} className={cn('card p-4', className)}>
|
||||
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
|
||||
<h3 className="text-sm font-medium text-muted">{label}</h3>
|
||||
{edited ? <EditedChip /> : null}
|
||||
</div>
|
||||
<p className="mt-1 flex items-baseline gap-2">
|
||||
<span className="nums text-4xl font-semibold leading-none tracking-tight">
|
||||
{formatNumber(value, digits)}
|
||||
</span>
|
||||
{unit ? <span className="text-sm text-muted">{unit}</span> : null}
|
||||
{delta !== null ? (
|
||||
<span
|
||||
className={cn(
|
||||
'nums text-sm font-medium',
|
||||
delta > 0 ? 'text-positive' : delta < 0 ? 'text-danger' : 'text-muted',
|
||||
)}
|
||||
>
|
||||
{formatSigned(delta, digits)} vs {baseline?.label}
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
|
||||
{points.length > 1 ? (
|
||||
<div className="mt-3">
|
||||
<Suspense
|
||||
fallback={
|
||||
// Reserve the exact chart height. A chart that pops in and pushes
|
||||
// the caption down is the layout shift this whole page is trying
|
||||
// not to have.
|
||||
<div
|
||||
style={{ height: CHART_HEIGHT }}
|
||||
className="w-full rounded-lg bg-surface-2"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MetricChart
|
||||
data={points}
|
||||
{...(baseline ? { baseline } : {})}
|
||||
height={CHART_HEIGHT}
|
||||
animate={!reducedMotion}
|
||||
digits={digits}
|
||||
/>
|
||||
</Suspense>
|
||||
{/* The chart is a picture of the table; the table is the accessible
|
||||
version of the picture. Both are the same numbers. */}
|
||||
<details className="mt-1">
|
||||
<summary className="tap inline-flex cursor-pointer items-center text-xs text-muted hover:text-fg">
|
||||
Show these points as a table
|
||||
</summary>
|
||||
<table className="nums mt-1.5 w-full border-collapse font-mono text-xs">
|
||||
<tbody className="divide-y divide-border">
|
||||
{points.map((point) => (
|
||||
<tr key={String(point.x)}>
|
||||
<th scope="row" className="py-1 text-left font-normal text-muted">
|
||||
{String(point.x)}
|
||||
</th>
|
||||
<td className="py-1 text-right">{formatNumber(point.y, digits)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</details>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{caption ? (
|
||||
<p className="mt-3 text-sm leading-relaxed text-muted">{caption}</p>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useState } from 'react';
|
||||
import { Check, Copy, ExternalLink } from 'lucide-react';
|
||||
import type { Provenance, RunRef } from '@/lib/demo-kit/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatDate } from './format';
|
||||
|
||||
export interface ProvenanceCardProps {
|
||||
provenance: Provenance;
|
||||
/** The run currently on screen, if the page is showing one. */
|
||||
run?: RunRef;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type CopyState = 'idle' | 'copied' | 'failed';
|
||||
|
||||
/**
|
||||
* Where these numbers came from, and the command that reproduces them.
|
||||
*
|
||||
* The command is the load-bearing element. Everything else on this page is us
|
||||
* telling you what happened; this is the line you paste into your own terminal
|
||||
* to find out for yourself, which is why it is verbatim and copy-pasteable
|
||||
* rather than prettified into something that would not actually run.
|
||||
*/
|
||||
export function ProvenanceCard({ provenance, run, className }: ProvenanceCardProps) {
|
||||
const [copyState, setCopyState] = useState<CopyState>('idle');
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
// `navigator.clipboard` is undefined on a non-secure origin, which is
|
||||
// exactly what a colleague testing over a LAN IP will hit. Fail visibly.
|
||||
await navigator.clipboard.writeText(provenance.command);
|
||||
setCopyState('copied');
|
||||
window.setTimeout(() => setCopyState('idle'), 2000);
|
||||
} catch {
|
||||
setCopyState('failed');
|
||||
}
|
||||
};
|
||||
|
||||
const rows: { label: string; value: string; mono?: boolean }[] = [
|
||||
{ label: 'Environment package', value: provenance.envPackage, mono: true },
|
||||
{ label: 'Taskset', value: provenance.tasksetId, mono: true },
|
||||
{ label: 'verifiers version', value: provenance.verifiersVersion, mono: true },
|
||||
];
|
||||
if (run) {
|
||||
rows.push(
|
||||
{ label: 'Model', value: run.model, mono: true },
|
||||
{ label: 'Captured', value: formatDate(run.capturedAt) },
|
||||
{ label: 'Seed', value: String(run.seed), mono: true },
|
||||
);
|
||||
if (run.intervention) rows.push({ label: 'Intervention', value: run.intervention });
|
||||
}
|
||||
|
||||
return (
|
||||
<section aria-label="Provenance" className={cn('card overflow-hidden', className)}>
|
||||
<header className="border-b border-border px-3 py-2">
|
||||
<h3 className="text-sm font-semibold">Provenance</h3>
|
||||
</header>
|
||||
|
||||
<dl className="divide-y divide-border">
|
||||
{rows.map((row) => (
|
||||
<div key={row.label} className="flex items-baseline gap-3 px-3 py-2">
|
||||
<dt className="flex-1 text-sm text-muted">{row.label}</dt>
|
||||
<dd
|
||||
className={cn(
|
||||
'min-w-0 break-all text-right text-sm',
|
||||
row.mono && 'nums font-mono text-xs',
|
||||
)}
|
||||
>
|
||||
{row.value}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<div className="border-t border-border p-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Run it yourself
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
className="tap inline-flex items-center gap-1.5 rounded-lg border border-border px-2.5 text-xs font-medium transition-colors duration-2 ease-enter hover:bg-surface-2"
|
||||
>
|
||||
{copyState === 'copied' ? (
|
||||
<Check className="h-3.5 w-3.5 text-positive" aria-hidden="true" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
)}
|
||||
{copyState === 'copied' ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="mt-2 overflow-x-auto rounded-lg bg-surface-2 p-3">
|
||||
<code className="font-mono text-xs leading-relaxed">{provenance.command}</code>
|
||||
</pre>
|
||||
<p role="status" className="mt-1 text-xs text-muted">
|
||||
{copyState === 'copied'
|
||||
? 'Command copied to your clipboard.'
|
||||
: copyState === 'failed'
|
||||
? 'Your browser blocked clipboard access — select the command above and copy it manually.'
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{provenance.credits.length > 0 ? (
|
||||
<div className="border-t border-border px-3 py-2.5">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted">
|
||||
Built on
|
||||
</h4>
|
||||
<ul className="mt-1.5 flex flex-wrap gap-x-4 gap-y-1">
|
||||
{provenance.credits.map((credit) => (
|
||||
<li key={credit.href}>
|
||||
<a
|
||||
href={credit.href}
|
||||
className="inline-flex items-center gap-1 text-sm text-accent-fg underline-offset-2 hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{credit.label}
|
||||
<ExternalLink className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import * as Slider from '@radix-ui/react-slider';
|
||||
import { ArrowDown, ArrowUp, Minus, RotateCcw } from 'lucide-react';
|
||||
import type { RewardSpec, RewardValues } from '@/lib/demo-kit/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { formatNumber, formatOrDash } from './format';
|
||||
import { scoreReward, shippedWeights } from './reward-math';
|
||||
import { EditedChip } from './StatStrip';
|
||||
|
||||
/** One recorded arm — a run, or a group of runs already reduced to one score. */
|
||||
export interface RewardArm {
|
||||
id: string;
|
||||
label: string;
|
||||
/** The environment's per-component scores. Re-weighted, never re-run. */
|
||||
values: RewardValues;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export interface RewardPreset {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
weights: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface RewardEditorProps {
|
||||
spec: RewardSpec;
|
||||
arms: RewardArm[];
|
||||
/** Two is the right number. More and the visitor reads instead of playing. */
|
||||
presets?: RewardPreset[];
|
||||
onWeightsChange?: (weights: Record<string, number>, edited: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const STEP = 0.05;
|
||||
|
||||
/**
|
||||
* Presets built from the spec's own labels, for a demo that does not supply
|
||||
* its own. Both are stated as the choice a buyer would actually argue for in a
|
||||
* meeting, not as "preset A" and "preset B".
|
||||
*/
|
||||
function derivePresets(spec: RewardSpec): RewardPreset[] {
|
||||
const shipped = shippedWeights(spec);
|
||||
const counterweights = spec.components.filter((c) => c.role === 'counterweight');
|
||||
const objectives = spec.components.filter((c) => c.role === 'objective');
|
||||
const presets: RewardPreset[] = [
|
||||
{
|
||||
id: 'shipped',
|
||||
label: 'What we ship',
|
||||
description: 'The weights in the environment as committed.',
|
||||
weights: shipped,
|
||||
},
|
||||
];
|
||||
|
||||
const firstObjective = objectives[0];
|
||||
if (counterweights.length > 0 && firstObjective) {
|
||||
const onlyObjective = { ...shipped };
|
||||
for (const component of counterweights) onlyObjective[component.key] = 0;
|
||||
presets.push({
|
||||
id: 'objective-only',
|
||||
label: `${firstObjective.label} at any cost`,
|
||||
description: `Drops ${counterweights
|
||||
.map((c) => c.label.toLowerCase())
|
||||
.join(' and ')} to zero.`,
|
||||
weights: onlyObjective,
|
||||
});
|
||||
|
||||
const doubled = { ...shipped };
|
||||
for (const component of counterweights) doubled[component.key] = component.weight * 2;
|
||||
presets.push({
|
||||
id: 'counterweight-heavy',
|
||||
label: `Double ${counterweights[0]?.label.toLowerCase() ?? 'the counterweight'}`,
|
||||
description: 'What a risk-averse buyer would ask for.',
|
||||
weights: doubled,
|
||||
});
|
||||
}
|
||||
return presets;
|
||||
}
|
||||
|
||||
function weightsEqual(a: Record<string, number>, b: Record<string, number>): boolean {
|
||||
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
||||
for (const key of keys) {
|
||||
if (Math.abs((a[key] ?? 0) - (b[key] ?? 0)) > 1e-9) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change what "good" means and watch the ranking move.
|
||||
*
|
||||
* The honesty problem this component has to solve: re-weighting recorded scores
|
||||
* is NOT training. It shows you the ranking a different reward would have
|
||||
* produced over these exact attempts; it cannot show you the different attempts
|
||||
* a model trained on that reward would have made. That distinction is the
|
||||
* permanent caption at the bottom, and it is not collapsible.
|
||||
*/
|
||||
export function RewardEditor({
|
||||
spec,
|
||||
arms,
|
||||
presets,
|
||||
onWeightsChange,
|
||||
className,
|
||||
}: RewardEditorProps) {
|
||||
const shipped = useMemo(() => shippedWeights(spec), [spec]);
|
||||
const [weights, setWeights] = useState<Record<string, number>>(shipped);
|
||||
const effectivePresets = useMemo(() => presets ?? derivePresets(spec), [presets, spec]);
|
||||
|
||||
const bounds = useMemo(() => {
|
||||
const values = spec.components.map((c) => c.weight);
|
||||
const max = Math.max(2, ...values.map((v) => Math.ceil(Math.abs(v) * 2)));
|
||||
const min = Math.min(0, ...values.map((v) => Math.floor(v)));
|
||||
return { min, max };
|
||||
}, [spec]);
|
||||
|
||||
const edited = !weightsEqual(weights, shipped);
|
||||
|
||||
const apply = (next: Record<string, number>) => {
|
||||
setWeights(next);
|
||||
onWeightsChange?.(next, !weightsEqual(next, shipped));
|
||||
};
|
||||
|
||||
const ranked = useMemo(() => {
|
||||
const shippedTotals = new Map(
|
||||
arms.map((arm) => [arm.id, scoreReward(spec, arm.values).total]),
|
||||
);
|
||||
const rows = arms.map((arm) => ({
|
||||
arm,
|
||||
total: scoreReward(spec, arm.values, weights).total,
|
||||
shippedTotal: shippedTotals.get(arm.id) ?? null,
|
||||
}));
|
||||
// Nulls sort last: an unscored arm is not a zero-scoring arm.
|
||||
const byTotal = (a: { total: number | null }, b: { total: number | null }) => {
|
||||
if (a.total === null && b.total === null) return 0;
|
||||
if (a.total === null) return 1;
|
||||
if (b.total === null) return -1;
|
||||
return b.total - a.total;
|
||||
};
|
||||
const shippedOrder = [...rows]
|
||||
.sort((a, b) => byTotal({ total: a.shippedTotal }, { total: b.shippedTotal }))
|
||||
.map((row) => row.arm.id);
|
||||
return [...rows].sort(byTotal).map((row, index) => ({
|
||||
...row,
|
||||
rank: index + 1,
|
||||
shippedRank: shippedOrder.indexOf(row.arm.id) + 1,
|
||||
}));
|
||||
}, [arms, spec, weights]);
|
||||
|
||||
const span = useMemo(() => {
|
||||
const totals = ranked.map((row) => row.total).filter((t): t is number => t !== null);
|
||||
if (totals.length === 0) return { lo: 0, hi: 1 };
|
||||
const lo = Math.min(0, ...totals);
|
||||
const hi = Math.max(...totals);
|
||||
return { lo, hi: hi === lo ? lo + 1 : hi };
|
||||
}, [ranked]);
|
||||
|
||||
const leader = ranked[0];
|
||||
|
||||
return (
|
||||
<div className={cn('grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]', className)}>
|
||||
<section aria-label="Reward weights" className="card p-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h3 className="text-sm font-semibold">Change what good means</h3>
|
||||
{edited ? <EditedChip /> : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => apply(shipped)}
|
||||
disabled={!edited}
|
||||
className="tap ml-auto inline-flex items-center gap-1.5 rounded-lg border border-border px-2.5 text-xs font-medium transition-colors duration-2 ease-enter hover:bg-surface-2 disabled:opacity-40"
|
||||
>
|
||||
<RotateCcw className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
Reset to shipped
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{effectivePresets.map((preset) => {
|
||||
const active = weightsEqual(weights, preset.weights);
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => apply({ ...preset.weights })}
|
||||
aria-pressed={active}
|
||||
title={preset.description ?? preset.label}
|
||||
className={cn(
|
||||
'tap rounded-lg border px-3 text-left text-xs font-medium transition-colors duration-2 ease-enter',
|
||||
active
|
||||
? 'border-brand bg-accent-subtle text-accent-fg'
|
||||
: 'border-border hover:bg-surface-2',
|
||||
)}
|
||||
>
|
||||
{preset.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{spec.components.map((component) => {
|
||||
const value = weights[component.key] ?? component.weight;
|
||||
const changed = Math.abs(value - component.weight) > 1e-9;
|
||||
return (
|
||||
<div key={component.key}>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
{/* A <label htmlFor> would point at Radix's root <span>,
|
||||
which is not a labelable element — the association would
|
||||
silently do nothing. The thumb takes its name from this
|
||||
text via aria-labelledby instead. */}
|
||||
<span
|
||||
id={`weight-label-${component.key}`}
|
||||
className="text-sm font-medium text-fg"
|
||||
>
|
||||
{component.label}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'nums font-mono text-sm',
|
||||
changed ? 'font-semibold text-accent-fg' : 'text-muted',
|
||||
)}
|
||||
>
|
||||
{formatNumber(value, 2)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mb-1.5 text-xs leading-snug text-muted">{component.description}</p>
|
||||
<Slider.Root
|
||||
className="relative flex h-6 w-full touch-none select-none items-center"
|
||||
min={bounds.min}
|
||||
max={bounds.max}
|
||||
step={STEP}
|
||||
value={[value]}
|
||||
onValueChange={(next) =>
|
||||
apply({ ...weights, [component.key]: next[0] ?? component.weight })
|
||||
}
|
||||
>
|
||||
<Slider.Track className="relative h-1.5 w-full grow rounded-full bg-surface-2">
|
||||
<Slider.Range className="absolute h-full rounded-full bg-brand" />
|
||||
</Slider.Track>
|
||||
{/* 44px of hit area around a 16px dot: the visible thumb is
|
||||
small enough to read the track under it, and still catches
|
||||
a thumb on a phone. */}
|
||||
<Slider.Thumb
|
||||
aria-labelledby={`weight-label-${component.key}`}
|
||||
className="block h-6 w-6 rounded-full border-4 border-brand bg-surface shadow-sm"
|
||||
/>
|
||||
</Slider.Root>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-label="Ranking under this reward" className="card flex flex-col p-3">
|
||||
<h3 className="text-sm font-semibold">
|
||||
Ranking under this reward {edited ? <EditedChip className="ml-1 align-middle" /> : null}
|
||||
</h3>
|
||||
|
||||
<ol className="mt-3 space-y-2">
|
||||
{ranked.map((row) => {
|
||||
const moved = row.rank - row.shippedRank;
|
||||
const width =
|
||||
row.total === null
|
||||
? 0
|
||||
: Math.max(2, ((row.total - span.lo) / (span.hi - span.lo)) * 100);
|
||||
return (
|
||||
<li
|
||||
key={row.arm.id}
|
||||
className={cn(
|
||||
'rounded-lg border p-2.5 transition-colors duration-3 ease-enter',
|
||||
row.rank === 1 ? 'border-brand bg-accent-subtle/50' : 'border-border',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="nums text-sm font-semibold text-muted">{row.rank}</span>
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-medium">
|
||||
{row.arm.label}
|
||||
</span>
|
||||
<RankMove moved={moved} />
|
||||
<span className="nums font-mono text-sm font-semibold">
|
||||
{formatOrDash(row.total)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-surface-2"
|
||||
>
|
||||
<div
|
||||
className="h-full rounded-full bg-brand transition-[width] duration-3 ease-enter"
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
</div>
|
||||
{row.arm.note ? (
|
||||
<p className="mt-1 text-xs text-muted">{row.arm.note}</p>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
|
||||
{/* User-initiated, so it is safe to announce here without fighting the
|
||||
shell's step-change region: the two never fire from one action. */}
|
||||
<p role="status" className="sr-only">
|
||||
{leader
|
||||
? `Leading under this reward: ${leader.arm.label}, ${formatOrDash(leader.total)}.`
|
||||
: 'No arms to rank.'}
|
||||
</p>
|
||||
|
||||
<p className="mt-auto pt-3 text-xs leading-relaxed text-muted">
|
||||
We re-scored the same recorded attempts under your reward. Training on it would change
|
||||
the behaviour, not just the ranking.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RankMove({ moved }: { moved: number }) {
|
||||
if (moved === 0) {
|
||||
return (
|
||||
<span className="inline-flex items-center text-muted" title="Same rank as shipped">
|
||||
<Minus className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span className="sr-only">unchanged</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const up = moved < 0;
|
||||
return (
|
||||
<span
|
||||
className={cn('nums inline-flex items-center text-xs', up ? 'text-positive' : 'text-danger')}
|
||||
title={`${Math.abs(moved)} place${Math.abs(moved) === 1 ? '' : 's'} ${up ? 'up' : 'down'} from the shipped reward`}
|
||||
>
|
||||
{up ? (
|
||||
<ArrowUp className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
) : (
|
||||
<ArrowDown className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
)}
|
||||
{Math.abs(moved)}
|
||||
<span className="sr-only">{up ? ' places up' : ' places down'}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
/**
|
||||
* A hand-written demo, used to build and exercise the shell without waiting for
|
||||
* real fixtures.
|
||||
*
|
||||
* It is NOT a fixture and it is not shipped as a demo: nothing in
|
||||
* `src/demos/` imports it, and the shell mounts it only for the reserved slug
|
||||
* `__mock` in a dev build. It exists so that every surface in this directory
|
||||
* has something complete to render — including the awkward cases a real trace
|
||||
* eventually produces: a step with no reasoning, a null model call, a
|
||||
* not-scored reward component, and a truncated run.
|
||||
*/
|
||||
import type {
|
||||
DemoEpisode,
|
||||
DemoModule,
|
||||
DemoStep,
|
||||
RewardValues,
|
||||
RunRef,
|
||||
} from '@/lib/demo-kit/types';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type MarkKind = 'exact' | 'present' | 'absent';
|
||||
|
||||
export interface MockGuess {
|
||||
word: string;
|
||||
marks: MarkKind[];
|
||||
}
|
||||
|
||||
export interface MockState {
|
||||
answer: string;
|
||||
guesses: MockGuess[];
|
||||
solved: boolean;
|
||||
}
|
||||
|
||||
const ANSWER = 'CRANE';
|
||||
const MAX_GUESSES = 6;
|
||||
|
||||
/** Standard Wordle marking, duplicates and all. */
|
||||
function mark(guess: string, answer: string): MarkKind[] {
|
||||
const marks: MarkKind[] = Array.from({ length: guess.length }, () => 'absent');
|
||||
const pool = new Map<string, number>();
|
||||
for (let i = 0; i < answer.length; i += 1) {
|
||||
const letter = answer[i] as string;
|
||||
if (guess[i] === letter) {
|
||||
marks[i] = 'exact';
|
||||
} else {
|
||||
pool.set(letter, (pool.get(letter) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < guess.length; i += 1) {
|
||||
if (marks[i] === 'exact') continue;
|
||||
const letter = guess[i] as string;
|
||||
const left = pool.get(letter) ?? 0;
|
||||
if (left > 0) {
|
||||
marks[i] = 'present';
|
||||
pool.set(letter, left - 1);
|
||||
}
|
||||
}
|
||||
return marks;
|
||||
}
|
||||
|
||||
const TILE: Record<MarkKind, string> = {
|
||||
exact: 'bg-tile-exact text-tile-exact-fg',
|
||||
present: 'bg-tile-present text-tile-present-fg',
|
||||
absent: 'bg-tile-absent text-tile-absent-fg',
|
||||
};
|
||||
|
||||
const GLYPH: Record<MarkKind, string> = { exact: '●', present: '◐', absent: '○' };
|
||||
|
||||
function MockSurface({ state, compact = false }: { state: MockState; compact?: boolean }) {
|
||||
const rows = Array.from({ length: MAX_GUESSES }, (_, index) => state.guesses[index]);
|
||||
return (
|
||||
<div
|
||||
className={cn('grid w-fit gap-1', compact && 'gap-0.5')}
|
||||
role="img"
|
||||
aria-label={
|
||||
state.guesses.length === 0
|
||||
? 'Empty board'
|
||||
: `Board after ${state.guesses.length} guesses: ${state.guesses
|
||||
.map((guess) => guess.word)
|
||||
.join(', ')}`
|
||||
}
|
||||
>
|
||||
{rows.map((guess, rowIndex) => (
|
||||
<div key={rowIndex} className={cn('flex gap-1', compact && 'gap-0.5')}>
|
||||
{Array.from({ length: 5 }, (_unused, colIndex) => {
|
||||
const letter = guess?.word[colIndex];
|
||||
const kind = guess?.marks[colIndex];
|
||||
return (
|
||||
<span
|
||||
key={colIndex}
|
||||
className={cn(
|
||||
'grid place-items-center rounded-md border border-border font-semibold uppercase',
|
||||
compact ? 'h-3.5 w-3.5 text-[7px]' : 'h-9 w-9 text-base',
|
||||
kind ? TILE[kind] : 'bg-surface-2 text-muted',
|
||||
)}
|
||||
>
|
||||
{compact ? (kind ? GLYPH[kind] : '') : (letter ?? '')}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const REWARD_SOURCE = `import verifiers as vf
|
||||
|
||||
|
||||
def reward_solved(state) -> float:
|
||||
"""1.0 if the answer was found, else 0.0."""
|
||||
return 1.0 if state["solved"] else 0.0
|
||||
|
||||
|
||||
# --8<-- efficiency
|
||||
def reward_efficiency(state) -> float:
|
||||
"""Pays for finding it early, and pays nothing for finding it late.
|
||||
|
||||
This is the counterweight. Without it the highest-scoring policy is one
|
||||
that burns every guess it is allowed, because the objective alone cannot
|
||||
tell a lucky third guess from a grudging sixth.
|
||||
"""
|
||||
if not state["solved"]:
|
||||
return 0.0
|
||||
used = len(state["guesses"])
|
||||
return max(0.0, (MAX_GUESSES - used + 1) / MAX_GUESSES)
|
||||
# --8<--
|
||||
|
||||
|
||||
def reward_legal(state) -> float:
|
||||
"""Every guess was a real five-letter word in the allowed list."""
|
||||
return 1.0 if all(g in ALLOWED for g in state["guesses"]) else 0.0
|
||||
`;
|
||||
|
||||
function turnBoard(episode: DemoEpisode, upto: number): MockState {
|
||||
const guesses: MockGuess[] = [];
|
||||
for (let i = 0; i <= upto && i < episode.turns.length; i += 1) {
|
||||
const info = episode.turns[i]?.info;
|
||||
const word = typeof info?.['guess'] === 'string' ? info['guess'] : null;
|
||||
if (!word) continue;
|
||||
guesses.push({ word, marks: mark(word, ANSWER) });
|
||||
}
|
||||
const last = guesses[guesses.length - 1];
|
||||
return { answer: ANSWER, guesses, solved: last?.word === ANSWER };
|
||||
}
|
||||
|
||||
function adapt(episode: DemoEpisode): DemoStep<MockState>[] {
|
||||
return episode.turns.map((turn, index) => {
|
||||
const state = turnBoard(episode, index);
|
||||
const last = state.guesses[state.guesses.length - 1];
|
||||
const word = last?.word ?? 'no guess';
|
||||
const exact = last?.marks.filter((m) => m === 'exact').length ?? 0;
|
||||
return {
|
||||
index,
|
||||
state,
|
||||
reply: turn.reply,
|
||||
reasoning: turn.reasoning,
|
||||
call: turn.call,
|
||||
announce: state.solved
|
||||
? `Guess ${index + 1}: ${word}. Solved.`
|
||||
: `Guess ${index + 1}: ${word}. ${exact} letters in the right place.`,
|
||||
caption: word.toLowerCase(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function verify(episode: DemoEpisode): RewardValues | null {
|
||||
if (episode.truncated) return null;
|
||||
const final = turnBoard(episode, episode.turns.length - 1);
|
||||
const used = final.guesses.length;
|
||||
return {
|
||||
solved: final.solved ? 1 : 0,
|
||||
efficiency: final.solved ? Math.max(0, (MAX_GUESSES - used + 1) / MAX_GUESSES) : 0,
|
||||
legal: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export const mockDemo: DemoModule<MockState> = {
|
||||
meta: {
|
||||
slug: '__mock',
|
||||
title: 'Five-letter word game (mock)',
|
||||
tagline: 'An agent guesses a hidden five-letter word from letter feedback.',
|
||||
vertical: 'reference',
|
||||
status: 'live',
|
||||
order: 0,
|
||||
icon: 'Grid3x3',
|
||||
persona: 'Anyone deciding whether to fund an environment',
|
||||
rewardLine: 'Pays for solving; takes away for stalling.',
|
||||
ogImage: '/og/mock.png',
|
||||
},
|
||||
narrative: {
|
||||
thesis:
|
||||
'An environment is an eval you can take the gradient of. The task, the legal moves and the grader are all code — so you can change what "good" means and watch the number move.',
|
||||
anxiety: 'Is this a benchmark I read, or a thing I can actually change?',
|
||||
beats: [
|
||||
{ id: 'hero', title: 'The run', claim: 'This is a recorded rollout, not a live request.', surface: 'hero' },
|
||||
{ id: 'anatomy', title: 'The machine', claim: 'Four boxes: task, legal actions, grader, score.', surface: 'anatomy' },
|
||||
{ id: 'play', title: 'Watch it think', claim: 'Every move has a reason and a cost, both recorded.', surface: 'split-play' },
|
||||
{ id: 'reward', title: 'Change what good means', claim: 'Move a weight and the ranking moves with it.', surface: 'reward-editor' },
|
||||
{ id: 'metric', title: 'The number that moves', claim: 'The score is a measurement, not a claim.', surface: 'metric' },
|
||||
{ id: 'receipt', title: 'The receipts', claim: 'Every number here has a command that reproduces it.', surface: 'receipt' },
|
||||
{ id: 'limits', title: 'What this does not teach', claim: 'A word game is not your business process.', surface: 'limits' },
|
||||
],
|
||||
limits: [
|
||||
{
|
||||
text: 'A five-letter word has one right answer. Most business decisions do not, and a grader that pretends otherwise scores confidence rather than correctness.',
|
||||
answeredBy: 'claims-triage',
|
||||
},
|
||||
{
|
||||
text: 'Nothing here has a cost of being wrong. A real reward has to price the mistake, not just count the win.',
|
||||
},
|
||||
],
|
||||
},
|
||||
reward: {
|
||||
components: [
|
||||
{
|
||||
key: 'solved',
|
||||
label: 'Found the word',
|
||||
description: 'One point if the hidden word was guessed within six tries.',
|
||||
weight: 1,
|
||||
role: 'objective',
|
||||
},
|
||||
{
|
||||
key: 'efficiency',
|
||||
label: 'Found it early',
|
||||
description: 'Pays more the fewer guesses it took. Zero if it never got there.',
|
||||
weight: 0.5,
|
||||
role: 'counterweight',
|
||||
},
|
||||
{
|
||||
key: 'legal',
|
||||
label: 'Played legal words',
|
||||
description: 'Every guess was a real five-letter word from the allowed list.',
|
||||
weight: 0.25,
|
||||
role: 'gate',
|
||||
},
|
||||
],
|
||||
metrics: [
|
||||
{ key: 'guesses_used', label: 'Guesses used', description: 'How many of the six were spent.' },
|
||||
{ key: 'unique_letters', label: 'Unique letters tried', description: 'Breadth of the search.' },
|
||||
],
|
||||
source: { path: 'envs/wordle_five/wordle_five/rewards.py', code: REWARD_SOURCE, marker: '--8<-- efficiency' },
|
||||
},
|
||||
provenance: {
|
||||
envPackage: 'wordle_five',
|
||||
tasksetId: 'wordle-five-v0-mock',
|
||||
verifiersVersion: '0.1.0',
|
||||
command: 'uv run vf-eval wordle-five -n 8 -m gpt-4.1-mini --seed 7',
|
||||
credits: [
|
||||
{ label: 'verifiers', href: 'https://github.com/PrimeIntellect-ai/verifiers' },
|
||||
],
|
||||
},
|
||||
anatomy: {
|
||||
task: 'Guess a hidden five-letter word in six tries, using the coloured feedback from each guess.',
|
||||
actions: 'One legal five-letter word per turn, drawn from the allowed list. Nothing else is a move.',
|
||||
grader: 'Deterministic Python: it marks the guess against the answer, checks legality, and scores the episode. No model judges it.',
|
||||
score: 'A weighted sum: one point for finding the word, half a point scaled by how early, a quarter as a legality gate.',
|
||||
},
|
||||
adapt,
|
||||
Surface: MockSurface,
|
||||
verify,
|
||||
};
|
||||
|
||||
function call(prompt: number, completion: number, reasoning: number | null, ms: number) {
|
||||
return {
|
||||
promptTokens: prompt,
|
||||
completionTokens: completion,
|
||||
reasoningTokens: reasoning,
|
||||
durationMs: ms,
|
||||
finishReason: 'stop',
|
||||
};
|
||||
}
|
||||
|
||||
export const mockEpisodes: Record<string, DemoEpisode> = {
|
||||
'mock-base': {
|
||||
runId: 'mock-base',
|
||||
seed: 7,
|
||||
model: 'gpt-4.1-mini',
|
||||
capturedAt: '2026-08-12',
|
||||
rewards: { solved: 1, efficiency: 0.5, legal: 1 },
|
||||
metrics: { guesses_used: 4, unique_letters: 14 },
|
||||
outcome: 'solved',
|
||||
turns: [
|
||||
{
|
||||
reply: 'SLATE',
|
||||
reasoning:
|
||||
'Opening with a word that covers three of the five most common letters and two common consonants. I want information, not a lucky hit.',
|
||||
call: call(412, 6, 84, 910),
|
||||
info: { guess: 'SLATE' },
|
||||
},
|
||||
{
|
||||
reply: 'TRACE',
|
||||
reasoning: null,
|
||||
call: call(486, 6, null, 640),
|
||||
info: { guess: 'TRACE' },
|
||||
},
|
||||
{
|
||||
reply: 'BRACE',
|
||||
reasoning:
|
||||
'A and E are placed. R is in the word but not where I put it. That leaves the first two positions to resolve.',
|
||||
call: call(551, 6, 121, 1180),
|
||||
info: { guess: 'BRACE' },
|
||||
},
|
||||
{
|
||||
reply: 'CRANE',
|
||||
reasoning: 'Only one word fits every constraint now.',
|
||||
call: call(618, 6, 96, 720),
|
||||
info: { guess: 'CRANE' },
|
||||
},
|
||||
],
|
||||
},
|
||||
'mock-prompted': {
|
||||
runId: 'mock-prompted',
|
||||
seed: 7,
|
||||
model: 'gpt-4.1-mini',
|
||||
capturedAt: '2026-08-12',
|
||||
// Written as the expression the environment computes, not as a decimal
|
||||
// literal: a hand-rounded 0.8333 would fail our own verifier.
|
||||
rewards: { solved: 1, efficiency: 5 / 6, legal: 1 },
|
||||
metrics: { guesses_used: 2, unique_letters: 9 },
|
||||
outcome: 'solved',
|
||||
turns: [
|
||||
{
|
||||
reply: 'TRACE',
|
||||
reasoning: 'Told to open with maximum letter coverage and to commit early once a word fits.',
|
||||
call: call(455, 6, 140, 1020),
|
||||
info: { guess: 'TRACE' },
|
||||
},
|
||||
{
|
||||
reply: 'CRANE',
|
||||
reasoning: 'Every constraint is satisfied by exactly one candidate.',
|
||||
call: call(512, 6, 88, 660),
|
||||
info: { guess: 'CRANE' },
|
||||
},
|
||||
],
|
||||
},
|
||||
'mock-truncated': {
|
||||
runId: 'mock-truncated',
|
||||
seed: 11,
|
||||
model: 'gpt-4.1-mini',
|
||||
capturedAt: '2026-08-12',
|
||||
// A run that hit the turn cap: the environment never reached a terminal
|
||||
// state, so two of the three components were never scored.
|
||||
rewards: { solved: null, efficiency: null, legal: 1 },
|
||||
metrics: { guesses_used: 2, unique_letters: 8 },
|
||||
truncated: true,
|
||||
outcome: 'aborted',
|
||||
turns: [
|
||||
{ reply: 'AUDIO', reasoning: null, call: call(410, 6, null, 880), info: { guess: 'AUDIO' } },
|
||||
{ reply: null, reasoning: null, call: null, info: {} },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const mockRuns: RunRef[] = [
|
||||
{
|
||||
id: 'mock-base',
|
||||
label: 'Out of the box',
|
||||
path: '/traces/__mock/base.json',
|
||||
kind: 'recorded',
|
||||
model: 'gpt-4.1-mini',
|
||||
capturedAt: '2026-08-12',
|
||||
seed: 7,
|
||||
},
|
||||
{
|
||||
id: 'mock-prompted',
|
||||
label: 'With a strategy prompt',
|
||||
path: '/traces/__mock/prompted.json',
|
||||
kind: 'intervened',
|
||||
intervention: 'System prompt rewritten',
|
||||
model: 'gpt-4.1-mini',
|
||||
capturedAt: '2026-08-12',
|
||||
seed: 7,
|
||||
},
|
||||
{
|
||||
id: 'mock-truncated',
|
||||
label: 'Hit the turn cap',
|
||||
path: '/traces/__mock/truncated.json',
|
||||
kind: 'recorded',
|
||||
model: 'gpt-4.1-mini',
|
||||
capturedAt: '2026-08-12',
|
||||
seed: 11,
|
||||
},
|
||||
];
|
||||
@@ -8,9 +8,9 @@ const STORAGE_KEY = 'pig-demo:contrast';
|
||||
|
||||
/**
|
||||
* `localStorage` is not always readable. In a cross-origin iframe with third-
|
||||
* party storage blocked, and in Safari private mode, the getter itself THROWS
|
||||
* rather than returning null — so every access has to be wrapped, not just
|
||||
* null-checked. Unreadable storage means "off", never a crash.
|
||||
* party storage blocked, and in Safari's private mode, the accessor itself
|
||||
* THROWS rather than returning null — so every access has to be wrapped, not
|
||||
* just null-checked. Unreadable storage means "off", never a crash.
|
||||
*/
|
||||
function readStored(): boolean {
|
||||
try {
|
||||
@@ -24,51 +24,72 @@ function writeStored(high: boolean): void {
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, high ? 'high' : 'normal');
|
||||
} catch {
|
||||
/* Preference is session-only here. The toggle still works. */
|
||||
/* Preference is session-only in this context. The toggle still works. */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One value shared by every mounted toggle, not one `useState` each.
|
||||
*
|
||||
* The header renders this control twice — once in the desktop bar, once inside
|
||||
* the mobile sheet — and only one of them is ever visible. With local state the
|
||||
* hidden one keeps a stale `aria-pressed`, so a visitor who toggles on a phone
|
||||
* and then rotates into the desktop layout is told the setting is off while the
|
||||
* page is plainly showing it on.
|
||||
*/
|
||||
let high = false;
|
||||
let initialised = false;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function applyToRoot(next: boolean): void {
|
||||
const root = document.documentElement;
|
||||
// Removed rather than set to "normal": the CSS keys off
|
||||
// `:root[data-contrast='high']`, and a leftover attribute makes the DOM lie
|
||||
// about which palette is actually applied.
|
||||
if (next) root.setAttribute('data-contrast', 'high');
|
||||
else root.removeAttribute('data-contrast');
|
||||
}
|
||||
|
||||
function setHigh(next: boolean): void {
|
||||
high = next;
|
||||
applyToRoot(next);
|
||||
writeStored(next);
|
||||
for (const listener of listeners) listener();
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
if (!initialised) {
|
||||
initialised = true;
|
||||
high = readStored();
|
||||
applyToRoot(high);
|
||||
}
|
||||
listeners.add(listener);
|
||||
return () => {
|
||||
listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
export function ContrastToggle({ className }: { className?: string }) {
|
||||
const [high, setHigh] = React.useState(false);
|
||||
const [mounted, setMounted] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const stored = readStored();
|
||||
setHigh(stored);
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!mounted) return;
|
||||
const root = document.documentElement;
|
||||
// Removing the attribute rather than setting it to "normal": the CSS keys
|
||||
// off `:root[data-contrast='high']`, and leaving a stale attribute behind
|
||||
// makes the DOM lie about the palette that is actually applied.
|
||||
if (high) root.setAttribute('data-contrast', 'high');
|
||||
else root.removeAttribute('data-contrast');
|
||||
}, [high, mounted]);
|
||||
// The server snapshot is `false` so a prerendered page never claims a
|
||||
// preference it cannot know; the real value lands on the first subscribe.
|
||||
const isHigh = React.useSyncExternalStore(
|
||||
subscribe,
|
||||
() => high,
|
||||
() => false,
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-touch"
|
||||
className={cn('lg:size-9', high && 'bg-accent-subtle text-accent-fg', className)}
|
||||
aria-pressed={high}
|
||||
aria-label={
|
||||
high ? 'High contrast tiles on. Turn off.' : 'High contrast tiles off. Turn on.'
|
||||
}
|
||||
className={cn('lg:size-9', isHigh && 'bg-accent-subtle text-accent-fg', className)}
|
||||
aria-pressed={isHigh}
|
||||
aria-label={isHigh ? 'High contrast tiles on. Turn off.' : 'High contrast tiles off. Turn on.'}
|
||||
title="High contrast tiles"
|
||||
onClick={() => {
|
||||
const next = !high;
|
||||
setHigh(next);
|
||||
writeStored(next);
|
||||
}}
|
||||
onClick={() => setHigh(!isHigh)}
|
||||
>
|
||||
<Contrast aria-hidden="true" />
|
||||
<span aria-live="polite" className="sr-only">
|
||||
{mounted ? (high ? 'High contrast on' : 'High contrast off') : ''}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -247,7 +247,14 @@ function TasksetSource({ className }: { className?: string }) {
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
|
||||
The actual taskset
|
||||
</p>
|
||||
<pre className="overflow-x-auto text-[11px] leading-relaxed text-fg">
|
||||
{/*
|
||||
Wrapped, not scrolled. The source is quoted verbatim — reformatting it
|
||||
to fit would make it stop being a quote — and the widest line is a third
|
||||
wider than this column at any font size worth reading. In a panel whose
|
||||
entire job is "this is real code", a visible wrap beats a third of the
|
||||
line hidden behind a scrollbar nobody in a boardroom will drag.
|
||||
*/}
|
||||
<pre className="overflow-x-auto whitespace-pre-wrap break-words text-[11px] leading-relaxed text-fg">
|
||||
<code className="font-mono">{TASKSET_SOURCE}</code>
|
||||
</pre>
|
||||
<a
|
||||
@@ -265,8 +272,11 @@ function TasksetSource({ className }: { className?: string }) {
|
||||
|
||||
function HowItWorksPanel({ ctaHref }: { ctaHref: string }) {
|
||||
return (
|
||||
<div className="w-[min(92vw,760px)] p-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
// 820px, not more: the panel hangs off the LEFT edge of the nav, which
|
||||
// starts about 100px in, so anything wider than this pushes past the right
|
||||
// edge of a 1024px laptop and gives the whole page a horizontal scrollbar.
|
||||
<div className="w-[min(90vw,820px)] p-4">
|
||||
<div className="grid grid-cols-[minmax(0,0.85fr)_minmax(0,1fr)] gap-4">
|
||||
<dl className="flex flex-col gap-3">
|
||||
{CONCEPTS.map((concept) => (
|
||||
<div key={concept.term} className="flex flex-col gap-0.5">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* or path moves, it moves in one line instead of in four pages.
|
||||
*/
|
||||
|
||||
import { demos } from '@/lib/demo-kit';
|
||||
import { demos } from '@/lib/demo-kit/registry';
|
||||
import type { DemoMeta, Vertical } from '@/lib/demo-kit/types';
|
||||
import { VERTICALS, verticalByKey } from '@/content/verticals';
|
||||
import type { VerticalEntry } from '@/content/verticals';
|
||||
@@ -26,13 +26,15 @@ export const routes = {
|
||||
galleryFiltered: (key: Vertical) => `/gallery?vertical=${key}`,
|
||||
} as const;
|
||||
|
||||
/** The public repository. Every claim on the site is meant to end up here. */
|
||||
export const REPO_URL = 'https://github.com/karti-ai/PIG-Demo';
|
||||
/**
|
||||
* The repository link belongs to the chrome, which already owns it. Re-exported
|
||||
* rather than repeated so the marketing pages and the footer cannot end up
|
||||
* pointing at two different repositories.
|
||||
*/
|
||||
export { REPO_URL } from '@/components/site/links';
|
||||
|
||||
/** Registry order is authorial; this is the order every list renders in. */
|
||||
export const allDemos: readonly DemoMeta[] = [...demos].sort(
|
||||
(a, b) => a.order - b.order || a.slug.localeCompare(b.slug),
|
||||
);
|
||||
/** Already sorted by `order` then slug by the registry. Never sort it again. */
|
||||
export const allDemos: readonly DemoMeta[] = demos;
|
||||
|
||||
export const liveDemos: readonly DemoMeta[] = allDemos.filter((d) => d.status === 'live');
|
||||
|
||||
|
||||
+63
-16
@@ -1,24 +1,24 @@
|
||||
/**
|
||||
* The demo-kit public barrel.
|
||||
* The demo-kit barrel.
|
||||
*
|
||||
* This is the ONLY module a demo under `src/demos/` is allowed to import from
|
||||
* the shared shell, and it deliberately exposes a small surface: the contract
|
||||
* types, the two `define*` wrappers, and the two pure helpers a demo's own
|
||||
* surface might need to render a score honestly.
|
||||
* Two audiences, and the split between them matters.
|
||||
*
|
||||
* The player, the registry, the verifier and the reward editor's arithmetic are
|
||||
* NOT here. They are shell machinery — a demo that reaches for `usePlayer` is a
|
||||
* demo that has started rendering its own chrome, and the whole point of the
|
||||
* contract is that the shell owns chrome so every demo gets the same one. The
|
||||
* shell imports those from their own modules:
|
||||
* A DEMO under `src/demos/` may import from `@/lib/demo-kit` and from nothing
|
||||
* else in the shell. What it should actually reach for is the first block
|
||||
* below: the contract types, the two `define*` wrappers, and the two pure
|
||||
* helpers it needs to render a score without inventing one. That restriction is
|
||||
* enforced by `scripts/check-demos.mjs`, not by what this file happens to
|
||||
* export — the shell's own surfaces import through here too, and splitting them
|
||||
* into a second barrel would only mean the check script had two paths to allow
|
||||
* instead of one.
|
||||
*
|
||||
* import { listDemos, loadDemoModule } from '@/lib/demo-kit/registry';
|
||||
* import { usePlayer } from '@/lib/demo-kit/player';
|
||||
* import { loadEpisode, listRuns } from '@/lib/demo-kit/episode';
|
||||
* import { decompose, reweight } from '@/lib/demo-kit/reward';
|
||||
* import { verifyEpisode } from '@/lib/demo-kit/verify';
|
||||
* The SHELL may import anything here, or reach into the individual modules
|
||||
* (`@/lib/demo-kit/player`, `/registry`, `/episode`, `/reward`, `/verify`) when
|
||||
* it wants one thing without the rest.
|
||||
*/
|
||||
|
||||
/* -- The contract. Demos live here. --------------------------------------- */
|
||||
|
||||
export type {
|
||||
DemoEpisode,
|
||||
DemoMeta,
|
||||
@@ -39,5 +39,52 @@ export type {
|
||||
|
||||
export { defineDemo, defineMeta } from './define';
|
||||
|
||||
/** `null` is "not scored", never 0.0. Demos render absences with these two. */
|
||||
/** `null` is "not scored", never 0.0. Every absence goes through these two. */
|
||||
export { isNotScored, rewardTotal } from './episode';
|
||||
|
||||
/* -- Shell machinery. ------------------------------------------------------ */
|
||||
|
||||
export {
|
||||
demos,
|
||||
getDemo,
|
||||
getVertical,
|
||||
hasDemo,
|
||||
listDemos,
|
||||
listVerticals,
|
||||
loadDemoModule,
|
||||
VERTICAL_LABELS,
|
||||
VERTICAL_ORDER,
|
||||
} from './registry';
|
||||
export type { AnyDemoModule, VerticalGroup } from './registry';
|
||||
|
||||
export {
|
||||
clearEpisodeCache,
|
||||
listRuns,
|
||||
loadEpisode,
|
||||
loadManifest,
|
||||
MANIFEST_PATH,
|
||||
scoredCount,
|
||||
} from './episode';
|
||||
export type { RunManifest } from './episode';
|
||||
|
||||
export {
|
||||
FALLBACK_STEP_MS,
|
||||
isPlaybackSpeed,
|
||||
PLAYBACK_SPEEDS,
|
||||
usePlayer,
|
||||
usePrefersReducedMotion,
|
||||
} from './player';
|
||||
export type { PlaybackSpeed, Player, PlayerOptions } from './player';
|
||||
|
||||
export {
|
||||
decompose,
|
||||
isEdited,
|
||||
pruneOverrides,
|
||||
reweight,
|
||||
WEIGHT_EPSILON,
|
||||
weightsAreDegenerate,
|
||||
} from './reward';
|
||||
export type { RewardBreakdown, RewardRow, WeightOverrides } from './reward';
|
||||
|
||||
export { recomputeRewards, verifyEpisode, verifySummary, VERIFY_TOLERANCE } from './verify';
|
||||
export type { ComponentComparison, VerifyResult, VerifyStatus } from './verify';
|
||||
|
||||
@@ -179,6 +179,14 @@ const demosBySlug: ReadonlyMap<string, DemoMeta> = (() => {
|
||||
|
||||
const orderedDemos: readonly DemoMeta[] = [...demosBySlug.values()].sort(byOrderThenSlug);
|
||||
|
||||
/**
|
||||
* The same list as `listDemos()`, as a frozen constant.
|
||||
*
|
||||
* Handy where a module wants the lineup at import time rather than in a render.
|
||||
* It is the SAME array every caller sees — never sort or splice it in place.
|
||||
*/
|
||||
export const demos: readonly DemoMeta[] = orderedDemos;
|
||||
|
||||
/** Every demo that survived validation, sorted by `order` then slug. */
|
||||
export function listDemos(): DemoMeta[] {
|
||||
return [...orderedDemos];
|
||||
|
||||
@@ -137,6 +137,29 @@ export function verifyEpisode(module: AnyDemoModule, episode: DemoEpisode): Veri
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Just the recomputed numbers, with none of the comparison.
|
||||
*
|
||||
* `verifyEpisode` is the one to reach for — it does the comparing, and the
|
||||
* comparing is where the honesty rules live. This exists for a surface that
|
||||
* wants to render the recomputed values itself and only needs the safe call:
|
||||
* a grader that is missing, throws, or declines returns `null` rather than
|
||||
* propagating, so no caller can turn a broken verifier into a zero.
|
||||
*/
|
||||
export function recomputeRewards(
|
||||
module: AnyDemoModule,
|
||||
episode: DemoEpisode,
|
||||
): RewardValues | null {
|
||||
if (typeof module.verify !== 'function') return null;
|
||||
if (episode.truncated === true) return null;
|
||||
try {
|
||||
return module.verify(episode);
|
||||
} catch (error: unknown) {
|
||||
console.error('[demo-kit] in-browser grader threw', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** One line an exec can read, given a result. Keeps the wording in one place. */
|
||||
export function verifySummary(result: VerifyResult): string {
|
||||
switch (result.status) {
|
||||
|
||||
+23
-11
@@ -3,21 +3,26 @@ import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { ArrowRight, FileText } from 'lucide-react';
|
||||
|
||||
import { iconFor } from '@/content/icons';
|
||||
import { allDemos, routes, verticalForDemo, verticalKeysInUse } from '@/content/lineup';
|
||||
import { PROPOSAL_NOTICE, verticalByKey } from '@/content/verticals';
|
||||
import {
|
||||
allDemos,
|
||||
demosForVertical,
|
||||
lineup,
|
||||
routes,
|
||||
verticalForDemo,
|
||||
verticalKeysInUse,
|
||||
} from '@/content/lineup';
|
||||
import { PROPOSAL_NOTICE } from '@/content/verticals';
|
||||
// The registry owns the taxonomy's exec-facing names. The lineup's own titles
|
||||
// are longer marketing headings ("Customer Support Resolution") and would wrap
|
||||
// two lines inside a filter chip on a phone, so chips use the registry label.
|
||||
import { VERTICAL_LABELS } from '@/lib/demo-kit/registry';
|
||||
import type { Vertical } from '@/lib/demo-kit/types';
|
||||
import * as s from '@/content/styles';
|
||||
|
||||
const ALL = 'all';
|
||||
|
||||
/**
|
||||
* A label for a `Vertical` key. Everything except `reference` has a vertical
|
||||
* entry to borrow the title from; `reference` is the hello-world demo and
|
||||
* belongs to no industry, so it is named for what it is.
|
||||
*/
|
||||
function verticalLabel(key: Vertical): string {
|
||||
if (key === 'reference') return 'Reference';
|
||||
return verticalByKey(key)?.title ?? key;
|
||||
return VERTICAL_LABELS[key];
|
||||
}
|
||||
|
||||
export default function Gallery() {
|
||||
@@ -44,6 +49,8 @@ export default function Gallery() {
|
||||
|
||||
const filters: readonly (Vertical | typeof ALL)[] = [ALL, ...verticalKeysInUse];
|
||||
|
||||
const unbuilt = lineup.filter((v) => demosForVertical(v.key).length === 0).length;
|
||||
|
||||
return (
|
||||
<main className={`${s.shell} py-10 sm:py-16`}>
|
||||
<p className={s.eyebrow}>Gallery</p>
|
||||
@@ -114,6 +121,7 @@ export default function Gallery() {
|
||||
// A spec is dimmed but never disabled: it goes to a real
|
||||
// page with a real specification on it, which is the only
|
||||
// thing that makes dimming it honest rather than teasing.
|
||||
aria-label={`${demo.title} — ${isSpec ? 'read the specification' : 'play it'}`}
|
||||
className={`${s.cardLink} h-full ${isSpec ? 'opacity-70 hover:opacity-100' : ''}`}
|
||||
to={routes.demo(demo.slug)}
|
||||
>
|
||||
@@ -169,8 +177,12 @@ export default function Gallery() {
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<div className="mt-12 card p-5 sm:p-7">
|
||||
<h2 className={s.h2}>The eleven we have not built</h2>
|
||||
<div className="card mt-12 p-5 sm:p-7">
|
||||
{/* Counted, not typed. A hard-coded "eleven" on a site about checkable
|
||||
numbers goes stale the first time a demo ships. */}
|
||||
<h2 className={s.h2}>
|
||||
The {unbuilt} we have not built
|
||||
</h2>
|
||||
<p className={`${s.prose} mt-3 max-w-2xl`}>
|
||||
The lineup is a set of proposals, written to the same four-part shape as the live one.
|
||||
Reading one takes a minute and tells you whether the idea survives contact with your own
|
||||
|
||||
+11
-2
@@ -77,6 +77,7 @@ export default function Home() {
|
||||
{helloWorldCitations.map((c) => (
|
||||
<li key={c.href}>
|
||||
<a
|
||||
aria-label={c.label}
|
||||
className={`${s.cardLink} h-full bg-surface-2`}
|
||||
href={c.href}
|
||||
rel="noreferrer noopener"
|
||||
@@ -186,7 +187,11 @@ export default function Home() {
|
||||
watch a recorded model play the same board, read the Python that scored it, and then
|
||||
move the reward weights and watch the ranking of two recorded runs change under you.
|
||||
</p>
|
||||
<Link className={`${s.cardLink} mt-6 sm:p-7`} to={routes.demo(Featured.slug)}>
|
||||
<Link
|
||||
aria-label={`Open the ${Featured.title} demo`}
|
||||
className={`${s.cardLink} mt-6 sm:p-7`}
|
||||
to={routes.demo(Featured.slug)}
|
||||
>
|
||||
<span className="flex flex-wrap items-center gap-2">
|
||||
<span className={`${s.pill} border-positive/30 bg-positive/10 text-positive`}>
|
||||
Live
|
||||
@@ -253,7 +258,11 @@ export default function Home() {
|
||||
const Icon = iconFor(v.icon);
|
||||
return (
|
||||
<li key={v.slug}>
|
||||
<Link className={`${s.cardLink} h-full`} to={routes.vertical(v.slug)}>
|
||||
<Link
|
||||
aria-label={v.title}
|
||||
className={`${s.cardLink} h-full`}
|
||||
to={routes.vertical(v.slug)}
|
||||
>
|
||||
<span className="flex items-start justify-between gap-3">
|
||||
<Icon aria-hidden="true" className="size-5 shrink-0 text-brand" />
|
||||
<span className="nums text-xs font-semibold text-muted">
|
||||
|
||||
+16
-3
@@ -113,7 +113,11 @@ export default function VerticalPage() {
|
||||
<section className="mt-10">
|
||||
<h2 className={s.h2}>What exists today</h2>
|
||||
{live ? (
|
||||
<Link className={`${s.cardLink} mt-4`} to={routes.demo(live.slug)}>
|
||||
<Link
|
||||
aria-label={`${live.title} — play it`}
|
||||
className={`${s.cardLink} mt-4`}
|
||||
to={routes.demo(live.slug)}
|
||||
>
|
||||
<span className={`${s.pill} self-start border-positive/30 bg-positive/10 text-positive`}>
|
||||
Live demo
|
||||
</span>
|
||||
@@ -125,7 +129,11 @@ export default function VerticalPage() {
|
||||
</span>
|
||||
</Link>
|
||||
) : spec ? (
|
||||
<Link className={`${s.cardLink} mt-4`} to={routes.demo(spec.slug)}>
|
||||
<Link
|
||||
aria-label={`${spec.title} — read the specification`}
|
||||
className={`${s.cardLink} mt-4`}
|
||||
to={routes.demo(spec.slug)}
|
||||
>
|
||||
<span className={`${s.pill} self-start gap-1`}>
|
||||
<FileText aria-hidden="true" className="size-3.5" />
|
||||
Published specification
|
||||
@@ -169,7 +177,11 @@ export default function VerticalPage() {
|
||||
{/* ── Move along the ranking ─────────────────────────────────────── */}
|
||||
<nav aria-label="Other verticals" className="mt-12 grid gap-3 sm:grid-cols-2">
|
||||
{previous ? (
|
||||
<Link className={`${s.cardLink} sm:items-start`} to={routes.vertical(previous.slug)}>
|
||||
<Link
|
||||
aria-label={`Previous: ${previous.title}`}
|
||||
className={`${s.cardLink} sm:items-start`}
|
||||
to={routes.vertical(previous.slug)}
|
||||
>
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted">
|
||||
<ArrowLeft aria-hidden="true" className="size-3.5" />
|
||||
Rank {previous.rank}
|
||||
@@ -181,6 +193,7 @@ export default function VerticalPage() {
|
||||
)}
|
||||
{next ? (
|
||||
<Link
|
||||
aria-label={`Next: ${next.title}`}
|
||||
className={`${s.cardLink} sm:col-start-2 sm:items-end sm:text-right`}
|
||||
to={routes.vertical(next.slug)}
|
||||
>
|
||||
|
||||
+10
-41
@@ -15,7 +15,7 @@
|
||||
* site is arguing.
|
||||
*/
|
||||
|
||||
import { Component, type ReactNode } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
createBrowserRouter,
|
||||
isRouteErrorResponse,
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type RouteObject,
|
||||
} from 'react-router-dom';
|
||||
import { getDemo, loadDemoModule } from '@/lib/demo-kit/registry';
|
||||
import { DemoErrorBoundary } from '@/components/demo/DemoErrorBoundary';
|
||||
import { SiteFooter } from '@/components/site/SiteFooter';
|
||||
import { SiteHeader } from '@/components/site/SiteHeader';
|
||||
import { SkipLink } from '@/components/site/SkipLink';
|
||||
@@ -132,7 +133,7 @@ function RootErrorBoundary(): ReactNode {
|
||||
);
|
||||
}
|
||||
|
||||
function DemoErrorBoundary(): ReactNode {
|
||||
function DemoRouteError(): ReactNode {
|
||||
const error = useRouteError();
|
||||
if (isRouteErrorResponse(error) && error.status === 404) {
|
||||
return (
|
||||
@@ -153,42 +154,6 @@ function DemoErrorBoundary(): ReactNode {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches errors thrown while a demo's own components RENDER.
|
||||
*
|
||||
* The route error boundary above only sees loader and lazy-import failures; a
|
||||
* demo whose `Surface` throws on a malformed board state would still white-page
|
||||
* the app without this.
|
||||
*/
|
||||
export class DemoRenderBoundary extends Component<
|
||||
{ children: ReactNode },
|
||||
{ error: Error | null }
|
||||
> {
|
||||
override state: { error: Error | null } = { error: null };
|
||||
|
||||
static getDerivedStateFromError(error: unknown): { error: Error } {
|
||||
return { error: error instanceof Error ? error : new Error(String(error)) };
|
||||
}
|
||||
|
||||
override componentDidCatch(error: unknown): void {
|
||||
console.error('[demo] render failed', error);
|
||||
}
|
||||
|
||||
override render(): ReactNode {
|
||||
const { error } = this.state;
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorCard
|
||||
heading="This demo failed to render"
|
||||
body="Only this demo is affected. The recorded runs and the environment source in the repository are unaffected by a bug in the viewer."
|
||||
detail={error.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
/** Shown while a lazy route's chunk is in flight. */
|
||||
function RouteFallback(): ReactNode {
|
||||
return (
|
||||
@@ -244,14 +209,18 @@ export const routes: RouteObject[] = [
|
||||
{
|
||||
path: 'demos/:slug',
|
||||
loader: demoLoader,
|
||||
errorElement: <DemoErrorBoundary />,
|
||||
errorElement: <DemoRouteError />,
|
||||
// Two boundaries, because they catch different things. `errorElement`
|
||||
// above catches a loader or chunk failure; this one catches a demo whose
|
||||
// own Surface throws while rendering a board state, which the router
|
||||
// never sees.
|
||||
lazy: async () => {
|
||||
const { default: DemoPage } = await import('@/pages/DemoPage');
|
||||
return {
|
||||
Component: () => (
|
||||
<DemoRenderBoundary>
|
||||
<DemoErrorBoundary>
|
||||
<DemoPage />
|
||||
</DemoRenderBoundary>
|
||||
</DemoErrorBoundary>
|
||||
),
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user