Files
Teamup/client/src/components/AgentFace.tsx
T

70 lines
2.2 KiB
TypeScript
Raw Normal View History

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