Pizza ordering app with AWS Lambda Functions

The Ably Realtime service provides a “pub/sub” (publish/subscribe) messaging system with which you can build sophisticated workflows and dataflow pipelines to meet the needs of your application. This can be as simple as publishing messages to named channels of your choice, and having users and server processes subscribe to those channels and respond with further messages as needed. But what if you don’t have a server to coordinate all of this, or simply don’t want to manage that side of things?

Even though Ably provides a full suite of authentication and authorization features to make sure users can only do what they’re permitted to do, we still need a way to connect your messaging workflow to your business logic – something that would normally happen on your application’s server. Doing so from a client-side browser application is risky, as it means exposing your business logic and other potentially sensitive data and services to the user.

With the rise of serverless solutions such as AWS Lambda, the problem goes away. Create server functions that contain your business logic, credentials and other sensitive data, hook them up to your application, and all of the sensitive work is hidden away from the user, not to mention being managed by world-class infrastructure providers such as Amazon, Google or Microsoft.

Ably’s Webhooks feature provides a way to define custom criteria that will automatically trigger different kinds of actions in response to channel messages, channel lifecycle events (such as a channel being created) and presence events (such as users entering or leaving a channel), and is how we’ll integrate with your server functions without exposing them to your users.

What we’ll be building

For this tutorial, we’ll be using Webhooks events in response to incoming messages in order to automatically invoke your own AWS Lambda functions. We’re going to build a simple pizza ordering system with a virtual pizza assistant chat bot. When a new customer hits your website, they’ll be assigned a random ID, and their messages will be published to a private channel created just for that customer. A webhook triggered by those messages will call your AWS Lambda functions to invoke the pizza chat bot to take their order. Using the Ably REST API, it will post its responses back on another channel that the web application subscribes to.

The chat bot will be built using the AWS Lex service, which provides specialized functionality for managing natural language conversations between users and software. There are a number of different chat bot services we could integrate with, but we’ll use the AWS Lex service. By doing this, you’ll be able to use the same account and credential management area for managing access to both the Lambda and Lex services. You’ll potentially also be able to take advantage of any other integrations between Lex and Lambda, seeing as they both belong to the same service provider. This may become more important if you expand on the basic Lex scripts we provide in this tutorial as a starting point.


Diagram of the tutorial project's architecture


Animated image of the browser app in action

Note: Setting up an AWS Lex bot is a bit of a complex topic, and the AWS user interface for doing so has various nuances and limitations that would take some time to explain. To fast-track you through the process, console-based setup scripts are provided. If you’d like to skip bot creation and instead have your Lambda function generate a set of simplified, predefined responses based on your own JavaScript logic (you’ll need to apply your own modifications to the Lambda function code included in this tutorial), then there is no need to invoke the Lex API, and thus you can safely skip “Step 2: AWS Lex”, further below.

Step 1: AWS account and credentials

The first thing you’ll need to do is make sure you have an AWS account with which you can create Lambda functions and your Lex bot, and to then install the AWS command line interface (CLI).

If you don’t have an AWS account, create a free one here.

(a) Credentials for use by the AWS CLI

Note: The following is only for use by the AWS CLI which is installed further down during step 2, for setting up the Lex chat bot. If you’re skipping the Lex portion of this tutorial, jump to the next sub-step (b), below.

It’s important for a real application to have properly-configured security credentials, but seeing as this is just a learning exercise, for setting up the chat bot we’ll keep things simple and just use your primary account credentials, but don’t deploy a real application until you’ve used AWS’s Identity and Access Management (IAM) service to properly lock things down.

Navigate to your security credentials area and generate a new key, making sure to copy and paste your key ID and secret key for use in this tutorial.


Select the My Security Credentials menu item


Click the Continue button


Click the Create New Access Key button

Copy and paste your key ID and secret key to a text file for later use:


Copy the access key ID and secret key

(b) IAM role for use by your Lambda function

You will need to configure an IAM role for use by your Lambda function, even if you’re bypassing the chat bot parts of this tutorial. Enter your AWS dashboard and click the services link at the top of the page, then, the IAM link from the menu:


Select the IAM service

From the side menu, select the roles link, followed by the “Create role” button:


Create a new role

Click the “AWS service” panel button, then the “Lambda” link, and finally the “Next: Permissions” button:


Choose the Lambda service

Type “Lex” into the search box, check the “AmazonLexRunBotsOnly” option, then click the “Next: Review” button. If you’re skipping the chat bot part of this tutorial, search for and select the “AWSLambdaRole” option instead:


Select a policy

Enter a name such as “Lambda-Lex-Role”, followed by a description of the role’s purpose, and then click the “Create Role” button:


Fill out the role details

(c) IAM user for use by your webhooks

In order for Ably to invoke your Lambda function, you’ll need to supply credentials when setting up your webhooks (covered later in this tutorial). The process is similar to the IAM role setup process above:

  1. Select “Users” from the left menu
  2. Click the blue “Add user” button
  3. For the “Add User” form that is displayed:
  4. For the username, enter Ably-Invoke-PizzaAssistant
  5. Check the “Programmatic access” checkbox
  6. Click the blue “Next: permissions” button in the bottom right corner of the page
  7. For the “Set permissions for Ably-Invoke-PizzaAssistant” page:
  8. Click the large square panel “Attach existing policies directly”
  9. Type AWSLambdaRole into the filter/search box
  10. Check the “AWSLambdaRole” checkbox
  11. Click the blue “Next: Review” in the bottom right corner of the page
  12. Click the blue “Create user” button in the bottom right corner of the page
  13. Your new user will be displayed in a table and you’ll see two columns; “Access key ID” and “Secret access key”:
  14. Copy and paste the access key ID into a text file for later use
  15. Click the “Show” link in the last column, which will reveal the secret key
  16. Copy and paste the secret key into your text file

Step 2: AWS Lex

Note: You can skip this step if you’re not going to use AWS Lex for your chat bot.

(a) The AWS CLI

Make sure you have the AWS CLI installed, by following the instructions at https://docs.aws.amazon.com/cli/latest/userguide/installing.html. When configuring the CLI, use the credentials you configured in part 1-a of this tutorial. You should also choose us-east-1 for the region (AWS Lex isn’t available in all regions), and json for the output format, which will ensure that the Lex setup scripts (below) install correctly.

aws configure

AWS Access Key ID [None]: (paste your key ID)
AWS Secret Access Key [None]: (paste your secret key)
Default region name [None]: us-east-1
Default output format [None]: json

(b) Creating and installing your pizza assistant bot

Now that you have the AWS CLI in place, we can go ahead and use it to configure a bot for use in this tutorial. The tutorial is about Ably’s Integrations service though, so the following section will fast-track the bot setup process. For a deeper understanding of how the AWS Lex service works, take a look at the documentation. CLI reference for configuring and invoking bots can be found here, and here.

The JSON configurations for a simple pizza assistant chat bot are included in the repository for this tutorial’s completed code.

This preconfigured bot is only a bare-bones implementation to demonstrate a proof of concept. Customising the bot’s behaviour and filling in the gaps, such that it is able to actually complete an order or manage a list of different toppings in a robust manner, is left as an exercise for the reader.

From your console or terminal, change to the directory where you’ve put the JSON files, and run the following:

aws lex-models put-slot-type --cli-input-json file://slottype-crust.json
aws lex-models put-slot-type --cli-input-json file://slottype-sauce.json
aws lex-models put-slot-type --cli-input-json file://slottype-size.json
aws lex-models put-slot-type --cli-input-json file://slottype-topping.json
aws lex-models put-intent --cli-input-json file://intent.json
aws lex-models put-bot --cli-input-json file://bot.json
aws lex-models put-bot-alias --cli-input-json file://alias.json

This will create and configure your chat bot so that it can take a simple pizza order via a text-based conversation with a user. You’ll use your Lambda function to route user messages from your Ably channel to Lex, and to post the bot’s response back to the user on the reply channel.

Step 3: AWS Lambda

Select the Lambda service, just as described in step 1-b earlier in this tutorial (selecting “Lambda” in the “Compute” section, rather than “Lex”).

Next, select the “Functions” option in the left menu, then click the orange “Create function” button:


Create a new function

Click the “Author from scratch” button:


Author from scratch

  1. Enter Invoke-PizzaAssistant in the “Name” field
  2. Ensure “Choose an existing role” is selected
  3. Select Lambda-Lex-Role, or if skipping the Lex portion of this tutorial, the role you created instead


Fill out the basic function setup

Note: If you’re skipping the Lex portion of this tutorial, make any changes below as needed. The code below is where you’ll need to provide your alternative implementation for processing user messages.

Here we’re going to enter some preliminary code just to test that everything is configured correctly. You’ll be changing this code later, so keep in mind that you’ll want to come back and edit this later.

  1. Scroll down and edit index.js in the Function Code section. Enter the following code into the editor:
const AWS = require('aws-sdk');

exports.handler = (event, context, callback) => {
    const lex = new AWS.LexRuntime();
    lex.postText({
        botName: 'PizzaAssistant',
        botAlias: 'PizzaAssistant',
        userId: event.user,
        inputText: event.message
    }, callback);
};
  1. Click the “Save” button to make sure you don’t lose your changes
  2. From the “Select a test event” dropdown, select “Configure test events”


Enter some code and test

Test events let you feed sample data to your Lambda function to see if it generates the kind of response you’re expecting. Your Lambda test function, which you’ve already configured above, will use this sample data to invoke the Lex bot, and you’ll be able to view the result to see if it generated the kind of response you expect.

  1. Make sure the “Hello World” event template is selected
  2. Enter testNewCustomer into the “Event name” field
  3. Enter the following JSON code into the editor:
{
  "user": "test-123",
  "message": "I'd like a pepperoni pizza, please"
}
  1. Click the “Create” button


Configure a test event

Now that your test event is configured, make sure it is selected from the dropdown box, then click the “Save and test” button:


Run the test event

If you’ve followed the above steps correctly, you should see a successful execution result displayed. The JSON output will have been generated by your Lex bot:


Verify the test result

Step 4: Ably Setup

(a) Create an Ably API key

We’re going to need an API key for use by your browser application, and for the AWS Lambda function to allow it to post a response back to Ably. For our purposes, only one API key is required. If you extend the tutorial with your own functionality, you may wish to create a separate API key for the Lambda function if you need it to have a different set of permissions than those assigned for use by the browser app.

First, log into your Ably dashboard, find the app you’re going to be using for this tutorial (the “Sandbox” app is probably fine), then click “Manage App”.


Manage your Ably app

Select the “API Keys” tab, and click “Create a new API key”:


Create a new API key

  1. Enter a name for your API key
  2. Select only the “publish” and “subscribe” options
  3. Select “Selected channels and queues”, and enter a channel prefix to ensure that the API key can only access channels that start with pizza:
  4. Click the “Create key” button


Fill out the API key form

Scroll down if necessary, and copy and paste your new API key to a text file for later use in your application.


Copy the API key

(b) Configure a webhook

The next thing is to set up a webhook, which we need in order to relay channel messages from the customer to our AWS Lambda function for processing. We’ll actually be using two channels – one outbound, and one inbound. We’ll do this to ensure that only the customer’s communications are sent to AWS, and so that the browser app need only process messages from the chat bot. Channels only consume resources while in use, so using two channels in this way is not a problem.

Just as in the previous step, go to the dashboard for the Ably app you’re using, choose the “Integrations” tab, then click “New Integration Rule”:


Create a new Integration rule

Choose the “Webhook” option:


Select the Webhook option

Choose the “AWS Lambda” option:


Select the AWS Lambda option

