BotFramework-WebChat - Адаптивная карточка

Есть ли способ добавить событие Onchange в поле ввода адаптивной карты, которое отображается в веб-чате (версия V4). Пример изменения значения количества (поле ввода адаптивной карты для номера типа) на экране оформления заказа должно обновить общее значение (текстовое поле адаптивной карты)

Для простоты.... На изображении ниже после того, как я изменю число в поле ввода, оно должно обновиться в текстовом поле ниже. все должно происходить на стороне клиента webchat V4(React)

AdaptiveCard

Ниже приведены варианты, которые я пробовал, здесь нет кода для отправки:

option1: Пытался добавить событие в поле ввода количества на карточке, исходящей от бота, с использованием промежуточного программного обеспечения, но не смог найти вариант однозначной идентификации поля ввода для добавления события (можно увидеть несколько полей ввода на основе отсутствия элементов в карта)

option2: создать новую карточку в интерфейсе на основе карточки, полученной от бота, и добавить события к этой новой карточке. Можно ли прервать сообщение боту и отправить карточку из интерфейса?

option3: добавить кнопку обновления на карту, чтобы общая сумма вычислялась в бэкэнде, а карта обновления отправлялась пользователю

Ниже приведена полезная нагрузка:

{
    "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
    "version": "1.0",
    "type": "AdaptiveCard",
    "body": [
        {
            "type": "TextBlock",
            "text": "Output",
            "weight": "Bolder",
            "horizontalAlignment": "Center",
            "size": "Large",
            "id": "output",
            "color": "Good"
        },
        {
            "type": "Container",
            "items": [
                {
                    "$data": "{items}",
                    "type": "Container",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": " ",
                            "id": "line",
                            "spacing": "None"
                        },
                        {
                            "type": "Image",
                            "altText": "",
                            "id": "myimage",
                            "url": "{imgUrl}",
                            "spacing": "None",
                            "size": "Stretch",
                            "width": "1000px",
                            "height": "100px"
                        },
                        {
                            "type": "ColumnSet",
                            "id": "imgset",
                            "columns": [
                                {
                                    "type": "Column",
                                    "width": 50,
                                    "id": "desc",
                                    "items": [
                                        {
                                            "type": "TextBlock",
                                            "text": "{description}",
                                            "weight": "Bolder",
                                            "spacing": "None",
                                            "id": "desc",
                                            "wrap": true,
                                            "maxLines": 4
                                        }
                                    ],
                                    "spacing": "None"
                                }
                            ],
                            "spacing": "None"
                        },
                        {
                            "type": "ColumnSet",
                            "spacing": "None",
                            "columns": [
                                {
                                    "type": "Column",
                                    "width": 50,
                                    "id": "qty",
                                    "items": [
                                        {
                                            "type": "Input.Number",
                                            "placeholder": "Quantity",
                                            "id": "myquantity",
                                            "min": 0,
                                            "max": 100,
                                            "value": "{quantity}",
                                            "spacing": "None"
                                        }
                                    ],
                                    "horizontalAlignment": "Left",
                                    "verticalContentAlignment": "Center",
                                    "spacing": "None"
                                },
                                {
                                    "type": "Column",
                                    "id": "pricec",
                                    "items": [
                                        {
                                            "type": "TextBlock",
                                            "text": "{price}",
                                            "id": "pricet",
                                            "horizontalAlignment": "Right",
                                            "spacing": "None"
                                        }
                                    ],
                                    "verticalContentAlignment": "Center",
                                    "horizontalAlignment": "Right",
                                    "width": 50,
                                    "spacing": "None"
                                }
                            ],
                            "id": "qtypset"
                        },
                        {
                            "type": "ColumnSet",
                            "spacing": "None",
                            "columns": [
                                {
                                    "type": "Column",
                                    "width": 1,
                                    "items": [
                                        {
                                            "type": "TextBlock",
                                            "text": "Sub Total",
                                            "size": "Medium",
                                            "id": "subtotal00",
                                            "weight": "Bolder",
                                            "spacing": "None"
                                        }
                                    ],
                                    "id": "subtotal1",
                                    "spacing": "None"
                                },
                                {
                                    "type": "Column",
                                    "width": 1,
                                    "items": [
                                        {
                                            "type": "TextBlock",
                                            "horizontalAlignment": "Right",
                                            "text": "{subtotal}",
                                            "size": "Medium",
                                            "weight": "Bolder",
                                            "id": "subtotalt0",
                                            "color": "Accent",
                                            "spacing": "None"
                                        }
                                    ],
                                    "id": "subtotal200",
                                    "spacing": "None"
                                }
                            ],
                            "id": "colsetsubtot00"
                        }
                    ],
                    "id": "itemcontainer",
                    "style": "emphasis",
                    "spacing": "None"
                }
            ],
            "id": "rootcontainer",
            "style": "accent"
        },
        {
            "type": "ColumnSet",
            "id": "totalset",
            "columns": [
                {
                    "type": "Column",
                    "width": 50,
                    "id": "totalcolumn",
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "Total",
                            "size": "Medium",
                            "isSubtle": true,
                            "weight": "Bolder",
                            "id": "total",
                            "color": "Dark"
                        }
                    ]
                },
                {
                    "type": "Column",
                    "width": 50,
                    "items": [
                        {
                            "type": "TextBlock",
                            "text": "{total}",
                            "size": "Medium",
                            "id": "totaltext",
                            "horizontalAlignment": "Right",
                            "weight": "Bolder",
                            "color": "Accent"
                        }
                    ],
                    "id": "totalcol2"
                }
            ]
        }
    ],
    "id": "final"
}

