feat(module2): chat with your agent — three canned prompts, then Layers 2/3
Module 2 is now a chat straight to the agent instead of Build & flash. Removed the "Open your node" links and the Live board feed card. New AgentChat component: three imperative canned prompts — - List the I2C devices on the bus - Count to 100 and print the value once a second in the LED matrix - Scroll GO CLAWS on the LED matrix Each sends to the agent (fire-and-forget, cloud) and streams its activity (tools/flash/reply) back into the transcript. A prompt is marked done on its first terminal step (flash/response); an error resets it to retry. Prompts run one at a time. Once all three have run successfully, Module 2 reveals "What's next" — the ADD Layer 2 (Skills) + Layer 3 (Policies & failure) capture — and Proceed gates on all-three-tried AND L2 + L3 filled. Note: BuildFlash / LiveBoardFeed / ActorMap are now orphaned (kept for possible reuse). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c2ef338888
commit
7780903278
@@ -0,0 +1,54 @@
|
|||||||
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||||
|
import { render, screen, act, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { AgentChat } from './AgentChat'
|
||||||
|
import { useSession } from '@/store/session'
|
||||||
|
import { sendPrompt } from '@/lib/api'
|
||||||
|
import type { WsEvent } from '@/types'
|
||||||
|
|
||||||
|
let emit: (e: WsEvent) => void = () => {}
|
||||||
|
vi.mock('@/lib/api', () => ({
|
||||||
|
sendPrompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
openTeamActivity: (_t: string, on: (e: WsEvent) => void) => {
|
||||||
|
emit = on
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
const mockSend = vi.mocked(sendPrompt)
|
||||||
|
|
||||||
|
describe('AgentChat', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useSession.getState().reset()
|
||||||
|
sessionStorage.clear()
|
||||||
|
mockSend.mockClear()
|
||||||
|
mockSend.mockResolvedValue(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends the canned prompt and shows it in the transcript', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<AgentChat />)
|
||||||
|
await user.click(screen.getByTestId('prompt-i2c'))
|
||||||
|
expect(mockSend).toHaveBeenCalledWith(expect.any(String), 'List the I2C devices on the bus', 'cloud')
|
||||||
|
expect(screen.getByTestId('chat-transcript')).toHaveTextContent(/list the i2c devices/i)
|
||||||
|
expect(screen.getByTestId('prompt-i2c')).toHaveAttribute('data-state', 'running')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks a prompt done on a terminal activity and reports progress', async () => {
|
||||||
|
const onProgress = vi.fn()
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<AgentChat onProgress={onProgress} />)
|
||||||
|
await user.click(screen.getByTestId('prompt-scroll'))
|
||||||
|
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'flash', label: 'Flashed to 0x80F0000', ts: '' }))
|
||||||
|
await waitFor(() => expect(screen.getByTestId('prompt-scroll')).toHaveAttribute('data-state', 'done'))
|
||||||
|
expect(onProgress).toHaveBeenLastCalledWith(1, 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('an error resets the prompt so it can be retried', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
render(<AgentChat />)
|
||||||
|
await user.click(screen.getByTestId('prompt-count'))
|
||||||
|
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'error', label: 'boom', ts: '' }))
|
||||||
|
await waitFor(() => expect(screen.getByTestId('prompt-count')).toHaveAttribute('data-state', 'idle'))
|
||||||
|
expect(screen.getByTestId('prompt-count')).toBeEnabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
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 } 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<NodeActivityKind, string> = {
|
||||||
|
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<Line[]>([])
|
||||||
|
const [status, setStatus] = useState<Record<string, Status>>({ i2c: 'idle', count: 'idle', scroll: 'idle' })
|
||||||
|
const running = useRef<string | null>(null)
|
||||||
|
const scroller = useRef<HTMLDivElement>(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, 'cloud')
|
||||||
|
} 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 (
|
||||||
|
<Card data-testid="agent-chat">
|
||||||
|
<CardHeader className="flex-row items-center justify-between space-y-0">
|
||||||
|
<CardTitle className="text-base">Chat with your agent</CardTitle>
|
||||||
|
<Badge variant="default" className="font-mono text-[10px] uppercase tracking-widest">
|
||||||
|
{doneCount}/{PROMPTS.length} tried
|
||||||
|
</Badge>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* canned prompts */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{PROMPTS.map((p) => {
|
||||||
|
const st = status[p.id]
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={p.id}
|
||||||
|
type="button"
|
||||||
|
data-testid={`prompt-${p.id}`}
|
||||||
|
data-state={st}
|
||||||
|
disabled={busy && st !== 'running'}
|
||||||
|
onClick={() => void run(p.id, p.text)}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2.5 text-left rounded-md border px-3 py-2 text-sm transition-colors disabled:opacity-50',
|
||||||
|
st === 'done'
|
||||||
|
? 'border-teal/40 bg-teal/10'
|
||||||
|
: st === 'running'
|
||||||
|
? 'border-amber/40 bg-amber/5'
|
||||||
|
: 'border-border hover:border-primary/40 hover:bg-muted',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'w-2 h-2 rounded-full shrink-0',
|
||||||
|
st === 'done' ? 'bg-teal' : st === 'running' ? 'bg-amber animate-pulse' : 'bg-muted-foreground/50',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="flex-1">{p.text}</span>
|
||||||
|
<span className="font-mono text-[9px] uppercase tracking-widest text-muted-foreground shrink-0">
|
||||||
|
{st === 'done' ? 'done ✓' : st === 'running' ? 'running…' : 'send →'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* transcript */}
|
||||||
|
<div
|
||||||
|
ref={scroller}
|
||||||
|
data-testid="chat-transcript"
|
||||||
|
className="rounded-md border border-border bg-background/50 p-3 h-56 overflow-y-auto space-y-2"
|
||||||
|
>
|
||||||
|
{lines.length === 0 ? (
|
||||||
|
<p className="font-mono text-[11px] text-muted-foreground">
|
||||||
|
Pick a prompt above — your agent’s work (tools, flashes, replies) shows up here.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
lines.map((l, i) =>
|
||||||
|
l.who === 'you' ? (
|
||||||
|
<div key={i} className="flex justify-end">
|
||||||
|
<span className="rounded-lg bg-primary/10 text-foreground px-3 py-1.5 text-sm max-w-[85%]">{l.text}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div key={i} className="flex items-start gap-2 font-mono text-[11px]">
|
||||||
|
<span className={cn('w-1.5 h-1.5 rounded-full shrink-0 mt-1.5', KIND_DOT[l.kind])} />
|
||||||
|
<span className={cn(l.kind === 'error' && 'text-destructive', l.kind === 'flash' && 'text-foreground font-medium')}>
|
||||||
|
{l.text}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
+41
-12
@@ -13,7 +13,6 @@ vi.mock('@/lib/api', async (orig) => ({
|
|||||||
emit = on
|
emit = on
|
||||||
return () => {}
|
return () => {}
|
||||||
},
|
},
|
||||||
getNodeStatus: vi.fn().mockResolvedValue({ teamId: 't', online: false }),
|
|
||||||
sendPrompt: vi.fn().mockResolvedValue(undefined),
|
sendPrompt: vi.fn().mockResolvedValue(undefined),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -25,6 +24,13 @@ function renderPage() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Click a canned prompt and let the agent reach a terminal (success) step. */
|
||||||
|
async function completePrompt(user: ReturnType<typeof userEvent.setup>, id: string) {
|
||||||
|
await user.click(screen.getByTestId(`prompt-${id}`))
|
||||||
|
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
|
||||||
|
await waitFor(() => expect(screen.getByTestId(`prompt-${id}`)).toHaveAttribute('data-state', 'done'))
|
||||||
|
}
|
||||||
|
|
||||||
describe('Module2', () => {
|
describe('Module2', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useSession.getState().reset()
|
useSession.getState().reset()
|
||||||
@@ -39,31 +45,54 @@ describe('Module2', () => {
|
|||||||
).toHaveAttribute('data-state', 'active')
|
).toHaveAttribute('data-state', 'active')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows the live board feed and the build & flash panel', () => {
|
it('is a chat with the three canned prompts — no live feed / build & flash', () => {
|
||||||
renderPage()
|
renderPage()
|
||||||
expect(screen.getByTestId('live-board-feed')).toBeInTheDocument()
|
expect(screen.getByTestId('agent-chat')).toBeInTheDocument()
|
||||||
expect(screen.getByTestId('activity-log')).toBeInTheDocument()
|
expect(screen.getByTestId('prompt-i2c')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('prompt-count')).toBeInTheDocument()
|
||||||
|
expect(screen.getByTestId('prompt-scroll')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByTestId('live-board-feed')).toBeNull()
|
||||||
|
expect(screen.queryByTestId('activity-log')).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('gates Proceed until the board acted and L2 + L3 are filled', async () => {
|
it('reveals "what\'s next" (the ADD layers) only after all three prompts succeed', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
renderPage()
|
||||||
|
expect(screen.queryByTestId('whats-next')).toBeNull()
|
||||||
|
expect(screen.queryByLabelText(/layer 2/i)).toBeNull()
|
||||||
|
|
||||||
|
await completePrompt(user, 'i2c')
|
||||||
|
await completePrompt(user, 'count')
|
||||||
|
expect(screen.queryByTestId('whats-next')).toBeNull() // still one to go
|
||||||
|
await completePrompt(user, 'scroll')
|
||||||
|
|
||||||
|
expect(screen.getByTestId('whats-next')).toBeInTheDocument()
|
||||||
|
expect(screen.getByLabelText(/layer 2/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('gates Proceed until all prompts ran AND L2 + L3 are filled', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
useSession.getState().setAddLayer('L2', 'escalate on critical')
|
useSession.getState().setAddLayer('L2', 'escalate on critical')
|
||||||
useSession.getState().setAddLayer('L3', 'drive damper on critical')
|
useSession.getState().setAddLayer('L3', 'drive damper on critical')
|
||||||
renderPage()
|
renderPage()
|
||||||
const proceed = screen.getByRole('button', { name: /proceed/i })
|
|
||||||
expect(proceed).toBeDisabled()
|
|
||||||
|
|
||||||
// the board acts (via Build & flash) → activity streams from the node
|
await completePrompt(user, 'i2c')
|
||||||
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
|
await completePrompt(user, 'count')
|
||||||
await waitFor(() => expect(proceed).toBeEnabled())
|
await completePrompt(user, 'scroll')
|
||||||
|
|
||||||
|
const proceed = screen.getByRole('button', { name: /proceed/i })
|
||||||
|
expect(proceed).toBeEnabled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('marks m2 complete on Proceed', async () => {
|
it('marks m2 complete on Proceed', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
renderPage()
|
renderPage()
|
||||||
|
await completePrompt(user, 'i2c')
|
||||||
|
await completePrompt(user, 'count')
|
||||||
|
await completePrompt(user, 'scroll')
|
||||||
await user.type(screen.getByLabelText(/layer 2/i), 'L2 text')
|
await user.type(screen.getByLabelText(/layer 2/i), 'L2 text')
|
||||||
await user.type(screen.getByLabelText(/layer 3/i), 'L3 text')
|
await user.type(screen.getByLabelText(/layer 3/i), 'L3 text')
|
||||||
act(() => emit({ type: 'node:activity', teamId: 'x', kind: 'response', label: 'Agent finished', ts: '' }))
|
await user.click(screen.getByRole('button', { name: /proceed/i }))
|
||||||
await user.click(await screen.findByRole('button', { name: /proceed/i }))
|
|
||||||
expect(useSession.getState().phases.m2).toBe(true)
|
expect(useSession.getState().phases.m2).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+36
-46
@@ -1,27 +1,22 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
import { Link, useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { PhaseStrip } from '@/components/PhaseStrip'
|
import { PhaseStrip } from '@/components/PhaseStrip'
|
||||||
import { LiveBoardFeed } from '@/components/LiveBoardFeed'
|
import { AgentChat } from '@/components/AgentChat'
|
||||||
import { BuildFlash } from '@/components/BuildFlash'
|
|
||||||
import { AddLayerForm } from '@/components/AddLayerForm'
|
import { AddLayerForm } from '@/components/AddLayerForm'
|
||||||
import { OpenYourNode } from '@/components/OpenYourNode'
|
|
||||||
import { useNodeFeed } from '@/lib/useNodeFeed'
|
|
||||||
import { useSession } from '@/store/session'
|
import { useSession } from '@/store/session'
|
||||||
|
|
||||||
export function Module2() {
|
export function Module2() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const teamId = useSession((s) => s.teamId)
|
|
||||||
const feed = useNodeFeed(teamId, true)
|
|
||||||
const l2 = useSession((s) => s.add.L2)
|
const l2 = useSession((s) => s.add.L2)
|
||||||
const l3 = useSession((s) => s.add.L3)
|
const l3 = useSession((s) => s.add.L3)
|
||||||
const completePhase = useSession((s) => s.completePhase)
|
const completePhase = useSession((s) => s.completePhase)
|
||||||
|
const [tried, setTried] = useState(0)
|
||||||
|
|
||||||
// The board acting on a prompt (via Build & flash below) shows up in its
|
const allTried = tried >= 3
|
||||||
// activity feed — that's proof the loop was exercised.
|
const ready = allTried && l2.trim().length > 0 && l3.trim().length > 0
|
||||||
const exercised = feed.activity.length > 0
|
|
||||||
const ready = exercised && l2.trim().length > 0 && l3.trim().length > 0
|
|
||||||
|
|
||||||
const onProceed = () => {
|
const onProceed = () => {
|
||||||
completePhase('m2')
|
completePhase('m2')
|
||||||
@@ -42,55 +37,37 @@ export function Module2() {
|
|||||||
|
|
||||||
<PhaseStrip active="m2" />
|
<PhaseStrip active="m2" />
|
||||||
|
|
||||||
<section className="px-8 py-10 max-w-5xl mx-auto space-y-6">
|
<section className="px-8 py-10 max-w-3xl mx-auto space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<Badge variant="secondary" className="font-mono text-[10px] tracking-widest uppercase mb-2">
|
<Badge variant="secondary" className="font-mono text-[10px] tracking-widest uppercase mb-2">
|
||||||
Phase 4 of 5 · ~90 min
|
Phase 4 of 5 · ~90 min
|
||||||
</Badge>
|
</Badge>
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Module 2 · Skills & policies</h1>
|
<h1 className="text-3xl font-bold tracking-tight">Module 2 · Skills & policies</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-2 max-w-xl">
|
<p className="text-sm text-muted-foreground mt-2 max-w-xl">
|
||||||
Your node ships with expert skills already. Decide which domain skills it needs, try them in the
|
Your agent already ships with expert skills — hardware, the MCU bridge, the LED matrix,
|
||||||
node dashboard, then capture Layers 2 and 3 — the skills it can invoke and the actuation gate
|
flashing, and more. Try them from the chat below: each prompt makes the agent use its
|
||||||
that governs them.
|
skills and tools on your real board.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-3">
|
|
||||||
<OpenYourNode variant="inline" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<AgentChat onProgress={(done) => setTried(done)} />
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">Skills reference</CardTitle>
|
{/* What's next — revealed once all three prompts have run successfully. */}
|
||||||
</CardHeader>
|
{allTried ? (
|
||||||
<CardContent className="space-y-3">
|
<div className="space-y-6" data-testid="whats-next">
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
<div className="pt-2">
|
||||||
The node already ships with expert skills — <span className="font-mono text-xs text-foreground">uno-q-hardware</span>,{' '}
|
<h2 className="text-lg font-semibold tracking-tight">What’s next</h2>
|
||||||
<span className="font-mono text-xs text-foreground">bridge</span>,{' '}
|
<p className="text-sm text-muted-foreground mt-1 max-w-xl">
|
||||||
<span className="font-mono text-xs text-foreground">led-matrix</span>,{' '}
|
You just watched the agent enumerate a bus and drive the matrix using its built-in
|
||||||
<span className="font-mono text-xs text-foreground">flashing</span>, and more. Layer 2 is not
|
skills. Now capture <span className="font-medium text-foreground">your domain’s</span>{' '}
|
||||||
about re-implementing those: it’s deciding which <em>domain</em> skills your node needs, then
|
skills and the policy that governs them — Layers 2 and 3 of your Agent Design Document.
|
||||||
trying them live in the dashboard.
|
|
||||||
</p>
|
</p>
|
||||||
<OpenYourNode variant="inline" />
|
</div>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-base">Live board feed</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<LiveBoardFeed feed={feed} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<BuildFlash />
|
|
||||||
|
|
||||||
<div className="grid lg:grid-cols-2 gap-6">
|
|
||||||
<AddLayerForm
|
<AddLayerForm
|
||||||
layer="L2"
|
layer="L2"
|
||||||
title="ADD · Layer 2 — Skills"
|
title="ADD · Layer 2 — Skills"
|
||||||
description="What skills can the node invoke to act on its domain?"
|
description="What skills can the agent invoke to act on its domain?"
|
||||||
placeholder="Flash a sketch to the MCU; scroll a message; drive the damper; sample the IMU."
|
placeholder="Flash a sketch to the MCU; scroll a message; drive the damper; sample the IMU."
|
||||||
/>
|
/>
|
||||||
<AddLayerForm
|
<AddLayerForm
|
||||||
@@ -106,13 +83,26 @@ export function Module2() {
|
|||||||
'• agent unsure → escalate to a human, do not actuate'
|
'• agent unsure → escalate to a human, do not actuate'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end pt-2">
|
<div className="flex justify-end pt-2">
|
||||||
<Button size="lg" disabled={!ready} onClick={onProceed}>
|
<Button size="lg" disabled={!ready} onClick={onProceed}>
|
||||||
Proceed to ADD builder →
|
Proceed to ADD builder →
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base text-muted-foreground">What’s next</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||||
|
Try all three prompts above. Once your agent has run each one successfully, we’ll
|
||||||
|
capture your domain’s skills and policies here.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user