Building a coffee delivery chatbot with React, ChatGPT and TypeChat 🤖
Tags: javascript, webdev, beginners, react
Published: 2023-07-24 10:56:30

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

<img width="1000" src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZmpxenU5Mzg3amU2NWt4MjdvYW90ZGUwdTd4Z25mcW0xc3lod2phZCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/jpheI1bVIEcZGHht3f/giphy.gif" />


---

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

``` json
{
  "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:

``` tsx
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:

```json
"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.

``` tsx
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: 

``` tsx
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:

<img width="500" src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kw858djr5jlge0rypxni.png" />

---

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

``` tsx
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](https://github.com/dev-bre/food-ordering-chatgpt-typechat/blob/main/server/coffeOrdersSchema.ts). The main part of this schema is definition of the Cart type:

```tsx
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: 

```jsx
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:

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

```tsx
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](https://docs.switchfeat.com), 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.

```jsx
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:

```jsx
const [showPremium, setShowPremium] = useState<boolean>(false);
```

Finally add the following `fetch` request to the component:

```jsx
 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:


![create segment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h581gtwda8d09pu5wx8f.png)

Then we need to create a new flag which gates the access to the premium feature using this segment:


![create flag](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bw3mwma8qmxv0foc1h7x.png)


Here is our new shiny flag which we can use in our API requests: 


![flags view](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ealdfpfjo6mk7u6spmha.png)

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.

{% youtube R94OGHljTvo%}

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](https://github.com/dev-bre/food-ordering-chatgpt-typechat) 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!

https://github.com/switchfeat-com/switchfeat

<img src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExZjV6NHRvazB6eDVlOXN0enNycGhsMW56cWN3d3hjYXQ1YXVjYnVwZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/uWlpPGquhGZNFzY90z/giphy.gif" />