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
+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>
);
}