6c2f3899c8
git.karti.ai/PIG/PIG-Demo is now the source of truth, public and anonymously cloneable. Every self-link in the site and the docs repoints there — Gitea serves file paths at /src/branch/main/, not /blob/main/, so those needed rewriting rather than a hostname swap. CI moves with it. An archived GitHub repo is read-only and its Actions stop firing, so leaving the workflow there would have meant a repo whose gates silently never run. .github/ is deleted rather than kept for reference: a workflow that can never execute is worse than no workflow, because it looks like coverage. The Gitea workflow is not a copy. That runner is aarch64 and installs pnpm through corepack from `packageManager` rather than pnpm/action-setup, uses checkout@v4 and setup-node@v4, and fetches uv from astral.sh directly. It is also configured `container.network: host` — nothing here needs a service container, but the comment says so, because that setting cost the sibling repo three failed runs. The header and footer icon changed from the GitHub mark to a neutral one. A GitHub logo pointing at a Gitea instance is a small lie about where the code lives, on a site whose argument is that you can go and check it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
556 lines
20 KiB
TypeScript
556 lines
20 KiB
TypeScript
import * as React from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { ArrowRight, ArrowUpRight, GitBranch, Menu } from 'lucide-react';
|
|
|
|
import { listDemos } from '@/lib/demo-kit/registry';
|
|
import type { DemoMeta } from '@/lib/demo-kit/types';
|
|
import { lineup, routes } from '@/content/lineup';
|
|
import type { VerticalEntry } from '@/content/verticals';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
NavigationMenu,
|
|
NavigationMenuContent,
|
|
NavigationMenuItem,
|
|
NavigationMenuLink,
|
|
NavigationMenuList,
|
|
NavigationMenuTrigger,
|
|
navigationMenuTriggerStyle,
|
|
} from '@/components/ui/navigation-menu';
|
|
import {
|
|
Accordion,
|
|
AccordionContent,
|
|
AccordionItem,
|
|
AccordionTrigger,
|
|
} from '@/components/ui/accordion';
|
|
import {
|
|
Sheet,
|
|
SheetBody,
|
|
SheetClose,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetFooter,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
SheetTrigger,
|
|
} from '@/components/ui/sheet';
|
|
import { ContrastToggle } from '@/components/site/ContrastToggle';
|
|
import { DemoIcon } from '@/components/site/DemoIcon';
|
|
import { PIG_URL, REPO_URL, VERIFIERS_WORDLE_URL } from '@/components/site/links';
|
|
import { ThemeToggle } from '@/components/site/ThemeToggle';
|
|
import { Wordmark } from '@/components/site/Wordmark';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/*
|
|
* This header is GENERATED. Nothing in it names a demo.
|
|
*
|
|
* Adding `src/demos/<slug>/` puts that demo in the Demos panel, in its
|
|
* vertical, and (if it is the first live one) behind the CTA, with zero edits
|
|
* to this file. The only hand-written lists here are the five concepts under
|
|
* "How it works", which are properties of the idea rather than of the lineup.
|
|
*/
|
|
|
|
/**
|
|
* The real upstream taskset, quoted rather than paraphrased.
|
|
*
|
|
* A mega-menu that contains source code instead of more links is the cheapest
|
|
* signal on the whole site that this is not a brochure — it is the first thing
|
|
* a technical buyer sees, and it is true before they have clicked anything.
|
|
*/
|
|
const TASKSET_SOURCE = `class WordleConfig(TextArenaConfig):
|
|
game: Literal["Wordle-v0"] = "Wordle-v0"
|
|
|
|
class WordleTaskset(TextArenaTaskset, vf.Taskset[TextArenaTask, WordleConfig]):
|
|
pass`;
|
|
|
|
const CONCEPTS: readonly { term: string; gloss: string }[] = [
|
|
{
|
|
term: 'Environment',
|
|
gloss: 'The task, the legal moves and the grader, packaged so anyone can install and run it.',
|
|
},
|
|
{
|
|
term: 'Rollout',
|
|
gloss: 'One episode. The model acts, the environment answers, and every turn is recorded.',
|
|
},
|
|
{
|
|
term: 'Reward',
|
|
gloss: 'The number the run is scored on. You write it, so you decide what "good" means.',
|
|
},
|
|
{
|
|
term: 'Harness',
|
|
gloss: 'The runner that plays a taskset against a model and keeps the receipts.',
|
|
},
|
|
{
|
|
term: 'Held-out grading',
|
|
gloss: 'Scored on problems the model has never seen, which is the only way the score means anything.',
|
|
},
|
|
];
|
|
|
|
/**
|
|
* The header's Verticals panel shows the LINEUP, not the registry's groups.
|
|
*
|
|
* `listVerticals()` only ever contains verticals that already have a demo —
|
|
* today that is one group, `reference`, which `src/content/verticals.ts` is
|
|
* 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 useLineup() {
|
|
return React.useMemo(() => {
|
|
// `listDemos` already returns a fresh array sorted by order then slug, and
|
|
// `listVerticals` already drops the empty verticals. Neither needs redoing
|
|
// here — if this file starts re-sorting the lineup, the header and the
|
|
// gallery will eventually disagree about what "first" means.
|
|
const demos = listDemos();
|
|
const live = demos.filter((demo) => demo.status === 'live');
|
|
const spec = demos.filter((demo) => demo.status === 'spec');
|
|
|
|
// Already sorted by rank in `@/content/lineup`. Copied rather than passed
|
|
// through, because the panel props are mutable arrays and `lineup` is the
|
|
// one every page reads.
|
|
const verticals: HeaderVertical[] = [...lineup];
|
|
|
|
// The CTA follows the lineup rather than naming a slug. Today the first
|
|
// live demo IS the word game, so this resolves to the wordle route; when a
|
|
// second one ships ahead of it, the button moves with it and this file does
|
|
// not change.
|
|
const primary = live[0] ?? demos[0];
|
|
const ctaHref = primary ? `/demos/${primary.slug}` : '/demos';
|
|
|
|
return { live, spec, verticals, ctaHref };
|
|
}, []);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ panels */
|
|
|
|
/**
|
|
* Spreads `...rest` onto the Link, and that is not tidiness.
|
|
*
|
|
* Both `NavigationMenuLink asChild` and `SheetClose asChild` work by cloning
|
|
* this element and merging their own `onClick` into it. A component that takes
|
|
* `demo` 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
|
|
* the page you just arrived at.
|
|
*/
|
|
function DemoRow({
|
|
demo,
|
|
muted = false,
|
|
className,
|
|
...rest
|
|
}: { demo: DemoMeta; muted?: boolean } & Omit<React.ComponentProps<typeof Link>, 'to'>) {
|
|
return (
|
|
<Link
|
|
to={`/demos/${demo.slug}`}
|
|
className={cn(
|
|
'flex gap-3 rounded-lg p-2.5 transition-colors duration-1 ease-enter hover:bg-surface-2',
|
|
muted && 'opacity-70 hover:opacity-100',
|
|
className,
|
|
)}
|
|
{...rest}
|
|
>
|
|
<DemoIcon name={demo.icon} className="mt-0.5 size-4 text-accent-fg" />
|
|
<span className="flex min-w-0 flex-col gap-0.5">
|
|
<span className="flex items-center gap-2 text-sm font-medium text-fg">
|
|
{demo.title}
|
|
{demo.status === 'spec' ? (
|
|
<Badge variant="outline" className="font-normal">
|
|
Spec
|
|
</Badge>
|
|
) : null}
|
|
</span>
|
|
<span className="text-xs leading-relaxed text-muted">
|
|
{/*
|
|
A spec demo shows the BUYER, not the technology: "Head of Claims"
|
|
says who is meant to care, where a taskset name says nothing to the
|
|
person reading this in a boardroom.
|
|
*/}
|
|
{demo.status === 'spec' ? `For the ${demo.persona}` : demo.tagline}
|
|
</span>
|
|
</span>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
function PanelHeading({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<p className="px-2.5 pb-1 text-xs font-semibold uppercase tracking-wider text-muted">
|
|
{children}
|
|
</p>
|
|
);
|
|
}
|
|
|
|
function DemosPanel({ live, spec }: { live: DemoMeta[]; spec: DemoMeta[] }) {
|
|
return (
|
|
<div className="w-[min(92vw,720px)]">
|
|
<div className="grid grid-cols-2 gap-4 p-4">
|
|
<section aria-label="Live now" className="flex flex-col gap-0.5">
|
|
<PanelHeading>Live now</PanelHeading>
|
|
{live.length > 0 ? (
|
|
live.map((demo) => (
|
|
<NavigationMenuLink asChild key={demo.slug}>
|
|
<DemoRow demo={demo} />
|
|
</NavigationMenuLink>
|
|
))
|
|
) : (
|
|
<p className="p-2.5 text-xs text-muted">No interactive demos published yet.</p>
|
|
)}
|
|
</section>
|
|
|
|
<section
|
|
aria-label="Shipping next"
|
|
className="flex flex-col gap-0.5 border-l border-border pl-4"
|
|
>
|
|
<PanelHeading>Shipping next</PanelHeading>
|
|
{spec.length > 0 ? (
|
|
spec.map((demo) => (
|
|
<NavigationMenuLink asChild key={demo.slug}>
|
|
<DemoRow demo={demo} muted />
|
|
</NavigationMenuLink>
|
|
))
|
|
) : (
|
|
<p className="p-2.5 text-xs text-muted">Nothing queued.</p>
|
|
)}
|
|
</section>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-4 border-t border-border bg-surface-2 px-5 py-3">
|
|
<p className="text-xs text-muted">
|
|
Every demo replays a real rollout from a real environment.
|
|
</p>
|
|
<NavigationMenuLink asChild>
|
|
<Link
|
|
to="/demos"
|
|
className="inline-flex shrink-0 items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline"
|
|
>
|
|
Browse all demos
|
|
<ArrowRight aria-hidden="true" className="size-3.5" />
|
|
</Link>
|
|
</NavigationMenuLink>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function VerticalsPanel({ verticals }: { verticals: HeaderVertical[] }) {
|
|
return (
|
|
<div className="w-[min(92vw,720px)] p-4">
|
|
{verticals.length > 0 ? (
|
|
<div className="grid grid-cols-2 gap-x-4 gap-y-0.5">
|
|
{verticals.map((vertical) => (
|
|
<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>
|
|
</Link>
|
|
</NavigationMenuLink>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="p-2.5 text-xs text-muted">No verticals in the lineup yet.</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TasksetSource({ className }: { className?: string }) {
|
|
return (
|
|
<div className={cn('flex flex-col gap-2 rounded-lg border border-border bg-bg p-3', className)}>
|
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted">
|
|
The actual taskset
|
|
</p>
|
|
{/*
|
|
Wrapped, not scrolled. The source is quoted verbatim — reformatting it
|
|
to fit would make it stop being a quote — and the widest line is a third
|
|
wider than this column at any font size worth reading. In a panel whose
|
|
entire job is "this is real code", a visible wrap beats a third of the
|
|
line hidden behind a scrollbar nobody in a boardroom will drag.
|
|
*/}
|
|
<pre className="overflow-x-auto whitespace-pre-wrap break-words text-[11px] leading-relaxed text-fg">
|
|
<code className="font-mono">{TASKSET_SOURCE}</code>
|
|
</pre>
|
|
<a
|
|
href={VERIFIERS_WORDLE_URL}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
className="inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline"
|
|
>
|
|
verifiers/environments/wordle
|
|
<ArrowUpRight aria-hidden="true" className="size-3.5" />
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function HowItWorksPanel({ ctaHref }: { ctaHref: string }) {
|
|
return (
|
|
<div
|
|
// 820px, not more: the panel hangs off the LEFT edge of the nav, which
|
|
// starts about 100px in, so anything wider pushes past the right edge of
|
|
// a 1024px laptop and gives the whole page a horizontal scrollbar.
|
|
className="w-[min(90vw,820px)] p-4"
|
|
>
|
|
<div className="grid grid-cols-[minmax(0,0.85fr)_minmax(0,1fr)] gap-4">
|
|
<dl className="flex flex-col gap-3">
|
|
{CONCEPTS.map((concept) => (
|
|
<div key={concept.term} className="flex flex-col gap-0.5">
|
|
<dt className="text-sm font-medium text-fg">{concept.term}</dt>
|
|
<dd className="text-xs leading-relaxed text-muted">{concept.gloss}</dd>
|
|
</div>
|
|
))}
|
|
<NavigationMenuLink asChild>
|
|
<Link
|
|
to={ctaHref}
|
|
className="mt-1 inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-4 hover:underline"
|
|
>
|
|
See all five on one recorded run
|
|
<ArrowRight aria-hidden="true" className="size-3.5" />
|
|
</Link>
|
|
</NavigationMenuLink>
|
|
</dl>
|
|
<TasksetSource />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ mobile */
|
|
|
|
function MobileNav({
|
|
live,
|
|
spec,
|
|
verticals,
|
|
ctaHref,
|
|
}: {
|
|
live: DemoMeta[];
|
|
spec: DemoMeta[];
|
|
verticals: HeaderVertical[];
|
|
ctaHref: string;
|
|
}) {
|
|
return (
|
|
<Sheet>
|
|
<SheetTrigger asChild>
|
|
<Button variant="ghost" size="icon-touch" aria-label="Open menu">
|
|
<Menu aria-hidden="true" />
|
|
</Button>
|
|
</SheetTrigger>
|
|
{/*
|
|
The sheet is a flex column: header, a scrolling body, then a pinned
|
|
footer. Scrolling the BODY rather than the content root is what keeps a
|
|
long lineup reachable on a short phone, and `overscroll-contain` on it
|
|
stops the flick chaining into the page underneath.
|
|
*/}
|
|
<SheetContent side="right" className="w-[min(92vw,24rem)]">
|
|
<SheetHeader>
|
|
<SheetTitle>Menu</SheetTitle>
|
|
<SheetDescription className="sr-only">
|
|
Demos, verticals, and how these environments work.
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<SheetBody>
|
|
<Accordion type="multiple" defaultValue={['demos']}>
|
|
<AccordionItem value="demos">
|
|
<AccordionTrigger>Demos</AccordionTrigger>
|
|
<AccordionContent className="flex flex-col gap-0.5">
|
|
<PanelHeading>Live now</PanelHeading>
|
|
{live.map((demo) => (
|
|
<SheetClose asChild key={demo.slug}>
|
|
<DemoRow demo={demo} />
|
|
</SheetClose>
|
|
))}
|
|
<PanelHeading>Shipping next</PanelHeading>
|
|
{spec.map((demo) => (
|
|
<SheetClose asChild key={demo.slug}>
|
|
<DemoRow demo={demo} muted />
|
|
</SheetClose>
|
|
))}
|
|
<SheetClose asChild>
|
|
<Link
|
|
to="/demos"
|
|
className="tap inline-flex items-center gap-1 p-2.5 text-xs font-medium text-accent-fg"
|
|
>
|
|
Browse all demos
|
|
<ArrowRight aria-hidden="true" className="size-3.5" />
|
|
</Link>
|
|
</SheetClose>
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
|
|
<AccordionItem value="verticals">
|
|
<AccordionTrigger>Verticals</AccordionTrigger>
|
|
<AccordionContent className="flex flex-col gap-0.5">
|
|
{verticals.map((vertical) => (
|
|
<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">
|
|
<AccordionTrigger>How it works</AccordionTrigger>
|
|
<AccordionContent className="flex flex-col gap-3">
|
|
<dl className="flex flex-col gap-3">
|
|
{CONCEPTS.map((concept) => (
|
|
<div key={concept.term} className="flex flex-col gap-0.5">
|
|
<dt className="text-sm font-medium text-fg">{concept.term}</dt>
|
|
<dd className="text-xs leading-relaxed text-muted">{concept.gloss}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
<TasksetSource />
|
|
</AccordionContent>
|
|
</AccordionItem>
|
|
</Accordion>
|
|
|
|
<a
|
|
href={PIG_URL}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
className="tap mt-2 flex items-center justify-between border-b border-border py-3 text-sm font-medium text-fg"
|
|
>
|
|
primeintellectgrowth.com
|
|
<ArrowUpRight aria-hidden="true" className="size-4 text-muted" />
|
|
</a>
|
|
<a
|
|
href={REPO_URL}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
className="tap flex items-center justify-between border-b border-border py-3 text-sm font-medium text-fg"
|
|
>
|
|
Source on Gitea
|
|
<GitBranch aria-hidden="true" className="size-4 text-muted" />
|
|
</a>
|
|
|
|
<div className="flex items-center gap-1 pt-3">
|
|
<ThemeToggle />
|
|
<ContrastToggle />
|
|
</div>
|
|
</SheetBody>
|
|
|
|
<SheetFooter>
|
|
<SheetClose asChild>
|
|
<Button asChild size="lg" className="w-full">
|
|
<Link to={ctaHref}>Play the demo</Link>
|
|
</Button>
|
|
</SheetClose>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ header */
|
|
|
|
export function SiteHeader() {
|
|
const { live, spec, verticals, ctaHref } = useLineup();
|
|
|
|
return (
|
|
<header
|
|
// The height token already folds in --safe-top, so the padding and the
|
|
// height come from the same source and a notch cannot push the row off
|
|
// the bottom edge of the bar.
|
|
className="sticky top-0 z-50 h-[var(--app-header-h)] border-b border-border bg-surface/80 pt-[var(--safe-top)] backdrop-blur-md supports-[backdrop-filter]:bg-surface/70"
|
|
>
|
|
<div className="mx-auto flex h-full max-w-canvas items-center gap-2 px-4 pl-[max(1rem,var(--safe-left))] pr-[max(1rem,var(--safe-right))]">
|
|
<Link
|
|
to="/"
|
|
className="tap flex shrink-0 items-center rounded-md pr-2 text-base"
|
|
aria-label="PIG demo, home"
|
|
>
|
|
<Wordmark />
|
|
</Link>
|
|
|
|
{/*
|
|
`h-full` is not cosmetic. The viewport hangs off the Root's
|
|
`top-full`, so a Root only as tall as its 36px triggers drops the
|
|
panel INSIDE the header, over its own bottom border.
|
|
*/}
|
|
<NavigationMenu className="hidden h-full lg:flex" delayDuration={120}>
|
|
<NavigationMenuList>
|
|
<NavigationMenuItem>
|
|
<NavigationMenuTrigger>Demos</NavigationMenuTrigger>
|
|
<NavigationMenuContent>
|
|
<DemosPanel live={live} spec={spec} />
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
|
|
<NavigationMenuItem>
|
|
<NavigationMenuTrigger>Verticals</NavigationMenuTrigger>
|
|
<NavigationMenuContent>
|
|
<VerticalsPanel verticals={verticals} />
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
|
|
<NavigationMenuItem>
|
|
<NavigationMenuTrigger>How it works</NavigationMenuTrigger>
|
|
<NavigationMenuContent>
|
|
<HowItWorksPanel ctaHref={ctaHref} />
|
|
</NavigationMenuContent>
|
|
</NavigationMenuItem>
|
|
|
|
<NavigationMenuItem>
|
|
<NavigationMenuLink
|
|
href={PIG_URL}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
className={cn(navigationMenuTriggerStyle(), 'text-muted hover:text-fg')}
|
|
>
|
|
primeintellectgrowth.com
|
|
<ArrowUpRight aria-hidden="true" className="size-3.5" />
|
|
</NavigationMenuLink>
|
|
</NavigationMenuItem>
|
|
</NavigationMenuList>
|
|
</NavigationMenu>
|
|
|
|
<div className="ml-auto flex items-center gap-1">
|
|
<div className="hidden items-center gap-1 lg:flex">
|
|
<ThemeToggle />
|
|
<ContrastToggle />
|
|
<Button asChild variant="ghost" size="icon">
|
|
<a
|
|
href={REPO_URL}
|
|
target="_blank"
|
|
rel="noreferrer noopener"
|
|
aria-label="Source on Gitea"
|
|
>
|
|
<GitBranch aria-hidden="true" />
|
|
</a>
|
|
</Button>
|
|
</div>
|
|
|
|
<Button asChild size="touch" className="hidden lg:inline-flex">
|
|
<Link to={ctaHref}>Play the demo</Link>
|
|
</Button>
|
|
|
|
<div className="lg:hidden">
|
|
<MobileNav live={live} spec={spec} verticals={verticals} ctaHref={ctaHref} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|