Build Your Own ChatGPT Clone with React and the OpenAI API

📅 4 months ago 🕒 21 min read 🙎‍♂️ by Madza

Build Your Own ChatGPT Clone with React and the OpenAI API

Chatbots have become indispensable tools for businesses and developers seeking to improve customer interactions and streamline user experiences in today's rapidly evolving digital landscape.

OpenAI's ChatGPT has transformed from a cutting-edge experiment into a powerhouse in chatbot development. Its meteoric rise to success is nothing short of remarkable, captivating users worldwide.

In this tutorial, we will build a custom Chatbot application. You will be able to ask questions and receive high-quality answers. The bot will remember previous prompts, simulating context-aware conversation.

preview

The demo code of the project is available on CodeSandbox. You will have to provide your own OpenAI API key in the .env file to test it live. To get one, create an account on the OpenAI, log in, navigate to the API keys and generate a new API key.

1. Planning features and UI

The application will be based on React, we will use OpenAI API to access the data and use CSS modules for styling.

Utilizing React will allow us to create a dynamic and responsive user interface, enhancing the overall user experience.

OpenAI API will let us gain access to advanced language processing capabilities, providing data to create insightful interactions.

Additionally, CSS modules will allow us to maintain a modular design, facilitating efficient development and customization of the app.

The features we will be implementing include:

  1. A designated input area where users will be able to craft prompts that they wish to ask, inviting contextually relevant inquiries.

  2. The submit button that will allow users to submit their prompts to the API, initiating the conversation process.

  3. Message items that will be showcased as chat-style messages within the conversation window, enhancing the interactive chat experience.

  4. Message items to display ChatGPT replies that will provide a conversational flow.

  5. History feature that will list all of the user's recent prompts. This will also allow user to revisit previous conversations.

  6. The clear button that will allow the removal of generated content, offering a clean slate for new conversations.

Let's take a look at the component-based wireframe:

wireframe

The whole application will be wrapped in the main container, that will hold all of the elements together. It will be further divided into a 2-column layout.

The first column will include all of the messages from the user and ChatGPT. At the bottom of the column, there will be an input area and a button to submit the prompt.

The second column will hold the history of all of the recent prompts. At the bottom of the column, there will be a clear button that will allow the user to wipe the generated content.

2. Picking a color scheme

The application design will prioritize the ease of content perception.

This will allow us to provide a couple of important benefits:

Users will be able to quickly comprehend the presented information, leading to a more intuitive and user-friendly experience.

It will also enhance accessibility, ensuring that individuals of varying backgrounds and abilities will be able to easily navigate and engage with the content.

Let's take a look at the following color scheme:

design

The background of the application will be black, while the messages, history items, and input form will be dark grey.

The text on the messages and input backgrounds will be white, so it would have a nice contrast and be easy to read.

To give the app some highlights the column titles, submit button, and response message avatars will use a bright lime-green tone.

To accent the clear button, a mild red tone will be used. This will also avoid clicking user the button accidentally.

3. Setting up React app

We will use create-react-app to create our application. Run npx create-react-app react-chatgpt to create a new React project.

Wait for a minute for the setup to complete and then change the working directory to the newly created folder by cd react-chatgpt and run npm start to start the developer server.

This should open up your project in your default browser. If it's not, navigate to http://localhost:3000 to open it manually. You should be presented with React welcome screen:

welcome

4. Add global styling

We will add a global styling to establish a consistent and unified visual appearance across all components of the application.

Open index.css and include the following styling rules:

@import url("https://fonts.googleapis.com/css2?family=Varela+Round&display=swap");

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Varela Round", sans-serif;
}

body {
  background-color: #121212;
}

First, we imported the Varela Round font and set the whole app to use it.

We also removed any pre-defined margins and paddings, as well as set box-sizing rules to border-box, so the app looks the same on different browsers.

Finally, we set the background of the body to a dark tone, so that it would allow us to highlight the content of the application.

5. Download the media

We will need a couple of avatars to represent the authors of the messages from the user and OpenAI API. This way they will be easier to distinguish.

Create new folder icons inside the src directory and include the icons bot.png and user.png.

You can download samples from icons directory here, or you can use custom ones from sites like FlatIcon or Icons8, as long as you keep the above file names.

6. Building the components

First, we need a well-organized file structure that matches the wireframe design.

We will use the terminal to create the necessary folder and component files. Each component will have its own JavaScript file for functionality and CSS file for styling.

