TL;DR
In this article I am going to show how to build a coffee delivery service in React and NodeJS, using a new AI-based library TypeChat.
Before we start… I have a favour to ask. 🤗
I am building an open source feature flags platform and if you could star the project on github it would mean the world to me! It would help me keep writing articles like this as well!
https://github.com/switchfeat-com/switchfeat
What is TypeChat? 🤔
TypeChat is a new experimental library built by Microsoft Typescript team which allows to translate natural language strings into type safe intents using the OpenAI infrastructure.
For example, providing a text like: “I would like a cappucino with a pack of sugar”, TypeChat can translate that string into the following structured JSON object which can be parsed and processed by our API much easier then manually tokenizing the string ourselves.
{
"items": [
{
"type": "lineitem",
"product": {
"type": "LatteDrinks",
"name": "cappuccino",
"options": [
{
"type": "Sweeteners",
"name": "sugar",
"optionQuantity": "regular"
}
]
},
"quantity": 1
}
]
}
Setting things up
First things first, we need to setup the usual boilerplate of create-react-app
and Express
. I am not going to go too much into the details of this, just because it’s boring and doesn’t really add much to this article.
Setup React app
Lets quickly create two folders, client
and server
and start installig our dependancies.
mkdir client server
In the client folder, let’s install create-react-app
:
cd client & npx create-react-app my-react-app --template typescript
Once the installation is complete, let’s install the react-router-dom
to manage the routing much easier:
npm install react-router-dom heroicons
Setup the Express server
Let’s move to the server folder and install Express and some other depedancies we are going use:
cd server & npm init -y
npm install express cors typescript @types/node @types/express types/cors dotenv
Once everything has been install, let’s initialize typescript:
npx tsc --init
Let’s create the index.ts
file, which is going to contain the main server API logic:
touch index.ts
That’s the easiest Express configuration you can have in typescript:
import express, { Express } from 'express';
import cors from "cors";
const app: Express = express();
const port = 4000;
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
app.use(cors);
app.get("/api", (req, res) => {
res.json({
message: "Food ordering chatbot",
});
});
app.listen(port, () => {
console.log(`⚡️[server]: Server is running on port 4000`);
});
In package.json
of the sever, we need to add a script to run it:
"scripts": {
"start": "node index.js"
}
To finish our setup, let’s compile typescript and run the server:
tsc & npm run start
Cool! let’s more to something much cooler shall we!?
Building the UI
We are going to use TailwindCSS to manage our styling which makes things much easier to manage. Let’s start from changing the App.tsx
file to this, which uses the React Router to render components on the page.
import './output.css';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { ChatBot } from './components/Chatbot';
export const App = () => {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<ChatBot />} />
</Routes>
</BrowserRouter>
);
}
Let’s create the ChatBot
component which is going to show the chatbot user interface and send API requests to our Express server.
Here is the markup of the ChatBot
component:
return (
<div className="flex min-h-full flex-1 flex-col justify-center overflow-hidden">
<div className="divide-y divide-gray-200 overflow-hidden flex flex-col
justify-between">
<div className="w-full h-full px-4 py-5 sm:p-6 mb-32">
<ul className="mb-8 h-full">
{chatSession.map((x, index) =>
(x.role !== 'system' ?
<div key={index}>
<li>
<div className="relative pb-8">
{index !== chatSession.length - 1 ? (
<span className="absolute left-4 top-4 -ml-px h-full w-0.5 bg-gray-200" aria-hidden="true" />
) : null}
<div className="relative flex space-x-3">
<div>
<span className={classNames('h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white', x.role === 'user' ? 'bg-slate-600' : 'bg-orange-500')}>
{x.role === 'user' && <UserIcon className="h-5 w-5 text-white" aria-hidden="true" />}
{x.role === 'assistant' && <RocketLaunchIcon className="h-5 w-5 text-white" aria-hidden="true" />}
</span>
</div>
<div className="flex min-w-0 flex-1 justify-between space-x-4 pt-1">
<div>
<p className="text-md text-gray-500" style={{ whiteSpace: "pre-wrap" }}>
{x.content}
</p>
</div>
</div>
</div>
</div>
</li>
</div> : <div key={index}></div>))
{isProcessing && (
<li key={'assistant-msg'}>
<div className="relative pb-8">
<div className="relative flex space-x-3">
<div>
<span className={classNames('h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white', 'bg-orange-500')}>
<RocketLaunchIcon className="h-5 w-5 text-white" aria-hidden="true" />
</span>
</div>
<div className="flex min-w-0 flex-1 justify-between space-x-4 pt-1">
<div>
<p ref={processingMessage} className="text-md text-gray-500" style={{ whiteSpace: "pre-wrap" }}>
{currentAssistantMessage}
</p>
</div>
</div>
</div>
</div>
</li>)
<div ref={messagesEndRef} />
{isProcessing && (
<button type="button" className="inline-flex items-center px-4 py-2 font-semibold leading-6 text-sm shadow rounded-md text-white bg-indigo-500 hover:bg-indigo-400 transition ease-in-out duration-150 cursor-not-allowed" disabled>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Processing...
</button>
)}
</ul>
</div>
<div className=" w-full bottom-0 mt-2 flex rounded-md px-4 py-5 sm:p-6 bg-slate-50 fixed">
<div className="relative flex flex-grow items-stretch focus-within:z-10 w-full">
<input
ref={chatInput}
type="text"
name="textValue"
className="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="your order here..."
/>
</div>
<button
onClick={processOrderRequest}
type="button"
className="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
>
<BarsArrowUpIcon className="-ml-0.5 h-5 w-5 text-gray-400" aria-hidden="true" />
Send
</button>
</div>
</div>
</div>);
There is not too much going on here, most of the code is all about updating chat history and add a button to send the order request to our API. Here it is how this component will look like on the page:
Sending the order request to the API
Add a POST
route to the express server which accepts a string containing the order in natural language, processes the order and returns the order which has been processed back to the user.
const processOrderRequest = async () => {
setIsProcessing(true);
if (!chatInput.current) {
setIsProcessing(false);
return;
}
chatSession.push({ role: "user", content: chatInput.current.value });
const tempArr: ChatMessage[] = [];
for (const t of chatSession) {
tempArr.push(t);
}
setChatSession([...tempArr]);
fetch("http://localhost:4000/api/order-request", {
method: "POST",
body: JSON.stringify({ newMessage: chatInput.current.value }),
headers: {
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then((result) => {
const order: any[] = [];
result.items.forEach((x: any) => {
const options = x.product.options ? x.product.options.map((opt: any) => { return {name: opt.name, quantity: opt.optionQuantity}; }) : [];
order.push({product: x.product.name, options: options, size: x.product.size});
});
const orderString: string[] = [];
order.forEach((x: any) => {
orderString.push(`${x.size} ${x.product} with ${x.options.map((x: any) => `${x.quantity === undefined ? "" : x.quantity} ${x.name}`).join(" and ")}`);
});
const resp = `🎉🎉 Thanks for your order! 🎉🎉 \n\n ☕> ${orderString.join("\n ☕> ")}`;
tempArr.push({ role: "assistant", content: resp });
setChatSession([...tempArr]);
setCurrentAssistantMessage("");
if (processingMessage.current)
processingMessage.current.innerText = "";
if (chatInput.current)
chatInput.current.value = "";
})
.catch((err) => console.error(err))
.finally(() => { setIsProcessing(false); });
};
The function above grabs the string in natural language from the input field, updates the chat history and sends the request to our API. Once we get the results, it parses the data and adds a new message into the chat history with the order which has been confirmed.
Setting up TypeChat on the Server
Here you will learn how to setup TypeChat and start sending requests to OpenAI. Let's move back to the server
folder and let's install the library from NPM:
npm install typechat
Once installed, we have to configure the schema that TypeChat will use to cast the AI response into the right types. For the purpose of this article, I have shamelessly used the schema definition from the official TypeChat example repo and moved into our project. Here it is the link. The main part of this schema is definition of the Cart type:
export interface Cart {
items: (LineItem | UnknownText)[];
}
// Use this type for order items that match nothing else
export interface UnknownText {
type: 'unknown',
text: string; // The text that wasn't understood
}
export interface LineItem {
type: 'lineitem',
product: Product;
quantity: number;
}
We will instruct TypeChat to use this specific model to translate the string coming from the request into a Cart object, which is basically a list of products items that the customer wants to buy.
Now we need to setup the OpenAI api key that TypeChat uses behind the scenes to execute the requests. The apiKey needs to stay private (do not share your apiKey with anyone!), but needs to be stored in the .env
file in the server folder of the project. It will have this structure:
OPENAI_MODEL="gpt-3.5-turbo"
OPENAI_API_KEY="<YOUR OPENAI APIKEY>"
The following snippet adds a new POST
endpoint to the Express app which receives the order request from the UI, uses TypeChat to extract an intent out of that string and finally processes the order:
const model = createLanguageModel(process.env);
const schema = fs.readFileSync(path.join(__dirname, "coffeOrdersSchema.ts"), "utf8");
const translator = createJsonTranslator<Cart>(model, schema, "Cart");
app.post("/api/order-request", async (req, res) => {
const { newMessage } = req.body;
console.log(newMessage);
if (!newMessage || newMessage === "") {
console.log("missing order");
res.json({error: "missing order"});
return;
}
// query TypeChat to translate this into an intent
const response: Result<Cart> = await translator.translate(newMessage as string);
if (!response.success) {
console.log(response.message);
res.json({error: response.message});
return;
}
await processOrder(response.data);
res.json({
items: response.data.items
});
});
For simplicity, our processOrder
function is going to be a simple console.log()
, but in reality it could be sending the order to a processing queue or to any other background process.
const processOrder = async (cart: Cart) => {
// add this to a queue or any other background process
console.log(JSON.stringify(cart, undefined, 2));
};
Big congrats! 🎉
If you made it this far, you managed to build a fully working chatbot which can take coffe orders. This system is flexible enough that you could apply the same architecture and logic to any other type of online ordering services like groceries, clothes, restaurants.. you name it! Just changing the schema, TypeChat will be able to generate structured responses based on your needs.
In the next section you will learn how to use SwitchFeat to evaluate the current user access to the premium features for this chatbot.
Using feature flags to show premium features
In this section we are going to use SwitchFeat API, to evaluate if a given user has access to the premium features of this chatbot, like fast delivery or weekends delivery.
At this current stage SwitchFeat doesn’t have a dedicated SDK yet, so we are going to use a simple fetch
to contact the API and evaluate the current user request.
Let’s change our component to track the current user data into a state variable. This information should come from a database or any datastore and could be stored in a Context Provider to be shared across multiple components. But for the purpose of this article let’s keep it simple.
const [userContext, setUserContext] =
useState<{}>({
username: 'a@switchfeat.com',
isPremium: true
});
Add another state variable which is going to define if the premium features should be shown to the current user or not:
const [showPremium, setShowPremium] = useState<boolean>(false);
Finally add the following fetch
request to the component:
useEffect(() => {
const formData = new FormData();
formData.append('flagKey', "premium-delivery");
formData.append('flagContext', JSON.stringify(userContext));
formData.append('correlationId', uuidv4());
const evaluateFlag = () => {
fetch(`http://localhost:4000/api/sdk/flag/`, {
method: "POST",
headers: {
Accept: "application/json",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Origin": "true"
},
body: formData
}).then(resp => {
return resp.json();
}).then(respJson => {
setShowPremium(respJson.data.match);
}).catch(error => { console.log(error); });
};
evaluateFlag();
}, []);
The snippet above sends an API request to SwitchFeat, checks if the current user is a premium user and evaluates if they are allowed to have premium features.
Finally just add this nippet wherever you want your PremiumFeatures
component should be rendered.
{showPremium && <PremiumFeatures />}
Why using a feature flag for this?
You already know if a user is premium right!? True… but… what if you want to disable that feature regardless?
There are multiple scenarios where you would want to pause the premium delivery (or any other feature), for scarcity of drivers, not enough premium orders available, etc. A simple logic which switches on/off that features with a single click in realtime, is really powerful and avoids building ad-hoc changes spread across the codebase.
Create your flag in SwitchFeat
First we need to create a new user segment to group all the users which are paying for the Premium service:
Then we need to create a new flag which gates the access to the premium feature using this segment:
Here is our new shiny flag which we can use in our API requests:
Now changing the status of the switch in SwitchFeat, you will be able to activate or deactivate the premium features without any code change or redeployment. Look at the video below.
Well done everyone! you managed to finish this article and get an idea of what TypeChat is and why is so cool to use it. I pushed the entire codebase of this article on Github if you want to check it out.
So.. Can you help? 😉
I hope this article was somehow interesting. If you could give a star to my repo would really make my day!
Top comments (21)
This was great I have been meaning to try out these new AI tools from Microsoft.
Thanks a lot Andrew! Glad you found this useful!
Great! Congrats!
What font did you use for the thumbnail? love it
Thanks! It's the default one from Tailwind.
Send me a coffee please haha
Medium or large! 😁
Cool stuff there. ❤️
Thank you so much!
Typechat is a cool library!! NLP parsing into JSON is API gold! Thank you.
Thanks! I am glad you found this article useful!
Nice application. About time for me to also get on the AI train and build something with it..
Thanks. Yeah it was a pretty cool excercise for me as well. feel free to grab my code and change it. enjoy!
This is awesome!
Thanks a lot Rachel!
Great tutorial. Its very detailed and actually solves a problem.
Thanks a lot Nilan! That's what I will try to do on a weekly basis! New article coming next Monday!
Great tutorial!
Thanks Andrei!
Great tutorial Andy 🙌
Thank you so much Luca!
Some comments have been hidden by the post's author - find out more