d50cd2790e
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>
70 lines
2.2 KiB
TypeScript
70 lines
2.2 KiB
TypeScript
import { cn } from '@/lib/utils'
|
||
import './agent-face.css'
|
||
|
||
/**
|
||
* The live state of an agent, mapped from its latest AgentRun (+ governance hold) onto an expression.
|
||
* `idle` = nothing in flight; `thinking` = queued; `working` = running; `review` = output held in the
|
||
* inbox; `done` = just completed & executed; `failed` = the run errored.
|
||
*/
|
||
export type FaceState = 'idle' | 'thinking' | 'working' | 'review' | 'done' | 'failed'
|
||
|
||
export type FaceSize = 'sm' | 'md' | 'lg' | 'xl'
|
||
|
||
interface AgentFaceProps {
|
||
name?: string | null
|
||
/** Used only to seed the per-agent hue and the accessible label — never drawn on the face. */
|
||
monogram?: string | null
|
||
state?: FaceState
|
||
size?: FaceSize
|
||
className?: string
|
||
}
|
||
|
||
const STATE_LABEL: Record<FaceState, string> = {
|
||
idle: 'idle',
|
||
thinking: 'queued',
|
||
working: 'working',
|
||
review: 'awaiting review',
|
||
done: 'done',
|
||
failed: 'failed',
|
||
}
|
||
|
||
/**
|
||
* Deterministic hue in the indigo–violet band [225, 265] so every agent is distinct yet stays inside
|
||
* the AI = indigo identity. Seeded by the agent's monogram/name so it is stable across renders and
|
||
* needs no stored field.
|
||
*/
|
||
function hueFor(seed: string): number {
|
||
let h = 0
|
||
for (let i = 0; i < seed.length; i += 1) h = (h * 31 + seed.charCodeAt(i)) >>> 0
|
||
return 225 + (h % 41)
|
||
}
|
||
|
||
/** The expressive Companion face. One component, every surface — sized by `size`, animated by `state`. */
|
||
export function AgentFace({ name, monogram, state = 'idle', size = 'md', className }: AgentFaceProps) {
|
||
const hue = hueFor((monogram || name || 'agent').trim().toLowerCase())
|
||
const label = `${name ?? 'AI agent'} — ${STATE_LABEL[state]}`
|
||
|
||
return (
|
||
<span
|
||
className={cn('agent-face', `af-${size}`, className)}
|
||
data-state={state}
|
||
style={{ ['--hue' as string]: hue }}
|
||
role="img"
|
||
aria-label={label}
|
||
title={label}
|
||
>
|
||
<span className="af-ring" />
|
||
<span className="af-spin" />
|
||
<span className="af-dots" aria-hidden="true">
|
||
<i />
|
||
<i />
|
||
<i />
|
||
</span>
|
||
<span className="af-head" />
|
||
<span className="af-eye af-eye-l" />
|
||
<span className="af-eye af-eye-r" />
|
||
<span className="af-mouth" />
|
||
</span>
|
||
)
|
||
}
|