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

# Guided tour

The widget walks a first-time visitor through your page: it speaks, moves beside what it is talking about, points and highlights, then goes back to its corner.

A guided tour is a short script the avatar performs on your page. On a visitor's
first click on the widget, the call opens as usual, the avatar comes to the middle
of the screen, and then talks the visitor through the page. As each sentence starts,
it moves beside the thing it names, points at it with an arrow, and sweeps a
highlight behind the text. When it is done it says a closing line, glides back to
its corner, and the call carries on as a normal conversation.

The tour is a prop on [`AvatarWidget`](/docs/sdk/react/widget). It is not available
on a plain `Avatar`.

```tsx
import { AvatarProvider, AvatarWidget, type WidgetTour } from "@avatarfactory/react";
import "@avatarfactory/react/styles.css";

const HOME_TOUR: WidgetTour = {
  steps: [
    { say: "Hi! Let me show you around." },
    { say: "This is everything we do, in one line.", point: "Live Presence", highlight: "Live Presence" },
    { say: "Pricing starts free.", go: "Pricing", highlight: { target: "Free forever", strong: true } },
    { say: "And when you are ready, start here.", point: "Get started" },
  ],
};

const SiteWidget = () => (
  <AvatarProvider
    config={{
      getSessionToken: async () => fetchSessionToken(),
      mode: "call",
      avatar: { avatarId: "default" },
    }}
  >
    <AvatarWidget tour={HOME_TOUR} appearance={{ teaser: { text: "Want a quick tour?" } }} />
  </AvatarProvider>
);
```

## How a visit plays out

1. The page loads. The widget sits in its corner; nothing connects and nothing is
   billed. If you set `appearance.teaser`, its pill appears beside the bubble.
2. The visitor clicks the bubble or the pill. That click is what lets the browser
   play sound, so **a tour never starts on its own**. It always rides a click.
3. The session opens exactly as you configured it, greeting included. The avatar
   glides to the middle of the screen while it greets.
4. Once the greeting is over, the avatar speaks the tour. Each step's actions run
   as its first word is heard.
5. The closing line plays while the avatar returns to its corner and the page
   scrolls back to the top. The call stays open; the visitor can ask anything.

The tour plays in the collapsed bubble, so the panel never covers what the avatar
is pointing at. After the tour, a click on the bubble opens the panel as normal.

> **A tour is one spoken line.** The steps and the closing line are joined and
>   spoken as a single scripted utterance, capped at **1000 characters**. A longer
>   tour is not played at all: the SDK logs a warning and the avatar goes back to its
>   corner. Keep each `say` to one short sentence.

## Tour options

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `steps` | `TourStep[]` | — | The script, one sentence per step, in order. When present it wins over generate. |
| `generate` | `boolean` | `false` | With no steps, have the server write a tour from the page the visitor is on. See Generated tours below. |
| `closing` | `string \| false` | `"I\'m right here, so let me know if you have any questions."` | Said while the avatar returns to its corner and the page scrolls to the top. Pass false to end silently. |
| `once` | `"visitor" \| "session" \| false` | `"visitor"` | How long a visitor counts as having had the tour. visitor remembers across visits (localStorage), session for this tab only (sessionStorage), false offers it on every first click. |
| `greeting` | `boolean` | `true` | Play the avatar's configured greeting before the tour. false lets the tour open the session instead; every later session greets as configured. |
| `highlightColor` | `string` | — | Any CSS colour for the highlighter. A strong highlight uses it as given; a normal one uses a lighter tint of it. Defaults to marker yellow. |

> `greeting: false` affects only the session the tour opens. The SDK never changes
>   your greeting setting on its own; if you configured no greeting, the tour simply
>   starts as soon as the call is connected.

## Writing steps

Each step is one sentence and what to show while it is said. Every action in a
step runs as that sentence starts.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `say` **(required)** | `string` | — | The sentence. A step with nothing to say is dropped. |
| `point` | `string \| TourTarget` | — | Move beside this target and point at it with the arrow. |
| `go` | `string \| TourTarget` | — | Move beside this target without the arrow. |
| `spot` | `string` | — | Move to one of nine places on the screen: top-left, top-center, top-right, middle-left, center, middle-right, bottom-left, bottom-center, bottom-right. |
| `highlight` | `string \| TourHighlight` | — | Sweep a highlight behind this text. A string is a normal highlight; { target, strong: true } is for the one thing the visitor must not miss. |
| `scroll` | `string` | — | Bring this target on screen without moving the avatar. |
| `gesture` | `string` | — | Play a gesture from the avatar's rig. Ignored by a rig that does not have it; useAvatar().availableActions lists the names. |

