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
isMessageAIGeneratedcheck. - 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
stopGeneratingis 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
textandattachmentson the composer state. - Calls
sendMessageafter the composer is updated. - Passes
isGeneratingandstopGeneratingto 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.