Redesign in Prime Intellect's design language, both themes, three viewports
ci / web (push) Successful in 2m42s
ci / python (push) Successful in 3m8s

Eighteen agents: three design directions judged on whether a COO on a phone
actually learns what an environment is, on craft, and on landing without a
rewrite; one spec; a foundation of measured tokens; six build lanes; three
browser verifiers; a final gate pass.

The materials are Prime Intellect's, measured from their site: near-black
grounds, one green, sharp radii, mono small-caps labels, Geist and Geist Mono
self-hosted because production CSP is font-src 'self'. Two of their own greys
fail contrast on their own ground (#737373 is 4.02:1, #6E6E6E is 3.73:1 on
#0F0F0F), so --muted is lifted and the CSS comment carries the number — or
someone will 'correct' it back. Every text-on-ground pair in both themes is
tabulated in src/index.css with its measured ratio.

The two rules that resolved every conflict: data is mono, sentences are sans;
the language wins on materials, the lesson wins on legibility. Light mode is a
finished paper theme, not an inversion.

What did not change: the derived-tabs contract, the honesty markers, the
isolation lint, every gate. 419 contract checks, entry chunk at 74% of budget,
zero horizontal overflow on any route at 390/1024/1440 in either theme.

Also flips Alert Triage to status 'live' — the pipeline built it but never
promoted it, so it was badged SPEC on its own playable page and the home page
counted one environment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
karti-ai
2026-08-28 21:41:15 -07:00
parent af15ba584e
commit a21596b3e4
85 changed files with 4717 additions and 2230 deletions
+5 -5
View File
@@ -28,11 +28,11 @@ intersection with the above, to derive envs/wordle_five/words/answers.json.
See envs/wordle_five/words/PROVENANCE.md for exactly how. See envs/wordle_five/words/PROVENANCE.md for exactly how.
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
Manrope — SIL Open Font License 1.1 Geist and Geist Mono — SIL Open Font License 1.1
https://github.com/sharanda/manrope https://github.com/vercel/geist-font
Copyright 2019 The Manrope Project Authors. Licence text: public/fonts/OFL.txt Copyright 2024 The Geist Project Authors. The sans and mono faces are embedded
"Manrope" is a Reserved Font Name under that licence: a modified version of the as variable woff2 files via @fontsource-variable/geist and
font may not be distributed under that name. @fontsource-variable/geist-mono (latin subset). Licence text: public/fonts/OFL.txt
Lucide — ISC Lucide — ISC
https://github.com/lucide-icons/lucide https://github.com/lucide-icons/lucide
+2
View File
@@ -19,6 +19,8 @@
"conformance": "node scripts/conformance.mjs" "conformance": "node scripts/conformance.mjs"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/geist": "^5.3.0",
"@fontsource-variable/geist-mono": "^5.3.0",
"@fontsource-variable/manrope": "^5.3.0", "@fontsource-variable/manrope": "^5.3.0",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-dialog": "^1.1.23",
+16
View File
@@ -8,6 +8,12 @@ importers:
.: .:
dependencies: dependencies:
'@fontsource-variable/geist':
specifier: ^5.3.0
version: 5.3.0
'@fontsource-variable/geist-mono':
specifier: ^5.3.0
version: 5.3.0
'@fontsource-variable/manrope': '@fontsource-variable/manrope':
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.3.0 version: 5.3.0
@@ -526,6 +532,12 @@ packages:
'@floating-ui/utils@0.2.12': '@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
'@fontsource-variable/geist-mono@5.3.0':
resolution: {integrity: sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw==}
'@fontsource-variable/geist@5.3.0':
resolution: {integrity: sha512-j0m+vLQuG5XAYoHtGCVu0spvlGreR3EzpECUVzkFmI1mTVnAO38l/NEPDCFgZ177JxzYJCLSmTQibIiYPilGrA==}
'@fontsource-variable/manrope@5.3.0': '@fontsource-variable/manrope@5.3.0':
resolution: {integrity: sha512-6D5dgokHsWDDMtmXHznKa0hK229NN+1a4BLPmUCLqcO1Pw5EEhWY5RFt0AcXnVRAljFFPfRtLkJePQj6LSsV6g==} resolution: {integrity: sha512-6D5dgokHsWDDMtmXHznKa0hK229NN+1a4BLPmUCLqcO1Pw5EEhWY5RFt0AcXnVRAljFFPfRtLkJePQj6LSsV6g==}
@@ -2117,6 +2129,10 @@ snapshots:
'@floating-ui/utils@0.2.12': {} '@floating-ui/utils@0.2.12': {}
'@fontsource-variable/geist-mono@5.3.0': {}
'@fontsource-variable/geist@5.3.0': {}
'@fontsource-variable/manrope@5.3.0': {} '@fontsource-variable/manrope@5.3.0': {}
'@jridgewell/gen-mapping@0.3.13': '@jridgewell/gen-mapping@0.3.13':
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2019 The Manrope Project Authors (https://github.com/sharanda/manrope) Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font) Geist-Italic[wght].ttf: Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font)
This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: This license is copied below, and is also available with a FAQ at:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 121 KiB

+19 -10
View File
@@ -4,14 +4,14 @@
* *
* This repo is public and Apache-2.0, and almost none of what makes the demo * This repo is public and Apache-2.0, and almost none of what makes the demo
* work is ours: the primitives are shadcn/ui over Radix, the answer list is * work is ours: the primitives are shadcn/ui over Radix, the answer list is
* Wordnik's filtered through SCOWL, the typeface is Manrope under the SIL Open * Wordnik's filtered through SCOWL, the typefaces are Geist and Geist Mono
* Font License, the icons are lucide, the charts are recharts. Every one of * under the SIL Open Font License, the icons are lucide, the charts are recharts. Every one of
* those licences is permissive, and every one of them requires attribution. * those licences is permissive, and every one of them requires attribution.
* *
* So `NOTICE` is not paperwork, it is a build artifact with a test. This script * So `NOTICE` is not paperwork, it is a build artifact with a test. This script
* is that test: it fails if NOTICE is missing any source we actually ship, and * is that test: it fails if NOTICE is missing any source we actually ship, and
* it fails if a source is named without its licence beside it — because * it fails if a source is named without its licence beside it — because
* "uses Manrope" without "OFL-1.1" is not attribution, it is a mention. * "uses Geist" without "OFL-1.1" is not attribution, it is a mention.
* *
* The OFL additionally requires that the licence text travel WITH the font, so * The OFL additionally requires that the licence text travel WITH the font, so
* `public/fonts/OFL.txt` is checked for separately. Shipping the .woff2 out of * `public/fonts/OFL.txt` is checked for separately. Shipping the .woff2 out of
@@ -64,13 +64,22 @@ const SOURCES = [
used: () => hasWordFile(/scowl/i), used: () => hasWordFile(/scowl/i),
}, },
{ {
label: 'Manrope', label: 'Geist',
match: /manrope/i, match: /\bgeist\b/i,
// The OFL is version-specific and the version matters: OFL-1.1 is the one // The OFL is version-specific and the version matters: OFL-1.1 is the one
// Manrope ships under, and it is the one whose terms are quoted in OFL.txt. // Geist ships under, and it is the one whose terms are quoted in OFL.txt.
licence: /OFL[-\s]?1\.1|SIL\s+Open\s+Font\s+License/i, licence: /OFL[-\s]?1\.1|SIL\s+Open\s+Font\s+License/i,
why: 'Manrope is the typeface, embedded as a variable woff2.', why: 'Geist is the sans typeface, embedded as a variable woff2.',
used: () => exists(abs('node_modules', '@fontsource-variable', 'manrope')), used: () => exists(abs('node_modules', '@fontsource-variable', 'geist')),
},
{
label: 'Geist Mono',
// A separate package with its own LICENSE file, so it is a separate entry:
// naming the sans face does not attribute the mono one.
match: /geist\s+mono|geist-mono/i,
licence: /OFL[-\s]?1\.1|SIL\s+Open\s+Font\s+License/i,
why: 'Geist Mono is the mono typeface, embedded as a variable woff2.',
used: () => exists(abs('node_modules', '@fontsource-variable', 'geist-mono')),
}, },
{ {
label: 'lucide', label: 'lucide',
@@ -162,8 +171,8 @@ report.check(
exists(OFL), exists(OFL),
rel(OFL), rel(OFL),
'OFL text ships with the font', 'OFL text ships with the font',
'Manrope is under SIL OFL-1.1, which requires the licence text to travel with the font files. ' + 'Geist and Geist Mono are under SIL OFL-1.1, which requires the licence text to travel with the font files. ' +
'Copy node_modules/@fontsource-variable/manrope/LICENSE to public/fonts/OFL.txt. ' + 'Copy node_modules/@fontsource-variable/geist/LICENSE to public/fonts/OFL.txt. ' +
'Shipping the woff2 and leaving the licence behind is the one violation on this list with teeth.', 'Shipping the woff2 and leaving the licence behind is the one violation on this list with teeth.',
); );
+50 -26
View File
@@ -1,9 +1,10 @@
import { useState } from 'react'; import { useState } from 'react';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import { Eye, Trophy } from 'lucide-react'; import { Eye } from 'lucide-react';
import type { DemoStep } from '@/lib/demo-kit/types'; import type { DemoStep } from '@/lib/demo-kit/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { InterventionChip, Label } from './chrome';
import { formatOrDash } from './format'; import { formatOrDash } from './format';
export interface BlindCompareRun<T> { export interface BlindCompareRun<T> {
@@ -39,6 +40,9 @@ export interface BlindCompareProps<T> {
* unknow it, and every "obviously the trained one looks better" is worthless * unknow it, and every "obviously the trained one looks better" is worthless
* after the fact. * after the fact.
* *
* The two vote buttons are OUTLINE, not green: two green fills on the Watch
* tab would compete with ▶, and a vote is a question, not a call to action.
*
* There is a "reveal without voting" escape on purpose: a visitor who does not * There is a "reveal without voting" escape on purpose: a visitor who does not
* want to play should not be held hostage by a modal-shaped page. * want to play should not be held hostage by a modal-shaped page.
*/ */
@@ -77,63 +81,82 @@ export function BlindCompare<T>({
return ( return (
<section aria-label="Blind comparison" className={cn('space-y-3', className)}> <section aria-label="Blind comparison" className={cn('space-y-3', className)}>
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1"> <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h3 className="text-sm font-semibold">{question}</h3> <h3 className="text-h3 text-fg">{question}</h3>
<p className="nums text-xs text-muted">Same puzzle, same seed ({seed}).</p> <p className="font-mono text-label uppercase tracking-label text-muted">
Same puzzle · seed {seed}
</p>
</div> </div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{sides.map(({ id, run }) => { {sides.map(({ id, run }) => {
const last = run.steps[run.steps.length - 1]; const last = run.steps[run.steps.length - 1];
const picked = vote === id; const picked = vote === id;
const won = revealed && winner === id;
return ( return (
<div <div
key={id} key={id}
className={cn( className={cn(
'card flex flex-col gap-3 p-3 transition-colors duration-2 ease-enter', 'flex flex-col gap-3 rounded-xl border bg-surface p-3 transition-colors duration-2 ease-enter sm:p-4',
picked && 'border-brand', won ? 'border-accent-fg' : picked ? 'border-border-strong' : 'border-border',
revealed && winner === id && 'border-positive',
)} )}
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<h4 className="text-sm font-semibold">Agent {id}</h4> <Label as="h4" className="text-fg">
<span className="nums text-xs text-muted">{run.steps.length} steps</span> Agent {id}
</Label>
<span className="font-mono text-label uppercase tracking-label text-muted">
{run.steps.length} steps
</span>
</div> </div>
<div className="rounded-lg bg-surface-2 p-3"> <div className="max-w-full overflow-x-auto rounded-md bg-surface-2 p-3">
{last ? ( {last ? (
<div className="mx-auto w-fit max-w-full sm:max-w-[20rem]">
<Surface state={last.state} /> <Surface state={last.state} />
</div>
) : ( ) : (
<p className="text-sm text-muted">This run recorded no steps.</p> <p className="text-ui text-muted">This run recorded no steps.</p>
)} )}
</div> </div>
{!revealed ? ( {!revealed ? (
<Button size="touch" className="w-full" onClick={() => commit(id)}> <Button
variant="outline"
size="touch"
className="w-full sm:min-h-12"
onClick={() => commit(id)}
>
Agent {id} is better Agent {id} is better
</Button> </Button>
) : ( ) : (
<dl className="space-y-1 text-sm"> <dl className="space-y-1 text-ui">
<div className="flex items-baseline justify-between gap-2"> <div className="flex items-baseline justify-between gap-2">
<dt className="text-muted">Identity</dt> <dt className="text-muted">Identity</dt>
<dd className="text-right font-medium">{run.label}</dd> <dd className="text-right font-medium text-fg">{run.label}</dd>
</div> </div>
<div className="flex items-baseline justify-between gap-2"> <div className="flex items-baseline justify-between gap-2">
<dt className="text-muted">Model</dt> <dt className="text-muted">Model</dt>
<dd className="nums text-right font-mono text-xs">{run.model}</dd> <dd className="min-w-0 break-all text-right font-mono text-mono-data text-fg">
{run.model}
</dd>
</div> </div>
<div className="flex items-baseline justify-between gap-2"> <div className="flex items-baseline justify-between gap-2">
<dt className="text-muted">Intervention</dt> <dt className="text-muted">Intervention</dt>
<dd className="text-right text-xs"> <dd className="text-right">
{run.intervention ?? ( {run.intervention ? (
<span className="text-muted">none plain rollout</span> <InterventionChip>{run.intervention}</InterventionChip>
) : (
<span className="text-small text-muted">none plain rollout</span>
)} )}
</dd> </dd>
</div> </div>
<div className="flex items-baseline justify-between gap-2 border-t border-border pt-1"> <div className="flex items-baseline justify-between gap-2 border-t border-border pt-1">
<dt className="text-muted">Reward</dt> <dt className="text-muted">Reward</dt>
<dd className="nums flex items-center gap-1 text-right font-mono font-semibold"> <dd className="flex items-center gap-1.5 text-right font-mono text-stat text-fg">
{revealed && winner === id ? ( {won ? (
<Trophy className="size-3.5 text-positive" aria-hidden="true" /> <span className="font-mono text-label uppercase tracking-label text-positive">
higher
</span>
) : null} ) : null}
{formatOrDash(run.total)} {formatOrDash(run.total)}
</dd> </dd>
@@ -150,24 +173,25 @@ export function BlindCompare<T>({
<Button variant="outline" size="touch" onClick={() => commit('tie')}> <Button variant="outline" size="touch" onClick={() => commit('tie')}>
Too close to call Too close to call
</Button> </Button>
<Button variant="ghost" size="touch" className="text-muted" onClick={() => setRevealed(true)}> <Button variant="ghost" size="touch" className="text-muted hover:text-fg" onClick={() => setRevealed(true)}>
<Eye aria-hidden="true" /> <Eye aria-hidden="true" />
Just show me Just show me
</Button> </Button>
</div> </div>
) : ( ) : (
<p role="status" className="card bg-surface-2 p-3 text-sm leading-relaxed"> <p
role="status"
className="max-w-measure rounded-xl border border-border bg-surface-2 p-3 text-prose text-fg-2 sm:p-4"
>
{vote === null ? ( {vote === null ? (
<>Revealed without a vote. </> <>Revealed without a vote. </>
) : vote === winner ? ( ) : vote === winner ? (
<> <>
<span className="font-semibold text-positive">You picked the higher-scoring run.</span>{' '} <span className="font-medium text-positive">You picked the higher-scoring run.</span>{' '}
</> </>
) : ( ) : (
<> <>
<span className="font-semibold text-warning"> <span className="font-medium text-warning">You picked the lower-scoring run.</span>{' '}
You picked the lower-scoring run.
</span>{' '}
</> </>
)} )}
The environment scored these two with the same grader, on the same seed. The difference The environment scored these two with the same grader, on the same seed. The difference
+32 -30
View File
@@ -1,8 +1,7 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { ExternalLink } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import * as st from '@/content/styles';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { External } from './chrome';
export interface CodeReceiptProps { export interface CodeReceiptProps {
/** The file's text, imported with `?raw` so it cannot drift from the source. */ /** The file's text, imported with `?raw` so it cannot drift from the source. */
@@ -14,7 +13,7 @@ export interface CodeReceiptProps {
* See `resolveMarkedRange` for the two conventions it supports. * See `resolveMarkedRange` for the two conventions it supports.
*/ */
marker?: string; marker?: string;
/** Link to the whole file — GitHub, usually. */ /** Link to the whole file in the repository. */
href?: string; href?: string;
/** Lines of context kept around the marked range while collapsed. */ /** Lines of context kept around the marked range while collapsed. */
context?: number; context?: number;
@@ -102,9 +101,9 @@ export function resolveMarkedRange(lines: string[], marker?: string): MarkedRang
* that says "this, right here, is the grader" — costs nothing and is the whole * that says "this, right here, is the grader" — costs nothing and is the whole
* reason the panel exists. * reason the panel exists.
* *
* The block scrolls horizontally inside itself. Long Python lines must never * The block scrolls on both axes inside its own box. Long Python lines must
* make the PAGE scroll sideways; on a phone that turns every vertical swipe * never make the PAGE scroll sideways; on a phone that turns every vertical
* into a fight. * swipe into a fight.
*/ */
export function CodeReceipt({ export function CodeReceipt({
code, code,
@@ -130,42 +129,43 @@ export function CodeReceipt({
const gutterWidth = `${String(lines.length).length + 1}ch`; const gutterWidth = `${String(lines.length).length + 1}ch`;
return ( return (
<section aria-label={`Source: ${path}`} className={cn('card overflow-hidden', className)}> <section
<header className="flex flex-col items-start gap-2 border-b border-border px-3 py-2 sm:flex-row sm:items-center sm:gap-x-3"> aria-label={`Source: ${path}`}
className={cn('overflow-hidden rounded-xl border border-border bg-surface', className)}
>
<header className="flex flex-col items-start gap-2 border-b border-border px-4 py-2.5 sm:flex-row sm:items-center sm:gap-x-3">
{/* Full width on a phone: sharing a row with two controls truncates a {/* Full width on a phone: sharing a row with two controls truncates a
path to "envs/wo…", which names nothing. */} path to "envs/wo…", which names nothing. */}
<h3 <h3 className="w-full min-w-0 truncate font-mono text-caption text-muted sm:flex-1" title={path}>
className="nums w-full min-w-0 truncate font-mono text-xs text-muted sm:flex-1"
title={path}
>
{path} {path}
</h3> </h3>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1"> <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
{range && !expanded ? ( {range ? (
<Button variant="outline" size="sm" className="tap" onClick={() => setExpanded(true)}> <Button
Show all {lines.length} lines variant="outline"
</Button> size="sm"
) : null} className="tap lg:min-h-8"
{range && expanded ? ( aria-expanded={expanded}
<Button variant="outline" size="sm" className="tap" onClick={() => setExpanded(false)}> onClick={() => setExpanded((was) => !was)}
Collapse to the marked part >
{expanded ? 'Collapse to the marked part' : `Expand all ${lines.length} lines`}
</Button> </Button>
) : null} ) : null}
{href ? ( {href ? (
<a <a
href={href} href={href}
rel="noreferrer" rel="noreferrer"
className={cn(st.link, 'tap inline-flex items-center gap-1 text-xs')} className="tap inline-flex items-center font-mono text-caption text-accent-fg underline decoration-border-strong underline-offset-4 hover:decoration-accent-fg"
> >
Read the whole file Open in repo
<ExternalLink className="size-3" aria-hidden="true" /> <External />
</a> </a>
) : null} ) : null}
</div> </div>
</header> </header>
<div className="max-h-96 overflow-auto"> <div className="max-h-[20rem] overflow-auto bg-surface-3 sm:max-h-[24rem]">
<pre className="w-max min-w-full py-2 font-mono text-xs leading-relaxed"> <pre className="w-max min-w-full py-2 font-mono text-code text-fg [font-feature-settings:'liga'_0]">
<code> <code>
{shown.from > 0 ? ( {shown.from > 0 ? (
<span className="block px-3 text-muted" aria-hidden="true"> <span className="block px-3 text-muted" aria-hidden="true">
@@ -180,17 +180,19 @@ export function CodeReceipt({
key={number} key={number}
className={cn( className={cn(
'block border-l-2 pr-4', 'block border-l-2 pr-4',
marked marked ? 'border-accent-fg bg-accent-subtle text-fg' : 'border-transparent text-fg-2',
? 'border-brand bg-accent-subtle/60 text-fg'
: 'border-transparent text-muted',
)} )}
> >
{/* Sticky so the line numbers survive a horizontal scroll — {/* Sticky so the line numbers survive a horizontal scroll —
without it they slide out of view exactly when a long without it they slide out of view exactly when a long
line makes you want them. */} line makes you want them. The gutter paints its own
ground so scrolled code does not show through it. */}
<span <span
aria-hidden="true" aria-hidden="true"
className="sticky left-0 inline-block select-none bg-surface pr-3 text-right text-muted" className={cn(
'sticky left-0 inline-block select-none pr-3 text-right text-muted',
marked ? 'bg-accent-subtle' : 'bg-surface-3',
)}
style={{ width: gutterWidth }} style={{ width: gutterWidth }}
> >
{number + 1} {number + 1}
+7 -7
View File
@@ -1,6 +1,6 @@
import { Component } from 'react'; import { Component } from 'react';
import type { ErrorInfo, ReactNode } from 'react'; import type { ErrorInfo, ReactNode } from 'react';
import { AlertTriangle, ExternalLink, RotateCcw } from 'lucide-react'; import { AlertTriangle, RotateCcw } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
const REPO_URL = 'https://git.karti.ai/PIG/PIG-Demo'; const REPO_URL = 'https://git.karti.ai/PIG/PIG-Demo';
@@ -53,21 +53,21 @@ export class DemoErrorBoundary extends Component<DemoErrorBoundaryProps, DemoErr
const { demoTitle, sourceHref } = this.props; const { demoTitle, sourceHref } = this.props;
return ( return (
<div role="alert" className="card mx-auto my-10 max-w-xl p-6"> <div role="alert" className="mx-auto my-10 max-w-xl rounded-xl border border-border bg-surface p-5 sm:p-6">
<div className="flex items-center gap-2 text-warning"> <div className="flex items-center gap-2 text-warning">
<AlertTriangle className="size-5" aria-hidden="true" /> <AlertTriangle className="size-5" aria-hidden="true" />
<h2 className="text-base font-semibold"> <h2 className="text-h3 text-fg">
{demoTitle ? `${demoTitle} failed to render` : 'This demo failed to render'} {demoTitle ? `${demoTitle} failed to render` : 'This demo failed to render'}
</h2> </h2>
</div> </div>
<p className="mt-3 text-sm leading-relaxed text-muted"> <p className="mt-3 max-w-measure text-prose text-fg-2">
Something in this demo threw while drawing. The rest of the site is unaffected every Something in this demo threw while drawing. The rest of the site is unaffected every
other demo is a separate module. The environment and the recorded runs behind this page other demo is a separate module. The environment and the recorded runs behind this page
are in the repository either way, and you can run them yourself. are in the repository either way, and you can run them yourself.
</p> </p>
<p className="mt-3 break-words rounded-lg bg-surface-2 px-3 py-2 font-mono text-xs text-muted"> <pre className="mt-3 whitespace-pre-wrap break-words rounded-md border border-border bg-surface-3 px-3 py-2 font-mono text-code text-fg-2">
{error.message || 'Unknown error'} {error.message || 'Unknown error'}
</p> </pre>
<div className="mt-5 flex flex-wrap gap-2"> <div className="mt-5 flex flex-wrap gap-2">
<Button size="touch" onClick={this.handleReset}> <Button size="touch" onClick={this.handleReset}>
<RotateCcw aria-hidden="true" /> <RotateCcw aria-hidden="true" />
@@ -76,7 +76,7 @@ export class DemoErrorBoundary extends Component<DemoErrorBoundaryProps, DemoErr
<Button variant="outline" size="touch" asChild> <Button variant="outline" size="touch" asChild>
<a href={sourceHref ?? REPO_URL} rel="noreferrer"> <a href={sourceHref ?? REPO_URL} rel="noreferrer">
Read the source Read the source
<ExternalLink aria-hidden="true" /> <span aria-hidden="true"></span>
</a> </a>
</Button> </Button>
</div> </div>
+201 -94
View File
@@ -1,8 +1,9 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import type { ComponentType, ReactNode } from 'react'; import type { ComponentProps, ComponentType, ReactNode } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { TermScope } from '@/components/site/Term';
import { listRuns, loadEpisode, rewardTotal } from '@/lib/demo-kit/episode'; import { listRuns, loadEpisode, rewardTotal } from '@/lib/demo-kit/episode';
import { usePlayer } from '@/lib/demo-kit/player'; import { usePlayer } from '@/lib/demo-kit/player';
import { loadDemoModule } from '@/lib/demo-kit/registry'; import { loadDemoModule } from '@/lib/demo-kit/registry';
@@ -12,6 +13,7 @@ import { useRunParam, useSpeedParam, useStepParam, useTabParam } from '@/lib/url
import * as st from '@/content/styles'; import * as st from '@/content/styles';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { BlindCompare } from './BlindCompare'; import { BlindCompare } from './BlindCompare';
import { Chip, Label, StatusPill } from './chrome';
import { CodeReceipt } from './CodeReceipt'; import { CodeReceipt } from './CodeReceipt';
import { DemoErrorBoundary } from './DemoErrorBoundary'; import { DemoErrorBoundary } from './DemoErrorBoundary';
import { DemoTabBar, TabClaim, resolveTab, visibleTabs } from './DemoTabs'; import { DemoTabBar, TabClaim, resolveTab, visibleTabs } from './DemoTabs';
@@ -33,14 +35,14 @@ import { StatStrip } from './StatStrip';
import type { Stat } from './StatStrip'; import type { Stat } from './StatStrip';
import { RecordedBadge, TracePlayer } from './TracePlayer'; import { RecordedBadge, TracePlayer } from './TracePlayer';
import { VerifyBadge } from './VerifyBadge'; import { VerifyBadge } from './VerifyBadge';
import { formatOrDash, useIsDesktop } from './format'; import { formatDate, formatOrDash, useIsDesktop } from './format';
const REPO_BLOB = 'https://git.karti.ai/PIG/PIG-Demo/src/branch/main/'; const REPO_BLOB = 'https://git.karti.ai/PIG/PIG-Demo/src/branch/main/';
/** /**
* The panel the step-detail strip inside the Watch tab opens on. * The panel the step-detail strip inside the Watch tab opens on.
* *
* This control is deliberately NOT in the URL. `?tab=` now belongs to the page's * This control is deliberately NOT in the URL. `?tab=` belongs to the page's
* four top-level tabs, and one param cannot address two nested controls without * four top-level tabs, and one param cannot address two nested controls without
* one of them silently winning; a permalink to `?tab=call` would land the reader * one of them silently winning; a permalink to `?tab=call` would land the reader
* on a page with no such top-level tab. * on a page with no such top-level tab.
@@ -50,6 +52,18 @@ const DEFAULT_DETAIL_PANEL = 'reasoning';
/** Reserved slug for the shell's own hand-written demo. Dev builds only. */ /** Reserved slug for the shell's own hand-written demo. Dev builds only. */
const MOCK_SLUG = '__mock'; const MOCK_SLUG = '__mock';
/**
* A top-level tab panel. Each one is its own `TermScope`, so "first occurrence
* per panel gets the underline" is literally per panel rather than per page.
*/
function Panel({ children, ...props }: ComponentProps<typeof TabsContent>) {
return (
<TabsContent {...props}>
<TermScope>{children}</TermScope>
</TabsContent>
);
}
export interface DemoBundle { export interface DemoBundle {
demo: AnyDemoModule; demo: AnyDemoModule;
runs: RunRef[]; runs: RunRef[];
@@ -58,7 +72,7 @@ export interface DemoBundle {
type LoadState = type LoadState =
| { status: 'loading' } | { status: 'loading' }
| { status: 'ready'; bundle: DemoBundle } | { status: 'ready'; bundle: DemoBundle; dropped: number }
| { status: 'error'; message: string }; | { status: 'error'; message: string };
/** /**
@@ -67,15 +81,20 @@ type LoadState =
* `loadDemoModule` and `loadEpisode` both cache their promises, so the route * `loadDemoModule` and `loadEpisode` both cache their promises, so the route
* loader having already fetched the module makes this resolve without a second * loader having already fetched the module makes this resolve without a second
* request. Runs are loaded with `allSettled` on purpose: one unreadable trace * request. Runs are loaded with `allSettled` on purpose: one unreadable trace
* drops that arm rather than blanking the page. * drops that arm rather than blanking the page — and the page SAYS it dropped
* one, because a missing arm that goes unmentioned is a missing arm nobody can
* ask about.
*/ */
async function loadBundle(slug: string): Promise<DemoBundle> { async function loadBundle(slug: string): Promise<{ bundle: DemoBundle; dropped: number }> {
if (slug === MOCK_SLUG) { if (slug === MOCK_SLUG) {
// Dynamic, so the mock lands in its own chunk and production never fetches // Dynamic, so the mock lands in its own chunk and production never fetches
// it. A static import would ship several hundred lines of fake trace to // it. A static import would ship several hundred lines of fake trace to
// every visitor of every real demo. // every visitor of every real demo.
const mock = await import('./mock'); const mock = await import('./mock');
return { demo: mock.mockDemo, runs: mock.mockRuns, episodes: mock.mockEpisodes }; return {
bundle: { demo: mock.mockDemo, runs: mock.mockRuns, episodes: mock.mockEpisodes },
dropped: 0,
};
} }
const demo = await loadDemoModule(slug); const demo = await loadDemoModule(slug);
@@ -83,14 +102,21 @@ async function loadBundle(slug: string): Promise<DemoBundle> {
const settled = await Promise.allSettled(runs.map((run) => loadEpisode(run))); const settled = await Promise.allSettled(runs.map((run) => loadEpisode(run)));
const episodes: Record<string, DemoEpisode> = {}; const episodes: Record<string, DemoEpisode> = {};
let dropped = 0;
settled.forEach((outcome, index) => { settled.forEach((outcome, index) => {
const run = runs[index]; const run = runs[index];
if (!run) return; if (!run) return;
if (outcome.status === 'fulfilled') episodes[run.id] = outcome.value; if (outcome.status === 'fulfilled') episodes[run.id] = outcome.value;
else console.error(`[pig-demo] dropped run "${run.id}":`, outcome.reason); else {
dropped += 1;
console.error(`[pig-demo] dropped run "${run.id}":`, outcome.reason);
}
}); });
return { demo, runs: runs.filter((run) => episodes[run.id] !== undefined), episodes }; return {
bundle: { demo, runs: runs.filter((run) => episodes[run.id] !== undefined), episodes },
dropped,
};
} }
export interface DemoShellProps { export interface DemoShellProps {
@@ -112,19 +138,19 @@ export function DemoShell({ slug: slugProp, bundle }: DemoShellProps) {
const params = useParams(); const params = useParams();
const slug = slugProp ?? params['slug'] ?? ''; const slug = slugProp ?? params['slug'] ?? '';
const [state, setState] = useState<LoadState>( const [state, setState] = useState<LoadState>(
bundle ? { status: 'ready', bundle } : { status: 'loading' }, bundle ? { status: 'ready', bundle, dropped: 0 } : { status: 'loading' },
); );
useEffect(() => { useEffect(() => {
if (bundle) { if (bundle) {
setState({ status: 'ready', bundle }); setState({ status: 'ready', bundle, dropped: 0 });
return; return;
} }
let live = true; let live = true;
setState({ status: 'loading' }); setState({ status: 'loading' });
loadBundle(slug) loadBundle(slug)
.then((loaded) => { .then((loaded) => {
if (live) setState({ status: 'ready', bundle: loaded }); if (live) setState({ status: 'ready', bundle: loaded.bundle, dropped: loaded.dropped });
}) })
.catch((error: unknown) => { .catch((error: unknown) => {
if (!live) return; if (!live) return;
@@ -142,11 +168,11 @@ export function DemoShell({ slug: slugProp, bundle }: DemoShellProps) {
if (state.status === 'error') { if (state.status === 'error') {
return ( return (
<main className={cn(st.shell, 'py-16')}> <main className={cn(st.shell, 'py-16')}>
<div role="alert" className="card max-w-xl p-6"> <div role="alert" className="max-w-xl rounded-xl border border-border bg-surface p-5 sm:p-6">
<h1 className={st.h2}>That demo is not here</h1> <h1 className="text-h2 text-fg">That environment is not here</h1>
<p className={cn(st.prose, 'mt-3')}>{state.message}</p> <p className="mt-3 max-w-measure text-prose text-fg-2">{state.message}</p>
<a href="/gallery" className={cn(st.btnSecondary, 'mt-6')}> <a href="/gallery" className={cn(st.btnSecondary, 'mt-6')}>
Back to the demos All environments
</a> </a>
</div> </div>
</main> </main>
@@ -158,12 +184,12 @@ export function DemoShell({ slug: slugProp, bundle }: DemoShellProps) {
// so a crash names it, and resetting re-renders the surfaces rather than // so a crash names it, and resetting re-renders the surfaces rather than
// re-navigating. // re-navigating.
<DemoErrorBoundary demoTitle={state.bundle.demo.meta.title}> <DemoErrorBoundary demoTitle={state.bundle.demo.meta.title}>
<DemoBody bundle={state.bundle} /> <DemoBody bundle={state.bundle} dropped={state.dropped} />
</DemoErrorBoundary> </DemoErrorBoundary>
); );
} }
function DemoBody({ bundle }: { bundle: DemoBundle }) { function DemoBody({ bundle, dropped }: { bundle: DemoBundle; dropped: number }) {
const { demo, runs, episodes } = bundle; const { demo, runs, episodes } = bundle;
const isDesktop = useIsDesktop(); const isDesktop = useIsDesktop();
@@ -271,25 +297,39 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
// agent's. With no recording to match, the demo's own first board will do. // agent's. With no recording to match, the demo's own first board will do.
const playSeed = run?.seed ?? 0; const playSeed = run?.seed ?? 0;
// The newest capture, for the Evidence readout. `runs` is manifest order,
// which is not date order.
const newestCapture = useMemo(
() =>
runs.reduce<string | null>(
(newest, candidate) =>
newest === null || candidate.capturedAt > newest ? candidate.capturedAt : newest,
null,
),
[runs],
);
const headerStats: Stat[] = episode const headerStats: Stat[] = episode
? [ ? [
{ {
label: 'Outcome', label: 'Outcome',
value: episode.outcome, value: episode.outcome,
tone: episode.outcome === 'solved' ? 'positive' : 'warning', tone: episode.outcome === 'solved' ? 'positive' : 'warning',
...(episode.truncated ? { title: 'Truncated before a terminal state' } : {}), title: episode.truncated
? 'Truncated before a terminal state'
: 'How the recorded run ended',
}, },
{ {
label: 'Reward', label: 'Reward',
value: formatOrDash(rewardTotal(episode.rewards, demo.reward.components)), value: formatOrDash(rewardTotal(episode.rewards, demo.reward.components)),
tone: 'brand', tone: 'brand',
title: 'Total under the shipped weights', title: 'Reward — total under the shipped weights',
}, },
{ label: 'Steps', value: steps.length, title: 'Model calls in this run' }, { label: 'Steps', value: steps.length, title: 'Steps — model calls in this run' },
{ {
label: 'Seed', label: 'Seed',
value: episode.seed, value: episode.seed,
title: 'The same seed reproduces this board', title: 'Seed — the same seed reproduces this board',
}, },
] ]
: []; : [];
@@ -322,19 +362,7 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
{ {
id: 'call', id: 'call',
label: 'Model call', label: 'Model call',
content: ( content: <ModelCallPanel call={current?.call ?? null} reply={current?.reply ?? null} />,
<div className="space-y-3">
<ModelCallPanel call={current?.call ?? null} />
{current?.reply ? (
<div className="card p-3">
<h3 className="text-sm font-semibold">Reply</h3>
<p className="mt-1 whitespace-pre-wrap font-mono text-xs leading-relaxed">
{current.reply}
</p>
</div>
) : null}
</div>
),
}, },
// A demo's own extra panels ride alongside the step detail, where they sit // A demo's own extra panels ride alongside the step detail, where they sit
// next to the step they are almost always about. With no recording there is // next to the step they are almost always about. With no recording there is
@@ -347,19 +375,6 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
? detailPanel ? detailPanel
: DEFAULT_DETAIL_PANEL; : DEFAULT_DETAIL_PANEL;
const timeline = (
<StepTimeline
steps={steps}
current={player.index}
onSelect={(next) => {
player.pause();
player.seek(next);
}}
Surface={Surface}
onTogglePlay={player.toggle}
/>
);
return ( return (
<main className={cn(st.shell, 'pb-16')}> <main className={cn(st.shell, 'pb-16')}>
{/* {/*
@@ -371,15 +386,23 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
{current?.announce ?? ''} {current?.announce ?? ''}
</div> </div>
<header className="pt-6 sm:pt-8"> <header className="pt-6">
<p className={cn(st.eyebrow, 'text-accent-fg')}>For {demo.meta.persona}</p> <div className="flex flex-wrap items-center gap-2">
<h1 className={cn(st.h2, 'mt-1')}>{demo.meta.title}</h1> <Label className="text-accent-fg">For {demo.meta.persona}</Label>
<p className={cn(st.lede, 'mt-2 max-w-prose')}>{demo.meta.tagline}</p> <StatusPill status={demo.meta.status} />
</div>
<h1 className="mt-1.5 text-h1 text-fg lg:text-h1-lg">{demo.meta.title}</h1>
<p className="mt-2 max-w-measure-wide text-pretty text-lede text-fg-2 lg:text-lede-lg">
{demo.meta.tagline}
</p>
{/* The buyer's question, kept quiet on purpose: it is the thing they {/* The buyer's question, kept quiet on purpose: it is the thing they
walked in with, not the thing this page is asserting. */} walked in with, not the thing this page is asserting. */}
<p className="mt-2 max-w-prose text-sm italic leading-relaxed text-muted"> <div className="mt-4 border-l-2 border-border-strong pl-3">
<Label>The question you walked in with</Label>
<p className="mt-0.5 max-w-[60ch] text-pretty text-prose text-fg-2">
{demo.narrative.anxiety} {demo.narrative.anxiety}
</p> </p>
</div>
{/* {/*
The stats describe the RECORDED RUN, so they only belong on the tabs The stats describe the RECORDED RUN, so they only belong on the tabs
whose subject is that run. On Play they sat above the visitor's own whose subject is that run. On Play they sat above the visitor's own
@@ -387,23 +410,22 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
having already failed before you have touched a key. having already failed before you have touched a key.
*/} */}
{run && headerStats.length > 0 && activeTab !== 'play' ? ( {run && headerStats.length > 0 && activeTab !== 'play' ? (
// One line that scrolls itself rather than a block that wraps: every <div className="mt-4 flex flex-wrap items-center gap-x-4 gap-y-2">
// row this header spends is a row of the interactive board pushed
// below the fold on a 390px phone.
<div className="mt-3 flex items-center gap-2 overflow-x-auto pb-1 sm:flex-wrap sm:overflow-visible sm:pb-0">
<StatStrip stats={headerStats} /> <StatStrip stats={headerStats} />
<RecordedBadge <RecordedBadge
model={run.model} model={run.model}
capturedAt={run.capturedAt} capturedAt={run.capturedAt}
{...(run.intervention ? { intervention: run.intervention } : {})} {...(run.intervention ? { intervention: run.intervention } : {})}
className="ml-0 shrink-0 flex-nowrap" {...(player.timingIsReal || activeTab !== 'watch'
? {}
: { timingNote: 'pacing approximate' })}
/> />
</div> </div>
) : null} ) : null}
<SlotRegion id="hero-aside" /> <SlotRegion id="hero-aside" />
</header> </header>
<Tabs value={activeTab} onValueChange={setTabParam} className="mt-4 sm:mt-5"> <Tabs value={activeTab} onValueChange={setTabParam} className="mt-5 sm:mt-6">
<DemoTabBar tabs={tabs} /> <DemoTabBar tabs={tabs} />
{tabs.includes('play') ? ( {tabs.includes('play') ? (
@@ -412,23 +434,45 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
// Reward and back. Radix leaves the hiding to the author under // Reward and back. Radix leaves the hiding to the author under
// `forceMount`, which is what the `data-[state=inactive]` class does — // `forceMount`, which is what the `data-[state=inactive]` class does —
// it is load-bearing, not belt-and-braces. // it is load-bearing, not belt-and-braces.
<TabsContent <Panel
value="play" value="play"
forceMount forceMount
className="mt-5 space-y-5 data-[state=inactive]:hidden sm:mt-6 sm:space-y-6" className="mt-5 space-y-6 data-[state=inactive]:hidden sm:mt-6"
> >
<TabClaim>{claims.play}</TabClaim> <TabClaim>{claims.play}</TabClaim>
<PlayYourself demo={demo} seed={playSeed} /> <PlayYourself
demo={demo}
seed={playSeed}
// The bridge to Watch, rendered only once the visitor has acted
// and only when there is a Watch tab to bridge to. No slug, no
// narrative literal: the same sentence on every environment.
{...(tabs.includes('watch')
? {
afterMove: (
<p className="max-w-measure text-prose text-fg-2">
The score on this board comes from the grader, not from us.{' '}
<button
type="button"
onClick={() => setTabParam('watch')}
className="font-medium text-accent-fg underline decoration-border-strong underline-offset-4 hover:decoration-accent-fg"
>
Watch a model take the same board
</button>
</p>
),
}
: {})}
/>
<SlotRegion id="below-board" /> <SlotRegion id="below-board" />
<div className="space-y-2"> <div className="space-y-3">
<h2 className={st.h3}>The machine you are inside</h2> <Label as="h2">The machine you are inside</Label>
<EnvAnatomy anatomy={demo.anatomy} rewardLine={demo.meta.rewardLine} compact /> <EnvAnatomy anatomy={demo.anatomy} rewardLine={demo.meta.rewardLine} compact />
</div> </div>
</TabsContent> </Panel>
) : null} ) : null}
{tabs.includes('watch') && run && episode ? ( {tabs.includes('watch') && run && episode ? (
<TabsContent value="watch" className="mt-6 space-y-4"> <Panel value="watch" className="mt-5 space-y-4 sm:mt-6">
<TabClaim>{claims.watch}</TabClaim> <TabClaim>{claims.watch}</TabClaim>
{runs.length > 1 ? ( {runs.length > 1 ? (
@@ -472,13 +516,15 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
/> />
<div className="grid grid-cols-1 gap-3 lg:grid-cols-[auto_minmax(0,1fr)] lg:items-start"> <div className="grid grid-cols-1 gap-3 lg:grid-cols-[auto_minmax(0,1fr)] lg:items-start">
<div className="card w-fit p-4"> <div className="w-fit max-w-full rounded-xl border border-border bg-surface p-3 sm:p-4">
{current ? <Surface state={current.state} /> : null} {current ? <Surface state={current.state} /> : null}
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<Tabs value={activeDetail} onValueChange={setDetailPanel}> <Tabs value={activeDetail} onValueChange={setDetailPanel}>
<TabsList aria-label="Details for this step" className="w-full overflow-x-auto"> {/* The nested list keeps the PILL segment style, so the
primary underline bar and this never look alike. */}
<TabsList aria-label="Details for this step" className="max-w-full overflow-x-auto">
{detailPanels.map((panel) => ( {detailPanels.map((panel) => (
<TabsTrigger key={panel.id} value={panel.id}> <TabsTrigger key={panel.id} value={panel.id}>
{panel.label} {panel.label}
@@ -486,7 +532,7 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
))} ))}
</TabsList> </TabsList>
{detailPanels.map((panel) => ( {detailPanels.map((panel) => (
<TabsContent key={panel.id} value={panel.id}> <TabsContent key={panel.id} value={panel.id} className="mt-3">
{panel.content} {panel.content}
</TabsContent> </TabsContent>
))} ))}
@@ -494,13 +540,27 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
</div> </div>
</div> </div>
{timeline} <StepTimeline
steps={steps}
current={player.index}
onSelect={(next) => {
player.pause();
player.seek(next);
}}
Surface={Surface}
onTogglePlay={player.toggle}
/>
{player.timingIsReal ? null : ( {player.timingIsReal ? null : (
<p className="text-xs text-muted"> <p className="max-w-measure text-small text-muted">
Some steps in this run carried no recorded latency, so their dwell on the timeline Some steps in this run carried no recorded latency, so their dwell on the timeline
is the player's fallback rather than a measurement. is the player's fallback rather than a measurement.
</p> </p>
)} )}
{dropped > 0 ? (
<p className="font-mono text-caption text-muted">
{dropped} recorded {dropped === 1 ? 'run' : 'runs'} could not be loaded
</p>
) : null}
<SlotRegion id="below-timeline" /> <SlotRegion id="below-timeline" />
{blindPair ? ( {blindPair ? (
@@ -529,10 +589,10 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
}} }}
/> />
) : null} ) : null}
</TabsContent> </Panel>
) : null} ) : null}
<TabsContent value="reward" className="mt-6 space-y-4"> <Panel value="reward" className="mt-5 space-y-4 sm:mt-6">
<TabClaim>{claims.reward}</TabClaim> <TabClaim>{claims.reward}</TabClaim>
{episode ? ( {episode ? (
@@ -543,7 +603,7 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
/> />
) : ( ) : (
<> <>
<p className={cn(st.prose, 'max-w-prose')}> <p className="max-w-measure text-prose text-fg-2">
No recorded run has been scored for this environment yet, so every term below No recorded run has been scored for this environment yet, so every term below
reads as not scored rather than as zero. The weights are the ones the environment reads as not scored rather than as zero. The weights are the ones the environment
ships. ships.
@@ -553,11 +613,10 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
)} )}
{/* The editor carries the ranking it re-orders in its own right-hand {/* The editor carries the ranking it re-orders in its own right-hand
column, so the two are never on screen apart. */} column, so the two are never on screen apart. Absent, not greyed,
when there is nothing to rank. */}
{arms.length > 1 ? <RewardEditor spec={demo.reward} arms={arms} /> : null} {arms.length > 1 ? <RewardEditor spec={demo.reward} arms={arms} /> : null}
{episode ? <VerifyBadge demo={demo} episode={episode} /> : null}
{run && episode ? ( {run && episode ? (
<HeadlineMetric <HeadlineMetric
label={`Total reward — ${run.label}`} label={`Total reward — ${run.label}`}
@@ -568,9 +627,9 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
) : null} ) : null}
<SlotRegion id="beside-reward" /> <SlotRegion id="beside-reward" />
</TabsContent> </Panel>
<TabsContent value="evidence" className="mt-6 space-y-6"> <Panel value="evidence" className="mt-5 space-y-6 sm:mt-6">
<TabClaim>{claims.evidence}</TabClaim> <TabClaim>{claims.evidence}</TabClaim>
{/* {/*
@@ -580,10 +639,45 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
tab a visitor opens to read rather than to do — Play stays a board tab a visitor opens to read rather than to do — Play stays a board
above the fold, which is the one thing a paragraph here would cost. above the fold, which is the one thing a paragraph here would cost.
*/} */}
<p className={cn(st.prose, 'max-w-prose')}>{demo.narrative.thesis}</p> <div className="space-y-3">
<p className="max-w-measure text-pretty text-prose text-fg-2">{demo.narrative.thesis}</p>
{/* The readout that left the landing. Every value is data. */}
<p className="flex flex-wrap gap-x-2 gap-y-1 font-mono text-caption text-muted">
<span>
<span className="uppercase tracking-label">verifiers</span>{' '}
<span className="text-fg">{demo.provenance.verifiersVersion}</span>
</span>
{newestCapture ? (
<>
<span aria-hidden="true">·</span>
<span>
<span className="uppercase tracking-label">captured</span>{' '}
<span className="text-fg">{formatDate(newestCapture)}</span>
</span>
</>
) : null}
{run ? (
<>
<span aria-hidden="true">·</span>
<span title="The same seed reproduces this board">
<span className="uppercase tracking-label">seed</span>{' '}
<span className="text-fg">{run.seed}</span>
</span>
</>
) : null}
</p>
</div>
<EnvAnatomy anatomy={demo.anatomy} rewardLine={demo.meta.rewardLine} /> <EnvAnatomy anatomy={demo.anatomy} rewardLine={demo.meta.rewardLine} />
{/*
The receipt sits on Evidence, not Reward: the Evidence claim both
demos make is "here is the grader, and here is proof your browser
ran it", and that proof belongs next to the printed grader rather
than under a slider. Reward keeps the breakdown and the editor.
*/}
{episode ? <VerifyBadge demo={demo} episode={episode} /> : null}
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:items-start"> <div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:items-start">
<ProvenanceCard provenance={demo.provenance} {...(run ? { run } : {})} /> <ProvenanceCard provenance={demo.provenance} {...(run ? { run } : {})} />
<CodeReceipt <CodeReceipt
@@ -605,7 +699,7 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
<SlotRegion id="before-limits" /> <SlotRegion id="before-limits" />
<LimitsCallout limits={demo.narrative.limits} /> <LimitsCallout limits={demo.narrative.limits} />
</TabsContent> </Panel>
</Tabs> </Tabs>
</main> </main>
); );
@@ -689,33 +783,46 @@ function RunSwitcher({
return ( return (
<div className="flex flex-wrap items-center gap-x-4 gap-y-2"> <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
<div className="flex flex-wrap items-center gap-2">
<Label as="span">Agent</Label>
<SegmentedControl <SegmentedControl
label="Agent" label="Agent"
options={arms.map((arm) => { options={arms.map((arm) => {
const intervention = arm.runs.find((r) => r.intervention)?.intervention; const intervention = arm.runs.find((r) => r.intervention)?.intervention;
return { return {
value: arm.label, value: arm.label,
label: arm.label, label: intervention ? (
<span className="inline-flex items-center gap-1.5">
{arm.label}
{/* PROMPTED: this arm's runs were intervened on. The chip
travels with the arm so a switch never hides it. */}
<Chip variant="default">prompted</Chip>
</span>
) : (
arm.label
),
...(intervention ? { title: intervention } : {}), ...(intervention ? { title: intervention } : {}),
}; };
})} })}
value={activeArm.label} value={activeArm.label}
onChange={pickArm} onChange={pickArm}
className="border border-border p-1"
optionClassName="tap px-3 text-sm"
/> />
</div>
{activeArm.runs.length > 1 ? ( {activeArm.runs.length > 1 ? (
<div className="flex flex-wrap items-center gap-2">
<Label as="span">Seed</Label>
<SegmentedControl <SegmentedControl
label="Hidden word" label="Seed"
options={activeArm.runs.map((run) => ({ options={activeArm.runs.map((run) => ({
value: run.id, value: run.id,
label: `#${run.seed}`, label: `#${run.seed}`,
title: `Seed ${run.seed} — the same seed reproduces this board`,
}))} }))}
value={active.id} value={active.id}
onChange={onSelect} onChange={onSelect}
className="border border-border p-1" optionClassName="px-2.5 font-mono text-caption"
optionClassName="tap px-2.5 text-xs nums"
/> />
</div>
) : null} ) : null}
</div> </div>
); );
@@ -730,23 +837,23 @@ function ShellSkeleton() {
return ( return (
<div className={cn(st.shell, 'py-10')} aria-busy="true"> <div className={cn(st.shell, 'py-10')} aria-busy="true">
<p className="sr-only">Loading the recorded run.</p> <p className="sr-only">Loading the recorded run.</p>
<Skeleton className="h-8 w-64" /> <Skeleton className="h-3 w-40" />
<Skeleton className="mt-3 h-4 w-full max-w-md" /> <Skeleton className="mt-3 h-8 w-64" />
<Skeleton className="mt-4 h-10 w-full max-w-sm" /> <Skeleton className="mt-3 h-5 w-full max-w-md" />
<Skeleton className="mt-6 h-11 w-full max-w-md" /> <Skeleton className="mt-6 h-11 w-full max-w-md" />
<Skeleton className="mt-6 h-80" /> <Skeleton className="mt-6 h-80" />
</div> </div>
); );
} }
/** /**
* The run a visitor sees before choosing one. * The run a visitor sees before choosing one.
* *
* `runs[0]` is the manifest's first entry — the weakest agent on seed 0, which * `runs[0]` is the manifest's first entry — the weakest agent on seed 0, which
* for the first demo shipped was a failed game with thinking off. So the Watch tab opened on a * for the first demo shipped was a failed game with thinking off. So the Watch
* loss with an empty reasoning panel and the Reward tab on a row of zeros: * tab opened on a loss with an empty reasoning panel and the Reward tab on a
* the model's least interesting attempt, chosen by accident of sort order. * row of zeros: the model's least interesting attempt, chosen by accident of
* sort order.
* *
* Prefer, in order: a run with recorded reasoning (there is something to * Prefer, in order: a run with recorded reasoning (there is something to
* stream), then a solved one (the board reaches a conclusion), then the * stream), then a solved one (the board reaches a conclusion), then the
+32 -18
View File
@@ -69,12 +69,25 @@ export function resolveTab(
} }
/** /**
* The tab strip. * The mono index prefix (`01 ``04 `) is the tab's position in the CONTRACT
* order, not in the visible list — a demo without Play still shows Watch as
* `02`, so the numbers mean the same thing on every page.
*/
function indexOf(id: DemoTabId): string {
return String(DEMO_TABS.indexOf(id) + 1).padStart(2, '0');
}
/**
* The primary tab strip: underline tabs.
* *
* Full width and equal columns below `sm` so four tabs land inside 390px * Underline rather than the pill segment for one reason — the nested lists
* without the list becoming a horizontal scroller — a tab bar you have to * inside Watch (Reasoning | Model call) keep the pill style, and primary and
* scroll hides the tabs, which is the one thing it exists to advertise. From * secondary navigation must never look alike. The primitive's `underline`
* `sm` up it shrinks back to its natural width and sits left. * variant supplies the rail, the 44px triggers, the equal columns below `sm`
* and the 2px bar; this adds the sticky behaviour and the index prefix.
* Sticky under the site header from `lg` only: on a phone a sticky strip eats
* a fifth of the viewport. `bg-bg/90` is one of the two alpha grounds the
* spec allows, because it sits on the page ground itself.
*/ */
export function DemoTabBar({ export function DemoTabBar({
tabs, tabs,
@@ -85,16 +98,19 @@ export function DemoTabBar({
}) { }) {
return ( return (
<TabsList <TabsList
variant="underline"
aria-label="How to explore this environment" aria-label="How to explore this environment"
className={cn('flex w-full sm:inline-flex sm:w-auto', className)} className={cn(
'sm:gap-2',
'lg:sticky lg:top-[var(--app-header-h)] lg:z-30 lg:bg-bg/90 lg:backdrop-blur-md',
className,
)}
> >
{tabs.map((id) => ( {tabs.map((id) => (
<TabsTrigger <TabsTrigger key={id} value={id} title={TAB_DESCRIPTIONS[id]}>
key={id} <span aria-hidden="true" className="mr-1.5 hidden font-mono text-label text-muted sm:inline">
value={id} {indexOf(id)}
title={TAB_DESCRIPTIONS[id]} </span>
className="min-w-0 flex-1 px-2 text-[0.8125rem] sm:flex-none sm:px-3 sm:text-sm"
>
{TAB_LABELS[id]} {TAB_LABELS[id]}
</TabsTrigger> </TabsTrigger>
))} ))}
@@ -111,15 +127,13 @@ export function DemoTabBar({
* every panel for that reason, and it is deliberately not an `<h2>`: the tab * every panel for that reason, and it is deliberately not an `<h2>`: the tab
* trigger is already this panel's accessible name, and a heading here would * trigger is already this panel's accessible name, and a heading here would
* make the claim navigable furniture instead of a thing someone reads. * make the claim navigable furniture instead of a thing someone reads.
*
* Never mono, never uppercase, never shortened. This is the one place the
* language yields to the lesson outright.
*/ */
export function TabClaim({ children, className }: { children: ReactNode; className?: string }) { export function TabClaim({ children, className }: { children: ReactNode; className?: string }) {
return ( return (
<p <p className={cn('max-w-[60ch] text-pretty text-claim text-fg lg:text-claim-lg', className)}>
className={cn(
'max-w-2xl text-pretty text-base leading-snug text-fg sm:text-lg lg:text-xl',
className,
)}
>
{children} {children}
</p> </p>
); );
+39 -64
View File
@@ -1,5 +1,7 @@
import type { LucideIcon } from 'lucide-react'; import { ArrowRight } from 'lucide-react';
import { ArrowRight, ListChecks, Scale, Target, TrendingUp } from 'lucide-react';
import { anatomyTerm } from '@/content/anatomy';
import type { AnatomyKey } from '@/content/anatomy';
import type { DemoModule } from '@/lib/demo-kit/types'; import type { DemoModule } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -9,107 +11,80 @@ export interface EnvAnatomyProps {
anatomy: Anatomy; anatomy: Anatomy;
/** `DemoMeta.rewardLine` — six words on what the reward pays for. */ /** `DemoMeta.rewardLine` — six words on what the reward pays for. */
rewardLine?: string; rewardLine?: string;
/** Set on the gallery/overview page, where the four boxes are context rather /**
* than the lesson, to drop the closing line and tighten the type. */ * The Play-tab form: four boxes and their bodies, no questions and no closing
* line, because the visitor has just acted and the boxes are a caption to
* what they did rather than the lesson itself.
*/
compact?: boolean; compact?: boolean;
className?: string; className?: string;
} }
interface Box {
key: keyof Anatomy;
kicker: string;
question: string;
Icon: LucideIcon;
}
/** /**
* The four boxes, in the order an executive builds the mental model: what is * The four boxes, in the order an executive builds the mental model: what is
* being asked, what the agent is allowed to do, who decides whether it was * being asked, what the agent is allowed to do, who decides whether it was
* good, and what number that decision moves. * good, and what number that decision moves.
* *
* The kickers are deliberately generic. This object is the site's one piece of * The kickers come from `@/content/anatomy` — the SAME strings the landing's
* definition strip renders. This object is the site's one piece of
* transferable explanation: someone who learns the machine on a word game * transferable explanation: someone who learns the machine on a word game
* should read the fraud demo as "same machine, different grader", and that only * should read the fraud screen as "same machine, different grader", and that
* works if the four labels never change between demos. * only works if the four labels never change between the landing and a demo.
* The order is the contract's, whatever order the content file lists them in.
*/ */
const BOXES: Box[] = [ const KEYS: readonly AnatomyKey[] = ['task', 'actions', 'grader', 'score'];
{ key: 'task', kicker: 'The task', question: 'What is the agent asked to do?', Icon: Target }, const BOXES = KEYS.map(anatomyTerm);
{
key: 'actions',
kicker: 'Legal actions',
question: 'What is it allowed to do?',
Icon: ListChecks,
},
{
key: 'grader',
kicker: 'The grader',
question: 'Who decides whether it was good?',
Icon: Scale,
},
{
key: 'score',
kicker: 'The score that moves',
question: 'What number does that produce?',
Icon: TrendingUp,
},
];
export function EnvAnatomy({ anatomy, rewardLine, compact = false, className }: EnvAnatomyProps) { export function EnvAnatomy({ anatomy, rewardLine, compact = false, className }: EnvAnatomyProps) {
return ( return (
<div className={cn('space-y-4', className)}> <div className={cn('space-y-4', className)}>
<ol className="flex flex-col lg:flex-row lg:items-stretch"> <ol className="flex flex-col lg:flex-row lg:items-stretch">
{BOXES.map((box, index) => ( {BOXES.map((box, position) => {
const grader = box.key === 'grader';
return (
<li <li
key={box.key} key={box.key}
className="flex flex-col items-stretch lg:flex-1 lg:flex-row lg:items-center" className="flex flex-col items-stretch lg:flex-1 lg:flex-row lg:items-stretch"
> >
<div <div
className={cn( className={cn(
'card flex flex-1 flex-col gap-2 p-4', 'flex flex-1 flex-col gap-2 rounded-xl border p-4',
// The grader is the box every later demo differs on. It is the // The grader is the box every later demo differs on. It is
// one the eye should land on second, after the task. // the one the eye should land on second, after the task.
box.key === 'grader' && 'border-brand/40 bg-accent-subtle/40', grader ? 'border-accent-fg bg-accent-subtle' : 'border-border bg-surface',
)} )}
> >
<div className="flex items-center gap-2"> <div className="flex items-baseline gap-2">
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-lg bg-accent-subtle text-accent-fg"> <span className="font-mono text-label text-muted" aria-hidden="true">
<box.Icon className="h-4 w-4" strokeWidth={2} aria-hidden="true" /> {box.index}
</span> </span>
<div className="min-w-0"> <p className="text-ui font-semibold leading-tight text-fg">{box.kicker}</p>
<p className="text-sm font-semibold leading-tight">
<span className="nums mr-1.5 text-muted">{index + 1}</span>
{box.kicker}
</p>
{!compact ? (
<p className="text-xs leading-tight text-muted">{box.question}</p>
) : null}
</div> </div>
{!compact ? <p className="text-small text-muted">{box.question}</p> : null}
<p className="text-pretty text-small text-fg-2">{anatomy[box.key]}</p>
</div> </div>
<p className={cn('text-pretty text-fg', compact ? 'text-xs' : 'text-sm')}> {position < BOXES.length - 1 ? (
{anatomy[box.key]}
</p>
</div>
{index < BOXES.length - 1 ? (
<div <div
aria-hidden="true" aria-hidden="true"
className="flex items-center justify-center py-2 text-muted lg:px-2 lg:py-0" className="flex items-center justify-center py-1.5 text-border-strong lg:px-1.5 lg:py-0"
> >
<ArrowRight className="h-4 w-4 rotate-90 lg:rotate-0" /> <ArrowRight className="size-4 rotate-90 lg:rotate-0" />
</div> </div>
) : null} ) : null}
</li> </li>
))} );
})}
</ol> </ol>
{rewardLine ? ( {rewardLine ? (
<p className="text-sm text-muted"> <p className="text-ui text-muted">
<span className="font-medium text-fg">This reward: </span> <span className="font-mono text-label uppercase tracking-label">This reward</span>{' '}
{rewardLine} <span className="text-fg-2">{rewardLine}</span>
</p> </p>
) : null} ) : null}
{!compact ? ( {!compact ? (
<p className="max-w-3xl text-pretty text-sm leading-relaxed text-muted"> <p className="max-w-measure text-pretty text-prose text-fg-2">
Every demo on this site is that same machine. The task changes, the legal actions Every demo on this site is that same machine. The task changes, the legal actions
change, and the grader changes but the grader is always code you can read, and the change, and the grader changes but the grader is always code you can read, and the
score is always a number you can watch move. That is what makes an environment score is always a number you can watch move. That is what makes an environment
+47 -33
View File
@@ -1,75 +1,89 @@
import { ArrowRight, ShieldQuestion } from 'lucide-react';
import { getDemo } from '@/lib/demo-kit/registry'; import { getDemo } from '@/lib/demo-kit/registry';
import type { Limit } from '@/lib/demo-kit/types'; import type { DemoStatus, Limit } from '@/lib/demo-kit/types';
import * as st from '@/content/styles';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export interface ResolvedDemo {
title: string;
href: string;
/** `spec` renders as "not built yet" beside the link. */
status?: DemoStatus;
}
export interface LimitsCalloutProps { export interface LimitsCalloutProps {
limits: Limit[]; limits: Limit[];
/** /**
* Turns a demo slug into something linkable. The shell has no registry * Turns a demo slug into something linkable. Defaults to the registry, so a
* dependency of its own, so the page that knows the routes supplies this. * limit pointing at a demo that does not exist yet renders as a stated gap
* rather than as a link to a 404 — the honest outcome, since the roadmap is
* allowed to be ahead of the repository.
*/ */
resolveDemo?: (slug: string) => { title: string; href: string } | undefined; resolveDemo?: (slug: string) => ResolvedDemo | undefined;
title?: string; title?: string;
className?: string; className?: string;
} }
/** function defaultResolve(slug: string): ResolvedDemo | undefined {
* A limit names the demo that closes it by SLUG. Resolving it through the
* registry means a limit pointing at a demo that does not exist yet renders as
* a stated gap rather than as a link to a 404 — which is the honest outcome,
* since the roadmap is allowed to be ahead of the repository.
*/
function defaultResolve(slug: string): { title: string; href: string } | undefined {
const meta = getDemo(slug); const meta = getDemo(slug);
return meta ? { title: meta.title, href: `/demos/${meta.slug}` } : undefined; return meta ? { title: meta.title, href: `/demos/${meta.slug}`, status: meta.status } : undefined;
} }
/** /**
* What this demo does not teach. * What this demo does not teach.
* *
* Styled as confidence, not apology. A vendor who tells you the limits of their * Styled as confidence, not apology: an amber card, because a limit is a
* own demo before you find them is a vendor you believe about everything else * caution about the data and amber is what every other caution on the site
* on the page — and every limit here names the demo that closes it, so the list * wears. A vendor who tells you the limits of their own demo before you find
* reads as a roadmap rather than a disclaimer. * them is a vendor you believe about everything else on the page — and every
* limit names the demo that closes it, so the list reads as a roadmap rather
* than a disclaimer.
*/ */
export function LimitsCallout({ export function LimitsCallout({
limits, limits,
resolveDemo = defaultResolve, resolveDemo = defaultResolve,
title = 'What this demo does not teach', title = 'What this does not teach',
className, className,
}: LimitsCalloutProps) { }: LimitsCalloutProps) {
if (limits.length === 0) return null; if (limits.length === 0) return null;
return ( return (
<section aria-label={title} className={cn('card p-4', className)}> <section
<div className="flex items-center gap-2"> aria-label={title}
<ShieldQuestion className="size-4 text-muted" aria-hidden="true" /> className={cn('rounded-xl border border-border bg-warning-subtle p-4 sm:p-5', className)}
<h3 className="text-sm font-semibold">{title}</h3> >
</div> <h3 className="font-mono text-label uppercase tracking-label text-warning">{title}</h3>
<ul className="mt-3 space-y-3"> <ol className="mt-3 space-y-3">
{limits.map((limit) => { {limits.map((limit, index) => {
const target = limit.answeredBy ? resolveDemo(limit.answeredBy) : undefined; const target = limit.answeredBy ? resolveDemo(limit.answeredBy) : undefined;
return ( return (
<li key={limit.text} className="border-l-2 border-border pl-3"> <li key={limit.text} className="flex gap-3">
<p className="text-pretty text-sm leading-relaxed text-fg">{limit.text}</p> <span className="pt-1 font-mono text-label text-muted" aria-hidden="true">
{String(index + 1).padStart(2, '0')}
</span>
<div className="min-w-0 flex-1">
<p className="max-w-measure text-pretty text-prose text-fg-2">{limit.text}</p>
{target ? ( {target ? (
<p className="mt-1 flex flex-wrap items-center gap-x-2 text-small">
<a <a
href={target.href} href={target.href}
className={cn(st.link, 'mt-1 inline-flex items-center gap-1 text-xs')} className="inline-flex min-h-11 items-center font-medium text-accent-fg underline decoration-border-strong underline-offset-4 hover:decoration-accent-fg sm:min-h-0"
> >
Answered by {target.title} answered by {target.title}
<ArrowRight className="size-3" aria-hidden="true" />
</a> </a>
{target.status === 'spec' ? (
<span className="font-mono text-label uppercase tracking-label text-muted">
not built yet
</span>
) : null}
</p>
) : ( ) : (
<p className="mt-1 text-xs text-muted"> <p className="mt-1 text-small text-muted">
No demo answers this one yet. It is a real gap, not a rhetorical one. No demo answers this one yet. It is a real gap, not a rhetorical one.
</p> </p>
)} )}
</div>
</li> </li>
); );
})} })}
</ul> </ol>
</section> </section>
); );
} }
+31 -18
View File
@@ -27,14 +27,24 @@ export interface MetricChartProps {
digits?: number; digits?: number;
} }
/*
* Every colour is an `hsl(var(--…))` string read from the token layer at paint
* time, so the chart follows the theme toggle without a re-render and no hex
* ever lands in this file. The series takes `--tab-underline`, which is the
* one token that is the accent fill in dark and the accent INK in light — a
* 2px line on the page ground is a state indicator, and the raw accent is
* 1.47:1 against white.
*/
const SERIES = 'hsl(var(--tab-underline))';
const GRID = 'hsl(var(--border))';
const BASELINE = 'hsl(var(--border-strong))';
const TICK = 'hsl(var(--muted))';
/** /**
* The chart, in its own module so `React.lazy` can hold recharts out of the * The chart, in its own module so `React.lazy` can hold recharts out of the
* entry chunk. Nothing else in the shell may import this file directly — an * entry chunk. Nothing else in the shell may import this file directly — an
* ordinary import here defeats the whole arrangement and the entry chunk grows * ordinary import here defeats the whole arrangement and the entry chunk grows
* by ~100 kB without anyone noticing. * by ~100 kB without anyone noticing.
*
* Every colour is read from the token layer at paint time rather than passed in
* as a literal, so the chart follows the theme toggle without a re-render.
*/ */
export default function MetricChart({ export default function MetricChart({
data, data,
@@ -44,19 +54,19 @@ export default function MetricChart({
digits = 3, digits = 3,
}: MetricChartProps) { }: MetricChartProps) {
return ( return (
<div style={{ height }} className="w-full"> <div style={{ height }} className="w-full font-mono">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: 4 }}> <LineChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: 4 }}>
<CartesianGrid stroke="hsl(var(--border))" strokeDasharray="2 4" vertical={false} /> <CartesianGrid stroke={GRID} strokeDasharray="2 4" vertical={false} />
<XAxis <XAxis
dataKey="x" dataKey="x"
tick={{ fill: 'hsl(var(--muted))', fontSize: 11 }} tick={{ fill: TICK, fontSize: 11, fontFamily: 'inherit' }}
tickLine={false} tickLine={false}
axisLine={{ stroke: 'hsl(var(--border))' }} axisLine={{ stroke: GRID }}
minTickGap={12} minTickGap={12}
/> />
<YAxis <YAxis
tick={{ fill: 'hsl(var(--muted))', fontSize: 11 }} tick={{ fill: TICK, fontSize: 11, fontFamily: 'inherit' }}
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
width={40} width={40}
@@ -65,18 +75,21 @@ export default function MetricChart({
{baseline ? ( {baseline ? (
<ReferenceLine <ReferenceLine
y={baseline.value} y={baseline.value}
stroke="hsl(var(--muted))" stroke={BASELINE}
strokeDasharray="5 4" strokeDasharray="5 4"
// Right-anchored: a baseline near zero sits on the x axis, and
// a left-anchored label lands on the first tick's text.
label={{ label={{
value: baseline.label, value: baseline.label,
position: 'insideTopLeft', position: 'insideTopRight',
fill: 'hsl(var(--muted))', fill: TICK,
fontSize: 11, fontSize: 11,
fontFamily: 'inherit',
}} }}
/> />
) : null} ) : null}
<Tooltip <Tooltip
cursor={{ stroke: 'hsl(var(--border))' }} cursor={{ stroke: BASELINE }}
content={(props: TooltipProps<number, string>) => ( content={(props: TooltipProps<number, string>) => (
<ChartTooltip {...props} digits={digits} /> <ChartTooltip {...props} digits={digits} />
)} )}
@@ -87,10 +100,10 @@ export default function MetricChart({
// recorded arm) that curve is a claim nobody measured. // recorded arm) that curve is a claim nobody measured.
type="linear" type="linear"
dataKey="y" dataKey="y"
stroke="hsl(var(--accent))" stroke={SERIES}
strokeWidth={2} strokeWidth={2}
dot={{ r: 2.5, fill: 'hsl(var(--accent))', strokeWidth: 0 }} dot={{ r: 3, fill: SERIES, strokeWidth: 0 }}
activeDot={{ r: 4 }} activeDot={{ r: 4.5, fill: SERIES, strokeWidth: 0 }}
isAnimationActive={animate} isAnimationActive={animate}
animationDuration={400} animationDuration={400}
/> />
@@ -109,9 +122,9 @@ function ChartTooltip({
if (!active || !payload || payload.length === 0) return null; if (!active || !payload || payload.length === 0) return null;
const value = payload[0]?.value; const value = payload[0]?.value;
return ( return (
<div className="card px-2.5 py-1.5 text-xs shadow-md"> <div className="rounded-md border border-border bg-surface px-2.5 py-1.5 font-mono text-caption shadow-overlay">
<p className="nums text-muted">{String(label)}</p> <p className="text-muted">{String(label)}</p>
<p className="nums font-mono font-semibold"> <p className="font-semibold text-fg">
{typeof value === 'number' ? formatNumber(value, digits) : '—'} {typeof value === 'number' ? formatNumber(value, digits) : '—'}
</p> </p>
</div> </div>
+58 -33
View File
@@ -1,8 +1,8 @@
import { Suspense, lazy, useMemo } from 'react'; import { Suspense, lazy, useMemo } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { formatNumber, formatSigned, usePrefersReducedMotion } from './format'; import { EditedChip, Label } from './chrome';
import { formatNumber, formatSigned, useMediaQuery, usePrefersReducedMotion } from './format';
import type { MetricPoint } from './MetricChart'; import type { MetricPoint } from './MetricChart';
import { EditedChip } from './StatStrip';
export type { MetricPoint } from './MetricChart'; export type { MetricPoint } from './MetricChart';
@@ -35,6 +35,10 @@ export interface MetricMoverProps {
* The reference rule is not decoration: a number on its own is a claim, and a * The reference rule is not decoration: a number on its own is a claim, and a
* number against the line it used to sit on is evidence. If a demo has no * number against the line it used to sit on is evidence. If a demo has no
* baseline to draw, it should not be using this component. * baseline to draw, it should not be using this component.
*
* Under `sm` the table IS the view and the chart is not fetched: a 168px line
* with four categorical ticks in a 326px column is a picture of a table that
* would fit better than the picture.
*/ */
export function MetricMover({ export function MetricMover({
label, label,
@@ -48,6 +52,7 @@ export function MetricMover({
className, className,
}: MetricMoverProps) { }: MetricMoverProps) {
const reducedMotion = usePrefersReducedMotion(); const reducedMotion = usePrefersReducedMotion();
const wide = useMediaQuery('(min-width: 640px)', true);
// The rule is still drawn when the headline IS the baseline — it is the line // The rule is still drawn when the headline IS the baseline — it is the line
// the other arms are read against. The delta text is not: "+0.000 vs itself" // the other arms are read against. The delta text is not: "+0.000 vs itself"
// is noise dressed up as a measurement. // is noise dressed up as a measurement.
@@ -55,39 +60,68 @@ export function MetricMover({
const delta = rawDelta !== null && Math.abs(rawDelta) > 1e-9 ? rawDelta : null; const delta = rawDelta !== null && Math.abs(rawDelta) > 1e-9 ? rawDelta : null;
const points = useMemo(() => series ?? [], [series]); const points = useMemo(() => series ?? [], [series]);
const table = (
<table className="w-full border-collapse font-mono text-caption">
<caption className="sr-only">{label}, one row per recorded arm</caption>
<thead>
<tr className="bg-surface-3 text-left text-label uppercase tracking-label text-muted">
<th scope="col" className="px-2 py-1.5 font-medium">
Arm
</th>
<th scope="col" className="px-2 py-1.5 text-right font-medium">
Total
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{points.map((point) => (
<tr key={String(point.x)}>
<th scope="row" className="px-2 py-1.5 text-left font-normal text-fg-2">
{String(point.x)}
</th>
<td className="px-2 py-1.5 text-right text-fg">{formatNumber(point.y, digits)}</td>
</tr>
))}
</tbody>
</table>
);
return ( return (
<section aria-label={label} className={cn('card p-4', className)}> <section
aria-label={label}
className={cn('rounded-xl border border-border bg-surface p-4 sm:p-5', className)}
>
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1"> <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h3 className="text-sm font-medium text-muted">{label}</h3> <Label as="h3">{label}</Label>
{edited ? <EditedChip /> : null} {edited ? <EditedChip /> : null}
</div> </div>
<p className="mt-1 flex items-baseline gap-2"> <p className="mt-1.5 flex flex-wrap items-baseline gap-x-2 gap-y-1">
<span className="nums text-4xl font-semibold leading-none tracking-tight"> <span className="font-mono text-metric text-fg">{formatNumber(value, digits)}</span>
{formatNumber(value, digits)} {unit ? <span className="text-ui text-muted">{unit}</span> : null}
</span>
{unit ? <span className="text-sm text-muted">{unit}</span> : null}
{delta !== null ? ( {delta !== null ? (
<span <span
className={cn( className={cn(
'nums text-sm font-medium', 'font-mono text-mono-data font-medium',
delta > 0 ? 'text-positive' : delta < 0 ? 'text-danger' : 'text-muted', delta > 0 ? 'text-positive' : delta < 0 ? 'text-danger' : 'text-muted',
)} )}
> >
{formatSigned(delta, digits)} vs {baseline?.label} {formatSigned(delta, digits)} <span className="text-muted">vs {baseline?.label}</span>
</span> </span>
) : null} ) : null}
</p> </p>
{points.length > 1 ? ( {points.length > 1 ? (
<div className="mt-3"> <div className="mt-3">
{wide ? (
<>
<Suspense <Suspense
fallback={ fallback={
// Reserve the exact chart height. A chart that pops in and pushes // Reserve the exact chart height. A chart that pops in and
// the caption down is the layout shift this whole page is trying // pushes the caption down is the layout shift this whole
// not to have. // page is trying not to have.
<div <div
style={{ height: CHART_HEIGHT }} style={{ height: CHART_HEIGHT }}
className="w-full rounded-lg bg-surface-2" className="w-full rounded-md bg-surface-2"
aria-hidden="true" aria-hidden="true"
/> />
} }
@@ -100,31 +134,22 @@ export function MetricMover({
digits={digits} digits={digits}
/> />
</Suspense> </Suspense>
{/* The chart is a picture of the table; the table is the accessible {/* The chart is a picture of the table; the table is the
version of the picture. Both are the same numbers. */} accessible version of the picture. Both are the same numbers. */}
<details className="mt-1"> <details className="mt-1">
<summary className="tap inline-flex cursor-pointer items-center text-xs text-muted hover:text-fg"> <summary className="tap inline-flex cursor-pointer items-center text-small text-muted hover:text-fg">
Show these points as a table Show these points as a table
</summary> </summary>
<table className="nums mt-1.5 w-full border-collapse font-mono text-xs"> <div className="mt-1.5 overflow-hidden rounded-md border border-border">{table}</div>
<tbody className="divide-y divide-border">
{points.map((point) => (
<tr key={String(point.x)}>
<th scope="row" className="py-1 text-left font-normal text-muted">
{String(point.x)}
</th>
<td className="py-1 text-right">{formatNumber(point.y, digits)}</td>
</tr>
))}
</tbody>
</table>
</details> </details>
</>
) : (
<div className="overflow-hidden rounded-md border border-border">{table}</div>
)}
</div> </div>
) : null} ) : null}
{caption ? ( {caption ? <p className="mt-3 max-w-measure text-small text-muted">{caption}</p> : null}
<p className="mt-3 text-sm leading-relaxed text-muted">{caption}</p>
) : null}
</section> </section>
); );
} }
+37 -21
View File
@@ -4,6 +4,8 @@ import { DASH, formatInt, formatMs, humaniseToken } from './format';
export interface ModelCallPanelProps { export interface ModelCallPanelProps {
call: ModelCall | null; call: ModelCall | null;
/** The model's visible reply for this step, verbatim. */
reply?: string | null;
className?: string; className?: string;
title?: string; title?: string;
} }
@@ -23,60 +25,74 @@ interface Row {
* not report reasoning tokens is a fact about the trace, and hiding the row * not report reasoning tokens is a fact about the trace, and hiding the row
* would turn "we do not know" into "there were none". * would turn "we do not know" into "there were none".
*/ */
export function ModelCallPanel({ call, className, title = 'Model call' }: ModelCallPanelProps) { export function ModelCallPanel({
call,
reply = null,
className,
title = 'Model call',
}: ModelCallPanelProps) {
const rows: Row[] = [ const rows: Row[] = [
{ {
label: 'finish_reason', label: 'prompt tokens',
value: call?.finishReason ? humaniseToken(call.finishReason) : DASH,
hint: 'Why the model stopped generating',
},
{
label: 'Prompt tokens',
value: formatInt(call?.promptTokens ?? null), value: formatInt(call?.promptTokens ?? null),
hint: 'Everything sent in: system, board, history', hint: 'Everything sent in: system, board, history',
}, },
{ {
label: 'Completion tokens', label: 'completion tokens',
value: formatInt(call?.completionTokens ?? null), value: formatInt(call?.completionTokens ?? null),
hint: 'The visible reply', hint: 'The visible reply',
}, },
{ {
label: 'Reasoning tokens', label: 'reasoning tokens',
value: formatInt(call?.reasoningTokens ?? null), value: formatInt(call?.reasoningTokens ?? null),
hint: 'Billed thinking, when the provider reports it', hint: 'Billed thinking, when the provider reports it',
}, },
{ {
label: 'Latency', label: 'duration',
value: formatMs(call?.durationMs ?? null), value: formatMs(call?.durationMs ?? null),
hint: 'Real elapsed time when the run was recorded', hint: 'Real elapsed time when the run was recorded',
}, },
{
label: 'finish reason',
value: call?.finishReason ? humaniseToken(call.finishReason) : DASH,
hint: 'Why the model stopped generating',
},
]; ];
return ( return (
<section aria-label={title} className={cn('card overflow-hidden', className)}> <section
aria-label={title}
className={cn('overflow-hidden rounded-lg border border-border bg-surface-2', className)}
>
<header className="border-b border-border px-3 py-2"> <header className="border-b border-border px-3 py-2">
<h3 className="text-sm font-semibold">{title}</h3> <h3 className="font-mono text-label uppercase tracking-label text-muted">{title}</h3>
</header> </header>
{call === null ? ( {call === null ? (
<p className="px-3 py-3 text-sm text-muted"> <p className="max-w-measure px-3 py-3 text-prose text-muted">
This step did not involve a model call it is a state change the environment made on This step did not involve a model call it is a state change the environment made on
its own. its own.
</p> </p>
) : ( ) : (
<dl className="divide-y divide-border"> <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 px-3 py-1.5 font-mono text-mono-data">
{rows.map((row) => ( {rows.map((row) => (
<div key={row.label} className="flex items-baseline gap-3 px-3 py-2"> <div key={row.label} className="col-span-2 grid grid-cols-subgrid items-baseline py-1">
<dt className="min-w-0 flex-1"> <dt className="text-label uppercase tracking-label text-muted" title={row.hint}>
<span className="block text-sm font-medium">{row.label}</span> {row.label}
{row.hint ? (
<span className="block text-xs leading-snug text-muted">{row.hint}</span>
) : null}
</dt> </dt>
<dd className="nums shrink-0 font-mono text-sm text-fg">{row.value}</dd> <dd className="text-right text-fg">{row.value}</dd>
</div> </div>
))} ))}
</dl> </dl>
)} )}
{reply ? (
<div className="border-t border-border">
<h4 className="px-3 pt-2 font-mono text-label uppercase tracking-label text-muted">Reply</h4>
<pre className="m-3 mt-1.5 overflow-x-auto whitespace-pre-wrap break-words rounded-md border border-border bg-surface-3 p-2.5 font-mono text-caption leading-relaxed text-fg [font-feature-settings:'liga'_0]">
{reply}
</pre>
</div>
) : null}
</section> </section>
); );
} }
+42 -17
View File
@@ -1,10 +1,13 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import { RotateCcw } from 'lucide-react'; import { RotateCcw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { LabelledContrastToggle } from '@/components/site/ContrastToggle';
import type { DemoModule } from '@/lib/demo-kit'; import type { DemoModule } from '@/lib/demo-kit';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import * as st from '@/content/styles'; import { Label } from './chrome';
/** /**
* The visitor's own attempt, on the same seed as the run beside it. * The visitor's own attempt, on the same seed as the run beside it.
@@ -20,10 +23,19 @@ import * as st from '@/content/styles';
export function PlayYourself<TState>({ export function PlayYourself<TState>({
demo, demo,
seed, seed,
afterMove,
className, className,
}: { }: {
demo: DemoModule<TState>; demo: DemoModule<TState>;
seed: number; seed: number;
/**
* Rendered under the board once the visitor has changed the state at all.
* The shell has no contract-level way to know a board is FINISHED — the
* state is the demo's own type and the shell never inspects it — so "has
* acted" is the honest gate for the bridge line, and the copy is written for
* that moment rather than for a terminal one.
*/
afterMove?: ReactNode;
className?: string; className?: string;
}) { }) {
const interactive = demo.interactive; const interactive = demo.interactive;
@@ -42,38 +54,51 @@ export function PlayYourself<TState>({
if (!interactive || state === null) return null; if (!interactive || state === null) return null;
const Controls = interactive.Controls; const Controls = interactive.Controls;
const moved = state !== initial;
return ( return (
// The SHELL does not decide how wide a demo's play surface is. A word grid <div className={cn('space-y-3', className)}>
// wants a narrow column with its keyboard beside it; an analyst screen full <div className="flex flex-wrap items-center justify-between gap-2">
// of tables wants most of the page. So this is a generous ceiling and each <Label as="h2">Your attempt</Label>
// demo's Surface and Controls cap themselves — wordle's keyboard carries <div className="flex items-center gap-1">
// its own max width for exactly that reason. {/* The contrast toggle sits here as well as in the header because
<div className={cn('w-full max-w-4xl space-y-3', className)}> this is where a colour-blind visitor is looking when they need it. */}
<div className="flex items-baseline justify-between gap-3"> <LabelledContrastToggle />
<h3 className={st.h3}>Your attempt</h3> <Button
<button
type="button" type="button"
variant="ghost"
size="touch"
className="text-muted hover:text-fg lg:min-h-9"
onClick={() => { onClick={() => {
setState(interactive.init(seed)); setState(interactive.init(seed));
setGeneration((g) => g + 1); setGeneration((g) => g + 1);
}} }}
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted transition-colors duration-1 hover:bg-surface-2 hover:text-fg"
> >
<RotateCcw aria-hidden="true" className="size-3.5" /> <RotateCcw aria-hidden="true" />
Start over Start over
</button> </Button>
</div>
</div> </div>
<div className="card w-fit p-4"> {/* The shell decides the CARD, the Surface decides its own width (§8):
a word grid is a narrow column with its keyboard beside it, an
analyst screen wants most of the page. `flex-wrap` lets either stack
under its controls the moment the two do not fit side by side. */}
<div className="flex flex-wrap items-start gap-4">
<div className="w-fit max-w-full rounded-xl border border-border bg-surface p-3 sm:p-4">
<Surface state={state} /> <Surface state={state} />
</div> </div>
{/* Remounting on `generation` clears any state the controls hold of their {/* Remounting on `generation` clears any state the controls hold of
own — a half-typed word survives a reset otherwise, and the board and their own — a half-typed word survives a reset otherwise, and the
the keyboard then disagree about what is on the current row. */} board and the keyboard then disagree about the current row. */}
<div className="min-w-[18rem] max-w-md flex-1 pb-[max(1rem,var(--safe-bottom))] sm:pb-0 [@media(pointer:coarse)]:max-w-lg">
<Controls key={generation} state={state} onChange={setState} seed={seed} /> <Controls key={generation} state={state} onChange={setState} seed={seed} />
</div> </div>
</div>
{moved && afterMove ? <div>{afterMove}</div> : null}
</div>
); );
} }
+35 -31
View File
@@ -1,9 +1,9 @@
import { useState } from 'react'; import { useState } from 'react';
import { Check, Copy, ExternalLink } from 'lucide-react'; import { Check, Copy } from 'lucide-react';
import type { Provenance, RunRef } from '@/lib/demo-kit/types'; import type { Provenance, RunRef } from '@/lib/demo-kit/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import * as st from '@/content/styles';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { External, Label } from './chrome';
import { formatDate } from './format'; import { formatDate } from './format';
export interface ProvenanceCardProps { export interface ProvenanceCardProps {
@@ -21,7 +21,8 @@ type CopyState = 'idle' | 'copied' | 'failed';
* The command is the load-bearing element. Everything else on this page is us * The command is the load-bearing element. Everything else on this page is us
* telling you what happened; this is the line you paste into your own terminal * telling you what happened; this is the line you paste into your own terminal
* to find out for yourself, which is why it is verbatim and copy-pasteable * to find out for yourself, which is why it is verbatim and copy-pasteable
* rather than prettified into something that would not actually run. * rather than prettified into something that would not actually run — and why
* it WRAPS on a phone: a copyable command hidden off-screen defeats its purpose.
*/ */
export function ProvenanceCard({ provenance, run, className }: ProvenanceCardProps) { export function ProvenanceCard({ provenance, run, className }: ProvenanceCardProps) {
const [copyState, setCopyState] = useState<CopyState>('idle'); const [copyState, setCopyState] = useState<CopyState>('idle');
@@ -38,34 +39,41 @@ export function ProvenanceCard({ provenance, run, className }: ProvenanceCardPro
} }
}; };
const rows: { label: string; value: string; mono?: boolean }[] = [ const rows: { label: string; value: string; sans?: boolean }[] = [
{ label: 'Environment package', value: provenance.envPackage, mono: true }, { label: 'Environment package', value: provenance.envPackage },
{ label: 'Taskset', value: provenance.tasksetId, mono: true }, { label: 'Taskset', value: provenance.tasksetId },
{ label: 'verifiers version', value: provenance.verifiersVersion, mono: true }, { label: 'verifiers version', value: provenance.verifiersVersion },
]; ];
if (run) { if (run) {
rows.push( rows.push(
{ label: 'Model', value: run.model, mono: true }, { label: 'Run', value: run.id },
{ label: 'Model', value: run.model },
{ label: 'Captured', value: formatDate(run.capturedAt) }, { label: 'Captured', value: formatDate(run.capturedAt) },
{ label: 'Seed', value: String(run.seed), mono: true }, { label: 'Seed', value: String(run.seed) },
); );
if (run.intervention) rows.push({ label: 'Intervention', value: run.intervention }); if (run.intervention) rows.push({ label: 'Intervention', value: run.intervention, sans: true });
} }
return ( return (
<section aria-label="Provenance" className={cn('card overflow-hidden', className)}> <section
<header className="border-b border-border px-3 py-2"> aria-label="Provenance"
<h3 className="text-sm font-semibold">Provenance</h3> className={cn('overflow-hidden rounded-xl border border-border bg-surface', className)}
>
<header className="border-b border-border px-4 py-2.5">
<Label as="h3">Provenance</Label>
</header> </header>
<dl className="divide-y divide-border"> <dl className="divide-y divide-border">
{rows.map((row) => ( {rows.map((row) => (
<div key={row.label} className="flex items-baseline gap-3 px-3 py-2"> <div
<dt className="flex-1 text-sm text-muted">{row.label}</dt> key={row.label}
className="flex flex-col gap-0.5 px-4 py-2 sm:flex-row sm:items-baseline sm:gap-3"
>
<dt className="text-ui text-muted sm:flex-1">{row.label}</dt>
<dd <dd
className={cn( className={cn(
'min-w-0 break-all text-right text-sm', 'min-w-0 break-all sm:text-right',
row.mono && 'nums font-mono text-xs', row.sans ? 'text-ui text-fg' : 'font-mono text-mono-data text-fg',
)} )}
> >
{row.value} {row.value}
@@ -74,12 +82,10 @@ export function ProvenanceCard({ provenance, run, className }: ProvenanceCardPro
))} ))}
</dl> </dl>
<div className="border-t border-border p-3"> <div className="border-t border-border p-4">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted"> <Label as="h4">Run it yourself</Label>
Run it yourself <Button variant="outline" size="touch" onClick={copy} className="lg:min-h-9">
</h4>
<Button variant="outline" size="sm" className="tap" onClick={copy}>
{copyState === 'copied' ? ( {copyState === 'copied' ? (
<Check className="text-positive" aria-hidden="true" /> <Check className="text-positive" aria-hidden="true" />
) : ( ) : (
@@ -88,10 +94,10 @@ export function ProvenanceCard({ provenance, run, className }: ProvenanceCardPro
{copyState === 'copied' ? 'Copied' : 'Copy'} {copyState === 'copied' ? 'Copied' : 'Copy'}
</Button> </Button>
</div> </div>
<pre className={cn(st.codeBlock, 'mt-2')}> <pre className="mt-2 whitespace-pre-wrap break-all rounded-md border border-border bg-surface-3 p-3 font-mono text-code text-fg [font-feature-settings:'liga'_0] sm:whitespace-pre sm:break-normal sm:overflow-x-auto">
<code>{provenance.command}</code> <code>{provenance.command}</code>
</pre> </pre>
<p role="status" className="mt-1 text-xs text-muted"> <p role="status" className="mt-1 text-small text-muted">
{copyState === 'copied' {copyState === 'copied'
? 'Command copied to your clipboard.' ? 'Command copied to your clipboard.'
: copyState === 'failed' : copyState === 'failed'
@@ -101,20 +107,18 @@ export function ProvenanceCard({ provenance, run, className }: ProvenanceCardPro
</div> </div>
{provenance.credits.length > 0 ? ( {provenance.credits.length > 0 ? (
<div className="border-t border-border px-3 py-2.5"> <div className="border-t border-border px-4 py-3">
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted"> <Label as="h4">Credits</Label>
Built on <ul className="mt-1.5 flex flex-col gap-1">
</h4>
<ul className="mt-1.5 flex flex-wrap gap-x-4 gap-y-1">
{provenance.credits.map((credit) => ( {provenance.credits.map((credit) => (
<li key={credit.href}> <li key={credit.href}>
<a <a
href={credit.href} href={credit.href}
className={cn(st.link, 'inline-flex items-center gap-1 text-sm')} className="inline-flex min-h-11 items-center font-mono text-mono-data text-accent-fg underline decoration-border-strong underline-offset-4 hover:decoration-accent-fg sm:min-h-0"
rel="noreferrer" rel="noreferrer"
> >
{credit.label} {credit.label}
<ExternalLink className="size-3" aria-hidden="true" /> <External />
</a> </a>
</li> </li>
))} ))}
+9 -8
View File
@@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { Brain, ChevronUp } from 'lucide-react'; import { ChevronUp } from 'lucide-react';
import { import {
Drawer, Drawer,
DrawerBody, DrawerBody,
@@ -10,6 +10,7 @@ import {
DrawerTrigger, DrawerTrigger,
} from '@/components/ui/drawer'; } from '@/components/ui/drawer';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ThinkingOffChip } from './chrome';
import { ReasoningPanel } from './ReasoningPanel'; import { ReasoningPanel } from './ReasoningPanel';
import type { ReasoningPanelProps } from './ReasoningPanel'; import type { ReasoningPanelProps } from './ReasoningPanel';
@@ -53,30 +54,30 @@ export function ReasoningDrawer({
> >
<DrawerTrigger <DrawerTrigger
className={cn( 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', 'tap flex w-full items-center gap-2 rounded-md border border-border-strong bg-surface px-3 py-2 text-ui font-medium text-fg transition-colors duration-1 ease-enter hover:bg-surface-2',
triggerClassName, triggerClassName,
)} )}
> >
<Brain className="size-4 text-muted" aria-hidden="true" />
<span>{hasReasoning ? 'Read the reasoning' : 'No reasoning on this step'}</span> <span>{hasReasoning ? 'Read the reasoning' : 'No reasoning on this step'}</span>
<ChevronUp className="ml-auto size-4 text-muted" aria-hidden="true" /> {hasReasoning ? null : <ThinkingOffChip />}
<ChevronUp className="ml-auto size-4 text-border-strong" aria-hidden="true" />
</DrawerTrigger> </DrawerTrigger>
{/* {/*
`h-full max-h-[97%]` overrides the wrapper's `h-auto max-h-[88svh]`. `h-full max-h-[97%]` overrides the wrapper's `h-auto max-h-[88svh]`.
Snap points size the sheet by translating a FIXED-height panel; on an Snap points size the sheet by translating a FIXED-height panel; on an
auto-height one the 0.4 snap and the 0.9 snap look identical. auto-height one the 0.4 snap and the 0.9 snap look identical.
*/} */}
<DrawerContent className="mx-auto h-full max-h-[97%] max-w-canvas"> <DrawerContent className="mx-auto h-full max-h-[97%] max-w-canvas shadow-overlay">
<DrawerHeader className="pb-2"> <DrawerHeader className="pb-2">
<DrawerTitle className="text-sm">{panel.title ?? 'Reasoning'}</DrawerTitle> <DrawerTitle className="text-ui">{panel.title ?? 'Reasoning'}</DrawerTitle>
<DrawerDescription className="text-xs"> <DrawerDescription className="text-small">
Recorded verbatim from step {panel.stepIndex + 1} of this run. Recorded verbatim from step {panel.stepIndex + 1} of this run.
</DrawerDescription> </DrawerDescription>
</DrawerHeader> </DrawerHeader>
<DrawerBody className="overflow-hidden"> <DrawerBody className="overflow-hidden">
{/* The panel keeps its own reserved height inside the sheet so the {/* The panel keeps its own reserved height inside the sheet so the
sheet does not resize as the text streams under the drag. */} sheet does not resize as the text streams under the drag. */}
<ReasoningPanel {...panel} className="h-full border-0" reservedLines={14} /> <ReasoningPanel {...panel} className="h-full" reservedLines={14} />
</DrawerBody> </DrawerBody>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
+31 -13
View File
@@ -3,9 +3,9 @@ import { useEffect, useRef, useState } from 'react';
// has to pin the viewport to the bottom as characters land, and that needs a // has to pin the viewport to the bottom as characters land, and that needs a
// ref on the viewport element, which the wrapper does not expose. // ref on the viewport element, which the wrapper does not expose.
import * as ScrollArea from '@radix-ui/react-scroll-area'; import * as ScrollArea from '@radix-ui/react-scroll-area';
import { Brain } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { usePrefersReducedMotion } from './format'; import { ThinkingOffChip } from './chrome';
import { formatMs, usePrefersReducedMotion } from './format';
import type { PlaybackSpeed } from '@/lib/demo-kit/player'; import type { PlaybackSpeed } from '@/lib/demo-kit/player';
/** /**
@@ -28,7 +28,7 @@ export interface ReasoningPanelProps {
/** /**
* Lines of height held open whether or not there is text. Reserving the box * 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 * 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. * the text arrives shoves the board down the page mid-run. Capped at 24rem.
*/ */
reservedLines?: number; reservedLines?: number;
title?: string; title?: string;
@@ -40,13 +40,20 @@ function charsPerSecond(length: number, durationMs: number | null): number {
return Math.min(Math.max((length / durationMs) * 1000, MIN_CPS), MAX_CPS); return Math.min(Math.max((length / durationMs) * 1000, MIN_CPS), MAX_CPS);
} }
/**
* The model's reasoning, streamed at the pace it was recorded.
*
* The text is SANS at the prose size. Reasoning is sentences, and a wall of
* 13px mono reads as a log to skim rather than a thought to follow — which is
* backwards on the one panel whose job is to show a model thinking.
*/
export function ReasoningPanel({ export function ReasoningPanel({
reasoning, reasoning,
durationMs, durationMs,
playing, playing,
speed, speed,
stepIndex, stepIndex,
reservedLines = 10, reservedLines = 9,
title = 'Reasoning', title = 'Reasoning',
className, className,
}: ReasoningPanelProps) { }: ReasoningPanelProps) {
@@ -87,17 +94,28 @@ export function ReasoningPanel({
}, [shouldStream, full, durationMs, speed, stepIndex]); }, [shouldStream, full, durationMs, speed, stepIndex]);
const streaming = visible < full.length; const streaming = visible < full.length;
// Prose is 16px at 1.6 — 1.6rem per line — capped at the spec's 24rem.
const height = `${Math.min(reservedLines * 1.6 + 1.25, 24)}rem`;
return ( return (
<section <section
aria-label={title} aria-label={title}
className={cn('card flex flex-col overflow-hidden', className)} className={cn('flex flex-col overflow-hidden rounded-lg border border-border bg-surface-2', className)}
> >
<header className="flex items-center gap-2 border-b border-border px-3 py-2"> <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="font-mono text-label uppercase tracking-label text-muted">
<h3 className="text-sm font-semibold">{title}</h3> {title}
{durationMs !== null ? (
<>
<span aria-hidden="true"> · </span>
<span className="sr-only">, recorded over </span>
{formatMs(durationMs)}
</>
) : null}
</h3>
{full.length === 0 ? <ThinkingOffChip className="ml-auto" /> : null}
{streaming ? ( {streaming ? (
<span className="nums ml-auto text-xs text-muted"> <span className="ml-auto font-mono text-caption text-muted">
{visible}/{full.length} {visible}/{full.length}
</span> </span>
) : null} ) : null}
@@ -108,7 +126,7 @@ export function ReasoningPanel({
className="min-h-0 flex-1" className="min-h-0 flex-1"
// Height, not min-height: the panel is the same size on every step, // Height, not min-height: the panel is the same size on every step,
// including the ones with no reasoning at all. // including the ones with no reasoning at all.
style={{ height: `${reservedLines * 1.45}rem` }} style={{ height }}
> >
<ScrollArea.Viewport <ScrollArea.Viewport
ref={viewportRef} ref={viewportRef}
@@ -118,18 +136,18 @@ export function ReasoningPanel({
aria-live="off" aria-live="off"
> >
{full.length === 0 ? ( {full.length === 0 ? (
<p className="text-sm leading-relaxed text-muted"> <p className="max-w-measure text-prose text-muted">
This step has no recorded reasoning. The run was captured with thinking disabled, so 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 there is nothing to show here which is different from the model having thought
nothing. nothing.
</p> </p>
) : ( ) : (
<p className="whitespace-pre-wrap font-mono text-[13px] leading-relaxed text-fg"> <p className="max-w-measure whitespace-pre-wrap text-prose text-fg-2">
{full.slice(0, visible)} {full.slice(0, visible)}
{streaming ? ( {streaming ? (
<span <span
aria-hidden="true" aria-hidden="true"
className="ml-px inline-block h-[1em] w-[0.5ch] translate-y-[0.15em] animate-pulse bg-brand align-baseline" className="ml-px inline-block h-[1em] w-[0.5ch] translate-y-[0.15em] animate-pulse bg-tab-underline align-baseline"
/> />
) : null} ) : null}
</p> </p>
@@ -139,7 +157,7 @@ export function ReasoningPanel({
orientation="vertical" orientation="vertical"
className="flex w-2 touch-none select-none p-0.5" className="flex w-2 touch-none select-none p-0.5"
> >
<ScrollArea.Thumb className="flex-1 rounded-full bg-border" /> <ScrollArea.Thumb className="flex-1 rounded-full bg-border-strong" />
</ScrollArea.Scrollbar> </ScrollArea.Scrollbar>
</ScrollArea.Root> </ScrollArea.Root>
</section> </section>
+88 -48
View File
@@ -1,18 +1,36 @@
import { Scale, Target, Weight } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { decompose } from '@/lib/demo-kit/reward'; import { decompose } from '@/lib/demo-kit/reward';
import type { RewardComponent, RewardSpec, RewardValues } from '@/lib/demo-kit/types'; import type { RewardComponent, RewardSpec, RewardValues } from '@/lib/demo-kit/types';
import { Badge } from '@/components/ui/badge'; import { Term } from '@/components/site/Term';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Chip, EditedChip } from './chrome';
import { DASH, formatNumber, formatOrDash } from './format'; import { DASH, formatNumber, formatOrDash } from './format';
import { EditedChip } from './StatStrip';
const ROLE_META: Record<RewardComponent['role'], { label: string; Icon: LucideIcon }> = { type Role = RewardComponent['role'];
objective: { label: 'Objective', Icon: Target },
counterweight: { label: 'Counterweight', Icon: Weight }, /**
gate: { label: 'Gate', Icon: Scale }, * The three roles a reward term can play. All three are glossary terms and
* render through `Term`, so the chip carries its gloss on hover, focus and
* tap. The counterweight is introduced in the sentence under the table as
* "the term that takes points away" before its label, because that phrase is
* the lesson and the label is only its name.
*/
const ROLE_META: Record<Role, { label: string; variant: 'outline' | 'warning' | 'muted' }> = {
objective: { label: 'Objective', variant: 'outline' },
counterweight: { label: 'Counterweight', variant: 'warning' },
gate: { label: 'Gate', variant: 'muted' },
}; };
function RoleChip({ role }: { role: Role }) {
const meta = ROLE_META[role];
return (
<Term term={role} className="no-underline">
<Chip variant={meta.variant} className="cursor-help">
{meta.label}
</Chip>
</Term>
);
}
export interface RewardBreakdownProps { export interface RewardBreakdownProps {
spec: RewardSpec; spec: RewardSpec;
values: RewardValues; values: RewardValues;
@@ -29,12 +47,17 @@ export interface RewardBreakdownProps {
} }
/** /**
* `score x weight = value`, per component, plus the total. * `score × weight = value`, per component, plus the total.
* *
* The counterweight row is called out because it is the row that makes the * 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 * reward an opinion rather than a scoreboard. Everyone understands "pay for
* solving it". The interesting engineering is the term that takes points away, * 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. * and an exec who leaves this page understanding only that has got the point.
*
* A term the environment did not score prints `—` with a title, never 0.000:
* a zero is a measurement and a dash is the absence of one. Below `sm` the
* SCORE column folds under the label as `score 0.125` so four columns never
* force the table to scroll.
*/ */
export function RewardBreakdown({ export function RewardBreakdown({
spec, spec,
@@ -49,18 +72,18 @@ export function RewardBreakdown({
const counterweights = rows.filter((row) => row.role === 'counterweight'); const counterweights = rows.filter((row) => row.role === 'counterweight');
return ( return (
<div className={cn('card overflow-hidden', className)}> <div className={cn('overflow-hidden rounded-xl border border-border bg-surface', className)}>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full border-collapse text-sm"> <table className="w-full border-collapse">
<caption className="sr-only"> <caption className="sr-only">
Reward components, their weights and their contribution to the total score Reward components, their weights and their contribution to the total score
</caption> </caption>
<thead> <thead>
<tr className="border-b border-border text-left text-xs uppercase tracking-wide text-muted"> <tr className="bg-surface-3 text-left font-mono text-label uppercase tracking-label text-muted">
<th scope="col" className="px-3 py-2 font-medium"> <th scope="col" className="px-3 py-2 font-medium">
Component Component
</th> </th>
<th scope="col" className="px-2 py-2 text-right font-medium"> <th scope="col" className="hidden px-2 py-2 text-right font-medium sm:table-cell">
Score Score
</th> </th>
<th scope="col" className="px-2 py-2 text-right font-medium"> <th scope="col" className="px-2 py-2 text-right font-medium">
@@ -73,56 +96,72 @@ export function RewardBreakdown({
</thead> </thead>
<tbody className="divide-y divide-border"> <tbody className="divide-y divide-border">
{rows.map((row) => { {rows.map((row) => {
const role = ROLE_META[row.role];
const isCounterweight = row.role === 'counterweight'; const isCounterweight = row.role === 'counterweight';
return ( return (
<tr key={row.key} className={cn(isCounterweight && 'bg-accent-subtle/40')}> <tr key={row.key} className={cn(isCounterweight && 'bg-accent-subtle')}>
<th scope="row" className="max-w-[22rem] px-3 py-2.5 text-left font-normal"> <th scope="row" className="max-w-[24rem] px-3 py-2.5 text-left font-normal align-top">
<span className="flex flex-wrap items-center gap-1.5"> <span className="flex flex-wrap items-center gap-1.5">
<span className="font-medium text-fg">{row.label}</span> <span className="text-ui font-medium text-fg">{row.label}</span>
<Badge variant={isCounterweight ? 'default' : 'muted'}> <RoleChip role={row.role} />
<role.Icon className="size-3" aria-hidden="true" />
{role.label}
</Badge>
</span> </span>
<span className="mt-0.5 block text-xs leading-snug text-muted"> <span className="mt-0.5 block text-small text-fg-2">{row.description}</span>
{row.description} <span className="mt-1 flex flex-wrap items-baseline gap-x-3 font-mono text-label text-muted">
</span> <span>{row.key}</span>
<span className="mt-0.5 block font-mono text-[11px] text-muted">{row.key}</span> <span className="sm:hidden">
</th> <span className="uppercase tracking-label">score</span>{' '}
<td className="nums px-2 py-2.5 text-right align-top font-mono"> <span className="text-mono-data text-fg">
{row.score === null ? ( {row.score === null ? (
<span className="text-muted" title="The environment did not score this run"> <span className="text-muted" title="not scored">
not scored {DASH}
</span>
) : (
formatNumber(row.score)
)}
</span>
</span>
</span>
</th>
<td className="hidden px-2 py-2.5 text-right align-top font-mono text-mono-data sm:table-cell">
{row.score === null ? (
<span className="text-muted" title="not scored">
{DASH}
</span> </span>
) : ( ) : (
formatNumber(row.score) formatNumber(row.score)
)} )}
</td> </td>
<td className="nums px-2 py-2.5 text-right align-top font-mono"> <td className="px-2 py-2.5 text-right align-top font-mono text-mono-data">
<span className={cn(edited && 'font-semibold text-accent-fg')}> <span className={cn(edited && 'font-semibold text-accent-fg')}>
{formatNumber(row.weight, 2)} {formatNumber(row.weight, 2)}
</span> </span>
</td> </td>
<td className="nums px-3 py-2.5 text-right align-top font-mono font-semibold"> <td className="px-3 py-2.5 text-right align-top font-mono text-stat text-fg">
{row.value === null ? DASH : formatNumber(row.value)} {row.value === null ? (
<span className="font-normal text-muted" title="not scored">
{DASH}
</span>
) : (
formatNumber(row.value)
)}
</td> </td>
</tr> </tr>
); );
})} })}
</tbody> </tbody>
<tfoot> <tfoot>
<tr className="border-t-2 border-border bg-surface-2"> <tr className="border-t-2 border-border-strong bg-surface-2">
<th scope="row" className="px-3 py-2.5 text-left"> <th scope="row" className="px-3 py-2.5 text-left" colSpan={2}>
<span className="flex items-center gap-2 font-semibold"> <span className="flex items-center gap-2 text-ui font-medium text-fg">
Total reward Total reward
{edited ? <EditedChip /> : null} {edited ? <EditedChip /> : null}
</span> </span>
</th> </th>
<td colSpan={2} /> <td className="hidden sm:table-cell" />
<td className="nums px-3 py-2.5 text-right font-mono text-base font-semibold"> <td className="px-3 py-2.5 text-right font-mono text-[15px] font-semibold text-accent-fg">
{total === null ? ( {total === null ? (
<span className="text-muted">not scored</span> <span className="font-normal text-muted" title="not scored">
{DASH}
</span>
) : ( ) : (
formatOrDash(total) formatOrDash(total)
)} )}
@@ -133,28 +172,29 @@ export function RewardBreakdown({
</div> </div>
{counterweights.length > 0 ? ( {counterweights.length > 0 ? (
<p className="border-t border-border bg-accent-subtle/40 px-3 py-2.5 text-xs leading-relaxed text-fg"> <p className="max-w-measure border-t border-border px-3 py-3 text-prose text-fg-2">
<span className="font-semibold"> <span className="font-medium text-fg">
{counterweights.map((row) => row.label).join(' and ')} {counterweights.map((row) => row.label).join(' and ')}
</span>{' '} </span>{' '}
{counterweights.length > 1 ? 'are counterweights' : 'is the counterweight'}: without a {counterweights.length > 1 ? 'are the terms that take points away' : 'is the term that takes points away'}
term pulling the other way, the cheapest way to maximise the objective is a behaviour : without one pulling the other way, the cheapest way to maximise the objective is a
you would never ship, and the model will find it. behaviour you would never ship, and the model will find it.
</p> </p>
) : null} ) : null}
{spec.metrics && spec.metrics.length > 0 ? ( {spec.metrics && spec.metrics.length > 0 ? (
<div className="border-t border-border px-3 py-2.5"> <div className="border-t border-border px-3 py-2.5">
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted"> <h4 className="font-mono text-label uppercase tracking-label text-muted">
Diagnostics reported, never summed Diagnostics reported, never summed
</h4> </h4>
<dl className="mt-1.5 grid grid-cols-1 gap-x-4 gap-y-1 sm:grid-cols-2"> <dl className="mt-1.5 grid grid-cols-1 gap-x-6 gap-y-1 sm:grid-cols-2">
{spec.metrics.map((metric) => ( {spec.metrics.map((metric) => (
<div key={metric.key} className="flex items-baseline justify-between gap-2"> <div key={metric.key} className="flex items-baseline justify-between gap-2">
<dt className="text-xs text-muted" title={metric.description}> <dt className="text-small text-muted" title={metric.description}>
{metric.label} {metric.label}{' '}
<span className="font-mono text-label">{metric.key}</span>
</dt> </dt>
<dd className="nums font-mono text-xs"> <dd className="font-mono text-mono-data text-fg">
{formatOrDash(metrics?.[metric.key] ?? null)} {formatOrDash(metrics?.[metric.key] ?? null)}
</dd> </dd>
</div> </div>
+112 -41
View File
@@ -12,8 +12,8 @@ import type { RewardSpec, RewardValues } from '@/lib/demo-kit/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Slider } from '@/components/ui/slider'; import { Slider } from '@/components/ui/slider';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { formatNumber, formatOrDash } from './format'; import { EditedChip, Label } from './chrome';
import { EditedChip } from './StatStrip'; import { DASH, formatNumber, formatOrDash } from './format';
/** One recorded arm — a run, or a group of runs already reduced to one score. */ /** One recorded arm — a run, or a group of runs already reduced to one score. */
export interface RewardArm { export interface RewardArm {
@@ -43,6 +43,19 @@ export interface RewardEditorProps {
const STEP = 0.05; const STEP = 0.05;
/**
* Rows shown before the disclosure. The shell groups runs into one arm per
* agent, so this is four today; a demo that ever yields more than eight arms
* gets eight and a "show all", because a ranking you have to scroll is not a
* ranking you can watch flip.
*/
const RANK_CAP = 8;
/** Row pitch in px: a 52px row (label + note) on an 8px gap. */
const ROW_H = 52;
const ROW_GAP = 8;
const PITCH = ROW_H + ROW_GAP;
/** /**
* Presets built from the spec's own labels, for a demo that does not supply its * Presets built from the spec's own labels, for a demo that does not supply its
* own. Each is phrased as a position someone would actually argue for in a * own. Each is phrased as a position someone would actually argue for in a
@@ -104,7 +117,8 @@ function sameWeights(spec: RewardSpec, overrides: WeightOverrides, preset: Rewar
* is NOT training. It shows the ranking a different reward would have produced * is NOT training. It shows the ranking a different reward would have produced
* over these exact attempts; it cannot show the different attempts a model * over these exact attempts; it cannot show the different attempts a model
* trained on that reward would have made. That distinction is the permanent * trained on that reward would have made. That distinction is the permanent
* caption at the bottom, and it is not collapsible. * caption at the bottom, and it is not collapsible. The EDITED chip is amber
* for the same reason: a re-scored ranking is a caution, not a brand moment.
* *
* Weights are normalised to sum to 1 before scoring — `reweight` does it — so * Weights are normalised to sum to 1 before scoring — `reweight` does it — so
* dragging one slider up trades weight away from the others instead of lifting * dragging one slider up trades weight away from the others instead of lifting
@@ -119,6 +133,7 @@ export function RewardEditor({
className, className,
}: RewardEditorProps) { }: RewardEditorProps) {
const [overrides, setOverrides] = useState<WeightOverrides>({}); const [overrides, setOverrides] = useState<WeightOverrides>({});
const [showAll, setShowAll] = useState(false);
const effectivePresets = useMemo(() => presets ?? derivePresets(spec), [presets, spec]); const effectivePresets = useMemo(() => presets ?? derivePresets(spec), [presets, spec]);
const edited = isEdited(overrides, spec.components); const edited = isEdited(overrides, spec.components);
@@ -156,9 +171,13 @@ export function RewardEditor({
const shippedOrder = [...rows] const shippedOrder = [...rows]
.sort((a, b) => byTotal({ total: a.shippedTotal }, { total: b.shippedTotal })) .sort((a, b) => byTotal({ total: a.shippedTotal }, { total: b.shippedTotal }))
.map((row) => row.arm.id); .map((row) => row.arm.id);
return [...rows].sort(byTotal).map((row, index) => ({ const order = [...rows].sort(byTotal).map((row) => row.arm.id);
// Returned in ARM order, not rank order, and positioned by rank in the
// DOM: React keeps each row's element, so the change of rank is a
// transform the browser can animate rather than a remount it cannot.
return rows.map((row) => ({
...row, ...row,
rank: index + 1, rank: order.indexOf(row.arm.id) + 1,
shippedRank: shippedOrder.indexOf(row.arm.id) + 1, shippedRank: shippedOrder.indexOf(row.arm.id) + 1,
})); }));
}, [arms, inForce, shippedNormalised, degenerate]); }, [arms, inForce, shippedNormalised, degenerate]);
@@ -171,13 +190,20 @@ export function RewardEditor({
return { lo, hi: hi === lo ? lo + 1 : hi }; return { lo, hi: hi === lo ? lo + 1 : hi };
}, [ranked]); }, [ranked]);
const leader = ranked[0]; const leader = ranked.find((row) => row.rank === 1);
const shownCount = showAll ? ranked.length : Math.min(ranked.length, RANK_CAP);
const shown = ranked.filter((row) => row.rank <= shownCount);
return ( return (
<div className={cn('grid grid-cols-1 gap-4 lg:grid-cols-2', className)}> <div className={cn('grid grid-cols-1 gap-4 lg:grid-cols-2 lg:items-start', className)}>
<section aria-label="Reward weights" className="card p-3"> <section
aria-label="Reward weights"
className="rounded-xl border border-border bg-surface p-4 sm:p-5"
>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<h3 className="text-sm font-semibold">Change what good means</h3> <Label as="h3" className="text-fg">
Change what good means
</Label>
{edited ? <EditedChip /> : null} {edited ? <EditedChip /> : null}
<Button <Button
variant="outline" variant="outline"
@@ -191,18 +217,21 @@ export function RewardEditor({
</Button> </Button>
</div> </div>
<div className="mt-2 flex flex-wrap gap-2"> <div className="mt-3 flex flex-wrap gap-2">
{effectivePresets.map((preset) => { {effectivePresets.map((preset) => {
const active = sameWeights(spec, overrides, preset); const active = sameWeights(spec, overrides, preset);
return ( return (
<Button <Button
key={preset.id} key={preset.id}
variant={active ? 'subtle' : 'outline'} variant="outline"
size="touch" size="touch"
aria-pressed={active} aria-pressed={active}
title={preset.description ?? preset.label} title={preset.description ?? preset.label}
onClick={() => apply({ ...preset.weights })} onClick={() => apply({ ...preset.weights })}
className="text-xs" className={cn(
'text-small',
active && 'border-accent-fg bg-accent-subtle text-accent-fg hover:bg-accent-subtle',
)}
> >
{preset.label} {preset.label}
</Button> </Button>
@@ -221,22 +250,20 @@ export function RewardEditor({
{/* A <label htmlFor> would point at Radix's root <span>, which {/* A <label htmlFor> would point at Radix's root <span>, which
is not a labelable element — the association would silently is not a labelable element — the association would silently
do nothing. The thumb takes its name from this text. */} do nothing. The thumb takes its name from this text. */}
<span id={`weight-label-${component.key}`} className="text-sm font-medium text-fg"> <span id={`weight-label-${component.key}`} className="text-ui font-medium text-fg">
{component.label} {component.label}
</span> </span>
<span <span className="font-mono text-mono-data">
className={cn( <span className={cn(changed ? 'font-semibold text-accent-fg' : 'text-fg')}>
'nums font-mono text-sm',
changed ? 'font-semibold text-accent-fg' : 'text-muted',
)}
>
{formatNumber(raw, 2)} {formatNumber(raw, 2)}
<span className="ml-1.5 text-xs font-normal text-muted"> </span>
{degenerate ? '—' : `${Math.round(share * 100)}%`} <span className="ml-1.5 text-caption text-muted" title="Share of the normalised reward">
{degenerate ? DASH : `${Math.round(share * 100)}%`}
</span> </span>
</span> </span>
</div> </div>
<p className="mb-1 text-xs leading-snug text-muted">{component.description}</p> <p className="text-small text-fg-2">{component.description}</p>
{/* The primitive's `py-3` around its 20px thumb is the 44px band. */}
<Slider <Slider
min={0} min={0}
max={max} max={max}
@@ -252,27 +279,36 @@ export function RewardEditor({
})} })}
</div> </div>
<p className="mt-3 text-xs leading-relaxed text-muted"> <p className="mt-2 max-w-measure text-small text-muted">
The percentage is the share of the reward each term carries once the weights are The percentage is the share of the reward each term carries once the weights are
normalised. Raising one lowers the others that is the trade a reward designer normalised. Raising one lowers the others that is the trade a reward designer
actually makes. actually makes.
</p> </p>
</section> </section>
<section aria-label="Ranking under this reward" className="card flex flex-col p-3"> <section
<h3 className="flex items-center gap-2 text-sm font-semibold"> aria-label="Ranking under this reward"
className="flex flex-col rounded-xl border border-border bg-surface p-4 sm:p-5"
>
<div className="flex flex-wrap items-center gap-2">
<Label as="h3" className="text-fg">
Ranking under this reward Ranking under this reward
</Label>
{edited ? <EditedChip /> : null} {edited ? <EditedChip /> : null}
</h3> </div>
{degenerate ? ( {degenerate ? (
<p className="mt-3 text-sm leading-relaxed text-muted"> <p className="mt-3 max-w-measure text-prose text-fg-2">
Every weight is zero, so there is no reward left to rank by. That is not a score of Every weight is zero, so there is no reward left to rank by. That is not a score of
nought it is a reward that expresses no preference at all. nought it is a reward that expresses no preference at all.
</p> </p>
) : ( ) : (
<ol className="mt-3 space-y-2"> <ol
{ranked.map((row) => { className="relative mt-3"
style={{ height: shown.length * PITCH - ROW_GAP }}
aria-label="Arms, best first"
>
{shown.map((row) => {
const moved = row.rank - row.shippedRank; const moved = row.rank - row.shippedRank;
const width = const width =
row.total === null row.total === null
@@ -281,37 +317,66 @@ export function RewardEditor({
return ( return (
<li <li
key={row.arm.id} key={row.arm.id}
// The transform is what animates over --dur-2. DOM order is
// arm order, so the rank is also spoken (`aria-label`) —
// a screen reader is not reading the transform.
aria-label={`Rank ${row.rank}: ${row.arm.label}, ${formatOrDash(row.total)}`}
style={{
transform: `translateY(${(row.rank - 1) * PITCH}px)`,
height: ROW_H,
}}
className={cn( className={cn(
'rounded-lg border p-2.5 transition-colors duration-3 ease-enter', 'absolute inset-x-0 top-0 flex flex-col justify-center rounded-md border px-3 py-1.5 motion-safe:transition-transform motion-safe:duration-2 motion-safe:ease-enter',
row.rank === 1 ? 'border-brand bg-accent-subtle/50' : 'border-border', row.rank === 1 ? 'border-accent-fg bg-accent-subtle' : 'border-border bg-surface',
)} )}
> >
<div className="flex items-baseline gap-2"> <div className="flex items-baseline gap-2">
<span className="nums text-sm font-semibold text-muted">{row.rank}</span> <span className="font-mono text-label text-muted">
<span className="min-w-0 flex-1 truncate text-sm font-medium"> {String(row.rank).padStart(2, '0')}
</span>
<span className="min-w-0 flex-1 truncate text-ui font-medium text-fg">
{row.arm.label} {row.arm.label}
</span> </span>
<RankMove moved={moved} /> <RankMove moved={moved} />
<span className="nums font-mono text-sm font-semibold"> <span className="font-mono text-stat text-fg">{formatOrDash(row.total)}</span>
{formatOrDash(row.total)}
</span>
</div> </div>
<div className="mt-1 flex items-center gap-2">
<div <div
aria-hidden="true" aria-hidden="true"
className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-surface-2" className="h-1.5 flex-1 overflow-hidden rounded-sm bg-surface-3"
> >
<div <div
className="h-full rounded-full bg-brand transition-[width] duration-3 ease-enter" className="h-full bg-tab-underline motion-safe:transition-[width] motion-safe:duration-2 motion-safe:ease-enter"
style={{ width: `${width}%` }} style={{ width: `${width}%` }}
/> />
</div> </div>
{row.arm.note ? <p className="mt-1 text-xs text-muted">{row.arm.note}</p> : null} {row.arm.note ? (
<span
className="max-w-[45%] truncate font-mono text-[10px] text-muted"
title={row.arm.note}
>
{row.arm.note}
</span>
) : null}
</div>
</li> </li>
); );
})} })}
</ol> </ol>
)} )}
{ranked.length > RANK_CAP ? (
<Button
variant="ghost"
size="touch"
className="mt-2 self-start text-muted hover:text-fg"
aria-expanded={showAll}
onClick={() => setShowAll((was) => !was)}
>
{showAll ? `Show the top ${RANK_CAP}` : `Show all ${ranked.length} arms`}
</Button>
) : null}
{/* Safe to announce here without fighting the shell's step-change {/* Safe to announce here without fighting the shell's step-change
region: a slider drag and a step advance never fire from one action. */} region: a slider drag and a step advance never fire from one action. */}
<p role="status" className="sr-only"> <p role="status" className="sr-only">
@@ -320,7 +385,7 @@ export function RewardEditor({
: ''} : ''}
</p> </p>
<p className="mt-auto pt-3 text-xs leading-relaxed text-muted"> <p className="mt-auto max-w-measure pt-3 text-small text-muted">
We re-scored the same recorded attempts under your reward. Training on it would change We re-scored the same recorded attempts under your reward. Training on it would change
the behaviour, not just the ranking. the behaviour, not just the ranking.
</p> </p>
@@ -329,6 +394,7 @@ export function RewardEditor({
); );
} }
/** ↑ / ↓ / — against the shipped ranking: a glyph AND a colour, never colour alone. */
function RankMove({ moved }: { moved: number }) { function RankMove({ moved }: { moved: number }) {
if (moved === 0) { if (moved === 0) {
return ( return (
@@ -342,7 +408,12 @@ function RankMove({ moved }: { moved: number }) {
const places = Math.abs(moved); const places = Math.abs(moved);
return ( return (
<span <span
className={cn('nums inline-flex items-center text-xs', up ? 'text-positive' : 'text-danger')} className={cn(
'inline-flex items-center font-mono text-caption',
// ↓ in danger only ever lands on the plain surface row: rank 1 (the
// accent-subtle row) cannot have moved down.
up ? 'text-positive' : 'text-danger',
)}
title={`${places} place${places === 1 ? '' : 's'} ${up ? 'up' : 'down'} from the shipped reward`} title={`${places} place${places === 1 ? '' : 's'} ${up ? 'up' : 'down'} from the shipped reward`}
> >
{up ? <ArrowUp className="size-3.5" aria-hidden="true" /> : <ArrowDown className="size-3.5" aria-hidden="true" />} {up ? <ArrowUp className="size-3.5" aria-hidden="true" /> : <ArrowDown className="size-3.5" aria-hidden="true" />}
+13 -3
View File
@@ -26,6 +26,10 @@ export interface SegmentedControlProps<T extends string> {
* control rather than through every option, and the arrow keys move within it. * control rather than through every option, and the arrow keys move within it.
* Getting that wrong is the most common a11y bug in a segmented control, which * Getting that wrong is the most common a11y bug in a segmented control, which
* is why there is one implementation here instead of three inline copies. * is why there is one implementation here instead of three inline copies.
*
* Shape (§9): a 6px rail on surface-2 with a 1px border; options 6px, 44px on
* touch and 36px from lg, the active one surface-3 + fg. No shadow — the
* ground step is the state.
*/ */
export function SegmentedControl<T extends string>({ export function SegmentedControl<T extends string>({
label, label,
@@ -78,7 +82,10 @@ export function SegmentedControl<T extends string>({
role="radiogroup" role="radiogroup"
aria-label={label} aria-label={label}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
className={cn('flex flex-wrap items-center gap-0.5 rounded-lg bg-surface-2 p-0.5', className)} className={cn(
'inline-flex flex-wrap items-center gap-0.5 rounded-md border border-border bg-surface-2 p-0.5',
className,
)}
> >
{options.map((option, position) => { {options.map((option, position) => {
const selected = option.value === value; const selected = option.value === value;
@@ -95,8 +102,11 @@ export function SegmentedControl<T extends string>({
}} }}
onClick={() => onChange(option.value)} onClick={() => onChange(option.value)}
className={cn( className={cn(
'min-h-9 rounded-md px-2.5 text-xs font-medium transition-colors duration-2 ease-enter', // `min-w-11` as well as `min-h-11`: a two-glyph option ("#1",
selected ? 'bg-surface text-fg shadow-sm' : 'text-muted hover:text-fg', // "1×") is 34px wide on padding alone, which is a thumb-sized
// row with a fingertip-sized hit area. Measured at 390px.
'min-h-11 min-w-11 rounded-md px-3 text-ui font-medium transition-colors duration-1 ease-enter lg:min-h-9 lg:min-w-9',
selected ? 'bg-surface-3 text-fg' : 'text-muted hover:text-fg',
optionClassName, optionClassName,
)} )}
> >
+13 -30
View File
@@ -1,15 +1,14 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
export type StatTone = 'default' | 'positive' | 'warning' | 'danger' | 'info' | 'brand'; export type StatTone = 'default' | 'positive' | 'warning' | 'danger' | 'info' | 'brand';
export interface Stat { export interface Stat {
/** Short. Two or three words; it sits beside the number. */ /** Short. One or two words; it sits beside the number as a mono key. */
label: string; label: string;
value: ReactNode; value: ReactNode;
tone?: StatTone; tone?: StatTone;
/** One clarifying line. A tooltip here, because the strip is one line high. */ /** The unit, as a sentence. A tooltip, because the strip is one line high. */
title?: string; title?: string;
} }
@@ -17,6 +16,7 @@ const TONE: Record<StatTone, string> = {
default: 'text-fg', default: 'text-fg',
positive: 'text-positive', positive: 'text-positive',
warning: 'text-warning', warning: 'text-warning',
// `danger` on surface-2 is 4.70 in dark — the last ground it is allowed on.
danger: 'text-danger', danger: 'text-danger',
info: 'text-info', info: 'text-info',
brand: 'text-accent-fg', brand: 'text-accent-fg',
@@ -25,23 +25,17 @@ const TONE: Record<StatTone, string> = {
/** /**
* The run's headline numbers, one line high. * The run's headline numbers, one line high.
* *
* This used to be a row of cards, which is the right thing in the middle of a * One surface-2 chip, keys mono 11 and values mono 13/600. It scrolls itself
* page and the wrong thing in a header: four stat cards push the interactive * rather than wrapping: every row this header spends is a row of the board
* board below the fold on a 390px phone, and the board arriving above the fold * pushed below the fold on a 390px phone, and the strip is one of the five
* is what the whole page is now organised around. So the cards are gone and * horizontal scrollers §11 permits.
* this is the only stat strip — a second, compact variant living beside the
* card version is how the two drift into disagreeing about what a stat looks
* like.
*
* It scrolls itself rather than wrapping: every row this header spends is a row
* of the board pushed down.
*/ */
export function StatStrip({ stats, className }: { stats: Stat[]; className?: string }) { export function StatStrip({ stats, className }: { stats: Stat[]; className?: string }) {
if (stats.length === 0) return null; if (stats.length === 0) return null;
return ( return (
<dl <dl
className={cn( className={cn(
'flex shrink-0 items-baseline gap-x-4 rounded-lg border border-border bg-surface-2 px-3 py-2', 'flex max-w-full shrink-0 items-baseline gap-x-4 overflow-x-auto whitespace-nowrap rounded-md border border-border bg-surface-2 px-3 py-2 [scrollbar-width:thin]',
className, className,
)} )}
> >
@@ -51,25 +45,14 @@ export function StatStrip({ stats, className }: { stats: Stat[]; className?: str
className="flex shrink-0 items-baseline gap-1.5" className="flex shrink-0 items-baseline gap-1.5"
{...(stat.title ? { title: stat.title } : {})} {...(stat.title ? { title: stat.title } : {})}
> >
<dt className="text-xs font-medium uppercase tracking-wide text-muted">{stat.label}</dt> <dt className="font-mono text-label uppercase tracking-label text-muted">{stat.label}</dt>
<dd className={cn('nums text-sm font-semibold', TONE[stat.tone ?? 'default'])}> <dd className={cn('font-mono text-stat', TONE[stat.tone ?? 'default'])}>{stat.value}</dd>
{stat.value}
</dd>
</div> </div>
))} ))}
</dl> </dl>
); );
} }
/** // Kept as a re-export so nothing that imported the chip from here breaks; the
* Marks a number the visitor caused rather than one we recorded. It appears on // chip itself lives with the other chips in chrome.tsx.
* every derived value in the reward editor; without it, an edited ranking export { EditedChip } from './chrome';
* screenshots identically to a measured one.
*/
export function EditedChip({ className }: { className?: string }) {
return (
<Badge className={cn('px-1.5 py-0 text-[10px] uppercase tracking-wide', className)}>
edited
</Badge>
);
}
+16 -7
View File
@@ -97,7 +97,9 @@ export function StepTimeline<T>({
aria-label="Steps in the recorded run" aria-label="Steps in the recorded run"
aria-keyshortcuts="ArrowLeft ArrowRight Home End Space" aria-keyshortcuts="ArrowLeft ArrowRight Home End Space"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={cn('flex snap-x gap-2 overflow-x-auto pb-2', className)} // One of the five horizontal scrollers §11 permits. The visible rule
// underneath says "this strip ends here" where a scrollbar may not show.
className={cn('flex snap-x gap-2 overflow-x-auto border-b border-border pb-3', className)}
> >
{steps.map((step, index) => { {steps.map((step, index) => {
const selected = index === current; const selected = index === current;
@@ -118,17 +120,22 @@ export function StepTimeline<T>({
}} }}
onClick={() => onSelect(index)} onClick={() => onSelect(index)}
className={cn( className={cn(
'tap w-[9.5rem] shrink-0 snap-center rounded-lg border p-2 text-left transition-colors duration-2 ease-enter', 'tap w-[8rem] shrink-0 snap-center rounded-lg border p-2 text-left transition-colors duration-2 ease-enter sm:w-[9rem]',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg',
// `accent-edge` is the green that reads as a border in both
// themes; the raw accent is 1.47:1 against white.
selected selected
? 'border-brand bg-accent-subtle/60' ? 'border-accent-edge bg-accent-subtle'
: 'border-border bg-surface hover:bg-surface-2', : 'border-border bg-surface hover:bg-surface-2',
)} )}
> >
<span className="flex items-baseline justify-between gap-1"> <span className="flex items-baseline justify-between gap-1">
<span className="nums text-xs font-semibold">Step {index + 1}</span> <span className="font-mono text-caption font-semibold uppercase tracking-label text-fg">
Step {index + 1}
</span>
{call?.finishReason ? ( {call?.finishReason ? (
<span <span
className="max-w-[4.5rem] truncate text-[10px] uppercase tracking-wide text-muted" className="max-w-[4.5rem] truncate font-mono text-[10px] uppercase tracking-label text-muted"
title={call.finishReason} title={call.finishReason}
> >
{humaniseToken(call.finishReason)} {humaniseToken(call.finishReason)}
@@ -143,12 +150,14 @@ export function StepTimeline<T>({
) : null} ) : null}
{step.caption ? ( {step.caption ? (
<span className="mt-1.5 block truncate text-xs text-muted" title={step.caption}> // fg-2, not muted: on the selected chip's accent-subtle ground
// muted is 4.93 in dark, and a caption is a sentence.
<span className="mt-1.5 block truncate text-caption text-fg-2" title={step.caption}>
{step.caption} {step.caption}
</span> </span>
) : null} ) : null}
<span className="nums mt-1 flex flex-wrap gap-x-2 text-[10px] text-muted"> <span className="mt-1 flex flex-wrap gap-x-2 font-mono text-[10px] text-muted">
<span title="Completion tokens"> <span title="Completion tokens">
{call?.completionTokens === null || call?.completionTokens === undefined {call?.completionTokens === null || call?.completionTokens === undefined
? DASH ? DASH
+32 -21
View File
@@ -1,15 +1,16 @@
import { ChevronLeft, ChevronRight, Circle, Pause, Play, RotateCcw } from 'lucide-react'; import { ChevronLeft, ChevronRight, Pause, Play, RotateCcw } from 'lucide-react';
import { PLAYBACK_SPEEDS } from '@/lib/demo-kit/player'; import { PLAYBACK_SPEEDS } from '@/lib/demo-kit/player';
import type { PlaybackSpeed } from '@/lib/demo-kit/player'; import type { PlaybackSpeed } from '@/lib/demo-kit/player';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Chip, InterventionChip } from './chrome';
import { formatDate } from './format'; import { formatDate } from './format';
import { SegmentedControl } from './SegmentedControl'; import { SegmentedControl } from './SegmentedControl';
/** `PlaybackSpeed` is `1 | 2 | 4 | 'instant'`; the control speaks strings. */ /** `PlaybackSpeed` is `1 | 2 | 4 | 'instant'`; the control speaks strings. */
const SPEED_OPTIONS = PLAYBACK_SPEEDS.map((speed) => ({ const SPEED_OPTIONS = PLAYBACK_SPEEDS.map((speed) => ({
value: String(speed), value: String(speed),
label: speed === 'instant' ? 'Instant' : `${speed}x`, label: speed === 'instant' ? 'INSTANT' : `${speed}×`,
title: speed === 'instant' ? 'Jump straight to the end of the run' : `${speed} times real pace`, title: speed === 'instant' ? 'Jump straight to the end of the run' : `${speed} times real pace`,
})); }));
@@ -50,6 +51,9 @@ export interface TracePlayerProps {
* believes they are watching a model think in real time has been misled by the * believes they are watching a model think in real time has been misled by the
* UI rather than the copy. Hence the permanent badge — not a disclosure tucked * UI rather than the copy. Hence the permanent badge — not a disclosure tucked
* into a footnote, but a fixture of the transport bar for the whole session. * into a footnote, but a fixture of the transport bar for the whole session.
*
* ▶ is the only green fill on the Watch tab. Everything else in the bar is a
* ghost, so the one thing that starts the run is the one thing that is lit.
*/ */
export function TracePlayer({ export function TracePlayer({
playing, playing,
@@ -70,7 +74,7 @@ export function TracePlayer({
const canPlay = stepCount > 1; const canPlay = stepCount > 1;
return ( return (
<div className={cn('card overflow-hidden', className)}> <div className={cn('overflow-hidden rounded-xl border border-border bg-surface', className)}>
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 p-2 sm:gap-x-4"> <div className="flex flex-wrap items-center gap-x-3 gap-y-2 p-2 sm:gap-x-4">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Button <Button
@@ -83,7 +87,7 @@ export function TracePlayer({
<ChevronLeft aria-hidden="true" /> <ChevronLeft aria-hidden="true" />
</Button> </Button>
<Button <Button
size="touch" size="icon-touch"
onClick={() => onPlayingChange(!playing)} onClick={() => onPlayingChange(!playing)}
disabled={!canPlay} disabled={!canPlay}
aria-label={playing ? 'Pause the recorded run' : 'Play the recorded run'} aria-label={playing ? 'Pause the recorded run' : 'Play the recorded run'}
@@ -110,8 +114,11 @@ export function TracePlayer({
</Button> </Button>
</div> </div>
<p className="nums text-sm text-muted"> <p className="font-mono text-mono-data text-muted">
Step <span className="font-semibold text-fg">{Math.min(step + 1, stepCount)}</span> of{' '} <span className="text-label uppercase tracking-label">Step</span>{' '}
<span className="font-semibold text-fg">{Math.min(step + 1, stepCount)}</span>
<span aria-hidden="true"> / </span>
<span className="sr-only">of </span>
{stepCount} {stepCount}
</p> </p>
@@ -120,6 +127,7 @@ export function TracePlayer({
options={SPEED_OPTIONS} options={SPEED_OPTIONS}
value={String(speed)} value={String(speed)}
onChange={(next) => onSpeedChange(toSpeed(next))} onChange={(next) => onSpeedChange(toSpeed(next))}
optionClassName="px-2.5 font-mono text-caption tracking-label"
/> />
<RecordedBadge <RecordedBadge
@@ -127,6 +135,7 @@ export function TracePlayer({
capturedAt={capturedAt} capturedAt={capturedAt}
{...(intervention ? { intervention } : {})} {...(intervention ? { intervention } : {})}
{...(timingIsReal ? {} : { timingNote: 'pacing approximate' })} {...(timingIsReal ? {} : { timingNote: 'pacing approximate' })}
className="ml-auto"
/> />
</div> </div>
@@ -135,10 +144,12 @@ export function TracePlayer({
exists so the pause between steps reads as time passing rather than as exists so the pause between steps reads as time passing rather than as
the page having stopped. `transition-[width]` covers the ~15 Hz at which the page having stopped. `transition-[width]` covers the ~15 Hz at which
the player pushes progress — without it the bar visibly ratchets. the player pushes progress — without it the bar visibly ratchets.
`tab-underline` is the token that is accent in dark and accent-fg in
light; a state indicator on a surface-3 track needs exactly that split.
*/} */}
<div className="h-0.5 w-full bg-surface-2" aria-hidden="true"> <div className="h-0.5 w-full bg-surface-3" aria-hidden="true">
<div <div
className="h-full bg-brand transition-[width] duration-1 ease-enter" className="h-full bg-tab-underline transition-[width] duration-1 ease-enter"
style={{ style={{
width: `${ width: `${
stepCount <= 1 ? 0 : ((step + Math.min(Math.max(progress, 0), 1)) / (stepCount - 1)) * 100 stepCount <= 1 ? 0 : ((step + Math.min(Math.max(progress, 0), 1)) / (stepCount - 1)) * 100
@@ -159,6 +170,12 @@ export interface RecordedBadgeProps {
className?: string; className?: string;
} }
/**
* `● RECORDED RUN · {model} · {date}`. The model string is printed verbatim
* from the manifest: the design adds no "model" word the data does not carry,
* because on some environments the recorded agent is a scripted reference
* policy and calling it a model would be the lie this badge exists to prevent.
*/
export function RecordedBadge({ export function RecordedBadge({
model, model,
capturedAt, capturedAt,
@@ -169,24 +186,18 @@ export function RecordedBadge({
return ( return (
<p <p
className={cn( className={cn(
'ml-auto flex flex-wrap items-center gap-x-1.5 gap-y-1 rounded-lg bg-surface-2 px-2.5 py-1.5 text-xs text-muted', 'flex flex-wrap items-center gap-x-1.5 gap-y-1 font-mono text-caption text-muted',
className, className,
)} )}
> >
<Circle className="size-2 shrink-0 fill-muted text-muted" aria-hidden="true" /> <span aria-hidden="true" className="size-2 shrink-0 rounded-full bg-border-strong" />
<span className="font-medium text-fg">Recorded run</span> <span className="font-medium uppercase tracking-label text-fg">Recorded run</span>
<span aria-hidden="true">·</span> <span aria-hidden="true">·</span>
<span className="nums font-mono">{model}</span> <span className="break-all">{model}</span>
<span aria-hidden="true">·</span> <span aria-hidden="true">·</span>
<span className="nums">{formatDate(capturedAt)}</span> <span>{formatDate(capturedAt)}</span>
{intervention ? ( {intervention ? <InterventionChip>{intervention}</InterventionChip> : null}
<span className="rounded-md bg-accent-subtle px-1.5 py-0.5 font-medium text-accent-fg"> {timingNote ? <Chip variant="muted">{timingNote}</Chip> : null}
{intervention}
</span>
) : null}
{timingNote ? (
<span className="rounded-md border border-border px-1.5 py-0.5">{timingNote}</span>
) : null}
</p> </p>
); );
} }
+49 -50
View File
@@ -1,9 +1,10 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { CheckCircle2, ChevronDown, HelpCircle, XCircle } from 'lucide-react'; import { ChevronDown } from 'lucide-react';
import { VERIFY_TOLERANCE, verifyEpisode } from '@/lib/demo-kit/verify'; import { VERIFY_TOLERANCE, verifyEpisode } from '@/lib/demo-kit/verify';
import type { AnyDemoModule } from '@/lib/demo-kit/registry'; import type { AnyDemoModule } from '@/lib/demo-kit/registry';
import type { DemoEpisode } from '@/lib/demo-kit/types'; import type { DemoEpisode } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Chip } from './chrome';
import { DASH, formatDelta, formatNumber, formatOrDash } from './format'; import { DASH, formatDelta, formatNumber, formatOrDash } from './format';
export interface VerifyBadgeProps { export interface VerifyBadgeProps {
@@ -27,7 +28,7 @@ export interface VerifyBadgeProps {
* it could not check anything is worse than no badge: it teaches the sceptic * it could not check anything is worse than no badge: it teaches the sceptic
* that the whole thing is decoration. The three states come straight from * that the whole thing is decoration. The three states come straight from
* `verifyEpisode`, and `unverifiable` is never dressed up as either of the * `verifyEpisode`, and `unverifiable` is never dressed up as either of the
* other two. * other two — it is a `?` on a plain ground, never a green tick.
*/ */
export function VerifyBadge({ demo, episode, className, defaultOpen = false }: VerifyBadgeProps) { export function VerifyBadge({ demo, episode, className, defaultOpen = false }: VerifyBadgeProps) {
const [open, setOpen] = useState(defaultOpen); const [open, setOpen] = useState(defaultOpen);
@@ -37,22 +38,23 @@ export function VerifyBadge({ demo, episode, className, defaultOpen = false }: V
return ( return (
<section <section
aria-label="Verification" aria-label="Verification"
className={cn('card bg-surface-2 p-3', className)} className={cn('rounded-xl border border-border bg-surface-2 p-3 sm:p-4', className)}
> >
<p className="flex items-start gap-2 text-sm"> <div className="flex flex-wrap items-center gap-2">
<HelpCircle className="mt-0.5 size-4 shrink-0 text-muted" aria-hidden="true" /> <span
<span> aria-hidden="true"
<span className="font-semibold">Unverifiable in your browser.</span>{' '} className="grid size-5 shrink-0 place-items-center rounded-sm border border-border-strong font-mono text-caption text-muted"
<span className="text-muted"> >
?
</span>
<Chip variant="muted">Unverifiable in your browser</Chip>
</div>
<p className="mt-2 max-w-measure text-prose text-fg-2">
{result.reason ?? 'This run cannot be re-computed here.'} {result.reason ?? 'This run cannot be re-computed here.'}
</span>
</span>
</p> </p>
{result.recorded !== null ? ( <p className="mt-2 font-mono text-caption text-muted">
<p className="nums mt-1.5 pl-6 font-mono text-xs text-muted"> recomputed {DASH} · recorded {formatOrDash(result.recorded)} · Δ {DASH}
Recorded {formatOrDash(result.recorded)} · recomputed {DASH} · Δ {DASH}
</p> </p>
) : null}
</section> </section>
); );
} }
@@ -62,40 +64,37 @@ export function VerifyBadge({ demo, episode, className, defaultOpen = false }: V
return ( return (
<section <section
aria-label="Verification" aria-label="Verification"
className={cn('card overflow-hidden', matched ? 'border-positive/40' : 'border-danger', className)} className={cn(
> 'overflow-hidden rounded-xl border',
<div className={cn('p-3', matched ? 'bg-positive/10' : 'bg-danger/10')}> // Match: accent-subtle ground, accent-fg edge. Mismatch: danger-subtle
<p className="flex items-start gap-2 text-sm leading-relaxed"> // ground, danger edge — danger text on danger-subtle is 4.72 dark /
{matched ? ( // 5.66 light, the last ground the red is allowed on.
<CheckCircle2 className="mt-0.5 size-4 shrink-0 text-positive" aria-hidden="true" /> matched ? 'border-accent-fg bg-accent-subtle' : 'border-danger bg-danger-subtle',
) : ( className,
<XCircle className="mt-0.5 size-4 shrink-0 text-danger" aria-hidden="true" />
)} )}
<span> >
<div className="p-3 sm:p-4">
<div className="flex flex-wrap items-center gap-2">
{matched ? ( {matched ? (
<> <Chip variant="positive" className="border-accent-fg">
<span className="font-semibold text-positive">Verified in your browser</span>{' '} Verified in your browser
<span className="text-fg"> </Chip>
re-ran every move through the TypeScript engine and rescored.
</span>
</>
) : ( ) : (
<> <Chip variant="danger" className="border-danger">
<span className="font-semibold text-danger"> {' '}
{result.culprit {result.culprit
? `Mismatch on "${result.culprit.label}"` ? `Mismatch on "${result.culprit.label}"`
: 'Mismatch against the recorded score'} : 'Mismatch against the recorded score'}
</span>{' '} </Chip>
<span className="text-fg">
the browser re-run disagrees with the published number. Trust the source in
the repository, not this page.
</span>
</>
)} )}
</span> </div>
<p className="mt-2 max-w-measure text-prose text-fg-2">
{matched
? 'Every move in this run was re-played through the TypeScript engine on this page and re-scored. The recorded number and the recomputed one agree.'
: 'The browser re-run disagrees with the published number. Trust the source in the repository, not this page.'}
</p> </p>
<p className="nums mt-1.5 pl-6 font-mono text-xs text-muted"> <p className={cn('mt-2 font-mono text-caption', matched ? 'text-fg-2' : 'text-danger')}>
Recomputed {formatOrDash(result.recomputed)} · recorded {formatOrDash(result.recorded)} · recomputed {formatOrDash(result.recomputed)} · recorded {formatOrDash(result.recorded)} ·
Δ {result.delta === null ? DASH : formatDelta(result.delta)} Δ {result.delta === null ? DASH : formatDelta(result.delta)}
</p> </p>
</div> </div>
@@ -104,21 +103,21 @@ export function VerifyBadge({ demo, episode, className, defaultOpen = false }: V
type="button" type="button"
onClick={() => setOpen((was) => !was)} onClick={() => setOpen((was) => !was)}
aria-expanded={open} 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" className="tap flex w-full items-center gap-1.5 border-t border-border bg-surface px-3 text-left text-small font-medium text-muted transition-colors duration-1 ease-enter hover:bg-surface-2 hover:text-fg"
> >
<ChevronDown <ChevronDown
className={cn('size-4 transition-transform duration-2 ease-enter', open && 'rotate-180')} className={cn('size-4 text-border-strong transition-transform duration-2 ease-enter', open && 'rotate-180')}
aria-hidden="true" aria-hidden="true"
/> />
{open ? 'Hide the component-by-component receipt' : 'Show the component-by-component receipt'} {open ? 'Hide the component-by-component receipt' : 'Show the component-by-component receipt'}
</button> </button>
{open ? ( {open ? (
<div className="border-t border-border"> <div className="border-t border-border bg-surface">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="nums w-full border-collapse font-mono text-xs"> <table className="w-full border-collapse font-mono text-caption">
<thead> <thead>
<tr className="text-left text-muted"> <tr className="bg-surface-3 text-left text-label uppercase tracking-label text-muted">
<th scope="col" className="px-3 py-1.5 font-medium"> <th scope="col" className="px-3 py-1.5 font-medium">
Component Component
</th> </th>
@@ -137,14 +136,14 @@ export function VerifyBadge({ demo, episode, className, defaultOpen = false }: V
{result.components.map((row) => { {result.components.map((row) => {
const bad = row.delta !== null && Math.abs(row.delta) > VERIFY_TOLERANCE; const bad = row.delta !== null && Math.abs(row.delta) > VERIFY_TOLERANCE;
return ( return (
<tr key={row.key} className={cn(bad && 'bg-danger/10')}> <tr key={row.key} className={cn(bad && 'bg-danger-subtle')}>
<th scope="row" className="whitespace-nowrap px-3 py-1.5 text-left font-normal"> <th scope="row" className="whitespace-nowrap px-3 py-1.5 text-left font-normal text-fg">
{row.label} {row.label}
</th> </th>
<td className="px-2 py-1.5 text-right"> <td className="px-2 py-1.5 text-right text-fg">
{row.recorded === null ? DASH : formatNumber(row.recorded, 6)} {row.recorded === null ? DASH : formatNumber(row.recorded, 6)}
</td> </td>
<td className="px-2 py-1.5 text-right"> <td className="px-2 py-1.5 text-right text-fg">
{row.recomputed === null ? DASH : formatNumber(row.recomputed, 6)} {row.recomputed === null ? DASH : formatNumber(row.recomputed, 6)}
</td> </td>
<td className={cn('px-3 py-1.5 text-right', bad ? 'text-danger' : 'text-muted')}> <td className={cn('px-3 py-1.5 text-right', bad ? 'text-danger' : 'text-muted')}>
@@ -158,7 +157,7 @@ export function VerifyBadge({ demo, episode, className, defaultOpen = false }: V
</tbody> </tbody>
</table> </table>
</div> </div>
<p className="px-3 py-2 text-[11px] leading-relaxed text-muted"> <p className="max-w-measure-wide px-3 py-2 text-small text-muted">
Tolerance {VERIFY_TOLERANCE.toExponential()}. The recorded numbers came out of Python Tolerance {VERIFY_TOLERANCE.toExponential()}. The recorded numbers came out of Python
and these came out of JavaScript; both are IEEE-754 doubles summing the same terms in and these came out of JavaScript; both are IEEE-754 doubles summing the same terms in
a different order, so the comparison is made within a tolerance rather than demanding a different order, so the comparison is made within a tolerance rather than demanding
+114
View File
@@ -0,0 +1,114 @@
/**
* The shell's small typographic parts, in one place.
*
* Spec §1 has two rules that every panel in this directory has to obey: data
* is mono, sentences are sans; and every "thing we are telling you about the
* data" (RECORDED RUN, EDITED, THINKING OFF, VERIFIED IN YOUR BROWSER…) is the
* same chip shape so a visitor learns after the second one what the shape
* means. Fourteen panels each writing `font-mono text-label uppercase` by hand
* is how those two rules drift, so they are written here once.
*/
import type { ComponentProps, ReactNode } from 'react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
/** Mono 11 / 500 / uppercase / +0.04em. The section-label style of §3. */
export const LABEL = 'font-mono text-label uppercase tracking-label text-muted';
/** Mono 13 / 400, tabular (body sets tabular-nums). Every compared number. */
export const MONO_DATA = 'font-mono text-mono-data';
/** A mono section label. `as` lets it be a heading when the panel needs one. */
export function Label({
as: Tag = 'p',
className,
children,
...rest
}: {
as?: 'p' | 'h2' | 'h3' | 'h4' | 'span' | 'dt';
className?: string;
children: ReactNode;
} & Omit<ComponentProps<'p'>, 'className' | 'children'>) {
return (
<Tag className={cn(LABEL, className)} {...rest}>
{children}
</Tag>
);
}
export type ChipProps = ComponentProps<typeof Badge>;
/**
* The honesty chip: the Badge under the name the spec uses for it (§9). The
* variant names are the spec's — `muted` for RECORDED RUN / PACING
* APPROXIMATE / THINKING OFF / GATE, `warning` for EDITED / COUNTERWEIGHT,
* `positive` for VERIFIED, `danger` for a mismatch, `default` for the
* intervention, `outline` for OBJECTIVE, `solid` for LIVE.
*/
export function Chip(props: ChipProps) {
return <Badge {...props} />;
}
/**
* Marks a number the visitor caused rather than one we recorded. Amber, not
* green: an edited ranking is a caution, not a brand moment, and without this
* chip it screenshots identically to a measured one.
*/
export function EditedChip({ className }: { className?: string }) {
return (
<Chip variant="warning" className={className}>
edited
</Chip>
);
}
/**
* The intervention, on an intervened run. The contract requires the text and
* it is a SENTENCE ("Told the model the rules before the first guess"), so it
* keeps the chip's ground and edge but sets in sans, mixed case, and wraps —
* an 80-character sentence in uppercase mono is unreadable and at 390px is
* the one thing on the page that makes the body scroll sideways.
*/
export function InterventionChip({ children, className }: { children: ReactNode; className?: string }) {
return (
<Chip
variant="default"
className={cn('max-w-full whitespace-normal text-left font-sans text-caption normal-case tracking-normal', className)}
>
{children}
</Chip>
);
}
/** `THINKING OFF`: the run was captured without reasoning. A chip, never a blank. */
export function ThinkingOffChip({ className }: { className?: string }) {
return (
<Chip variant="muted" className={className} title="This run was recorded with thinking disabled">
thinking off
</Chip>
);
}
/** A trailing ↗ for an external link. Text, so it inherits the link's colour. */
export function External() {
return (
<span aria-hidden="true" className="ml-0.5">
</span>
);
}
/** The status pill beside a title: LIVE (green fill) or SPEC (outline). */
export function StatusPill({ status, className }: { status: 'live' | 'spec'; className?: string }) {
return status === 'live' ? (
<Chip variant="solid" className={cn('rounded-full border-accent-edge', className)}>
live
</Chip>
) : (
<Chip variant="outline" className={cn('rounded-full', className)}>
spec
</Chip>
);
}
+56 -8
View File
@@ -31,11 +31,11 @@ function writeStored(high: boolean): void {
/** /**
* One value shared by every mounted toggle, not one `useState` each. * One value shared by every mounted toggle, not one `useState` each.
* *
* The header renders this control twice — once in the desktop bar, once inside * This control renders in three places — the desktop bar, the mobile sheet and
* the mobile sheet — and only one of them is ever visible. With local state the * the Play tab — and at most two are ever visible at once. With local state the
* hidden one keeps a stale `aria-pressed`, so a visitor who toggles on a phone * hidden one keeps a stale `aria-pressed`, so a visitor who toggles on the
* and then rotates into the desktop layout is told the setting is off while the * board and then opens the menu is told the setting is off while the page is
* page is plainly showing it on. * plainly showing it on.
*/ */
let high = false; let high = false;
let initialised = false; let initialised = false;
@@ -69,14 +69,57 @@ function subscribe(listener: () => void): () => void {
}; };
} }
export function ContrastToggle({ className }: { className?: string }) { function useContrast(): boolean {
// The server snapshot is `false` so a prerendered page never claims a // The server snapshot is `false` so a prerendered page never claims a
// preference it cannot know; the real value lands on the first subscribe. // preference it cannot know; the real value lands on the first subscribe.
const isHigh = React.useSyncExternalStore( return React.useSyncExternalStore(
subscribe, subscribe,
() => high, () => high,
() => false, () => false,
); );
}
export interface ContrastToggleProps {
className?: string;
/**
* Print "High contrast" beside the icon. This is the form the Play tab and
* the mobile sheet use: a colour-blind visitor looking at the board finds the
* mode by its name, not by guessing what a half-filled circle means.
*/
labelled?: boolean;
}
export function ContrastToggle({ className, labelled = false }: ContrastToggleProps) {
const isHigh = useContrast();
const ariaLabel = isHigh ? 'High contrast tiles on. Turn off.' : 'High contrast tiles off. Turn on.';
if (labelled) {
return (
<Button
type="button"
variant="outline"
size="touch"
// 44px below lg, 36px at lg — the same step as the icon form, so the
// Play tab's row lines up with the ghost button beside it.
className={cn(
'justify-start lg:min-h-9',
isHigh && 'border-accent-edge bg-accent-subtle text-accent-fg',
className,
)}
aria-pressed={isHigh}
aria-label={ariaLabel}
onClick={() => setHigh(!isHigh)}
>
<Contrast aria-hidden="true" />
<span className="text-ui">High contrast</span>
{/* The state as a word as well as a fill, on the control that exists
for people who cannot rely on the fill. */}
<span className="font-mono text-label uppercase tracking-label text-muted">
{isHigh ? 'on' : 'off'}
</span>
</Button>
);
}
return ( return (
<Button <Button
@@ -85,7 +128,7 @@ export function ContrastToggle({ className }: { className?: string }) {
size="icon-touch" size="icon-touch"
className={cn('lg:size-9', isHigh && 'bg-accent-subtle text-accent-fg', className)} className={cn('lg:size-9', isHigh && 'bg-accent-subtle text-accent-fg', className)}
aria-pressed={isHigh} aria-pressed={isHigh}
aria-label={isHigh ? 'High contrast tiles on. Turn off.' : 'High contrast tiles off. Turn on.'} aria-label={ariaLabel}
title="High contrast tiles" title="High contrast tiles"
onClick={() => setHigh(!isHigh)} onClick={() => setHigh(!isHigh)}
> >
@@ -93,3 +136,8 @@ export function ContrastToggle({ className }: { className?: string }) {
</Button> </Button>
); );
} }
/** The Play tab's form of the toggle. Same store, same state, with its name on it. */
export function LabelledContrastToggle({ className }: { className?: string }) {
return <ContrastToggle labelled className={className} />;
}
+38
View File
@@ -0,0 +1,38 @@
import { ANATOMY } from '@/content/anatomy';
import { cn } from '@/lib/utils';
/**
* The definition, before the catalogue.
*
* Four cells, one per part of an environment, read from `@/content/anatomy` —
* the same source `EnvAnatomy` renders on every Play and Evidence tab. That is
* the whole point: a visitor learns the four labels here, on the landing, and
* then meets them again on the word game and the fraud screen and reads both
* as "same machine, different grader". If this strip and the anatomy boxes
* ever disagreed on the words, the lesson would break at the first click.
*
* Hairline above and below; 2×2 at a phone width, four across from `lg`.
*/
export function DefinitionStrip({ className }: { className?: string }) {
return (
<ol
aria-label="The four parts of an environment"
className={cn(
'grid grid-cols-2 gap-x-6 gap-y-5 border-y border-border py-5 lg:grid-cols-4 lg:gap-x-8',
className,
)}
>
{ANATOMY.map((part) => (
<li key={part.key} className="flex min-w-0 flex-col gap-1">
<p className="flex items-baseline gap-2">
<span className="font-mono text-label uppercase tracking-label text-muted">
{String(part.index).padStart(2, '0')}
</span>
<span className="text-ui font-semibold text-fg">{part.kicker}</span>
</p>
<p className="text-small text-fg-2">{part.gloss}</p>
</li>
))}
</ol>
);
}
+300 -180
View File
@@ -1,30 +1,34 @@
/** /**
* The environment picker: one card shape, two pages. * The environment picker: one catalogue, three shapes.
* *
* Home leads with it and Gallery filters it, so the card lives here rather than * The list is built at module scope from the registry and the vertical lineup —
* twice. The list is built at module scope from the registry and the vertical * creating `src/demos/<slug>/` adds an entry with no edit to this file, which is
* lineup — creating `src/demos/<slug>/` adds a card with no edit to this file, * the same discovery-by-existence property the registry itself has. The header
* which is the same discovery-by-existence property the registry itself has. * reads the same `environments` array, so the menu, the landing and the gallery
* can never disagree about what exists.
* *
* The asymmetry is the honest part. Almost everything in the lineup is written * Three shapes:
* rather than built, and a wall of identical tiles would imply a wall of * - `FeaturedEnvironment`: a built environment, with its own board and the
* working demos on a site whose entire argument is that its claims are * mini tab strip. The only filled call to action on the landing.
* checkable. So a built environment gets its own board, its own block and the * - `EnvironmentRow`: one line of the list. Built and written environments
* only filled call to action, and the written ones read as a published roadmap * share it; the pill and the absent board are the whole difference. There is
* underneath it: dimmed, badged, and each linking to a real specification * NO opacity dimming — a spec row links to a real specification and is
* rather than to nothing. Both pages count them from the data, never from a * told apart by token, not by fading.
* number typed into the copy. * - `StatusPill`: LIVE / SPEC, exported so the header prints the same pill.
*
* Both pages count from the data, never from a number typed into the copy.
*/ */
import { useEffect, useState, type ReactNode } from 'react'; import { useEffect, useState, type ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ArrowRight, FileText, Play } from 'lucide-react'; import { ChevronRight, Play } from 'lucide-react';
import { TAB_LABELS, visibleTabs } from '@/components/demo/DemoTabs';
import { DemoIcon } from '@/components/site/DemoIcon'; import { DemoIcon } from '@/components/site/DemoIcon';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { lineup, routes } from '@/content/lineup'; import { lineup, routes } from '@/content/lineup';
import * as s from '@/content/styles'; import { listRuns } from '@/lib/demo-kit/episode';
import { import {
listDemos, listDemos,
listVerticals, listVerticals,
@@ -32,10 +36,10 @@ import {
VERTICAL_LABELS, VERTICAL_LABELS,
VERTICAL_ORDER, VERTICAL_ORDER,
} from '@/lib/demo-kit/registry'; } from '@/lib/demo-kit/registry';
import type { DemoStatus, Vertical } from '@/lib/demo-kit/types'; import type { DemoStatus, DemoTabId, Vertical } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
/** One card's worth of environment, whether it is built or only written. */ /** One environment, whether it is built or only written. */
export interface EnvironmentEntry { export interface EnvironmentEntry {
/** React key. Namespaced, because a demo and a vertical can share a slug. */ /** React key. Namespaced, because a demo and a vertical can share a slug. */
id: string; id: string;
@@ -56,12 +60,17 @@ export interface EnvironmentEntry {
verticalKey: Vertical | null; verticalKey: Vertical | null;
/** Set when the entry is a built demo; drives the board thumbnail. */ /** Set when the entry is a built demo; drives the board thumbnail. */
slug: string | null; slug: string | null;
/** A standing qualifier printed on the card, e.g. "Not in the first set". */ /** A standing qualifier printed on the row, e.g. "Not in the first set". */
note: string | null; note: string | null;
/**
* 1-based position among the written environments, printed as `01``12`.
* `null` for a built one: the built ones are not ranked, they are done.
*/
rank: number | null;
} }
/** /**
* The card wants one sentence. `verticals.ts` writes two or three, because the * The row wants one sentence. `verticals.ts` writes two or three, because the
* vertical page needs the whole argument. Trimming here rather than adding a * vertical page needs the whole argument. Trimming here rather than adding a
* `tagline` field to the data is what stops a specification and its summary * `tagline` field to the data is what stops a specification and its summary
* drifting apart — there is only ever one copy of the sentence. * drifting apart — there is only ever one copy of the sentence.
@@ -71,14 +80,14 @@ function firstSentence(text: string): string {
const match = /^[\s\S]*?[.!?](?=\s|$)/.exec(trimmed); const match = /^[\s\S]*?[.!?](?=\s|$)/.exec(trimmed);
const first = match?.[0]?.trim(); const first = match?.[0]?.trim();
// A very short first fragment means the split found an abbreviation rather // A very short first fragment means the split found an abbreviation rather
// than a sentence end, and half a clause on a card is worse than three lines. // than a sentence end, and half a clause on a row is worse than three lines.
return first !== undefined && first.length >= 24 ? first : trimmed; return first !== undefined && first.length >= 24 ? first : trimmed;
} }
/** Verticals a demo already exists for. Those are shown as the demo, not as a proposal. */ /** Verticals a demo already exists for. Those are shown as the demo, not as a proposal. */
const builtVerticals = new Set<Vertical>(listVerticals().map((group) => group.vertical)); const builtVerticals = new Set<Vertical>(listVerticals().map((group) => group.vertical));
const demoEntries: readonly EnvironmentEntry[] = listDemos().map((meta) => ({ const demoEntries: readonly Omit<EnvironmentEntry, 'rank'>[] = listDemos().map((meta) => ({
id: `demo:${meta.slug}`, id: `demo:${meta.slug}`,
title: meta.title, title: meta.title,
tagline: meta.tagline, tagline: meta.tagline,
@@ -100,7 +109,7 @@ const demoEntries: readonly EnvironmentEntry[] = listDemos().map((meta) => ({
note: null, note: null,
})); }));
const proposalEntries: readonly EnvironmentEntry[] = lineup const proposalEntries: readonly Omit<EnvironmentEntry, 'rank'>[] = lineup
.filter((vertical) => vertical.key === null || !builtVerticals.has(vertical.key)) .filter((vertical) => vertical.key === null || !builtVerticals.has(vertical.key))
.map((vertical) => ({ .map((vertical) => ({
id: `vertical:${vertical.slug}`, id: `vertical:${vertical.slug}`,
@@ -111,7 +120,7 @@ const proposalEntries: readonly EnvironmentEntry[] = lineup
icon: vertical.icon, icon: vertical.icon,
status: 'spec' as const, status: 'spec' as const,
// Its own page, with the task, the reward, the counterweight and the // Its own page, with the task, the reward, the counterweight and the
// caveat written out. A dimmed card is only honest if it goes somewhere. // caveat written out. A spec row is only honest if it goes somewhere.
href: routes.vertical(vertical.slug), href: routes.vertical(vertical.slug),
verticalLabel: vertical.key === null ? null : VERTICAL_LABELS[vertical.key], verticalLabel: vertical.key === null ? null : VERTICAL_LABELS[vertical.key],
verticalKey: vertical.key, verticalKey: vertical.key,
@@ -119,13 +128,21 @@ const proposalEntries: readonly EnvironmentEntry[] = lineup
note: vertical.plannedForV1 ? null : 'Not in the first set', note: vertical.plannedForV1 ? null : 'Not in the first set',
})); }));
/** Everything, built first. `lineup` is already ranked, so proposals keep that order. */ const unranked: readonly Omit<EnvironmentEntry, 'rank'>[] = [
export const environments: readonly EnvironmentEntry[] = [
...demoEntries.filter((entry) => entry.status === 'live'), ...demoEntries.filter((entry) => entry.status === 'live'),
...demoEntries.filter((entry) => entry.status === 'spec'), ...demoEntries.filter((entry) => entry.status === 'spec'),
...proposalEntries, ...proposalEntries,
]; ];
/** Everything, built first. `lineup` is already ranked, so proposals keep that order. */
export const environments: readonly EnvironmentEntry[] = (() => {
let next = 0;
return unranked.map((entry) => ({
...entry,
rank: entry.status === 'spec' ? ++next : null,
}));
})();
/** Playable in this tab. */ /** Playable in this tab. */
export const builtEnvironments: readonly EnvironmentEntry[] = environments.filter( export const builtEnvironments: readonly EnvironmentEntry[] = environments.filter(
(entry) => entry.status === 'live', (entry) => entry.status === 'live',
@@ -148,122 +165,66 @@ export const filterKeys: readonly Vertical[] = VERTICAL_ORDER.filter((key) =>
environments.some((entry) => entry.verticalKey === key), environments.some((entry) => entry.verticalKey === key),
); );
/* ------------------------------------------------------------------ cards */ /* ------------------------------------------------------------------ atoms */
function StatusBadge({ status }: { status: DemoStatus }) { /**
if (status === 'live') { * LIVE / SPEC. The one status pill on the site, shared with the header so a
return ( * visitor learns the shape once. Filled green means playable now; an outline
<Badge variant="positive" className="uppercase tracking-wide"> * means a specification you can read. Nothing is dimmed to say "not yet".
*/
export function StatusPill({ status, className }: { status: DemoStatus; className?: string }) {
return status === 'live' ? (
<Badge variant="solid" className={cn('rounded-full', className)}>
Live Live
</Badge> </Badge>
); ) : (
} <Badge variant="outline" className={cn('rounded-full', className)}>
return (
<Badge variant="outline" className="uppercase tracking-wide">
<FileText aria-hidden="true" className="size-3.5" />
Spec Spec
</Badge> </Badge>
); );
} }
/** /** Mono key / sans value, the two lines an executive scans for. */
* The two lines an executive actually scans for: whose budget this is, and function MetaRows({ entry, className }: { entry: EnvironmentEntry; className?: string }) {
* what the number pays for. The vertical is not repeated here — both card
* layouts already print it beside the icon.
*/
function MetaRows({ entry }: { entry: EnvironmentEntry }) {
return ( return (
<dl className="mt-4 space-y-1.5 text-sm"> <dl className={cn('space-y-1.5', className)}>
<div className="flex gap-2"> <div className="flex gap-2">
<dt className="w-16 shrink-0 text-muted">For</dt> <dt className="w-16 shrink-0 pt-0.5 font-mono text-label uppercase tracking-label text-muted">
<dd className="min-w-0 text-fg">{entry.persona}</dd> For
</dt>
<dd className="min-w-0 text-ui text-fg">{entry.persona}</dd>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<dt className="w-16 shrink-0 text-muted">Reward</dt> <dt className="w-16 shrink-0 pt-0.5 font-mono text-label uppercase tracking-label text-muted">
<dd className="min-w-0 text-fg">{entry.rewardLine}</dd> Reward
</dt>
<dd className="min-w-0 text-ui text-fg">{entry.rewardLine}</dd>
</div> </div>
</dl> </dl>
); );
} }
/* ----------------------------------------------------------- the featured */
/** /**
* One environment, as a card. * What a built demo ships, loaded from its own lazy chunk.
* *
* The whole rectangle is the link — one target, one focus ring, no nested * `tabs` is derived through the shell's own `visibleTabs`, never declared:
* interactive elements to trap a keyboard in. * `play` is whether the module has an interactive mode, `watch` is whether the
* manifest lists a recorded run for it. (The demo page additionally requires
* the run to adapt to at least one step; a card does not fetch an episode to
* find that out, so a run listed but unplayable would show a WATCH chip the
* page then drops. The manifest gate in check-demos is what keeps those equal.)
*/ */
export function EnvironmentCard({ interface DemoShape {
entry, board: ReactNode;
className, tabs: readonly DemoTabId[];
}: {
entry: EnvironmentEntry;
className?: string;
}) {
const isSpec = entry.status === 'spec';
return (
<Link
aria-label={`${entry.title}${isSpec ? 'read the specification' : 'play it'}`}
className={cn(
s.cardLink,
'h-full',
// Dimmed, never disabled. It goes to a real specification, which is the
// only thing that makes dimming it honest rather than teasing.
isSpec && 'opacity-75 hover:opacity-100 focus-visible:opacity-100',
className,
)}
to={entry.href}
>
<span className="flex items-start justify-between gap-3">
<span className="flex min-w-0 items-center gap-2">
<span
className={cn(
'grid size-10 shrink-0 place-items-center rounded-lg',
isSpec ? 'bg-surface-2 text-muted' : 'bg-accent-subtle text-accent-fg',
)}
>
<DemoIcon className="size-5" name={entry.icon} />
</span>
{entry.verticalLabel === null ? null : (
<span className="truncate text-xs text-muted">{entry.verticalLabel}</span>
)}
</span>
<StatusBadge status={entry.status} />
</span>
<h3 className="mt-4 text-base font-semibold leading-snug tracking-tight text-fg">
{entry.title}
</h3>
<p className={`${s.prose} mt-1.5 text-sm`}>{entry.tagline}</p>
<MetaRows entry={entry} />
{/* `mt-auto` so the call to action sits on the same line in every card of
a row, however long the tagline above it ran. */}
<span className="mt-auto flex flex-wrap items-center justify-between gap-x-3 gap-y-1 pt-4">
<span className="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>
{entry.note === null ? null : <span className="text-xs text-muted">{entry.note}</span>}
</span>
</Link>
);
} }
/**
* The board, drawn by the demo's own `Surface`.
*
* A screenshot would go stale the first time the board changed and nobody would
* notice. This cannot: it is the same component the demo page renders, in the
* state `interactive.init` hands a new player.
*/
const THUMBNAIL_SEED = 0; const THUMBNAIL_SEED = 0;
function BoardThumbnail({ slug }: { slug: string }) { function useDemoShape(slug: string): DemoShape | null {
const [board, setBoard] = useState<ReactNode>(null); const [shape, setShape] = useState<DemoShape | null>(null);
useEffect(() => { useEffect(() => {
let live = true; let live = true;
@@ -273,11 +234,17 @@ function BoardThumbnail({ slug }: { slug: string }) {
* away from — so the cost is a prefetch of the destination, and the board * away from — so the cost is a prefetch of the destination, and the board
* arriving is proof the destination is already loaded. * arriving is proof the destination is already loaded.
*/ */
void loadDemoModule(slug) void Promise.all([loadDemoModule(slug), listRuns(slug).catch(() => [])])
.then((module) => { .then(([module, runs]) => {
if (!live || !module.interactive) return; if (!live) return;
const { Surface } = module; const { Surface } = module;
setBoard(<Surface state={module.interactive.init(THUMBNAIL_SEED)} />); const board = module.interactive ? (
<Surface state={module.interactive.init(THUMBNAIL_SEED)} />
) : null;
setShape({
board,
tabs: visibleTabs({ play: Boolean(module.interactive), watch: runs.length > 0 }),
});
}) })
.catch(() => { .catch(() => {
// A card without a board is still a complete card. A demo whose chunk // A card without a board is still a complete card. A demo whose chunk
@@ -288,16 +255,53 @@ function BoardThumbnail({ slug }: { slug: string }) {
}; };
}, [slug]); }, [slug]);
return shape;
}
/**
* The four tabs the page will open with, as mono chips: `01 PLAY · 02 WATCH`.
* Same-height placeholder until the shape lands, so the card does not jump.
*/
function MiniTabStrip({ tabs }: { tabs: readonly DemoTabId[] | null }) {
return (
<ul aria-label="Tabs on this environment's page" className="flex min-h-5 flex-wrap gap-1.5">
{tabs === null
? Array.from({ length: 4 }, (_, i) => (
<li key={i}>
<Skeleton className="h-5 w-16 rounded-sm" />
</li>
))
: tabs.map((id, index) => (
<li key={id}>
<Badge variant="muted">
{String(index + 1).padStart(2, '0')} {TAB_LABELS[id]}
</Badge>
</li>
))}
</ul>
);
}
/**
* The board, drawn by the demo's own `Surface`, in a surface-2 inset.
*
* A screenshot would go stale the first time the board changed and nobody would
* notice. This cannot: it is the same component the demo page renders, in the
* state `interactive.init` hands a new player.
*/
function BoardThumbnail({ board }: { board: ReactNode | null }) {
return ( return (
// Decorative: the title, the tagline and the reward already say everything // Decorative: the title, the tagline and the reward already say everything
// this board says, and an unplayed board is a shape, not information. // this board says, and an unplayed board is a shape, not information.
<div aria-hidden="true"> <div aria-hidden="true" className="flex flex-col gap-2">
<div className="rounded-lg border border-border bg-surface-2 p-3"> <div className="rounded-lg border border-border bg-surface-2 p-3">
{board ?? <Skeleton className="aspect-[5/6] w-full" />} {board ?? <Skeleton className="aspect-[5/6] w-full" />}
</div> </div>
{/* An unplayed board is easy to read as a broken one. One line stops {/* An unplayed board is easy to read as a broken one. One line stops
that, and it is true of any demo's Surface, not just this one. */} that, and it is true of any demo's Surface, not just this one. */}
<p className="mt-2 text-xs text-muted">The environments own board, before a move.</p> <p className="font-mono text-label uppercase tracking-label text-muted">
The environments own board, before a move.
</p>
</div> </div>
); );
} }
@@ -305,9 +309,10 @@ function BoardThumbnail({ slug }: { slug: string }) {
/** /**
* The built environment, given the weight it has earned. * The built environment, given the weight it has earned.
* *
* Wider than a card, with the real board in it and the only filled call to * Whole card a link — one target, one focus ring, no nested controls. The copy
* action on the page. If a second demo ever ships, this renders for that one * is FIRST in the source; the board sits to the right from `lg` and follows
* too and the picker stays honest without an edit. * the copy, centred, below that, so on a phone the name of the environment and
* the call to action are never pushed under the fold by a board.
*/ */
export function FeaturedEnvironment({ export function FeaturedEnvironment({
entry, entry,
@@ -316,64 +321,195 @@ export function FeaturedEnvironment({
entry: EnvironmentEntry; entry: EnvironmentEntry;
className?: string; className?: string;
}) { }) {
return entry.slug === null ? (
<FeaturedCard entry={entry} className={className} shape={null} withBoard={false} />
) : (
<FeaturedWithShape entry={entry} slug={entry.slug} className={className} />
);
}
function FeaturedWithShape({
entry,
slug,
className,
}: {
entry: EnvironmentEntry;
slug: string;
className?: string;
}) {
const shape = useDemoShape(slug);
return <FeaturedCard entry={entry} className={className} shape={shape} withBoard />;
}
function FeaturedCard({
entry,
shape,
withBoard,
className,
}: {
entry: EnvironmentEntry;
shape: DemoShape | null;
withBoard: boolean;
className?: string;
}) {
const isSpec = entry.status === 'spec';
return ( return (
<Link <Link
aria-label={`${entry.title} — play it`} aria-label={`${entry.title}${isSpec ? 'read the specification' : 'play it'}`}
className={cn(s.cardLink, 'p-5 sm:p-7', className)} className={cn(
'card group flex flex-col p-4 transition-colors duration-1 ease-enter hover:border-border-strong sm:p-5',
className,
)}
to={entry.href} to={entry.href}
> >
<div className="grid gap-6 lg:grid-cols-[minmax(0,17rem)_minmax(0,1fr)] lg:gap-10"> <div
{/* className={cn(
The copy is FIRST in the source and the board is pulled left only from 'grid gap-5',
`lg`. On a phone a full-width board above the title pushes the name of withBoard && 'lg:grid-cols-[minmax(0,1fr)_minmax(0,17rem)] lg:gap-8',
the environment and its call to action below the fold, which is the )}
one thing this card exists to avoid. >
*/} <div className="flex min-w-0 flex-col">
<div className="flex flex-col">
<span className="flex flex-wrap items-center gap-2"> <span className="flex flex-wrap items-center gap-2">
<StatusBadge status={entry.status} /> <StatusPill status={entry.status} />
{entry.verticalLabel === null ? null : ( {entry.verticalLabel === null ? null : (
<span className="inline-flex items-center gap-1.5 text-xs text-muted"> <span className="inline-flex items-center gap-1.5 font-mono text-label uppercase tracking-label text-muted">
<DemoIcon className="size-3.5" name={entry.icon} /> <DemoIcon className="size-3.5 text-border-strong" name={entry.icon} />
{entry.verticalLabel} {entry.verticalLabel}
</span> </span>
)} )}
</span> </span>
<h3 className="mt-3 text-2xl font-bold tracking-tight text-fg sm:text-3xl"> <h3 className="mt-3 text-h2 text-fg">{entry.title}</h3>
{entry.title} <p className="mt-2 max-w-measure text-prose text-fg-2">{entry.tagline}</p>
</h3>
<p className={`${s.lede} mt-2 text-base sm:text-lg`}>{entry.tagline}</p>
<MetaRows entry={entry} /> <MetaRows entry={entry} className="mt-4" />
<p className={`${s.prose} mt-4 text-sm`}> <div className="mt-4">
Four tabs. Play the board, watch a recorded model play the same board, move the reward <MiniTabStrip tabs={shape?.tabs ?? (withBoard ? null : [])} />
weights and see the ranking change, then read the Python that scored it. </div>
</p>
<span className="mt-5 flex flex-wrap items-center gap-x-4 gap-y-2"> <span className="mt-5 flex flex-wrap items-center gap-x-4 gap-y-2">
{/* Styled as the button it behaves as. It is not a <button>: the {/* Styled as the button it behaves as. It is not a <button>: the
whole card is already the one link, and a control inside a link whole card is already the one link, and a control inside a link
is a keyboard trap dressed up as a call to action. */} is a keyboard trap dressed up as a call to action. */}
<span aria-hidden="true" className={s.btnPrimary}> <span
<Play className="size-4" /> aria-hidden="true"
Play it className="inline-flex min-h-11 items-center gap-2 rounded-md border border-accent-edge bg-brand px-4 text-ui font-medium text-accent-on"
>
{isSpec ? null : <Play className="size-4" />}
{isSpec ? 'Read the specification' : 'Play it'}
</span>
<span className="text-small text-muted">
{isSpec
? 'The task, the reward, the counterweight and where it stops being honest.'
: 'Opens on the board. No sign-up, no sales call.'}
</span> </span>
<span className="text-xs text-muted">Opens on the board. No sign-up, no sales call.</span>
</span> </span>
</div> </div>
{entry.slug === null ? null : ( {withBoard ? (
<div className="max-w-[17rem] lg:order-first"> <div className="mx-auto w-full max-w-[17rem] lg:mx-0">
<BoardThumbnail slug={entry.slug} /> <BoardThumbnail board={shape?.board ?? null} />
</div> </div>
)} ) : null}
</div> </div>
</Link> </Link>
); );
} }
/* --------------------------------------------------------------- the list */
/**
* One environment, as a row. Whole row a link; 56px from `lg`, 64px below.
*
* At a phone width it is two lines: title + pill, then the persona. The reward
* line, the tagline and the icon square come in from `lg`, where there is room
* for a row to carry the whole argument in one glance.
*/
export function EnvironmentRow({
entry,
rank = entry.rank,
className,
}: {
entry: EnvironmentEntry;
/** The printed numeral. Defaults to the entry's own rank; a filtered list passes its own. */
rank?: number | null;
className?: string;
}) {
const isSpec = entry.status === 'spec';
return (
// An `<li>`, because the pages render rows straight into their own `<ol>`
// and the hairline between rows belongs to the row, not to the list.
<li className={cn('border-b border-border last:border-b-0', className)}>
<Link
aria-label={`${entry.title}${isSpec ? 'read the specification' : 'play it'}`}
to={entry.href}
className="group flex min-h-16 items-center gap-3 px-2 py-2 transition-colors duration-1 ease-enter hover:bg-surface-2 lg:min-h-14 lg:gap-4"
>
<span className="hidden w-6 shrink-0 font-mono text-mono-data text-muted lg:block">
{rank === null || rank === undefined ? '' : String(rank).padStart(2, '0')}
</span>
<span className="hidden size-9 shrink-0 place-items-center rounded-md bg-surface-3 lg:grid">
<DemoIcon className="size-4 text-border-strong" name={entry.icon} />
</span>
<span className="flex min-w-0 flex-1 flex-col gap-0.5 lg:flex-row lg:items-center lg:gap-4">
<span className="flex min-w-0 flex-col lg:w-[19rem] lg:shrink-0">
<span className="flex items-center gap-2">
<span className="truncate text-[0.9375rem] font-medium leading-tight text-fg">
{entry.title}
</span>
<StatusPill status={entry.status} className="lg:hidden" />
</span>
<span className="hidden truncate text-small text-muted lg:block">{entry.tagline}</span>
</span>
<span className="truncate text-small text-muted lg:w-[11rem] lg:shrink-0 lg:text-fg-2">
{entry.persona}
</span>
<span className="hidden min-w-0 flex-1 truncate text-small text-fg-2 lg:block">
{entry.rewardLine}
</span>
</span>
<span className="hidden shrink-0 items-center gap-2 lg:flex">
{entry.note === null ? null : <Badge variant="muted">{entry.note}</Badge>}
<StatusPill status={entry.status} />
</span>
<ChevronRight
aria-hidden="true"
className="size-4 shrink-0 text-border-strong transition-transform duration-1 ease-enter group-hover:translate-x-0.5"
/>
</Link>
</li>
);
}
/** The rows in one ordered list, so a screen reader gets the count. */
export function EnvironmentList({
entries,
className,
}: {
entries: readonly EnvironmentEntry[];
className?: string;
}) {
return (
<ol className={cn('border-y border-border', className)}>
{entries.map((entry) => (
<EnvironmentRow entry={entry} key={entry.id} />
))}
</ol>
);
}
/**
* @deprecated The catalogue is a list, not a grid of cards. Kept as an alias
* so a page that still imports the old name renders the rows; use
* `EnvironmentList`.
*/
export const EnvironmentGrid = EnvironmentList;
/* ----------------------------------------------------------------- filter */ /* ----------------------------------------------------------------- filter */
/** The "no vertical chosen" value. Not a `Vertical`, so it cannot collide with one. */ /** The "no vertical chosen" value. Not a `Vertical`, so it cannot collide with one. */
@@ -382,8 +518,9 @@ export const ALL_VERTICALS = 'all';
export type VerticalFilterValue = Vertical | typeof ALL_VERTICALS; export type VerticalFilterValue = Vertical | typeof ALL_VERTICALS;
/** /**
* Vertical chips. Wraps on a phone rather than scrolling sideways — a chip * Vertical chips for `/gallery?vertical=`. Wraps on a phone rather than
* hidden off the right edge is a filter nobody knows exists. * scrolling sideways — a chip hidden off the right edge is a filter nobody
* knows exists. 44px tall; the border is the affordance, so it is border-strong.
*/ */
export function VerticalFilter({ export function VerticalFilter({
active, active,
@@ -404,10 +541,12 @@ export function VerticalFilter({
<button <button
aria-pressed={selected} aria-pressed={selected}
className={cn( className={cn(
'tap inline-flex items-center rounded-lg border px-4 py-2 text-sm font-medium transition-colors duration-2 ease-enter', // `text-sm`, not `text-ui`: composed through `cn`, where
// tailwind-merge v2 reads an unknown `text-*` as a colour.
'tap inline-flex items-center rounded-md border px-3.5 text-sm font-medium transition-colors duration-1 ease-enter',
selected selected
? 'border-brand bg-accent-subtle text-accent-fg' ? 'border-accent-edge bg-accent-subtle text-accent-fg'
: 'border-border bg-surface text-muted hover:bg-surface-2 hover:text-fg', : 'border-border-strong bg-surface text-fg hover:bg-surface-2',
)} )}
onClick={() => onSelect(key)} onClick={() => onSelect(key)}
type="button" type="button"
@@ -420,22 +559,3 @@ export function VerticalFilter({
</ul> </ul>
); );
} }
/** Cards in a responsive grid. One list, so a screen reader gets the count. */
export function EnvironmentGrid({
entries,
className,
}: {
entries: readonly EnvironmentEntry[];
className?: string;
}) {
return (
<ul className={cn('grid gap-4 sm:grid-cols-2 lg:grid-cols-3', className)}>
{entries.map((entry) => (
<li className="flex" key={entry.id}>
<EnvironmentCard entry={entry} />
</li>
))}
</ul>
);
}
+37 -32
View File
@@ -1,60 +1,65 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { GitBranch } from 'lucide-react'; import { ArrowUpRight, GitBranch } from 'lucide-react';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { PIG_URL, REPO_URL, repoFile } from '@/components/site/links'; import { PIG_URL, REPO_URL, repoFile } from '@/components/site/links';
import { Wordmark } from '@/components/site/Wordmark'; import { Wordmark } from '@/components/site/Wordmark';
import * as s from '@/content/styles';
const navLink =
'tap inline-flex items-center gap-2 py-1 text-ui text-fg underline-offset-4 transition-colors duration-1 ease-enter hover:text-accent-fg hover:underline';
/**
* The footer. The wordmark and the thesis on the left, the three destinations
* every page must reach on the right, then the three legal lines verbatim.
*
* The reproducibility and licence lines are set in the mono label style: they
* are statements about the repository, the kind of thing a machine reads. The
* NYT line is a sentence, so it stays in the sans. No line implying Prime
* Intellect endorsement is added here; that is a business-owner copy decision.
*/
export function SiteFooter() { export function SiteFooter() {
return ( return (
<footer className="mt-16 border-t border-border bg-surface"> <footer className="mt-12 border-t border-border bg-surface sm:mt-16">
<div className="mx-auto max-w-canvas px-4 pb-[max(2rem,var(--safe-bottom))] pl-[max(1rem,var(--safe-left))] pr-[max(1rem,var(--safe-right))] pt-8"> <div className={`${s.shell} pb-[max(2rem,var(--safe-bottom))] pt-8`}>
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between"> <div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<Wordmark className="text-base" /> <Link to="/" className="tap inline-flex w-fit items-center rounded-md" aria-label="PIG demo, home">
<p className="max-w-sm text-sm text-muted"> <Wordmark />
</Link>
<p className="max-w-[38ch] text-small text-muted">
An environment is an eval you can take the gradient of. An environment is an eval you can take the gradient of.
</p> </p>
</div> </div>
<nav aria-label="Footer" className="flex flex-col gap-1 text-sm"> <nav aria-label="Footer" className="flex flex-col gap-1">
<a <a className={navLink} href={REPO_URL} target="_blank" rel="noreferrer noopener">
className="tap inline-flex items-center gap-2 py-1 text-fg underline-offset-4 hover:text-accent-fg hover:underline" <GitBranch aria-hidden="true" className="size-4 text-border-strong" />
href={REPO_URL}
target="_blank"
rel="noreferrer noopener"
>
<GitBranch aria-hidden="true" className="size-4" />
Source on Gitea Source on Gitea
</a> </a>
<Link <Link className={navLink} to="/honesty">
className="tap inline-flex items-center py-1 text-fg underline-offset-4 hover:text-accent-fg hover:underline"
to="/honesty"
>
What we are not claiming What we are not claiming
</Link> </Link>
<a <a className={navLink} href={PIG_URL} target="_blank" rel="noreferrer noopener">
className="tap inline-flex items-center py-1 text-fg underline-offset-4 hover:text-accent-fg hover:underline"
href={PIG_URL}
target="_blank"
rel="noreferrer noopener"
>
primeintellectgrowth.com primeintellectgrowth.com
<ArrowUpRight aria-hidden="true" className="size-4 text-border-strong" />
</a> </a>
</nav> </nav>
</div> </div>
<Separator className="my-6" /> <Separator className="my-6" />
<div className="flex flex-col gap-2 text-xs leading-relaxed text-muted"> <div className="flex flex-col gap-2">
<p> {/* Mono 11, but NOT uppercase: these are sentences about the
repository, and a forty-word sentence in small caps is unreadable. */}
<p className="font-mono text-label normal-case tracking-normal text-muted">
Every number on this site is reproducible from the repository: the environments, the Every number on this site is reproducible from the repository: the environments, the
recorded rollouts and the command that produced them all ship with the source. recorded rollouts and the command that produced them all ship with the source.
</p> </p>
<p> <p className="font-mono text-label normal-case tracking-normal text-muted">
Licensed{' '} Licensed{' '}
<a <a
className="underline underline-offset-2 hover:text-accent-fg" className="underline underline-offset-2 transition-colors duration-1 ease-enter hover:text-accent-fg"
href={repoFile('LICENSE')} href={repoFile('LICENSE')}
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
@@ -64,12 +69,12 @@ export function SiteFooter() {
. .
</p> </p>
{/* {/*
Stated plainly and kept in the footer of every page. The word game Stated plainly and kept in the footer of every page, verbatim. The
demo is an independent implementation of a public game mechanic; the word game demo is an independent implementation of a public game
trademark belongs to someone else and this site must never read as mechanic; the trademark belongs to someone else and this site must
if it were theirs. never read as if it were theirs.
*/} */}
<p> <p className="text-caption text-muted">
Not affiliated with, endorsed by, or connected to The New York Times Company. Wordle is Not affiliated with, endorsed by, or connected to The New York Times Company. Wordle is
their trademark. their trademark.
</p> </p>
+309 -288
View File
@@ -2,10 +2,12 @@ import * as React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ArrowRight, ArrowUpRight, GitBranch, Menu } from 'lucide-react'; import { ArrowRight, ArrowUpRight, GitBranch, Menu } from 'lucide-react';
import { listDemos } from '@/lib/demo-kit/registry'; import { VERTICAL_LABELS, VERTICAL_ORDER } from '@/lib/demo-kit/registry';
import type { DemoMeta } from '@/lib/demo-kit/types'; import type { Vertical } from '@/lib/demo-kit/types';
import { lineup, routes } from '@/content/lineup'; import { routes } from '@/content/lineup';
import type { VerticalEntry } from '@/content/verticals'; import { CORE_TERMS } from '@/content/glossary';
import { PROPOSAL_NOTICE } from '@/content/verticals';
import * as s from '@/content/styles';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
@@ -36,6 +38,8 @@ import {
} from '@/components/ui/sheet'; } from '@/components/ui/sheet';
import { ContrastToggle } from '@/components/site/ContrastToggle'; import { ContrastToggle } from '@/components/site/ContrastToggle';
import { DemoIcon } from '@/components/site/DemoIcon'; import { DemoIcon } from '@/components/site/DemoIcon';
import { environments, StatusPill } from '@/components/site/EnvironmentPicker';
import type { EnvironmentEntry } from '@/components/site/EnvironmentPicker';
import { PIG_URL, REPO_URL, VERIFIERS_WORDLE_URL } from '@/components/site/links'; import { PIG_URL, REPO_URL, VERIFIERS_WORDLE_URL } from '@/components/site/links';
import { ThemeToggle } from '@/components/site/ThemeToggle'; import { ThemeToggle } from '@/components/site/ThemeToggle';
import { Wordmark } from '@/components/site/Wordmark'; import { Wordmark } from '@/components/site/Wordmark';
@@ -44,10 +48,18 @@ import { cn } from '@/lib/utils';
/* /*
* This header is GENERATED. Nothing in it names a demo. * This header is GENERATED. Nothing in it names a demo.
* *
* Adding `src/demos/<slug>/` puts that demo in the Demos panel, in its * The Environments panel is the whole catalogue — the same `environments`
* vertical, and (if it is the first live one) behind the CTA, with zero edits * array the landing and the gallery render, so a demo that exists is in the
* to this file. The only hand-written lists here are the five concepts under * menu, in its vertical, and (if it is the first live one) behind the CTA,
* "How it works", which are properties of the idea rather than of the lineup. * with zero edits to this file. The only hand-written text here is chrome:
* the labels of the four destinations and the quoted taskset.
*
* Built for fifty entries: rows are 40px, groups carry sticky mono headers
* inside the panel's own scroll, and the count in each column header is
* derived. A type-to-filter input is DEFERRED until the catalogue passes
* about twenty entries — it is console plumbing the content does not yet
* justify, and a search box over fourteen rows says "we could not organise
* fourteen rows".
*/ */
/** /**
@@ -63,212 +75,241 @@ const TASKSET_SOURCE = `class WordleConfig(TextArenaConfig):
class WordleTaskset(TextArenaTaskset, vf.Taskset[TextArenaTask, WordleConfig]): class WordleTaskset(TextArenaTaskset, vf.Taskset[TextArenaTask, WordleConfig]):
pass`; pass`;
const CONCEPTS: readonly { term: string; gloss: string }[] = [ /* -------------------------------------------------------------- catalogue */
{
term: 'Environment', interface Group {
gloss: 'The task, the legal moves and the grader, packaged so anyone can install and run it.', key: Vertical | null;
}, label: string;
{ entries: EnvironmentEntry[];
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.',
},
];
/** /**
* The header's Verticals panel shows the LINEUP, not the registry's groups. * Entries grouped under their vertical, in `VERTICAL_ORDER`, keeping each
* * group's internal order (registry order for demos, lineup rank for
* `listVerticals()` only ever contains verticals that already have a demo — * proposals). A proposal the contract has no key for lands in a final group so
* today that is one group, `reference`, which `src/content/verticals.ts` is * it is reachable from the bar like everything else.
* explicit is not an industry and must never be presented as one. The twelve
* proposals each have a real page at `/verticals/<slug>`, so the panel links
* there. `VerticalEntry.slug` is the route's slug; the registry's `Vertical`
* key is a different, shorter string and is deliberately not used for URLs.
*/ */
type HeaderVertical = VerticalEntry; function groupByVertical(entries: readonly EnvironmentEntry[]): Group[] {
const buckets = new Map<Vertical | null, EnvironmentEntry[]>();
for (const entry of entries) {
const bucket = buckets.get(entry.verticalKey);
if (bucket) bucket.push(entry);
else buckets.set(entry.verticalKey, [entry]);
}
const groups: Group[] = VERTICAL_ORDER.flatMap((key) => {
const bucket = buckets.get(key);
return bucket ? [{ key, label: VERTICAL_LABELS[key], entries: bucket }] : [];
});
const rest = buckets.get(null);
if (rest) groups.push({ key: null, label: 'Beyond the lineup', entries: rest });
return groups;
}
function useLineup() { interface Catalogue {
live: EnvironmentEntry[];
spec: EnvironmentEntry[];
liveGroups: Group[];
specGroups: Group[];
/** The primary CTA: `Play {title}` for the first live environment. */
cta: { href: string; label: string; short: string };
}
function useCatalogue(): Catalogue {
return React.useMemo(() => { return React.useMemo(() => {
// `listDemos` already returns a fresh array sorted by order then slug, and // `environments` is already ordered live-first, then registry specs, then
// `listVerticals` already drops the empty verticals. Neither needs redoing // the ranked lineup. Not re-sorted here: if this file starts re-sorting,
// here — if this file starts re-sorting the lineup, the header and the // the header and the gallery will eventually disagree about "first".
// gallery will eventually disagree about what "first" means. const live = environments.filter((entry) => entry.status === 'live');
const demos = listDemos(); const spec = environments.filter((entry) => entry.status === 'spec');
const live = demos.filter((demo) => demo.status === 'live'); const primary = live[0];
const spec = demos.filter((demo) => demo.status === 'spec'); // When nothing is live, the bar points at the catalogue rather than
// promising a board that does not exist.
// Already sorted by rank in `@/content/lineup`. Copied rather than passed const cta = primary
// through, because the panel props are mutable arrays and `lineup` is the ? { href: primary.href, label: `Play ${primary.title}`, short: 'Play' }
// one every page reads. : { href: routes.gallery, label: 'All environments', short: 'Browse' };
const verticals: HeaderVertical[] = [...lineup]; return {
live,
// The CTA follows the lineup rather than naming a slug. Today the first spec,
// live demo IS the word game, so this resolves to the wordle route; when a liveGroups: groupByVertical(live),
// second one ships ahead of it, the button moves with it and this file does specGroups: groupByVertical(spec),
// not change. cta,
const primary = live[0] ?? demos[0]; };
const ctaHref = primary ? `/demos/${primary.slug}` : '/demos';
return { live, spec, verticals, ctaHref };
}, []); }, []);
} }
/* ------------------------------------------------------------------ panels */ /* ------------------------------------------------------------------- atoms */
/** The mono column / group label. */
function PanelLabel({
children,
className,
sticky = false,
}: {
children: React.ReactNode;
className?: string;
sticky?: boolean;
}) {
return (
<p
className={cn(
// Arbitrary size rather than `text-label`: this string goes through
// `cn`, and tailwind-merge v2 reads an unknown `text-*` as a COLOUR, so
// `text-label text-muted` would silently drop the size.
'px-2 pb-1.5 pt-2 font-mono text-[0.6875rem] font-medium uppercase leading-[1.2] tracking-label text-muted',
// Sticky inside the panel's own scroll, on the panel's own surface, so
// the vertical a row belongs to is never off-screen at fifty entries.
sticky && 'sticky top-0 z-10 bg-surface',
className,
)}
>
{children}
</p>
);
}
/** /**
* Spreads `...rest` onto the Link, and that is not tidiness. * Spreads `...rest` onto the Link, and that is not tidiness.
* *
* Both `NavigationMenuLink asChild` and `SheetClose asChild` work by cloning * Both `NavigationMenuLink asChild` and `SheetClose asChild` work by cloning
* this element and merging their own `onClick` into it. A component that takes * this element and merging their own `onClick` into it. A component that takes
* `demo` and ignores everything else silently drops those handlers: the row * `entry` and ignores everything else silently drops those handlers: the row
* still navigates, and the mega panel or the mobile sheet stays open on top of * still navigates, and the mega panel or the mobile sheet stays open on top of
* the page you just arrived at. * the page you just arrived at.
*/ */
function DemoRow({ function DemoRow({
demo, entry,
muted = false,
className, className,
...rest ...rest
}: { demo: DemoMeta; muted?: boolean } & Omit<React.ComponentProps<typeof Link>, 'to'>) { }: { entry: EnvironmentEntry } & Omit<React.ComponentProps<typeof Link>, 'to'>) {
const isSpec = entry.status === 'spec';
return ( return (
<Link <Link
to={`/demos/${demo.slug}`} to={entry.href}
className={cn( className={cn(
'flex gap-3 rounded-lg p-2.5 transition-colors duration-1 ease-enter hover:bg-surface-2', // 44px below lg (the sheet), 40px from lg (the panel). Never both
muted && 'opacity-70 hover:opacity-100', // visible at once, so one row serves both.
'flex min-h-11 items-start gap-2.5 rounded-md px-2 py-1.5 transition-colors duration-1 ease-enter hover:bg-surface-2 lg:min-h-10',
className, className,
)} )}
{...rest} {...rest}
> >
<DemoIcon name={demo.icon} className="mt-0.5 size-4 text-accent-fg" /> {entry.rank === null ? null : (
<span className="flex min-w-0 flex-col gap-0.5"> <span className="w-5 shrink-0 pt-0.5 font-mono text-mono-data text-muted">
<span className="flex items-center gap-2 text-sm font-medium text-fg"> {String(entry.rank).padStart(2, '0')}
{demo.title}
{demo.status === 'spec' ? (
<Badge variant="outline" className="font-normal">
Spec
</Badge>
) : null}
</span> </span>
<span className="text-xs leading-relaxed text-muted"> )}
<DemoIcon name={entry.icon} className="mt-0.5 size-4 text-border-strong" />
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="flex flex-wrap items-center gap-x-2 gap-y-1">
<span className="text-ui font-medium leading-tight text-fg">{entry.title}</span>
<StatusPill status={entry.status} />
{entry.note === null ? null : <Badge variant="muted">{entry.note}</Badge>}
</span>
<span className="text-small text-muted">
{/* {/*
A spec demo shows the BUYER, not the technology: "Head of Claims" A spec row shows the BUYER, not the technology: "For the Head of
says who is meant to care, where a taskset name says nothing to the Claims" says who is meant to care, where a taskset name says
person reading this in a boardroom. nothing to the person reading this in a boardroom.
*/} */}
{demo.status === 'spec' ? `For the ${demo.persona}` : demo.tagline} {isSpec ? `For the ${entry.persona}` : entry.tagline}
</span> </span>
</span> </span>
</Link> </Link>
); );
} }
function PanelHeading({ children }: { children: React.ReactNode }) { /**
* Groups of rows under sticky vertical labels. `wrap` is what turns a row into
* a panel link or a sheet-closing link — the two contexts close differently.
*/
function GroupedRows({
groups,
wrap,
empty,
}: {
groups: Group[];
wrap: (row: React.ReactElement, key: string) => React.ReactNode;
empty: string;
}) {
if (groups.length === 0) {
return <p className="px-2 py-2 text-small text-muted">{empty}</p>;
}
return ( return (
<p className="px-2.5 pb-1 text-xs font-semibold uppercase tracking-wider text-muted"> <>
{children} {groups.map((group) => (
</p> <section key={group.key ?? 'none'} aria-label={group.label}>
<PanelLabel sticky>{group.label}</PanelLabel>
{group.entries.map((entry) => wrap(<DemoRow entry={entry} />, entry.id))}
</section>
))}
</>
); );
} }
function DemosPanel({ live, spec }: { live: DemoMeta[]; spec: DemoMeta[] }) { // `text-sm`, not `text-ui`: this string is composed through `cn`, where
// tailwind-merge v2 treats an unknown `text-*` as a colour and drops it.
const panelLink =
'inline-flex min-h-10 items-center gap-1 rounded-md px-2 text-sm font-medium text-accent-fg underline-offset-4 hover:underline';
/* ------------------------------------------------------------------ panels */
function EnvironmentsPanel({ live, spec, liveGroups, specGroups }: Catalogue) {
return ( return (
<div className="w-[min(92vw,720px)]"> <div className="flex w-[min(92vw,880px)] flex-col">
<div className="grid grid-cols-2 gap-4 p-4"> <div className="grid max-h-[70vh] grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] gap-4 overflow-y-auto overscroll-contain p-3">
<section aria-label="Live now" className="flex flex-col gap-0.5"> <section aria-label={`Live, ${live.length}`} className="flex flex-col">
<PanelHeading>Live now</PanelHeading> <PanelLabel className="text-fg">
{live.length > 0 ? ( Live <span className="text-muted">({String(live.length).padStart(2, '0')})</span>
live.map((demo) => ( </PanelLabel>
<NavigationMenuLink asChild key={demo.slug}> <GroupedRows
<DemoRow demo={demo} /> groups={liveGroups}
empty="No playable environment yet."
wrap={(row, key) => (
<NavigationMenuLink asChild key={key}>
{row}
</NavigationMenuLink> </NavigationMenuLink>
))
) : (
<p className="p-2.5 text-xs text-muted">No interactive demos published yet.</p>
)} )}
/>
</section> </section>
<section <section aria-label={`Specifications, ${spec.length}`} className="flex flex-col border-l border-border pl-4">
aria-label="Shipping next" <PanelLabel className="text-fg">
className="flex flex-col gap-0.5 border-l border-border pl-4" Spec <span className="text-muted">({String(spec.length).padStart(2, '0')})</span>
> </PanelLabel>
<PanelHeading>Shipping next</PanelHeading> <GroupedRows
{spec.length > 0 ? ( groups={specGroups}
spec.map((demo) => ( empty="Nothing written that is not built."
<NavigationMenuLink asChild key={demo.slug}> wrap={(row, key) => (
<DemoRow demo={demo} muted /> <NavigationMenuLink asChild key={key}>
{row}
</NavigationMenuLink> </NavigationMenuLink>
))
) : (
<p className="p-2.5 text-xs text-muted">Nothing queued.</p>
)} )}
/>
</section> </section>
</div> </div>
<div className="flex items-center justify-between gap-4 border-t border-border bg-surface-2 px-5 py-3"> <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 border-t border-border bg-surface-2 px-4 py-2.5">
<p className="text-xs text-muted"> <p className="text-small text-muted">
Every demo replays a real rollout from a real environment. Every playable environment replays a real recorded rollout.
</p> </p>
<span className="flex items-center gap-3">
<Badge variant="outline">{PROPOSAL_NOTICE}</Badge>
<NavigationMenuLink asChild> <NavigationMenuLink asChild>
<Link <Link to={routes.gallery} className={panelLink}>
to="/demos" All environments
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" /> <ArrowRight aria-hidden="true" className="size-3.5" />
</Link> </Link>
</NavigationMenuLink> </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) => (
<NavigationMenuLink asChild key={vertical.slug}>
<Link
to={routes.vertical(vertical.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.title}</span>
{/* The budget holder, not the reward sentence: a nav row has
one line, and "who buys this" is the more useful half. */}
<span className="text-xs leading-relaxed text-muted">{vertical.persona}</span>
</span> </span>
</Link>
</NavigationMenuLink>
))}
</div> </div>
) : (
<p className="p-2.5 text-xs text-muted">No verticals in the lineup yet.</p>
)}
</div> </div>
); );
} }
function TasksetSource({ className }: { className?: string }) { function TasksetSource({ className }: { className?: string }) {
return ( return (
<div className={cn('flex flex-col gap-2 rounded-lg border border-border bg-bg p-3', className)}> <div className={cn('flex flex-col gap-2 rounded-md border border-border bg-surface-3 p-3', className)}>
<p className="text-xs font-semibold uppercase tracking-wider text-muted"> <p className="font-mono text-label uppercase tracking-label text-muted">The actual taskset</p>
The actual taskset
</p>
{/* {/*
Wrapped, not scrolled. The source is quoted verbatim — reformatting it Wrapped, not scrolled. The source is quoted verbatim — reformatting it
to fit would make it stop being a quote — and the widest line is a third to fit would make it stop being a quote — and the widest line is a third
@@ -276,14 +317,14 @@ function TasksetSource({ className }: { className?: string }) {
entire job is "this is real code", a visible wrap beats a third of the entire job is "this is real code", a visible wrap beats a third of the
line hidden behind a scrollbar nobody in a boardroom will drag. line hidden behind a scrollbar nobody in a boardroom will drag.
*/} */}
<pre className="overflow-x-auto whitespace-pre-wrap break-words text-[11px] leading-relaxed text-fg"> <pre className="whitespace-pre-wrap break-words font-mono text-[0.6875rem] leading-relaxed text-fg [font-feature-settings:'liga'_0]">
<code className="font-mono">{TASKSET_SOURCE}</code> <code>{TASKSET_SOURCE}</code>
</pre> </pre>
<a <a
href={VERIFIERS_WORDLE_URL} href={VERIFIERS_WORDLE_URL}
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
className="inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline" className="inline-flex min-h-10 w-fit items-center gap-1 font-mono text-mono-data text-accent-fg underline-offset-4 hover:underline"
> >
verifiers/environments/wordle verifiers/environments/wordle
<ArrowUpRight aria-hidden="true" className="size-3.5" /> <ArrowUpRight aria-hidden="true" className="size-3.5" />
@@ -292,6 +333,19 @@ function TasksetSource({ className }: { className?: string }) {
); );
} }
function Glossary() {
return (
<dl className="flex flex-col gap-3">
{CORE_TERMS.map((entry) => (
<div key={entry.key} className="flex flex-col gap-0.5">
<dt className="text-ui font-medium text-fg">{entry.term}</dt>
<dd className="text-small text-fg-2">{entry.gloss}</dd>
</div>
))}
</dl>
);
}
function HowItWorksPanel({ ctaHref }: { ctaHref: string }) { function HowItWorksPanel({ ctaHref }: { ctaHref: string }) {
return ( return (
<div <div
@@ -300,25 +354,17 @@ function HowItWorksPanel({ ctaHref }: { ctaHref: string }) {
// a 1024px laptop and gives the whole page a horizontal scrollbar. // a 1024px laptop and gives the whole page a horizontal scrollbar.
className="w-[min(90vw,820px)] p-4" className="w-[min(90vw,820px)] p-4"
> >
<div className="grid grid-cols-[minmax(0,0.85fr)_minmax(0,1fr)] gap-4"> <div className="grid grid-cols-[minmax(0,0.85fr)_minmax(0,1fr)] gap-5">
<dl className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{CONCEPTS.map((concept) => ( <Glossary />
<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> <NavigationMenuLink asChild>
<Link <Link to={ctaHref} className={cn(panelLink, '-ml-2 mt-1 w-fit')}>
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 See all five on one recorded run
<ArrowRight aria-hidden="true" className="size-3.5" /> <ArrowRight aria-hidden="true" className="size-3.5" />
</Link> </Link>
</NavigationMenuLink> </NavigationMenuLink>
</dl> </div>
<TasksetSource /> <TasksetSource className="self-start" />
</div> </div>
</div> </div>
); );
@@ -326,22 +372,17 @@ function HowItWorksPanel({ ctaHref }: { ctaHref: string }) {
/* ------------------------------------------------------------------ mobile */ /* ------------------------------------------------------------------ mobile */
function MobileNav({ const sheetRow =
live, 'tap flex items-center justify-between gap-3 border-b border-border py-3 text-ui font-medium text-fg';
spec,
verticals, function MobileNav(catalogue: Catalogue) {
ctaHref, const { liveGroups, specGroups, cta } = catalogue;
}: {
live: DemoMeta[];
spec: DemoMeta[];
verticals: HeaderVertical[];
ctaHref: string;
}) {
return ( return (
<Sheet> <Sheet>
<SheetTrigger asChild> <SheetTrigger asChild>
<Button variant="ghost" size="icon-touch" aria-label="Open menu"> <Button variant="ghost" size="touch" className="px-3" aria-label="Open menu">
<Menu aria-hidden="true" /> <Menu aria-hidden="true" />
<span aria-hidden="true">Menu</span>
</Button> </Button>
</SheetTrigger> </SheetTrigger>
{/* {/*
@@ -350,110 +391,95 @@ function MobileNav({
long lineup reachable on a short phone, and `overscroll-contain` on it long lineup reachable on a short phone, and `overscroll-contain` on it
stops the flick chaining into the page underneath. stops the flick chaining into the page underneath.
*/} */}
<SheetContent side="right" className="w-[min(92vw,24rem)]"> <SheetContent side="right" className="w-[min(92vw,24rem)] rounded-l-xl shadow-overlay">
<SheetHeader> <SheetHeader>
<SheetTitle>Menu</SheetTitle> <SheetTitle className="font-mono text-label uppercase tracking-label text-muted">
Menu
</SheetTitle>
<SheetDescription className="sr-only"> <SheetDescription className="sr-only">
Demos, verticals, and how these environments work. Every environment, how they work, and the rest of the site.
</SheetDescription> </SheetDescription>
</SheetHeader> </SheetHeader>
<SheetBody> <SheetBody className="px-4 py-2">
<Accordion type="multiple" defaultValue={['demos']}> {/* Environments open by default: the menu's first job is the catalogue. */}
<AccordionItem value="demos"> <Accordion type="multiple" defaultValue={['environments']}>
<AccordionTrigger>Demos</AccordionTrigger> <AccordionItem value="environments">
<AccordionContent className="flex flex-col gap-0.5"> <AccordionTrigger>Environments</AccordionTrigger>
<PanelHeading>Live now</PanelHeading> <AccordionContent className="flex flex-col">
{live.map((demo) => ( <PanelLabel className="text-fg">Live</PanelLabel>
<SheetClose asChild key={demo.slug}> <GroupedRows
<DemoRow demo={demo} /> groups={liveGroups}
empty="No playable environment yet."
wrap={(row, key) => (
<SheetClose asChild key={key}>
{row}
</SheetClose> </SheetClose>
))} )}
<PanelHeading>Shipping next</PanelHeading> />
{spec.map((demo) => ( <PanelLabel className="mt-2 text-fg">Spec</PanelLabel>
<SheetClose asChild key={demo.slug}> <GroupedRows
<DemoRow demo={demo} muted /> groups={specGroups}
empty="Nothing written that is not built."
wrap={(row, key) => (
<SheetClose asChild key={key}>
{row}
</SheetClose> </SheetClose>
))} )}
/>
<SheetClose asChild> <SheetClose asChild>
<Link {/* Explicit `min-h-11`, not `.tap`: `.tap` lives in the
to="/demos" components layer and the utility `min-h-10` in `panelLink`
className="tap inline-flex items-center gap-1 p-2.5 text-xs font-medium text-accent-fg" would win over it, leaving a 40px target on a phone. */}
> <Link to={routes.gallery} className={cn(panelLink, 'mt-1 min-h-11 w-fit')}>
Browse all demos All environments
<ArrowRight aria-hidden="true" className="size-3.5" /> <ArrowRight aria-hidden="true" className="size-3.5" />
</Link> </Link>
</SheetClose> </SheetClose>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
<AccordionItem value="verticals">
<AccordionTrigger>Verticals</AccordionTrigger>
<AccordionContent className="flex flex-col gap-0.5">
{verticals.map((vertical) => (
<SheetClose asChild key={vertical.slug}>
<Link
to={routes.vertical(vertical.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.title}</span>
<span className="text-xs leading-relaxed text-muted">
{vertical.persona}
</span>
</span>
</Link>
</SheetClose>
))}
</AccordionContent>
</AccordionItem>
<AccordionItem value="how"> <AccordionItem value="how">
<AccordionTrigger>How it works</AccordionTrigger> <AccordionTrigger>How it works</AccordionTrigger>
<AccordionContent className="flex flex-col gap-3"> <AccordionContent className="flex flex-col gap-3">
<dl className="flex flex-col gap-3"> <Glossary />
{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 /> <TasksetSource />
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
<a <SheetClose asChild>
href={PIG_URL} <Link to={routes.honesty} className={sheetRow}>
target="_blank" Honesty
rel="noreferrer noopener" <ArrowRight aria-hidden="true" className="size-4 text-border-strong" />
className="tap mt-2 flex items-center justify-between border-b border-border py-3 text-sm font-medium text-fg" </Link>
> </SheetClose>
primeintellectgrowth.com <a href={REPO_URL} target="_blank" rel="noreferrer noopener" className={sheetRow}>
<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 Gitea Source on Gitea
<GitBranch aria-hidden="true" className="size-4 text-muted" /> <ArrowUpRight aria-hidden="true" className="size-4 text-border-strong" />
</a>
<a href={PIG_URL} target="_blank" rel="noreferrer noopener" className={sheetRow}>
primeintellectgrowth.com
<ArrowUpRight aria-hidden="true" className="size-4 text-border-strong" />
</a> </a>
<div className="flex items-center gap-1 pt-3"> <div className="grid grid-cols-2 gap-2 py-3">
<ThemeToggle /> <ThemeToggle labelled />
<ContrastToggle /> <ContrastToggle labelled />
</div> </div>
</SheetBody> </SheetBody>
<SheetFooter> <SheetFooter>
<SheetClose asChild> <SheetClose asChild>
<Button asChild size="lg" className="w-full"> <Button
<Link to={ctaHref}>Play the demo</Link> asChild
size="lg"
className="min-h-12 w-full border border-accent-edge text-[0.9375rem]"
>
<Link to={cta.href}>
{cta.label}
<ArrowRight aria-hidden="true" />
</Link>
</Button> </Button>
</SheetClose> </SheetClose>
</SheetFooter> </SheetFooter>
@@ -465,19 +491,20 @@ function MobileNav({
/* ------------------------------------------------------------------ header */ /* ------------------------------------------------------------------ header */
export function SiteHeader() { export function SiteHeader() {
const { live, spec, verticals, ctaHref } = useLineup(); const catalogue = useCatalogue();
const { cta } = catalogue;
return ( return (
<header <header
// The height token already folds in --safe-top, so the padding and the // The height token already folds in --safe-top (56px + safe-top at
// height come from the same source and a notch cannot push the row off // EVERY width), so the padding and the height come from the same source
// the bottom edge of the bar. // 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" className="sticky top-0 z-50 h-[var(--app-header-h)] border-b border-border bg-surface/85 pt-[var(--safe-top)] backdrop-blur-md"
> >
<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))]"> <div className={cn(s.shell, 'flex h-full items-center gap-2')}>
<Link <Link
to="/" to="/"
className="tap flex shrink-0 items-center rounded-md pr-2 text-base" className="tap -ml-2 flex shrink-0 items-center rounded-md px-2"
aria-label="PIG demo, home" aria-label="PIG demo, home"
> >
<Wordmark /> <Wordmark />
@@ -491,35 +518,24 @@ export function SiteHeader() {
<NavigationMenu className="hidden h-full lg:flex" delayDuration={120}> <NavigationMenu className="hidden h-full lg:flex" delayDuration={120}>
<NavigationMenuList> <NavigationMenuList>
<NavigationMenuItem> <NavigationMenuItem>
<NavigationMenuTrigger>Demos</NavigationMenuTrigger> <NavigationMenuTrigger>Environments</NavigationMenuTrigger>
<NavigationMenuContent> <NavigationMenuContent>
<DemosPanel live={live} spec={spec} /> <EnvironmentsPanel {...catalogue} />
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>Verticals</NavigationMenuTrigger>
<NavigationMenuContent>
<VerticalsPanel verticals={verticals} />
</NavigationMenuContent> </NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem> <NavigationMenuItem>
<NavigationMenuTrigger>How it works</NavigationMenuTrigger> <NavigationMenuTrigger>How it works</NavigationMenuTrigger>
<NavigationMenuContent> <NavigationMenuContent>
<HowItWorksPanel ctaHref={ctaHref} /> <HowItWorksPanel ctaHref={cta.href} />
</NavigationMenuContent> </NavigationMenuContent>
</NavigationMenuItem> </NavigationMenuItem>
<NavigationMenuItem> <NavigationMenuItem>
<NavigationMenuLink <NavigationMenuLink asChild>
href={PIG_URL} <Link to={routes.honesty} className={navigationMenuTriggerStyle()}>
target="_blank" Honesty
rel="noreferrer noopener" </Link>
className={cn(navigationMenuTriggerStyle(), 'text-muted hover:text-fg')}
>
primeintellectgrowth.com
<ArrowUpRight aria-hidden="true" className="size-3.5" />
</NavigationMenuLink> </NavigationMenuLink>
</NavigationMenuItem> </NavigationMenuItem>
</NavigationMenuList> </NavigationMenuList>
@@ -530,23 +546,28 @@ export function SiteHeader() {
<ThemeToggle /> <ThemeToggle />
<ContrastToggle /> <ContrastToggle />
<Button asChild variant="ghost" size="icon"> <Button asChild variant="ghost" size="icon">
<a <a href={REPO_URL} target="_blank" rel="noreferrer noopener" aria-label="Source on Gitea">
href={REPO_URL}
target="_blank"
rel="noreferrer noopener"
aria-label="Source on Gitea"
>
<GitBranch aria-hidden="true" /> <GitBranch aria-hidden="true" />
</a> </a>
</Button> </Button>
</div> </div>
<Button asChild size="touch" className="hidden lg:inline-flex"> {/* Desktop: the full verb + title, 40px. Phone: the verb only, so it
<Link to={ctaHref}>Play the demo</Link> fits at 320px beside the menu button and is still 44px. */}
<Button asChild className="ml-1 hidden h-10 border border-accent-edge lg:inline-flex">
<Link to={cta.href}>
{cta.label}
<ArrowRight aria-hidden="true" />
</Link>
</Button>
<Button asChild size="touch" className="border border-accent-edge lg:hidden">
<Link to={cta.href} aria-label={cta.label}>
{cta.short}
</Link>
</Button> </Button>
<div className="lg:hidden"> <div className="lg:hidden">
<MobileNav live={live} spec={spec} verticals={verticals} ctaHref={ctaHref} /> <MobileNav {...catalogue} />
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -7,7 +7,7 @@ export function SkipLink() {
return ( return (
<a <a
href="#main" href="#main"
className="sr-only focus-visible:not-sr-only focus-visible:fixed focus-visible:left-3 focus-visible:top-[max(0.75rem,var(--safe-top))] focus-visible:z-[60] focus-visible:inline-flex focus-visible:min-h-11 focus-visible:items-center focus-visible:rounded-md focus-visible:border focus-visible:border-border focus-visible:bg-surface focus-visible:px-4 focus-visible:text-sm focus-visible:font-medium focus-visible:text-fg focus-visible:shadow-lg" className="sr-only focus-visible:not-sr-only focus-visible:fixed focus-visible:left-3 focus-visible:top-[max(0.75rem,var(--safe-top))] focus-visible:z-[60] focus-visible:inline-flex focus-visible:min-h-11 focus-visible:items-center focus-visible:rounded-md focus-visible:border focus-visible:border-border focus-visible:bg-surface focus-visible:px-4 focus-visible:text-sm focus-visible:font-medium focus-visible:text-fg focus-visible:shadow-overlay"
> >
Skip to content Skip to content
</a> </a>
+224
View File
@@ -0,0 +1,224 @@
import * as React from 'react';
import { X } from 'lucide-react';
import { GLOSSARY } from '@/content/glossary';
import type { GlossaryEntry, GlossaryKey } from '@/content/glossary';
import { cn } from '@/lib/utils';
/**
* A glossed word inside running text.
*
* The glossary lives in `@/content/glossary`; this is the one component that
* renders it inline. A term is a dotted-underlined word of the same size as
* the words around it — never a 44px target inflating a sentence — with the
* gloss in a panel that opens on hover and on keyboard focus, and, because
* hover does not exist on a phone, pins open on tap. A pinned panel carries
* the 44px close target; the term itself keeps its inline size. Outside tap
* or Escape dismisses it.
*
* One panel, not a Radix Tooltip plus a popover. Radix's uncontrolled state
* (react-use-controllable-state 1.2.x under React 19) fires `onChange` in a
* post-commit effect, AFTER the freshly mounted content has registered its
* `tooltip.open` listener — so a tooltip opened by keyboard focus closes
* itself on the same commit. Measured, not guessed: the trigger went
* `closed → instant-open → closed` inside one tick. Hover survived only by
* accident of timing. A term the keyboard cannot open fails the whole point of
* glossing, so the panel is ours: three inputs, one boolean, no portal.
*
* Screen readers get the gloss through `aria-describedby` on a hidden span
* that is always in the DOM, so the description does not depend on which
* visual affordance happens to be open.
*
* Demos cannot import this (rule 9), so glossing happens in shell chrome and
* pages only; surfaces stay literal. A Term is never placed inside a link —
* it is a button, and a button inside an anchor is a keyboard trap.
*/
const byKey: ReadonlyMap<string, GlossaryEntry> = new Map(GLOSSARY.map((entry) => [entry.key, entry]));
/* ------------------------------------------------------------------ scope */
/**
* "First occurrence per panel gets the underline; later ones do not."
*
* A scope is a set of keys already glossed within one panel. Terms claim their
* key in a layout effect and release it on unmount, so React StrictMode's
* mount/unmount/mount rehearsal cannot leave a key claimed by nobody. Without
* a provider every Term shares one page-wide scope, which is the right default
* for a page with a single panel.
*/
interface Scope {
claim: (key: string) => boolean;
release: (key: string) => void;
}
function createScope(): Scope {
const seen = new Set<string>();
return {
claim: (key) => {
if (seen.has(key)) return false;
seen.add(key);
return true;
},
release: (key) => {
seen.delete(key);
},
};
}
const ScopeContext = React.createContext<Scope>(createScope());
/** Wrap a tab panel in one of these so each panel gets its own "first use". */
export function TermScope({ children }: { children: React.ReactNode }) {
const scope = React.useMemo(createScope, []);
return <ScopeContext.Provider value={scope}>{children}</ScopeContext.Provider>;
}
/* ------------------------------------------------------------------- term */
export interface TermProps {
/** A glossary key, e.g. `'rollout'`. Typed, so a typo fails the build, not the reader. */
term: GlossaryKey;
/** The word as it appears in the sentence. Defaults to the glossary's own term. */
children?: React.ReactNode;
className?: string;
}
export function Term({ term, children, className }: TermProps) {
const entry = byKey.get(term);
const scope = React.useContext(ScopeContext);
const id = React.useId();
const [first, setFirst] = React.useState(false);
const [hovered, setHovered] = React.useState(false);
const [focused, setFocused] = React.useState(false);
const [pinned, setPinned] = React.useState(false);
const [align, setAlign] = React.useState<'left' | 'right'>('left');
const rootRef = React.useRef<HTMLSpanElement>(null);
const panelRef = React.useRef<HTMLSpanElement>(null);
const open = pinned || hovered || focused;
React.useLayoutEffect(() => {
if (!entry) return undefined;
const got = scope.claim(entry.key);
setFirst(got);
return () => {
if (got) scope.release(entry.key);
};
}, [scope, entry]);
// Outside tap or Escape dismisses. Listeners exist only while the panel is
// open, so a page with forty terms is not forty document listeners.
React.useEffect(() => {
if (!open) return undefined;
const onPointer = (event: PointerEvent) => {
if (rootRef.current && !rootRef.current.contains(event.target as Node)) {
setPinned(false);
setHovered(false);
}
};
const onKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setPinned(false);
setHovered(false);
// Stays dismissed until the next focus event, so Escape means "go
// away" and not "flicker".
setFocused(false);
}
};
document.addEventListener('pointerdown', onPointer);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('pointerdown', onPointer);
document.removeEventListener('keydown', onKey);
};
}, [open]);
// Anchor the panel to whichever edge keeps it on screen. Measured once per
// open, after paint, so the first frame is already in the right place.
React.useLayoutEffect(() => {
if (!open) return;
const panel = panelRef.current;
if (!panel) return;
const rect = panel.getBoundingClientRect();
setAlign(rect.right > window.innerWidth - 16 ? 'right' : 'left');
}, [open]);
// An unknown key renders the plain word. A sentence must never fail to
// render because the glossary is missing an entry.
if (!entry) return <>{children ?? term}</>;
const glossId = `${id}-gloss`;
return (
<span ref={rootRef} className="relative inline">
<button
type="button"
aria-describedby={glossId}
aria-expanded={pinned}
onClick={() => setPinned((value) => !value)}
// Touch pointers never hover; a tap is a click, handled above.
onPointerEnter={(event) => {
if (event.pointerType !== 'touch') setHovered(true);
}}
onPointerLeave={() => setHovered(false)}
// Keyboard focus only. A mouse click also focuses the button, and
// opening on that would double up with the click toggle.
onFocus={(event) => {
if (event.currentTarget.matches(':focus-visible')) setFocused(true);
}}
onBlur={() => setFocused(false)}
className={cn(
// Inherit everything: the term is a word in a sentence, not a
// control that happens to hold a word.
'inline cursor-help appearance-none rounded-sm bg-transparent p-0 text-inherit [font:inherit] [letter-spacing:inherit]',
first
? 'underline decoration-border-strong decoration-dotted decoration-[1.5px] underline-offset-[0.2em]'
: 'no-underline',
className,
)}
>
{children ?? entry.term}
</button>
<span id={glossId} className="sr-only">
{entry.term}: {entry.gloss}
</span>
{open ? (
<span
ref={panelRef}
data-term-panel={pinned ? 'pinned' : 'hover'}
// Pinned, it is a small dialog with its own close control. Otherwise
// it is a purely visual echo of the sr-only description above, so it
// is hidden from the accessibility tree rather than read twice.
role={pinned ? 'dialog' : undefined}
aria-label={pinned ? entry.term : undefined}
aria-hidden={pinned ? undefined : true}
className={cn(
'absolute top-full z-50 mt-1.5 flex w-max max-w-[min(36ch,calc(100vw-2.5rem))] items-start gap-2 rounded-md border border-border bg-surface p-3 text-left font-sans text-[0.8125rem] font-normal normal-case leading-[1.45] tracking-normal text-fg-2 shadow-overlay',
pinned && 'pr-1',
align === 'left' ? 'left-0' : 'right-0',
)}
>
<span className={cn('min-w-0', pinned && 'py-2')}>
<span className="mr-1.5 font-mono text-label uppercase tracking-label text-muted">
{entry.term}
</span>
{entry.gloss}
</span>
{pinned ? (
<button
type="button"
aria-label="Close"
onClick={() => setPinned(false)}
className="tap -mr-0.5 -mt-1 inline-flex shrink-0 items-center justify-center rounded-md text-muted transition-colors duration-1 ease-enter hover:bg-surface-2 hover:text-fg"
>
<X aria-hidden="true" className="size-4" />
</button>
) : null}
</span>
) : null}
</span>
);
}
+36 -6
View File
@@ -23,19 +23,52 @@ const LABEL: Record<Mode, string> = {
* the wrong one and then swaps it — the flash this component exists to avoid. * the wrong one and then swaps it — the flash this component exists to avoid.
* The placeholder is the same size as the button so the header does not reflow * The placeholder is the same size as the button so the header does not reflow
* when it resolves. * when it resolves.
*
* `labelled` is the mobile-sheet form: the same control with its current state
* printed beside the icon, because a row of two icon-only buttons in a menu
* reads as decoration. Both forms are 44px below `lg`; the icon form drops to
* 36px in the desktop bar.
*/ */
export function ThemeToggle({ className }: { className?: string }) { export function ThemeToggle({ className, labelled = false }: { className?: string; labelled?: boolean }) {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const [mounted, setMounted] = React.useState(false); const [mounted, setMounted] = React.useState(false);
React.useEffect(() => setMounted(true), []); React.useEffect(() => setMounted(true), []);
if (!mounted) { if (!mounted) {
return <div className={cn('size-11 lg:size-9', className)} aria-hidden="true" />; return (
<div
className={cn(labelled ? 'h-11 w-32' : 'size-11 lg:size-9', className)}
aria-hidden="true"
/>
);
} }
const current: Mode = theme === 'dark' || theme === 'light' ? theme : 'system'; const current: Mode = theme === 'dark' || theme === 'light' ? theme : 'system';
const next = ORDER[(ORDER.indexOf(current) + 1) % ORDER.length] ?? 'system'; const next = ORDER[(ORDER.indexOf(current) + 1) % ORDER.length] ?? 'system';
const Icon = current === 'light' ? Sun : current === 'dark' ? Moon : Laptop; const Icon = current === 'light' ? Sun : current === 'dark' ? Moon : Laptop;
// The label states where you ARE and where the press takes you, because an
// icon-only toggle announced as just "Theme" tells a screen-reader user
// nothing about what pressing it will do.
const ariaLabel = `Theme: ${LABEL[current]}. Switch to ${LABEL[next].toLowerCase()}.`;
if (labelled) {
return (
<Button
type="button"
variant="outline"
size="touch"
className={cn('justify-start', className)}
onClick={() => setTheme(next)}
aria-label={ariaLabel}
>
<Icon aria-hidden="true" />
<span className="flex flex-col items-start leading-tight">
<span className="font-mono text-label uppercase tracking-label text-muted">Theme</span>
<span className="text-ui text-fg">{LABEL[current]}</span>
</span>
</Button>
);
}
return ( return (
<Button <Button
@@ -44,10 +77,7 @@ export function ThemeToggle({ className }: { className?: string }) {
size="icon-touch" size="icon-touch"
className={cn('lg:size-9', className)} className={cn('lg:size-9', className)}
onClick={() => setTheme(next)} onClick={() => setTheme(next)}
// The label states where you ARE and where the press takes you, because aria-label={ariaLabel}
// an icon-only toggle announced as just "Theme" tells a screen-reader
// user nothing about what pressing it will do.
aria-label={`Theme: ${LABEL[current]}. Switch to ${LABEL[next].toLowerCase()}.`}
title={`Theme: ${LABEL[current]}`} title={`Theme: ${LABEL[current]}`}
> >
<Icon aria-hidden="true" /> <Icon aria-hidden="true" />
+93
View File
@@ -0,0 +1,93 @@
import { cn } from '@/lib/utils';
/**
* A static row of scored tiles, drawn from the tile tokens.
*
* The landing's "the grader computes" passage needs a board fragment beside
* it. It cannot import the word game's Surface — the site lane may never reach
* into `src/demos/*` — and a screenshot would go stale. So this is the shape of
* a scored row, painted with the same `--tile-*` tokens the real board uses,
* which means it follows the theme AND the high-contrast toggle without knowing
* either exists. It is a figure, not a game: nothing here is interactive.
*
* Each tile carries a second signal in high-contrast mode (● exact, ◆ present,
* nothing for absent), exactly as the real board does, so the figure reads for
* the same visitor the toggle exists for.
*/
export type TileState = 'exact' | 'present' | 'absent';
/** The pattern the landing caption spells out: `G Y X X G`. */
const DEFAULT_PATTERN: readonly TileState[] = ['exact', 'present', 'absent', 'absent', 'exact'];
const DEFAULT_LETTERS = 'CRANE';
const STATE_WORD: Record<TileState, string> = {
exact: 'exact',
present: 'present',
absent: 'absent',
};
const TILE_CLASS: Record<TileState, string> = {
// Glyph contrast on each fill is measured in index.css: 13.08 / 11.16 / 7.75
// (light 5.74) in the standard palette, and ≥4.74 in high contrast.
exact: 'border-tile-exact bg-tile-exact text-tile-exact-fg',
present: 'border-tile-present bg-tile-present text-tile-present-fg',
absent: 'border-tile-absent bg-tile-absent text-tile-absent-fg',
};
const GLYPH: Record<TileState, string | null> = { exact: '●', present: '◆', absent: null };
export interface TileRowProps {
/** Five letters, one per tile. Uppercased for display. */
letters?: string;
/** One state per letter. Defaults to the `G Y X X G` pattern. */
pattern?: readonly TileState[];
className?: string;
}
export function TileRow({
letters = DEFAULT_LETTERS,
pattern = DEFAULT_PATTERN,
className,
}: TileRowProps) {
const cells = Array.from(letters.toUpperCase()).map((letter, index) => ({
letter,
state: pattern[index] ?? 'absent',
}));
// One sentence for a screen reader, instead of five unlabelled squares.
const label = cells.map((cell) => `${cell.letter} ${STATE_WORD[cell.state]}`).join(', ');
return (
<div
role="img"
aria-label={`Scored row: ${label}.`}
className={cn('flex w-fit gap-1 sm:gap-1.5', className)}
>
{cells.map((cell, index) => {
const glyph = GLYPH[cell.state];
return (
<span
key={index}
aria-hidden="true"
className={cn(
// Board letters are content, not a terminal: sans 600 uppercase,
// 22px rising to 24px from sm, the same as the real tiles.
'relative grid size-10 place-items-center rounded-tile border-2 font-sans text-[1.375rem] font-semibold uppercase leading-none sm:size-12 sm:text-2xl',
TILE_CLASS[cell.state],
)}
>
{cell.letter}
{glyph === null ? null : (
<span
className="pointer-events-none absolute bottom-0.5 right-0.5 text-[0.5em] leading-none opacity-0 [[data-contrast=high]_&]:opacity-100"
>
{glyph}
</span>
)}
</span>
);
})}
</div>
);
}
+10 -6
View File
@@ -1,17 +1,21 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
/** /**
* Two words, one tone shift: the brand carries the accent, the qualifier does * Two words, two faces: "PIG" is a name and is set in the sans; "demo" is a
* not. Rendered as a single string for a screen reader so it is announced * qualifier that reads like a tag, so it takes the mono the site uses for
* "PIG demo" rather than as two unrelated fragments. * every label. One tone shift — the brand carries the accent, the qualifier
* does not — and a single sr-only string so a screen reader announces
* "PIG demo" rather than two unrelated fragments.
*
* This is the site's only mark. Prime Intellect's logo is never rendered here.
*/ */
export function Wordmark({ className }: { className?: string }) { export function Wordmark({ className }: { className?: string }) {
return ( return (
<span className={cn('inline-flex items-baseline gap-1 font-semibold tracking-tight', className)}> <span className={cn('inline-flex items-baseline gap-1.5 text-base', className)}>
<span aria-hidden="true" className="text-accent-fg"> <span aria-hidden="true" className="font-sans font-semibold tracking-tight text-accent-fg">
PIG PIG
</span> </span>
<span aria-hidden="true" className="font-normal lowercase text-muted"> <span aria-hidden="true" className="font-mono text-[0.8125rem] font-normal lowercase text-muted">
demo demo
</span> </span>
<span className="sr-only">PIG demo</span> <span className="sr-only">PIG demo</span>
+8 -2
View File
@@ -13,6 +13,12 @@ function AccordionItem({
return <AccordionPrimitive.Item className={cn('border-b border-border', className)} {...props} />; return <AccordionPrimitive.Item className={cn('border-b border-border', className)} {...props} />;
} }
/**
* 44px rows: the accordion lives in the mobile sheet, where every row is a
* thumb target. The chevron is `border-strong` — the ≥3:1 non-text token — so
* it reads at rest in dark, where `muted` on a surface is text-weight ink and
* `border` is invisible.
*/
function AccordionTrigger({ function AccordionTrigger({
className, className,
children, children,
@@ -22,7 +28,7 @@ function AccordionTrigger({
<AccordionPrimitive.Header className="flex"> <AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger <AccordionPrimitive.Trigger
className={cn( className={cn(
'tap flex flex-1 items-center justify-between gap-3 py-3 text-left text-sm font-medium text-fg transition-colors duration-1 ease-enter hover:text-accent-fg [&[data-state=open]>svg]:rotate-180', 'flex min-h-11 flex-1 items-center justify-between gap-3 py-2.5 text-left font-sans text-sm font-medium text-fg transition-colors duration-1 ease-enter hover:text-accent-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg [&[data-state=open]>svg]:rotate-180',
className, className,
)} )}
{...props} {...props}
@@ -30,7 +36,7 @@ function AccordionTrigger({
{children} {children}
<ChevronDown <ChevronDown
aria-hidden="true" aria-hidden="true"
className="size-4 shrink-0 text-muted transition-transform duration-2 ease-enter" className="size-4 shrink-0 text-border-strong transition-transform duration-2 ease-enter"
/> />
</AccordionPrimitive.Trigger> </AccordionPrimitive.Trigger>
</AccordionPrimitive.Header> </AccordionPrimitive.Header>
+20 -8
View File
@@ -3,19 +3,31 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
/**
* The Chip (spec §9). One shape for every machine-made label on the site —
* LIVE, SPEC, RECORDED RUN, SYNTHETIC DATA, EDITED, VERIFIED IN YOUR BROWSER —
* so a visitor learns after the second one that this shape means "a thing we
* are telling you about the data". Mono, uppercase, 11px, 4px radius. Status
* pills (LIVE, SPEC) pass `rounded-full`; SYNTHETIC DATA passes
* `border-dashed border-warning` and the merge keeps it.
*
* Grounds are the `*-subtle` tokens, never an alpha tint: `bg-warning/10`
* vanishes on #0F0F0F. Every text/ground pair here is measured ≥ 4.5 in both
* themes (index.css carries the numbers).
*/
const badgeVariants = cva( const badgeVariants = cva(
'inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium leading-5 transition-colors duration-1 ease-enter', 'inline-flex items-center gap-1 whitespace-nowrap rounded-sm border px-1.5 py-0.5 font-mono text-[0.6875rem] font-medium uppercase leading-[1.2] tracking-label transition-colors duration-1 ease-enter',
{ {
variants: { variants: {
variant: { variant: {
default: 'border-transparent bg-accent-subtle text-accent-fg', default: 'border-transparent bg-accent-subtle text-accent-fg',
outline: 'border-border text-muted', outline: 'border-border-strong bg-transparent text-muted',
solid: 'border-transparent bg-brand text-accent-on', solid: 'border-accent-edge bg-brand text-accent-on',
positive: 'border-transparent bg-positive/10 text-positive', positive: 'border-transparent bg-accent-subtle text-positive',
warning: 'border-transparent bg-warning/10 text-warning', warning: 'border-transparent bg-warning-subtle text-warning',
danger: 'border-transparent bg-danger/10 text-danger', danger: 'border-transparent bg-danger-subtle text-danger',
info: 'border-transparent bg-info/10 text-info', info: 'border-transparent bg-surface-3 text-info',
muted: 'border-border bg-surface-2 text-muted', muted: 'border-transparent bg-surface-3 text-muted',
}, },
}, },
defaultVariants: { variant: 'default' }, defaultVariants: { variant: 'default' },
+19 -9
View File
@@ -5,32 +5,42 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
/** /**
* shadcn "new-york" button, retuned to PIG's tokens. * shadcn "new-york" button, retuned to PIG's tokens (spec §9).
* *
* Note `accent` in tailwind.config.js is the SUBTLE hover surface, not the * Note `accent` in tailwind.config.js is the SUBTLE hover surface, not the
* brand — that mapping is deliberate and documented there. So the solid CTA * brand — that mapping is deliberate and documented there. So the solid CTA
* uses `bg-brand text-accent-on`, never `bg-accent`. * uses `bg-brand text-accent-on`, never `bg-accent`.
*
* Every variant carries a 1px border (transparent where it has no colour) so
* the variants line up to the pixel when they sit in one row; a borderless
* ghost next to a bordered outline is otherwise 2px shorter.
*
* The solid CTA's hover differs by theme on purpose. In dark the fill has a
* darker sibling (`brand/90`). In light the neon green has none — darkening it
* reads brown — so the hover flips to the subtle ground with the text green,
* and the accent-edge border is what keeps it reading as the same control.
*/ */
const buttonVariants = cva( const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors duration-1 ease-enter disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md border border-transparent font-sans text-sm font-medium transition-colors duration-1 ease-enter focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-brand text-accent-on shadow-sm hover:bg-brand/90', default:
secondary: 'bg-surface-2 text-fg hover:bg-surface-2/70', 'border-accent-edge bg-brand text-accent-on hover:bg-accent-subtle hover:text-accent-fg dark:hover:bg-brand/90 dark:hover:text-accent-on',
outline: 'border border-border bg-surface text-fg hover:bg-surface-2', secondary: 'bg-surface-2 text-fg hover:bg-surface-3',
outline: 'border-border-strong bg-surface text-fg hover:bg-surface-2',
ghost: 'text-fg hover:bg-surface-2', ghost: 'text-fg hover:bg-surface-2',
subtle: 'bg-accent-subtle text-accent-fg hover:bg-accent-subtle/70', subtle: 'bg-accent-subtle text-accent-fg hover:border-accent-edge',
destructive: 'bg-danger text-white shadow-sm hover:bg-danger/90', destructive: 'bg-danger text-danger-on hover:bg-danger/90',
link: 'text-accent-fg underline-offset-4 hover:underline', link: 'text-accent-fg underline-offset-4 hover:underline',
}, },
size: { size: {
// `h-9` is 36px, which is fine for a mouse and too small for a thumb. // `h-9` is 36px, which is fine for a mouse and too small for a thumb.
// Anything a phone visitor taps gets `size="touch"` or the `.tap` // Anything a phone visitor taps gets `size="touch"` or the `.tap`
// helper on top — see SiteHeader. // helper on top — see SiteHeader.
sm: 'h-8 rounded-md px-3 text-xs [&_svg]:size-3.5', sm: 'h-8 px-3 text-[0.8125rem] [&_svg]:size-3.5',
default: 'h-9 px-4 py-2 [&_svg]:size-4', default: 'h-9 px-4 py-2 [&_svg]:size-4',
lg: 'h-11 rounded-lg px-6 [&_svg]:size-4', lg: 'h-11 px-6 [&_svg]:size-4',
touch: 'min-h-11 px-4 py-2 [&_svg]:size-4', touch: 'min-h-11 px-4 py-2 [&_svg]:size-4',
icon: 'size-9 [&_svg]:size-4', icon: 'size-9 [&_svg]:size-4',
'icon-touch': 'size-11 [&_svg]:size-[1.125rem]', 'icon-touch': 'size-11 [&_svg]:size-[1.125rem]',
+11 -7
View File
@@ -2,30 +2,34 @@ import * as React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
/** `.card` is defined in src/index.css so the shell and the demos agree on /**
* one surface treatment; this component is the React face of it. */ * `.card` is defined in src/index.css (12px, surface, 1px border, no shadow)
* so the shell and the demos agree on one surface treatment; this component
* is the React face of it. Where a card is a link the caller adds
* `hover:border-border-strong` — the border is the hover, never a shadow.
*/
function Card({ className, ...props }: React.ComponentProps<'div'>) { function Card({ className, ...props }: React.ComponentProps<'div'>) {
return <div className={cn('card text-fg', className)} {...props} />; return <div className={cn('card text-fg', className)} {...props} />;
} }
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return <div className={cn('flex flex-col gap-1.5 p-5', className)} {...props} />; return <div className={cn('flex flex-col gap-1.5 p-4 sm:p-5', className)} {...props} />;
} }
function CardTitle({ className, ...props }: React.ComponentProps<'h3'>) { function CardTitle({ className, ...props }: React.ComponentProps<'h3'>) {
return <h3 className={cn('font-semibold leading-tight tracking-tight', className)} {...props} />; return <h3 className={cn('text-h3 text-fg', className)} {...props} />;
} }
function CardDescription({ className, ...props }: React.ComponentProps<'p'>) { function CardDescription({ className, ...props }: React.ComponentProps<'p'>) {
return <p className={cn('text-sm text-muted', className)} {...props} />; return <p className={cn('text-small text-muted', className)} {...props} />;
} }
function CardContent({ className, ...props }: React.ComponentProps<'div'>) { function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return <div className={cn('p-5 pt-0', className)} {...props} />; return <div className={cn('p-4 pt-0 sm:p-5 sm:pt-0', className)} {...props} />;
} }
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return <div className={cn('flex items-center p-5 pt-0', className)} {...props} />; return <div className={cn('flex items-center p-4 pt-0 sm:p-5 sm:pt-0', className)} {...props} />;
} }
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }; export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter };
+16 -4
View File
@@ -27,6 +27,12 @@ function DrawerOverlay({
return <DrawerPrimitive.Overlay className={cn('fixed inset-0 z-50 bg-fg/40', className)} {...props} />; return <DrawerPrimitive.Overlay className={cn('fixed inset-0 z-50 bg-fg/40', className)} {...props} />;
} }
/**
* An overlay (spec §9): surface, 1px border, 12px on the free (top) edge,
* `shadow-overlay`. The handle is `border-strong`, not `border`: in dark the
* hairline is 1.14:1 against the surface and a handle nobody can see is not a
* drag affordance.
*/
function DrawerContent({ function DrawerContent({
className, className,
children, children,
@@ -37,14 +43,17 @@ function DrawerContent({
<DrawerOverlay /> <DrawerOverlay />
<DrawerPrimitive.Content <DrawerPrimitive.Content
className={cn( className={cn(
'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto max-h-[88svh] flex-col rounded-t-xl border border-border bg-surface pb-[var(--safe-bottom)]', 'fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto max-h-[88svh] flex-col rounded-t-xl border border-border bg-surface pb-[var(--safe-bottom)] shadow-overlay',
className, className,
)} )}
{...props} {...props}
> >
{/* The grab handle is decorative; the drawer is also closable with {/* The grab handle is decorative; the drawer is also closable with
Escape and by the close control the caller renders. */} Escape and by the close control the caller renders. */}
<div className="mx-auto mt-3 h-1.5 w-12 shrink-0 rounded-full bg-border" aria-hidden="true" /> <div
className="mx-auto mt-3 h-1.5 w-12 shrink-0 rounded-full bg-border-strong"
aria-hidden="true"
/>
{children} {children}
</DrawerPrimitive.Content> </DrawerPrimitive.Content>
</DrawerPortal> </DrawerPortal>
@@ -70,7 +79,10 @@ function DrawerFooter({ className, ...props }: React.ComponentProps<'div'>) {
function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) { function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
return ( return (
<DrawerPrimitive.Title className={cn('text-base font-semibold text-fg', className)} {...props} /> <DrawerPrimitive.Title
className={cn('font-sans text-base font-semibold text-fg', className)}
{...props}
/>
); );
} }
@@ -79,7 +91,7 @@ function DrawerDescription({
...props ...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) { }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return ( return (
<DrawerPrimitive.Description className={cn('text-sm text-muted', className)} {...props} /> <DrawerPrimitive.Description className={cn('text-small text-muted', className)} {...props} />
); );
} }
+8 -3
View File
@@ -57,8 +57,13 @@ function NavigationMenuList({
const NavigationMenuItem = NavigationMenuPrimitive.Item; const NavigationMenuItem = NavigationMenuPrimitive.Item;
/**
* 40px rows at the desktop bar (spec §5: nav rows 40px desktop / 44px touch —
* this menu only renders from lg). Plain link items in the bar reuse this
* style, so the hover and open grounds are the same surface-2 for both.
*/
const navigationMenuTriggerStyle = cva( const navigationMenuTriggerStyle = cva(
'group inline-flex h-9 w-max items-center justify-center gap-1 rounded-md px-3 py-2 text-sm font-medium text-fg transition-colors duration-1 ease-enter hover:bg-surface-2 disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-surface-2', 'group inline-flex h-10 w-max items-center justify-center gap-1 rounded-md px-3 py-2 font-sans text-sm font-medium text-fg transition-colors duration-1 ease-enter hover:bg-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-surface-2 data-[state=open]:text-fg',
); );
function NavigationMenuTrigger({ function NavigationMenuTrigger({
@@ -74,7 +79,7 @@ function NavigationMenuTrigger({
{children} {children}
<ChevronDown <ChevronDown
aria-hidden="true" aria-hidden="true"
className="relative top-px size-3.5 text-muted transition-transform duration-3 ease-enter group-data-[state=open]:rotate-180" className="relative top-px size-3.5 text-border-strong transition-transform duration-3 ease-enter group-data-[state=open]:rotate-180 group-hover:text-fg group-data-[state=open]:text-fg"
/> />
</NavigationMenuPrimitive.Trigger> </NavigationMenuPrimitive.Trigger>
); );
@@ -112,7 +117,7 @@ function NavigationMenuViewport({
<div className="absolute left-0 top-full z-50 flex justify-center"> <div className="absolute left-0 top-full z-50 flex justify-center">
<NavigationMenuPrimitive.Viewport <NavigationMenuPrimitive.Viewport
className={cn( className={cn(
'relative mt-2 h-[var(--radix-navigation-menu-viewport-height)] w-full origin-top overflow-hidden rounded-xl border border-border bg-surface text-fg shadow-xl', 'relative mt-2 h-[var(--radix-navigation-menu-viewport-height)] w-full origin-top overflow-hidden rounded-xl border border-border bg-surface text-fg shadow-overlay',
'transition-[width,height] duration-3 ease-enter', 'transition-[width,height] duration-3 ease-enter',
'data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 'data-[state=closed]:animate-out data-[state=open]:animate-in data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
'md:w-[var(--radix-navigation-menu-viewport-width)]', 'md:w-[var(--radix-navigation-menu-viewport-width)]',
+5 -1
View File
@@ -3,6 +3,10 @@ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
/**
* The thumb is `border-strong`, not `border`: a scrollbar is the only signal
* that a box scrolls, and `border` is 1.14:1 against a dark surface.
*/
function ScrollBar({ function ScrollBar({
className, className,
orientation = 'vertical', orientation = 'vertical',
@@ -19,7 +23,7 @@ function ScrollBar({
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" /> <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border-strong" />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
); );
} }
+17 -8
View File
@@ -22,17 +22,24 @@ function SheetOverlay({ className, ...props }: React.ComponentProps<typeof Sheet
); );
} }
/**
* An overlay (spec §9): surface, 1px border on the attached edge, 12px radius
* on the free edge, `shadow-overlay`. The side sheets default to
* `min(92vw, 24rem)` — the mobile menu's width from §5 — so the header needs
* no override. The free-edge radius is per side: a right sheet rounds its
* left corners, and so on.
*/
const sheetVariants = cva( const sheetVariants = cva(
'fixed z-50 flex flex-col gap-0 bg-surface shadow-xl duration-3 ease-enter data-[state=open]:animate-in data-[state=closed]:animate-out', 'fixed z-50 flex flex-col gap-0 bg-surface shadow-overlay duration-3 ease-enter data-[state=open]:animate-in data-[state=closed]:animate-out',
{ {
variants: { variants: {
side: { side: {
top: 'inset-x-0 top-0 border-b border-border pt-[var(--safe-top)] data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', top: 'inset-x-0 top-0 rounded-b-xl border-b border-border pt-[var(--safe-top)] data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom: bottom:
'inset-x-0 bottom-0 border-t border-border pb-[var(--safe-bottom)] data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', 'inset-x-0 bottom-0 rounded-t-xl border-t border-border pb-[var(--safe-bottom)] data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-[min(88vw,22rem)] border-r border-border pl-[var(--safe-left)] data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left', left: 'inset-y-0 left-0 h-full w-[min(92vw,24rem)] rounded-r-xl border-r border-border pl-[var(--safe-left)] data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left',
right: right:
'inset-y-0 right-0 h-full w-[min(88vw,22rem)] border-l border-border pr-[var(--safe-right)] data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right', 'inset-y-0 right-0 h-full w-[min(92vw,24rem)] rounded-l-xl border-l border-border pr-[var(--safe-right)] data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right',
}, },
}, },
defaultVariants: { side: 'right' }, defaultVariants: { side: 'right' },
@@ -60,7 +67,7 @@ function SheetContent({
{children} {children}
{showClose ? ( {showClose ? (
<SheetPrimitive.Close <SheetPrimitive.Close
className="tap absolute right-3 top-3 inline-flex items-center justify-center rounded-md p-2 text-muted transition-colors duration-1 ease-enter hover:bg-surface-2 hover:text-fg" className="absolute right-2 top-[max(0.5rem,var(--safe-top))] inline-flex size-11 items-center justify-center rounded-md text-muted transition-colors duration-1 ease-enter hover:bg-surface-2 hover:text-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
aria-label="Close" aria-label="Close"
> >
<X aria-hidden="true" className="size-5" /> <X aria-hidden="true" className="size-5" />
@@ -113,7 +120,7 @@ function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) { function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
return ( return (
<SheetPrimitive.Title <SheetPrimitive.Title
className={cn('text-base font-semibold text-fg', className)} className={cn('font-sans text-base font-semibold text-fg', className)}
{...props} {...props}
/> />
); );
@@ -123,7 +130,9 @@ function SheetDescription({
className, className,
...props ...props
}: React.ComponentProps<typeof SheetPrimitive.Description>) { }: React.ComponentProps<typeof SheetPrimitive.Description>) {
return <SheetPrimitive.Description className={cn('text-sm text-muted', className)} {...props} />; return (
<SheetPrimitive.Description className={cn('text-small text-muted', className)} {...props} />
);
} }
export { export {
+3 -2
View File
@@ -5,13 +5,14 @@ import { cn } from '@/lib/utils';
/** /**
* `bg-muted` would be wrong here: in this palette `muted` is the muted TEXT * `bg-muted` would be wrong here: in this palette `muted` is the muted TEXT
* colour, a mid grey that reads as a filled block rather than a placeholder. * colour, a mid grey that reads as a filled block rather than a placeholder.
* The placeholder surface is `surface-2`. * The placeholder ground is `surface-3` — one step above the card it sits in,
* so it reads on a surface in both themes (spec §9).
*/ */
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
return ( return (
<div <div
aria-hidden="true" aria-hidden="true"
className={cn('animate-pulse rounded-md bg-surface-2', className)} className={cn('animate-pulse rounded-md bg-surface-3', className)}
{...props} {...props}
/> />
); );
+31 -7
View File
@@ -4,9 +4,20 @@ import * as SliderPrimitive from '@radix-ui/react-slider';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
/** /**
* One thumb per value, not one hard-coded thumb: the reward editor drives this * The reward editor's control (spec §9). Track 6px on surface-3, range in the
* with a single weight today and a range tomorrow, and a single-thumb slider * text green (`accent-fg` equals `accent` in dark and is the only green that
* fed a two-value array silently drops the second value. * reads on paper), a 20px square-ish thumb with a 2px green edge and no
* shadow. The thumb element itself is a 44px square around the 20px knob, so
* the slider handle a phone measures is fingertip-sized and not the knob; the
* root's `py-3` keeps the row the same 44px band either side of it.
*
* "Physical" is the hover/active pair: the thumb tints on hover, grows a
* little under the pointer, and swaps to a grabbing cursor. Nothing here
* animates layout — `transform` only, clamped by the reduced-motion block.
*
* One thumb per value, not one hard-coded thumb: the editor drives this with
* a single weight today and a range tomorrow, and a single-thumb slider fed a
* two-value array silently drops the second value.
*/ */
function Slider({ function Slider({
className, className,
@@ -17,19 +28,32 @@ function Slider({
const thumbCount = (value ?? defaultValue ?? [0]).length; const thumbCount = (value ?? defaultValue ?? [0]).length;
return ( return (
<SliderPrimitive.Root <SliderPrimitive.Root
className={cn('relative flex w-full touch-none select-none items-center py-2', className)} className={cn(
'group relative flex w-full cursor-pointer touch-none select-none items-center py-3 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',
className,
)}
{...(value === undefined ? {} : { value })} {...(value === undefined ? {} : { value })}
{...(defaultValue === undefined ? {} : { defaultValue })} {...(defaultValue === undefined ? {} : { defaultValue })}
{...props} {...props}
> >
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-surface-2"> <SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-surface-3">
<SliderPrimitive.Range className="absolute h-full bg-brand" /> <SliderPrimitive.Range className="absolute h-full bg-accent-fg" />
</SliderPrimitive.Track> </SliderPrimitive.Track>
{Array.from({ length: thumbCount }, (_, i) => ( {Array.from({ length: thumbCount }, (_, i) => (
// The focusable thumb is a 44px square (the `role="slider"` element a
// finger actually lands on); the visible 20px knob is a child. Radix
// centres the thumb with a -50% translate, so the larger box changes
// nothing about where the knob sits. The hover/active growth and the
// focus ring are drawn on the knob, not on the invisible square.
<SliderPrimitive.Thumb <SliderPrimitive.Thumb
key={i} key={i}
className="block size-5 rounded-full border-2 border-brand bg-surface shadow-sm transition-colors duration-1 ease-enter disabled:pointer-events-none disabled:opacity-50" className="group/thumb flex size-11 cursor-grab items-center justify-center focus-visible:outline-none active:cursor-grabbing disabled:pointer-events-none"
>
<span
aria-hidden="true"
className="block size-5 rounded-md border-2 border-accent-fg bg-surface transition-[transform,background-color] duration-1 ease-enter group-hover/thumb:scale-110 group-hover/thumb:bg-accent-subtle group-focus-visible/thumb:ring-2 group-focus-visible/thumb:ring-ring group-focus-visible/thumb:ring-offset-2 group-focus-visible/thumb:ring-offset-bg group-active/thumb:scale-110 group-active/thumb:bg-accent-subtle"
/> />
</SliderPrimitive.Thumb>
))} ))}
</SliderPrimitive.Root> </SliderPrimitive.Root>
); );
+59 -7
View File
@@ -5,23 +5,67 @@ import { cn } from '@/lib/utils';
const Tabs = TabsPrimitive.Root; const Tabs = TabsPrimitive.Root;
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) { /**
* Two looks, one primitive (spec §9).
*
* `segment` — a bordered surface-2 rail with a surface-3 pill for the active
* trigger. The default, so every nested caller (Reasoning | Model call) keeps
* compiling unchanged. 36px triggers at lg, 44px below.
*
* `underline` — the site's primary navigation: a transparent full-width rail
* with a hairline underneath and a 2px `tab-underline` bar on the active
* trigger, flush with the rule. Triggers are 44px at every width; equal
* columns below sm so four tabs fit 390px without a scroller, natural width
* from sm. The two must never look alike — primary and secondary navigation
* are told apart by shape.
*
* The variant reaches the triggers by context, not by prop: Radix gives a
* trigger no handle on its list, and a `variant` prop on every trigger would
* be an API change that also lets one trigger disagree with its rail.
*/
type TabsVariant = 'segment' | 'underline';
const TabsVariantContext = React.createContext<TabsVariant>('segment');
const listStyles: Record<TabsVariant, string> = {
segment:
'inline-flex items-center justify-start gap-1 rounded-md border border-border bg-surface-2 p-1 text-muted',
underline: 'flex w-full items-stretch justify-start gap-0 border-b border-border text-muted',
};
const triggerStyles: Record<TabsVariant, string> = {
segment:
'min-h-11 rounded-md px-3 py-1.5 lg:min-h-9 hover:text-fg data-[state=active]:bg-surface-3 data-[state=active]:text-fg',
// `after:-bottom-px` drops the bar onto the rail's 1px rule so the two read
// as one line; the bar is always present (transparent) so activation only
// changes colour and never shifts layout.
underline:
'relative min-h-11 min-w-0 flex-1 px-2 text-[0.8125rem] sm:flex-none sm:px-3 sm:text-sm hover:text-fg data-[state=active]:text-fg after:pointer-events-none after:absolute after:inset-x-0 after:-bottom-px after:h-0.5 after:bg-transparent after:transition-colors after:duration-2 after:ease-enter after:content-[""] data-[state=active]:after:bg-tab-underline',
};
export interface TabsListProps extends React.ComponentProps<typeof TabsPrimitive.List> {
variant?: TabsVariant;
}
function TabsList({ className, variant = 'segment', ...props }: TabsListProps) {
return ( return (
<TabsVariantContext.Provider value={variant}>
<TabsPrimitive.List <TabsPrimitive.List
className={cn( data-variant={variant}
'inline-flex items-center justify-start gap-1 rounded-lg border border-border bg-surface-2 p-1 text-muted', className={cn(listStyles[variant], className)}
className,
)}
{...props} {...props}
/> />
</TabsVariantContext.Provider>
); );
} }
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) { function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
const variant = React.useContext(TabsVariantContext);
return ( return (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
className={cn( className={cn(
'tap inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium transition-colors duration-1 ease-enter disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-surface data-[state=active]:text-fg data-[state=active]:shadow-sm', 'inline-flex items-center justify-center whitespace-nowrap font-sans text-sm font-medium transition-colors duration-1 ease-enter focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg disabled:pointer-events-none disabled:opacity-50',
triggerStyles[variant],
className, className,
)} )}
{...props} {...props}
@@ -30,7 +74,15 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
} }
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) { function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
return <TabsPrimitive.Content className={cn('mt-4', className)} {...props} />; return (
<TabsPrimitive.Content
className={cn(
'mt-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg',
className,
)}
{...props}
/>
);
} }
export { Tabs, TabsList, TabsTrigger, TabsContent }; export { Tabs, TabsList, TabsTrigger, TabsContent };
+6 -1
View File
@@ -7,6 +7,11 @@ const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root; const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger; const TooltipTrigger = TooltipPrimitive.Trigger;
/**
* A tooltip is a sentence (a gloss, a unit), so it is sans 13 in prose ink and
* capped at 36ch — never mono, never uppercase. It is an overlay, so it is the
* one flat-looking thing allowed a shadow.
*/
function TooltipContent({ function TooltipContent({
className, className,
sideOffset = 6, sideOffset = 6,
@@ -19,7 +24,7 @@ function TooltipContent({
className={cn( className={cn(
// z-50 and not z-10: the tooltip has to clear the sticky header, // z-50 and not z-10: the tooltip has to clear the sticky header,
// which is itself z-50 and creates a stacking context of its own. // which is itself z-50 and creates a stacking context of its own.
'z-50 overflow-hidden rounded-md border border-border bg-surface px-2.5 py-1.5 text-xs text-fg shadow-md', 'z-50 max-w-[36ch] overflow-hidden rounded-md border border-border bg-surface px-2.5 py-1.5 font-sans text-small text-fg-2 shadow-overlay',
'duration-1 ease-enter animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', 'duration-1 ease-enter animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
'data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1', 'data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
className, className,
+67
View File
@@ -0,0 +1,67 @@
/**
* The four parts of an environment, as data.
*
* This is the site's one transferable piece of explanation. The landing strip
* (`DefinitionStrip`) and every demo page's four boxes (`EnvAnatomy`) both read
* these strings, so a visitor who learns the labels on the word game reads the
* fraud screen as "same machine, different grader". That only works if the
* four labels never change between surfaces — which is why they live here and
* nowhere else. Do not paraphrase them in a page.
*
* `key` is the field name on `DemoModule.anatomy`, so a demo's own text for
* each part is looked up with it.
*/
import type { DemoModule } from '@/lib/demo-kit/types';
export type AnatomyKey = keyof DemoModule['anatomy'];
export interface AnatomyTerm {
key: AnatomyKey;
/** Zero-padded ordinal, rendered in mono: `01``04`. */
index: string;
/** The label. Sans, 600. The same four words on every surface. */
kicker: string;
/** One line, for the landing strip. A sentence, not a fragment. */
gloss: string;
/** The question the box answers, for the full `EnvAnatomy`. */
question: string;
}
export const ANATOMY: readonly AnatomyTerm[] = [
{
key: 'task',
index: '01',
kicker: 'The task',
gloss: 'What the agent is asked to do.',
question: 'What is the agent asked to do?',
},
{
key: 'actions',
index: '02',
kicker: 'Legal actions',
gloss: 'What it is allowed to do.',
question: 'What is it allowed to do?',
},
{
key: 'grader',
index: '03',
kicker: 'The grader',
gloss: 'Code that computes, never a judge.',
question: 'Who decides whether it was good?',
},
{
key: 'score',
index: '04',
kicker: 'The score that moves',
gloss: 'The number you train against.',
question: 'What number does that produce?',
},
];
/** One term by its key. Throws at build time rather than rendering a blank box. */
export function anatomyTerm(key: AnatomyKey): AnatomyTerm {
const term = ANATOMY.find((t) => t.key === key);
if (!term) throw new Error(`No anatomy term "${key}".`);
return term;
}
+7
View File
@@ -59,6 +59,13 @@ export const trainingResult = {
method: 'SFT warm-up, then multi-turn RL with group-relative advantages (GRPO)', method: 'SFT warm-up, then multi-turn RL with group-relative advantages (GRPO)',
/** From the same README: 20 held-out words, 3 rollouts each. */ /** From the same README: 20 held-out words, 3 rollouts each. */
evalDescription: '20 held-out words the model never trained on, played 3 times each', evalDescription: '20 held-out words the model never trained on, played 3 times each',
/**
* Rendered wherever the number is, in these words. Home and Honesty both
* print it, and a caveat that appears on one page and not the other is the
* kind of thing a careful reader notices first.
*/
caveat:
'We quote the win rate only. The reward numbers in that write-up span versions of the environment and were never re-measured together.',
source: { source: {
label: 'prime-rl / examples / basic / wordle', label: 'prime-rl / examples / basic / wordle',
href: 'https://github.com/PrimeIntellect-ai/prime-rl/tree/main/examples/basic/wordle', href: 'https://github.com/PrimeIntellect-ai/prime-rl/tree/main/examples/basic/wordle',
+161
View File
@@ -0,0 +1,161 @@
/**
* Every piece of jargon the site uses, with the one-sentence gloss a visitor
* gets when they hover or tap it.
*
* One list, two audiences. The five `core` terms are the vocabulary of the
* idea itself and are rendered as a set: in the header's "How it works" panel
* and on the landing page. The extended terms are glossed inline, one at a
* time, by `Term` wherever a page or the shell first uses the word.
*
* Glosses are sentences a board member can repeat. They say what the thing IS
* before they say what it is for, and they never lean on another term from
* this list to do it.
*/
export type GlossaryKey =
| 'environment'
| 'rollout'
| 'reward'
| 'harness'
| 'held-out'
| 'seed'
| 'arm'
| 'objective'
| 'counterweight'
| 'gate'
| 'intervention'
| 'verify'
| 'taskset'
| 'verifiers'
| 'tokens'
| 'latency';
export interface GlossaryEntry {
key: GlossaryKey;
/** As it is written in running text. Sentence case. */
term: string;
gloss: string;
/** One of the five rendered as a set. */
core: boolean;
}
export const GLOSSARY: readonly GlossaryEntry[] = [
{
key: 'environment',
term: 'Environment',
gloss:
'The task, the legal moves and the grader, packaged so anyone can install and run it.',
core: true,
},
{
key: 'rollout',
term: 'Rollout',
gloss: 'One episode. The model acts, the environment answers, and every turn is recorded.',
core: true,
},
{
key: 'reward',
term: 'Reward',
gloss: 'The number the run is scored on. You write it, so you decide what "good" means.',
core: true,
},
{
key: 'harness',
term: 'Harness',
gloss: 'The runner that plays a taskset against a model and keeps the receipts.',
core: true,
},
{
key: 'held-out',
term: 'Held-out grading',
gloss:
'Scored on problems the model has never seen, which is the only way the score means anything.',
core: true,
},
{
key: 'seed',
term: 'Seed',
gloss:
'The number that fixes the board. The same seed reproduces the same hidden word, so two agents can be compared on the same puzzle.',
core: false,
},
{
key: 'arm',
term: 'Arm',
gloss:
'One model under one setting, with its recorded runs. The ranking compares arms, not companies.',
core: false,
},
{
key: 'objective',
term: 'Objective',
gloss:
'What the task is for: the term the agent is paid to maximise. Every other term in the reward exists to keep this one honest.',
core: false,
},
{
key: 'counterweight',
term: 'Counterweight',
gloss:
'The term that takes points away. It pulls against the objective so the crude way of winning does not pay.',
core: false,
},
{
key: 'gate',
term: 'Gate',
gloss:
'A term every good attempt also scores full marks on. It rules out cheating; it does not rank.',
core: false,
},
{
key: 'intervention',
term: 'Intervention',
gloss:
'Something done to a run that was not training: a changed prompt, a hint. Always labelled, never presented as a training result.',
core: false,
},
{
key: 'verify',
term: 'Verify',
gloss:
'Re-run the grader in your browser over the recorded moves and compare the answer to the number shipped in the file.',
core: false,
},
{
key: 'taskset',
term: 'Taskset',
gloss: 'The list of problems an environment draws from. On the word game, the hidden words.',
core: false,
},
{
key: 'verifiers',
term: 'verifiers',
gloss:
'Prime Intellects open-source library for writing environments. Every environment on this site is built with it.',
core: false,
},
{
key: 'tokens',
term: 'Tokens',
gloss:
'The units a model reads and writes. The counts here are what the recording measured, not a price.',
core: false,
},
{
key: 'latency',
term: 'Latency',
gloss:
'Wall-clock time per model call, as recorded on that day against that endpoint. Not a benchmark.',
core: false,
},
];
/** The five rendered as a set, in the order they are taught. */
export const CORE_TERMS: readonly GlossaryEntry[] = GLOSSARY.filter((entry) => entry.core);
/** One entry by key. Throws at build time rather than rendering an empty tooltip. */
export function glossaryEntry(key: GlossaryKey): GlossaryEntry {
const entry = GLOSSARY.find((e) => e.key === key);
if (!entry) throw new Error(`No glossary entry "${key}".`);
return entry;
}
+61
View File
@@ -4,6 +4,10 @@
* This is the ONLY place the marketing pages touch the registry. Home, Gallery * This is the ONLY place the marketing pages touch the registry. Home, Gallery
* and Vertical all read demos through here, so when the registry's export name * and Vertical all read demos through here, so when the registry's export name
* or path moves, it moves in one line instead of in four pages. * or path moves, it moves in one line instead of in four pages.
*
* Every count a page prints comes from a list in this file. A typed "twelve"
* on a site whose whole argument is that its claims are checkable goes stale
* the first time a demo ships.
*/ */
import { demos } from '@/lib/demo-kit/registry'; import { demos } from '@/lib/demo-kit/registry';
@@ -38,6 +42,8 @@ export const allDemos: readonly DemoMeta[] = demos;
export const liveDemos: readonly DemoMeta[] = allDemos.filter((d) => d.status === 'live'); export const liveDemos: readonly DemoMeta[] = allDemos.filter((d) => d.status === 'live');
export const specDemos: readonly DemoMeta[] = allDemos.filter((d) => d.status === 'spec');
/** /**
* The demo the site leads with. `reference` is the hello-world vertical, and * The demo the site leads with. `reference` is the hello-world vertical, and
* the first live one in it is the front door; if there is none yet, any live * the first live one in it is the front door; if there is none yet, any live
@@ -74,3 +80,58 @@ export function verticalForDemo(demo: DemoMeta): VerticalEntry | undefined {
/** The lineup, in the order we would build it. */ /** The lineup, in the order we would build it. */
export const lineup: readonly VerticalEntry[] = [...VERTICALS].sort((a, b) => a.rank - b.rank); export const lineup: readonly VerticalEntry[] = [...VERTICALS].sort((a, b) => a.rank - b.rank);
/**
* Lineup entries with nothing behind them: no live demo and no published spec
* demo. This is the honest count for "environments that do not exist", and it
* shrinks by one every time a proposal is built or specified.
*/
export const unbuiltLineup: readonly VerticalEntry[] = lineup.filter(
(v) => v.key === null || demosForVertical(v.key).length === 0,
);
/* ---------------------------------------------------------------- format */
/** `4` → `04`. Rank numerals and counts in mono labels. */
export function pad2(n: number): string {
return String(Math.max(0, Math.trunc(n))).padStart(2, '0');
}
const WORDS = [
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'ten',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
'sixteen',
'seventeen',
'eighteen',
'nineteen',
'twenty',
] as const;
/**
* A count as a word inside a sentence: "Two of these are built". Digits past
* twenty, because "twenty-three of these" is where a word stops reading.
*/
export function countWord(n: number): string {
const whole = Math.max(0, Math.trunc(n));
return WORDS[whole] ?? String(whole);
}
/** Capitalised, for a sentence that starts with the number. */
export function CountWord(n: number): string {
const word = countWord(n);
return word.charAt(0).toUpperCase() + word.slice(1);
}
+94 -22
View File
@@ -6,6 +6,16 @@
* characters of Tailwind copied into five pages and drifting apart. Strings, * characters of Tailwind copied into five pages and drifting apart. Strings,
* not components, so nothing here reaches for React. * not components, so nothing here reaches for React.
* *
* Two rules from the design spec are enforced by what is and is not in here:
*
* 1. Data is mono, sentences are sans. The `label`, `chip*`, `monoData` and
* `codeBlock` styles carry `font-mono`; nothing with a verb in it does.
* 2. Nothing that explains is smaller than 16px. `prose` is the floor, and
* `small` / `caption` exist for metadata, never for a paragraph.
*
* Weight ceiling is 600 — the type scale in tailwind.config.js bakes weight
* into each size, so `font-bold` never appears on a page.
*
* Horizontal padding is written against the safe-area tokens rather than a * Horizontal padding is written against the safe-area tokens rather than a
* flat value: on a notched phone in landscape, a flat `px-5` puts the first * flat value: on a notched phone in landscape, a flat `px-5` puts the first
* character of every heading under the rounded corner. * character of every heading under the rounded corner.
@@ -15,47 +25,109 @@
export const shell = export const shell =
'mx-auto w-full max-w-canvas pl-[max(1.25rem,var(--safe-left))] pr-[max(1.25rem,var(--safe-right))] sm:pl-[max(2rem,var(--safe-left))] sm:pr-[max(2rem,var(--safe-right))]'; 'mx-auto w-full max-w-canvas pl-[max(1.25rem,var(--safe-left))] pr-[max(1.25rem,var(--safe-right))] sm:pl-[max(2rem,var(--safe-left))] sm:pr-[max(2rem,var(--safe-right))]';
/** Vertical rhythm between top-level sections. */ /** Vertical rhythm between top-level sections. The language's density, not an essay's. */
export const section = 'py-12 sm:py-16'; export const section = 'py-12 sm:py-16';
/** Small uppercase label above a section heading. */ /* ------------------------------------------------------------------- type */
export const eyebrow = 'text-xs font-semibold uppercase tracking-[0.14em] text-muted';
export const h1 = /** Mono small-caps section label. The same style as every chip and table head. */
'text-[2rem] leading-[1.08] font-extrabold tracking-tight text-fg sm:text-5xl lg:text-6xl'; export const label = 'font-mono text-label uppercase text-muted';
export const h2 = 'text-2xl font-bold tracking-tight text-fg sm:text-3xl'; /** Kept under its old name: the shell and the router read `eyebrow`. */
export const eyebrow = label;
export const h3 = 'text-base font-semibold text-fg'; /** The landing h1 only. 36 → 48 → 56. */
export const display = 'text-display text-fg lg:text-display-lg xl:text-display-xl';
export const lede = 'text-lg leading-relaxed text-muted sm:text-xl'; /** Every other page's h1. 28 → 32. */
export const h1 = 'text-h1 text-fg lg:text-h1-lg';
export const prose = 'text-[0.9375rem] leading-relaxed text-muted'; export const h2 = 'text-h2 text-fg lg:text-h2-lg';
/** Filled call to action. 44px tall via `.tap`. */ export const h3 = 'text-h3 text-fg';
export const btnPrimary =
'tap inline-flex items-center justify-center gap-2 rounded-lg bg-brand px-5 py-2.5 text-sm font-semibold text-accent-on transition-colors duration-2 ease-enter hover:bg-accent-fg'; /** Under an h1. Prose ink, 72ch. */
export const lede = 'max-w-measure-wide text-lede text-fg-2 lg:text-lede-lg';
/** The education floor: 16/1.6, prose ink, 62ch. Never smaller. */
export const prose = 'max-w-measure text-prose text-fg-2';
/** Metadata beside a thing, never a paragraph. */
export const small = 'text-small text-muted';
export const caption = 'text-caption text-muted';
/** A number or an identifier in running chrome. */
export const monoData = 'font-mono text-mono-data';
/* ---------------------------------------------------------------- buttons */
const btnBase =
'tap inline-flex items-center justify-center gap-2 rounded-md px-5 py-2.5 text-ui font-medium transition-colors duration-1 ease-enter';
/**
* Filled call to action. The 1px `accent-edge` border is what makes it a
* control on a light projector, where the fill is 1.47:1 against white. The
* neon fill has no darker sibling on paper, so light hovers to the subtle
* ground with accent ink; dark dims the fill.
*/
export const btnPrimary = `${btnBase} border border-accent-edge bg-brand text-accent-on hover:bg-accent-subtle hover:text-accent-fg dark:hover:bg-brand/90 dark:hover:text-accent-on`;
/** Outlined call to action, for the second-choice action beside a primary. */ /** Outlined call to action, for the second-choice action beside a primary. */
export const btnSecondary = export const btnSecondary = `${btnBase} border border-border-strong bg-surface text-fg hover:bg-surface-2`;
'tap inline-flex items-center justify-center gap-2 rounded-lg border border-border bg-surface px-5 py-2.5 text-sm font-semibold text-fg transition-colors duration-2 ease-enter hover:bg-surface-2';
/** Inline link inside running text. Underlined, because colour alone is not a link. */ /** Inline link inside running text. Underlined, because colour alone is not a link. */
export const link = export const link =
'font-medium text-accent-fg underline decoration-accent-fg/40 underline-offset-4 transition-colors duration-1 ease-enter hover:decoration-accent-fg'; 'font-medium text-accent-fg underline decoration-border-strong underline-offset-4 transition-colors duration-1 ease-enter hover:decoration-accent-fg';
/** Small pill. Neutral by default; pass a colour class after it. */ /** A quiet navigation link — back, next, "all twelve". 44px tall. */
export const pill = export const navLink =
'inline-flex items-center gap-1.5 rounded-md border border-border bg-surface-2 px-2 py-1 text-xs font-medium text-muted'; 'tap inline-flex items-center gap-1.5 text-ui font-medium text-muted transition-colors duration-1 ease-enter hover:text-fg';
/* ------------------------------------------------------------------ chips */
/**
* The chip family. One shape, mono 11 uppercase, 4px radius. A visitor learns
* after the second one that this shape means "a thing we are telling you about
* the data", so no chip on a page is ever sans or rounder than this.
*/
const chipBase =
'inline-flex items-center gap-1 rounded-sm border px-1.5 py-0.5 font-mono text-label uppercase';
/** Outline: SPEC, PROPOSED BY PIG-DEMO, OBJECTIVE, RANK. */
export const chipOutline = `${chipBase} border-border-strong text-muted`;
/** Muted: RECORDED RUN, NOT IN THE FIRST SET, GATE. */
export const chipMuted = `${chipBase} border-transparent bg-surface-3 text-muted`;
/** Solid: LIVE. Pass `rounded-full` for a status pill. */
export const chipSolid = `${chipBase} border-accent-edge bg-brand text-accent-on`;
/** Warning: EDITED, COUNTERWEIGHT, WHERE THIS STOPS. */
export const chipWarning = `${chipBase} border-transparent bg-warning-subtle text-warning`;
/** Kept under the old names for the two callers outside this lane. */
export const pill = chipOutline;
/** The standing "these are our proposals" badge. */ /** The standing "these are our proposals" badge. */
export const proposalPill = export const proposalPill = chipOutline;
'inline-flex items-center gap-1.5 rounded-md border border-border bg-surface-2 px-2 py-1 text-[0.6875rem] font-semibold uppercase tracking-wider text-muted';
/* ------------------------------------------------------------- surfaces */
/** A card that is also a link: the whole rectangle is the target. */ /** A card that is also a link: the whole rectangle is the target. */
export const cardLink = export const cardLink =
'card group flex flex-col p-5 transition-colors duration-2 ease-enter hover:border-brand/40 hover:bg-surface-2'; 'card group flex flex-col p-4 transition-colors duration-1 ease-enter hover:border-border-strong sm:p-5';
/** A list row that is a link. 64px on a phone, 56px from lg. */
export const rowLink =
'flex min-h-16 items-center gap-3 py-3 transition-colors duration-1 ease-enter hover:bg-surface-2 lg:min-h-14';
/** Monospaced command block. Scrolls itself rather than the page. */ /** Monospaced command block. Scrolls itself rather than the page. */
export const codeBlock = export const codeBlock =
'nums overflow-x-auto rounded-lg border border-border bg-surface-2 p-3 font-mono text-[0.8125rem] leading-relaxed text-fg'; 'overflow-x-auto rounded-md border border-border bg-surface-3 p-3 font-mono text-code text-fg [font-feature-settings:"liga"_0]';
/** The buyer's question, as a quiet quote. 2px rule, no italics. */
export const quote = 'max-w-[60ch] border-l-2 border-border-strong pl-4 text-prose text-fg-2';
/** A hairline between blocks. */
export const rule = 'border-t border-border';
+66 -33
View File
@@ -1,4 +1,4 @@
import { useId, useMemo, useState } from 'react'; import { useId, useMemo, useState, type ReactNode } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -24,17 +24,29 @@ type Panel = 'none' | 'close' | 'escalate';
const price = (minutes: number) => `+${hours(minutes)} h`; const price = (minutes: number) => `+${hours(minutes)} h`;
function Section({ title, children }: { title: string; children: React.ReactNode }) { /** A price or a "done" beside a verb: mono, muted, the way a cost is printed. */
function Price({ children }: { children: ReactNode }) {
return <span className="font-mono text-caption font-normal text-muted">{children}</span>;
}
function Section({ title, children }: { title: string; children: ReactNode }) {
return ( return (
<fieldset className="min-w-0 rounded-lg border border-border p-3"> <fieldset className="min-w-0 rounded-control border border-border p-3">
<legend className="px-1 text-xs font-semibold uppercase tracking-wide text-muted">{title}</legend> <legend className="px-1 font-mono text-label uppercase text-muted">{title}</legend>
{children} {children}
</fieldset> </fieldset>
); );
} }
/**
* A native select. The border is the only affordance a select has on this
* ground, so it is `border-strong` (≥3:1 in both themes) rather than the
* decorative hairline; the focus ring is the site's.
*/
const SELECT = const SELECT =
'tap min-w-0 rounded-md border border-border bg-surface px-2 text-sm text-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand disabled:opacity-50'; 'min-h-11 min-w-0 rounded-control border border-border-strong bg-surface px-2.5 font-sans text-ui text-fg ' +
'transition-colors duration-1 hover:bg-surface-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg ' +
'disabled:cursor-not-allowed disabled:opacity-50';
/** The citable ids, grouped the way the screen groups them. */ /** The citable ids, grouped the way the screen groups them. */
function groupShown(shown: readonly string[]) { function groupShown(shown: readonly string[]) {
@@ -132,10 +144,10 @@ export function Controls({
: `Hours spent so far: ${hours(state.minutesSpent)}. ${left} ${left === 1 ? 'reply' : 'replies'} left.`; : `Hours spent so far: ${hours(state.minutesSpent)}. ${left} ${left === 1 ? 'reply' : 'replies'} left.`;
return ( return (
<div className="flex flex-col gap-3" style={{ paddingBottom: 'max(0px, var(--safe-bottom))' }}> <div className="flex flex-col gap-3 pb-[max(1rem,var(--safe-bottom))]">
{/* The board is a group of tables; the outcome of every click is said {/* The board is a group of tables; the outcome of every click is said
here in words, where it announces itself. */} here in words, where it announces itself. */}
<p aria-live="polite" className="text-sm text-muted"> <p aria-live="polite" className="font-sans text-ui text-fg-2">
{status} {status}
</p> </p>
@@ -147,7 +159,7 @@ export function Controls({
</label> </label>
<select <select
id={`${ids}-month`} id={`${ids}-month`}
className={cn(SELECT, 'min-w-[7rem] flex-1')} className={cn(SELECT, 'min-w-[7rem] flex-1 font-mono text-mono-data')}
value={month} value={month}
disabled={done || monthsLeft.length === 0} disabled={done || monthsLeft.length === 0}
onChange={(event) => setMonth(event.target.value)} onChange={(event) => setMonth(event.target.value)}
@@ -163,13 +175,14 @@ export function Controls({
type="button" type="button"
variant="outline" variant="outline"
size="touch" size="touch"
className="shrink-0"
disabled={done || month === ''} disabled={done || month === ''}
onClick={() => { onClick={() => {
send({ action: 'lookup', what: 'history', month }); send({ action: 'lookup', what: 'history', month });
setMonth(''); setMonth('');
}} }}
> >
History <span className="nums text-muted">{price(PRICES.history)}</span> History <Price>{price(PRICES.history)}</Price>
</Button> </Button>
</div> </div>
@@ -179,7 +192,7 @@ export function Controls({
</label> </label>
<select <select
id={`${ids}-cp`} id={`${ids}-cp`}
className={cn(SELECT, 'min-w-[7rem] flex-1')} className={cn(SELECT, 'min-w-[7rem] flex-1 font-mono text-mono-data')}
value={cp} value={cp}
disabled={done || cpsLeft.length === 0} disabled={done || cpsLeft.length === 0}
onChange={(event) => setCp(event.target.value)} onChange={(event) => setCp(event.target.value)}
@@ -195,17 +208,19 @@ export function Controls({
type="button" type="button"
variant="outline" variant="outline"
size="touch" size="touch"
className="shrink-0"
disabled={done || cp === ''} disabled={done || cp === ''}
onClick={() => { onClick={() => {
send({ action: 'lookup', what: 'counterparty', id: cp }); send({ action: 'lookup', what: 'counterparty', id: cp });
setCp(''); setCp('');
}} }}
> >
Counterparty <span className="nums text-muted">{price(PRICES.counterparty)}</span> Counterparty <Price>{price(PRICES.counterparty)}</Price>
</Button> </Button>
</div> </div>
<div className="flex flex-wrap gap-2"> {/* Two per row on a phone, so the pair reads as the pair it is. */}
<div className="grid grid-cols-2 gap-2">
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
@@ -213,7 +228,7 @@ export function Controls({
disabled={done || priorsMade} disabled={done || priorsMade}
onClick={() => send({ action: 'lookup', what: 'prior_alerts' })} onClick={() => send({ action: 'lookup', what: 'prior_alerts' })}
> >
Prior alerts <span className="nums text-muted">{priorsMade ? 'done' : price(LOOKUP_MINUTES)}</span> Prior alerts <Price>{priorsMade ? 'done' : price(LOOKUP_MINUTES)}</Price>
</Button> </Button>
<Button <Button
type="button" type="button"
@@ -222,18 +237,21 @@ export function Controls({
disabled={done || docsMade} disabled={done || docsMade}
onClick={() => send({ action: 'lookup', what: 'documents' })} onClick={() => send({ action: 'lookup', what: 'documents' })}
> >
Documents <span className="nums text-muted">{docsMade ? 'done' : price(DOC_MINUTES)}</span> Documents <Price>{docsMade ? 'done' : price(DOC_MINUTES)}</Price>
</Button> </Button>
</div> </div>
</div> </div>
</Section> </Section>
<Section title="Disposition — ends the episode"> <Section title="Disposition — ends the episode">
<div className="flex flex-wrap gap-2" role="group" aria-label="Choose a disposition"> {/* A toggle pair: the pressed half is the one green fill in the
controls, with the accent edge that keeps it a control on paper. */}
<div className="grid grid-cols-2 gap-2" role="group" aria-label="Choose a disposition">
<Button <Button
type="button" type="button"
size="touch" size="touch"
variant={panel === 'close' ? 'default' : 'outline'} variant={panel === 'close' ? 'default' : 'outline'}
className={cn(panel === 'close' && 'border border-accent-edge shadow-none')}
aria-pressed={panel === 'close'} aria-pressed={panel === 'close'}
disabled={done} disabled={done}
onClick={() => setPanel(panel === 'close' ? 'none' : 'close')} onClick={() => setPanel(panel === 'close' ? 'none' : 'close')}
@@ -244,6 +262,7 @@ export function Controls({
type="button" type="button"
size="touch" size="touch"
variant={panel === 'escalate' ? 'default' : 'outline'} variant={panel === 'escalate' ? 'default' : 'outline'}
className={cn(panel === 'escalate' && 'border border-accent-edge shadow-none')}
aria-pressed={panel === 'escalate'} aria-pressed={panel === 'escalate'}
disabled={done} disabled={done}
onClick={() => setPanel(panel === 'escalate' ? 'none' : 'escalate')} onClick={() => setPanel(panel === 'escalate' ? 'none' : 'escalate')}
@@ -260,8 +279,8 @@ export function Controls({
submitDisposition(); submitDisposition();
}} }}
> >
<label className="flex flex-col gap-1 text-xs text-muted"> <label className="flex flex-col gap-1">
{panel === 'close' ? 'Reason' : 'Typology'} <span className="font-mono text-label uppercase text-muted">{panel === 'close' ? 'Reason' : 'Typology'}</span>
{panel === 'close' ? ( {panel === 'close' ? (
<select className={SELECT} value={reason} onChange={(event) => setReason(event.target.value as CloseReason)}> <select className={SELECT} value={reason} onChange={(event) => setReason(event.target.value as CloseReason)}>
{CLOSE_REASONS.map((r) => ( {CLOSE_REASONS.map((r) => (
@@ -282,13 +301,13 @@ export function Controls({
</label> </label>
<fieldset className="min-w-0"> <fieldset className="min-w-0">
<legend className="mb-1 text-xs text-muted"> <legend className="mb-1 font-sans text-small text-fg-2">
Cite the facts that decide it only ids you have been shown Cite the facts that decide it only ids you have been shown
</legend> </legend>
<div className="flex max-h-56 flex-col gap-2 overflow-y-auto rounded-md border border-border p-2"> <div className="flex max-h-56 flex-col gap-2.5 overflow-y-auto overscroll-contain rounded-control border border-border-strong p-2">
{groups.map((group) => ( {groups.map((group) => (
<div key={group.label}> <div key={group.label}>
<p className="mb-1 text-[0.65rem] font-semibold uppercase tracking-wide text-muted">{group.label}</p> <p className="mb-1 font-mono text-label uppercase text-muted">{group.label}</p>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{group.ids.map((id) => { {group.ids.map((id) => {
const on = cites.includes(id); const on = cites.includes(id);
@@ -296,12 +315,16 @@ export function Controls({
<label <label
key={id} key={id}
className={cn( className={cn(
'inline-flex cursor-pointer select-none items-center gap-1 rounded-md border px-1.5 py-1 font-mono text-[0.7rem] transition-colors duration-1', 'inline-flex cursor-pointer select-none items-center gap-1 rounded-tile border px-1.5 py-1.5 font-mono text-label tracking-normal transition-colors duration-1',
on ? 'border-brand bg-accent-subtle text-fg' : 'border-border text-muted hover:bg-surface-2', 'has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2 has-[:focus-visible]:ring-offset-bg',
on
? 'border-accent-edge bg-accent-subtle text-fg'
: 'border-border-strong text-fg-2 hover:bg-surface-2',
)} )}
title={id.startsWith('kyc.') ? fieldLabel(id) : id} title={id.startsWith('kyc.') ? fieldLabel(id) : id}
> >
<input type="checkbox" className="sr-only" checked={on} onChange={() => toggleCite(id)} /> <input type="checkbox" className="sr-only" checked={on} onChange={() => toggleCite(id)} />
{on ? <span aria-hidden="true"></span> : null}
{id} {id}
</label> </label>
); );
@@ -312,26 +335,31 @@ export function Controls({
</div> </div>
</fieldset> </fieldset>
<label className="flex flex-col gap-1 text-xs text-muted"> <label className="flex flex-col gap-1">
<span> <span className="flex flex-wrap items-center gap-2">
{panel === 'close' ? 'Note' : 'Narrative'}{' '} <span className="font-mono text-label uppercase text-muted">{panel === 'close' ? 'Note' : 'Narrative'}</span>
<span className="rounded border border-border px-1 py-px text-[0.65rem] uppercase tracking-wide"> <span className="rounded-tile border border-border-strong px-1.5 py-px font-mono text-label uppercase text-muted">
displayed, never scored displayed, never scored
</span> </span>
</span> </span>
<textarea <textarea
className="min-h-16 rounded-md border border-border bg-surface px-2 py-1 text-sm text-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand" className="min-h-16 rounded-control border border-border-strong bg-surface px-2.5 py-1.5 font-sans text-ui text-fg placeholder:text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-bg"
value={prose} value={prose}
onChange={(event) => setProse(event.target.value)} onChange={(event) => setProse(event.target.value)}
placeholder={panel === 'close' ? 'Why this is consistent with the file…' : 'What the pattern is and why it is reportable…'} placeholder={panel === 'close' ? 'Why this is consistent with the file…' : 'What the pattern is and why it is reportable…'}
/> />
</label> </label>
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-3">
<Button type="submit" size="touch" disabled={cites.length === 0}> <Button
type="submit"
size="touch"
className="w-full border border-accent-edge shadow-none sm:w-auto"
disabled={cites.length === 0}
>
{panel === 'close' ? 'Close the alert' : 'Escalate for filing'} {panel === 'close' ? 'Close the alert' : 'Escalate for filing'}
</Button> </Button>
<span className="nums text-xs text-muted"> <span className="font-mono text-caption text-muted">
{cites.length} cited · {price(TURN_MINUTES)} {cites.length} cited · {price(TURN_MINUTES)}
</span> </span>
</div> </div>
@@ -340,9 +368,14 @@ export function Controls({
</Section> </Section>
{lastReply ? ( {lastReply ? (
<details className="text-xs text-muted"> <details className="group font-mono text-label uppercase text-muted">
<summary className="cursor-pointer">The reply the engine received</summary> <summary className="min-h-11 cursor-pointer list-none py-2 [&::-webkit-details-marker]:hidden">
<pre className="mt-1 overflow-x-auto rounded-md bg-surface-2 p-2 font-mono text-[0.7rem] text-fg [contain:inline-size]"> <span aria-hidden="true" className="mr-1.5 inline-block transition-transform duration-1 group-open:rotate-90">
</span>
The reply the engine received
</summary>
<pre className="overflow-x-auto rounded-control border border-border bg-surface-3 p-2 font-mono text-code normal-case tracking-normal text-fg [contain:inline-size]">
{lastReply} {lastReply}
</pre> </pre>
</details> </details>
+1 -1
View File
@@ -14,7 +14,7 @@ export default defineMeta({
tagline: tagline:
'Work one transaction-monitoring alert against the customers file and twelve months of history, then close it or escalate it for a filing.', 'Work one transaction-monitoring alert against the customers file and twelve months of history, then close it or escalate it for a filing.',
vertical: 'financial-crime', vertical: 'financial-crime',
status: 'spec', status: 'live',
order: 0, order: 0,
icon: 'Siren', icon: 'Siren',
persona: 'BSA / AML Officer', persona: 'BSA / AML Officer',
+260 -161
View File
@@ -11,10 +11,15 @@ import type { Lookup, Reveal, Screen, Transaction, TriageState } from './types';
* playing, the recorded replay, and the timeline thumbnail — because three * playing, the recorded replay, and the timeline thumbnail — because three
* near-identical boards is how they drift. * near-identical boards is how they drift.
* *
* It is an instrument, not a document: dense, every id and number in mono,
* every sentence in sans. It draws no outer chrome — the shell's card supplies
* the border, ground and padding — and nothing inside it is a link or a
* button; the actions live in the controls beside it.
*
* Two things this board must do that a game board does not: * Two things this board must do that a game board does not:
* *
* · Say, on the board itself, that the data is synthetic. Not in a footnote. * · Say, on the board itself, that the data is synthetic. Not in a footnote.
* The label is the first row of every render. * The label is the first row of every render, the thumbnail included.
* · Show the answer only once the episode is over. `state.reveal` is null * · Show the answer only once the episode is over. `state.reveal` is null
* until then, so the surface cannot leak the label even by accident. * until then, so the surface cannot leak the label even by accident.
* *
@@ -27,12 +32,17 @@ const MAX_TURNS = 8;
/* ---------------------------------------------------------------- pieces */ /* ---------------------------------------------------------------- pieces */
/**
* Warning ink on the warning ground: 6.08 light / 9.12 dark (index.css). The
* dashed edge is the one place on the site a border is dashed, so the shape
* itself says "a note about the data".
*/
function SyntheticLabel({ compact }: { compact?: boolean }) { function SyntheticLabel({ compact }: { compact?: boolean }) {
return ( return (
<p <p
className={cn( className={cn(
'rounded-md border border-dashed border-warning/60 bg-warning/5 font-semibold uppercase tracking-wide text-warning', 'rounded-tile border border-dashed border-warning bg-warning-subtle font-mono font-medium uppercase tracking-label text-warning',
compact ? 'px-1 py-0.5 text-[7px] leading-tight' : 'px-2 py-1 text-[0.65rem]', compact ? 'px-1 py-0.5 text-[7px] leading-tight' : 'px-2 py-1 text-label',
)} )}
> >
Synthetic data no real customer, account or transaction Synthetic data no real customer, account or transaction
@@ -40,20 +50,37 @@ function SyntheticLabel({ compact }: { compact?: boolean }) {
); );
} }
/** An identifier. `fg` for something the analyst can cite, `muted` for a reference to one. */
function Id({ children, tone = 'muted' }: { children: ReactNode; tone?: 'muted' | 'fg' }) { function Id({ children, tone = 'muted' }: { children: ReactNode; tone?: 'muted' | 'fg' }) {
return <code className={cn('font-mono', tone === 'muted' ? 'text-muted' : 'text-fg')}>{children}</code>;
}
/** The mono 11 uppercase label, the same style as the shell's chrome. */
function Label({ children, className }: { children: ReactNode; className?: string }) {
return <span className={cn('font-mono text-label uppercase text-muted', className)}>{children}</span>;
}
/** A 4px outline chip for a state word. Never colour alone: the word is the signal. */
function Chip({ children, className, title }: { children: ReactNode; className?: string; title?: string }) {
return ( return (
<code className={cn('nums font-mono text-[0.8em]', tone === 'muted' ? 'text-muted' : 'text-fg')}> <span
className={cn(
'inline-flex items-center gap-1 whitespace-nowrap rounded-tile border border-border-strong px-1.5 py-px font-mono text-label uppercase',
className,
)}
title={title}
>
{children} {children}
</code> </span>
); );
} }
function Block({ title, aside, children }: { title: string; aside?: ReactNode; children: ReactNode }) { function Block({ title, aside, children }: { title: string; aside?: ReactNode; children: ReactNode }) {
return ( return (
<section className="rounded-lg border border-border bg-surface p-3"> <section className="min-w-0 rounded-control border border-border bg-surface p-3">
<header className="mb-2 flex items-baseline justify-between gap-2"> <header className="mb-2 flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted">{title}</h4> <h4 className="font-mono text-label uppercase text-muted">{title}</h4>
{aside ? <div className="text-xs text-muted">{aside}</div> : null} {aside ? <div className="min-w-0 font-mono text-caption text-muted">{aside}</div> : null}
</header> </header>
{children} {children}
</section> </section>
@@ -70,31 +97,43 @@ function Scroll({ children }: { children: ReactNode }) {
return <div className="overflow-x-auto [contain:inline-size]">{children}</div>; return <div className="overflow-x-auto [contain:inline-size]">{children}</div>;
} }
/** Table styling shared by every table on the screen: mono 12, heads mono 11 on surface-3. */
const TABLE = 'w-full border-collapse font-mono text-caption';
const TH = 'bg-surface-3 px-2 py-1 text-left font-mono text-label font-medium uppercase text-muted first:rounded-l-tile last:rounded-r-tile';
const TD = 'px-2 py-1 align-baseline';
const NUM = 'whitespace-nowrap text-right';
function TransactionTable({ rows, currency }: { rows: Transaction[]; currency: string }) { function TransactionTable({ rows, currency }: { rows: Transaction[]; currency: string }) {
if (rows.length === 0) return <p className="text-xs text-muted">No transactions.</p>; if (rows.length === 0) return <p className="font-sans text-small text-muted">No transactions.</p>;
return ( return (
<Scroll> <Scroll>
<table className="w-full min-w-[26rem] text-xs"> <table className={cn(TABLE, 'min-w-[26rem]')}>
<thead className="text-left text-muted"> <thead>
<tr> <tr>
<th className="pb-1 pr-2 font-medium">id</th> <th className={TH}>id</th>
<th className="pb-1 pr-2 font-medium">date</th> <th className={cn(TH, 'text-right')}>date</th>
<th className="pb-1 pr-2 font-medium">channel</th> <th className={TH}>channel</th>
<th className="pb-1 pr-2 text-right font-medium">amount</th> <th className={cn(TH, 'text-right')}>amount</th>
<th className="pb-1 font-medium">where</th> <th className={TH}>where</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{rows.map((t) => ( {rows.map((t) => (
<tr key={t.id} className="border-t border-border/60"> <tr key={t.id} className="border-t border-border">
<td className="py-0.5 pr-2"> <td className={TD}>
<Id tone="fg">{t.id}</Id> <Id tone="fg">{t.id}</Id>
</td> </td>
<td className="nums py-0.5 pr-2 whitespace-nowrap">{t.date}</td> <td className={cn(TD, NUM)}>{t.date}</td>
<td className="py-0.5 pr-2 whitespace-nowrap">{channelLabel(t.channel)}</td> <td className={cn(TD, 'whitespace-nowrap font-sans text-small')}>{channelLabel(t.channel)}</td>
<td className="nums py-0.5 pr-2 text-right whitespace-nowrap">{money(t.amount, currency)}</td> <td className={cn(TD, NUM, 'text-fg')}>{money(t.amount, currency)}</td>
<td className="py-0.5 whitespace-nowrap"> <td className={cn(TD, 'whitespace-nowrap')}>
{t.branch ? `branch ${t.branch}` : t.cp ? <Id>{t.cp}</Id> : '—'} {t.branch ? (
<span className="font-sans text-small">branch {t.branch}</span>
) : t.cp ? (
<Id>{t.cp}</Id>
) : (
<span className="text-muted"></span>
)}
</td> </td>
</tr> </tr>
))} ))}
@@ -107,15 +146,17 @@ function TransactionTable({ rows, currency }: { rows: Transaction[]; currency: s
function AlertBlock({ screen }: { screen: Screen }) { function AlertBlock({ screen }: { screen: Screen }) {
const a = screen.alert; const a = screen.alert;
return ( return (
<Block title="Alert" aside={<span className="nums">fired {a.fired}</span>}> <Block title="Alert" aside={<span>fired {a.fired}</span>}>
<p className="text-sm"> <p className="font-sans text-small text-fg">
<Id tone="fg">{a.id}</Id> <Id tone="fg">{a.id}</Id>
<span className="mx-1.5 text-muted">·</span> <span className="mx-1.5 text-muted">·</span>
<span className="font-mono text-xs font-semibold">{a.rule}</span> <span className="font-mono font-semibold">{a.rule}</span>
<span className="mx-1.5 text-muted"></span> <span className="mx-1.5 text-muted"></span>
{a.rule_text} <span className="text-fg-2">{a.rule_text}</span>
</p>
<p className="mb-1 mt-2">
<Label>Triggering transactions</Label>
</p> </p>
<p className="mb-1 mt-2 text-xs text-muted">Triggering transactions</p>
<TransactionTable rows={screen.triggering_transactions} currency={screen.currency} /> <TransactionTable rows={screen.triggering_transactions} currency={screen.currency} />
</Block> </Block>
); );
@@ -133,14 +174,12 @@ function KycBlock({ screen }: { screen: Screen }) {
const c = screen.customer; const c = screen.customer;
return ( return (
<Block title="KYC file" aside={<Id>{c.id}</Id>}> <Block title="KYC file" aside={<Id>{c.id}</Id>}>
<p className="mb-2 text-sm font-medium">{c.name}</p> <p className="mb-2 font-sans text-ui font-medium text-fg">{c.name}</p>
<dl className="grid grid-cols-1 gap-x-4 gap-y-1 text-xs sm:grid-cols-2"> <dl className="grid grid-cols-1 gap-x-4 gap-y-1.5 sm:grid-cols-2">
{kycEntries(c as unknown as Record<string, unknown>).map(([key, value]) => ( {kycEntries(c as unknown as Record<string, unknown>).map(([key, value]) => (
<div key={key} className="flex min-w-0 flex-col"> <div key={key} className="flex min-w-0 flex-col border-t border-border pt-1">
<dt className="text-muted"> <dt className="font-mono text-caption text-muted">kyc.{key}</dt>
<Id>kyc.{key}</Id> <dd className="break-words font-sans text-small text-fg">{kycValue(key, value, screen.currency)}</dd>
</dt>
<dd className="nums break-words">{kycValue(key, value, screen.currency)}</dd>
</div> </div>
))} ))}
</dl> </dl>
@@ -168,28 +207,28 @@ function SummaryBlock({ screen }: { screen: Screen }) {
} }
> >
<Scroll> <Scroll>
<table className="w-full min-w-[30rem] text-xs"> <table className={cn(TABLE, 'min-w-[30rem]')}>
<thead className="text-left text-muted"> <thead>
<tr> <tr>
<th className="pb-1 pr-2 font-medium">month</th> <th className={TH}>month</th>
{cols.map(([key, label]) => ( {cols.map(([key, label]) => (
<th key={key} className="pb-1 pr-2 text-right font-medium"> <th key={key} className={cn(TH, 'text-right')}>
{label} {label}
</th> </th>
))} ))}
<th className="pb-1 text-right font-medium">txns</th> <th className={cn(TH, 'text-right')}>txns</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{screen.summary.map((row) => ( {screen.summary.map((row) => (
<tr key={row.month} className="border-t border-border/60"> <tr key={row.month} className="border-t border-border">
<td className="nums py-0.5 pr-2 whitespace-nowrap">{row.month}</td> <td className={cn(TD, 'whitespace-nowrap text-fg')}>{row.month}</td>
{cols.map(([key]) => ( {cols.map(([key]) => (
<td key={key} className="nums py-0.5 pr-2 text-right"> <td key={key} className={cn(TD, NUM)}>
{wholeUnits(row[key])} {wholeUnits(row[key])}
</td> </td>
))} ))}
<td className="nums py-0.5 text-right">{row.count}</td> <td className={cn(TD, NUM, 'text-muted')}>{row.count}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -202,19 +241,33 @@ function SummaryBlock({ screen }: { screen: Screen }) {
function CounterpartyBlock({ screen }: { screen: Screen }) { function CounterpartyBlock({ screen }: { screen: Screen }) {
return ( return (
<Block title="Counterparties on screen" aside="top by twelve-month volume"> <Block title="Counterparties on screen" aside="top by twelve-month volume">
<ul className="space-y-0.5 text-xs"> <Scroll>
<table className={cn(TABLE, 'min-w-[20rem]')}>
<thead>
<tr>
<th className={TH}>id</th>
<th className={TH}>direction</th>
<th className={cn(TH, 'text-right')}>txns</th>
<th className={cn(TH, 'text-right')}>total</th>
</tr>
</thead>
<tbody>
{screen.screen_counterparties.map((cp) => ( {screen.screen_counterparties.map((cp) => (
<li key={cp.id} className="flex flex-wrap items-baseline gap-x-2"> <tr key={cp.id} className="border-t border-border">
<td className={TD}>
<Id tone="fg">{cp.id}</Id> <Id tone="fg">{cp.id}</Id>
<span className="text-muted">{cp.direction === 'in' ? 'inbound' : 'outbound'}</span> </td>
<span className="nums">{cp.count} txns</span> <td className={cn(TD, 'font-sans text-small text-muted')}>{cp.direction === 'in' ? 'inbound' : 'outbound'}</td>
<span className="nums">{money(cp.total, screen.currency)}</span> <td className={cn(TD, NUM)}>{cp.count}</td>
</li> <td className={cn(TD, NUM, 'text-fg')}>{money(cp.total, screen.currency)}</td>
</tr>
))} ))}
</ul> </tbody>
<p className="mt-2 text-xs text-muted"> </table>
</Scroll>
<p className="mt-2 font-sans text-small text-muted">
Reporting threshold in this jurisdiction:{' '} Reporting threshold in this jurisdiction:{' '}
<span className="nums text-fg">{money(screen.threshold, screen.currency)}</span> <span className="font-mono text-fg">{money(screen.threshold, screen.currency)}</span>
</p> </p>
</Block> </Block>
); );
@@ -226,7 +279,7 @@ function LookupCard({ lookup, currency }: { lookup: Lookup; currency: string })
return ( return (
<Block <Block
title={`History ${lookup.result.month}`} title={`History ${lookup.result.month}`}
aside={<span className="nums">{lookup.result.transactions.length} transactions</span>} aside={<span>{lookup.result.transactions.length} transactions</span>}
> >
<TransactionTable rows={lookup.result.transactions} currency={currency} /> <TransactionTable rows={lookup.result.transactions} currency={currency} />
</Block> </Block>
@@ -234,19 +287,19 @@ function LookupCard({ lookup, currency }: { lookup: Lookup; currency: string })
case 'counterparty': { case 'counterparty': {
const r = lookup.result; const r = lookup.result;
return ( return (
<Block title={`Counterparty ${r.id}`} aside={<span className="nums">first seen {r.first_seen}</span>}> <Block title={`Counterparty ${r.id}`} aside={<span>first seen {r.first_seen}</span>}>
<p className="text-sm font-medium">{r.name}</p> <p className="font-sans text-ui font-medium text-fg">{r.name}</p>
<p className="text-xs text-muted"> <p className="font-sans text-small text-muted">
{r.type.replace(/_/g, ' ')} · {r.jurisdiction} {r.type.replace(/_/g, ' ')} · {r.jurisdiction}
</p> </p>
<p className="mt-1 text-xs"> <p className="mt-1.5 font-sans text-small text-fg-2">
Other alerted accounts sharing this counterparty in twelve months:{' '} Other alerted accounts sharing this counterparty in twelve months:{' '}
<span className="nums font-semibold">{r.other_alerted_accounts}</span> <span className="font-mono font-semibold text-fg">{r.other_alerted_accounts}</span>
</p> </p>
<p className="mt-1 text-xs text-muted"> <p className="mt-1.5 font-sans text-small text-muted">
transactions:{' '} transactions:{' '}
{r.transactions.map((id) => ( {r.transactions.map((id) => (
<span key={id} className="mr-1"> <span key={id} className="mr-1.5">
<Id>{id}</Id> <Id>{id}</Id>
</span> </span>
))} ))}
@@ -256,22 +309,23 @@ function LookupCard({ lookup, currency }: { lookup: Lookup; currency: string })
} }
case 'prior_alerts': case 'prior_alerts':
return ( return (
<Block title="Prior alerts" aside={<span className="nums">{lookup.result.alerts.length} on file</span>}> <Block title="Prior alerts" aside={<span>{lookup.result.alerts.length} on file</span>}>
{lookup.result.alerts.length === 0 ? ( {lookup.result.alerts.length === 0 ? (
<p className="text-xs text-muted">None on file.</p> <p className="font-sans text-small text-muted">None on file.</p>
) : ( ) : (
<ul className="space-y-1.5 text-xs"> <ul className="divide-y divide-border">
{lookup.result.alerts.map((p) => ( {lookup.result.alerts.map((p) => (
<li key={p.id}> <li key={p.id} className="py-1.5 first:pt-0 last:pb-0">
<div className="flex flex-wrap items-baseline gap-x-2"> <div className="flex flex-wrap items-baseline gap-x-2 font-mono text-caption">
<Id tone="fg">{p.id}</Id> <Id tone="fg">{p.id}</Id>
<span className="font-mono">{p.rule}</span> <span className="font-semibold text-fg">{p.rule}</span>
<span className="nums text-muted">fired {p.fired}</span> <span className="text-muted">fired {p.fired}</span>
<span> <span className="font-sans text-small text-fg-2">
{p.disposition} after {p.elapsed_days} {p.elapsed_days === 1 ? 'day' : 'days'} by {p.analyst} {p.disposition} after <span className="font-mono">{p.elapsed_days}</span>{' '}
{p.elapsed_days === 1 ? 'day' : 'days'} by {p.analyst}
</span> </span>
</div> </div>
<p className="text-muted">{p.rationale}</p> <p className="mt-0.5 font-sans text-small text-fg-2">{p.rationale}</p>
</li> </li>
))} ))}
</ul> </ul>
@@ -280,15 +334,15 @@ function LookupCard({ lookup, currency }: { lookup: Lookup; currency: string })
); );
case 'documents': case 'documents':
return ( return (
<Block title="Documents on file" aside={<span className="nums">{lookup.result.documents.length}</span>}> <Block title="Documents on file" aside={<span>{lookup.result.documents.length}</span>}>
<ul className="space-y-1 text-xs"> <ul className="divide-y divide-border">
{lookup.result.documents.map((d) => ( {lookup.result.documents.map((d) => (
<li key={d.id} className="flex flex-col gap-0.5 sm:flex-row sm:gap-2"> <li key={d.id} className="flex flex-col gap-0.5 py-1.5 first:pt-0 last:pb-0 sm:flex-row sm:gap-3">
<span className="shrink-0"> <span className="shrink-0 font-mono text-caption">
<Id tone="fg">{d.id}</Id>{' '} <Id tone="fg">{d.id}</Id>{' '}
<span className="text-muted">[{d.kind.replace(/_/g, ' ')}]</span> <span className="text-muted">[{d.kind.replace(/_/g, ' ')}]</span>
</span> </span>
<span>{d.description}</span> <span className="font-sans text-small text-fg-2">{d.description}</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -297,14 +351,20 @@ function LookupCard({ lookup, currency }: { lookup: Lookup; currency: string })
} }
} }
/**
* A cited id. Three states, each with a word for screen readers and a mark
* for everyone else: planted (✓ on the green ground), not planted (struck
* through), and unknown (a plain strong-edged chip — the episode is not
* over, or the disposition was not scored).
*/
function CiteChip({ id, hit }: { id: string; hit?: boolean | null }) { function CiteChip({ id, hit }: { id: string; hit?: boolean | null }) {
return ( return (
<span <span
className={cn( className={cn(
'inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 font-mono text-[0.7rem]', 'inline-flex items-center gap-1 rounded-tile border px-1.5 py-0.5 font-mono text-label tracking-normal',
hit === true && 'border-positive/50 bg-positive/10 text-fg', hit === true && 'border-positive bg-accent-subtle text-fg',
hit === false && 'border-border text-muted line-through decoration-muted/60', hit === false && 'border-border text-muted line-through',
(hit === null || hit === undefined) && 'border-border text-fg', (hit === null || hit === undefined) && 'border-border-strong text-fg',
)} )}
> >
{hit === true ? <span aria-hidden="true"></span> : null} {hit === true ? <span aria-hidden="true"></span> : null}
@@ -315,42 +375,45 @@ function CiteChip({ id, hit }: { id: string; hit?: boolean | null }) {
); );
} }
function NeverScored() {
return <Chip className="text-muted">displayed, never scored</Chip>;
}
function DispositionCard({ state }: { state: TriageState }) { function DispositionCard({ state }: { state: TriageState }) {
const d = state.disposition; const d = state.disposition;
if (!d) return null; if (!d) return null;
const planted = state.reveal ? bestAlternate(d.cites, state.reveal.planted) : null; const planted = state.reveal ? bestAlternate(d.cites, state.reveal.planted) : null;
const scored = state.reveal !== null && state.outcome === 'solved'; const scored = state.reveal !== null && state.outcome === 'solved';
const prose = (d.action === 'close' ? d.note : d.narrative).trim();
return ( return (
<Block <Block
title={d.action === 'close' ? 'Closed' : 'Escalated'} title={d.action === 'close' ? 'Closed' : 'Escalated'}
aside={ aside={<span className="text-fg">{d.action === 'close' ? enumLabel(d.reason) : d.typology}</span>}
<span className="font-mono">{d.action === 'close' ? enumLabel(d.reason) : d.typology}</span>
}
> >
<p className="mb-1 text-xs text-muted">Cites</p> <p className="mb-1">
<Label>Cites</Label>
</p>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{d.cites.map((id) => ( {d.cites.map((id) => (
<CiteChip key={id} id={id} hit={planted && scored ? planted.includes(id) : null} /> <CiteChip key={id} id={id} hit={planted && scored ? planted.includes(id) : null} />
))} ))}
</div> </div>
{(d.action === 'close' ? d.note : d.narrative).trim() ? ( {prose ? (
<div className="mt-2"> <div className="mt-2.5">
<p className="mb-1 flex items-baseline gap-2 text-xs text-muted"> <p className="mb-1 flex flex-wrap items-center gap-2">
<span>{d.action === 'close' ? 'Note' : 'Narrative'}</span> <Label>{d.action === 'close' ? 'Note' : 'Narrative'}</Label>
<span className="rounded border border-border px-1 py-px text-[0.65rem] uppercase tracking-wide"> <NeverScored />
displayed, never scored
</span>
</p> </p>
<blockquote className="border-l-2 border-border pl-2 text-xs leading-relaxed"> <blockquote className="border-l-2 border-border-strong pl-2.5 font-sans text-small text-fg-2">{prose}</blockquote>
{d.action === 'close' ? d.note : d.narrative}
</blockquote>
</div> </div>
) : null} ) : null}
</Block> </Block>
); );
} }
function verdictLine(state: TriageState, reveal: Reveal): { text: string; tone: 'positive' | 'danger' | 'warning' } { type Tone = 'positive' | 'danger' | 'warning';
function verdictLine(state: TriageState, reveal: Reveal): { text: string; tone: Tone } {
if (state.status === 'aborted') { if (state.status === 'aborted') {
return { return {
text: 'No disposition in eight replies. The alert stays in the queue and the episode scores 0.000 on every component.', text: 'No disposition in eight replies. The alert stays in the queue and the episode scores 0.000 on every component.',
@@ -359,7 +422,10 @@ function verdictLine(state: TriageState, reveal: Reveal): { text: string; tone:
} }
if (state.outcome === 'solved') { if (state.outcome === 'solved') {
return { return {
text: reveal.label === 'benign' ? 'Right call: the alert was benign and it was closed.' : 'Right call: the alert was suspicious and it was escalated.', text:
reveal.label === 'benign'
? 'Right call: the alert was benign and it was closed.'
: 'Right call: the alert was suspicious and it was escalated.',
tone: 'positive', tone: 'positive',
}; };
} }
@@ -375,103 +441,129 @@ function verdictLine(state: TriageState, reveal: Reveal): { text: string; tone:
}; };
} }
/**
* Verdict ink sits on surface-2, where all three status colours clear 4.5
* in both themes (danger is 4.70 dark — surface-2 is the darkest ground it is
* allowed on, see index.css). The sentence begins with its word.
*/
const TONE_TEXT: Record<Tone, string> = {
positive: 'text-positive',
danger: 'text-danger',
warning: 'text-warning',
};
function RevealCard({ state }: { state: TriageState }) { function RevealCard({ state }: { state: TriageState }) {
const reveal = state.reveal; const reveal = state.reveal;
if (!reveal) return null; if (!reveal) return null;
const cites = state.disposition?.cites ?? []; const cites = state.disposition?.cites ?? [];
const best = bestAlternate(cites, reveal.planted); const best = bestAlternate(cites, reveal.planted);
const verdict = verdictLine(state, reveal); const verdict = verdictLine(state, reveal);
const TONE = { positive: 'text-positive', danger: 'text-danger', warning: 'text-warning' } as const;
return ( return (
<Block <Block
title="What the generator planted" title="What the generator planted"
aside={ aside={
<span className="font-mono"> <span>
{reveal.template} {reveal.template}
{reveal.overlay ? ` + ${reveal.overlay.replace(/_/g, ' ')}` : ''} {reveal.overlay ? ` + ${reveal.overlay.replace(/_/g, ' ')}` : ''}
</span> </span>
} }
> >
<p className="text-sm"> <p className="flex flex-wrap items-baseline gap-x-2 gap-y-1 font-sans text-small">
<span className={cn('font-semibold', reveal.label === 'benign' ? 'text-positive' : 'text-danger')}> <span className={cn('font-mono font-semibold uppercase', reveal.label === 'benign' ? 'text-positive' : 'text-danger')}>
{reveal.label} {reveal.label}
</span> </span>
{reveal.typology ? ( {reveal.typology ? <span className="font-mono text-caption text-fg">{reveal.typology}</span> : null}
<>
<span className="mx-1.5 text-muted">·</span>
<span className="font-mono text-xs">{reveal.typology}</span>
</>
) : null}
{reveal.tier === 'hidden' ? ( {reveal.tier === 'hidden' ? (
<> <span className="text-muted">hidden tier: the screen is a benign case; the signal was only in a lookup</span>
<span className="mx-1.5 text-muted">·</span>
<span className="text-xs text-muted">hidden tier: the screen is a benign case; the signal was only in a lookup</span>
</>
) : null} ) : null}
</p> </p>
<p className={cn('mt-1 text-xs font-medium', TONE[verdict.tone])} role="status"> <p
className={cn('mt-2 rounded-control bg-surface-2 px-2.5 py-2 font-sans text-small font-medium', TONE_TEXT[verdict.tone])}
role="status"
>
{verdict.text} {verdict.text}
</p> </p>
<p className="mb-1 mt-2 text-xs text-muted"> <p className="mb-1 mt-2.5">
The facts that decide it{reveal.planted.length > 1 ? ' (best of the alternates)' : ''} <Label>The facts that decide it{reveal.planted.length > 1 ? ' (best of the alternates)' : ''}</Label>
</p> </p>
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{best.map((id) => ( {best.map((id) => (
<CiteChip key={id} id={id} hit={cites.includes(id) ? true : null} /> <CiteChip key={id} id={id} hit={cites.includes(id) ? true : null} />
))} ))}
</div> </div>
<p className="mt-2 text-xs text-muted"> <p className="mt-2.5 flex flex-wrap gap-x-3 gap-y-1 font-mono text-caption text-muted">
Hours spent <span className="nums text-fg">{hours(state.minutesSpent)}</span> <span>
<Label>Hours spent</Label> <span className="text-fg">{hours(state.minutesSpent)}</span>
</span>
{reveal.referenceMinutes !== null ? ( {reveal.referenceMinutes !== null ? (
<> <span>
{' '} <Label>Cheapest shipped analyst that got it right</Label>{' '}
· cheapest shipped analyst that got it right{' '} <span className="text-fg">{hours(reveal.referenceMinutes)}</span>
<span className="nums text-fg">{hours(reveal.referenceMinutes)}</span> </span>
</>
) : null} ) : null}
</p> </p>
</Block> </Block>
); );
} }
function Meter({ state }: { state: TriageState }) { /**
const STATUS: Record<TriageState['status'], string> = { * The meter: what the episode has cost so far, and where it stands. The hour
* count climbs with every lookup and the eight reply pips fill one per turn,
* so the budget is visible without reading a number — and the numbers are
* there for anyone who does.
*/
const STATUS_WORD: Record<TriageState['status'], string> = {
playing: 'in the queue', playing: 'in the queue',
closed: 'closed', closed: 'closed',
escalated: 'escalated', escalated: 'escalated',
aborted: 'left in the queue', aborted: 'left in the queue',
}; };
const STATUS_TONE: Record<TriageState['status'], string> = {
playing: 'text-muted',
closed: 'text-positive',
escalated: 'text-warning',
aborted: 'text-danger',
};
function Meter({ state }: { state: TriageState }) {
return ( return (
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted"> <div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 font-mono text-caption">
<span> <span className="flex items-baseline gap-1.5">
Hours <span className="nums font-semibold text-fg">{hours(state.minutesSpent)}</span> <Label>Hours</Label>
<span className="text-mono-data font-semibold text-fg">{hours(state.minutesSpent)}</span>
</span> </span>
<span> <span className="flex items-center gap-1.5">
Replies{' '} <Label>Replies</Label>
<span className="nums font-semibold text-fg"> <span className="text-mono-data font-semibold text-fg">
{state.turnsSpent} of {MAX_TURNS} {state.turnsSpent}
<span className="font-normal text-muted"> of {MAX_TURNS}</span>
</span>
<span className="inline-flex items-center gap-0.5" aria-hidden="true">
{Array.from({ length: MAX_TURNS }, (_, i) => (
<span
key={i}
className={cn(
'h-2.5 w-1.5 rounded-[1px] border',
i < state.turnsSpent ? 'border-fg bg-fg' : 'border-border-strong bg-transparent',
)}
/>
))}
</span> </span>
</span> </span>
{state.rejected > 0 ? ( {state.rejected > 0 ? (
<span> <span className="flex items-baseline gap-1.5">
refused <span className="nums font-semibold text-fg">{state.rejected}</span> <Label className="text-warning">Refused</Label>
<span className="text-mono-data font-semibold text-warning">{state.rejected}</span>
</span> </span>
) : null} ) : null}
<span <Chip className={STATUS_TONE[state.status]}>{STATUS_WORD[state.status]}</Chip>
className={cn(
'rounded-md border px-1.5 py-px font-medium',
state.status === 'playing' && 'border-border',
state.status === 'closed' && 'border-positive/50 text-positive',
state.status === 'escalated' && 'border-warning/60 text-warning',
state.status === 'aborted' && 'border-danger/50 text-danger',
)}
>
{STATUS[state.status]}
</span>
{state.screen.held_out ? ( {state.screen.held_out ? (
<span className="rounded-md border border-border px-1.5 py-px" title="fnv1a(seed) % 8 == 7: second currency, renamed rules, KYC before the alert"> <Chip
className="text-fg"
title="fnv1a(seed) % 8 == 7: second currency, renamed rules, KYC before the alert"
>
held-out seed held-out seed
</span> </Chip>
) : null} ) : null}
</div> </div>
); );
@@ -481,8 +573,8 @@ function Meter({ state }: { state: TriageState }) {
/** /**
* The thumbnail: what the timeline chip and the gallery card show. A whole * The thumbnail: what the timeline chip and the gallery card show. A whole
* screen at 7rem is noise, so this is the shape of the run instead — the rule, * screen at 7.5rem is noise, so this is the shape of the run instead — the
* the twelve months of cash as bars, and what has happened so far. * rule, the twelve months of cash as bars, and what has happened so far.
*/ */
function CompactBoard({ state }: { state: TriageState }) { function CompactBoard({ state }: { state: TriageState }) {
const rows = state.screen.summary; const rows = state.screen.summary;
@@ -491,42 +583,44 @@ function CompactBoard({ state }: { state: TriageState }) {
const label = const label =
`${state.screen.alert.rule}, ${hours(state.minutesSpent)} hours, ${state.lookups.length} lookups` + `${state.screen.alert.rule}, ${hours(state.minutesSpent)} hours, ${state.lookups.length} lookups` +
(d ? `, ${d.action === 'close' ? 'closed' : 'escalated'}` : state.status === 'aborted' ? ', aborted' : ''); (d ? `, ${d.action === 'close' ? 'closed' : 'escalated'}` : state.status === 'aborted' ? ', aborted' : '');
const CHIP = 'rounded-[2px] border px-0.5 leading-[1.3]';
return ( return (
<div className="w-[7.5rem] text-[9px] leading-tight" role="img" aria-label={label}> <div className="w-[7.5rem] font-mono text-[9px] leading-tight" role="img" aria-label={label}>
<SyntheticLabel compact /> <SyntheticLabel compact />
<div className="mt-1 flex items-baseline justify-between gap-1"> <div className="mt-1 flex items-baseline justify-between gap-1">
<span className="truncate font-mono font-semibold">{state.screen.alert.rule}</span> <span className="truncate font-semibold text-fg">{state.screen.alert.rule}</span>
<span className="nums shrink-0 text-muted">{hours(state.minutesSpent)} h</span> <span className="shrink-0 text-muted">{hours(state.minutesSpent)} h</span>
</div> </div>
<div className="mt-1 flex h-4 items-end gap-px" aria-hidden="true"> <div className="mt-1 flex h-4 items-end gap-px" aria-hidden="true">
{rows.map((r) => ( {rows.map((r) => (
<span <span
key={r.month} key={r.month}
className="flex-1 rounded-sm bg-muted/50" className="flex-1 rounded-[1px] bg-border-strong"
style={{ height: `${Math.max(6, Math.round((r.cash_in / peak) * 100))}%` }} style={{ height: `${Math.max(6, Math.round((r.cash_in / peak) * 100))}%` }}
/> />
))} ))}
</div> </div>
<div className="mt-1 flex flex-wrap gap-0.5"> <div className="mt-1 flex flex-wrap gap-0.5">
{state.lookups.map((l, i) => ( {state.lookups.map((l, i) => (
<span key={i} className="rounded border border-border px-0.5 text-muted"> <span key={i} className={cn(CHIP, 'border-border text-muted')}>
{l.what === 'history' ? l.key.slice(5) : l.what === 'counterparty' ? l.key : l.what === 'prior_alerts' ? 'priors' : 'docs'} {l.what === 'history' ? l.key.slice(5) : l.what === 'counterparty' ? l.key : l.what === 'prior_alerts' ? 'priors' : 'docs'}
</span> </span>
))} ))}
{state.rejected > 0 ? ( {state.rejected > 0 ? (
<span className="rounded border border-warning/60 px-0.5 text-warning">!{state.rejected}</span> <span className={cn(CHIP, 'border-warning text-warning')}>!{state.rejected}</span>
) : null} ) : null}
{d ? ( {d ? (
<span <span
className={cn( className={cn(
'rounded border px-0.5 font-semibold uppercase', CHIP,
d.action === 'close' ? 'border-positive/50 text-positive' : 'border-warning/60 text-warning', 'font-semibold uppercase',
d.action === 'close' ? 'border-positive text-positive' : 'border-warning text-warning',
)} )}
> >
{d.action === 'close' ? 'close' : 'escalate'} {d.action === 'close' ? 'close' : 'escalate'}
</span> </span>
) : state.status === 'aborted' ? ( ) : state.status === 'aborted' ? (
<span className="rounded border border-danger/50 px-0.5 font-semibold uppercase text-danger">aborted</span> <span className={cn(CHIP, 'border-danger font-semibold uppercase text-danger')}>aborted</span>
) : null} ) : null}
</div> </div>
</div> </div>
@@ -550,13 +644,15 @@ export const Board = memo(function Board({ state, compact }: { state: TriageStat
// shrink-to-fit card on every tab, so `w-full` resolves to nothing and // shrink-to-fit card on every tab, so `w-full` resolves to nothing and
// `max-w-full` resolves against a parent that sized itself to this // `max-w-full` resolves against a parent that sized itself to this
// board — measured: 640px on a 390px phone, 304px of page overflow. // board — measured: 640px on a 390px phone, 304px of page overflow.
// 5rem is the page gutters plus the card padding at the phone breakpoint. // 4.5rem is the phone gutters (2 × 1.25rem) plus the shell card's p-3
// (2 × 0.75rem) with half a rem for its borders: the shell's padding
// and this constant are coupled and move together.
// `max-w-full` is for the one place the parent IS definite: a blind // `max-w-full` is for the one place the parent IS definite: a blind
// comparison's grid cell, which is narrower than 40rem at 1280px and // comparison's grid cell, which is narrower than 40rem at 1280px and
// measured 31px of page overflow without it. // measured 31px of page overflow without it.
className="w-[min(40rem,calc(100vw-5rem))] max-w-full min-w-0 space-y-3 text-sm" className="w-[min(40rem,calc(100vw-4.5rem))] min-w-0 max-w-full space-y-3 font-sans text-small text-fg"
role="group" role="group"
aria-label={`Alert ${screen.alert.id} on customer ${screen.customer.id}. ${state.lookups.length} lookups made, ${hours(state.minutesSpent)} hours spent.`} aria-label={`Alert ${screen.alert.id} on customer ${screen.customer.id}. ${state.lookups.length} lookups made, ${hours(state.minutesSpent)} hours spent, ${STATUS_WORD[state.status]}.`}
> >
<SyntheticLabel /> <SyntheticLabel />
<Meter state={state} /> <Meter state={state} />
@@ -567,8 +663,11 @@ export const Board = memo(function Board({ state, compact }: { state: TriageStat
<LookupCard key={`${lookup.what}-${lookup.key ?? i}`} lookup={lookup} currency={screen.currency} /> <LookupCard key={`${lookup.what}-${lookup.key ?? i}`} lookup={lookup} currency={screen.currency} />
))} ))}
{state.lastRejection ? ( {state.lastRejection ? (
<p role="status" className="rounded-md border border-warning/60 bg-warning/5 px-3 py-2 text-xs text-warning"> <p
Reply refused: {state.lastRejection}. It still cost a turn. role="status"
className="rounded-control border border-warning bg-warning-subtle px-3 py-2 font-sans text-small text-warning"
>
<span className="font-mono text-label uppercase">Reply refused</span> {state.lastRejection}. It still cost a turn.
</p> </p>
) : null} ) : null}
<DispositionCard state={state} /> <DispositionCard state={state} />
+72 -31
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { play, type BoardState } from './engine'; import { play, type BoardState } from './engine';
@@ -8,14 +9,16 @@ import { allowedNow, ANSWERS, primeGuessList } from './words';
const ROWS = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm']; const ROWS = ['qwertyuiop', 'asdfghjkl', 'zxcvbnm'];
type TileState = 'G' | 'Y' | 'X';
/** Best-known state of each letter, for tinting the keys. */ /** Best-known state of each letter, for tinting the keys. */
function letterStates(board: BoardState): Record<string, 'G' | 'Y' | 'X'> { function letterStates(board: BoardState): Record<string, TileState> {
const rank = { X: 0, Y: 1, G: 2 } as const; const rank = { X: 0, Y: 1, G: 2 } as const;
const out: Record<string, 'G' | 'Y' | 'X'> = {}; const out: Record<string, TileState> = {};
for (const { guess, pattern } of board.rows) { for (const { guess, pattern } of board.rows) {
for (let i = 0; i < guess.length; i += 1) { for (let i = 0; i < guess.length; i += 1) {
const letter = guess[i]!; const letter = guess[i]!;
const tile = pattern[i] as 'G' | 'Y' | 'X'; const tile = pattern[i] as TileState;
const current = out[letter]; const current = out[letter];
if (!current || rank[tile] > rank[current]) out[letter] = tile; if (!current || rank[tile] > rank[current]) out[letter] = tile;
} }
@@ -23,12 +26,31 @@ function letterStates(board: BoardState): Record<string, 'G' | 'Y' | 'X'> {
return out; return out;
} }
const KEY_TINT: Record<string, string> = { /**
G: 'bg-tile-exact text-tile-exact-fg', * A used key takes the tile token at FULL opacity. The old `/70` on absent
Y: 'bg-tile-present text-tile-present-fg', * made the grey key a fourth colour the board never shows; at full strength
X: 'bg-tile-absent/70 text-tile-absent-fg', * the key is the tile, and the ink on it is measured there (7.75 dark, 5.74
* light — see index.css).
*/
const KEY_TINT: Record<TileState, string> = {
G: 'bg-tile-exact text-tile-exact-fg hover:bg-tile-exact',
Y: 'bg-tile-present text-tile-present-fg hover:bg-tile-present',
X: 'bg-tile-absent text-tile-absent-fg hover:bg-tile-absent',
}; };
/** The board's high-contrast glyphs, repeated on the keys so the keyboard has the same second channel. */
const KEY_GLYPH: Record<TileState, string> = { G: '●', Y: '◆', X: '' };
/**
* One key. Letter keys are `flex-1` with no minimum width: ten of them plus
* nine gaps have to fit 326px on a phone, which is roughly 29px each — the
* 44px target is the height. ENTER and DEL are wider and set in mono, the
* way a command is.
*/
const KEY =
'min-h-11 min-w-0 select-none rounded-control transition-[background-color,color,transform] duration-1 ease-enter ' +
'motion-safe:active:scale-[0.94] disabled:pointer-events-none disabled:opacity-40';
export function Keyboard({ export function Keyboard({
state, state,
onChange, onChange,
@@ -69,7 +91,7 @@ export function Keyboard({
const handler = (event: KeyboardEvent) => { const handler = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey || event.altKey) return; if (event.metaKey || event.ctrlKey || event.altKey) return;
const target = event.target as HTMLElement | null; const target = event.target as HTMLElement | null;
if (target && /^(INPUT|TEXTAREA)$/.test(target.tagName)) return; if (target && /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)) return;
if (event.key === 'Enter') press('enter'); if (event.key === 'Enter') press('enter');
else if (event.key === 'Backspace') press('back'); else if (event.key === 'Backspace') press('back');
else if (/^[a-zA-Z]$/.test(event.key)) press(event.key.toLowerCase()); else if (/^[a-zA-Z]$/.test(event.key)) press(event.key.toLowerCase());
@@ -85,36 +107,37 @@ export function Keyboard({
return ( return (
// Own cap, not the shell's. Unconstrained, these flex rows stretch to the // Own cap, not the shell's. Unconstrained, these flex rows stretch to the
// page and the keys drift a screen away from the board they belong to. // page and the keys drift a screen away from the board they belong to.
<div className="grid w-full max-w-md gap-1 sm:gap-1.5" style={{ paddingBottom: 'max(0px, var(--safe-bottom))' }}> <div
// 28rem caps a letter key at 39px on a 1024px iPad; a coarse pointer
// gets 32rem, which is the narrowest width at which ten keys and nine
// 6px gaps are all 44px. The phone never reaches either cap.
className="grid w-full max-w-md gap-1 pb-[max(0px,var(--safe-bottom))] sm:gap-1.5 [@media(pointer:coarse)]:max-w-lg"
>
{/* The solver, live, on whatever word you picked — not a recording. This {/* The solver, live, on whatever word you picked — not a recording. This
is the difference between a demo and a video: it answers for boards no is the difference between a demo and a video: it answers for boards no
rollout on this site ever covered. */} rollout on this site ever covered. */}
{hint.status !== 'unavailable' && !done ? ( {hint.status !== 'unavailable' && !done ? (
<div className="mb-1 flex min-h-9 flex-wrap items-center gap-2 text-xs"> <div className="mb-1 flex flex-wrap items-center gap-x-3 gap-y-1.5">
<button <Button
type="button" type="button"
variant="outline"
size="touch"
onClick={() => solver.ask(state.rows)} onClick={() => solver.ask(state.rows)}
disabled={hint.status === 'thinking'} disabled={hint.status === 'thinking'}
className="tap inline-flex items-center rounded-md border border-border px-2.5 py-1 font-medium transition-colors duration-1 hover:bg-accent-subtle disabled:opacity-50"
> >
{hint.status === 'thinking' ? 'Thinking…' : 'What would the best player guess?'} {hint.status === 'thinking' ? 'Thinking…' : 'What would the best player guess?'}
</button> </Button>
<span aria-live="polite" className="text-muted"> <span aria-live="polite" className="font-mono text-mono-data text-muted">
{hint.status === 'ready' ? ( {hint.status === 'ready' ? (
<> <>
<span className="font-mono font-semibold uppercase text-fg"> <span className="font-semibold uppercase text-fg">{hint.suggestion.guess}</span>
{hint.suggestion.guess} {' — '}
</span>{' '} {hint.suggestion.bits.toFixed(2)} bits from{' '}
<span className="nums">{hint.suggestion.bits.toFixed(2)}</span> bits from{' '} {hint.suggestion.candidatesBefore.toLocaleString('en-US')} remaining
<span className="nums">{hint.suggestion.candidatesBefore.toLocaleString('en-US')}</span>{' '}
remaining
{hint.suggestion.viable ? null : ( {hint.suggestion.viable ? null : (
<> <>
{' '} {' · '}
·{' '} <span className="text-warning">cannot win it is buying information</span>
<span className="text-warning">
cannot win it is buying information
</span>
</> </>
)} )}
</> </>
@@ -129,12 +152,17 @@ export function Keyboard({
type="button" type="button"
onClick={() => press('enter')} onClick={() => press('enter')}
disabled={done} disabled={done}
className="tap flex-[1.6] rounded-md bg-surface-2 px-1 text-[0.65rem] font-semibold uppercase tracking-wide transition-colors duration-1 hover:bg-accent-subtle disabled:opacity-40" className={cn(
KEY,
'flex-[1.6] bg-surface-2 px-1 font-mono text-label uppercase text-fg hover:bg-surface-3',
)}
> >
Enter Enter
</button> </button>
) : null} ) : null}
{[...row].map((letter) => ( {[...row].map((letter) => {
const used = states[letter];
return (
<button <button
key={letter} key={letter}
type="button" type="button"
@@ -142,20 +170,33 @@ export function Keyboard({
disabled={done} disabled={done}
aria-label={letter.toUpperCase()} aria-label={letter.toUpperCase()}
className={cn( className={cn(
'tap min-w-0 flex-1 rounded-md text-sm font-semibold uppercase transition-colors duration-1 disabled:opacity-40', KEY,
states[letter] ? KEY_TINT[states[letter]!] : 'bg-surface-2 hover:bg-accent-subtle', 'relative flex-1 font-sans text-[0.8125rem] font-semibold uppercase',
used ? KEY_TINT[used] : 'bg-surface-2 text-fg hover:bg-surface-3',
)} )}
> >
{letter} {letter}
{used && KEY_GLYPH[used] ? (
<span
aria-hidden="true"
className="pointer-events-none absolute bottom-0.5 right-0.5 text-[0.5em] leading-none opacity-0 [:root[data-contrast='high']_&]:opacity-100"
>
{KEY_GLYPH[used]}
</span>
) : null}
</button> </button>
))} );
})}
{index === 2 ? ( {index === 2 ? (
<button <button
type="button" type="button"
onClick={() => press('back')} onClick={() => press('back')}
disabled={done} disabled={done}
aria-label="Backspace" aria-label="Backspace"
className="tap flex-[1.6] rounded-md bg-surface-2 px-1 text-[0.65rem] font-semibold uppercase tracking-wide transition-colors duration-1 hover:bg-accent-subtle disabled:opacity-40" className={cn(
KEY,
'flex-[1.6] bg-surface-2 px-1 font-mono text-label uppercase text-fg hover:bg-surface-3',
)}
> >
Del Del
</button> </button>
+92 -30
View File
@@ -1,4 +1,4 @@
import { memo } from 'react'; import { memo, useEffect, useState } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -7,9 +7,22 @@ import { MAX_GUESSES, WORD_LENGTH, type BoardState, type Pattern } from './engin
/** /**
* The board. One component for all three jobs — you playing, the replay, and * The board. One component for all three jobs — you playing, the replay, and
* the gallery thumbnail — because three near-identical boards is how they drift. * the gallery thumbnail — because three near-identical boards is how they drift.
*
* It draws no outer chrome: the shell's card supplies the border, ground and
* padding. Every colour is a `tile-*` token, which is what lets the
* high-contrast palette swap the fills without this file knowing.
*/ */
const TILE_CLASS: Record<string, string> = { type TileState = 'G' | 'Y' | 'X';
/**
* Fill, border and glyph ink per state. The fills are the same in both themes
* (the board is the one theme-invariant object on the site) and the `-fg`
* tokens are measured against their own fill: exact 13.08, present 11.16,
* absent 7.75 dark / 5.74 light; in high contrast white on each fill clears
* 4.9 or better. The numbers live beside the tokens in index.css.
*/
const TILE_CLASS: Record<TileState, string> = {
G: 'bg-tile-exact text-tile-exact-fg border-tile-exact', G: 'bg-tile-exact text-tile-exact-fg border-tile-exact',
Y: 'bg-tile-present text-tile-present-fg border-tile-present', Y: 'bg-tile-present text-tile-present-fg border-tile-present',
X: 'bg-tile-absent text-tile-absent-fg border-tile-absent', X: 'bg-tile-absent text-tile-absent-fg border-tile-absent',
@@ -18,12 +31,40 @@ const TILE_CLASS: Record<string, string> = {
/** /**
* A glyph per state, shown only in high-contrast mode. * A glyph per state, shown only in high-contrast mode.
* *
* Green/yellow/grey is a colour-only distinction, which is exactly why the * Green/amber/grey is a colour-only distinction, which is exactly why the
* original game ships a high-contrast mode. A second channel means the result * original game ships a high-contrast mode. A second channel means the result
* survives deuteranopia, a projector with the colour balance wrong, and a * survives deuteranopia, a projector with the colour balance wrong, and a
* screenshot printed in black and white. * screenshot printed in black and white. Absent has no glyph: a filled grey
* tile with nothing in the corner is the third state.
*/ */
const TILE_GLYPH: Record<string, string> = { G: '●', Y: '◆', X: '' }; const TILE_GLYPH: Record<TileState, string> = { G: '●', Y: '◆', X: '' };
/** Flip length and per-tile stagger, matching the keyframes in index.css. */
const FLIP_MS = 520;
const STAGGER_MS = 100;
/**
* Whether a revealing tile has reached the half-way point of its flip.
*
* The real flip lands the colour at 90°, when the face is edge-on: before that
* the tile still looks typed, after it the tile is scored. Applying the colour
* on mount and rotating a coloured tile reads as a wobble, not a reveal. Under
* reduced motion the colour lands immediately — there is no flip to hide it
* behind, and the row's announcement carries the result regardless.
*/
function useLanded(revealing: boolean, index: number): boolean {
const [landed, setLanded] = useState(!revealing);
useEffect(() => {
if (!revealing) return;
if (typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
setLanded(true);
return;
}
const id = window.setTimeout(() => setLanded(true), index * STAGGER_MS + FLIP_MS / 2);
return () => window.clearTimeout(id);
}, [revealing, index]);
return landed;
}
function Tile({ function Tile({
letter, letter,
@@ -33,35 +74,45 @@ function Tile({
compact, compact,
}: { }: {
letter: string; letter: string;
tile: Pattern[number] | null; tile: TileState | null;
index: number; index: number;
revealing: boolean; revealing: boolean;
compact?: boolean; compact?: boolean;
}) { }) {
const filled = letter !== ''; const filled = letter !== '';
const landed = useLanded(revealing && tile !== null, index);
const scored = tile !== null && landed;
return ( return (
<div <div
className={cn( className={cn(
'relative grid place-items-center select-none font-semibold uppercase', 'relative grid select-none place-items-center font-sans font-semibold uppercase',
'aspect-square rounded-md border-2 transition-colors', 'aspect-square rounded-tile transition-colors duration-1',
compact ? 'text-[0.55rem] border' : 'text-xl sm:text-2xl', compact ? 'border text-[0.55rem]' : 'border-2 text-[1.375rem] sm:text-2xl',
tile scored
? TILE_CLASS[tile] ? TILE_CLASS[tile]
: filled : filled
? 'border-muted/60 bg-surface text-fg' ? // Typed, unscored: a strong edge says "this is yours", the letter is
: 'border-border bg-surface-2/40 text-fg', // plain ink. No fill, so the scored fill is an unmistakable change.
// The flip is a rotation about X with the colour landing at the 'border-border-strong bg-transparent text-fg'
// half-way point, staggered along the row. `prefers-reduced-motion` : 'border-tile-empty bg-transparent text-fg',
// The flip is a rotation about X, staggered along the row; the colour
// lands at the half-way point (see useLanded). `prefers-reduced-motion`
// clamps it to nothing globally, which is why announceRow() exists. // clamps it to nothing globally, which is why announceRow() exists.
revealing && tile && 'motion-safe:animate-[tile-flip_520ms_ease-enter_both]', revealing && tile && 'motion-safe:animate-[tile-flip_520ms_ease-enter_both]',
filled && !tile && 'motion-safe:animate-[tile-pop_120ms_ease-enter]', filled && !tile && 'motion-safe:animate-[tile-pop_120ms_ease-enter]',
)} )}
style={revealing ? { animationDelay: `${index * 100}ms` } : undefined} style={revealing && tile ? { animationDelay: `${index * STAGGER_MS}ms` } : undefined}
aria-hidden="true" aria-hidden="true"
> >
{letter} {letter}
{tile ? ( {scored ? (
<span className="pointer-events-none absolute bottom-0 right-0.5 text-[0.5em] leading-none opacity-0 [:root[data-contrast='high']_&]:opacity-90"> <span
className={cn(
'pointer-events-none absolute bottom-0 right-0.5 leading-none opacity-0',
"[:root[data-contrast='high']_&]:opacity-100",
compact ? 'text-[0.6em]' : 'text-[0.5em]',
)}
>
{TILE_GLYPH[tile]} {TILE_GLYPH[tile]}
</span> </span>
) : null} ) : null}
@@ -83,19 +134,25 @@ function Row({
compact?: boolean; compact?: boolean;
}) { }) {
const letters = guess.padEnd(WORD_LENGTH, ' ').slice(0, WORD_LENGTH); const letters = guess.padEnd(WORD_LENGTH, ' ').slice(0, WORD_LENGTH);
// The shake is armed by the prop and disarmed by its own end event, so a
// second refusal shakes the row again instead of leaving the class in place.
const [shaking, setShaking] = useState(false);
useEffect(() => {
if (shake) setShaking(true);
}, [shake]);
return ( return (
<div <div
className={cn( className={cn('grid gap-1 sm:gap-1.5', shaking && 'motion-safe:animate-[tile-shake_400ms_ease-enter]')}
'grid gap-1 sm:gap-1.5',
shake && 'motion-safe:animate-[tile-shake_600ms_ease-enter]',
)}
style={{ gridTemplateColumns: `repeat(${WORD_LENGTH}, minmax(0, 1fr))` }} style={{ gridTemplateColumns: `repeat(${WORD_LENGTH}, minmax(0, 1fr))` }}
onAnimationEnd={(event) => {
if (event.animationName === 'tile-shake') setShaking(false);
}}
> >
{[...letters].map((letter, i) => ( {[...letters].map((letter, i) => (
<Tile <Tile
key={i} key={i}
letter={letter.trim()} letter={letter.trim()}
tile={pattern ? (pattern[i] as Pattern[number]) : null} tile={pattern ? (pattern[i] as TileState) : null}
index={i} index={i}
revealing={revealing} revealing={revealing}
compact={compact} compact={compact}
@@ -105,6 +162,14 @@ function Row({
); );
} }
function boardLabel(state: BoardState): string {
const played = state.rows.length;
if (state.status === 'won') return `Solved in ${played} of ${MAX_GUESSES} guesses.`;
if (state.status === 'lost') return `All ${MAX_GUESSES} guesses played, not solved.`;
if (played === 0) return `Empty board, ${MAX_GUESSES} guesses remaining.`;
return `${played} of ${MAX_GUESSES} guesses played.`;
}
export const Board = memo(function Board({ export const Board = memo(function Board({
state, state,
compact, compact,
@@ -119,26 +184,23 @@ export const Board = memo(function Board({
return ( return (
<div <div
className={cn( className={cn(
'grid gap-1 sm:gap-1.5', 'grid',
// An explicit width, not `w-full max-w-sm`. An empty board has no // An explicit width, not `w-full max-w-sm`. An empty board has no
// intrinsic width, so inside a shrink-to-fit parent `w-full` resolves // intrinsic width, so inside a shrink-to-fit parent `w-full` resolves
// to nothing and the board collapses to a stub — which is exactly the // to nothing and the board collapses to a stub — which is exactly the
// state the interactive board starts in. // state the interactive board starts in.
compact ? 'w-[7rem]' : 'w-[min(100%,20rem)] min-w-[15rem]', compact ? 'w-[7rem] gap-0.5' : 'w-[min(100%,20rem)] min-w-[15rem] gap-1 sm:gap-1.5',
)} )}
role="img" role="img"
aria-label={ aria-label={boardLabel(state)}
rows.length === 0
? 'Empty board, six guesses remaining.'
: `${rows.length} of ${MAX_GUESSES} guesses played.`
}
> >
{rows.map((row, i) => ( {rows.map((row, i) => (
<Row <Row
key={`${row.guess}-${i}`} key={`${row.guess}-${i}`}
guess={row.guess} guess={row.guess}
pattern={row.pattern} pattern={row.pattern}
revealing={i === rows.length - 1} // The thumbnail is the shape of the run, not a replay: no flip there.
revealing={!compact && i === rows.length - 1}
compact={compact} compact={compact}
/> />
))} ))}
+229 -83
View File
@@ -2,41 +2,107 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/*
* Faces. Geist (sans) and Geist Mono, both SIL OFL 1.1, self-hosted from the
* @fontsource-variable packages. Production CSP is `font-src 'self'`, so a
* hosted font would silently fall through to the system stack; Vite resolves
* these bare-package URLs and emits the woff2 under /assets, which is 'self'.
* Latin subset only — every string on the site is English and the ids are
* ASCII; the other subsets would be 400 kB of bytes nobody renders.
*
* ABC Favorit (Prime Intellect's display mono) is a licensed trial font and is
* NOT used. Geist Mono is the honest substitute.
*/
@font-face { @font-face {
font-family: 'Manrope Variable'; font-family: 'Geist Variable';
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
font-weight: 200 800; font-weight: 100 900;
src: url('@fontsource-variable/manrope/files/manrope-latin-wght-normal.woff2') format('woff2-variations'); src:
url('@fontsource-variable/geist/files/geist-latin-wght-normal.woff2') format('woff2-variations'),
url('@fontsource-variable/geist/files/geist-latin-wght-normal.woff2') format('woff2');
}
@font-face {
font-family: 'Geist Mono Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src:
url('@fontsource-variable/geist-mono/files/geist-mono-latin-wght-normal.woff2') format('woff2-variations'),
url('@fontsource-variable/geist-mono/files/geist-mono-latin-wght-normal.woff2') format('woff2');
} }
/* /*
* PIG's token layer, with the accent inlined as literals rather than written * PIG's token layer, inlined as literals rather than written onto the root at
* onto the root at runtime. This site has no user-chosen accent, so a runtime * runtime. This site has no user-chosen accent, so a runtime theme module
* theme module would be a moving part that buys nothing here. * would be a moving part that buys nothing here.
* *
* Every colour is an HSL channel triple — `H S% L%` — so Tailwind can compose * Every colour is an HSL channel triple — `H S% L%` — so Tailwind can compose
* it with an opacity modifier (`bg-brand/10`). Writing `hsl(...)` into the * it with an opacity modifier (`bg-fg/40`). Writing `hsl(...)` into the
* variable breaks that silently: the class compiles and the opacity is ignored. * variable breaks that silently: the class compiles and the opacity is ignored.
*
* The palette adopts Prime Intellect's materials (pure-neutral greys, one green,
* amber and red for status) — measured from primeintellect.ai, not guessed. It
* does not impersonate: no PI logo, no PI wordmark, and the greys that fail
* WCAG on their own ground are lifted until they pass (see --muted).
*
* Every ratio in this file is WCAG 2.x relative-luminance contrast computed by
* scratchpad/contrast.mjs from these exact triples. Change a triple, re-run
* the script, and change the comment: a stale number here is worse than none.
*
* Rules that have no lint and rely on review:
* - `--danger` as text goes only on bg, surface, surface-2 and danger-subtle.
* Never on surface-3 or accent-subtle (4.28 / 4.13 in dark — both fail).
* - `--accent` is a FILL token. As text it is only valid in dark, where it
* equals `--accent-fg`. Use `--accent-fg` for any green text or icon.
* - Every green-filled control carries a 1px `--accent-edge` border. In light
* the fill is 1.47:1 against white; the edge is what makes it a control.
* - No alpha tints on grounds (`bg-warning/5`, `border-border/60`) — they
* vanish on #0F0F0F. Use the `*-subtle` tokens. Alpha survives only on the
* overlay scrim (`bg-fg/40`) and the light-mode CTA hover.
* - There is no `--muted-2`. Icons at rest and hairlines that must read use
* `--border-strong`, which clears 3:1 (non-text) in both themes.
*
* Bare :root is the LIGHT theme; next-themes stamps data-theme="dark" (including
* for a system-dark visitor), so no prefers-color-scheme block is needed.
*/ */
:root { :root {
--bg: 240 5% 98%; /* ---------------------------------------------------------- light grounds */
--surface: 0 0% 100%; --bg: 0 0% 98%; /* #FAFAFA */
--surface-2: 240 5% 97%; --surface: 0 0% 100%; /* #FFFFFF cards, header, panels */
--border: 240 6% 90%; --surface-2: 0 0% 96%; /* #F5F5F5 insets, idle keys, chips, row hover */
--fg: 240 10% 4%; --surface-3: 0 0% 93%; /* #EDEDED code grounds, table heads, active tab, surface-2 hover */
--muted: 240 4% 46%; --border: 0 0% 89%; /* #E3E3E3 decorative hairline: 1.28 vs surface, never the sole signal */
--border-strong: 0 0% 55%; /* #8C8C8C any boundary that IS the signal: 3.36 vs surface, 3.08 vs surface-2, 3.22 vs bg */
--accent: 262 83% 55%; /* -------------------------------------------------------------- light ink */
--accent-fg: 263 70% 46%; --fg: 0 0% 6%; /* #0F0F0F headings, claims, values: 19.17 on surface */
--accent-on: 0 0% 100%; --fg-2: 0 0% 22%; /* #383838 prose ink: 11.73 on surface, 9.92 on danger-subtle */
--accent-subtle: 270 100% 97%; --muted: 0 0% 40%; /* #666666 captions, labels: 5.74 on surface, 4.90 on surface-3, 4.86 on danger-subtle */
--positive: 160 84% 24%; /* ------------------------------------------------------------ light accent */
--warning: 32 95% 31%; --accent: 112 77% 69%; /* #83ED73 FILL ONLY — 1.47 on white as text, forbidden as text in light */
--danger: 0 72% 45%; --accent-fg: 112 60% 27%; /* #276E1C accent as text/icon/link: 6.30 on surface, 6.04 on bg, 5.33 on danger-subtle */
--info: 201 90% 32%; --accent-on: 0 0% 6%; /* #0F0F0F text on an accent fill: 13.08 */
--shadow: 240 10% 4%; --accent-edge: 112 60% 27%;/* #276E1C the 1px edge of every green-filled control: 6.30 vs white */
--accent-subtle: 112 60% 93%; /* #E5F8E2 selected rows, grader box, verified badge */
--ring: 112 60% 27%; /* #276E1C focus ring — the neon ring would be 1.47 on white */
--positive: 112 60% 27%; /* = accent-fg. One green: live, pass, solved, exact */
/* ------------------------------------------------------------ light status */
--warning: 32 95% 30%; /* #955104 text only: 6.08 on surface. Reads brown as a fill, so tiles never use it */
--warning-on: 0 0% 100%; /* text on a warning fill: 6.08 */
--warning-subtle: 39 90% 94%; /* #FDF4E2 synthetic banner, EDITED chip, counterweight chip, limits card */
--danger: 357 80% 40%; /* #B8141D 6.65 on surface, 5.63 on danger-subtle */
--danger-on: 0 0% 100%; /* text on a danger fill: 6.65 */
--danger-subtle: 357 85% 95%; /* #FDE7E8 verify mismatch ground, out-of-tolerance rows */
--info: 210 22% 36%; /* #485C70 kept for StatTone 'info' only: 6.91 on surface */
--shadow: 0 0% 6%;
/* The tab-bar underline. `--accent` is a fill token and the underline is a
2px state indicator on the page ground, so in light it takes the text
green (6.04 vs bg); in dark the two are the same colour. */
--tab-underline: 112 60% 27%;
/* /*
* Board tiles. Foregrounds are stated explicitly and measured, because the * Board tiles. Foregrounds are stated explicitly and measured, because the
@@ -44,30 +110,44 @@
* tile and a grey tile differ only by fill, and a letter that fails contrast * tile and a grey tile differ only by fill, and a letter that fails contrast
* against its own tile makes the result unreadable exactly when it matters. * against its own tile makes the result unreadable exactly when it matters.
* *
* The present tile does NOT reuse --warning. That token is 32 95% 31%, darkened * The exact and present fills are the SAME in both themes: the board is the
* until white text clears 4.5:1 on it, and at that lightness it reads BROWN. * one theme-invariant object on the site. A green-and-amber board on a white
* On a board where people arrive with a hard prior that this square is yellow, * page is deliberate and will be called "loud" on the first screenshot; the
* a brown square reads as a bug in the scorer rather than as a design choice — * answer is the glyph contrast below and the high-contrast mode. Do not
* which on a page whose whole argument is "the grader is correct" is the * soften the fills.
* single worst thing it could look like. So the fill stays a real yellow and
* the GLYPH goes dark instead, which is both more expected and higher contrast
* than the version that was trying to be careful.
* *
* Measured against each tile's own fill: * The present tile does NOT reuse --warning. That token is darkened until
* exact white on 160 84% 24% -> 6.03:1 * white text clears 4.5:1 on it, and at that lightness it reads BROWN. On a
* present near-black on 45 93% 47% -> 10.02:1 * board where people arrive with a hard prior that this square is yellow, a
* absent white on 240 4% 46% -> 4.61:1 * brown square reads as a bug in the scorer — which on a page whose whole
* argument is "the grader is correct" is the worst thing it could look like.
* So the fill stays amber and the GLYPH goes dark.
*
* Measured against each tile's own fill (light):
* exact near-black on 112 77% 69% -> 13.08
* present near-black on 39 87% 65% -> 11.16
* absent white on 0 0% 40% -> 5.74 (tile vs surface 5.74)
* exact vs present 1.17 — hue-only, same as the original game; HC mode is the path
*/ */
--tile-exact: 160 84% 24%; --tile-exact: 112 77% 69%;
--tile-exact-fg: 0 0% 100%; --tile-exact-fg: 0 0% 6%;
--tile-present: 45 93% 47%; --tile-present: 39 87% 65%;
--tile-present-fg: 240 10% 4%; --tile-present-fg: 0 0% 6%;
--tile-absent: 240 4% 46%; --tile-absent: 0 0% 40%;
--tile-absent-fg: 0 0% 100%; --tile-absent-fg: 0 0% 100%;
--tile-empty-border: 0 0% 78%; /* #C7C7C7 unplayed tile edge: 1.69 vs surface */
--dur-1: 120ms; /* ------------------------------------------------------------- non-colour */
--dur-2: 180ms; --radius-control: 0.375rem; /* 6px: buttons, inputs, tabs, selects, chips */
--dur-3: 240ms; --radius-surface: 0.75rem; /* 12px: cards, panels, sheets */
--radius-tile: 0.25rem; /* 4px: board tiles, table cells, cite chips */
--measure: 62ch; /* explaining prose */
--measure-wide: 72ch; /* ledes */
--dur-1: 120ms; /* hover, colour */
--dur-2: 180ms; /* ranking reorder, tab underline */
--dur-3: 240ms; /* sheet, drawer */
--ease-out: cubic-bezier(0.2, 0, 0, 1); --ease-out: cubic-bezier(0.2, 0, 0, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1); --ease-in: cubic-bezier(0.4, 0, 1, 1);
@@ -76,62 +156,98 @@
--safe-left: env(safe-area-inset-left, 0px); --safe-left: env(safe-area-inset-left, 0px);
--safe-right: env(safe-area-inset-right, 0px); --safe-right: env(safe-area-inset-right, 0px);
/* One bar, 56px + safe-top at EVERY width. The old lg step to 4rem is gone:
the demo tab bar sticks under this and the language's bar is 56px. */
--app-header-h: calc(3.5rem + var(--safe-top)); --app-header-h: calc(3.5rem + var(--safe-top));
/* Overlays only — sheet, drawer, tooltip, navigation panel. Flat components
(buttons, tabs, cards, chips) carry no shadow; their borders do the work. */
--shadow-overlay: 0 8px 24px hsl(var(--shadow) / 0.12);
} }
@media (min-width: 1024px) {
:root { --app-header-h: calc(4rem + var(--safe-top)); }
}
:root[data-theme='dark'] {
--bg: 240 10% 4%;
--surface: 240 8% 7%;
--surface-2: 240 6% 11%;
--border: 240 5% 18%;
--fg: 0 0% 98%;
--muted: 240 5% 60%;
--accent: 258 90% 70%;
--accent-fg: 255 92% 78%;
--accent-on: 264 80% 12%;
--accent-subtle: 260 35% 20%;
--positive: 158 64% 48%;
--warning: 38 92% 58%;
--danger: 0 84% 65%;
--info: 199 89% 58%;
/* /*
* Dark tiles lighten and take a dark glyph. Keeping the light theme's dark * Dark — the home theme. Grounds and greys are Prime Intellect's measured
* fills would have made the board the only near-black object on a near-black * values (#0F0F0F, #141414, #1C1C1C, #242424, #F5F5F5). Two of their greys
* page, so a solved row read as an empty one. * FAIL on their own ground: #737373 is 4.04:1 and #6E6E6E is 3.76:1 on
* exact 240 10% 4% on 158 64% 48% -> 7.31:1 * #0F0F0F. `--muted` is deliberately lifted to 58% (#949494, 6.32 on bg,
* present 240 10% 4% on 38 92% 58% -> 9.24:1 * 4.94 on accent-subtle) so captions pass everywhere they land. Do not
* absent 0 0% 98% on 240 5% 28% -> 8.94:1 * "correct" it back to PI's number.
*/ */
--tile-exact: 158 64% 48%; :root[data-theme='dark'] {
--tile-exact-fg: 240 10% 4%; --bg: 0 0% 6%; /* #0F0F0F */
--tile-present: 38 92% 58%; --surface: 0 0% 8%; /* #141414 */
--tile-present-fg: 240 10% 4%; --surface-2: 0 0% 11%; /* #1C1C1C */
--tile-absent: 240 5% 28%; --surface-3: 0 0% 14%; /* #242424 */
--tile-absent-fg: 0 0% 98%; --border: 0 0% 13%; /* #212121 1.14 vs surface, by design — never the sole signal */
--border-strong: 0 0% 42%; /* #6B6B6B 3.46 vs surface, 3.20 vs surface-2, 3.60 vs bg */
--fg: 0 0% 96%; /* #F5F5F5 17.58 on bg, 13.74 on accent-subtle */
--fg-2: 0 0% 80%; /* #CCCCCC 11.94 on bg, 9.33 on accent-subtle */
--muted: 0 0% 58%; /* #949494 6.32 on bg, 5.12 on surface-3, 4.94 on accent-subtle (lifted, see above) */
--accent: 112 77% 69%; /* #83ED73 fills: CTA, progress hairline, tile-exact, bars */
--accent-fg: 112 77% 69%; /* = accent in dark: 13.08 on bg, 10.22 on accent-subtle */
--accent-on: 0 0% 6%; /* #0F0F0F on accent: 13.08 */
--accent-edge: 112 77% 69%;/* = accent: the 1px edge is a no-op in dark and exists so the class is theme-blind */
--accent-subtle: 112 30% 13%; /* #1A2B17 */
--ring: 112 77% 69%;
--positive: 112 77% 69%; /* = accent */
--warning: 39 87% 65%; /* #F3BD58 PI amber: 11.16 on bg, 8.72 on accent-subtle */
--warning-on: 0 0% 6%; /* on a warning fill: 11.16 */
--warning-subtle: 39 40% 12%; /* #2B2212 */
--danger: 357 100% 60%; /* #FF333D PI red: 5.29 on bg, 5.08 on surface, 4.70 on surface-2, 4.72 on danger-subtle.
FAILS on surface-3 (4.28) and accent-subtle (4.13) — never put it there */
--danger-on: 0 0% 6%; /* on a danger fill: 5.29. `text-white` on this red is 3.63 and fails */
--danger-subtle: 357 40% 13%; /* #2E1415 */
--info: 210 16% 72%; /* #ACB8C3 near-neutral steel, StatTone 'info' only: 9.49 on bg */
--shadow: 0 0% 0%;
--tab-underline: 112 77% 69%; /* = accent */
/*
* Measured against each tile's own fill (dark):
* exact near-black on 112 77% 69% -> 13.08
* present near-black on 39 87% 65% -> 11.16
* absent 0 0% 96% on 0 0% 30% -> 7.75 (tile vs surface 2.18 — raised from 24% so an absent tile reads FILLED, not empty)
* empty border 0 0% 24% vs surface 1.70 — a visible step above --border (1.14), quieter than border-strong across 30 tiles
*/
--tile-exact: 112 77% 69%;
--tile-exact-fg: 0 0% 6%;
--tile-present: 39 87% 65%;
--tile-present-fg: 0 0% 6%;
--tile-absent: 0 0% 30%;
--tile-absent-fg: 0 0% 96%;
--tile-empty-border: 0 0% 24%;
--shadow-overlay: 0 8px 24px hsl(var(--shadow) / 0.35);
} }
/* /*
* High contrast. Green/yellow/grey is a colour-only distinction, which is * High contrast — tiles only. Green/amber/grey is a colour-only distinction,
* exactly why the original game ships a high-contrast mode. Toggled from the * which is exactly why the original game ships a high-contrast mode. Toggled
* header and persisted per-viewer; the tiles also carry a glyph in this mode, * from the header and the Play tab, persisted per-viewer; the tiles also carry
* a glyph (● exact, ◆ present, none for absent) at full opacity in this mode,
* so the result survives being printed, screenshotted or seen by someone with * so the result survives being printed, screenshotted or seen by someone with
* deuteranopia. * deuteranopia.
*
* White glyph on each fill:
* exact 209 100% 40% (#0069CC) -> 5.39 (3.41 vs the dark surface, 5.39 vs the light one)
* present 27 100% 36% (#B85300) -> 4.91 THE PREVIOUSLY SHIPPED 27 100% 42% MEASURES 3.80 AND FAILS. One number, committed here.
* absent 0 0% 38% (#616161) -> 6.19 light theme; 2.97 vs the dark surface, hence the override below
* exact vs present 1.10 — the glyphs are the distinction, not the hues
*/ */
:root[data-contrast='high'] { :root[data-contrast='high'] {
--tile-exact: 209 100% 40%; --tile-exact: 209 100% 40%;
--tile-present: 27 100% 42%; --tile-present: 27 100% 36%;
--tile-absent: 240 4% 38%; --tile-absent: 0 0% 38%;
--tile-exact-fg: 0 0% 100%; --tile-exact-fg: 0 0% 100%;
--tile-present-fg: 0 0% 100%; --tile-present-fg: 0 0% 100%;
--tile-absent-fg: 0 0% 100%; --tile-absent-fg: 0 0% 100%;
} }
:root[data-theme='dark'][data-contrast='high'] {
--tile-absent: 0 0% 45%; /* #737373: white glyph 4.74; 3.89 vs the dark surface so it still reads filled */
}
@layer base { @layer base {
* { @apply border-border; } * { @apply border-border; }
@@ -140,8 +256,12 @@
/* Painted explicitly: the viewer's chrome shows through a transparent body /* Painted explicitly: the viewer's chrome shows through a transparent body
and the page would borrow the host's theme instead of its own. */ and the page would borrow the host's theme instead of its own. */
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
/* Tabular figures site-wide: every number on this site is compared to
another number, and a proportional "1" in a reward column makes a
ranking look re-ordered when it is not. */
font-variant-numeric: tabular-nums;
} }
/* Tabular figures everywhere a number is compared to another number. */ /* Kept as an explicit no-op for compat: body already sets tabular-nums. */
.nums { font-variant-numeric: tabular-nums; } .nums { font-variant-numeric: tabular-nums; }
:focus-visible { @apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-bg; } :focus-visible { @apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-bg; }
} }
@@ -153,6 +273,32 @@
.tap { @apply min-h-11 min-w-11; } .tap { @apply min-h-11 min-w-11; }
} }
/*
* Touch targets follow the POINTER, not the width. Components step their
* controls down from 44px to 36-40px at `lg` (1024px) on the assumption that
* a 1024px viewport has a mouse. An iPad in landscape is exactly 1024px and
* has a finger; measured there, the header icons were 36px, the nav triggers
* 40px and the nested tab strips 36px. On a coarse pointer every control keeps
* the 44px floor at every width, and icon-only controls (an aria-label and a
* lone svg) keep it on both axes. `:root :is(...)` outranks the single-class
* `lg:min-h-9` utilities without `!important`. `a[href]` rather than a class:
* the header CTA is `hidden lg:inline-flex`, so no display class is reliable,
* and inline links in prose are unaffected because min-height does not apply
* to inline boxes.
*/
@media (pointer: coarse) {
:root :is(button, a[href], [role='button'], [role='tab'], [role='radio'], [role='switch'], [role='slider'], summary, select) {
min-height: 2.75rem;
}
/* Icon-only controls, and the segmented options ("#1", "1×") whose text is
two glyphs wide, keep the floor on both axes. Segments wrap, so this
cannot widen a row past the viewport. */
:root :is(button, a)[aria-label]:has(> svg:only-child),
:root [role='radio'] {
min-width: 2.75rem;
}
}
/* /*
* Reduced motion clamps everything to effectively zero. That is correct, and * Reduced motion clamps everything to effectively zero. That is correct, and
* it is also why every state change in this app must ALSO be announced: with * it is also why every state change in this app must ALSO be announced: with
@@ -172,7 +318,7 @@
* Board motion. Declared here rather than in the demo because Tailwind's * Board motion. Declared here rather than in the demo because Tailwind's
* arbitrary `animate-[...]` needs the keyframes to exist in the stylesheet, and * arbitrary `animate-[...]` needs the keyframes to exist in the stylesheet, and
* a demo-local <style> block would be a second place for the token values to * a demo-local <style> block would be a second place for the token values to
* drift from. * drift from. Pop 120ms, flip 520ms staggered 100ms/tile, shake 400ms.
*/ */
@keyframes tile-flip { @keyframes tile-flip {
0% { transform: rotateX(0deg); } 0% { transform: rotateX(0deg); }
+55 -4
View File
@@ -1,11 +1,62 @@
import { clsx, type ClassValue } from 'clsx'; import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { extendTailwindMerge } from 'tailwind-merge';
/** /**
* The shadcn `cn`. `twMerge` is v2 here — importing from a v3 path * The shadcn `cn`, on a `twMerge` that knows this project's theme.
* (`tailwind-merge/v3` or the `createTailwindMerge` split entry) resolves at *
* type level and then fails at bundle time, so keep this import bare. * `tailwind-merge` is v2 here — importing from a v3 path (`tailwind-merge/v3`
* or the `createTailwindMerge` split entry) resolves at type level and then
* fails at bundle time, so keep this import bare.
*
* The extension is load-bearing, not cosmetic. Stock `twMerge` only knows the
* t-shirt font sizes, so it files every custom `text-<size>` from
* tailwind.config.js (`text-label`, `text-ui`, `text-h3`, …) under text
* COLOUR — and `cn('text-label text-muted')` silently drops the size. Every
* `cva` variant and every `className` override runs through here, so without
* this list the whole type scale loses to the first colour class it meets.
* The other groups exist so an override can actually override
* (`shadow-overlay` beats `shadow-sm`, `rounded-control` beats `rounded-md`).
* Keep these lists in step with `theme.extend` in tailwind.config.js.
*/ */
const FONT_SIZES = [
'display',
'display-lg',
'display-xl',
'h1',
'h1-lg',
'h2',
'h2-lg',
'h3',
'lede',
'lede-lg',
'claim',
'claim-lg',
'prose',
'ui',
'small',
'caption',
'label',
'mono-data',
'stat',
'metric',
'figure',
'figure-lg',
'code',
];
const twMerge = extendTailwindMerge({
extend: {
classGroups: {
'font-size': [{ text: FONT_SIZES }],
shadow: [{ shadow: ['overlay'] }],
rounded: [{ rounded: ['tile', 'control', 'surface'] }],
tracking: [{ tracking: ['label'] }],
'max-w': [{ 'max-w': ['canvas', 'measure', 'measure-wide'] }],
ease: [{ ease: ['enter', 'exit'] }],
},
},
});
export function cn(...inputs: ClassValue[]): string { export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
+51 -52
View File
@@ -1,42 +1,32 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { DefinitionStrip } from '@/components/site/DefinitionStrip';
import { import {
ALL_VERTICALS, ALL_VERTICALS,
EnvironmentGrid, EnvironmentList,
FeaturedEnvironment, FeaturedEnvironment,
environments, environments,
filterKeys, filterKeys,
VerticalFilter, VerticalFilter,
type VerticalFilterValue, type VerticalFilterValue,
} from '@/components/site/EnvironmentPicker'; } from '@/components/site/EnvironmentPicker';
import { routes } from '@/content/lineup'; import { pad2, routes } from '@/content/lineup';
import * as s from '@/content/styles';
import { PROPOSAL_NOTICE } from '@/content/verticals'; import { PROPOSAL_NOTICE } from '@/content/verticals';
import { VERTICAL_LABELS } from '@/lib/demo-kit/registry'; import { VERTICAL_LABELS } from '@/lib/demo-kit/registry';
import type { Vertical } from '@/lib/demo-kit/types'; import type { Vertical } from '@/lib/demo-kit/types';
import * as s from '@/content/styles';
import { pageTitle, useSeo } from '@/lib/seo'; import { pageTitle, useSeo } from '@/lib/seo';
/** /**
* The fuller list. * The fuller list.
* *
* Home leads with the picker; this is where you go to see everything and cut it * Home leads with the picker; this is where you go to see everything and cut it
* by industry. The cards are the same component, so the two pages cannot drift * by industry. The built environments are the same `FeaturedEnvironment` cards
* into describing the same environment two different ways — which is how an * and the written ones the same `EnvironmentRow` rows, so the two pages cannot
* honest site quietly becomes a dishonest one. * drift into describing the same environment two different ways — which is how
* an honest site quietly becomes a dishonest one.
*/ */
/** What a `spec` has to contain before it is allowed on this page. Contract rule 12. */
const SPEC_PARTS: readonly { label: string; body: string }[] = [
{ label: 'A task', body: 'One unit of work with a beginning and an end.' },
{ label: 'Legal moves', body: 'What the agent is allowed to do, and what is refused.' },
{ label: 'A grader', body: 'Deterministic code that marks the attempt. No judge, no rubric.' },
{
label: 'A counterweight',
body: 'The term that stops the objective being maximised the crude way.',
},
];
export default function Gallery() { export default function Gallery() {
useSeo({ useSeo({
title: pageTitle('Every environment'), title: pageTitle('Every environment'),
@@ -74,14 +64,15 @@ export default function Gallery() {
} }
return ( return (
<main className={`${s.shell} py-10 sm:py-16`}> <main className={`${s.shell} py-10 sm:py-14`}>
<p className={s.eyebrow}>Gallery</p> <p className={s.label}>
<h1 className={`${s.h1} mt-3 max-w-3xl`}>Every environment we have built or specified.</h1> Gallery · {pad2(environments.length)} environments
<p className={`${s.lede} mt-5 max-w-2xl`}> </p>
A built environment is playable in this tab, on the same code the repository ships. A spec is <h1 className={`${s.h1} mt-3 max-w-[24ch]`}>Every environment we have built or specified.</h1>
a written environment task, action set, grader, counterweight and the command that <p className={`${s.lede} mt-4`}>
evaluates it published in full, with no interactive surface yet. There are no coming-soon A built environment is playable in this tab, on the code the repository ships. A spec is a
cards here. written environment task, legal actions, grader, counterweight and the command that
evaluates it published in full, with no board yet. There are no coming-soon cards here.
</p> </p>
{filterKeys.length > 1 ? ( {filterKeys.length > 1 ? (
@@ -95,14 +86,21 @@ export default function Gallery() {
The count changes without a page load and without moving focus, so it 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. is announced. Without this, a filter tap is silent to a screen reader.
*/} */}
<p aria-live="polite" className="mt-6 text-sm text-muted"> <p aria-live="polite" className={`${s.label} mt-6`}>
{shown.length === 1 ? '1 environment' : `${shown.length} environments`} {[
{active === ALL_VERTICALS ? '' : ` in ${VERTICAL_LABELS[active]}`} `${pad2(shown.length)} ${shown.length === 1 ? 'environment' : 'environments'}`,
{shown.length === 0 ? '' : ` · ${built.length} built · ${written.length} written`} active === ALL_VERTICALS ? null : VERTICAL_LABELS[active],
// Zero segments are dropped, not printed as `00`: a filter with no
// live entry says so by omission, the same way the landing does.
built.length > 0 ? `${pad2(built.length)} live` : null,
written.length > 0 ? `${pad2(written.length)} spec` : null,
]
.filter((part): part is string => part !== null)
.join(' · ')}
</p> </p>
{shown.length === 0 ? ( {shown.length === 0 ? (
<div className="card mt-4 p-6"> <div className="card mt-4 p-4 sm:p-5">
{/* Two different nothings. Telling a visitor "no environment is filed {/* Two different nothings. Telling a visitor "no environment is filed
under this vertical" when they have not filtered anything reads as under this vertical" when they have not filtered anything reads as
a broken page rather than an empty one. */} a broken page rather than an empty one. */}
@@ -129,15 +127,17 @@ export default function Gallery() {
) : null} ) : null}
{built.length > 0 ? ( {built.length > 0 ? (
<section aria-labelledby="built" className="mt-4"> <section aria-labelledby="built" className="mt-6">
<h2 className={s.h2} id="built"> <h2 className={s.h2} id="built">
{built.length === 1 ? 'The one you can play' : 'The ones you can play'} {built.length === 1 ? 'The one you can play' : 'The ones you can play'}
</h2> </h2>
<div className="mt-4 space-y-4"> <ul className={`mt-4 grid gap-4 ${built.length >= 2 ? 'lg:grid-cols-2' : ''}`}>
{built.map((entry) => ( {built.map((entry) => (
<FeaturedEnvironment entry={entry} key={entry.id} /> <li className="flex" key={entry.id}>
<FeaturedEnvironment className="w-full" entry={entry} />
</li>
))} ))}
</div> </ul>
</section> </section>
) : null} ) : null}
@@ -147,32 +147,31 @@ export default function Gallery() {
<h2 className={s.h2} id="written"> <h2 className={s.h2} id="written">
{written.length === 1 ? 'The one that is written' : `The ${written.length} that are written`} {written.length === 1 ? 'The one that is written' : `The ${written.length} that are written`}
</h2> </h2>
<span className={s.proposalPill}>{PROPOSAL_NOTICE}</span> <span className={s.chipOutline}>{PROPOSAL_NOTICE}</span>
</div> </div>
<EnvironmentGrid className="mt-4" entries={written} /> {/* The same rows as the landing, with the same numerals: a filtered
list keeps each row's place in the whole lineup rather than
renumbering from 01, so `04` means the same thing on both pages. */}
<EnvironmentList className="mt-4" entries={written} />
</section> </section>
) : null} ) : null}
<div className="card mt-12 p-5 sm:p-7"> {/* What a spec has to contain — the same four parts as everywhere else,
<h2 className={s.h2}>What a spec has to contain</h2> read from the same source, so this page cannot teach a fifth word. */}
<p className={`${s.prose} mt-3 max-w-2xl`}> <section aria-labelledby="contains" className="mt-12 sm:mt-16">
<h2 className={s.h2} id="contains">
What a spec has to contain
</h2>
<p className={`${s.prose} mt-3`}>
A card that says a demo is coming is not a specification, and the build refuses one. Every A card that says a demo is coming is not a specification, and the build refuses one. Every
written environment above states all four of these, and the reward weights that go with written environment above states all four of these, and the counterweight that stops the
them, before it is allowed on this page. score being farmed, before it is allowed on this page.
</p> </p>
<ol className="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> <DefinitionStrip className="mt-6" />
{SPEC_PARTS.map((part, i) => (
<li className="card bg-surface-2 p-4" key={part.label}>
<span className="nums text-xs font-semibold text-brand">0{i + 1}</span>
<h3 className={`${s.h3} mt-1.5`}>{part.label}</h3>
<p className={`${s.prose} mt-1.5 text-sm`}>{part.body}</p>
</li>
))}
</ol>
<Link className={`${s.btnSecondary} mt-6`} to={routes.honesty}> <Link className={`${s.btnSecondary} mt-6`} to={routes.honesty}>
What we measured, and what we didnt What we measured, and what we didnt
</Link> </Link>
</div> </section>
</main> </main>
); );
} }
+402 -100
View File
@@ -1,37 +1,208 @@
import { useEffect, useRef, useState, type ReactNode, type RefObject } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ArrowRight, ArrowUpRight } from 'lucide-react'; import { ArrowDown, ArrowRight, ArrowUpRight } from 'lucide-react';
import { EnvAnatomy } from '@/components/demo/EnvAnatomy';
import { DefinitionStrip } from '@/components/site/DefinitionStrip';
import { import {
EnvironmentGrid, EnvironmentList,
FeaturedEnvironment, FeaturedEnvironment,
builtEnvironments, builtEnvironments,
environments,
writtenEnvironments, writtenEnvironments,
} from '@/components/site/EnvironmentPicker'; } from '@/components/site/EnvironmentPicker';
import { Term } from '@/components/site/Term';
import { TileRow } from '@/components/site/TileRow';
import { Skeleton } from '@/components/ui/skeleton';
import { helloWorldCitations, trainingResult } from '@/content/evidence'; import { helloWorldCitations, trainingResult } from '@/content/evidence';
import { routes } from '@/content/lineup'; import { CountWord, countWord, pad2, routes } from '@/content/lineup';
import { PROPOSAL_NOTICE } from '@/content/verticals';
import * as s from '@/content/styles'; import * as s from '@/content/styles';
import { PROPOSAL_NOTICE } from '@/content/verticals';
import { listRuns, loadDemoModule, loadEpisode, verifyEpisode, verifySummary } from '@/lib/demo-kit';
import type { AnyDemoModule, RunRef, VerifyResult } from '@/lib/demo-kit';
import { pageTitle, useSeo } from '@/lib/seo'; import { pageTitle, useSeo } from '@/lib/seo';
/** /**
* The landing page: a line of thesis, then the picker. * The landing page. Definition before catalogue, catalogue before reading list.
* *
* The page used to open with the argument and bury the demos below it. That is * The h1 is the site's thesis and it is jargon on its own, so the lede opens
* an essay, and an executive who has read the first sentence has already * with the plain definition and the four-part strip under it names the parts
* decided whether to click something. So the picker is the primary object here * a visitor will meet again on every Play and Evidence tab. Then the picker,
* and everything that used to lead — the credential, the measured number — sits * then the machine explained once with the live demo's own numbers, then the
* underneath it as support for a visitor who wants it before they click. * twelve written environments as a list, then the evidence.
* *
* Counts in the copy are derived from the registry and the lineup, never typed. * Counts in the copy are derived from the registry and the lineup, never
* A hard-coded "eleven" on a site whose whole argument is that its claims are * typed. The three figures in "How it works" are read off the featured demo's
* checkable goes stale the first time a demo ships. * own module and its newest recorded run, loaded lazily when the section
* scrolls near — never typed, and never on the first paint's critical path.
*/ */
/* ------------------------------------------------------------ lazy figures */
/** True once the element has come within a screen of the viewport. */
function useNearViewport<T extends Element>(): [RefObject<T | null>, boolean] {
const ref = useRef<T | null>(null);
const [near, setNear] = useState(false);
useEffect(() => {
const node = ref.current;
if (!node || near) return undefined;
// No observer (a prerender capture, an old engine) means "load now": a
// missing figure is worse than an early fetch.
if (typeof IntersectionObserver === 'undefined') {
setNear(true);
return undefined;
}
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) setNear(true);
},
{ rootMargin: '100% 0px' },
);
observer.observe(node);
return () => observer.disconnect();
}, [near]);
return [ref, near];
}
/** The most recently captured run. Ties keep manifest order, last wins. */
function newestRun(runs: readonly RunRef[]): RunRef | undefined {
let best: RunRef | undefined;
for (const run of runs) {
if (!best || run.capturedAt >= best.capturedAt) best = run;
}
return best;
}
interface Figures {
module: AnyDemoModule | null;
verify: VerifyResult | null;
}
/**
* The featured demo's module and the verification of its newest run.
*
* Both start `null` and stay `null` on any failure: the placeholders print
* dashes, which is the honest rendering of "not loaded" — a zero here would be
* a claim about the grader.
*/
function useFigures(slug: string | null, near: boolean): Figures {
const [figures, setFigures] = useState<Figures>({ module: null, verify: null });
useEffect(() => {
if (!slug || !near) return undefined;
let live = true;
void loadDemoModule(slug)
.then(async (module) => {
if (!live) return;
setFigures({ module, verify: null });
const run = newestRun(await listRuns(slug));
if (!run) return;
const episode = await loadEpisode(run);
if (!live) return;
setFigures({ module, verify: verifyEpisode(module, episode) });
})
.catch(() => {
// The passages still read without their figures. The demo page has
// its own error card; the landing does not repeat it.
});
return () => {
live = false;
};
}, [slug, near]);
return figures;
}
/** A reward total, three places; `—` for not scored. */
function fixed(value: number | null, digits: number): string {
if (value === null || !Number.isFinite(value)) return '—';
// Below the last printed digit a negative rounding residue would print as
// "-0.0000000", which reads as a disagreement that is not there.
const clamped = Math.abs(value) < 10 ** -digits / 2 ? 0 : value;
return clamped.toFixed(digits);
}
/* ------------------------------------------------------------------ pieces */
function SectionLabel({ children, id }: { children: ReactNode; id?: string }) {
return (
<p className={s.label} id={id}>
{children}
</p>
);
}
/**
* One numbered passage in "How it works": a claim, a paragraph and a mono
* figure. Figure right from `lg`, under the paragraph on a phone.
*/
function Passage({
index,
title,
figure,
children,
}: {
index: string;
title: string;
figure: ReactNode;
children: ReactNode;
}) {
return (
<li className="grid gap-4 border-t border-border py-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,22rem)] lg:gap-10">
<div>
<h3 className={`${s.h3} flex items-baseline gap-3`}>
<span className="font-mono text-label uppercase text-muted">{index}</span>
{title}
</h3>
<p className={`${s.prose} mt-2`}>{children}</p>
</div>
<div className="min-w-0 lg:pt-1">{figure}</div>
</li>
);
}
/**
* A mono figure with a caption. A caption that is data (a call, a run id) is
* mono; one that is a sentence (the verifier's verdict) is sans, because it
* has a verb in it.
*/
function Figure({
caption,
sentence = false,
children,
}: {
caption: ReactNode;
sentence?: boolean;
children: ReactNode;
}) {
return (
<figure className="rounded-lg border border-border bg-surface-2 p-3">
<div className="min-w-0">{children}</div>
<figcaption
className={
sentence ? 'mt-2 text-small text-fg-2' : 'mt-2 break-words font-mono text-caption text-muted'
}
>
{caption}
</figcaption>
</figure>
);
}
/** The figure's own line: mono, wrapping at the operators rather than clipping. */
const FIGURE_LINE = 'break-words font-mono text-mono-data text-fg';
/* -------------------------------------------------------------------- page */
export default function Home() { export default function Home() {
// The head is set here as well as baked by `scripts/prerender.mjs`, and the // The head is set here as well as baked by `scripts/prerender.mjs`, and the
// two are not redundant: prerender covers the crawler that fetches the // two are not redundant: prerender covers the crawler that fetches the
// document, this covers the visitor who arrives on another route and clicks // document, this covers the visitor who arrives on another route and clicks
// through without ever fetching a new one. Prerender prefers whatever the // through without ever fetching a new one.
// app set, so this is also the single source for the baked tags.
useSeo({ useSeo({
title: pageTitle(), title: pageTitle(),
description: description:
@@ -40,75 +211,210 @@ export default function Home() {
ogImage: '/og/home.png', ogImage: '/og/home.png',
}); });
const [featured, ...otherBuilt] = builtEnvironments; const featured = builtEnvironments[0];
const builtCount = builtEnvironments.length; const builtCount = builtEnvironments.length;
const writtenCount = writtenEnvironments.length; const writtenCount = writtenEnvironments.length;
const totalCount = environments.length;
// Written out rather than interpolated inline so the zero and the singular const [howRef, howNear] = useNearViewport<HTMLElement>();
// both read as English. Both numbers come from the data — `VERTICALS` has const { module, verify } = useFigures(featured?.slug ?? null, howNear);
// twelve entries and none of them is keyed `reference`, so the one live demo
// suppresses no proposal and the lineup reads one and twelve today. It will // The mono eyebrow. Zero segments are dropped rather than printed as `00`.
// not stay that way, and the sentence has to survive the day it changes. const eyebrow = [
const builtSentence = 'Environments',
builtCount === 1 builtCount > 0 ? `${pad2(builtCount)} live` : null,
? 'One environment is built and playable in this tab.' writtenCount > 0 ? `${pad2(writtenCount)} spec` : null,
: `${builtCount} environments are built and playable in this tab.`; ]
const writtenSentence = .filter((part): part is string => part !== null)
writtenCount === 1 .join(' · ');
? 'One more is a written specification:'
: `The other ${writtenCount} are written specifications:`; // The second sentence of the lede, in English at every count.
const builtClause =
builtCount === 0
? 'None of these is built yet'
: `${CountWord(builtCount)} of these ${builtCount === 1 ? 'is' : 'are'} built and playable`;
const writtenClause =
writtenCount === 0
? ''
: `; ${countWord(writtenCount)} more ${writtenCount === 1 ? 'is' : 'are'} written`;
const pickSentence =
builtCount === 0
? 'Nothing is playable yet; every environment below is a written specification.'
: `${CountWord(builtCount)} ${builtCount === 1 ? 'environment is' : 'environments are'} built and playable in this tab.${
writtenCount === 0
? ''
: ` The other ${countWord(writtenCount)} ${writtenCount === 1 ? 'is' : 'are'} written: the task, the grader, the counterweight and the command that evaluates it.`
}`;
const equation = module
? `${module.reward.components
.map((component) => `${component.weight.toFixed(2)} · ${component.key}`)
.join(' + ')} = reward`
: null;
return ( return (
<main> <main>
{/* ── The thesis, in two sentences ───────────────────────────────── */} {/* ── 1. Masthead ─────────────────────────────────────────────────── */}
<section className={`${s.shell} pt-10 sm:pt-16`}> <section className={`${s.shell} pt-10 sm:pt-14`}>
<p className={s.eyebrow}>Environments, demonstrated</p> <SectionLabel>{eyebrow}</SectionLabel>
<h1 className={`${s.h1} mt-3 max-w-4xl`}> <h1 className={`${s.display} mt-3 max-w-[20ch]`}>
An environment is an eval you can take the gradient of. An environment is an eval you can take the gradient of.
</h1> </h1>
<p className={`${s.lede} mt-5 max-w-2xl`}> <p className={`${s.lede} mt-5`}>
You write down what good means, in code, and then you train against that number and watch A task, the moves that are allowed, a grader that computes instead of judging, and a score
it move or watch it sit still, which you found out in an afternoon instead of a quarter. that moves when the model gets better. {builtClause}
{writtenClause}.
</p> </p>
<DefinitionStrip className="mt-8" />
<div className="mt-6 flex flex-col gap-3 sm:flex-row sm:items-center">
{featured ? (
<Link className={`${s.btnPrimary} min-h-12 sm:min-h-11`} to={featured.href}>
Play {featured.title}
<ArrowRight aria-hidden="true" className="size-4" />
</Link>
) : null}
<a className={`${s.btnSecondary} min-h-12 sm:min-h-11`} href="#how">
What an environment is
<ArrowDown aria-hidden="true" className="size-4" />
</a>
</div>
</section> </section>
{/* ── The picker: the built one ──────────────────────────────────── */} {/* ── 2. Pick one ─────────────────────────────────────────────────── */}
<section aria-labelledby="pick" className={`${s.shell} mt-10 sm:mt-14`}> <section aria-labelledby="pick" className={`${s.shell} ${s.section}`} id="play">
<h2 className={s.h2} id="pick"> <SectionLabel>Playable</SectionLabel>
<h2 className={`${s.h2} mt-2`} id="pick">
Pick one. You are playing it in a click. Pick one. You are playing it in a click.
</h2> </h2>
<p className={`${s.prose} mt-3 max-w-3xl`}> <p className={`${s.prose} mt-3`}>{pickSentence}</p>
{builtSentence}{' '}
{writtenCount === 0 ? null : (
<>
{writtenSentence} the task, the grader, the counterweight that stops the grader being
farmed, and the command that evaluates it. Nothing here is a coming-soon card.
</>
)}
</p>
{featured ? <FeaturedEnvironment className="mt-6" entry={featured} /> : null} {builtCount > 0 ? (
{otherBuilt.length > 0 ? <EnvironmentGrid className="mt-4" entries={otherBuilt} /> : null} <ul
className={`mt-6 grid gap-4 ${
builtCount >= 3 ? 'lg:grid-cols-3' : builtCount === 2 ? 'lg:grid-cols-2' : ''
}`}
>
{builtEnvironments.map((entry) => (
<li className="flex" key={entry.id}>
<FeaturedEnvironment className="w-full" entry={entry} />
</li>
))}
</ul>
) : null}
</section> </section>
{/* ── The picker: the written ones ───────────────────────────────── */} {/* ── 3. How it works ─────────────────────────────────────────────── */}
{/* Dropped entirely rather than rendered as "The 0 we have written": the <section
day every proposal becomes a demo, this section should disappear. */} aria-labelledby="how-heading"
className={`${s.shell} ${s.section} scroll-mt-[var(--app-header-h)]`}
id="how"
ref={howRef}
>
<SectionLabel>
The same machine, {countWord(totalCount)} times
</SectionLabel>
<h2 className={`${s.h2} mt-2`} id="how-heading">
The same machine, four times over.
</h2>
<p className={`${s.prose} mt-3`}>
Every environment on this site is the four parts above. The task changes and the grader
changes; the shape never does. Here it is on the {featured ? featured.title : 'first'}{' '}
board, with that environments own text in each box.
</p>
<div className="mt-6">
{module ? (
<EnvAnatomy anatomy={module.anatomy} rewardLine={module.meta.rewardLine} />
) : (
// The same footprint as the four boxes, so the page does not jump
// when the module lands.
<div aria-hidden="true" className="grid gap-3 lg:grid-cols-4">
{[0, 1, 2, 3].map((i) => (
<Skeleton className="h-32 rounded-xl" key={i} />
))}
</div>
)}
</div>
<ol className="mt-8 border-b border-border">
<Passage
figure={
<Figure caption="compare(guess, answer) → G Y X X G">
<TileRow />
</Figure>
}
index="01"
title="The grader computes."
>
Five letters go in, five verdicts come out, and the same guess scores the same way every
time. No judge reads the reply and no rubric is consulted. A recorded{' '}
<Term term="rollout">rollout</Term> is a file of those comparisons, one per turn.
</Passage>
<Passage
figure={
<Figure caption={module ? `${module.meta.title} · shipped weights` : 'shipped weights'}>
<p className={`min-h-[1.4em] ${FIGURE_LINE}`}>
{equation ?? <span className="text-muted"> · + · + · = reward</span>}
</p>
</Figure>
}
index="02"
title="The reward is the product."
>
Pay for winning, take away for wasted guesses, and the model will find whichever
behaviour you forgot to price. The term that takes points away is the{' '}
<Term term="counterweight">counterweight</Term>; it is the half of the reward worth an
argument.
</Passage>
<Passage
figure={
<Figure
caption={
verify
? verifySummary(verify)
: 'Recomputed in your browser from the recorded moves, once they load.'
}
sentence
>
<p className={FIGURE_LINE}>
recomputed {fixed(verify?.recomputed ?? null, 3)} · recorded{' '}
{fixed(verify?.recorded ?? null, 3)} · Δ {fixed(verify?.delta ?? null, 7)}
</p>
</Figure>
}
index="03"
title="The numbers are receipts."
>
Every score on this site is recomputed in your browser from the recorded moves and set
beside the number in the file. The one published training result is measured on{' '}
<Term term="held-out">held-out</Term> words the model never trained on.
</Passage>
</ol>
</section>
{/* ── 4. Written but not built ────────────────────────────────────── */}
{writtenCount === 0 ? null : ( {writtenCount === 0 ? null : (
<section aria-labelledby="written" className={`${s.shell} mt-10 sm:mt-14`}> <section aria-labelledby="written" className={`${s.shell} ${s.section}`}>
<div className="flex flex-wrap items-end justify-between gap-3"> <div className="flex flex-wrap items-end justify-between gap-3">
<h2 className={s.h2} id="written"> <h2 className={s.h2} id="written">
The {writtenCount} we have written but not built. The {writtenCount} we have written but not built.
</h2> </h2>
<span className={s.proposalPill}>{PROPOSAL_NOTICE}</span> <span className={s.chipOutline}>{PROPOSAL_NOTICE}</span>
</div> </div>
<p className={`${s.prose} mt-3 max-w-3xl`}> <p className={`${s.prose} mt-3`}>
Each one is a task an environment could run, a reward stated in a number your board already Each one is a task, a reward in a number your board already reads, and the counterweight
reads, and the counterweight that stops that reward being maximised the crude way. Every that stops that reward being farmed. Every row opens the whole argument, including where
card opens the whole argument, including where it stops being honest. it stops being honest.
</p> </p>
<EnvironmentGrid className="mt-6" entries={writtenEnvironments} /> {/* A list, not a wall of cards: twelve rows the eye can scan in one
pass, each ranked in the order we would build it. */}
<EnvironmentList className="mt-6" entries={writtenEnvironments} />
<Link className={`${s.btnSecondary} mt-6`} to={routes.gallery}> <Link className={`${s.btnSecondary} mt-6`} to={routes.gallery}>
Filter the lineup by industry Filter the lineup by industry
@@ -117,35 +423,36 @@ export default function Home() {
</section> </section>
)} )}
{/* ── The support: why this game, and the one measured number ────── */} {/* ── 5. Evidence ─────────────────────────────────────────────────── */}
<section aria-labelledby="evidence" className={`${s.shell} ${s.section}`}> <section aria-labelledby="evidence" className={`${s.shell} ${s.section}`}>
<h2 className="sr-only" id="evidence"> <h2 className="sr-only" id="evidence">
Why this environment, and what has been measured on it Why this environment, and what has been measured on it
</h2> </h2>
<div className="grid gap-4 lg:grid-cols-2"> <div className="grid gap-4 lg:grid-cols-2">
<div className="card p-5 sm:p-7"> <div className="card p-4 sm:p-5">
<p className={s.eyebrow}>Why a word game</p> <SectionLabel>Why a word game</SectionLabel>
<h3 className="mt-2 text-xl font-bold tracking-tight text-fg sm:text-2xl"> <h3 className={`${s.h3} mt-2`}>We didnt pick a game. We picked theirs.</h3>
We didnt pick a game. We picked theirs.
</h3>
<p className={`${s.prose} mt-3`}> <p className={`${s.prose} mt-3`}>
Wordle is Prime Intellects own hello-world: a basic example in their trainer, a The word game is Prime Intellects own hello-world: a basic example in their trainer, a
shipped environment in their library, and the environment their official tutorial shipped environment in their library, and the environment their official tutorial
optimises prompts against. optimises prompts against.
</p> </p>
<ul className="mt-4 space-y-2"> <ul className="mt-4 divide-y divide-border">
{helloWorldCitations.map((c) => ( {helloWorldCitations.map((c) => (
<li key={c.href}> <li key={c.href}>
<a <a
className="tap flex items-start gap-2 rounded-md py-1 text-sm text-muted transition-colors duration-1 ease-enter hover:text-fg" className="tap flex items-start gap-2 py-2.5 transition-colors duration-1 ease-enter hover:text-fg"
href={c.href} href={c.href}
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
<ArrowUpRight aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-brand" /> <ArrowUpRight
<span> aria-hidden="true"
<span className="font-mono text-[0.8125rem] text-fg">{c.label}</span> className="mt-0.5 size-4 shrink-0 text-border-strong"
<span className="block">{c.claim}</span> />
<span className="min-w-0">
<span className="block font-mono text-mono-data text-fg">{c.label}</span>
<span className="block text-small text-fg-2">{c.claim}</span>
<span className="sr-only">(opens in a new tab)</span> <span className="sr-only">(opens in a new tab)</span>
</span> </span>
</a> </a>
@@ -154,26 +461,25 @@ export default function Home() {
</ul> </ul>
</div> </div>
<div className="card p-5 sm:p-7"> {/* A citation of a published result, worded as one. No logo, no
<p className={s.eyebrow}>Published by Prime Intellect</p> "partner", nothing that reads as endorsement. */}
<p className="nums mt-3 flex flex-wrap items-baseline gap-x-3 gap-y-1"> <div className="card p-4 sm:p-5">
<span className="text-4xl font-extrabold tracking-tight text-muted sm:text-5xl"> <SectionLabel>Published by Prime Intellect</SectionLabel>
{trainingResult.before} <p className="mt-3 flex flex-wrap items-baseline gap-x-3 gap-y-1 font-mono">
</span> <span className="text-figure text-muted lg:text-figure-lg">{trainingResult.before}</span>
<ArrowRight aria-hidden="true" className="size-6 shrink-0 text-muted" /> <ArrowRight aria-hidden="true" className="size-6 shrink-0 self-center text-border-strong" />
<span className="text-4xl font-extrabold tracking-tight text-positive sm:text-5xl"> <span className="text-figure text-positive lg:text-figure-lg">{trainingResult.after}</span>
{trainingResult.after}
</span>
</p> </p>
<p className={`${s.prose} mt-3`}> <p className={`${s.prose} mt-3`}>
{trainingResult.model} {trainingResult.metric} on this task, before and after training. <span className="font-mono text-mono-data text-fg">{trainingResult.model}</span>{' '}
Out of the box it never once guesses the word; after an {trainingResult.method}, it {trainingResult.metric} on this task, before and after training. Out of the box it
wins about six games in ten. Measured on {trainingResult.evalDescription}, and both never once guesses the word; after an {trainingResult.method}, it wins about six games
checkpoints are public, so the claim is checkable rather than quotable. in ten. Measured on {trainingResult.evalDescription}. Both checkpoints are public, so
the claim is checkable rather than quotable.
</p> </p>
<p className="mt-4 flex flex-wrap gap-2"> <p className="mt-4 flex flex-wrap gap-2">
<a <a
className={s.pill} className="tap inline-flex items-center gap-1 rounded-md border border-border-strong px-3 font-mono text-mono-data text-fg transition-colors duration-1 ease-enter hover:bg-surface-2"
href={trainingResult.source.href} href={trainingResult.source.href}
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
@@ -183,33 +489,29 @@ export default function Home() {
</a> </a>
{trainingResult.checkpoints.map((c) => ( {trainingResult.checkpoints.map((c) => (
<a <a
className={s.pill} className="tap inline-flex items-center gap-1 rounded-md border border-border-strong px-3 font-mono text-mono-data text-fg transition-colors duration-1 ease-enter hover:bg-surface-2"
href={c.href} href={c.href}
key={c.href} key={c.href}
rel="noreferrer noopener" rel="noreferrer noopener"
target="_blank" target="_blank"
> >
<span className="font-mono">{c.label.replace('PrimeIntellect/', '')}</span> {c.label.replace('PrimeIntellect/', '')}
<ArrowUpRight aria-hidden="true" className="size-3.5" /> <ArrowUpRight aria-hidden="true" className="size-3.5" />
</a> </a>
))} ))}
</p> </p>
{/* <p className={`${s.caption} mt-3`}>{trainingResult.caveat}</p>
The same write-up publishes average-reward figures for these runs.
They are deliberately not on this page — see /honesty.
*/}
<p className="mt-3 text-xs text-muted">
We quote the win rate only. The reward numbers in that write-up span versions of the
environment and were never re-measured together.
</p>
</div> </div>
</div> </div>
</section>
<div className="mt-6 flex flex-col gap-3 sm:flex-row sm:items-center"> {/* ── 6. Close ────────────────────────────────────────────────────── */}
<section className={`${s.shell} pb-12 sm:pb-16`}>
<div className="flex flex-col gap-3 border-t border-border pt-8 sm:flex-row sm:items-center">
<Link className={s.btnSecondary} to={routes.honesty}> <Link className={s.btnSecondary} to={routes.honesty}>
What we measured, and what we didnt What we measured, and what we didnt
</Link> </Link>
<p className="text-sm text-muted"> <p className={s.small}>
Every number on this site, with the one that is ours and the ones that are not. Every number on this site, with the one that is ours and the ones that are not.
</p> </p>
</div> </div>
+222 -167
View File
@@ -2,6 +2,7 @@ import type { ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ArrowUpRight } from 'lucide-react'; import { ArrowUpRight } from 'lucide-react';
import { Term } from '@/components/site/Term';
import { import {
helloWorldCitations, helloWorldCitations,
optimalPlay, optimalPlay,
@@ -10,10 +11,19 @@ import {
trainingResult, trainingResult,
wordList, wordList,
} from '@/content/evidence'; } from '@/content/evidence';
import { REPO_URL, routes } from '@/content/lineup'; import { countWord, CountWord, lineup, REPO_URL, routes, unbuiltLineup } from '@/content/lineup';
import * as s from '@/content/styles';
import { PROPOSAL_NOTICE } from '@/content/verticals'; import { PROPOSAL_NOTICE } from '@/content/verticals';
import { pageTitle, useSeo } from '@/lib/seo'; import { pageTitle, useSeo } from '@/lib/seo';
import * as s from '@/content/styles';
/**
* Every claim on the site, sorted by how much weight it can carry.
*
* The numbers here are the site's receipts, so they follow the same rules as
* the demo pages: anything measured is mono, anything with a verb in it is
* sans, and no count is typed — the proposals are counted from the lineup, so
* the day one of them is built this page corrects itself.
*/
/** External link with the new-tab affordance and the announcement to match. */ /** External link with the new-tab affordance and the announcement to match. */
function Out({ children, href }: { children: ReactNode; href: string }) { function Out({ children, href }: { children: ReactNode; href: string }) {
@@ -26,6 +36,11 @@ function Out({ children, href }: { children: ReactNode; href: string }) {
); );
} }
/** A measured number inside a sentence. */
function Num({ children }: { children: ReactNode }) {
return <span className="font-mono text-mono-data font-semibold text-fg">{children}</span>;
}
function Heading({ children, id }: { children: ReactNode; id: string }) { function Heading({ children, id }: { children: ReactNode; id: string }) {
return ( return (
<h2 className={`${s.h2} scroll-mt-[calc(var(--app-header-h)+1rem)]`} id={id}> <h2 className={`${s.h2} scroll-mt-[calc(var(--app-header-h)+1rem)]`} id={id}>
@@ -54,43 +69,53 @@ export default function Honesty() {
ogImage: '/og/honesty.png', ogImage: '/og/honesty.png',
}); });
const withCaveat = lineup.filter((v) => v.caveat !== undefined);
const answers = wordList.answers.toLocaleString('en-US');
const guesses = wordList.guesses.toLocaleString('en-US');
return ( return (
<main className={`${s.shell} py-10 sm:py-16`}> <main className={`${s.shell} py-10 sm:py-14`}>
<p className={s.eyebrow}>Honesty</p> <p className={s.label}>Honesty</p>
<h1 className={`${s.h1} mt-3 max-w-3xl`}>What is measured, what is cited, and what is not.</h1> <h1 className={`${s.h1} mt-3 max-w-[24ch]`}>What is measured, what is cited, and what is not.</h1>
<p className={`${s.lede} mt-5 max-w-2xl`}> <p className={`${s.lede} mt-4`}>
This site argues that a number you can check beats a claim you have to trust. That argument 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 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. how much weight it can carry.
</p> </p>
<nav aria-label="On this page" className="card mt-8 p-5"> <nav aria-label="On this page" className="card mt-8 p-4 sm:p-5">
<p className={s.eyebrow}>On this page</p> <p className={s.label}>On this page</p>
<ul className="mt-3 grid gap-x-6 gap-y-2 sm:grid-cols-2"> <ol className="mt-3 grid gap-x-6 sm:grid-cols-2">
{CONTENTS.map((item) => ( {CONTENTS.map((item, index) => (
<li key={item.id}> <li key={item.id}>
<a className={`${s.link} text-sm`} href={`#${item.id}`}> <a
className="tap inline-flex items-center gap-3 py-1.5 text-ui text-fg transition-colors duration-1 ease-enter hover:text-accent-fg"
href={`#${item.id}`}
>
<span className="font-mono text-label uppercase text-muted">
{String(index + 1).padStart(2, '0')}
</span>
{item.label} {item.label}
</a> </a>
</li> </li>
))} ))}
</ul> </ol>
</nav> </nav>
{/* ── Measured ───────────────────────────────────────────────────── */} {/* ── Measured ───────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl"> <section className="mt-12">
<Heading id="measured">What we measured ourselves</Heading> <Heading id="measured">What we measured ourselves</Heading>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
Everything in this section is reproducible from a clone of this repository, offline, with 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 no account and no key. If any of it does not reproduce, that is a bug and we want the
issue. issue.
</p> </p>
<ul className={`${s.prose} mt-4 space-y-4`}> <ol className="mt-4 max-w-measure space-y-5">
<li> <li className={s.prose}>
<strong className="text-fg">The environment runs.</strong> The word game is a real <strong className="font-semibold text-fg">The environment runs.</strong> The word game
environment in this repository, not a mock behind the page. Three commands score a model is a real environment in this repository, not a mock behind the page. Three commands
with it: score a model with it:
<pre className={`${s.codeBlock} mt-2`}> <pre className={`${s.codeBlock} mt-2 whitespace-pre-wrap sm:whitespace-pre`}>
<code> <code>
{reproduce.clone} {reproduce.clone}
{'\n'} {'\n'}
@@ -100,43 +125,45 @@ export default function Honesty() {
</code> </code>
</pre> </pre>
</li> </li>
<li> <li className={s.prose}>
<strong className="text-fg">The word lists rebuild byte for byte.</strong> Both source <strong className="font-semibold text-fg">The word lists rebuild byte for byte.</strong>{' '}
dictionaries are committed beside the script that filters them, so{' '} Both source dictionaries are committed beside the script that filters them, so{' '}
<code className="font-mono text-fg">{wordList.rebuild}</code> produces the same{' '} <code className="font-mono text-mono-data text-fg">{wordList.rebuild}</code> produces the
<span className="nums">{wordList.answers.toLocaleString('en-US')}</span> answers and{' '} same <Num>{answers}</Num> answers and <Num>{guesses}</Num> legal guesses on any machine.
<span className="nums">{wordList.guesses.toLocaleString('en-US')}</span> legal guesses on
any machine.
</li> </li>
<li> <li className={s.prose}>
<strong className="text-fg">The two implementations of the rules agree.</strong> The <strong className="font-semibold text-fg">
grader exists twice once in Python for the environment, once in TypeScript for the 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 board in your browser. Continuous integration scores every ordered pair of words in the
answer list through both, about{' '} answer list through both, about <Num>21.2 million</Num> feedback patterns, and compares
<span className="nums">21.2 million</span> feedback patterns, and compares a hash. A a hash. A divergence fails the build rather than showing you one result and scoring
divergence fails the build rather than showing you one result and scoring another. another.
</li> </li>
<li> <li className={s.prose}>
<strong className="text-fg">The scores on a recorded run are re-derived, not <strong className="font-semibold text-fg">
recited.</strong> Where a demo ships a verifier, the page recomputes the reward in your The scores on a recorded run are re-derived, not recited.
browser from the recorded turns instead of printing a number stored in the file. If a </strong>{' '}
trace is truncated, it renders as unverifiable never as zero. 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> </li>
</ul> </ol>
</section> </section>
{/* ── Cited ──────────────────────────────────────────────────────── */} {/* ── Cited ──────────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl"> <section className="mt-12">
<Heading id="cited">What we cite from elsewhere</Heading> <Heading id="cited">What we cite from elsewhere</Heading>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
Three claims on this site are not ours. They are linked so you can go and check them Three claims on this site are not ours. They are linked so you can go and check them
instead of taking them from us. instead of taking them from us.
</p> </p>
<div className="card mt-5 p-5"> <div className="card mt-5 max-w-measure-wide p-4 sm:p-5">
<h3 className={s.h3}> <h3 className={`${s.h3} font-mono`}>
{trainingResult.model}: {trainingResult.before} {trainingResult.after}{' '} {trainingResult.model}: {trainingResult.before} {trainingResult.after}{' '}
{trainingResult.metric} <span className="font-sans font-normal text-fg-2">{trainingResult.metric}</span>
</h3> </h3>
<p className={`${s.prose} mt-2`}> <p className={`${s.prose} mt-2`}>
Published by Prime Intellect, measured on {trainingResult.evalDescription}, after{' '} Published by Prime Intellect, measured on {trainingResult.evalDescription}, after{' '}
@@ -146,32 +173,46 @@ export default function Honesty() {
<Out href={trainingResult.source.href}>the write-up</Out>. <Out href={trainingResult.source.href}>the write-up</Out>.
</p> </p>
<p className={`${s.prose} mt-3`}> <p className={`${s.prose} mt-3`}>
We quote the win rate and nothing else. The same write-up gives average-reward figures {trainingResult.caveat} A win rate survives a reward change. An average reward does not.
for those runs; we leave them out because the reward function has changed across We have also not reproduced this result: their environment and word list are not ours,
versions of that environment and the two figures were never re-measured together. A win and a number measured on a different task list is not a number about this one.
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> </p>
</div> </div>
<div className="card mt-4 p-5"> <div className="card mt-4 max-w-measure-wide p-4 sm:p-5">
<h3 className={s.h3}>Wordle is Prime Intellects own hello-world</h3> <h3 className={s.h3}>The word game is Prime Intellects own hello-world</h3>
<p className={`${s.prose} mt-2`}> <p className={`${s.prose} mt-2`}>
We say this because a demo picked to flatter itself is worth nothing. It appears in three We say this because a demo picked to flatter itself is worth nothing. It appears in
places in their stack: three places in their stack:
</p> </p>
<ul className={`${s.prose} mt-3 space-y-2`}> <ul className="mt-3 divide-y divide-border">
{helloWorldCitations.map((c) => ( {helloWorldCitations.map((c) => (
<li key={c.href}> <li className="py-2.5" key={c.href}>
<Out href={c.href}>{c.label}</Out> {c.claim} <a
className="tap flex items-start gap-2 transition-colors duration-1 ease-enter hover:text-fg"
href={c.href}
rel="noreferrer noopener"
target="_blank"
>
<ArrowUpRight
aria-hidden="true"
className="mt-0.5 size-4 shrink-0 text-border-strong"
/>
<span className="min-w-0">
<span className="block font-mono text-mono-data text-fg">{c.label}</span>
<span className="block text-small text-fg-2">{c.claim}</span>
<span className="sr-only">(opens in a new tab)</span>
</span>
</a>
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
<div className="card mt-4 p-5"> <div className="card mt-4 max-w-measure-wide p-4 sm:p-5">
<h3 className={s.h3}>{optimalPlay.average} is not our number</h3> <h3 className={s.h3}>
<span className="font-mono">{optimalPlay.average}</span> is not our number
</h3>
<p className={`${s.prose} mt-2`}> <p className={`${s.prose} mt-2`}>
The famous optimum is <Out href={optimalPlay.href}>{optimalPlay.label}</Out>. It belongs 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 to that list and that solver. It is not the optimum for our list, we have not computed
@@ -181,27 +222,28 @@ export default function Honesty() {
</section> </section>
{/* ── Recorded ───────────────────────────────────────────────────── */} {/* ── Recorded ───────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl"> <section className="mt-12">
<Heading id="recorded">Every rollout on this site is recorded, not live</Heading> <Heading id="recorded">Every rollout on this site is recorded, not live</Heading>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
When you watch a model play here, you are watching a file. No model is called while you When you watch a model play here, you are watching a file. Every{' '}
<Term term="rollout">rollout</Term> was captured once, and no model is called while you
read this page. That is a choice, and these are the reasons for it: read this page. That is a choice, and these are the reasons for it:
</p> </p>
<ul className={`${s.prose} mt-4 list-disc space-y-3 pl-5`}> <ul className={`${s.prose} mt-4 list-disc space-y-3 pl-5`}>
<li> <li>
<strong className="text-fg">The argument is about determinism.</strong> A page whose <strong className="font-semibold text-fg">The argument is about determinism.</strong> A
model says something different on every reload cannot be used to make the case that the page whose model says something different on every reload cannot be used to make the
grader stays put. The demo has to behave like the thing it is describing. case that the grader stays put. The demo has to behave like the thing it is describing.
</li> </li>
<li> <li>
<strong className="text-fg">There is nowhere to hide a cherry-pick.</strong> Every run is <strong className="font-semibold text-fg">There is nowhere to hide a cherry-pick.</strong>{' '}
a file in the repository carrying its model, its seed and the date it was captured. A bad Every run is a file in the repository carrying its model, its{' '}
run is in there being bad. A live demo lets you re-roll until it looks good and tell <Term term="seed">seed</Term> and the date it was captured. A bad run is in there being
nobody. bad. A live demo lets you re-roll until it looks good and tell nobody.
</li> </li>
<li> <li>
<strong className="text-fg">It works.</strong> No key, no rate limit, no bill, no <strong className="font-semibold text-fg">It works.</strong> No key, no rate limit, no
dependence on a provider being up during the ten minutes you are looking at it. bill, no dependence on a provider being up during the ten minutes you are looking at it.
</li> </li>
</ul> </ul>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
@@ -212,32 +254,34 @@ export default function Honesty() {
</section> </section>
{/* ── Reward editor ──────────────────────────────────────────────── */} {/* ── Reward editor ──────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl"> <section className="mt-12">
<Heading id="editor">The reward editor re-scores. It does not train anything.</Heading> <Heading id="editor">The reward editor re-scores. It does not train anything.</Heading>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
Moving a weight changes how the recorded attempts are scored, and the page recomputes and 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 re-ranks them. That is the entire mechanism. Nothing is fine-tuned, no model is called,
nothing learns while you drag a slider. and nothing learns while you drag a slider.
</p> </p>
<p className={`${s.prose} mt-4`}> <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 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 change what you are paying for. That is a fact about your definition of good, not about
models and it is the reason the definition is worth an hour of your teams attention the models and it is the reason the definition is worth an hour of your teams
before the training run is worth a GPU. attention before the training run is worth a GPU.
</p> </p>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
The other half actually training against the reward you just wrote takes GPUs and 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 hours, and it does not belong in a browser tab. That is what a trainer like{' '}
for, and the number at the top of the home page is what it produced. <span className="font-mono text-mono-data text-fg">prime-rl</span> is for, and the number
on the home page is what it produced.
</p> </p>
</section> </section>
{/* ── Proposals ──────────────────────────────────────────────────── */} {/* ── Proposals ──────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl"> <section className="mt-12">
<Heading id="proposals">The verticals are our proposals</Heading> <Heading id="proposals">The verticals are our proposals</Heading>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
Eleven of the twelve entries in the lineup describe environments that do not exist. They {CountWord(unbuiltLineup.length)} of the {countWord(lineup.length)} entries in the lineup
are labelled <span className="font-semibold text-fg">{PROPOSAL_NOTICE}</span> on every describe environments that do not exist. They are labelled{' '}
<span className={`${s.chipOutline} align-[0.1em]`}>{PROPOSAL_NOTICE}</span> on every
surface that shows them, and that label is the literal truth: we wrote them. surface that shows them, and that label is the literal truth: we wrote them.
</p> </p>
<ul className={`${s.prose} mt-4 list-disc space-y-3 pl-5`}> <ul className={`${s.prose} mt-4 list-disc space-y-3 pl-5`}>
@@ -247,50 +291,50 @@ export default function Honesty() {
customer, a reference or a logo, because none is one. customer, a reference or a logo, because none is one.
</li> </li>
<li> <li>
Where a page names a KPI, we are describing how that function usually measures itself. We Where a page names a KPI, we are describing how that function usually measures itself.
are not reporting anyones numbers, and there are no numbers on those pages to report. We are not reporting anyones numbers, and there are no numbers on those pages to
report.
</li> </li>
</ul> </ul>
{withCaveat.length > 0 ? (
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
Two of them carry a standing caveat on their own page the{' '} {CountWord(withCaveat.length)} of them {withCaveat.length === 1 ? 'carries' : 'carry'} a
<Link className={s.link} to={routes.vertical('legal-playbook-redline')}> standing caveat on {withCaveat.length === 1 ? 'its' : 'their'} own page:{' '}
legal redline {withCaveat.map((v, index) => (
<span key={v.slug}>
{index > 0 ? (index === withCaveat.length - 1 ? ' and ' : ', ') : ''}
<Link className={s.link} to={routes.vertical(v.slug)}>
{v.title}
</Link> </Link>
, where grading the drafting half collapses into an LLM judge, and{' '} </span>
<Link className={s.link} to={routes.vertical('semiconductor-ppa-closure')}> ))}
semiconductor timing closure . Each says, in its own words, where the argument stops being checkable.
</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> </p>
) : null}
</section> </section>
{/* ── Word list ──────────────────────────────────────────────────── */} {/* ── Word list ──────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl"> <section className="mt-12">
<Heading id="words">The word list is our own construction</Heading> <Heading id="words">The word list is our own construction</Heading>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
We did not copy the original games word lists. Our answers are the intersection of two We did not copy the original games word lists. Our answers are the intersection of two
permissively-licensed sources every five-letter headword in{' '} 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[0].href}>{wordList.sources[0].label}</Out> that also appears
<Out href={wordList.sources[1].href}>{wordList.sources[1].label}</Out>, minus a short 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. hand-written blocklist. Legal guesses are the whole five-letter Wordnik set.
</p> </p>
<dl className="card mt-5 grid gap-4 p-5 sm:grid-cols-3"> <dl className="card mt-5 grid max-w-measure-wide gap-4 p-4 sm:grid-cols-3 sm:p-5">
<div> <div>
<dt className={s.eyebrow}>Answers</dt> <dt className={s.label}>Answers</dt>
<dd className="nums mt-1 text-2xl font-bold text-fg"> <dd className="mt-1 font-mono text-metric text-fg">{answers}</dd>
{wordList.answers.toLocaleString('en-US')}
</dd>
</div> </div>
<div> <div>
<dt className={s.eyebrow}>Legal guesses</dt> <dt className={s.label}>Legal guesses</dt>
<dd className="nums mt-1 text-2xl font-bold text-fg"> <dd className="mt-1 font-mono text-metric text-fg">{guesses}</dd>
{wordList.guesses.toLocaleString('en-US')}
</dd>
</div> </div>
<div> <div>
<dt className={s.eyebrow}>Original games answers</dt> <dt className={s.label}>Original games answers</dt>
<dd className="nums mt-1 text-2xl font-bold text-muted"> <dd className="mt-1 font-mono text-metric text-muted">
{wordList.originalAnswers.toLocaleString('en-US')} {wordList.originalAnswers.toLocaleString('en-US')}
</dd> </dd>
</div> </div>
@@ -303,116 +347,127 @@ export default function Honesty() {
</section> </section>
{/* ── Harder ────────────────────────────────────────────────────── */} {/* ── Harder ────────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl"> <section className="mt-12">
<Heading id="harder">This environment is harder than the original game</Heading> <Heading id="harder">This environment is harder than the original game</Heading>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
Two things pull in opposite directions here, so we measured rather than argued. Our answer Two things pull in opposite directions here, so we measured rather than argued. Our
pool comes from a dictionary rule, so it keeps the regular plurals and past tenses the answer pool comes from a dictionary rule, so it keeps the regular plurals and past tenses
original games editor removed by hand:{' '} the original games editor removed by hand: <Num>{wordList.endsInS}</Num> of our answers
<span className="nums font-semibold text-fg">{wordList.endsInS}</span> of our answers end end in a plain <span className="font-mono text-fg">S</span> and{' '}
in a plain <span className="font-mono">S</span> and{' '} <Num>{wordList.endsInEd}</Num> in <span className="font-mono text-fg">-ED</span>. That is
<span className="nums font-semibold text-fg">{wordList.endsInEd}</span> in{' '} an exploitable regularity, and it makes our list easier. Our pool is also roughly twice
<span className="font-mono">-ED</span>. That is an exploitable regularity, and it makes our the size, which makes it harder.
list easier. Our pool is also roughly twice the size, which makes it harder.
</p> </p>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
The net effect is the second one. We ran the same greedy solver over both pools, on{' '} The net effect is the second one. We ran the same greedy solver over both pools, on{' '}
<span className="nums font-semibold text-fg">{poolComparison.sampled}</span> sampled words <Num>{poolComparison.sampled}</Num> sampled words each:
each:
</p> </p>
<div className="mt-4 overflow-x-auto"> <div className="mt-4 max-w-measure-wide overflow-x-auto rounded-md border border-border">
<table className="w-full min-w-[26rem] border-collapse text-sm"> <table className="w-full min-w-[26rem] border-collapse font-mono text-mono-data">
<thead> <thead>
<tr className="border-b border-border text-left text-muted"> <tr className="bg-surface-3 text-left">
<th className="py-2 pr-4 font-medium">Answer pool</th> <th className={`${s.label} px-3 py-2`} scope="col">
<th className="py-2 pr-4 font-medium">Best opener</th> Answer pool
<th className="py-2 pr-4 font-medium">Mean guesses</th> </th>
<th className="py-2 font-medium">Worst case</th> <th className={`${s.label} px-3 py-2`} scope="col">
Best opener
</th>
<th className={`${s.label} px-3 py-2 text-right`} scope="col">
Mean guesses
</th>
<th className={`${s.label} px-3 py-2 text-right`} scope="col">
Worst case
</th>
</tr> </tr>
</thead> </thead>
<tbody className="nums"> <tbody>
<tr className="border-b border-border"> <tr className="border-t border-border text-fg-2">
<td className="py-2 pr-4">Original, {poolComparison.original.size}</td> <td className="px-3 py-2">
<td className="py-2 pr-4 font-mono">{poolComparison.original.opener}</td> <span className="font-sans">Original,</span> {poolComparison.original.size}
<td className="py-2 pr-4">{poolComparison.original.mean}</td> </td>
<td className="py-2">{poolComparison.original.worst}</td> <td className="px-3 py-2">{poolComparison.original.opener}</td>
<td className="px-3 py-2 text-right">{poolComparison.original.mean}</td>
<td className="px-3 py-2 text-right">{poolComparison.original.worst}</td>
</tr> </tr>
<tr> <tr className="border-t border-border font-semibold text-fg">
<td className="py-2 pr-4 font-semibold text-fg">Ours, {poolComparison.ours.size}</td> <td className="px-3 py-2">
<td className="py-2 pr-4 font-mono">{poolComparison.ours.opener}</td> <span className="font-sans">Ours,</span> {poolComparison.ours.size}
<td className="py-2 pr-4 font-semibold text-fg">{poolComparison.ours.mean}</td> </td>
<td className="py-2">{poolComparison.ours.worst}</td> <td className="px-3 py-2">{poolComparison.ours.opener}</td>
<td className="px-3 py-2 text-right">{poolComparison.ours.mean}</td>
<td className="px-3 py-2 text-right">{poolComparison.ours.worst}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
About a seventh of a guess harder, and one worse in the worst case. Both rows are the same About a seventh of a guess harder, and one worse in the worst case. Both rows are the same
algorithm, which is the only fair comparison available setting our greedy number against algorithm, which is the only fair comparison available setting our greedy number
the published{' '} against the published <Num>3.4212</Num> optimum for the original list would be comparing
<span className="nums">3.4212</span> optimum for the original list would be comparing two two different algorithms and calling the difference a property of the words.
different algorithms and calling the difference a property of the words.
</p> </p>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
The practical instruction is unchanged: do not compare a guess count from this site to your The practical instruction is unchanged: do not compare a guess count from this site to
own statistics from the original game. It is a different game with a different shape. your own statistics from the original game. It is a different game with a different shape.
</p> </p>
</section> </section>
{/* ── Unmeasured ─────────────────────────────────────────────────── */} {/* ── Unmeasured ─────────────────────────────────────────────────── */}
<section className="mt-12 max-w-3xl"> <section className="mt-12">
<Heading id="unmeasured">What we have not measured</Heading> <Heading id="unmeasured">What we have not measured</Heading>
<p className={`${s.prose} mt-4`}> <p className={`${s.prose} mt-4`}>
This is the list we would want if we were the ones being sold to. This is the list we would want if we were the ones being sold to.
</p> </p>
<ul className={`${s.prose} mt-4 list-disc space-y-3 pl-5`}> <ul className={`${s.prose} mt-4 list-disc space-y-3 pl-5`}>
<li> <li>
<strong className="text-fg">Any training result of our own.</strong> We have not trained <strong className="font-semibold text-fg">Any training result of our own.</strong> We
a model against this environment. The before-and-after number on the home page is Prime have not trained a model against this environment. The before-and-after number on the
Intellects, measured on their word list, and we did not rerun it on ours. home page is Prime Intellects, measured on their word list, and we did not rerun it on
ours.
</li> </li>
<li> <li>
<strong className="text-fg">Variance.</strong> The recorded runs are individual seeds, <strong className="font-semibold text-fg">Variance.</strong> The recorded runs are
not samples. There is no confidence interval anywhere on this site and no claim that a individual seeds, not samples. There is no confidence interval anywhere on this site and
given run is typical. A demonstration with one seed is a demonstration, not evidence. no claim that a given run is typical. A demonstration with one seed is a demonstration,
not evidence.
</li> </li>
<li> <li>
<strong className="text-fg">An optimum, or a human baseline, for our list.</strong> We do <strong className="font-semibold text-fg">
not know how few guesses perfect play needs on our answers, and we have not asked people An optimum, or a human baseline, for our list.
to play it. </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>
<li> <li>
<strong className="text-fg">Cost and latency.</strong> No number here is a price, a <strong className="font-semibold text-fg">Cost and latency.</strong> No number here is a
token count you should budget from, or a wall-clock comparison between models. The price, a <Term term="tokens">token</Term> count you should budget from, or a wall-clock
durations shown in a trace are what that recording measured, on that day, on that comparison between models. The durations shown in a trace are what that recording
endpoint. measured, on that day, on that endpoint.
</li> </li>
<li> <li>
<strong className="text-fg">Anything at all about the eleven proposals.</strong> Nothing <strong className="font-semibold text-fg">
in those pages has been run. Every reward, counterweight and KPI there is a design. Anything at all about the {countWord(unbuiltLineup.length)} proposals.
</strong>{' '}
Nothing in those pages has been run. Every reward, counterweight and KPI there is a
design.
</li> </li>
<li> <li>
<strong className="text-fg">A full review of the answer list.</strong> We block a short <strong className="font-semibold text-fg">A full review of the answer list.</strong> We
hand-written set of words from ever being the hidden answer. We have not audited the block a short hand-written set of words from ever being the hidden answer. We have not
remaining {wordList.answers.toLocaleString('en-US')} exhaustively, and a five-letter audited the remaining <Num>{answers}</Num> exhaustively, and a five-letter English
English dictionary contains words somebody will not want on a boardroom screen. dictionary contains words somebody will not want on a boardroom screen.
</li> </li>
</ul> </ul>
</section> </section>
<div className="card mt-14 p-5 sm:p-7"> <div className="card mt-14 max-w-measure-wide p-4 sm:p-5">
<h2 className={s.h3}>Found something on this site that is wrong?</h2> <h2 className={s.h3}>Found something on this site that is wrong?</h2>
<p className={`${s.prose} mt-2 max-w-2xl`}> <p className={`${s.prose} mt-2`}>
Every claim above is traceable to a file in the repository. If one of them does not hold, 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. open an issue with the page and the sentence and we will fix the sentence or the code.
</p> </p>
<div className="mt-5 flex flex-col gap-3 sm:flex-row"> <div className="mt-5 flex flex-col gap-3 sm:flex-row">
<a <a className={s.btnPrimary} href={REPO_URL} rel="noreferrer noopener" target="_blank">
className={s.btnPrimary}
href={REPO_URL}
rel="noreferrer noopener"
target="_blank"
>
Read the source Read the source
</a> </a>
<Link className={s.btnSecondary} to={routes.home}> <Link className={s.btnSecondary} to={routes.home}>
+27 -24
View File
@@ -1,7 +1,8 @@
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { ArrowRight } from 'lucide-react'; import { ArrowRight } from 'lucide-react';
import { allDemos, lineup, REPO_URL, routes } from '@/content/lineup'; import { StatusPill } from '@/components/site/EnvironmentPicker';
import { allDemos, lineup, pad2, REPO_URL, routes } from '@/content/lineup';
import * as s from '@/content/styles'; import * as s from '@/content/styles';
import { pageTitle, useSeo } from '@/lib/seo'; import { pageTitle, useSeo } from '@/lib/seo';
@@ -27,12 +28,12 @@ export default function NotFound() {
return ( return (
<main className={`${s.shell} py-16 sm:py-24`}> <main className={`${s.shell} py-16 sm:py-24`}>
<p className={`${s.eyebrow} nums`}>404</p> <p className={s.label}>404</p>
<h1 className={`${s.h1} mt-3 max-w-2xl`}>That page isnt here.</h1> <h1 className={`${s.h1} mt-3`}>That page isnt here.</h1>
<p className={`${s.lede} mt-5 max-w-2xl`}> <p className={`${s.lede} mt-4`}>
Nothing is served at{' '} Nothing is served at{' '}
<code className="break-all font-mono text-fg">{pathname}</code>. Either the link is old or <code className="break-all font-mono text-mono-data text-fg">{pathname}</code>. Either the
we moved something. Here is everything this site has. link is old or we moved something. Here is everything this site has.
</p> </p>
<div className="mt-8 flex flex-col gap-3 sm:flex-row"> <div className="mt-8 flex flex-col gap-3 sm:flex-row">
@@ -46,43 +47,45 @@ export default function NotFound() {
<div className="mt-12 grid gap-8 sm:grid-cols-2"> <div className="mt-12 grid gap-8 sm:grid-cols-2">
<nav aria-labelledby="nf-demos"> <nav aria-labelledby="nf-demos">
<h2 className={s.h3} id="nf-demos"> <h2 className={s.label} id="nf-demos">
Demos Environments · {pad2(allDemos.length)}
</h2> </h2>
<ul className="mt-3 space-y-2"> <ul className="mt-3 divide-y divide-border border-y border-border">
{allDemos.map((demo) => ( {allDemos.map((demo) => (
<li key={demo.slug}> <li key={demo.slug}>
<Link className={`${s.link} text-sm`} to={routes.demo(demo.slug)}> <Link
className="tap flex items-center justify-between gap-3 py-3 text-ui font-medium text-fg transition-colors duration-1 ease-enter hover:text-accent-fg"
to={routes.demo(demo.slug)}
>
{demo.title} {demo.title}
<StatusPill status={demo.status} />
</Link> </Link>
<span className="ml-2 text-xs text-muted">
{demo.status === 'live' ? 'Live' : 'Specification'}
</span>
</li> </li>
))} ))}
{allDemos.length === 0 ? ( {allDemos.length === 0 ? (
<li className="text-sm text-muted">No demos are registered yet.</li> <li className={`${s.small} py-3`}>No environments are registered yet.</li>
) : null} ) : null}
</ul> </ul>
</nav> </nav>
<nav aria-labelledby="nf-verticals"> <nav aria-labelledby="nf-verticals">
<h2 className={s.h3} id="nf-verticals"> <h2 className={s.label} id="nf-verticals">
The lineup The lineup · {pad2(lineup.length)}
</h2> </h2>
<ul className="mt-3 space-y-2"> <ul className="mt-3 divide-y divide-border border-y border-border">
{topVerticals.map((v) => ( {topVerticals.map((v) => (
<li key={v.slug}> <li key={v.slug}>
<Link className={`${s.link} text-sm`} to={routes.vertical(v.slug)}> <Link
className="tap flex items-center gap-3 py-3 text-ui font-medium text-fg transition-colors duration-1 ease-enter hover:text-accent-fg"
to={routes.vertical(v.slug)}
>
<span className="font-mono text-label uppercase text-muted">{pad2(v.rank)}</span>
{v.title} {v.title}
</Link> </Link>
</li> </li>
))} ))}
<li> <li>
<Link <Link className={`${s.navLink} py-3`} to={routes.home}>
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} All {lineup.length}
<ArrowRight aria-hidden="true" className="size-3.5" /> <ArrowRight aria-hidden="true" className="size-3.5" />
</Link> </Link>
@@ -91,9 +94,9 @@ export default function NotFound() {
</nav> </nav>
</div> </div>
<div className="card mt-12 p-5"> <div className="card mt-12 p-4 sm:p-5">
<h2 className={s.h3}>Looking for the numbers?</h2> <h2 className={s.h3}>Looking for the numbers?</h2>
<p className={`${s.prose} mt-2 max-w-2xl`}> <p className={`${s.prose} mt-2`}>
What is measured, what is cited from elsewhere, and what we have deliberately not measured What is measured, what is cited from elsewhere, and what we have deliberately not measured
all live on one page. all live on one page.
</p> </p>
+116 -85
View File
@@ -1,12 +1,41 @@
import type { ReactNode } from 'react';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { ArrowLeft, ArrowRight, FileText, Play, Quote, TriangleAlert } from 'lucide-react'; import { ArrowLeft, ArrowRight, Play } from 'lucide-react';
import { StatusPill } from '@/components/site/EnvironmentPicker';
import { Term } from '@/components/site/Term';
import { iconFor } from '@/content/icons'; import { iconFor } from '@/content/icons';
import { demosForVertical, featuredDemo, lineup, routes } from '@/content/lineup'; import { demosForVertical, featuredDemo, lineup, pad2, routes } from '@/content/lineup';
import { PROPOSAL_NOTICE, verticalBySlug } from '@/content/verticals';
import * as s from '@/content/styles'; import * as s from '@/content/styles';
import { PROPOSAL_NOTICE, verticalBySlug } from '@/content/verticals';
import { pageTitle, useSeo } from '@/lib/seo'; import { pageTitle, useSeo } from '@/lib/seo';
/**
* One proposal, in full.
*
* The page is the same shape as a demo page's header — persona, the buyer's
* question as a quiet quote, then the environment in its parts — so a visitor
* who has seen the word game reads this as the same machine with the board
* missing. Every count and every neighbour is derived from the lineup.
*/
function Part({
label,
tone = 'default',
children,
}: {
label: ReactNode;
tone?: 'default' | 'warning';
children: ReactNode;
}) {
return (
<article className={`card p-4 sm:p-5 ${tone === 'warning' ? 'bg-warning-subtle' : ''}`}>
<h3 className={`${s.label} ${tone === 'warning' ? 'text-warning' : ''}`}>{label}</h3>
<p className={`${s.prose} mt-2 text-fg`}>{children}</p>
</article>
);
}
export default function VerticalPage() { export default function VerticalPage() {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const vertical = verticalBySlug(slug); const vertical = verticalBySlug(slug);
@@ -18,7 +47,7 @@ export default function VerticalPage() {
title: pageTitle(vertical?.title ?? 'No such vertical'), title: pageTitle(vertical?.title ?? 'No such vertical'),
description: vertical description: vertical
? `${vertical.task} ${vertical.reward}` ? `${vertical.task} ${vertical.reward}`
: 'There is no proposal at that address. The twelve we have written are all on the home page.', : `There is no proposal at that address. The ${lineup.length} we have written are all on the home page.`,
...(slug ? { canonical: routes.vertical(slug) } : {}), ...(slug ? { canonical: routes.vertical(slug) } : {}),
...(vertical ? { ogImage: `/og/vertical-${vertical.slug}.png` } : {}), ...(vertical ? { ogImage: `/og/vertical-${vertical.slug}.png` } : {}),
}); });
@@ -26,10 +55,12 @@ export default function VerticalPage() {
if (!vertical) { if (!vertical) {
return ( return (
<main className={`${s.shell} py-16`}> <main className={`${s.shell} py-16`}>
<h1 className={s.h1}>No such vertical.</h1> <p className={s.label}>No such vertical</p>
<p className={`${s.lede} mt-4 max-w-xl`}> <h1 className={`${s.h1} mt-3`}>There is no proposal at that address.</h1>
There is no proposal at <code className="font-mono text-fg">/verticals/{slug}</code>. The <p className={`${s.lede} mt-4`}>
twelve we have written are all on the home page. Nothing is written at{' '}
<code className="break-all font-mono text-mono-data text-fg">/verticals/{slug}</code>. The{' '}
{lineup.length} we have written are all on the home page.
</p> </p>
<div className="mt-6 flex flex-col gap-3 sm:flex-row"> <div className="mt-6 flex flex-col gap-3 sm:flex-row">
<Link className={s.btnPrimary} to={routes.home}> <Link className={s.btnPrimary} to={routes.home}>
@@ -53,90 +84,82 @@ export default function VerticalPage() {
const next = index >= 0 ? lineup[index + 1] : undefined; const next = index >= 0 ? lineup[index + 1] : undefined;
return ( return (
<main className={`${s.shell} py-10 sm:py-14`}> <main className={`${s.shell} py-8 sm:py-12`}>
<Link <Link className={s.navLink} to={routes.home}>
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" /> <ArrowLeft aria-hidden="true" className="size-4" />
All twelve All {lineup.length}
</Link> </Link>
<header className="mt-4"> <header className="mt-4">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<span className={s.proposalPill}>{PROPOSAL_NOTICE}</span> <span className={s.chipOutline}>{PROPOSAL_NOTICE}</span>
<span className={s.pill}>Rank {vertical.rank} of {lineup.length}</span> <span className={s.chipOutline}>
<span className={s.pill}> Rank {pad2(vertical.rank)} / {pad2(lineup.length)}
</span>
<span className={s.chipMuted}>
{vertical.plannedForV1 ? 'In the first set' : 'Not in the first set'} {vertical.plannedForV1 ? 'In the first set' : 'Not in the first set'}
</span> </span>
</div> </div>
<div className="mt-5 flex items-start gap-4"> <div className="mt-5 flex items-start gap-4">
<span className="card grid size-12 shrink-0 place-items-center bg-surface-2"> <span className="grid size-11 shrink-0 place-items-center rounded-md bg-surface-3">
<Icon aria-hidden="true" className="size-6 text-brand" /> <Icon aria-hidden="true" className="size-5 text-border-strong" />
</span> </span>
<h1 className={`${s.h1} min-w-0`}>{vertical.title}</h1> <h1 className={`${s.h1} min-w-0`}>{vertical.title}</h1>
</div> </div>
<p className={`${s.label} mt-5 text-accent-fg`}>For the {vertical.persona}</p>
<p className={`${s.label} mt-4`}>The question you walked in with</p>
<blockquote className={`${s.quote} mt-2`}>
<p>{vertical.anxiety}</p>
</blockquote>
</header> </header>
{/* ── Who this is for, and what is already worrying them ─────────── */} {/* ── The environment, in the same parts every time ───────────────── */}
<section className="mt-8"> <section aria-labelledby="parts" className="mt-10">
<p className={s.eyebrow}>Who it is for</p> <h2 className={s.h2} id="parts">
<p className="mt-2 text-lg font-semibold text-fg">{vertical.persona}</p> The environment
<blockquote className="card mt-4 max-w-3xl p-5"> </h2>
<Quote aria-hidden="true" className="size-5 text-muted" /> <p className={`${s.prose} mt-3`}>
<p className="mt-2 text-lg leading-relaxed text-fg">{vertical.anxiety}</p> The same machine as the word game: a task, a score in the number this desk already
<footer className="mt-3 text-sm text-muted"> reports, and the term that takes points away the{' '}
The question in the room before anyone opens a laptop. <Term term="counterweight">counterweight</Term>. A <Term term="reward">reward</Term> with
</footer> nothing pulling against it is a target, and a target is what a model learns to hit
</blockquote> instead of doing the job.
</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> </p>
<div className="mt-5 grid gap-4 lg:grid-cols-3">
<Part label={<>01 &nbsp;The task</>}>{vertical.task}</Part>
<Part label={<>04 &nbsp;The score that moves</>}>{vertical.reward}</Part>
<Part label="The counterweight" tone="warning">
{vertical.counterweight}
</Part>
</div>
</section>
{vertical.caveat ? ( {vertical.caveat ? (
<section className="card mt-8 border-warning/40 bg-warning/5 p-5 sm:p-6"> <section aria-labelledby="stops" className="card mt-6 bg-warning-subtle p-4 sm:p-5">
<h2 className={`${s.h3} flex items-center gap-2 text-warning`}> <h2 className={`${s.label} text-warning`} id="stops">
<TriangleAlert aria-hidden="true" className="size-4 shrink-0" />
Where this stops Where this stops
</h2> </h2>
<p className={`${s.prose} mt-2 max-w-3xl text-fg`}>{vertical.caveat}</p> <p className={`${s.prose} mt-2 text-fg`}>{vertical.caveat}</p>
</section> </section>
) : null} ) : null}
{/* ── What actually exists for this vertical, stated plainly ─────── */} {/* ── What actually exists for this vertical, stated plainly ─────── */}
<section className="mt-10"> <section aria-labelledby="exists" className="mt-10">
<h2 className={s.h2}>What exists today</h2> <h2 className={s.h2} id="exists">
What exists today
</h2>
{live ? ( {live ? (
<Link <Link
aria-label={`${live.title} — play it`} aria-label={`${live.title} — play it`}
className={`${s.cardLink} mt-4`} className={`${s.cardLink} mt-4`}
to={routes.demo(live.slug)} to={`${routes.demo(live.slug)}?tab=play`}
> >
<span className={`${s.pill} self-start border-positive/30 bg-positive/10 text-positive`}> <span className="flex flex-wrap items-center gap-2">
Live demo <StatusPill status="live" />
</span> </span>
<span className="mt-3 text-xl font-bold tracking-tight text-fg">{live.title}</span> <span className={`${s.h3} mt-3`}>{live.title}</span>
<span className={`${s.prose} mt-1.5`}>{live.tagline}</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"> <span className="mt-4 inline-flex items-center gap-1.5 text-ui font-medium text-accent-fg">
<Play aria-hidden="true" className="size-4" /> <Play aria-hidden="true" className="size-4" />
Play it Play it
</span> </span>
@@ -147,34 +170,23 @@ export default function VerticalPage() {
className={`${s.cardLink} mt-4`} className={`${s.cardLink} mt-4`}
to={routes.demo(spec.slug)} to={routes.demo(spec.slug)}
> >
<span className={`${s.pill} self-start gap-1`}> <span className="flex flex-wrap items-center gap-2">
<FileText aria-hidden="true" className="size-3.5" /> <StatusPill status="spec" />
Published specification
</span> </span>
<span className="mt-3 text-xl font-bold tracking-tight text-fg">{spec.title}</span> <span className={`${s.h3} mt-3`}>{spec.title}</span>
<span className={`${s.prose} mt-1.5`}>{spec.tagline}</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"> <span className="mt-4 inline-flex items-center gap-1.5 text-ui font-medium text-accent-fg">
Read the specification Read the specification
<ArrowRight aria-hidden="true" className="size-4" /> <ArrowRight aria-hidden="true" className="size-4" />
</span> </span>
</Link> </Link>
) : ( ) : (
<div className="card mt-4 p-5 sm:p-6"> <div className="card mt-4 p-4 sm:p-5">
<p className={`${s.prose} max-w-3xl text-fg`}> <p className={`${s.prose} text-fg`}>
Nothing. There is no environment for this vertical, no recorded run, and no number. It 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 is a proposal, written to the same shape as the ones that are built so you can judge
on the same terms. it on the same terms.
</p> </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"> <div className="mt-5 flex flex-col gap-3 sm:flex-row">
<Link className={s.btnSecondary} to={routes.gallery}> <Link className={s.btnSecondary} to={routes.gallery}>
See what is built See what is built
@@ -187,6 +199,25 @@ export default function VerticalPage() {
)} )}
</section> </section>
{/* ── First visit ─────────────────────────────────────────────────── */}
{!live && featuredDemo ? (
<section aria-labelledby="first" className="mt-8 border-t border-border pt-6">
<h2 className={s.label} id="first">
If this is your first visit
</h2>
<p className={`${s.prose} mt-2`}>
Play {featuredDemo.title} first. It is the same four parts on a board you already know,
and every word on this page task, grader, counterweight, score is met there before
it is used here. It is deliberately not one of these {lineup.length}: it shows the
machinery without implying we have built yours.
</p>
<Link className={`${s.btnPrimary} mt-4`} to={`${routes.demo(featuredDemo.slug)}?tab=play`}>
Play {featuredDemo.title}
<ArrowRight aria-hidden="true" className="size-4" />
</Link>
</section>
) : null}
{/* ── Move along the ranking ─────────────────────────────────────── */} {/* ── Move along the ranking ─────────────────────────────────────── */}
<nav aria-label="Other verticals" className="mt-12 grid gap-3 sm:grid-cols-2"> <nav aria-label="Other verticals" className="mt-12 grid gap-3 sm:grid-cols-2">
{previous ? ( {previous ? (
@@ -195,9 +226,9 @@ export default function VerticalPage() {
className={`${s.cardLink} sm:items-start`} className={`${s.cardLink} sm:items-start`}
to={routes.vertical(previous.slug)} to={routes.vertical(previous.slug)}
> >
<span className="inline-flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted"> <span className={`${s.label} inline-flex items-center gap-1.5`}>
<ArrowLeft aria-hidden="true" className="size-3.5" /> <ArrowLeft aria-hidden="true" className="size-3.5" />
Rank {previous.rank} Rank {pad2(previous.rank)}
</span> </span>
<span className={`${s.h3} mt-1.5`}>{previous.title}</span> <span className={`${s.h3} mt-1.5`}>{previous.title}</span>
</Link> </Link>
@@ -210,8 +241,8 @@ export default function VerticalPage() {
className={`${s.cardLink} sm:col-start-2 sm:items-end sm:text-right`} className={`${s.cardLink} sm:col-start-2 sm:items-end sm:text-right`}
to={routes.vertical(next.slug)} to={routes.vertical(next.slug)}
> >
<span className="inline-flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted"> <span className={`${s.label} inline-flex items-center gap-1.5`}>
Rank {next.rank} Rank {pad2(next.rank)}
<ArrowRight aria-hidden="true" className="size-3.5" /> <ArrowRight aria-hidden="true" className="size-3.5" />
</span> </span>
<span className={`${s.h3} mt-1.5`}>{next.title}</span> <span className={`${s.h3} mt-1.5`}>{next.title}</span>
+75 -7
View File
@@ -7,7 +7,9 @@ import animate from 'tailwindcss-animate';
* *
* The palette is PIG's, inlined as literals rather than injected at runtime: * The palette is PIG's, inlined as literals rather than injected at runtime:
* this site has no user-chosen accent, so a runtime theme layer would be a * this site has no user-chosen accent, so a runtime theme layer would be a
* moving part that buys nothing. * moving part that buys nothing. Every value here is `hsl(var(--token))` —
* the triples themselves, with their measured contrast ratios, live in
* src/index.css. Nothing in this file is a raw colour.
*/ */
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
@@ -20,11 +22,16 @@ export default {
background: 'hsl(var(--bg))', background: 'hsl(var(--bg))',
surface: 'hsl(var(--surface))', surface: 'hsl(var(--surface))',
'surface-2': 'hsl(var(--surface-2))', 'surface-2': 'hsl(var(--surface-2))',
'surface-3': 'hsl(var(--surface-3))',
border: 'hsl(var(--border))', border: 'hsl(var(--border))',
// The only boundary token that clears 3:1 as a non-text signal. Inputs,
// selects, outline chips and icons at rest use it; `border` is decorative.
'border-strong': 'hsl(var(--border-strong))',
input: 'hsl(var(--border))', input: 'hsl(var(--border))',
ring: 'hsl(var(--accent))', ring: 'hsl(var(--ring))',
fg: 'hsl(var(--fg))', fg: 'hsl(var(--fg))',
foreground: 'hsl(var(--fg))', foreground: 'hsl(var(--fg))',
'fg-2': 'hsl(var(--fg-2))',
muted: 'hsl(var(--muted))', muted: 'hsl(var(--muted))',
'muted-foreground': 'hsl(var(--muted))', 'muted-foreground': 'hsl(var(--muted))',
// shadcn's `accent` is its SUBTLE hover surface, not the brand. Mapping // shadcn's `accent` is its SUBTLE hover surface, not the brand. Mapping
@@ -34,15 +41,24 @@ export default {
brand: 'hsl(var(--accent))', brand: 'hsl(var(--accent))',
'accent-fg': 'hsl(var(--accent-fg))', 'accent-fg': 'hsl(var(--accent-fg))',
'accent-on': 'hsl(var(--accent-on))', 'accent-on': 'hsl(var(--accent-on))',
'accent-edge': 'hsl(var(--accent-edge))',
'accent-subtle': 'hsl(var(--accent-subtle))', 'accent-subtle': 'hsl(var(--accent-subtle))',
// The tab bar's 2px state bar: accent in dark, accent-fg in light, so
// the trigger needs one class and no theme variant.
'tab-underline': 'hsl(var(--tab-underline))',
primary: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-on))' }, primary: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-on))' },
secondary: { DEFAULT: 'hsl(var(--surface-2))', foreground: 'hsl(var(--fg))' }, secondary: { DEFAULT: 'hsl(var(--surface-2))', foreground: 'hsl(var(--fg))' },
card: { DEFAULT: 'hsl(var(--surface))', foreground: 'hsl(var(--fg))' }, card: { DEFAULT: 'hsl(var(--surface))', foreground: 'hsl(var(--fg))' },
popover: { DEFAULT: 'hsl(var(--surface))', foreground: 'hsl(var(--fg))' }, popover: { DEFAULT: 'hsl(var(--surface))', foreground: 'hsl(var(--fg))' },
destructive: { DEFAULT: 'hsl(var(--danger))', foreground: 'hsl(0 0% 100%)' }, // `danger-on`, not white: white on the dark theme's red is 3.63:1.
destructive: { DEFAULT: 'hsl(var(--danger))', foreground: 'hsl(var(--danger-on))' },
positive: 'hsl(var(--positive))', positive: 'hsl(var(--positive))',
warning: 'hsl(var(--warning))', warning: 'hsl(var(--warning))',
'warning-on': 'hsl(var(--warning-on))',
'warning-subtle': 'hsl(var(--warning-subtle))',
danger: 'hsl(var(--danger))', danger: 'hsl(var(--danger))',
'danger-on': 'hsl(var(--danger-on))',
'danger-subtle': 'hsl(var(--danger-subtle))',
info: 'hsl(var(--info))', info: 'hsl(var(--info))',
// The board's three tile states. Named by MEANING, not by colour, so // The board's three tile states. Named by MEANING, not by colour, so
// the high-contrast palette can swap the values without renaming. // the high-contrast palette can swap the values without renaming.
@@ -53,16 +69,68 @@ export default {
'present-fg': 'hsl(var(--tile-present-fg))', 'present-fg': 'hsl(var(--tile-present-fg))',
absent: 'hsl(var(--tile-absent))', absent: 'hsl(var(--tile-absent))',
'absent-fg': 'hsl(var(--tile-absent-fg))', 'absent-fg': 'hsl(var(--tile-absent-fg))',
// An unplayed tile's edge (`border-tile-empty`).
empty: 'hsl(var(--tile-empty-border))',
}, },
}, },
fontFamily: { fontFamily: {
sans: ['Manrope Variable', 'Avenir Next', 'ui-sans-serif', 'system-ui', 'sans-serif'], sans: ['Geist Variable', 'Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'],
mono: ['ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'], mono: ['Geist Mono Variable', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'],
}, },
borderRadius: { md: '0.5rem', lg: '0.75rem', xl: '1rem' }, /*
* The type scale (spec §3). Sans unless the name says mono; the face is
* still applied by `font-mono` on the element, these only fix size,
* leading, weight and tracking. Responsive steps are the `-lg` / `-xl`
* siblings (`text-display lg:text-display-lg xl:text-display-xl`).
* Weight ceiling is 600 everywhere.
*/
fontSize: {
display: ['2.25rem', { lineHeight: '1.05', fontWeight: '600', letterSpacing: '-0.02em' }],
'display-lg': ['3rem', { lineHeight: '1.05', fontWeight: '600', letterSpacing: '-0.02em' }],
'display-xl': ['3.5rem', { lineHeight: '1.05', fontWeight: '600', letterSpacing: '-0.02em' }],
h1: ['1.75rem', { lineHeight: '1.15', fontWeight: '600', letterSpacing: '-0.015em' }],
'h1-lg': ['2rem', { lineHeight: '1.15', fontWeight: '600', letterSpacing: '-0.015em' }],
h2: ['1.5rem', { lineHeight: '1.15', fontWeight: '600', letterSpacing: '-0.015em' }],
'h2-lg': ['1.875rem', { lineHeight: '1.15', fontWeight: '600', letterSpacing: '-0.015em' }],
h3: ['1.125rem', { lineHeight: '1.3', fontWeight: '600', letterSpacing: '-0.01em' }],
lede: ['1.125rem', { lineHeight: '1.5', fontWeight: '400' }],
'lede-lg': ['1.25rem', { lineHeight: '1.5', fontWeight: '400' }],
// The tab claim: never mono, never uppercase, never shortened.
claim: ['1.125rem', { lineHeight: '1.35', fontWeight: '500' }],
'claim-lg': ['1.25rem', { lineHeight: '1.35', fontWeight: '500' }],
// The education floor. Nothing that explains is ever smaller.
prose: ['1rem', { lineHeight: '1.6', fontWeight: '400' }],
ui: ['0.875rem', { lineHeight: '1.4' }],
small: ['0.8125rem', { lineHeight: '1.45', fontWeight: '400' }],
caption: ['0.75rem', { lineHeight: '1.4' }],
// mono, uppercase — pair with `font-mono uppercase`.
label: ['0.6875rem', { lineHeight: '1.2', fontWeight: '500', letterSpacing: '0.04em' }],
'mono-data': ['0.8125rem', { lineHeight: '1.4', fontWeight: '400' }],
stat: ['0.8125rem', { lineHeight: '1.4', fontWeight: '600' }],
metric: ['2rem', { lineHeight: '1.1', fontWeight: '600', letterSpacing: '-0.01em' }],
// The landing's 0% → ~60% figure.
figure: ['2.5rem', { lineHeight: '1', fontWeight: '500', letterSpacing: '-0.02em' }],
'figure-lg': ['3.5rem', { lineHeight: '1', fontWeight: '500', letterSpacing: '-0.02em' }],
code: ['0.8125rem', { lineHeight: '1.6' }],
},
letterSpacing: { label: '0.04em' },
// A real four-step scale. 4px tiles/cells, 6px controls, 8px inner blocks,
// 12px surfaces. Named aliases resolve to the same CSS variables index.css
// declares, so a component can say what it means (`rounded-control`).
borderRadius: {
sm: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
xl: '0.75rem',
tile: 'var(--radius-tile)',
control: 'var(--radius-control)',
surface: 'var(--radius-surface)',
},
// Overlays only. Flat components carry no shadow; borders do the work.
boxShadow: { overlay: 'var(--shadow-overlay)' },
maxWidth: { canvas: '80rem', measure: 'var(--measure)', 'measure-wide': 'var(--measure-wide)' },
transitionDuration: { 1: 'var(--dur-1)', 2: 'var(--dur-2)', 3: 'var(--dur-3)' }, transitionDuration: { 1: 'var(--dur-1)', 2: 'var(--dur-2)', 3: 'var(--dur-3)' },
transitionTimingFunction: { enter: 'var(--ease-out)', exit: 'var(--ease-in)' }, transitionTimingFunction: { enter: 'var(--ease-out)', exit: 'var(--ease-in)' },
maxWidth: { canvas: '80rem' },
}, },
}, },
plugins: [animate], plugins: [animate],