How to handle tap action on adaptive card bot framework v4? - botframework

I am having an adaptive card which will be displayed in waterfall dialog. I want to know is it possible to capture the tap action on the adaptive card I don't want to add button to handle click. I am using v4 version of bot framework.
Tap dialog is triggered twice
My adaptive card :
var Card1 = {
"type": "AdaptiveCard",
"selectAction": {
"type": "Action.Submit",
"id": "tap",
"title": "tap",
"data": "data": { tap: 'tap' }
},
"backgroundImage": "https://download-ssl.msgamestudios.com/content/mgs/ce/production/SolitaireWin10/dev/adapative_card_assets/v1/card_background.png",
"body": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "Image",
"url": "https://download-ssl.msgamestudios.com/content/mgs/ce/production/SolitaireWin10/dev/adapative_card_assets/v1/tile_spider.png",
"size": "Stretch"
}
],
"width": 1
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"horizontalAlignment": "Center",
"weight": "Bolder",
"color": "Light",
"text": "Click here to play another game of Spider in Microsoft Solitaire Collection!",
"wrap": true
}
],
"width": 1
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "0.5"
};
This is how i have updated the code
Code
// Add prompts that will be used by the main dialogs.
this.dialogs.add(new TextPrompt(TAP_PROMPT, () => true));
// Create a dialog that asks the user for their name.
this.dialogs.add(new WaterfallDialog(PUBLICTRANS, [
this.promptForTap.bind(this),
this.captureTap.bind(this)
]));
async promptForTap(step) {
var Card1 = {
"type": "AdaptiveCard",
"selectAction": {
"type": "Action.Submit",
"id": "tap",
"title": "tap",
"data": "data": { tap: 'tap' }
},
"backgroundImage": "https://download-ssl.msgamestudios.com/content/mgs/ce/production/SolitaireWin10/dev/adapative_card_assets/v1/card_background.png",
"body": [
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "Image",
"url": "https://download-ssl.msgamestudios.com/content/mgs/ce/production/SolitaireWin10/dev/adapative_card_assets/v1/tile_spider.png",
"size": "Stretch"
}
],
"width": 1
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"horizontalAlignment": "Center",
"weight": "Bolder",
"color": "Light",
"text": "Click here to play another game of Spider in Microsoft Solitaire Collection!",
"wrap": true
}
],
"width": 1
}
]
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0"
};
const reply = {
attachments: [CardFactory.adaptiveCard(Card1)]
};
return await step.prompt(TAP_PROMPT, { prompt: reply });
}
async captureTap(step) {
// Edited from original answer
await step.context.sendActivity(`You selected `);
// Send Back Channel Event
await step.context.sendActivity({ type: 'event', name: 'tapEvent' });
return await step.endDialog();
}
output the card is triggered twice

