( #ModusHack) Build an AI Chatbot with Dgraph Cloud, Gemini API, React, and GraphQL

Alex AnieAlex Anie
27 min read

The rise of artificial intelligence (AI) has led to the development of an ecosystem of intelligent applications, with chatbots being a prominent example. AI chatbots, such as ChatGPT, Claude, and Gemini, are advanced interactive systems built on massive datasets, designed to generate human-like responses to user queries.

This guide will explore how to build an efficient and scalable AI chatbot by integrating cutting-edge technologies like Dgraph Cloud, Google's Gemini API, React, and GraphQL. This walkthrough will demonstrate how developers can create high-performance conversational experiences by combining the strengths of these modern tools.

We'll start by configuring a robust backend powered by Dgraph Cloud, leveraging its graph database to efficiently manage conversational data. Next, we'll integrate Google's Gemini API for natural language processing (NLP), enabling the chatbot to understand and respond intelligently to user inputs. The frontend will be developed using React, offering a responsive and intuitive interface, while GraphQL will serve as the data query layer, ensuring seamless communication between the frontend and backend.

By the end of this tutorial, you will have a solid understanding of how to create a sophisticated AI chatbot, utilizing React for the frontend, Dgraph Cloud for data management, GraphQL for efficient queries, and the Gemini API for NLP.

Whether you're an experienced developer looking to enhance your AI integration skills or a technologist interested in modern web development, this guide offers practical insights and hands-on implementation strategies for building state-of-the-art AI-driven chatbots.

Prerequisites

  • Basic understanding of:

    • JavaScript and React

    • GraphQL fundamentals

    • API integration concepts

  • Required tools and accounts:

    • Node.js and npm

    • Google Cloud account (for Gemini API)

    • Dgraph Cloud account

    • React development environment setup with vite

Project Overview

This is the complete project of what we will be building. Click here to view it live.

You can find the full course code here.

Dgraph Cloud Configuration and Setup

Dgraph is a native GraphQL database designed to handle complex datasets through graph structures. In this model, data is represented as nodes, which are connected by edges. These edges define relationships and are characterized by predicates, enabling precise and granular management of data for GraphQL developers.

In this section, we'll explore the process of setting up a graph database using Dgraph Cloud for hosting, which will serve as the storage for our dataset.

Create A Dgraph Account

To begin, visit dgraph.io and sign up for a free account. Once registered, select the "Launch a backend" option as shown below.

Next, select the “Free shared cluster“ option, and change the region to “US West (Oregon)“ or any region new to you. Enter a name as “chatbot-app“. Click Launch to initiate the backend.

You should see a pop-up message “Your backend is now live“. click Next to continue.

Now that we have our account setup, let’s write some schema for our dataset.

Schema Setup

Click on the schema tab, paste the schema on the schema editor, and click deploy.

Next, select the “Access“ tab and enable Anonymous Access. Then, check the read and write boxes, as shown below.

GraphQL Endpoint

To access the endpoint, navigate to the Overview section and click the chain icon to copy the endpoint URL. Ensure you save it for future use.

API Key

To generate an API key, navigate to the “Settings” tab and select the “API Key” section. Click “Create New” to open the dialog box, then copy the generated API key and securely store it for future reference

Testing API with Postman

To validate the dataset against the defined schema, go to the GraphQL tab and input the following data.

Open a Postman client and enter the GraphQL endpoint. Specify the dataset to retrieve; in this example, we are querying all sample data. Once configured, click the Query button to execute the request and fetch the data

The dataset should return the sample data stored in the Dgraph database. If an error occurs, verify the settings in the Access tab and ensure the appropriate access permissions are granted. Navigate to Authorization, select Bearer Token, and input the API endpoint. This step is crucial if Anonymous Access is disabled.

Frontend Development with React

Now that we have the database setup, let’s set up the frontend of our application using React and Vite.

I have a React.js setup using Vite.js, with a simple boilerplate of Hello World text.

The frontend application is structured into two primary components:

  1. AsideNav: This component stores and displays the chat history. It allows users to create new chats and navigate between existing conversations.

  2. MainContext: This is the core interface of the application, where users can input prompts and receive responses

The AsideNav Components.

Create a folder called Components and a file named AsideNav as src/components/AsideNav.jsx

export default function AsideNav() {
  return (
    <div>
        {/* <!-- Sidebar --> */}
      <aside className="sidebar">
        <section className="sidebar-container">
          <header>
            <a href="/">
            <nav>
              <div className="new-chat-logo">
                <div>
                  <img src="https://framerusercontent.com/images/iZ4EXrlvpPlxiLFsOrcqwzPbOc.svg" alt="hypermode logo" />
                </div>
                <p>New Chat</p>
              </div>

              <div className="new-chat-icon">
                  <div>
                    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="icon-md"><path fillRule="evenodd" clipRule="evenodd" d="M16.7929 2.79289C18.0118 1.57394 19.9882 1.57394 21.2071 2.79289C22.4261 4.01184 22.4261 5.98815 21.2071 7.20711L12.7071 15.7071C12.5196 15.8946 12.2652 16 12 16H9C8.44772 16 8 15.5523 8 15V12C8 11.7348 8.10536 11.4804 8.29289 11.2929L16.7929 2.79289ZM19.7929 4.20711C19.355 3.7692 18.645 3.7692 18.2071 4.2071L10 12.4142V14H11.5858L19.7929 5.79289C20.2308 5.35499 20.2308 4.64501 19.7929 4.20711ZM6 5C5.44772 5 5 5.44771 5 6V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V14C19 13.4477 19.4477 13 20 13C20.5523 13 21 13.4477 21 14V18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18V6C3 4.34314 4.34315 3 6 3H10C10.5523 3 11 3.44771 11 4C11 4.55228 10.5523 5 10 5H6Z" fill="currentColor"></path></svg>
                  </div>
              </div>
            </nav>
          </a>

            {/* <!-- History --> */}
            <div className="history cus-scrollbar">
              <ul>

              </ul>
            </div>

            {/* <!-- Upgrade plan and User profile  --> */}
            <div className="bottom-info">
                <div className="Upgrade-plan">
                    <a href='https://ai.google.dev/' target="_black" className="">
                        <div className="">
                            {/* <!-- SVG --> */}
                            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="white" viewBox="0 0 24 24" className="icon-sm shrink-0"><path fill="currentColor" d="M6.394 4.444c.188-.592 1.024-.592 1.212 0C8.4 8.9 9.1 9.6 13.556 10.394c.592.188.592 1.024 0 1.212C9.1 12.4 8.4 13.1 7.606 17.556c-.188.592-1.024.592-1.212 0C5.6 13.1 4.9 12.4.444 11.606c-.592-.188-.592-1.024 0-1.212C4.9 9.6 5.6 8.9 6.394 4.444m8.716 9.841a.41.41 0 0 1 .78 0c.51 2.865.96 3.315 3.825 3.826.38.12.38.658 0 .778-2.865.511-3.315.961-3.826 3.826a.408.408 0 0 1-.778 0c-.511-2.865-.961-3.315-3.826-3.826a.408.408 0 0 1 0-.778c2.865-.511 3.315-.961 3.826-3.826Zm2.457-12.968a.454.454 0 0 1 .866 0C19 4.5 19.5 5 22.683 5.567a.454.454 0 0 1 0 .866C19.5 7 19 7.5 18.433 10.683a.454.454 0 0 1-.866 0C17 7.5 16.5 7 13.317 6.433a.454.454 0 0 1 0-.866C16.5 5 17 4.5 17.567 1.317"></path></svg>
                        </div>
                        <div className="">
                            <p className="">Upgrade plan</p>
                            <p className="">Google's AI, @ AI Studio </p>
                        </div>
                      </a>
                </div>

                <div className="user-profile">
                    <a href="https://ocxigin.hashnode.dev/" target="_blank" className="flex gap-3">
                        <div className="">
                            <img src="https://user-images.githubusercontent.com/78242022/242978218-d0e4eba2-62f7-4464-b2fb-c89835b6e592.jpg" alt="" className=""/>
                        </div>
                        <p>Alex Anie</p>
                    </a>
                </div>
            </div>
          </header>
        </section>
      </aside>
    </div>
  )
}

The above code consists of the New Chat button for adding a new chat when a new conversation is required and a history section where all chat Histories are saved. The history section is specified at <div className="history cus-scrollbar">.

Next, import the necessary CDN links for the fonts used and specify them in the HTML file. Click on the index.html and restructure the code as follows.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>HypermodeGTP3.5</title>
    <meta name="author" content="Alex Anie">
    <link rel="icon" type="image/svg+xml" href="../src/assets/hypermode-logo-black-white.png" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700&display=swap" rel="stylesheet">
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>

Next, import the AsideNav.jsx components on the App.jsx components.

import './App.css'
👉 import AsideNav from './components/asideNav'
function App() {
  return (
    <>
      <main className='site'>
      👉 <AsideNav /> 
      </main>   
    </>
  )
}
export default App

Now let’s add some CSS styles to make the AsideNav look nice.

*, *::after, *::before {
    font-family: var(--quicksand);
    margin: 0;
    padding: 0;
  }

  img {
    width: 100%;
  }

  a{
    text-decoration: none;
    color: white;
  }

  :root {
      --quicksand: "Quicksand", sans-serif;
      --bg-sidebar: #171717;
      --bg-main: #212121;
      --fainted-gray: #9b9b9b;
  }

  /* Main */
  .site {
    display: flex;
  }

  /* Sidebar */
  .sidebar {
    display: none;
    background-color: var(--bg-sidebar);
    width: 18em;
    height: 100vh;
    color: white;
    position: relative;
  }

  @media (min-width: 768px) {
    .sidebar {
      display: block;
    }
  }

  .sidebar-container {
    width: 90%;
    margin: 1em auto;
  }

  .new-chat-logo div {
    width: 30px;
    height: 30px;
    background: linear-gradient(90deg, rgba(46,46,163,1) 14%, rgba(3,1,34,1) 81%);
    border-radius: 100px;
    padding: 4px;
  }

  .sidebar nav {
    display: flex;
    justify-content: space-between;
    background-color: var(--bg-sidebar);
    width: 90%;
    position: absolute;
    top: 1em;
    border-radius: 10px;
    -webkit-user-select: none;
    user-select: none;
    cursor: pointer;
  }

  .sidebar .new-chat-logo {
      display: flex;
      justify-content: space-between;
      cursor: pointer;
      border-radius: 10px;
      padding: 6px;
  }

  .sidebar nav:hover {
    background-color: rgba(255,255,255, 0.2);
  }

  .new-chat-logo p {
    display: inline-block;
    transform: translate(10px, 10px);
  }

  .new-chat-icon div svg {
    display: inline-block;
    transform: translate(-10px, 14px);
  }

/* History */
.history {
margin-top: 5em;
height: 24em;

overflow-y: auto;
}

.cus-scrollbar::-webkit-scrollbar {
width: 1em;
height: 1em;
}

.cus-scrollbar::-webkit-scrollbar-track {
background: white;
border-radius: 100vw;
margin-block: 0.5em;
}

.cus-scrollbar::-webkit-scrollbar-thumb {
background: var(--bg-main);
border: 0.25em solid rgb(240, 147, 7);
border-radius: 100vw;
}

.cus-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(120, 22%, 83%);
}
.history ul li {
font-size: 12px;
padding: 4px;
margin: 4px;
cursor: pointer;
border-radius: 10px;
list-style-type: none;
}

