Google Chat Bot. Пов’язані діалоги

Мене вже не зупинити 😅, тож це вже третя стаття про розробку Google Chat ботів.

В цій частині я розповім як робити пов’язані діалоги при комунікації з ботом.

Завдяки попередній частині ми вже опанували навичку створення діалогу та навчились реагувати на дії користувачів. Тож настав час трошки розширити цей функціонал — ми навчимось реагувати та визивати пов’язані діалоги.

Пов’язані діалоги

Пов’язані діалоги — це коли ми на команду користувача відкриваємо діалог, і далі, реагуючи на його дії у діалозі, знов відкриваємо наступний діалог, і так далі, поки не отримаємо все що хотіли. Таким чином, ми можемо створювати ієрархію діалогів, наче це якийсь багатошаровий додаток.

Але давайте спочатку подивимось, як наразі працює бот за версією 2.0.0, ось тут приведу діаграму послідовності:

#
# https://sequencediagram.org/
#
title Dialogs

actor "User" as U
materialdesignicons F0822 "Chat" as C
materialdesignicons F167A "Apps Script" as S

U->C:/command from chat
C->S:event: slash command ID
S->C:"DIALOG"

note over U,C: Description              [ input field ]\n\nImage URL 1,2,3,... [ input field ]\n\n                                [  Send   ]

C->S:event: form inputs
S->C:"NEW_MESSAGE"

note over C: + ––––––––– +\n |     image      | \n+ ––––––––– +\n\nDescription...

Працює, то не лізь ☝️🧐

Тож я хочу змінити поведінку бота, хочу додати додатковий крок, щоб перед тим як відправити картку до чату була можливість попередньо подивитись як буде виглядати картка, та за потреби внести зміни до картки.

Функціонал /card в нас вже є у файлі slashCard.gs, давайте внесемо зміни до нього, щоб в нас була визвана інша функція:

/**
 * Opens a dialog in Google Chat.
 *
 * @param {Object} event the event object from Chat API.
 *
 * @return {object} open a Dialog in Google Chat.
 */
