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
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.