Fill out the form to provide Ably with the details it needs to invoke your Lambda function.

  1. Select us-east-1 for the region.
  2. Enter Invoke-PizzaAssistant – the name of your Lambda function.
  3. Select the “AWS Credentials” option.
  4. The key ID and secret key you’ll be using are for the IAM user you created in step 1-c of this tutorial. The key ID and secret key should be pasted one after the other with a colon in-between. For example if your key ID is foo and your secret key is bar, you’d enter foo:bar into the credentials field.
  5. Make sure “Message” is set as the source.
  6. Enter a regular expression that the webhooks will use to whitelist the Ably channels for which the event should be triggered. ^pizza:customer: means that the channel name must begin with the string pizza:customer:, with no restrictions on the characters that follow.
  7. Check that the “Enveloped” checkbox is checked, then click the blue “Create” button.


Fill out the form

Your webhook will now be created, and you’ll be able to continue developing your app.

Step 5: Developing the browser app

The browser app will be a simple text-based conversation with the AWS Lex-driven chat bot. We’ll have a list of messages published by the customer and the chat bot, followed by a text box in which the user can type their responses to the chat bot’s messages. We’ll also include an initial message that serves as the default greeting that the pizza store will give to each new customer.

In the repository for this tutorial, in addition to the completed code further below, you’ll find the CSS to go with the code below, and a subtle background image to complement the app and give it some visual polish.

Note that all of the code below can be found in this tutorial’s repository, if you just want to skip to the end and see the application running.

(a) Getting the application skeleton in place

  1. Create a new project directory on your hard drive
  2. Create a blank file named app.js in your project directory. We’ll edit it later.
  3. Either create a blank file named app.css in your project directory, or copy the pre-made file of the same name from the repository.
  4. Create an HTML file named index.html for the user interface. Start with the code below:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Ably's Pizza</title>

  <script src="https://cdn.ably.com/lib/ably.min-1.js"></script>

  <link rel="stylesheet" href="app.css">
</head>
<body>
  <div id="chat-interface">
    <h1>Ably's Realtime Pizza</h1>

    <div id="chat-log">
      <div class="bot message">
        Welcome to Ably's Realtime Pizza - the most responsive pizza this side
        of the sun! How may we assist you today?
      </div>
    </div>

    <div id="user-input">
      <input id="input-field" type="text" placeholder="Type your message here...">
    </div>
  </div>

  <!-- We include our application script at the end, to ensure the HTML above is
       already loaded and ready to interface with before the script runs: -->
  <script src="app.js"></script>
</body>
</html>

(b) Getting a basic application script working

The application script has a few parts to it, but isn’t too complex. The script will need to update the user interface at various points, and because the layout is fairly simple, we’re just going to use vanilla JavaScript code to accomplish this, rather than over-complicating things with frameworks such as Angular or React. For anything more ambitious, you’ll likely want to upgrade the code in this tutorial to use a framework of your choosing.

The first thing to do is to instantiate the Ably client library (already linked for you in the above HTML) and verify that we can send and receive messages successfully. The following code will define a channel, subscribe to it, and use it to publish customer messages. If everything is working, the fact that we’re subscribed to the same channel that we’re publishing to means that our messages will simply be echoed back to us via the receiveMessage handler function, which will then append them to the chat log in the HTML.

// This Ably client library is available due to the script in the HTML header:
const ably = new Ably.Realtime('YOUR_API_KEY_GOES_HERE');

// Remember when creating your API key that you specified that it would only
// work for channels starting with the string "pizza:". When testing the script,
// try changing the channel name to something else and see how Ably emits an
// error to your browser developer console.
const channel = ably.channels.get('pizza:messages');
channel.subscribe(receiveMessage);

// Define the listener function used above, and have it add the received message
// to the chat log in the user interface:
function receiveMessage(message) {
  // The appendMessageElement function is defined further below.
  appendMessageElement('bot', message.data);
}

// Retrieve references to the message input field and the chat log container:
const inputField = document.getElementById('input-field');
const chatLog = document.getElementById('chat-log');

// Define an event listener function to be triggered when a key is pressed:
function processInput(e) {
  if (e.which !== 13) return; // If it's not the ENTER key, don't do anything

  const message = inputField.value.trim(); // Grab the trimmed input message
  if (message.length > 0) { // Don't post blank messages

    // Asynchronously post the message to the Ably channel. The channel
    // subscription we created above will echo the message shortly:
    channel.publish('user', { message });
  }

  // Clear the input field, ready for the next message:
  inputField.value = '';
}

