Make a Simple Chatbot with JavaScript!

Sylvia Pap - Mar 19 '20 - - Dev Community

Working from home got you feeling lonely? Missing human social interaction? Well it's finally acceptable to suggest making yourself a nice chat bot to talk to instead of going out into the world.

When I say 'from scratch' or 'vanilla JS,' I just mean I'm not using any additional libraries or APIs. This is more an exercise in JS fundamentals than any kind of artificial intelligence or machine learning.

Alt Text

But I also got a lot of this code/inspiration from existing blog posts and YouTube tutorials! So basically I'm trying to be as original as possible here, but you can only avoid re-inventing the wheel for so long.

Alt text of image

Step 1

First off is a simple index.html file:



<!DOCTYPE html>
<html>
<head>
<title>Chatbot</title>
<script type="text/javascript" src="index.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div id="main">
    <div><input id="input" type="text" placeholder="Say something..." autocomplete="off"/></div>
</div>
</body>
</html>


Enter fullscreen mode Exit fullscreen mode

And similarly simple styles.css file:



body { 
    color: #421; 
    font-weight: bold; 
    font-size: 18px; 
    font-family: "Courier New"; 
    background: rgb(200, 232, 241); 


}
body::after {
    content: "";
    background-image: url("bot.png");
    background-repeat: repeat-y; 
    opacity: 0.5;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    position: absolute;
    z-index: -1;   
  }
span { 
    color: rgb(36, 17, 119); 
} 
::-webkit-input-placeholder { 
    color: #711 
}
#main { 
    position: fixed; 
    top: 40%; 
    right: 200px; 
    width: 400px; 
    border: 0px solid #421; 
    padding: 40px; 
}
#main div { 
    margin: 10px; 
} 
#input { 
    border: 0; 
    padding: 5px; 
    border: 1px solid #421; 
}


Enter fullscreen mode Exit fullscreen mode

I am clearly not an HTML or CSS expert, but that's not what I came here to do! Maybe I left these intentionally basic so you can be free to customize without trying to understand my complex styling. Here is a tip I found especially helpful on making the background slightly transparent, considering my background image was a little too dark if the window is condensed and the text shows over it. The background is just a bot.png image that I found on Google images. You could replace that with anything!

Alt Text
I am replaceable

Step 2

Now for the fun stuff! Create a .js file, and start with some basics.



//index.js

document.addEventListener("DOMContentLoaded", () => {
  document.querySelector("#input").addEventListener("keydown", function(e) {
    if (e.code === "Enter") {
        console.log("You clicked the form and pressed the enter button!")
    }
  });
});


Enter fullscreen mode Exit fullscreen mode

Adding an event listener to the document for the condition of DOMContentLoaded means your JS won't run until the HTML has loaded. This is almost always good practice. Then the EventListener for keypress enter button. Notice we must also select the #input for the form submission, or else our event listener would respond every time we pressed the enter key!

There are some interesting and deprecated alternatives here. .keycode, .which, and keypress are all deprecated. These are all just ways of telling the event listener that we only care about the enter key - that's what makes the nice, dynamic effect of instant rendering when we type a message and press enter! No more tedious clicking of a 'submit' button while messaging our bot friend. See more on the KeyboardEvent object, but basically it seems like the most up-to-date, accessible, and universal method for this event listener, if your browser supports it. But you might still see something with a code of 13 to represent the enter key.



document.addEventListener("DOMContentLoaded", () => {
    const inputField = document.getElementById("input")
    inputField.addEventListener("keydown", function(e) {
        if (e.code === "Enter") {
            let input = inputField.value;
            inputField.value = "";
            output(input);
    }
  });
});


Enter fullscreen mode Exit fullscreen mode

Now we are moving past that console.log() and onto some important functions. But first! Notice we select .value and set it to a variable for input. This is whatever we type into the form. We can verify this with another con log!



    if (e.code === "Enter") {
      let input = inputField.value;
      console.log(`I typed '${input}'`)
    }


Enter fullscreen mode Exit fullscreen mode

Alt Text

Cool! One last thing on this part - setting .value = "" ensures our form is cleared after submission. You can also do .reset() on an HTMLFormElement, but it doesn't work here since our input field isn't really a form tag.

Step 3: Functions!

Now for the functions that actually make this guy a bot.



function () {

//remove all characters except word characters, space, and digits
  let text = input.toLowerCase().replace(/[^\w\s\d]/gi, "");

// 'tell me a story' -> 'tell me story'
// 'i feel happy' -> 'happy'
  text = text
    .replace(/ a /g, " ")
    .replace(/i feel /g, "")
    .replace(/whats/g, "what is")
    .replace(/please /g, "")
    .replace(/ please/g, "");
}


Enter fullscreen mode Exit fullscreen mode

Before anything, I want to take whatever the user types in the input field, and make it a little more standard with some basic RegExp action. As noted in the comments, these methods make everything in the input lowercase, remove any rogue characters that would make matches difficult, and replace certain things like whats up to what is up. If the user says what is going on, whats going on, or what's going on, they will all lead to the same valid bot response, instead of having to account for these differences separately somehow.

Now that we've got a good idea of what our text input could look like, I'm going to make some simple arrays of arrays that include possible triggers (user text) and responses (bot text). To start I'll keep them short, and defined in global variables:



const trigger = [
//0 
["hi", "hey", "hello"],
//1
["how are you", "how are things"],
//2
["what is going on", "what is up"],
//3
["happy", "good", "well", "fantastic", "cool"],
//4
["bad", "bored", "tired", "sad"],
//5
["tell me story", "tell me joke"],
//6
["thanks", "thank you"],
//7
["bye", "good bye", "goodbye"]
];