Я использую приведенный ниже пример в качестве отправной точки https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/e.piping-to-redux

webchat.js:

import React from 'react';

import ReactWebChat, { createDirectLine, createStore } from 'botframework-webchat';
import directLineDisconnect from 'botframework-webchat-core/lib/actions/disconnect';
import dispatchIncomingActivityMiddleware from './dispatchIncomingActivityMiddleware';
import uuid from 'uuid';

export default class extends React.Component {
  constructor(props) {
    super(props);

    this.store = createStore({}, dispatchIncomingActivityMiddleware(props.appDispatch, this));
    this.activityMiddleware = this.setActivityMiddleware();
    this.attachmentMiddleware = this.setAttachmentMiddleware();

    this.state = {};

  }

  componentDidMount() {
    this.fetchToken();
    this.setSendBox();
  }

  componentWillUnmount(){

  }

  async fetchToken() {
    const myHeaders = new Headers();
    const userDetails = uuid.v4();
    myHeaders.append('Authorization', 'Bearer ' + 'mytoken'); 
    myHeaders.append('Content-type', 'application/json');
    const res = await fetch('https://directline.botframework.com/v3/directline/tokens/generate', { 
                        body: JSON.stringify({ user: { id: userDetails, name: userDetails }}),
                        method: 'POST', headers: myHeaders });
    const { token } = await res.json();
    console.log("My Token: " + token);
    this.setState(() => ({
      directLine: createDirectLine({ token })
    }));
  }

  setActivityMiddleware(){
    return () => next => card => {
      return children => (
        <div
          className={card.activity.attachments && (card.activity.attachments[0].content.id === "output") ? card.activity.attachments && card.activity.attachments[0].content.id : ''}
        >
          {next(card)(children)}
        </div>
      );
    };

  }


  setAttachmentMiddleware(){
    return () => next => ({ card, activity, attachment: baseAttachment }) => {
      let attachment = baseAttachment;
      if (baseAttachment.content.body){
      switch (baseAttachment.content.body[0].id) {
        case 'review':                   
         for (let i = 0; i < attachment.content.body[1].items.length; i++) {
         attachment.content.body[1].items[i].items[3].columns[0].items[0].value = baseAttachment.content.body[1].items[i].items[3].columns[0].items[0].value.toString();
                                                                           } //for loop
         break;

         default:
           break;
        }
    }
    return next({ card, activity, attachment });
    };

  }

