How to Build a Gemini AI Chatbot in React.js and CSS | Step-By-Step Guide

1

How to Build a Gemini AI Chatbot in React.js and CSS - Dark Theme

In today’s tech-driven world, AI chatbots are everywhere, helping with tasks like shopping and customer support. Have you thought about creating your own chatbot? Imagine designing a smart Gemini chatbot, similar to ChatGPT, where users can chat with AI, access chat history, and switch between dark and light themes, all using the free Google Gemini API. Sounds exciting, right?

At first glance, building a chatbot might seem overwhelming. However, with frameworks like React.js and CSS, it’s more attainable than you think. By following a few simple steps, you can create a sleek and responsive chatbot that conveys professionalism. This project will help you strengthen your React skills, including components, state management, API handling, and utilizing local storage for saving data.

In this tutorial, I’ll guide you through the process of developing a Google Gemini AI chatbot from scratch using React.js and CSS. We’ll focus on user experience by incorporating features such as a collapsible sidebar for chat history, smooth transitions between dark and light modes, and automatic conversation saving.

Why Build a Gemini AI Chatbot with React.js?

Building this AI chatbot teaches you way more than just chatbot development. Here’s what you’ll learn along the way:

  • React Components: Work with functional components, JSX, and manage state using hooks. The building blocks of React.
  • API Integration: Connect to the Gemini API and handle asynchronous requests and responses easily.
  • Theme Switching: Let users toggle between dark and light modes using simple state management and clean CSS.
  • Local Storage: Save chat histories in the browser so conversations stick around even after a page refresh.
  • Portfolio Project: Add a polished project to your portfolio that shows off your React and API skills.

If you’d rather build the same Gemini AI chatbot in vanilla JavaScript, feel free to check out my previous blog post on how to create a Gemini AI chatbot in HTML, CSS, and JavaScript. It’s a great option if you want to start with the basics and don’t want to dive into React just yet.

Video Demo of Gemini AI Chatbot in React.js & CSS

Check out the short video demo above, where I showcase how our Gemini AI chatbot works. You’ll get a closer look at its appearance and the key features that make it stand out.

Setting Up the Project

Before building the Gemini AI chatbot with React.js and CSS, ensure Node.js is installed on your computer. If not, download and install it from the official Node.js website.

Create a Project Folder:

  • Make a new folder, for instance, “gemini-chatbot-reactjs”.
  • Open this folder in your VS Code editor.

Initialize the Project:

Open your terminal by pressing Ctrl + J and then use Vite to create a new React app with this command:

npm create vite@latest ./ -- --template react

Install necessary dependencies and start the development server:

npm install
npm install lucide-react
npm run dev

If your project is running in your browser, congratulations! You’ve successfully set up your chatbot app. Now, let’s move on to modifying folders and files.

Modify folder and CSS Files:

  • Remove the default assets folder and App.css file.
  • Download the Gemini SVG logo and place it directly into your public folder.
  • Replace the content of index.css with the provided CSS code.
/* Import Google Font - Poppins */
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap");

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

:root {
  /* Dark theme colors */
  --color-text-primary: #EDF3FF;
  --color-text-secondary: #D7E5FF;
  --color-text-placeholder: #A0B1CF;
  --color-bg-primary: #111827;
  --color-bg-secondary: #233043;
  --color-bg-sidebar: #1E2939;
  --color-border-hr: #364153;
  --color-hover-secondary: #33435B;
  --color-hover-secondary-alt: #415472;
  --gradient-blue-purple: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);

  --sidebar-open-width: 260px;
  --sidebar-closed-width: 80px;
}

.app-container.light-theme {
  /* Light theme colors */
  --color-text-primary: #2A2A2A;
  --color-text-secondary: #4A5565;
  --color-text-placeholder: #7F93B7;
  --color-bg-primary: #F3F7FF;
  --color-bg-secondary: #E3EBF6;
  --color-bg-sidebar: #E6EDF8;
  --color-border-hr: #D9DBDD;
  --color-hover-secondary: #D4DCED;
  --color-hover-secondary-alt: #C9D4EA;
}

.app-container {
  display: flex;
  height: 100vh;
  width: 100vw;
  color: var(--color-text-primary);
  background: var(--color-bg-primary);
}

