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'}
)
}