How to Create a ChatBot Assistant using NextJs, TailwindCss and Strapi

Shada - Dec 1 '21 - - Dev Community

Communication is key to building great brands, in this digital age we have got lots of opportunities and ways to carry out great communications. From social media to emails, to text and calls, the limit of communications has been tested. One major innovation in communication has got to be ChatBots.

Simply put a chatbot is a software application used to conduct an online chat conversation via text or text-to-speech. It can be used to substitute a real chat with a live human agent. It has been used in various scenarios ranging but not limited to personal assistants, customer support, booking of tickets, e-shopping, also e-banking to mention a few.

This tutorial will show you how to build a Rule-Based Chatbot Assistant using Next.js and Tailwind CSS as frontend and Strapi headless CMS as backend. .

You can find the link to the finished frontend code here, as well as the backend code here.

Advantages of using Chatbot

It is necessary that before we proceed further into this article, we have a picture of what chatbot brings to the table when used adequately. Some of the advantages of using a chatbot include:

  1. Availability: Since a chatbot is a software application that can be hosted, it means it doesn't sleep, hence it is always available at any time of the day. This gives a company or person a great brand image that at any time of the day clients can get support on their varying requests.
  2. Cost reduction: As an application that can be deployed in multiple places, it reduces the cost of running a large number of customer care representatives.
  3. Efficiency: A single chatbot can be deployed in various places and also simultaneously attend to multiple requests at once.
  4. Improved online presence: Some chatbots allow for the integration to other messaging platforms, this allows a consistent response sent across multiple platforms and which leads to improved brand identity.

Prerequisite

Before starting this tutorial, you need to have

  • Node.js installed on your local machine (v12 or v14) - Check this tutorial for instructions on how to install Node.js
  • Basic understanding of Strapi - Get started with this quick guide
  • Basic knowledge of Next.js
  • Basic knowledge of Tailwind CSS

What is Next Js

Next.js is an awesome React framework for building highly dynamic applications. It comes with pre-rendering, automatic code-splitting amongst many other great features out of the box.

What is Tailwind CSS

Tailwind CSS is a utility-first CSS framework for rapidly building custom user interfaces. With Tailwind CSS, we write our CSS directly in our HTML classes. This is quite useful as we don't need to import an external stylesheet or use a separate library for UI designs.

What is Strapi

Strapi is a Node.js open-source headless CMS that allows us to develop APIs and manage content easily without the hassle of building out a project from scratch. It allows for customization and self-hosting as opposed to the rigid traditional CMS we are used to.

We can easily build out APIs faster and consume the contents via APIs using any REST API client or GraphQL.

Scaffolding a Strapi Project

To setup a new Strapi Project is quite straightforward, just run this command:

    npx create-strapi-app chatbot-assistant --quickstart
Enter fullscreen mode Exit fullscreen mode

Change chatbot-assistant to the preferred name of your project

Nb: During the setup, we would not be using any Strapi template

This would install and create a Strapi application and set up the project locally.

After installation, the browser would open a page on localhost:1337 which would prompt to set up a super admin account to proceed with Strapi.

Building the Interchange collection

Next, we will create a new collection type that will store the details of each question and their respective answers.

Hence, create a collection type called ‘interchange’ that has these fields: question, answer

Clicking “Continue” would bring up another screen to select the fields for this collection. Choose the “Text” field from the list and provide question as its name.

Next, we select the Long Text type in the Base Settings, this would allow us to enter longer and more descriptive questions when creating an interchange.

Next, we proceed to the “Advanced settings” tab and check the “Required field” box to ensure this field is required when creating a new interchange. Also, we check the “Unique field” box to prevent having the same question our interchange.

We click on the Add another field to add the answer field. Below is a table showing the properties for both fields in this collection:

Field Name Field Type Required Unique
question long text true true
answer rich text true false

Seeding Strapi

We would proceed next to define the questions and respective answers given by our bot.

To add data to the collection, we select the Interchanges Collection on the left sidebar, click “Add New Interchanges” and fill in the details.

