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]>
259 lines
10 KiB
TypeScript
259 lines
10 KiB
TypeScript
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 { 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'
|
||
|
||
/**
|
||
* The persistent instrument rail (right pane of the cockpit). A dark "device
|
||
* screen" — intentionally dark in both themes. Six sections: node heartbeat,
|
||
* agent activity log, and ADD progress are wired to real state; the LED matrix,
|
||
* I2C bus, and live acceleration come from the telemetry seam (`useTelemetry`),
|
||
* which is a **simulated** source today (marked SIM) until the ADXL355 stream
|
||
* lands. See the plan's WS3.
|
||
*/
|
||
|
||
const LOG_COLOR: Record<NodeActivityKind, string> = {
|
||
thinking: 'text-rail-dim2',
|
||
tool: 'text-rail-text2',
|
||
flash: 'text-rail-blue',
|
||
error: 'text-rail-spike',
|
||
response: 'text-rail-green',
|
||
fallback: 'text-[#d9a441]',
|
||
}
|
||
|
||
function RailSection({ label, light, children }: { label: string; light?: boolean; children: React.ReactNode }) {
|
||
return (
|
||
<div className="mt-5">
|
||
<div className={cn('font-mono text-[9.5px] tracking-[0.18em]', light ? 'text-ink-3' : 'text-rail-dim')}>{label}</div>
|
||
<div className="mt-2">{children}</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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 feed = useNodeFeed(teamId, connected)
|
||
const tel = useTelemetry(connected)
|
||
const online = connected && feed.online
|
||
// On the "agent" rail (Meet your agent) the matrix shows Clawd, the crab —
|
||
// 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 = useSharedAgentChat()
|
||
const claw = useClawd(chat.status)
|
||
// 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)
|
||
const matrixDots = agentView ? claw.dots : mirror ?? tel.matrix
|
||
const matrixLive = mirror != null
|
||
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)'
|
||
// 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)
|
||
|
||
// The agent rail is theme-aware (light in light mode); the instrument rail
|
||
// stays a fixed-dark device screen. The LED matrix itself is a screen either way.
|
||
const label = agentView ? 'text-ink-3' : 'text-rail-dim'
|
||
|
||
return (
|
||
<aside
|
||
className={cn(
|
||
'sticky top-6 rounded-[18px] p-[22px] shadow-[0_20px_50px_-24px_rgba(0,0,0,0.5)]',
|
||
agentView ? 'border border-line bg-surface-soft text-ink' : 'bg-rail-bg text-rail-text',
|
||
)}
|
||
>
|
||
{/* 1 · node header / heartbeat */}
|
||
<div className="flex items-center gap-[11px]">
|
||
<span
|
||
className={cn(
|
||
'h-2.5 w-2.5 rounded-full',
|
||
online
|
||
? agentView
|
||
? 'bg-green shadow-[0_0_10px_#3fd28a] animate-pulse'
|
||
: 'bg-rail-green shadow-[0_0_10px_#3fd28a] animate-pulse'
|
||
: agentView
|
||
? 'bg-ink-3'
|
||
: 'bg-rail-dim2',
|
||
)}
|
||
/>
|
||
<span
|
||
className={cn(
|
||
'font-mono text-sm font-semibold tracking-[0.02em]',
|
||
agentView ? 'text-ink' : 'text-rail-text3',
|
||
)}
|
||
>
|
||
{nodeName}
|
||
</span>
|
||
<span
|
||
className={cn(
|
||
'font-mono text-[9px] tracking-[0.14em] rounded border px-[7px] py-0.5',
|
||
online
|
||
? agentView
|
||
? 'text-green border-green/50'
|
||
: 'text-rail-green border-[#2c6b4f]'
|
||
: agentView
|
||
? 'text-ink-3 border-line'
|
||
: 'text-rail-dim2 border-rail-line',
|
||
)}
|
||
>
|
||
{online ? 'LIVE' : 'OFFLINE'}
|
||
</span>
|
||
</div>
|
||
|
||
{/* 2 · LED matrix — board mirror (full rail) or Clawd the crab (agent) */}
|
||
<div className="mt-5">
|
||
<div className="flex items-center justify-between">
|
||
<div className={cn('font-mono text-[9.5px] tracking-[0.18em]', label)}>
|
||
{agentView ? 'LED MATRIX · 26×16' : 'LED MATRIX · 13×8'}
|
||
</div>
|
||
{!agentView && matrixLive && (
|
||
<span className="font-mono text-[8.5px] tracking-[0.12em] text-rail-green">● MIRROR</span>
|
||
)}
|
||
</div>
|
||
<div className={cn('mt-2', agentView && 'flex items-stretch gap-5')}>
|
||
<div
|
||
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',
|
||
)}
|
||
>
|
||
<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>
|
||
|
||
{/* 3 & 4 · sensor telemetry — omitted on the "agent" variant (Meet your agent) */}
|
||
{variant !== 'agent' && (
|
||
<>
|
||
{/* 3 · I2C bus (telemetry) */}
|
||
<RailSection label="I2C BUS · 100 kHz">
|
||
<div className="rounded-[10px] border border-rail-line2 bg-rail-panel px-1 py-1.5 font-mono text-xs">
|
||
{tel.i2c.map((d, i) => (
|
||
<div
|
||
key={d.addr}
|
||
className={cn('flex items-center gap-2.5 px-3 py-2', i === 0 && 'border-b border-rail-line')}
|
||
>
|
||
<span className="text-rail-blue">{d.addr}</span>
|
||
<span className="text-rail-text2">{d.name}</span>
|
||
<span className={cn('ml-auto', d.synced ? 'text-rail-green' : 'text-rail-dim3')}>
|
||
{d.synced ? '● synced' : '○ idle'}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="mt-[7px] font-mono text-[10px] text-rail-dim2">
|
||
{tel.live ? `SYNC/INT aligned · drift ${tel.driftMs.toFixed(1)} ms` : 'run i2c_scan to enumerate the bus'}
|
||
</div>
|
||
</RailSection>
|
||
|
||
{/* 4 · live acceleration (telemetry) */}
|
||
<RailSection label="LIVE ACCELERATION · g">
|
||
<div className="-mt-[18px] mb-2 flex items-center justify-end gap-2">
|
||
{tel.live && tel.simulated && (
|
||
<span className="rounded-[4px] border border-[#d9a441]/40 px-[5px] py-[1px] font-mono text-[8.5px] tracking-[0.12em] text-[#d9a441]">
|
||
SIM
|
||
</span>
|
||
)}
|
||
<span
|
||
className={cn(
|
||
'font-mono text-[9.5px] tracking-[0.1em]',
|
||
!tel.live ? 'text-rail-dim' : tel.event === 'impact' ? 'text-rail-spike' : 'text-rail-green',
|
||
)}
|
||
>
|
||
{!tel.live ? 'AWAITING STREAM' : tel.event === 'impact' ? 'IMPACT SPIKE' : 'NOMINAL'}
|
||
</span>
|
||
</div>
|
||
{tel.live ? (
|
||
<WaveformCanvas wave={tel.wave} impact={tel.event === 'impact'} />
|
||
) : (
|
||
<div className="flex h-[100px] items-center justify-center rounded-[10px] border border-rail-line bg-rail-inset font-mono text-[10px] text-rail-dim3">
|
||
adxl355_stream — awaiting board
|
||
</div>
|
||
)}
|
||
<div className="mt-2 grid grid-cols-2 gap-2 font-mono text-[11.5px]">
|
||
{([['ACC 1', tel.acc1], ['ACC 2', tel.acc2]] as const).map(([n, a]) => (
|
||
<div key={n} className="rounded-lg border border-rail-line2 bg-rail-panel px-[11px] py-[9px]">
|
||
<div className="text-[9.5px] tracking-[0.12em] text-rail-dim">{n}</div>
|
||
{(['x', 'y', 'z'] as const).map((axis) => (
|
||
<div key={axis} className="text-rail-text2">
|
||
{axis}{' '}
|
||
<span className="text-rail-text3 tabular-nums">
|
||
{tel.live ? a[axis].toFixed(3) : '—'}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</RailSection>
|
||
</>
|
||
)}
|
||
|
||
{/* 5 · agent — chat + logs as separate panes (agent view) or the read-only log */}
|
||
{agentView ? (
|
||
<RailAgent
|
||
messages={chat.messages}
|
||
logs={chat.logs}
|
||
sending={chat.sending}
|
||
online={online}
|
||
onSend={(t) => void chat.send(t)}
|
||
/>
|
||
) : (
|
||
<RailSection label="AGENT ACTIVITY">
|
||
<div className="h-[132px] overflow-hidden rounded-[10px] border border-rail-line bg-rail-inset px-[13px] py-[11px] font-mono text-[11px] leading-[1.75]">
|
||
{log.length === 0 ? (
|
||
<div className="text-rail-dim2">idle — prompt your agent to see it work</div>
|
||
) : (
|
||
log.map((e, i) => (
|
||
<div key={i} className={cn('truncate', LOG_COLOR[e.kind])}>
|
||
{e.label}
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</RailSection>
|
||
)}
|
||
|
||
</aside>
|
||
)
|
||
}
|