A step can use **at most one** of `point`, `go` and `spot`. The avatar cannot stand
in two places, and the type will not let you write it.

- The first step starts in the centre of the screen unless it says where to go.
- Each step clears the previous step's arrow and highlight. The avatar stays where
  it is until a later step moves it.
- A target off screen is scrolled into view before the avatar moves to it. A step
  with only a `highlight` scrolls its text into view too.
- While the avatar stands beside a target, it moves with the page if the visitor
  scrolls.

### Choosing a side

By default the avatar stands on whichever side of the target fits on screen, trying
right, left, below and above in that order. To ask for a side, pass a `TourTarget`:

```tsx
{
  say: "We work with the voice stack you already use.",
  point: { target: "Supported providers", from: "above" },
}
```

`from` is the side the avatar **stands on**, so `"above"` points down at the
target. If that side does not fit, the usual order takes over.

### How targets are found

Targets are the page's **visible text**, not selectors, so a script reads like
plain English and survives a redesign that keeps the words.

- Matching ignores case and extra whitespace. An exact match beats text that starts
  with the target, which beats text that merely contains it.
- Text nodes come first, then labelled elements (headings, buttons, links, labels,
  list items, paragraphs), then `aria-label` and image `alt` text. A panel with no
  visible title can still be a target through its `aria-label`.
- Hidden elements, anything under `aria-hidden="true"`, and the widget itself are
  never matched.
- A target is looked up when its step fires, so content that renders late is
  still found. A target longer than 80 characters is ignored.

If a target is not on the page, that action is skipped, the rest of the step still
runs, and a warning is logged. Editing a heading on the page means editing the
tour that names it.

## Once per visitor

A visitor is marked as having had the tour **on the click that starts it**, so a
tour they interrupted is not forced on them again. The record is keyed by the tour's
content: change the steps and every visitor is offered the new tour. If browser
storage is blocked, the tour is still offered, just not remembered.