// Attach the above listener function to the message input field:
inputField.addEventListener('keydown', processInput);

// And finally, this function will append messages to the chat log. The first
// argument indicates whether the message comes from the user or the bot:
function appendMessageElement(type, message) {
  const div = document.createElement('div');

  // Add two CSS classes, because later we'll want to be able to visually
  // differentiate user messages from bot messages:
  div.classList.add(type, 'message');
  div.textContent = message;
  chatLog.appendChild(div);

  // If the conversation goes on for a while, messages will fall off the bottom
  // of the page. Instead, let's scroll to the bottom of the page automatically:
  const el = document.scrollingElement;
  el.scrollTop = el.scrollHeight;
}

With the above in place, fire up your browser and give it a go. Enter chat messages into the input box, press ENTER, and if all is well, they should be echoed back to you in less than a second. Try submitting a few messages one after the other.

If you’re not seeing your messages in the window, open your browser’s developer tools and check the console to see if there are any error messages being emitted by your script.

© Filling out the primary functionality

Now that you’ve verified that you’re able to connect to Ably and send and receive messages, it’s time to update the code to integrate it with everything you did earlier in this tutorial.

First, we’ll need two channels rather than one. The first channel is the outbound channel, and is where the webhook will be listening for messages to forward on to your AWS Lambda function. The second channel is the inbound channel where the bot’s response messages will be posted, and is the channel to which your script will attach a listener.

If two different customers are trying to order a pizza at the same time, we don’t want their messages getting mixed in together. To address this, we’ll be appending a random customer identification value to the inbound and outbound channel names, with the effect being that every customer is allocated their own unique pair of inbound and outbound channels. The customer ID will also need to be included in outbound messages so that your AWS Lambda function can figure out where to send responses. The channels will only be used for the duration of the chat session, which means the maximum number of active channels at any one time will be equal to twice the number of customers currently placing pizza orders. See the pricing page for details about the peak connection limit available for your Ably account.

Here’s some code to generate a random customer ID. This will be run when the page first loads, and you’ll be able to then use the customerID in your channel names:

// Generate a customer id, being sure to sandbox the setup code inside a closure
const customerId = (function () {

  // The full set of characters that can be included in a customer ID
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

  // An arrow function to a random character from `chars`
  const randomChar = () => chars[Math.floor(Math.random() * chars.length)];

  // Change this if you want a shorter or longer ID
  const idLength = 16;

  // Prepare an array of our chosen customer ID size
  const id = new Array(idLength);

  // Populate the array with random characters
  for (let i = 0; i < idLength; i++) {
    id[i] = randomChar();
  }

  // Join the array entries together to form the customer ID string
  return id.join('');
})();

Split your channel initialization code to generate two channels instead of one:

// The customer's messages will be posted here
const outboundChannel = ably.channels.get('pizza:customer:' + customerId);

// Messages from the bot will be posted here, where we'll be listening for responses
const inboundChannel = ably.channels.get('pizza:bot:' + customerId);

// Update your subscription to listen to the inbound channel
inboundChannel.subscribe(receiveMessage);

Let’s refactor the message publishing code into its own function. We’ll also modify what we’re sending so that it includes the customer ID in the message data. This will make it easy for the Lambda function to reply on the correct channel without having to extract the customer ID from the name of the inbound channel:

function postMessage(message) {
  // Note the additional call to appendMessageElement, which previously was only
  // being used by the receiveMessage handler. Because we want to visually
  // distinguish user messages from bot messages, the first argument is 'user'
  // for outbound messages, and 'bot' for inbound messages. These are simply the
  // names of the CSS classes to be attached to the message div element appended
  // to the DOM.
  appendMessageElement('user', inputField.value.trim());

  // Post to the outbound channel, and wrap both the message and the customer ID
  // in a plain object that the webhook will include when forwarding the data
  // on to your AWS Lambda function.
  outboundChannel.publish('user', { user: customerId, message });
}

