📅 10 months ago 🕒 21 min read 🙎♂️ by Madza
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.
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.
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:
A designated input area where users will be able to craft prompts that they wish to ask, inviting contextually relevant inquiries.
The submit button that will allow users to submit their prompts to the API, initiating the conversation process.
Message items that will be showcased as chat-style messages within the conversation window, enhancing the interactive chat experience.
Message items to display ChatGPT replies that will provide a conversational flow.
History feature that will list all of the user's recent prompts. This will also allow user to revisit previous conversations.
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:
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.
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:
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
.
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.
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.
Notice, the question was submitted and the answer was received.
Now let's try to create a conversation.
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.
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.
As expected the contents from the app were cleared. Useful if there is a lot of content and the user wants to start fresh.
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.