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 andApp.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.
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.
This looks soooo good! Thanks 🙂