const reply = [
//0 
["Hello!", "Hi!", "Hey!", "Hi there!"], 
//1
[
    "Fine... how are you?",
    "Pretty well, how are you?",
    "Fantastic, how are you?"
  ],
//2
[
    "Nothing much",
    "Exciting things!"
  ],
//3
["Glad to hear it"],
//4
["Why?", "Cheer up buddy"],
//5
["What about?", "Once upon a time..."],
//6
["You're welcome", "No problem"],
//7
["Goodbye", "See you later"],
];

const alternative = [
  "Same",
  "Go on...",
  "Try again",
  "I'm listening...",
  "Bro..."
];


Enter fullscreen mode Exit fullscreen mode

Notice the comments for index at each of the arrays, and how they line up. If we get user input that matches an option at trigger[0], such as 'hi', the bot will respond with an option from its reply[0], such as 'Hello!' and so on. The alternative array is, of course, for everything that doesn't match in the first array! This kind of explains why every basic chatbot you've ever used, let's say on a customer service website, is so.. limited. AI isn't going to kill us all yet! Right now, this bot is pretty much as intelligent as this guy...

Alt text of image

That is, if you don't say something that falls into one of our defined responses, there's a very high chance he will say something like...

Alt Text

Now I add the function that actually compares these arrays:



function compare(triggerArray, replyArray, text) {
  let item;
  for (let x = 0; x < triggerArray.length; x++) {
    for (let y = 0; y < replyArray.length; y++) {
      if (triggerArray[x][y] == text) {
        items = replyArray[x];
        item = items[Math.floor(Math.random() * items.length)];
      }
    }
  }
  return item;
}


Enter fullscreen mode Exit fullscreen mode

and then add this function back into our original, plus accounting for the 'alternative' response:



function output(input) {
  let product;
  let text = input.toLowerCase().replace(/[^\w\s\d]/gi, "");
  text = text
    .replace(/ a /g, " ")
    .replace(/i feel /g, "")
    .replace(/whats/g, "what is")
    .replace(/please /g, "")
    .replace(/ please/g, "");

//compare arrays
//then search keyword
//then random alternative

  if (compare(trigger, reply, text)) {
    product = compare(trigger, reply, text);
  } else if (text.match(/robot/gi)) {
    product = robot[Math.floor(Math.random() * robot.length)];
  } else {
    product = alternative[Math.floor(Math.random() * alternative.length)];
  }

  //update DOM
  addChat(input, product);
}


Enter fullscreen mode Exit fullscreen mode

I added another option for matching user input to bot response here. It adds a bit more flexibility in user input, but less specificity in the response. See where I added an else if for text.match(/robot/gi) - this guarantees a response from a separate "robot related" array if the user enters anything with the word robot anywhere in it.



const robot = ["How do you do, fellow human", "I am not a bot"];


Enter fullscreen mode Exit fullscreen mode

Alt Text

You can imagine abstracting this out to be a separate kind of dynamic search function... or just have multiple else ifs, or case and switch.

Alt Text

The final step is to update the DOM so our messages actually display! A simple way to do this is by having a single element for User and Bot text that is updated every time you enter a new message, and this only requires changing the first event listener function to:



document.addEventListener("DOMContentLoaded", () => {
...
    if (e.code === "Enter") {
        let input = document.getElementById("input").value;
        document.getElementById("user").innerHTML = input;
        output(input);    
     }
  });
});


Enter fullscreen mode Exit fullscreen mode

and then in function output():



function output(input) {
    let product;
    let text = (input.toLowerCase()).replace(/[^\w\s\d]/gi, "");
...
    document.getElementById("chatbot").innerHTML = product;
    speak(product);

    //clear input value
    document.getElementById("input").value = "";
}


Enter fullscreen mode Exit fullscreen mode

Or, you could do it so that the user and bot fields are updated every time, creating a thread of messages. I wanted to keep them all on the page, so my current function looks like..



function addChat(input, product) {
  const mainDiv = document.getElementById("main");
  let userDiv = document.createElement("div");
  userDiv.id = "user";
  userDiv.innerHTML = `You: <span id="user-response">${input}</span>`;
  mainDiv.appendChild(userDiv);

  let botDiv = document.createElement("div");
  botDiv.id = "bot";
  botDiv.innerHTML = `Chatbot: <span id="bot-response">${product}</span>`;
  mainDiv.appendChild(botDiv);
  speak(product);
}


Enter fullscreen mode Exit fullscreen mode

There are so many different ways to accomplish this DOM manipulation. .innerHTML vs. .innerText is a good one. .append vs. .appendChild fulfill almost the exact same purpose here, but can have different uses later on. And if I get around to adding a Rails backend to this guy, I would have liked to add .dataset attributes for each message. It also seems that I do not have the ability to scroll once the thread gets long enough. Once again, I am a beginner, and this post is more about JS logic than views!

Another final note...

I said I wasn't going to use APIs, but one of the example videos I found while trying to do this used voice to text, and all you have to do for that is add the following:



function speak(string) {
  const u = new SpeechSynthesisUtterance();
  allVoices = speechSynthesis.getVoices();
  u.voice = allVoices.filter(voice => voice.name === "Alex")[0];
  u.text = string;
  u.lang = "en-US";
  u.volume = 1; //0-1 interval
  u.rate = 1;
  u.pitch = 1; //0-2 interval
  speechSynthesis.speak(u);
}


Enter fullscreen mode Exit fullscreen mode

I actually couldn't quite figure out how to specify different voice names here, but looking into the Web Speech API docs was interesting, and I can recommend altering .pitch to 2 for a truly terrifying voice that does sound capable of taking over the human race.

Further Reading

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