import { createContext, useContext, useCallback, useState } from 'react' import { useNavigate } from 'react-router-dom' import { ReactFlow, ReactFlowProvider, Background, Controls, Handle, Position, addEdge, useEdgesState, useNodesState, type Edge, type Connection, type NodeTypes, } from '@xyflow/react' import { PanelHeading } from '@/components/cockpit/PanelChrome' import { AgentChatPane, StarterRow } from '@/components/cockpit/AgentChatPieces' import { WaveformCanvas } from '@/components/cockpit/WaveformCanvas' import { useSetProceed } from '@/lib/ProceedContext' import { useSharedAgentChat } from '@/lib/AgentChatContext' import { useSession } from '@/store/session' import { useNodeFeed } from '@/lib/useNodeFeed' import { useTelemetry } from '@/lib/useTelemetry' import { useMatrixMirror } from '@/lib/useMatrixMirror' import { cn } from '@/lib/utils' /** * Module 1 · UnoQ Dashboard — a configurator built on React Flow. The chat card * and the UnoQ dashboard are two nodes; the participant plumbs them together by * dragging an edge from the chat's right handle to the dashboard's left handle. * Until they're wired, no data flows through the dashboard (matrix off, no accel, * no logs). Once connected the edge animates and the board goes live — then they * drive the LED matrix + sensors through the agent and watch the data arrive. * Wiring it up unlocks Proceed. (React Flow: https://reactflow.dev/learn) */ // Whether data is flowing (the edge is connected AND the board is online). const LiveContext = createContext(false) // ── the UnoQ dashboard node (right) ────────────────────────────────────────── function UnoQNode() { const live = useContext(LiveContext) const teamId = useSession((s) => s.teamId) const team = useSession((s) => s.team) const tel = useTelemetry(live) const mirror = useMatrixMirror(teamId, live) const { logs } = useSharedAgentChat() const dots = mirror ?? tel.matrix const nodeName = team.name ? team.name.toLowerCase().replace(/\s+/g, '-') : 'crimson-node' const recent = logs.slice(-6) return (
{nodeName} {live ? 'DATA FLOWING' : 'NO DATA'}
{/* LED matrix */}
LED MATRIX · 13×8
{Array.from({ length: 104 }).map((_, i) => { const lit = live && dots[i] return ( ) })}
{/* live acceleration */}
LIVE ACCELERATION · g {!live ? 'NO STREAM' : tel.event === 'impact' ? 'IMPACT SPIKE' : 'NOMINAL'}
{live ? ( ) : (
— no data flowing —
)}
{/* logs */}
AGENT LOGS
{!live ? (
— no data flowing —
) : recent.length === 0 ? (
idle — ask your agent to do something
) : ( recent.map((e, i) => (
{e.label}
)) )}
) } // ── the chat node (left) ───────────────────────────────────────────────────── function ChatNode() { const connected = useSession((s) => s.device.connected) const feed = useNodeFeed(useSession((s) => s.teamId), connected) const online = connected && feed.online const { messages, starters, sending, send } = useSharedAgentChat() return (
send(t)} heightClass="h-[200px]" />
) } const NODE_TYPES: NodeTypes = { chat: ChatNode, unoq: UnoQNode } const INITIAL_NODES = [ // chat is pinned on the left (static); the dashboard floats free (draggable) { id: 'chat', type: 'chat', position: { x: 24, y: 24 }, draggable: false, data: {} }, { id: 'unoq', type: 'unoq', position: { x: 560, y: 24 }, data: {} }, ] function Configurator({ onPlumbedChange }: { onPlumbedChange: (v: boolean) => void }) { const [nodes, , onNodesChange] = useNodesState(INITIAL_NODES) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const onConnect = useCallback( (c: Connection) => { setEdges((es) => { const next = addEdge({ ...c, animated: true }, es) onPlumbedChange(next.length > 0) return next }) }, [setEdges, onPlumbedChange], ) const handleEdgesChange = useCallback( (changes: Parameters[0]) => { onEdgesChange(changes) // recompute after a delete setEdges((es) => { onPlumbedChange(es.length > 0) return es }) }, [onEdgesChange, setEdges, onPlumbedChange], ) return (
) } export function ModuleDashboard() { const navigate = useNavigate() const connected = useSession((s) => s.device.connected) const teamId = useSession((s) => s.teamId) const feed = useNodeFeed(teamId, connected) const online = connected && feed.online const completePhase = useSession((s) => s.completePhase) // wiring state, lifted out of the flow so Proceed + the status line can read it const [wired, setWired] = useState(false) const live = wired && online const ready = wired const onProceed = () => { completePhase('m2') navigate('/workshop/add') } // the advance button lives in the sidebar useSetProceed({ label: 'Proceed to Module 3 →', disabled: !ready, onClick: onProceed }) return (

Drag the chat’s right handle onto the dashboard’s left edge to wire them — the dashboard floats free, so drag it wherever you like.

{wired ? (live ? '● wired · data flowing' : '● wired · board offline') : '○ not wired — drag the chat to the dashboard'}
) }