Interchanges collection

For this tutorial, I would be seeding the interchange with an about me interchange in mind, so our bot can act as an assistant that tells visitors more about us and our services. After seeding our collection looks thus:

Seeded Interchanges collection

Allowing Public access

By default, whenever you create an API, Strapi creates six endpoints from the name given to the API. The endpoints generated for Interchange should look like this:

By default, they’re all going to be restricted from public access. We need to tell Strapi that you’re okay with exposing these checked endpoints to the public. Go to Settings > Users & Permissions Plugin > Roles and click to edit the Public Role. Next, scroll down to permissions and check find for Interchange.

This endpoint: http://localhost:1337/interchanges should now be available. And the sample response this :

    [{"id":2,"question":"What are your skills?","answer":"I am skilled at frontend development as well as backend development.","published_at":"2021-11-10T22:04:01.379Z","created_at":"2021-11-10T22:00:18.983Z","updated_at":"2021-11-10T22:04:01.445Z"},
    {"id":3,"question":"How can I reach you?","answer":"You can reach me via my phone line: 0900000000 or via my twitter handle: interchange_demo.","published_at":"2021-11-10T22:04:09.033Z","created_at":"2021-11-10T22:01:07.886Z","updated_at":"2021-11-10T22:04:09.039Z"},
    {"id":4,"question":"How long have you been a developer?","answer":"6 years.","published_at":"2021-11-10T22:04:15.757Z","created_at":"2021-11-10T22:01:50.037Z","updated_at":"2021-11-10T22:04:15.765Z"},
    {"id":5,"question":"Do you have leadership experience?","answer":"Yes, I do.","published_at":"2021-11-10T22:04:21.346Z","created_at":"2021-11-10T22:02:23.115Z","updated_at":"2021-11-10T22:04:21.354Z"},
    {"id":6,"question":"What other skills do you have apart from software development?\n","answer":"I am a technical writer and also a community builder.","published_at":"2021-11-10T22:04:26.091Z","created_at":"2021-11-10T22:03:21.103Z","updated_at":"2021-11-10T22:04:26.102Z"}]
Enter fullscreen mode Exit fullscreen mode

Scaffolding a NextJs project

Create a Next.js app

To create a Next.js app, open your terminal, cd into the directory you’d like to create the app in, and run the following command:

    npx create-next-app -e with-tailwindcss nextjs-chatbot
Enter fullscreen mode Exit fullscreen mode

This would also configure Tailwind CSS with the project

Run the Next.js development server

Next we cd into the newly created directory, in our case that would be nextjs-chatbot

    cd nextjs-chatbot
Enter fullscreen mode Exit fullscreen mode

After which we start up the development server, by running this command:

    npm run dev
Enter fullscreen mode Exit fullscreen mode

If everything was set up fine, the next.js server should now be running on http://localhost:3000 we should get this shown on our browser:

Building NextJs Components

Next, we proceed to any text editor of our choice to develop the rest of the application. We use Visual Studio Code for this tutorial. Opening up the installed project, we should have a folder structure such as this:

