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:
Omar Sobh
2026-07-23 07:38:14 -07:00
co-authored by Claude Opus 4.8
parent 6c82b79b01
commit 1a39457940
28 changed files with 1240 additions and 434 deletions
+66 -84
View File
@@ -1,109 +1,91 @@
import { useState } from 'react'
import { Link, useNavigate } 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 { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { AgentChat } from '@/components/AgentChat'
import { AddLayerForm } from '@/components/AddLayerForm'
import { PanelHeading, PanelCard, ProceedButton } from '@/components/cockpit/PanelChrome'
import { useSession } from '@/store/session'
const L2_PREFILL =
'i2c_scan to enumerate both ADXL355Z; matrix_text and matrix_count for on-board readouts; adxl355_stream for live vibration.'
const L3_PREFILL =
'Report only on exception; escalate ambiguous or high-consequence calls to the cloud model; never suppress a stale-sensor alarm.'
export function Module2() {
const navigate = useNavigate()
const l2 = useSession((s) => s.add.L2)
const l3 = useSession((s) => s.add.L3)
const tried = useSession((s) => s.tried)
const setTried = useSession((s) => s.setTried)
const setAddLayer = useSession((s) => s.setAddLayer)
const completePhase = useSession((s) => s.completePhase)
const [tried, setTried] = useState(0)
const allTried = tried >= 3
const ready = allTried && l2.trim().length > 0 && l3.trim().length > 0
// On reaching 3/3, seed Layers 2 & 3 with a starting draft (only if untouched).
useEffect(() => {
if (!allTried) return
if (!useSession.getState().add.L2.trim()) setAddLayer('L2', L2_PREFILL)
if (!useSession.getState().add.L3.trim()) setAddLayer('L3', L3_PREFILL)
}, [allTried, setAddLayer])
const onProceed = () => {
completePhase('m2')
navigate('/workshop/add')
}
return (
<main className="min-h-screen bg-background">
<header className="px-8 py-5 border-b border-border flex items-center justify-between">
<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/module1" className="font-mono text-[11px] text-muted-foreground hover:text-foreground tracking-widest uppercase">
← Module 1
</Link>
</header>
<PhaseStrip active="m2" />
<section className="px-8 py-10 max-w-3xl mx-auto space-y-6">
<div>
<Badge variant="secondary" className="font-mono text-[10px] tracking-widest uppercase mb-2">
Phase 4 of 5 · ~90 min
</Badge>
<h1 className="text-3xl font-bold tracking-tight">Module 2 · Skills &amp; policies</h1>
<p className="text-sm text-muted-foreground mt-2 max-w-xl">
Your agent already ships with expert skills — hardware, the MCU bridge, the LED matrix,
flashing, and more. Try them from the chat below: each prompt makes the agent use its
skills and tools on your real board.
</p>
</div>
<section>
<PanelHeading
eyebrow="PHASE 4 OF 5 · ~90 MIN"
title="Module 2 · Skills & policies"
intro="Talk to your agent and watch it run real tools on your board. Each prompt makes it use a built-in skill — then capture your domain's skills and the policy that governs them."
size={46}
/>
<div className="mt-9">
<AgentChat onProgress={(done) => setTried(done)} />
</div>
{/* What's next — revealed once all three prompts have run successfully. */}
{allTried ? (
<div className="space-y-6" data-testid="whats-next">
<div className="pt-2">
<h2 className="text-lg font-semibold tracking-tight">What&rsquo;s next</h2>
<p className="text-sm text-muted-foreground mt-1 max-w-xl">
You just watched the agent enumerate a bus and drive the matrix using its built-in
skills. Now capture <span className="font-medium text-foreground">your domain&rsquo;s</span>{' '}
skills and the policy that governs them — Layers 2 and 3 of your Agent Design Document.
</p>
</div>
<AddLayerForm
layer="L2"
title="ADD · Layer 2 — Skills"
description="What skills can the agent invoke to act on its domain?"
placeholder="Flash a sketch to the MCU; scroll a message; drive the damper; sample the IMU."
/>
<AddLayerForm
layer="L3"
title="ADD · Layer 3 — Policies & failure"
description="The actuation gate — and what happens when things break. For each failure you can name, which way does it fail? A fail-safe must never quietly report 'normal'."
placeholder={
'Autonomous: log + alert. Needs approval: drive the actuator. E-stop: operator halts actuation at any time.\n\n' +
'Failure states → response:\n' +
'• sensor disconnected / stuck value / drifting → mark UNKNOWN, never "nominal"\n' +
'• reading older than 60 s → treat as no reading\n' +
'• cloud unreachable → decide on-board, flag reduced confidence\n' +
'• agent unsure → escalate to a human, do not actuate'
}
/>
<div className="flex justify-end pt-2">
<Button size="lg" disabled={!ready} onClick={onProceed}>
Proceed to ADD builder →
</Button>
</div>
{allTried ? (
<div className="mt-8 space-y-5" data-testid="whats-next">
<div>
<h2 className="text-[22px] font-semibold tracking-[-0.01em]">What&rsquo;s next</h2>
<p className="mt-1 max-w-[560px] text-[15px] text-ink-2">
You just watched the agent enumerate the bus and drive the matrix with its built-in
skills. Now capture <span className="font-medium text-ink">your domain&rsquo;s</span> skills and
the policy that governs them — Layers 2 and 3, drafted below.
</p>
</div>
) : (
<Card>
<CardHeader>
<CardTitle className="text-base text-muted-foreground">What&rsquo;s next</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground leading-relaxed">
Try all three prompts above. Once your agent has run each one successfully, we&rsquo;ll
capture your domain&rsquo;s skills and policies here.
</p>
</CardContent>
</Card>
)}
</section>
</main>
<AddLayerForm
layer="L2"
title="ADD · Layer 2 — Skills"
description="What skills can the agent invoke to act on its domain?"
placeholder="Flash a sketch to the MCU; scroll a message; drive the damper; sample the IMU."
/>
<AddLayerForm
layer="L3"
title="ADD · Layer 3 — Policies & failure"
description="The actuation gate — and what happens when things break. A fail-safe must never quietly report 'normal'."
placeholder="Report only on exception; escalate ambiguous calls to the cloud; never suppress a stale-sensor alarm."
/>
<div className="flex justify-end">
<ProceedButton disabled={!ready} onClick={onProceed}>
Proceed to Module 3 →
</ProceedButton>
</div>
</div>
) : (
<PanelCard className="mt-6">
<div className="text-[15px] font-semibold text-ink-2">What&rsquo;s next</div>
<p className="mt-2 text-[14px] leading-[1.5] text-ink-3">
Try all three prompts above. Once your agent has run each one successfully, we&rsquo;ll
capture your domain&rsquo;s skills and policies here.
</p>
</PanelCard>
)}
</section>
)
}