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 status and messages works 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 state

Attributes

Everything is optional except the agent. Anything you don't set comes from the agent's name, so it stays the same everywhere.

AttributeWhat it doesExample
agentYour agent's name or ID. The same value always gives the same character.agent="support-bot"
stateWhat the agent is doing or feeling (see States).state="thinking"
sizeWidth and height, in pixels or any CSS length. Default 96.size="120"
inputA text box to follow: it listens while people type and thinks when they send.input="#message"
keyYour claimed character's key. It shows only on your domains (see Your own character).key="ak_…"
speciesPin 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, wearPin any other trait. See the options in the collection.colorway="mint"
kitDress 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, accentYour brand colours, on its outfit and products. Claimed characters only.brand="#2fa39b"
productYour product in its hands: cup, phone, parcel, card or book. Claimed characters only.product="cup"
modesvg always uses the 2D version. By default it's 3D, with 2D as the fallback.mode="svg"
followfalse stops it turning to follow the pointer.follow="false"
interactivefalse turns off drag-to-spin and reactions to taps.interactive="false"
arrivefalse makes it appear in place instead of dropping in.arrive="false"
pastimes, pastime-afterWhether, and after how many quiet seconds, it finds something to do (a phone, a coffee, a nap).pastime-after="30"
shadowtrue draws a soft ground shadow.shadow="true"
animatedfalse shows a still pose (2D).animated="false"

Agent events

Pass these to react(). Each one maps to the state that fits it.

EventState
session.startgreeting
session.endgoodbye
user.typinglistening
user.speakinglistening
agent.thinkingthinking
agent.tool_callworking
agent.searchingsearching
agent.speakingspeaking
agent.waitingwaiting
agent.needs_confirmationnervous
agent.idleidle
agent.sleepingsleeping
task.successsuccess
task.errorerror
task.retrydetermined

States

Every character can show all 29 states. Set one with state="…" or face.state.

StateUse it for
idleDefault resting state between turns.
greetingSession start, or when the user says hello.
listeningWhile the user is typing or speaking.
thinkingReasoning before the first token arrives.
speakingWhile streaming a reply or playing voice output.
workingRunning a tool, writing code, doing a long task.
searchingWeb search, retrieval, looking something up.
waitingQueued, rate-limited, or waiting on the user.
successTask finished, tests passed, goal reached.
errorAn error, a failed tool call, or an unclear request.
apologizingOwning a mistake or delivering bad news gently.
goodbyeSession end or sign-off.
sleepingOffline, paused, or idle for a long time.
happyGood news, kind words, a pleasant moment.
excitedBig wins, new ideas, enthusiastic replies.
laughingJokes and genuinely funny moments.
loveThanks, appreciation, warm feedback.
surprisedUnexpected results or news.
sadEmpathy for the user's bad day, or a loss.
angryRepeated failures, blocked by something. Use sparingly.
shyReceiving a compliment, a little embarrassed.
proudShowing off finished work.
curiousAsking a clarifying question, exploring.
calmReassuring, mindful, low-stakes moments.
nervousRisky actions, asking for confirmation.
determinedTaking on a hard task, retrying with resolve.
playfulLight banter, easter eggs, fun moments.
eurekaFound the answer, had an idea.
sleepyLate 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_url needs the chat:write.customize scope; 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.