10. Web UI

Web-интерфейсы, пользовательские компоненты и визуальная интеграция с платформой.

Web-формы в чат-боте и Web Apps

Web-формы в чат-боте и Web Apps

Введение. Виды форм. Принцип работы.

Теоретическая часть для разработчика сайта (разработчика HTML-формы) и для разработчика бота.

Веб-формы расширяют канал коммуникаций с ботом и позволяют в текстовом боте использовать все возможности HTML и JavaScript, таким образом бот, по функционалу становится полноценной заменой любому веб-сайту.  Бот теперь не ограничивает пользователя в коммуникациях только обменом текстом, аудио или файлами. 

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

Разработка веб-формы, расширяющей диалоговую коммуникацию происходит в несколько шагов:

  1. Перед началом разработки необходимо выбрать один из вариантов реализации формы и согласовать с разработчиком веб-формы и разработчиком чат-бота (если форма и чат-бот разрабатываются параллельно). Также необходимо согласовать формат обмена данными. 
  2. После того как вы определились с видом формы, необходимо разработать HTML-форму, разместить его на вашем сервере, подключить ее к backend вашего сайта (для обработки AJAX / API запросов), чтобы backend мог принять данные с формы и отправить их в API в Metabot. 

Для разработчика формы: реализация любого вида формы не имеет принципиальных архитектурных отличий, одна и также веб-форма может использоваться для любого вида формы путем добавления не сложных разветвлений в JS-коде веб-формы. Отличие только в том, что вариант с keyboad-кнопкой не требует дополнительного слоя для backend, но при этом имеет ряд ограничений, подробнее см в описании этого вида формы ниже. Мы рекомендуем использовать вариант Web App с inline-кнопокй + универсальную форму для любого мессенджера.

Для разработчика бота: см отдельную страницу документации для реализации необходимого вида формы в боте.

Дополнительное изучение возможностей работы Telegram Web Apps.

Универсальная форма в виде ссылки

Ключевая особенность: универсальный вариант, подходит для любого мессенджера.

Форма открывается в отдельной странице браузера. Как обычная страница web-браузера.

Для разработчика бота: вы можете добавить в боте условия, проверяя текущий мессенджер лида, чтобы открывать для нативного Telegram-канала форму с помощью inline кнопки, а для других каналов отправлять ссылку на форму в виде сообщения. Учтите, что в других мессенджерах нельзя удалять сообщения, поэтому заранее предусмотрите время жизни хэш-кода лида который указывается в ссылке на форму

Для Telegram ссылку можно отправить в виде inline-кнопки, но это все равно будет не "Web App приложение", а обычная страница открытая в браузере.

Если в мессенджере открытие ссылок во внешнем браузере выключено то форма откроется в браузере мессенджера, но все равно работа с данным видом формы будет отличаться от формы в виде Web App для inline/keyboard кнопки. При этом пользователю все равно доступна возможность позволяющая открыть ссылку во внешнем браузере. После закрытия формы нет гарантий, что пользователь вернется в браузер, поэтому желательно прислать пользователю уведомление в бота, чтобы он нажал на него и вернулся в бота.

Если вы планируете использовать формы только в Telegram, то можете пропустить данный вид формы и перейти к описанию формы на основе Web App, открывающейся с помощью inline-кнопки. Но, желательно понимать отличия и изучить весь раздел документации.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в мессенджер в виде обычного сообщения.

  3. Пользователь бота нажимает на ссылку, открывается браузер с формой.

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. Форма отправляет данные на backend сайта, где размещена форма.
  6. Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
  7. Metabot принимает и сохраняет данные заполненной формы .
  8. Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).

Пример JS кода для открытия ссылки на Telegram бота и закрытия страницы

  <script>
    function closeForm() {
        location.href="tg://resolve?domain=metabot_test_form_bot";
        window.close();
    }
  </script>

После заполнения формы, данные введенные пользователем необходимо отправить на бэк вашего сайта, а затем в Metabot API используя токен бота (чтобы токен бота не фигурировал в коде HTML-формы). Далее страницу браузера необходимо автоматически закрыть, с помощью JS-кода встраиваемого в HTML-форму. Рекомендуется отправлять данные на бэк с помощью REST API, для того чтобы после отправки не выполнять дополнительный рендеринг страницы (чтобы потом ее просто закрыть). Т.е. отправляем данные на бэк, убеждаемся что ответ от API - 200, т.е. все ОК и сразу закрываем форму, пользователь возвращается в бота. При этом желательно учесть в коде HTML-формы варианты, если что-то пошло не так, или пользователь остался на форме, а хэш код лида для формы истек (если вы генерируете токен со сроком действия), в этих случаях можно вывести сообщение о том что форма устарела, что пользователю необходимо вернуться в бота и запросить форму повторно, также дополнительно можно уведомить Metabot по API, чтобы бот автоматически выслал ссылку на новую форму.

Есть нюанс: перед закрытием формы можно вызвать открытие ссылки для перехода в мессенджер (на случай если после закрытия страницы не будет открывается мессенджер). Но, для PC, например, открытие ссылки с Telegram ботом не срабатывает автоматически и у пользователя остается открытой другая вкладка в браузере (если у него включено открытие ссылок мессенджера во внешнем браузере и в браузере открыто несколько вкладок).

Cсылка может быть отправлена в бота в виде простого текста или inline-кнопки Telegram, cодержащей ссылку на форму. Но в любом случае, для рассматриваемого варианта формы, нажатие на ссылку или кнопку приведет к открытию странице в браузере и после заполнения формы страницу необходимо автоматически закрывать. Здесь речь идет именно об обычной inline-кнопке c ссылкой для Telegram, а не о Telegram WebApp-форме, на основе inline-кнопки которая открывает форму в Web App (во всплывающем окне, на котором расположен WebView). Описание вариантов форм с WebApp смотрите ниже.

Информация для разработчика бота:

Отличие между Inline Web App Button и обычной inline кнопки в виде ссылки заключается в том, что Inline Web App Button формируется передачей параметров web_app и url а для обычная inline-кнопка в виде ссылки просто передачей параметра url. Детали можно найти в описании API Telegram.

https://core.telegram.org/bots/webapps#inline-button-web-apps
https://core.telegram.org/bots/api#inlinekeyboardbutton
https://core.telegram.org/bots/api#inlinekeyboardmarkup
https://core.telegram.org/bots/api#sendmessage

Inline кнопка с ссылкой для Telegram реализуется на основе JS Callback команды бота и метода bot.sendMessage или с помощью плагина Common.TelegramComponents.MenuHelper.sendMessage с передачей дополнительных параметров для работы inline-кнопки как ссылки.

Отличие bot.sendMessage от плагина Common.TelegramComponents.MenuHelper.sendMessage в том что функция плагина сама внутри вызывает bot.sendMessage, и при этом автоматически сохраняет ID последних сообщений в атрибутах лида. Этот атрибут нужен для использования в коде JS Callback, а также маршруте бота, если требуется удаление кнопки-ссылки на форму или самого сообщения с ссылкой. Функционал хранения идентичен работе с Фото-слайдером для Telegram (описано в документации по работе с командой JS Callback и файлами Telegram).

Команда JS callback необходима для обработки нажатия других кнопок которые могут быть отправлены вместе с кнопкой-ссылкой на форму или для fallback, если форму не заполнили, но отправили любой текст в мессенджер. 

Команда бота JS Callback не должна в данном случае обрабатывать событие отправки формы. Т.к. событие отлавливается с помощью Internal API Endpoint. В JS Callback можно отлавливать событие заполнения формы только для формы реализованной на основе keyboad-кнопки в Telegram.

WebApp-форма, на основе inline-кнопки

Данный вид формы работает только в Telegram.

Рекомендуемый вид формы для Telegram бота.

Ключевая особенность: форма открывается в всплывающем окне на котором расположен WebView (встроенный браузер).

Отличие данного варианта от универсальной формы в виде ссылки, в том, что форма работает как Web App, т.е. это специальный режиме браузера встроенного в Telegram. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в виде inline-кнопки в Telegram-мессенджер.

  3. Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. Форма отправляет данные на backend сайта, где размещена форма.
  6. Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
  7. Metabot принимает и сохраняет данные заполненной формы.
  8. Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).

WebApp-форма, на основе keyboard-кнопки

Данный вид формы работает только в Telegram.

Альтернативный вид формы для бота в Telegram. Используется, если для вас крайне затратно реализовать дополнительный backend-слой для отправки данных в Metabot API

Имеет ряд ограничений и неудобств (список смотрите ниже)

Ключевая особенность: форма открывается в всплывающем окне на котором расположен WebView (встроенный браузер). Для отправки данных в Metabot API не требуется дополнительный backend.

Для пользователя форма работает аналогично виду формы на основе inline-кнопки, но только кнопка для открытия формы расположена в нижней клавиатуре, в так называемой "кнопке-калькулятора".

Отличие данного варианта от универсальной формы в виде ссылки, в том, что форма работает как Web App, т.е. это специальный режиме браузера встроенного в Telegram. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.

Форма открываемая с помощью keyboard-кнопки (так называемой "кнопки-калькулятора"), это упрощенный вариант, чтобы не подключать дополнительный back-end к HTML-форме для пересылки результата сбора данных с HTML-формы в Metabot API. На первый взгляд такой вариант более простой, но имеет ограничения, описанные ниже, поэтому рекомендуется для Telegram использовать вариант WebApp-формы открываемой с помощью inline-кнопки.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в виде keyboard-кнопки в Telegram-мессенджер ("кнопка-калькулятор" отображаемая в нижней части мессенджера).

  3. Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. В коде HTML формы размещается JS код, для передачи введенных данных в телеграмм: window.Telegram.WebApp.sendData(JSON.stringify(formData)).
  6. Для дополнительной защиты, желательно, в отправляемые данные включить уникальный хэш-код лида.
  7. Metabot принимает данные заполненной формы от Telegram и сохраняет эти данные.
  8. Страница с формой в браузере будет закрыта автоматически, после выполнения метода Telegram.WebApp.sendData, если же вы не используете sendData, то можно вызвать метод Telegram.WebApp.close().

Ограничения и неудобства данного вида формы:

image.png

image.png

Web-формы в чат-боте и Web Apps

Создание HTML формы.

Инструкция для разработчика веб-формы.

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

Исходный код примера веб-формы
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>Заявка для полета на Марс</title>

  <!-- Подключаем Telegram Web App -->
  <script src="https://telegram.org/js/telegram-web-app.js"></script>

  <!-- Подключаем JQuery и JQ suggestions для dadata -->
  <!-- PS: JQuery подключать не обязательно, у вас могут быть свои библиотеки для работы с формой и отпарвки ajax запросов -->
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"
          integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
  <script src="https://app.metabot24.com/lib/jquery.suggestions/js/jquery.suggestions.min.js"></script>
  <link href="https://app.metabot24.com/lib/jquery.suggestions/css/suggestions.min.css" rel="stylesheet"/>

  <script language="JavaScript">
    let botId = ID_ВАШЕГО_БОТА
    let botToken = 'ТОКЕН_ВАШЕГО_БОТА'
    let dadataToken = 'ТОКЕН_DADATA'

    // URL для POST запроса на который будет отправлять данные с формы
    // В качестве примера мы отправляем напрямую в бота
    // Но в вашем production боте вы должны отправлять на данные на бэк
    // А с бэка уже пересылать в бота, чтобы, например "не светить" токен бота в коде html-формы
    let sendUrl = '/api/v1/bots/' + botId + '/call/submit-form'

    let sendBody = {}
    let sendHeaders = {}
    let mode = null
    let tgWebApp = null

    $(function () {
      /* Определяем режим работы формы

         Режим передается в query url (GET параметр mode=)

         Режимы текущей html:
          - '' - пустая стркоа или null, когда в чат-боте ссылка на форму приходит в виде ссылки
                 это универсальный вариант который будет работать в любом мессенджере
          - 'tg_inline' -  для Telegram чат-бота, когда ссылка на форму приходит в виде inline-кнопки
          - 'tg_keyboard' - для Telegram чат-бота, когда ссылка на форму приходит в виде keyboard-кнопки

          Для вашего бота может быть достаточно одного из режимов
          В качестве примера просто приведены 3 варианта, чтобы вы могли выбрать подходящий и понять разницу
       */
      if (typeof (urlParams["mode"]) === 'string') {
        mode = urlParams["mode"]
      }
      // Переменная для доступа к Telegram Web App, чтобы не писать везде window.Telegram.WebApp
      if (mode === 'tg_inline' || mode === 'tg_keyboard') {
        if (window.Telegram && window.Telegram.WebApp) {
          tgWebApp = window.Telegram.WebApp
        }
      }

      // Инициалиця Dadata для автокомплита поля с адресом
      $(".dadata-suggestion").suggestions({
        token: dadataToken,
        type: "ADDRESS"
      })

      // Получаем хэш-код лида из request url (GET параметр q=)
      $('#q').val(urlParams["q"])

      // Событие нажатия на кнопку "Отправить данные"
      $(document).on('click', '#submit-mars-form', (e) => {
        let form = $('#form-mars')
        if (!form[0].checkValidity()) {
          form[0].reportValidity()
          return
        }

        let formData = getFormData(form)

        formData['tg_query_id'] = ''
        formData['mode'] = mode

        if (mode === 'tg_inline') {
          if (tgWebApp && tgWebApp.initDataUnsafe && tgWebApp.initDataUnsafe.query_id) {
            formData['tg_query_id'] = tgWebApp.initDataUnsafe.query_id
          }
        }

        if (tgWebApp) {
          // https://core.telegram.org/bots/webapps#initializing-web-apps
          tgWebApp.expand() // необязательно
          tgWebApp.ready() // необязательно
        }

        // Для универсального режима или режима tg_inline
        if (mode !== 'tg_keyboard') {

          // Отправляем данные внутри script_request_params и указываем токен и др заголовки для Metabot API
          // Но в вашем production, здесь вы должны просто отправить данные на ваш бэк, а с бэка уже в Metabot API
          // отправлять необзяталеьно через JSON API, можно делать это просто через ACTION формы SUBMIT кнопку на форме
          //
          // Если вы реализуете универсальный режим и выполняете отправку через action forma(сабмит без REST API),
          // то ваша форма разрывается на два шага
          // - заполнение формы клиентом
          // - получаем данные на бэке
          //      и после приема данных рендерим опять HTML в коде которого размещаем JavaScript который закроет страницу (вызовет метод closeForm())
          // Поэтому проще данные на бэк отправить по REST API и сразу же закрыть форму
          // Именно такой вариант и реализован в данной HTML форме
          sendBody = {"script_request_params": formData} //form.serializeArray()
          sendHeaders = {
            "Authorization": "Bearer " + botToken,
            'Content-Type': 'application/json',
            'Accept': 'application/json',
          }

          // Отправка запроса с помощью библиотеки JQuery
          sendJQueryRequest('POST', sendUrl, sendBody, sendHeaders, function (response, isError, jqXHR, textStatus, errorThrown) {
            if (!isError) {
              // Запрос завершён. Здесь можно обрабатывать результат.
              //console.log(response)

              closeForm()
            } else {
              // Произошла ошибка
              alert("Ошибка обработки API запроса")

              console.log(jqXHR)
            }
          })

          // Отправка запроса без дополнительных библиотек (с помощью XHR)
          // Могут быть проблемы с отправкой, возможно нужна корректировка кода под ваш бэкенд вашего сайта
          /*sendXhrRequest('POST', sendUrl, sendBody, sendHeaders, function(xhr) {
              //https://developer.mozilla.org/ru/docs/Web/API/XMLHttpRequest/send#%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80_get
              if(xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
                // Запрос завершён. Здесь можно обрабатывать результат.
                console.log(xhr)
                closeForm()
              } else {
                // Произошла ошибка
                alert("Ошибка обработки API запроса")
                console.log(xhr)
              }
          })*/
        } else {
          // Если это режим tg_keyboard

          // Внимание! Лимит строки для sendData - 4096 байт !
          // Поэтому такой режим менее универсален, хотя на первый взгляд проще и не требует бэкенда
          // Но в будущем могут возникнуть проблемы, если форма будет усложнена

          // Если необходимо вывести сообщение (например для отладки),
          //   тк обычные alert и console.log для telegram Web App не сработают
          //tgWebApp.showAlert('Все ок!')

          tgWebApp.sendData(JSON.stringify(formData));

          // Если не выполняем sendData, то закрываем форму сами
          //closeForm()
        }
      })
    })

    /**
     * Метод для закрытия формы
     */
    function closeForm() {
      if (mode === null || mode === '') {
        location.href = "tg://resolve?domain=metabot_test_form_bot"
        window.close()
      } else {
        tgWebApp.close()
      }
    }

    // https://stackoverflow.com/questions/11338774/serialize-form-data-to-json
    function getFormData($form) {
      var unindexed_array = $form.serializeArray();
      var indexed_array = {};

      $.map(unindexed_array, function (n, i) {
        indexed_array[n['name']] = n['value'];
      });

      return indexed_array;
    }

    //https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
    var urlParams = (function (a) {
      if (a == "") return {}
      var b = {}
      for (var i = 0; i < a.length; ++i) {
        var p = a[i].split('=', 2)
        if (p.length == 1)
          b[p[0]] = ""
        else
          b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "))
      }
      return b
    })(window.location.search.substr(1).split('&'))

    // Отправка POST запроса по API с помощью JQuery
    // https://reqbin.com/code/javascript/wzp2hxwh/javascript-post-request-example
    function sendJQueryRequest(method, url, data, jsonHeaders, callback) {
      $.ajax({
        url: url,
        type: method,
        contentType: 'application/json; charset=utf-8',
        dataType: 'json',
        async: false,

        headers: jsonHeaders,
        data: JSON.stringify(data),

        success: function (response) {
          callback(response, false)
        },
        error: function (jqXHR, textStatus, errorThrown) {
          //alert("Произошла ошибка при обработке API запроса")
          callback(null, true, jqXHR, textStatus, errorThrown)
        }
      })
    }

    // Отправка POST запроса по API с помощью XHR
    // https://reqbin.com/code/javascript/wzp2hxwh/javascript-post-request-example
    function sendXhrRequest(method, url, data, jsonHeaders, callback) {
      let xhr = new XMLHttpRequest();
      xhr.open(method, url);

      xhr.setRequestHeader("Accept", "application/json")
      xhr.setRequestHeader("Content-Type", "application/json")

      if (typeof (jsonHeaders) != "undefined") {
        for (let key in jsonHeaders) {
          xhr.setRequestHeader(key, jsonHeaders[key])
        }
      }

      xhr.onload = () => console.log(xhr.responseText)

      xhr.onload = function () {
        // Запрос завершён. Здесь можно обрабатывать результат.
        callback(xhr)
      };

      //Вызывает функцию при смене состояния.
      //xhr.onreadystatechange = function() {
      //  callback(xhr)
      //}

      xhr.send(JSON.stringify(data))
    }
  </script>

  <!-- Стили формы, в вашей релизации будет свой блок кода, подлючаемый в виде css файла -->
  <style>
    body {
      background-color: #070619;
      /*width: 100%;
      height: 100%;*/
      font-size: 18px;
    }

    .main-container {
      width: 95%;
      height: 95%;
    }

    form {
      color: #fff;
      border: 1px solid silver;
      border-radius: 10px;
      padding: 10px;

      margin: 10px auto 0 auto;
      width: 95%;
      max-width: 500px;
      height: 100%;
    }

    form .form-title {
      font-size: 20px;
      text-align: center;
      font-weight: bold;
    }

    form .form-group {
      margin: 10px;
    }

    form .form-group input {
      font-size: 18px;
    }

    form .form-group input[type=checkbox] {
      width: 20px;
      height: 20px;
    }

    form .form-group input.dadata-suggestion {
      max-width: 75%;
    }

    form .form-group select {
      font-size: 18px;
    }

    .suggestions-suggestions {
      color: #000
    }

    form .form-button {
      text-align: center;
      margin-bottom: 10px;
    }

    form .send-button {
      font-size: 18px;
      border: 1px solid #fff;
      border-radius: 3px;
      background-color: #fff;
      padding: 5px;
      font-weight: bold;
      text-decoration: none;
    }

    form .form-button button {
      font-size: 18px;
      font-weight: bold;
    }

    .mars {
      position: absolute;
      left: calc(55vw);
      /*right: 0;*/
      top: 0;
      z-index: -10;
      opacity: 0.8;
    }
  </style>
