DEV Community

Cover image for Build Your First AI Chatbot: A React and TypeScript Project with Groq Cloud API
Grëg Häris
Grëg Häris

Posted on • Updated on

Build Your First AI Chatbot: A React and TypeScript Project with Groq Cloud API

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Headers.tsx

import { ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

const Headings = ({ children }: Props) => {
  return <div>{children}</div>;
};

export default Headings;
Enter fullscreen mode Exit fullscreen mode

searchBar.tsx

import { ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

const SearchBar = ({ children }: Props) => {
  return <div className="searchBar">{children}</div>;
};

export default SearchBar;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Chat.tsx

import { ReactNode } from 'react';

interface Props {
  children: ReactNode;
}

const Chat = ({ children }: Props) => {
  return <div className="chat">{children}</div>;
};

export default Chat;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

This is the result you will get:

Result after adding app components

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;

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

.env created in the root folder

API key Added

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,
});

Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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.

.env files included in .gitIgnore list

  • 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[]>([]);
};
Enter fullscreen mode Exit fullscreen mode

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);
};
Enter fullscreen mode Exit fullscreen mode

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('');
  }
 };
Enter fullscreen mode Exit fullscreen mode

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();
   }

Enter fullscreen mode Exit fullscreen mode

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>
  </>
);
Enter fullscreen mode Exit fullscreen mode

Explanation
1- Chat component:

  • chatMessages.map: Iterates over the chatMessages 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>
Enter fullscreen mode Exit fullscreen mode
  • 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;

Enter fullscreen mode Exit fullscreen mode

Here is the result:

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[];
}
Enter fullscreen mode Exit fullscreen mode

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);
});

Enter fullscreen mode Exit fullscreen mode

Step 3: Use the useEffect hook to save the state to local storage whenever it changes.

useEffect(() => {
  localStorage.setItem('appState', JSON.stringify(state));
}, [state]);
Enter fullscreen mode Exit fullscreen mode

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,
  }));
};
Enter fullscreen mode Exit fullscreen mode

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: '',
    }));
  }
};
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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');
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

Oboy!!!! This is awesome.

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;
}

Enter fullscreen mode Exit fullscreen mode

To ensure that the css style is implemented, check your main.tsx file to ensure that the css file is imported.

Showing importation of App.css

Result

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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() === '';
Enter fullscreen mode Exit fullscreen mode

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: '',
    }));
  }
};
Enter fullscreen mode Exit fullscreen mode

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');
};
Enter fullscreen mode Exit fullscreen mode

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>
  </>
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 ❤️!

Top comments (0)