feat(web): two-pane "cockpit" redesign with a live board rail
Restructure the workshop flow into the designer's cockpit: a persistent shell (header + 5-step stepper + sticky instrument rail) wrapping the phase routes via a React-Router layout route, so the rail stays mounted across navigation. - Design system: IBM Plex Mono + Newsreader; the full cockpit token set (light + dark) in index.css; a working light/dark theme toggle (store `theme` + useApplyTheme); a `switch` ui primitive. - Shell: CockpitLayout, Stepper (forward-gated), PanelChrome helpers. Every phase page restyled to the editorial panels + the WORKSHOP-FLOW fixes (channels-after-bind, domain framing + L1 prefill, L2/L3 prefill at 3/3, in-place submission finale). Store gains `tried` + `channels.saidHi` (v4). - Live rail (CockpitRail): real node heartbeat + agent activity log + ADD progress; sim telemetry (useTelemetry) for the waveform/accel/I2C behind a seam, marked SIM. - LED-matrix PIXEL MIRROR (real): the rail shows exactly what the physical matrix displays — API GET /nodes/:team/matrix reads the board's framebuffer off the :9999 relay (readMatrixFrame + the `matrixget` relay command); useMatrixMirror polls it and unpacks the 104 bits. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6c82b79b01
commit
1a39457940
+105
-103
@@ -1,124 +1,126 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { PhaseStrip } from '@/components/PhaseStrip'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { AddLayerForm } from '@/components/AddLayerForm'
|
||||
import { AddDocument } from '@/components/AddDocument'
|
||||
import { OpenYourNode } from '@/components/OpenYourNode'
|
||||
import { PanelHeading, ProceedButton } from '@/components/cockpit/PanelChrome'
|
||||
import { ADD_LAYERS } from '@/lib/addLayers'
|
||||
import { useSession } from '@/store/session'
|
||||
import { makeSubmissionCode } from '@/lib/submission'
|
||||
|
||||
export function AddBuilder() {
|
||||
const navigate = useNavigate()
|
||||
const team = useSession((s) => s.team)
|
||||
const add = useSession((s) => s.add)
|
||||
const submission = useSession((s) => s.submission)
|
||||
const setSubmission = useSession((s) => s.setSubmission)
|
||||
const completePhase = useSession((s) => s.completePhase)
|
||||
|
||||
const complete =
|
||||
add.L1.trim().length > 0 &&
|
||||
add.L2.trim().length > 0 &&
|
||||
add.L3.trim().length > 0 &&
|
||||
add.L4.trim().length > 0 &&
|
||||
add.L5.trim().length > 0
|
||||
|
||||
const onExport = () => window.print()
|
||||
|
||||
const onSubmit = () => {
|
||||
const code = makeSubmissionCode(team, add)
|
||||
setSubmission({ code, submittedAt: new Date().toISOString() })
|
||||
completePhase('add')
|
||||
// best-effort push to the collective is wired in the sync layer (Part B)
|
||||
}
|
||||
|
||||
const complete = ADD_LAYERS.every(({ key }) => add[key].trim().length > 0)
|
||||
const submitted = !!submission.code
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background">
|
||||
<header className="px-8 py-5 border-b border-border flex items-center justify-between print:hidden">
|
||||
<div className="font-mono text-xs tracking-widest uppercase">
|
||||
APESS <span className="text-primary font-bold">2026</span>
|
||||
<span className="text-muted-foreground"> · Workshop</span>
|
||||
</div>
|
||||
<Link to="/workshop/module2" className="font-mono text-[11px] text-muted-foreground hover:text-foreground tracking-widest uppercase">
|
||||
← Module 2
|
||||
</Link>
|
||||
</header>
|
||||
const onExport = () => window.print()
|
||||
const onSubmit = () => {
|
||||
setSubmission({ code: makeSubmissionCode(team, add), submittedAt: new Date().toISOString() })
|
||||
completePhase('add')
|
||||
}
|
||||
|
||||
<div className="print:hidden">
|
||||
<PhaseStrip active="add" />
|
||||
</div>
|
||||
|
||||
<section className="px-8 py-10 max-w-5xl mx-auto space-y-6">
|
||||
<div className="print:hidden">
|
||||
<Badge variant="secondary" className="font-mono text-[10px] tracking-widest uppercase mb-2">
|
||||
Phase 5 of 5 · ~90 min · deadline 19:00
|
||||
</Badge>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Module 3 · Harness, Loops & submit</h1>
|
||||
<p className="text-sm text-muted-foreground mt-2 max-w-xl">
|
||||
Finish Layers 4 and 5 — how your node reasons and how it runs over time — review the assembled
|
||||
Agent Design Document, export a PDF, and submit before the deadline.
|
||||
// ── Submission finale (design panel6) ──
|
||||
if (submitted) {
|
||||
return (
|
||||
<section className="print:hidden">
|
||||
<div className="mt-5 rounded-[18px] border border-[var(--green-border-2)] bg-[linear-gradient(180deg,var(--green-bg),var(--surface))] px-12 py-14 text-center">
|
||||
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-[var(--green)] text-[32px] text-white">
|
||||
✓
|
||||
</div>
|
||||
<h1 className="mt-6 text-[44px] font-semibold tracking-[-0.02em]">ADD submitted</h1>
|
||||
<p className="mx-auto mt-3.5 max-w-[460px] text-[18px] leading-[1.5] text-ink-2">
|
||||
Team <strong className="font-semibold text-ink">{team.name || 'Matrix'}</strong> ·{' '}
|
||||
{(team.name || 'crimson-node').toLowerCase().replace(/\s+/g, '-')} — your Agent Design Document
|
||||
is in for judging. Your node keeps running the design you just shipped.
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<OpenYourNode variant="inline" />
|
||||
<div className="mt-7 inline-flex flex-wrap justify-center gap-2.5 font-mono text-[11px] tracking-[0.1em] text-[var(--green)]">
|
||||
{ADD_LAYERS.map(({ n, title }) => (
|
||||
<span key={n} className="rounded-md border border-[var(--green-border)] px-[11px] py-1.5">
|
||||
L{n} {title.split(' ')[0].toUpperCase()}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/workshop')}
|
||||
className="rounded-[9px] border border-line-2 bg-surface px-[22px] py-2.5 text-[15px] text-ink transition-colors hover:border-blue hover:text-blue-ink"
|
||||
>
|
||||
← Back to start
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-6 print:hidden">
|
||||
<AddLayerForm
|
||||
layer="L4"
|
||||
title="ADD · Layer 4 — Harness (where each decision runs)"
|
||||
description="Which decisions run on the board, which escalate to the cloud — and what still works with no network at all. Describe the degradation path, not just the happy path."
|
||||
placeholder={
|
||||
'Routine checks: on-board model, no network needed.\n' +
|
||||
'Ambiguous or high-consequence calls: escalate to the cloud model.\n\n' +
|
||||
'Degradation path:\n' +
|
||||
'• cloud slow or rate-limited → fall back on-board, note reduced confidence\n' +
|
||||
'• no network at all → keep sensing, logging and safing locally; queue anything that needs escalation\n' +
|
||||
'• on-board model unavailable → stop actuating, alert, keep recording'
|
||||
}
|
||||
/>
|
||||
<AddLayerForm
|
||||
layer="L5"
|
||||
title="ADD · Layer 5 — Loops (cadence, and what happens when a cycle fails)"
|
||||
description="How often it checks, what it reports by exception — and how the loop behaves when a cycle fails: stale readings, missed ticks, partial data."
|
||||
placeholder={
|
||||
'Heartbeat every 30 s; sample the sensor each minute; report only on exception; daily summary.\n\n' +
|
||||
'When a cycle fails:\n' +
|
||||
'• missed tick → skip, do not back-fill invented data\n' +
|
||||
'• partial data → report what is missing, not an average of what is left\n' +
|
||||
'• N consecutive failures → escalate to a human and stop acting on the readings'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card className="print:border-0 print:shadow-none">
|
||||
<CardHeader className="print:hidden flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="text-base">Assembled document</CardTitle>
|
||||
<Button variant="outline" size="sm" onClick={onExport}>Export PDF</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="print:p-0">
|
||||
<AddDocument />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center justify-between gap-4 pt-2 print:hidden">
|
||||
{submitted ? (
|
||||
<div className="border border-teal/40 bg-teal/5 rounded-md px-4 py-3">
|
||||
<div className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">Submitted</div>
|
||||
<div className="font-mono text-sm font-bold text-teal">{submission.code}</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{complete ? 'All five layers complete — ready to submit.' : 'Complete all five layers to submit.'}
|
||||
</span>
|
||||
)}
|
||||
<Button size="lg" disabled={!complete || submitted} onClick={onSubmit}>
|
||||
{submitted ? 'Submitted ✓' : 'Submit ADD'}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="print:hidden">
|
||||
<PanelHeading
|
||||
eyebrow="PHASE 5 OF 5 · ~90 MIN · DEADLINE 19:00"
|
||||
title="Module 3 · Harness, Loops & submit"
|
||||
intro="Finish Layers 4 and 5 — how your node reasons and how it runs over time — review the assembled Agent Design Document, export a PDF, and submit before the deadline."
|
||||
size={44}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 grid gap-4 md:grid-cols-2 print:hidden">
|
||||
<AddLayerForm
|
||||
layer="L4"
|
||||
title="ADD · Layer 4 — Harness"
|
||||
description="Which decisions run on the board, which escalate — and the degradation path, not just the happy path."
|
||||
placeholder={
|
||||
'Routine checks: on-board model, no network needed.\n' +
|
||||
'Ambiguous or high-consequence calls: escalate to the cloud model.\n\n' +
|
||||
'Degradation path:\n' +
|
||||
'• cloud slow → fall back on-board, note reduced confidence\n' +
|
||||
'• no network → keep sensing, logging and safing locally\n' +
|
||||
'• on-board model down → stop actuating, alert, keep recording'
|
||||
}
|
||||
/>
|
||||
<AddLayerForm
|
||||
layer="L5"
|
||||
title="ADD · Layer 5 — Loops"
|
||||
description="How often it checks, what it reports by exception — and how the loop behaves when a cycle fails."
|
||||
placeholder={
|
||||
'Heartbeat every 30 s; sample each minute; report only on exception.\n\n' +
|
||||
'When a cycle fails:\n' +
|
||||
'• missed tick → skip, do not back-fill invented data\n' +
|
||||
'• partial data → report what is missing\n' +
|
||||
'• N failures → escalate to a human and stop acting'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 rounded-[14px] border border-line bg-surface p-[26px_28px]">
|
||||
<div className="flex items-center justify-between print:hidden">
|
||||
<div className="text-[19px] font-semibold">Assembled document</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onExport}
|
||||
className="rounded-lg border border-line-2 bg-surface px-4 py-2 text-[14px] text-ink transition-colors hover:border-blue hover:text-blue-ink"
|
||||
>
|
||||
Export PDF
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-[18px] print:mt-0">
|
||||
<AddDocument />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center justify-between gap-4 print:hidden">
|
||||
<span className={complete ? 'text-[15px] text-[var(--green)]' : 'text-[15px] text-[var(--muted-2)]'}>
|
||||
{complete ? 'All five layers complete — ready to submit.' : 'Complete all five layers to submit.'}
|
||||
</span>
|
||||
<ProceedButton disabled={!complete} onClick={onSubmit}>
|
||||
Submit ADD
|
||||
</ProceedButton>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user