To begin the design of the interface, we would remove all the content of the index.js file which is found inside the pages folder, this is the entry point of the NextJs project. We replace it with the content below:

    import Head from 'next/head'
    import { useState, useEffect } from 'react'
    import { createMarkup, tranformInterchanges, showBotTyping, getBotAnswer, fetchQuery } from '../utils/helper'
    export default function Home( { interchanges }) {
      const [userQuestion, setUserQuestion] = useState('')
      const [allow, setAllow] = useState(false)
      const [interchange, setInterchange] = useState([])

    useEffect(async () => {
     await showBotTyping(setInterchange, [], setAllow)
     setInterchange([{
      owner: false,
      text: tranformInterchanges(interchanges, true)
    }])
    }, [interchanges])

    const handleSubmit = async (e) => {
      e.preventDefault()
      if(!userQuestion || !allow) return
      const uQ = userQuestion
      const newInterchange = [...interchange, {
        owner: true,
        text: userQuestion
      }]
      setInterchange(newInterchange)
      setUserQuestion('')
      setAllow(false)
      getBotAnswer(interchanges, setInterchange,  uQ, newInterchange, setAllow)
    }
      return (
        <div className="flex flex-col font-mono items-center justify-center min-h-screen">
          <Head>
            <title>ChatBot Assistant</title>
            <link rel="icon" href="/favicon.ico" />
          </Head>
          <form className="flex flex-col w-full flex-1" onSubmit={handleSubmit}>

            <header className="flex w-full h-24 fixed bg-black border-b">

              <span className="flex items-center text-white font-bold text-lg p-2"> Bot Assistant</span>
           </header>
            <div className="flex flex-col mt-24 bg-gray-200  overflow-scroll p-2 w-full" style={{ height: "80vh"}}>
            {interchange.map((chat,i) => (
              chat.owner ? 
              <div key={i} className = "user flex flex-row my-2 w-full p-2">
              <span className = "w-2/3"></span>
              <span className = "w-1/3 bg-gray-100 p-2 rounded">
               {chat.text}
              </span>
            </div>
             :   
              <div key={i} className = "bot my-2 bg-gray-100 w-1/2 lg:w-1/3  p-2 rounded">
                <span dangerouslySetInnerHTML={createMarkup(chat.text)} />
              </div>
            ))}
            <div id="scrollTo"></div>
            </div>
            <footer className = "flex flex-row justify-between items-center p-1 h-5/6  w-full -bottom-5">
            <div className = "flex flex-row justify-between flex-1 bg-white w-full">
              <input className = " bg-gray-200 w-2/3 p-2 " placeholder="Type a message" value={userQuestion} onChange={ (e) => { setUserQuestion(e.target.value)}}/>
              <button className = " bg-black p-2 ml-2 w-1/3  text-white" type="submit"> Send</button>
            </div>
            </footer>
          </form>

        </div>
      )
    }

    export async function getStaticProps() {
      const interchanges = await fetchQuery('interchanges')
      return {
        props: {
          interchanges
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

Explanation of above code

The above code is used to display the UI for the chat interface, that is both the messages, input area as well and the send button.

In the upper section we imported the helper function from a helper.js file, these functions would be used to make the app functional:

    import Head from 'next/head'
    import { useState, useEffect } from 'react'
    import { createMarkup, tranformInterchanges, showBotTyping, getBotAnswer, fetchQuery } from '../utils/helper'
Enter fullscreen mode Exit fullscreen mode

Next, using the useState hooks, we create multiple variables we would use later in the app to achieve various purposes:
The userQuestion is used to keep track of the current question being typed by the user, that is the text in the input box.

The allow state is used to prevent a user from sending multiple messages when the bot is currently still replying to a previously sent message.

The interchange state is used to store internally the messages between both the bot and the user

      const [userQuestion, setUserQuestion] = useState('')
      const [allow, setAllow] = useState(false)
      const [interchange, setInterchange] = useState([])
Enter fullscreen mode Exit fullscreen mode

The portion of code in the useEffect fires once the app is started and does the following:
Firstly, it shows an animation of the bot typing for a few seconds.

Next, it sets the first/default message sent by the bot to the user, this message contains the question gotten from the Strapi API when the app was started.

    useEffect(async () => {
     await showBotTyping(setInterchange, [], setAllow)
     setInterchange([{
      owner: false,
      text: tranformInterchanges(interchanges, true)
    }])
    }, [interchanges])
Enter fullscreen mode Exit fullscreen mode

The handleSubmit function is the handler that gets called once the user sends their message to the bot.

We only allow a successful submit if the user indeed has entered value in the text box and also only if the bot doesn’t have a question it is currently processing.

Next, add the new question of the user into the interchange state, remove the typed text of the user from the input box and set the allow state to false, to prevent the user from sending another text till the bot returns with a result.

The getBotAnswer helper function is then used to get the answer from the bot and update the UI.

    const handleSubmit = async (e) => {
      e.preventDefault()
      if(!userQuestion || !allow) return
      const uQ = userQuestion
      const newInterchange = [...interchange, {
        owner: true,
        text: userQuestion
      }]
      setInterchange(newInterchange)
      setUserQuestion('')
      setAllow(false)
      getBotAnswer(interchanges, setInterchange,  uQ, newInterchange, setAllow)
    }
Enter fullscreen mode Exit fullscreen mode

As can be seen in the index.js file, we imported a helper.js file. This means we need to create this file, to do this we create a folder at the root of our application called utils and then create a file in it called helper.js.

Next, copy the content of the file below into the newly created helper.js file:

    const baseUrl = process.env.BASE_URL || 'localhost:1337'
    export const createMarkup = (text) => {
      return {__html: text};
    }
    export const tranformInterchanges = (interchanges, initial = false) => {
      let initialText = initial ? `<b>Welcome to my page, glad to have you here 🥰</b> <br/>
     Tell me what you would like to know: <br/> <br/> `: ''

      interchanges.map((e, i) => {
          initialText += `${(i+1)}. ${e.question} <br /> <br />`
      })
      return initialText
    }
    export const searchInterchange = (interchanges, question) => {
      let result = interchanges.find(e => e.question.toLowerCase().includes(question.toLowerCase()))
      if(result) return result.answer
      return `Cant seem to understand your question, please try again 😔<br><br>
        Here are the options again: <br/> <br/>
        ${tranformInterchanges(interchanges)}
      `
    }

    export const showBotTyping = async (setInterchange, prevState, setAllow) => {
      scrollDown()
      await new Promise(resolve => setTimeout(resolve, 1000));
      setInterchange([...prevState, {
        owner: false,
        text: 'Bot Assistant is typing.'
      }])
      scrollDown()
      await new Promise(resolve => setTimeout(resolve, 1000));
      setInterchange([...prevState, {
        owner: false,
        text: 'Bot Assistant is typing..'
      }])
      scrollDown()
      await new Promise(resolve => setTimeout(resolve, 1000));
      setInterchange([...prevState, {
        owner: false,
        text: 'Bot Assistant is typing...'
      }])
      scrollDown()

      await new Promise(resolve => setTimeout(resolve, 1000));
      setAllow(true)
      scrollDown()
    }

    export const getBotAnswer = async (interchanges, setInterchange, question, prevState, setAllow) => {
      await showBotTyping(setInterchange, prevState, setAllow)
      setInterchange([...prevState, {
        owner: false,
        text: searchInterchange(interchanges,question)
      }])
      scrollDown()
    }

    const scrollDown = () => {
      document.getElementById('scrollTo').scrollIntoView({behavior: "smooth", block: "start"});
    }

    export const fetchQuery = async (path, params = null) => {
      let url
      if (params !== null) {
        url = `${baseUrl}/${path}/${params}`
      } else {
        url = `${baseUrl}/${path}`
      }
      const response = await fetch(`http://${url}`)
      const data = await response.json()
      return data
    }
Enter fullscreen mode Exit fullscreen mode

Explanation of above code

The above code contains various helper functions that are used in the app.

First, we set the base URL for the API, which in our case is localhost:1337 but if we eventually host the Strapi API, we would then update the BASE_URL in the .env file to the remote Strapi API.

Next, we use createMarkup to show the messages from the bot with any HTML formatting it comes with.

    const baseUrl = process.env.BASE_URL || 'localhost:1337'
    export const createMarkup = (text) => {
      return {__html: text};
    }
Enter fullscreen mode Exit fullscreen mode

The tranformInterchanges function is used to display the first message sent by the bot to the user, which contains all possible questions that can be asked in a finely formatted way:

    export const tranformInterchanges = (interchanges, initial = false) => {
      let initialText = initial ? `<b>Welcome to my page, glad to have you here 🥰</b> <br/>
     Tell me what you would like to know: <br/> <br/> `: ''

      interchanges.map((e, i) => {
          initialText += `${(i+1)}. ${e.question} <br /> <br />`
      })
      return initialText
    }
Enter fullscreen mode Exit fullscreen mode

The searchInterchange is the heart of the app, it is where all user questions are scanned to see if there is an adequate response for it. If there is, it returns a response from the provided answers else it returns a response indicating that no results were found while and also displaying all questions the user can ask.

    export const searchInterchange = (interchanges, question) => {
      let result = interchanges.find(e => e.question.toLowerCase().includes(question.toLowerCase()))
      if(result) return result.answer
      return `Cant seem to understand your question, please try again 😔<br><br>
        Here are the options again: <br/> <br/>
        ${tranformInterchanges(interchanges)}
      `
    }

    export const showBotTyping = async (setInterchange, prevState, setAllow) => {
      scrollDown()
      await new Promise(resolve => setTimeout(resolve, 1000));
      setInterchange([...prevState, {
        owner: false,
        text: 'Bot Assistant is typing.'
      }])
      scrollDown()
      await new Promise(resolve => setTimeout(resolve, 1000));
      setInterchange([...prevState, {
        owner: false,
        text: 'Bot Assistant is typing..'
      }])
      scrollDown()
      await new Promise(resolve => setTimeout(resolve, 1000));
      setInterchange([...prevState, {
        owner: false,
        text: 'Bot Assistant is typing...'
      }])
      scrollDown()

      await new Promise(resolve => setTimeout(resolve, 1000));
      setAllow(true)
      scrollDown()
    }
Enter fullscreen mode Exit fullscreen mode

The showBotTyping is used to mimic a human, it gives the bot a human feeling by delaying its response a bit and using that time to display a message that the bot is typing. This typing animation would be removed from the chat history once a response comes back from the bot.

    export const showBotTyping = async (setInterchange, prevState, setAllow) => {
      scrollDown()
      await new Promise(resolve => setTimeout(resolve, 1000));
      setInterchange([...prevState, {
        owner: false,
        text: 'Bot Assistant is typing.'
      }])
      scrollDown()
      await new Promise(resolve => setTimeout(resolve, 1000));
      setInterchange([...prevState, {
        owner: false,
        text: 'Bot Assistant is typing..'
      }])
      scrollDown()
      await new Promise(resolve => setTimeout(resolve, 1000));
      setInterchange([...prevState, {
        owner: false,
        text: 'Bot Assistant is typing...'
      }])
      scrollDown()

      await new Promise(resolve => setTimeout(resolve, 1000));
      setAllow(true)
      scrollDown()
    }
Enter fullscreen mode Exit fullscreen mode

The getBotAnswer function sends the user question to the bot and then updates the chat with the bot response.

    export const getBotAnswer = async (interchanges, setInterchange, question, prevState, setAllow) => {
      await showBotTyping(setInterchange, prevState, setAllow)
      setInterchange([...prevState, {
        owner: false,
        text: searchInterchange(interchanges,question)
      }])
      scrollDown()
    }

    const scrollDown = () => {
      document.getElementById('scrollTo').scrollIntoView({behavior: "smooth", block: "start"});
    }
Enter fullscreen mode Exit fullscreen mode

We use the scrollDown function to smoothly bring the current message sent by either the bot or the user into view. This makes it more like a chat app, with users not having to manually scroll down to see recent messages.

    const scrollDown = () => {
      document.getElementById('scrollTo').scrollIntoView({behavior: "smooth", block: "start"});
    }
Enter fullscreen mode Exit fullscreen mode

And that's it for the code section of the frontend part of this tutorial. In the next section, we would be seeing a demo of the finished app.

Finished App

The finished app looks like this:

Conclusion

There’s no doubt that the benefit a chatbot assistant brings outweigh any negative effect it could have. This method allows us to free up our time by allowing this bot to answer repetitive questions about us, our business, our organization, or event. The possibilities of this are endless and can only be left to one’s imagination.

This tutorial demonstrated how easily you can build a bot using technologies already very much common. If this interests you, check out Strapi's website to learn more about the headless CMS.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .