Bots
Animated characters that react to an AI agent. You bring the model and the stream; the bots are its face.
Install
npm i glyphflowBots ship in the same package, from 3.2.0: no second install, no extra dependency, and nothing loads until you import it. The peer dependencies are the same as the icons (Angular 20 to 22).
Four entry points, pay for what you import
Each one is a separate import, so you only pay for the pieces you name. Weights are KB gzip, measured by the bundle check in CI on every push.
| Entry point | What it gives you | Weight |
|---|---|---|
glyphflow/bots | The engine, <gf-bot> and the shapes (catShape, mochiShape, ghostShape…) | 49.5 KB engine · 57.2 KB with one shape · 65.4 KB with the component |
glyphflow/bots/gestures | 20 gestures, each one importable on its own | +7.4 KB the first, +0.3 KB each after that |
glyphflow/bots/extras | Hats, toys and work routines, also one by one | +26.7 KB if you take all of them |
glyphflow/bots/ai | bindAgent and the Vercel AI SDK and Anthropic adapters | 0.6 KB |
Icons and bots are independent in both directions: installing one never loads the other.
Your first bot
A bot needs a shape. Each shape is a plain object you import, so the ones you do not use never reach your bundle. Pick a look with skin and name your gestures in gestures: a gesture you do not import is a gesture you do not pay for.
import { Component } from '@angular/core';
import { GfBotComponent, catShape } from 'glyphflow/bots';
import { superBounce } from 'glyphflow/bots/gestures';
@Component({
selector: 'app-mascot',
imports: [GfBotComponent],
template: '<gf-bot [shape]="shape" skin="g1" [gestures]="gestures" label="Cat" />',
})
export class Mascot {
protected readonly shape = catShape;
protected readonly gestures = { superBounce };
}Gestures
There are 20, and each is an object you import from glyphflow/bots/gestures. Calling one returns a handle.
frontFlipbackflipdoubleFlipsuperBouncesideCartwheelsideDodgeghostSwoopspinSquashtornadoSpinjellyWobblewaveThroughBodyinflateReleasestretchSnapscaredRecoilpuddleMorphballMorphjellyDropsquishTeleportpeekPopdiveEmerge
// bot = viewChild(GfBotComponent)
const run = bot()?.api?.gesture('superBounce', { intensity: 0.7, policy: 'queue' });
await run?.finished; // 'done' | 'interrupted' | 'ignored' (never rejects)
run?.cancel(); // cut it if it is still runningpolicy decides what happens when one is already running: replace (the default) cuts it, queue waits its turn (up to three) and ignore drops the new one. intensity goes from 0 to 2, and 1 is the gesture as written.
Make it yours
Everything here is data you pass in. Nothing registers globally, so none of it costs anything until you use it.
Your own colors
palette takes three CSS colors, [light, mid, shadow], or { colors, rim } to also fix the outline color. A malformed one fails when the bot is created, with a clear message.
Your own hat or toy
defineHat takes a drawing in a local frame where (0, 0) is the crown of the head and up is negative y. Hats go in through extras. defineToy does the same for toys and lets you pick one of three choreographies: ball, star or treat.
import { createHatsExtra, defineHat } from 'glyphflow/bots/extras';
const partyCap = defineHat({
label: 'Party cap',
draw: (p) => `<rect class="${p}-cap" x="-12" y="-14" width="24" height="14" rx="4" fill="#E0457B"/>`,
});
// <gf-bot [shape]="shape" [palette]="['#FFD6E8', '#FF4F9A', '#7A1049']" hat="partyCap" [extras]="extras" />
const extras = { hats: createHatsExtra({ partyCap }) }; // expose it as a field of your componentYour own skin
A skin is an id with the x- prefix (skin="x-sunset") painted with CSS variables: --gf-skin-fill, --gf-skin-gloss, --gf-skin-edge and --gf-skin-edge-width. It changes color only: a skin with its own geometry is not supported.
/* <gf-bot class="sunset" [shape]="shape" skin="x-sunset" /> */
gf-bot.sunset {
--gf-skin-fill: #ff8a5c; /* body fill */
--gf-skin-gloss: #fff3d6; /* soft highlight on top (optional) */
--gf-skin-edge: #b63a1e; /* outline (optional) */
--bot-base: #ff8a5c; /* hats and toys inherit the --bot-* variables */
--bot-primary: #ffb48f;
--bot-shadow: #b63a1e;
}Use it with an AI
glyphflow does not provide an AI API, a key or an endpoint. You bring your own model and its stream. The bots only need to know what the agent is doing, and that comes down to eight steps.
The eight steps
| Step | What it means |
|---|---|
prompt | The user just sent a message |
thinking | The model is reasoning |
tool | A tool is running |
loading | Waiting: for the model, or for a person to approve |
writing | The answer is streaming in |
done | Finished |
error | Something failed |
idle | Stopped: the bot goes back to rest |
bindAgent reads your SDK stream and turns every event into one of those steps. It does not depend on any SDK, so it adds nothing to your package.json. Repeated steps collapse: a text-delta per token looks like a single writing.
Vercel AI SDK
import { bindAgent, vercelAi } from 'glyphflow/bots/ai';
const result = streamText({ model, prompt }); // your code, your key
const run = bindAgent(bot.api, result.fullStream, vercelAi);
await run.done; // 'finished' | 'error' | 'stopped' (never rejects)
run.stop(); // cut the stream and release the botAnthropic Messages API
import { bindAgent, anthropic } from 'glyphflow/bots/ai';
const stream = client.messages.stream({ model, max_tokens: 1024, messages }); // your code, your key
const run = bindAgent(bot.api, stream, anthropic);What the bot sees for each event
This table is computed by running the real adapters, not written by hand.
| SDK | Event | Step the bot sees |
|---|---|---|
| Vercel AI SDK | start-step | loading |
| Vercel AI SDK | reasoning-delta | thinking |
| Vercel AI SDK | tool-call | tool |
| Vercel AI SDK | tool-result | no change |
| Vercel AI SDK | tool-approval-request | loading |
| Vercel AI SDK | tool-error | no change |
| Vercel AI SDK | text-delta | writing |
| Vercel AI SDK | finish | done |
| Vercel AI SDK | error | error |
| Vercel AI SDK | abort | idle |
| Anthropic | message_start | thinking |
| Anthropic | content_block_start (thinking) | thinking |
| Anthropic | content_block_start (tool_use) | tool |
| Anthropic | content_block_start (text) | writing |
| Anthropic | message_stop (stop_reason: end_turn) | done |
| Anthropic | message_stop (stop_reason: tool_use) | no change |
| Anthropic | error | error |
Try every type of output
The chat on the Bots page simulates eight types of output: a plain answer, reasoning, one tool call, parallel tool calls, a tool that fails, waiting for a person to approve, an error mid-answer and a stop. Each runs with a stream shaped like the SDK you pick, through these same adapters.
Following the pointer
<gf-bot> follows the cursor by default; [followPointer]="false" turns it off. It never follows with reduced motion, while paused or asleep, while dragged, or on touch screens. When a gesture runs the head straightens, and once it ends the bot looks at the cursor again. With createBot it stays explicit: bot.followPointer(true).
<gf-bot [shape]="shape" [followPointer]="false" />Server rendering and accessibility
On the server the host renders empty with its aspect ratio reserved, and the bot mounts after the first render, so there is no layout shift when it hydrates. With a label the bot is an image with a name; without one it is decorative. With prefers-reduced-motion the continuous motion (following the pointer, idle wandering) turns off and each gesture shrinks to a short hop.