# SDK Integration

AI components work with the React Chat SDK out of the box.

## StreamedMessageText

`StreamedMessageText` ships with the SDK. It renders instead of `MessageText` when the [`isMessageAIGenerated` function](/chat/docs/sdk/react/v13/components/core-components/chat/#ismessageaigenerated) marks a message as AI-generated.

It displays the message with a typewriter animation and handles streaming updates for you.

Override it via the `StreamedMessageText` prop on `Channel`.

### Example usage

```tsx
const isMessageAIGenerated = (message) => !!message.ai_generated;

const App = ({ children }) => (
  <Chat client={client} isMessageAIGenerated={isMessageAIGenerated}>
    {children}
  </Chat>
);
```

The example above renders `StreamedMessageText` for messages where `ai_generated` is `true`.

## AIStateIndicator

`AIStateIndicator` isn’t rendered by default. Render it as a child of `Channel`. It shows when the AI state is one of:

- `AI_STATE_GENERATING`
- `AI_STATE_THINKING`

## Best Practices

- Use `isMessageAIGenerated` to keep AI rendering consistent across the app.
- Show `AIStateIndicator` only for active generation states.
- Provide a stop button to let users cancel long responses.
- Keep streaming message UI compatible with default message layout.
- Validate attachments and model inputs before submission.

### Example usage

```tsx
const isMessageAIGenerated = (message) => !!message.ai_generated;

const App = () => (
  <Chat client={client} isMessageAIGenerated={isMessageAIGenerated}>
    <Channel channel={channel}>
      <MessageList />
      <AIStateIndicator />
      <MessageInput />
    </Channel>
  </Chat>
);
```

This shows the indicator above `MessageInput` when the AI state matches one of the states above.

![AI Indicator Example](@chat-sdk/react/v13/_assets/ai-indicator-example.png)

## StopAIGenerationButton

`StopAIGenerationButton` lets users stop a response mid-stream. It renders instead of the `SendMessage` button when the AI state is:

- `AI_STATE_GENERATING`
- `AI_STATE_THINKING`

Override it via `Channel`’s `StopAIGenerationButton` prop.

<admonition type="note">

If you want to prevent rendering of the component altogether you can set the `StopAIGenerationButton` to `null`.

</admonition>

### `StreamingMessage`

Integrating `StreamingMessage` is straightforward. The SDK uses `isMessageAIGenerated` to determine when to render its internal streaming message. If you consider a message AI-generated when `message.ai_generated === true`, your factory function could look like this:

```tsx
const isMessageAIGenerated = (message: LocalMessage) => !!message.ai_generated;
```

Then, provide your custom streaming message:

```tsx
import { StreamingMessage } from "@stream-io/chat-react-ai";

const CustomStreamingMessage = () => {
  const { message } = useMessageContext();
  return <StreamingMessage text={message.text ?? ""} />;
};

// ...

<Channel {...otherChannelProps} StreamedMessageText={CustomStreamingMessage} />;
```

This renders messages using `@stream-io/chat-react-ai`’s `StreamingMessage` instead of the default implementation.

Because this component still renders inside [`MessageSimple`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Message/MessageSimple.tsx), you’ll also see other UI (reactions, read status, etc.). If that doesn’t suit your needs, build a custom message component while keeping the default styles:

```tsx
import {
  type MessageUIComponentProps,
  useMessageContext,
  MessageSimple,
  Attachment,
  MessageErrorIcon,
  Channel,
} from "stream-chat-react";

import { StreamingMessage } from "@stream-io/chat-react-ai";

const CustomMessage = (props: MessageUIComponentProps) => {
  const { message, isMyMessage, handleAction } = useMessageContext();

  const attachments = message.attachments ?? [];
  const hasAttachments = attachments.length > 0;

  const rootClassName = clsx(
    "str-chat__message str-chat__message-simple",
    `str-chat__message--${message.type}`,
    `str-chat__message--${message.status}`,
    {
      "str-chat__message--me": isMyMessage(),
      "str-chat__message--other": !isMyMessage(),
      "str-chat__message--has-attachment": hasAttachments,
    },
  );

  if (!isMessageAIGenerated(message)) return <MessageSimple {...props} />;

  return (
    <div className={rootClassName}>
      <div className="str-chat__message-inner" data-testid="message-inner">
        <div className="str-chat__message-bubble">
          {hasAttachments && (
            <Attachment
              actionHandler={handleAction}
              attachments={attachments}
            />
          )}

          <StreamingMessage text={message?.text || ""} />

          <MessageErrorIcon />
        </div>
      </div>
    </div>
  );
};

// ...

<Channel {...otherChannelProps} Message={CustomMessage} />;
```

### `AIMessageComposer`

To use `AIMessageComposer`, wire it to the existing `MessageComposer`:

```tsx
import {
  useMessageComposer,
  useChannelActionContext,
  useAttachmentsForPreview,
} from "stream-chat-react";
import { AIMessageComposer } from "@stream-io/chat-react-ai";

const CustomMessageComposer = () => {
  const composer = useMessageComposer();
  const { sendMessage } = useChannelActionContext();
  const { attachments } = useAttachmentsForPreview();

  return (
    <AIMessageComposer
      onChange={(event) => {
        const input = event.currentTarget.elements.namedItem(
          "attachments",
        ) as HTMLInputElement | null;
        const files = input?.files ?? null;
        if (files) composer.attachmentManager.uploadFiles(files);
      }}
      onSubmit={async (event) => {
        event.preventDefault();

        const target = event.currentTarget;
        const formData = new FormData(target);

        const message = formData.get("message");
        composer.textComposer.setText(message as string);

        const composedData = await composer.compose();

        if (!composedData) return;

        target.reset();
        composer.clear();

        await sendMessage(composedData);
      }}
    >
      <AIMessageComposer.AttachmentPreview>
        {attachments.map((attachment) => (
          <AIMessageComposer.AttachmentPreview.Item
            key={attachment.localMetadata.id}
            file={attachment.localMetadata.file}
            state={attachment.localMetadata.uploadState}
            imagePreviewSource={attachment.localMetadata.previewUri as string}
            onDelete={() => {
              composer.attachmentManager.removeAttachments([
                attachment.localMetadata.id,
              ]);
            }}
            onRetry={() => {
              composer.attachmentManager.uploadAttachment(attachment);
            }}
          />
        ))}
      </AIMessageComposer.AttachmentPreview>
      <AIMessageComposer.TextInput />
      <AIMessageComposer.FileInput />
      <AIMessageComposer.SpeechToTextButton />
      <AIMessageComposer.ModelSelect />
      <AIMessageComposer.SubmitButton />
    </AIMessageComposer>
  );
};

//...

<MessageInput Input={CustomMessageComposer} />;
```

You can pick and choose which input components to render, and wrap them to customize layout. The inputs are intentionally minimal with helpful defaults. `TextInput`, `FileInput`, and `SpeechToTextButton` manage internal state, which you can access via `useText` or `useAttachments`.


---

This page was last updated at 2026-04-21T07:55:48.103Z.

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