feat(workshop): reworked phase flow — agent makeup, sidebar advance, responsive

Rebuild the participant flow around the agent itself:
- Phases: Team registration (+ agent name) -> Meet your agent -> Skills &
  Policies (edit the agent's makeup) -> UnoQ Dashboard (React Flow
  configurator) -> Submit. Swapped/renamed the two module pages
  (Module1/2 -> ModuleDashboard/ModuleMakeup).
- Agent name captured at registration, shown on the dashboard and used in
  the chat greeting (live, not hard-coded), pushed to the board.
- One shared conversation across pages (AgentChatContext); Clawd the crab
  animates the LED matrix and reacts to chat status.
- MAKEUP slide-outs: view, edit (wrench), or AI-refine (wand) the agent's
  personality files and save them back to the board.
- Advance button moved into the sidebar for every phase (ProceedContext).
- Dropped the ADD-doc build + submission gate; Submit just records the entry.
- Responsive pass: sidebar auto-collapses below lg, fluid padding, React
  Flow pan/zoom/fit + Controls, architecture panel becomes a mobile overlay.
- Add @xyflow/react for the configurator.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-27 10:55:34 +02:00
co-authored by Claude Opus 4.8
parent 0797923933
commit 05207ba986
30 changed files with 1690 additions and 687 deletions
+40 -108
View File
@@ -1,16 +1,14 @@
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { useSession } from '@/store/session'
import { useNodeFeed } from '@/lib/useNodeFeed'
import { useTelemetry } from '@/lib/useTelemetry'
import { useMatrixMirror } from '@/lib/useMatrixMirror'
import { useClawd, GRID_W } from '@/lib/clawSprite'
import { useAgentChat } from '@/lib/useAgentChat'
import { useSharedAgentChat } from '@/lib/AgentChatContext'
import { WaveformCanvas } from './WaveformCanvas'
import { RailAgent } from './RailAgent'
import { AgentArchitectureStrip } from './AgentArchitecture'
import type { NodeActivityKind } from '@/types'
import { cn } from '@/lib/utils'
import { ADD_LAYERS } from '@/lib/addLayers'
/**
* The persistent instrument rail (right pane of the cockpit). A dark "device
@@ -30,13 +28,6 @@ const LOG_COLOR: Record<NodeActivityKind, string> = {
fallback: 'text-[#d9a441]',
}
// which layers are "next" on each route
const NEXT_BY_PATH: Record<string, string[]> = {
'/workshop/module1': ['L1'],
'/workshop/module2': ['L2', 'L3'],
'/workshop/add': ['L4', 'L5'],
}
function RailSection({ label, light, children }: { label: string; light?: boolean; children: React.ReactNode }) {
return (
<div className="mt-5">
@@ -50,10 +41,6 @@ export function CockpitRail({ variant = 'full' }: { variant?: 'full' | 'agent' }
const teamId = useSession((s) => s.teamId)
const team = useSession((s) => s.team)
const connected = useSession((s) => s.device.connected)
const add = useSession((s) => s.add)
const submitted = useSession((s) => s.submission.code != null)
const setTried = useSession((s) => s.setTried)
const { pathname } = useLocation()
const feed = useNodeFeed(teamId, connected)
const tel = useTelemetry(connected)
@@ -62,13 +49,8 @@ export function CockpitRail({ variant = 'full' }: { variant?: 'full' | 'agent' }
// not a board mirror — driven by the live chat status, so we skip the mirror
// poll and animate the sprite instead.
const agentView = variant === 'agent'
const chat = useAgentChat(teamId, agentView && online)
const chat = useSharedAgentChat()
const claw = useClawd(chat.status)
// Folding the canned starters into the rail means their completions now drive
// the Module 2 prefill (`tried`); bump it as starters finish (never lower it).
useEffect(() => {
if (agentView && chat.doneCount > 0) setTried(chat.doneCount)
}, [agentView, chat.doneCount, setTried])
// Pixel-perfect mirror of the physical matrix (real board frame). Falls back
// to the sim frame until the first real frame arrives.
const mirror = useMatrixMirror(teamId, online && !agentView)
@@ -77,13 +59,12 @@ export function CockpitRail({ variant = 'full' }: { variant?: 'full' | 'agent' }
const matrixCols = agentView ? GRID_W : 13
// Clawd flashes green on a reply; the board mirror stays ASCII-orange.
const litColor = agentView && claw.color === 'green' ? 'oklch(0.82 0.17 152)' : 'oklch(0.7 0.2 34)'
const nodeName = team.name ? team.name.toLowerCase().replace(/\s+/g, '-') : 'crimson-node'
const nextSet = new Set(NEXT_BY_PATH[pathname] ?? [])
const layerState = (key: string): 'idle' | 'next' | 'done' => {
if (submitted || add[key as keyof typeof add]?.trim()) return 'done'
return nextSet.has(key) ? 'next' : 'idle'
}
// Prefer the name the team gave their agent at registration.
const nodeName = team.agentName?.trim()
? team.agentName.trim()
: team.name
? team.name.toLowerCase().replace(/\s+/g, '-')
: 'crimson-node'
const log = feed.activity.slice(0, 6)
@@ -134,7 +115,6 @@ export function CockpitRail({ variant = 'full' }: { variant?: 'full' | 'agent' }
>
{online ? 'LIVE' : 'OFFLINE'}
</span>
<span className={cn('ml-auto font-mono text-[10px]', label)}>arduino uno q</span>
</div>
{/* 2 · LED matrix — board mirror (full rail) or Clawd the crab (agent) */}
@@ -143,44 +123,41 @@ export function CockpitRail({ variant = 'full' }: { variant?: 'full' | 'agent' }
<div className={cn('font-mono text-[9.5px] tracking-[0.18em]', label)}>
{agentView ? 'LED MATRIX · 26×16' : 'LED MATRIX · 13×8'}
</div>
{agentView ? (
<span
className="font-mono text-[8.5px] tracking-[0.12em]"
style={{ color: claw.color === 'green' ? '#3fd28a' : '#ff7a3c' }}
>
{claw.color === 'green' ? '● REPLIED' : chat.status === 'working' ? '● WORKING' : '● CLAWD'}
</span>
) : (
matrixLive && <span className="font-mono text-[8.5px] tracking-[0.12em] text-rail-green">● MIRROR</span>
{!agentView && matrixLive && (
<span className="font-mono text-[8.5px] tracking-[0.12em] text-rail-green">● MIRROR</span>
)}
</div>
<div
className={cn(
'mt-2 rounded-[10px] border border-rail-line bg-rail-inset',
agentView ? 'p-3' : 'flex justify-center px-[13px] py-3',
)}
>
<div className={cn('mt-2', agentView && 'flex items-stretch gap-5')}>
<div
className={cn('grid', agentView ? 'w-full gap-[2px]' : 'gap-1')}
style={{ gridTemplateColumns: `repeat(${matrixCols}, 1fr)` }}
className={cn(
'rounded-[10px] border border-rail-line bg-rail-inset',
agentView ? 'min-w-0 flex-1 p-3' : 'flex justify-center px-[13px] py-3',
)}
>
{matrixDots.map((on, i) => {
const lit = agentView ? on : tel.live && on
return (
<span
key={i}
className={cn(
'rounded-[2px] transition-[background] duration-75',
agentView ? 'aspect-square w-full' : 'h-[9px] w-[9px]',
)}
style={{
background: lit ? litColor : 'oklch(0.28 0.01 260)',
boxShadow: lit ? `0 0 5px ${litColor}` : 'none',
}}
/>
)
})}
<div
className={cn('grid', agentView ? 'w-full gap-[2px]' : 'gap-1')}
style={{ gridTemplateColumns: `repeat(${matrixCols}, 1fr)` }}
>
{matrixDots.map((on, i) => {
const lit = agentView ? on : tel.live && on
return (
<span
key={i}
className={cn(
'rounded-[2px] transition-[background] duration-75',
agentView ? 'aspect-square w-full' : 'h-[9px] w-[9px]',
)}
style={{
background: lit ? litColor : 'oklch(0.28 0.01 260)',
boxShadow: lit ? `0 0 5px ${litColor}` : 'none',
}}
/>
)
})}
</div>
</div>
{/* the agent's makeup — icons that open per-file explainers, beside Clawd */}
{agentView && <AgentArchitectureStrip className="mt-4" />}
</div>
</div>
@@ -258,8 +235,7 @@ export function CockpitRail({ variant = 'full' }: { variant?: 'full' | 'agent' }
logs={chat.logs}
sending={chat.sending}
online={online}
starters={chat.starters}
onSend={(t, id) => void chat.send(t, id)}
onSend={(t) => void chat.send(t)}
/>
) : (
<RailSection label="AGENT ACTIVITY">
@@ -277,50 +253,6 @@ export function CockpitRail({ variant = 'full' }: { variant?: 'full' | 'agent' }
</RailSection>
)}
{/* 6 · ADD progress (real, local) */}
<RailSection label="AGENT DESIGN DOC" light={agentView}>
<div className="flex flex-col gap-px font-mono text-[11px]">
{ADD_LAYERS.map(({ key, n, title }) => {
const st = layerState(key)
return (
<div key={key} className="flex items-center gap-2.5 px-0.5 py-[7px]">
<span
className={cn(
st === 'idle' ? (agentView ? 'text-ink-3' : 'text-[#4a5060]') : agentView ? 'text-blue' : 'text-rail-blue',
)}
>
L{n}
</span>
<span
className={cn(
st === 'idle' ? (agentView ? 'text-faint' : 'text-rail-dim2') : agentView ? 'text-ink-2' : 'text-rail-text2',
)}
>
{title}
</span>
<span
className={cn(
'ml-auto',
st === 'done'
? agentView
? 'text-green'
: 'text-rail-green'
: st === 'next'
? agentView
? 'text-amber'
: 'text-[#d9a441]'
: agentView
? 'text-ink-3'
: 'text-rail-dim3',
)}
>
{st === 'done' ? 'done' : st === 'next' ? 'next' : '—'}
</span>
</div>
)
})}
</div>
</RailSection>
</aside>
)
}