In this tutorial, we will demonstrate how easy it is to create an AI assistant for React Native using Stream Chat. As an example, we will leverage the StreamChat integration with Vercel's AI SDK; however, developers are free to use whichever LLM provider they like and still benefit from Stream's rich UI support for Markdown, tables, code samples, charts etc. To follow along with this tutorial, we recommend creating a free account and checking out our main React Native Chat SDK tutorial as a refresher.
Running the Backend
Before adding AI features to our iOS app, let's set up our node.js backend. The backend will expose two methods for starting and stopping an AI agent for a particular channel. If the agent is started, it listens to all new messages and sends them to the LLM provider of your choice. It provides the results by sending a message and updating its text.
The sample also supports sending different states of the typing indicator (for example, Thinking, Checking external sources, etc), client-side MCP tools, suggestions, summaries, memory with mem0 and much more.
You can find a working implementation of the backend here.
1. Install dependencies
1npm install @stream-io/chat-ai-sdk express cors dotenv
@stream-io/chat-ai-sdk brings the Agent, AgentManager, tool helpers, and the streaming logic. Express/cors/dotenv provide the basic HTTP server.
2. Configure Stream credentials
Create a .env file with:
STREAM_API_KEY=your_key
STREAM_API_SECRET=your_secret
OPENAI_API_KEY=your_open_api_key
ANTHROPIC_API_KEY=your_anthropic_api_key
XAI_API_KEY=your_xai_api_key
GOOGLE_API_KEY=your_google_api_key
MEM0_API_KEY=your_mem0_api_key
Apart from the Stream API key and secret, every other API key is optional. You would need to add at least one LLM service, for the AI to work.
Load it at the top of your entry file:
1import 'dotenv/config';
3. Bootstrap Express and the AgentManager
Add the following to your index.ts (or similar):
1234567891011121314151617181920212223242526import express from 'express'; import cors from 'cors'; import { AgentManager, AgentPlatform, createDefaultTools, } from '@stream-io/chat-ai-sdk'; const app = express(); app.use(express.json()); app.use(cors({ origin: '*' })); const buildAgentUserId = (channelId: string): string => `ai-bot-${channelId.replace(/!/g, '')}`; const agentManager = new AgentManager({ serverToolsFactory: () => createDefaultTools(), agentIdResolver: buildAgentUserId, }); const normalizeChannelId = (raw: string): string => { const trimmed = raw?.trim() ?? ''; if (!trimmed) return trimmed; const parts = trimmed.split(':'); return parts.length > 1 ? parts[1] : trimmed; };
AgentManager owns the agent cache, pending state, and inactivity cleanup. Each channel uses an ID pattern such as ai-bot-{channelId}.
4. Starting an Agent
Next, let's add the endpoint that will start the AI agent. First, we need to validate the payload, normalize the channel, and then ask the AgentManager to start or reuse the agent:
1234567891011121314151617181920212223242526272829303132333435app.post('/start-ai-agent', async (req, res) => { const { channel_id, channel_type = 'messaging', platform = AgentPlatform.ANTHROPIC, model, } = req.body; if (!channel_id) { res.status(400).json({ error: 'Missing channel_id' }); return; } const channelId = normalizeChannelId(channel_id); if (!channelId) { res.status(400).json({ error: 'Invalid channel_id' }); return; } try { await agentManager.startAgent({ userId: buildAgentUserId(channelId), channelId, channelType: channel_type, platform, model, }); res.json({ message: 'AI Agent started' }); } catch (error) { res.status(500).json({ error: 'Failed to start AI Agent', reason: (error as Error).message, }); } });
5. Stopping an Agent
To stop the agent and clean the cache, we can do the following:
1234567891011121314151617app.post('/stop-ai-agent', async (req, res) => { const channelId = normalizeChannelId(req.body?.channel_id ?? ''); if (!channelId) { res.status(400).json({ error: 'Invalid channel_id' }); return; } try { await agentManager.stopAgent(buildAgentUserId(channelId)); res.json({ message: 'AI Agent stopped' }); } catch (error) { res.status(500).json({ error: 'Failed to stop AI Agent', reason: (error as Error).message, }); } });
6. Register Client Side Tools
Next, we can expose an endpoint to allow clients to register MCP tools that can be handled client-side:
12345678910111213141516app.post('/register-tools', (req, res) => { const { channel_id, tools } = req.body ?? {}; if (typeof channel_id !== 'string' || !channel_id.trim()) { res.status(400).json({ error: 'Missing or invalid channel_id' }); return; } if (!Array.isArray(tools)) { res.status(400).json({ error: 'Missing or invalid tools array' }); return; } const channelId = normalizeChannelId(channel_id); agentManager.registerClientTools(channelId, tools); res.json({ message: 'Client tools registered', count: tools.length }); });
7. Start the server
Add the log when the server is started.
1234const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Server is running on <http://localhost>:${port}`); });
You can start the server by running:
npm start
React Native Integration
Next, let’s setup things on on the JS side. You can find a working implementation of this project here.
1. Project Setup
In order to follow this tutorial, we must ensure a minimum version of 8.10.0 of the Stream Chat React Native SDK. It also expects that a minimum version of React Native 0.76.0 is used, with new architecture enabled.
As a final note, this tutorial will be done with React Native Community CLI - however, feel free to replicate it with Expo as well, and the results will be the same.
First, we will create and set up a new React Native project using our SDK. Let's initialize a new React Native project:
12npx @react-native-community/cli@latest init RNChatAISample cd RNChatAISample
Next, let’s install our Chat SDK:
12yarn add stream-chat-react-native yarn add stream-chat
As well as its required dependencies:
1yarn add @react-native-community/netinfo react-native-fs react-native-gesture-handler react-native-reanimated react-native-worklets react-native-svg
In order to use our new AI SDK, we’ll install it as well including its missing dependencies:
1yarn add @stream-io/chat-react-native-ai victory-native @shopify/react-native-skia react-native-image-picker @react-native-clipboard/clipboard @babel/plugin-proposal-export-namespace-from
We will additionally need to make sure the required babel plugins are included in the babel.config.js file, like so:
1234567module.exports = { presets: ['module:@react-native/babel-preset'], plugins: [ '@babel/plugin-proposal-export-namespace-from', 'react-native-worklets/plugin', ], };
Make sure that react-native-worklets/plugin is always listed last.
Finally, let’s also add the required Android and iOS permissions to make sure we can use the full extent of our new AI features.
First, let’s add them for iOS within ios/RNChatAISample/Info.plist:
12345678<key>NSPhotoLibraryUsageDescription</key> <string>$(PRODUCT_NAME) would like access to your photo gallery to share an image in a message.</string> <key>NSCameraUsageDescription</key> <string>$(PRODUCT_NAME) would like to use your camera to share an image in a message.</string> <key>NSMicrophoneUsageDescription</key> <string>$(PRODUCT_NAME) would like to access your microphone to capture your voice.</string> <key>NSSpeechRecognitionUsageDescription</key> <string>$(PRODUCT_NAME) would like to access speech recognition to transcribe your voice.</string>
and then for Android within android/app/src/main/AndroidManifest.xml:
12<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
Finally, since we’ll need a navigation library you can install React Navigation as described in their documentation.
123yarn add @react-navigation/native yarn add react-native-screens react-native-safe-area-context yarn add @react-navigation/drawer
Finally, to install everything you may run:
12yarn install npx pod-install
at the root of your new project.
After all this, we can run the app by running yarn start —reset-cache to start Metro and running yarn run ios for iOS or yarn run android for Android and we should see the React Native welcome screen.
2. Backend Interaction APIs
Before we dive into implementing the actual chat screens, let’s create some utilities that will help us do HTTP requests towards our backend.
To do this, we’ll introduce two more files:
http/api.ts
12345678910111213141516171819export const post = async (url: string, data: unknown = {}) => { try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error(`An HTTP Error has occurred. Status: ${response.status}`); } return await response.json(); } catch (error) { console.error('Error:', error); } };
http/requests.ts
12345678import { post } from './api.ts'; export const startAI = async (channelId: string) => post('http://localhost:3000/start-ai-agent', { channel_id: channelId }); export const stopAI = async (channelId: string) => post('http://localhost:3000/stop-ai-agent', { channel_id: channelId }); export const summarize = async (text: string) => post('http://localhost:3000/summarize', { text });
3. Setting up the MessageList
Since we want to already begin talking to the LLM, let’s begin by building our main chat screen.
For this, we will use the Channel and MessageList components from stream-chat-react-native. We will also implement a DrawerNavigator to hold the navigation together.
To do this, let’s first create a couple of new files for clarity:
./chatConfig.ts
1234export const chatApiKey = 'zcgvnykxsfm8'; export const chatUserId = 'rn-chatgpt-demo-test-user'; export const chatUserToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoicm4tY2hhdGdwdC1kZW1vLXRlc3QtdXNlciJ9.cE-FOtn8jz7k4ddRJ1kCA7g456tTEwxXEn4adka1p2s'; export const chatUserName = 'Bob';
contexts/AppContext.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445import React, { PropsWithChildren, useMemo, useState } from 'react'; import { Channel, StreamChat } from 'stream-chat'; import { chatUserId } from '../chatConfig.ts'; export type AppContextValue = { channel: Channel | undefined; setChannel: (channel: Channel) => void; }; export const AppContext = React.createContext<AppContextValue>({ setChannel: () => {}, channel: undefined, }); const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-'; export const nanoid = (size: number = 21): string => { let id = ''; for (let i = 0; i < size; i++) { const r = Math.floor(Math.random() * ALPHABET.length); id += ALPHABET[r]; } return id; }; export const createChannel = (client: StreamChat) => client.channel('messaging', nanoid(), { members: [chatUserId], }); export const AppProvider = ({ client, children, }: PropsWithChildren<{ client: StreamChat }>) => { const [channel, setChannel] = useState<Channel>(() => createChannel(client)); const contextValue = useMemo(() => ({ channel, setChannel }), [channel]); return ( <AppContext.Provider value={contextValue}>{children}</AppContext.Provider> ); }; export const useAppContext = () => React.useContext(AppContext);
This will allow us to easily consume whichever Channel is currently selected and initialize the chat with a brand new one every time we open the app.
Next, let’s tie everything together in App.tsx:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374import { NavigationContainer } from '@react-navigation/native'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { StyleSheet, View } from 'react-native'; import { SafeAreaProvider, useSafeAreaInsets, } from 'react-native-safe-area-context'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import React from 'react'; import { Chat, OverlayProvider, useCreateChatClient, } from 'stream-chat-react-native'; import { AppProvider } from './contexts/AppContext.tsx'; import { chatApiKey, chatUserId, chatUserName, chatUserToken, } from './chatConfig.ts'; import { StreamTheme } from '@stream-io/chat-react-native-ai'; import { ChatContent } from './screens/ChatContent.tsx'; const Drawer = createDrawerNavigator(); const DrawerNavigator = () => ( <Drawer.Navigator screenOptions={{ drawerStyle: { width: 300, }, }} > <Drawer.Screen name="Chat" component={ChatContent} /> </Drawer.Navigator> ); const App = () => { const chatClient = useCreateChatClient({ apiKey: chatApiKey, tokenOrProvider: chatUserToken, userData: { id: chatUserId, name: chatUserName }, }); if (!chatClient) { return null; } return ( <SafeAreaProvider> <AppProvider client={chatClient}> <StreamTheme> <GestureHandlerRootView style={styles.container}> <OverlayProvider> <Chat client={chatClient}> <NavigationContainer> <DrawerNavigator /> </NavigationContainer> </Chat> </OverlayProvider> </GestureHandlerRootView> </StreamTheme> </AppProvider> </SafeAreaProvider> ); }; const styles = StyleSheet.create({ container: { flex: 1 }, }); export default App;
It is now time to implement our UI that resembles ChatGPT. But first, we want to let the SDK know how we resolve AI messages. Since the AI backend is currently set up to return the custom field ai_generated together with our message, we will check for that. However, you can check for any additional fields here to determine whether the message is AI-generated or not.
This can be done using the isMessageAIGenerated on the Chat component we created earlier, like so:
12345678910111213141516import { LocalMessage } from 'stream-chat'; // ...other imports const isMessageAIGenerated = (message: LocalMessage) => !!message.ai_generated; // ... other code <Chat client={chatClient} isMessageAIGenerated={isMessageAIGenerated} > <NavigationContainer> <DrawerNavigator /> </NavigationContainer> </Chat>
and additionally add the new custom type, as explained here, as custom-types.d.ts at our root level:
123456789101112131415161718192021222324252627282930313233343536373839import { DefaultAttachmentData, DefaultChannelData, DefaultCommandData, DefaultEventData, DefaultMemberData, DefaultMessageData, DefaultPollData, DefaultPollOptionData, DefaultReactionData, DefaultThreadData, DefaultUserData, } from 'stream-chat-react-native'; declare module 'stream-chat' { interface CustomAttachmentData extends DefaultAttachmentData {} interface CustomChannelData extends DefaultChannelData {} interface CustomCommandData extends DefaultCommandData {} interface CustomEventData extends DefaultEventData {} interface CustomMemberData extends DefaultMemberData {} interface CustomUserData extends DefaultUserData {} interface CustomMessageData extends DefaultMessageData { ai_generated?: boolean; } interface CustomPollOptionData extends DefaultPollOptionData {} interface CustomPollData extends DefaultPollData {} interface CustomReactionData extends DefaultReactionData {} interface CustomThreadData extends DefaultThreadData {} }
Since this is done, our SDK is aware of an AI-generated message, and we can proceed further.
Let us now create a new file, screens/ChatContent.tsx which will contain our new MessageList and add a couple of code snippets to it:
123456789101112131415161718192021222324252627282930313233343536373839404142434445const CustomMessage = (props: MessageProps) => { const { theme } = useTheme(); const { message } = props; const isFromBot = message.ai_generated; const hasPendingAttachments = useMemo( () => (message.attachments ?? []).some( attachment => (attachment.image_url && isLocalUrl(attachment.image_url)) || (attachment.asset_url && isLocalUrl(attachment.asset_url)), ), [message.attachments], ); const modifiedTheme = useMemo( () => mergeThemes({ theme, style: { messageSimple: isFromBot ? { content: { containerInner: { backgroundColor: 'transparent', borderRadius: 0, borderColor: 'transparent', }, }, } : { wrapper: { opacity: hasPendingAttachments ? 0.5 : 1, }, }, }, }), [theme, isFromBot, hasPendingAttachments], ); return ( <ThemeProvider mergedStyle={modifiedTheme}> <Message {...props} preventPress={true} /> </ThemeProvider> ); };
This component does 2 things:
- It overrides the styles of messages specifically from the chatbot so that they’re essentially full width within the
MessageList(similar to how they’re displayed in ChatGPT for instance) - It makes sure to reduce the opacity of the messages send by us in the event of having pending attachments to upload, so that we can signify that the message hasn’t been sent yet
12345678const CustomStreamingMessageView = () => { const { message } = useMessageContext(); return ( <View style={styles.streamingMessageViewWrapper}> <StreamingMessageView text={message.text ?? ''} /> </View> ); };
This component overrides the default StreamingMessageView provided by stream-chat-react-native and displays the enriched StreamingMessageView from the AI SDK.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152const 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 safeAreaInsets = useSafeAreaInsets(); const insets = useMemo( () => ({ ...safeAreaInsets, bottom: safeAreaInsets.bottom + (Platform.OS === 'android' ? (StatusBar.currentHeight ?? 0) * 2 : 0), }), [safeAreaInsets], ); 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 bottomSheetInsets={insets} onSendMessage={serializeToMessage} isGenerating={isGenerating} stopGenerating={stopGenerating} /> ); };
This component is a thin wrapper around the ComposerView from the AI SDK, making sure we update the SDK state before we send the message and we pass down the AIState of our channel downwards to it.
12345const EmptyStateIndicator = () => ( <View style={styles.emptyContainer}> <Text style={styles.emptyContainerText}>What can I help with ?</Text> </View> );
This customization allows us to display some default UI for a “headless” channel (an empty channel which we have not created yet, but want to use to start a new chat with the bot).
And finally, let’s tie it all together:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121import { AIStates, Channel, isLocalUrl, mergeThemes, Message, MessageList, MessageProps, ThemeProvider, useAIState, useChannelContext, useMessageComposer, useMessageContext, useMessageInputContext, useStableCallback, useTheme, } from 'stream-chat-react-native'; import { useAppContext } from '../contexts/AppContext.tsx'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { startAI } from '../http/requests.ts'; import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'; import { Platform, StatusBar, StyleSheet, Text, View } from 'react-native'; import { ComposerView, ComposerViewProps, StreamingMessageView, } from '@stream-io/chat-react-native-ai'; import { useCallback, useMemo } from 'react'; // ... our code snippets from above const RenderNull = () => null; const additionalFlatListProps = { maintainVisibleContentPosition: { minIndexForVisible: 0, autoscrollToTopThreshold: 0, }, ListHeaderComponent: null, }; export const ChatContent = () => { const { channel } = useAppContext(); const { bottom } = useSafeAreaInsets(); const preSendMessageRequest = useStableCallback(async ({ localMessage }) => { if (!channel) { return; } if (!channel.initialized) { await channel.watch({ created_by_id: localMessage.user_id, }); summarize(localMessage.text).then((response) => { const { summary } = response as { summary: string }; channel.update({ name: summary }) }); } if ( !Object.keys(channel.state.watchers).some(watcher => watcher.startsWith('ai-bot'), ) && channel.id ) { await startAI(channel.id); } }); if (!channel) { return null; } return ( <Animated.View key={channel.id} style={[styles.wrapper, { paddingBottom: bottom }]} entering={FadeIn.duration(200)} exiting={FadeOut.duration(200)} > <Channel channel={channel} keyboardVerticalOffset={Platform.OS === 'ios' ? 95 : -300} initializeOnMount={false} preSendMessageRequest={preSendMessageRequest} StreamingMessageView={CustomStreamingMessageView} Message={CustomMessage} enableSwipeToReply={false} EmptyStateIndicator={EmptyStateIndicator} allowSendBeforeAttachmentsUpload={true} NetworkDownIndicator={RenderNull} MessageAvatar={RenderNull} MessageFooter={RenderNull} > <MessageList additionalFlatListProps={additionalFlatListProps} /> <CustomComposerView /> </Channel> </Animated.View> ); }; const styles = StyleSheet.create({ wrapper: { flex: 1, backgroundColor: '#fcfcfc' }, emptyContainer: { flex: 1, width: '100%', backgroundColor: 'transparent', justifyContent: 'center', alignItems: 'center', }, emptyContainerText: { fontSize: 24, fontWeight: 'bold' }, streamingMessageViewWrapper: { maxWidth: '100%', paddingHorizontal: 16, }, aiTypingIndicatorWrapper: { paddingHorizontal: 24, paddingVertical: 12, }, });
This implementation covers a couple of things that help replicate the screen display style of a chatbot:
Additionally, we pass some props to our Channel component to give the app a chatbot look and feel:
initializeOnMount- This prop makes sure you don’t immediately create a newchannelbefore sending a messagepreSendMessageRequest- This callback prop is run before any other effects in the chat SDK state, but right after the first optimistic update and we use it to create thechannelif it does not exist as well as start the agent server-side as well as update its name
With this, our app can now send messages to and handle responses from the AI agent, create channels on demand as well as make sure the UI has a chatbot style to it.
4. Create an AITypingIndicator
Since we can now communicate with the chatbot, it would also be useful for us to know its current generation state. For example, we’d want to know when the LLM is “thinking” about our question, when it’s generating an answer, whether it’s looking at external resource and so on.
For this, you can use the AITypingIndicatorView component from @stream-io/chat-ai-sdk for rendering purposes as it will provide a cool indicator with a shimmering animation. We can rely on the useAIState hook from stream-chat-react-native to listen for state changes around this and react accordingly.
To do that we can add the following code in screens/ChatContent.tsx:
123456789101112131415161718192021222324252627282930313233343536373839import { AITypingIndicatorView, ComposerView, StreamingMessageView, } from '@stream-io/chat-react-native-ai'; // ... rest of the code const CustomAITypingIndicatorView = () => { 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={styles.aiTypingIndicatorWrapper}> <AITypingIndicatorView text={allowedStates[aiState]} /> </View> ); }; // ... const styles = StyleSheet.create({ // ...other styles aiTypingIndicatorWrapper: { paddingHorizontal: 24, paddingVertical: 12, }, });
and then simply add it between our MessageList and CustomComposerView (that we just created), like so:
1234567// ... <MessageList additionalFlatListProps={additionalFlatListProps} /> <CustomAITypingIndicatorView /> <CustomComposerView /> // ...
Now, whenever we talk to an agent we will be able to see its state as it’s working on a response.
5. Setting up the ChannelList
As a next step, let’s present the Stream Chat channel list component. Similar to how chatbot applications look and feel, we will render it inside of a side-drawer. When a channel is tapped, want to display it together with the MessageList on the main screen. We will also customize the channel previews within the list to contain the name of the channel itself, for better readability.
Let us include a new file under screens/MenuDrawer.tsx and include the following code to it:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192import { chatUserId } from '../chatConfig.ts'; import { ChannelSort } from 'stream-chat'; import { ChannelList, ChannelPreviewMessengerProps, useChannelsContext, useStableCallback, Copy, useChatContext, } from 'stream-chat-react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { DrawerContentComponentProps } from '@react-navigation/drawer'; import { createChannel, useAppContext } from '../contexts/AppContext.tsx'; import { Channel as ChannelClass } from 'stream-chat'; import { SafeAreaView } from 'react-native-safe-area-context'; import React from 'react'; const filters = { members: { $in: [chatUserId], }, }; const sort: ChannelSort = { last_updated: -1 }; const ChannelPreview = (props: ChannelPreviewMessengerProps) => { const channel = props.channel; const { onSelect } = useChannelsContext(); const onPress = useStableCallback(() => { onSelect?.(channel); }); return ( <Pressable style={({ pressed }) => ({ paddingVertical: 8, paddingHorizontal: 12, opacity: pressed ? 0.6 : 1, })} onPress={onPress} > <Text style={styles.previewText} numberOfLines={1}> {channel.data?.name ?? channel.cid} </Text> </Pressable> ); }; export const MenuDrawer = ({ navigation }: DrawerContentComponentProps) => { const { client } = useChatContext(); const { setChannel } = useAppContext(); const onSelect = useStableCallback((channel: ChannelClass) => { setChannel(channel); navigation.closeDrawer(); }); const onCreateNewChat = useStableCallback(() => { setChannel(createChannel(client)); navigation.closeDrawer(); }); return ( <SafeAreaView style={styles.wrapper}> <View style={styles.container}> <Text style={styles.title}>Conversations</Text> <Pressable onPress={onCreateNewChat}> <Copy /> </Pressable> </View> <ChannelList filters={filters} sort={sort} onSelect={onSelect} Preview={ChannelPreview} /> </SafeAreaView> ); }; const styles = StyleSheet.create({ wrapper: { flex: 1 }, container: { flexDirection: 'row', justifyContent: 'space-between', marginHorizontal: 12, paddingVertical: 16, borderBottomWidth: 1, borderBottomColor: 'grey', }, title: { fontSize: 15, fontWeight: 'bold' }, previewText: { fontSize: 15, fontWeight: 'bold' }, });
and add the new component as drawerContent to our main navigator within App.tsx, like so:
1234567891011121314151617181920// ... rest of the imports import { MenuDrawer } from './screens/MenuDrawer.tsx'; const Drawer = createDrawerNavigator(); const DrawerNavigator = () => ( <Drawer.Navigator drawerContent={MenuDrawer} screenOptions={{ drawerStyle: { width: 300, }, }} > <Drawer.Screen name="Chat" component={ChatContent} /> </Drawer.Navigator> ); // ... rest of the code
When we run the app at this point, we will be able to open a drawer on top of the main screen and see the channel list. When we tap on an item, we will set the channel within our AppContext to the one we just clicked, changing the main screen in the process. We can also create a brand new chat by clicking on the new chat button in the top-right corner.
Conclusion
In this tutorial, we have built an AI assistant bot that works mostly out of the box with the Stream Chat React Native SDK and the React Native Chat AI SDK.
- We have shown how to use our AI components for message rendering of LLM responses, such as markdown, code, tables, charts etc.
- We have shown how to create our server that will start and stop AI agents that will respond to user questions
- You have learned how to customize our React Native SDK to integrate these new AI features
If you want to learn more about our AI capabilities, head to our AI landing page. Additionally, check our React Native Docs to learn how you can provide more customizations to your chat apps. Get started by signing up for a free Stream account today.
