# Composer

`AIMessageComposer` is where the user writes to your agent. It's a form with a text input that grows with its lines, file attachments with previews, dictation through the browser's speech recognition, a menu of chat options such as "Think harder", and a model picker. You put it together from its parts, and it hands you what the user wrote when they send it:

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

export const Composer = () => (
  <AIMessageComposer onSubmit={handleSubmit}>
    <AIMessageComposer.AttachmentPreview />
    <AIMessageComposer.TextInput />
    <div className="composer-actions">
      <AIMessageComposer.FileInput name="attachments" />
      <AIMessageComposer.SpeechToTextButton />
      <AIMessageComposer.SubmitButton />
    </div>
  </AIMessageComposer>
);
```

```css
.composer-actions {
  display: flex;
  align-items: center;
  gap: 0.25rem;
}

/* The send button, last in the row, goes to the far end. */
.composer-actions > :last-child {
  margin-inline-start: auto;
}
```

>
> **Info:** `AIMessageComposer` is part of the `stream-chat-react/ai-components` entry point. See the [Quickstart](https://getstream.io/chat/docs/sdk/react/ai-components/) to install it and load its stylesheet.
>

The parts are static properties of `AIMessageComposer`, not separate exports, and each reads the composer's state from it, so render them inside it. Lay them out with your own elements around them as you like. The form stacks what's inside it, with the text field growing to fill it, so give the buttons a row of their own, as above. The form also grows to fill a flex container it's placed in, so put it in an element of its own when it sits under a conversation, and render it inside an element with the `str-chat` class, the SDK's theme scope, for the SDK's fonts and colors.

## Send what the user writes

`AIMessageComposer` renders a `form`, and takes every prop of one. It keeps the text and the picked files in a store of its own, and these props on top:

| Prop                       | Description                                                                                                                      | Default                                              |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `onSubmit`                 | Called when the user sends: with the submit button, or Enter in the text input.                                                  | -                                                    |
| `onChange`                 | Called on every change in the form, before the composer reads it.                                                                | -                                                    |
| `disabled`                 | Disables every part of the composer.                                                                                             | -                                                    |
| `nameMapping`              | The names of the inputs the composer reads the text and the files from.                                                          | `{ message: "message", attachments: "attachments" }` |
| `resetAttachmentsOnSelect` | Clears the file input after each selection, so the same file can be picked again. The picked files stay in the composer's store. | `true`                                               |

The text input is named `message` already. `FileInput` has no name of its own, so give it `name="attachments"` for the composer to pick up its files, or another name that `nameMapping` maps.

Since the file input is cleared after each selection, the form's data on submit holds the text, but not the files. Keep the files as the user picks them, in `onChange`, and show them in `AttachmentPreview`:

```tsx
import { type FormEvent, useState } from "react";
import { AIMessageComposer } from "stream-chat-react/ai-components";

type Picked = { file: File; id: string };

export const Composer = ({
  onSend,
}: {
  onSend: (text: string, files: File[]) => void;
}) => {
  const [picked, setPicked] = useState<Picked[]>([]);

  const handleChange = (event: FormEvent<HTMLFormElement>) => {
    const input = event.target;
    if (input instanceof HTMLInputElement && input.name === "attachments") {
      const files = Array.from(input.files ?? []);
      setPicked((current) => [
        ...current,
        ...files.map((file) => ({ file, id: crypto.randomUUID() })),
      ]);
    }
  };

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const text = new FormData(event.currentTarget).get("message");
    onSend(
      typeof text === "string" ? text : "",
      picked.map(({ file }) => file),
    );
    event.currentTarget.reset();
    setPicked([]);
  };

  return (
    <AIMessageComposer onChange={handleChange} onSubmit={handleSubmit}>
      <AIMessageComposer.AttachmentPreview>
        {picked.map(({ file, id }) => (
          <AIMessageComposer.AttachmentPreview.Item
            file={file}
            key={id}
            onDelete={() =>
              setPicked((current) => current.filter((item) => item.id !== id))
            }
          />
        ))}
      </AIMessageComposer.AttachmentPreview>
      <AIMessageComposer.TextInput />
      <div className="composer-actions">
        <AIMessageComposer.FileInput name="attachments" />
        <AIMessageComposer.SubmitButton />
      </div>
    </AIMessageComposer>
  );
};
```

Resetting the form clears the composer's text and files, and keeps it disabled if `disabled` says so.

To send through a Stream Chat channel, with its uploads and its stop button, render the composer as the channel's composer, as [SDK integration](https://getstream.io/chat/docs/sdk/react/ai-components/sdk-integration/#with-aimessagecomposer) shows.

## Control it from your code

Inside the composer, these hooks read and change its state:

| Hook                            | Returns                                                                                                              |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `useText()`                     | `text`, and `setText(text)` to replace it.                                                                           |
| `useAttachments()`              | `attachments`, each a `{ id, file, meta? }`, with `removeAttachment(idOrFile)` and `updateAttachments(ids, update)`. |
| `useIsDisabled()`               | `disabled`, and `setDisabled(disabled)`.                                                                             |
| `useAIMessageComposerContext()` | The composer's store, an `AIMessageComposerStore` in a `StateStore`.                                                 |

They're exported from `stream-chat-react/ai-components`. Outside a composer, they read a store no composer uses. For example, a button that fills in a prompt:

```tsx
import { Button } from "stream-chat-react";
import { AIMessageComposer, useText } from "stream-chat-react/ai-components";