If the visitor dismisses the [teaser](/docs/sdk/react/widget#appearance) pill with
its close button while a tour is waiting, that declines the tour as well.

The widget renders one tour at a time. For a different tour per route, pass the
right `tour` for the current page. The widget survives client-side navigation and
checks the new tour on its own.

## Interruptions

The visitor is always in charge. If they start talking over the avatar, hang up, or
the call fails to connect, the tour ends where it stands: the arrow and highlight
clear and the avatar goes back to its corner **without** the closing line. If the
speech ends before every step was reached, the remaining steps are skipped and the
avatar still goes home.

## Generated tours

With `generate: true` and no `steps`, the server writes the tour from the page the
visitor is on, while the greeting plays.

```tsx
<AvatarWidget tour={{ generate: true }} />
```

| Requirement | Why |
| --- | --- |
| mode: \"call\" | The page is read as part of a call session. |
| Read the page | Granted on the avatar in its Tool calling panel. The writer works from the page's section and button names, and only targets it was given are kept. |

If the tour cannot be written (no page grant, nothing usable on the page, or no
answer within 12 seconds), the avatar goes back to its corner and the call carries
on. A generated tour is remembered per page path rather than by content.

> A generated tour reads your page and sends it to the server, the same read that
>   [page awareness](/docs/sdk/react/app-events) uses. A written `steps` tour sends
>   only its own script.

## Controlling the tour

`useAvatarWidget()` exposes the tour alongside the widget's other controls, so you
can add your own "Take the tour" button. It works whether or not the visitor has
had the tour.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `tourState` | `TourState` | — | Where the tour is. See the table below. |
| `startTour` | `() => void` | — | Play the widget's tour, seen or not. Opens the session if none is running, so call it from a real click handler or the avatar comes up mute. |
| `stopTour` | `() => void` | — | End the tour: the speech stops, the arrow and highlight clear, and the avatar returns to its corner. The session stays open. |

| TourState | Meaning |
| --- | --- |
| idle | Not started in this page load. |
| starting | Started; the session is connecting or the greeting is playing. |
| playing | The tour is being spoken. |
| done | Finished, interrupted, or stopped. |

> `stopTour()` ends the tour where it stands: the speech stops, the arrow and highlight
>   clear, and the avatar returns to its corner. The call itself stays open.

## How it looks

The avatar points with a small dark **arrow badge** that turns toward the target
once the avatar has landed beside it. The highlight is a flat **marker** that sweeps
left to right behind the text as it is said, one rendered line at a time; the text
stays on top and is recoloured as the marker passes. A strong highlight is full
marker yellow with darker text.

Both are drawn in the SDK's own fixed layer above the page; your page's elements
are never modified. For visitors who prefer reduced motion, the arrow and marker
appear without animating.

Restyle them with CSS custom properties, set on `:root` or any ancestor of the page.
A dark site will want to, since the marker blends by multiplying with the page:

| Property | Default | Styles |
| --- | --- | --- |
| --af-pointer-bg | #0e0e10 | Arrow badge background. |
| --af-pointer-fg | #ffffff | Arrow glyph. |
| --af-highlight-color | #fef08a | Normal highlight. |
| --af-highlight-ink | #171717 | Text colour under a normal highlight. |
| --af-highlight-strong-color | #facc15 | Strong highlight. |
| --af-highlight-strong-ink | #0a0a0a | Text colour under a strong highlight. |

`tour.highlightColor` overrides the two highlight colours for this tour only. It
exists because a hosted deployment can carry a tour but cannot ship CSS.

## Complete example

```tsx
import {
  AvatarProvider,
  AvatarWidget,
  useAvatarWidget,
  type WidgetTour,
} from "@avatarfactory/react";
import "@avatarfactory/react/styles.css";

const PRODUCT_TOUR: WidgetTour = {
  steps: [
    { say: "Hello! I'm here to show you around." },
    {
      say: "Everything starts with your dashboard.",
      point: "Dashboard",
      highlight: "Dashboard",
    },
    {
      say: "Most teams are live in under ten minutes.",
      point: { target: "Quick start", from: "left" },
      highlight: { target: "under ten minutes", strong: true },
      gesture: "wave",
    },
    { say: "Questions about plans? They're all here.", go: "Pricing" },
  ],
  closing: "That's the tour. Ask me anything.",
  once: "visitor",
  greeting: false,
  highlightColor: "#a7f3d0",
};

const fetchSessionToken = async () => {
  const response = await fetch("/api/avatar-session", { method: "POST" });
  const { token } = await response.json();
  return token;
};

function TakeTheTourButton() {
  const { startTour, tourState } = useAvatarWidget();
  const isTouring = tourState === "starting" || tourState === "playing";

  return (
    <button onClick={startTour} disabled={isTouring}>
      {isTouring ? "Touring…" : "Take the tour"}
    </button>
  );
}

export default function SiteAssistant() {
  return (
    <AvatarProvider
      config={{
        getSessionToken: fetchSessionToken,
        mode: "call",
        avatar: { avatarId: "default" },
      }}
    >
      <TakeTheTourButton />
      <AvatarWidget
        tour={PRODUCT_TOUR}
        appearance={{ teaser: { text: "Want a quick tour?", delayMs: 3000 } }}
      />
    </AvatarProvider>
  );
}
```

## Types

```tsx
type WidgetTour = {
  steps?: TourStep[];
  generate?: boolean;
  closing?: string | false;
  once?: "visitor" | "session" | false;
  greeting?: boolean;
  highlightColor?: string;
};

type TourStep = {
  say: string;
  // At most one of point, go and spot.
  point?: string | TourTarget;
  go?: string | TourTarget;
  spot?:
    | "top-left" | "top-center" | "top-right"
    | "middle-left" | "center" | "middle-right"
    | "bottom-left" | "bottom-center" | "bottom-right";
  highlight?: string | TourHighlight;
  scroll?: string;
  gesture?: string;
};

type TourTarget = { target: string; from?: TourSide };
type TourSide = "above" | "below" | "left" | "right";
type TourHighlight = { target: string; strong?: boolean };
type TourState = "idle" | "starting" | "playing" | "done";
```