Change the working directory in the src folder by running cd src and then run the command mkdir components && cd components && touch Message.js Message.module.css Input.js Input.module.css History.js History.module.css Clear.js Clear.module.css.

The command above will first create components folder, then change the working directory to it, and create all the necessary files inside it.

Message component

This component will be displaying user prompts and API responses within the conversation, facilitating the real-time exchange of information between the user and the chatbot.

Open the Message.js file and include the following code:

import bot from "../icons/bot.png";
import user from "../icons/user.png";

import styles from "./Message.module.css";

export default function Message({ role, content }) {
  return (
    <div className={styles.wrapper}>
      <div>
        <img
          src={role === "assistant" ? bot : user}
          className={styles.avatar}
          alt="profile avatar"
        />
      </div>
      <div>
        <p>{content}</p>
      </div>
    </div>
  );
}

First, we imported the downloaded icons for avatars and then imported the external CSS rules for styling.

After that, we created the wrapper for the Message component that will contain both icons and text content.

We used the role prop in the conditional to display the appropriate avatar as the image src.

We also used the content prop which will be passed in as the text response from the OpenAI API and user input prompt.

Now, let's style the component so it looks like a chat message!

Open the Message.module.css file and include the following rules:

.wrapper {
  display: grid;
  grid-template-columns: 60px auto;
  min-height: 60px;
  padding: 20px;
  margin-bottom: 20px;
  border-radius: 10px;
  background-color: #1b1b1d;
}

.avatar {
  width: 40px;
  height: 40px;
}

We divided the layout into two columns with the avatars shown in the fixed-width container on the right and the text on the left.

Next, we added some padding and margin to the bottom of the message. We also styled the message to have round borders and set the background to dark grey.

Finally, we set the avatar icon to a fixed width and height.

Input component

This will be an interface element designed to capture user queries, serving as the means through which users interact and engage with the chatbot.

Open the Input.js file and include the following code:

import styles from "./Input.module.css";

export default function Input({ value, onChange, onClick }) {
  return (
    <div className={styles.wrapper}>
      <input
        className={styles.text}
        placeholder="Your prompt here..."
        value={value}
        onChange={onChange}
      />
      <button className={styles.btn} onClick={onClick}>
        Go
      </button>
    </div>
  );
}

We first imported the external style sheet to style the component.

We returned the component wrapper that includes the input field for the user prompts and the button to submit it to the API.

We set the placeholder value to be displayed when the input form is empty, created the value prop to hold the entered prompt as well as the onChange prop that will be called once the input value changes.

For the button, the onClick prop will be called once the user clicks on the button.

Now, let's style the component so that the input area looks beautiful and the user is encouraged to provide prompts!

Open the Input.module.css file and include the following rules:

.wrapper {
  display: grid;
  grid-template-columns: auto 100px;
  height: 60px;
  border-radius: 10px;
  background-color: #323236;
}

.text {
  border: none;
  outline: none;
  background: none;
  padding: 20px;
  color: white;
  font-size: 16px;
}

.btn {
  border: none;
  border-radius: 0 10px 10px 0;
  font-size: 16px;
  font-weight: bold;
  background-color: rgb(218, 255, 170);
}

.btn:hover {
  cursor: pointer;
  background-color: rgb(200, 253, 130);
}

We set the wrapper to be divided into two columns with a fixed width for the button and the rest of the available width to be dedicated to the input area.

We also defined the specific height of the component, set the rounded borders for it as well as set the background to dark grey.

For the input area, we removed the default border, outline, background and added some padding. We set the text to be white and set a specific font size.

History component

This component will display the sequence of past user and chatbot interactions, providing users with a contextual reference of their conversation.

Open the History.js file and include the following code:

import styles from "./History.module.css";

export default function History({ question, onClick }) {
  return (
    <div className={styles.wrapper} onClick={onClick}>
      <p>{question.substring(0, 15)}...</p>
    </div>
  );
}

We first imported the external style rules for the component.

Then we returned the wrapper that will include the text.

The text value will be passed in as a question prop from the user prompt and only the first 15 characters of the text string will be displayed.

Users will be allowed to click on the history items and we will pass the onClick prop to control the click behavior.

Now, let's style the component to ensure it is visually appealing and fits well in the sidebar!

Open the History.module.css file and include the following rules:

.wrapper {
  padding: 20px;
  margin-bottom: 20px;
  border-radius: 10px;
  background-color: #1b1b1d;
}

