How to Create a ChatBot Assistant using NextJs, TailwindCss and Strapi
Tags: tailwindcss, strapi, nextjs, cms
Published: 2021-12-01 12:12:23

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](https://github.com/chyke007/strapi-nextjs-tutorial-chatbot), as well as the [backend code here](https://github.com/chyke007/strapi-tutorial-chatbot).


## 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](https://codex.so/how-to-install-the-latest-node-js-npm-and-yarn) for instructions on how to install Node.js
- Basic understanding of Strapi - Get started with this [quick guide](https://strapi.io/documentation/developer-docs/latest/getting-started/quick-start.html)
- Basic knowledge of [Next.js](https://nextjs.org/docs/getting-started)
- Basic knowledge of [Tailwind CSS](https://tailwindcss.com/docs)
 
## 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:

```bash
    npx create-strapi-app chatbot-assistant --quickstart
```

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`

![](https://paper-attachments.dropbox.com/s_61EC144653D77A5A3225C05DD53ECF56DD8B12AF0D4564DE52DDD1C630653BE1_1636572411431_Screenshot+2021-11-10+at+20.26.45.png)

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. 


![](https://paper-attachments.dropbox.com/s_61EC144653D77A5A3225C05DD53ECF56DD8B12AF0D4564DE52DDD1C630653BE1_1636572552842_Screenshot+2021-11-10+at+20.29.07.png)


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`.


![](https://paper-attachments.dropbox.com/s_61EC144653D77A5A3225C05DD53ECF56DD8B12AF0D4564DE52DDD1C630653BE1_1636572663219_Screenshot+2021-11-10+at+20.30.59.png)


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.

![](https://paper-attachments.dropbox.com/s_61EC144653D77A5A3225C05DD53ECF56DD8B12AF0D4564DE52DDD1C630653BE1_1636572861278_Screenshot+2021-11-10+at+20.34.17.png)


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](https://paper-attachments.dropbox.com/s_61EC144653D77A5A3225C05DD53ECF56DD8B12AF0D4564DE52DDD1C630653BE1_1636573966990_Screenshot+2021-11-10+at+20.52.41.png)


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](https://paper-attachments.dropbox.com/s_61EC144653D77A5A3225C05DD53ECF56DD8B12AF0D4564DE52DDD1C630653BE1_1636582185036_Screenshot+2021-11-10+at+23.09.41.png)



## 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:



![](https://paper-attachments.dropbox.com/s_61EC144653D77A5A3225C05DD53ECF56DD8B12AF0D4564DE52DDD1C630653BE1_1636582427444_Screenshot+2021-11-10+at+23.13.40.png)



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"}]
```

## 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:

```bash
    npx create-next-app -e with-tailwindcss nextjs-chatbot
```

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`

```bash
    cd nextjs-chatbot
```

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

```bash
    npm run dev
```

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:


![](https://paper-attachments.dropbox.com/s_1F009F9E3EE9CBA1F20D406E4458E971D06EA9323A4B53D1F0303E815DE8B180_1634387555599_Screenshot+2021-10-16+at+13.32.30.png)



## 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:

![](https://paper-attachments.dropbox.com/s_61EC144653D77A5A3225C05DD53ECF56DD8B12AF0D4564DE52DDD1C630653BE1_1636639126217_Screenshot+2021-11-11+at+14.58.42.png)


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

## 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'
```

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([])
```

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])
```

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

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

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

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

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

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

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

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

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:

<iframe width="560" height="315" src="https://www.youtube.com/embed/Dlc3WVtsPMM" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

## 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](https://strapi.io/) to learn more about the headless CMS.