const PromptButton = ({ prompt }: { prompt: string }) => {
  const { setText } = useText();
  return (
    <Button
      appearance="outline"
      onClick={() => setText(prompt)}
      size="sm"
      type="button"
      variant="secondary"
    >
      {prompt}
    </Button>
  );
};

<AIMessageComposer onSubmit={handleSubmit}>
  <AIMessageComposer.TextInput />
  <div className="composer-actions">
    <PromptButton prompt="Summarize this thread" />
    <AIMessageComposer.SubmitButton />
  </div>
</AIMessageComposer>;
```

### Chat options

`AIMessageComposer.ChatOptions` adds a menu of ways to ask, such as **Think harder**, and shows the one picked as a chip the user can remove. With a `name`, the picked option's `id` goes with the form's data:

```tsx
import {
  AIMessageComposer,
  type AIChatOption,
} from "stream-chat-react/ai-components";

const thinkHarder: AIChatOption = {
  id: "think_harder",
  title: "Think harder",
  description: "Takes longer, for harder questions",
  shortTitle: "Think",
};

export const Composer = () => (
  <AIMessageComposer onSubmit={handleSubmit}>
    <AIMessageComposer.TextInput />
    <div className="composer-actions">
      <AIMessageComposer.FileInput name="attachments" />
      <AIMessageComposer.ChatOptions name="option" options={[thinkHarder]} />
      <AIMessageComposer.SubmitButton />
    </div>
  </AIMessageComposer>
);
```

On submit, `new FormData(event.currentTarget).get("option")` is `"think_harder"`, or `""` when nothing is picked.

| Prop                | Description                                                                                                            | Default        |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------- |
| `options`           | The options: an `id` and a `title`, and an optional `description`, `icon`, `shortTitle` for the chip and `customData`. | -              |
| `name`              | Submits the picked option's `id` with the form under this name.                                                        | -              |
| `onSelectedChange`  | Called with the option the user picks, or `null` when they remove it.                                                  | -              |
| `selectedId`        | The picked option's `id`, when you control it.                                                                         | -              |
| `defaultSelectedId` | The option picked at first, when you don't.                                                                            | `null`         |
| `icon`              | The menu button's icon.                                                                                                | A plus         |
| `disabled`          | Disables the menu and the chip.                                                                                        | The composer's |

Picking the option that's already picked removes it. `ChatOptionsList` and `SelectedChatOptionChip`, the menu's items and the chip, are exported too, to build a menu of your own.

## Stop the reply

`AIMessageComposer` has no stop button. Add one of your own while the agent answers. With a Stream Chat channel, read the channel's AI state with `useAIState`, and stop the reply with `channel.stopAIResponse()`, which sends `ai_indicator.stop` to the channel:

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

const activeStates: AIState[] = [
  AIStates.Thinking,
  AIStates.ExternalSources,
  AIStates.Generating,
];

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

  if (!activeStates.includes(aiState)) return null;

  return (
    <Button
      appearance="outline"
      onClick={() => channel.stopAIResponse()}
      size="sm"
      type="button"
      variant="secondary"
    >
      Stop
    </Button>
  );
};
```

Render it in the composer's row of buttons, in place of the send button while it shows, and the composer inside `Channel`. `type="button"` keeps the click from submitting the form. The button doesn't hide itself on click: it goes once `ai_indicator.stop`, or the backend's `ai_indicator.clear`, reaches the channel and moves `aiState` out of the active states.

The default `MessageComposer` of `stream-chat-react` has a stop button of its own, `StopAIGenerationButton`. It shows only while `aiState` is `AI_STATE_THINKING` or `AI_STATE_GENERATING`, not during `AI_STATE_EXTERNAL_SOURCES`, so the send button comes back while a tool runs. Replacing the composer's UI with `AIMessageComposer` removes it. [SDK integration](https://getstream.io/chat/docs/sdk/react/ai-components/sdk-integration/#stopaigenerationbutton) shows how to restyle or remove it.

[Stop the reply](https://getstream.io/chat/docs/sdk/react/ai-components/conversation-state/#stop-the-reply) covers what the backend does with the event.

## Dictation

`AIMessageComposer.SpeechToTextButton` turns speech into text with the browser's Web Speech API, and writes the transcript into the composer's text input. Its `options` go to the speech recognizer:

```tsx
<AIMessageComposer onSubmit={handleSubmit}>
  <AIMessageComposer.TextInput />
  <div className="composer-actions">
    <AIMessageComposer.SpeechToTextButton
      options={{ lang: "en-US", continuous: false, interimResults: true }}
    />
    <AIMessageComposer.SubmitButton />
  </div>
</AIMessageComposer>
```

| Option            | Description                                                                       | Default                 |
| ----------------- | --------------------------------------------------------------------------------- | ----------------------- |
| `lang`            | The language to recognize.                                                        | `"en-US"`               |
| `interimResults`  | Writes the words so far while the user speaks.                                    | `true`                  |
| `continuous`      | Keeps listening after the user stops speaking.                                    | `false`                 |
| `maxAlternatives` | The most alternatives the browser returns for each result. The first one is used. | `1`                     |
| `onTranscript`    | Called with the transcript, besides writing it into the input.                    | -                       |
| `onError`         | Called with an English description of what went wrong, in place of logging it.    | Logs to `console.error` |

The button takes the props of a `button` too. When clicked, the browser asks for the microphone, and the button shows as pressed (`aria-pressed`) while it listens. Clicking it again stops it.

The button renders nothing in browsers without the Web Speech API, such as Firefox. Support is only known in the browser, so with server rendering the button appears after hydration.

To dictate into an input of your own, use the [`useSpeechToText`](https://getstream.io/chat/docs/sdk/react/reference/hooks/ai-hooks/#usespeechtotext) hook the button is built on. It takes the same options and returns `isSupported`, `isListening`, `startListening` and `stopListening`.

To let the user talk to your agent instead, rather than dictate, see the [Voice agent](https://getstream.io/chat/docs/sdk/react/ai-components/use-cases/voice-agent/) use case.

## Customize the composer

Each part takes the props of the element it renders, so you can name it, style it or handle its events:

| Part                                       | Renders                                                                                                                                                                   |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AIMessageComposer.TextInput`              | A `textarea` named `message` that grows with its lines up to 200 px, then scrolls. Enter sends, and Shift+Enter starts a new line.                                        |
| `AIMessageComposer.FileInput`              | A round button that picks one or more files. The native file input stays focusable for keyboard users, and `labelProps` go to the round button.                           |
| `AIMessageComposer.SpeechToTextButton`     | The [dictation](#dictation) button.                                                                                                                                       |
| `AIMessageComposer.ChatOptions`            | The [chat options](#chat-options) menu and chip.                                                                                                                          |
| `AIMessageComposer.ModelSelect`            | A `select` of models. Pass your own `<option>` elements as `options`.                                                                                                     |
| `AIMessageComposer.SubmitButton`           | The send button. `active` highlights it, for example while files are picked.                                                                                              |
| `AIMessageComposer.AttachmentPreview`      | The picked files. Without children, it shows the composer's own files, each with a delete button, and nothing while there are none. With children, it shows them instead. |
| `AIMessageComposer.AttachmentPreview.Item` | One file: a thumbnail for an image, a card with the name and size for any other file, and a delete button.                                                                |

`ModelSelect` offers two OpenAI models by default, with values such as `openai|gpt-4o-mini`. Give it your own list and a `name`, so the pick goes with the form's data, and check on your backend that it's a model you offer:

```tsx
<AIMessageComposer.ModelSelect
  name="model"
  options={
    <>
      <option value="fast">Fast</option>
      <option value="smart">Smart</option>
    </>
  }
/>
```

An `AttachmentPreview.Item` takes the `file`, an `onDelete`, and optionally a `title` in place of the file's name, an `imagePreviewSource` for an image, and an upload `state`. With `state="failed"`, it shows a button that calls `onRetry`. The children of `AttachmentPreview` can also be a function of what `useAttachments()` returns.

## Styling and texts

The composer follows the SDK's theme in light and dark mode, and its colors come from the `--str-chat__ai-*` variables described in [Theming](https://getstream.io/chat/docs/sdk/react/ai-components/#theming): `border`, `surface-hover` and `surface-pressed` for the composer, and `button-bg`, `scrollbar-track` and `scrollbar-thumb` for the previews. Its class names start with `str-chat__ai-message-composer`, `str-chat__ai-attachment-preview` and `str-chat__ai-chat-options`.

It shows the SDK's icons, which you replace through `WithComponents` like any other icon. Its texts go through the SDK's translations, such as `aiComponents.messageComposer.textInput.placeholder` ("Ask a question..."), `aiComponents.messageComposer.speechToText.ariaLabel` and `aiComponents.chatOptions.chatOptions.ariaLabel`, which also name the buttons for screen readers. English is built in. To change or translate them, register your own dictionary, as described in [Localization](https://getstream.io/chat/docs/sdk/react/features/app-experience/localization/).

---

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