Animated agent faces driven by live run state
Each AI agent now has an expressive Companion face (AgentFace) whose animation maps to its real AgentRun state — idle, thinking (queued), working (running), review (held), done, failed — so a glance at the board or org chart reads as live status, the same way the seat-state triad reads human/open/AI. Pure CSS keyframes (no animation dependency), em-scaled across four sizes, per-agent hue derived deterministically in the indigo band, reduced-motion respected. Adds a per-team agent-activity read endpoint (latest run status per agent) and a self-contained polling hook (useAgentActivity) that merges run activity with governance holds. Wired into the board assignee chips and the org chart (a custom React Flow seat node with hidden handles so edges still connect). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { api } from '@/lib/api'
|
||||
import type { FaceState } from '@/components/AgentFace'
|
||||
|
||||
interface AgentActivity {
|
||||
agentId: string
|
||||
status: string
|
||||
workItemId: string
|
||||
updatedAtUtc: string
|
||||
}
|
||||
|
||||
interface PendingReview {
|
||||
agentId: string
|
||||
}
|
||||
|
||||
/** A just-completed run shows the `done` (teal) face for this long, then settles to `idle`. */
|
||||
const DONE_WINDOW_MS = 45_000
|
||||
const POLL_MS = 4_000
|
||||
|
||||
function faceFor(activity: AgentActivity | undefined, held: boolean): FaceState {
|
||||
if (held) return 'review'
|
||||
if (!activity) return 'idle'
|
||||
switch (activity.status) {
|
||||
case 'Failed':
|
||||
return 'failed'
|
||||
case 'Running':
|
||||
return 'working'
|
||||
case 'Queued':
|
||||
return 'thinking'
|
||||
case 'Completed': {
|
||||
const age = Date.now() - new Date(activity.updatedAtUtc).getTime()
|
||||
return age >= 0 && age < DONE_WINDOW_MS ? 'done' : 'idle'
|
||||
}
|
||||
default:
|
||||
return 'idle'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polls per-agent run activity (Assembler) and pending holds (Governance) and maps each agent to a
|
||||
* live face state. Self-contained polling — no query client needed. Pass the agent ids currently on
|
||||
* screen (the caller already holds them via its seats); an empty list disables the poll.
|
||||
*/
|
||||
export function useAgentActivity(organizationId: string | null, agentIds: (string | null | undefined)[]) {
|
||||
const ids = agentIds.filter((x): x is string => !!x)
|
||||
const key = [...new Set(ids)].sort().join(',')
|
||||
|
||||
const [activity, setActivity] = useState<Record<string, AgentActivity>>({})
|
||||
const [held, setHeld] = useState<Set<string>>(new Set())
|
||||
const keyRef = useRef(key)
|
||||
keyRef.current = key
|
||||
|
||||
useEffect(() => {
|
||||
if (!key) {
|
||||
setActivity({})
|
||||
setHeld(new Set())
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
const tick = async () => {
|
||||
try {
|
||||
const [runs, reviews] = await Promise.all([
|
||||
api.get<AgentActivity[]>(`/api/assembler/agent-activity?agentIds=${encodeURIComponent(key)}`),
|
||||
organizationId
|
||||
? api.get<PendingReview[]>(`/api/governance/reviews?organizationId=${organizationId}&status=Pending`)
|
||||
: Promise.resolve([] as PendingReview[]),
|
||||
])
|
||||
if (cancelled) return
|
||||
setActivity(Object.fromEntries(runs.map((r) => [r.agentId, r])))
|
||||
setHeld(new Set(reviews.map((r) => r.agentId)))
|
||||
} catch {
|
||||
// Keep the last known state on a transient failure — the face just stops updating briefly.
|
||||
}
|
||||
}
|
||||
|
||||
void tick()
|
||||
const timer = setInterval(tick, POLL_MS)
|
||||
return () => {
|
||||
cancelled = true
|
||||
clearInterval(timer)
|
||||
}
|
||||
// `key` captures the set of agent ids; re-poll when it or the org changes.
|
||||
}, [key, organizationId])
|
||||
|
||||
return useCallback(
|
||||
(agentId?: string | null): FaceState =>
|
||||
agentId ? faceFor(activity[agentId], held.has(agentId)) : 'idle',
|
||||
[activity, held],
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user