Make a Simple Chatbot with JavaScript!
Tags: javascript, beginners, codenewbie, html
Published: 2020-03-19 20:06:19

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](https://dev-to-uploads.s3.amazonaws.com/i/zdly69wpvwfcstx17z6g.png)

But I also got a lot of this code/inspiration from existing blog posts and YouTube [tutorials](https://www.youtube.com/watch?v=AMakWEj5vKQ)! 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](https://media.giphy.com/media/gL3SzyoWAIu3F0il0T/giphy.gif)

### Step 1

First off is a simple `index.html` file:

```html
<!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>
```

And similarly simple `styles.css` file:

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

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](https://css-tricks.com/snippets/css/transparent-background-images/) 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](https://dev-to-uploads.s3.amazonaws.com/i/wuqpaaqyem22uwfqdd5s.png)<figcaption>I am replaceable</figcaption>

### Step 2

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

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

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](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode), [.which](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/which), and [keypress](https://developer.mozilla.org/en-US/docs/Web/API/Document/keypress_event) 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](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent), 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.

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

```javascript
    if (e.code === "Enter") {
      let input = inputField.value;
      console.log(`I typed '${input}'`)
    }
```
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zo8y9z00f1ed7s05x0ki.png)

Cool! One last thing on this part - setting `.value = ""` ensures our form is cleared after submission. You can also do `.reset()` on an [`HTMLFormElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/reset), 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.

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

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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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:

```javascript
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..."
];
```
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](https://i.kym-cdn.com/entries/icons/facebook/000/028/539/DyqSKoaX4AATc2G.jpg)

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](https://dev-to-uploads.s3.amazonaws.com/i/mkm3k4u2ojqohy7i74u8.png)

Now I add the function that actually compares these arrays:

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

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

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

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.

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/uph5fk48yy1jj8l73h6k.png)

You can imagine abstracting this out to be a separate kind of dynamic search function... or just have multiple `else if`s, or `case` and [`switch`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch).

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/hc4jkaqnibdsoyfkpwia.png)

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:

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

and then in `function output()`:

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

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

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

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](https://www.youtube.com/watch?v=AMakWEj5vKQ) I found while trying to do this used voice to text, and all you have to do for that is add the following:

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

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
* [Using the Web Speech API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API)
* [YouTube tutorial by Heri Kaniugu](https://www.youtube.com/watch?v=AMakWEj5vKQ)
* [Simple chatbot example with more APIs](https://github.com/ddsky/chatbot)
* [My full code](https://github.com/sylviapap/chatbot)
* [Bot Demo](https://sylviapap.github.io/chatbot/)