Handling Adaptive cards in Microsoft Bot Framework

2020-07-26 12:32发布

问题:

  return new Promise((resolve, reject) => {
                            x = context.sendActivity({
                            text: 'hi',
                             attachments: [CardFactory.adaptiveCard(menuJson)]
                            })

I am trying to send an adaptive card, which contains a Input.text field in it...Now my question is how to get the input data from the user in my program using a context object ?

i.e How to Handle adaptive cards in bot framework v4 using node js ?

回答1:

Adaptive Cards send their Submit results a little different than regular user text. When a user types in the chat and sends a normal message, it ends up in context.activity.text. When a user fills out an input on an Adaptive Card, it ends up in context.activity.value, which is an object where the key names are the id in your menuJson and the values are the field values in the adaptive card.

For example, the json:

{
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "TextBlock",
            "text": "Test Adaptive Card"
        },
        {
            "type": "ColumnSet",
            "columns": [
                {
                    "type": "Column",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Text:"
                        }
                    ],
                    "width": 20
                },
                {
                    "type": "Column",
                    "items": [
                        {
                            "type": "Input.Text",
                            "id": "userText",
                            "placeholder": "Enter Some Text"
                        }
                    ],
                    "width": 80
                }
            ]
        }
    ],
    "actions": [
        {
            "type": "Action.Submit",
            "title": "Submit"
        }
    ],
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.0"
}

.. creates a card that looks like:

If a user enters "Testing Testing 123" in the text box and hits Submit, context.activity will look something like:

{ type: 'message',
  value: { userText: 'Testing Testing 123' },
  from: { id: 'xxxxxxxx-05d4-478a-9daa-9b18c79bb66b', name: 'User' },
  locale: '',
  channelData: { postback: true },
  channelId: 'emulator',
  conversation: { id: 'xxxxxxxx-182b-11e9-be61-091ac0e3a4ac|livechat' },
  id: 'xxxxxxxx-182b-11e9-ad8e-63b45e3ebfa7',
  localTimestamp: 2019-01-14T18:39:21.000Z,
  recipient: { id: '1', name: 'Bot', role: 'bot' },
  timestamp: 2019-01-14T18:39:21.773Z,
  serviceUrl: 'http://localhost:58453' }

The user submission can be seen in context.activity.value.userText.

Note that adaptive card submissions are sent as a postBack, which means that the submission data doesn't appear in the chat window as part of the conversation--it stays on the Adaptive Card.

Using Adaptive Cards with Waterfall Dialogs

Your question doesn't quite relate to this, but since you may end up attempting this, I thought it might be important to include in my answer.

Natively, Adaptive Cards don't work like prompts. With a prompt, the prompt will display and wait for user input before continuing. But with Adaptive Cards (even if it contains an input box and a submit button), there is no code in an Adaptive Card that will cause a Waterfall Dialog to wait for user input before continuing the dialog.

So, if you're using an Adaptive Card that takes user input, you generally want to handle whatever the user submits outside of the context of a Waterfall Dialog.

That being said, if you want to use an Adaptive Card as part of a Waterfall Dialog, there is a workaround. Basically, you:

  1. Display the Adaptive Card
  2. Display a Text Prompt
  3. Convert the user's Adaptive Card input into the input of a Text Prompt

In your Waterfall Dialog file (steps 1 and 2):

async displayCard(step) {
    // Display the Adaptive Card
    await step.context.sendActivity({
        text: 'Adaptive Card',
        attachments: [yourAdaptiveCard],
});
    // Display a Text Prompt
    return await step.prompt('textPrompt', 'waiting for user input...');
}

async handleResponse(step) {
    // Do something with step.result
    // Adaptive Card submissions are objects, so you likely need to JSON.parse(step.result)
    ...
    return await step.next();

In your bot.ts file (step 3):

const activity = dc.context.activity;

if (!activity.text && activity.value) {
    activity.text = JSON.stringify(activity.value);
}


回答2:

I'm using Adaptive Card in a ComponentDialog with WaterfallDialog, I want to handle Input.submit action.

My problem is: How to handle the response, get the input value, and go to next dialog step correctly?

I try 2 ways to resolve my problem.

My adaptive card's json like:

{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "type": "AdaptiveCard",
    "version": "1.0",
    "body": [
        {
            "type": "TextBlock",
            "text": "Create Schedule",
            "size": "large",
            "weight": "bolder"
        },
        {
            "type": "TextBlock",
            "text": "Name",
            "size": "small"
        },
        {
            "type": "Input.Text",
            "id": "name"
        }
    ],
    "actions": [
        {
            "type": "Action.Submit",
            "title": "Save",
            "data": {
                "result": "save"
            }
        },
        {
            "type": "Action.Submit",
            "title": "Cancel",
            "data": {
                "result": "cancel"
            }
        }
    ] 
}

1. Using prompt and prompt validate

This way uses prompt validate function to handle Input.submit postback action.

Because postback action doesn't send a text message (not show in channel), that makes TextPrompt's default validate can't pass (send retryPrompt), so I write a prompt validate function and validate response is postback action.

class MyDialog extends ComponentDialog{
    constructor(dialogId) {
        // ...
        this.addDialog(new TextPrompt('textPropmt', this.promptValidator);
        this.addDialog(new WaterfallDailog(dialogId, [
            // dialog steps
            async function(step){
                await step.context.sendActivity({
                    attachments: [CardFactory.adaptiveCard(FormCard)]
                })
                await step.prompt('TextPrompt', 'waiting for your submit.')
            },

            async function(step){
                await step.context.sendActivity('get response.');

                // get adaptive card input value
                const resultValue = step.context.activity.value; 

                return await step.endDialog();
            }
        ]));
    }

    // prompt validate function
    async promptValidator(promptContext){
        const activity = promptContext.context.activity;
        return activity.type === 'message' && activity.channelData.postback;
    }

    // ..
}

2. Using Dialog.EndOfTurn

This way uses Dialog.EndOfTurn to end turn. If user sends any response, the bot will go to next dialog step.

Please remember to check if the response is adaptive card submit action (postback), if not, do something to reject it or retry.

class MyDialog extends ComponentDialog{
    constructor(dialogId) {
        // ...

        this.addDialog(new WaterfallDialog(dialogId, [
            // dialog steps
            async function(step) {
                await step.context.sendActivity({
                    attachments: [CardFactory.adaptiveCard(FormCard)]
                });

                return Dialog.EndOfTurn;
            },

            async function(step) {
                await step.context.sendActivity('get response.');
                const activity = step.context.activity;

                if (activity.channelData.postback) {
                    // get adaptive card input value
                    const resultValue = activity.value;
                } else {
                    await step.context.sendActivity("Sorry, I don't understand.");
                }

                return await step.endDialog();
            }
        ]));
    }
    // ...
}

In the end, I would choose the second way (Dialog.EndOfTurn) to solve the problem, because I think it is easier to control the dialog step and handle user interrupt, for example, when user wants to cancel this action and return to main dialog.