  setSendBox() {

    this.store.dispatch({
      type: 'WEB_CHAT/SET_SEND_BOX',
      payload: { text: 'sample:redux-middleware' }
    });
/*

    this.store.dispatch({
      type: 'WEB_CHAT/SEND_EVENT',
      payload: { name: 'membersAdded',
                 value: { language: window.navigator.language }
               }  
    }); */
  }


  render() {
    return this.state.directLine ? (
      <ReactWebChat
        activityMiddleware={this.activityMiddleware}
        attachmentMiddleware={this.attachmentMiddleware}
        directLine={this.state.directLine}
        store={this.store}
        styleOptions={{
          backgroundColor: 'Transparent',
          hideUploadButton: true
        }}
      />
    ) : (
      <div>Connecting to bot&hellip;</div>
    );
  }

}

dispatchIncomingActivityMiddleware.js:

export default function(dispatch, thisvariable) {
    return () => next => action => {
      if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
        const { activity } = action.payload;


        if (activity.from.role === 'bot'){
        var inputBox=document.getElementsByClassName("css-eycyw2");
        if (inputBox.length > 0){
          inputBox[inputBox.length - 1].style.display='block';
        }
                                          }

      }


      if ((action.type === 'WEB_CHAT/SEND_POST_BACK') || (action.type === 'WEB_CHAT/SEND_MESSAGE')) { 
        var inputBox=document.getElementsByClassName("css-eycyw2");
        if (inputBox.length > 0){
          inputBox[inputBox.length - 1].style.display='none';
          dispatch(setInputVisibility(true));
        }
      }

      return next(action);
    };
  }

1 ответ

Решение

Первое, что нужно понять, это то, что веб-чат использует SDK Adaptive Cards JavaScript, доступный в виде пакета npm. Веб-чат в основном использует готовые к работе функции визуализации SDK, но он меняет одну важную вещь - способ обработки действий. Без настраиваемого обработчика действия отправки не будут отправляться боту.

adaptiveCard.onExecuteAction = handleExecuteAction;

Вот как приложения должны использовать адаптивные карты. Хотя большая часть функций обрабатывается на стороне SDK, есть несколько вещей, которые приложение должно сделать, чтобы адаптивные карты работали для этого конкретного приложения. Хотя вы можете видеть, что веб-чат назначает функциюonExecuteAction "event" свойство конкретного экземпляра Adaptive Card, существует также статический аналог onExecuteAction к которому можно получить доступ так:

AdaptiveCard.onExecuteAction = handleExecuteAction;

Использование статического события применит обработчик для всех адаптивных карточек, а не только для одной, но он будет переопределен любыми обработчиками, примененными к конкретным экземплярам. Причина, по которой я говорю вам это, заключается в том, что существует гораздо больше статических событий, и некоторые из них, в частности, будут полезны в вашей ситуации:

static onAnchorClicked: (element: CardElement, anchor: HTMLAnchorElement) => boolean = null;
static onExecuteAction: (action: Action) => void = null;
static onElementVisibilityChanged: (element: CardElement) => void = null;
static onImageLoaded: (image: Image) => void = null;
static onInlineCardExpanded: (action: ShowCardAction, isExpanded: boolean) => void = null;
static onInputValueChanged: (input: Input) => void = null;
static onParseElement: (element: CardElement, json: any, errors?: Array<HostConfig.IValidationError>) => void = null;
static onParseAction: (element: Action, json: any, errors?: Array<HostConfig.IValidationError>) => void = null;
static onParseError: (error: HostConfig.IValidationError) => void = null;
static onProcessMarkdown: (text: string, result: IMarkdownProcessingResult) => void = null;