.history ul li:hover{
background-color: rgba(255, 255, 255, 0.2);
}

.bottom-info {
position: absolute;
bottom: 0;
padding: 4px;
background-color: var(--bg-sidebar);
width: 90%;
}

.bottom-info .Upgrade-plan {
padding: 2px 3px;
border-radius: 10px;
-webkit-user-select: none;
user-select: none;
}

.bottom-info .Upgrade-plan:hover {
background-color: rgba(255, 255, 255, 0.2);
}

.bottom-info a {
display: flex;
gap: 0.75rem;
text-decoration: none;
color: white;
padding: 4px;
}

.bottom-info a div:first-child {
width: 30px;
height: 30px;
border: 1px solid white;
border-radius: 100px;
padding: 2px;
}

.bottom-info a div svg {
display: block;
padding: 4px;
}

.bottom-info a div:last-child {
transform: translateY(-2px);
}

.bottom-info a div:last-child p:first-child {
font-size: 14px;
}

.bottom-info a div:last-child p:last-child {
font-size: 11px;
color: var(--fainted-gray);
}
.off {
  display: none;
}

@media (min-width: 768px) {
    .off {
      display: block;
    }
  }

.bottom-info .user-profile {
padding: 4px;
border-radius: 10px;
-webkit-user-select: none;
user-select: none;
}