.sidebar {
  position: sticky;
  top: 0;
  z-index: 20;
  flex-shrink: 0;
  display: flex;
  white-space: nowrap;
  flex-direction: column;
  width: var(--sidebar-open-width);
  background: var(--color-bg-sidebar);
  overflow: hidden;
  transition: width 0.3s ease;
}

.sidebar.closed {
  width: var(--sidebar-closed-width);
}

.sidebar .sidebar-header {
  padding: 16px 16px 23px;
  display: flex;
  gap: 30px;
  align-items: center;
  flex-direction: column;
  border-bottom: 1px solid var(--color-border-hr);
}

.sidebar-header .sidebar-toggle {
  border: none;
  cursor: pointer;
  width: 45px;
  height: 45px;
  border-radius: 50%;
  display: flex;
  align-self: start;
  align-items: center;
  justify-content: center;
  color: var(--color-text-primary);
  background: var(--color-hover-secondary);
  transition: 0.3s ease;
}

.sidebar-header .sidebar-toggle:hover {
  background: var(--color-hover-secondary-alt);
}

.sidebar-header .new-chat-btn {
  gap: 8px;
  font-weight: 500;
  color: #fff;
  background: var(--gradient-blue-purple);
  transition: all 0.3s ease;
}

.sidebar-header .new-chat-btn,
.sidebar-footer .theme-toggle {
  overflow: hidden;
  display: flex;
  cursor: pointer;
  border: none;
  font-size: 1rem;
  min-height: 48px;
  padding: 0 15px;
  border-radius: 100px;
  align-items: center;
  justify-content: center;
  width: calc(var(--sidebar-open-width) - 32px);
  transition: all 0.3s ease;
}

.sidebar.closed .sidebar-header .new-chat-btn,
.sidebar.closed .sidebar-footer .theme-toggle {
  gap: 0;
  width: 48px;
  min-height: 48px;
}

.sidebar-header .new-chat-btn svg,
.sidebar-footer .theme-toggle svg {
  flex-shrink: 0;
}

.sidebar-header .new-chat-btn span,
.sidebar-footer .theme-toggle span {
  overflow: hidden;
  transition: opacity 0.2s ease;
}

.sidebar.closed .sidebar-header .new-chat-btn span,
.sidebar.closed .sidebar-footer .theme-toggle span {
  width: 0;
  opacity: 0;
}

.sidebar .sidebar-content {
  flex: 1;
  padding: 8px;
  overflow: hidden auto;
  scrollbar-color: var(--color-text-placeholder) transparent;
  transition: opacity 0.3s ease;
}

.sidebar.closed .sidebar-content {
  opacity: 0;
  pointer-events: none;
}

.sidebar-content .sidebar-title {
  padding: 12px;
  font-size: 0.95rem;
  font-weight: 500;
  color: var(--color-text-secondary);
}

.sidebar-content .conversation-list {
  list-style: none;
}

.conversation-list .conversation-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 9px 12px;
  border-radius: 100px;
  font-size: 1rem;
  margin-top: 1px;
  cursor: pointer;
  transition: 0.3s ease;
}

.conversation-list .conversation-item:is(:hover, .active) {
  background-color: var(--color-hover-secondary);
}

.conversation-item .conversation-title {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.conversation-item .conversation-icon-title {
  display: flex;
  gap: 12px;
  align-items: center;
  overflow: hidden;
}

.conversation-item .conversation-icon {
  width: 28px;
  height: 28px;
  color: #fff;
  flex-shrink: 0;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--gradient-blue-purple);
}

.conversation-item .delete-btn {
  opacity: 0;
  background: none;
  border: none;
  height: 30px;
  width: 30px;
  cursor: pointer;
  padding: 4px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-text-primary);
  transition: 0.3s ease;
}

.conversation-item:hover .delete-btn:not(.hide) {
  opacity: 1;
}

.conversation-item .delete-btn:hover {
  color: #ef4444;
  background-color: rgba(239, 68, 68, 0.1);
}

.sidebar .sidebar-footer {
  padding: 16px;
  border-top: 1px solid var(--color-border-hr);
}

.sidebar-footer .theme-toggle {
  gap: 12px;
  font-size: 1rem;
  color: var(--color-text-primary);
  background: var(--color-hover-secondary);
}

