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 overview for the setup.

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.