Pin seed->word across both languages with a shared hash

engine.ts used mulberry32 and engine.py used random.Random(seed). Same seed,
different word — so every ?seed= permalink on the site would have shown a
different puzzle than the recorded run it claimed to be replaying, and nobody
would have noticed until someone checked one by hand.

Both now derive the index from FNV-1a 32-bit over the decimal seed. A hash
rather than a PRNG because there is no honest one-line JavaScript equivalent of
Mersenne Twister, and this way there is nothing to keep in step: both sides
compute the same integer from the same string. Math.imul on the JS side is
load-bearing — a plain multiply overflows into a double and diverges after the
first few bytes.

Twelve seeds are pinned as a vector in both test suites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
karti-ai
2026-08-28 15:42:17 -07:00
parent a56f097f28
commit 69607fbfe9
22 changed files with 3508 additions and 86 deletions
+15
View File
@@ -72,6 +72,21 @@ def test_seeded_answer_is_deterministic() -> None:
assert answer_for_seed(1) != answer_for_seed(2)
# Pinned so the browser cannot drift. `src/demos/wordle/__tests__/engine.test.ts`
# asserts the same twelve words for the same twelve seeds. If a language's
# built-in RNG were used instead of the shared hash, these two lists would
# differ and every ?seed= permalink would show a different word than the
# recorded run it claims to replay.
SEED_VECTORS = [
"wants", "amber", "spume", "toady", "divot", "filly",
"bobby", "clews", "hikes", "lawns", "wreak", "twist",
]
def test_seed_vectors_match_the_typescript_port() -> None:
assert [answer_for_seed(s) for s in range(12)] == SEED_VECTORS
def test_rejections_do_not_consume_a_row() -> None:
game = Game(seed=3, answer="tares")
assert game.play("zzzzz")[1] is not None # not a word
+18 -7
View File
@@ -10,7 +10,6 @@ from __future__ import annotations
import hashlib
import json
import random
from collections import Counter
from dataclasses import dataclass, field
from functools import lru_cache
@@ -105,15 +104,27 @@ def hard_mode_violation(guess: str, prev_guess: str, prev_pattern: str) -> str |
return None
def answer_for_seed(seed: int) -> str:
"""The hidden word for a seed.
def fnv1a32(text: str) -> int:
"""FNV-1a, 32-bit. Chosen because it is trivial to reproduce exactly.
Seeded from a dedicated Random rather than the module-global one, because
TextArena seeds the process-global RNG and anything sharing it becomes
order-dependent under concurrency.
A language's built-in RNG is not portable: `random.Random(7)` is a
Mersenne Twister and there is no honest one-line JavaScript equivalent, so
seed 7 would pick one word here and a different one in the browser. Every
permalink on the site would then disagree with the recorded run it claims
to show. A hash sidesteps the whole problem — both sides compute the same
integer from the same string, and there is nothing to keep in step.
"""
h = 0x811C9DC5
for byte in text.encode():
h ^= byte
h = (h * 0x01000193) & 0xFFFFFFFF
return h
def answer_for_seed(seed: int) -> str:
"""The hidden word for a seed. Identical in engine.ts — see fnv1a32."""
pool = answers()
return pool[random.Random(seed).randrange(len(pool))]
return pool[fnv1a32(str(seed)) % len(pool)]
@dataclass
+71 -3
View File
@@ -316,17 +316,43 @@ export function literalAfter(src, anchor, open = '{') {
return null;
}
/** Marker key on the stand-in an unresolvable identifier evaluates to. */
export const UNRESOLVED = '__pigUnresolvedIdentifier__';
export const isUnresolved = (v) => Boolean(v) && typeof v === 'object' && UNRESOLVED in v;
/**
* A sandbox in which every free identifier resolves to a labelled stand-in.
*
* `RewardSpec.source.code` is legitimately an identifier — the Python is
* imported with `?raw` and cannot exist in plain Node — so the reward literal
* must be readable WITHOUT its `code`. Strict mode is still the default: a
* weight that turns out to be a stand-in is a contract failure, not a skip.
*/
function lenientSandbox() {
return new Proxy(Object.create(null), {
has: () => true,
get: (_target, key) => {
if (key === Symbol.unscopables) return undefined;
if (typeof key !== 'string') return undefined;
return { [UNRESOLVED]: key };
},
});
}
/**
* Evaluates a TypeScript object/array literal as plain data.
*
* `as const` and `satisfies T` are stripped from code spans only. Anything else
* a literal might carry — an identifier, a call, a spread of an import — throws,
* and callers turn that into a contract failure with the file named.
* and callers turn that into a contract failure with the file named. Pass
* `{lenient: true}` to get stand-ins for free identifiers instead.
*
* @param {string} text
* @param {string} label
* @param {{lenient?: boolean}} [options]
*/
export function evalLiteral(text, label) {
export function evalLiteral(text, label, options = {}) {
const js = segment(text)
.map((s) =>
s.code
@@ -338,7 +364,8 @@ export function evalLiteral(text, label) {
)
.join('');
try {
const value = vm.runInNewContext(`(${js})`, Object.create(null), { timeout: 2000 });
const sandbox = options.lenient ? lenientSandbox() : Object.create(null);
const value = vm.runInNewContext(`(${js})`, sandbox, { timeout: 2000 });
return { ok: true, value, error: null };
} catch (error) {
return {
@@ -421,6 +448,47 @@ export function loadAllMetas() {
return { metas, errors };
}
/** Every .ts/.tsx file that belongs to one demo. */
export function demoFiles(slug) {
return walk(path.join(DEMOS_DIR, slug), (f) => /\.tsx?$/.test(f));
}
/**
* Finds one named literal anywhere inside a demo's own source.
*
* A demo is free to put `reward` in `reward.ts` or inline it in `demo.tsx`;
* the contract is about the values, not the file layout. First match in
* filename order wins, and the file it came from is returned so failures can
* name it.
*
* @param {string} slug
* @param {RegExp[]} anchors
* @param {'{' | '['} open
* @param {string} label
* @param {{lenient?: boolean}} [options]
*/
export function findInDemo(slug, anchors, open, label, options = {}) {
for (const file of demoFiles(slug)) {
const src = read(file);
for (const anchor of anchors) {
let text = null;
try {
text = literalAfter(src, anchor, open);
} catch (error) {
return { file: rel(file), error: `could not brace-match the ${label} literal: ${error.message}` };
}
if (!text) continue;
const result = evalLiteral(text, label, options);
if (!result.ok) return { file: rel(file), error: result.error, text };
return { file: rel(file), value: result.value, text };
}
}
return {
file: null,
error: `no ${label} literal found in any .ts/.tsx file under ${rel(path.join(DEMOS_DIR, slug))}`,
};
}
/* ------------------------------------------------------------------ verticals */
export const VERTICALS_FILE = abs('src', 'content', 'verticals.ts');
+82
View File
@@ -0,0 +1,82 @@
import type { ModelCall } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
import { DASH, formatInt, formatMs, humaniseToken } from './format';
export interface ModelCallPanelProps {
call: ModelCall | null;
className?: string;
title?: string;
}
interface Row {
label: string;
value: string;
hint?: string;
}
/**
* The model call, straight off the trace.
*
* Nothing here is computed, averaged or estimated. Every field is a value the
* recorder wrote down, and a field the recorder did not write down renders as
* an em dash — never as zero, and never quietly omitted. A provider that does
* not report reasoning tokens is a fact about the trace, and hiding the row
* would turn "we do not know" into "there were none".
*/
export function ModelCallPanel({ call, className, title = 'Model call' }: ModelCallPanelProps) {
const rows: Row[] = [
{
label: 'finish_reason',
value: call?.finishReason ? humaniseToken(call.finishReason) : DASH,
hint: 'Why the model stopped generating',
},
{
label: 'Prompt tokens',
value: formatInt(call?.promptTokens ?? null),
hint: 'Everything sent in: system, board, history',
},
{
label: 'Completion tokens',
value: formatInt(call?.completionTokens ?? null),
hint: 'The visible reply',
},
{
label: 'Reasoning tokens',
value: formatInt(call?.reasoningTokens ?? null),
hint: 'Billed thinking, when the provider reports it',
},
{
label: 'Latency',
value: formatMs(call?.durationMs ?? null),
hint: 'Real elapsed time when the run was recorded',
},
];
return (
<section aria-label={title} className={cn('card overflow-hidden', className)}>
<header className="border-b border-border px-3 py-2">
<h3 className="text-sm font-semibold">{title}</h3>
</header>
{call === null ? (
<p className="px-3 py-3 text-sm text-muted">
This step did not involve a model call it is a state change the environment made on
its own.
</p>
) : (
<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="min-w-0 flex-1">
<span className="block text-sm font-medium">{row.label}</span>
{row.hint ? (
<span className="block text-xs leading-snug text-muted">{row.hint}</span>
) : null}
</dt>
<dd className="nums shrink-0 font-mono text-sm text-fg">{row.value}</dd>
</div>
))}
</dl>
)}
</section>
);
}
+83
View File
@@ -0,0 +1,83 @@
import { useState } from 'react';
import { Drawer } from 'vaul';
import { Brain, ChevronUp } from 'lucide-react';
import { cn } from '@/lib/utils';
import { ReasoningPanel } from './ReasoningPanel';
import type { ReasoningPanelProps } from './ReasoningPanel';
const SNAP_POINTS = [0.4, 0.9];
export interface ReasoningDrawerProps extends ReasoningPanelProps {
/** Controlled from the shell when it wants the drawer open on a step change. */
open?: boolean;
onOpenChange?: (open: boolean) => void;
triggerClassName?: string;
}
/**
* The reasoning panel, for a phone.
*
* Below `lg` there is no room for a column beside the board, and putting the
* reasoning under the board means the visitor watches the run with the thinking
* off-screen. A drawer at 40% shows the first few lines without covering the
* board; dragging to 90% is the "let me actually read this" gesture.
*
* Rendering is caller-gated rather than CSS-gated: mounting a vaul drawer on
* desktop and hiding it with `lg:hidden` still locks body scroll when it opens,
* so the shell mounts this only under `lg`.
*/
export function ReasoningDrawer({
open,
onOpenChange,
triggerClassName,
...panel
}: ReasoningDrawerProps) {
const [snap, setSnap] = useState<number | string | null>(SNAP_POINTS[0] ?? 0.4);
const hasReasoning = (panel.reasoning ?? '').length > 0;
return (
<Drawer.Root
snapPoints={SNAP_POINTS}
activeSnapPoint={snap}
setActiveSnapPoint={setSnap}
{...(open === undefined ? {} : { open })}
{...(onOpenChange ? { onOpenChange } : {})}
>
<Drawer.Trigger
className={cn(
'tap flex w-full items-center gap-2 rounded-lg border border-border bg-surface px-3 py-2 text-sm font-medium transition-colors duration-2 ease-enter hover:bg-surface-2',
triggerClassName,
)}
>
<Brain className="h-4 w-4 text-muted" aria-hidden="true" />
<span>{hasReasoning ? 'Read the reasoning' : 'No reasoning on this step'}</span>
<ChevronUp className="ml-auto h-4 w-4 text-muted" aria-hidden="true" />
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 z-40 bg-fg/40" />
<Drawer.Content
className="fixed inset-x-0 bottom-0 z-50 mx-auto flex h-full max-h-[97%] max-w-canvas flex-col rounded-t-xl border border-border bg-surface outline-none"
style={{ paddingBottom: 'var(--safe-bottom)' }}
>
<div
aria-hidden="true"
className="mx-auto mt-2 h-1.5 w-12 shrink-0 rounded-full bg-border"
/>
<div className="px-4 pb-2 pt-3">
<Drawer.Title className="text-sm font-semibold">
{panel.title ?? 'Reasoning'}
</Drawer.Title>
<Drawer.Description className="text-xs text-muted">
Recorded verbatim from step {panel.stepIndex + 1} of this run.
</Drawer.Description>
</div>
<div className="min-h-0 flex-1 overflow-hidden px-4 pb-4">
{/* The panel keeps its own reserved height inside the sheet so the
sheet does not resize as the text streams under the drag. */}
<ReasoningPanel {...panel} className="h-full border-0" reservedLines={14} />
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}
+144
View File
@@ -0,0 +1,144 @@
import { useEffect, useRef, useState } from 'react';
import * as ScrollArea from '@radix-ui/react-scroll-area';
import { Brain } from 'lucide-react';
import { cn } from '@/lib/utils';
import { usePrefersReducedMotion } from './format';
import type { PlaybackSpeed } from './TracePlayer';
/**
* Characters per second, clamped. A 40-character reasoning trace recorded over
* 30 seconds would otherwise crawl at 1.3 chars/s and read as a hung page,
* and a 6,000-character one recorded in 800 ms would flash past unread.
*/
const MIN_CPS = 24;
const MAX_CPS = 900;
const FALLBACK_CPS = 90;
export interface ReasoningPanelProps {
reasoning: string | null;
/** The recorded latency of the call this reasoning came from. */
durationMs: number | null;
playing: boolean;
speed: PlaybackSpeed;
/** Changing this restarts the stream. Pass the step index. */
stepIndex: number;
/**
* Lines of height held open whether or not there is text. Reserving the box
* is not a nicety: this panel sits beside the board, and letting it grow as
* the text arrives shoves the board down the page mid-run.
*/
reservedLines?: number;
title?: string;
className?: string;
}
function charsPerSecond(length: number, durationMs: number | null): number {
if (!durationMs || durationMs <= 0 || length === 0) return FALLBACK_CPS;
return Math.min(Math.max((length / durationMs) * 1000, MIN_CPS), MAX_CPS);
}
export function ReasoningPanel({
reasoning,
durationMs,
playing,
speed,
stepIndex,
reservedLines = 10,
title = 'Reasoning',
className,
}: ReasoningPanelProps) {
const reducedMotion = usePrefersReducedMotion();
const full = reasoning ?? '';
const [visible, setVisible] = useState(full.length);
const viewportRef = useRef<HTMLDivElement | null>(null);
// Whether this step's text should stream at all. Scrubbing to a step while
// paused shows it whole — someone reading at their own pace is not asking to
// be typed at.
const shouldStream = playing && speed !== 'instant' && !reducedMotion && full.length > 0;
useEffect(() => {
if (!shouldStream) {
setVisible(full.length);
return;
}
setVisible(0);
const cps = charsPerSecond(full.length, durationMs) * (typeof speed === 'number' ? speed : 1);
const started = performance.now();
let frame = 0;
let last = -1;
const tick = (now: number) => {
const next = Math.min(Math.floor(((now - started) / 1000) * cps), full.length);
// Only re-render when a character actually lands; at 120 Hz the naive
// version re-renders the whole panel twice per revealed character.
if (next !== last) {
last = next;
setVisible(next);
const viewport = viewportRef.current;
if (viewport) viewport.scrollTop = viewport.scrollHeight;
}
if (next < full.length) frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, [shouldStream, full, durationMs, speed, stepIndex]);
const streaming = visible < full.length;
return (
<section
aria-label={title}
className={cn('card flex flex-col overflow-hidden', className)}
>
<header className="flex items-center gap-2 border-b border-border px-3 py-2">
<Brain className="h-4 w-4 text-muted" aria-hidden="true" />
<h3 className="text-sm font-semibold">{title}</h3>
{streaming ? (
<span className="nums ml-auto text-xs text-muted">
{visible}/{full.length}
</span>
) : null}
</header>
<ScrollArea.Root
type="auto"
className="min-h-0 flex-1"
// Height, not min-height: the panel is the same size on every step,
// including the ones with no reasoning at all.
style={{ height: `${reservedLines * 1.45}rem` }}
>
<ScrollArea.Viewport
ref={viewportRef}
className="h-full w-full px-3 py-2.5"
// The shell owns the page's single polite live region. A streaming
// region here would read every partial word over the top of it.
aria-live="off"
>
{full.length === 0 ? (
<p className="text-sm leading-relaxed text-muted">
This step has no recorded reasoning. The run was captured with thinking disabled, so
there is nothing to show here which is different from the model having thought
nothing.
</p>
) : (
<p className="whitespace-pre-wrap font-mono text-[13px] leading-relaxed text-fg">
{full.slice(0, visible)}
{streaming ? (
<span
aria-hidden="true"
className="ml-px inline-block h-[1em] w-[0.5ch] translate-y-[0.15em] animate-pulse bg-brand align-baseline"
/>
) : null}
</p>
)}
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
orientation="vertical"
className="flex w-2 touch-none select-none p-0.5"
>
<ScrollArea.Thumb className="flex-1 rounded-full bg-border" />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</section>
);
}
+172
View File
@@ -0,0 +1,172 @@
import { Scale, Target, Weight } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import type { RewardComponent, RewardSpec, RewardValues } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
import { DASH, formatNumber, formatOrDash } from './format';
import { scoreReward } from './reward-math';
import { EditedChip } from './StatStrip';
const ROLE_META: Record<RewardComponent['role'], { label: string; Icon: LucideIcon }> = {
objective: { label: 'Objective', Icon: Target },
counterweight: { label: 'Counterweight', Icon: Weight },
gate: { label: 'Gate', Icon: Scale },
};
export interface RewardBreakdownProps {
spec: RewardSpec;
values: RewardValues;
/** Unweighted diagnostics. Rendered, never summed — the contract is explicit. */
metrics?: Record<string, number | null>;
/** Overridden weights from the editor. Absent means the shipped weights. */
weights?: Record<string, number>;
/** Set when `weights` came from the visitor rather than the environment. */
edited?: boolean;
className?: string;
}
/**
* `score x weight = value`, per component, plus the total.
*
* The counterweight row is called out because it is the row that makes the
* reward an opinion rather than a scoreboard. Everyone understands "pay for
* solving it". The interesting engineering is the term that takes points away,
* and an exec who leaves this page understanding only that has got the point.
*/
export function RewardBreakdown({
spec,
values,
metrics,
weights,
edited = false,
className,
}: RewardBreakdownProps) {
const { rows, total } = scoreReward(spec, values, weights);
const counterweights = rows.filter((row) => row.component.role === 'counterweight');
return (
<div className={cn('card overflow-hidden', className)}>
<table className="w-full border-collapse text-sm">
<caption className="sr-only">
Reward components, their weights and their contribution to the total score
</caption>
<thead>
<tr className="border-b border-border text-left text-xs uppercase tracking-wide text-muted">
<th scope="col" className="px-3 py-2 font-medium">
Component
</th>
<th scope="col" className="px-2 py-2 text-right font-medium">
Score
</th>
<th scope="col" className="px-2 py-2 text-right font-medium">
Weight
</th>
<th scope="col" className="px-3 py-2 text-right font-medium">
Value
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{rows.map((row) => {
const role = ROLE_META[row.component.role];
const isCounterweight = row.component.role === 'counterweight';
return (
<tr
key={row.component.key}
className={cn(isCounterweight && 'bg-accent-subtle/40')}
>
<th scope="row" className="max-w-0 px-3 py-2.5 text-left font-normal">
<span className="flex flex-wrap items-center gap-1.5">
<span className="font-medium text-fg">{row.component.label}</span>
<span
className={cn(
'inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
isCounterweight
? 'bg-brand/15 text-accent-fg'
: 'bg-surface-2 text-muted',
)}
>
<role.Icon className="h-3 w-3" aria-hidden="true" />
{role.label}
</span>
</span>
<span className="mt-0.5 block text-xs leading-snug text-muted">
{row.component.description}
</span>
<span className="mt-0.5 block font-mono text-[11px] text-muted">
{row.component.key}
</span>
</th>
<td className="nums px-2 py-2.5 text-right align-top font-mono">
{row.score === null ? (
<span className="text-muted" title="The environment did not score this run">
not scored
</span>
) : (
formatNumber(row.score)
)}
</td>
<td className="nums px-2 py-2.5 text-right align-top font-mono">
<span className={cn(edited && 'text-accent-fg')}>
{formatNumber(row.weight, 2)}
</span>
</td>
<td className="nums px-3 py-2.5 text-right align-top font-mono font-semibold">
{row.value === null ? DASH : formatNumber(row.value)}
</td>
</tr>
);
})}
</tbody>
<tfoot>
<tr className="border-t-2 border-border bg-surface-2">
<th scope="row" className="px-3 py-2.5 text-left">
<span className="flex items-center gap-2 font-semibold">
Total reward
{edited ? <EditedChip /> : null}
</span>
</th>
<td colSpan={2} />
<td className="nums px-3 py-2.5 text-right font-mono text-base font-semibold">
{total === null ? (
<span className="text-muted">not scored</span>
) : (
formatOrDash(total)
)}
</td>
</tr>
</tfoot>
</table>
{counterweights.length > 0 ? (
<p className="border-t border-border bg-accent-subtle/40 px-3 py-2.5 text-xs leading-relaxed text-fg">
<span className="font-semibold">
{counterweights.map((row) => row.component.label).join(' and ')}
</span>{' '}
{counterweights.length > 1 ? 'are counterweights' : 'is the counterweight'}: without a
term pulling the other way, the cheapest way to maximise the objective is a behaviour
you would never ship, and the model will find it.
</p>
) : null}
{spec.metrics && spec.metrics.length > 0 ? (
<div className="border-t border-border px-3 py-2.5">
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted">
Diagnostics reported, never summed
</h4>
<dl className="mt-1.5 grid gap-x-4 gap-y-1 sm:grid-cols-2">
{spec.metrics.map((metric) => (
<div key={metric.key} className="flex items-baseline justify-between gap-2">
<dt className="text-xs text-muted" title={metric.description}>
{metric.label}
</dt>
<dd className="nums font-mono text-xs">
{formatOrDash(metrics?.[metric.key] ?? null)}
</dd>
</div>
))}
</dl>
</div>
) : null}
</div>
);
}
+164
View File
@@ -0,0 +1,164 @@
import { useEffect, useRef } from 'react';
import type { ComponentType, KeyboardEvent } from 'react';
import type { DemoStep } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
import { DASH, formatInt, formatMs, humaniseToken, usePrefersReducedMotion } from './format';
export interface StepTimelineProps<T> {
steps: DemoStep<T>[];
current: number;
onSelect: (index: number) => void;
/** The demo's own board, drawn small. Omit and the chips are text only. */
Surface?: ComponentType<{ state: T; compact?: boolean }>;
/** Bound to Space, per the transport convention on the rest of the page. */
onTogglePlay?: () => void;
className?: string;
}
/**
* The scrubber: one chip per recorded step, each carrying its own board
* preview and its own real numbers.
*
* Why numbers on a chip at all — the chips are the only place the cost of the
* run is visible without opening a panel, and "this took 6 calls and 4,200
* tokens" is the sentence a buyer repeats to their CFO.
*
* Keyboard: this is a radiogroup, so arrows move AND select, which is the
* standard pattern. Space is bound to play/pause rather than to select — a
* deliberate break from the radio pattern, because the timeline sits under a
* transport bar where Space means play everywhere else, and being internally
* consistent beats being technically canonical. It is advertised on the group
* with `aria-keyshortcuts`.
*/
export function StepTimeline<T>({
steps,
current,
onSelect,
Surface,
onTogglePlay,
className,
}: StepTimelineProps<T>) {
const reducedMotion = usePrefersReducedMotion();
const chipRefs = useRef<(HTMLDivElement | null)[]>([]);
const scrollerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const chip = chipRefs.current[current];
const scroller = scrollerRef.current;
if (!chip || !scroller) return;
// `scrollIntoView` on the element would also scroll the PAGE to the
// timeline on every step, which is intolerable while the run plays. Scroll
// the strip only.
const chipBox = chip.getBoundingClientRect();
const viewBox = scroller.getBoundingClientRect();
const delta = chipBox.left - viewBox.left - (viewBox.width - chipBox.width) / 2;
scroller.scrollBy({ left: delta, behavior: reducedMotion ? 'auto' : 'smooth' });
}, [current, reducedMotion]);
const move = (next: number, event: KeyboardEvent) => {
event.preventDefault();
const clamped = Math.min(Math.max(next, 0), steps.length - 1);
onSelect(clamped);
chipRefs.current[clamped]?.focus();
};
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
switch (event.key) {
case 'ArrowRight':
case 'ArrowDown':
move(current + 1, event);
break;
case 'ArrowLeft':
case 'ArrowUp':
move(current - 1, event);
break;
case 'Home':
move(0, event);
break;
case 'End':
move(steps.length - 1, event);
break;
case ' ':
case 'Spacebar':
if (onTogglePlay) {
event.preventDefault();
onTogglePlay();
}
break;
default:
break;
}
};
return (
<div
ref={scrollerRef}
role="radiogroup"
aria-label="Steps in the recorded run"
aria-keyshortcuts="ArrowLeft ArrowRight Home End Space"
onKeyDown={handleKeyDown}
className={cn('flex snap-x gap-2 overflow-x-auto pb-2', className)}
>
{steps.map((step, index) => {
const selected = index === current;
const call = step.call;
return (
// A div rather than a button: the chip embeds the demo's own Surface,
// and a Surface is a grid of divs. Nesting flow content inside a
// <button> is invalid HTML and browsers reflow it unpredictably. The
// radio role plus the group's key handling gives the same semantics.
<div
key={step.index}
role="radio"
aria-checked={selected}
// Roving tabindex: one stop for the whole strip, arrows inside it.
tabIndex={selected ? 0 : -1}
ref={(node) => {
chipRefs.current[index] = node;
}}
onClick={() => onSelect(index)}
className={cn(
'tap w-[9.5rem] shrink-0 snap-center rounded-lg border p-2 text-left transition-colors duration-2 ease-enter',
selected
? 'border-brand bg-accent-subtle/60'
: 'border-border bg-surface hover:bg-surface-2',
)}
>
<span className="flex items-baseline justify-between gap-1">
<span className="nums text-xs font-semibold">Step {index + 1}</span>
{call?.finishReason ? (
<span
className="max-w-[4.5rem] truncate text-[10px] uppercase tracking-wide text-muted"
title={call.finishReason}
>
{humaniseToken(call.finishReason)}
</span>
) : null}
</span>
{Surface ? (
<span className="mt-1.5 block overflow-hidden rounded-md bg-surface-2 p-1.5">
<Surface state={step.state} compact />
</span>
) : null}
{step.caption ? (
<span className="mt-1.5 block truncate text-xs text-muted" title={step.caption}>
{step.caption}
</span>
) : null}
<span className="nums mt-1 flex flex-wrap gap-x-2 text-[10px] text-muted">
<span title="Completion tokens">
{call?.completionTokens === null || call?.completionTokens === undefined
? DASH
: `${formatInt(call.completionTokens)} tok`}
</span>
<span title="Recorded latency">{formatMs(call?.durationMs ?? null)}</span>
</span>
</div>
);
})}
</div>
);
}
+244
View File
@@ -0,0 +1,244 @@
import { useMemo, useState } from 'react';
import { CheckCircle2, ChevronDown, HelpCircle, XCircle } from 'lucide-react';
import { verifyEpisode } from '@/lib/demo-kit';
import type { DemoEpisode, DemoModule, RewardValues } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
import { DASH, formatDelta, formatNumber, formatOrDash } from './format';
import { rewardDelta, scoreReward } from './reward-math';
/**
* Float tolerance for "the browser agrees with Python".
*
* 1e-9 would be theatre: the two runtimes accumulate a sum in a different
* order, and IEEE-754 does not promise associativity. 1e-6 is well below any
* difference a reward change would produce and well above the noise.
*/
const EPSILON = 1e-6;
type Verdict =
| { kind: 'match'; recomputed: RewardValues; recordedTotal: number | null; recomputedTotal: number | null; maxDelta: number; rows: VerifyRow[] }
| { kind: 'mismatch'; recomputed: RewardValues; recordedTotal: number | null; recomputedTotal: number | null; maxDelta: number; rows: VerifyRow[]; guilty: string[] }
| { kind: 'unverifiable'; reason: string };
interface VerifyRow {
key: string;
label: string;
recorded: number | null;
recomputed: number | null;
delta: number;
}
export interface VerifyBadgeProps<T> {
demo: DemoModule<T>;
episode: DemoEpisode;
className?: string;
/** Open the receipt on load. The sceptic we are writing for opens it anyway. */
defaultOpen?: boolean;
}
/**
* The receipt.
*
* This object exists for one person: the engineer sitting next to the CEO who
* assumes the numbers on a vendor's demo page are hard-coded. It re-runs every
* recorded move through the TypeScript engine in the visitor's own browser,
* rescores it, and prints the comparison — including the delta, to seven
* decimals, because a comparison without a delta is an assertion.
*
* It must therefore be allowed to FAIL loudly. A verifier that silently
* degrades to "verified" when it cannot check anything is worse than no
* verifier: it teaches the sceptic that the badge is decoration.
*/
export function VerifyBadge<T>({ demo, episode, className, defaultOpen = false }: VerifyBadgeProps<T>) {
const [open, setOpen] = useState(defaultOpen);
const verdict = useMemo<Verdict>(() => {
if (!demo.verify) {
return {
kind: 'unverifiable',
reason:
'This demo does not ship a browser-side engine, so the recorded scores cannot be re-derived here. The Python that produced them is in the repository and the eval command is below.',
};
}
let recomputed: RewardValues | null;
try {
recomputed = verifyEpisode(demo, episode);
} catch (error) {
// A verifier that throws is a bug on our side, not a failed run. Say so
// rather than showing a red mismatch that blames the recorded numbers.
return {
kind: 'unverifiable',
reason: `The in-browser verifier threw while re-running this episode: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
if (recomputed === null) {
return {
kind: 'unverifiable',
reason: episode.truncated
? 'This run was truncated before the environment reached a terminal state, so there is nothing complete to re-score. The recorded partial numbers are shown as they were captured.'
: 'The environment could not re-derive this episode from the recorded transcript. Nothing here is being asserted as verified.',
};
}
const deltas = rewardDelta(episode.rewards, recomputed);
const labels = new Map(demo.reward.components.map((c) => [c.key, c.label]));
const rows: VerifyRow[] = deltas
.map(({ key, delta }) => ({
key,
label: labels.get(key) ?? key,
recorded: episode.rewards[key] ?? null,
recomputed: recomputed[key] ?? null,
delta,
}))
.sort((a, b) => b.delta - a.delta || a.key.localeCompare(b.key));
const recordedTotal = scoreReward(demo.reward, episode.rewards).total;
const recomputedTotal = scoreReward(demo.reward, recomputed).total;
const totalDelta =
recordedTotal === null || recomputedTotal === null
? recordedTotal === recomputedTotal
? 0
: Number.POSITIVE_INFINITY
: Math.abs(recordedTotal - recomputedTotal);
const maxDelta = rows.reduce((worst, row) => Math.max(worst, row.delta), totalDelta);
const guilty = rows.filter((row) => row.delta > EPSILON).map((row) => row.label);
if (guilty.length === 0 && maxDelta <= EPSILON) {
return { kind: 'match', recomputed, recordedTotal, recomputedTotal, maxDelta, rows };
}
return { kind: 'mismatch', recomputed, recordedTotal, recomputedTotal, maxDelta, rows, guilty };
}, [demo, episode]);
if (verdict.kind === 'unverifiable') {
return (
<section
aria-label="Verification"
className={cn('card border-border bg-surface-2 p-3', className)}
>
<p className="flex items-start gap-2 text-sm">
<HelpCircle className="mt-0.5 h-4 w-4 shrink-0 text-muted" aria-hidden="true" />
<span>
<span className="font-semibold">Unverifiable in your browser.</span>{' '}
<span className="text-muted">{verdict.reason}</span>
</span>
</p>
</section>
);
}
const matched = verdict.kind === 'match';
return (
<section
aria-label="Verification"
className={cn(
'card overflow-hidden',
matched ? 'border-positive/40' : 'border-danger',
className,
)}
>
<div className={cn('p-3', matched ? 'bg-positive/10' : 'bg-danger/10')}>
<p className="flex items-start gap-2 text-sm leading-relaxed">
{matched ? (
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-positive" aria-hidden="true" />
) : (
<XCircle className="mt-0.5 h-4 w-4 shrink-0 text-danger" aria-hidden="true" />
)}
<span>
{matched ? (
<>
<span className="font-semibold text-positive">Verified in your browser</span>{' '}
<span className="text-fg">
re-ran every move through the TypeScript engine and rescored.
</span>
</>
) : (
<>
<span className="font-semibold text-danger">
Mismatch on {verdict.guilty.join(', ')}
</span>{' '}
<span className="text-fg">
the browser re-run disagrees with the recorded score. Trust the source, not
this page.
</span>
</>
)}
</span>
</p>
<p className="nums mt-1.5 pl-6 font-mono text-xs text-muted">
Recomputed {formatOrDash(verdict.recomputedTotal)} · recorded{' '}
{formatOrDash(verdict.recordedTotal)} · Δ {formatDelta(verdict.maxDelta)}
</p>
</div>
<button
type="button"
onClick={() => setOpen((was) => !was)}
aria-expanded={open}
className="tap flex w-full items-center gap-1.5 border-t border-border px-3 text-left text-xs font-medium text-muted transition-colors duration-2 ease-enter hover:bg-surface-2 hover:text-fg"
>
<ChevronDown
className={cn('h-4 w-4 transition-transform duration-2 ease-enter', open && 'rotate-180')}
aria-hidden="true"
/>
{open ? 'Hide the component-by-component receipt' : 'Show the component-by-component receipt'}
</button>
{open ? (
<div className="overflow-x-auto border-t border-border">
<table className="nums w-full border-collapse font-mono text-xs">
<thead>
<tr className="text-left text-muted">
<th scope="col" className="px-3 py-1.5 font-medium">
Component
</th>
<th scope="col" className="px-2 py-1.5 text-right font-medium">
Recorded
</th>
<th scope="col" className="px-2 py-1.5 text-right font-medium">
Recomputed
</th>
<th scope="col" className="px-3 py-1.5 text-right font-medium">
Δ
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{verdict.rows.map((row) => {
const bad = row.delta > EPSILON;
return (
<tr key={row.key} className={cn(bad && 'bg-danger/10')}>
<th scope="row" className="px-3 py-1.5 text-left font-normal">
{row.label}
</th>
<td className="px-2 py-1.5 text-right">
{row.recorded === null ? DASH : formatNumber(row.recorded, 6)}
</td>
<td className="px-2 py-1.5 text-right">
{row.recomputed === null ? DASH : formatNumber(row.recomputed, 6)}
</td>
<td
className={cn('px-3 py-1.5 text-right', bad ? 'text-danger' : 'text-muted')}
>
{Number.isFinite(row.delta) ? formatDelta(row.delta) : 'not comparable'}
</td>
</tr>
);
})}
</tbody>
</table>
<p className="px-3 py-2 text-[11px] leading-relaxed text-muted">
Tolerance {EPSILON.toExponential()}. The browser engine and the Python environment
can sum the same terms in a different order, and IEEE-754 addition is not
associative, so the comparison is made within a tolerance rather than demanding
bit-identical floats.
</p>
</div>
) : null}
</section>
);
}
+73
View File
@@ -0,0 +1,73 @@
import type { RewardComponent, RewardSpec, RewardValues } from '@/lib/demo-kit/types';
export interface ScoredRow {
component: RewardComponent;
/** The environment's raw per-component score. `null` means NOT SCORED. */
score: number | null;
/** The weight in force — shipped, or the visitor's edit. */
weight: number;
/** `score x weight`, or null when the component was not scored. */
value: number | null;
}
export interface ScoredReward {
rows: ScoredRow[];
/**
* The weighted sum over components that were actually scored. `null` when
* none of them were: a total of 0 would claim the run scored nothing, which
* is a different and much stronger statement than "we could not score it".
*/
total: number | null;
}
/** The weights the environment ships, as a plain map the editor can copy. */
export function shippedWeights(spec: RewardSpec): Record<string, number> {
const out: Record<string, number> = {};
for (const component of spec.components) out[component.key] = component.weight;
return out;
}
export function scoreReward(
spec: RewardSpec,
values: RewardValues,
weights?: Record<string, number>,
): ScoredReward {
let total = 0;
let anyScored = false;
const rows = spec.components.map((component) => {
const raw = values[component.key];
const score = raw === undefined ? null : raw;
const weight = weights?.[component.key] ?? component.weight;
const value = score === null ? null : score * weight;
if (value !== null) {
total += value;
anyScored = true;
}
return { component, score, weight, value };
});
return { rows, total: anyScored ? total : null };
}
/** True when two reward maps agree to within float noise on every key. */
export function rewardDelta(a: RewardValues, b: RewardValues): { key: string; delta: number }[] {
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
const out: { key: string; delta: number }[] = [];
for (const key of keys) {
const left = a[key];
const right = b[key];
// Both absent or both explicitly not-scored is agreement, not a zero
// delta on a number nobody produced.
if ((left === null || left === undefined) && (right === null || right === undefined)) {
out.push({ key, delta: 0 });
continue;
}
if (left === null || left === undefined || right === null || right === undefined) {
// One side scored and the other did not. That is a real disagreement and
// it has no numeric magnitude, so flag it as infinite rather than as 0.
out.push({ key, delta: Number.POSITIVE_INFINITY });
continue;
}
out.push({ key, delta: Math.abs(left - right) });
}
return out;
}
+24 -76
View File
@@ -1,90 +1,38 @@
import {
Boxes,
BookOpen,
Code2,
Database,
FlaskConical,
Gauge,
Grid3x3,
Headset,
Landmark,
LifeBuoy,
Network,
Package,
PhoneCall,
Plane,
RadioTower,
Receipt,
Scale,
ShieldCheck,
ShoppingCart,
Stethoscope,
Target,
Truck,
Wallet,
Zap,
type LucideIcon,
} from 'lucide-react';
import type { Vertical } from '@/lib/demo-kit/types';
import { iconFor } from '@/content/icons';
import { cn } from '@/lib/utils';
/**
* `DemoMeta.icon` is a lucide NAME, not a component — that is deliberate, and
* types.ts says why: importing the component in the meta would drag lucide into
* the entry chunk for every demo at once.
*
* Resolving the name therefore has to happen against a static map. A dynamic
* `import * as lucide` here would work and would also pull all 1,500 icons into
* this chunk, which is the exact cost the contract was avoiding. So: named
* imports, tree-shaken to what is listed, and an unknown name falls back to a
* neutral glyph rather than rendering nothing. If you add a demo whose icon
* lands on the fallback, add the name here — that is the one line the header
* ever needs.
* `DemoMeta.icon` is a lucide NAME, not a component — types.ts explains why:
* a component in the meta would drag lucide into the entry chunk for every
* demo at once. `@/content/icons` is the one place that turns a name back into
* a component, by name, so it stays tree-shaken. Resolve through it rather than
* growing a second map here, or the same icon name renders as two different
* glyphs depending on which surface you are looking at.
*/
const ICONS: Record<string, LucideIcon> = {
BookOpen,
Boxes,
Code2,
Database,
FlaskConical,
Gauge,
Grid3x3,
Headset,
Landmark,
LifeBuoy,
Network,
Package,
PhoneCall,
Plane,
RadioTower,
Receipt,
Scale,
ShieldCheck,
ShoppingCart,
Stethoscope,
Target,
Truck,
Wallet,
Zap,
};
export function DemoIcon({ name, className }: { name: string; className?: string }) {
const Icon = iconFor(name);
return <Icon aria-hidden="true" className={cn('size-4 shrink-0', className)} />;
}
/** Used when a vertical has no icon of its own to offer. */
export const VERTICAL_ICONS: Record<string, string> = {
/**
* The registry groups demos by vertical and gives each group a label, but no
* icon — an icon belongs to a demo, not to a taxonomy key. The header wants one
* anyway, so this is the mapping, and it is exhaustive over the union: adding a
* thirteenth vertical to the contract fails the build here rather than shipping
* a blank square in the menu.
*/
export const VERTICAL_ICONS: Readonly<Record<Vertical, string>> = {
reference: 'Grid3x3',
support: 'Headset',
healthcare: 'Stethoscope',
insurance: 'ShieldCheck',
'financial-crime': 'Landmark',
'financial-crime': 'Siren',
energy: 'Zap',
logistics: 'Truck',
code: 'Code2',
retail: 'ShoppingCart',
code: 'Braces',
retail: 'Tag',
telecom: 'RadioTower',
data: 'Database',
data: 'Table2',
legal: 'Scale',
};
export function DemoIcon({ name, className }: { name: string; className?: string }) {
const Icon = ICONS[name] ?? Boxes;
return <Icon aria-hidden="true" className={cn('size-4 shrink-0', className)} />;
}
+524
View File
@@ -0,0 +1,524 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight, ArrowUpRight, Github, Menu } from 'lucide-react';
import { listDemos, listVerticals, type VerticalGroup } from '@/lib/demo-kit/registry';
import type { DemoMeta } from '@/lib/demo-kit/types';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
import {
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { ContrastToggle } from '@/components/site/ContrastToggle';
import { DemoIcon, VERTICAL_ICONS } from '@/components/site/DemoIcon';
import { PIG_URL, REPO_URL, VERIFIERS_WORDLE_URL } from '@/components/site/links';
import { ThemeToggle } from '@/components/site/ThemeToggle';
import { Wordmark } from '@/components/site/Wordmark';
import { cn } from '@/lib/utils';
/*
* This header is GENERATED. Nothing in it names a demo.
*
* Adding `src/demos/<slug>/` puts that demo in the Demos panel, in its
* vertical, and (if it is the first live one) behind the CTA, with zero edits
* to this file. The only hand-written lists here are the five concepts under
* "How it works", which are properties of the idea rather than of the lineup.
*/
/**
* The real upstream taskset, quoted rather than paraphrased.
*
* A mega-menu that contains source code instead of more links is the cheapest
* signal on the whole site that this is not a brochure it is the first thing
* a technical buyer sees, and it is true before they have clicked anything.
*/
const TASKSET_SOURCE = `class WordleConfig(TextArenaConfig):
game: Literal["Wordle-v0"] = "Wordle-v0"
class WordleTaskset(TextArenaTaskset, vf.Taskset[TextArenaTask, WordleConfig]):
pass`;
const CONCEPTS: readonly { term: string; gloss: string }[] = [
{
term: 'Environment',
gloss: 'The task, the legal moves and the grader, packaged so anyone can install and run it.',
},
{
term: 'Rollout',
gloss: 'One episode. The model acts, the environment answers, and every turn is recorded.',
},
{
term: 'Reward',
gloss: 'The number the run is scored on. You write it, so you decide what "good" means.',
},
{
term: 'Harness',
gloss: 'The runner that plays a taskset against a model and keeps the receipts.',
},
{
term: 'Held-out grading',
gloss: 'Scored on problems the model has never seen, which is the only way the score means anything.',
},
];
/** A vertical group as the header renders it: the registry's group plus a glyph. */
interface HeaderVertical extends VerticalGroup {
icon: string;
}
function useLineup() {
return React.useMemo(() => {
// `listDemos` already returns a fresh array sorted by order then slug, and
// `listVerticals` already drops the empty verticals. Neither needs redoing
// here — if this file starts re-sorting the lineup, the header and the
// gallery will eventually disagree about what "first" means.
const demos = listDemos();
const live = demos.filter((demo) => demo.status === 'live');
const spec = demos.filter((demo) => demo.status === 'spec');
const verticals: HeaderVertical[] = listVerticals().map((group) => ({
...group,
icon: VERTICAL_ICONS[group.vertical],
}));
// The CTA follows the lineup rather than naming a slug. Today the first
// live demo IS the word game, so this resolves to the wordle route; when a
// second one ships ahead of it, the button moves with it and this file does
// not change.
const primary = live[0] ?? demos[0];
const ctaHref = primary ? `/demos/${primary.slug}` : '/demos';
return { demos, live, spec, verticals, ctaHref };
}, []);
}
/* ------------------------------------------------------------------ panels */
function DemoRow({ demo, muted = false }: { demo: DemoMeta; muted?: boolean }) {
return (
<Link
to={`/demos/${demo.slug}`}
className={cn(
'group/row flex gap-3 rounded-lg p-2.5 transition-colors duration-1 ease-enter hover:bg-surface-2',
muted && 'opacity-70 hover:opacity-100',
)}
>
<DemoIcon name={demo.icon} className="mt-0.5 size-4 text-accent-fg" />
<span className="flex min-w-0 flex-col gap-0.5">
<span className="flex items-center gap-2 text-sm font-medium text-fg">
{demo.title}
{demo.status === 'spec' ? (
<Badge variant="outline" className="font-normal">
Spec
</Badge>
) : null}
</span>
<span className="text-xs leading-relaxed text-muted">
{/*
A spec demo shows the BUYER, not the technology: "Head of Claims"
says who is meant to care, where a taskset name says nothing to the
person reading this in a boardroom.
*/}
{demo.status === 'spec' ? `For the ${demo.persona}` : demo.tagline}
</span>
</span>
</Link>
);
}
function PanelHeading({ children }: { children: React.ReactNode }) {
return (
<p className="px-2.5 pb-1 text-xs font-semibold uppercase tracking-wider text-muted">
{children}
</p>
);
}
function DemosPanel({ live, spec }: { live: DemoMeta[]; spec: DemoMeta[] }) {
return (
<div className="w-[min(92vw,720px)]">
<div className="grid grid-cols-2 gap-4 p-4">
<section aria-label="Live now" className="flex flex-col gap-0.5">
<PanelHeading>Live now</PanelHeading>
{live.length > 0 ? (
live.map((demo) => (
<NavigationMenuLink asChild key={demo.slug}>
<DemoRow demo={demo} />
</NavigationMenuLink>
))
) : (
<p className="p-2.5 text-xs text-muted">No interactive demos published yet.</p>
)}
</section>
<section
aria-label="Shipping next"
className="flex flex-col gap-0.5 border-l border-border pl-4"
>
<PanelHeading>Shipping next</PanelHeading>
{spec.length > 0 ? (
spec.map((demo) => (
<NavigationMenuLink asChild key={demo.slug}>
<DemoRow demo={demo} muted />
</NavigationMenuLink>
))
) : (
<p className="p-2.5 text-xs text-muted">Nothing queued.</p>
)}
</section>
</div>
<div className="flex items-center justify-between gap-4 border-t border-border bg-surface-2 px-5 py-3">
<p className="text-xs text-muted">
Every demo replays a real rollout from a real environment.
</p>
<NavigationMenuLink asChild>
<Link
to="/demos"
className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline"
>
Browse all demos
<ArrowRight aria-hidden="true" className="size-3.5" />
</Link>
</NavigationMenuLink>
</div>
</div>
);
}
function VerticalsPanel({ verticals }: { verticals: HeaderVertical[] }) {
return (
<div className="w-[min(92vw,720px)] p-4">
{verticals.length > 0 ? (
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5">
{verticals.map((vertical) => {
// The lead demo is the lowest-order one in the vertical; its reward
// line is what the vertical is actually promising.
const lead = vertical.demos[0]!;
return (
<NavigationMenuLink asChild key={vertical.vertical}>
<Link
to={`/demos/${lead.slug}`}
className="flex gap-3 rounded-lg p-2.5 transition-colors duration-1 ease-enter hover:bg-surface-2"
>
<DemoIcon name={vertical.icon} className="mt-0.5 size-4 text-accent-fg" />
<span className="flex min-w-0 flex-col gap-0.5">
<span className="text-sm font-medium text-fg">{vertical.label}</span>
<span className="text-xs leading-relaxed text-muted">{lead.rewardLine}</span>
</span>
</Link>
</NavigationMenuLink>
);
})}
</div>
) : (
<p className="p-2.5 text-xs text-muted">No verticals in the lineup yet.</p>
)}
</div>
);
}
function TasksetSource({ className }: { className?: string }) {
return (
<div className={cn('flex flex-col gap-2 rounded-lg border border-border bg-bg p-3', className)}>
<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">
<code className="font-mono">{TASKSET_SOURCE}</code>
</pre>
<a
href={VERIFIERS_WORDLE_URL}
target="_blank"
rel="noreferrer noopener"
className="inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline"
>
verifiers/environments/wordle
<ArrowUpRight aria-hidden="true" className="size-3.5" />
</a>
</div>
);
}
function HowItWorksPanel({ ctaHref }: { ctaHref: string }) {
return (
<div className="w-[min(92vw,760px)] p-4">
<div className="grid grid-cols-2 gap-4">
<dl className="flex flex-col gap-3">
{CONCEPTS.map((concept) => (
<div key={concept.term} className="flex flex-col gap-0.5">
<dt className="text-sm font-medium text-fg">{concept.term}</dt>
<dd className="text-xs leading-relaxed text-muted">{concept.gloss}</dd>
</div>
))}
<NavigationMenuLink asChild>
<Link
to={ctaHref}
className="mt-1 inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline"
>
See all five on one recorded run
<ArrowRight aria-hidden="true" className="size-3.5" />
</Link>
</NavigationMenuLink>
</dl>
<TasksetSource />
</div>
</div>
);
}
/* ------------------------------------------------------------------ mobile */
function MobileNav({
live,
spec,
verticals,
ctaHref,
}: {
live: DemoMeta[];
spec: DemoMeta[];
verticals: HeaderVertical[];
ctaHref: string;
}) {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" size="icon-touch" aria-label="Open menu">
<Menu aria-hidden="true" />
</Button>
</SheetTrigger>
{/*
The sheet is a flex column: header, a scrolling body, then a pinned
footer. Scrolling the BODY rather than the content root is what keeps a
long lineup reachable on a short phone, and `overscroll-contain` on it
stops the flick chaining into the page underneath.
*/}
<SheetContent side="right" className="w-[min(92vw,24rem)]">
<SheetHeader>
<SheetTitle>Menu</SheetTitle>
<SheetDescription className="sr-only">
Demos, verticals, and how these environments work.
</SheetDescription>
</SheetHeader>
<SheetBody>
<Accordion type="multiple" defaultValue={['demos']}>
<AccordionItem value="demos">
<AccordionTrigger>Demos</AccordionTrigger>
<AccordionContent className="flex flex-col gap-0.5">
<PanelHeading>Live now</PanelHeading>
{live.map((demo) => (
<SheetClose asChild key={demo.slug}>
<DemoRow demo={demo} />
</SheetClose>
))}
<PanelHeading>Shipping next</PanelHeading>
{spec.map((demo) => (
<SheetClose asChild key={demo.slug}>
<DemoRow demo={demo} muted />
</SheetClose>
))}
<SheetClose asChild>
<Link
to="/demos"
className="tap inline-flex items-center gap-1 p-2.5 text-xs font-medium text-accent-fg"
>
Browse all demos
<ArrowRight aria-hidden="true" className="size-3.5" />
</Link>
</SheetClose>
</AccordionContent>
</AccordionItem>
<AccordionItem value="verticals">
<AccordionTrigger>Verticals</AccordionTrigger>
<AccordionContent className="flex flex-col gap-0.5">
{verticals.map((vertical) => {
const lead = vertical.demos[0]!;
return (
<SheetClose asChild key={vertical.vertical}>
<Link
to={`/demos/${lead.slug}`}
className="flex gap-3 rounded-lg p-2.5 hover:bg-surface-2"
>
<DemoIcon name={vertical.icon} className="mt-0.5 text-accent-fg" />
<span className="flex min-w-0 flex-col gap-0.5">
<span className="text-sm font-medium text-fg">{vertical.label}</span>
<span className="text-xs leading-relaxed text-muted">
{lead.rewardLine}
</span>
</span>
</Link>
</SheetClose>
);
})}
</AccordionContent>
</AccordionItem>
<AccordionItem value="how">
<AccordionTrigger>How it works</AccordionTrigger>
<AccordionContent className="flex flex-col gap-3">
<dl className="flex flex-col gap-3">
{CONCEPTS.map((concept) => (
<div key={concept.term} className="flex flex-col gap-0.5">
<dt className="text-sm font-medium text-fg">{concept.term}</dt>
<dd className="text-xs leading-relaxed text-muted">{concept.gloss}</dd>
</div>
))}
</dl>
<TasksetSource />
</AccordionContent>
</AccordionItem>
</Accordion>
<a
href={PIG_URL}
target="_blank"
rel="noreferrer noopener"
className="tap mt-2 flex items-center justify-between border-b border-border py-3 text-sm font-medium text-fg"
>
primeintellectgrowth.com
<ArrowUpRight aria-hidden="true" className="size-4 text-muted" />
</a>
<a
href={REPO_URL}
target="_blank"
rel="noreferrer noopener"
className="tap flex items-center justify-between border-b border-border py-3 text-sm font-medium text-fg"
>
Source on GitHub
<Github aria-hidden="true" className="size-4 text-muted" />
</a>
<div className="flex items-center gap-1 pt-3">
<ThemeToggle />
<ContrastToggle />
</div>
</SheetBody>
<SheetFooter>
<SheetClose asChild>
<Button asChild size="lg" className="w-full">
<Link to={ctaHref}>Play the demo</Link>
</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
/* ------------------------------------------------------------------ header */
export function SiteHeader() {
const { live, spec, verticals, ctaHref } = useLineup();
return (
<header
// The height token already folds in --safe-top, so the padding and the
// height come from the same source and a notch cannot push the row off
// the bottom edge of the bar.
className="sticky top-0 z-50 h-[var(--app-header-h)] border-b border-border bg-surface/80 pt-[var(--safe-top)] backdrop-blur-md supports-[backdrop-filter]:bg-surface/70"
>
<div className="mx-auto flex h-full max-w-canvas items-center gap-2 px-4 pl-[max(1rem,var(--safe-left))] pr-[max(1rem,var(--safe-right))]">
<Link
to="/"
className="tap flex shrink-0 items-center rounded-md pr-2 text-base"
aria-label="PIG demo, home"
>
<Wordmark />
</Link>
{/*
`h-full` is not cosmetic. The viewport hangs off the Root's
`top-full`, so a Root only as tall as its 36px triggers drops the
panel INSIDE the header, over its own bottom border.
*/}
<NavigationMenu className="hidden h-full lg:flex" delayDuration={120}>
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>Demos</NavigationMenuTrigger>
<NavigationMenuContent>
<DemosPanel live={live} spec={spec} />
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Verticals</NavigationMenuTrigger>
<NavigationMenuContent>
<VerticalsPanel verticals={verticals} />
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>How it works</NavigationMenuTrigger>
<NavigationMenuContent>
<HowItWorksPanel ctaHref={ctaHref} />
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuLink
href={PIG_URL}
target="_blank"
rel="noreferrer noopener"
className={cn(navigationMenuTriggerStyle(), 'text-muted hover:text-fg')}
>
primeintellectgrowth.com
<ArrowUpRight aria-hidden="true" className="size-3.5" />
</NavigationMenuLink>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
<div className="ml-auto flex items-center gap-1">
<div className="hidden items-center gap-1 lg:flex">
<ThemeToggle />
<ContrastToggle />
<Button asChild variant="ghost" size="icon">
<a
href={REPO_URL}
target="_blank"
rel="noreferrer noopener"
aria-label="Source on GitHub"
>
<Github aria-hidden="true" />
</a>
</Button>
</div>
<Button asChild size="touch" className="hidden lg:inline-flex">
<Link to={ctaHref}>Play the demo</Link>
</Button>
<div className="lg:hidden">
<MobileNav live={live} spec={spec} verticals={verticals} ctaHref={ctaHref} />
</div>
</div>
</div>
</header>
);
}
+43
View File
@@ -0,0 +1,43 @@
/**
* The demo-kit public 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.
*
* 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:
*
* 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';
*/
export type {
DemoEpisode,
DemoMeta,
DemoModule,
DemoStatus,
DemoStep,
Limit,
ModelCall,
Narrative,
Provenance,
RewardComponent,
RewardSpec,
RewardValues,
RunRef,
StoryBeat,
Vertical,
} from './types';
export { defineDemo, defineMeta } from './define';
/** `null` is "not scored", never 0.0. Demos render absences with these two. */
export { isNotScored, rewardTotal } from './episode';
+314
View File
@@ -0,0 +1,314 @@
/**
* The replay transport.
*
* A recorded rollout is played back on the timings the model actually took.
* That is not decoration: "the second guess took four seconds and 900 tokens"
* is one of the few things on this page an executive can feel rather than read.
*
* So the hard rule here is that **no code path invents a duration silently**.
* When a step's `call.durationMs` is null the player falls back to
* `FALLBACK_STEP_MS` and says so `timingIsReal` goes false and `invented[i]`
* marks the step so the UI can label the timeline as approximate instead of
* quietly presenting a made-up number as a measurement.
*
* Playback always starts PAUSED. A board that animates itself the moment the
* page loads has already played its best moment to a visitor who was still
* reading the headline.
*/
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
import type { DemoStep } from './types';
/** 1x, 2x, 4x, or straight to the end. */
export type PlaybackSpeed = 1 | 2 | 4 | 'instant';
export const PLAYBACK_SPEEDS: readonly PlaybackSpeed[] = [1, 2, 4, 'instant'];
export function isPlaybackSpeed(value: unknown): value is PlaybackSpeed {
return value === 1 || value === 2 || value === 4 || value === 'instant';
}
/**
* Dwell for a step whose real duration was not recorded. Exported and named so
* that when it appears on screen it can be labelled as the estimate it is.
*/
export const FALLBACK_STEP_MS = 900;
/**
* How often the fractional progress within a step is pushed into React state.
*
* Not every frame, on purpose: `progress` re-renders the whole demo page, and
* at 60 Hz on a phone that is the difference between a smooth board and a warm
* one. ~15 Hz is plenty as long as whatever consumes it has a CSS transition on
* the property it drives give your progress bar `transition-[width]
* duration-1` and the gaps disappear.
*/
const PROGRESS_TICK_MS = 66;
const REDUCED_MOTION_QUERY = '(prefers-reduced-motion: reduce)';
function subscribeReducedMotion(onChange: () => void): () => void {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
return () => undefined;
}
const query = window.matchMedia(REDUCED_MOTION_QUERY);
query.addEventListener('change', onChange);
return () => query.removeEventListener('change', onChange);
}
function readReducedMotion(): boolean {
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
return window.matchMedia(REDUCED_MOTION_QUERY).matches;
}
/** Live `prefers-reduced-motion`. Re-renders when the OS setting changes. */
export function usePrefersReducedMotion(): boolean {
return useSyncExternalStore(subscribeReducedMotion, readReducedMotion, () => false);
}
/** The recorded duration, or `null` when the trace did not carry a usable one. */
function recordedDurationMs(step: DemoStep<unknown> | undefined): number | null {
const recorded = step?.call?.durationMs;
if (recorded === null || recorded === undefined) return null;
// A zero or negative duration is a recording artefact, not a measurement.
if (!Number.isFinite(recorded) || recorded <= 0) return null;
return recorded;
}
export interface PlayerOptions {
/** Where to start — typically the `step` URL param. Clamped. */
initialIndex?: number;
initialSpeed?: PlaybackSpeed;
/** Fired on every index change, including seeks. Used to sync the permalink. */
onIndexChange?: (index: number) => void;
/** Override the invented dwell. Still reported as invented. */
fallbackStepMs?: number;
}
export interface Player<TState> {
index: number;
/** `undefined` only when there are no steps at all. */
step: DemoStep<TState> | undefined;
stepCount: number;
isPlaying: boolean;
speed: PlaybackSpeed;
/** 0..1 through the current step. Pinned to 0 under reduced motion. */
progress: number;
elapsedMs: number;
totalMs: number;
atStart: boolean;
atEnd: boolean;
/** The OS setting, surfaced so surfaces can skip their own animations too. */
reducedMotion: boolean;
/** False when ANY step's dwell was invented. Label the timeline when false. */
timingIsReal: boolean;
/** Per step: was this dwell invented? */
invented: readonly boolean[];
/** The dwell actually used per step, real or invented. */
durationsMs: readonly number[];
fallbackStepMs: number;
play: () => void;
pause: () => void;
toggle: () => void;
/** Jump to a step. Does NOT change play/pause — a scrubber drag keeps playing. */
seek: (index: number) => void;
/** Manual stepping pauses: you asked to look at this one. */
next: () => void;
prev: () => void;
restart: () => void;
setSpeed: (speed: PlaybackSpeed) => void;
}
export function usePlayer<TState>(
steps: readonly DemoStep<TState>[],
options: PlayerOptions = {},
): Player<TState> {
const fallbackStepMs = options.fallbackStepMs ?? FALLBACK_STEP_MS;
const stepCount = steps.length;
const lastIndex = Math.max(0, stepCount - 1);
const reducedMotion = usePrefersReducedMotion();
const [index, setIndexState] = useState(() =>
clamp(options.initialIndex ?? 0, 0, Math.max(0, steps.length - 1)),
);
const [isPlaying, setIsPlaying] = useState(false);
const [speed, setSpeedState] = useState<PlaybackSpeed>(options.initialSpeed ?? 1);
const [progress, setProgress] = useState(0);
const indexRef = useRef(index);
const elapsedRef = useRef(0);
const lastProgressPushRef = useRef(0);
// Held in a ref so changing the callback never restarts the animation loop.
const onIndexChangeRef = useRef(options.onIndexChange);
onIndexChangeRef.current = options.onIndexChange;
const durationsMs = useMemo(
() => steps.map((step) => recordedDurationMs(step) ?? fallbackStepMs),
[steps, fallbackStepMs],
);
const invented = useMemo(() => steps.map((step) => recordedDurationMs(step) === null), [steps]);
const timingIsReal = useMemo(() => !invented.includes(true), [invented]);
const cumulativeMs = useMemo(() => {
let running = 0;
return durationsMs.map((duration) => {
const start = running;
running += duration;
return start;
});
}, [durationsMs]);
const totalMs = useMemo(() => durationsMs.reduce((sum, d) => sum + d, 0), [durationsMs]);
const dwellAt = useCallback(
(at: number) => durationsMs[at] ?? fallbackStepMs,
[durationsMs, fallbackStepMs],
);
const commitIndex = useCallback((next: number) => {
if (indexRef.current === next) return;
indexRef.current = next;
setIndexState(next);
onIndexChangeRef.current?.(next);
}, []);
// A new steps array means a new run. Rewind rather than leaving the transport
// pointing at step 7 of a rollout that only has four turns.
const stepsRef = useRef(steps);
useEffect(() => {
if (stepsRef.current === steps) return;
stepsRef.current = steps;
elapsedRef.current = 0;
setIsPlaying(false);
setProgress(0);
commitIndex(0);
}, [steps, commitIndex]);
const seek = useCallback(
(to: number) => {
elapsedRef.current = 0;
setProgress(0);
commitIndex(clamp(to, 0, Math.max(0, stepsRef.current.length - 1)));
},
[commitIndex],
);
const play = useCallback(() => {
if (stepsRef.current.length === 0) return;
// Pressing play on the final step replays from the top; the alternative is
// a button that visibly does nothing.
if (indexRef.current >= stepsRef.current.length - 1) {
elapsedRef.current = 0;
setProgress(0);
commitIndex(0);
}
setIsPlaying(true);
}, [commitIndex]);
const pause = useCallback(() => setIsPlaying(false), []);
const toggle = useCallback(() => {
if (isPlaying) pause();
else play();
}, [isPlaying, pause, play]);
const next = useCallback(() => {
setIsPlaying(false);
seek(indexRef.current + 1);
}, [seek]);
const prev = useCallback(() => {
setIsPlaying(false);
seek(indexRef.current - 1);
}, [seek]);
const restart = useCallback(() => {
setIsPlaying(false);
seek(0);
}, [seek]);
const setSpeed = useCallback((nextSpeed: PlaybackSpeed) => setSpeedState(nextSpeed), []);
useEffect(() => {
if (!isPlaying || stepCount === 0) return;
let raf = 0;
let previous = performance.now();
const frame = (now: number): void => {
// `instant` is expressed as infinite elapsed time rather than an infinite
// rate: `(now - previous) * Infinity` is NaN on the very first frame,
// where `now === previous`, and NaN would freeze the transport forever.
const gained = speed === 'instant' ? Number.POSITIVE_INFINITY : (now - previous) * speed;
previous = now;
const startIndex = indexRef.current;
let elapsed = elapsedRef.current + gained;
let at = startIndex;
while (at < lastIndex && elapsed >= dwellAt(at)) {
elapsed -= dwellAt(at);
at += 1;
}
if (at >= lastIndex && elapsed >= dwellAt(lastIndex)) {
elapsedRef.current = dwellAt(lastIndex);
commitIndex(lastIndex);
setProgress(1);
setIsPlaying(false);
return; // Run over. Deliberately not scheduling another frame.
}
elapsedRef.current = elapsed;
if (at !== startIndex) {
commitIndex(at);
lastProgressPushRef.current = now;
// Reduced motion still advances on real time — the content is not the
// animation — but no fractional progress is emitted, so nothing on the
// page is being driven frame by frame.
setProgress(reducedMotion ? 0 : Math.min(1, elapsed / dwellAt(at)));
} else if (!reducedMotion && now - lastProgressPushRef.current >= PROGRESS_TICK_MS) {
lastProgressPushRef.current = now;
setProgress(Math.min(1, elapsed / dwellAt(at)));
}
raf = requestAnimationFrame(frame);
};
raf = requestAnimationFrame(frame);
return () => cancelAnimationFrame(raf);
}, [isPlaying, speed, stepCount, lastIndex, dwellAt, reducedMotion, commitIndex]);
const elapsedMs = (cumulativeMs[index] ?? 0) + progress * dwellAt(index);
return {
index,
step: steps[index],
stepCount,
isPlaying,
speed,
progress,
elapsedMs,
totalMs,
atStart: index === 0,
atEnd: stepCount === 0 || index >= lastIndex,
reducedMotion,
timingIsReal,
invented,
durationsMs,
fallbackStepMs,
play,
pause,
toggle,
seek,
next,
prev,
restart,
setSpeed,
};
}
function clamp(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return min;
return Math.min(max, Math.max(min, Math.trunc(value)));
}
+112
View File
@@ -0,0 +1,112 @@
/**
* Per-route document head.
*
* `scripts/prerender.mjs` bakes these tags into the static HTML at build time,
* which is what crawlers and link unfurlers actually read. This hook exists for
* the other half: a visitor who lands on `/` and clicks through to a demo never
* fetches a new document, so without it the tab title and the canonical link
* would still say "home" three pages later.
*
* Deliberately no cleanup. Restoring the previous head on unmount would mean
* every navigation flickers back to the old title before the next route sets
* its own; the next route always sets one, so the last writer simply wins.
*/
import { useEffect } from 'react';
export const SITE_ORIGIN = 'https://demo.primeintellectgrowth.com';
export const SITE_NAME = 'PIG Demo';
/** `Wordle-five — PIG Demo`. One place, so every tab reads the same shape. */
export function pageTitle(name?: string): string {
return name && name.trim() !== '' ? `${name}${SITE_NAME}` : SITE_NAME;
}
/**
* Absolutise a site-root path. Open Graph consumers do not resolve relative
* URLs a relative `og:image` is simply no image, silently, and you only find
* out when someone pastes the link into Slack.
*/
export function absoluteUrl(pathOrUrl: string): string {
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
return `${SITE_ORIGIN}${pathOrUrl.startsWith('/') ? '' : '/'}${pathOrUrl}`;
}
export interface SeoInput {
/** Used verbatim as `document.title`. Wrap with `pageTitle()` if you want the suffix. */
title: string;
description?: string;
/** Site-root path or absolute URL. */
canonical?: string;
/** Site-root path or absolute URL. */
ogImage?: string;
}
type MetaKey = { name: string } | { property: string };
function upsertMeta(key: MetaKey, content: string): void {
const selector =
'name' in key ? `meta[name="${key.name}"]` : `meta[property="${key.property}"]`;
let tag = document.head.querySelector<HTMLMetaElement>(selector);
if (!tag) {
tag = document.createElement('meta');
if ('name' in key) tag.setAttribute('name', key.name);
else tag.setAttribute('property', key.property);
document.head.appendChild(tag);
}
tag.setAttribute('content', content);
}
function upsertCanonical(href: string): void {
let link = document.head.querySelector<HTMLLinkElement>('link[rel="canonical"]');
if (!link) {
link = document.createElement('link');
link.setAttribute('rel', 'canonical');
document.head.appendChild(link);
}
link.setAttribute('href', href);
}
export function applySeo(input: SeoInput): void {
if (typeof document === 'undefined') return;
document.title = input.title;
upsertMeta({ property: 'og:title' }, input.title);
upsertMeta({ name: 'twitter:title' }, input.title);
if (input.description) {
upsertMeta({ name: 'description' }, input.description);
upsertMeta({ property: 'og:description' }, input.description);
upsertMeta({ name: 'twitter:description' }, input.description);
}
if (input.canonical) {
const href = absoluteUrl(input.canonical);
upsertCanonical(href);
upsertMeta({ property: 'og:url' }, href);
}
if (input.ogImage) {
const href = absoluteUrl(input.ogImage);
upsertMeta({ property: 'og:image' }, href);
upsertMeta({ name: 'twitter:image' }, href);
}
}
/**
* Set the head for this route.
*
* Deps are the individual strings rather than the object, so a caller can pass
* an inline literal without re-running this on every render.
*/
export function useSeo(input: SeoInput): void {
const { title, description, canonical, ogImage } = input;
useEffect(() => {
applySeo({
title,
...(description === undefined ? {} : { description }),
...(canonical === undefined ? {} : { canonical }),
...(ogImage === undefined ? {} : { ogImage }),
});
}, [title, description, canonical, ogImage]);
}
+250
View File
@@ -0,0 +1,250 @@
/**
* Every permalink parameter on this site, in one module.
*
* The demo pages are meant to be sent to someone: "look at step 4 of the
* fine-tuned run". That only works if the URL is the state, and it only stays
* readable if a clean state produces a clean URL. So the two rules here are:
*
* 1. Defaults are OMITTED. `/demos/wordle-five` and
* `/demos/wordle-five?step=0&speed=1&tab=play` are the same page, and only
* the first one is worth pasting into an email.
* 2. Unknown params SURVIVE. Every write is built from the params that are
* already there, so a campaign tag or a future param added by another page
* is not silently eaten by a scrub of the timeline.
*/
import { useCallback, useMemo } from 'react';
import { useLocation, useSearchParams } from 'react-router-dom';
import { isPlaybackSpeed, type PlaybackSpeed } from '@/lib/demo-kit/player';
export const PARAM = {
run: 'run',
step: 'step',
tab: 'tab',
seed: 'seed',
speed: 'speed',
} as const;
/** The tab a demo page opens on when the URL says nothing. */
export const DEFAULT_TAB = 'play';
export interface DemoUrlState {
/** Run id from the manifest. `null` means "the demo's first run". */
run: string | null;
/** Zero-based step index. */
step: number;
tab: string;
/** `null` means "the demo's own default seed". */
seed: number | null;
speed: PlaybackSpeed;
}
export type DemoUrlPatch = Partial<DemoUrlState>;
export interface PatchOptions {
/**
* Replace the history entry instead of pushing one. Left unset, params that
* change during playback (`step`, `speed`) replace and everything else
* pushes so Back leaves the tab you were on rather than rewinding the
* scrubber one frame at a time.
*/
replace?: boolean;
}
/** Params that move while the visitor is just watching, not navigating. */
const TRANSIENT_PARAMS = new Set<keyof DemoUrlState>(['step', 'speed']);
function parseIndex(raw: string | null): number {
if (raw === null) return 0;
const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed) || parsed < 0) return 0;
return parsed;
}
function parseSeed(raw: string | null): number | null {
if (raw === null || raw.trim() === '') return null;
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) ? parsed : null;
}
function parseSpeed(raw: string | null): PlaybackSpeed {
if (raw === null) return 1;
if (raw === 'instant') return 'instant';
const parsed = Number.parseInt(raw, 10);
return isPlaybackSpeed(parsed) ? parsed : 1;
}
export function readUrlState(params: URLSearchParams, defaultTab = DEFAULT_TAB): DemoUrlState {
const run = params.get(PARAM.run);
const tab = params.get(PARAM.tab);
return {
run: run === null || run.trim() === '' ? null : run,
step: parseIndex(params.get(PARAM.step)),
tab: tab === null || tab.trim() === '' ? defaultTab : tab,
seed: parseSeed(params.get(PARAM.seed)),
speed: parseSpeed(params.get(PARAM.speed)),
};
}
/**
* Apply a patch to a set of params, dropping anything that is at its default.
* Returns a NEW URLSearchParams; the input is never mutated.
*/
export function writeUrlState(
params: URLSearchParams,
patch: DemoUrlPatch,
defaultTab = DEFAULT_TAB,
): URLSearchParams {
const next = new URLSearchParams(params);
const put = (key: string, value: string | null): void => {
if (value === null) next.delete(key);
else next.set(key, value);
};
if ('run' in patch) put(PARAM.run, patch.run ?? null);
if ('step' in patch) {
const step = patch.step ?? 0;
put(PARAM.step, step > 0 ? String(Math.trunc(step)) : null);
}
if ('tab' in patch) {
const tab = patch.tab ?? defaultTab;
put(PARAM.tab, tab === defaultTab ? null : tab);
}
if ('seed' in patch) {
const seed = patch.seed;
put(PARAM.seed, seed === null || seed === undefined ? null : String(Math.trunc(seed)));
}
if ('speed' in patch) {
const speed = patch.speed ?? 1;
put(PARAM.speed, speed === 1 ? null : String(speed));
}
return next;
}
export interface UrlStateApi {
state: DemoUrlState;
patch: (changes: DemoUrlPatch, options?: PatchOptions) => void;
/** Clear every param this module owns; anything else in the URL survives. */
reset: (options?: PatchOptions) => void;
/** `/demos/x?step=3` — path plus search, for a router `<Link to>`. */
hrefFor: (changes?: DemoUrlPatch) => string;
/** Absolute URL, for a copy-link button. */
permalinkFor: (changes?: DemoUrlPatch) => string;
}
export function useUrlState(options?: { defaultTab?: string }): UrlStateApi {
const defaultTab = options?.defaultTab ?? DEFAULT_TAB;
const [params, setParams] = useSearchParams();
const { pathname } = useLocation();
const state = useMemo(() => readUrlState(params, defaultTab), [params, defaultTab]);
const patch = useCallback(
(changes: DemoUrlPatch, patchOptions?: PatchOptions) => {
const keys = Object.keys(changes) as (keyof DemoUrlState)[];
const replace = patchOptions?.replace ?? keys.every((key) => TRANSIENT_PARAMS.has(key));
// The updater form matters: two patches in the same tick (the player
// advancing a step while the visitor clicks a tab) would otherwise both
// read the pre-render params and the second would undo the first.
setParams((prev) => writeUrlState(prev, changes, defaultTab), {
replace,
preventScrollReset: true,
});
},
[setParams, defaultTab],
);
const reset = useCallback(
(patchOptions?: PatchOptions) => {
setParams(
(prev) => {
const next = new URLSearchParams(prev);
for (const key of Object.values(PARAM)) next.delete(key);
return next;
},
{ replace: patchOptions?.replace ?? false, preventScrollReset: true },
);
},
[setParams],
);
const hrefFor = useCallback(
(changes: DemoUrlPatch = {}) => {
const search = writeUrlState(params, changes, defaultTab).toString();
return search ? `${pathname}?${search}` : pathname;
},
[params, pathname, defaultTab],
);
const permalinkFor = useCallback(
(changes: DemoUrlPatch = {}) => {
const href = hrefFor(changes);
// Prerendering runs this file in a browser too, but guard anyway: a
// permalink is not worth throwing a page away for.
const origin = typeof window === 'undefined' ? '' : window.location.origin;
return `${origin}${href}`;
},
[hrefFor],
);
return { state, patch, reset, hrefFor, permalinkFor };
}
/* ------------------------------------------------------------------------- *
* Single-param conveniences. Each is `[value, setValue]` and each writes
* through the same omit-the-default path, so mixing them cannot produce a URL
* that `useUrlState` reads back differently.
* ------------------------------------------------------------------------- */
export function useRunParam(): [string | null, (run: string | null, options?: PatchOptions) => void] {
const { state, patch } = useUrlState();
const set = useCallback(
(run: string | null, options?: PatchOptions) => {
// A new run invalidates the step index: step 6 of a nine-turn rollout is
// not step 6 of a three-turn one.
patch({ run, step: 0 }, options);
},
[patch],
);
return [state.run, set];
}
export function useStepParam(): [number, (step: number, options?: PatchOptions) => void] {
const { state, patch } = useUrlState();
const set = useCallback(
(step: number, options?: PatchOptions) => patch({ step }, options),
[patch],
);
return [state.step, set];
}
export function useTabParam(
defaultTab = DEFAULT_TAB,
): [string, (tab: string, options?: PatchOptions) => void] {
const { state, patch } = useUrlState({ defaultTab });
const set = useCallback(
(tab: string, options?: PatchOptions) => patch({ tab }, options),
[patch],
);
return [state.tab, set];
}
export function useSeedParam(): [number | null, (seed: number | null, options?: PatchOptions) => void] {
const { state, patch } = useUrlState();
const set = useCallback(
(seed: number | null, options?: PatchOptions) => patch({ seed }, options),
[patch],
);
return [state.seed, set];
}
export function useSpeedParam(): [PlaybackSpeed, (speed: PlaybackSpeed, options?: PatchOptions) => void] {
const { state, patch } = useUrlState();
const set = useCallback(
(speed: PlaybackSpeed, options?: PatchOptions) => patch({ speed }, options),
[patch],
);
return [state.speed, set];
}
+35
View File
@@ -0,0 +1,35 @@
/**
* App entry.
*
* `next-themes` is configured with `attribute="data-theme"` because that is
* what `tailwind.config.js` darkMode and every `:root[data-theme='dark']` block
* in `index.css` key off. Switch it to the default `class` and the site
* compiles, runs, and is stuck in light mode forever with no error anywhere.
*/
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ThemeProvider } from 'next-themes';
import { RouterProvider } from 'react-router-dom';
import { router } from '@/router';
import './index.css';
const container = document.getElementById('root');
if (!container) {
throw new Error('No #root element — index.html and main.tsx disagree.');
}
createRoot(container).render(
<StrictMode>
<ThemeProvider
attribute="data-theme"
defaultTheme="system"
enableSystem
/* Without this, switching theme animates every colour on the page at once
because half the site has a `transition-colors` on it. */
disableTransitionOnChange
>
<RouterProvider router={router} />
</ThemeProvider>
</StrictMode>,
);
+185
View File
@@ -0,0 +1,185 @@
import { useMemo } from 'react';
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 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;
}
export default function Gallery() {
/*
* The filter lives in the URL, not in component state. A filtered gallery is
* the thing somebody pastes into a message, and it also means the back button
* undoes a filter change, which is what people expect it to do.
*/
const [params, setParams] = useSearchParams();
const raw = params.get('vertical');
const active: Vertical | typeof ALL =
raw && verticalKeysInUse.includes(raw as Vertical) ? (raw as Vertical) : ALL;
const shown = useMemo(
() => (active === ALL ? allDemos : allDemos.filter((d) => d.vertical === active)),
[active],
);
function select(next: Vertical | typeof ALL) {
// `replace` so a run of filter taps leaves one entry in history, not eight.
if (next === ALL) setParams({}, { replace: true });
else setParams({ vertical: next }, { replace: true });
}
const filters: readonly (Vertical | typeof ALL)[] = [ALL, ...verticalKeysInUse];
return (
<main className={`${s.shell} py-10 sm:py-16`}>
<p className={s.eyebrow}>Gallery</p>
<h1 className={`${s.h1} mt-3 max-w-3xl`}>Every environment we have built or specified.</h1>
<p className={`${s.lede} mt-5 max-w-2xl`}>
A live demo is playable in this tab. A spec is a written environment task, action set,
grader, counterweight and the command that evaluates it published in full, with no
interactive surface yet. There are no coming-soon cards here.
</p>
{filters.length > 2 ? (
<div className="mt-8">
<h2 className="sr-only">Filter by vertical</h2>
<ul aria-label="Filter demos by vertical" className="flex flex-wrap gap-2">
{filters.map((key) => {
const selected = key === active;
return (
<li key={key}>
<button
aria-pressed={selected}
className={`tap inline-flex items-center rounded-lg border px-4 py-2 text-sm font-medium transition-colors duration-2 ease-enter ${
selected
? 'border-brand bg-accent-subtle text-accent-fg'
: 'border-border bg-surface text-muted hover:bg-surface-2 hover:text-fg'
}`}
onClick={() => select(key)}
type="button"
>
{key === ALL ? 'All' : verticalLabel(key)}
</button>
</li>
);
})}
</ul>
</div>
) : null}
{/*
The count changes without a page load and without moving focus, so it
is announced. Without this, a filter tap is silent to a screen reader.
*/}
<p aria-live="polite" className="mt-6 text-sm text-muted">
{shown.length === 1 ? '1 environment' : `${shown.length} environments`}
{active === ALL ? '' : ` in ${verticalLabel(active)}`}
</p>
{shown.length === 0 ? (
<div className="card mt-4 p-6">
<p className={s.h3}>Nothing here yet.</p>
<p className={`${s.prose} mt-2`}>
No environment is filed under this vertical. The proposal for it is still on the
verticals page, written out in full.
</p>
<button className={`${s.btnSecondary} mt-4`} onClick={() => select(ALL)} type="button">
Show every environment
</button>
</div>
) : (
<ul className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{shown.map((demo) => {
const Icon = iconFor(demo.icon);
const isSpec = demo.status === 'spec';
const vertical = verticalForDemo(demo);
return (
<li className="flex" key={demo.slug}>
<article className="flex w-full flex-col">
<Link
// 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.
className={`${s.cardLink} h-full ${isSpec ? 'opacity-70 hover:opacity-100' : ''}`}
to={routes.demo(demo.slug)}
>
<span className="flex items-start justify-between gap-3">
<Icon aria-hidden="true" className="size-5 shrink-0 text-brand" />
{isSpec ? (
<span className={`${s.pill} gap-1`}>
<FileText aria-hidden="true" className="size-3.5" />
Spec
</span>
) : (
<span className={`${s.pill} border-positive/30 bg-positive/10 text-positive`}>
Live
</span>
)}
</span>
<h3 className="mt-3 text-lg font-bold tracking-tight text-fg">{demo.title}</h3>
<p className={`${s.prose} mt-1.5 text-sm`}>{demo.tagline}</p>
<dl className="mt-4 space-y-1.5 text-sm">
<div className="flex gap-2">
<dt className="shrink-0 text-muted">For</dt>
<dd className="text-fg">{demo.persona}</dd>
</div>
<div className="flex gap-2">
<dt className="shrink-0 text-muted">Reward</dt>
<dd className="text-fg">{demo.rewardLine}</dd>
</div>
</dl>
<span className="mt-4 inline-flex items-center gap-1.5 text-sm font-semibold text-accent-fg">
{isSpec ? 'Read the specification' : 'Play it'}
<ArrowRight
aria-hidden="true"
className="size-4 transition-transform duration-2 ease-enter group-hover:translate-x-0.5"
/>
</span>
</Link>
{vertical ? (
<p className="mt-2 px-1 text-xs text-muted">
<Link className={s.link} to={routes.vertical(vertical.slug)}>
{vertical.title}
</Link>{' '}
· {PROPOSAL_NOTICE}
</p>
) : null}
</article>
</li>
);
})}
</ul>
)}
<div className="mt-12 card p-5 sm:p-7">
<h2 className={s.h2}>The eleven 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
numbers.
</p>
<Link className={`${s.btnPrimary} mt-5`} to={routes.home}>
See the lineup
</Link>
</div>
</main>
);
}
+382
View File
@@ -0,0 +1,382 @@
import type { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { ArrowUpRight } from 'lucide-react';
import { helloWorldCitations, optimalPlay, reproduce, trainingResult, wordList } from '@/content/evidence';
import { REPO_URL, routes } from '@/content/lineup';
import { PROPOSAL_NOTICE } from '@/content/verticals';
import * as s from '@/content/styles';
/** External link with the new-tab affordance and the announcement to match. */
function Out({ children, href }: { children: ReactNode; href: string }) {
return (
<a className={s.link} href={href} rel="noreferrer noopener" target="_blank">
{children}
<ArrowUpRight aria-hidden="true" className="ml-0.5 inline size-3.5 align-[-0.1em]" />
<span className="sr-only"> (opens in a new tab)</span>
</a>
);
}
function Heading({ children, id }: { children: ReactNode; id: string }) {
return (
<h2 className={`${s.h2} scroll-mt-[calc(var(--app-header-h)+1rem)]`} id={id}>
{children}
</h2>
);
}
const CONTENTS: readonly { id: string; label: string }[] = [
{ id: 'measured', label: 'What we measured ourselves' },
{ id: 'cited', label: 'What we cite from elsewhere' },
{ id: 'recorded', label: 'The rollouts are recorded' },
{ id: 'editor', label: 'The reward editor does not train' },
{ id: 'proposals', label: 'The verticals are proposals' },
{ id: 'words', label: 'The word list is ours' },
{ id: 'easier', label: 'This game is easier than the original' },
{ id: 'unmeasured', label: 'What we have not measured' },
];
export default function Honesty() {
return (
<main className={`${s.shell} py-10 sm:py-16`}>
<p className={s.eyebrow}>Honesty</p>
<h1 className={`${s.h1} mt-3 max-w-3xl`}>What is measured, what is cited, and what is not.</h1>
<p className={`${s.lede} mt-5 max-w-2xl`}>
This site argues that a number you can check beats a claim you have to trust. That argument
only works if we hold our own numbers to it. So here is every claim on the site, sorted by
how much weight it can carry.
</p>
<nav aria-label="On this page" className="card mt-8 p-5">
<p className={s.eyebrow}>On this page</p>
<ul className="mt-3 grid gap-x-6 gap-y-2 sm:grid-cols-2">
{CONTENTS.map((item) => (
<li key={item.id}>
<a className={`${s.link} text-sm`} href={`#${item.id}`}>
{item.label}
</a>
</li>
))}
</ul>
</nav>
{/* ── Measured ───────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl">
<Heading id="measured">What we measured ourselves</Heading>
<p className={`${s.prose} mt-4`}>
Everything in this section is reproducible from a clone of this repository, offline, with
no account and no key. If any of it does not reproduce, that is a bug and we want the
issue.
</p>
<ul className={`${s.prose} mt-4 space-y-4`}>
<li>
<strong className="text-fg">The environment runs.</strong> The word game is a real
environment in this repository, not a mock behind the page. Three commands score a model
with it:
<pre className={`${s.codeBlock} mt-2`}>
<code>
{reproduce.clone}
{'\n'}
{reproduce.install}
{'\n'}
{reproduce.evaluate}
</code>
</pre>
</li>
<li>
<strong className="text-fg">The word lists rebuild byte for byte.</strong> Both source
dictionaries are committed beside the script that filters them, so{' '}
<code className="font-mono text-fg">{wordList.rebuild}</code> produces the same{' '}
<span className="nums">{wordList.answers.toLocaleString('en-US')}</span> answers and{' '}
<span className="nums">{wordList.guesses.toLocaleString('en-US')}</span> legal guesses on
any machine.
</li>
<li>
<strong className="text-fg">The two implementations of the rules agree.</strong> The
grader exists twice once in Python for the environment, once in TypeScript for the
board in your browser. Continuous integration scores every ordered pair of words in the
answer list through both, about{' '}
<span className="nums">21.2 million</span> feedback patterns, and compares a hash. A
divergence fails the build rather than showing you one result and scoring another.
</li>
<li>
<strong className="text-fg">The scores on a recorded run are re-derived, not
recited.</strong> Where a demo ships a verifier, the page recomputes the reward in your
browser from the recorded turns instead of printing a number stored in the file. If a
trace is truncated, it renders as unverifiable never as zero.
</li>
</ul>
</section>
{/* ── Cited ──────────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl">
<Heading id="cited">What we cite from elsewhere</Heading>
<p className={`${s.prose} mt-4`}>
Three claims on this site are not ours. They are linked so you can go and check them
instead of taking them from us.
</p>
<div className="card mt-5 p-5">
<h3 className={s.h3}>
{trainingResult.model}: {trainingResult.before} {trainingResult.after}{' '}
{trainingResult.metric}
</h3>
<p className={`${s.prose} mt-2`}>
Published by Prime Intellect, measured on {trainingResult.evalDescription}, after{' '}
{trainingResult.method}. Both checkpoints are on Hugging Face, so you can run the eval
yourself: <Out href={trainingResult.checkpoints[0].href}>the SFT checkpoint</Out> and{' '}
<Out href={trainingResult.checkpoints[1].href}>the RL checkpoint</Out>, with{' '}
<Out href={trainingResult.source.href}>the write-up</Out>.
</p>
<p className={`${s.prose} mt-3`}>
We quote the win rate and nothing else. The same write-up gives average-reward figures
for those runs; we leave them out because the reward function has changed across
versions of that environment and the two figures were never re-measured together. A win
rate survives a reward change. An average reward does not. We have also not reproduced
this result: their environment and word list are not ours, and a number measured on a
different task list is not a number about this one.
</p>
</div>
<div className="card mt-4 p-5">
<h3 className={s.h3}>Wordle is Prime Intellects own hello-world</h3>
<p className={`${s.prose} mt-2`}>
We say this because a demo picked to flatter itself is worth nothing. It appears in three
places in their stack:
</p>
<ul className={`${s.prose} mt-3 space-y-2`}>
{helloWorldCitations.map((c) => (
<li key={c.href}>
<Out href={c.href}>{c.label}</Out> {c.claim}
</li>
))}
</ul>
</div>
<div className="card mt-4 p-5">
<h3 className={s.h3}>{optimalPlay.average} is not our number</h3>
<p className={`${s.prose} mt-2`}>
The famous optimum is <Out href={optimalPlay.href}>{optimalPlay.label}</Out>. It belongs
to that list and that solver. It is not the optimum for our list, we have not computed
ours, and you should not read it as a bar this environment is being measured against.
</p>
</div>
</section>
{/* ── Recorded ───────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl">
<Heading id="recorded">Every rollout on this site is recorded, not live</Heading>
<p className={`${s.prose} mt-4`}>
When you watch a model play here, you are watching a file. No model is called while you
read this page. That is a choice, and these are the reasons for it:
</p>
<ul className={`${s.prose} mt-4 list-disc space-y-3 pl-5`}>
<li>
<strong className="text-fg">The argument is about determinism.</strong> A page whose
model says something different on every reload cannot be used to make the case that the
grader stays put. The demo has to behave like the thing it is describing.
</li>
<li>
<strong className="text-fg">There is nowhere to hide a cherry-pick.</strong> Every run is
a file in the repository carrying its model, its seed and the date it was captured. A bad
run is in there being bad. A live demo lets you re-roll until it looks good and tell
nobody.
</li>
<li>
<strong className="text-fg">It works.</strong> No key, no rate limit, no bill, no
dependence on a provider being up during the ten minutes you are looking at it.
</li>
</ul>
<p className={`${s.prose} mt-4`}>
The cost is real: you cannot make a model react to a word you chose. If that is what you
want, run the environment on your own machine against your own endpoint. It is three
commands and the code is above.
</p>
</section>
{/* ── Reward editor ──────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl">
<Heading id="editor">The reward editor re-scores. It does not train anything.</Heading>
<p className={`${s.prose} mt-4`}>
Moving a weight changes how the recorded attempts are scored, and the page recomputes and
re-ranks them. That is the entire mechanism. Nothing is fine-tuned, no model is called, and
nothing learns while you drag a slider.
</p>
<p className={`${s.prose} mt-4`}>
What it is there to show is narrow and worth the space: two runs can swap places when you
change what you are paying for. That is a fact about your definition of good, not about the
models and it is the reason the definition is worth an hour of your teams attention
before the training run is worth a GPU.
</p>
<p className={`${s.prose} mt-4`}>
The other half actually training against the reward you just wrote takes GPUs and
hours, and it does not belong in a browser tab. That is what a trainer like prime-rl is
for, and the number at the top of the home page is what it produced.
</p>
</section>
{/* ── Proposals ──────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl">
<Heading id="proposals">The verticals are our proposals</Heading>
<p className={`${s.prose} mt-4`}>
Eleven of the twelve entries in the lineup describe environments that do not exist. They
are labelled <span className="font-semibold text-fg">{PROPOSAL_NOTICE}</span> on every
surface that shows them, and that label is the literal truth: we wrote them.
</p>
<ul className={`${s.prose} mt-4 list-disc space-y-3 pl-5`}>
<li>They are not on Prime Intellects roadmap and we do not speak for Prime Intellect.</li>
<li>
No customer asked for any of them. No company appears anywhere on this site as a
customer, a reference or a logo, because none is one.
</li>
<li>
Where a page names a KPI, we are describing how that function usually measures itself. We
are not reporting anyones numbers, and there are no numbers on those pages to report.
</li>
</ul>
<p className={`${s.prose} mt-4`}>
Two of them carry a standing caveat on their own page the{' '}
<Link className={s.link} to={routes.vertical('legal-playbook-redline')}>
legal redline
</Link>
, where grading the drafting half collapses into an LLM judge, and{' '}
<Link className={s.link} to={routes.vertical('semiconductor-ppa-closure')}>
semiconductor timing closure
</Link>
, which has the best reward on the page and is the one thing here we are telling you we are
not going to build.
</p>
</section>
{/* ── Word list ──────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl">
<Heading id="words">The word list is our own construction</Heading>
<p className={`${s.prose} mt-4`}>
We did not copy the original games word lists. Our answers are the intersection of two
permissively-licensed sources every five-letter headword in{' '}
<Out href={wordList.sources[0].href}>{wordList.sources[0].label}</Out> that also appears in{' '}
<Out href={wordList.sources[1].href}>{wordList.sources[1].label}</Out>, minus a short
hand-written blocklist. Legal guesses are the whole five-letter Wordnik set.
</p>
<dl className="card mt-5 grid gap-4 p-5 sm:grid-cols-3">
<div>
<dt className={s.eyebrow}>Answers</dt>
<dd className="nums mt-1 text-2xl font-bold text-fg">
{wordList.answers.toLocaleString('en-US')}
</dd>
</div>
<div>
<dt className={s.eyebrow}>Legal guesses</dt>
<dd className="nums mt-1 text-2xl font-bold text-fg">
{wordList.guesses.toLocaleString('en-US')}
</dd>
</div>
<div>
<dt className={s.eyebrow}>Original games answers</dt>
<dd className="nums mt-1 text-2xl font-bold text-muted">
{wordList.originalAnswers.toLocaleString('en-US')}
</dd>
</div>
</dl>
<p className={`${s.prose} mt-4`}>
The list is built by a stated rule from sources anyone can fetch, rather than lifted from
somebodys editorial selection. That is the point of it. It also means every comparison to
the original game is a comparison between two different games.
</p>
</section>
{/* ── Easier ─────────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl">
<Heading id="easier">This environment is materially easier than the original game</Heading>
<p className={`${s.prose} mt-4`}>
Our answer pool comes from a dictionary rule, so it keeps the regular plurals and past
tenses that the original games editor removed by hand.{' '}
<span className="nums font-semibold text-fg">{wordList.endsInS}</span> of our answers end
in a plain <span className="font-mono">S</span> and{' '}
<span className="nums font-semibold text-fg">{wordList.endsInEd}</span> end in{' '}
<span className="font-mono">-ED</span> {' '}
<span className="nums font-semibold text-fg">{wordList.endsInSorEd}</span> in one or the
other.
</p>
<p className={`${s.prose} mt-4`}>
That is a large, exploitable regularity. A guess that tests a trailing{' '}
<span className="font-mono">S</span> splits our pool roughly one-third to two-thirds every
single game, and buys information that the same guess simply does not buy in a list where
plurals were deliberately stripped out. Nobody has to be clever to use it; a fixed opening
pair picks it up for free.
</p>
<p className={`${s.prose} mt-4`}>
One thing cuts the other way, and we would rather say it than be caught leaving it out: our
pool is roughly twice the size of the originals, which is harder. We have not computed an
optimal average for our list, so we cannot give you a single number for the net effect. The
practical instruction is the same either way do not compare a guess count from this site
to your own statistics from the original game. It is a different game with a different
shape.
</p>
</section>
{/* ── Unmeasured ─────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl">
<Heading id="unmeasured">What we have not measured</Heading>
<p className={`${s.prose} mt-4`}>
This is the list we would want if we were the ones being sold to.
</p>
<ul className={`${s.prose} mt-4 list-disc space-y-3 pl-5`}>
<li>
<strong className="text-fg">Any training result of our own.</strong> We have not trained
a model against this environment. The before-and-after number on the home page is Prime
Intellects, measured on their word list, and we did not rerun it on ours.
</li>
<li>
<strong className="text-fg">Variance.</strong> The recorded runs are individual seeds,
not samples. There is no confidence interval anywhere on this site and no claim that a
given run is typical. A demonstration with one seed is a demonstration, not evidence.
</li>
<li>
<strong className="text-fg">An optimum, or a human baseline, for our list.</strong> We do
not know how few guesses perfect play needs on our answers, and we have not asked people
to play it.
</li>
<li>
<strong className="text-fg">Cost and latency.</strong> No number here is a price, a
token count you should budget from, or a wall-clock comparison between models. The
durations shown in a trace are what that recording measured, on that day, on that
endpoint.
</li>
<li>
<strong className="text-fg">Anything at all about the eleven proposals.</strong> Nothing
in those pages has been run. Every reward, counterweight and KPI there is a design.
</li>
<li>
<strong className="text-fg">A full review of the answer list.</strong> We block a short
hand-written set of words from ever being the hidden answer. We have not audited the
remaining {wordList.answers.toLocaleString('en-US')} exhaustively, and a five-letter
English dictionary contains words somebody will not want on a boardroom screen.
</li>
</ul>
</section>
<div className="card mt-14 p-5 sm:p-7">
<h2 className={s.h3}>Found something on this site that is wrong?</h2>
<p className={`${s.prose} mt-2 max-w-2xl`}>
Every claim above is traceable to a file in the repository. If one of them does not hold,
open an issue with the page and the sentence and we will fix the sentence or the code.
</p>
<div className="mt-5 flex flex-col gap-3 sm:flex-row">
<a
className={s.btnPrimary}
href={REPO_URL}
rel="noreferrer noopener"
target="_blank"
>
Read the source
</a>
<Link className={s.btnSecondary} to={routes.home}>
Back to the demo
</Link>
</div>
</div>
</main>
);
}
+101
View File
@@ -0,0 +1,101 @@
import { Link, useLocation } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
import { allDemos, lineup, REPO_URL, routes } from '@/content/lineup';
import * as s from '@/content/styles';
/**
* A 404 with somewhere to go.
*
* The destinations are read from the registry and the lineup rather than
* hard-coded, so this page cannot become the one place on the site that still
* links to a demo we removed.
*/
export default function NotFound() {
const { pathname } = useLocation();
const topVerticals = lineup.slice(0, 4);
return (
<main className={`${s.shell} py-16 sm:py-24`}>
<p className={`${s.eyebrow} nums`}>404</p>
<h1 className={`${s.h1} mt-3 max-w-2xl`}>That page isnt here.</h1>
<p className={`${s.lede} mt-5 max-w-2xl`}>
Nothing is served at{' '}
<code className="break-all font-mono text-fg">{pathname}</code>. Either the link is old or
we moved something. Here is everything this site has.
</p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
<Link className={s.btnPrimary} to={routes.home}>
Start at the beginning
</Link>
<Link className={s.btnSecondary} to={routes.gallery}>
Every environment
</Link>
</div>
<div className="mt-12 grid gap-8 sm:grid-cols-2">
<nav aria-labelledby="nf-demos">
<h2 className={s.h3} id="nf-demos">
Demos
</h2>
<ul className="mt-3 space-y-2">
{allDemos.map((demo) => (
<li key={demo.slug}>
<Link className={`${s.link} text-sm`} to={routes.demo(demo.slug)}>
{demo.title}
</Link>
<span className="ml-2 text-xs text-muted">
{demo.status === 'live' ? 'Live' : 'Specification'}
</span>
</li>
))}
{allDemos.length === 0 ? (
<li className="text-sm text-muted">No demos are registered yet.</li>
) : null}
</ul>
</nav>
<nav aria-labelledby="nf-verticals">
<h2 className={s.h3} id="nf-verticals">
The lineup
</h2>
<ul className="mt-3 space-y-2">
{topVerticals.map((v) => (
<li key={v.slug}>
<Link className={`${s.link} text-sm`} to={routes.vertical(v.slug)}>
{v.title}
</Link>
</li>
))}
<li>
<Link
className="inline-flex items-center gap-1.5 text-sm font-medium text-muted transition-colors duration-1 ease-enter hover:text-fg"
to={routes.home}
>
All {lineup.length}
<ArrowRight aria-hidden="true" className="size-3.5" />
</Link>
</li>
</ul>
</nav>
</div>
<div className="card mt-12 p-5">
<h2 className={s.h3}>Looking for the numbers?</h2>
<p className={`${s.prose} mt-2 max-w-2xl`}>
What is measured, what is cited from elsewhere, and what we have deliberately not measured
all live on one page.
</p>
<div className="mt-4 flex flex-col gap-3 sm:flex-row">
<Link className={s.btnSecondary} to={routes.honesty}>
Honesty
</Link>
<a className={s.btnSecondary} href={REPO_URL} rel="noreferrer noopener" target="_blank">
The source on GitHub
</a>
</div>
</div>
</main>
);
}
+197
View File
@@ -0,0 +1,197 @@
import { Link, useParams } from 'react-router-dom';
import { ArrowLeft, ArrowRight, FileText, Play, Quote, TriangleAlert } from 'lucide-react';
import { iconFor } from '@/content/icons';
import { demosForVertical, featuredDemo, lineup, routes } from '@/content/lineup';
import { PROPOSAL_NOTICE, verticalBySlug } from '@/content/verticals';
import * as s from '@/content/styles';
export default function VerticalPage() {
const { slug } = useParams<{ slug: string }>();
const vertical = verticalBySlug(slug);
if (!vertical) {
return (
<main className={`${s.shell} py-16`}>
<h1 className={s.h1}>No such vertical.</h1>
<p className={`${s.lede} mt-4 max-w-xl`}>
There is no proposal at <code className="font-mono text-fg">/verticals/{slug}</code>. The
twelve we have written are all on the home page.
</p>
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<Link className={s.btnPrimary} to={routes.home}>
See the lineup
</Link>
<Link className={s.btnSecondary} to={routes.gallery}>
See what is built
</Link>
</div>
</main>
);
}
const Icon = iconFor(vertical.icon);
const demos = demosForVertical(vertical.key);
const live = demos.find((d) => d.status === 'live');
const spec = demos.find((d) => d.status === 'spec');
const index = lineup.findIndex((v) => v.slug === vertical.slug);
const previous = index > 0 ? lineup[index - 1] : undefined;
const next = index >= 0 ? lineup[index + 1] : undefined;
return (
<main className={`${s.shell} py-10 sm:py-14`}>
<Link
className="tap inline-flex items-center gap-1.5 text-sm font-medium text-muted transition-colors duration-1 ease-enter hover:text-fg"
to={routes.home}
>
<ArrowLeft aria-hidden="true" className="size-4" />
All twelve
</Link>
<header className="mt-4">
<div className="flex flex-wrap items-center gap-2">
<span className={s.proposalPill}>{PROPOSAL_NOTICE}</span>
<span className={s.pill}>Rank {vertical.rank} of {lineup.length}</span>
<span className={s.pill}>
{vertical.plannedForV1 ? 'In the first set' : 'Not in the first set'}
</span>
</div>
<div className="mt-5 flex items-start gap-4">
<span className="card grid size-12 shrink-0 place-items-center bg-surface-2">
<Icon aria-hidden="true" className="size-6 text-brand" />
</span>
<h1 className={`${s.h1} min-w-0`}>{vertical.title}</h1>
</div>
</header>
{/* ── Who this is for, and what is already worrying them ─────────── */}
<section className="mt-8">
<p className={s.eyebrow}>Who it is for</p>
<p className="mt-2 text-lg font-semibold text-fg">{vertical.persona}</p>
<blockquote className="card mt-4 max-w-3xl p-5">
<Quote aria-hidden="true" className="size-5 text-muted" />
<p className="mt-2 text-lg leading-relaxed text-fg">{vertical.anxiety}</p>
<footer className="mt-3 text-sm text-muted">
The question in the room before anyone opens a laptop.
</footer>
</blockquote>
</section>
{/* ── The environment, in the same three parts every time ────────── */}
<section className="mt-10 grid gap-4 lg:grid-cols-3">
<article className="card p-5">
<h2 className={s.eyebrow}>The task</h2>
<p className={`${s.prose} mt-2 text-[0.9375rem] text-fg`}>{vertical.task}</p>
</article>
<article className="card p-5">
<h2 className={s.eyebrow}>The reward</h2>
<p className={`${s.prose} mt-2 text-[0.9375rem] text-fg`}>{vertical.reward}</p>
</article>
<article className="card border-warning/40 p-5">
<h2 className={`${s.eyebrow} text-warning`}>The counterweight</h2>
<p className={`${s.prose} mt-2 text-[0.9375rem] text-fg`}>{vertical.counterweight}</p>
</article>
</section>
<p className={`${s.prose} mt-4 max-w-3xl text-sm`}>
The counterweight is the part worth arguing about. A reward with nothing pulling against it
is a target, and a target is what a model learns to hit instead of doing the job.
</p>
{vertical.caveat ? (
<section className="card mt-8 border-warning/40 bg-warning/5 p-5 sm:p-6">
<h2 className={`${s.h3} flex items-center gap-2 text-warning`}>
<TriangleAlert aria-hidden="true" className="size-4 shrink-0" />
Where this stops
</h2>
<p className={`${s.prose} mt-2 max-w-3xl text-fg`}>{vertical.caveat}</p>
</section>
) : null}
{/* ── What actually exists for this vertical, stated plainly ─────── */}
<section className="mt-10">
<h2 className={s.h2}>What exists today</h2>
{live ? (
<Link 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>
<span className="mt-3 text-xl font-bold tracking-tight text-fg">{live.title}</span>
<span className={`${s.prose} mt-1.5`}>{live.tagline}</span>
<span className="mt-4 inline-flex items-center gap-1.5 text-sm font-semibold text-accent-fg">
<Play aria-hidden="true" className="size-4" />
Play it
</span>
</Link>
) : spec ? (
<Link 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
</span>
<span className="mt-3 text-xl font-bold tracking-tight text-fg">{spec.title}</span>
<span className={`${s.prose} mt-1.5`}>{spec.tagline}</span>
<span className="mt-4 inline-flex items-center gap-1.5 text-sm font-semibold text-accent-fg">
Read the specification
<ArrowRight aria-hidden="true" className="size-4" />
</span>
</Link>
) : (
<div className="card mt-4 p-5 sm:p-6">
<p className={`${s.prose} max-w-3xl text-fg`}>
Nothing. There is no environment for this vertical, no recorded run, and no number. It
is a proposal, written to the same shape as the one that is built so you can judge it
on the same terms.
</p>
{featuredDemo ? (
<p className={`${s.prose} mt-3 max-w-3xl`}>
The environment that does exist is{' '}
<Link className={s.link} to={routes.demo(featuredDemo.slug)}>
{featuredDemo.title}
</Link>
. It is a word game, and it is deliberately not one of these twelve: it is there to
show the machinery, not to imply we have built yours.
</p>
) : null}
<div className="mt-5 flex flex-col gap-3 sm:flex-row">
<Link className={s.btnSecondary} to={routes.gallery}>
See what is built
</Link>
<Link className={s.btnSecondary} to={routes.honesty}>
What we have not measured
</Link>
</div>
</div>
)}
</section>
{/* ── 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)}>
<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}
</span>
<span className={`${s.h3} mt-1.5`}>{previous.title}</span>
</Link>
) : (
<span />
)}
{next ? (
<Link
className={`${s.cardLink} sm:col-start-2 sm:items-end sm:text-right`}
to={routes.vertical(next.slug)}
>
<span className="inline-flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted">
Rank {next.rank}
<ArrowRight aria-hidden="true" className="size-3.5" />
</span>
<span className={`${s.h3} mt-1.5`}>{next.title}</span>
</Link>
) : null}
</nav>
</main>
);
}
+275
View File
@@ -0,0 +1,275 @@
/**
* Routes, built from the registry.
*
* There is exactly one demo route `/demos/:slug` and it resolves through
* `@/lib/demo-kit/registry`. Adding a demo to this site is creating a directory
* under `src/demos/`; it is not, and must never become, an edit to this file.
* The same goes for `/verticals/:slug`, which groups whatever the registry
* found rather than a list written down anywhere.
*
* Every route below `/` sits under one layout with a root error boundary, and
* the demo route adds its own on top. That nesting is the point: a demo whose
* chunk fails to load, or whose module is malformed, renders a card inside the
* normal page chrome. A single broken demo taking the whole site to a white
* screen would be the most expensive bug this repo could ship, given what the
* site is arguing.
*/
import { Component, type ReactNode } from 'react';
import {
createBrowserRouter,
isRouteErrorResponse,
Link,
Outlet,
ScrollRestoration,
useRouteError,
type LoaderFunctionArgs,
type RouteObject,
} from 'react-router-dom';
import { getDemo, loadDemoModule } from '@/lib/demo-kit/registry';
import { SiteFooter } from '@/components/site/SiteFooter';
import { SiteHeader } from '@/components/site/SiteHeader';
import { SkipLink } from '@/components/site/SkipLink';
import * as s from '@/content/styles';
import Home from '@/pages/Home';
/**
* The one element that renders on every route.
*
* The header, the footer and the skip link live here rather than in each page,
* so a new page cannot ship without them. `#main` is this wrapper, not the
* page's own `<main>`: pages own their landmark, and a second `<main>` around
* theirs would be invalid HTML and a duplicate landmark in a screen reader's
* rotor. The wrapper is only what the skip link focuses.
*/
export function RootLayout(): ReactNode {
return (
<div className="flex min-h-dvh flex-col bg-bg text-fg">
<SkipLink />
<SiteHeader />
{/* Restores scroll on Back, and puts a fresh route at the top rather
than halfway down the previous page. */}
<ScrollRestoration />
<div id="main" tabIndex={-1} className="flex-1 outline-none">
<Outlet />
</div>
<SiteFooter />
</div>
);
}
function ErrorCard({
heading,
body,
detail,
}: {
heading: string;
body: string;
detail?: string;
}): ReactNode {
return (
<main className={`${s.shell} py-16 sm:py-24`}>
<div className="card max-w-2xl p-6 sm:p-8" role="alert">
<p className={`${s.eyebrow} text-danger`}>Something broke</p>
<h1 className={`${s.h2} mt-2`}>{heading}</h1>
<p className={`${s.prose} mt-3`}>{body}</p>
{detail ? (
<pre className="mt-4 overflow-x-auto rounded-md border border-border bg-surface-2 p-3 text-xs text-muted">
<code>{detail}</code>
</pre>
) : null}
<div className="mt-6 flex flex-wrap gap-3">
<Link to="/gallery" className={s.btnPrimary}>
Back to the demos
</Link>
<Link to="/" className={s.btnSecondary}>
Home
</Link>
</div>
</div>
</main>
);
}
/** Turns whatever react-router threw into something a person can read. */
function describeError(error: unknown): { heading: string; body: string; detail?: string } {
if (isRouteErrorResponse(error)) {
if (error.status === 404) {
return {
heading: 'That page does not exist',
body: 'The link may be from an older version of the site, or the demo it pointed at has been renamed.',
detail: typeof error.data === 'string' ? error.data : undefined,
};
}
return {
heading: `${error.status} ${error.statusText}`,
body: 'The page could not be loaded.',
detail: typeof error.data === 'string' ? error.data : undefined,
};
}
if (error instanceof Error) {
return {
heading: 'This page failed to load',
body: 'The rest of the site still works. If this keeps happening, the source is on GitHub and the issue is reproducible from it.',
detail: error.message,
};
}
return {
heading: 'This page failed to load',
body: 'The rest of the site still works.',
};
}
function RootErrorBoundary(): ReactNode {
const error = useRouteError();
const described = describeError(error);
return (
<ErrorCard
heading={described.heading}
body={described.body}
{...(described.detail === undefined ? {} : { detail: described.detail })}
/>
);
}
function DemoErrorBoundary(): ReactNode {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) {
return (
<ErrorCard
heading="No demo by that name"
body="Every demo on this site is a directory in the repository, so a missing one is usually a renamed slug rather than a deleted page."
{...(typeof error.data === 'string' ? { detail: error.data } : {})}
/>
);
}
const described = describeError(error);
return (
<ErrorCard
heading="This demo failed to load"
body="Only this demo is affected — the others are separate bundles and still work."
{...(described.detail === undefined ? {} : { detail: described.detail })}
/>
);
}
/**
* 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 (
<div
className="mx-auto w-full max-w-canvas px-4 py-24 sm:px-6"
role="status"
aria-live="polite"
>
<p className="text-sm text-muted">Loading</p>
</div>
);
}
/**
* Validates the slug and starts the demo's chunk during the navigation rather
* than after the page mounts. `loadDemoModule` caches the promise, so the page
* calling it again resolves from cache instead of fetching twice.
*/
async function demoLoader({ params }: LoaderFunctionArgs) {
const slug = params.slug;
if (typeof slug !== 'string' || slug === '') {
throw new Response('No demo slug in the URL.', { status: 404 });
}
const meta = getDemo(slug);
if (!meta) {
throw new Response(`No demo named "${slug}".`, { status: 404 });
}
return { meta, module: await loadDemoModule(slug) };
}
export const routes: RouteObject[] = [
{
path: '/',
element: <RootLayout />,
errorElement: <RootErrorBoundary />,
hydrateFallbackElement: <RouteFallback />,
children: [
// Home is the landing page and is imported eagerly on purpose: making the
// first paint wait on a second network round trip to save bytes on a page
// almost every visitor sees is the wrong trade.
{ index: true, element: <Home /> },
// Two paths, one page. `/gallery` is what the site's own links use;
// `/demos` is the shape people guess and the one older links used, and a
// redirect would cost a round trip to say the same thing.
{
path: 'gallery',
lazy: async () => ({ Component: (await import('@/pages/Gallery')).default }),
},
{
path: 'demos',
lazy: async () => ({ Component: (await import('@/pages/Gallery')).default }),
},
{
path: 'demos/:slug',
loader: demoLoader,
errorElement: <DemoErrorBoundary />,
lazy: async () => {
const { default: DemoPage } = await import('@/pages/DemoPage');
return {
Component: () => (
<DemoRenderBoundary>
<DemoPage />
</DemoRenderBoundary>
),
};
},
},
{
path: 'verticals/:slug',
lazy: async () => ({ Component: (await import('@/pages/Vertical')).default }),
},
{
path: 'honesty',
lazy: async () => ({ Component: (await import('@/pages/Honesty')).default }),
},
{
path: '*',
lazy: async () => ({ Component: (await import('@/pages/NotFound')).default }),
},
],
},
];
export const router = createBrowserRouter(routes);