.sidebar-footer .theme-toggle:hover {
  background: var(--color-hover-secondary-alt);
}

.main-container {
  display: flex;
  width: 100%;
  padding-top: 30px;
  flex-direction: column;
  justify-content: space-between;
}

.main-container :where(.message, .prompt-wrapper, .disclaimer-text) {
  position: relative;
  margin: 0 auto;
  width: 100%;
  padding: 0 20px;
  max-width: 1000px;
}

.messages-container {
  display: flex;
  gap: 20px;
  padding: 0 0 100px;
  overflow-y: auto;
  flex-direction: column;
  scrollbar-color: var(--color-text-placeholder) transparent;
}

.messages-container .message {
  display: flex;
  gap: 11px;
  align-items: center;
}

.messages-container .bot-message .avatar {
  width: 43px;
  height: 43px;
  flex-shrink: 0;
  align-self: flex-start;
  border-radius: 50%;
  padding: 6px;
  margin-right: -7px;
  background: var(--color-bg-secondary);
  border: 1px solid var(--color-hover-secondary);
}

.messages-container .bot-message.loading .avatar {
  animation: rotate 3s linear infinite;
}

@keyframes rotate {
  100% {
    transform: rotate(360deg);
  }
}

.messages-container .message .text {
  padding: 3px 16px;
  word-wrap: break-word;
  white-space: pre-line;
}

.messages-container .bot-message {
  margin: 9px auto;
}

.messages-container .user-message {
  flex-direction: column;
  align-items: flex-end;
}

.messages-container .user-message .text {
  padding: 12px 16px;
  max-width: 75%;
  background: var(--color-bg-secondary);
  border-radius: 13px 13px 3px 13px;
}

.messages-container .message.error {
  color: #d62939;
}

.main-container .prompt-container {
  padding: 16px 0;
  width: 100%;
  background: var(--color-bg-primary);
}

.prompt-container .prompt-form {
  height: 54px;
  width: 100%;
  position: relative;
  border-radius: 130px;
  background: var(--color-bg-secondary);
  border: 1px solid var(--color-border-hr);
}

.prompt-form .prompt-input {
  width: 100%;
  height: 100%;
  background: none;
  outline: none;
  border: none;
  font-size: 1rem;
  padding-left: 24px;
  color: var(--color-text-primary);
}

.prompt-form .prompt-input::placeholder {
  color: var(--color-text-placeholder);
}

.prompt-wrapper .send-prompt-btn {
  width: 43px;
  height: 43px;
  position: absolute;
  top: 50%;
  right: 6px;
  transform: translateY(-50%);
  flex-shrink: 0;
  cursor: pointer;
  display: none;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  font-size: 1.4rem;
  border: none;
  color: #fff;
  background: #1d7efd;
  transition: 0.3s ease;
}

.prompt-wrapper .prompt-form .prompt-input:valid~.send-prompt-btn {
  display: flex;
}

.prompt-form .send-prompt-btn:hover {
  background: #358cfd;
}

.prompt-container .disclaimer-text {
  font-size: 0.9rem;
  text-align: center;
  padding: 16px 20px 0;
  color: var(--color-text-placeholder);
}

.welcome-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  height: 60vh;
  width: 100%;
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.welcome-logo {
  width: 70px;
  height: 70px;
  margin-bottom: 24px;
  padding: 10px;
  border-radius: 50%;
  background: var(--color-bg-secondary);
  border: 1px solid var(--color-hover-secondary);
}

.welcome-heading {
  font-size: 2.2rem;
  font-weight: 600;
  margin-bottom: 16px;
  background: var(--gradient-blue-purple);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

.welcome-text {
  font-size: 1.1rem;
  max-width: 400px;
  line-height: 1.6;
  color: var(--color-text-secondary);
}

.main-header {
  display: none;
  padding: 12px 20px;
  background: var(--color-bg-primary);
  border-bottom: 1px solid var(--color-bg-secondary);
}

.main-header .sidebar-toggle {
  border: none;
  cursor: pointer;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-self: start;
  align-items: center;
  justify-content: center;
  color: var(--color-text-primary);
  background-color: var(--color-hover-secondary);
  transition: 0;
}

.overlay {
  height: 100%;
  width: 100%;
  position: fixed;
  left: 0;
  top: 0;
  /* backdrop-filter: blur(5px); */
  background: rgba(0, 0, 0, 0.6);
  z-index: 15;
  opacity: 0;
  pointer-events: none;
  transition: 0.2s ease;
}

/* Responsive media query code for small screens */
@media (max-width: 768px) {
  .sidebar.closed {
    width: var(--sidebar-open-width);
  }

  .sidebar {
    position: fixed;
    height: 100%;
    left: calc(-1 * var(--sidebar-open-width));
    transition: left 0.3s ease;
  }

  .sidebar.closed .sidebar-header .new-chat-btn span,
  .sidebar.closed .sidebar-footer .theme-toggle span {
    width: auto;
  }

  .sidebar.open {
    left: 0;
  }

  .main-container {
    padding-top: 0;
  }

  .main-header {
    display: block;
  }

  .overlay.show {
    opacity: 1;
    pointer-events: auto;
  }

  .messages-container {
    padding-top: 20px;
    margin-bottom: auto;
  }

  .welcome-logo {
    height: 60px;
    width: 60px;
  }

  .welcome-heading {
    font-size: 1.8rem;
  }

  .welcome-text {
    font-size: 1rem
  }
}

Creating the Components

Within the src directory of your project, organize your files by creating a “components” folder. Inside the components folder, create the following files:

  • Sidebar.jsx
  • Message.jsx
  • PromptForm.jsx

Adding the Codes

Add the respective code to each newly created file to define the layout and functionality of your Gemini AI Chatbot.

In components/sidebar.jsx, add the code to build a collapsible sidebar where users can manage their chats.

import { Menu, Moon, Plus, Sparkles, Sun, Trash2 } from "lucide-react";

const Sidebar = ({ isSidebarOpen, setIsSidebarOpen, conversations, setConversations, activeConversation, setActiveConversation, theme, setTheme }) => {
  // Create new conversation
  const createNewConversation = () => {
    // Check if any existing conversation is empty
    const emptyConversation = conversations.find((conv) => conv.messages.length === 0);

    if (emptyConversation) {
      // If an empty conversation exists, make it active instead of creating a new one
      setActiveConversation(emptyConversation.id);
      return;
    }

    // Only create a new conversation if there are no empty ones
    const newId = `conv-${Date.now()}`;
    setConversations([{ id: newId, title: "New Chat", messages: [] }, ...conversations]);
    setActiveConversation(newId);
  };

  // Delete conversation and handle active selection
  const deleteConversation = (id, e) => {
    e.stopPropagation(); // Prevent triggering conversation selection

    // Check if this is the last conversation
    if (conversations.length === 1) {
      // Create new conversation with ID "default"
      const newConversation = { id: "default", title: "New Chat", messages: [] };
      setConversations([newConversation]);
      setActiveConversation("default"); // Set active to match the new conversation ID
    } else {
      // Remove the conversation
      const updatedConversations = conversations.filter((conv) => conv.id !== id);
      setConversations(updatedConversations);

      // If deleting the active conversation, switch to another one
      if (activeConversation === id) {
        // Find the first conversation that isn't being deleted
        const nextConversation = updatedConversations[0];
        setActiveConversation(nextConversation.id);
      }
    }
  };

  return (
    <aside className={`sidebar ${isSidebarOpen ? "open" : "closed"}`}>
      {/* Sidebar Header */}
      <div className="sidebar-header">
        <button className="sidebar-toggle" onClick={() => setIsSidebarOpen((prev) => !prev)}>
          <Menu size={18} />
        </button>
        <button className="new-chat-btn" onClick={createNewConversation}>
          <Plus size={20} />
          <span>New chat</span>
        </button>
      </div>

      {/* Conversation List */}
      <div className="sidebar-content">
        <h2 className="sidebar-title">Chat history</h2>
        <ul className="conversation-list">
          {conversations.map((conv) => (
            <li key={conv.id} className={`conversation-item ${activeConversation === conv.id ? "active" : ""}`} onClick={() => setActiveConversation(conv.id)}>
              <div className="conversation-icon-title">
                <div className="conversation-icon">
                  <Sparkles size={14} />
                </div>
                <span className="conversation-title">{conv.title}</span>
              </div>

              {/* Only show delete button if more than one chat or not a new chat */}
              <button className={`delete-btn ${conversations.length > 1 || conv.title !== "New Chat" ? "" : "hide"}`} onClick={(e) => deleteConversation(conv.id, e)}>
                <Trash2 size={16} />
              </button>
            </li>
          ))}
        </ul>
      </div>

      {/* Theme Toggle */}
      <div className="sidebar-footer">
        <button className="theme-toggle" onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
          {theme === "light" ? (
            <>
              <Moon size={20} />
              <span>Dark mode</span>
            </>
          ) : (
            <>
              <Sun size={20} />
              <span>Light mode</span>
            </>
          )}
        </button>
      </div>
    </aside>
  );
};

export default Sidebar;

This component handles creating new conversations, switching between chats, and deleting them if needed. It also includes a simple dark/light theme toggle at the bottom. All actions like adding, removing, or selecting chats are handled cleanly through props and local state updates. functionality along with some others.

In components/Message.jsx, add the code to create the layout for each message.

const Message = ({ message }) => {
  return (
    <div id={message.id} className={`message ${message.role}-message ${message.loading ? "loading" : ""} ${message.error ? "error" : ""}`}>
      {message.role === "bot" && <img className="avatar" src="gemini.svg" alt="Bot Avatar" />}
      <p className="text">{message.content}</p>
    </div>
  );
};

export default Message;

Depending on whether the message is from the user or the AI, we show different styles and the Gemini avatar for bot responses. If a message is still loading or has an error, we style it differently to give users clear visual feedback.

In components/PromptForm.jsx, add the code to set up the input field where users type their messages.

import { ArrowUp } from "lucide-react";
import { useState } from "react";

const PromptForm = ({ conversations, setConversations, activeConversation, generateResponse, isLoading, setIsLoading }) => {
  const [promptText, setPromptText] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (isLoading || !promptText.trim()) return;

    setIsLoading(true);
    const currentConvo = conversations.find((convo) => convo.id === activeConversation) || conversations[0];

    // Set conversation title from first message if new chat
    let newTitle = currentConvo.title;
    if (currentConvo.messages.length === 0) {
      newTitle = promptText.length > 25 ? promptText.substring(0, 25) + "..." : promptText;
    }

    // Add user message
    const userMessage = {
      id: `user-${Date.now()}`,
      role: "user",
      content: promptText,
    };

    // Create API conversation without the "thinking" message
    const apiConversation = {
      ...currentConvo,
      messages: [...currentConvo.messages, userMessage],
    };

    // Update UI with user message
    setConversations(conversations.map((conv) => (conv.id === activeConversation ? { ...conv, title: newTitle, messages: [...conv.messages, userMessage] } : conv)));

    // Clear input
    setPromptText("");

    // Add bot response after short delay for better UX
    setTimeout(() => {
      const botMessageId = `bot-${Date.now()}`;
      const botMessage = {
        id: botMessageId,
        role: "bot",
        content: "Just a sec...",
        loading: true,
      };

      // Only update the UI with the thinking message, not the conversation for API
      setConversations((prev) => prev.map((conv) => (conv.id === activeConversation ? { ...conv, title: newTitle, messages: [...conv.messages, botMessage] } : conv)));

      // Pass the API conversation without the thinking message
      generateResponse(apiConversation, botMessageId);
    }, 300);
  };

  return (
    <form className="prompt-form" onSubmit={handleSubmit}>
      <input placeholder="Message Gemini..." className="prompt-input" value={promptText} onChange={(e) => setPromptText(e.target.value)} required />
      <button type="submit" className="send-prompt-btn">
        <ArrowUp size={20} />
      </button>
    </form>
  );
};

