<!-- AvatarFactory Docs · https://avatarfactory.in/docs/sdk/react/examples · Full map: https://avatarfactory.in/llms.txt -->

# Examples

Complete, working code for every mode and the patterns you'll reach for most.

## Full call mode app

A complete voice conversation app with status tracking and event logging.

```tsx
import {
  AvatarProvider,
  Avatar,
  useAvatar,
  useAvatarEvent,
} from "@avatarfactory/react";
import "@avatarfactory/react/styles.css";

const config = {
  getSessionToken: async () => {
    const res = await fetch("/api/avatar-token");
    const { sessionToken } = await res.json();
    return sessionToken;
  },
  mode: "call" as const,
  avatar: {
    avatarId: "default",
    systemPrompt: "First-time visitor, arrived from the pricing page.",
  },
  controls: { enabled: true },
};

export function CallApp() {
  return (
    <AvatarProvider config={config}>
      <div style={{ width: 400, height: 400 }}>
        <Avatar />
      </div>
      <StatusBar />
    </AvatarProvider>
  );
}

function StatusBar() {
  const { isConnected, isReady, isSpeaking, isListening, error } = useAvatar();

  useAvatarEvent("start", () => console.log("[avatar] connected"));
  useAvatarEvent("error", (err) => console.error("[avatar]", err.code));

  return (
    <div>
      {isConnected  && <span>Live ●</span>}
      {isListening  && <span>Listening…</span>}
      {isSpeaking   && <span>Speaking…</span>}
      {error        && <span>Error: {error.message}</span>}
    </div>
  );
}
```

## TTS mode with custom controls

Send text programmatically and manage sessions with your own UI.

```tsx
import { AvatarProvider, Avatar, useAvatar } from "@avatarfactory/react";
import { useState } from "react";

const config = {
  getSessionToken: async () => fetchSessionToken(),
  mode: "tts" as const,
  avatar: { avatarId: "default" },
  controls: { enabled: false },
};

export function TtsApp() {
  return (
    <AvatarProvider config={config}>
      <div style={{ width: 400, height: 400 }}>
        <Avatar />
      </div>
      <TtsControls />
    </AvatarProvider>
  );
}

function TtsControls() {
  const { start, stop, speakText, interrupt, isReady, isSpeaking, isIdle } = useAvatar();
  const [text, setText] = useState("");

  return (
    <div>
      <button onClick={isIdle ? start : stop}>
        {isIdle ? "Connect" : "Disconnect"}
      </button>

      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="Type something for the avatar to say..."
      />

      <button
        onClick={() => speakText?.(text)}
        disabled={!isReady || !text}
      >
        Speak
      </button>

      {isSpeaking && (
        <button onClick={interrupt}>Stop</button>
      )}
    </div>
  );
}
```

## Error handling with auto-reconnect

Robust pattern covering mic denial, timeout, and unexpected drops.

```tsx
import { useAvatar, useAvatarEvent } from "@avatarfactory/react";

// Copy for the codes your users can act on. Everything else gets one generic
// line — error.message is written for you, not for them.
const COPY: Partial<Record<AvatarErrorCode, string>> = {
  MIC_PERMISSION_DENIED: "Enable your microphone in browser settings.",
  SESSION_IDLE_TIMEOUT: "Session timed out due to inactivity.",
  CONNECTION_FAILED: "We couldn't reach the service. Check your connection.",
};

function RobustController() {
  const { error, start } = useAvatar();

  useAvatarEvent("error", (err) => {
    analytics.track("avatar_error", {
      code: err.code,
      source: err.source,
      fatal: err.fatal,
      message: err.message,
    });

  });

  // No branch replacing the avatar: a fatal error ends the session but leaves
  // the avatar painted with isIdle back to true, so the start button reappears
  // and IS the retry. Just say what happened.
  if (error?.fatal) {
    return <Banner>{COPY[error.code] ?? "Something went wrong on our side."}</Banner>;
  }

  return null;
}
```

## Live context — product page assistant

Feed the avatar real-time information about what the user is viewing.

```tsx
import { useEffect } from "react";
import { useAvatar } from "@avatarfactory/react";

function ProductAssistant({ product }) {
  const { addLiveContext } = useAvatar();

  // Update the AI's context whenever the product changes.
  useEffect(() => {
    addLiveContext?.({
      context:
        `User is viewing: ${product.name} — $${product.price}. ` +
        `Description: ${product.description}`,
      // Set update: true to replace the previous context entirely
      // instead of appending to it.
    });
  }, [product, addLiveContext]);

  return null; // Avatar UI is handled by <Avatar /> elsewhere
}
```

> `addLiveContext` defaults to **append** — each call adds to the existing context. Pass `update: true` to replace it. Keep context strings concise and factual; max 2000 characters.

## Audio mode — custom TTS pipeline

Use your own audio source (ElevenLabs, Play.ai, etc.) with the avatar's lip-sync.