.bottom-info .user-profile:hover {
background-color: rgba(255, 255, 255, 0.2);
}

.bottom-info .user-profile a {
display: flex;
gap: 0.75rem ;
text-decoration: none;
color: white;
}

.bottom-info .user-profile a div {
width: 30px;
height: 30px;
}

.bottom-info .user-profile a div img {
  width: 100%;
  border-radius: 100px;
}

.bottom-info .user-profile a div p {
transform: translateY(-5px);
}

Your AsideNav should look similar to this.

Here, we have a simple UI for displaying all chat conversations and a new chat button for initiating a new discussion. These are very useful implementations as they provide a better way of interacting with our application.

The mainContent Components.

Now let’s create the mainContent components. Create a file components/MainContent.jsx

export default function MainContent() {
  return (
   <>
          {/* <!-- Main Content --> */}
      <article className="main-content">
        <section className="main-content-section">
          {/* <!-- Heading section --> */}
          <header>
            <nav className="">
              {/* <!-- Hamburger Icon  --> */}
              <div className="hamburger-icon">
                  <div>
                     {/* <!-- SVG --> */}
                     {/* <!-- https://feathericons.dev/?search=align-left&iconset=feather --> */}
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" className="main-grid-item-icon" fill="none" stroke="white" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="17" x2="3" y1="10" y2="10" />
                          <line x1="21" x2="3" y1="6" y2="6" />
                          <line x1="21" x2="3" y1="14" y2="14" />
                          <line x1="17" x2="3" y1="18" y2="18" />
                        </svg>
                  </div>
              </div>

              {/* <!-- Heading Text  --> */}
              <div className="heading-text">
                <h1>HypermodeGPT</h1>
                <span>3.5</span>
                <span>
                {/* <!-- https://feathericons.dev/?search=chevron-down&iconset=feather --> */}
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                      <polyline points="6 9 12 15 18 9" />
                    </svg>
                </span>
              </div>

              {/* <!--  GitHub Link  --> */}
              <div className="github-link">
                <a href="https://github.com/alex-anie/hypermode-GTP-AI-Chat-app.git" target="_blank">
                  {/* <!-- SVG  --> */}
                  {/* <!-- https://feathericons.dev/?search=github&iconset=feather --> */}
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                      <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" />
                    </svg>
                  </a>
              </div>

            </nav>
        </header>

           {/* <!-- Content --> */}
           <div id="response" className="cus-scrollbar">
            <article className="display-content">
              <div className="hypermode-logo-container">
                  <div className="hypermode-logo-wrapper">
                      <div className="">
                        {/* <!-- SVG or Image --> */}
                        <img src="https://framerusercontent.com/images/iZ4EXrlvpPlxiLFsOrcqwzPbOc.svg" alt="hypermode logo" />
                      </div>
                  </div>
                  <h1 className="">How can I help you today?</h1>
              </div>

              <div className="sample-text">
                  <div className="sample-text-container">
                      <h1 className="">What is Physics</h1>
                        <p className="">Learn about what make Physics awesome</p>
                        <div  className="arrow-up-container">
                          {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                              <line x1="12" x2="12" y1="19" y2="5" />
                              <polyline points="5 12 12 5 19 12" />
                            </svg>
                        </div>
                  </div>

                  <div className="sample-text-container">
                    <h1 className="">What is closure in JavaScript</h1>
                    <p className="">Discover how to write better closure in JavaScript</p>
                    <div  className="arrow-up-container">
                        {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="12" x2="12" y1="19" y2="5" />
                          <polyline points="5 12 12 5 19 12" />
                        </svg>
                    </div>
                  </div>

                  <div className="sample-text-container off">
                    <h1 className="">Create a New Year Resolution Plan</h1>
                    <p className="">Discover how to better new year resolution plan</p>
                    <div  className="arrow-up-container">
                         {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="12" x2="12" y1="19" y2="5" />
                          <polyline points="5 12 12 5 19 12" />
                        </svg>
                    </div>
                  </div>

                  <div className="sample-text-container off">
                    <h1 className="">Who was Euclid?</h1>
                    <p className="">Considered the father of geometry</p>
                    <div  className="arrow-up-container">
                         {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="12" x2="12" y1="19" y2="5" />
                          <polyline points="5 12 12 5 19 12" />
                        </svg>
                    </div>
                  </div>
              </div>
          </article>
           </div>

              {/* <!-- Input section --> */}
              <section className="input-section">
                  <form id="mydata" className="mydata">
                      <input autoFocus name="query" placeholder="Ask hypermodeGPT any questions" type="text" id="inputData"></input>
                      <button type="submit">
                         {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="25" height="25" className="main-grid-item-icon" fill="none" stroke="white" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="12" x2="12" y1="19" y2="5" />
                          <polyline points="5 12 12 5 19 12" />
                        </svg>
                      </button>
                  </form>
                  <p className="copyright">Copyright © 2024 - HypermodeGPT AI Model with gemini at pro created by <a href="https://twitter.com/alexanie_" target="_blank"  className="">Alex Anie</a></p>
               </section>
        </section>
      </article>
      </>
  )
}

Next, navigate to App.jsx and import the MainContent.jsx.

import './App.css'
import AsideNav from './components/AsideNav'
👉 import MainContent from './components/mainContent'

function App() {
  return (
    <>
      <main className='site'>
        <AsideNav />
       👉 <MainContent />
      </main>  
    </>
  )
}
export default App

Next, apply the CSS files below to style the MainContent.jsx.

 /* Main Content */
  .main-content {
    background-color: var(--bg-main);
    height: 100vh;
    width: 100%;
    color: white;
  }

  .main-content header nav {
    display: flex;
    justify-content: space-between;
    margin: 1em 2em;
  }

  .main-content header nav .hamburger-icon {
    padding: 2px;
    border-radius: 10px;
    cursor: pointer;
    -webkit-user-select: none;
    user-select: none;
    transition: all 0.5s ease;
  }

  .main-content header nav .hamburger-icon:hover{
    background-color: rgba(255, 255, 255, 0.2);
  }

  .main-content header nav .hamburger-icon svg {
    display: inline-block;
  }

  @media (min-width: 768px) {
    .main-content header nav .hamburger-icon {
        display: none;
    }
  }

 .main-content .heading-text {
    display: flex;
    padding: 4px;
    cursor: pointer;
    border-radius: 10px;
    -webkit-user-select: none;
    user-select: none;
    transition: all 0.5s ease;
  }

  .main-content .heading-text:hover {
    background-color: rgba(255, 255, 255, 0.2);
  }

  .main-content .github-link {
    padding: 4px;
    cursor: pointer;
    border-radius: 10px;
    -webkit-user-select: none;
    user-select: none;
    transition: all 0.5s ease;
  }

  .main-content .github-link:hover {
    background-color: rgba(255, 255, 255, 0.2);
  }

  .main-content .github-link a {
     text-decoration: none;
     color: white;
  }

/*display-content*/
.main-content .display-content {
  display: flex;
  justify-content: space-between;
  flex-direction: column;
  align-items: center;
}

.main-content .hypermode-logo-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  margin-top: 3em;
}

.main-content .hypermode-logo-wrapper {
  display: flex;
  flex-direction: row;
  justify-content: center;
}

.main-content .hypermode-logo-wrapper {
  width: 50px;
  height: 50px;
  padding: 10px;
  background: linear-gradient(90deg, rgba(46,46,163,1) 14%, rgba(3,1,34,1) 81%);
  border-radius: 100%;
}

.main-content .hypermode-logo-container h1 {
  font-size: 1.5rem;
  line-height: 2rem;
  font-weight: bold;
  text-align: center;
  margin: 10px 0;
}

.main-content .sample-text {
  display: grid;
  grid-template-columns: repeat(1, minmax(0, 1fr));
  gap: 1em;
}

@media (min-width: 768px) {
  .main-content .sample-text {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.main-content .sample-text-container {
  border: 1px solid var(--fainted-gray);
  border-radius: 10px;
  padding: 10px;
  cursor: pointer;
  position: relative;
}

.main-content .sample-text-container:hover {
  background-color: rgba(255, 255, 255, 0.2);
}

.main-content .sample-text-container h1 {
  font-size: 14px;
}

.main-content .sample-text-container p {
  font-size: 11px;
  color: var(--fainted-gray);
}

.main-content .sample-text-container .arrow-up-container {
  display: none;
  background-color: var(--bg-sidebar);
  padding: 1px;
  width: -moz-fit-content;
  width: fit-content;
  border-radius: 5px;
  position: absolute;
  right: 5px;
  bottom: 13px;
}

.main-content .sample-text-container:hover .arrow-up-container {
  display: block;
}

/* input section... */

.input-section {
    width: 80%;
    margin: 4px auto;
    position: absolute;
    bottom: 3.5em;
    left: 0;
    right: 0;
  }

  @media (min-width: 768px) {
  .input-section {
    width: 50%;
    position: absolute;
    bottom: 3.5em;
    left: 35%;
    margin: 0;
  }
  }

  .input-section .mydata {
  display: flex;
  justify-content: center;
  align-items: center;

  border: 1px solid var(--fainted-gray);
  border-radius: 10px;
  padding: 10px;
  background-color: var(--bg-main);
  }

  .input-section .mydata input {
  width: 80%;
  height: 20px;
  background-color: transparent;
  border: none;
  padding: 5px;
  color : white;
  font-size: 16px;
  }

  .input-section .mydata input::placeholder {
  color: var(--fainted-gray);
  }

  .input-section .mydata:focus-within {
  outline: 1px solid var(--fainted-gray);
  }

  .input-section .mydata input:focus {
  outline: none; 
  }

  .input-section .mydata button {
    height: 30px;
    width: 30px;
    margin-left: 10px;
    background-color: var(--fainted-gray);
    border-radius: 10px;
    cursor: pointer;
    -webkit-user-select: none;
    user-select: none;
    transition: all 0.5s ease;
  }

  .input-section .mydata button:hover {
    background-color: var(--bg-sidebar);
  }

  .input-section .copyright {
    margin: 1em auto;
    font-size: 14px;
    color: var(--fainted-gray);
    text-align: center;
  }

  .input-section .copyright a {
  color: white;
  text-decoration: none;
  }

  .input-section .copyright a:hover {
  color: orange;
  text-decoration: underline;
  }

/* RESPONSE ELEMENT */
.main-content .response {
  width: 60%;
  height: 300px;
  margin: 10px auto;
  padding: 20px;
  overflow-y:auto;
  line-height: 30px;
}

Your App should look similar to this.

Here is where we type our prompt and also get a response. We can interact with the predefined template text as a quick way to test the prompt.

Apollo and GraphQL setup

To setup GraphQL, we need to install the following npm packages; Apollo Server, GraphQL, and GraphQL Tag.

npm install @apollo/server graphql graphql-tag

Next, create a src/schema.js file and type the code below to define the schema for the GraphQL data.

import gql from "graphql-tag";

const typeDefs = gql`
  type Chat {
    id: ID!
    title: String!
    message: String!    # Add message field
    author: String!     # Add author field
    timestamp: String!
    userId: ID!
  }

  type Query {
    getChatHistory(userId: ID!): [Chat]
    getChatById(id: ID!): Chat
  }

  type Mutation {
    createChat(title: String!, message: String!, author: String!, userId: ID!): Chat
    switchChat(id: ID!, userId: ID!): Chat
  }
`;

export default typeDefs;

Next, create another file src/server.js. Here we will setup the Apollo Server with GraphQL.

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import resolvers from './resolver.js';
import TrackAPI from './datasources/track-api.js';
import typeDefs from './schema.js';
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

async function startApolloServer() {
  const server = new ApolloServer({
    typeDefs,
    resolvers,
  });

  const { url } = await startStandaloneServer(server, {
    context: async () => {
      // Use Apollo Client to interact with Dgraph from the server side
      const client = new ApolloClient({
        uri: import.meta.env.VITE_DGRAPH_ENDPOINT,
        headers: {
          'Authorization': `Bearer ${import.meta.env.VITE_DGRAPH_API_KEY}`,
        },
        cache: new InMemoryCache(),
      });

      return {
        dataSources: {
          trackAPI: new TrackAPI({ client }),
        },
      };
    },
  });

  console.log(`Server is running! Query at ${url}`);
}

startApolloServer();

Next, create a file as resolver.js

import { v4 as uuidv4 } from 'uuid'; // For generating unique IDs


const resolvers = {
  Query: {
    // Fetch chat history for a specific user
    getChatHistory: async (_, { userId }, { dgraphClient }) => {
      const query = `
        query getChatHistory($userId: string) {
          getChatHistory(func: eq(Chat.userId, $userId)) {
            id
            title
            message       # Include message
            author        # Include author
            timestamp
            userId
          }
        }
      `;

      const response = await dgraphClient.newTxn().queryWithVars(query, { $userId: userId });
      const result = response.getJson();
      return result.getChatHistory || [];
    },

    // Fetch a specific chat by ID
    getChatById: async (_, { id }, { dgraphClient }) => {
      const query = `
        query getChatById($id: string) {
          getChatById(func: eq(Chat.id, $id)) {
            id
            title
            message      
            author       
            timestamp
            userId
          }
        }
      `;

      const response = await dgraphClient.newTxn().queryWithVars(query, { $id: id });
      const result = response.getJson();
      return result.getChatById[0] || null;
    },
  },

  Mutation: {
    // Create a new chat
    createChat: async (_, { title, message, author, userId }, { dgraphClient }) => {
      const chat = {
        id: uuidv4(),
        title,
        message,       
        author,  
        timestamp: new Date().toISOString(),
        userId,
      };

      const mutation = {
        set: [chat],
      };

      const txn = dgraphClient.newTxn();
      try {
        await txn.mutate({ setJson: mutation });
        await txn.commit();
        return chat;
      } finally {
        await txn.discard();
      }
    },

    // Switch chat by ID
    switchChat: async (_, { id, userId }, { dgraphClient }) => {
      const query = `
        query getChatById($id: string) {
          getChatById(func: eq(Chat.id, $id)) {
            id
            title
            message     
            author     
            timestamp
            userId
          }
        }
      `;

      const response = await dgraphClient.newTxn().queryWithVars(query, { $id: id });
      const result = response.getJson();
      return result.getChatById[0] || null;
    },
  },
};

export default resolvers;

Next, open up the terminal and install Apollo-Client. We use this to setup Apollo on our app

npm install @apollo/client

Next, navigate tomain.jsx and configure the Apollo server as follows

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx';

import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache(),
});

createRoot(document.getElementById('root')).render(
  <ApolloProvider client={client}>
     <StrictMode>
      <App />
  </StrictMode>
  </ApolloProvider>,
)

Next, install nodemon — Nodemon enables the server to restart itself when a change it made.

npm install nodemon

Next, open the package.json file and configure nodemon as follows.

  "scripts": {
   //... some code here
    "nodemon": "nodemon src/server.js",
    // ...some code commented here
  },

Nodemon will startup the server for the app, you should see a display text on the terminal that reads “Server is running... http://localhost:4000/“. This indicates the server is now running.

Integrating Gemini API

Google Gemini offers an API that allows developers to interact with it by sending prompts and receiving responses. To begin using the API, visit ai.google.dev and click on the "Get API Key" button in Google AI Studio to generate your credentials.

Click the "Create API Key" button to generate an API key, then copy the key. Next, create a .env file in your project directory and add the API key to it for secure access.

Your project's .env file should also contain the GraphQL endpoint from Dgraph along with the corresponding API key. For a Vite app, environment variables must be prefixed with VITE_. Ensure to name each endpoint and API key clearly and appropriately following this convention.

Next, To use the Gemini API in your application, you need to install the GoogleGenerativeAI package for Node.js:

npm install @google/generative-ai

Implementing Chatbots functionality

So far we’ve added lot’s of packages that makes up the core functionality of our app. Here we are going to bring everything together so as to have a functioning chatbot application. Create a folder and inside it create a file as context/ContextProvider.jsx.

import { createContext, useState, useEffect } from "react";
import { GoogleGenerativeAI } from '@google/generative-ai';

export const Context = createContext();

const ContextProvider = (props) => {
    const [input, setInput] = useState("");
    const [messages, setMessages] = useState([]);
    const [isLoading, setIsLoading] = useState(false);
    const [chatHistory, setChatHistory] = useState([]); 
    const [currentChatId, setCurrentChatId] = useState(null); 

    // Load chat history from localStorage on initial render
    useEffect(() => {
        const savedHistory = localStorage.getItem('chatHistory');
        if (savedHistory) {
            setChatHistory(JSON.parse(savedHistory));
        }
    }, []);

    // Save chat history to localStorage whenever it changes
    useEffect(() => {
        localStorage.setItem('chatHistory', JSON.stringify(chatHistory));
    }, [chatHistory]);

    // Initialize Gemini AI
    const apiKey = import.meta.env.VITE_GEMINI_API_KEY;
    if (!apiKey) {
        console.error("Gemini API key is missing. Please check your .env file.");
    }

    const genAI = new GoogleGenerativeAI(apiKey);
    const model = genAI.getGenerativeModel({ model: "gemini-pro" });
    const chat = model.startChat({
        history: [],
        generationConfig: {
            maxOutputTokens: 500,
        },
    });

    // Create new chat
    const createNewChat = () => {
        const newChatId = Date.now().toString();
        const newChat = {
            id: newChatId,
            title: "New Chat",
            messages: [],
            timestamp: new Date().toISOString()
        };

        setChatHistory(prev => [newChat, ...prev]);
        setCurrentChatId(newChatId);
        setMessages([]);
    };

    // Switch to existing chat
    const switchChat = (chatId) => {
        const chat = chatHistory.find(c => c.id === chatId);
        if (chat) {
            setCurrentChatId(chatId);
            setMessages(chat.messages);
        }
    };

    // Update chat title based on first message
    const updateChatTitle = (chatId, firstMessage) => {
        setChatHistory(prev => prev.map(chat => {
            if (chat.id === chatId) {
                // Truncate title if it's too long
                const title = firstMessage.length > 30 
                    ? firstMessage.substring(0, 30) + '...' 
                    : firstMessage;
                return { ...chat, title };
            }
            return chat;
        }));
    };

    // Handle sending messages
    const handleSendMessage = async (e) => {
        e?.preventDefault();
        if (!input.trim()) return;

        // Create new chat if none exists
        if (!currentChatId) {
            createNewChat();
        }

        const userMessage = { role: 'user', content: input, timestamp: new Date().toISOString() };

        // Update messages and chat history
        setMessages(prev => [...prev, userMessage]);
        setChatHistory(prev => prev.map(chat => {
            if (chat.id === currentChatId) {
                const updatedMessages = [...chat.messages, userMessage];
                // Update title if this is the first message
                if (chat.messages.length === 0) {
                    updateChatTitle(currentChatId, input);
                }
                return { ...chat, messages: updatedMessages };
            }
            return chat;
        }));

        setIsLoading(true);
        try {
            if (!apiKey) throw new Error("API key is missing");

            const result = await chat.sendMessageStream(input);
            let responseText = "";

            for await (const chunk of result.stream) {
                const chunkText = chunk.text();
                responseText += chunkText;

                // Update messages in real-time
                const assistantMessage = { 
                    role: 'assistant', 
                    content: responseText,
                    timestamp: new Date().toISOString()
                };

                setMessages(prev => {
                    const newMessages = [...prev];
                    const lastMessage = newMessages[newMessages.length - 1];
                    if (lastMessage?.role === 'assistant') {
                        newMessages[newMessages.length - 1] = assistantMessage;
                    } else {
                        newMessages.push(assistantMessage);
                    }
                    return newMessages;
                });

                // Update chat history
                setChatHistory(prev => prev.map(chat => {
                    if (chat.id === currentChatId) {
                        const updatedMessages = [...chat.messages];
                        const lastMessage = updatedMessages[updatedMessages.length - 1];
                        if (lastMessage?.role === 'assistant') {
                            updatedMessages[updatedMessages.length - 1] = assistantMessage;
                        } else {
                            updatedMessages.push(assistantMessage);
                        }
                        return { ...chat, messages: updatedMessages };
                    }
                    return chat;
                }));
            }
        } catch (error) {
            console.error("Detailed error:", error);
            const errorMessage = {
                role: 'assistant',
                content: `Error: ${error.message || 'Something went wrong. Please try again.'}`,
                isError: true,
                timestamp: new Date().toISOString()
            };
            setMessages(prev => [...prev, errorMessage]);
            setChatHistory(prev => prev.map(chat => {
                if (chat.id === currentChatId) {
                    return { ...chat, messages: [...chat.messages, errorMessage] };
                }
                return chat;
            }));
        } finally {
            setIsLoading(false);
            setInput("");
        }
    };

    const handleSampleClick = async (heading) => {
        setInput(heading);
        await handleSendMessage();
    };

    const contextValue = {
        input,
        setInput,
        handleSampleClick,
        messages,
        isLoading,
        handleSendMessage,
        chatHistory,
        currentChatId,
        createNewChat,
        switchChat
    };

    return (
        <Context.Provider value={contextValue}>
            {props.children}
        </Context.Provider>
    );
};

export default ContextProvider;

Here we implement a ContextProvider component, that uses the Google Gemini API to handle chat interactions with generative AI. It manages chat history, messages, and interactions using useState and useEffect.

  1. State Management:

    • input: Stores the user's input.

    • messages: Stores the current conversation's messages.

    • isLoading: Indicates whether a message is being processed.

    • chatHistory: Stores a list of previous chats, each with its own messages and metadata.

    • currentChatId: Tracks the active chat session.

  2. Loading and Saving Chat History:

    • On component mount, chat history is loaded from localStorage.

    • Chat history is saved to localStorage whenever it changes.

  3. Google Gemini API Initialization:

    • The Gemini AI is initialized using an API key stored in the .env file.

    • A chat session is created using the startChat() method from the Gemini API.

  4. Chat Functionality:

    • createNewChat: Initializes a new chat, adding it to the chat history.

    • switchChat: Switches between existing chats based on the selected chat ID.

    • updateChatTitle: Updates the title of a chat based on the first message.

    • handleSendMessage: Sends user input to Gemini AI, processes the response in real-time, and updates the chat messages. It handles potential errors and updates the chat history dynamically.

  5. Stream Handling:

    • Real-time streaming of Gemini AI responses is handled within the handleSendMessage function, updating the messages and history as new content is received.
  6. Context Provider:

    • The component uses the createContext API to expose the state and methods (input handling, message sending, chat switching, etc.) to child components via Context.Provider.

Overall, this component enables a chat interface with Google Gemini, storing conversations in local storage, and managing multiple chat sessions.

Next, let’s update the MainContext.jsx component using the return values from ContextProvider component.

import { useContext } from "react";
import { Context } from "../context/ContextProvider";

export default function MainContent() {
  function templeteMsg(text){
    setInput(text)
  }

  const { 
    setInput, 
    input, 
    handleSampleClick, 
    messages, 
    isLoading,
    handleSendMessage 
} = useContext(Context);

  return (
   <>
          {/* <!-- Main Content --> */}
      <article className="main-content">
        <section className="main-content-section">
          {/* <!-- Heading section --> */}
          <header>
            <nav className="">
              {/* <!-- Hamburger Icon  --> */}
              <div className="hamburger-icon">
                  <div>
                     {/* <!-- SVG --> */}
                     {/* <!-- https://feathericons.dev/?search=align-left&iconset=feather --> */}
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" className="main-grid-item-icon" fill="none" stroke="white" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="17" x2="3" y1="10" y2="10" />
                          <line x1="21" x2="3" y1="6" y2="6" />
                          <line x1="21" x2="3" y1="14" y2="14" />
                          <line x1="17" x2="3" y1="18" y2="18" />
                        </svg>
                  </div>
              </div>

              {/* <!-- Heading Text  --> */}
              <div className="heading-text">
                <h1>HypermodeGPT</h1>
                <span>3.5</span>
                <span>
                {/* <!-- https://feathericons.dev/?search=chevron-down&iconset=feather --> */}
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                      <polyline points="6 9 12 15 18 9" />
                    </svg>
                </span>
              </div>

              {/* <!--  GitHub Link  --> */}
              <div className="github-link">
                <a href="https://github.com/alex-anie/hypermode-GTP-AI-Chat-app.git" target="_blank">
                  {/* <!-- SVG  --> */}
                  {/* <!-- https://feathericons.dev/?search=github&iconset=feather --> */}
                    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                      <path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" />
                    </svg>
                  </a>
              </div>

            </nav>
        </header>

           {/* <!-- Content --> */}
           <div id="response" className={`cus-scrollbar ${messages.length > 0 ? 'response' : ''}`}>
            <article className="display-content">
              {
                messages.length === 0 ? (
                  <>
                    <div className="hypermode-logo-container">
                  <div className="hypermode-logo-wrapper">
                      <div className="">
                        {/* <!-- SVG or Image --> */}
                        <img src="https://framerusercontent.com/images/iZ4EXrlvpPlxiLFsOrcqwzPbOc.svg" alt="hypermode logo" />
                      </div>
                  </div>
                  <h1 className="">How can I help you today?</h1>
              </div>

              <div className="sample-text" >
                  <div className="sample-text-container" onClick={()=> templeteMsg('What is Dgraph')}>
                      <h1 className="">What is Dgraph</h1>
                        <p className="">Learn about what make Dgraph awesome</p>
                        <div  className="arrow-up-container">
                          {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                              <line x1="12" x2="12" y1="19" y2="5" />
                              <polyline points="5 12 12 5 19 12" />
                            </svg>
                        </div>
                  </div>

                  <div className="sample-text-container" onClick={()=> templeteMsg('What is closure in JavaScript')}>
                    <h1 className="">What is closure in JavaScript</h1>
                    <p className="">Discover how to write better closure in JavaScript</p>
                    <div  className="arrow-up-container">
                        {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="12" x2="12" y1="19" y2="5" />
                          <polyline points="5 12 12 5 19 12" />
                        </svg>
                    </div>
                  </div>

                  <div className="sample-text-container off"  onClick={()=> templeteMsg('Create a New Year Resolution Plan')}>
                    <h1 className="">Create a New Year Resolution Plan</h1>
                    <p className="">Discover how to better new year resolution plan</p>
                    <div  className="arrow-up-container">
                         {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="12" x2="12" y1="19" y2="5" />
                          <polyline points="5 12 12 5 19 12" />
                        </svg>
                    </div>
                  </div>

                  <div className="sample-text-container off" onClick={()=> templeteMsg('Who was Euclid?')}>
                    <h1 className="">Who was Euclid?</h1>
                    <p className="">Considered the father of geometry</p>
                    <div  className="arrow-up-container">
                         {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" className="main-grid-item-icon" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="12" x2="12" y1="19" y2="5" />
                          <polyline points="5 12 12 5 19 12" />
                        </svg>
                    </div>
                  </div>
              </div>
                  </>
                ) 
                : 
                (
                  <div className="chat-messages">
                    {messages.map((message, index) => (
                        <div 
                            key={index} 
                            className={`message ${message.role === 'user' ? 'user-message' : 'assistant-message'}`}
                        >
                            <div className="message-content">
                                {message.content}
                            </div>
                        </div>
                    ))}
                    {isLoading && (
                        <div className="typing-indicator">
                            AI is typing...
                        </div>
                    )}
              </div>
                )
              }

          </article>
           </div>

              {/* <!-- Input section --> */}
              <section className="input-section">
                  <form id="mydata" className="mydata" onSubmit={handleSendMessage}>
                      <input 
                      autoFocus 
                      name="query" 
                      placeholder="Ask hypermodeGPT any questions" 
                      type="text" 
                      id="inputData"  
                      value={input}
                      onChange={(e) => setInput(e.target.value)}/>
                      <button type="submit" disabled={isLoading}>
                         {/* <!-- https://feathericons.dev/?search=arrow-up&iconset=feather --> */}
                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="25" height="25" className="main-grid-item-icon" fill="none" stroke="white" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
                          <line x1="12" x2="12" y1="19" y2="5" />
                          <polyline points="5 12 12 5 19 12" />
                        </svg>
                      </button>
                  </form>
                  <p className="copyright">Copyright © 2024 - HypermodeGPT AI Model with gemini at pro created by <a href="https://twitter.com/alexanie_" target="_blank"  className="">Alex Anie</a></p>
               </section>
        </section>
      </article>
      </>
  )
}

Next, navigate to main.jsx and import the ContextProivder as follows

// ... some code above here
import ContextProvider from './context/ContextProvider.jsx';

const client = new ApolloClient({
//... some code here
});

createRoot(document.getElementById('root')).render(
  <ApolloProvider client={client}>
    <ContextProvider> // make your change here
        <StrictMode>
          <App />
      </StrictMode>
    </ContextProvider> // make your change here
  </ApolloProvider>,
)

Now lets apply some CSS to make things look nice.

/* AI Chat Styling */
.chat-messages {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1rem;
}

.message {
  max-width: 80%;
  padding: 1rem;
  border-radius: 0.5rem;
  animation: fadeIn 0.3s ease-in-out;
}

.user-message {
  align-self: flex-end;
  background-color: #2563eb;
  color: white;
}

.assistant-message {
  align-self: flex-start;
  background-color: #1f2937;
  color: white;
}

.message-content {
  white-space: pre-wrap;
  word-break: break-word;
}

.typing-indicator {
  align-self: flex-start;
  color: #6b7280;
  font-style: italic;
  padding: 0.5rem;
}

@keyframes fadeIn {
  from {
      opacity: 0;
      transform: translateY(10px);
  }
  to {
      opacity: 1;
      transform: translateY(0);
  }
}

/* Second changes */
/* Add these styles to your CSS */
.chat-history-item {
  padding: 0.75rem 1rem;
  margin: 0.25rem 0;
  border-radius: 0.5rem;
  cursor: pointer;
  transition: background-color 0.2s ease;
}

.chat-history-item:hover {
  background-color: rgba(255, 255, 255, 0.1);
}

.chat-history-item.active {
  background-color: rgba(255, 255, 255, 0.15);
}

.chat-history-content {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.chat-title {
  font-size: 0.875rem;
  font-weight: 500;
  color: white;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.chat-date {
  font-size: 0.75rem;
  color: rgba(255, 255, 255, 0.6);
}

.history {
  flex-grow: 1;
  overflow-y: auto;
  margin: 1rem 0;
}

.history ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

Now save all your changes. Your app should look similar as indicated below.

from the image illustration, you will notice that the responses are generated in markdown format, and loading them directly on plain HTML makes everything look ugly. Let’s fix this using React Markdown.

npm i react-markdown

Next, navigate to MainContext.jsx, import Markdown form react-markdown, and use it to wrap the message.content object. This will help to properly format the markdown text to HTML.

//...some code here
import Markdown from 'react-markdown'

export default function MainContent() {
 // ... some code are commented here
  return (
   <>
       (
          <div className="chat-messages">
             {messages.map((message, index) => (
            👉 <Markdown>
                  {message.content}
            👉 </Markdown>
             )
        )
      </>
  )
}

Now let’s try our app again. Try inputting any text or click on the first button sample “What is Dgraph“ and wait for the response.

Congratulations! 🥳 our app is now functioning as expected.

But wait! We are not done yet, let’s add the history functionality to the AsideNav.

import { useContext } from "react";
import { Context } from "../context/ContextProvider";

export default function AsideNav() {
👉  const { 
    chatHistory, 
    currentChatId, 
    createNewChat, 
    switchChat 
  } = useContext(Context);

👉  const formatDate = (timestamp) => {
    const date = new Date(timestamp);
    return date.toLocaleDateString('en-US', {
      month: 'short',
      day: 'numeric'
    });
  };

  return (
    <>
        {/* <!-- Sidebar --> */}
      <aside className="sidebar">
     👉  <div onClick={createNewChat} style={{ cursor: 'pointer' }}>
                {/* some code here */}
           </div>

            {/* <!-- History --> */}
            <div className="history cus-scrollbar">
              <ul>
           👉  {chatHistory?.map((chat) => (
                  <li 
                    key={chat.id}
                    onClick={() => switchChat(chat.id)}
                    className={`chat-history-item ${currentChatId === chat.id ? 'active' : ''}`}
                  >
                    <div className="chat-history-content">
                 👉     <span className="chat-title">{chat.title}</span>
                      <span className="chat-date">{formatDate(chat.timestamp)}</span>
                    </div>
                  </li>
                ))}
              </ul>
            </div>
              {/*some code here*/}     
        </section>
      </aside>
    </>
  )
}

Next, update the main.jsx as follows

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx';

import { ApolloProvider, ApolloClient, InMemoryCache } from '@apollo/client';
import ContextProvider from './context/ContextProvider.jsx';

const client = new ApolloClient({
  uri: `${import.meta.env.VITE_DGRAPH_ENDPOINT}`, // Dgraph Cloud End point
  headers: {
    'Authorization': `Bearer ${import.meta.env.VITE_DGRAPH_API_KEY}`,  // API Key
  },
  cache: new InMemoryCache(),
});

createRoot(document.getElementById('root')).render(
  <ApolloProvider client={client}>
    <ContextProvider>
        <StrictMode>
          <App />
      </StrictMode>
    </ContextProvider>
  </ApolloProvider>,
)

Now lets test our fullstack app. Type any text as prompt or any of the available buttons.

Congratulations! 🤩 Once again, if you made it this far, we have come to the end of this project. Feel free to play with the code here (full project).

Conclusion

In the just concluded blog post, we explored how to build an efficient and scalable AI chatbot by integrating cutting-edge technologies like Dgraph Cloud, Google's Gemini API, React, and GraphQL. This walkthrough helps demonstrate how developers can create high-performance conversational experiences by combining the strengths of these modern tools.

0
Subscribe to my newsletter

Read articles from Alex Anie directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alex Anie
Alex Anie

A Frontend Developer | Technical Writer | JavaScript Developer | React.js | Freelancer at LambdaTest Inc. Contact: Twitter @alexanie_ | Email: ocxigin@gmail.com