Bringing in the AI

Connecting to an Application Programming Interface (API)

Welcome back! It's time to finally get us someone to talk to. You might be familiar with ELIZA, the world's first chatbot. I decided to set up a version of it that I found online, although it's quirky and uses weird slang that I'm pretty sure wasn't even a thing back in the early 2000s when it was developed.

This is going to be a relatively short and easy chapter, which is a good thing because what's coming after can be a bit intimidating. So hold on to these positive vibes! There's only one change we need to make to our current prototype: whenever we submit text, we send it to the Eliza chatbot running somewhere online, and we post the reply back in our chat window. Remember our click event on the paper plane icon in chat.js:

// Add a click event to our "send" button to actually put the message on screen
new_message_icon.on(Events.Click, function() {
  // Create balloon
  add_text_balloon(new_message_text.value, true);

  // Empty our input field for the next message
  new_message_text.value = "";
});

Towards the end of this Event — but before we clear the text input! — we can send our message and wait for a reply. We're going to do this by sending a message through the internet, similarly to how you would submit a form on a website. However, instead of navigating away after completing the form, we send the data behind the scenes and update our interface (by adding a speech bubble) when we receive a response. We can do this using basic JavaScript features, in particular the XMLHttpRequest object. Somewhere at the top of your chat.js file, we'll create the object we'll need every time we want to submit something:

var xhttp = new XMLHttpRequest();

Okay, now we'll use this xhttp object from within our click event:

// Add a click event to our "send" button to actually put the message on screen
new_message_icon.on(Events.Click, function() {
    // Create balloon
    add_text_balloon(new_message_text.value, true);

    xhttp.open("POST", "http://eliza.lt3.nl/", true);
    xhttp.send(JSON.stringify({"message": new_message_text.value}));

    // Empty our input field for the next message
    new_message_text.value = "";
});

What these two new lines do is open a connection to the Eliza bot that is running on my webserver, and it then sends our message as JSON, which is a format that is specifically designed to transfer complex structures, like the dictionary we are providing it, between two systems. So in this case, we're sending a dictionary:

{
    "message": new_message_text.value
} 

The JSON.stringify converts this to a string for us, so that it can be transported. Now all that's left is for us to respond to a response from Eliza. This could should be placed somewhere outside of the click event:

xhttp.onreadystatechange = function() {
  if (this.readyState == 4 && this.status == 200) {
    var response = JSON.parse(this.responseText);
    Utils.delay(2, function() {
      add_text_balloon(response.message, false);
    });
  }
};

We wait for an event, onreadystatechange, which happens as we receive a message back from our connection to the external source. We check if all is well with the message (readyState should be 4 and status should be 200), and if that's the case we know that we can expect a specific structure from our remote source. We call these types of sources an Application Programming Interface, or API. They usually come with documentation describing what kind of information you should provide to the different functions, and what you can expect to receive in return.

It's important to know that these connections to external sources happen asynchronously, meaning that your code will continue to run after you put in a request. In this case, this means that new_message_text.value = ""; is executed before our reply from Eliza comes in. If your internet connection is bad, it could take longer for the reply to be processed. Something to keep in mind when you're connecting with external systems!

You can also see an example of the opposite of what we did above: now we receive a string describing an object, and we want to convert it back into a dictionary, so we use JSON.parse so that we can simply retrieve response.message after, as if it were a dictionary we created ourselves.

You will notice that we also add a short delay of 2 seconds, using the FramerJS Utils.delay(). After that, we simply call the add_text_balloon() function we define in the previous chapter, and we indicate that the message is not originating from us (false). That's all! We can now slowly move towards talking with real people, by adding the ability to create accounts and log in.

chat.js

import pages from './pagecomponent.js';

var md = require('./modules/md.coffee');

var xhttp = new XMLHttpRequest();

// The page itself
var chat_page = new Layer({
  width: pages.width,
  height: pages.height,
  backgroundColor: "rgb(240, 240, 240)",
  parent: pages.content
});

pages.addPage(chat_page);

// The bar at the top, containing a title and a button for navigation
var header = new md.Header({
  icon: 'arrow-left',
  title: 'Sprinkles',
  parent: chat_page
});

// This container provides scrolling functionality and will hold all the chat messages
var chat_container = new ScrollComponent({
  y: 80,
  height: chat_page.height - 50,
  width: chat_page.width,
  backgroundColor: "rgb(240, 240, 240)",
  scrollHorizontal: false,
  parent: chat_page
});

// This layer at the bottom of the screen will host the input field and send button
var new_message_container = new Layer({
  y: Align.bottom,
  height: 70,
  width: chat_page.width,
  backgroundColor: md.Theme.colors.primary.main,
  parent: chat_page
});

// A nice rounded background for our text
var new_message_background = new Layer({
  x: 10,
  y: 10,
  height: 50,
  width: chat_page.width - 80,
  backgroundColor: "rgb(255, 255, 255)",
  borderRadius: 3,
  parent: new_message_container
});

// We remove all the indicators and borders from our TextArea since we have our own background layer
var new_message_text = new md.TextArea({
  tint: "rgba(0, 0, 0, 0)",
  width: chat_page.width - 120,
  parent: new_message_background
});

// The paper plane icon to send our message
var new_message_icon = new md.Icon({
  x: Align.right(-25),
  y: Align.center,
  icon: "send",
  color: "rgb(255, 255, 255)",
  parent: new_message_container
});

// Add a click event to our "send" button to actually put the message on screen
new_message_icon.on(Events.Click, function() {
  // Create balloon
  add_text_balloon(new_message_text.value, true);

  // Send our message to Eliza
  xhttp.open("POST", "http://eliza.lt3.nl/", true);
  xhttp.send(JSON.stringify({"message": new_message_text.value}));

  // Empty our input field for the next message
  new_message_text.value = "";
});

// A reply from Eliza!
xhttp.onreadystatechange = function() {
  if (this.readyState == 4 && this.status == 200) {
    var response = JSON.parse(this.responseText);
    Utils.delay(2, function() {
      add_text_balloon(response.message, false);
    });
  }
};

// Initialize the y-coordinate for our text balloons
var cur_y = 16;

// Function to add a text balloon to the ScrollComponent
var add_text_balloon = function(text, is_mine) {
  var card = new md.Card({
    x: Align.left(16),
    y: cur_y,
    superLayer: chat_container.content
  });

  var txt = new md.Regular({
    text: text
  });

  if (txt.width > Screen.width / 2) {
    txt.width = Screen.width / 2;
  }

  var txt_width = txt._element.children[0].offsetWidth;
  var txt_height = txt._element.children[0].offsetHeight;

  card.width = txt_width + 32;
  card.height = txt_height + 8;
  card.addToStack(txt);

  if (is_mine) {
    card.backgroundColor = md.Theme.colors.secondary.light;
    card.x = Align.right(-16);
  }

  cur_y += txt_height + 48;
  chat_container.scrollToLayer(card);
};

export default chat_page;




Add a comment