</head>
<body>

<!-- HTML КОД формы  -->

<div class="main-container">
  <!--Для отправки с помощью submit укажите аттрибут action данной формы-->
  <form id="form-mars" autocomplete="off">
    <div class="form-title">Заявка для полета на Марс</div>

    <input type="hidden" id="q" name="q" value="">

    <div class="form-group">
      <label for="name">
        Ваше имя:
      </label>
      <div>
        <input type="text" name="name" id="name" placeholder="Юрий Гагарин" required autofocus>
      </div>
    </div>

    <div class="form-group">
      <label for="email">
        Почта:
      </label>
      <div>
        <input type="email" name="email" id="email" placeholder="yuri@gagarin.ru">
      </div>
    </div>

    <div class="form-group">
      <label for="age">
        Возраст:
      </label>
      <div>
        <input type="number" name="age" id="age" min=12 max=777 step=1>
      </div>
    </div>

    <div class="form-group">
      <label for="specialization">
        Профессия:
      </label>
      <div>
        <select name="specialization" id="specialization" required>
          <option value="engineer" selected>Инженер</option>
          <option value="scientist">Учёный</option>
          <option value="psychologist">Психолог</option>
          <option value="other">Другая</option>
        </select>
      </div>
    </div>

    <div class="form-group">
      <label for="address">
        Ваш адрес проживания:
      </label>
      <input class="dadata-suggestion" type="text" name="address" id="address" placeholder="" required>
    </div>

    <div class="form-group">
      <label for="is_qualified">
        Прошел курсы в Центре<br>подготовки космонавтов
        <input type="checkbox" name="is_qualified" id="is_qualified" value="1">
      </label>
    </div>

    <div class="form-group">
      <label for="has_experience">
        Я уже летал в космос (имею опыт)
        <input type="checkbox" name="has_experience" id="has_experience" value="1">
      </label>
    </div>

    <!-- Если нужна отправка фото -->
    <!--<div class="form-group">
      <label>
        Фото:
        <input type="file" accept="image/jpeg" name="photo" required>
      </label>
    </div>-->

    <div class="form-button">
      <!--<button type="submit">Отправить заявку</button>--> <!-- Для отправки с помощью submit формы -->
      <a class="send-button" id="submit-mars-form" href="#">Отправить заявку</a>
    </div>
  </form>

  <img class="mars" src="./mars1.gif"/>
</div>
</body>
</html>

При использовании данного примера замените в следующих строках данные на актуальные для вашего бота:

И измените код отправки заполненных данных, чтобы данные сначала отправлялись на ваш бэк, а затем с бэка в Metabot API. Также уберите отправку данных внутри параметра script_request_params, чтобы данные к вам на бэк отправлялись без этого параметра, но когда отправляете в Metabot API учитывайте эту особенность.

Речь идет про строку:

sendBody = {"script_request_params": formData}

Которую для вас нужно заменить на:

sendBody = formData

Также для понимания принципа работы изучите комментарии в HTML-коде.

Код не требует большого уровня владения HTML или JS для создания подобной формы, но заметим, что важной частью является UI/UX и может потребоваться профессиональный Frontend-разработчик, также необходимо учесть отработку всех исключительных ситуаций, например если произошел сбой API, о чем было упомянуто в теоретической части, во введении, заметим что выше приведен просто пример, все аспекты не проработаны досконально, тк это возможно только на конкретном рабочем примере для вашего бота.

Web-формы в чат-боте и Web Apps

Универсальная форма в виде ссылки (для любого мессенджера)

Инструкция для разработчика бота.

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

Форма открывается в отдельной странице браузера. Как обычная страница web-браузера.

Для Telegram ссылку можно отправить в виде inline-кнопки, но это все равно будет не "Web App приложение", а обычная страница открытая в браузере.

Если в мессенджере открытие ссылок во внешнем браузере выключено то форма откроется в браузере мессенджера, но все равно работа с данным видом формы будет отличаться от формы в виде Web App для inline/keyboard кнопки. При этом пользователю все равно доступна возможность позволяющая открыть ссылку во внешнем браузере. После закрытия формы нет гарантий, что пользователь вернется в браузер (например для PC), поэтому желательно прислать пользователю уведомление в бота, чтобы он нажал на него и вернулся в бота.

Если вы планируете использовать формы только в Telegram, то можете перейти к разделу описывающему форму на основе Web App, открывающейся с помощью inline-кнопки.

Краткое описание принципа работы бота:

  1. В бот отправляется ссылка на форму (или inline-кнопка в виде ссылки). При нажатии на ссылку (или inline-кнопку в виде ссылки) открывается внешний браузер (или браузер встроенный в Telegram).
  2. После заполнения формы данные должны быть отправлены на сторонний бэк, а из бэка данные должны быть отправлены в Metabot API. Для понимания смотрите исходный код веб-формы.
  3. Данные пришедшие с формы сохраняются в JS Internal API Endpoint, который сохраняет данные и вызывает скрипт бота для продолжения беседы, этот скрипт работает с сохраненными данными формы, запрашивает подтверждения, что все введено верно, а также предлагает заполнить форму повторно.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в мессенджер в виде обычного сообщения.

  3. Пользователь бота нажимает на ссылку, открывается браузер с формой.

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. Форма отправляет данные на backend сайта, где размещена форма.
  6. Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
  7. Metabot принимает и сохраняет данные заполненной формы.
  8. Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).

Таблицы для работы с формами

Чтобы рассматриваемый пример бота с формами работал, вам необходимо создать две кастомные таблицы.

Таблица: Хэш-коды лидов

Таблица необходима для хранения соответствий между лидом и хэшом для лида.

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

Структура таблицы:

image.pngimage.png


Таблица: Данные форм

Таблица необходима для хранения данных заполненных форм.

Данная таблица реализована как универсальная, все данные пришедшие с формы сохраняются в текстовом поле в виде JSON, поэтому она может работать с любой формой, но работать с строкой в виде JSON в JS неудобно, тк для доступа к данным приходится выполнять JSON.parse(), вы можете нормализовать данные, реализуя для каждой формы бота отдельную таблицу с необходимыми полями для хранения, например такими как: Имя, Фамилия, Адрес и т.д., чтобы в дальнейшем было проще работать с данными и выполнять поиск по таблице. Также, в вашем случае таблица для хранения данных может и не потребоваться, если всплывающая форма очень простая и предназначена, например, для заполнения одного поля, например, адреса с авто комплитом от Dadata или для выбора выпадающего списка или даты в календаре. Т.е. форма может реализовывать даже один компонент для заполнения которого необходим HTML + JS.

Структура таблицы:

PKCu1KBiYK3GrQC6-image.png

image.png

Чтобы посмотреть исходный код внутреннего API, сначала перейдите в бота по ссылке на любой из скриптов.

Нажмите в предупреждающем сообщении на ссылку для смены активного бота, а затем перейдите на страницу c внутренним API в настройках бота или по ссылке.

Если у вас нет доступа к указанному скрипту, то запросите шаблон бота в технической поддерже Metabot.

Скрипт для формирования и отправки ссылки в мессенджер

Пример скрипта:

image.png

Скрипт отправки ссылки состоит из следующих команд бота:

let md5 = require('Common.Utils.Md5')

let expireAt = new Date()
expireAt.setSeconds(expireAt.getSeconds() + 300)

// Todo: Можно вынести в сниппет
let hashItems = table.find('lead_form_hash', [], [
  ['form', '=', 'mars'],
  ['lead_id', '=', leadId],
])

let leadHash
if (hashItems.length > 0) {
  leadHash = hashItems[0].hash
} else {
  const salt = 'YourSuperSecretString' + (new Date()).getTime()
  leadHash = md5(salt + leadId)
  
  table.createItem('lead_form_hash', {
    'form': 'mars',
    'lead_id': leadId,
    'hash': leadHash,
    'expireAt': expireAt
  })
}

// Вы можете вынести эту ссылку в атрибуты бота
let url = 'https://app.metabot24.com/testWidgets/go-to-the-mars.html?q=' + leadHash

memory.setAttr('formUrl', url)

PS: Здесь же можно отправить ссылку в виде inline кнопки (не inline Web App, а обычную кнопку с ссылкой) 
реализовав это в виде  команды JS Callback
но такой вариант будет работать только в Telegram
поэтому чтобы данный вариант формы был универсальным отправляем ее просто в виде ссылки

Перейдите по ссылке ниже и заполните форму

{{ &$formUrl }}

Внутреннее api для приема данных с формы и сохранения их в таблицу

Это точка API, куда приходит запрос после заполнения формы.

image.png

JavaScript для вычисления API ответа (Response Body):

let requestParams = request.json
let formLeadId = null

if (!request.json || !request.json.q) {
  return {"result": false, "message": "Invalid Lead Hash"}
}

// Todo: Можно вынести в сниппет
let hashItems = table.find('lead_form_hash', [], [
  ['form', '=', 'mars'],
  ['hash', '=', request.json.q],
])

let found = 0

if (hashItems.length > 0) {
  formLeadId = hashItems[0].lead_id
  hashItems.forEach(hashItem => hashItem.delete())
}

if (formLeadId > 0) {
  // Удаляем предыдущие ответы
  // Todo: Можно вынести в сниппет
  oldResults = table.find('form_results', [], [
    ['form', '=', 'mars'],
    ['lead_id', '=', formLeadId],
  ])
  if (oldResults.length > 0) {
    oldResults.forEach(oldResult => oldResult.delete())
  }
  
  // Предварительно сохраняем данные в таблице
  table.createItem('form_results', {
    "form": "mars",
    "lead_id": formLeadId,
    "data": request.string
  })

  bot.scheduleScriptByCode('after_submit', formLeadId)
  
  //Для передачи данных в скрипт без предварительного сохранения в таблицу
  //bot.scheduleScriptByCode('after_submit', leadId, null, {"script_request_params": requestParams})
  return {"result": true}
} else {
  return {"result": false, "message": "Lead by hash is not found"}
}

Скрипт выполняемый после вызова API

Данный скрипт нужен чтобы пользователь продолжил диалог с ботом, а также демонстрирует как использовать данные сохраненные в таблицу результатов и вывести их ввиде текста.

Пример скрипта.

Скрипт отправки ссылки состоит из следующих команд бота и пунктов меню:

const menuHelper = require('Common.TelegramComponents.MenuHelper')

// --------------------------------------------------------------
// Удаляем кнопку с формой для Inline формы, если она выведена

if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку с ссылкой на форму
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// --------------------------------------------------------------
// Для Inline кнопки c формой
// Удаляем кнопку с формой , если она выведена
// Удаляем здесь - тк после заполнения формы в JS-Callback мы уже не попадаем
// Тк прием данных с формы осуществляет через Internal Endpoiint
// Для keyboard кнопки ссылку на форму с кнопкой здесь удалять не нужно, 
// тк для keyboard кнопки мы возвращаемся в JS-Callbak после заполнении формы
if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// Если получаем данные напрямую, без таблицы form_results
//let data = request.json
  
// Если получаем данные из таблицы form_results
// Todo: Можно вынести в сниппет
let data = table.find('form_results', [], [
  ['form', '=', 'mars'],
  ['lead_id', '=', leadId]
])

if (data.length > 0 && typeof(data[0].data) === 'string' && data[0].data.length > 0) {
  data = JSON.parse(data[0].data)
} else {
  data = {}
  bot.sendText('Ошибка! Данные не найдены')
}

// Для определения какую кнопку показывать в меню ниже
// См условие аунктов меню текущего скрипта
memory.setAttr('mode', '')
if (typeof(data.mode) === 'string' && data.mode.length > 0) {
  memory.setAttr('mode', data.mode)
}


/*
// Сообщение о результате отработки формы, отправляется в бота
// Но проблема что система распознает сообщение как вебхук от лида
// Поэтому использовать можно только в JS-Callback и игнорировать вебхук вручную
// Или через регулярку маршрута, запуская отдельны скрипт
//
// Данное сообщение также автоматом закрывает web-view
// Но пробелм с закрытием нет, это выполняется в JS итак с помощью
// кода window.Telegram.WebApp.sendData(JSON.stringify(formData));
// или при выполнении window.Telegram.WebApp.close()
//
// Обычно используется для игры в web_view для вывода очков после победы или проигрыша
if (data.tg_query_id && data.tg_query_id.length > 0) {
  //https://core.telegram.org/bots/api#sentwebappmessage
  //https://core.telegram.org/bots/api#inlinequeryresultarticle
  bot.sendPayload('answerWebAppQuery', {
    "web_app_query_id": data.tg_query_id,
    "result": {
      "type": "article", 
      "id": "1", // Unique identifier for this result, 1-64 Bytes
      "title": "Форма заполнена", 
      //"description": "Description", 
      "input_message_content": {"message_text": "Спасибо! Данные сохранены!"}
    }
  })
}
*/