You can send a Back Channel Event to WebChat from the Bot when the user clicks on an AdaptiveCard to trigger an event on the web page.
Simple AdaptiveCard with Select Action
First, create an AdaptiveCard that has a selectAction. Note, you can either add a selectAction to the whole card or different components in the card like an image or a column. When the user clicks on a part of the card that has a selectAction, it will send an activity to the Bot that contains the data attribute from the action that we can use to determine which action was triggered on the bot side.
Be sure to set the title attribute, the data field, and the type which should be Action.Submit.
{
"type": "AdaptiveCard",
"selectAction": {
"type": "Action.Submit",
"id": "tap",
"title": "tap",
"data": { "tap": "Play again"}
},
"body": [
{
"type": "TextBlock",
"horizontalAlignment": "Center",
"size": "Medium",
"weight": "Bolder",
"text": "Click Me!"
},
{
"type": "Image",
"url": "https://usercontent2.hubstatic.com/13896379.jpg"
}],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0"
}
Bot Code - Node Js
Constructor
const TAP_PROMPT = 'tap_prompt';
constructor(conversationState, userState) {
...
// Add prompts that will be used by the main dialogs.
this.dialogs.add(new TextPrompt(TAP_PROMPT, () => true));
// Create a dialog that asks the user for their name.
this.dialogs.add(new WaterfallDialog(WHO_ARE_YOU, [
this.promptForTap.bind(this),
this.captureTap.bind(this)
]));
}
AdaptiveCard Prompt
In one of the steps of your Waterfall, you can create a reply object that has the AdaptiveCard as an attachment. Then you can pass that reply to the prompt. I would recommend using a TextPrompt for this step.
// This step in the dialog sends the user an adaptive card with a selectAction
async promptForTap(step) {
const reply = {
attachments: [CardFactory.adaptiveCard(Card)]
}
return await step.prompt(TAP_PROMPT, { prompt: reply });
}
Capture AdaptiveCard selecteAction
In the next step of the Waterfall, you can get the data attribute sent from the AdaptiveCard by accessing step.value. Here, send an activity to the bot with a type and a name property. These two attributes will be used to filter incoming activities in WebChat and trigger the proper event.
// This step captures the tap and sends a back channel event to WebChat
async captureTap(step) {
// Edited from original answer
await step.context.sendActivity(`You selected ${step.context.activity.value.tap}`);
// Send Back Channel Event
await step.context.sendActivity({type: 'event', name: 'tapEvent'});
return await step.endDialog();
}
WebChat Middleware
In WebChat, we are going to create a custom middleware to check incoming activities. When we encounter an activity that has a name and type that we recognize, trigger your event on the webpage. In the example below, I just alerted the use that they clicked on the AdaptiveCard.
const store = window.WebChat.createStore(
{},
({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
const { name, type } = action.payload.activity;
if (type === 'event' && name === 'tapEvent') {
alert("You tapped on the AdaptiveCard.");
}
}
return next(action);
}
);
window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({ token }),
store,
}, document.getElementById('webchat'));
For more details on back channel events and creating a custom middleware in WebChat, checkout this sample in the WebChat Repo.
Hope this helps!

Related

Why is Slack not formatting my message payload?

I have built a message to be returned in slack from a payload generated in a js module. The message was formatted in block kit builder as:
{
"blocks": [
{
"type": "section",
"text": {
"text": "Tides for *Aberdeen* on _Saturday, 5th February 2023 (GMT)_",
"type": "mrkdwn"
},
"fields": [
{
"type": "mrkdwn",
"text": "*Time*"
},
{
"type": "mrkdwn",
"text": "*Height*"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "For multiple days and tide height predictions use <https://script.google.com/macros/s/AKfycbwdzfkbP-YiBD0r7moZ80MrjgU68CYQ-pNqm8YbPoDQd_iSOk7vGOxV7xBdZrxQSzsafQ/exec|:uktides: Uk Tides>."
}
}
]
}
And this gives the following in block kit builder:
However when the app runs, the returned payload is displayed as:
i.e. the formatting is somehow ignored.
The relevant section of my code is:
var payload = {
"blocks": [
{
"type": "section",
"text": {
"text": "Tides for *Aberdeen* on _Saturday, 5th February 2023 (GMT)_",
"type": "mrkdwn"
},
"fields": [
{
"type": "mrkdwn",
"text": "*Time*"
},
{
"type": "mrkdwn",
"text": "*Height*"
}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "For multiple days and tide height predictions use <https://script.google.com/macros/s/AKfycbwdzfkbP-YiBD0r7moZ80MrjgU68CYQ-pNqm8YbPoDQd_iSOk7vGOxV7xBdZrxQSzsafQ/exec|:uktides: Uk Tides>."
}
}
]
}
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
socketMode: true,
appToken: process.env.SLACK_APP_TOKEN,
// Socket Mode doesn't listen on a port, but in case you want your app to respond to OAuth,
// you still need to listen on some port!
port: process.env.PORT || 3000,
extendedErrorHandler: true
});
// Listens to incoming messages that contain "hello"
app.message('hello', async ({ message, say }) => {
// say() sends a message to the channel where the event was triggered
await say(`Hey there <#${message.user}>! my name is Ollie 2.1, how can I help you?`);
});
app.command('/tides', async ({ command, ack, say, respond }) => {
// Acknowledge command request
await ack();
await parseCommand(`${command.text}`);
await say(JSON.stringify(payload));
//await respond(`${command.text}`);
});
Can anyone see what I am doing wrong?
Thanks.
I realised that I did not need to JSON.stringify the payload and by simply returning the payload, it formatted correctly. However I receive warnings in the console output so not fully resolved yet!

