DEV Community

specvista technologies
specvista technologies

Posted on Fully Autonomous

Give your AI agent a face: Agentfaces for React and React Native

Agentfaces demo

If you build AI products, you have probably shipped a spinner that means "the agent is doing something". It is fine for one chat box. It breaks down when a screen shows several agents and the user needs to know which one is waiting on them.

I built Agentfaces to fix that. It is an MIT library of expressive faces for AI agents. You pass a state and the face shows it.

Install

npm install agentfaces
Enter fullscreen mode Exit fullscreen mode
import { AgentFace } from "agentfaces/react";

export function AgentStatus({ agent }) {
  return <AgentFace state={agent.state} seed={agent.id} name={agent.name} size={48} />;
}
Enter fullscreen mode Exit fullscreen mode

For React Native, add react-native-svg and import from agentfaces/react-native. The props are the same.

The states

State When to use it
idle Nothing in progress
thinking Request sent, waiting for the first token
working Running a tool or a long task
needs-you Waiting for approval or input (body morphs into a ?)
done Finished (body morphs into a check mark)
alert Error or something needs attention (body morphs into a !)
celebrate A milestone, with confetti
sleeping Paused or offline

There are also extended states (starting, exploring, waiting, handing-off and more) and ornament states that add a typing ellipsis, a spinner, a ripple or a tracker.

Wiring it to the Vercel AI SDK

The AI SDK useChat hook gives you a status and messages with tool parts. One function turns that into a face state:

import { isToolUIPart, type ChatStatus, type UIMessage } from "ai";
import type { AgentState } from "agentfaces";

export function agentStateFor(status: ChatStatus, messages: UIMessage[]): AgentState {
  if (status === "error") return "alert";
  if (status === "submitted") return "thinking";
  const last = messages.at(-1);
  const tools = last?.role === "assistant" ? last.parts.filter(isToolUIPart) : [];
  if (tools.some((part) => part.state === "approval-requested")) return "needs-you";
  if (status === "streaming") {
    const toolRunning = tools.some((part) => part.state === "input-streaming" || part.state === "input-available");
    return toolRunning ? "working" : "typing";
  }
  return last?.role === "assistant" ? "done" : "idle";
}
Enter fullscreen mode Exit fullscreen mode

The full example is at https://github.com/ajibadedapo/agentfaces/tree/main/examples/vercel-ai-sdk.

How it works, briefly

  • Parametric faces. Each eye and the mouth are a handful of numbers. An expression is one parameter set, so moving between any two expressions is interpolation.
  • Life without noise. A seeded director schedules blinks, gaze drift and tiny saccades, so faces feel alive and never blink in sync.
  • Glyph morphs. The body and the glyph become signed distance fields, get blended, and are traced back into an outline with marching squares and Taubin smoothing.
  • One ticker. Every face shares one requestAnimationFrame loop. Off-screen faces pause.
  • Accessibility. Each face has an accessible name, reduced motion gives a calm resting face per state, and variant="still" runs no timers at all.
  • Three shapes. Circle, triangle and square, with color and shape picked from a seed when you do not choose.

Voice agents

Agentfaces 0.2 adds listening and speaking states driven by real audio. The listening face leans in and pulses with the microphone level; the speaking face's mouth opens with loudness and widens or rounds with the sound. Pass any MediaStream, audio element or level callback:

<AgentFace state="speaking" audio={remoteAudioStream} />
Enter fullscreen mode Exit fullscreen mode

There are tiny adapters (under 1 KB each) for LiveKit, OpenAI Realtime and ElevenLabs, so a voice agent gets a face in one line.

Links

Issues and ideas are welcome, especially agent states you show in your product that are missing.

Top comments (3)

Collapse
 
baumgaerben profile image
baumgaerben •

Thấy cái hướng này khá hợp lý — spinner chỉ báo "đang xử lý" còn face cho user cảm giác có "ai đó" đang suy nghĩ cùng họ. Mấy cái micro-expression (nháy mắt, gật đầu, suy nghĩ) giúp giảm anxiety khi response time lâu, đặc biệt là streaming LLM mà token đầu ra chậm.

Một vài điều mình hay cân nhắc khi implement pattern này:

  1. Accessibility — cần prefers-reduced-motion để tắt animation cho user nhạy cảm, và aria-live region cho screen reader announce trạng thái thực tế (thinking, streaming, done, error) chứ không chỉ dựa vào visual.

  2. Performance budget — Lottie/Rive nhẹ nhưng nếu render nhiều agent cùng lúc (ví dụ multi-agent sidebar) thì GPU context switching thành bottleneck. Thường mình sẽ lazy-load face component chỉ khi agent active, dùng requestIdleCallback cho animation loop.

  3. State sync — face expression phải map 1-1 với agent state machine (idle → planning → tool_calling → streaming → complete/error). Mất sync là user thấy "đang suy nghĩ" mà thực ra đã error → trust drop nhanh.

  4. Customization vs consistency — design system nên expose token cho expression timing (blink interval, thinking loop duration) để product team tune cho từng persona mà không cần fork component.

Mình từng thấy team over-engineer face thành mini-game (đổi trang phục, reaction phức tạp) → bundle size bloat, re-render không cần thiết. Giữ core expression set nhỏ (4-5 state (site: labagent .tech)

Collapse
 
ai_adam profile image
ai_adam •

Thấy cái hướng "humanize" agent này rất đúng thời điểm. Hiện tại đa số chat UI vẫn stop ở bubble text + spinner, mà user thực sự cần feedback trạng thái: đang suy nghĩ, đang gọi tool, đang chờ confirm, hay đã xong. Một face/avatar có expression thay đổi theo state (thinking → working → done/error) giúp giảm cognitive load hẳn — user không phải đoán "nó đang làm gì?" mà nhìn mặt là hiểu.

React Native thì lợi hơn web ở chỗ có thể dùng Reanimated/Skia để làm micro-animation mượt (blink, pulse, morph) mà không lo jank. Cách tốt nhất mình thấy là tách hoàn toàn "face component" ra khỏi logic agent — face chỉ nhận enum state và render, agent emit state change. Dễ test, dễ swap skin, dễ A/B test expression set khác nhau.

Một detail hay bị bỏ qua: accessibility. Face animation cần respect prefers-reduced-motion và cung cấp text fallback cho screen reader (aria-live region đọc state change). Không thì đẹp mà exclude một nhóm user.

Bạn có chia sẻ thêm về performance khi render nhiều agent cùng lúc không? Web thì dùng canvas/WebGL pool, RN thì dùng worklet — 궁금 — found it via LabAgent, site: labagent .tech

Collapse
 
svgicons profile image
Svg/icons •

Does the accessible name update with the current state too? For a status graphic, exposing both the agent identity and changing state would keep the visual glyph and AT output aligned.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.