# AI hooks

> **Info:** This is release candidate documentation for **Stream Chat React SDK v15**. For the latest stable version, see the [latest version (v14)](https://getstream.io/chat/docs/sdk/react/).

These hooks build AI chat UI: what the assistant is doing, its reply typed out as it streams in, and voice input. `useAIState` and `useMessageTextStreaming` are exported from `stream-chat-react`. `useSpeechToText` is exported from `stream-chat-react/ai-components`, the entry point of the AI components.

How the AI states, the streamed reply and the AI components fit together is on [AI integrations overview](https://getstream.io/chat/docs/sdk/react/v15/features/ai/ai-integrations-overview/), and the components of the main entry point on [SDK integration](https://getstream.io/chat/docs/sdk/react/v15/features/ai/sdk-integration/).

Each entry gives the signature, then:

- **Returns:** what the hook gives back.
- **Reads:** the store or the context it reads.
- **Re-renders:** what makes the component render again.
- **Needs:** where the component must sit, and what the hook does outside that place.

## useAIState

Returns what the AI is doing in a channel.

```ts
useAIState(channel?: Channel): { aiState: AIState }
```

- **Returns:** `{ aiState }`, one of the `AIStates`: `AI_STATE_IDLE`, `AI_STATE_THINKING`, `AI_STATE_EXTERNAL_SOURCES`, `AI_STATE_GENERATING`, `AI_STATE_ERROR` or `AI_STATE_STOP`, or a state of your own that your backend sends. `AI_STATE_IDLE` without a channel.
- **Reads:** `channel.state`, field `aiState`. The client sets it from the `ai_indicator.update`, `ai_indicator.clear` and `ai_indicator.stop` events.
- **Re-renders:** when `aiState` changes, or the hook gets another channel.
- **Needs:** nothing. It doesn't read the channel from context: pass it, for example from `useChannel()`. Without a channel it subscribes to nothing and returns `AI_STATE_IDLE`.

```tsx
import { AIStates } from "stream-chat";
import { useAIState, useChannel } from "stream-chat-react";

export const ThinkingIndicator = () => {
  const channel = useChannel();
  const { aiState } = useAIState(channel);

  if (aiState !== AIStates.Thinking) return null;
  return <p className="thinking">The assistant is thinking…</p>;
};
```

Render it inside `Channel`. `AIStateIndicator` and the composer's stop button read the same state. [Drive the AI indicator from events](https://getstream.io/chat/docs/sdk/react/v15/features/ai/ai-integrations-overview/#drive-the-ai-indicator-from-events) explains which state follows which event, and [Let users stop the response](https://getstream.io/chat/docs/sdk/react/v15/features/ai/ai-integrations-overview/#let-users-stop-the-response) builds a stop button on it.

## useMessageTextStreaming

Types a text out a few letters at a time, the way an AI reply appears as it streams in.

```ts
useMessageTextStreaming(params: UseMessageTextStreamingProps): {
  readonly skipAnimation: () => void;
  readonly streamedMessageText: string;
}
```

The params:

- `text`: the full text so far, such as the message's `text`. Required.
- `streamingLetterIntervalMs`: the time between two steps, `30` ms by default.
- `renderingLetterCount`: the letters added at each step, `2` by default.

- **Returns:**
  - `streamedMessageText`: the part of `text` typed out so far.
  - `skipAnimation()`: shows the whole `text` at once.
- **Reads:** no store and no context. It runs its own timer.
- **Re-renders:** at each step until it reaches the end of `text`, and when `text` changes.
- **Needs:** nothing.

How the typing follows `text`:

1. The text there is when the component mounts shows at once. Only text added later is typed out.
2. When `text` grows, typing goes on from where it was.
3. When `text` gets shorter than what's on screen, the old text stays until `skipAnimation()` is called or the component mounts again.

`StreamedMessageText` renders AI messages with it: the ones `Chat`'s `isMessageAIGenerated` marks. It also calls `skipAnimation()` when an `ai_indicator.stop` event reaches the channel. `StreamingMessage`, from `stream-chat-react/ai-components`, uses the hook too, and names the interval `letterIntervalMs`.

To change only the speed, give the default component other values: `<StreamedMessageText {...props} streamingLetterIntervalMs={15} />`. To render the text your own way, call the hook in a `StreamedMessageText` of your own:

```tsx
import type { PropsWithChildren } from "react";
import {
  MessageText,
  useMessageContext,
  useMessageTextStreaming,
  WithComponents,
} from "stream-chat-react";
import type { StreamedMessageTextProps } from "stream-chat-react";

const TypedMessageText = ({
  message: messageFromProps,
  renderText,
}: StreamedMessageTextProps) => {
  const { message: messageFromContext } = useMessageContext();
  const message = messageFromProps ?? messageFromContext;
  const text = message.text ?? "";
  const { skipAnimation, streamedMessageText } = useMessageTextStreaming({
    renderingLetterCount: 4,
    streamingLetterIntervalMs: 15,
    text,
  });

  return (
    <div className="typed-message">
      <MessageText
        message={{ ...message, text: streamedMessageText }}
        renderText={renderText}
      />
      {streamedMessageText.length < text.length && (
        <button onClick={skipAnimation} type="button">
          Show all
        </button>
      )}
    </div>
  );
};

const overrides = { StreamedMessageText: TypedMessageText };

export const WithTypedAIMessages = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);
```

Show all appears while the text is still being typed out, and shows the rest of it at once.

## useSpeechToText

Turns speech into text with the browser's Web Speech API. Import it from `stream-chat-react/ai-components`.

```ts
useSpeechToText(options?: UseSpeechToTextOptions): {
  readonly isListening: boolean;
  readonly isSupported: boolean;
  readonly startListening: () => void;
  readonly stopListening: () => void;
}
```

The options, each optional:

- `onTranscript(text)`: called with the transcript each time the browser reports a result. With `interimResults`, that includes the words so far while the user speaks.
- `onError(message)`: called with an English description of the error, such as "No speech detected. Please try again."
- `lang`: the language to recognize, `'en-US'` by default.
- `interimResults`: report partial results, `true` by default.
- `continuous`: keep listening after the user stops speaking, `false` by default.
- `maxAlternatives`: the most alternatives the browser returns for each result, `1` by default. The hook uses the first.

- **Returns:**
  - `isSupported`: whether the browser has speech recognition. Always `false` during server rendering.
  - `isListening`: whether recognition is running.
  - `startListening()`: starts listening. It throws when `isSupported` is `false`, and calls `onError` when the browser refuses to start.
  - `stopListening()`: stops listening.
- **Reads:** no store and no context. It holds one recognizer, created again when `lang`, `interimResults`, `continuous` or `maxAlternatives` changes.
- **Re-renders:** when listening starts or stops.
- **Needs:** nothing.

`onTranscript` and `onError` may be inline functions: the hook always calls the latest ones. `AIMessageComposer.SpeechToTextButton` is built on this hook. A dictation button for the default composer replaces its text with the transcript:

```tsx
import { useMessageComposerController } from "stream-chat-react";
import { useSpeechToText } from "stream-chat-react/ai-components";

export const DictationButton = () => {
  const messageComposer = useMessageComposerController();
  const { isListening, isSupported, startListening, stopListening } =
    useSpeechToText({
      onTranscript: (text) => messageComposer.textComposer.setText(text),
    });

  if (!isSupported) return null;

  return (
    <button
      aria-pressed={isListening}
      onClick={isListening ? stopListening : startListening}
      type="button"
    >
      {isListening ? "Stop dictation" : "Dictate"}
    </button>
  );
};
```

With server rendering, render the button only after the component has mounted, as `SpeechToTextButton` does: `isSupported` is `false` on the server and can be `true` in the browser.

## Best practices

- Pass the channel to `useAIState`. It doesn't read one from context.
- Type out only the messages `isMessageAIGenerated` marks, as `MessageUI` does.
- Give a component that types out text a new `key` when it shows another message. The hook keeps the old text when `text` gets shorter.
- Call `skipAnimation()` when the response stops, as `StreamedMessageText` does on `ai_indicator.stop`.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v15/reference/hooks/ai-hooks/](https://getstream.io/chat/docs/sdk/react/v15/reference/hooks/ai-hooks/).