Tutorial - Build a chatbot with React and OpenAI
Published: 2023-02-21 16:11:00

---
title: Tutorial - Build a chatbot with React and OpenAI
published: true
description: 
tags: 
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5bti85j6lxclwb3zksjb.png
# Use a ratio of 100:42 for best results.
published_at: 2023-02-21 16:11 +0000
---

> This article is the second article and tutorial sent on the [frontendfresh.com](https://www.frontendfresh.com/) newsletter. Subscribe to the Front-end Fresh newsletter to get front-end engineering tips, tutorials, and projects sent to your inbox on a weekly basis!

[Last week](https://newsletter.frontendfresh.com/archive/tutorial-interacting-with-openai-in-nodejs-and/), we built a Node.js/Express server that exposes an `/ask` endpoint. When this endpoint is triggered and we include a text `prompt`, the endpoint interacts with OpenAI's `/completions` API to generate and return a continuation of that text.

When we tested this with an example prompt like `"How is the weather in Dubai?"`, the API returned a valid answer to us.

![image.png](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/8eb32f4a-63c9-490d-871d-544175354a1a.png)

Today, we're going to build a User Interface (i.e. UI) that resembles a chatbot where the user can type a question and receive an answer from the Node.js backend API we created.

## Scaffolding a React app

We'll be building the UI of our app with the [React](https://reactjs.org/) JavaScript library. To get started, we'll first want to scaffold a React development environment quickly and we'll do this with the help of [Vite](https://vitejs.dev/).

I have plans on writing an email that does a bit more of a deep-dive into Vite but in summary, Vite is a build tool and development server that is designed to optimize the development experience of modern web applications. Think [Webpack](https://webpack.js.org/) but with faster build/start times and a few additional improvements.

To get started in scaffolding our React app, we'll follow the [Getting Started documentation](https://vitejs.dev/guide/) section of Vite, and we'll run the following in our terminal.

```shell
npm create vite@latest
```

We'll then be given a few prompts to fill. We'll state that we'll want our project to be named `custom_chat_gpt_frontend` and we'll want it to be a React/JavaScript app.

```shell
$ npm create vite@latest
✔ Project name: custom_chat_gpt_frontend
✔ Select a framework: › React
✔ Select a variant: › JavaScript
```

We can then navigate into the project directory and run the following to install the project dependencies.

```shell
npm install
```

When the project dependencies have finished installing, we'll run our front-end server with:

```shell
npm run dev
```

We'll then be presented with the running scaffolded application at [http://localhost:5173/](http://localhost:5173/).

![image.png](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/4e02920d-5d92-4ba2-a3e5-147a1c57a1d8.png)

## Creating the markup & styles

We'll begin our work by first focusing on building the markup (i.e. HTML/JSX) and styles (i.e. CSS) of our app.

In the scaffolded React application, we'll notice a bunch of files and directories have been created for us. We'll be working entirely within the `src/` directory. To get things started, we'll modify the autogenerated code in our `src/App.jsx` component to simply return "Hello world!".

```jsx
import "./App.css";

function App() {
  return <h2>Hello world!</h2>;
}

export default App;
```

We'll remove the scaffolded CSS styles in our `src/index.css` file and only have the following.

```css
html,
body,
#root {
  height: 100%;
  font-size: 14px;
  font-family: arial, sans-serif;
  margin: 0;
}
```

And in the `src/App.css` file, we'll remove all the initially provided CSS classes.

```css
/* App.css CSS styles to go here */
/* ... */
```

Saving our changes, we'll be presented with a "Hello world!" message.

![image.png](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/746ed435-b787-454d-bc78-ef9e4cf1fcae.png)

We won't spend a lot of time in this email breaking down _how_ our UI is styled. To summarize quickly, our final app will only contain a single input field section that both captures what the user types and the returned answer from the API.

![image.png](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/a59127ad-c349-4c59-9b01-e99101da729b.png)

We'll style the UI of our app with standard CSS. We'll paste the following CSS into our `src/App.css` file which will contain all the CSS we'll need.

```css
.app {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: rgba(0, 0, 0, 0.1);
}

.app-container {
  width: 1000px;
  max-width: 100%;
  padding: 0 20px;
  text-align: center;
}

.spotlight__wrapper {
  border-radius: 12px;
  border: 1px solid #dfe1e5;
  margin: auto;
  max-width: 600px;
  background-color: #fff;
}

.spotlight__wrapper:hover,
.spotlight__wrapper:focus {
  background-color: #fff;
  box-shadow: 0 1px 6px rgb(32 33 36 / 28%);
  border-color: rgba(223, 225, 229, 0);
}

.spotlight__input {
  display: block;
  height: 56px;
  width: 80%;
  border: 0;
  border-radius: 12px;
  outline: none;
  font-size: 1.2rem;
  color: #000;
  background-position: left 17px center;
  background-repeat: no-repeat;
  background-color: #fff;
  background-size: 3.5%;
  padding-left: 60px;
}

.spotlight__input::placeholder {
  line-height: 1.5em;
}

.spotlight__answer {
  min-height: 115px;
  line-height: 1.5em;
  letter-spacing: 0.1px;
  padding: 10px 30px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.spotlight__answer p::after {
  content: "";
  width: 2px;
  height: 14px;
  position: relative;
  top: 2px;
  left: 2px;
  background: black;
  display: inline-block;
  animation: cursor-blink 1s steps(2) infinite;
}

@keyframes cursor-blink {
  0% {
    opacity: 0;
  }
}
```

We'll now move towards establishing the markup/JSX of our `<App />` component. In the `src/App.jsx` file, we'll update the component to first return a few wrapper `<div />` elements.

```jsx
import "./App.css";

function App() {
  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          /* ... */
        </div>
      </div>
    </div>
  );
}

export default App;
```

Within our wrapper elements, we'll place an `<input />` element and a `<div />` element to represent the input section and the answer section respectively.

```jsx
import "./App.css";
import lens from "./assets/lens.png";

function App() {
  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          <input
            type="text"
            className="spotlight__input"
            placeholder="Ask me anything..."
            style={{
              backgroundImage: `url(${lens})`,
            }}
          />
          <div className="spotlight__answer">
            Dubai is a desert city and has a warm and sunny climate throughout
          </div>
        </div>
      </div>
    </div>
  );
}

export default App;
```

For the `<input />` element, we're adding an inline `backgroundImage` style property where the value is the `.png` image of a magnifying glass that we've saved in our `src/assets/` directory. You can find a copy of this image [here](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/f94fa1e8-1ceb-421a-9034-d7b0a3ff4d95.png).

With our changes saved, we'll now be presented with the UI of the app the way we expected it to look.

![image.png](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/12218ce9-3103-4676-9736-de28c9b880dd.png)

## Capturing the prompt value

Our next step is to capture the `prompt` value the user is typing. This needs to be done since we intend to send this value to the API when the input has been submitted. We'll capture the user input value in a state property labeled `prompt` and we'll initialize it with `undefined`.

```jsx
import { useState } from "react";
import "./App.css";
import lens from "./assets/lens.png";

function App() {
  const [prompt, updatePrompt] = useState(undefined);

  return (
    /* ... */
  );
}

export default App;
```

When the user types into the `<input />` element, we'll update the state `prompt` value by using the `onChange()` event handler.

```jsx
import { useState } from "react";
import "./App.css";
import lens from "./assets/lens.png";

function App() {
  const [prompt, updatePrompt] = useState(undefined);

  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          <input
            // ...
            onChange={(e) => updatePrompt(e.target.value)}
          />
          // ...
        </div>
      </div>
    </div>
  );
}

export default App;
```

We want the input to be "submitted" at the moment the user presses the "Enter" key. To do this, we'll use the `onKeyDown()` event handler and have it trigger a `sendPrompt()` function we'll create.

In the `sendPrompt()` function, we'll return early if the user enters a key that is not the `"Enter"` key. Otherwise, we'll `console.log()` the `prompt` state value.

```jsx
import { useState } from "react";
import "./App.css";
import lens from "./assets/lens.png";

function App() {
  const [prompt, updatePrompt] = useState(undefined);

  const sendPrompt = async (event) => {
    if (event.key !== "Enter") {
      return;
    }

    console.log('prompt', prompt)
  }

  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          <input
            // ...
            onChange={(e) => updatePrompt(e.target.value)}
            onKeyDown={(e) => sendPrompt(e)}
          />
          // ...
        </div>
      </div>
    </div>
  );
}

export default App;
```

Now, if we type something into the input and press the "Enter" key, we'll be presented with that input value in our console.

![image.png](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/e0fcb346-4629-4a53-9ae0-e7b56939a490.png)

## Triggering the API

The final step to our implementation is triggering the API when the user presses the "Enter" key after typing a prompt in the input. 

We'll want to capture two other state properties that will reflect the information of our API request — the `loading` state of our request and the `answer` returned from a successful request. We'll initialize `loading` with `false` and `answer` with `undefined`.

```jsx
import { useState } from "react";
import "./App.css";
import lens from "./assets/lens.png";

function App() {
  const [prompt, updatePrompt] = useState(undefined);
  const [loading, setLoading] = useState(false);
  const [answer, setAnswer] = useState(undefined);

  const sendPrompt = async (event) => {
    // ...
  }

  return (
    // ...
  );
}

export default App;
```

In our `sendPrompt()` function, we'll use a `try/catch` statement to handle errors that may occur from the asynchronous request to our API.

```jsx
const sendPrompt = async (event) => {
  if (event.key !== "Enter") {
    return;
  }

  try {
  
  } catch (err) {
  
  }
}
```

At the beginning of the `try` block, we'll set the state `loading` property to `true`. We'll then prepare our request options and then use the native browser [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) method to trigger our request. We'll make our request hit an endpoint labeled `api/ask` (we'll explain why in a second).

```jsx
const sendPrompt = async (event) => {
  if (event.key !== "Enter") {
    return;
  }

  try {
    setLoading(true);

    const requestOptions = {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ prompt }),
    };

    const res = await fetch("/api/ask", requestOptions);
  } catch (err) {
  
  }
}
```

If the response is not successful, we'll throw an error (and `console.log()` it). Otherwise, we'll capture the response value and update our `answer` state property with it.

This makes our `sendPrompt()` function in its complete state look like the following:

```ts
const sendPrompt = async (event) => {
  if (event.key !== "Enter") {
    return;
  }

  try {
    setLoading(true);

    const requestOptions = {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ prompt }),
    };

    const res = await fetch("/api/ask", requestOptions);

    if (!res.ok) {
      throw new Error("Something went wrong");
    }

    const { message } = await res.json();
    setAnswer(message);
  } catch (err) {
    console.error(err, "err");
  } finally {
    setLoading(false);
  }
};
```

Before we move towards testing that our request works as expected, we'll add a few more changes to our component.

When our `loading` state property is `true`, we'll want the input to be disabled and we'll also want to display a spinning indicator in place of the magnifying lens image (to convey to the user that the request is "loading").
 
We'll display a spinning indicator by conditionally dictating the value of the `backgroundImage` style of the `<input />` element based on the status of the `loading` value. We'll use this [spinner GIF](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/aa42cf2f-a63b-4f91-a82e-cf53c5a8446e.png) that we'll save int our `src/assets/` directory.

```jsx
import { useState } from "react";
import "./App.css";
import loadingGif from "./assets/loading.gif";
import lens from "./assets/lens.png";

function App() {
  // ...

  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          <input
            // ...
            disabled={loading}
            style={{
              backgroundImage: loading ? `url(${loadingGif})` : `url(${lens})`,
            }}
            // ...
          />
          // ...
        </div>
      </div>
    </div>
  );
}
```

In the answer section of our markup, we'll conditionally add a paragraph tag that contains the `{answer}` value if it is defined.

```jsx
import { useState } from "react";
import "./App.css";
import loadingGif from "./assets/loading.gif";
import lens from "./assets/lens.png";

function App() {
  // ...

  return (
    <div className="app">
      <div className="app-container">
        <div className="spotlight__wrapper">
          // ...
          <div className="spotlight__answer">{answer && <p>{answer}</p>}</div>
        </div>
      </div>
    </div>
  );
}
```

The last thing we'll want to do is have the `{answer}` state value set back to `undefined` if the user ever clears the input. We'll do this with the help of the React `useEffect()` Hook.

```jsx
import { useState, useEffect } from "react";
// ...

function App() {
  const [prompt, updatePrompt] = useState(undefined);
  const [loading, setLoading] = useState(false);
  const [answer, setAnswer] = useState(undefined);

  useEffect(() => {
    if (prompt != null && prompt.trim() === "") {
      setAnswer(undefined);
    }
  }, [prompt]);

  // ...

  return (
    // ...
  );
}

export default App;
```

That's all the changes we'll make to our `<App />` component! There's one small thing we have to do before we can test our app.

## Proxying the request

In our Vite React project, we want to make API requests to a backend server running on a different origin (i.e. a different port at `localhost:5000`) than the one the web application is served from (`localhost:5173`). However, due to the [same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) enforced by web browsers, such requests can be blocked for security reasons.

To get around this when working within a development environment, we can set up a reverse proxy on the frontend server (i.e. our Vite server) to forward requests to the backend server, effectively making the backend server's API available on the same origin as the frontend application.

Vite allows us to do this by modifying the [`server.proxy`](https://vitejs.dev/config/server-options.html#server-proxy) value in the Vite configuration file (which is `vite.config.js`).

In the `vite.config.js` file that already exists in our project, we'll specify the proxy to be the `/api` endpoint. The `/api` endpoint will get forwarded to `http://localhost:5000`.

```js
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:5000",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },
});
```

Now, when our front-end makes a request to `/api/ask`, it gets forwarded to the backend server running at `http://localhost:5000/ask`.

## Testing our app

We've finished building our simple chatbot app. Let's test our work!

First, we need to have our Node/Express server from the last tutorial running. We'll navigate into that project directory and run `node index.js` to get that going.

```shell
$ custom_chat_gpt: node index.js
```

We'll save our changes in our front-end app, and restart the front-end server.

```shell
$ custom_chat_gpt_frontend: npm run dev
``` 

In the UI of our front-end app, we'll provide a prompt and press "Enter". There should be a brief loading period before the answer is then populated and shown to us!

![custom_chat_gpt_frontend.gif](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/7b3f745a-e120-4c05-bcb7-24315b02913c.gif) 

We can even try and ask our chatbot something more specific like `"What are the best doughnuts in Toronto Canada?"`.

![custom_chat_gpt_frontend_2.gif](https://buttondown-attachments.s3.us-west-2.amazonaws.com/images/ab6ea49c-59a0-4098-8c2a-b617dd9f46e7.gif)

Funny enough, when I search for the Castro's Lounge bakery here in Toronto, I get a [bar and live-music venue](https://www.google.com/search?q=castro%27s+lounge+doughnuts+toronto&sxsrf=AJOqlzUXJmwG8RgrwLoRQeQD9Y3Kk7_HDg%3A1676409710787&source=hp&ei=bvvrY-GdKtfH0PEP-62DsAg&iflsig=AK50M_UAAAAAY-wJfqOKi-4NU6bl_eqXQ1Y_DQMVPNrC&ved=0ahUKEwjhjLfq-JX9AhXXIzQIHfvWAIYQ4dUDCAs&uact=5&oq=castro%27s+lounge+doughnuts+toronto&gs_lcp=Cgdnd3Mtd2l6EAMyBAgjECdQAFgAYKYCaABwAHgAgAFriAFrkgEDMC4xmAEAoAECoAEB&sclient=gws-wiz), not a bakery. And Glazed & Confused Donuts appears to be in [Syracuse, New York](https://www.google.com/search?q=glazed+%26+confused+&sxsrf=AJOqlzW7hXrn7yTyDMTrwB2FwiqX8D1p_g%3A1676409625525&ei=GfvrY-jTH-KuptQP_f-0gAk&ved=0ahUKEwioxOjB-JX9AhVil4kEHf0_DZAQ4dUDCBA&uact=5&oq=glazed+%26+confused+&gs_lcp=Cgxnd3Mtd2l6LXNlcnAQAzIECCMQJzINCC4QgAQQxwEQrwEQCjIHCAAQgAQQCjINCC4QgAQQxwEQrwEQCjIHCC4QgAQQCjIHCAAQgAQQCjIFCAAQgAQyBwgAEIAEEAoyDQguEIAEEMcBEK8BEAoyDQguEIAEEMcBEK8BEAo6CggAEEcQ1gQQsANKBAhBGABKBAhGGABQuwJYuwJgjQVoAXABeACAAYABiAGAAZIBAzAuMZgBAKABAcgBCMABAQ&sclient=gws-wiz-serp) — not Toronto. It looks like there's room to fine-tune our chatbot a bit better — we'll talk about this in our last tutorial email of this series, next week 🙂.

## Closing thoughts

- You can find the source code for this article at [frontend-fresh/articles_source_code/custom_chat_gpt_frontend/](https://github.com/djirdehh/frontend-fresh/tree/main/articles_source_code/custom_chat_gpt_frontend).
- To control the length of the information returned from OpenAI's `/completions` endpoint, you can modify the `max_tokens` setting field in the OpenAI configuration (see example [here](https://github.com/djirdehh/frontend-fresh/blob/main/articles_source_code/custom_chat_gpt/index.js#L26)).
- Subscribe to https://www.frontendfresh.com/ for more tutorials like this to hit your inbox on a weekly basis!

That's it for today! 🙂

— Hassan ([@djirdehh](https://twitter.com/djirdehh))