Initial Thoughts
This tutorial will teach you how to build your own customer support chat experience and create a serverless chatbot! The end result will look much like this:
The customer-facing chat experience is shown on the left, and the screen on the right shows the interface for support agents.
Here’s a GIF of the chatbot in action:
In order to help you achieve this end result and understand how you did so, we’ll walk you through the following sections:
Part 1 - React Chat Customer Service Chatbot
Part 2 - React Chat Agent View
Part 3 - LUIS Training for Your Chatbot
Part 4 - Webhook Integration with Stream
Part 5 - Serverless Azure Hello World
Part 6 - Serverless Webhook Handler Using Azure
The final code is stored in a GitHub repo; we encourage you to take the time to go through the process below in order to understand how to produce your very own chatbot, however, if you get stuck at any point during this tutorial you can look up the source code in the GitHub repo.
Part 1 - React Chat Customer Support Chatbot Tutorial
As a first step, we’re going to build the React interface that enables customers to ask questions. The end result of this part of the tutorial will look like this:
Setting up the frontend for customer support is easy. In this case, we’ll use React. If you want to use a different platform you’ll want to check out these tutorials: React Chat Tutorial, React Native Chat Tutorial, iOS/Swift chat Tutorial, and, Android Chat Tutorial.
Step 1: Create a New React App
Make sure you have node
and yarn
:
$ brew install node && brew install yarn
Create the chatbot-tutorial
app:
$ mkdir chatbot-tutorial $ cd chatbot-tutorial $ yarn global add create-react-app $ create-react-app chat-frontend $ cd chat-frontend $ yarn add stream-chat-react $ yarn && yarn start
Visit http://localhost:3000
and you should see the default Create React App screen:
Step 2: Get A Stream API Key
- Visit Stream and register to get an API key.
- For the purpose of this tutorial, we’ll disable auth and permissions checks (note you should obviously not do this in a production app, but it allows you to focus on your chat experience instead of integrating your auth system). Open your Stream Dashboard, click your app, select “Chat” and disable authentication & permissions:
Important: You will need to click on your application and enable the “Disable Auth Checks” toggle as well as the “Disable Permissions Checks” toggle.
- Open your Stream Dashboard and copy your API key to your
.env
file:
You can create your .env
file in your favorite editor and add the Stream API key. The full path of the file where you’ll want to add your API key is chatbot-tutorial/chat-frontend/.env
. The line where you add your API to your .env
file should be formatted as such:
REACT_APP_STREAM_API_KEY=YOUR_STREAM_KEY_HERE
Step 3: Edit App.js
As a first step, we’ll add a toggle button in the bottom right corner that prompts the user to click it to start or continue a chat session. To start, replace the code in src/App.js
with the following code snippet:
import React from "react"; import { Chat, Channel, Window, MessageList, TypingIndicator, MessageInputFlat, MessageCommerce, MessageInput, ChannelContext, Avatar } from "stream-chat-react"; import { StreamChat } from "stream-chat"; import "stream-chat-react/dist/css/index.css"; const chatClient = new StreamChat(process.env.REACT_APP_STREAM_API_KEY); /** * A little button component to toggle the chat interface */ const Button = ({ open, onClick }) => ( <div onClick={onClick} className={`button ${open ? "button--open" : "button--closed"}`} > {open ? ( <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg"> <path d="M19.333 2.547l-1.88-1.88L10 8.12 2.547.667l-1.88 1.88L8.12 10 .667 17.453l1.88 1.88L10 11.88l7.453 7.453 1.88-1.88L11.88 10z" fillRule="evenodd" /> </svg> ) : ( <svg width="24" height="20" xmlns="http://www.w3.org/2000/svg"> <path d="M.011 20L24 10 .011 0 0 7.778 17.143 10 0 12.222z" fillRule="evenodd" /> </svg> )} </div> ); function App () { const [open, setOpen] = React.useState(true); const [channel, setChannel] = React.useState(null); const toggle = () => { setOpen(!open); }; return ( <div className={`wrapper ${open ? "wrapper--open" : ""}`}> <Button onClick={toggle} open={open} /> </div> ); } export default App;
Note: At first, you will get warning messages about unused imports; to keep things simple, we included all imports now so that it is easier to follow this tutorial. Once our app is completed, you should not see these warnings again.
After you’ve updated your App.js
file, open src/index.css
and replace the CSS with the following:
html, body { margin: 0; padding: 0; box-sizing: border-box; } *, *:after, *:before { box-sizing: inherit; } #root { width: 100%; height: 100vh; display: flex; justify-content: flex-end; background: url('https://images.pexels.com/photos/1421903/pexels-photo-1421903.jpeg') top left no-repeat; background-size: cover; } .str-chat { height: auto; max-height: 500px; } .str-chat-channel { max-height: 0 !important; min-height: 500px; width: 375px; } .str-chat-channel .str-chat__container { height: auto; max-height: 500px; border-radius: 10px; max-width: 375px; } .wrapper { display: flex; flex-direction: column; position: fixed; right: 20px; top: 60px; align-items: flex-end; justify-content: flex-end; height: 100%; height: calc(100% - 40px); padding: 30px 0; } .wrapper--open .str-chat__container { height: 100%; } .button { height: 60px; width: 60px; margin-top: 16px; border-radius: 60px; display: flex; align-items: center; justify-content: center; flex: 0 0 60px; background: #ffffff; background-image: linear-gradient( -180deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 0, 0, 0.1) 100% ); border: 1px solid rgba(255, 255, 255, 0.18); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.5), inset 0 1px 0 0 rgba(255, 255, 255, 0.24); cursor: pointer; } .button svg { fill: #000; opacity: 0.8; } .button--closed svg { position: relative; left: 3px; fill: #0043f7; } .button:hover svg { opacity: 1; }
By now, your chat experience should look like this:
You can toggle the button, but it doesn’t do anything just yet. Note how clean the App.js code is, as a result of using React Hooks!
Step 4: Guest User Interface
For the purpose of this demo, we will ask the user to provide their name and email. Stream Chat supports 3 types of user authentication, and it depends on your use case as to which one works best:
Regular users (Use this if the user asking support questions is already logged in – especially for in-app support it’s common for users to be logged in)
Guest Users (Allow users to set a guest name and email)
Anonymous Users (Allow users to chat without providing any details)
To begin the process of gathering information to authenticate the user, go ahead and open up src/App.js
and add the GuestUserInput
component to the file (just above your App
class:
/** * A little interface for the user to set up their name and email before * The chat starts. */ export function GuestUserInput({ setChannel, ...props }) { const [name, setName] = React.useState(""); const [email, setEmail] = React.useState(""); const handleSubmit = async event => { event.preventDefault(); const userID = window.btoa(email).replace(/=/g, ""); // in a real app you would do some round robin on active agents.. const assignedSupportAgent = "support-agent-123"; const user = await chatClient.setGuestUser({ id: userID, name: name, email: email }); const channel = chatClient.channel("commerce", userID, { members: [chatClient.user.id, assignedSupportAgent], assigned: assignedSupportAgent, status: "open" }); channel.watch({ presence: true }); setChannel(channel); }; return ( <div className="str-chat str-chat-channel commerce light"> <div className="str-chat__container"> <div className="str-chat__main-panel"> <div className="str-chat__header-livestream"> <div> Hi, feel free to ask any questions or share your feedback. We're happy to help! <Avatar image="https://pbs.twimg.com/profile_images/897621870069112832/dFGq6aiE_400x400.jpg" /> <Avatar image="https://i.pravatar.cc/300" /> <Avatar image="https://i.pravatar.cc/200" /> </div> </div> <div className="str-chat__list "> <div> Hi, what's your name and email? <form onSubmit={handleSubmit}> <label> Name: <input type="text" value={name} onChange={event => { setName(event.target.value); }} /> </label> <br /> <label> Email: <input type="text" value={email} onChange={event => { setEmail(event.target.value); }} /> </label> <br /> <input type="submit" value="Submit" /> </form> </div> </div> </div> </div> </div> ); }
Next, we need to change the App
component to render the GuestUserInput
and the chat interface. We’ll update our App
component to look like this:
function App() { const [open, setOpen] = React.useState(true); const [channel, setChannel] = React.useState(null); const toggle = () => { setOpen(!open); }; function renderChat() { return } let nodes = ""; if (open) { if (channel) { nodes = renderChat(); } else { nodes = <GuestUserInput setChannel={setChannel} />; } } return ( <div className={`wrapper ${open ? "wrapper--open" : ""}`}> {nodes} <Button onClick={toggle} open={open} /> </div> ); }
By now, your chat experience should show you an interface to set up your guest user:
If you submit your details, you’ll get an API error logged, since we didn’t create a user account for the support agent yet; let’s do that now!
Step 5: Create a User Account for the Support Agent
Start by installing the getstream-cli
:
1yarn global add getstream-cli
Next, configure your account:
1stream config:set
It will automatically ask you for some configs. Be sure to use your own API key and secret:
And, finally, create the user with a userID
of “support-agent-123”:
1stream chat:user:create -u support-agent-123 --role user
Step 6: Render a Chat Interface
You will notice that you see an empty screen after entering your details. As a next step, we’ll add the actual chat components.
Replace the dummy renderChat
function in your App
component in src/App.js
with the following:
function renderChat() { return ( <Chat client={chatClient} theme={"commerce light"}> <Channel channel={channel}> <Window> <MessageList TypingIndicator={TypingIndicator} Message={MessageCommerce} /> <MessageInput Input={MessageInputFlat} /> </Window> </Channel> </Chat> ); }
You’ll now see a functional chat interface when you fill in your user details:
Note how easy it is to build a chat interface by leveraging the Channel
, MessageList
and MessageInput
components. All of these are fully customizable, and you can write your own if you want to! Have a look at the React Chat Tutorial to learn more.
Step 7: User Presence & Showing Who Is Online
It would be nice to show a ChannelHeader
that shows who is currently online... So, let’s create a custom ChannelHeader
component called MyChannelHeader
:
/** * A custom channel header element which shows who is currently online */ function MyChannelHeader() { const channelContext = React.useContext(ChannelContext); const channel = channelContext.channel; const client = channelContext.client; const [members, setMembers] = React.useState(channel.state.members); React.useEffect(() => { function handleUserPresenceChange(event) { setMembers(channel.state.members); } client.on("user.presence.changed", handleUserPresenceChange); return function cleanup() { client.off("user.presence.changed", handleUserPresenceChange); }; }); const onlineUsers = []; if (members) { for (let m of Object.values(members)) { if (m.user.online) { onlineUsers.push(m.user); } } } if (!onlineUsers.length) { return ( <div className="str-chat__header-livestream"> Sorry, nobody is online at the moment. A support agent has been notified. </div> ); } return ( <div className="str-chat__header-livestream"> Currently online: {onlineUsers.map((value, index) => { return ( <div key={index}> <Avatar image={value.image} name={value.name} /> {value.name} </div> ); })} </div> ); }
Next we’ll add the MyChannelHeader
to the renderChat
function. The updated function now looks like this:
function renderChat() { return ( <Chat client={chatClient} theme={"commerce light"}> <Channel channel={channel}> <Window> <MyChannelHeader /> <MessageList TypingIndicator={TypingIndicator} Message={MessageCommerce} /> <MessageInput Input={MessageInputFlat} /> </Window> </Channel> </Chat> ); }
Run yarn start
in the chat-frontend directory and visit http://localhost:3000
. You’ll now see a functional chat experience with a header which shows who is currently online:
If you got stuck in one of these steps, you can also copy and paste the end result from the GitHub repo’s AppStep7.js.
Part 2 - Dashboard for the Live Chat
For the agent, we want an interface that allows them to productively talk to many people at once. To begin to create this, we’ll start with a Create React App app, again:
Step 1: Create a New React App
Make sure that you are in the chatbot-tutorial
directory, and run:
$ create-react-app chat-agent-dashboard $ cd chat-agent-dashboard $ yarn add stream-chat-react $ yarn && yarn start
Step 2: Create Your .env.development
Copy your .env
file from the frontend project:
1cp ../chat-frontend/.env .env
The API key is the same as you used in Part 1. In the case that you’ve forgotten what it was, head over to Stream’s dashboard to look it up again (or check the .env
file you created in the last section).
Step 3: Edit src/App.js
Open up chat-agent-dashboard/src/App.js
and replace the contents with:
import React from "react"; import { Chat, Channel, ChannelHeader, Thread, MessageList, ChannelList, TypingIndicator, MessageInputFlat, MessageCommerce, MessageInput, Window } from "stream-chat-react"; import { StreamChat, DevToken } from "stream-chat"; import "stream-chat-react/dist/css/index.css"; const chatClient = new StreamChat(process.env.REACT_APP_STREAM_API_KEY); chatClient.setUser( { id: "support-agent-123", name: "Jessica", image: "https://getstream.io/random_svg/?id=support-agent-123&name=Jessica" }, chatClient.devToken("support-agent-123") ); const filters = { type: "commerce" }; const sort = { last_message_at: -1 }; const channels = chatClient.queryChannels(filters, sort, { watch: true, presence: true }); function App() { return ( <div> <Chat client={chatClient} theme={"commerce light"}> <ChannelList filters={filters} sort={sort} /> <Channel> <Window> <ChannelHeader /> <MessageList TypingIndicator={TypingIndicator} Message={MessageCommerce} /> <MessageInput Input={MessageInputFlat} /> </Window> </Channel> </Chat> </div> ); } export default App;
Step 4: Run yarn start
Be sure to run yarn start in the chat-agent-dashboard directory and visit http://localhost:3001. Your agent interface will now look like this:
Note: If you get an error about an invalid token, be sure to disable auth and permissions checks in the dashboard.
Some cool features to note:
- File Uploads
You can upload files to the chat via drag & drop, copy/pasting or manually selecting a file:
- Rich URL Previews
If you post a URL, you’ll see a rich preview in the chat interface:
- Typing Indicators
When either user is typing, a dot animation will pop up to indicate this to the opposite party:
- Online Status for Agent and Customer
The customer will be able to see which agents are online at the top of the interface:
Part 3 - LUIS Azure Training for Your Chatbot
To begin setting up the questions/statements your chatbot will be able to address and the responses it should have, head over to https://www.luis.ai/ to set up your Language Understanding Service (LUIS). LUIS analyzes messages and tells you what the intent is. Here are some examples:
For the purpose of this blog post, we’ll teach LUIS to recognize the “restaurant reservation” intent and we’ll teach it the answer to life the universe and everything.
Step 1: Create an App
Sign in to the LUIS dashboard and create an application. Once logged in, click “Create new app” as shown in the screenshot below:
When prompted, specify the following for your chatbot (and, then, click “Done”):
Name: ChatbotTutorial
Culture: English
Step 2: Create a New Intent
As a next step, we’ll create a new Intent called “Answer”, inspired by the amazing Hitchhiker's Guide to the Galaxy. Click on the “Build” button at the top right, and then click on “Create new intent”:
You will be prompted for the intent name (“Answer”). Once loaded, specify your “utterances” to LUIS:
- What’s the answer to life the universe and everything
- What’s the answer
- The answer please
- Tell me the answer
- What’s the meaning of life
- What’s the answer to the universe
Step 3: Leverage a Built-In Intent
We’ll also add a built-in Intent. Click on the Intents button to take you to the previous page. From there, click the “Add prebuilt domain intent” button.
Select the RestaurantReservation.Reserve intent and then click the “Done” button.
Step 4: Train the Model
Now that we’ve updated the settings, we’ll want to train the model. Hit the “Train” button in the top right corner:
Step 5: Publish the Model to Production
Hit the “Publish” button and select Production. This is important; if you select “Staging”, the next steps in the tutorial will fail. Wait a few seconds for your model to be published.
In the meantime, let’s take note of 3 settings... To access them, click Manage -> Application Information:
- Your application ID:
- Your primary key and (3) region:
Part 4 - Learn How to Use Stream Webhooks
As a next step, we’ll connect Stream’s webhook to Azure. To do this, we’ll leverage the Stream CLI.
Step 1: Verify Stream-CLI is installed
Verify that you have Stream’s CLI installed (you used it to configure the user in Part 1 of this tutorial):
1$ stream version
If you don’t have it installed yet, you can install the CLI by running the following yarn
command:
1$ yarn global add getstream-cli
And set up your API key like this:
1$ stream config:set
Step 2: Test the Webhook
To test the webhook integration, we’ll create a new request bin (it doesn’t need to be private).
Next, configure your new request bin as your webhook URL for Stream. Be sure to replace the URL in the following command with the request bin URL you’ve just created:
1$ stream chat:push:webhook --url https://enfspya8mur9f.x.pipedream.net
If you visit http://localhost:3000 and leave a chat message you should see the message.new event shows up in the request bin:
Awesome! Now that you’ve learned how to set up Stream’s webhooks, we’ll continue to the serverless part of this tutorial.
Part 5 - Serverless Azure Chat
We’ll want to leverage an Azure Function app to connect Stream & LUIS. The function needs to do these 3 things:
Parse the Stream webhook data
Run intent analysis with LUIS
Send a reply on the channel if we recognize the intent
Follow along with the steps below to get your Azure Function up and running:
Step 1: Set Up Azure
Go to https://portal.azure.com and set up your account. You get $200 in credits when you sign up so you can complete this tutorial without setting up any billing. Microsoft recently changed Azure to ask you to verify your phone number & credit card in the next step. (which is a bit annoying...)
Step 2: Create a Function App
Search for “Function App” and select it:
Click Add and once on the next screen, you’ll want to select the following:
Function App Name: ChatbotTutorial
Runtime Stack: Node
and click Review & Create.
Now, hit Create to spin up your function app! This takes anywhere from a few seconds to a few minutes.
Your function app should now be provisioned:
Great! Now that we’ve created our function app, it’s time to write a little bit of code:
Step 3: Verify Azure Functionality
Make sure you have azure functions core tools up and running:
1$ yarn global add azure-functions-core-tools
You can verify it worked by running func version
. As of January 2020, the current version is 2.7.1948.
Step 4: Set Up Serverless Directory
Set up your serverless directory using the following shell commands:
123$ mkdir serverless $ cd serverless $ func init --worker-runtime node --language javascript
Step 5: Create New Message Handler Function
In this next step, we’ll create a handler function called NewMessageTrigger
:
1$ func new -t "HTTP trigger" --name "NewMessageTrigger"
Step 6: Test New Message Handler Function
Next, we’ll want to test our NewMessageTrigger
function locally:
1$ func start
Note: If you receive an error about your version of Node.js, you can fix that using the following installing node v10.14.1 or by using NVM as shown below.
1234$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash $ nvm install 10.14.1 $ nvm alias default 10.14.1 $ yarn install
You should now be able to try out your local function by visiting:
http://localhost:7071/api/NewMessageTrigger?name=John
Part 6 - Serverless Webhook Handler with Azure
Ok, now that we’ve got the basics in place, let’s connect all the dots! Remember, we want to accomplish the following:
Handle the incoming data from Stream’s webhook platform
Run intent analysis using Azure’s LUIS
Reply to the user in the chat
Step 1: Create Your Serverless Project
Go to the serveless folder and install the dependencies:
1$ yarn add stream-chat axios dotenv
Step 2: Set Up Your Function Handler
Replace the code in NewMessageTrigger/index.js
with the following:
const axios = require("axios"); const StreamChat = require("stream-chat").StreamChat; require("dotenv").config(); /** * getStreamClient - returns the Stream Chat client * * @returns {object} Stream chat client */ function getStreamClient() { const client = new StreamChat( process.env.STREAM_API_KEY, process.env.STREAM_API_SECRET ); return client; } /** * analyseIntentWithLUIS - runs intent analysis on the message with LUIS * * @param {string} messageText the message text to analyse * @returns {object} returns the top intent as {intent: 'name', score: 0.9} */ async function analyseIntentWithLUIS(context, messageText) { const appID = process.env.LUIS_APP_ID; const key = process.env.LUIS_SUBSCRIPTION_KEY; const region = process.env.LUIS_REGION; const url = `https://${region}.api.cognitive.microsoft.com/luis/v2.0/apps/${appID}?subscription-key=${key}&q=${messageText}`; try { const response = await axios.get(url); const data = response.data; return data.topScoringIntent; } catch (e) { context.log(`Failed to analyse intent with luis, error ${e.response.data}`); throw e; } } /** * anonymous function - This request handler does a few things * * 1. Handle the Stream webhook data * 2. Run intent analysis with LUIS * 3. Send a reply on the channel */ module.exports = async function(context, req) { // show a nice error if you send a GET request if (req.method === "GET") { context.res = { body: { error: "Invalid request, only POST requests are allowed" } }; return; } // important: validate that the request came from Stream const chatClient = getStreamClient(); const valid = chatClient.verifyWebhook( req.rawBody, req.headers["x-signature"] ); if (!valid) { context.res = { body: { error: "Invalid request, signature is invalid" } }; return; } // reply to message.new, but not to messages written by the bot // (you get an interesting loop if you don't exclude bot messages) if (req.body.type === "message.new" && req.body.message.user.id !== "mrbot") { // parse the stream webhook format const messageText = req.body.message.text; const cID = req.body.cid; const channelType = cID.split(":")[0]; const channelID = cID.split(":")[1]; // run intent analysis with LUIS context.log("starting to analyse intent with LUIS"); const topIntent = await analyseIntentWithLUIS(context, messageText); const intent = topIntent.intent; const score = topIntent.score; context.log( `Received a message.new with text "${messageText}" and found intent ${intent} with score ${score}` ); // if we understand this intend, send a reply const channel = chatClient.channel(channelType, channelID); const botUser = { id: "mrbot", name: "MR Bot" }; if (intent === "Answer") { await channel.sendMessage({ text: "42 is the answer", user: botUser }); } else if (intent === "RestaurantReservation.Reserve") { await channel.sendMessage({ text: "Great idea, I'm hungry", user: botUser }); } // send a 200 response with some extra info context.res = { body: { messageText: messageText, intent: intent, score: score } }; } else { let userID = null; if (req.body.message && req.body.message.user) { userID = req.body.message.user.id; } const msg = `Skipping request of type ${req.body.type} from userID ${userID}`; context.log(msg); context.res = { body: { error: msg } }; } };
Let’s take a moment to understand the above code... We start by receiving the request; the body of the request (req.body
) has the information from Stream’s webhook. When we receive an event of type message.new
, and (this is important) the message is not written by a bot, we run intent analysis via LUIS. If we understand the intent, the bot replies on the channel.
Step 3: Create a Local .env File
Create a file called .env
, the full path is serverless/.env
with the following settings:
STREAM_API_KEY=YOUR_STREAM_API_KEY STREAM_API_SECRET=YOUR_STREAM_API_SECRET LUIS_APP_ID=YOUR_LUIS_APP_ID LUIS_SUBSCRIPTION_KEY=YOUR_LUIS_SUBSCRIPTION_KEY LUIS_REGION=westus
Step 4: Configure Your Production .env
To go live, we’ll want to configure those same environment settings in Azure’s web interface. First click “Go to resource”:
Next click “Function app settings”:
And select “Manage application settings”:
On this page you can add the same 5 environment variables you’ve just added to your serverless/.env
file:
Login to Azure using the following command:
1$ az login
You will be prompted to authenticate via your browser as shown in the screenshot below (this is really well done):
And deploy the function using this command:
1$ func azure functionapp publish ChatbotTutorial
Note: If you run into an error, ensure that you have installed the azure-cli:
1$ brew update && brew install azure-cli
Note: The invocation URL will be provided in the response body after publishing your function (as shown in the above screenshot).
When you run the above command, you’ll receive an “invoke url”; you’ll want to update your webhook URL to point to your invocation URL a shown below:
1$ stream chat:push:webhook --url yourinvokeurl
After following this last step, your bot will be fully connected to the chat interface! Try it out by typing something such as:
“What’s the answer?”
OR
“make a reservation”
In the chat interface:
A side note on Debugging with NGROK
Webhooks can be a little bit hard to work with. A tool that helps with local debugging is NGROK, which gives you a public URL for your local endpoint.
To utilize NGROK, first, ensure that your server is running by running:
1func start
Next, install NGROK by following the instructions on their site:
Then, expose your local web server using ngrok
:
1$ ngrok http 7071
This makes it easier to test changes (rather than you having to deploy to Azure while you’re coding). Let’s see if it works by configuring Stream’s webhook:
1$ stream chat:push:webhook --url http://<YOUR_NGROK_URL>/api/NewMessageTrigger\?name\=thierry
Concluding the Chatbot Tutorial
After following the above tutorial you should have a fully functional chatbot for your application! It should look very similar to this:
If at some point you got stuck, head over to the GitHub repo to check out the end result of this tutorial and compare to the code you’ve written.
Additionally, this tutorial used React for the user interface. Have a look at the React Native Chat Tutorial, iOS/Swift Chat Tutorial or Android/Kotlin Chat Tutorial, if you want to use a different platform!
To learn more you might want to check out:
- Stream Chat API tutorial
- Stream Chat Tutorials for iOS/Swift
- Android
- React Native and React
- LUIS language understanding tutorial