# SDK integration

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

The main `stream-chat-react-native` entry point has three parts for AI replies: `StreamingMessageView` types out a reply as it streams, `AITypingIndicatorView` shows what the LLM is doing, and the default composer's `StopMessageStreamingButton` lets the user stop it. This page shows how to turn each on, and how to use the components from `stream-chat-react-native/ai-components` in their place. The [AI integrations overview](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/ai-integrations-overview/) covers those components, and how the backend drives all of this. With Expo, import from `stream-chat-expo` and `stream-chat-expo/ai-components` instead.

For example, the user asks the support bot a question:

1. The bot posts an empty reply, marked `ai_generated`, and sends `ai_indicator.update` with `AI_STATE_THINKING`.
2. The client sets the channel's AI state to thinking. `AITypingIndicatorView` shows "Thinking...", and the composer's send button becomes the stop button.
3. The bot writes the answer into the reply, a few words at a time. `StreamingMessageView` types each new part out.
4. The bot stores the final text and sends `ai_indicator.clear`. The indicator goes away, and the send button comes back.

## Best practices

- Mark AI replies on the backend, and tell `Chat` how to recognize them with `isMessageAIGenerated`, so every message list renders them the same way.
- Show an indicator only while the LLM is busy: thinking, checking sources or generating.
- Keep a way to stop a long reply. The default composer has one, and a composer of your own needs its own button.
- Keep the default message layout around streamed text unless you need a message component of your own.
- Validate attachments and model choices on the backend, not only in the composer.

## StreamingMessageView

The message bubble renders `StreamingMessageView` in place of the regular message text for every message that `isMessageAIGenerated` returns `true` for. It shows the text the message has when it mounts, then types out whatever the text grows by. A reply already stored doesn't replay the animation when the user scrolls back to it. It renders the text with the same Markdown rules as any other message.

Pass `isMessageAIGenerated` to `Chat`. AI replies carry the custom field `ai_generated`, which the client puts under `message.custom`:

```tsx title="App.tsx"
import type { PropsWithChildren } from "react";
import type { LocalMessage, StreamChat } from "stream-chat";
import { Chat, OverlayProvider } from "stream-chat-react-native";

// Declared outside the component, so Chat gets the same function on every render.
const isMessageAIGenerated = (message: LocalMessage) =>
  !!message.custom?.ai_generated;

export const App = ({
  children,
  client,
}: PropsWithChildren<{ client: StreamChat }>) => (
  <OverlayProvider>
    <Chat client={client} isMessageAIGenerated={isMessageAIGenerated}>
      {children}
    </Chat>
  </OverlayProvider>
);
```

`Chat` puts the function in its context, and every message reads it from there. A new function on every render would re-render every message on screen. For TypeScript, declare the field, optional, in `CustomMessageData`:

```ts title="stream-custom-data.d.ts"
import "stream-chat";

declare module "stream-chat" {
  interface CustomMessageData {
    ai_generated?: boolean;
    generating?: boolean;
  }
}
```