.wrapper:hover {
  cursor: pointer;
  background-color: #323236;
}

We set some padding, added the margin to the bottom, and set the rounded corners for the history items. We also set the background color to dark grey.

Once the user hovers over the item, the cursor will change to a pointer and the background color will change to a lighter shade of grey.

Clear component

This will be UI element designed to reset or clear the ongoing conversation, providing users with a quick way to start a new interaction without navigating away from the current interface.

Open the Clear.js file and include the following code:

import styles from "./Clear.module.css";

export default function Clear({ onClick }) {
  return (
    <button className={styles.wrapper} onClick={onClick}>
      Clear
    </button>
  );
}

We first imported the external style sheet to style the component.

We returned the button that will allow the user to clear the content of the application. We will pass onClick prop to achieve the desired behavior.

Now, let's style the component to make it stand out and reduce the chances of users pressing it accidentally!

Open the Clear.module.css file and include the following rules:

.wrapper {
  width: 100%;
  height: 60px;
  background-color: #ff9d84;
  border: none;
  border-radius: 10px;
  font-size: 16px;
  font-weight: bold;
}

.wrapper:hover {
  cursor: pointer;
  background-color: #ff886b;
}

We set the button to fill the available width of the column, set the specific height, and set the background color to mild red.

We also removed the default border, set the rounded corners, set a specific font size, and made it bold.

On the hover, the cursor will change to the pointer and the background color will change to a darker shade of red.

7. Building the user interface

In the previous section, we built all of the necessary components, now let's put them together and build the user interface for the application.

We will configure their functionality to create a functional and interactive chatbot interface with organized and reusable code.

Open the App.js file and include the following code:

import { useState } from "react";

import Message from "./components/Message";
import Input from "./components/Input";
import History from "./components/History";
import Clear from "./components/Clear";

import "./styles.css";

export default function App() {
  const [input, setInput] = useState("");
  const [messages, setMessages] = useState([]);
  const [history, setHistory] = useState([]);

  return (
    <div className="App">
      <div className="Column">
        <h3 className="Title">Chat Messages</h3>
        <div className="Content">
          {messages.map((el, i) => {
            return <Message key={i} role={el.role} content={el.content} />;
          })}
        </div>
        <Input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onClick={input ? handleSubmit : undefined}
        />
      </div>
      <div className="Column">
        <h3 className="Title">History</h3>
        <div className="Content">
          {history.map((el, i) => {
            return (
              <History
                key={i}
                question={el.question}
                onClick={() =>
                  setMessages([
                    { role: "user", content: history[i].question },
                    { role: "assistant", content: history[i].answer },
                  ])
                }
              />
            );
          })}
        </div>
        <Clear onClick={clear} />
      </div>
    </div>
  );
}

First, we imported the useState hook that we will use to track the data state for the application. Then we imported all of the components we built and the external style sheet for styling.

Then we created the input state variable to store the user prompt input, messages to store the conversation between the user and ChatGPT, and history to store the history of user prompts.

We created the main wrapper for the whole app that will hold two columns.

Each column will have a title and content wrapper that will include the conversation messages, input area, and submit button for the first column and history items and the clear button for the second column.

The conversation messages will be generated by mapping through the messages state variable and the history items - by mapping through the history state variable.

We set the input onChange prop to update the input state variable each time user enters any value in the input form.

Once the user clicks the send button the user prompt will be sent to the OpenAI API to process and receive the reply.

For the history items, we set the onClick prop so that the messages state variable gets updated to the specific prompt and answer.

Finally, for the clear button, we passed onClick prop a function that will clear both the message and history values, clearing the application data.

8. Creating the App layout

In this section, we will arrange the user interface components to create an intuitive structure for effective user interaction.

Open App.css and include the following styling rules:

.App {
  display: grid;
  grid-template-columns: auto 200px;
  gap: 20px;
  max-width: 1000px;
  margin: 0 auto;
  min-height: 100vh;
  padding: 20px;
}

.Column {
  color: white;
}

.Title {
  padding: 20px;
  margin-bottom: 20px;
  border-radius: 10px;
  color: black;
  background-color: rgb(218, 255, 170);
}

.Content {
  height: calc(100vh - 200px);
  overflow-y: scroll;
  margin-bottom: 20px;
}

::-webkit-scrollbar {
  display: none;
}

We split the main app wrapper into two columns separated by a gap by using the grid layout and set the left column for history items to a fixed width.

