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>
|
|||
|
|
)
|
|||
|
|
}
|