memory.setJsonAttr('data', data)
memory.setAttr('is_qualified', data.is_qualified ? 'Да' : 'Нет')
memory.setAttr('has_experience', data.has_experience ? 'Да' : 'Нет')
Ваши данные:
Имя: {{ &$$data.name }}
Email: {{ &$$data.email }}
Возраст: {{ &$$data.age }}
Профессия: {{ &$$data.specialization}}
Ваш адрес проживания: {{ &$$data.address}}
Прошел курсы в Центре подготовки космонавтов: {{ &$is_qualified }}
Я уже летал в космос (имею опыт): {{ &$has_experience}}

Все данные в виде JSON:
{{ &$$data }}
Все верно?

image.png

Условия кнопок необходимы для вызова скрипта соответствующего текущему формату формы.

Условие для вывода универсальной формы в виде ссылки:

let mode = memory.getAttr('mode')
if (mode === 'tg_inline') {
  return true
}

Условие для вывода формы в Web App при нажатии на inline-кнопку:

let mode = memory.getAttr('mode')
if (mode !== 'tg_inline' && mode !== 'tg_keyboard') {
  return true
}

Условие для вывода формы в Web App при нажатии на keyboard-кнопку:

let mode = memory.getAttr('mode')
if (mode === 'tg_keyboard') {
  return true
}
Web-формы в чат-боте и Web Apps

Web App форма c использованием inline-кнопки (только для Telegram)

Инструкция для разработчика бота.

Данный вид формы работает только в Telegram.

Рекомендуемый вид формы для Telegram бота.

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

Отличие данного варианта от универсальной формы в виде ссылки, в том, что форма работает как Web App, т.е. это специальный режиме браузера встроенного в Telegram. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.

Краткое описание принципа работы бота:

  1. В бот отправляется inline-кнопка (в формате Telegram Web App). При нажатии на кнопку открывается всплывающее окно, в которое встроено Web View, пользователь не может открыть эту ссылку во внешнем браузере, а также просмотреть исходный код формы (если только не откроет сам Telegram в браузере).
  2. После заполнения формы данные должны быть отправлены на сторонний бэк, а из бэка данные должны быть отправлены в Metabot API. Для понимания смотрите исходный код веб-формы.
  3. Данные пришедшие с формы сохраняются в JS Internal API Endpoint, который сохраняет данные и вызывает скрипт бота для продолжения беседы, этот скрипт работает с сохраненными данными формы, запрашивает подтверждения, что все введено верно, а также предлагает заполнить форму повторно.

Принцип работы, по шагам:

  1. Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.

  2. Бот отправляет эту ссылку в виде inline-кнопки в Telegram-мессенджер.

  3. Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).

  4. Пользователь бота заполняет форму и нажимает кнопку для отправки формы.

  5. Форма отправляет данные на backend сайта, где размещена форма.
  6. Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
  7. Metabot принимает и сохраняет данные заполненной формы.
  8. Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).

Таблицы для работы с формами

Чтобы рассматриваемый пример бота с формами работал, вам необходимо создать две кастомные таблицы.

Таблица: Хэш-коды лидов

Таблица необходима для хранения соответствий между лидом и хэшом для лида.

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

Структура таблицы:

image.pngimage.png


Таблица: Данные форм

Таблица необходима для хранения данных заполненных форм.

Данная таблица реализована как универсальная, все данные пришедшие с формы сохраняются в текстовом поле в виде JSON, поэтому она может работать с любой формой, но работать с строкой в виде JSON в JS неудобно, тк для доступа к данным приходится выполнять JSON.parse(), вы можете нормализовать данные, реализуя для каждой формы бота отдельную таблицу с необходимыми полями для хранения, например такими как: Имя, Фамилия, Адрес и т.д., чтобы в дальнейшем было проще работать с данными и выполнять поиск по таблице. Также, в вашем случае таблица для хранения данных может и не потребоваться, если всплывающая форма очень простая и предназначена, например, для заполнения одного поля, например, адреса с авто комплитом от Dadata или для выбора выпадающего списка или даты в календаре. Т.е. форма может реализовывать даже один компонент для заполнения которого необходим HTML + JS.

Структура таблицы:

PKCu1KBiYK3GrQC6-image.png

image.png

Чтобы посмотреть исходный код внутреннего API, сначала перейдите в бота по ссылке на любой из скриптов.

Нажмите в предупреждающем сообщении на ссылку для смены активного бота, а затем перейдите на страницу c внутренним API в настройках бота или по ссылке.

Если у вас нет доступа к указанному скрипту, то запросите шаблон бота в технической поддерже Metabot.

Скрипт для формирования ссылки и отправки кнопки в мессенджер

Пример скрипта.

Скрипт отправки ссылки состоит из единственной команды бота:

image.png

const menuHelper = require('Common.TelegramComponents.MenuHelper')
let md5 = require('Common.Utils.Md5')

// ---------------------------------------------------------------------------------------------------
// Инициализация компонента
if (menuHelper.isFirstImmediateCall()) {
  // !!! ИНИЦИАЛИЗАЦИЯ !!!

  // Скрываем предыдущее меню c кнопкой формы
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
  
  // --------------------------
  //ГЕНЕРИРУМ ССЫЛКУ С ХЭШЕМ
  let expireAt = new Date()
  expireAt.setSeconds(expireAt.getSeconds() + 300)

  let hashItems = table.find('lead_form_hash', [], [
    ['form', '=', 'mars'],
    ['lead_id', '=', leadId],
  ])

  // Todo: Можно вынести в сниппет
  let leadHash
  if (hashItems.length > 0) {
    leadHash = hashItems[0].hash
  } else {
    const salt = 'YourSuperSecretString' + (new Date()).getTime()
    leadHash = md5(salt + leadId)

    table.createItem('lead_form_hash', {
      'form': 'mars',
      'lead_id': leadId,
      'hash': leadHash,
      'expireAt': expireAt
    })
  }

  // Вы можете вынести эту ссылку в атрибуты бота
  let formUrl = 'https://app.metabot24.com/testWidgets/go-to-the-mars.html?mode=tg_inline&q=' + leadHash
  // ----------------------------

  // Отправляем кнопку с формой при инициализации
  let message = 'Для продолжения, пожалуйста заполните форму'
  let buttons = [
    [
      /*{
        "text": 'Отмена',
        "callback_data": "btn_static_cancel",
      },*/
      {
        "text": 'Заполнить форму',
        //"callback_data": "btn_static_web_app",
        "web_app": {
          "url": formUrl
        },
      }
    ]
  ]
  let apiAdditionalParams = {
    "reply_markup": {
      "inline_keyboard": buttons
    }
  }
  menuHelper.sendMessage(message, null, null, apiAdditionalParams)
  
  return false
}
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// ЛОГИКА КОМПОНЕНТА
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// Обработка нажатых кнопок или входящего текста

if (["btn_static_cancel", "0"].includes(bot.getIncomingMessage().toLowerCase())) {
  // Скрываем предыдущее меню c кнопкой формы
  menuHelper.removeInlineKeyboard()

  // Или удаляем предыдущее сообщение, если необходимо
  // Не забываем выполнять аналогичное удаление при выходе из компонента с помощью маршрута
  //this.deletePrevMessage()

  // Или останавливаем анимацию ожидания на кнопке
  //menuHelper.answerCallbackQuery()    
  
  // Выходим из команды
  return {
    "break": true,
    //"run_script_by_code": "код_запускаемого_скрипта",
  }
} else {
  // Если получаем любое другое текстовое сообщение
  if (bot.getIncomingMessage() !== "") {
    bot.sendText('Пожалуйста заполните форму')
    
    return false
    
    // Чтобы вопрос не уходил вверх, тогда после удаления необходимо повторить вопрос
    //this.deletePrevMessage()    
    
    // Повторяем вопрос, если удаляем пред. вопрос строкой выше с помощью deletePrevMessage
    // ...
  } //else if(1 === 2) {
    // Данные отправленные из webview обрабатываем не здесь, а в Internal API Endpint
    //return true
  //}
}
// ---------------------------------------------------------------------------------------------------

Внутреннее api для приема данных с формы и сохранения их в таблицу

Это точка API, куда приходит запрос после заполнения формы.

image.png

JavaScript для вычисления API ответа (Response Body):

let requestParams = request.json
let formLeadId = null

if (!request.json || !request.json.q) {
  return {"result": false, "message": "Invalid Lead Hash"}
}

// Todo: Можно вынести в сниппет
let hashItems = table.find('lead_form_hash', [], [
  ['form', '=', 'mars'],
  ['hash', '=', request.json.q],
])

let found = 0

if (hashItems.length > 0) {
  formLeadId = hashItems[0].lead_id
  hashItems.forEach(hashItem => hashItem.delete())
}

if (formLeadId > 0) {
  // Удаляем предыдущие ответы
  // Todo: Можно вынести в сниппет
  oldResults = table.find('form_results', [], [
    ['form', '=', 'mars'],
    ['lead_id', '=', formLeadId],
  ])
  if (oldResults.length > 0) {
    oldResults.forEach(oldResult => oldResult.delete())
  }
  
  // Предварительно сохраняем данные в таблице
  table.createItem('form_results', {
    "form": "mars",
    "lead_id": formLeadId,
    "data": request.string
  })

  bot.scheduleScriptByCode('after_submit', formLeadId)
  
  //Для передачи данных в скрипт без предварительного сохранения в таблицу
  //bot.scheduleScriptByCode('after_submit', leadId, null, {"script_request_params": requestParams})
  return {"result": true}
} else {
  return {"result": false, "message": "Lead by hash is not found"}
}

Скрипт выполняемый после вызова API

Данный скрипт нужен чтобы пользователь продолжил диалог с ботом, а также демонстрирует как использовать данные сохраненные в таблицу результатов и вывести их в виде текста.

Пример скрипта.

Скрипт отправки ссылки состоит из следующих команд бота и пунктов меню:

const menuHelper = require('Common.TelegramComponents.MenuHelper')

// --------------------------------------------------------------
// Удаляем кнопку с формой для Inline формы, если она выведена

if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку с ссылкой на форму
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// --------------------------------------------------------------
// Для Inline кнопки c формой
// Удаляем кнопку с формой , если она выведена
// Удаляем здесь - тк после заполнения формы в JS-Callback мы уже не попадаем
// Тк прием данных с формы осуществляет через Internal Endpoiint
// Для keyboard кнопки ссылку на форму с кнопкой здесь удалять не нужно, 
// тк для keyboard кнопки мы возвращаемся в JS-Callbak после заполнении формы
if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// Если получаем данные напрямую, без таблицы form_results
//let data = request.json
  
// Если получаем данные из таблицы form_results
// Todo: Можно вынести в сниппет
let data = table.find('form_results', [], [
  ['form', '=', 'mars'],
  ['lead_id', '=', leadId]
])

if (data.length > 0 && typeof(data[0].data) === 'string' && data[0].data.length > 0) {
  data = JSON.parse(data[0].data)
} else {
  data = {}
  bot.sendText('Ошибка! Данные не найдены')
}

// Для определения какую кнопку показывать в меню ниже
// См условие аунктов меню текущего скрипта
memory.setAttr('mode', '')
if (typeof(data.mode) === 'string' && data.mode.length > 0) {
  memory.setAttr('mode', data.mode)
}


/*
// Сообщение о результате отработки формы, отправляется в бота
// Но проблема что система распознает сообщение как вебхук от лида
// Поэтому использовать можно только в JS-Callback и игнорировать вебхук вручную
// Или через регулярку маршрута, запуская отдельны скрипт
//
// Данное сообщение также автоматом закрывает web-view
// Но пробелм с закрытием нет, это выполняется в JS итак с помощью
// кода window.Telegram.WebApp.sendData(JSON.stringify(formData));
// или при выполнении window.Telegram.WebApp.close()
//
// Обычно используется для игры в web_view для вывода очков после победы или проигрыша
if (data.tg_query_id && data.tg_query_id.length > 0) {
  //https://core.telegram.org/bots/api#sentwebappmessage
  //https://core.telegram.org/bots/api#inlinequeryresultarticle
  bot.sendPayload('answerWebAppQuery', {
    "web_app_query_id": data.tg_query_id,
    "result": {
      "type": "article", 
      "id": "1", // Unique identifier for this result, 1-64 Bytes
      "title": "Форма заполнена", 
      //"description": "Description", 
      "input_message_content": {"message_text": "Спасибо! Данные сохранены!"}
    }
  })
}
*/

memory.setJsonAttr('data', data)
memory.setAttr('is_qualified', data.is_qualified ? 'Да' : 'Нет')
memory.setAttr('has_experience', data.has_experience ? 'Да' : 'Нет')
Ваши данные:
Имя: {{ &$$data.name }}
Email: {{ &$$data.email }}
Возраст: {{ &$$data.age }}
Профессия: {{ &$$data.specialization}}
Ваш адрес проживания: {{ &$$data.address}}
Прошел курсы в Центре подготовки космонавтов: {{ &$is_qualified }}
Я уже летал в космос (имею опыт): {{ &$has_experience}}

Все данные в виде JSON:
{{ &$$data }}
Все верно?

image.png

Условия кнопок необходимы для вызова скрипта соответствующего текущему формату формы.

Условие для вывода универсальной формы в виде ссылки:

let mode = memory.getAttr('mode')
if (mode === 'tg_inline') {
  return true
}

Условие для вывода формы в Web App при нажатии на inline-кнопку:

let mode = memory.getAttr('mode')
if (mode !== 'tg_inline' && mode !== 'tg_keyboard') {
  return true
}

Условие для вывода формы в Web App при нажатии на keyboard-кнопку:

let mode = memory.getAttr('mode')
if (mode === 'tg_keyboard') {
  return true
}
Web-формы в чат-боте и Web Apps

Web App форма c использованием keyboard-кнопки (только для Telegram)

Инструкция для разработчика бота.

Данный вид формы работает только в Telegram.

Ключевая особенность: форма открывается в всплывающем окне на котором расположен WebView (встроенный браузер). Для отправки данных в Metabot API не требуется дополнительный backend.

Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html

Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot

Отличие данного варианта от универсальной формы в виде ссылки, в том, что форма работает как Web App, т.е. это специальный режиме браузера встроенного в Telegram. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.

Краткое описание принципа работы бота:

  1.  В бот отправляется keyboard-кнопка (в формате Telegram Web App). При нажатии на кнопку открывается всплывающее окно, в которое встроен Web View, пользователь не может открыть эту ссылку во внешнем браузере, а также просмотреть исходный код формы (если только не откроет сам Telegram в браузере).
  2.  После заполнения формы и нажатия на кнопку Отправить данные, все данные будут отправлены в Metabot, в команду JavaSctript Callback, Internal API endpoint в данном случае не требуется. Для понимания смотрите исходный код веб-формы.
  3. Данные пришедшие с формы доступны в команде JavaSctript Callback в виде обычного вебхука. Необходимо сохранить их в таблице.
  4. Далее  необходимо вызвать скрипт бота для продолжения беседы, этот скрипт работает с сохраненными данными формы, запрашивает подтверждение, что все введено верно, а также предлагает заполнить форму повторно.

Принцип работы, по шагам:

Таблицы для работы с формами

Чтобы рассматриваемый пример бота с формами работал, вам необходимо создать две кастомные таблицы.

Таблица: Хэш-коды лидов

Таблица необходима для хранения соответствий между лидом и хэшом для лида.

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

Структура таблицы:

image.pngimage.png

Таблица: Данные форм

Таблица необходима для хранения данных заполненных форм.

Данная таблица реализована как универсальная, все данные пришедшие с формы сохраняются в текстовом поле в виде JSON, поэтому она может работать с любой формой, но работать с строкой в виде JSON в JS неудобно, тк для доступа к данным приходится выполнять JSON.parse(), вы можете нормализовать данные, реализуя для каждой формы бота отдельную таблицу с необходимыми полями для хранения, например такими как: Имя, Фамилия, Адрес и т.д., чтобы в дальнейшем было проще работать с данными и выполнять поиск по таблице. Также, в вашем случае таблица для хранения данных может и не потребоваться, если всплывающая форма очень простая и предназначена, например, для заполнения одного поля, например, адреса с авто комплитом от Dadata или для выбора выпадающего списка или даты в календаре. Т.е. форма может реализовывать даже один компонент для заполнения которого необходим HTML + JS.

Структура таблицы:

PKCu1KBiYK3GrQC6-image.png

image.png

Чтобы посмотреть исходный код внутреннего API, сначала перейдите в бота по ссылке на любой из скриптов.

Нажмите в предупреждающем сообщении на ссылку для смены активного бота, а затем перейдите на страницу c внутренним API в настройках бота или по ссылке.

