Working with custom buttons to drive conversations

If I’ve said anything about bots, it’s that they’re apps. They’re just apps with a conversational interface. This style of interface can be extremely powerful, as it allows the user to better express themselves, or “skip to the end” if they already know what it is they’re trying to accomplish. The problem, though, is without a bit of forethought to the design of the bot it’s easy to wind up back in this scenario, where the user isn’t sure what to do next:

Command Prompt

If you’re well versed in the set of commands you can quickly perform any operation you desire. But there is no guidance provided by the system. Just as they’re no guidance provided here:

Command Prompt

Buttons are a good thing

We need to guide the user.

Buttons exist for a reason. They succinctly show the user what options are available, and can guide the user towards what they’re looking for. In addition, they help reduce the amount of typing required, which is especially important when talking about someone accessing a bot on a mobile device with a tiny keyboard.

Providing choices

The most obvious place where buttons shine is when providing a list of choices for a user to select from. This might be a shipping method, a category for filtering, or, really, any other set of options. To support a list of choices, BotBuilder provides a choice prompt. The choice prompt, as you might expect, provides the user a list of options for them to choose from, and then provides access to that in the next step of the dialog.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// sample waterfall dialog for a choice
(session, args, next) => {
builder.Prompts.choice(
session,
`What color do you want?`,
['Red', 'Green', 'Blue'],
{
listStyle: builder.ListStyle.button,
retryPrompt: `Please choose from the list of options`,
maxRetries: 2,
}
);
},
(session, results, next) => {
if(results.response && results.response.entity)
session.endConversation(`You chose ${results.response.entity}`);
else
session.endConversation(`Sorry, I didn't understand your choice.`);
}

The choice prompt limits the user’s response to just the list of options you provide. You can limit the number of times the bot will ask the user for a response before moving onto the next step in the waterfall.

While choice is certainly nice for providing a simple list of options, it does force the user into choosing one of those options. As a result, it’s not as easy to use choice when trying to guide the user with a list of options while also allowing them to type free-form, which is what you’ll want to do when the user first starts a session with the bot. In addition, you don’t get control over the interface provided.

Customizing the list of prompts

If you wish to customize the list of prompts, you need to set up a card. This can be an Adaptive Card, or one of the built-in cards such as thumbnail or hero. By using a card you can provide a bit more guidance to the channel on how you’d like your list of options to provide.

To allow the user to select from a list of options, you will add buttons to the card. Buttons can be set to either imBack, meaning the client will send the message back to the bot just as if the user typed it, or postBack, meaning the client will send the message to the bot without displaying it inside the client. Generally speaking, imBack is a better choice, as it makes it clear to the user something has happened, and can give the user a clue as to what to type in the future, should they so decide.

WARNING!!!

The code below is the wrong way to use buttons to provide a list of options, but it’s the most common mistake I see people make when using buttons with Bot Framework.

In the code snippet below, I want you to notice the addition of the buttons using builder.CardAction.imBack, and the call to session.send (where the mistake is).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(session, args, next) => {
const card = new builder.ThumbnailCard(session)
.text('Please choose from the list of colors')
.title('Colors')
.buttons([
new builder.CardAction.imBack(session, 'Red', 'Red'),
new builder.CardAction.imBack(session, 'Blue', 'Blue'),
new builder.CardAction.imBack(session, 'Green', 'Green'),
]);
const message = new builder.Message(session)
.addAttachment(card);
session.send(message);
},
(session, results, next) => {
if(results.response && results.response.entity)
session.endConversation(`You chose ${results.response.entity}`);
else
session.endConversation(`Sorry, I didn't understand your choice.`);
}

If you added this dialog to a bot and ran it, you’d see the following output:

Repeating buttons

The mistake, as I mentioned above, is at session.send. When using session.send in the middle of a waterfall dialog, the bot is left in a state where it’s not expecting the user to respond. As a result, when the user does respond by clicking on Blue, the bot simply returns back to the current step in the waterfall, and not to the next one. You can click the buttons as long as you’d like, and you’ll see them continuing to pop up.

The correct way to do it

In order for the bot to be in a state that expects user input and continues to the next step of a waterfall, you must use a prompt. When using buttons inside of a card, you can choose either a text or choice prompt. When using a text prompt, the bot can accept any input in addition to the buttons you provided. This can allow the user to be more free-form as needed. choice prompts, however, will limit the user to the list of choices, just as if you created it the traditional way mentioned earlier.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Using a choice prompt with custom buttons
// For simplicity, I removed the retry prompts, but you can continue to use them
// If you wanted to use a text prompt, you'd simply use:
// builder.Prompts.text(session, message);
(session, args, next) => {
const choices = ['Red', 'Blue', 'Green']
const card = new builder.ThumbnailCard(session)
.text('Please choose from the list of colors')
.title('Colors')
.buttons(choices.map(choice => new builder.CardAction.imBack(session, choice, choice)));
const message = new builder.Message(session)
.addAttachment(card);
builder.Prompts.choice(session, message, choices);
},
(session, results, next) => {
if(results.response && results.response.entity)
session.endConversation(`You chose ${results.response.entity}`);
else
session.endConversation(`Sorry, I didn't understand your choice.`);
}

Providing a menu

As I mentioned at the beginning of this post[1], one of the keys to a good user experience in a bot is to provide guidance to the user, otherwise you’re just giving them a C-prompt.Again, the easiest way to do this is via buttons.

We’ve already seen that imBack behaves just as if the user typed the value manually. We can take advantage of this fact by providing the list of options, and ensuring the values match the intents provided in the bot.

You’ll notice in the code sample below I created a bot with two simple dialogs, and the default dialog sends down the buttons inside of a card. By calling endConversation, the bot sends down the card and closes off the conversation. When the user clicks on a button it’s just as if the user typed in the value, and the bot will then route the request to the appropriate dialog. The user is free at this point to either click one of the provided buttons, or type in whatever command they desire.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const bot = new builder.UniversalBot(
new builder.ChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
}),
(session) => {
const card = new builder.ThumbnailCard(session)
.title('Sample bot')
.text(`Hi there! I'm the sample bot.
You can choose one of the options below
or type in a command of your own
(assuming I support it)`)
.buttons([
builder.CardAction.imBack(session, 'Hello', 'Hello'),
builder.CardAction.imBack(session, 'Greetings', 'Greetings'),
]);
const message = new builder.Message(session)
.addAttachment(card);
session.endConversation(message);
}
);
bot.dialog('Hello', (session) => {
session.endConversation(`The Hello Dialog`)
}).triggerAction( { matches: /Hello/ } );
bot.dialog('Greetings', (session) => {
session.endConversation(`The Greetings Dialog`)
}).triggerAction( { matches: /Greetings/ } );

The updated bot now performs as displayed below. In the dialog I started by typing test to trigger the bot. I then clicked on Hello, which displayed the Hello Dialog message. I completed the exchange by typing Hello, which, as you see, sent the same Hello Dialog message.

Introduction with buttons

Conclusion

I’ve said it before, and I’ll certainly say it again - buttons exist for a reason. Buttons can help you provide a good UI/UX for users in any type of application, and bots are no exception. You can use buttons to both limit the amount of typing required, and to help guide the user’s experience with the bot.

[1] This exceedingly long post?