Next, we set the wrapper to never exceed a certain width, centered it on the screen, made it use all of the screen viewport height, and added some padding inside it.

For each column's contents, we set the text color to white.

For the column titles, we set some padding, added the bottom margin, and set the rounded corners. We also set the title element background color to lime green and set the text color to black.

We also styled the columns themselves by setting the rule that the content would not exceed a certain height and set the content to be scrollable if it reaches outside the height. We also added a margin to the bottom.

We also hide the scrollbars, so that we would not have to style them to override the default values for each browser. This rule is optional and you could leave it out.

9. Getting API key from OpenAI

If you did not already set up the API key for the Sandbox in the introduction of this tutorial, make sure to create an account on the OpenAI website.

Next, log in and navigate to the API keys and generate a new API key.

api

Copy the key to the clipboard and open your project.

Create a new file .env in your project root and paste the value for the following key like this: REACT_APP_OPENAI_API_KEY=paste-your-code-here.

10. Preparing Request call to OpenAI API

Through the OpenAI API, your chatbot will be able to send textual prompts to the OpenAI server, which will then then process the input and generate human-like responses.

This is achieved by leveraging a powerful language model that has been trained on diverse text sources. By providing the model with a conversation history and the current user prompt, your chatbot will receive context-aware responses from the API.

In this section, we will prepare the request and implement the call to the API to receive the response and set the data to the state variable we defined earlier.

Open the App.js again and add the code below as shown below:

// imported modules ...

export default function App() {
  // useState variables ...

  const handleSubmit = async () => {
    const prompt = {
      role: "user",
      content: input,
    };

    setMessages([...messages, prompt]);

    await fetch("https://api.openai.com/v1/chat/completions", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.REACT_APP_OPENAI_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        model: "gpt-3.5-turbo",
        messages: [...messages, prompt],
      }),
    })
      .then((data) => data.json())
      .then((data) => {
        const res = data.choices[0].message.content;
        setMessages((messages) => [
          ...messages,
          {
            role: "assistant",
            content: res,
          },
        ]);
        setHistory((history) => [...history, { question: input, answer: res }]);
        setInput("");
      });
  };

  const clear = () => {
    setMessages([]);
    setHistory([]);
  };

  return <div className="App">// returned elements ...</div>;
}

First, we created a separate function handleSubmit, which will be executed once the user has entered the prompt in the input form and clicks the submit button.

Inside handleSubmit, we first created the prompt variable that will hold the role user and the prompt itself as an object. The role is important since when storing our messages, we will need to know which ones are user messages.

Then we updated the messages state variable with the user prompt.

Next, we made an actual fetch call to the https://api.openai.com/v1/chat/completions endpoint to access the data from the OpenAI API.

We specified, that it is a POST request, and set the headers with the authorization token and the content type. For the body parameters, we specified which API model to use and passed the messages variable as the content from the user.

Once the response is received we store it in the res variable. We added the object consisting of the role assistant and the response itself to the message state variable.

We also updated the history state variable with the object with the question and corresponding answer as the keys.

After the response was received and state variables were updated, we cleared the input state variable to prepare the input form for the next user prompt.

Finally, we created a simple clear function to clear the messages and history state variables, allowing the user to clear the data of the application.

11. Testing the application

Congratulations, at this point you should be created a fully functional chat application! The last thing left to do is to test the application.

First, let's try to ask ChatGPT a single question.

single

Notice, the question was submitted and the answer was received.

Now let's try to create a conversation.

multiple

As you can see the chatbot remembers the context from the previous messages, so you can speak with it while being fully context-aware.

Now let's see what happens once you click on the history item.

history

Notice the chat switched to the respective user prompt and answer.

This could be useful if you want to resume the conversation from a specific point.

Finally, let's click on the Clear button.

clear

As expected the contents from the app were cleared. Useful if there is a lot of content and the user wants to start fresh.

12. Conclusion

In this tutorial, we learned how to create easy to use user interface, how to structure your code via components, how to work with states, how to make API calls, and how to process the received data.

With the combination of advanced natural language processing capabilities of the OpenIAI API and the flexibility of React, you will now be able to create sophisticated chatbot applications, that you could customize further to your liking.

Notice, this tutorial stores the API key in the front end which might not be secure for the production. If you would want to deploy the project, you would want to create an Express server and use the API key there.

Also, if you want the history prompts to be available after the next initial launch, you could store and then read them from local storage or even connect a database to your app and store and read data from there.