Если у вас нет доступа к указанному скрипту, то запросите шаблон бота в технической поддерже Metabot.

Скрипт для формирования ссылки и отправки кнопки в мессенджер

Пример скрипта.

Скрипт отправки ссылки состоит из единственной команды бота:

image.png

const menuHelper = require('Common.TelegramComponents.MenuHelper')
let md5 = require('Common.Utils.Md5')

// ---------------------------------------------------------------------------------------------------
// Инициализация компонента
if (menuHelper.isFirstImmediateCall()) {
  // !!! ИНИЦИАЛИЗАЦИЯ !!!

  // Скрываем предыдущее меню c кнопкой формы
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
  
  // --------------------------
  //ГЕНЕРИРУМ ССЫЛКУ С ХЭШЕМ
  let expireAt = new Date()
  expireAt.setSeconds(expireAt.getSeconds() + 300)

  let hashItems = table.find('lead_form_hash', [], [
    ['form', '=', 'mars'],
    ['lead_id', '=', leadId],
  ])

  // Todo: Можно вынести в сниппет
  let leadHash
  if (hashItems.length > 0) {
    leadHash = hashItems[0].hash
  } else {
    const salt = 'YourSuperSecretString' + (new Date()).getTime()
    leadHash = md5(salt + leadId)

    table.createItem('lead_form_hash', {
      'form': 'mars',
      'lead_id': leadId,
      'hash': leadHash,
      'expireAt': expireAt
    })
  }

  // Вы можете вынести эту ссылку в атрибуты бота
  let formUrl = 'https://app.metabot24.com/testWidgets/go-to-the-mars.html?mode=tg_keyboard&q=' + leadHash
  // ----------------------------

  // Отправляем кнопку с формой при инициализации
  let message = 'Для продолжения, пожалуйста заполните форму'
  let buttons = [
    [
      {
        "text": "Заполнить форму",
        "web_app": {
          "url": formUrl
        }
      }
    ]
  ]
  let apiAdditionalParams = {
    "reply_markup": {
      "keyboard": buttons, 
      "resize_keyboard": true // Для того чтобы кнопка не была слишком большой по высоте
    }
  }
  menuHelper.sendMessage(message, null, null, apiAdditionalParams)
  
  return false
}
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// ЛОГИКА КОМПОНЕНТА
// ---------------------------------------------------------------------------------------------------

// ---------------------------------------------------------------------------------------------------
// Обработка нажатых кнопок или входящего текста

let webhook = bot.getWebhookPayload() 
let webAppData = null
let data = null
let rawJsonData = null

if (typeof webhook.message == 'object') {
  if (typeof webhook.message.web_app_data == 'object') {
    webAppData = webhook.message.web_app_data
    
    //Example:
    //"web_app_data": {
    //  "button_text": "Заполнить форму",
    //    "data": "{...}"
    //}
    
    if (typeof webAppData.data === 'string') {
      rawJsonData = webAppData.data
      data = JSON.parse(rawJsonData)
      if (typeof data !== 'object') {
        rawJsonData = null
        data = null
      }
    }
  }
}

// ---------------------------------
if (data !== null && typeof data === 'object' && typeof data.q === 'string') {
  // СОХРАНЕНИЕ ДАННЫХ
  // КОД ИДЕНТИЧНЫЙ API INTERNAL ENDPOINT
  // TODO: МОЖНО ВЫНЕСТИ В СНИППЕТ
  
  // Доп защита
  // Все равно проверяем хэш (на случай чтобы не взломали)
  // А также чтобы почистить формы ожидающие заполнения по лиду (хэши по лиду)
  // Можно не выполнять эту проверку здесь
  // Todo: Можно вынести в сниппет
  let hashItems = table.find('lead_form_hash', [], [
    ['form', '=', 'mars'],
    ['hash', '=', data.q],
  ])

  let found = 0

  if (hashItems.length > 0) {
    formLeadId = hashItems[0].lead_id
    hashItems.forEach(hashItem => hashItem.delete())
  }

  // Доп защита
  // Проверяем что к нам пришла форма принадлежащая лиду
  // Можно не выполнять эту проверку здесь
  if (formLeadId === leadId) {
    // Удаляем предыдущие ответы
    // Todo: Можно вынести в сниппет
    oldResults = table.find('form_results', [], [
      ['form', '=', 'mars'],
      ['lead_id', '=', formLeadId],
    ])
    if (oldResults.length > 0) {
      oldResults.forEach(oldResult => oldResult.delete())
    }

    // Предварительно сохраняем данные в таблице
    table.createItem('form_results', {
      "form": "mars",
      "lead_id": formLeadId,
      "data": rawJsonData
    })

    // Удаляем кнопку калькулятора, если она есть !
    // Внимание! Удалять можно только с отправкой сообщения!
    menuHelper.removeReplyKeyboardWithMessage('Спасибо, данные получены!')
    
    //bot.scheduleScriptByCode('after_submit', formLeadId)
    
    // Выходим из цикла и запускаем скрипт уведомления о принятой форме
    return {
      "break": true,
      "run_script_by_code": "after_submit"
    }
  } else {
    // Можно вывести сообщение - что чтото прилетело из того что не ожидали 
    // или выслать письмо администратору
    // ...
  }
}
// ---------------------------------

// Todo: Здесь можно повторно отправить кнопку, тк она скрывается с экрана если отправить сообщение в бота
bot.sendText('Пожалуйста, заполните форму, для этого нажмите на кнопку в нижней части мессенджера,'
             + ' после этого откроется форма, которую вам необходимо заполнить'
             + ' и нажать кнопку на форме "Отправить заявку"')

return false

// ---------------------------------------------------------------------------------------------------

Обратите внимание на строки с кодом запуска скрипта, после сохранения данных с формы:

    // Выходим из цикла и запускаем скрипт уведомления о принятой форме
    return {
      "break": true,
      "run_script_by_code": "after_submit"
    }

Этот код может быть заменен на "return true" для выхода из цикла и на команду бота - "Выполнить скрипт", которую в таком случае необходимо разместить после команды JavaScript Callback. Подробнее смотрите в описании команды Выполнить JavaScript Callback.

Скрипт выполняемый после сохранения данных 

Данный скрипт запускается из JavaScript Callback, код которой приведен выше.

Данный скрипт нужен чтобы пользователь продолжил диалог с ботом, а также демонстрирует как использовать данные сохраненные в таблицу результатов и вывести их в виде текста.

Пример скрипта.

Скрипт отправки ссылки состоит из следующих команд бота и пунктов меню:

const menuHelper = require('Common.TelegramComponents.MenuHelper')

// --------------------------------------------------------------
// Удаляем кнопку с формой для Inline формы, если она выведена

if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку с ссылкой на форму
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// --------------------------------------------------------------
// Для Inline кнопки c формой
// Удаляем кнопку с формой , если она выведена
// Удаляем здесь - тк после заполнения формы в JS-Callback мы уже не попадаем
// Тк прием данных с формы осуществляет через Internal Endpoiint
// Для keyboard кнопки ссылку на форму с кнопкой здесь удалять не нужно, 
// тк для keyboard кнопки мы возвращаемся в JS-Callbak после заполнении формы
if (menuHelper.hasLastTelegramMessageId()) {
  // Удаляем кнопку
  menuHelper.removeInlineKeyboard()

  // Сбрасываем ID последнего сообщения
  menuHelper.clearLastTelegramMessageId()
}
// --------------------------------------------------------------

// Если получаем данные напрямую, без таблицы form_results
//let data = request.json
  
// Если получаем данные из таблицы form_results
// Todo: Можно вынести в сниппет
let data = table.find('form_results', [], [
  ['form', '=', 'mars'],
  ['lead_id', '=', leadId]
])

if (data.length > 0 && typeof(data[0].data) === 'string' && data[0].data.length > 0) {
  data = JSON.parse(data[0].data)
} else {
  data = {}
  bot.sendText('Ошибка! Данные не найдены')
}

// Для определения какую кнопку показывать в меню ниже
// См условие аунктов меню текущего скрипта
memory.setAttr('mode', '')
if (typeof(data.mode) === 'string' && data.mode.length > 0) {
  memory.setAttr('mode', data.mode)
}


/*
// Сообщение о результате отработки формы, отправляется в бота
// Но проблема что система распознает сообщение как вебхук от лида
// Поэтому использовать можно только в JS-Callback и игнорировать вебхук вручную
// Или через регулярку маршрута, запуская отдельны скрипт
//
// Данное сообщение также автоматом закрывает web-view
// Но пробелм с закрытием нет, это выполняется в JS итак с помощью
// кода window.Telegram.WebApp.sendData(JSON.stringify(formData));
// или при выполнении window.Telegram.WebApp.close()
//
// Обычно используется для игры в web_view для вывода очков после победы или проигрыша
if (data.tg_query_id && data.tg_query_id.length > 0) {
  //https://core.telegram.org/bots/api#sentwebappmessage
  //https://core.telegram.org/bots/api#inlinequeryresultarticle
  bot.sendPayload('answerWebAppQuery', {
    "web_app_query_id": data.tg_query_id,
    "result": {
      "type": "article", 
      "id": "1", // Unique identifier for this result, 1-64 Bytes
      "title": "Форма заполнена", 
      //"description": "Description", 
      "input_message_content": {"message_text": "Спасибо! Данные сохранены!"}
    }
  })
}
*/

memory.setJsonAttr('data', data)
memory.setAttr('is_qualified', data.is_qualified ? 'Да' : 'Нет')
memory.setAttr('has_experience', data.has_experience ? 'Да' : 'Нет')
Ваши данные:
Имя: {{ &$$data.name }}
Email: {{ &$$data.email }}
Возраст: {{ &$$data.age }}
Профессия: {{ &$$data.specialization}}
Ваш адрес проживания: {{ &$$data.address}}
Прошел курсы в Центре подготовки космонавтов: {{ &$is_qualified }}
Я уже летал в космос (имею опыт): {{ &$has_experience}}

Все данные в виде JSON:
{{ &$$data }}
Все верно?

image.png

Условия кнопок необходимы для вызова скрипта соответствующего текущему формату формы.

Условие для вывода универсальной формы в виде ссылки:

let mode = memory.getAttr('mode')
if (mode === 'tg_inline') {
  return true
}

Условие для вывода формы в Web App при нажатии на inline-кнопку:

let mode = memory.getAttr('mode')
if (mode !== 'tg_inline' && mode !== 'tg_keyboard') {
  return true
}

Условие для вывода формы в Web App при нажатии на keyboard-кнопку:

let mode = memory.getAttr('mode')
if (mode === 'tg_keyboard') {
  return true
}

Методология

Методология

Методика разработки дизайна

1. Введение

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

Для достижения этой цели дизайнер должен организовать рабочий процесс так, чтобы:


2. Этап визуальной концепции (мудборды)

2.1. Назначение и значение этапа

Мудборд (moodboard) — это инструмент визуализации стилистического направления проекта. Он представляет собой подборку изображений, шрифтов, цветовых сочетаний и композиционных решений, отражающих предполагаемое настроение и атмосферу будущего продукта.

Создание мудборда необходимо для:

Рекомендуемые материалы:

2.2. Практические рекомендации


3. Типографика

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

Рекомендуемые материалы:

Рекомендации:


4. Сетки и структурирование контента

Сетка является основой композиционной организации макета. Она обеспечивает визуальный порядок и согласованность элементов.

Рекомендуемые материалы:

Рекомендации:


5. Цветовое решение

Цветовая палитра формирует эмоциональный отклик и визуальный баланс интерфейса. Цвет должен подбираться осознанно, с учётом целевой аудитории и контекста бренда.

Рекомендуемые материалы:

Рекомендации:


6. Композиция

Композиция определяет визуальный ритм, баланс и иерархию элементов интерфейса.

Рекомендуемые материалы:

Рекомендации:


7. UI Kit (дизайн-система проекта)

UI Kit — это систематизированный набор компонентов интерфейса (кнопки, поля ввода, карточки, иконки, стили текста и пр.), который обеспечивает единообразие визуальных решений.

Рекомендуемые материалы:

Рекомендации:


8. Автолейауты

Автолейаут (Auto Layout) — инструмент Figma, позволяющий создавать гибкие и адаптивные интерфейсы.

Рекомендуемые материалы:

Рекомендации:


9. Кликабельные прототипы

Кликабельный прототип имитирует поведение готового продукта и позволяет наглядно продемонстрировать структуру и логику интерфейса заказчику.

Рекомендуемый ресурс:

Рекомендации:


10. Коммуникация с заказчиком

10.1. Прямое взаимодействие

Дизайнер должен всегда взаимодействовать с заказчиком напрямую, а не через посредников (например, менеджеров без дизайнерской компетенции).
Прямой контакт позволяет:

Отсутствие прямой коммуникации часто приводит к некорректным правкам и неверному пониманию целей проекта.


10.2. Работа с брендбуком

Если у заказчика имеется брендбук, дизайнер обязан строго следовать указанным в нём правилам:

Если брендбук отсутствует, недопустимо ориентироваться на формулировки вроде «сделайте, как у нас на сайте».
Существующий сайт может содержать устаревшие или некорректные решения, поэтому дизайнер должен опираться на профессиональные стандарты, принципы эргономики и визуальной гармонии.


10.3. Согласование и правки


Заключение

Методика разработки дизайна для заказчиков основана на профессиональной экспертизе, чёткой коммуникации и документировании решений.
Дизайнер обязан выступать не исполнителем пожеланий, а экспертом, формирующим визуальную стратегию проекта.
Только системный подход, прямой диалог с заказчиком и следование правилам визуальной организации позволяют создать эффективный и качественный продукт.

Методология

Стандарты Web UI

1. Цель документа

Этот документ устанавливает единые стандарты работы над дизайном для заказчиков, чтобы минимизировать правки, повысить предсказуемость процесса и обеспечить стабильное качество результата. Он помогает команде говорить с клиентами на одном языке, быстрее согласовывать решения и формировать профессиональный подход к каждому проекту.


2. Базовые принципы


3. Процесс работы (Pipeline)

Шаг 1. Получение задачи и всех материалов

Шаг 2. Прототипирование

Шаг 3. Согласование прототипа с менеджером и Art (Artem)

Шаг 4. Полноценный дизайн

Шаг 5. Передача дизайна фронту

Шаг 6. Доработки на этапе интеграции


4. Требования к дизайну (Design Standards)

4.1 Сетка

Сетка — основа композиции и структурирования контента. Она обеспечивает порядок, предсказуемость и согласованность элементов внутри макетов.
Сетка должна быть создана в Figma инструментом Layout Grid и применена ко всем фреймам и страницам проекта.

4.1.1 Типы сеток

В Figma используются 3 вида сеток:

  1. Column Grid (колонки)
    Подходит для лендингов, сайтов, сложных интерфейсов, адаптивной верстки.
    Примеры использования:

    • 12 колонок с gutter 20–32

    • 8 колонок для мобильных

    • 4 колонки для компактных блоков

  2. Row Grid (ряды)
    Используется реже, но помогает выстроить строгий вертикальный ритм.

  3. Grid (равномерная сетка)
    Идеальна для карточек, плиток, галерей, иконок, dashboard-интерфейсов.

4.1.2 Выбор сетки

Выбор сетки зависит от задач проекта:

Важно:
Сетка должна быть выбрана и утверждена на этапе прототипа и неизменно применяться на всех страницах.

4.1.3 Применение сетки

image.png

image.png


4.2 Auto Layout

Auto Layout — обязательный инструмент для всех элементов.
100% блоков, карточек, кнопок, форм и секций должны быть собраны через Auto Layout.

4.2.1 Основные правила Auto Layout

4.2.3 Почему нельзя использовать ручные отступы

4.2.4 Стандартизация отступов

Отступы — не «на глаз». Их нужно утверждать заранее и использовать одинаково по проекту.

Примеры стандартизации:

Все эти значения:

4.2.5 Variables для отступов и размеров

Если проект крупный, количество размеров растёт. Чтобы не хранить их в голове — используется:

Figma Variables → Spacing Variables

Создаются переменные:

Преимущества:

4.2.6 Auto Layout как структура интерфейса

Любой сложный блок собирается как дерево Auto Layout:

Секция → Контейнер → Колонка/Ряд → Компоненты → Элементы

Для примера:

4.3 Типографика

4.3.1 Шрифты

4.3.2 Стиль текста (Text Styles)

image.png

4.3.3 Иерархия и насыщенность

4.4 Цвета

4.4.1 Основная палитра

4.4.2 Color Styles

image.png


4.4.3 Градиенты и эффекты

4.5 Компоненты

4.5.1 Общие правила

4.5.2 Состояния