function slashCard (event) {
  return {
    'action_response': {
      'type': RESPONSE_TYPE_DIALOG,
      'dialog_action': {
        'dialog': {
          'body': {
            'sections': [
              {
                'header': 'Card Builder',
                'collapsible': true,
                'uncollapsibleWidgetsCount': 2,
                'widgets': [
                  /* .. cropped .. */
                ]
              }
            ],
            'fixedFooter': {
              'primaryButton': {
                'icon': {
                  'materialIcon': {
                    'name': 'send'
                  }
                },
                'text': 'Preview',
                'onClick': {
                  'action': {
                    'function': 'openCard'
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Також додамо підтримку openCard у функції onCardClick():

/**
 * Responds to a CARD_CLICKED event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onCardClick(event) {
  switch (event.common.invokedFunction) {
    case 'openCard':
      return openCard(event)
    case 'receiveCard':
      return receiveCard(event)
  }
}

Для функції openCard створимо окремий файл openCard.gs, більша частину коду цієї функції взята з receiveCard.gs з попереднього уроку, відмінність полягає у відповіді яку ми надсилаємо:

/**
 * @param {Object} event the event object from Google Chat
 */
function openCard(event) {

  //
  // ... the code was cropped
  //

  return {
    'action_response': {
      'type': RESPONSE_TYPE_DIALOG, // 'DIALOG'
      'dialog_action': {
        'dialog': {
          'body': {
            'sections': [
              {
                'header': 'Card Builder',
                'collapsible': false,
                'widgets': widgets
              }
            ],
            'fixedFooter': {
              'primaryButton': {
                'text': 'Send'
              },
              "secondaryButton": {
                "text": "Edit"
              }
            }
          }
        }
      }
    }
  }
}

Так, все просто, для того щоб в нас відкрився наступний діалог то треба знов повертати DIALOG у якості action_response:type.
Але це ще не всі зміни, крім цього, я ще додав елемент fixedFooter з двома кнопками primaryButton з текстом Send та secondaryButton з текстом Edit. За моїм задумом, кнопка Send буде відправляти картку до спейсу, а кнопка Edit – поверне нас на попередній крок, щоб ми могли внести зміни до тексту або посилань на зображення.

Але тут є нюанс, нам ще треба якось текст та посилання якось передати до тих функцій.

Передача параметрів

Почнемо з того, що додаємо обробник події onclick до кнопок, та будемо визивати функції які в нас вже є, це slashCard, яка відповідає за діалог з формою, та receiveCard, це функція яка відповідає за публікацію картки до спейсу:

/**
 * @param {Object} event the event object from Google Chat
 */
function openCard(event) {

  //
  // The code was cropped
  //

  return {
    'action_response': {
      'type': RESPONSE_TYPE_DIALOG, // 'DIALOG'
      'dialog_action': {
        'dialog': {
          'body': {
            'sections': [
              {
                'header': 'Card Builder',
                'collapsible': false,
                'widgets': widgets
              }
            ],
            'fixedFooter': {
              'primaryButton': {
                'text': 'Send',
                'onClick': {
                  'action': {
                    'function': 'receiveCard',
                    'parameters': [ /* ??? */ ]
                  }
                }
              },
              "secondaryButton": {
                "text": "Edit",
                "onClick": {
                  "action": {
                    "function": "slashCard",
                    'parameters': [ /* ??? */ ]
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Для того щоб передати до тих функцій параметри, треба передавати масив параметрів у вигляді ключ та значення:

// ...
"onClick": {
  "action": {
     // Specifies which function to run
     // in response to the card click.
     "function": "someFunctionName",
       "parameters": [
         {
            "key": "some key",
            "value": "some value"
         }
       ]
  }
}
// ...                     

Тож для кнопки Edit з визовом функції slashCard це може виглядати наступним чином:

// ...
"secondaryButton": {
  "text": "Edit",
  "onClick": {
     "action": {
        "function": "slashCard",
        "parameters": [
           {
             "key": "description",
             "value": formHandler.getTextValue('description')
           },
           {
             "key": "image_1",
             "value": formHandler.getTextValue('image_1')
           },
           {
             "key": "image_2",
             "value": formHandler.getTextValue('image_2')
           },
           // etc.
       ]
     }
  }
}
// ...

Якщо зараз спробувати визвати команду /card, внесті дані та відправити, то ви вже можете подивитися як виглядає попередній перегляд картки:

Якщо спробуєте скористатися кнопкою Edit то отримаєте лише помилку, бо вам ще треба додати обробку функції slashCard до onCardClick:

/**
 * Responds to a CARD_CLICKED event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onCardClick(event) {
  switch (event.common.invokedFunction) {
    case 'slashCard':
      return slashCard(event)
    case 'openCard':
      return openCard(event)
    case 'receiveCard':
      return receiveCard(event)
  }
}

Після цього ще треба внести зміни до функції slashCard, щоб спіймати передані параметри та додати їх до діалогу:

/**
 * Opens a dialog in Google Chat.
 *
 * @param {Object} event the event object from Chat API.
 *
 * @return {object} open a Dialog in Google Chat.
 */
function slashCard (event) {

  const parameters = event.common.parameters

  return {
    'action_response': {
      'type': RESPONSE_TYPE_DIALOG,
      'dialog_action': {
        'dialog': {
          'body': {
            'sections': [
              {
                'header': 'Card Builder',
                'collapsible': true,
                'uncollapsibleWidgetsCount': 2,
                'widgets': [
                  {
                    'textInput': {
                      'name': 'description',
                      'type': 'MULTIPLE_LINE',
                      'label': '📝 Description',
                      'value': parameters ? (parameters['description'] || '') : ''
                    }
                  },
                  {
                    'textInput': {
                      'name': 'image_1',
                      'label': '1️⃣ Image URL',
                      'value': parameters ? (parameters['image_1'] || '') : '',
                      'placeholderText': 'https://source.unsplash.com/featured/320x320'
                    }
                  },
                  {
                    'textInput': {
                      'name': 'image_2',
                      'label': '2️⃣',
                      'value': parameters ? (parameters['image_2'] || '') : ''
                    }
                  },
                  {
                    'textInput': {
                      'name': 'image_3',
                      'label': '3️⃣',
                      'value': parameters ? (parameters['image_3'] || '') : ''
                    }
                  }
                ]
              }
            ],
            'fixedFooter': {
              'primaryButton': {
                'icon': {
                  'materialIcon': {
                    'name': 'send'
                  }
                },
                'text': 'Preview',
                'onClick': {
                  'action': {
                    'function': 'openCard'
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Після цих змін функціонал кнопки Edit повинен працювати як слід, спробуйте.

Повернемося до функції openCard, нам треба внести зміни до обробника onClick, щоб передавати параметри до функції receiveCard, це ті самі параметри, як ми передавали в прикладі вище до обробника кнопки Edit:

"primaryButton": {
  "text": "Send",
  "onClick": {
     "action": {
        "function": "receiveCard",
        "parameters": [
           {
             "key": "description",
             "value": formHandler.getTextValue('description')
           },
           {
             "key": "image_1",
             "value": formHandler.getTextValue('image_1')
           },
           {
             "key": "image_2",
             "value": formHandler.getTextValue('image_2')
           },
           // etc.
       ]
     }
  }
}

Тепер слід внести зміни до функції receiveCard яку ми розробляли у попередньому уроці, там теж треба внести трішки змін, та замість обробки formInputs нам треба брати дані з event.common.parameters:

/**
 * @param {Object} event the event object from Google Chat
 */
function receiveCard(event) {

  const parameters = event.common.parameters

  let description = parameters['description']

  let images = [
    parameters['image_1'],
    parameters['image_2'],
    parameters['image_3'],
    parameters['image_4'],
    parameters['image_5'],
  ]

  images = images.filter(n => n)

  //
  // ... the code was cropped
  //
}

Тепер в нас все працює як слід:

  1. за командою /card відкривається діалог для створення картки slashCard()
  2. коли ми ввели дані до форми, та нажали кнопку Preview, то в нас буде визвана функція openCard()
  3. якщо ми нажимаємо Edit, то повертаємось на попередній крок до функції slashCard()
  4. якщо ми нажимаємо Send, то ми викличем функцію receiveCard()

Діаграма послідовності

Тепер подивіться як змінилась наша діаграма:

#
# https://sequencediagram.org/
#
title Sequential dialogs

actor "User" as U
materialdesignicons F0822 "Chat" as C
materialdesignicons F167A "Apps Script" as S

U->C:/command from chat
C->S:event: slash command ID
S->C:"DIALOG"

note over U,C: Description              [ input field ]\n\nImage URL 1,2,3,... [ input field ]\n\n                                [  Preview   ]

C->S:event: form inputs
S->C:"DIALOG"

note over U,C:+ ––––––––– +\n |     image      | \n+ ––––––––– +\n\nDescription...\n\n[ Edit ]   [ Send ]

C-->S:event: invoked function "edit"\n with common parameters
C->S:event: invoked function "send"\n with common parameters
S->C:"NEW_MESSAGE"

note over C: + ––––––––– +\n |     image      | \n+ ––––––––– +\n\nDescription...

Домашнє завдання

Так, ну і щоб краще запам’ятовувалося, давайте зробимо ще один приклад пов’язаного діалогу — будемо формувати Meeting Minutes, щоб не губилися у вашому спейсі, та виділялись такі повідомлення:

Для цього нам треба буде додати ще одну slash-команду /notes, та спіймати її у onMessage функції:

/**
 * Responds to a MESSAGE event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onMessage(event) {
  if (event.message.slashCommand) {
    switch (event.message.slashCommand.commandId) {
      //
      // ...
      //
      case 21:
        return slashNotes(event)
    }
  }
  //
  // ...
  //
}

І після цього реалізувати наступні функції:

  1. slashNotes() — буде відповідати за форму створення та редагування
  2. openNotes() — діалог для попереднього перегляду повідомлення
  3. receiveNotes() — буде відповідати за публікацію картки до спейсу

Весь код я вже не буду приводити на блозі, його ви знайдете на GitHub.

Вам також слід поцікавитись стосовно деяких змін в коді:

  • навіщо потрібна функція getParameters()?
  • що за функція лежить у файлі helpers.gs, та яку проблему вона повинна вирішувати

Source Code

Код бота доступний на GitHub, реліз 3.0.0 відповідає коду з цієї статті.

Bender, реліз 3.0.0
Bender, білд 3.0.0

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.