import { useEffect, useRef, useState } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { cn } from '@/lib/utils' import { useSession } from '@/store/session' import { sendPrompt, openTeamActivity, AGENT } from '@/lib/api' import type { NodeActivityKind, WsEvent } from '@/types' /** * The three canned prompts, in order. Each drives the agent to use its skills + * tools on the real board: enumerate the I2C bus, then two LED-matrix sketches. * KEEP THESE IMPERATIVE — the small on-board model reliably calls tools when told * to *do* something but stalls when *asked a question* (measured on the board). */ const PROMPTS: { id: string; text: string }[] = [ { id: 'i2c', text: 'List the I2C devices on the bus' }, { id: 'count', text: 'Count to 100 and print the value once a second in the LED matrix' }, { id: 'scroll', text: 'Scroll GO CLAWS on the LED matrix' }, ] type Status = 'idle' | 'running' | 'done' type Line = | { who: 'you'; text: string } | { who: 'agent'; kind: NodeActivityKind; text: string } const KIND_DOT: Record = { thinking: 'bg-muted-foreground', tool: 'bg-amber', flash: 'bg-primary', error: 'bg-destructive', response: 'bg-teal', fallback: 'bg-rose', } // A prompt is "done" once the agent reaches a terminal step for it. const isSuccess = (k: NodeActivityKind) => k === 'flash' || k === 'response' export interface AgentChatProps { /** Called whenever the set of successfully-tried prompts changes. */ onProgress?: (doneCount: number, total: number) => void } /** * A chat window straight to the team's agent. Click a canned prompt and watch * the agent use its skills/tools on the real board — its activity streams back * as the reply. Tracks which of the three prompts have completed successfully. */ export function AgentChat({ onProgress }: AgentChatProps) { const teamId = useSession((s) => s.teamId) const [lines, setLines] = useState([]) const [status, setStatus] = useState>({ i2c: 'idle', count: 'idle', scroll: 'idle' }) const running = useRef(null) const scroller = useRef(null) const doneCount = Object.values(status).filter((s) => s === 'done').length const busy = running.current !== null || Object.values(status).some((s) => s === 'running') // Report progress without making the parent's inline callback a dependency // (that would re-fire every render and loop with the parent's setState). const onProgressRef = useRef(onProgress) onProgressRef.current = onProgress useEffect(() => { onProgressRef.current?.(doneCount, PROMPTS.length) }, [doneCount]) useEffect(() => { if (scroller.current) scroller.current.scrollTop = scroller.current.scrollHeight }, [lines]) // Stream this team's own board activity as the agent's replies. useEffect(() => { return openTeamActivity(teamId, (ev: WsEvent) => { if (ev.type !== 'node:activity') return setLines((prev) => [...prev, { who: 'agent', kind: ev.kind, text: ev.label }]) const active = running.current if (!active) return if (ev.kind === 'error') { running.current = null setStatus((s) => ({ ...s, [active]: 'idle' })) // let them retry } else if (isSuccess(ev.kind)) { running.current = null setStatus((s) => ({ ...s, [active]: 'done' })) } }) }, [teamId]) const run = async (id: string, text: string) => { if (busy) return running.current = id setStatus((s) => ({ ...s, [id]: 'running' })) setLines((prev) => [...prev, { who: 'you', text }]) try { await sendPrompt(teamId, text, AGENT) } catch { running.current = null setStatus((s) => ({ ...s, [id]: 'idle' })) setLines((prev) => [ ...prev, { who: 'agent', kind: 'error', text: 'Could not reach your agent — is the board online?' }, ]) } } return ( Chat with your agent {doneCount}/{PROMPTS.length} tried

Send one of these to your agent and watch it use its skills and tools on the real board — its activity streams back here. Try all three to see what it can already do.

{/* canned prompts */}
{PROMPTS.map((p) => { const st = status[p.id] return ( ) })}
{/* transcript */}
{lines.length === 0 ? (

Pick a prompt above — your agent’s work (tools, flashes, replies) shows up here.

) : ( lines.map((l, i) => l.who === 'you' ? (
{l.text}
) : (
{l.text}
), ) )}
) }