Каждый компонент обязан иметь необходимые стейты:

4.5.3 Примеры компонентов

4.6 UI KIT

4.6.1 Структура и хранение

4.6.2 Обновление

4.6.3 Использование

image.png


5. Требования к верстке (Frontend Standards)

Фронтенд-разработка должна быть структурированной, стандартизированной и основанной на утверждённом дизайне. Все правила ниже направлены на уменьшение количества правок, повышение стабильности и ускорение разработки.


5.1 Структура проекта и организация репозитория

  1. Создание CORE-репозитория
    Ведущий фронтендер создаёт базовое Git-хранилище (CORE), где размещаются:

    • основная структура проекта

    • базовые зависимости

    • настройки линтеров и форматтеров

    • глобальные темы (цвета, размеры, шрифты)

    • базовая архитектура модулей

    • единые UI-компоненты

  2. Фиксация архитектурного подхода
    До начала разработки команда фиксирует:

    • структуру директорий

    • логику именования компонентов

    • способ подключения стилей

    • принципы адаптива

    • оформление глобальных переменных (теминг, токены)

  3. Разделение задач внутри команды

    • Один разработчик занимается версткой (UI), другой — логикой и бизнес-правилами, третий — интеграцией.

    • Или: один делает страницу А, другой — страницу Б.
      Важно: разные разработчики не пересекаются в одних и тех же блоках, чтобы избежать конфликтов.

  4. Сборка результата перед тестированием
    После завершения задач ведущий фронтенд:

    • собирает работу команды в отдельную ветку (например feature/ui-assembly)

    • приводит код к единым стандартам

    • проверяет консистентность компонентов

    • отправляет версию на тестирование

5.2 Требования к стилям

  1. Работа строго по дизайн-системе

    • Цвета, размеры, расстояния, шрифты — строго по UI Kit, без самодеятельности.

    • Никаких произвольных значений в стилях: каждый размер должен соответствовать токену дизайна.

  2. Использование токенов (design tokens)
    Все базовые параметры оформляются как переменные:

    • токены цветов

    • токены типографики

    • токены отступов

    • токены компонентов
      Это исключает расхождения между дизайном и версткой.

  3. Единая система отступов
    Все spacing-значения берутся только из списка утверждённых размеров.
    Никаких случайных значений типа 17px, 23px.
    Если в дизайне 30px → значит 30px в коде.

  4. Модульность и переиспользование

    • Каждый визуальный элемент должен быть оформлен как компонент.

    • Общие компоненты (кнопки, карточки, поля) находятся в CORE и не копируются локально.

  5. Адаптивность

    • Используются те точки перелома, которые указаны в дизайн-системе.

    • Строго соблюдается сетка, выбранная в дизайне.

    • Разработчик не имеет права менять структуру блоков без согласования.

  6. Пиксельная точность (Pixel-Perfect)

    • Все размеры и расстояния должны соответствовать макету.

    • Допуск по отклонениям — такой, какой установил дизайнер при передаче макетов (обычно ±10 px).

5.3 Работа с компонентами


5.4 Кодовые стандарты

  1. Линтеры обязательны (ESLint / Stylelint / Prettier) — ведущий фронт настраивает их в CORE.

  2. Коммиты оформляются по единому стандарту (Conventional Commits или договорённый вариант).

  3. Структура кода не должна зависеть от личных предпочтений каждого разработчика.

  4. Каждый PR должен проходить:

    • code review

    • проверку соответствия макету

    • проверку на совпадение с токенами дизайна


5.5 Требования к взаимодействию с дизайном

  1. Фронт старается задать все вопросы до начала разработки, а не в процессе.

  2. Если элемент кажется неполным — дизайнер обновляет UI Kit, а не разработчик «делает как кажется логичным».

  3. Любые расхождения между макетом и версткой фиксируются.

  4. После сборки версии — проводится walkthrough с дизайнером.



6. Коммуникации

Коротко:


7. V0.1 → Что будем добавлять дальше

Например:



Методология

Типовые ошибки в дизайне интерфейсов при переходе от “рисования” к реальной UI/UX-разработке

(и как их избежать)

Когда дизайнер начинает работать не просто над красивыми картинками, а над настоящими интерфейсами (мини-аппы, формы, продукты, панели, CRM), всплывают типичные ошибки. Это не потому, что дизайнер “плохой”, а потому что UI — это инженерная дисциплина, а не художественная.

Вот ключевые ошибки, которые совершают 90% начинающих дизайнеров — и рекомендации, как это исправить.


🔶 Ошибка 1. “Картинка вместо системы”

Дизайнер делает интерфейс как постер в Photoshop:

Почему это плохо

Такой дизайн невозможно:

Как правильно

UI — не картинка, а система элементов:

✔ Совет:

Представляй интерфейс не как рисунок, а как скелет + мышцы + кожа.
Сначала структура (скелет), потом функциональные группы (мышцы), потом визуал (кожа).


🔶 Ошибка 2. Злоупотребление автолэйаутами (“20 контейнеров ради контейнеров”)

Новичок услышал “автолэйаут — это база” и начинает:

Почему это плохо

Как правильно

Автолэйаут — не цель, а инструмент.

Использовать его нужно:

✔ Совет:

Если элемент не должен тянуться → не делай его резиновым.
Если элемент не является логической группой → не кладите его в контейнер.


🔶 Ошибка 3. Непонимание паттернов адаптивности

Новички делают:

Почему это плохо

В реальном интерфейсе:

Как правильно

Задавать:

✔ Совет:

Тестируй интерфейс в Figma: сожми фрейм → растяни → проверь, как ведут себя элементы.


🔶 Ошибка 4. Отсутствие компонентного мышления

Новички делают:

Почему это плохо

Как правильно

Мыслить компонентами:

Все вариации — в Variants.

✔ Совет:

Проектируй интерфейс так, будто его собирают в React — из готовых компонентов.


🔶 Ошибка 5. Непонимание типографики

Типовые проблемы новичков:

Как правильно

✔ Совет:

Хорошая типографика = 70% ощущение “дорогого” интерфейса.


🔶 Ошибка 6. Цвет как “интуиция”, а не система

Новички:

Правильно

✔ Совет:

Цвет = язык.
Его нельзя менять “на вкус”.


🔶 Ошибка 7. Ручные отступы vs система отступов

Новичок ставит:

И сам путается, фронтендер путается, всё плавает.

Правильно

Использовать единую сетку:
4pt / 8pt / 16pt — и никаких случайных чисел.

✔ Совет:

Если отступ нельзя объяснить — он неправильный.


🔶 Ошибка 8. Нет связи дизайнер ↔ фронтендер

Типично:

Правильно

2 мини-связки:

1️⃣ Передача макета →
дизайнер делает walkthrough:
“Вот компоненты, вот группы, вот отступы, вот логика адаптива.”

2️⃣ Перед стартом верстки →
фронтендер задаёт вопросы.

✔ Совет:

UI — командная работа, не сольная.


🔶 Ошибка 9. “На глазок” вместо UX

Новички принимают визуальные решения без понимания:

Правильно

Каждому элементу нужен ответ:

✔ Совет:

UX начинается не в Figma, а в голове.


🔶 Ошибка 10. Непонимание важности прототипа

Новички сразу рисуют дизайн, пропуская этап каркаса:
несогласованная логика, невидимые ошибки, “симпатичная каша”.

Правильно

Сначала прототип (черно-белый), потом UI.

✔ Совет:

Прототип — 80% успеха.


🟧 Резюме: что нужно, чтобы перестать допускать эти ошибки

✔ 1. Автолэйаут — да, но с головой

Не везде и не “ради галочки”.

✔ 2. Компонентное мышление

Думай как React-разработчик: системой.

✔ 3. Сетка и типографика

Строгая дисциплина, минимум хаоса.

✔ 4. Прототип → дизайн → верстка

Без прыжков через этапы.

✔ 5. Обратная связь от фронтендера

Каждый макет должен пройти “технический осмотр”.

✔ 6. Менее “рисовать”, больше “проектировать”

UI — это инженерия.

✔ 7. Постепенно: не революция, а эволюция

Каждый проект — улучшение на 5–10%.

Методология

Тренды в UI/UX: перенос архитектуры на этап дизайна

В данной публикации показан тренд куда движется UI/UX как индустрия.


✔ 1. Автолэйаут — это перекладывание архитектурного мышления на дизайнера

Это ключевой момент.

Раньше:
дизайнер рисовал “красивую картинку” → фронтендер разбирал её мозгами, как умеет → получалось, как получится.

С автолэйаутами:
дизайнер строит структуру, аналогичную HTML/CSS:

👉 То есть дизайнер перестаёт быть “художником”
и становится архитектором интерфейса.

Это то, чему учат в сильных школах интерфейса.
Это то, чего не хватает большинству новичков.


✔ 2. Figma эволюционирует в сторону “дизайн = полуверстка”

Это важный тренд.

Figma:

Мир идёт к тому, что:

“Дизайн → почти готовая верстка”.


✔ 3. Продумывание архитектуры должно быть на этапе дизайна, а не на этапе разработки

Это чистая истина.

Есть золотое правило:

Каждый час, потраченный дизайнером на архитектуру, экономит 3–5 часов разработки.

Поэтому автолэйаут:


✔ 4. Эта культура делает дизайнеров сильнее

Дизайнер, который:

— это уже не “рисуем-как-чувствуем”.
Это уже UI/UX-специалист уровня компании.

Это стратегически правильно.


✔ 5. В будущем часть разработки будет делаться автоматически

Уже сейчас:

Через 2–3 года:

“Макет → код” станет обычной практикой.
А разработчики будут дорабатывать только логику и интеграции.

Это направление всей индустрии.


✔ 6. А теперь ключевое:

Автолэйаут — это НЕ про “делать всё правильно”.

Это про движение команды к будущему, где дизайн = система.

Важно строить систему:

→ это полностью совпадает с трендами.

Но:

👉 Если команда НЕ ГОТОВА пересесть на это сразу.

Нужно:


✔ 8. Сложно ли этому научиться?

Для нормального дизайнера:

🔹 Базовый автолэйаут — 1 день
🔹 Уверенная работа — 3–5 дней
🔹 Грамотные сложные структуры — 2–3 недели
🔹 Полное мышление “как фронтендер” — 1–2 месяца

Это реализуемо.
Только нужно:

Методология

Как понимать контекст проекта

Этот материал объясняет:


🟧 Два мира разработки: продукты vs мини-аппы. Как отличать, как работать и какие процессы нужны

Мы сталкиваемся с разными типами проектов. Где-то мы делаем полноценный продукт, который живёт годами, влияет на бизнес-процессы и проходит десятки согласований. А где-то — небольшой мини-апп, простой инструмент или небольшую коммуникационную вставку внутри чат-бота.

Чтобы команда работала эффективно и не пыталась применить “тяжёлые” процессы там, где они только мешают, важно понимать фундаментальную разницу между двумя классами проектов:

Это два разных мира, требующих двух разных подходов — в дизайне, разработке, коммуникациях и управлении ожиданиями.


🟧 1. Что такое Mission Critical, и почему эти проекты другие

Mission Critical — это продукты, без которых бизнес не сможет выполнять свою основную функцию.

Примеры:

У промышленной компании, например фармацевтической, mission critical — это производство таблеток, логистика, ERP, склад, документоборот.

Чат-боты, мини-формы, калькуляторы, маркетинговые инструменты — не mission critical.
Их можно улучшать, допиливать, полировать, но они не остановят бизнес.

Отличительные признаки Mission Critical:

Там нет “примерно нормально” — там только идеально и надёжно.


🟧 2. Что такое Non-Mission-Critical (наши текущие мини-аппы)

Мини-аппы, калькуляторы, формы, промо-страницы, быстрые B2B-калькуляторы в мессенджерах — это дополняющие интерфейсы.

Они помогают:

Но не являются ядром бизнеса.

Отличительные признаки таких проектов:

Это не значит “делать плохо”, это значит не переусложнять там, где это не нужно.


🟧 3. Почему процессы должны быть разными

Если на non-mission-critical проекты натянуть процессы от больших продуктовых студий, произойдёт следующее:

Но если на mission critical проект натянуть подход “как для мини-аппа” — бизнес словит катастрофу.

Поэтому нужен двухрежимный подход.


🟧 4. Должен ли дизайнер встречаться с заказчиком? Да — но не всегда.

Два типа проектов = два типа участия дизайнера.

🟩 Тип 1. Полноценные продукты — дизайнер участвует активно

Тут дизайнер — не художник и не исполнитель.
Он — аналитик, переговорщик, продуктовый партнёр.

Его обязанности:

Без прямой коммуникации тут работать невозможно.


🟦 Тип 2. Мини-аппы — дизайнеру достаточно 1-2 встречи

В этих проектах:

Поэтому дизайнеру достаточно:

Прямой контакт с клиентом после этого ничего не улучшит, а часто только ухудшит:

Дизайнер защищает эстетику и логику →
Менеджер фильтрует запросы →
Команда работает спокойно.

Это правильный процесс.


🟧 5. Разные типы клиентского опыта (CX): “деловой” vs “приятный”

Чтобы правильно строить интерфейсы, важно понимать разницу между двумя видами клиентского опыта.

🟩 Тип 1. “Деловой” клиентский опыт (B2B, сервисный, утилитарный)

Цель: дать результат быстро, чётко и без лишних действий.

Примеры:

Тут интерфейс должен быть:

Это “инструмент”, а не “впечатление”.


🟦 Тип 2. “Приятный” клиентский опыт (развлечения / эмоции / smacking experience)

Цель: вовлечь, развлечь, удержать внимание.

Примеры:

Здесь можно:


🟧 6. Как классифицировать проект за 1 минуту

Задаём два вопроса:

❓ Если интерфейс сломается — пострадает ли основная миссия бизнеса?

Если да → это mission critical → полноценный продуктовый процесс.
Если нет → это мини-апп → упрощённый процесс.


🟧 7. Что важно

  1. Не надо натягивать продуктовые процессы на мини-аппы.
    Это убивает скорость и ценность проекта.

  2. Не надо упрощать миссион-критичные проекты.
    Там нужна глубина.

  3. Дизайнер не должен общаться с заказчиком там, где это во вред.
    Коммуникация — ресурс, его надо дозировать.

  4. Нужно уметь классифицировать тип задачи с первых минут проекта.

  5. Наша цель — гибкость.
    Мы должны одинаково уверенно вести оба типа проектов.


🟧 8. Тонкое различие, которое важно осознать

Мы не “студия, которая делает всё одинаково”.
Мы команда, которая умеет менять подход под контекст.

Где-то нужен продуктовый UX, десятки встреч и прототипы.
Где-то достаточно одного звонка, одного прототипа и менеджерского фильтра.

Зрелость = умение различать.

Уроки

Уроки

Как построить визуальный no-code редактор воронок и AI пайплайнов на React поверх low-code ядра Metabot

image.pngПопробовать: https://cjm.metabot24.ru/cjm-designer 

Этот урок — про то, как поверх Metabot (который сам по себе полный low-code/full-code backend для чат-ботов и ассистентов) построить новый продукт: визуальный конструктор чат-ботов.

То есть вы создадите реальный продуктовый интерфейс — свой ManyChat / BotHelp, но умнее и современнее.

Главная идея:

Metabot — это low-code/full-code ядро.
Пройдя этот урок, вы научитесь строить целые продукты поверх ядра, создавая собственный фронт, визуальный редактор, AI-функции и конструкторы.

Часть 1. Что будет в этом уроке — и чего здесь не будет

Чтобы вы сразу понимали рамки: этот урок — это фундамент, первая версия конструктора, на котором вы сможете дальше строить полноценный продукт. Здесь мы создаём работающий скелет визуального редактора и настраиваем импорт в Metabot.

А теперь — по пунктам.


Что будет в уроке

1. Создадим собственный JSON-формат воронки

Вы:

То есть вы спроектируете свой DSL (domain-specific language) — язык описания воронок вашего продукта.


2. Разберётесь, как работает Mapper

Мы подробно посмотрим, как:

То есть вы поймёте логику трансформации вашего формата → формат Metabot.

Вы получите исходный код Mapper'а.


3. Разберётесь, как работает Builder

Мы детально пройдём по механике:

И главное — вы увидите, как Builder напрямую манипулирует базой данных Metabot (PostgreSQL) через PHP/V8.

Это даёт понимание того, как работает ядро платформы и как можно строить свои надстройки.

Вы получите исходный код Builder'а.


4. Научитесь подключать Metabot как backend

Вы увидите, как:

То есть Metabot превращается для вас в backend-as-a-platform, который вы можете использовать под любые проекты.