export default PromptForm;

On form submission, it updates the conversation with the user’s prompt, temporarily shows a “Just a sec…” message, and then triggers the API call to get the AI’s reply. It also updates conversation titles dynamically based on the first message.

Finally, update src/App.jsx with the provided code, which integrates all components to build the chatbot. It manages core features like theme switching, active conversations, and chat history, while saving preferences to localStorage.

import { useEffect, useRef, useState } from "react";
import Message from "./components/Message";
import PromptForm from "./components/PromptForm";
import Sidebar from "./components/Sidebar";
import { Menu } from "lucide-react";

const App = () => {
  // Main app state
  const [isLoading, setIsLoading] = useState(false);
  const typingInterval = useRef(null);
  const messagesContainerRef = useRef(null);
  const [isSidebarOpen, setIsSidebarOpen] = useState(() => window.innerWidth > 768);

  const [theme, setTheme] = useState(() => {
    const savedTheme = localStorage.getItem("theme");
    if (savedTheme) {
      return savedTheme;
    }
    const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
    return prefersDark ? "dark" : "light";
  });

  const [conversations, setConversations] = useState(() => {
    try {
      // Load conversations from localStorage or use default
      const saved = localStorage.getItem("conversations");
      return saved ? JSON.parse(saved) : [{ id: "default", title: "New Chat", messages: [] }];
    } catch {
      return [{ id: "default", title: "New Chat", messages: [] }];
    }
  });

  const [activeConversation, setActiveConversation] = useState(() => {
    return localStorage.getItem("activeConversation") || "default";
  });

  useEffect(() => {
    localStorage.setItem("activeConversation", activeConversation);
  }, [activeConversation]);

  // Save conversations to localStorage
  useEffect(() => {
    localStorage.setItem("conversations", JSON.stringify(conversations));
  }, [conversations]);

  // Handle theme changes
  useEffect(() => {
    localStorage.setItem("theme", theme);
    document.documentElement.classList.toggle("dark", theme === "dark");
  }, [theme]);

  // Get current active conversation
  const currentConversation = conversations.find((c) => c.id === activeConversation) || conversations[0];

  // Scroll to bottom of container
  const scrollToBottom = () => {
    if (messagesContainerRef.current) {
      messagesContainerRef.current.scrollTo({
        top: messagesContainerRef.current.scrollHeight,
        behavior: "smooth",
      });
    }
  };

  // Effect to scroll when messages change
  useEffect(() => {
    scrollToBottom();
  }, [conversations, activeConversation]);

  const typingEffect = (text, messageId) => {
    let textElement = document.querySelector(`#${messageId} .text`);
    if (!textElement) return;

    // Initially set the content to empty and mark as loading
    setConversations((prev) =>
      prev.map((conv) =>
        conv.id === activeConversation
          ? {
              ...conv,
              messages: conv.messages.map((msg) => (msg.id === messageId ? { ...msg, content: "", loading: true } : msg)),
            }
          : conv
      )
    );

    // Set up typing animation
    textElement.textContent = "";
    const words = text.split(" ");
    let wordIndex = 0;
    let currentText = "";

    clearInterval(typingInterval.current);
    typingInterval.current = setInterval(() => {
      if (wordIndex < words.length) {
        // Update the current text being displayed
        currentText += (wordIndex === 0 ? "" : " ") + words[wordIndex++];
        textElement.textContent = currentText;

        // Update state with current progress
        setConversations((prev) =>
          prev.map((conv) =>
            conv.id === activeConversation
              ? {
                  ...conv,
                  messages: conv.messages.map((msg) => (msg.id === messageId ? { ...msg, content: currentText, loading: true } : msg)),
                }
              : conv
          )
        );

        scrollToBottom();
      } else {
        // Animation complete
        clearInterval(typingInterval.current);

        // Final update, mark as finished loading
        setConversations((prev) =>
          prev.map((conv) =>
            conv.id === activeConversation
              ? {
                  ...conv,
                  messages: conv.messages.map((msg) => (msg.id === messageId ? { ...msg, content: currentText, loading: false } : msg)),
                }
              : conv
          )
        );
        setIsLoading(false);
      }
    }, 40);
  };

  // Generate AI response
  const generateResponse = async (conversation, botMessageId) => {
    // Format messages for API
    const formattedMessages = conversation.messages?.map((msg) => ({
      role: msg.role === "bot" ? "model" : msg.role,
      parts: [{ text: msg.content }],
    }));

    try {
      const res = await fetch(import.meta.env.VITE_API_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ contents: formattedMessages }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error.message);

      // Clean up response formatting
      const responseText = data.candidates[0].content.parts[0].text.replace(/\*\*([^*]+)\*\*/g, "$1").trim();

      typingEffect(responseText, botMessageId);
    } catch (error) {
      setIsLoading(false);
      updateBotMessage(botMessageId, error.message, true);
    }
  };

  // Update specific bot message
  const updateBotMessage = (botId, content, isError = false) => {
    setConversations((prev) =>
      prev.map((conv) =>
        conv.id === activeConversation
          ? {
              ...conv,
              messages: conv.messages.map((msg) => (msg.id === botId ? { ...msg, content, loading: false, error: isError } : msg)),
            }
          : conv
      )
    );
  };

  return (
    <div className={`app-container ${theme === "light" ? "light-theme" : "dark-theme"}`}>
      <div className={`overlay ${isSidebarOpen ? "show" : "hide"}`} onClick={() => setIsSidebarOpen(false)}></div>
      <Sidebar conversations={conversations} setConversations={setConversations} activeConversation={activeConversation} setActiveConversation={setActiveConversation} theme={theme} setTheme={setTheme} isSidebarOpen={isSidebarOpen} setIsSidebarOpen={setIsSidebarOpen} />

      <main className="main-container">
        <header className="main-header">
          <button onClick={() => setIsSidebarOpen(true)} className="sidebar-toggle">
            <Menu size={18} />
          </button>
        </header>

        {currentConversation.messages.length === 0 ? (
          // Welcome container
          <div className="welcome-container">
            <img className="welcome-logo" src="gemini.svg" alt="Gemini Logo" />
            <h1 className="welcome-heading">Message Gemini</h1>
            <p className="welcome-text">Ask me anything about any topic. I'm here to help!</p>
          </div>
        ) : (
          // Messages container
          <div className="messages-container" ref={messagesContainerRef}>
            {currentConversation.messages.map((message) => (
              <Message key={message.id} message={message} />
            ))}
          </div>
        )}

        {/* Prompt input */}
        <div className="prompt-container">
          <div className="prompt-wrapper">
            <PromptForm conversations={conversations} setConversations={setConversations} activeConversation={activeConversation} generateResponse={generateResponse} isLoading={isLoading} setIsLoading={setIsLoading} />
          </div>
          <p className="disclaimer-text">Gemini can make mistakes, so double-check it.</p>
        </div>
      </main>
    </div>
  );
};

