2
\$\begingroup\$

I am creating a very basic simulated chat bot/dialogue tree. I have the whole conversation mapped based on what a user selects. However, I am doing this by storing all of the conversation in append() functions in javascript. Which, no doubt makes this a pretty clunky experience.

In each 'question' the bot asks, I need to:

  1. Append the value of the button a user clicks (to simulate chat)
  2. Remove the chat controls
  3. Append the bot's response
  4. Append a follow up question
  5. Append the new question's chat controls

Which, is undoubtably a little much. This is what an average question/response function looks like:

$(".chat-controls").on("click", ".options.one", function() {
    input2 = $(this).text();
    $(".one").remove();

    if (input2 == "Nope, never heard, tell me more!") {
      //explanation
      $(".main-content").append(
        '<div class="user-dialog">' + input2 + "</div>"
      );
      $(".main-content").append(
        '<div class="dialog">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur a justo non nunc ultrices lacinia sit amet vel lorem. Aenean eget lobortis purus, a dictum justo.</div>'
      );
      $(".main-content").append(
        '<div class="dialog">Phasellus aliquet tellus ac molestie elementum. Fusce dapibus augue et lectus aliquet maximus. Maecenas feugiat tortor elit, eu imperdiet lectus cursus at.</div>'
      );

      $(".main-content").append(
        '<div class="dialog">Well ' +
        username +
        ", Maecenas feugiat tortor elit, eu imperdiet lectus cursus at?</div>"
      );
    } else {
      $(".main-content").append(
        '<div class="user-dialog">' + input2 + "</div>"
      );
      //questions

      $(".main-content").append(
        '<div class="dialog">Well ' +
        username +
        ", Maecenas feugiat tortor elit, eu imperdiet lectus cursus at?</div>"
      );
    }

    $(".chat-controls").append(
      '<button class="options two" type="button">I LOVE THEM</button>'
    );
    $(".chat-controls").append(
      '<button class="options two" type="button">They\'re alright, I guess</button>'
    );
    $(".chat-controls").append(
      '<button class="options two" type="button">Not a fan at all!</button>'
    );
  });

What ways could I optimize this? Making it easy to read while working on the JS and easy to add new steps. Here is a JSFiddle of the whole javascript.

\$\endgroup\$
  • \$\begingroup\$ Just a side note: Instead of providing an external JSFiddle you could use a runnable code snippet directly in your question. That would be more convenient. \$\endgroup\$ – πάντα ῥεῖ May 9 '17 at 17:25
2
\$\begingroup\$

Caching references to DOM objects

One simple way to optimize is to identify places where you're querying the DOM for a specific element multiple times, and replace those with variable references.

var mainContent = $(".main-content");
var chatControls = $(".chat-controls");

// ...

mainContent.append(
    '<div class="user-dialog">' + input2 + "</div>"
  );

Changing the way your store the dialog

A more maintainable approach would be to store the salient bits of your dialog tree (the questions and possible responses) in a traversable object.

var DialogTree = {
  question: "Have you ever heard of conversational UIs?",
  responses: [{
    response: "Yeah, they\'re pretty cool!",
    followup: {
      question: "How do you feel about them?",
      responses: [{
        response: "I LOVE THEM",
        followup: {
          question: "Yeah, same here."
        }
      }, {
        response: "They're alright, I guess.",
        followup: {
          question: "What's got you down?"
        }
      }, {
        response: "Not a fan at all!",
        followup: {
          question: "I can't say I blame you."
    }
  }]
}
  }, {
    response: "Nope, never heard, tell me more!",
    followup: {
      question: "They're pretty cool.",
      responses: [{
        response: "Oh.",
        followup: {
          question: "Don't you believe me?"
        }
      }, {
        response: "Very enlightening.",
        followup: {
          question: "Thank you."
        }
      }]
    }
  }]
};

Note that in the above example, each time the bot talks is represented by an object with a question string property and responses array property, and each object in the responses array is formatted as {response:"string",followup:{}} with followup representing a new question from the bot.

This is recursively traversable so that you'd only need one common function to read a question, write it to the chat history, and display the possible responses.

One benefit of this is that the data (your questions/responses) is separated logically from the view (directly updating the DOM with HTML and text). You can change or improve the way the questions are displayed without having to reformat your data.

Here's a barebones proof of concept:

var DialogTree = {
  question: "Have you ever heard of conversational UIs?",
  responses: [{
    response: "Yeah, they\'re pretty cool!",
    followup: {
      question: "How do you feel about them?",
      responses: [{
        response: "I LOVE THEM",
        followup: {
          question: "Yeah, same here."
        }
      }, {
        response: "They're alright, I guess.",
        followup: {
          question: "What's got you down?"
        }
      }, {
        response: "Not a fan at all!",
        followup: {
          question: "I can't say I blame you."
        }
      }]
    }
  }, {
    response: "Nope, never heard, tell me more!",
    followup: {
      question: "They're pretty cool.",
      responses: [{
        response: "Oh.",
        followup: {
          question: "Don't you believe me?"
        }
      }, {
        response: "Very enlightening.",
        followup: {
          question: "Thank you."
        }
      }]
    }
  }]
};

