Search code examples
node.jstypescriptbotframeworkprompt

Bot framework (v4) Prompt choice in carousel using HeroCards not going to next step


I’m trying to use HeroCards along with a prompt choice in a carousel. So the options to be selected by the user are displayed as HeroCards. As soon as the user clicks in the button of a card it should goes to the next waterfall function.

Here is a working example in bot framework v3. It does work as expected.

  const cards = (data || []).map(i => {
    return new builder.HeroCard(session)
      .title(`${i.productName} ${i.brandName}`)
      .subtitle(‘product details’)
      .text(‘Choose a product’)
      .images([builder.CardImage.create(session, i.image)])
      .buttons([builder.CardAction.postBack(session, `${i.id.toString()}`, ‘buy’)]);
  });

  const msg = new builder.Message(session);
  msg.attachmentLayout(builder.AttachmentLayout.carousel);
  msg.attachments(cards);

  builder.Prompts.choice(session, msg, data.map(i => `${i.id.toString()}`), {
    retryPrompt: msg,
  });

Below I’m trying to do the same with bot framework v4 but it does not work. It never goes to the next function in my waterfall.

How can I do the same with v4?

this.addDialog(new ChoicePrompt(PRODUCTS_CAROUSEL));

const productOptions: Partial<Activity> = MessageFactory.carousel(
  item.map((p: Product) =>
    CardFactory.heroCard(
      p.productName,
      ‘product details’,
      [p.image || ''],
      [
        {
          type: ActionTypes.PostBack,
          title: ‘buy’,
          value: p.id,
        },
      ],
    ),
  ),
  ‘Choose a product’,
);

return await step.prompt(PRODUCTS_CAROUSEL, productOptions);

UPDATE:

Follow full code with the suggestion from @Drew Marsh

export class ProductSelectionDialog extends ComponentDialog {
  private selectedProducts: Product[] = [];
  private productResult: Product[][];
  private stateAccessor: StatePropertyAccessor<State>;

  static get Name() {
    return PRODUCT_SELECTION_DIALOG;
  }

  constructor(stateAccessor: StatePropertyAccessor<State>) {
    super(PRODUCT_SELECTION_DIALOG);

    if (!stateAccessor) {
      throw Error('Missing parameter.  stateAccessor is required');
    }

    this.stateAccessor = stateAccessor;

    const choicePrompt = new ChoicePrompt(PRODUCTS_CAROUSEL);
    choicePrompt.style = ListStyle.none;

    this.addDialog(
      new WaterfallDialog<State>(REVIEW_PRODUCT_OPTIONS_LOOP, [
        this.init.bind(this),
        this.selectionStep.bind(this),
        this.loopStep.bind(this),
      ]),
    );

    this.addDialog(choicePrompt);
  }

  private init = async (step: WaterfallStepContext<State>) => {
    const state = await this.stateAccessor.get(step.context);
    if (!this.productResult) this.productResult = state.search.productResult;
    return await step.next();
  };

  private selectionStep = async (step: WaterfallStepContext<State>) => {
    const item = this.productResult.shift();

    const productOptions: Partial<Activity> = MessageFactory.carousel(
      item.map((p: Product) =>
        CardFactory.heroCard(
          p.productName,
          'some text',
          [p.image || ''],
          [
            {
              type: ActionTypes.ImBack,
              title: 'buy',
              value: p.id,
            },
          ],
        ),
      ),
      'Choose a product',
    );

    return await step.prompt(PRODUCTS_CAROUSEL, {
      prompt: productOptions,
      choices: item.map((p: Product) => p.id),
    });
  };

  private loopStep = async (step: WaterfallStepContext<State>) => {
    console.log('step.result: ', step.result);
  };
}

PARENT DIALOG BELOW:

...

this.addDialog(new ProductSelectionDialog(stateAccessor));

...

if (search.hasIncompletedProducts) await step.beginDialog(ProductSelectionDialog.Name);

...

return await step.next();

...

MY BOT DIALOG STRUCTURE

onTurn()
>>> await this.dialogContext.beginDialog(MainSearchDialog.Name) (LUIS)
>>>>>> await step.beginDialog(QuoteDialog.Name)
>>>>>>>>> await step.beginDialog(ProductSelectionDialog.Name)

UPDATE

Replacing the ChoicePrompt with TextPromt (as suggested by Kyle Delaney) seems to have the same result (do not go to the next step) but I realised that if remove return from the prompt like this:

return await step.prompt(PRODUCTS_CAROUSEL, `What is your name, human?`); TO await step.prompt(PRODUCTS_CAROUSEL, `What is your name, human?`);

it does work but when I'm returning the original code with ChoicePrompt without return like this:

await step.prompt(PRODUCTS_CAROUSEL, {
  prompt: productOptions,
  choices: item.map((p: Product) => p.id),
});

I'm getting another error in the framework:

error:  TypeError: Cannot read property 'length' of undefined
    at values.sort (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/choices/findValues.js:84:48)
    at Array.sort (native)
    at Object.findValues (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/choices/findValues.js:84:25)
    at Object.findChoices (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/choices/findChoices.js:58:25)
    at Object.recognizeChoices (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/choices/recognizeChoices.js:75:33)
    at ChoicePrompt.<anonymous> (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/prompts/choicePrompt.js:62:39)
    at Generator.next (<anonymous>)
    at /xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/prompts/choicePrompt.js:7:71
    at new Promise (<anonymous>)
    at __awaiter (/xxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/prompts/choicePrompt.js:3:12)

this is the line:

    // Sort values in descending order by length so that the longest value is searched over first.
    const list = values.sort((a, b) => b.value.length - a.value.length);

I can see the data from my state is coming properly prompt: <-- the data is ok choices: <-- the data is ok too

Sometimes I'm getting this error too:

error:  TypeError: Cannot read property 'status' of undefined
    at ProductSelectionDialog.<anonymous> (/xxxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/componentDialog.js:92:28)
    at Generator.next (<anonymous>)
    at fulfilled (/xxxx/Workspace/temp/13.basic-bot/node_modules/botbuilder-dialogs/lib/componentDialog.js:4:58)
    at <anonymous>
    at process._tickDomainCallback (internal/process/next_tick.js:228:7)

this line

            // Check for end of inner dialog
            if (turnResult.status !== dialog_1.DialogTurnStatus.waiting) {

Solution

  • You're using a ChoicePrompt, but when you call prompt you're only passing through an activity (the carousel). ChoicePrompt is going to try to validate the input against a set of choices that you should be passing in when you call prompt. Because you're not doing this, the prompt is not recognizing the post back value as valid and technically should be reprompting you with the carousel again to make a valid choice.

    The fix here should be to call prompt with PromptOptions instead of just a raw Activity and set the choices of the PromptOptions to an array that contains all the values you expect back (e.g. the same value you set for the value of the post back button).

    This should end up looking a little something like this:

    Since you're providing the choices UX with your cards, you want to set the ListStyle on the ChoicePrompt to none

    const productsPrompt = new ChoicePrompt(PRODUCTS_CAROUSEL);
    productsPrompt.style = ListStyle.none;
    
    this.addDialog(productsPrompt);
    

    Then, set the available choices for the specific prompt:

    return await step.prompt(PRODUCTS_CAROUSEL, {
          prompt: productOptions,
          choices: items.map((p: Product) => p.id),
      });