Info:
This is documentation for Stream Chat React Native SDK v9, which is no longer actively maintained. For up-to-date documentation, see the latest version (v10).

SDK Integration

The AI components integrate directly with the React Native SDK. The examples import them from stream-chat-react-native/ai-components. With Expo, import them from stream-chat-expo/ai-components instead, and import everything else from stream-chat-expo. See the Quickstart for the setup.

This page shows the AI components inside the Chat SDK's Channel, with its MessageList and composer. For an assistant laid out by turn, with each reply's reasoning, tool calls and generative UI, use AIConversation instead, which doesn't need Channel.

Best Practices

  • Keep AI-specific rendering gated by a stable isMessageAIGenerated check.
  • Use the SDK composer APIs to preserve uploads, drafts, and optimistic UI.
  • Stream AI state through a single channel source to avoid conflicting UI signals.
  • Render typing indicators only for active AI states to reduce noise.
  • Ensure stopGenerating is always available when AI is producing output.

StreamingMessageView

The SDK exposes an isMessageAIGenerated factory that tells contexts when to render a streaming view.

Example: treat message.ai_generated === true as AI-generated.

In that case, our factory function would look something like this:

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

Then pass your custom StreamingMessageView using WithComponents:

import { StreamingMessageView } from "stream-chat-react-native/ai-components";
import { WithComponents } from "stream-chat-react-native";

const CustomStreamingMessageView = () => {
  const { message } = useMessageContext();
  return (
    <View style={{ width: "100%", paddingHorizontal: 16 }}>
      <StreamingMessageView text={message.text ?? ""} />
    </View>
  );
};

// ...

<WithComponents
  overrides={{ StreamingMessageView: CustomStreamingMessageView }}
>
  <Channel {...otherChannelProps} />
</WithComponents>;

This renders a full-width message using the AI StreamingMessageView instead of the default.

ComposerView

To use ComposerView, create a send resolver and use it instead of MessageComposer:

import { ComposerView } from "stream-chat-react-native/ai-components";

const CustomComposerView = () => {
  const messageComposer = useMessageComposer();
  const { sendMessage } = useMessageInputContext();
  const { channel } = useChannelContext();

  const { aiState } = useAIState(channel);

  const stopGenerating = useCallback(
    () => channel?.stopAIResponse(),
    [channel],
  );

  const isGenerating = [AIStates.Thinking, AIStates.Generating].includes(
    aiState,
  );

  const serializeToMessage = useStableCallback(
    async ({ text, attachments }: { text: string; attachments?: any[] }) => {
      messageComposer.textComposer.setText(text);
      if (attachments && attachments.length > 0) {
        const localAttachments = await Promise.all(
          attachments.map((a) =>
            messageComposer.attachmentManager.fileToLocalUploadAttachment(a),
          ),
        );
        messageComposer.attachmentManager.upsertAttachments(localAttachments);
      }

      await sendMessage();
    },
  );

  return (
    <ComposerView
      onSendMessage={serializeToMessage}
      isGenerating={isGenerating}
      stopGenerating={stopGenerating}
    />
  );
};

What this does:

  • Sets text and attachments on the composer state.
  • Calls sendMessage after the composer is updated.
  • Passes isGenerating and stopGenerating to control the stop button.

AITypingIndicatorView

You can render a thinking indicator below MessageList and above ComposerView:

import { AITypingIndicatorView } from "stream-chat-react-native/ai-components";

const CustomAIThinkingIndicatorView = () => {
  const { channel } = useChannelContext();
  const { aiState } = useAIState(channel);

  const allowedStates = {
    [AIStates.Thinking]: "Thinking about the question...",
    [AIStates.Generating]: "Generating a response...",
    [AIStates.ExternalSources]: "Checking external sources...",
  };

  if (aiState === AIStates.Idle || aiState === AIStates.Error) {
    return null;
  }

  return (
    <View
      style={{
        paddingHorizontal: 24,
        paddingVertical: 12,
      }}
    >
      <AITypingIndicatorView text={allowedStates[aiState]} />
    </View>
  );
};

// ...

<Channel {...channelProps}>
  <MessageList />
  <CustomAIThinkingIndicatorView />
  <CustomComposerView />
</Channel>;

VoiceGlow

VoiceGlow 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:

import {
  AIStates,
  useAIState,
  useChannelContext,
} 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",
};

const AgentGlow = () => {
  const { channel } = useChannelContext();
  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 as the audioLevel of a VoiceWave. The ExpoChatGPTSample has a complete hands-free voice mode built this way.