export default App;

The component also handles API requests to generate AI responses, updates the conversation with user input, and creates a smooth, interactive experience through features like message scrolling and typing effects. This file ties everything together to ensure the chatbot functions seamlessly.

Connect Chatbot to the Gemini API

Important: Your chatbot won’t generate responses until it’s connected to the Gemini API. To do this, sign up for a free API key from Google AI Studio. After obtaining your key, create a .env file in the root directory of your project and add the following line, replacing YOUR-API-KEY-HERE with your actual key:

VITE_API_URL=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=YOUR-API-KEY-HERE

Your key should look something like this: AIzaSyAtpnKGX14bTgmx0l_gQeatYvdWvY_wOTQ.

Once you’ve set this up, congratulations! If you’ve followed all the steps correctly, your Gemini AI chatbot should now be live in your browser. Test it by asking questions, toggling the sidebar, switching between themes, and exploring how it responds.

How to Build a Gemini AI Chatbot in React.js and CSS - Light Theme

Conclusion and final words

In conclusion, creating your own Gemini-powered AI chatbot is a fulfilling venture that allows you to dive into the fascinating realms of AI and front-end development. By utilizing the Gemini API alongside React.js, you can develop a polished and functional chatbot equipped with features like theme switching, chat history, and a smooth typing effect.

This project helps you improve your development skills and adds a valuable piece to your portfolio. The experience you gain here will also prepare you for building more advanced applications and exploring AI-powered web solutions.

Keep experimenting! You can add features like file upload, copy to clipboard, speech-to-text, or text-to-speech. Try improving error handling and adding other cool updates to make your chatbot even better.

If you run into any issues, you can download the source code for this Gemini AI chatbot project by clicking the “Download” button. Don’t forget to check the README.md file for setup and usage instructions. If you need help, you’ll also find support information there.

 

Previous articleCreate Responsive Card Slider in HTML CSS & JavaScript | SwiperJS

1 COMMENT

LEAVE A REPLY

Please enter your comment!
Please enter your name here