Docs
A face for your agent, in one tag.
Quick start
Add the script once, then put a face beside your chat with one tag. agent is your agent's name or ID: the same agent always gets the same character, on every device. Then wrap your agent's stream (next section) and it follows along on its own.
<script src="https://taamtoom.com/embed.js" defer></script>
<ai-face agent="support-bot"></ai-face>- It's a standard web component: it works in plain HTML, React, Vue, Svelte, Angular and any other framework, with no dependencies.
- About 80 KB gzipped, 3D included. Everything runs in the page; nothing about your users is sent anywhere.
Follow your agent's stream
Already streaming from OpenAI, Anthropic or the Vercel AI SDK? Pass the stream through the face and keep using it as before. It reads along: thinking, looking things up, working, talking, then settling into the reply's tone, and sorry if something fails.
const face = document.querySelector("ai-face");
// Your own chat endpoint (server-sent events or JSON lines)
const res = face.follow(await fetch("/api/chat", { method: "POST", body }));
// An SDK stream: iterate it exactly as you did before
const events = face.follow(await openai.responses.create({ model, input, stream: true }));
const reply = face.follow(await anthropic.messages.create({ model, max_tokens, messages, stream: true }));- It understands OpenAI (Responses and Chat Completions), Anthropic (Messages), the Vercel AI SDK (UI message streams and
fullStream) and AG-UI, and works out which one it's reading. - You get your stream back untouched: the face reads its own copy, so your code doesn't change, and nothing leaves the page.
- A failed request (a server error, a rate limit) shows as sorry, with no extra code.
React and the Vercel AI SDK
With useChat, hand the chat to the face whenever it changes. Sent, it thinks; streaming, it talks; done, it settles into the reply's tone; an error, it's sorry.
import { useEffect, useRef } from "react";
import { useChat } from "@ai-sdk/react";
export function Assistant() {
const chat = useChat();
const face = useRef(null);
useEffect(() => { face.current?.follow(chat); }, [chat.status, chat.messages]);
return <ai-face ref={face} agent="support-bot" />;
}- Any chat object with a
statusandmessagesworks the same way, so other chat hooks need no adapter.
Let it decide
Rather not map anything? It can work out what to feel on its own, from what your page and your agent already do.
<!-- Follows your chat box: listens while people type, thinks when they send -->
<ai-face agent="support-bot" input="#message"></ai-face>input="#message"points it at your text box: typing makes it listen (with a nod when people pause), and sending makes it think, with no code.face.speak(reply)reads the tone of the reply and settles into it: thanks bring a warm face, an apology a sorry one, "all done" a happy one.- Speaks AG-UI? Agents built with LangGraph, CrewAI, Mastra, Pydantic AI, CopilotKit and other AG-UI frameworks can forward every event as it comes:
agent.subscribe({ onEvent: ({ event }) => face.react(event) }). Runs and reasoning show as thinking, tools that look things up as searching and other tools as working, streamed replies as talking (then the reply's tone), and errors as errors.
React to your agent
Set a state directly, or forward the events your agent already emits and let the face pick the state.
const face = document.querySelector("ai-face");
face.state = "thinking"; // any state below
face.react("agent.searching"); // an agent event: picks the state for you
face.react("user.typing"); // send on every keystroke: it listens, and nods when the user pauses
face.speak("All done!"); // talks in the rhythm of the words, then settles into their mood
face.speak(reply, { audio }); // or lip-syncs to your voice: an <audio> element or a MediaStream
face.play("celebrate"); // a one-off move on top of the stateAttributes
Everything is optional except the agent. Anything you don't set comes from the agent's name, so it stays the same everywhere.
| Attribute | What it does | Example |
|---|---|---|
| agent | Your agent's name or ID. The same value always gives the same character. | agent="support-bot" |
| state | What the agent is doing or feeling (see States). | state="thinking" |
| size | Width and height, in pixels or any CSS length. Default 96. | size="120" |
| input | A text box to follow: it listens while people type and thinks when they send. | input="#message" |
| key | Your claimed character's key. It shows only on your domains (see Your own character). | key="ak_…" |
| species | Pin the family instead of letting the agent's name choose. Free: blob, bean, squircle, drop, mochi, onigiri, bao, puff; the rest need a claimed character (`key`). | species="mochi" |
| variant, colorway, fabric, eyes, accessory, wear | Pin any other trait. See the options in the collection. | colorway="mint" |
| kit | Dress it for your industry: health, fashion, beauty, food, fitness, education, cafe, retail, support, tee or hoodie. Claimed characters only, like brand colours, products and marks. | kit="support" |
| brand, accent | Your brand colours, on its outfit and products. Claimed characters only. | brand="#2fa39b" |
| product | Your product in its hands: cup, phone, parcel, card or book. Claimed characters only. | product="cup" |
| mode | svg always uses the 2D version. By default it's 3D, with 2D as the fallback. | mode="svg" |
| follow | false stops it turning to follow the pointer. | follow="false" |
| interactive | false turns off drag-to-spin and reactions to taps. | interactive="false" |
| arrive | false makes it appear in place instead of dropping in. | arrive="false" |
| pastimes, pastime-after | Whether, and after how many quiet seconds, it finds something to do (a phone, a coffee, a nap). | pastime-after="30" |
| shadow | true draws a soft ground shadow. | shadow="true" |
| animated | false shows a still pose (2D). | animated="false" |
Agent events
Pass these to react(). Each one maps to the state that fits it.
| Event | State |
|---|---|
| session.start | greeting |
| session.end | goodbye |
| user.typing | listening |
| user.speaking | listening |
| agent.thinking | thinking |
| agent.tool_call | working |
| agent.searching | searching |
| agent.speaking | speaking |
| agent.waiting | waiting |
| agent.needs_confirmation | nervous |
| agent.idle | idle |
| agent.sleeping | sleeping |
| task.success | success |
| task.error | error |
| task.retry | determined |
States
Every character can show all 29 states. Set one with state="…" or face.state.
| State | Use it for |
|---|---|
| idle | Default resting state between turns. |
| greeting | Session start, or when the user says hello. |
| listening | While the user is typing or speaking. |
| thinking | Reasoning before the first token arrives. |
| speaking | While streaming a reply or playing voice output. |
| working | Running a tool, writing code, doing a long task. |
| searching | Web search, retrieval, looking something up. |
| waiting | Queued, rate-limited, or waiting on the user. |
| success | Task finished, tests passed, goal reached. |
| error | An error, a failed tool call, or an unclear request. |
| apologizing | Owning a mistake or delivering bad news gently. |
| goodbye | Session end or sign-off. |
| sleeping | Offline, paused, or idle for a long time. |
| happy | Good news, kind words, a pleasant moment. |
| excited | Big wins, new ideas, enthusiastic replies. |
| laughing | Jokes and genuinely funny moments. |
| love | Thanks, appreciation, warm feedback. |
| surprised | Unexpected results or news. |
| sad | Empathy for the user's bad day, or a loss. |
| angry | Repeated failures, blocked by something. Use sparingly. |
| shy | Receiving a compliment, a little embarrassed. |
| proud | Showing off finished work. |
| curious | Asking a clarifying question, exploring. |
| calm | Reassuring, mindful, low-stakes moments. |
| nervous | Risky actions, asking for confirmation. |
| determined | Taking on a hard task, retrying with resolve. |
| playful | Light banter, easter eggs, fun moments. |
| eureka | Found the answer, had an idea. |
| sleepy | Late nights, winding down, long waits. |
Moves
One-off moves for face.play(), on top of whatever state it's in (3D only).
| Move |
|---|
| spin |
| jump |
| hop |
| dance |
| celebrate |
| wiggle |
| nod |
| shake |
| boop |
| peek |
Your own character
Free characters are shared: the same name gives the same character anywhere. Claim one and it's yours alone: nobody else can get it, and it shows only on your domains.
<ai-face key="ak_your_key"></ai-face>- Claim any character from its page in the collection, pay once, and we send your key. Tell us the domains it should appear on (subdomains too if you like, and localhost for building).
- On any other site the key draws nothing. For emails and chat apps, add
key=ak_…to an image URL.
Images
For Slack, Teams, WhatsApp, email, bot avatars and anywhere scripts can't run: the same character as an image, from a URL. Swap the state as your agent works.
<img src="https://taamtoom.com/api/mascot/support-bot.png?state=happy&size=128" alt="Support bot" width="128" height="128">
// Slack: the agent's face on every message, in the state that fits it
await slack.chat.postMessage({
channel,
text: reply,
icon_url: `https://taamtoom.com/api/mascot/support-bot.png?state=${state}&size=128`,
});- Use .png for chat apps, email and avatars, and .svg on web pages (sharper, and it moves).
- In Slack, a custom
icon_urlneeds thechat:write.customizescope; or set the same image once as the app's icon. - Query options: state, size (16–1024, default 256), bg (none, auto or a hex colour), animated=false, shadow=true, and any trait from Attributes.
- Ask for .json instead to get the character's traits.
- A claimed character: add
key=ak_…(the name in the address is then ignored).
Accessibility and performance
Built to sit quietly inside a product.
- Every character has a screen reader label, and all motion stops for people who prefer reduced motion.
- Nothing about your users ever leaves the page. Beyond downloading the script (which we count by your site's address, to know who uses it), the only request it makes is for a claimed character's design, by its key.
- It pauses when it's off-screen, and many characters on one page share a single 3D context.
- Where 3D isn't available, the 2D version takes over on its own.
Make it yours alone
Claim a character to unlock every family, your brand colour and outfit, a name of your choosing, and no credit link. Paid once. See pricing.