AdaptiveCard is shown without app icon/name in ms teams

We are implementing search command for microsoft teams.
When composeExtension/query is invoked then bot returns list of attachments with previews that includes a Tap Tap = new CardAction { Type = "invoke", Value.... Like in this example https://learn.microsoft.com/en-us/microsoftteams/platform/messaging-extensions/how-to/search-commands/respond-to-search?tabs=dotnet#response-example
After this teams shows search result. When user clicks on any result then bot is invoked with composeExtension/selectItem and returns new AdaptiveCard as result. Then teams shows this card but without app icon/title. Also following error is logged in teams
0-angular-jquery.min-eee9041.js:114 2021-01-15T16:29:22.175Z AppsService: getInstalledAppForUser - Invalid appId specified
(anonymous) # 0-angular-jquery.min-eee9041.js:114
(anonymous) # 3.2-app.min-e6c3257.js:1
...
Uncaught (in promise) Invalid appId specified
But when user submit this card then it's shown correctly (with app icon/title and etc).
Can somebody help to fix this problem? Thanks in advance :)
This is the response for composeExtension/selectItem
{
"composeExtension": {
"attachments": [
{
"content": {
"type": "AdaptiveCard",
"body": [
{
"items": [
{
"text": "Untitled task",
"type": "TextBlock"
...
}
],
"separator": false,
"type": "Container"
}
//...
],
"actions": [
{
"url": "https://.....",
"title": "View activity",
"type": "Action.OpenUrl"
}
],
"version": "1.2"
},
"contentType": "application/vnd.microsoft.card.adaptive",
"preview": {
"content": {
"title": "Preview"
},
"contentType": "application/vnd.microsoft.card.hero"
}
}
],
"type": "result",
"attachmentLayout": "list"
},
"responseType": "composeExtension"
}
Thank you for the Insights. We have raised a bug on the issue and it is being tracked internally.

Send Hero or Adaptive card & Get User Input on Welcome

I am developing Bot in .NET Core 3.1 C#. I want to send Hero card with 4 buttons & welcome prompt as soon as user joins /activates bot. I have tried it in OnMembersAddedAsync
if (member.Id != turnContext.Activity.Recipient.Id)
{
var welcomeCard = CreateAdaptiveCardAttachment();
var response = MessageFactory.Attachment(welcomeCard);
await turnContext.SendActivityAsync( response, cancellationToken);
}
This will display adaptive card where type is Action.Submit. But I am not sure how to get values of the button which customer click on. I tried it on OnMessageActivityAsync
if (turnContext.Activity.Value != null)
{
var mainMenu = turnContext.Activity.Value;
}
But values are always null. Json for adaptive card is :
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.0",
"body": [
{
"type": "TextBlock",
"spacing": "medium",
"size": "default",
"weight": "bolder",
"text": "Welcome to ABC Bank !",
"wrap": true,
"maxLines": 0
},
{
"type": "TextBlock",
"size": "default",
"isSubtle": true,
"text": "Please select user type from below ....",
"wrap": true,
"maxLines": 0
}
],
"actions": [
{
"type": "Action.Submit",
"title": "Consumer"
},
{
"type": "Action.Submit",
"title": "Client"
},
{
"type": "Action.Submit",
"title": "Merchant"
}
]
}
Yes. As said in comments you need to add data property. This is not required. But if you want to collect which option user has provided you need to specify it. Since you don't have any other input field. This will act as an input to carry further operations.
Note: For any input element you need to use id property to identify collected input when submit action is performed.
Similarly for Submit action data. Make sure value in data for each action is unique. If you want 2 buttons to perform same action (nativate to same dialog) then you can specify same value in data
Here is the official link which gives you an idea.
Submit action
Hope this helps