Вы могли бы придумать решение, использующее onInputValueChangedсобытие, которое срабатывает каждый раз при изменении любого ввода в карточке. Ваш обработчик может искать в карточке другие элементы, которые ему необходимо использовать в качестве операндов для вычисления, а также ему нужно будет искать на карточке элемент, который отобразит результат. Вместо того, чтобы выполнять всю эту работу каждый раз при вводе символа, я предпочитаю решение, которое выполняет поиск на карточке только один раз в начале в поисках элементов, которые она будет использовать при вычислении. Альтернативой прослушиванию событий в классе Adaptive Card или экземпляре Adaptive Card является прослушивание событий в определенных элементах, таких как входы. Итак, в моем примере будет использоваться статическийonParseElement событие, чтобы получить необходимые элементы, а затем использовать onValueChanged событие для определенных экземпляров ввода, которые он находит.

Перед написанием кода для обработчика нам нужно придумать способ, чтобы код знал, какие элементы использовать для операндов и результат вычисления. Например, вы можете просто заставить код объединить каждый ввод в карточке (или в контейнере) и поместить результат в последний найденный текстовый блок. В моем примере я придумал схему именования, которую может использовать код. Есть два ключевых слова, "всего" и "цена", и код ищет их в каждом идентификаторе элемента. Я хочу прояснить, что эта схема абсолютно произвольна и что вы можете сделать что-то другое, если хотите. Вот мой пример карты:

{
  "type": "AdaptiveCard",
  "version": "1.0",
  "body": [
    {
      "type": "TextBlock",
      "text": "$10.00",
      "id": "foo_a_price"
    },
    {
      "type": "Input.Text",
      "id": "foo_a"
    },
    {
      "type": "TextBlock",
      "text": "$2.00",
      "id": "foo_b_price"
    },
    {
      "type": "Input.Text",
      "id": "foo_b"
    },
    {
      "type": "TextBlock",
      "text": "total",
      "id": "total_foo"
    }
  ],
  "actions": [
    {
      "type": "Action.Submit",
      "title": "Submit"
    }
  ]
}

Глядя на это, вы могли догадаться, что идея состоит в том, чтобы один текстовый блок имел идентификатор, который начинается с "total_" и имеет некоторый идентификатор после него. Количества, которые вы хотите сложить, начинаются с того же идентификатора, и цена, которую вы хотите умножить на каждое количество, имеет тот же идентификатор, что и количество, но с суффиксом "_price". Я рекомендую использовать числовые вводы вместо ввода текста, но этот пример показывает, что текст по-прежнему работает. А вот код моего примера приложения, которое читает схему:

import * as adaptiveCardsPackage from 'adaptivecards';

adaptiveCardsPackage.AdaptiveCard.onParseElement = element => {
  const PREFIX_TOTAL = 'total_';
  const SUFFIX_PRICE = '_price';

  if (element.id && element.id.startsWith(PREFIX_TOTAL)) {
    const itemPrefix = element.id.slice(PREFIX_TOTAL.length);
    const card = element.getRootElement();
    const inputs = card.getAllInputs().filter(input => input.id.startsWith(itemPrefix));
    const products = {};

    for (const input of inputs) {
      const priceElement = card.getElementById(input.id + SUFFIX_PRICE);
      const price = Number(priceElement.text.replace(/[^0-9.-]+/g, '')) || 0;

      // `sender` will be the same as `input`.
      // You could capture the input const instead of using the argument,
      // but I'm demonstrating that you don't need to.
      input.onValueChanged = sender => {
        const quantity = Number(sender.value) || 0;

        products[sender.id] = price * quantity;

        const sum = Object.values(products).reduce((a, b) => a + b);

        element.setText("$" + sum.toFixed(2));
        element.renderedElement.replaceWith(element.render());
      };
    }
  }
};

У меня есть основания полагать, что это изменение AdaptiveCard класс будет автоматически применен к AdaptiveCardclass в пакете, который импортирует Web Chat, поскольку это тот же класс в том же пакете. Однако теперь веб-чат позволяет вам предоставить свой собственный пакет адаптивных карточек в качестве свойства, поэтому вы можете убедиться, что веб-чат использует этот пакет с вашим специальным обработчиком событий:

<ReactWebChat
  directLine={createDirectLine({secretOrToken})}
  adaptiveCardsPackage={adaptiveCardsPackage}
/>

Самостоятельно обновляемая карта

Другие вопросы по тегам