In the previous post, we added some helpers and enabled the user to call Microsoft Graph. We used examples with GraphClient
and PnPJs Graph
for hooking into the Azure Bot Service auth flow. And we also added some validation to the dialog using Microsoft Graph.
Now we will get a little fancy using Adaptive cards in the Bot. And if we want to use these in Microsoft Teams in prompts, it gets a little tricky. I will walk you through my ways of handling this.
The finished source code: https://github.com/simonagren/simon-blog-bot-v7
I’ve added two options in the tsconfig
file that enable us to use import
when it comes to JSON files. Without these, we would need to use a require
syntax.
"resolveJsonModule": true,"esModuleInterop": true,
import WelcomeCard from '../resources/welcome.json';
In the src
folder we create an additional folder: resources
. The folder contains 4 new files:
It’s not in scope to teach you everything regarding what Adaptive Cards are. You can read more Here. Essentially you create a JSON structure with semantic values. The text could be Large
and the color Accent
- you declare an approximate, then the host
renders the card. The Adaptive Card will look like native Teams, or Outlook, Cortana etc. You don’t care about font-sizes and pixels while creating the cards.
As we saw in the first post, we get normal text responses from the user via context.activity.text
. Adaptive Card responses in Teams comes back via context.activity.value
.
We need to be aware of this in general, but this also leads to workarounds for prompts in dialogs.
A workaround that I normally use is to catch the context.activity.value
in the OnMessage
method, and then set the empty context.activity.text
with the value we got from context.activity.value
if (context.activity.text === undefined && context.activity.value ) {context.activity.text = JSON.stringify(context.activity.value);}
This will not interrupt the normal flow. The Bot will, just as before, kick off the Main Dialog with the context
that we slightly adapt. It will work the same as if the user would just have entered a text response in a normal prompt.
// Run the Dialog with the new message Activity.await (this.dialog as MainDialog).run(context, this.dialogState);
We import CardFactory
from botbuilder
. You could either paste an Adaptive Card JSON, or import the value from a file like I have done (less cluttering).
const welcomeCard = CardFactory.adaptiveCard(WelcomeCard);
The adaptiveCard
method creates an Attachment
object with the correct content type
and the card JSON itself. For those of you familiar with these cards might have seen the content type before:
'application/vnd.microsoft.card.adaptive'
And this is what the static method looks like
public static adaptiveCard(card: any): Attachment {return { contentType: CardFactory.contentTypes.adaptiveCard, content: card };}
We change from just sending a message to send a card as an attachment. When using attachments
in context.sendActivity
it expects an array of attachments. So make sure you put the card into brackets.
await context.sendActivity({ attachments: [welcomeCard] });
This is what OnMessage looks like now
this.onMessage(async (context, next) => {// If result comes from an Adaptive Cardif (context.activity.text === undefined && context.activity.value ) {context.activity.text = JSON.stringify(context.activity.value);}// Run the Dialog with the new message Activity.await (this.dialog as MainDialog).run(context, this.dialogState);// By calling next() you ensure that the next BotHandler is run.await next();});
This is what OnMembersAdded looks like now
this.onMembersAdded(async (context, next) => {const membersAdded = context.activity.membersAdded;const welcomeCard = CardFactory.adaptiveCard(WelcomeCard);for (const member of membersAdded) {if (member.id !== context.activity.recipient.id) {await context.sendActivity({ attachments: [welcomeCard] });}}// By calling next() you ensure that the next BotHandler is run.await next();});
I removed CHOICE_PROMPT
and TITLE_PROMPT
constants, and also the .addDialog()
for them.
Imports from botbuilder
for types and Attachment creation.
import { Attachment, AttachmentLayoutTypes, CardFactory } from 'botbuilder';
Adaptive cards
import GenericCard from '../resources/generic.json';import SiteTypesCard from '../resources/siteTypes.json';import SummaryCard from '../resources/summary.json';
This step has changed from a ChoicePrompt
to a TextPrompt
. We will instead use the adaptive card SiteTypesCard
for the choices. If you recall, it contains an array of three different adaptive cards in JSON-format. That’s why I’m using .map()
to create an array of Attachments. We use a different attachmentLayout
in the form of a carousel.
We first send the Adaptive Card as an attachment. We then send a Text prompt - but with no text. The Text prompts makes sure we are waiting for user input. When the user clicks on one of the cards we will (through the workaround) catch the response and add it to the siteDetails
just like before.
private async siteTypeStep(stepContext: WaterfallStepContext): Promise<DialogTurnResult> {const siteDetails = stepContext.options as SiteDetails;if (!siteDetails.siteType) {const siteTypeCards: Attachment[] = SiteTypesCard.cards.map((card: any) => CardFactory.adaptiveCard(card));await stepContext.context.sendActivity({attachmentLayout: AttachmentLayoutTypes.Carousel,attachments: siteTypeCards});return await stepContext.prompt(TEXT_PROMPT, '');} else {return await stepContext.next(siteDetails.siteType);}}
In this step the validation is removed. It was just an example that we don’t need. Now we are using a TEXT_PROMPT
here as well.
We are using the GenericCard
that was prepared with a $Placeholder
variable. We will re-use this generic card in many of the steps and do a .replace()
in the JSON with the “text value” we want to prompt the user. We first send the card and then prompt the user with an empty text value to await a response.
private async titleStep(stepContext: WaterfallStepContext): Promise<DialogTurnResult> {const siteDetails = stepContext.options as SiteDetails;siteDetails.siteType = stepContext.result.value;if (!siteDetails.title) {const promptText = `Provide a title for your ${siteDetails.siteType} site`;const titleCard: Attachment = CardFactory.adaptiveCard(JSON.parse(JSON.stringify(GenericCard).replace('$Placeholder', promptText)));await stepContext.context.sendActivity({ attachments: [titleCard] });return await stepContext.prompt(TEXT_PROMPT, '');} else {return await stepContext.next(siteDetails.title);}}
This step uses the SummaryCard
that I prepared with multiple placeholder values. Worth noting is that it’s still a ConfirmPrompt.
private async confirmStep(stepContext: WaterfallStepContext): Promise<DialogTurnResult> {const siteDetails = stepContext.options as SiteDetails;// Capture the results of the previous stepsiteDetails.alias = stepContext.result;const summaryCard: Attachment = CardFactory.adaptiveCard(JSON.parse(JSON.stringify(SummaryCard).replace('$Title', siteDetails.title).replace('$Desc', siteDetails.description).replace('$Owner', siteDetails.owner).replace('$Type', siteDetails.siteType).replace('$Alias', siteDetails.alias ? siteDetails.alias : '' )));await stepContext.context.sendActivity({ attachments: [summaryCard] });// Offer a YES/NO prompt.return await stepContext.prompt(CONFIRM_PROMPT, { prompt: '' });}
Another workaround would be to create your own Prompt class.
I just created a prompt that extends the normal text prompt, and I override the onRecognize method to work with both value
and text
import { TextPrompt, PromptOptions, PromptRecognizerResult, PromptValidator } from "botbuilder-dialogs";import { TurnContext } from "botbuilder";export class AdaptiveTextPrompt extends TextPrompt {/*** Creates a new Adaptive Card TextPrompt instance.*/constructor(dialogId: string, validator?: PromptValidator<string>){super(dialogId, validator);}async onRecognize(context: TurnContext, state: any, options: PromptOptions): Promise<PromptRecognizerResult<string>> {let value: any;if (context.activity.text === undefined && context.activity.value ) {value = context.activity.value;return typeof value === 'object' && value != undefined ? { succeeded: true, value: value } : { succeeded: false };}else if (context.activity.value === undefined && context.activity.text ) {value = context.activity.text;return typeof value === 'string' && value.length > 0 ? { succeeded: true, value: value } : { succeeded: false };}}}
In this post, we looked at using Adaptive Cards in our prompts.
In the next post, we will use proactive messaging in a secure manner.