@avatar-state-machine-interface/react
v0.2.0
Published
Drop-in React components and hooks for embedding ASMI avatars (Avatar State Machine Interface). Bundles the correctness-critical session + face primitives so coding AIs don't hand-roll them. Pairs with @avatar-state-machine-interface/runtime.
Maintainers
Readme
@avatar-state-machine-interface/react
Drop-in React components and hooks for embedding ASMI avatars
(Avatar State Machine Interface) into any React site. Pairs with the
runtime package
@avatar-state-machine-interface/runtime.
You still design the avatar at broen.tech/apps/asmi. This package handles the correctness-critical UI surface:
- Live mid-turn expression swaps (
neutral → attentive → thinking → smiling) via the runtime's trace hook - Animation playback with all four trigger types (
timer,mouse-enter,click,response-received) - Idle auto-return so the face doesn't freeze on the last turn's expression
- Transparent face region separated from the chat shell so the avatar blends into the host site's background
npm install @avatar-state-machine-interface/react \
@avatar-state-machine-interface/runtimeTurnkey usage — <AsmiAvatar>
import { AsmiAvatar } from "@avatar-state-machine-interface/react";
import type { LlmProvider } from "@avatar-state-machine-interface/react";
import { definition } from "./asmi-definition.json"; // fetched via MCP
const llmProvider: LlmProvider = {
async generate({ systemPrompt, userPrompt, history, temperature, maxTokens }) {
// Call OpenAI / Anthropic / Gemini / whatever your site already uses.
return (await yourLlmClient.complete({
system: systemPrompt,
user: userPrompt,
history,
temperature,
maxTokens,
})).text;
},
};
export default function Page() {
return <AsmiAvatar definition={definition} llmProvider={llmProvider} debug />;
}Drop debug once you've verified the state machine paints the
expected expression trail (use devtools' console filter to see every
state transition, action, expression emission, and LLM call).
Theming
Auto-detection is on by default. <AsmiAvatar> reads the host
site's body background + text color and scans for the primary CTA
button to derive the accent, then sets those as inline --asmi-*
CSS custom properties on the widget wrapper. You get a widget that
matches the host palette out of the box — no prop wiring required.
To override explicitly (precise brand colors, dark-mode switching,
or anything the detector misses), set the --asmi-* variables on
:root or any ancestor of the widget — those win via normal CSS
specificity. Pass autoTheme={false} to skip the detection entirely
if you have a fully hand-crafted override:
:root {
--asmi-panel-bg: transparent;
--asmi-panel-fg: #0f172a;
--asmi-panel-muted: #64748b;
--asmi-panel-border: rgba(0,0,0,0.1);
--asmi-accent: #D36135;
--asmi-accent-contrast: #fff;
--asmi-bubble-bg: rgba(0,0,0,0.04);
--asmi-input-bg: transparent;
--asmi-shadow: 0 20px 60px rgba(0,0,0,0.25);
--asmi-radius: 16px;
--asmi-font: inherit;
--asmi-scroll-thumb: rgba(128,128,128,0.3);
--asmi-scroll-thumb-hover: rgba(128,128,128,0.5);
}Manually applying the detected theme
If you compose the primitives directly (below), use useHostTheme to
spread the same auto-detected palette onto your own wrapper:
import { useHostTheme, useAsmiSession, AsmiFace } from "@avatar-state-machine-interface/react";
export function CustomAvatar({ definition, llmProvider }) {
const session = useAsmiSession(definition, llmProvider);
const themeVars = useHostTheme();
return <div style={themeVars}>{/* your layout */}</div>;
}Custom chat UI — useAsmiSession + <AsmiFace>
Want file uploads, paste handling, voice input, or a radically different layout? Drop the turnkey wrapper and compose the primitives yourself:
import { useAsmiSession, AsmiFace } from "@avatar-state-machine-interface/react";
export function CustomAvatar({ definition, llmProvider }) {
const session = useAsmiSession(definition, llmProvider, { debug: true });
return (
<div className="my-layout">
<AsmiFace
definition={definition}
expression={session.state.expression}
size={320}
responseTriggerKey={session.history.length}
/>
<MyCustomChatLog history={session.history} />
<MyCustomInputWithFileUpload onSubmit={session.send} disabled={session.sending} />
</div>
);
}Everything the state machine needs is encapsulated in the hook. The
face primitive is a pure render of the expression prop (plus its
own animation-trigger state). Replace the chat surface freely — the
kernel keeps working.
Example: file upload + clipboard paste
function InputWithAttachments({ onSubmit, disabled }: {
onSubmit: (text: string) => void;
disabled: boolean;
}) {
const [text, setText] = useState("");
const [attachment, setAttachment] = useState<File | null>(null);
async function handleSend() {
// Upload attachment via your own API, then include a reference in
// the user's message or pass it to your LlmProvider out-of-band.
if (attachment) {
const url = await uploadFile(attachment);
onSubmit(`${text}\n(attached: ${url})`);
} else {
onSubmit(text);
}
setText("");
setAttachment(null);
}
return (
<div>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
onPaste={(e) => {
const file = e.clipboardData.files?.[0];
if (file) setAttachment(file);
}}
disabled={disabled}
/>
<input type="file" onChange={(e) => setAttachment(e.target.files?.[0] ?? null)} />
<button onClick={handleSend} disabled={disabled}>Send</button>
</div>
);
}Multimodal LlmProvider.generate (structured attachments payload)
is on the roadmap for v0.2. v0.1 is text-only; the API surface is
designed so attachments can be added additively without breaking.
Example: voice input (speech-to-text)
Wire any STT library (Web Speech API, Deepgram, AssemblyAI) to a
button, then call session.send(transcript) when the user stops
speaking. The hook doesn't care where the string came from.
API
useAsmiSession(definition, llmProvider, options?)
| Option | Type | Default | Notes |
|---|---|---|---|
| idleTimeoutSeconds | number | 45 | Seconds before state snaps back to idle/neutral. 0 disables. History is preserved. |
| debug | boolean | false | console.info every trace event. |
| onTrace | (e: TraceEvent) => void | undefined | Fires for every internal step (state transition, action exec, expression emit, LLM call). |
| onOutboundEvent | (e: OutboundEvent) => void | undefined | Fires for each outbound app event (asmi:handoff, etc). Also auto-dispatched on window. |
| initialState | SessionState | idle/neutral | Rehydrate from persistence. |
| initialHistory | HistoryEntry[] | [] | Rehydrate from persistence. |
| initialMetadata | SessionMetadata | fresh | Rehydrate from persistence. |
| sessionContext | Record<string, unknown> | undefined | Extra context passed to processMessage — e.g. { page, visitorTimezone }. |
Returns { state, history, metadata, sending, send, reset, setHistory, setExpression, bumpActivity }.
<AsmiFace>
Pure render primitive. Accepts definition, expression, size,
className, style, onClick, ariaLabel, responseTriggerKey.
Plays animations configured on the expression asset.
<AsmiAvatar>
Turnkey wrapper. Accepts every useAsmiSession option plus:
collapsedSize, expandedSize, position, floating,
placeholder, renderInput, renderMessage, className.
Use renderInput for custom input surfaces (file uploads, voice) and
renderMessage for custom bubble styling.
What's NOT in this package
- Multimodal
LlmProvider.generate(attachments, images, files) — v0.2 roadmap. - Automatic persistence (localStorage history, session resume) —
the integrator wires
initialHistory+setHistoryto whatever storage they already use. - Framework wrappers (Vue, Svelte, vanilla JS, Web Components) — React first.
License
MIT. See LICENSE.