ShowDialogUI(DialogTree);

function ShowDialogUI(dialog) {
  var history = document.getElementById("chathistory");
  var buttons = document.getElementById("buttons");
  if (dialog.question) {
    history.appendChild(document.createElement("div")).appendChild(document.createTextNode(dialog.question));
  }
  clearChildren(buttons);
  if (dialog.responses) {
    var i = 0,
      len = dialog.responses.length,
      response, button;
    while (i < len) {
      response = dialog.responses[i];
      button = buttons.appendChild(document.createElement("button"));
      button.appendChild(document.createTextNode(response.response));
      if (response.followup) {
        (function(response) {
          button.addEventListener("click", function() {
            history.appendChild(document.createElement("div")).appendChild(document.createTextNode("> "+response.response));
            ShowDialogUI(response.followup);
          });
        })(response);
      }
      i++;
    }
  }
}

function clearChildren(el) {
  var len = el.children.length;
  while (len > 0) {
    len -= 1;
    el.removeChild(el.children[len]);
  }
}
<div id="chathistory"></div>
<div id="buttons"></div>

Storing your dialog tree as a monolithic traversable object is probably a good starting point so that you can arrange the rest of your code around that model, but in the long run you might want to break it up into discrete, reusable chunks. This would give you the benefit of allowing you to return to previously traversed bits of dialog without needing to repeat yourself in the code.

You could, for example, convert the questions to reusable functions.

function question_1() {
  return {
    question: "Have you ever heard of conversational UIs?",
    responses: [{
      response: "Yeah, they\'re pretty cool!",
      followup: question_2
    }, {
      response: "Nope, never heard, tell me more!",
      followup: question_3
    }]
  };
}

This allows you to use a defined question as a followup question for any number of responses. It even allows you to jump back and forth between bits of previously traversed dialog.

Here's an example where I've converted three questions into reusable functions:

function question_1() {
  return {
    question: "Have you ever heard of conversational UIs?",
    responses: [{
      response: "Yeah, they\'re pretty cool!",
      followup: question_2
    }, {
      response: "Nope, never heard, tell me more!",
      followup: question_3
    }]
  };
}

function question_2() {
  return {
    question: "How do you feel about them?",
    responses: [{
      response: "I LOVE THEM",
      followup: function() {
        return {
          question: "Yeah, same here."
        };
      }
    }, {
      response: "They're alright, I guess.",
      followup: function() {
        return {
          question: "What's got you down?"
        };
      }
    }, {
      response: "Not a fan at all!",
      followup: function() {
        return {
          question: "I can't say I blame you."
        };
      }
    }, {
      response: "Wait, what are we talking about again?",
      followup: question_1
    }]
  };
}

function question_3() {
  return {
    question: "Conversational UIs are UIs that are conversational.",
    responses: [{
      response: "Now that you mention it, I have heard of them!",
      followup: question_2
    }, {
      response: "Very enlightening.",
      followup: question_2
    }]
  };
}
ShowDialogUI(question_1());

function ShowDialogUI(dialog) {
  var history = document.getElementById("chathistory");
  var buttons = document.getElementById("buttons");
  if (dialog.question) {
    history.appendChild(document.createElement("div")).appendChild(document.createTextNode(dialog.question));
  }
  clearChildren(buttons);
  if (dialog.responses) {
    var i = 0,
      len = dialog.responses.length,
      response, button;
    while (i < len) {
      response = dialog.responses[i];
      button = buttons.appendChild(document.createElement("button"));
      button.appendChild(document.createTextNode(response.response));
      if (response.followup) {
        (function(response) {
          button.addEventListener("click", function() {
            history.appendChild(document.createElement("div")).appendChild(document.createTextNode("> "+response.response));
            ShowDialogUI(response.followup());
          });
        })(response);
      }
      i++;
    }
  }
}

function clearChildren(el) {
  var len = el.children.length;
  while (len > 0) {
    len -= 1;
    el.removeChild(el.children[len]);
  }
}
<div id="chathistory"></div>
<div id="buttons"></div>

These aren't complete examples (I didn't want to rewrite your code), and I'm sure they could benefit from additional code reviews, but I hope it gives you an idea of how you might approach the problem from a different angle.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ This is awesome, I ended up rebuilding it similarly but using nested conditionals, this seems a lot cleaner though. Thanks so much! \$\endgroup\$ – knocked loose May 10 '17 at 18:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.