Don’t forget to also update your processInput function to use postMessage rather than publishing directly:

function processInput(e) {
  if (e.which !== 13) { // Character code 13 is the ENTER key
    return;
  }

  const message = inputField.value.trim();
  if (message.length > 0) {
    postMessage(message);
  }

  inputField.value = '';
}

We could test the above now, and you certainly can do so if you want to be sure that there aren’t any errors in your code. However, the conversation is going to be a little one-sided if we don’t update your Lambda function to respond properly. You’ll recall that earlier in the tutorial, the Lambda function was set up with some test code which would be revised later, but we never came back to it.

Go back and edit your Lambda function with the following code (which you can find in the tutorial repository):

// We'll need the Node.js HTTPS module to post a response back to Ably
const https = require("https")

// Lambda functions are always preloaded with the AWS SDK package in order to
// make it easier to integrate with all of AWS' other services.
const AWS = require('aws-sdk');

// -----------------------------------------
const ablyApiKey = 'YOUR_API_KEY_GOES_HERE';
// -----------------------------------------

// This is the handler function that AWS will invoke:
exports.handler = (event, context, callback) => {

  // Get the Lex runtime from the AWS client library:
  const lex = new AWS.LexRuntime();

  // Extract the customer message and ID from the data received from Ably:
  const data = JSON.parse(event.messages[0].data);

  // `data.user` is the customer ID from our postMessage function:
  const channel = 'pizza:bot:' + data.user;

  // Construct a standard Node.js-style callback function that will be invoked
  // when our call AWS Lex completes:
  const onResponse = (err, result) => err
    ? callback(err)
    : postMessage(ablyApiKey, channel, result.message, callback);

  // Below, in addition the customer's message, we include their ID, because Lex
  // can use it to "remember" the conversation for a little while, which allows
  // the customer to reply with short messages that refer contextually to
  // previous responses emitted by the bot.
  lex.postText({
    botName: 'PizzaAssistant',
    botAlias: 'PizzaAssistant',
    userId: data.user,
    inputText: data.message
  }, onResponse);
};

// The onResponse callback function, defined above, calls postMessage in order
// to dispatch the response back to Ably. The Ably client API isn't available on
// AWS, at least not without a bunch of extra setup. Instead, the postMessage
// function will simply make use of Ably's REST API.

function postMessage (apiKey, channel, message, callback) {
  // Prepare the data to post to Ably:
  const data = JSON.stringify({
    name: 'bot',
    data: message
  });

  // HTTP configuration that includes the bot response channel and your API key,
  // included as a base64-encoded authorization header:
  const options = {
    host: 'rest.ably.io',
    port: 443,
    path: `/channels/${channel}/messages`,
    method: 'POST',
    headers: {
      'Authorization': `Basic ${new Buffer(apiKey).toString('base64')}`,
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(data)
    }
  };

  // Construct the HTTPS request using the above configuration data:
  const req = https.request(options, (res) => {
    let output = '';
    res.setEncoding('utf8');
    res.on('data', (chunk) => output += chunk);
    res.on('end', () => (statusCode, result) => {
      // We should still use the Lambda function's default callback, in case
      // there are any HTTPS errors returned by the Ably API. We can use further
      // AWS tools for investigation, if this is the case.
      callback(null, { statusCode, result });
    });
  });

  // As above, but trap errors that prevent the call from happening at all:
  req.on('error', (err) => {
    callback(err);
  });

  // Send the payload, and we're done:
  req.write(data);
  req.end();
}

One last step before leaving the AWS control panel – it’d be good if we could quickly test the updated Lambda function to make sure it’s working before proceeding. Update the test configuration you originally created, replacing the short JSON configuration you had earlier with the JSON in the code block below (also in the tutorial repository). This JSON data reflects the fact that Ably delivers the customer’s message wrapped in some additional metadata, which is why we need to unwrap the payload (note the JSON.parse line in the script block above) to get access to the data we want.