Cancel button on microsoft adaptive cards for teams

I am using the ms adaptive cards for teams using nodejs. I can see actions has button of type Action.Submit to pass form data. However, I want to understand how to handle cancel case.
Is there a way to simply close the form on clicking cancel button or I have to let it behave like save button and return nothing from server side when the cancel button is pressed.
my card is like below
{
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"size": "Medium",
"weight": "Bolder",
"text": "{title}"
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "Image",
"style": "Person",
"url": "{creator.profileImage}",
"size": "Small"
}
],
"width": "auto"
},
{
"type": "Column",
"items": [
{
"type": "TextBlock",
"weight": "Bolder",
"text": "{creator.name}",
"wrap": true
},
{
"type": "TextBlock",
"spacing": "None",
"text": "Created {{DATE({createdUtc},SHORT)}}",
"isSubtle": true,
"wrap": true
}
],
"width": "stretch"
}
]
}
],
"actions": [
{
"type": "Action.ShowCard",
"title": "Set due date",
"card": {
"type": "AdaptiveCard",
"body": [
{
"type": "Input.Text",
"id": "comment",
"placeholder": "Add a comment",
"isMultiline": true
}
],
"actions": [
{
"type": "Action.Submit",
"title": "OK"
},
{
"type": "Action.Submit",
"title": "Cancel"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json"
}
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0"
}
There is no exact defined way to handle Cancel functionality.
You have to manage it code behind additionally, Yes you are right like Save Action you also have to set functionality for Cancel
For example what I did is when my user choose sorry, not now (Think
like Cancel) I took that response and under a Switch-Case reply as
required.
//Check Each User Input
switch (checkUserInput.ToLower())
{
case "sorry, not now":
await turnContext.SendActivityAsync(MessageFactory.Text("Okay, Can I help with anything else?"), cancellationToken);
//Send Another Yes/No Card
var yesNoFlow = _customFlowRepository.YesNoFlow();
await turnContext.SendActivityAsync(yesNoFlow).ConfigureAwait(false);
break;
default: //When nothing found in user intent
await turnContext.SendActivityAsync(MessageFactory.Text("What are you looking for?"), cancellationToken);
break;
}
You could have a look the screen shot below:
Hope this would help you to figure out your issue. Let me know if you have any more concern.
There's no way to "Cancel" a card per se, to make it go away - if the user doesn't want to continue, they can simply stop interacting with the card. However, here are some possible alternatives:
You -could- implement a "cancel" button as a submit action, which you could detect in the bot, and reply with an appropriate message
You could look at consider the "ShowCard" action? It basically lets you collapse part of your card, and only open it when a user clicks on a button. That way you could possibly group your card into sections and show each one at a time. See here for more.
Another option in future is the new ToggleVisibility action in AdaptiveCards 1.2, but it's only if your client supports 1.2. (e.g. it's only available in Developer Preview for Teams right now (so very likely/hopefully coming in future, but not available at the moment))
There is nothing such as a cancel button in Adaptive Cards. If you want to close the card/not show the card anymore you could try updating that card with another new card that you would like to show.
From your code, the Cancel Button acts just like that of Submit. It's
because the Submit button will be associated with all the fields and
inputs that you are providing from your Bot and the same applies for
Cancel as it is defaulted to all Inputs.
By adding the changes as that of below, once you hit the Cancel Button,
you will come out of the form and navigate to whichever dialog/flow
that follows.
{
"type": "Action.Submit",
"title": "Cancel",
"associatedInputs": "none"
}
For more information please find this link:
https://adaptivecards.io/explorer/Action.Submit.html.
I believe this might resolve the problem.
another approach is to look for a cancel data object:
new AdaptiveSubmitAction
{
Title = "Login",
Style="positive",
AssociatedInputs = AdaptiveAssociatedInputs.Auto,
Id="ok",
},
new AdaptiveSubmitAction
{
Title = "Cancel",
Style="negative",
AssociatedInputs = AdaptiveAssociatedInputs.None,
Data = CANCEL_VALUE,
Id="cancel"
}
You can then detect the cancel like:
private async Task<DialogTurnResult> SignUserInStepAsync(WaterfallStepContext stepContext, CancellationToken cancellationToken)
{
if (stepContext.Context.Activity.AsMessageActivity()?.Text == CANCEL_VALUE)
{
return new DialogTurnResult(DialogTurnStatus.Cancelled);
}
NOTE:It's important to indicate that the cancel button is not associated with the rest of the card's fields, by setting AssociatedInputs = AdaptiveAssociatedInputs.None.

Adaptive Card rendered as image on FB shows "Title" text/element

I understand that adaptive cards are down-rendered as image on channels that does not support them. I just want to know how to either remove or set the "Title" element as shown on the fb channel:
The AdaptiveCard.Title element is deprecated and I did try setting that, it did not have any effect.
Here is my sample json:
{
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"id": "Title",
"horizontalAlignment": "Center",
"size": "Large",
"weight": "Bolder",
"text": "See results on our website!"
},
{
"type": "Image",
"horizontalAlignment": "Center",
"url": "mylogo.png",
"size": "Stretch"
},
{
"type": "TextBlock",
"id": "Subtitle",
"horizontalAlignment": "Center",
"size": "ExtraLarge",
"text": "This channel does not allow us to display your results. Click the button to view it on our website.",
"wrap": true
}
],
"actions": [
{
"type": "Action.OpenUrl",
"id": "OpenUrl",
"title": "Take me there!"
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0"
}
Unfortunately, when the BotFramewrok renders the card into an image for Facebook Messenger it adds the title above the actions which is strange. The only way to get rid of it is to remove the action from the card, which defeats its purpose in this case. An alternative is to send a Facebook Button Template in the activity's channel data instead of the adaptive card when the user is on Facebook Messenger. For more details checkout the Facebook Documentation on Button Templates and the code snippet below.
Screenshot
Bot Code - Node
async onTurn(turnContext) {
if (turnContext.activity.type === ActivityTypes.Message) {
if (turnContext.activity.channelId === 'facebook') {
await turnContext.sendActivity({
channelData: {
"attachment": {
"type": "template",
"payload": {
"template_type":"button",
"text":"This channel does not allow us to display your results. Click the button to view it on our website.",
"buttons":[{
"type":"web_url",
"url":"https://www.microsoft.com",
"title":"Take me there!"
}]
}
}
}
});
} else {
await turnContext.sendActivity({
attachments: [this.createAdaptiveCard()],
});
}
}
}
Hope this helps!
I had the same problem but I find an alternative using HeroCard.
If you are developing it with C# there it goes the code:
// first of all check if it is Facebook channel
// note that Channels is from Microsoft.Bot.Connector, the old one is deprecated.
if (turnContext.Activity.ChannelId == Channels.Facebook)
{
Activity replyToConversation = _flowService.ConvertMarkdownUrlToFacebookUrl(turnContext.Activity, response.Answer);
await turnContext.SendActivityAsync(replyToConversation, cancellationToken: cancellationToken);
}
public Activity ConvertMarkdownUrlToFacebookUrl(Activity message, string queryResponse)
{
var buttons = getButtons(queryResponse, out string result);
Activity replyToConversation = message.CreateReply();
replyToConversation.Attachments = new List<Attachment>();
List<CardAction> actions = new List<CardAction>();
foreach (var button in buttons)
{
actions.Add(new CardAction()
{
Title = button.Key, // text hyperlink
Type = ActionTypes.OpenUrl,
Value = button.Value // url
});
}
Attachment attachment = new HeroCard
{
Text = result, // your text
Buttons = actions
}.ToAttachment();
replyToConversation.Attachments.Add(attachment);
return replyToConversation;
}
You'll obtain something like this:
(sorry I had to delete the text)
Maybe it's not perfect (my first time with cards and attachments) but I hope that this will help someone 🙂

Resources