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:
co-authored by
Claude Opus 4.8
parent
0797923933
commit
05207ba986
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user