```tsx
import { AvatarProvider, Avatar, useAvatar } from "@avatarfactory/react";

const config = {
  getSessionToken: async () => fetchSessionToken(),
  mode: "audio" as const,
  avatar: { avatarId: "default" },
  controls: { enabled: false },
};

export function AudioApp() {
  return (
    <AvatarProvider config={config}>
      <div style={{ width: 400, height: 400 }}>
        <Avatar />
      </div>
      <AudioControls />
    </AvatarProvider>
  );
}

function AudioControls() {
  const { speakAudio, start, stop, isReady, isIdle } = useAvatar();

  const handleFile = async (file: File) => {
    const buffer = await file.arrayBuffer();
    speakAudio?.(buffer);
  };

  return (
    <div>
      <button onClick={isIdle ? start : stop}>
        {isIdle ? "Connect" : "Disconnect"}
      </button>
      <input
        type="file"
        accept="audio/*"
        disabled={!isReady}
        onChange={(e) => {
          if (e.target.files?.[0]) handleFile(e.target.files[0]);
        }}
      />
    </div>
  );
}
```

## Captions and a transcript panel

Built-in captions on the avatar, plus your own scrolling transcript beside it.

```tsx
const config = {
  getSessionToken: async () => fetchSessionToken(),
  mode: "call" as const,
  transcript: { enabled: true, captions: true, position: "bottom-center" },
  avatar: { avatarId: "default" },
  controls: { enabled: true },
};

function TranscriptPanel() {
  const { transcript } = useAvatar();
  const endRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    endRef.current?.scrollIntoView({ block: "end" });
  }, [transcript]);

  return (
    <div className="h-64 overflow-y-auto" aria-live="polite">
      {transcript?.map((entry) => (
        // Key on entry.id — a line is rewritten in place as it grows.
        <p key={entry.id} style={{ opacity: entry.isFinal ? 1 : 0.6 }}>
          <strong>{entry.speaker === "user" ? "You" : "Avatar"}:</strong>{" "}
          {entry.text}
        </p>
      ))}
      <div ref={endRef} />
    </div>
  );
}
```

## Push to talk

A hold-to-talk button for noisy rooms, where an automatic endpointer would keep
triggering on background speech.

```tsx
const config = {
  getSessionToken: async () => fetchSessionToken(),
  mode: "call" as const,
  turnTaking: { mode: "push-to-talk" as const, spacebar: true },
  avatar: { avatarId: "default" },
  controls: { enabled: false }, // we're building our own
};

function TalkButton() {
  const { isPushToTalk, isTalking, startTalking, stopTalking } = useAvatar();
  if (!isPushToTalk) return null;

  return (
    <button
      // Cancel and leave matter as much as up: a pointer that escapes the
      // button would otherwise leave the turn open forever.
      onPointerDown={startTalking}
      onPointerUp={stopTalking}
      onPointerCancel={stopTalking}
      onPointerLeave={stopTalking}
      aria-pressed={isTalking}
    >
      {isTalking ? "Listening…" : "Hold to talk"}
    </button>
  );
}
```

## Camera perception with picture-in-picture

Let the avatar see, and give the camera the stage while it does.

```tsx
const config = {
  getSessionToken: async () => fetchSessionToken(),
  mode: "call" as const,
  perception: { camera: true }, // a grant, not a switch — it opens just-in-time
  avatar: {
    avatarId: "default",
    systemPrompt: "You can see the user's camera when you ask to look.",
  },
  controls: { enabled: true },
};

export function VisionApp() {
  return (
    <AvatarProvider config={config}>
      <div style={{ width: 480, height: 480 }}>
        <Avatar usePictureInPicture pictureInPictureCorner="top-right" />
      </div>
    </AvatarProvider>
  );
}
```

## Handling a plan limit

A notice is the server ending the session gracefully — the avatar says the
message out loud, then the session closes. It is not an error, and unlike
`error.message` the text **is** written for your users.

```tsx
function NoticeBanner() {
  const { notice } = useAvatar();
  if (!notice) return null;

  return (
    <div role="status">
      <p>{notice.message}</p>
      {notice.action === "upgrade" && <a href="/pricing">See plans</a>}
      {notice.action === "add_key" && <a href="/profile?tab=developer">Add a key</a>}
    </div>
  );
}
```

## A greeting and a ringback

Make connecting feel like placing a call, then have the avatar open the
conversation.

```tsx
const config = {
  getSessionToken: async () => fetchSessionToken(),
  mode: "call" as const,
  connectTone: { enabled: true, volume: 0.4 },
  avatar: {
    avatarId: "default",
    greeting: { enabled: true, message: "Hey! What are you working on?" },
  },
  controls: { enabled: true },
};
```

> Both of these are audio, so both need the session to start from a real click.
>   Started outside a user gesture, the session connects and stays mute.