Note: The metadata exists due to the “Enveloped” option that was checked during the Integrations rule setup process. If you’d like to simplify things and omit all of Ably’s metadata, update the Integrations rule settings so that the “Enveloped” checkbox is unchecked. You can then modify the code to use JSON.parse(event) instead of JSON.parse(event.messages[0].data)).

{
  "source": "channel.message",
  "appId": "-6daoA",
  "channel": "pizza:customer:stKsXBKsYlPvCqVn",
  "site": "ap-southeast-2-A",
  "ruleId": "Xau9vA",
  "messages": [
    {
      "id": "fkFZMeSPmA:9:0",
      "name": "user",
      "connectionId": "fkFZMeSPmA",
      "timestamp": 1511223865774,
      "encoding": "json",
      "data": "{\"user\":\"stKsXBKsYlPvCqVn\",\"message\":\"I'd like a pepperoni pizza, please\"}"
    }
  ]
}

Run the test in the Lambda control panel and if all goes well, you should see in the response data that the Lex bot has replied correctly.

Once you’ve verified that the above is working, and you’ve made the appropriate changes to your application script, it’s time to test the app and have a conversation with your bot!

(d) Polishing up the experience

You may have noticed a short delay between your message submission and the bot’s response. It’d be good if we could get some visual feedback that they’re going to respond, so we’ll add a brief message that pretends they’re another person typing a response.

Also, the UI is not very useful unless the text input box is focused at all times, so we’ll add some additional event listeners to make sure that no matter where the user clicks on the page, or whether they leave or return to the browser window, the input box always automatically refocuses itself.

Modify the appendMessage function to return a reference to the HTML element it has just appended to the DOM:

function appendMessageElement(type, message) {
  const div = document.createElement('div');
  div.classList.add(type, 'message');
  div.textContent = message;
  chatLog.appendChild(div);

  const el = document.scrollingElement;
  el.scrollTop = el.scrollHeight;

  return div; // Add this line
}

Next, we’ll define a special “waiting” message that we add when the user types a message, and remove when a reply is received:

let waitingElement; // persist a reference to the HTML element

function setWaiting(isWaiting) { // isWaiting should be true or false
  if (isWaiting) {
    // If the waiting element already exists, remove it and re-add it so that it
    // is always the last message in the chat log:
    if (waitingElement) {
      waitingElement.remove();
    }
    // Note the "waiting" class which, if you're using the CSS provided in the
    // repository for this tutorial, will style the message so that it pulses
    // to indicate that something is happening in the background:
    waitingElement = appendMessageElement('waiting', 'typing ...');
  }
  else {
    // We'll call setWaiting(false) when the bot's response message is received,
    // which will trigger the following code to remove the waiting element:
    if (waitingElement) {
      waitingElement.remove();
      waitingElement = null;
    }
  }
}

Update the code to call the above function when sending and receiving messages:

function receiveMessage(message) {
  setWaiting(false); // The bot has replied, so remove the waiting message
  appendMessageElement('bot', message.data);
}

function postMessage(message) {
  appendMessageElement('user', inputField.value.trim());
  setWaiting(true); // Add a temporary waiting message
  outboundChannel.publish('user', { user: customerId, message });
}

Finally, at the end of the script where your text box event listener is defined, hook up a couple of extra event listeners to keep the user focused on the input text box:

function focusInputField() {
  inputField.focus();
}

// Re-focus the input field when the window regains focus:
window.addEventListener('focus', focusInputField);

// ... and if the user clicks anywhere else on the page:
document.body.addEventListener('click', focusInputField);

// When the page loads for the first time, focus the input field by default:
focusInputField();

Congratulations, you’re done!


Animated image of the browser app in action

Next Steps

1. The Lex bot we built isn’t exactly robust (or even complete, for that matter). If you’d like to improve it, spend some time investigating the Lex documentation and API to come up with some creative approaches of your own.
2. Take a look at the Webhooks documentation for further details about what was described in this tutorial.
3. If you would like to find out more about Ably features and capabilities, see Ably Integrations.
4. Learn more about Ably features by stepping through our other Ably tutorials
5. Gain a good technical overview of how the Ably realtime platform works
6. Get in touch if you need help