[Mark assistant messages with `ai_generated`](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/ai-integrations-overview/#mark-assistant-messages-with-ai_generated) explains both fields and how the backend sets them. `MessageFooter` also hides the "Edited" label on these messages, since every streamed update edits them.

The bubble places the AI text through the `'ai_text'` entry of `messageContentOrder`, and the `'text'` entry skips AI messages. The default order has both. If you pass your own `messageContentOrder` to `Channel` or `Thread`, include `'ai_text'`, or AI replies render without text.

The default component takes `letterInterval` (milliseconds between ticks, `0` by default) and `renderingLetterCount` (letters per tick, `2` by default), but the bubble renders it without props. To change the speed, override it with a component that renders `StreamingMessageView` with your values, or that builds on the `useStreamingMessage({ text, letterInterval, renderingLetterCount })` hook, which returns `streamedMessageText`.

### Using the AI components' StreamingMessageView instead

Replace `StreamingMessageView` through `WithComponents`. This one renders the text with `StreamingMessageView` from `stream-chat-react-native/ai-components`, which adds code highlighting with a copy button, tables and charts:

```tsx title="WithMarkdownReplies.tsx"
import type { PropsWithChildren } from "react";
import { StyleSheet, View } from "react-native";
import { useMessageContext, WithComponents } from "stream-chat-react-native";
import {
  StreamingMessageView,
  StreamTheme,
} from "stream-chat-react-native/ai-components";

const MarkdownStreamingMessageView = () => {
  const { message } = useMessageContext();

  return (
    <View style={styles.text}>
      <StreamingMessageView text={message.text ?? ""} />
    </View>
  );
};

const overrides = { StreamingMessageView: MarkdownStreamingMessageView };

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

const styles = StyleSheet.create({
  text: { paddingHorizontal: 12, paddingVertical: 8 },
});
```

The override receives no props: it reads the message from `useMessageContext()`. The components from the AI entry point throw outside `StreamTheme`, so keep one above them, as here, or higher in the app. [Usage](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/ai-integrations-overview/#usage) lists what the entry point needs installed.

### A message component of your own

The override above still renders inside the default message, with its avatar, reactions and footer. To render AI replies differently, replace `MessageItemView`, and keep the default for every other message:

```tsx title="WithAIMessages.tsx"
import type { PropsWithChildren } from "react";
import { StyleSheet, View } from "react-native";
import {
  MessageItemView,
  type MessageItemViewProps,
  useChatContext,
  useMessageContext,
  WithComponents,
} from "stream-chat-react-native";
import {
  StreamingMessageView,
  StreamTheme,
} from "stream-chat-react-native/ai-components";

const AIAwareMessageItemView = (props: MessageItemViewProps) => {
  const { isMessageAIGenerated } = useChatContext();
  const { message } = useMessageContext();

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

  // A full-width reply without a bubble.
  return (
    <View style={styles.aiReply}>
      <StreamingMessageView text={message.text ?? ""} />
    </View>
  );
};

const overrides = { MessageItemView: AIAwareMessageItemView };

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

const styles = StyleSheet.create({
  aiReply: { paddingHorizontal: 16, paddingVertical: 8, width: "100%" },
});
```

The AI reply then renders only its text: it has no attachments, reactions or long-press menu. [Overriding components](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/overriding-components/) explains the override keys, and [Message list UI](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/message-list-ui/#replacing-parts) lists the parts of a message.

## AITypingIndicatorView

`AITypingIndicatorView` shows "Thinking..." while the channel's AI state is `AI_STATE_THINKING`, and "Generating..." while it's `AI_STATE_GENERATING`. In every other state it renders nothing.

No SDK component renders it, so render it yourself, inside `Channel`. Between the message list and the composer, it sits just above where the user types:

```tsx title="AssistantChannel.tsx"
import type { Channel as StreamChannel } from "stream-chat";
import {
  AITypingIndicatorView,
  Channel,
  MessageComposer,
  MessageList,
} from "stream-chat-react-native";

export const AssistantChannel = ({ channel }: { channel: StreamChannel }) => (
  <Channel channel={channel}>
    <MessageList />
    <AITypingIndicatorView />
    <MessageComposer />
  </Channel>
);
```

It reads the channel from `Channel`. Its `channel` prop shows another channel's state instead, but it still has to render inside a `Channel` or a `Thread`. To show other states, such as `AI_STATE_EXTERNAL_SOURCES`, read the state with `useAIState(channel)`, as [Drive the AI indicator from events](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/ai-integrations-overview/#drive-the-ai-indicator-from-events) shows.

The two texts are the translation keys `aiTypingIndicator.thinking.label` and `aiTypingIndicator.generating.label`, described in [Localization](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/localization/). The `aiTypingIndicatorView` key of the theme styles its `container` and `text`, as described in [Theming](https://getstream.io/chat/docs/sdk/react-native/v10/ui/customizing/theming/). With accessibility on, and `announceTypingIndicator` set in the `accessibility` config of `OverlayProvider`, screen readers announce the state when it changes. [Accessibility](https://getstream.io/chat/docs/sdk/react-native/v10/features/app-experience/accessibility/) describes the config.

## StopMessageStreamingButton

While the channel's AI state is `AI_STATE_THINKING` or `AI_STATE_GENERATING`, the composer shows `StopMessageStreamingButton` in place of the send button. It also takes the place of the edit button, the cooldown timer and the record button, as [The send button and its replacements](https://getstream.io/chat/docs/sdk/react-native/v10/ui/components/message-composer-ui/#the-send-button-and-its-replacements) lists. Pressing it calls `Channel.stopAIResponse()`, which sends `ai_indicator.stop` to the channel. The backend stops the reply when it gets the event.

Replace it through `WithComponents`. Your component receives the `onPress` that stops the reply:

```tsx title="WithStopButton.tsx"
import type { PropsWithChildren } from "react";
import { Pressable, StyleSheet, Text } from "react-native";
import {
  type StopMessageStreamingButtonProps,
  WithComponents,
} from "stream-chat-react-native";

const StopButton = ({ onPress }: StopMessageStreamingButtonProps) => (
  <Pressable
    accessibilityLabel="Stop the reply"
    accessibilityRole="button"
    onPress={onPress}
    style={styles.button}
  >
    <Text>Stop</Text>
  </Pressable>
);

const overrides = { StopMessageStreamingButton: StopButton };

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

const styles = StyleSheet.create({
  button: { padding: 8 },
});
```

The default button draws the `Stop` icon, which you can replace through the `icons` key of the overrides instead. The `stopMessageStreamingButton` and `stopMessageStreamingIcon` keys under `messageComposer` in the theme style it.

To remove the button, set the key to `undefined`. The composer then shows its usual buttons while the reply is generated, so the user can send a message, and nothing in the composer stops the reply:

```tsx title="WithoutStopButton.tsx"
import type { PropsWithChildren } from "react";
import { WithComponents } from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";

// Declared once: WithComponents reads its overrides when it mounts.
const overrides: ComponentOverrides = { StopMessageStreamingButton: undefined };

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

The overrides are spread over the defaults, so a key set to `undefined` replaces the default rather than falling back to it. `null` isn't accepted: the key is typed as a component.

[Let users stop the response](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/ai-integrations-overview/#let-users-stop-the-response) lists when the default button doesn't show, and what the backend does with the event.

### With ComposerView

To use `ComposerView` as the composer, render it inside `Channel` in place of `MessageComposer`, and send what it collects through the channel's message composer. Sending and uploads then work as with the default composer. `ComposerView` keeps the text in its own state, and the text reaches the message composer only on send, so drafts, mentions and commands don't apply to it. `ComposerView` takes the stop button as props, so this one passes the AI state:

```tsx title="AIComposer.tsx"
import { AIStates, type AIState } from "stream-chat";
import {
  useAIState,
  useChannel,
  useMessageComposer,
  useMessageInputContext,
  useStableCallback,
} from "stream-chat-react-native";
import {
  ComposerView,
  type ComposerViewProps,
} from "stream-chat-react-native/ai-components";

type SendData = Parameters<ComposerViewProps["onSendMessage"]>[0];

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

// Render inside StreamTheme, and inside Channel in place of MessageComposer.
export const AIComposer = () => {
  const channel = useChannel();
  const { aiState } = useAIState(channel);
  const messageComposer = useMessageComposer();
  const { sendMessage } = useMessageInputContext();

  const onSendMessage = useStableCallback(
    async ({ attachments, text }: SendData) => {
      messageComposer.textComposer.setText(text);

      if (attachments?.length) {
        await messageComposer.attachmentManager.uploadFiles(
          attachments.map((asset) => ({
            ...asset,
            // The pickers can leave these out, and an upload needs them.
            name: asset.name ?? asset.uri.split("/").pop() ?? "image.jpg",
            size: asset.size ?? 0,
            type: asset.type ?? "image/jpeg",
          })),
        );
      }

      await sendMessage();
    },
  );

  const stopGenerating = useStableCallback(() => channel.stopAIResponse());

  return (
    <ComposerView
      isGenerating={activeStates.includes(aiState)}
      onSendMessage={onSendMessage}
      stopGenerating={stopGenerating}
    />
  );
};
```

How it fits together:

- **Text.** On send, the component copies the text into the message composer with `TextComposer.setText()`, then calls `sendMessage()` from `useMessageInputContext()`. That is the same call the default composer makes: it sends with `MessageComposer.send()`, and stops with an alert when the text has a link and the channel doesn't allow links.
- **Attachments.** The photos picked in `ComposerView` are uploaded through the message composer's `attachmentManager`, and the message is sent once the uploads finish. To send the message at once and let the uploads finish after it, see [Sending with pending uploads](https://getstream.io/chat/docs/sdk/react-native/v10/features/messaging/sending-with-pending-uploads/).
- **Stopping.** `isGenerating` swaps the send button for the stop button. This one also includes `AI_STATE_EXTERNAL_SOURCES`, so the button stays while a tool call runs.

`ComposerView` clears its input before it calls `onSendMessage`. The [sample app](https://github.com/GetStream/stream-chat-react-native/blob/V10/examples/ExpoChatGPTSample/screens/ChatContent.tsx) renders its channel screen this way. [Usage](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/ai-integrations-overview/#usage) lists what the entry point needs installed.

## VoiceGlow

`VoiceGlow`, from the AI components, can show what the channel's agent is doing, for example in a voice mode or next to the composer. Map the channel's AI state, from `useAIState`, to the glow's `state`:

```tsx title="AgentGlow.tsx"
import { AIStates } from "stream-chat";
import { useAIState, useChannel } from "stream-chat-react-native";
import {
  type VoiceAgentState,
  VoiceGlow,
} from "stream-chat-react-native/ai-components";

const voiceStates: Record<string, VoiceAgentState> = {
  [AIStates.Thinking]: "thinking",
  [AIStates.ExternalSources]: "thinking",
  [AIStates.Generating]: "speaking",
};

// Render inside StreamTheme, and inside Channel.
export const AgentGlow = () => {
  const channel = useChannel();
  const { aiState } = useAIState(channel);

  return (
    <VoiceGlow
      state={voiceStates[aiState] ?? "idle"}
      style={{ width: 160, height: 160 }}
    />
  );
};
```

While the agent's reply is read aloud, set the state to `"speaking"`, and to `"listening"` while the user dictates, with [`useDictationAudioLevel`](https://getstream.io/chat/docs/sdk/react-native/v10/features/ai/ai-integrations-overview/#usedictationaudiolevel) as the `audioLevel` of a `VoiceWave`. The [ExpoChatGPTSample](https://github.com/GetStream/stream-chat-react-native/tree/V10/examples/ExpoChatGPTSample) has a complete hands-free voice mode built this way.

---

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