TL;DR
In this project, we will build an AI Chatbot application using React, TypeScript, and the Groq Cloud AI API.
This project is designed for beginners who want to learn React and TypeScript while building a practical application. By the end of this tutorial, you'll have a fully functional chatbot that can interact with users and save chat history using local storage.
I got stuck learning Vanilla JavaScript for weeks so I decided to jump head first into React. Then I said to myself, "Why not use TypeScript instead of Vanilla JavaScript?"
I used to see TypeScript as a big deal. I thought I would need to learn new syntax and everything. But trust me, if you have a good knowledge of JavaScript, or at least if you have taken a JavaScript course beyond the foundations, you can understand and start using TypeScript in less than two hours.
To get myself up and running in React and TypeScript, I took this one-hour course on YouTube on basic React and TypeScript. After taking the course, I decided to build a project using everything I learned.
That's how this project was born.
Enough Talking. Now let's dive right in.
Here is the finished version of the project we will be building:
Prerequisite Knowledge
Before diving into this tutorial, you should have an understanding of the following technologies:
- HTML & CSS: Intermediate understanding of web development.
- JavaScript: Intermediate knowledge, including ES6+ features (e.g., classes, modules, arrow functions), DOM manipulation, and asynchronous programming.
- React: Basic understanding of React components, props, and state.
- TypeScript: Basic understanding of TypeScript, including type annotations and interfaces.
- Node.js & npm: Basic knowledge of setting up a Node.js project and using npm.
This Youtube course here is all you need to get started if you already know beyond foundations of HTML, CSS, and JavaScript. So, if you are like me and just know intermediate HTML, CSS, and JavaScript and No knowledge of React and TypeScript, and how to set up a React App with Typescript and you want to build along with me, I recommend taking the course and then comeback.
Tools & Set up
We'll be using Visual Studio Code as our code editor. Visual Studio Code is recommended for its powerful extensions, integrated terminal, and excellent support for TypeScript and React.
To create the React app, we'll use Vite, which offers faster build times and modern tooling compared to Create React App
Creating a React App
- To create the react app, run
npm create vite@latest
on your terminal window. - On the prompt to rename it choose the name you want
- When asked to choose a framework, choose
React
. - Finally, when asked to choose a variant, choose
TypeScript
.
Now run:
-
cd your-project-name
to switch to the project, then -
npm install
to install all packages and dependencies, and finally -
npm run dev
to run your web server.
If you have gotten to this point, congratulations, you have created your React web app
Next step- Building the components:
Inside your src folder, create a components folder using mkdir components
We'll create five components to build the UI:
-
AppName.tsx
: Displays the app name. -
Headings.tsx
: Displays the welcome message. -
SearchBar.tsx
: Contains the input field for user messages and the send button to send the button to the API. -
Button.tsx
: Renders buttons for sending messages and clearing chat. -
Chat.tsx
: Displays the chat messages.
So, inside your components folder, create these four components using:
touch AppName.tsx Headings.tsx SearchBar.tsx Button.tsx Chat.tsx
Now open these components in your text editor.
Let's start building
AppName.tsx
import { ReactNode } from 'react';
interface Props {
children: ReactNode;
}
const AppName = ({ children }: Props) => {
return <div className="app-name">{children}</div>;
};
export default AppName;
Headers.tsx
import { ReactNode } from 'react';
interface Props {
children: ReactNode;
}
const Headings = ({ children }: Props) => {
return <div>{children}</div>;
};
export default Headings;
searchBar.tsx
import { ReactNode } from 'react';
interface Props {
children: ReactNode;
}
const SearchBar = ({ children }: Props) => {
return <div className="searchBar">{children}</div>;
};
export default SearchBar;
Button.tsx
interface Props {
textContent: string;
handleClick: () => void;
// optional logic to disable the button
disabled?: boolean;
}
const Button = ({ textContent, handleClick, disabled }: Props) => {
return (
<button type="submit" onClick={handleClick} disabled={disabled}>
{textContent}
</button>
);
};
export default Button;
Chat.tsx
import { ReactNode } from 'react';
interface Props {
children: ReactNode;
}
const Chat = ({ children }: Props) => {
return <div className="chat">{children}</div>;
};
export default Chat;
Now let's add these components to our App
App.tsx
// App.tsx
import AppName from './components/AppName';
import Chat from './components/Chat';
import Headings from './components/Headings';
import SearchBar from './components/SearchBar';
import Button from './components/Button';
const App = () => {
const handleSearch = () => {
alert('Search button clicked!');
};
return (
<>
<AppName>
<div>
<span>Grëg's </span>ChatBot
</div>
</AppName>
<div>
<Headings>
<div>
<h1>Hi, Welcome.</h1>
</div>
<div>
<h3>How can I help you today?</h3>
</div>
</Headings>
</div>
<div className="chat-container">
<Chat>
<div> </div>
</Chat>
</div>
<div className="searchBar-container">
<SearchBar>
<textarea
className="search-input"
placeholder="Enter your text"
/>
<Button
textContent="Send"
handleClick={handleSearch}
/>
</SearchBar>
</div>
</>
);
};
export default App;
This is the result you will get:
Next step, we set the 'send' button to print the input Value in the Chat component:
// App.tsx
import AppName from './components/AppName';
import Button from './components/Button';
import Chat from './components/Chat';
import Headings from './components/Headings';
import SearchBar from './components/SearchBar';
import { useState } from 'react';
const App = () => {
// State to manage the input value
const [inputValue, setInputValue] = useState('');
// State to manage chat messages
const [chatMessages, setChatMessages] = useState<string[]>([]);
// Function to handle input change
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(event.target.value);
};
// Function to handle button click
const handleSend = () => {
if (inputValue.trim() === '') return;
// Add the input value to the chat messages
setChatMessages([...chatMessages, inputValue]);
// Clear the input field
setInputValue('');
};
return (
<>
<AppName>
<div>
<span>Grëg's </span>ChatBot
</div>
</AppName>
<div>
<Headings>
<div>
<h1>Hi, Welcome.</h1>
</div>
<div>
<h3>How can I help you today?</h3>
</div>
</Headings>
</div>
<div className="chat-container">
<Chat>
{/* Render chat messages */}
{chatMessages.map((message, index) => (
<div key={index} className="chat-message">
{message}
</div>
))}
</Chat>
</div>
<div className="searchBar-container">
<SearchBar>
<textarea
className="search-input"
placeholder="Enter your text"
value={inputValue}
onChange={handleInputChange}
/>
<Button textContent="Send" handleClick={handleSend} />
</SearchBar>
</div>
</>
);
};
export default App;
Result:
Break down of what we did:
1- State Management:
We used the
useState
hook to manage the state of our application. This hook allows us to create and update state variables in functional components.inputValue: Stores the current value of the textarea.
chatMessages: Stores an array of chat messages.
2- Input Change Handler:
- handleInputChange: Updates the 'inputValue' state whenever the textarea value changes.
3- Button Click Handler:
- handleSend: Adds the current 'inputValue' to the 'chatMessages' array and clears the textarea.
4- Render Logic:
- The chat messages are rendered by mapping over the 'chatMessages' array and displaying each message in a div.
You Catch? Any Questions? Please drop it in the comments.
Moving on!!!
Now we get to the most exciting part of the project. Can you guess the next step?
Yes!! You are correct guys. Next step is to grap our 'Groq Cloud' API Key and then connect the API to our app.
Are you Ready Guys?
What is Groq Cloud?
Groq Cloud API is a cloud-based service that provides access to Groq's powerful AI hardware and software. This allows developers to offload complex AI computations to Groq's infrastructure, which is optimized for high-performance AI workloads. By integrating Groq Cloud API into their applications, developers can perform tasks such as:
- Model inference: Running AI models for tasks like image recognition and natural language processing.
- Data processing: Performing complex data transformations and analysis.
- Real-time predictions: Generating real-time predictions based on input data.
Groq Cloud API is designed to be scalable and secure, ensuring that developers can handle varying workloads and protect their data. The API is also user-friendly, with comprehensive documentation and SDKs available for popular programming languages.
To learn more, check out their website and Groq Cloud API documentation.
Why Use Groq API For This Project?
When I wanted to build this, I planned on using OpenAI's GPT4 API but it wasn't free. So I needed a free option. And I got Groq API.
Before this, I have neither heard of 'Groq cloud' nor 'Groq API' until I researched on free LLM API's to build with.
Therefore, I used it because IT'S FREE... And, it's ease of use.
Let's get back to business.
Next, let's grab your Groq API key.
Head over to the Groq Console: https://console.groq.com/ , sign up, read the docs (if you wish), and create your API Key.
If you have done that, now let's integrate that into our APP
Step 1: Install the Groq SDK
First, you need to install the Groq SDK if you haven't already. You can do this using npm or yarn:
npm install --save groq-sdk
Step 2: Step 2: Set Up Environment Variables
Since we initiated our project using Vite, create a .env
file in the root folder of your project. Then Add your Api key in the file like so:
VITE_REACT_APP_GROQ_API_KEY=<your-api-key-here>
Easy pizzy... Yes?
Step 3: Initialize the Groq Client
Initialize the Groq client with your API key in your App.tsx
file:
import Groq from 'groq-sdk';
const groq = new Groq({
apiKey: import.meta.env.VITE_REACT_APP_GROQ_API_KEY,
dangerouslyAllowBrowser: true,
});
DANGER! DANGER! DANGER!!!!:
From the Groq API official documentation, the recommended way to initialize the client is:
const groq = new Groq({ apiKey: process.env.GROQ_API_KEY });
However, since we are using Vite and not a backend server, this will return a process undefined error
.
Using the following code:
const groq = new Groq({
apiKey: import.meta.env.VITE_REACT_APP_GROQ_API_KEY,
dangerouslyAllowBrowser: true,
});
This allows us to make the API call from the client side.
Security Implications:
The downside of using the above code snippet is that Groq API blocks client-side API calls for obvious security reasons (exposing your API key). By setting dangerouslyAllowBrowser: true
, you are explicitly allowing the API key to be used in the browser, which can expose it to potential security risks.
Risk Mitigation:
-
Environment Variables: If you will upload this to GitHub, always ensure that your .env file is included in your
.gitignore
file to prevent it from being committed to version control.
Server-Side Requests: Whenever possible, make API calls from a backend server to keep your API keys secure.
Secure Sharing: If you need to share your project, make sure to remove or obfuscate the .env file that contains your API key.
Step 4: Define the State
Define the state to manage the input value and chat messages. The chatMessages
state will hold an array of objects, each containing a prompt
and a response
.
To do this we will use the built-in React useState
Hook.
import { useState } from 'react';
interface ChatMessage {
prompt: string;
response: string;
}
const App = () => {
const [inputValue, setInputValue] = useState('');
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
};
Step 5: Handle Input Change
Create a function to handle changes in the input field.
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(event.target.value);
};
Step 6: Integrate the API Request and Response to the Send
Button
const handleSend = async () => {
if (inputValue.trim() === '') return;
const chatPrompt = `You: ${inputValue}`;
try {
const chatCompletion = await groq.chat.completions.create({
messages: [
{
role: 'user',
content: inputValue,
},
],
model: 'llama3-8b-8192',
});
const responseContent =
chatCompletion.choices[0]?.message?.content || 'No response';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: responseContent,
};
setChatMessages([...chatMessages, newChatMessage]);
} catch (error) {
console.error('Error fetching chat completion:', error);
const errorMessage = 'Error fetching chat completion';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: errorMessage,
};
setChatMessages([...chatMessages, newChatMessage]);
} finally {
setInputValue('');
}
};
Also create a function to call handleSend
that will be attached to the search-input
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault(); // Prevent the default action (newline)
handleSend();
}
Step 7: Render the UI
return (
<>
<AppName>
<div>
<span>Grëg's </span>ChatBot
</div>
</AppName>
<div>
<Headings>
<div>
<h1>Hi, Welcome.</h1>
</div>
<div>
<h3>How can I help you today?</h3>
</div>
</Headings>
</div>
<div className="chat-container">
<Chat>
{/* Render chat messages */}
{chatMessages.map((message, index) => (
<div key={index} className="chat-message">
<div className="chat-prompt">{message.prompt}</div>
<div className="chat-response">{message.response}</div>
</div>
))}
</Chat>
</div>
<div className="searchBar-container">
<SearchBar>
<textarea
className="search-input"
placeholder="Enter your text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<Button textContent="Send" handleClick={handleSend} />
</SearchBar>
</div>
</>
);
Explanation
1- Chat component:
-
chatMessages.map
: Iterates over thechatMessages
array to render each message. So it dynamically renders the chat messages, ensuring that the UI updates as new messages are added. -
div key={index} className="chat-message"
: A container for each individual chat message. -
div className="chat-prompt"
: Displays the user's prompt. -
div className="chat-response"
: Displays the API's response.
2- Search Bar Component:
<div className="searchBar-container">
<SearchBar>
<textarea
className="search-input"
placeholder="Enter your text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<Button textContent="Send" handleClick={handleSend} />
</SearchBar>
</div>
-
onChange={handleInputChange}
: Handles changes in the input field. -
onKeyDown={handleKeyDown}
: Handles the Enter key press to send the message if it's pressed without the shift key.
Here's is our updated App.tsx
with the Groq API integration:
// App.tsx
import AppName from './components/AppName';
import Chat from './components/Chat';
import Headings from './components/Headings';
import SearchBar from './components/SearchBar';
import Button from './components/Button';
import { useState } from 'react';
import Groq from 'groq-sdk';
const groq = new Groq({
apiKey: import.meta.env.VITE_REACT_APP_GROQ_API_KEY,
dangerouslyAllowBrowser: true,
});
interface ChatMessage {
prompt: string;
response: string;
}
const App = () => {
// State to manage the input value
const [inputValue, setInputValue] = useState('');
// State to manage chat messages
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
// Function to handle input change
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(event.target.value);
};
// Function to handle button click
const handleSend = async () => {
if (inputValue.trim() === '') return;
const chatPrompt = `You: ${inputValue}`;
try {
const chatCompletion = await groq.chat.completions.create({
messages: [
{
role: 'user',
content: inputValue,
},
],
model: 'llama3-8b-8192',
});
const responseContent =
chatCompletion.choices[0]?.message?.content || 'No response';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: responseContent,
};
// Add the new chat message to the chat messages
setChatMessages([...chatMessages, newChatMessage]);
} catch (error) {
console.error('Error fetching chat completion:', error);
const errorMessage = 'Error fetching chat completion';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: errorMessage,
};
// Add the error message to the chat messages
setChatMessages([...chatMessages, newChatMessage]);
} finally {
// Clear the input field
setInputValue('');
}
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault(); // Prevent the default action (newline)
handleSend();
}
}
return (
<>
<AppName>
<div>
<span>Grëg's </span>ChatBot
</div>
</AppName>
<div>
<Headings>
<div>
<h1>Hi, Welcome.</h1>
</div>
<div>
<h3>How can I help you today?</h3>
</div>
</Headings>
</div>
<div className="chat-container">
<Chat>
{/* Render chat messages */}
{chatMessages.map((message, index) => (
<div key={index} className="chat-message">
<div className="chat-prompt">{message.prompt}</div>
<div className="chat-response">{message.response}</div>
</div>
))}
</Chat>
</div>
<div className="searchBar-container">
<SearchBar>
<textarea
className="search-input"
placeholder="Enter your text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<Button textContent="Send" handleClick={handleSend} />
</SearchBar>
</div>
</>
)};
export default App;
Congratulations. You have successfully built an AI ChatBot.
Don't run away yet. Don't over celebrate. There's more!
We need to make our app look cool, more professional and we don't want our browser to clear our chat history when we refresh the page. Yes? Great!!
If you are OK with this Ugly state, then, run ahead. Else let's go ahead.
Next Phase
In this phase of the project, we will need to
- Save our chat state using LocalStorage and React's built-in
useEffect
Hook. - Enhance the app's user interface with visually appealing styles and additional features.
Guys, are you excited like me here? No time to waste let's dive in.
Save App State:
We'll use the useEffect
hook to save the state to local storage whenever it changes. This ensures that the chat history is preserved even after the page is refreshed.
Step 1: Define the State Structure
First, define the state structure that includes the input value and chat messages.
interface ChatMessage {
prompt: string;
response: string;
}
interface AppState {
inputValue: string;
chatMessages: ChatMessage[];
}
Step 2: Initialize State with Local Storage
Initialize the state using the 'useState' hook. If there is a saved state in local storage, use it; otherwise, use the default values.
const [state, setState] = useState<AppState>(() => {
const localValue = localStorage.getItem('appState');
if (localValue === null) {
return {
inputValue: '',
chatMessages: [],
};
}
return JSON.parse(localValue);
});
Step 3: Use the useEffect hook to save the state to local storage whenever it changes.
useEffect(() => {
localStorage.setItem('appState', JSON.stringify(state));
}, [state]);
Step 4: Handle Input Change
Create a function to handle changes in the input field.
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setState((prevState) => ({
...prevState,
inputValue: event.target.value,
}));
};
Step 5: Update the handleSend
function
const handleSend = async () => {
if (state.inputValue.trim() === '') return;
const chatPrompt = `You: ${state.inputValue}`;
try {
const chatCompletion = await groq.chat.completions.create({
messages: [
{
role: 'user',
content: state.inputValue,
},
],
model: 'llama3-8b-8192',
});
const responseContent =
chatCompletion.choices[0]?.message?.content || 'No response';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: responseContent,
};
setState((prevState) => ({
...prevState,
chatMessages: [...prevState.chatMessages, newChatMessage],
inputValue: '',
}));
} catch (error) {
console.error('Error fetching chat completion:', error);
const errorMessage = 'Error fetching chat completion';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: errorMessage,
};
setState((prevState) => ({
...prevState,
chatMessages: [...prevState.chatMessages, newChatMessage],
inputValue: '',
}));
}
};
Explanation of useEffect
Hook
The useEffect
hook is used to perform side effects in functional components. In this case, it is used to save the state to local storage whenever the state changes.
useEffect(() => {
localStorage.setItem('appState', JSON.stringify(state));
}, [state]);
BreakDown:
1- The function inside useEffect is the effect that will be executed. Here, it saves the state to local storage.
localStorage.setItem('appState', JSON.stringify(state));
2- The second argument to useEffect
is the dependency array. It specifies when the effect should run. In this case, the effect runs whenever the state changes.
[state]
Got it? Any questions? Drop 'em in the comments.
Now we have made our code persistent. How can we clear the local Storage? Any idea?
Correct!!! We add a clear button to clear the chat and state.
Add a clear button to clear the chat and the state
Step 1: handleClearChat
function:
Create a function to handle the "Clear Chat" button click, which will clear the chat history and remove the state from local storage.
const handleClearChat = () => {
setState((prevState) => ({
...prevState,
chatMessages: [],
inputValue: '',
}));
// Remove chat history from localStorage
localStorage.removeItem('appState');
};
Step 2: Render the UI:
Add the clear chat
button using the Botton component.
<div className="chat-container">
<Chat>
{/* Map over the chat messages to render each one */}
{state.chatMessages.map((message, index) => (
<div key={index} className="chatConversations">
<div className="chat-prompt">{message.prompt}</div>
<div className="chat-response">{message.response}</div>
</div>
))}
<Button textContent="Clear Chat" handleClick={handleClearChat} />
</Chat>
</div>
Putting it all together:
Here the complete implementation of the App
component with the local storage save state functionality and the "Clear Chat" button:
// App.tsx
import AppName from './components/AppName';
import Button from './components/Button';
import Chat from './components/Chat';
import Groq from 'groq-sdk';
import Headings from './components/Headings';
import SearchBar from './components/SearchBar';
import { useState, useEffect } from 'react';
const groq = new Groq({
apiKey: import.meta.env.VITE_REACT_APP_GROQ_API_KEY,
dangerouslyAllowBrowser: true,
});
interface ChatMessage {
prompt: string;
response: string;
}
interface AppState {
inputValue: string;
chatMessages: ChatMessage[];
}
const App = () => {
// Initialize state with an empty string
const [state, setState] = useState<AppState>(() => {
const localValue = localStorage.getItem('appState');
if (localValue === null) {
return {
inputValue: '',
chatMessages: [],
};
}
return JSON.parse(localValue);
});
// Save state to localStorage whenever it changes
useEffect(() => {
localStorage.setItem('appState', JSON.stringify(state));
}, [state]);
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
// Update state with the new input value
setState((prevState) => ({
...prevState,
inputValue: event.target.value,
}));
};
// Send the prompt to the API
const handleSend = async () => {
if (state.inputValue.trim() === '') return;
const chatPrompt = `You: ${state.inputValue}`;
try {
const chatCompletion = await groq.chat.completions.create({
messages: [
{
role: 'user',
content: state.inputValue,
},
],
model: 'llama3-8b-8192',
});
const responseContent =
chatCompletion.choices[0]?.message?.content || 'No response';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: responseContent,
};
// Append the new chat message to the array
setState((prevState) => ({
...prevState,
chatMessages: [...prevState.chatMessages, newChatMessage],
inputValue: '',
}));
} catch (error) {
console.error('Error fetching chat completion:', error);
const errorMessage = 'Error fetching chat completion';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: errorMessage,
};
// Append the error message to the array
setState((prevState) => ({
...prevState,
chatMessages: [...prevState.chatMessages, newChatMessage],
inputValue: '',
}));
}
};
const handleClearChat = () => {
// Clear the chat messages state
setState((prevState) => ({
...prevState,
chatMessages: [],
inputValue: '',
}));
// Remove chat history from localStorage
localStorage.removeItem('appState');
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault(); // Prevent the default action (newline)
handleSend();
}
};
return (
<>
<AppName>
<div>
<span>Grëg's </span>ChatBot
</div>
</AppName>
<div>
<Headings>
<div>
<h1>Hi, Welcome.</h1>
</div>
<div>
<h3>How can I help you today?</h3>
</div>
</Headings>
</div>
<div className="chat-container">
<Chat>
{/* Map over the chat messages to render each one */}
{state.chatMessages.map((message, index) => (
<div key={index} className="chatConversations">
<div className="chat-prompt">{message.prompt}</div>
<div className="chat-response">{message.response}</div>
</div>
))}
<Button textContent="Clear Chat" handleClick={handleClearChat} />
</Chat>
</div>
<div className="searchBar-container">
<SearchBar>
<textarea
className="search-input"
placeholder="Enter your text"
value={state.inputValue}
// Use the handleInputChange function
onChange={handleInputChange}
// Use the handleKeyDown function
onKeyDown={handleKeyDown}
/>
<Button
textContent="Send"
handleClick={handleSend}
/>
</SearchBar>
</div>
</>
);
};
export default App;
We are approaching the end of our amazing project.
Final Features.
Now we make our app look more professional. Yes?
What do I mean?
We'll add CSS styles to enhance the user interface. The styles will make our chatbot look more professional and user-friendly.
Styling.
Create an App.css
file in your src folder if you don't have one. Then add this Css styles to it.
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-size: 14;
font-family: 'Montserrat', sans-serif, system-ui;
}
#root {
display: flex;
flex-direction: column;
justify-content: center;
margin: 2rem;
align-items: center;
line-height: 2;
}
.app-name {
font-weight: 600;
display: flex;
}
.app-name div {
background: rgba(0, 0, 0, 0.1);
padding: 0 1rem;
border-radius: 1rem;
}
.app-name span {
font-family: cursive;
font-style: italic;
font-size: 1.5rem;
}
h1 {
font-size: 5rem;
}
h3 {
font-size: 2rem;
text-align: center;
}
button {
border-radius: 30%;
border: none;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
}
button:hover {
opacity: 0.9;
}
.search-input {
resize: none;
flex: 1;
background: transparent;
outline: none;
height: 20rem;
padding: 0.5rem 0.5rem 0.5rem 2rem;
font-size: inherit;
}
.searchBar-container {
align-self: flex-end;
width: 90%;
}
.searchBar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
padding: 1rem;
}
.searchBar button {
background-color: rgb(63, 128, 226);
color: white;
flex: 0;
height: 2.5em;
padding: 0 2em;
}
.searchBar button:disabled {
background-color: rgb(162, 192, 238);
}
.chat-container {
width: 50%;
margin-top: 3em;
}
.chatConversations {
display: flex;
flex-direction: column;
}
.chat-prompt {
align-self: flex-end;
margin: 1rem 0;
font-weight: 600;
background-color: rgba(0, 0, 0, 0.1);
padding: 0.5rem;
}
.chat-container button {
background-color: rgb(205, 56, 56);
color: white;
padding: 0.5em 1.5em;
margin: 2rem 0;
}
To ensure that the css style is implemented, check your main.tsx
file to ensure that the css file is imported.
Good job Guys..
Finally Guys
Let's disable the send button if there is no inputValue to prevent us sending empty prompts.
Then we hide the Headings
component when there is a chat conversation and also we hide the Chat
component when there is no chat.
Here's what I mean. When there is no chat, we have our AppName, Headings, SearchBar components with their elements. But when there is a chat, the Headings are hidden automatically and we have our AppName, Chat component with all our chats, and SearchBar components with all their elements.
Whenever we clear the chat, then the Chat component gets hidden again and our Headings component gets displayed.
You get the gist. Yes?
Alright. So much explanation, let's get back to work.
Step 1: Update AppState
interface
We add the visibility flags for the Headings and Chat components to our AppState
interface.
interface AppState {
inputValue: string;
chatMessages: ChatMessage[];
isChatVisible: boolean;
isHeadersVisible: boolean;
}
Step 2: Update our state initialization
We set the initial visibility of our Headings and Chat components.
const [state, setState] = useState<AppState>(() => {
const localValue = localStorage.getItem('appState');
if (localValue === null) {
return {
inputValue: '',
chatMessages: [],
isChatVisible: false,
isHeadersVisible: true,
};
}
return JSON.parse(localValue);
});
Step 3: Check Input Value
Create a function to check if the input value is empty or contains only whitespace.
const noChatPrompt = state.inputValue.trim() === '';
step 4: Update handleSend
function
We update the handleSend
function check the inputValue before sending. If empty, it wont send. It will also hide the Headings
component and display the Chat
component, when we send a chat conversation:
const handleSend = async () => {
if (noChatPrompt) return;
const chatPrompt = `You: ${state.inputValue}`;
try {
const chatCompletion = await groq.chat.completions.create({
messages: [
{
role: 'user',
content: state.inputValue,
},
],
model: 'llama3-8b-8192',
});
const responseContent =
chatCompletion.choices[0]?.message?.content || 'No response';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: responseContent,
};
setState((prevState) => ({
...prevState,
chatMessages: [...prevState.chatMessages, newChatMessage],
isChatVisible: true,
isHeadersVisible: false,
inputValue: '',
}));
} catch (error) {
console.error('Error fetching chat completion:', error);
const errorMessage = 'Error fetching chat completion';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: errorMessage,
};
setState((prevState) => ({
...prevState,
chatMessages: [...prevState.chatMessages, newChatMessage],
// set Headings and Chat component visibilities
isChatVisible: true,
isHeadersVisible: false,
inputValue: '',
}));
}
};
Step 5: Update the Clear chat
button function.
When clicked, the Clear chat
button will also reset the visibility flags.
const handleClearChat = () => {
setState((prevState) => ({
...prevState,
chatMessages: [],
isChatVisible: false,
isHeadersVisible: true,
inputValue: '',
}));
// Remove chat history from localStorage
localStorage.removeItem('appState');
};
Step 6: Render the UI
Render the UI components, including the headings and chat components, conditionally based on the visibility flags. Disable the "Send" button if there is no input value.
return (
<>
<AppName>
<div>
<span>Grëg's </span>ChatBot
</div>
</AppName>
{state.isHeadersVisible && (
<div>
<Headings>
<div>
<h1>Hi, Welcome.</h1>
</div>
<div>
<h3>How can I help you today?</h3>
</div>
</Headings>
</div>
)}
{state.isChatVisible && (
<div className="chat-container">
<Chat>
{/* Map over the chat messages to render each one */}
{state.chatMessages.map((message, index) => (
<div key={index} className="chatConversations">
<div className="chat-prompt">{message.prompt}</div>
<div className="chat-response">{message.response}</div>
</div>
))}
<Button textContent="Clear Chat" handleClick={handleClearChat} />
</Chat>
</div>
)}
<div className="searchBar-container">
<SearchBar>
<textarea
className="search-input"
placeholder="Enter your text"
value={state.inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<Button
textContent="Send"
handleClick={handleSend}
disabled={noChatPrompt}
/>
</SearchBar>
</div>
</>
);
Now Let's bring it all together:
Here is the complete implementation of the App
component with the display and hiding of the Headings and Chat components, along with disabling the "Send" button if there is no input value:
// App.tsx
import AppName from './components/AppName';
import Button from './components/Button';
import Chat from './components/Chat';
import Groq from 'groq-sdk';
import Headings from './components/Headings';
import SearchBar from './components/SearchBar';
import { useState, useEffect } from 'react';
const groq = new Groq({
apiKey: import.meta.env.VITE_REACT_APP_GROQ_API_KEY,
dangerouslyAllowBrowser: true,
});
interface ChatMessage {
prompt: string;
response: string;
}
interface AppState {
inputValue: string;
chatMessages: ChatMessage[];
isChatVisible: boolean;
isHeadersVisible: boolean;
}
const App = () => {
// Initialize state with an empty string
const [state, setState] = useState<AppState>(() => {
const localValue = localStorage.getItem('appState');
if (localValue === null) {
return {
inputValue: '',
chatMessages: [],
isChatVisible: false,
isHeadersVisible: true,
};
}
return JSON.parse(localValue);
});
// Save state to localStorage whenever it changes
useEffect(() => {
localStorage.setItem('appState', JSON.stringify(state));
}, [state]);
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
// Update state with the new input value
setState((prevState) => ({
...prevState,
inputValue: event.target.value,
}));
};
// Check if the input value is empty or contains only whitespace
const noChatPrompt = state.inputValue.trim() === '';
// Send the prompt to the API
const handleSend = async () => {
if (noChatPrompt) return;
const chatPrompt = `You: ${state.inputValue}`;
try {
const chatCompletion = await groq.chat.completions.create({
messages: [
{
role: 'user',
content: state.inputValue,
},
],
model: 'llama3-8b-8192',
});
const responseContent =
chatCompletion.choices[0]?.message?.content || 'No response';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: responseContent,
};
// Append the new chat message to the array
setState((prevState) => ({
...prevState,
chatMessages: [...prevState.chatMessages, newChatMessage],
isChatVisible: true,
isHeadersVisible: false,
inputValue: '',
}));
} catch (error) {
console.error('Error fetching chat completion:', error);
const errorMessage = 'Error fetching chat completion';
const newChatMessage: ChatMessage = {
prompt: chatPrompt,
response: errorMessage,
};
// Append the error message to the array
setState((prevState) => ({
...prevState,
chatMessages: [...prevState.chatMessages, newChatMessage],
isChatVisible: true,
isHeadersVisible: false,
inputValue: '',
}));
}
};
const handleClearChat = () => {
// Clear the chat messages state
setState((prevState) => ({
...prevState,
chatMessages: [],
isChatVisible: false,
isHeadersVisible: true,
inputValue: '',
}));
// Remove chat history from localStorage
localStorage.removeItem('appState');
};
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault(); // Prevent the default action (newline)
handleSend();
}
};
return (
<>
<AppName>
<div>
<span>Grëg's </span>ChatBot
</div>
</AppName>
{state.isHeadersVisible && (
<div>
<Headings>
<div>
<h1>Hi, Welcome.</h1>
</div>
<div>
<h3>How can I help you today?</h3>
</div>
</Headings>
</div>
)}
{state.isChatVisible && (
<div className="chat-container">
<Chat>
{/* Map over the chat messages to render each one */}
{state.chatMessages.map((message, index) => (
<div key={index} className="chatConversations">
<div className="chat-prompt">{message.prompt}</div>
<div className="chat-response">{message.response}</div>
</div>
))}
<Button textContent="Clear Chat" handleClick={handleClearChat} />
</Chat>
</div>
)}
<div className="searchBar-container">
<SearchBar>
<textarea
className="search-input"
placeholder="Enter your text"
value={state.inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<Button
textContent="Send"
handleClick={handleSend}
disabled={noChatPrompt}
/>
</SearchBar>
</div>
</>
);
};
export default App;
Final Result
And so Ladies, Gentlemen and all, we have come to the end of our project.
Congratulations
Assignment
So guys, as an assignment, I want you to set the API response to be displayed as Markdown, not as pure text as it's currently returning.
With Markdown, the response will be formatted better thus improve the readability of the responses.
Let me know in the comments if you attempted it or how you solved it.
Conclusion
Congratulations! we've successfully built an AI Chatbot application using React, TypeScript, and the Groq Cloud AI API. Feel free to experiment with the code, add more features, and customize the chatbot to your liking.
Thanks for joining me today to build this amazing project. I can't wait to meet you on our next project. Goodbye for now and see you soon on our next project.
Until then guys, let's keep learning and experimenting.
From Grëg Häris with ❤️
Happy coding ❤️!