5. Сделаем визуальный редактор на React (с помощью V0 + Cursor)

Мы:

Вы получите:


6. Настроим рабочий цикл разработки

Первые итерации будут такие:

1) создаём JSON вручную → 
2) отправляем через Postman → 
3) смотрим, как Metabot создал воронку →
4) фиксируем ошибки →
5) делаем свои команды →
6) строим визуальный редактор, который уже генерит нужный JSON →
7) импортируем в Metabot одной кнопкой

Это честный, практичный, продуктовый процесс.


7. Вы получите всё, чтобы создать свой продукт

В итоге у вас будет:

На этом можно строить:


🚫 Чего в этом уроке НЕ будет

Чтобы не вводить в заблуждение — сразу фиксируем:

1. Не будет двусторонней синхронизации

Мы делаем только импорт из вашего конструктора → Metabot.

То есть:

Это осознанное упрощение.

Вторую сторону можно сделать, но это отдельный блок задач:
читать данные Metabot → конвертировать → отображать → синхронизировать версии.

Мы это оставляем для следующего урока, если будет запрос.


2. Не будет полноценного менеджмента трафика

Это очень важный момент.

В реальной жизни по ранее импортированной воронке уже могут идти живые пользователи.
Если вы меняете воронку, возникают вопросы:

Это целая философия, и у каждого продукта — свой подход.
Мы эту сложность сознательно не включаем в этот урок.

Но если понадобится понадобится помощь — дайте знать, попробуем помочь.


3. Не будет полноценной "платформенности"

В этом уроке:

Мы делаем первую версию конструктора, но она уже рабочая и расширяемая.


Принял. Делаю первую итерацию — без фанатизма, но уже структурно, лаконично, красиво, чтобы можно было вложить в документацию и развивать дальше.
Ты дал огромный объём информации, поэтому я сейчас фиксирую каркас главы 2, + делаю первый блок: спецификация команд Metabot + JSON-формат, + простую таблицу команд, красиво оформленную, + мини-описание модели данных (sentences, phrases, references).

В следующей итерации я смогу:


Глава 2. JSON-формат и фундаментальная модель данных Metabot

Чтобы создать собственный визуальный конструктор поверх Metabot, нужно понимать две вещи:

  1. Какие команды реально существуют в ядре платформы

  2. Как Metabot хранит эти команды в базе данных

Эта глава даёт разработчику язык конструктора — JSON-формат шага, таблицу команд и модель хранения внутри Metabot.


2.1. Базовая модель данных Metabot

Метабот — low-code движок. Его диалоговая логика физически живёт в трёх таблицах:

sentences — это скрипты

Каждый шаг в нашем редакторе превратится в отдельный скрипт Metabot.

Поле Что значит
id ID скрипта
code Уникальный код (мы сами создаём)
name Читабельное имя
bot_id Бот, которому принадлежит скрипт
section_id Папка/категория (наша “воронка”)
... Остальные поля нам пока не нужны

Пример:

id: 85681
code: "test-script"
name: "TestScript"
bot_id: 2370

phrases — это команды внутри скрипта

Одна запись = одна команда.
Каждый шаг в нашем редакторе может порождать от 1 до 10 фраз.

Поле Описание
id ID команды
type Тип команды (send_text, run_javascript, …)
sentence_id Скрипт, которому команда принадлежит
content Текст или JSON-контент команды
sort_order Порядок выполнения
alias Уникальный ID команды

Пример вывода send_text:

type: "send_text"
sentence_id: 85681
content: "Hello, World!"
sort_order: 0

Пример ввода пользователя value_input:

id: 231462
type: value_input
sentence_id: 85681
content: {"attribute_key":"var","prompt":"Введите число",...}
sort_order: 1

references — это кнопки (меню)

Меню — это не фраза, а отдельная таблица (так исторически, да). Каждая запись — это кнопка.

Поле Значение
sentence_id Скрипт, которому меню принадлежит
caption Текст кнопки
code Код/значение кнопки
jump_sentence_id Куда переходить
sort_order Порядок кнопок
line_num Для многострочных клавиатур
condition_script_code Условие отображения

Пример:

sentence_id: 85681
caption: "Start Over"
code: "1"
jump_sentence_id: 85680

📌 Все 21 команды Метабота

На момент написания этого урока в Metabot есть 20 команд в скриптах + одна неявная команда (меню).

Команда Что делает Где хранится Пример content в БД
1 send_text отправляет текстовое сообщение phrases Hello, World!!!
2 value_input запрашивает ввод и валидирует phrases {"attribute_key":"abc","prompt":"Введите число", ...}
3 run_javascript выполняет JS на сервере phrases lead.setAttr('abc', 123);
4 send_image отправляет изображение phrases {"path":"bot/.../logo.png","name":"logo.png"}
5 send_file отправляет файл (любые документы) phrases {"path":"bot/...","name":"file.pdf"}
6 run_sentence выполняет другой скрипт, останавливает текущий phrases 85681
7 send_email отправляет email phrases {"recipient":"user@mail","subject":"...","body":"..."}
8 run_trigger запускает триггер phrases {"trigger_id":"7222","run_at":"2025-06-11","run_after_sec":300}
9 set_lead_status меняет статус лида phrases 7783
10 add_lead_tags добавляет теги phrases "tag"
11 remove_lead_tags удаляет теги phrases "tag"
12 nlp_detect_intent определяет интент phrases {"text_to_detect":"intent"}
13 run_js_callback делает JS-callback phrases {"prompt":"...","error_message":"...","js_code":"..."}
14 async_api_call async вызов API (например, обращение к LLM) phrases {"js_code":"...","is_immediate_call":1}
15 add_lead_contexts добавляет контекст phrases "context"
16 remove_lead_contexts удаляет контекст phrases "context"
17 forward_to_operator переводит на оператора phrases 7784
18 return_to_bot возвращает в бота phrases 7783
19 repeat_sentence повторяет текущий скрипт phrases {}
20 stop полностью останавливает выполнение phrases {}
21 menu (reference) кнопки меню, переходы, inline actions references caption="Start", jump_sentence_id=85680

Палитра команд платформы:

image.png

Короткое объяснение логики исполнения команд


«Зачем нам свой конструктор и свои команды»

После того как мы разобрались, какие команды реально существуют внутри Metabot и посмотрели, как они физически хранятся в базе (sentences, phrases, references), мы можем перейти к ключевой идее нашего урока:

Мы не будем создавать интерфейс, который один-в-один повторяет Metabot.
Мы создадим свой собственный визуальный конструктор, и в нём у нас будут свои команды — более удобные, более компактные и намного более выразительные.

Почему так?


Почему в Metabot всё разбито на 21 команду

(и почему это отлично для движка, но неудобно для дизайнера чат-ботов)

Metabot — это low-code ядро. Каждая команда — это минимальная атомарная операция:

Это правильно для движка:
движок должен быть предсказуемым и детерминированным.

Но для человека, который рисует сценарий, это слишком низкий уровень.
Чтобы собрать один «узел» диалога, дизайнеру нужно вручную собрать:

В итоге один визуальный узел превращается в:

→ 6–12 низкоуровневых фраз в базе
→ 2–4 скрипта (sentences)
→ и пачку записей в references

Именно поэтому мы не хотим заставлять пользователя руками разбираться в 21 команде ядра. Мы хотим сократить этот слой.


Теперь проектируем наш собственный редактор

Мы создаём свою систему узлов — такую, в которой:

Это и есть смысл маппера и билдера.


Какие наши команды мы сделаем первыми


1. Start / Entry Node

Стартовый узел воронки.
Даёт визуальную точку входа + будущую связь с диплинками.

Вход в воронку возможен как из других воронок / сценариев, так и снаружи при переходе в мессенджер из приложений или из контента в Интернет.

Пример шага в новом no code редакторе:

image.png

Пример настроек в новом no code редакторе:

image.png

Пример JSON-кода шага:

{
  "code": "entry_point_main",
  "type": "entry_point",
  "name": "Старт",
  "next_step": "log_action_2ec8cbbe",
  "deep_links": [
    {
      "code": "waylogger",
      "title": "New Deep Link",
      "active": true,
      "parameters": [
        {
          "key": "ref_source",
          "value": "vk_ads"
        },
        {
          "key": "source",
          "value": "cpc"
        },
        {
          "key": "campaign",
          "value": "waylogger"
        },
        {
          "key": "content",
          "value": "1"
        }
      ]
    }
  ],
  "coordinates": {
    "x": 650,
    "y": 728
  }
}

2. Send Text (расширенная)

Это не просто send_text из Metabot.
Наша версия включает:

В Metabot для этого потребовалось бы от 6 до 12 низкоуровневых команд.
У нас — один визуальный блок.

Пример шага в новом no code редакторе:

image.png

Форма редактирования стандартной команды "Отправить текст" в low-code редакторе:

image.png

Форма редактирования новой команды "Отправить текст" в новом no-code редакторе, сравните возможности:

image.png

JSON:

{
  "code": "send_text_27607f82",
  "type": "send_text",
  "content": "3. Кто вы?",
  "next_step": "log_action_a47418f3",
  "buttons": [
    {
      "title": "Владелец проекта",
      "next_step": null,
      "input_code": "1",
      "js_condition": "",
      "value": "Владелец проекта",
      "add_tags": [],
      "remove_tags": []
    },
    {
      "title": "Маркетолог",
      "next_step": null,
      "input_code": "2",
      "js_condition": "",
      "value": "Маркетолог",
      "add_tags": [],
      "remove_tags": []
    },
    {
      "title": "Разработчик",
      "next_step": null,
      "input_code": "3",
      "js_condition": "",
      "value": "Разработчик",
      "add_tags": [],
      "remove_tags": []
    },
    {
      "title": "Другое",
      "next_step": null,
      "input_code": "4",
      "js_condition": "",
      "value": "Другое",
      "add_tags": [],
      "remove_tags": []
    }
  ],
  "buttons_value_target": {
    "scope": "lead",
    "key": "role"
  },
  "log_way_steps": [
    {
      "type": "step",
      "way": "waylogger",
      "step": "role_selected",
      "event": "",
      "tag": "",
      "tag_action": "add",
      "utter": ""
    }
  ],
  "coordinates": {
    "x": 197,
    "y": 1044
  }
}

3. Записать в аналитику

Под "капотом" это будет метаботовская команда run_javascript в которой будет вызов плагина аналитики WayLogger.
Мы прячем всё JS-пекло от пользователя.

image.png

В нашем интерфейсе есть:

image.png

JSON:

{
  "code": "log_action_a47418f3",
  "type": "log_action",
  "log_type": "step",
  "way": "waylogger",
  "step": "finished_quiz",
  "event": "",
  "tag": "",
  "tag_action": "add",
  "utter": "",
  "next_step": "send_pdf",
  "coordinates": {
    "x": 474,
    "y": 1142
  }
}

4. Переход к следующему шагу

В Metabot это команда run_sentence.

image.png


В нашем редакторе — просто стрелка между узлами. 

image.png

А в JSON это параметр шага, кнопки и так далее, например, next_step.


5. Обращение к LLM (call_llm)

В Metabot — большой JS код с настройками в команде async_api_call. Например:

const LLMClient = require("Common.MetabotAI.LLMClient")

const llm = new LLMClient("UserDialog")
llm.setProvider("OpenAI")
llm.setModel("gpt-3.5-turbo")
llm.setErrorScript("SupportBot:ErrorFallback")
llm.setPromptTable("gpt_prompts", "SupportBot")

if (isFirstImmediateCall) {
  llm.addSystemPrompt(`Ты должен ответить пользователю, используя найденную информацию из базы знаний.`)
  llm.addSystemPrompt(
    `Если в базе знаний будут найдены ссылки на медиа контент, `+ 
    `определи релевантность контента, проанализировав их описание, и включи релевантные ссылки в формате markdown. ` +
    `Сам ссылки не придумывай!`)
  llm.addSystemPrompt(`Информация из базы: {{$kb_chunks}}`)
  llm.addSystemPrompt(`Распознаннный интент: {{$user_intent}}`)
  llm.addHistoryToPrompts() // Добавляем историю
  llm.addUserPrompt(`{{$user_input}}`)
  llm.addSystemPrompt(`$UserDialog:final`) // Промпт агента из таблицы
  llm.addSystemPrompt(`@общий`) // Общий промпт из таблицы

  llm.prepareRequest()
  return llm.sendRequest()
}

llm.handleResponse()

llm.sendFormattedResponse()

return true

Это не самый свежий пример кода, но рабочий. Здесь мы делаем:

Всё это — внутри одного команды и требует базового владения JS кодом.

В новом редакторе упакуем все это в один no code узел: 

image.png

И одну форму, чтобы пользователю не пришлось писать JS код — мы его будем генерить за него при импорте в low-code:

image.png

JSON:

{
  "code": "call_llm_ccbfc9c0",
  "type": "call_llm",
  "title": "Формируем ответ LLM",
  "agent_name": "SupportBot",
  "provider": "OpenAI",
  "model": "gpt-3.5-turbo",
  "prompt_table": "gpt_prompts",
  "system_prompts": {
    "start": [
      "Ты должен ответить пользователю, используя найденную информацию из базы знаний.",
      "Если в базе знаний будут найдены ссылки на медиа контент, \nопредели релевантность контента, проанализировав их описание, и включи релевантные ссылки в формате markdown. \nСам ссылки не придумывай!",
      "Информация из базы: {{$kb_chunks}}",
      "Распознаннный интент: {{$user_intent}}"
    ],
    "final": [
      "$UserDialog:final",
      "@общий"
    ]
  },
  "history": {
    "enabled": true,
    "save_to_attr": "chat_history_str",
    "max_length": 4,
    "add_to_prompts": true
  },
  "user_query": {
    "enabled": true,
    "attr": "user_input",
    "add_to_prompts": true
  },
  "response": {
    "enabled": true,
    "save_to_attr": "user_intent",
    "display_to_user": true,
    "format": "markdown"
  },
  "trace_enabled": false,
  "next_step": "search_kb_394b4ae2",
  "error_step": "send_text_47451106",
  "coordinates": {
    "x": -336.29289321881345,
    "y": 2063.166664123535
  }
}

6. Поиск по базе знаний (search_knowledgebase)

Аналогично — в Metabot при импорте создаем JS команду и необходимые связки.

let userIntent = lead.getAttr('user_intent')

// Если интент есть — ищем по базе знаний
if (userIntent) {
  const KnowbaseSearch = require('Common.MetabotAI.KnowbaseSearch')
  const kbSearch = new KnowbaseSearch()
  const chunks = kbSearch.findBestChunks("DefaultKB", userIntent, "default")
  // Сохраняем все, что нашли в kb_chunks 
  lead.setJsonAttr("kb_chunks", chunks)  
  bot.sendMessage(`chunks: ${JSON.stringify(chunks)}`)
} else {
  lead.setJsonAttr("kb_chunks", []) 
}

А в no code —  один узел, один JSON.

image.png

Обратите внимание у узла мы добавили три выхода: успешный, ничего не найдено и ошибка.

image.png

JSON:

{
  "code": "search_kb_394b4ae2",
  "type": "search_knowledgebase",
  "knowbase_name": "DefaultKB",
  "query_attr": "user_intent",
  "domain": "default",
  "save_results_to_attr": "kb_chunks",
  "trace_enabled": false,
  "next_step": "call_llm_417fc51a",
  "not_found_step": "send_text_455c4afe",
  "error_step": "send_text_47451106",
  "coordinates": {
    "x": -337,
    "y": 2417
  }
}

Почему наши команды будут «упакованными»

(концепция compact high-level commands)

Каждая наша команда — это высокоуровневый «комбинированный» блок.
Когда пользователь его настраивает, мы:

То есть пользователь видит простую, компактную визуальную воронку, а под капотом строится настоящая сложная система Metabot с триггерами, скриптами, условиями, логированием и разветвлёнными сценариями.


Экономический эффект / ROI

Чтобы собрать один логический узел вручную, в Metabot приходится склеивать десятки микродействий: несколько сообщений, кнопки, переходы, теги, аналитику, JS-обработчики, триггеры, условия. Каждое из этих действий занимает секунды или минуты. А в реальном проекте — это часы.

Чем сложнее сценарий, чем массивнее продукт — тем больше времени уходит не на смысловую работу, а на рутину: перенести текст, добавить теги, прописать переходы, подправить сортировку, создать скрипт, связать кнопки. Всё это множится в геометрической прогрессии.

И вот здесь становится очевиден смысл no-code-редактора.

Один визуальный узел в нашем редакторе может вместить всю эту низкоуровневую сложность: аналитику, кнопки, ссылки, условия, LLM-вызовы, JS-код, обработку ввода, переходы — всё внутри одного компактного блока с настройками.

Пользователь просто выбирает тип узла, заполняет пару полей — и весь сложный набор действий раскладывается движком автоматически.
Вместо десятка операций — одна.
Вместо нескольких минут — несколько секунд.
Вместо часов — десятки минут.

На дистанции это превращается в огромную экономию ресурсов.
За неделю — это часы.
За месяц — дни.
За год — целые недели чистого рабочего времени, которые раньше уходили на рутину.

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

При этом мы сохраняем лучшее из двух миров:

Нужна свобода — подключили разработчика и дописали кастомный JS или сложную логику.
Нужна скорость — делаем всё в визуальном редакторе.

И это не все: наш JSON-формат позволяет генерировать воронки автоматически.
Вы можете написать в ChatGPT: «Сгенерируй мне воронку для такого-то кейса» — и импортировать её одним кликом.

Та самая воронка на скриншоте в начале этого урока, состоящая из восьми шагов, создаётся в 5–10 раз быстрее, чем через классический low-code.

Это и есть настоящая эффективность:
быстрее создавать — быстрее тестировать — быстрее зарабатывать.


JSON-формат нашего визуального конструктора

После того, как мы разобрались с тем, как устроены низкоуровневые команды Metabot и почему нам нужен собственный слой абстракции, самое время поговорить о JSON-формате, который станет языком нашего визуального редактора.

По сути, мы создаём свой диалоговый DSL/язык доменной области — простой, человекочитаемый, легко генерируемый (вплоть до того, что его можно создавать через ChatGPT). Этот формат затем преобразуется в структуры Metabot: скрипты, команды, переходы, кнопки, аналитику, LLM-вызовы.

Чтобы понимать, как его проектировать, нужно осознать два ключевых отличия от классического low-code формата Metabot.


1. В чём принципиальное отличие от формата Metabot

✔ В Metabot каждая логика — это скрипт, который содержит много команд

Скрипты связаны переходами (run_sentence), а внутри одного скрипта команды выполняются последовательно — как патроны в обойме.

Это отличный формат для движка, но он низкоуровневый и слишком детализированный для проектирования диалогов.


✔ Наш новый формат — это машина состояний

Мы мыслим не скриптами, а шага́ми.

Каждый шаг — это одно состояние, из которого есть переходы в другие состояния.
Похожая логика используется в BPMN, state-machine диаграммах, автоматах Мили/Мура.

Внутри шага могут происходить десятки внутренних действий, но в JSON это будет один объект.

Таким образом:

Этот подход радикально упрощает визуальное проектирование и делает структуру понятной.


2. Общая структура JSON-формата

Мы храним всю воронку в одном JSON-документе, который содержит:

1) Параметры проекта (bot id, версия, формат и т.д.)
2) Массив steps — список шагов машины состояний

Вот упрощённая структура:

{
  "script_request_params": {
    "format": "cjm",
    "version": "1.0",
    "bot_id": 2370,
    "code": "quiz_lead_magnet_tags",
    "title": "Воронка WayLogger",

    "channels": {
      "telegram_bot_name": "metabot",
      "whatsapp_phone_number": "79150465850",
      "vk_group_name": "metabot_platform",
      "use_chat_widget": true
    },

    "steps": [ ... ]
  }
}

Обращаем внимание на обёртку script_request_params без которой платформа Metabot "не услышит" передаваемые данные. 


3. Зачем нужен «format» и «version»

Мы сознательно включаем поля:

Это позволяет:

✔ поддерживать совместимость старых воронок

Если формат JSON изменится (а он будет меняться), старые импорты не сломаются.

✔ нескольким продуктам использовать разные форматы

Один редактор → один формат.
Другой редактор → другой формат.
Все они могут жить в одной экосистеме Metabot.

✔ при импорте определять, как парсить данные

Mapper видит format + version и понимает, какой алгоритм применять.


4. Название и код воронки

Поля:

В метаботе мы формируем имя секции:

cjm:1.0:quiz_lead_magnet_tags:Воронка WayLogger

Секция (папка) создаётся заново при каждом импорте, что даёт чистый, предсказуемый результат.

image.png


5. Массив шагов steps — сердце машины состояний

Каждый шаг описывается JSON-объектом:

Идея проста:

Один наш шаг = целый набор низкоуровневых команд Metabot.

Визуально шаг выглядит как блок на диаграмме (см. скриншот), а в импорте он превращается в цепочку скриптов и команд.


6. Как мы создаём скрипты и команды при импорте

Во время импорта Metabot получает:

Mapper делает следующее:

✔ 1. Находит и удаляет старую cекцию/папку со всеми скриптами

(имя: format:version:code:title)

✔ 2. Создаёт новую секцию

Внутрь неё будут помещаться все скрипты.

✔ 3. Для каждого шага создаёт один или несколько скриптов

Количество зависит от сложности шага:

✔ 4. Формирует уникальные коды скриптов

Это ключевой момент.

Мы склеиваем:

<format>:<version>:<сjm_funnel_code>:<step_code>

Например:

cjm:1.0:quiz_lead_magnet_tags:entry_point_main
cjm:1.0:quiz_lead_magnet_tags:hello_intro
cjm:1.0:quiz_lead_magnet_tags:send_pdf
cjm:1.0:quiz_lead_magnet_tags:log_action_2ec8cbbe

Так обеспечивается:

✔ 5. Внутрь каждого скрипта складывается набор команд (phrases)

Например, шаг «Send Text» превратится в:


7. Почему шагам обязательно нужен уникальный code

Каждый узел должен иметь уникальный идентификатор.
Без него невозможно:

Пользователь может менять коды шагов — но внутри маппера этот код всегда станет частью полного имени скрипта.


8. Что происходит со старой версией воронки

В текущей версии урока мы применяем упрощённый алгоритм:

✔ удалить старую секцию полностью
✔ создать новую с тем же именем
✔ импортировать шаги заново

Это быстрый, предсказуемый, безопасный метод.

Если нужно:


9. Что нужно, чтобы импортировать воронку

Чтобы импортировать JSON:

1. Создаём пользователя с API-доступом

TODO: как это сделать — в отдельном мануале. Обратитесь в поддержку, если здесь все еще нет ссылки и поиск по документации не увенчался успехом.

2. Создаём endpoint в боте

В разделе Internal API создаём endpoint, например, с таким алиасом:

cjm/import

3. Кладём внутрь endpoint JS-код:

const { getSuccessResponse, getErrorResponse } = require('Common.Utils.Response')
const { Mapper } = require('Common.CJM.Mapper')

const {
  id = null,
  format,
  version,
  bot_id,
  code,
  title,
  steps = []
} = request.json || {}

if (!format || !version || !bot_id || !code || !title) {
  return getErrorResponse("Missing required fields: format, version, bot_id, code, title")
}

try {
  const mapper = new Mapper()
  result = mapper.runImport(bot_id, format, version, code, title, steps)
  return getSuccessResponse({result})
} catch (error) {
  return getErrorResponse(`Error saving CJM: ${error.message}`)
}

Всё. Теперь можно импортировать воронки из:


Глава 3. Import: Mapper и Builder — ядро нашего no-code импорта

Теперь, когда мы разобрали сам формат JSON-схемы, можно перейти к механике её импорта в Metabot. На платформе в Common.CJM доступны два ключевых плагина, которые вместе образуют мост между вашим JSON и реальными скриптами в базе данных:

  1. Mapper (JavaScript) — читает JSON, создаёт структуру карты, генерирует команды и меню.

  2. Builder (PHP) — низкоуровневый слой, который напрямую создаёт секции, скрипты, команды и переходы в базе.

Эти плагины доступны всем пользователям платформы, но важно понимать их статус:



Как работает связка Mapper → Builder

Чтобы показать структуру максимально практично, мы дальше будем двигаться блок за блоком:
показываем фрагмент кода → объясняем, что он делает → даём рекомендации, где можно улучшить.

Но вначале — краткая логика работы всей цепочки.


1. Устанавливаем контекст: выбор бота и проверка доступа

Mapper — это JS-плагин, но он работает через Builder, а Builder — PHP-уровень, который:

Поэтому первым шагом Mapper вызывает:

setBotById(botId)

Если бот не относится к текущему бизнесу — импорт прерывается.
Это фундаментальный слой безопасности, и Builder строго следит, чтобы вы не могли повредить данные другого бота.


2. Создаём секцию под карту

При импорте мы:

Это важно, потому что карта имеет жёсткую целостность:
скрипты, переходы, обработчики кнопок, логирование — всё должно меняться согласованно.


3. Почему импорт идёт в три слоя, а не линейно?

Вот здесь та часть, которую многим разработчикам сложно понять.

Нельзя просто:

Почему?

Потому что кнопки и переходы требуют ID-скриптов, а ID знает только Builder, и только после создания всей секции.

Поэтому алгоритм у нас такой:

Шаг А — создаём скрипты.

Это даёт нам ID каждого шага.

Шаг Б — наполняем команды.

Включая аналитику, custom script, call_llm, search_kb.

Шаг В — собираем меню (кнопки).

Каждая кнопка — это либо прямой переход, либо создание отдельного скрипта-обработчика.

Шаг Г — собираем переходы.

Все переходы хранятся временно в массиве transitions,
и только в самом конце добавляются в базу.

Именно это обеспечивает корректную топологию карты.

Этот подход можно усложнить и оптимизировать,
но важно понимать, что «слоистость» — не прихоть, а необходимость, если мы хотим:


Исходный код Маппера и Билдера

Полный код Mapper и Builder из этого урока:


ЧАСТЬ 3.1 — Builder (PHP): низкоуровневое ядро конструктора

Builder — это низкоуровневый плагин, который напрямую работает с таблицами Metabot:

Mapper на JavaScript работает «поверх» Builder’а, но именно Builder делает грязную работу:

✔ создаёт реальные записи в БД
✔ удаляет секции и весь вложенный контент
✔ проверяет политику доступа
✔ обеспечивает целостность ссылок
✔ создаёт команды и меню
✔ соединяет скрипты между собой

Builder — это ваш «микро ORM + сервис создания бота».
Mapper — лишь удобная оболочка.


Зачем Builder существует вообще?

Потому что:

Mapper вызывает Builder так:

const Builder = require('Common.CJM.Builder')

Методы Builder становятся доступными JS-коду.


Архитектура Builder

Builder хранит три ключевых контекста:

protected ?Business $_business = null;
protected ?Bot $_runFromBot = null;
protected ?Bot $_bot = null;
protected int $_botId;
protected ?Lead $_lead = null;

Зачем?

Потому что Builder не должен позволять JS-коду выкрутить руки бизнес-логике:

Это критично.


🔐 CheckPolicy(): сердце безопасности

Каждый метод Builder начинает с этого:

$this->checkPolicy();

Он проверяет:

  1. передан ли текущий бизнес

  2. передан ли текущий бот

  3. принадлежит ли бот действующему бизнесу

  4. корректен ли lead

  5. корректна ли топология

  6. установлен ли botId

Без этого Builder вообще не работает.


Методы Builder: полный разбор

Теперь — весь API Builder, который нам нужен для маппера и для создания собственного конструктора.


1) setBotById(botId)

Устанавливает текущий бот для всех дальнейших операций.

Почему нельзя делать это в JS напрямую?
Потому что выбор бота — потенциальная точка атаки.

Метод делает:

Возвращает:

['success' => true, 'bot_id' => X]

2) createSection(sectionTitle)

Создаёт папку (ScriptSection) для новой карты:

Builder не создаёт вложенность, всё плоское.
Вложенность симулируется именованием.


3) deleteSectionByCodeDeep(sectionCode)

Самый опасный и самый важный метод.

Удаляет всю секцию по коду и все её вложенные элементы.

Пример: для карты с кодом testmap удалит всё, что содержит "testmap".

Делает:

Это глубокая очистка, без которой импорт JSON невозможен.

Логи приходится удалять, потому что иначе Metabot не даст удалить скрипты из-за целостности БД. Это не очень хорошая практика т.к. по воронке могли пройти лиды и логи нужны. Это к вопросу, который мы поднимали выше, что простого одностороннего импорта, удаляющего старую версию, в полноценном продукте не достаточно. Для урока мы сознательно упрощаем.


4) createScript(sectionId, code, name)

Создаёт пустой скрипт в конкретной секции:

Возвращает:

['id' => sentence.id, 'code' => sentence.code]

Это строительный блок №1 для Mapper.


5) findScript(code)

Возвращает скрипт или null.
Mapper использует это для проверок.


6) deleteScript(code)

Удаляет скрипт (предложение) + все фразы.


7) createCommand(scriptCode, type, content, commandCode, sort_order)

Создаёт команду (фразу) внутри скрипта.

Поля:

Это фундаментальная часть, потому что Mapper весь low-code строит только на этих операциях.


8) existsScript / existsCommand

Проверяют существование.
Mapper использует для валидаций.


9) deleteCommand(scriptCode, commandCode)

Удаляет фразу с нужным alias.

Обратите внимание, что в low-code интерфейсе конструктора у команд нет алиасов, а в БД есть.


10) createMenuItem()

Создаёт кнопку в меню:

Поля:

Mapper формирует их в конце, когда все ID известны.


11) findSectionByCode / findSectionByTitle

Используются для поиска секций перед удалением.


12) deleteSectionByIdDeep

Глубокое удаление секции (аналогично deleteSectionByCodeDeep).
Именно этот метод делает большую часть работы:


В сумме Builder умеет:

1) Управлять секциями

создавать, искать, удалять с зависимостями

2) Управлять скриптами

создавать, искать, удалять, проверять существование

3) Управлять командами

создавать, удалять, находить

4) Управлять меню

создавать кнопки и переходы

5) Очищать полностью старую карту

deep delete секции

6) Гарантировать безопасность через CheckPolicy

никаких операций вне текущего бизнеса


Итого о Билдере 

Builder — это низкоуровневый API конструктора Metabot, на котором можно собрать:

Mapper лишь использует Builder, но Builder — это фундамент.

🔎 Примечание о расширении Builder

Если вам понадобится добавить в Builder новый функционал — вы можете это сделать, но важно учесть следующее:

Подробнее о возможностях выделенных серверов и коробочных лицензий смотрите здесь:
https://metabot24.ru/price/tarify-na-platformu/


3.2 — Маппер: подробный разбор архитектуры и алгоритма импорта

Маппер — это верхнеуровневый JS-плагин, который принимает JSON-описание вашей схемы (воронки) и преобразует её в реальную структуру скриптов Metabot через Builder. Mapper не работает с базой напрямую — он опирается на PHP-плагин Builder, а сам выполняет только логику трансформации структуры шагов, генерацию JavaScript для команд и построение связей.

Маппер вы можете разместить в плагины вашего бизнеса и использовать на любом сервере, включая общий облачный. Либо можете использовать нашу последнюю версию из Common.CJM.Mapper.

Основная ответственность Mapper:

  1. Проверить формат и подключить нужного бота

  2. Создать секцию (папку) под воронку

  3. Создать скрипты под каждый шаг

  4. Наполнить эти скрипты командами

  5. Сгенерировать переходы (default/аналитика/кнопки)

  6. Сгенерировать и подключить JS-команды: LLM, KB-поиск, кастомный скрипт, логгер

  7. Построить меню-кнопки и обработчики

  8. Добавить все переходы между скриптами

Далее — полный разбор устройства Mapper с примерами кода и объяснением, зачем всё так устроено.


3.2.1. Подключение Builder и базовая инициализация

Mapper работает в JS, но Builder — это PHP-плагин. Их соединяет V8-wrapper:

const Builder = require('Common.CJM.Builder')

class Mapper {
  constructor() {
    this.builder = Builder
  }

Builder подгружается как JS-объект, но внутри вызывает PHP-код.

Перед началом импорта Mapper обязан:

  1. Установить бота

  2. Пройти CheckPolicy внутри Builder (безопасность)

setBotById(botId) {
  const result = this.builder.setBotById(botId)
  if (result.success) {
    this.botId = botId
  }
  return result
}

Если бот не принадлежит бизнесу, Builder возвращает ошибку. Это важно: так мы не позволяем создать скрипт в чужом боте.


3.2.2. Главный метод: runImport()

Это сердце Mapper. Он получает JSON из внешнего API:

runImport(botId, importFormat, importVersion, mapCode, mapTitle, steps = [])
Шаг 1. Установка бота

Если бот не найден или доступ запрещён — бросаем ошибку:

const { success, message } = this.setBotById(botId)
if (!success) throw new Error(message)
Шаг 2. Генерация префикса и названия папки

Маппер работает по строгой системе кодирования:

{format}:{version}:{mapCode}:{stepCode}

Папка:

cjm:1.0:quiz_lead_magnet_tags:Воронка WayLogger

Это ключевой принцип: каждый объект (скрипт, команда и др.) в Metabot получает уникальный код, который невозможно перепутать между воронками.

const mapPrefix = `${importFormat}:${importVersion}:${mapCode}`;
const sectionTitle = `${mapPrefix}:${mapTitle}`
Шаг 3. Удаление старой секции

Mapper не обновляет кусочно — он делает полную пересборку:

this.builder.deleteSectionByCodeDeep(mapCode)

Если вам нужна версия 1.0 и 1.1 одновременно — просто укажите другую version перед импортом.

Шаг 4. Создание новой секции
const section = this.builder.createSection(sectionTitle)
const sectionId = section.id

3.2.3. Фаза 1 — Создание скриптов (без наполнения)

Это важнейший архитектурный момент:

Мы не можем создавать команды или кнопки, пока не созданы ВСЕ скрипты.

Почему?
Потому что кнопка указывает на next_step, а Builder требует ИД скрипта — а его нет, если скрипт ещё не создан.

Поэтому первая фаза:

for (const step of steps) {
  if (!supportedStepTypes.includes(step.type)) continue

  const scriptCode = `${mapPrefix}:${step.code}`
  const script = this.builder.createScript(sectionId, scriptCode, step.name || step.code)

  scriptIds[scriptCode] = script.id
  validSteps.push(step)
}

В результате у нас есть:


3.2.4. Фаза 2 — Наполнение скриптов командами

Теперь, когда все скрипты созданы — начинаем обрабатывать каждый шаг.

Главные переменные:

const nextScriptCode = step.next_step ? `${mapPrefix}:${step.next_step}` : false
let addDefaultExit = true
let loggerScriptCode = false
1. Работа с аналитикой (log_way_steps)

Если у шага есть log_way_steps, мы создаём отдельный логгер-скрипт:

if (Array.isArray(step.log_way_steps)) {
  loggerScriptCode = `${scriptCode}_logger`
  const loggerScript = this.builder.createScript(sectionId, loggerScriptCode, `${step.code}_logger`)

Затем в него добавляется JS-команда:

const js = this._buildLoggerContent(logStep)
this.builder.createCommand(loggerScriptCode, 'run_javascript', js)

Почему логгер вынесен в отдельный скрипт?

2. switch-case по типам команд

Ключевой блок:

switch(step.type) {
  case 'send_text':
  case 'entry_point':
  case 'log_action':
  case 'run_custom_script':
  case 'call_llm':
  case 'search_knowledgebase':
    ...
}

Разберём каждый.


send_text

this.builder.createCommand(
  scriptCode,
  'send_text',
  step.content,
  step.code,
  0
)

Если есть кнопки — мы НЕ создаём дефолтный выход:

if (step.buttons.length > 0) addDefaultExit = false

Кнопки в этот момент НЕ создаются. Они добавляются позже (см. Phase 3).


entry_point

Пока заглушка, но сюда можно добавить:


log_action

Это упрощённая команда аналитики:

this.builder.createCommand(scriptCode, 'run_javascript', this._buildLoggerContent(step))

run_custom_script

Здесь мы вручную генерим JS, который переходит в другой скрипт:

const js = this._buildCustomScriptJs(step)
this.builder.createCommand(scriptCode, 'run_javascript', js)

Почему не используем нативную команду run_sentence?

Потому что она привязана к зависимостям БД, и при удалении секций может нарушать целостность. JavaScript не проверяет foreign keys — это гибче. 


call_llm

Здесь начинается генерация JS-кода для LLM-вызова:

this.builder.createCommand(
  scriptCode,
  'run_js_callback',
  this._buildCallLLMJs(step, mapPrefix)
)

Mapper анализирует:

И превращает всё это в JS-«рецепт», который выполняется в Metabot.


search_knowledgebase

Похожий алгоритм:

this.builder.createCommand(
  scriptCode,
  'run_js_callback',
  this._buildSearchKbJs(step, mapPrefix)
)

Здесь генерится код:


3.2.5. Фаза 3 — Default Exit и Transition Collection

После switch-блока мы добавляем переходы:

Логика:

  1. Если есть логгер — первым переходом идём в логгер.

  2. Потом логгер → next_step

  3. Если логгера нет — просто script → next_step

  4. Но если есть кнопки — дефолтный выход НЕ создаётся.

Mapper пока не создаёт команду перехода — только сохраняет их:

transitions.push({ from: scriptCode, to: nextScriptCode })

Это тоже важно: мы не можем создавать переходы, пока не созданы ВСЕ скрипты.


3.2.6. Фаза 4 — Обработка кнопок и создание handler-скриптов

Блок обработки кнопок — наиболее сложный и многослойный участок маппера.
Причина простая: каждая кнопка — это мини-сценарий внутри сценария, и внутренняя логика кнопки может требовать построения отдельных скриптов:

1) Скрипт-обработчик кнопки (handler script)

Создаётся, если:

2) Скрипт логирования (logger script)

Подключается только если в шаге, к которому относится кнопка, включена аналитика.

3) Целевой скрипт (next_step)

Это обычный переход — либо от самой кнопки (button.next_step), либо от шага (step.next_step).

Таким образом, нажатие кнопки может порождать цепочку:

Handler Script → Logger Script → Next Step

Или:

Logger Script → Next Step

Или:

Handler Script → Next Step

Или просто:

Next Step

Все четыре варианта должны корректно работать. Отсюда и вся сложность логики.


3.2.7. Фаза 5 — Финальное объединение скриптов (TransitionMap)

Теперь, когда:

— наконец можно создавать команды-переходы run_sentence — рёбра между объектами.

Mapper строит карту:

for (const { from, to } of transitionMap.values()) {
  const toScriptId = scriptIds[to]
  this.builder.createCommand(from, 'run_sentence', String(toScriptId), null, 777)
}

Почему sort_order = 777?
Чтобы команда перехода гарантированно была последней.


3.2.8. Генераторы JS-кода (LLM, KB, CustomScript, Logger)

Mapper содержит важные приватные функции:

Они генерируют JavaScript прямо на лету, подставляя параметры шага.

Это архитектурно мощно, потому что:

Однако, лучше это вынести в отдельный модуль или Snippet, чтобы код маппера был чище.


3.2.9. Итог импорта

Mapper возвращает:

return {
  success: true,
  section_id: sectionId,
  created_scripts: createdScriptsCnt
}

3.2.10. Архитектурные преимущества Mapper

  1. Чистая модель автомата состояний

  2. Поддержка аналитики на уровне схемы

  3. Универсальные handler-скрипты для кнопок

  4. Отсутствие foreign-key зависимостей

  5. JS-код, генерируемый на лету

  6. Изоляция каждой воронки через namespace кодов

  7. Предсказуемая однопроходная структура импорта


3.2.11 — Псевдокод полного алгоритма импорта

function runImport(config) {
    validate(config)

    setBot(config.bot_id)

    prefix = `${format}:${version}:${mapCode}`
    sectionTitle = `${prefix}:${title}`

    deleteOldSection(mapCode)
    sectionId = createSection(sectionTitle)

    scriptIds = {}
    validSteps = []

    // ------------------------
    //  PHASE 1 — Create scripts
    // ------------------------
    for step in config.steps:
        if (!supported(step.type)) continue
        scriptCode = `${prefix}:${step.code}`
        scriptId = createScript(sectionId, scriptCode)
        scriptIds[scriptCode] = scriptId
        validSteps.push(step)

    transitions = []
    menuButtons = []

    // ------------------------
    // PHASE 2 — Fill scripts
    // ------------------------
    for step in validSteps:
        scriptCode = prefix + ":" + step.code
        nextScript = prefix + ":" + step.next_step

        if step.log_way_steps:
            loggerCode = createLoggerScript()
            transitions.push( scriptCode → loggerCode )

        switch(step.type):
            case send_text:
                addSendText(scriptCode)
                if (step.buttons) menuButtons.push(...)
                else transitions.push(scriptCode → nextScript)

            case run_custom_script:
                addJS(scriptCode, buildCustomJS(step))
                transitions.push(scriptCode → nextScript)

            case call_llm:
                addJS(scriptCode, buildLLM(step))
                transitions.push(scriptCode → nextScript)

            case search_knowledgebase:
                addJS(scriptCode, buildKB(step))
                transitions.push(scriptCode → nextScript)

            case log_action:
                addJS(scriptCode, buildLogger(step))
                transitions.push(scriptCode → nextScript)

    // -------------------------
    //  PHASE 3 — Buttons logic
    // -------------------------
    for btn in menuButtons:
        if btn.hasHandler:
            handlerCode = createHandlerScript()
            addTagLogic(handlerCode)
            addAttrLogic(handlerCode)
            addTransition(handlerCode → logger → next)
            createMenuItem(scriptId, handlerCode)
        else:
            jumpScript = resolveJump()
            createMenuItem(scriptId, jumpScript)

    // -------------------------
    // PHASE 4 — Final transitions
    // -------------------------
    mergeDuplicates(transitions)
    for t in transitions:
        addCommand(t.from, "run_sentence", scriptIds[t.to])

    return success(sectionId)
}

✅ Завершение части про JSON-формат, Mapper и Builder

Мы разобрали JSON-формат, логику импорта, работу Mapper и низкоуровневого Builder.
Теперь у вас есть полный набор кирпичиков, чтобы:

Важно подчеркнуть:
Mapper — полностью JavaScript-плагин.
Его можно размещать прямо в бизнесе, на любом боте, в том числе на общем облачном сервере.
Вы можете свободно модифицировать его под себя, расширять функционал, менять формат, добавлять новые типы шагов и логику.

Builder — PHP-плагин.
Он даёт прямой доступ к базе Metabot и обеспечивает всю работу с «железом» конструктора (создание скриптов, команд, секций, меню и т.д.).
Он уже содержит достаточный набор функций, чтобы реализовать практически любой no-code → low-code транслятор.

Напомним:

Если вам требуются новые возможности в Builder или вы хотите собрать свой собственный Builder, доступны несколько вариантов:

  1. Развернуть выделенный сервер,

  2. Приобрести коробочную версию MetaBot,

  3. Или предложить свой функционал — пришлите описание в поддержку, и мы рассмотрим возможность включить расширения в официальный билд.
    Подробнее о тарифах: https://metabot24.ru/price/tarify-na-platformu/

На этом завершаем блок работы с JSON-схемой, импортом и серверной частью конструктора.


Часть 4. Визуальный редактор воронок на React/Next.js с помощью V0 или Cursor

Теперь, когда мы полностью разобрали JSON-формат, Mapper, Builder и серверную архитектуру импорта, мы можем перейти к самому вдохновляющему этапу — созданию визуального no-code редактора воронок, работающего поверх MetaBot.

Этот редактор позволяет:


4.1. Общая концепция редактора

Наша реализация состоит из:


4.2. Ссылки на спецификации и исходники

Спецификации редактора

Здесь представлены основные промпты, которые удалось восстановить из реального проекта:

GitHub: https://github.com/art-yg/metabot-cjm-designer-specs

GitVerse: https://gitverse.ru/metabot/metabot-cjm-designer-specs 

Основная версия интерфейса редактора была изначально создана в V0 — интерфейс-генераторе, разработанном командой создателей Next.js.
Это важный момент: V0 обладает огромной обучающей базой готовых дизайнов, использует лучшие паттерны современного React/Next.js-подхода и умеет генерировать чистый, аккуратный, минималистичный интерфейс, который отлично подходит для:

Именно поэтому первые две версии визуального редактора были сделаны полностью в V0.

После того как каркас был собран, мы переехали в Cursor.
В Cursor уже:

Финальный вариант, который вы видите в репозитории, — это результат совместной работы V0 и Cursor, где:

Что касается спецификаций:

Таким образом:

V0 дал foundation — Cursor дал refinement.
В итоге вы получаете хороший прототип, который можно расширять, углублять, менять и развивать под коммерческий продукт.

💻 Исходный код фронтенда (React/Next.js)

GitHub: https://github.com/art-yg/metabot-cjm-designer

GitVerse: https://gitverse.ru/metabot/metabot-cjm-designer 

Исходный код фронтенда — это не эталон и не финальный продукт.
Это рабочий прототип, созданный для демонстрации концепции no-code редактора, на котором вы можете:

В реальных продуктах вы можете — и вполне вероятно будете — улучшать структуру, оптимизацию, убирать лишнее, переносить логику, оптимизировать производительность.

Пример JSON импорта

Две воронки (онбординг в продукт + работа с LLM) в одном файле:

GitHub: https://github.com/art-yg/metabot-cjm-designer-specs/blob/main/json/cjm-schema-example1.json 

GitVerse: https://gitverse.ru/metabot/metabot-cjm-designer-specs/content/main/json/cjm-schema-example1.json 


4.3. Принципы UI/UX, на которых строится редактор

Мы сознательно выбрали подход «минимум интерфейса — максимум пространства», вдохновившись инструментами вроде Miro.

Основные моменты:

1) Большая канва, минимум панелей

Ничего не перекрывает рабочее пространство.
Узлы и связи всегда в центре внимания.

2) Popup-редактирование вместо боковых панелей

Первые версии имели боковую панель, но она отнимала пространство.
Popup-окно лучше работает для:

3) React Flow для прототипа

React Flow — идеальная библиотека для быстрого прототипирования и небольших проектов.
Но не идеальна для:

Для полноценных продуктов рекомендуем подобрать более оптимизированную библиотеку.


Перед тем как завершить

Мы не углубляемся в детали разработки приложений на React/Next.js — это отдельная большая тема, и этот урок посвящён именно тому, как построить no-code редактор поверх ядра Metabot, а не фронтенд-разработке как таковой.

Если вы хотите разобраться глубже — есть два лучших пути:

  1. Взять исходники и спецификации редактора, загрузить архив в ChatGPT, и пошагово изучить архитектуру, структуры данных, паттерны и логику.
    Это максимально практичный способ.

  2. Взять спецификации из папки /specs и попробовать воссоздать редактор самостоятельно — используя V0, Cursor или любой другой генератор UI.
    Спеки в этом проекте очень детализированные и позволяют полностью повторить дизайнер с нуля.

Эти два подхода дадут вам гораздо больше, чем любой абстрактный учебник по React — потому что вы будете изучать реальный рабочий продукт, который уже интегрирован с Metabot.


🎉 Поздравляем — урок завершён!

Мы подошли к финалу большого и важного пути.
Теперь у вас есть все инструменты, чтобы создавать собственные no-code редакторы поверх ядра Metabot, точно так же, как это делают разработчики самой платформы.

Давайте коротко зафиксируем, что именно вы теперь умеете.


✅ Что вы освоили в этом уроке

1. Поняли внутреннюю механику Metabot

Вы разобрались:

Теперь вы умеете работать с двигателем платформы, а не только с UI.


2. Создали собственный JSON-формат воронок

Вы:

Этот JSON — основа любого no-code редактора.


3. Полностью разобрали Mapper

Вы увидели, как работает реальный JS-транслятор:

То есть вы освоили алгоритм импорта в Metabot от начала до конца.

Mapper — это ваш мост между визуальным редактором и low-code ядром.


4. Научились импортировать воронки через API

Вы теперь умеете:

Это полноценный backend-конвейер для вашего конструктора.


5. Построили визуальный редактор на React

Вы узнали:

А самое главное — вы получили исходники редактора и спецификации, на которых его можно воспроизвести полностью.


6. Получили материалы для самостоятельного развития

В этом уроке вы получили:

Это достаточный набор, чтобы:


🔧 Примечание

Если вам понадобится углублять функциональность Builder, создавать собственные PHP-плагины или расширять серверную логику конструктора — это можно делать:

На общем облаке установка PHP-плагинов отключена по безопасности.
Если хотите предложить улучшение Builder — напишите нам, мы рассмотрим и, возможно, включим в официальный билд.


🚀 Напоследок

Мы сознательно не углублялись в React/Next.js как учебник — это не цель урока.
Но у вас есть всё необходимое, чтобы:

Это лучший способ освоить реальный продуктовый подход.


🎉 Ещё раз поздравляем!

Теперь вы умеете создавать no-code редакторы поверх Metabot, понимаете, как писать свои команды, как превращать JSON в скрипты, как строить визуальные пайплайны и как надстраивать интерфейсы над low-code ядром.

Вы научились тому, что большинство разработчиков видит только как «магия» в интерфейсе — а вы теперь умеете реализовывать её сами.

И это уже уровень архитекторов платформ, а не просто разработчиков чат-ботов.

Удачи вам в ваших продуктах — и создавайте смело.
Если нужно — мы рядом.