10. Web UI
Web-формы в чат-боте и Web Apps
Введение. Виды форм. Принцип работы.
Теоретическая часть для разработчика сайта (разработчика HTML-формы) и для разработчика бота.
Веб-формы расширяют канал коммуникаций с ботом и позволяют в текстовом боте использовать все возможности HTML и JavaScript, таким образом бот, по функционалу становится полноценной заменой любому веб-сайту. Бот теперь не ограничивает пользователя в коммуникациях только обменом текстом, аудио или файлами.
Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html
Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot
Разработка веб-формы, расширяющей диалоговую коммуникацию происходит в несколько шагов:
Перед началом разработки необходимо выбрать один из вариантов реализации формы и согласовать с разработчиком веб-формы и разработчиком чат-бота (если форма и чат-бот разрабатываются параллельно). Также необходимо согласовать формат обмена данными.
Универсальная в виде ссылки (для любого мессенджера).
WebApp, на основе inline-кнопки (только для Telegram).
WebApp, на основе keyboard-кнопки (только для Telegram).
После того как вы определились с видом формы, необходимо разработать 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-кнопки. Но, желательно понимать отличия и изучить весь раздел документации.
Принцип работы, по шагам:
Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.
Бот отправляет эту ссылку в мессенджер в виде обычного сообщения.
Пользователь бота нажимает на ссылку, открывается браузер с формой.
Пользователь бота заполняет форму и нажимает кнопку для отправки формы.
Форма отправляет данные на backend сайта, где размещена форма.
Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
Metabot принимает и сохраняет данные заполненной формы .
Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).
Пример JS кода для открытия ссылки на Telegram бота и закрытия страницы
После заполнения формы, данные введенные пользователем необходимо отправить на бэк вашего сайта, а затем в 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. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.
Принцип работы, по шагам:
Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.
Бот отправляет эту ссылку в виде inline-кнопки в Telegram-мессенджер.
Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).
Пользователь бота заполняет форму и нажимает кнопку для отправки формы.
Форма отправляет данные на backend сайта, где размещена форма.
Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
Metabot принимает и сохраняет данные заполненной формы.
Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого 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-кнопки.
Принцип работы, по шагам:
Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.
Бот отправляет эту ссылку в виде keyboard-кнопки в Telegram-мессенджер ("кнопка-калькулятор" отображаемая в нижней части мессенджера).
Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).
Пользователь бота заполняет форму и нажимает кнопку для отправки формы.
В коде HTML формы размещается JS код, для передачи введенных данных в телеграмм: window.Telegram.WebApp.sendData(JSON.stringify(formData)).
Для дополнительной защиты, желательно, в отправляемые данные включить уникальный хэш-код лида.
Metabot принимает данные заполненной формы от Telegram и сохраняет эти данные.
Страница с формой в браузере будет закрыта автоматически, после выполнения метода Telegram.WebApp.sendData, если же вы не используете sendData, то можно вызвать метод Telegram.WebApp.close().
Ограничения и неудобства данного вида формы:
Для такого вида формы: максимальный размер данных отправляемых с формы в бота составляет 4096 байт;
Менее удобный UI/UX в виде кнопки отображаемой в нижней части браузера, также это меняет шаблон поведения пользователя, например, пользователь использовал inline-кнопки в боте, а теперь ему нужно дополнительно отслеживать кнопки "калькулятора";
Кнопка может скрываться, если пользователь напишет что-то в чат-бот (возможен вариант повторного обновления кнопки, чтобы она не исчезала);
В некоторых альтернативных мобильных приложениях для Telegram (например Plus Messenger для Android) список keyboard-кнопок может быть изначально схлопнут и необходимо нажимать в нижней части мессенджера на иконку для отображения кнопок меню (см скрины ниже).
Создание HTML формы.
Инструкция для разработчика веб-формы.
Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html
Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot
Исходный код примера веб-формы
Заявка для полета на Марс
При использовании данного примера замените в следующих строках данные на актуальные для вашего бота:
let botId = ID_ВАШЕГО_БОТА;
let botToken = 'ТОКЕН_ВАШЕГО_БОТА';
let dadataToken = 'ТОКЕН_DADATA'.
И измените код отправки заполненных данных, чтобы данные сначала отправлялись на ваш бэк, а затем с бэка в Metabot API. Также уберите отправку данных внутри параметра script_request_params, чтобы данные к вам на бэк отправлялись без этого параметра, но когда отправляете в Metabot API учитывайте эту особенность.
Речь идет про строку:
sendBody = {"script_request_params": formData}
Которую для вас нужно заменить на:
sendBody = formData
Также для понимания принципа работы изучите комментарии в HTML-коде.
Код не требует большого уровня владения HTML или JS для создания подобной формы, но заметим, что важной частью является UI/UX и может потребоваться профессиональный Frontend-разработчик, также необходимо учесть отработку всех исключительных ситуаций, например если произошел сбой API, о чем было упомянуто в теоретической части, во введении, заметим что выше приведен просто пример, все аспекты не проработаны досконально, тк это возможно только на конкретном рабочем примере для вашего бота.Универсальная форма в виде ссылки (для любого мессенджера)
Инструкция для разработчика бота.
Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: 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-кнопки.
Краткое описание принципа работы бота:
В бот отправляется ссылка на форму (или inline-кнопка в виде ссылки). При нажатии на ссылку (или inline-кнопку в виде ссылки) открывается внешний браузер (или браузер встроенный в Telegram).
После заполнения формы данные должны быть отправлены на сторонний бэк, а из бэка данные должны быть отправлены в Metabot API. Для понимания смотрите исходный код веб-формы.
Данные пришедшие с формы сохраняются в JS Internal API Endpoint, который сохраняет данные и вызывает скрипт бота для продолжения беседы, этот скрипт работает с сохраненными данными формы, запрашивает подтверждения, что все введено верно, а также предлагает заполнить форму повторно.
Принцип работы, по шагам:
Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.
Бот отправляет эту ссылку в мессенджер в виде обычного сообщения.
Пользователь бота нажимает на ссылку, открывается браузер с формой.
Пользователь бота заполняет форму и нажимает кнопку для отправки формы.
Форма отправляет данные на backend сайта, где размещена форма.
Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
Metabot принимает и сохраняет данные заполненной формы.
Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).
Таблицы для работы с формами
Чтобы рассматриваемый пример бота с формами работал, вам необходимо создать две кастомные таблицы.
Таблица: Хэш-коды лидов
Таблица необходима для хранения соответствий между лидом и хэшом для лида.
При каждом вызове формы выполняется поиск хэша по лиду, если хэш не найден, то формируется новый. После отправки формы, запись с хэш-кодом удаляется, чтобы ссылка на каждый новый опрос содержала уникальный хэш. Поиск предыдущего хэша необходим, если есть формы, ожидающие заполнения, для случаев, если лид закрыл форму, а затем спустя время время решил ее заполнить, нажав на туже кнопку вызова формы. При этом в таблицу заложено поле (expire_at) - дата истечения хэш-кода, вы можете использовать это поле для реализации времени жизни хэш-кода, но в таком случае в веб-форме необходимо предусмотреть уведомление о том, что форма истекла, или закрывать ее автоматом после истечения, или формировать в боте новую ссылку, если лид пытается отправить форму, хэш которой истек. Это требование не обязательно, может реализовываться по мере необходимости и зависит от конкретной задачи.
Структура таблицы:
Таблица: Данные форм
Таблица необходима для хранения данных заполненных форм.
Данная таблица реализована как универсальная, все данные пришедшие с формы сохраняются в текстовом поле в виде JSON, поэтому она может работать с любой формой, но работать с строкой в виде JSON в JS неудобно, тк для доступа к данным приходится выполнять JSON.parse(), вы можете нормализовать данные, реализуя для каждой формы бота отдельную таблицу с необходимыми полями для хранения, например такими как: Имя, Фамилия, Адрес и т.д., чтобы в дальнейшем было проще работать с данными и выполнять поиск по таблице. Также, в вашем случае таблица для хранения данных может и не потребоваться, если всплывающая форма очень простая и предназначена, например, для заполнения одного поля, например, адреса с авто комплитом от Dadata или для выбора выпадающего списка или даты в календаре. Т.е. форма может реализовывать даже один компонент для заполнения которого необходим HTML + JS.
Структура таблицы:
Чтобы посмотреть исходный код внутреннего API, сначала перейдите в бота по ссылке на любой из скриптов.
Нажмите в предупреждающем сообщении на ссылку для смены активного бота, а затем перейдите на страницу c внутренним API в настройках бота или по ссылке.
Если у вас нет доступа к указанному скрипту, то запросите шаблон бота в технической поддерже Metabot.
Скрипт для формирования и отправки ссылки в мессенджер
Пример скрипта:
Скрипт отправки ссылки состоит из следующих команд бота:
Выполнить JavaScript:
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, куда приходит запрос после заполнения формы.
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
Данный скрипт нужен чтобы пользователь продолжил диалог с ботом, а также демонстрирует как использовать данные сохраненные в таблицу результатов и вывести их ввиде текста.
Пример скрипта.
Скрипт отправки ссылки состоит из следующих команд бота и пунктов меню:
Команда Выполнить JavaScript:
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 }}
Команда Отправить текст:
Все верно?
Пункты меню данного скрипта, с условиями:
Условия кнопок необходимы для вызова скрипта соответствующего текущему формату формы.
Условие для вывода универсальной формы в виде ссылки:
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 App форма c использованием inline-кнопки (только для Telegram)
Инструкция для разработчика бота.
Данный вид формы работает только в Telegram.
Рекомендуемый вид формы для Telegram бота.
Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html
Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot
Отличие данного варианта от универсальной формы в виде ссылки, в том, что форма работает как Web App, т.е. это специальный режиме браузера встроенного в Telegram. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.
Краткое описание принципа работы бота:
В бот отправляется inline-кнопка (в формате Telegram Web App). При нажатии на кнопку открывается всплывающее окно, в которое встроено Web View, пользователь не может открыть эту ссылку во внешнем браузере, а также просмотреть исходный код формы (если только не откроет сам Telegram в браузере).
После заполнения формы данные должны быть отправлены на сторонний бэк, а из бэка данные должны быть отправлены в Metabot API. Для понимания смотрите исходный код веб-формы.
Данные пришедшие с формы сохраняются в JS Internal API Endpoint, который сохраняет данные и вызывает скрипт бота для продолжения беседы, этот скрипт работает с сохраненными данными формы, запрашивает подтверждения, что все введено верно, а также предлагает заполнить форму повторно.
Принцип работы, по шагам:
Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.
Бот отправляет эту ссылку в виде inline-кнопки в Telegram-мессенджер.
Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).
Пользователь бота заполняет форму и нажимает кнопку для отправки формы.
Форма отправляет данные на backend сайта, где размещена форма.
Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
Metabot принимает и сохраняет данные заполненной формы.
Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).
Таблицы для работы с формами
Чтобы рассматриваемый пример бота с формами работал, вам необходимо создать две кастомные таблицы.
Таблица: Хэш-коды лидов
Таблица необходима для хранения соответствий между лидом и хэшом для лида.
При каждом вызове формы выполняется поиск хэша по лиду, если хэш не найден, то формируется новый. После отправки формы, запись с хэш-кодом удаляется, чтобы ссылка на каждый новый опрос содержала уникальный хэш. Поиск предыдущего хэша необходим, если есть формы, ожидающие заполнения, для случаев, если лид закрыл форму, а затем спустя время время решил ее заполнить, нажав на туже кнопку вызова формы. При этом в таблицу заложено поле (expire_at) - дата истечения хэш-кода, вы можете использовать это поле для реализации времени жизни хэш-кода, но в таком случае в веб-форме необходимо предусмотреть уведомление о том, что форма истекла, или закрывать ее автоматом после истечения, или формировать в боте новую ссылку, если лид пытается отправить форму, хэш которой истек. Это требование не обязательно, может реализовываться по мере необходимости и зависит от конкретной задачи.
Структура таблицы:
Таблица: Данные форм
Таблица необходима для хранения данных заполненных форм.
Данная таблица реализована как универсальная, все данные пришедшие с формы сохраняются в текстовом поле в виде JSON, поэтому она может работать с любой формой, но работать с строкой в виде JSON в JS неудобно, тк для доступа к данным приходится выполнять JSON.parse(), вы можете нормализовать данные, реализуя для каждой формы бота отдельную таблицу с необходимыми полями для хранения, например такими как: Имя, Фамилия, Адрес и т.д., чтобы в дальнейшем было проще работать с данными и выполнять поиск по таблице. Также, в вашем случае таблица для хранения данных может и не потребоваться, если всплывающая форма очень простая и предназначена, например, для заполнения одного поля, например, адреса с авто комплитом от Dadata или для выбора выпадающего списка или даты в календаре. Т.е. форма может реализовывать даже один компонент для заполнения которого необходим HTML + JS.
Структура таблицы:
Чтобы посмотреть исходный код внутреннего API, сначала перейдите в бота по ссылке на любой из скриптов.
Нажмите в предупреждающем сообщении на ссылку для смены активного бота, а затем перейдите на страницу c внутренним API в настройках бота или по ссылке.
Если у вас нет доступа к указанному скрипту, то запросите шаблон бота в технической поддерже Metabot.
Скрипт для формирования ссылки и отправки кнопки в мессенджер
Пример скрипта.
Скрипт отправки ссылки состоит из единственной команды бота:
Выполнить JavaScript Callback:
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, куда приходит запрос после заполнения формы.
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
Данный скрипт нужен чтобы пользователь продолжил диалог с ботом, а также демонстрирует как использовать данные сохраненные в таблицу результатов и вывести их в виде текста.
Пример скрипта.
Скрипт отправки ссылки состоит из следующих команд бота и пунктов меню:
Команда Выполнить JavaScript:
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 }}
Команда Отправить текст:
Все верно?
Пункты меню данного скрипта, с условиями:
Условия кнопок необходимы для вызова скрипта соответствующего текущему формату формы.
Условие для вывода универсальной формы в виде ссылки:
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 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. Этот режим гарантирует возврат в бота после заполнения формы, а также не позволит открыть ссылку во внешнем браузере (а также включает дополнительный функционал для взаимодействия веб-страницы с ботом). В обычном же режиме для универсальной формы, нет гарантий, что когда веб-страница будет закрыта, пользователя перекинет в бота.
Краткое описание принципа работы бота:
В бот отправляется keyboard-кнопка (в формате Telegram Web App). При нажатии на кнопку открывается всплывающее окно, в которое встроен Web View, пользователь не может открыть эту ссылку во внешнем браузере, а также просмотреть исходный код формы (если только не откроет сам Telegram в браузере).
После заполнения формы и нажатия на кнопку Отправить данные, все данные будут отправлены в Metabot, в команду JavaSctript Callback, Internal API endpoint в данном случае не требуется. Для понимания смотрите исходный код веб-формы.
Данные пришедшие с формы доступны в команде JavaSctript Callback в виде обычного вебхука. Необходимо сохранить их в таблице.
Далее необходимо вызвать скрипт бота для продолжения беседы, этот скрипт работает с сохраненными данными формы, запрашивает подтверждение, что все введено верно, а также предлагает заполнить форму повторно.
Принцип работы, по шагам:
Бот генерирует ссылку с уникальным хэш-кодом, привязанным к лиду.
Бот отправляет эту ссылку в виде inline-кнопки в Telegram-мессенджер.
Пользователь бота нажимает на кнопку, открывается Web App с формой (т.е. пользователь остается в Telegram, в мессенджере открывается встроенный браузер).
Пользователь бота заполняет форму и нажимает кнопку для отправки формы.
Форма отправляет данные на backend сайта, где размещена форма.
Backend сайта отправляет данные формы в API Metabot, в данные должен быть включен уникальный хэш лида.
Metabot принимает и сохраняет данные заполненной формы.
Если все ок, то страница с формой в браузере должна быть закрыта, это делается с помощью простого JS кода (подробности рассматриваются на отдельной странице документации с описанием создания HTML-формы).
Таблицы для работы с формами
Чтобы рассматриваемый пример бота с формами работал, вам необходимо создать две кастомные таблицы.
Таблица: Хэш-коды лидов
Таблица необходима для хранения соответствий между лидом и хэшом для лида.
При каждом вызове формы выполняется поиск хэша по лиду, если хэш не найден, то формируется новый. После отправки формы, запись с хэш-кодом удаляется, чтобы ссылка на каждый новый опрос содержала уникальный хэш. Поиск предыдущего хэша необходим, если есть формы, ожидающие заполнения, для случаев, если лид закрыл форму, а затем спустя время время решил ее заполнить, нажав на туже кнопку вызова формы. При этом в таблицу заложено поле (expire_at) - дата истечения хэш-кода, вы можете использовать это поле для реализации времени жизни хэш-кода, но в таком случае в веб-форме необходимо предусмотреть уведомление о том, что форма истекла, или закрывать ее автоматом после истечения, или формировать в боте новую ссылку, если лид пытается отправить форму, хэш которой истек. Это требование не обязательно, может реализовываться по мере необходимости и зависит от конкретной задачи.
Структура таблицы:
Таблица: Данные форм
Таблица необходима для хранения данных заполненных форм.
Данная таблица реализована как универсальная, все данные пришедшие с формы сохраняются в текстовом поле в виде JSON, поэтому она может работать с любой формой, но работать с строкой в виде JSON в JS неудобно, тк для доступа к данным приходится выполнять JSON.parse(), вы можете нормализовать данные, реализуя для каждой формы бота отдельную таблицу с необходимыми полями для хранения, например такими как: Имя, Фамилия, Адрес и т.д., чтобы в дальнейшем было проще работать с данными и выполнять поиск по таблице. Также, в вашем случае таблица для хранения данных может и не потребоваться, если всплывающая форма очень простая и предназначена, например, для заполнения одного поля, например, адреса с авто комплитом от Dadata или для выбора выпадающего списка или даты в календаре. Т.е. форма может реализовывать даже один компонент для заполнения которого необходим HTML + JS.
Структура таблицы:
Чтобы посмотреть исходный код внутреннего API, сначала перейдите в бота по ссылке на любой из скриптов.
Нажмите в предупреждающем сообщении на ссылку для смены активного бота, а затем перейдите на страницу c внутренним API в настройках бота или по ссылке.
Если у вас нет доступа к указанному скрипту, то запросите шаблон бота в технической поддерже Metabot.
Скрипт для формирования ссылки и отправки кнопки в мессенджер
Пример скрипта.
Скрипт отправки ссылки состоит из единственной команды бота:
Выполнить JavaScript Callback:
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, код которой приведен выше.
Данный скрипт нужен чтобы пользователь продолжил диалог с ботом, а также демонстрирует как использовать данные сохраненные в таблицу результатов и вывести их в виде текста.
Пример скрипта.
Скрипт отправки ссылки состоит из следующих команд бота и пунктов меню:
Команда Выполнить JavaScript:
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 }}
Команда Отправить текст:
Все верно?
Пункты меню данного скрипта, с условиями:
Условия кнопок необходимы для вызова скрипта соответствующего текущему формату формы.
Условие для вывода универсальной формы в виде ссылки:
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) — это инструмент визуализации стилистического направления проекта. Он представляет собой подборку изображений, шрифтов, цветовых сочетаний и композиционных решений, отражающих предполагаемое настроение и атмосферу будущего продукта.
Создание мудборда необходимо для:
согласования визуального направления с заказчиком до начала детальной проработки макетов;
минимизации количества правок на последующих этапах;
формирования общего понимания эстетики проекта.
Рекомендуемые материалы:
Что такое мудборд — Яндекс.Практикум
Мудборд — глоссарий Contented
2.2. Практические рекомендации
Подготовьте несколько вариантов мудбордов, отражающих разные стилистические решения.
Обсудите и согласуйте выбранное направление напрямую с заказчиком.
После утверждения мудборда зафиксируйте его как основу визуальной концепции проекта.
3. Типографика
Типографика определяет структуру и читаемость интерфейса. Правильный выбор шрифтов и их иерархия обеспечивают удобство восприятия информации пользователями.
Рекомендуемые материалы:
10 правил типографики в интерфейсах — Contented
Типографика в веб-дизайне в цифрах — VC.ru
Рекомендации:
Используйте не более двух-трёх шрифтовых пар в одном проекте.
Формируйте чёткую иерархию заголовков, подзаголовков и основного текста.
Проверяйте читаемость на различных разрешениях экранов.
4. Сетки и структурирование контента
Сетка является основой композиционной организации макета. Она обеспечивает визуальный порядок и согласованность элементов.
Рекомендуемые материалы:
Сетки в веб-дизайне — VC.ru
Сетки в веб-дизайне — Skedraw
Пример сетки — Figma Community
Рекомендации:
Используйте модульные и колоночные сетки.
Соблюдайте пропорциональность и единообразие отступов.
Структура должна быть адаптивной для различных устройств.
5. Цветовое решение
Цветовая палитра формирует эмоциональный отклик и визуальный баланс интерфейса. Цвет должен подбираться осознанно, с учётом целевой аудитории и контекста бренда.
Рекомендуемые материалы:
Цветовая палитра для сайта — Яндекс.Практикум
Цвет в веб-дизайне и его влияние — VC.ru
Рекомендации:
Используйте ограниченную палитру: 1–2 основных цвета и 1 акцентный.
Проверяйте контрастность и читаемость текста.
При наличии брендбука следуйте корпоративным цветовым стандартам.
6. Композиция
Композиция определяет визуальный ритм, баланс и иерархию элементов интерфейса.
Рекомендуемые материалы:
Основы композиции в дизайне — Contented
Основные правила композиции — Яндекс.Практикум
Рекомендации:
Используйте принципы визуального равновесия и акцентирования.
Избегайте перегруженности экрана второстепенными элементами.
Соблюдайте целостность восприятия интерфейса.
7. UI Kit (дизайн-система проекта)
UI Kit — это систематизированный набор компонентов интерфейса (кнопки, поля ввода, карточки, иконки, стили текста и пр.), который обеспечивает единообразие визуальных решений.
Рекомендуемые материалы:
UI Kit — Яндекс.Практикум
Глоссарий Contented: UI Kit
UI Kits — Figma Community
Рекомендации:
Создавайте компоненты со всеми необходимыми состояниями.
Настраивайте связь UI Kit с автолейаутами.
Используйте UI Kit как единый источник истины по стилю проекта.
8. Автолейауты
Автолейаут (Auto Layout) — инструмент Figma, позволяющий создавать гибкие и адаптивные интерфейсы.
Рекомендуемые материалы:
Auto Layout в Figma — Contented
Пример файла — Figma Community
Рекомендации:
Используйте автоотступы и выравнивание для стабильности макета.
Комбинируйте автолейауты с компонентами и вариантами.
Применяйте для подготовки кликабельных прототипов.
9. Кликабельные прототипы
Кликабельный прототип имитирует поведение готового продукта и позволяет наглядно продемонстрировать структуру и логику интерфейса заказчику.
Рекомендуемый ресурс:
Бесплатный курс по Figma — Tilda School
Рекомендации:
Обязательно предоставляйте заказчику кликабельный прототип перед финальным утверждением.
Прототип помогает выявить UX-проблемы на раннем этапе.
Презентацию макета проводите лично, сопровождая её профессиональными комментариями.
10. Коммуникация с заказчиком
10.1. Прямое взаимодействие
Дизайнер должен всегда взаимодействовать с заказчиком напрямую, а не через посредников (например, менеджеров без дизайнерской компетенции).Прямой контакт позволяет:
корректно понимать требования и контекст задач;
оперативно согласовывать решения;
минимизировать искажения при передаче информации.
Отсутствие прямой коммуникации часто приводит к некорректным правкам и неверному пониманию целей проекта.
10.2. Работа с брендбуком
Если у заказчика имеется брендбук, дизайнер обязан строго следовать указанным в нём правилам:
использовать утверждённые шрифты, цвета и логотипы;
соблюдать композиционные и визуальные принципы бренда.
Если брендбук отсутствует, недопустимо ориентироваться на формулировки вроде «сделайте, как у нас на сайте».Существующий сайт может содержать устаревшие или некорректные решения, поэтому дизайнер должен опираться на профессиональные стандарты, принципы эргономики и визуальной гармонии.
10.3. Согласование и правки
Все ключевые визуальные и функциональные решения фиксируются и утверждаются на этапе дизайна.
После утверждения дизайн считается согласованным. Внесение изменений осуществляется только при дополнительной оплате или по технической необходимости.
Любые корректировки должны иметь обоснование, связанное с задачами продукта, а не субъективными предпочтениями заказчика.
Заключение
Методика разработки дизайна для заказчиков основана на профессиональной экспертизе, чёткой коммуникации и документировании решений.Дизайнер обязан выступать не исполнителем пожеланий, а экспертом, формирующим визуальную стратегию проекта.Только системный подход, прямой диалог с заказчиком и следование правилам визуальной организации позволяют создать эффективный и качественный продукт.Стандарты Web UI
1. Цель документа
Этот документ устанавливает единые стандарты работы над дизайном для заказчиков, чтобы минимизировать правки, повысить предсказуемость процесса и обеспечить стабильное качество результата. Он помогает команде говорить с клиентами на одном языке, быстрее согласовывать решения и формировать профессиональный подход к каждому проекту.
2. Базовые принципы
Все ключевые решения принимаются и фиксируются на этапе дизайна
Визуальное направление всегда согласуется через мудборды или по брендбуку до проработки макетов
При наличии брендбука строго соблюдаются его правила
Макеты строятся на сетках с продуманной структурой контента
Адаптивный дизайн делается только при наличии задачи/заказа на адаптив
Интерфейсы собираются из системных компонентов и UI Kit
Все макеты делаются на автолейаутах
Перед финальным утверждением заказчику всегда предоставляется прототип
Дизайнер сам представляет свой прототип заказчику
После утверждения дизайна любые изменения вносятся только при крайней необходимости (недоработки со стороны дизайнера, несоответствие логики и действий, критичные правки от заказчика)
3. Процесс работы (Pipeline)
Шаг 1. Получение задачи и всех материалов
Менеджер формулирует ТЗ и передаёт дизайнеру весь контекст задачи и материалы полученные от заказчика
Шаг 2. Прототипирование
Мудборд/брендбук
Расстановка элементов по сетке/правилам
Базовая логика переходов/реакций компонентов
Шаг 3. Согласование прототипа с менеджером и Art (Artem)
Утверждаются ключевые элементы/логика/цвета/картинки
Шаг 4. Полноценный дизайн
Работа с цветами
Типографика
Названия компонентов
UI-kit
Автолэйаут
Шаг 5. Передача дизайна фронту
Дизайнер передаёт финальные макеты фронтенду вместе с необходимыми пояснениями: повторяющиеся блоки, компоненты, намеренные отступы, допустимые вариации.
Фронтенд проверяет дизайн и задаёт вопросы. Если обнаружены элементы, которые невозможно реализовать или реализация чрезмерно сложна, дизайн возвращается на доработку или обсуждается альтернативное решение, исходя из потребностей заказчика.
Шаг 6. Доработки на этапе интеграции
Мелкие правки (коррекции цветов, шрифтов, иконок, изображений) принимаются без проблем
Крупные изменения (перестройка структуры, изменение расположения элементов, добавление новых блоков или новых версий макета) отклоняются, так как требуют значительного переработки дизайна и вёрстки. Масштабные правки согласуются отдельно и выполняются только при дополнительном бюджете и сроках
4. Требования к дизайну (Design Standards)
4.1 Сетка
Сетка — основа композиции и структурирования контента. Она обеспечивает порядок, предсказуемость и согласованность элементов внутри макетов.Сетка должна быть создана в Figma инструментом Layout Grid и применена ко всем фреймам и страницам проекта.
4.1.1 Типы сеток
В Figma используются 3 вида сеток:
Column Grid (колонки)Подходит для лендингов, сайтов, сложных интерфейсов, адаптивной верстки.Примеры использования:
12 колонок с gutter 20–32
8 колонок для мобильных
4 колонки для компактных блоков
Row Grid (ряды)Используется реже, но помогает выстроить строгий вертикальный ритм.
Grid (равномерная сетка)Идеальна для карточек, плиток, галерей, иконок, dashboard-интерфейсов.
4.1.2 Выбор сетки
Выбор сетки зависит от задач проекта:
Корпоративный сайт / лендинг — 12 колонок (desktop), 4–8 колонок (tablet), 2–4 (mobile).
Личный кабинет / платформа / интерфейс с таблицами — сетка с широкими колонками + плотный вертикальный ритм.
Каталоги, плитки, карточки — grid 4–8 px step.
Простые страницы — 960 px или 1200 px контейнер.
Важно:Сетка должна быть выбрана и утверждена на этапе прототипа и неизменно применяться на всех страницах.
4.1.3 Применение сетки
Сетка включается у всех главных фреймов.
Элементы выравниваются строго по колонкам и кратно выбранным отступам.
Блоки, заголовки, карточки, формы следуют вертикальному ритму (например 100 px между секциями).
Если используется адаптив — создаётся отдельная сетка для каждой точки перелома (mobile / tablet / desktop).
4.2 Auto Layout
Auto Layout — обязательный инструмент для всех элементов.100% блоков, карточек, кнопок, форм и секций должны быть собраны через Auto Layout.
4.2.1 Основные правила Auto Layout
Все компоненты и фреймы должны иметь Auto Layout.
Отступы задаются только через Auto Layout (padding / gap).
Никаких ручных pixel-perfect отступов между элементами.
Выравнивание внутри контейнера: left, center, space-between — задаётся заранее и одинаково используется в проекте. Не используем нижнее выравнивание.
Auto Layout применяется даже для мелких элементов:
кнопок
карточек
тегов
списков
пунктов меню
табов
любых повторяющихся структур
4.2.3 Почему нельзя использовать ручные отступы
Ручные расстояния ломают масштабирование.
При адаптиве или любом изменении текстов блок разваливается.
При переносе в UI Kit компонент перестаёт быть универсальным.
Разметка становится несинхронной с фронтендом.
4.2.4 Стандартизация отступов
Отступы — не «на глаз». Их нужно утверждать заранее и использовать одинаково по проекту.
Примеры стандартизации:
Отступ после заголовка: 30 px
Расстояние между секциями: 100 px
Между карточками: 24 px
Между строками таблицы: 16 px
Внутренние отступы кнопки: 12–16 px по вертикали
Все эти значения:
согласуются заранее
записываются в документацию
применяются авто-лэйаутами
НЕ меняются в каждом новом блоке
4.2.5 Variables для отступов и размеров
Если проект крупный, количество размеров растёт. Чтобы не хранить их в голове — используется:
Figma Variables → Spacing Variables
Создаются переменные:
Spacing/Section = 100 px
Spacing/Title = 30 px
Spacing/CardGap = 24 px
Padding/ButtonY = 12 px
Padding/ButtonX = 24 px
Преимущества:
можно менять отступы централизованно
можно ограничивать их применение (например, variable только для paddings)
дизайнер не ошибётся в цифрах
все блоки сохраняют единый ритм
4.2.6 Auto Layout как структура интерфейса
Любой сложный блок собирается как дерево Auto Layout:
Секция → Контейнер → Колонка/Ряд → Компоненты → Элементы
Для примера:
Главный блок
Внутри контейнер шириной 1200
Внутри — заголовок (auto layout)
Под заголовком — текстовый блок (auto layout)
Карточки (auto layout grid)
Каждая карточка — компонент из auto layout
Кнопка внутри карточки — тоже auto layout
4.3 Типографика
4.3.1 Шрифты
Основной шрифт утверждается в начале проекта:— либо из брендбука, если он существует;— либо выбирается дизайнером и согласуется с заказчиком до начала дизайна.
После утверждения шрифта весь проект использует только его, без самовольной подмены.
4.3.2 Стиль текста (Text Styles)
В Figma создаются текстовые стили для всех уровней иерархии: H1, H2, H3, Body, Caption и т. д.
Названия стилей должны быть структурированы и информативны, например:H1_Montserrat_B_64_21где:— шрифт,— насыщенность,— размер,— line-height.
На всех макетах используется только стиль, никакой ручной типографики.
Изменения типографики в будущем делаются в одном месте — через обновление стиля.
4.3.3 Иерархия и насыщенность
Bold — ключевые заголовки и визуальные акценты.
Medium — структурные подзаголовки и ключевые UI-подписи.
Regular — основной текст, длинные параграфы, описания.
4.4 Цвета
4.4.1 Основная палитра
Цветовая палитра должна быть утверждена заранее:— из брендбука,— или согласуется как часть визуальной концепции.
Определяются:— основные цвета— акцентный— фоновые— состояния (hover, active, disabled)
4.4.2 Color Styles
В Figma создаются цветовые стили:Primary/Brand, Secondary, Accent, BG/Light, Error/Red и т. д.
Все элементы интерфейса используют только эти стили.
Если цвет меняется — обновление происходит в одном месте через стиль.
4.4.3 Градиенты и эффекты
Используются только если они согласованы как часть визуальной системы.
Градиенты, тени, размытия — тоже оформляются как отдельные стили эффектов.
4.5 Компоненты
4.5.1 Общие правила
Все повторяющиеся элементы упаковываются в компоненты.
Никаких «одноразовых» копий — всё идёт в UI Kit.
Компоненты строятся через авто-лэйауты и используют стили типографики и цветов.
4.5.2 Состояния
Каждый компонент обязан иметь необходимые стейты:
Default
Hover
Active
Disabled
Focused(если релевантно, может быть Loading / Error / Success)
4.5.3 Примеры компонентов
Кнопки — при необходимости размеры (S, M, L), обязательные состояния.
Формы — поля, подписи, хинты, ошибки, валидации, маски.
Карточки — единый паттерн построения, масштабируемая структура.
Элементы навигации — меню, пагинация, табы.
Иконки — единый стиль, единые размеры.
4.6 UI KIT
4.6.1 Структура и хранение
UI Kit хранится в Figma Team Library, подключённой ко всем проектам, либо в рабочем проекте.
Компоненты организуются по разделам:— Buttons— Forms— Navigation— Cards— Icons— Layout / Grid— Typography— Colors / Effects
4.6.2 Обновление
Все обновления выполняются централизованно дизайнером проекта.
Изменения сопровождаются комментариями при необходимости.
Никакие локальные копии компонентов в макетах не допускаются.
4.6.3 Использование
Все макеты собираются только из компонентов UI Kit.
Любой новый компонент сначала утверждается, затем добавляется в библиотеку.
5. Требования к верстке (Frontend Standards)
Фронтенд-разработка должна быть структурированной, стандартизированной и основанной на утверждённом дизайне. Все правила ниже направлены на уменьшение количества правок, повышение стабильности и ускорение разработки.
5.1 Структура проекта и организация репозитория
Создание CORE-репозиторияВедущий фронтендер создаёт базовое Git-хранилище (CORE), где размещаются:
основная структура проекта
базовые зависимости
настройки линтеров и форматтеров
глобальные темы (цвета, размеры, шрифты)
базовая архитектура модулей
единые UI-компоненты
Фиксация архитектурного подходаДо начала разработки команда фиксирует:
структуру директорий
логику именования компонентов
способ подключения стилей
принципы адаптива
оформление глобальных переменных (теминг, токены)
Разделение задач внутри команды
Один разработчик занимается версткой (UI), другой — логикой и бизнес-правилами, третий — интеграцией.
Или: один делает страницу А, другой — страницу Б.Важно: разные разработчики не пересекаются в одних и тех же блоках, чтобы избежать конфликтов.
Сборка результата перед тестированиемПосле завершения задач ведущий фронтенд:
собирает работу команды в отдельную ветку (например feature/ui-assembly)
приводит код к единым стандартам
проверяет консистентность компонентов
отправляет версию на тестирование
5.2 Требования к стилям
Работа строго по дизайн-системе
Цвета, размеры, расстояния, шрифты — строго по UI Kit, без самодеятельности.
Никаких произвольных значений в стилях: каждый размер должен соответствовать токену дизайна.
Использование токенов (design tokens)Все базовые параметры оформляются как переменные:
токены цветов
токены типографики
токены отступов
токены компонентовЭто исключает расхождения между дизайном и версткой.
Единая система отступовВсе spacing-значения берутся только из списка утверждённых размеров.Никаких случайных значений типа 17px, 23px.Если в дизайне 30px → значит 30px в коде.
Модульность и переиспользование
Каждый визуальный элемент должен быть оформлен как компонент.
Общие компоненты (кнопки, карточки, поля) находятся в CORE и не копируются локально.
Адаптивность
Используются те точки перелома, которые указаны в дизайн-системе.
Строго соблюдается сетка, выбранная в дизайне.
Разработчик не имеет права менять структуру блоков без согласования.
Пиксельная точность (Pixel-Perfect)
Все размеры и расстояния должны соответствовать макету.
Допуск по отклонениям — такой, какой установил дизайнер при передаче макетов (обычно ±10 px).
5.3 Работа с компонентами
Полное соответствие компонентам FigmaЕсли в UI Kit есть компонент — он должен быть использован.Если нет — фронт обращается к дизайнеру для добавления.
Состояния элементовВсе состояния кнопок, полей, карточек должны быть реализованы так же, как в дизайне:
default
hover
active
disabled
focus
error (для форм)
Гибкость и масштабируемостьКомпонент должен быть готов к:
изменению текста
изменению количества элементов
адаптиву
локализации
5.4 Кодовые стандарты
Линтеры обязательны (ESLint / Stylelint / Prettier) — ведущий фронт настраивает их в CORE.
Коммиты оформляются по единому стандарту (Conventional Commits или договорённый вариант).
Структура кода не должна зависеть от личных предпочтений каждого разработчика.
Каждый PR должен проходить:
code review
проверку соответствия макету
проверку на совпадение с токенами дизайна
5.5 Требования к взаимодействию с дизайном
Фронт старается задать все вопросы до начала разработки, а не в процессе.
Если элемент кажется неполным — дизайнер обновляет UI Kit, а не разработчик «делает как кажется логичным».
Любые расхождения между макетом и версткой фиксируются.
После сборки версии — проводится walkthrough с дизайнером.
6. Коммуникации
Коротко:
Дизайнер не ходит на все созвоны
Дизайнер подключается только на старт + прототип
Все правки проходят через менеджера
Спорные моменты решает Артем
Прямолинейная критика — через Артема, а не напрямую
7. V0.1 → Что будем добавлять дальше
Например:
Стандарты анимаций.
Стандарты текста.
Стандарты иконок.
Палитра состояния ошибок/валидаторов.
Общий компонентный UI KIT для всех проектов.
Типовые ошибки в дизайне интерфейсов при переходе от “рисования” к реальной UI/UX-разработке
(и как их избежать)
Когда дизайнер начинает работать не просто над красивыми картинками, а над настоящими интерфейсами (мини-аппы, формы, продукты, панели, CRM), всплывают типичные ошибки. Это не потому, что дизайнер “плохой”, а потому что UI — это инженерная дисциплина, а не художественная.
Вот ключевые ошибки, которые совершают 90% начинающих дизайнеров — и рекомендации, как это исправить.
🔶 Ошибка 1. “Картинка вместо системы”
Дизайнер делает интерфейс как постер в Photoshop:
ручные отступы,
элементы расставлены “на глаз”,
нет структуры, нет вложенности,
всё в одном слое,
блоки не связаны логически.
Почему это плохо
Такой дизайн невозможно:
адаптировать под разные экраны,
передавать фронтендеру,
масштабировать,
переиспользовать.
Как правильно
UI — не картинка, а система элементов:
каждый блок — контейнер,
внутри контейнера — логическая группа,
всё должно быть в единой архитектуре,
каждый элемент имеет своё место.
✔ Совет:
Представляй интерфейс не как рисунок, а как скелет + мышцы + кожа.Сначала структура (скелет), потом функциональные группы (мышцы), потом визуал (кожа).
🔶 Ошибка 2. Злоупотребление автолэйаутами (“20 контейнеров ради контейнеров”)
Новичок услышал “автолэйаут — это база” и начинает:
вкладывать блоки друг в друга без необходимости,
делать 10 уровней вложенности,
слепо wrap'ить всё, что видит,
использовать контейнеры не по логике, а “потому что надо”.
Почему это плохо
структура становится нечитаемой,
фронтендеру страшно открывать макет,
невозможно понять, что адаптивно, а что статично,
всё “живёт собственной жизнью”.
Как правильно
Автолэйаут — не цель, а инструмент.
Использовать его нужно:
когда элементы должны жить как группа,
когда блок должен масштабироваться,
когда есть логическая вертикаль/горизонталь,
когда UI строится как компонент.
✔ Совет:
Если элемент не должен тянуться → не делай его резиновым.Если элемент не является логической группой → не кладите его в контейнер.
🔶 Ошибка 3. Непонимание паттернов адаптивности
Новички делают:
фиксированные ширины,
вручную выставленные позиции,
нулевые минимальные и максимальные размеры,
текст, который не помещается при растяжении,
колонки, которые ломаются.
Почему это плохо
В реальном интерфейсе:
экраны бывают 320, 375, 414, 768, 1024, 1440, 1920…
UI должен жить на любом разрешении.
Как правильно
Задавать:
min-width / max-width,
фиксированные или резиновые контейнеры по назначению,
ограничение ширины текста,
грамотную поддержку “узкого”, “среднего” и “широкого” вида.
✔ Совет:
Тестируй интерфейс в Figma: сожми фрейм → растяни → проверь, как ведут себя элементы.
🔶 Ошибка 4. Отсутствие компонентного мышления
Новички делают:
каждый блок уникальным,
копируют элементы вручную,
вносят изменения в 15 мест одновременно,
не собирают UI KIT,
не используют компоненты.
Почему это плохо
правки умножаются на количество копий,
дизайн не масштабируется,
фронт получает 100 вариаций одной и той же кнопки.
Как правильно
Мыслить компонентами:
кнопка
поле
карточка
модалка
теги
переключатели
и т.д.
Все вариации — в Variants.
✔ Совет:
Проектируй интерфейс так, будто его собирают в React — из готовых компонентов.
🔶 Ошибка 5. Непонимание типографики
Типовые проблемы новичков:
7–10 разных размеров текста, без логики,
смесь Regular / Medium / Bold в одном абзаце,
отсутствие иерархии заголовков,
большие расстояния между строками,
неправильный контраст.
Как правильно
3–4 уровня заголовков,
1–2 вида параграфного текста,
единая иерархия,
строгая логика жирностей,
контраст согласно WCAG (или хотя бы визуальному здравому смыслу).
✔ Совет:
Хорошая типографика = 70% ощущение “дорогого” интерфейса.
🔶 Ошибка 6. Цвет как “интуиция”, а не система
Новички:
используют 10 оттенков синего,
берут цвета “на глаз”,
игнорируют брендбук,
смешивают пастель с кислотой,
нарушают контраст.
Правильно
палитра: основной / вторичный / акцент / фон / текст
строгое использование,
никакой самодеятельности.
✔ Совет:
Цвет = язык.Его нельзя менять “на вкус”.
🔶 Ошибка 7. Ручные отступы vs система отступов
Новичок ставит:
12px тут,
14px там,
17px снизу,
9px сверху.
И сам путается, фронтендер путается, всё плавает.
Правильно
Использовать единую сетку: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:
контейнеры,
вложенность,
направление,
растягивание,
фиксированные/резиновые элементы,
ограничения (constraints),
поведение при resize.
👉 То есть дизайнер перестаёт быть “художником”и становится архитектором интерфейса.
Это то, чему учат в сильных школах интерфейса.Это то, чего не хватает большинству новичков.
✔ 2. Figma эволюционирует в сторону “дизайн = полуверстка”
Это важный тренд.
Figma:
давно стала аналогом Flexbox/Grid;
уже экспортирует CSS для большинства компонентов;
умеет показывать DOM-структуру;
скоро будет делать полноценные прототипы с логикой;
уже внедряет Figma Dev Mode — почти окружение для фронта;
может экспортавать компонентные модели.
Мир идёт к тому, что:
“Дизайн → почти готовая верстка”.
✔ 3. Продумывание архитектуры должно быть на этапе дизайна, а не на этапе разработки
Это чистая истина.
Есть золотое правило:
Каждый час, потраченный дизайнером на архитектуру, экономит 3–5 часов разработки.
Поэтому автолэйаут:
уменьшает количество переделок на фронте,
уменьшает количество “а давайте поправим на глазок”,
уменьшает “случайные пиксели”,
избавляет от “вёрстка развалилась на 375px”.
✔ 4. Эта культура делает дизайнеров сильнее
Дизайнер, который:
думает вложенностью,
понимает контентные блоки,
чувствует адаптивность,
видит где фикс, а где резина,
знает, что такое baseline grid,
понимает продуктовую логику,
делает прототипы, устойчивые к масштабированию,
— это уже не “рисуем-как-чувствуем”.Это уже UI/UX-специалист уровня компании.
Это стратегически правильно.
✔ 5. В будущем часть разработки будет делаться автоматически
Уже сейчас:
Figma Plugins экспортируют React компоненты, Tailwind классы, HTML/CSS.
Locofy, Anima, Relume → генерируют рабочую верстку из Figma.
Vercel AI + Figma → создают JSX из макетов.
Figma Dev Mode → уже показывает почти готовые фрагменты кода.
Через 2–3 года:
“Макет → код” станет обычной практикой.А разработчики будут дорабатывать только логику и интеграции.
Это направление всей индустрии.
✔ 6. А теперь ключевое:
Автолэйаут — это НЕ про “делать всё правильно”.
Это про движение команды к будущему, где дизайн = система.
Важно строить систему:
стандарты,
UI KIT,
автолэйауты,
компоненты,
мини-конструктор виджетов,
Web UI-платформу,
возможность автоматической генерации UI,
построение интерфейсов “из коробки”,
→ это полностью совпадает с трендами.
Но:
👉 Если команда НЕ ГОТОВА пересесть на это сразу.
Нужно:
вести команду правильно,
не ломать людей,
вводить стандарты итеративно,
не создавать конфликтов,
не делать резких переходов.
✔ 8. Сложно ли этому научиться?
Для нормального дизайнера:
🔹 Базовый автолэйаут — 1 день🔹 Уверенная работа — 3–5 дней🔹 Грамотные сложные структуры — 2–3 недели🔹 Полное мышление “как фронтендер” — 1–2 месяца
Это реализуемо.Только нужно:
обучающий модуль,
маленькие упражнения,
разбор ошибок,
примеры из проектов.
Как понимать контекст проекта
Этот материал объясняет:
разницу типов проектов,
что такое mission critical / non-mission-critical,
когда дизайнер должен общаться с заказчиком,
а когда избыток коммуникаций — во вред,
как отличается клиентский опыт в продуктах и в мини-апах,
как всё это классифицировать,
как команда может ориентироваться в этих режимах и не путать одно с другим.
🟧 Два мира разработки: продукты vs мини-аппы. Как отличать, как работать и какие процессы нужны
Мы сталкиваемся с разными типами проектов. Где-то мы делаем полноценный продукт, который живёт годами, влияет на бизнес-процессы и проходит десятки согласований. А где-то — небольшой мини-апп, простой инструмент или небольшую коммуникационную вставку внутри чат-бота.
Чтобы команда работала эффективно и не пыталась применить “тяжёлые” процессы там, где они только мешают, важно понимать фундаментальную разницу между двумя классами проектов:
Полноценные продукты / Mission Critical
Мини-аппы и поддерживающие интерфейсы / Non-Mission-Critical
Это два разных мира, требующих двух разных подходов — в дизайне, разработке, коммуникациях и управлении ожиданиями.
🟧 1. Что такое Mission Critical, и почему эти проекты другие
Mission Critical — это продукты, без которых бизнес не сможет выполнять свою основную функцию.
Примеры:
Яндекс.Такси — приложение не работает → такси не существует
Интернет-банк — ошибка → бизнес клиента остановился
Личный кабинет госуслуг → критичен для доступа к услугам
Управляющая панель в промышленности (логистика, производство, расчёты)
У промышленной компании, например фармацевтической, mission critical — это производство таблеток, логистика, ERP, склад, документоборот.
Чат-боты, мини-формы, калькуляторы, маркетинговые инструменты — не mission critical.Их можно улучшать, допиливать, полировать, но они не остановят бизнес.
Отличительные признаки Mission Critical:
UI = бизнес-операция, ошибка стоит дорого
требуется глубокая выверенная архитектура
десятки согласований
UX-исследования
строгая типографика, модульные сетки, дизайн-системы
дизайнер обязан общаться напрямую с заказчиком
множество рабочих встреч
нужно понимать роли, сценарии, ограничения
всё должно быть документировано
много уровней ответственности
Там нет “примерно нормально” — там только идеально и надёжно.
🟧 2. Что такое Non-Mission-Critical (наши текущие мини-аппы)
Мини-аппы, калькуляторы, формы, промо-страницы, быстрые B2B-калькуляторы в мессенджерах — это дополняющие интерфейсы.
Они помогают:
сократить путь пользователя,
поддержать маркетинг,
объяснить продукт,
загрузить данные,
собрать обратную связь.
Но не являются ядром бизнеса.
Отличительные признаки таких проектов:
одна или две встречи достаточно
можно опираться на вкус и здравый смысл
нет 50 согласований
нет десятков UX-сценариев
проще требования к цветам, сетке, блокам
допускаются разумные компромиссы
UI — не синоним бизнес-операции, а инструмент коммуникации
Это не значит “делать плохо”, это значит не переусложнять там, где это не нужно.
🟧 3. Почему процессы должны быть разными
Если на non-mission-critical проекты натянуть процессы от больших продуктовых студий, произойдёт следующее:
сроки вырастут x3–5
стоимость выйдет за рамки бюджета
ценность UI станет ниже цены его производства
клиент удивится, почему такие простые задачи делаются так дорого
команда будет перегружена ненужными встречами
дизайнеры будут ловить лишние правки и вкусовщину
фронтендеры погрязнут в овердизайне
Но если на mission critical проект натянуть подход “как для мини-аппа” — бизнес словит катастрофу.
Поэтому нужен двухрежимный подход.
🟧 4. Должен ли дизайнер встречаться с заказчиком? Да — но не всегда.
Два типа проектов = два типа участия дизайнера.
🟩 Тип 1. Полноценные продукты — дизайнер участвует активно
Тут дизайнер — не художник и не исполнитель.Он — аналитик, переговорщик, продуктовый партнёр.
Его обязанности:
собирать требования
уточнять задачи
выяснять сценарии
согласовывать логику
прототипировать
решать UX-проблемы
участвовать в созвонах
защищать решения с цифрами и логикой
Без прямой коммуникации тут работать невозможно.
🟦 Тип 2. Мини-аппы — дизайнеру достаточно 1-2 встречи
В этих проектах:
нет сложной логики,
нет рискованных пользовательских сценариев,
нет десятков ролей и use-case'ов,
нет mission critical-функций,
интерфейс не решает судьбу компании.
Поэтому дизайнеру достаточно:
1 вводной встречи,
1 обсуждения прототипа,
остальная коммуникация — через менеджера.
Прямой контакт с клиентом после этого ничего не улучшит, а часто только ухудшит:
начинается вкусовщина,
“попробуйте зелёный”,
“давайте кнопку побольше”,
“а сделайте фон потемнее”,
дизайнер теряет фокус,
дизайн разваливается,
сроки растут.
Дизайнер защищает эстетику и логику →Менеджер фильтрует запросы →Команда работает спокойно.
Это правильный процесс.
🟧 5. Разные типы клиентского опыта (CX): “деловой” vs “приятный”
Чтобы правильно строить интерфейсы, важно понимать разницу между двумя видами клиентского опыта.
🟩 Тип 1. “Деловой” клиентский опыт (B2B, сервисный, утилитарный)
Цель: дать результат быстро, чётко и без лишних действий.
Примеры:
B2B калькуляторы
формы заказа
заявки
рабочие панели
кабинки операторов
функциональные мини-аппы
Тут интерфейс должен быть:
быстрым
простым
прямым
без флейвора
без украшательств
минимум шагов
минимум когнитивной нагрузки
Это “инструмент”, а не “впечатление”.
🟦 Тип 2. “Приятный” клиентский опыт (развлечения / эмоции / smacking experience)
Цель: вовлечь, развлечь, удержать внимание.
Примеры:
рестораны, еда, сервис, доставка
игры
развлекательные проекты
брендинг
storytelling-интерфейсы
Здесь можно:
растягивать путь,
играть с эмоцией,
добавлять анимации,
давать “вкус” и “медленность”,
делать journey красивым.
🟧 6. Как классифицировать проект за 1 минуту
Задаём два вопроса:
❓ Если интерфейс сломается — пострадает ли основная миссия бизнеса?
Если да → это mission critical → полноценный продуктовый процесс.Если нет → это мини-апп → упрощённый процесс.
🟧 7. Что важно
Не надо натягивать продуктовые процессы на мини-аппы.Это убивает скорость и ценность проекта.
Не надо упрощать миссион-критичные проекты.Там нужна глубина.
Дизайнер не должен общаться с заказчиком там, где это во вред.Коммуникация — ресурс, его надо дозировать.
Нужно уметь классифицировать тип задачи с первых минут проекта.
Наша цель — гибкость.Мы должны одинаково уверенно вести оба типа проектов.
🟧 8. Тонкое различие, которое важно осознать
Мы не “студия, которая делает всё одинаково”.Мы команда, которая умеет менять подход под контекст.
Где-то нужен продуктовый UX, десятки встреч и прототипы.Где-то достаточно одного звонка, одного прототипа и менеджерского фильтра.
Зрелость = умение различать.Уроки
Как построить визуальный no-code редактор воронок и AI пайплайнов на React поверх low-code ядра Metabot
Попробовать: 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
Мы подробно посмотрим, как:
входящий JSON обрабатывается,
шаги проверяются,
неподдерживаемые типы фильтруются,
нужные команды собираются,
создаются переходы, кнопки, логгирование, LLM-вызовы и так далее.
То есть вы поймёте логику трансформации вашего формата → формат Metabot.
Вы получите исходный код Mapper'а.
3. Разберётесь, как работает Builder
Мы детально пройдём по механике:
создания секции/папки скриптов в Metabot,
создания скриптов,
добавления low-code команд в скрипт,
создания переходов и menu-кнопок,
удаления старых версий воронки.
И главное — вы увидите, как Builder напрямую манипулирует базой данных Metabot (PostgreSQL) через PHP/V8.
Это даёт понимание того, как работает ядро платформы и как можно строить свои надстройки.
Вы получите исходный код Builder'а.
4. Научитесь подключать Metabot как backend
Вы увидите, как:
передавать JSON в Metabot через API,
запускать импорт,
пересобирать воронку целиком,
структурировать код для своего продукта.
То есть Metabot превращается для вас в backend-as-a-platform, который вы можете использовать под любые проекты.
5. Сделаем визуальный редактор на React (с помощью V0 + Cursor)
Мы:
создадим canvas и интерфейс узлов,
свяжем их с JSON,
добавим импорт и экспорт в/из файла,
добавим базовые настройки шагов и валидации,
подготовим front-end архитектуру.
Вы получите:
исходный код редактора,
архитектуру компонентов,
промпты, с помощью которых редактор собирался (это важно — это обучает самостоятельному созданию продуктов).
6. Настроим рабочий цикл разработки
Первые итерации будут такие:
1) создаём JSON вручную →
2) отправляем через Postman →
3) смотрим, как Metabot создал воронку →
4) фиксируем ошибки →
5) делаем свои команды →
6) строим визуальный редактор, который уже генерит нужный JSON →
7) импортируем в Metabot одной кнопкой
Это честный, практичный, продуктовый процесс.
7. Вы получите всё, чтобы создать свой продукт
В итоге у вас будет:
работающий визуальный редактор,
свой JSON-формат,
свой набор команд,
сохранение в файл и загрузка из файла,
исходники маппера/билдера,
связка React → JSON → Metabot.
На этом можно строить:
no-code конструктор,
CJM-редактор,
AI-конструктор цепочек,
бизнесовый продукт поверх Metabot.
🚫 Чего в этом уроке НЕ будет
Чтобы не вводить в заблуждение — сразу фиксируем:
1. Не будет двусторонней синхронизации
Мы делаем только импорт из вашего конструктора → Metabot.
То есть:
если воронка существует — она удаляется и создаётся заново,
мы не читаем текущую структуру из Metabot,
мы не отображаем её обратно в редактор.
Это осознанное упрощение.
Вторую сторону можно сделать, но это отдельный блок задач:читать данные Metabot → конвертировать → отображать → синхронизировать версии.
Мы это оставляем для следующего урока, если будет запрос.
2. Не будет полноценного менеджмента трафика
Это очень важный момент.
В реальной жизни по ранее импортированной воронке уже могут идти живые пользователи.Если вы меняете воронку, возникают вопросы:
что делать с активными пользователями?
надо ли их перенаправлять в новую версию?
надо ли блокировать обновления?
нужно ли создавать копии воронок?
нужно ли делать миграции шагов?
как правильно писать стратегию версионирования?
Это целая философия, и у каждого продукта — свой подход.Мы эту сложность сознательно не включаем в этот урок.
Но если понадобится понадобится помощь — дайте знать, попробуем помочь.
3. Не будет полноценной "платформенности"
В этом уроке:
нет авторизации и регистрации — импорт по API токену пользователя, привязанному к боту.
нет совместной работы.
Мы делаем первую версию конструктора, но она уже рабочая и расширяемая.
Принял. Делаю первую итерацию — без фанатизма, но уже структурно, лаконично, красиво, чтобы можно было вложить в документацию и развивать дальше.Ты дал огромный объём информации, поэтому я сейчас фиксирую каркас главы 2, + делаю первый блок: спецификация команд Metabot + JSON-формат, + простую таблицу команд, красиво оформленную, + мини-описание модели данных (sentences, phrases, references).
В следующей итерации я смогу:
добить оставшиеся разделы,
оформить финальный CJM JSON на 20 шагов,
добавить продвинутую постановку задач.
Глава 2. JSON-формат и фундаментальная модель данных Metabot
Чтобы создать собственный визуальный конструктор поверх Metabot, нужно понимать две вещи:
Какие команды реально существуют в ядре платформы
Как 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
Палитра команд платформы:
Короткое объяснение логики исполнения команд
Команды внутри одного скрипта (sentence) выполняются сверху вниз по sort_order.
Любая команда типа run_sentence или run_javascript может мгновенно передать управление в другой скрипт.
Кнопки (references) — это отдельная сущность: они не лежат в phrases, но участвуют в сценарии. В интерфейсе меню расположено внизу скрипта, после списка команд.
«Зачем нам свой конструктор и свои команды»
После того как мы разобрались, какие команды реально существуют внутри Metabot и посмотрели, как они физически хранятся в базе (sentences, phrases, references), мы можем перейти к ключевой идее нашего урока:
Мы не будем создавать интерфейс, который один-в-один повторяет Metabot.Мы создадим свой собственный визуальный конструктор, и в нём у нас будут свои команды — более удобные, более компактные и намного более выразительные.
Почему так?
Почему в Metabot всё разбито на 21 команду
(и почему это отлично для движка, но неудобно для дизайнера чат-ботов)
Metabot — это low-code ядро. Каждая команда — это минимальная атомарная операция:
отправить текст,
отправить картинку,
записать тег,
выполнить JS,
перейти в скрипт,
дождаться вводаи так далее.
Это правильно для движка:движок должен быть предсказуемым и детерминированным.
Но для человека, который рисует сценарий, это слишком низкий уровень.Чтобы собрать один «узел» диалога, дизайнеру нужно вручную собрать:
1–3 send_text
условия и JS
настройки под разные каналы
кнопки меню (в references)
обработчики кликов (ещё несколько run_sentence + JS)
JS для логирования для аналитики
триггеры для отслеживания ссылок
переходы к следующему шагу
В итоге один визуальный узел превращается в:
→ 6–12 низкоуровневых фраз в базе
→ 2–4 скрипта (sentences)
→ и пачку записей в references
Именно поэтому мы не хотим заставлять пользователя руками разбираться в 21 команде ядра. Мы хотим сократить этот слой.
Теперь проектируем наш собственный редактор
Мы создаём свою систему узлов — такую, в которой:
один узел = одна логическая операция сценария,
пользователь видит высокоуровневую настройку,
а всё сложное — «распаковывается» автоматически внутрь Metabot при импорте.
Это и есть смысл маппера и билдера.
Какие наши команды мы сделаем первыми
1. Start / Entry Node
Стартовый узел воронки.Даёт визуальную точку входа + будущую связь с диплинками.
Вход в воронку возможен как из других воронок / сценариев, так и снаружи при переходе в мессенджер из приложений или из контента в Интернет.
Пример шага в новом no code редакторе:
Пример настроек в новом no code редакторе:
Пример 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.Наша версия включает:
💬 универсальное сообщение
💬 кастомизация сообщения под каналы (Telegram, WhatsApp, VK, WebChat и так далее)
🔘 поддержка кнопок
⚙️ обработчики событий нажатия на кнопку
📌 запись в аналитику прямо внутри узла
🔗 поддержка ссылок (с автогенерацией триггеров)
В Metabot для этого потребовалось бы от 6 до 12 низкоуровневых команд.У нас — один визуальный блок.
Пример шага в новом no code редакторе:
Форма редактирования стандартной команды "Отправить текст" в low-code редакторе:
Форма редактирования новой команды "Отправить текст" в новом no-code редакторе, сравните возможности:
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-пекло от пользователя.
В нашем интерфейсе есть:
выбор типа логирования: step, event, tag, utter,
выбор пути (way),
параметры.
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.
В нашем редакторе — просто стрелка между узлами.
А в 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
Это не самый свежий пример кода, но рабочий. Здесь мы делаем:
выбор провайдера и модели
error / fallback узел
устанавливаем агента и подключаем таблицу промптов
system / user prompts
способ отображения ответа
параметры истории
отправляем запрос, принимаем, выводим
Всё это — внутри одного команды и требует базового владения JS кодом.
В новом редакторе упакуем все это в один no code узел:
И одну форму, чтобы пользователю не пришлось писать JS код — мы его будем генерить за него при импорте в low-code:
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.
Обратите внимание у узла мы добавили три выхода: успешный, ничего не найдено и ошибка.
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)
Каждая наша команда — это высокоуровневый «комбинированный» блок.Когда пользователь его настраивает, мы:
собираем данные в простой JSON (наш формат),
передаём этот JSON мапперу,
маппер превращает один наш узел в:
1 или несколько sentences
десятки phrases
references
JS-код
триггеры
диплинки
переходы
аналитику
и так далее
То есть пользователь видит простую, компактную визуальную воронку, а под капотом строится настоящая сложная система Metabot с триггерами, скриптами, условиями, логированием и разветвлёнными сценариями.
Экономический эффект / ROI
Чтобы собрать один логический узел вручную, в Metabot приходится склеивать десятки микродействий: несколько сообщений, кнопки, переходы, теги, аналитику, JS-обработчики, триггеры, условия. Каждое из этих действий занимает секунды или минуты. А в реальном проекте — это часы.
Чем сложнее сценарий, чем массивнее продукт — тем больше времени уходит не на смысловую работу, а на рутину: перенести текст, добавить теги, прописать переходы, подправить сортировку, создать скрипт, связать кнопки. Всё это множится в геометрической прогрессии.
И вот здесь становится очевиден смысл no-code-редактора.
Один визуальный узел в нашем редакторе может вместить всю эту низкоуровневую сложность: аналитику, кнопки, ссылки, условия, LLM-вызовы, JS-код, обработку ввода, переходы — всё внутри одного компактного блока с настройками.
Пользователь просто выбирает тип узла, заполняет пару полей — и весь сложный набор действий раскладывается движком автоматически.Вместо десятка операций — одна.Вместо нескольких минут — несколько секунд.Вместо часов — десятки минут.
На дистанции это превращается в огромную экономию ресурсов.За неделю — это часы.За месяц — дни.За год — целые недели чистого рабочего времени, которые раньше уходили на рутину.
И это уже прямой экономический эффект:команды создают продукты быстрее, дешевле, рентабельнее;клиенты получают большую ценность за те же деньги;а вы можете делать более сложные решения теми же силами.
При этом мы сохраняем лучшее из двух миров:
Скорость и простоту no-code для типовых сценариев
Гибкость low-code/full-code ядра Metabot для любых нестандартных кейсов
Нужна свобода — подключили разработчика и дописали кастомный 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 это будет один объект.
Таким образом:
Metabot = «список команд, выполняющихся последовательно»
Новый формат = «автомат состояний»: step → step → step
Этот подход радикально упрощает визуальное проектирование и делает структуру понятной.
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»
Мы сознательно включаем поля:
format: имя формата (например, cjm)
version: версия схемы
Это позволяет:
✔ поддерживать совместимость старых воронок
Если формат JSON изменится (а он будет меняться), старые импорты не сломаются.
✔ нескольким продуктам использовать разные форматы
Один редактор → один формат.Другой редактор → другой формат.Все они могут жить в одной экосистеме Metabot.
✔ при импорте определять, как парсить данные
Mapper видит format + version и понимает, какой алгоритм применять.
4. Название и код воронки
Поля:
code — машинное имя воронки
title — человекочитаемое название
В метаботе мы формируем имя секции:
cjm:1.0:quiz_lead_magnet_tags:Воронка WayLogger
Секция (папка) создаётся заново при каждом импорте, что даёт чистый, предсказуемый результат.
5. Массив шагов steps — сердце машины состояний
Каждый шаг описывается JSON-объектом:
уникальный code (имя узла)
тип шага (send_text, analytics, llm_call, …)
переходы в другие шаги
кнопки
аналитика
делегируемые действия (напр. отправить файл, записать теги)
координаты (для визуального редактора)
любые дополнительные поля
Идея проста:
Один наш шаг = целый набор низкоуровневых команд Metabot.
Визуально шаг выглядит как блок на диаграмме (см. скриншот), а в импорте он превращается в цепочку скриптов и команд.
6. Как мы создаём скрипты и команды при импорте
Во время импорта Metabot получает:
format
version
code (воронки)
steps (шаги)
Mapper делает следующее:
✔ 1. Находит и удаляет старую cекцию/папку со всеми скриптами
(имя: format:version:code:title)
✔ 2. Создаёт новую секцию
Внутрь неё будут помещаться все скрипты.
✔ 3. Для каждого шага создаёт один или несколько скриптов
Количество зависит от сложности шага:
send_text → 1 скрипт
send_text + buttons + analytics → 1–3 скрипта
и так далее.
✔ 4. Формирует уникальные коды скриптов
Это ключевой момент.
Мы склеиваем:
::<сjm_funnel_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» превратится в:
send_text
run_sentence (переход)
кнопки в references
еще один скрипт с run_javascript (аналитика)
7. Почему шагам обязательно нужен уникальный code
Каждый узел должен иметь уникальный идентификатор.Без него невозможно:
связать узлы между собой
построить диаграмму
обновлять узлы
делать автогенерацию
однозначно создать скрипты в Metabot
Пользователь может менять коды шагов — но внутри маппера этот код всегда станет частью полного имени скрипта.
8. Что происходит со старой версией воронки
В текущей версии урока мы применяем упрощённый алгоритм:
✔ удалить старую секцию полностью
✔ создать новую с тем же именем
✔ импортировать шаги заново
Это быстрый, предсказуемый, безопасный метод.
Если нужно:
версионность → меняем version
несколько версий воронки → клонируем формат
продвинутый sync → пишем собственный маппер (за пределами этого урока)
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}`)
}
Всё. Теперь можно импортировать воронки из:
Postman
вашего React/Vue редактора
ChatGPT (сгенерировали JSON → вставили → импортировали)
Глава 3. Import: Mapper и Builder — ядро нашего no-code импорта
Теперь, когда мы разобрали сам формат JSON-схемы, можно перейти к механике её импорта в Metabot. На платформе в Common.CJM доступны два ключевых плагина, которые вместе образуют мост между вашим JSON и реальными скриптами в базе данных:
Mapper (JavaScript) — читает JSON, создаёт структуру карты, генерирует команды и меню.
Builder (PHP) — низкоуровневый слой, который напрямую создаёт секции, скрипты, команды и переходы в базе.
Эти плагины доступны всем пользователям платформы, но важно понимать их статус:
Общий Mapper и Builder — это обучающие версии, не полноценный коммерческий конструктор.Они упрощены, без множества проверок и UI-логики. Их задача — показать принципы, на которых строится no-code.
Исходники мы даём прямо в статье — вы можете взять их за основу и создать свой собственный конструктор.
На общем сервере Metabot нельзя ставить новые PHP-плагины (по соображениям безопасности), но на выделенном сервере или коробочной установке — можно.
Как работает связка Mapper → Builder
Чтобы показать структуру максимально практично, мы дальше будем двигаться блок за блоком:показываем фрагмент кода → объясняем, что он делает → даём рекомендации, где можно улучшить.
Но вначале — краткая логика работы всей цепочки.
1. Устанавливаем контекст: выбор бота и проверка доступа
Mapper — это JS-плагин, но он работает через Builder, а Builder — PHP-уровень, который:
проверяет принадлежность бота к бизнесу;
отслеживает попытки добавления скриптов в чужой бот;
обеспечивает гарантии целостности данных.
Поэтому первым шагом Mapper вызывает:
setBotById(botId)
Если бот не относится к текущему бизнесу — импорт прерывается.Это фундаментальный слой безопасности, и Builder строго следит, чтобы вы не могли повредить данные другого бота.
2. Создаём секцию под карту
При импорте мы:
формируем префикс (format:version:mapCode),
удаляем предыдущую секцию целиком,
создаём свежую секцию с чистой структурой.
Это важно, потому что карта имеет жёсткую целостность:скрипты, переходы, обработчики кнопок, логирование — всё должно меняться согласованно.
3. Почему импорт идёт в три слоя, а не линейно?
Вот здесь та часть, которую многим разработчикам сложно понять.
Нельзя просто:
взять шаг,
создать под него скрипт,
добавить команды,
добавить кнопки,
создать переходы.
Почему?
Потому что кнопки и переходы требуют ID-скриптов, а ID знает только Builder, и только после создания всей секции.
Поэтому алгоритм у нас такой:
Шаг А — создаём скрипты.
Это даёт нам ID каждого шага.
Шаг Б — наполняем команды.
Включая аналитику, custom script, call_llm, search_kb.
Шаг В — собираем меню (кнопки).
Каждая кнопка — это либо прямой переход, либо создание отдельного скрипта-обработчика.
Шаг Г — собираем переходы.
Все переходы хранятся временно в массиве transitions,и только в самом конце добавляются в базу.
Именно это обеспечивает корректную топологию карты.
Этот подход можно усложнить и оптимизировать,но важно понимать, что «слоистость» — не прихоть, а необходимость, если мы хотим:
избежать циклических зависимостей;
избежать ситуаций, когда ссылка создаётся раньше объекта;
сохранять атомарность структуры карты.
Исходный код Маппера и Билдера
Полный код Mapper и Builder из этого урока:
Common.CJM.Builder (PHP)
GitVerse: https://gitverse.ru/metabot/metabot-cjm-designer/content/main/metabot/Common.CJM.Builder.php
GitHub: https://github.com/art-yg/metabot-cjm-designer/blob/main/metabot/Common.CJM.Builder.php
Common.CJM.Mapper (JS)
GitVerse: https://gitverse.ru/metabot/metabot-cjm-designer/content/main/metabot/Common.CJM.Mapper.js
GitHub: https://github.com/art-yg/metabot-cjm-designer/blob/main/metabot/Common.CJM.Mapper.js
ЧАСТЬ 3.1 — Builder (PHP): низкоуровневое ядро конструктора
Builder — это низкоуровневый плагин, который напрямую работает с таблицами Metabot:
sentences — скрипты;
phrases — команды внутри скриптов;
script_sections — секции (папки);
references — меню (кнопки переходов);
botlog — логи.
Mapper на JavaScript работает «поверх» Builder’а, но именно Builder делает грязную работу:
✔ создаёт реальные записи в БД✔ удаляет секции и весь вложенный контент✔ проверяет политику доступа✔ обеспечивает целостность ссылок✔ создаёт команды и меню✔ соединяет скрипты между собой
Builder — это ваш «микро ORM + сервис создания бота».Mapper — лишь удобная оболочка.
Зачем Builder существует вообще?
Потому что:
V8-движок Metabot может выполнять JavaScript, но не имеет прямого доступа к БД;
PHP — серверное ядро платформы, которое защищено, логирует действия и выполняет транзакции;
Builder — это bridge между JS и PHP, через V8 Wrapper.
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();
Он проверяет:
передан ли текущий бизнес
передан ли текущий бот
принадлежит ли бот действующему бизнесу
корректен ли lead
корректна ли топология
установлен ли botId
Без этого Builder вообще не работает.
Методы Builder: полный разбор
Теперь — весь API Builder, который нам нужен для маппера и для создания собственного конструктора.
1) setBotById(botId)
Устанавливает текущий бот для всех дальнейших операций.
Почему нельзя делать это в JS напрямую?Потому что выбор бота — потенциальная точка атаки.
Метод делает:
ищет Bot
проверяет, что bot.business_id принадлежит текущему бизнесу
сохраняет _bot и _botId
Возвращает:
['success' => true, 'bot_id' => X]
2) createSection(sectionTitle)
Создаёт папку (ScriptSection) для новой карты:
bot_id
name (полное имя "format:version:mapCode:mapTitle")
сортировка = 0
Builder не создаёт вложенность, всё плоское.Вложенность симулируется именованием.
3) deleteSectionByCodeDeep(sectionCode)
Самый опасный и самый важный метод.
Удаляет всю секцию по коду и все её вложенные элементы.
Пример: для карты с кодом testmap удалит всё, что содержит "testmap".
Делает:
ищет section
удаляет все скрипты в этой секции
удаляет все фразы этих скриптов
удаляет все меню (reference)
удаляет все переходы в run_sentence, которые ссылались на эти скрипты
удаляет логи
удаляет саму секцию
Это глубокая очистка, без которой импорт JSON невозможен.
Логи приходится удалять, потому что иначе Metabot не даст удалить скрипты из-за целостности БД. Это не очень хорошая практика т.к. по воронке могли пройти лиды и логи нужны. Это к вопросу, который мы поднимали выше, что простого одностороннего импорта, удаляющего старую версию, в полноценном продукте не достаточно. Для урока мы сознательно упрощаем.
4) createScript(sectionId, code, name)
Создаёт пустой скрипт в конкретной секции:
sentence.bot_id = this->_botId
sentence.section_id = sectionId
sentence.code = code
sentence.name = 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)
Создаёт команду (фразу) внутри скрипта.
Поля:
sentence_id — скрипт
type — тип команды (send_text, run_javascript, run_sentence…)
content — JSON или строка
alias — идентификатор команды (UUID либо переданный)
sort_order — порядок
Это фундаментальная часть, потому что Mapper весь low-code строит только на этих операциях.
8) existsScript / existsCommand
Проверяют существование.Mapper использует для валидаций.
9) deleteCommand(scriptCode, commandCode)
Удаляет фразу с нужным alias.
Обратите внимание, что в low-code интерфейсе конструктора у команд нет алиасов, а в БД есть.
10) createMenuItem()
Создаёт кнопку в меню:
Поля:
sentence_id — скрипт, где кнопка находится
jump_sentence_id — скрипт, куда ведёт
caption — текст кнопки
code — иногда это input_code
sort_order — порядок кнопки
line_num — расположение ряда (для Telegram клавиатур)
js_condition — условие показа
Mapper формирует их в конце, когда все ID известны.
11) findSectionByCode / findSectionByTitle
Используются для поиска секций перед удалением.
12) deleteSectionByIdDeep
Глубокое удаление секции (аналогично deleteSectionByCodeDeep).Именно этот метод делает большую часть работы:
удаляет скрипты, фразы, меню, логи
удаляет переходы между скриптами
очищает всё до пустого состояния
В сумме Builder умеет:
1) Управлять секциями
создавать, искать, удалять с зависимостями
2) Управлять скриптами
создавать, искать, удалять, проверять существование
3) Управлять командами
создавать, удалять, находить
4) Управлять меню
создавать кнопки и переходы
5) Очищать полностью старую карту
deep delete секции
6) Гарантировать безопасность через CheckPolicy
никаких операций вне текущего бизнеса
Итого о Билдере
Builder — это низкоуровневый API конструктора Metabot, на котором можно собрать:
no-code редактор
low-code редактор
визуальный CJM editor
auto-generator воронок из JSON, GPT, CRM
свой собственный mini-n8n или BotHelp-лайк движок
Mapper лишь использует Builder, но Builder — это фундамент.
🔎 Примечание о расширении Builder
Если вам понадобится добавить в Builder новый функционал — вы можете это сделать, но важно учесть следующее:
PHP-плагины запрещены на общем облачном сервере Metabot в целях безопасности.Поэтому собственные версии Builder можно устанавливать только:
на выделенном сервере, или
в коробочной (on-premise) версии платформы.
Если вам требуется расширить Builder именно под свой продукт, вы можете создать собственный плагин Builder и развивать его так, как вам нужно. Это полностью поддерживается на выделенных/коробочных установках.
Если же вы хотите предложить улучшение или новый метод в общий Builder, вы можете прислать ваш вариант реализации или концепцию в поддержку Metabot.Мы рассмотрим предложение и, если оно подходит под архитектуру платформы, подумаем о включении в будущие версии общего плагина.
Подробнее о возможностях выделенных серверов и коробочных лицензий смотрите здесь:https://metabot24.ru/price/tarify-na-platformu/
3.2 — Маппер: подробный разбор архитектуры и алгоритма импорта
Маппер — это верхнеуровневый JS-плагин, который принимает JSON-описание вашей схемы (воронки) и преобразует её в реальную структуру скриптов Metabot через Builder. Mapper не работает с базой напрямую — он опирается на PHP-плагин Builder, а сам выполняет только логику трансформации структуры шагов, генерацию JavaScript для команд и построение связей.
Маппер вы можете разместить в плагины вашего бизнеса и использовать на любом сервере, включая общий облачный. Либо можете использовать нашу последнюю версию из Common.CJM.Mapper.
Основная ответственность Mapper:
Проверить формат и подключить нужного бота
Создать секцию (папку) под воронку
Создать скрипты под каждый шаг
Наполнить эти скрипты командами
Сгенерировать переходы (default/аналитика/кнопки)
Сгенерировать и подключить JS-команды: LLM, KB-поиск, кастомный скрипт, логгер
Построить меню-кнопки и обработчики
Добавить все переходы между скриптами
Далее — полный разбор устройства 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 обязан:
Установить бота
Пройти 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)
}
В результате у нас есть:
полный список скриптов
их ID
steps, которые мы точно умеем обработать
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
Пока заглушка, но сюда можно добавить:
генерацию deep links
автогенерацию триггеров
multi-entry routing
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 анализирует:
system_prompts (start/final)
provider/model
prompt_table
error_step / next_step
history
response formatting
И превращает всё это в JS-«рецепт», который выполняется в Metabot.
search_knowledgebase
Похожий алгоритм:
this.builder.createCommand(
scriptCode,
'run_js_callback',
this._buildSearchKbJs(step, mapPrefix)
)
Здесь генерится код:
вызов поиска через KnowbaseSearch
сохранение результата в lead.attr
опциональный Telegram-debug
переход в success/not_found/error шаги
3.2.5. Фаза 3 — Default Exit и Transition Collection
После switch-блока мы добавляем переходы:
Логика:
Если есть логгер — первым переходом идём в логгер.
Потом логгер → next_step
Если логгера нет — просто script → next_step
Но если есть кнопки — дефолтный выход НЕ создаётся.
Mapper пока не создаёт команду перехода — только сохраняет их:
transitions.push({ from: scriptCode, to: nextScriptCode })
Это тоже важно: мы не можем создавать переходы, пока не созданы ВСЕ скрипты.
3.2.6. Фаза 4 — Обработка кнопок и создание handler-скриптов
Блок обработки кнопок — наиболее сложный и многослойный участок маппера.Причина простая: каждая кнопка — это мини-сценарий внутри сценария, и внутренняя логика кнопки может требовать построения отдельных скриптов:
1) Скрипт-обработчик кнопки (handler script)
Создаётся, если:
нужно выставить значение в lead/bot
нужно добавить теги
нужно удалить теги
есть кастомная логика кнопки
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)
Теперь, когда:
все скрипты созданы
команды добавлены
кнопки созданы
handler-скрипты созданы
— наконец можно создавать команды-переходы 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 содержит важные приватные функции:
_buildLoggerContent(step)
_buildCustomScriptJs(step)
_buildCallLLMJs(step)
_buildSearchKbJs(step)
Они генерируют JavaScript прямо на лету, подставляя параметры шага.
Это архитектурно мощно, потому что:
позволяет описывать сложные команды чистым JSON
Mapper превращает JSON → JS → low-code конструкцию
можно легко добавлять новые типы шагов
Однако, лучше это вынести в отдельный модуль или Snippet, чтобы код маппера был чище.
3.2.9. Итог импорта
Mapper возвращает:
return {
success: true,
section_id: sectionId,
created_scripts: createdScriptsCnt
}
3.2.10. Архитектурные преимущества Mapper
Чистая модель автомата состояний
Поддержка аналитики на уровне схемы
Универсальные handler-скрипты для кнопок
Отсутствие foreign-key зависимостей
JS-код, генерируемый на лету
Изоляция каждой воронки через namespace кодов
Предсказуемая однопроходная структура импорта
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.Теперь у вас есть полный набор кирпичиков, чтобы:
проектировать свои форматы воронок,
писать собственные команды,
упаковывать сложные многошаговые кейсы в компактные no-code узлы,
импортировать схемы из внешнего редактора,
и автоматически разворачивать их в полнофункциональные low-code скрипты MetaBot.
Важно подчеркнуть:Mapper — полностью JavaScript-плагин.Его можно размещать прямо в бизнесе, на любом боте, в том числе на общем облачном сервере.Вы можете свободно модифицировать его под себя, расширять функционал, менять формат, добавлять новые типы шагов и логику.
Builder — PHP-плагин.Он даёт прямой доступ к базе Metabot и обеспечивает всю работу с «железом» конструктора (создание скриптов, команд, секций, меню и т.д.).Он уже содержит достаточный набор функций, чтобы реализовать практически любой no-code → low-code транслятор.
Напомним:
Если вам требуются новые возможности в Builder или вы хотите собрать свой собственный Builder, доступны несколько вариантов:
Развернуть выделенный сервер,
Приобрести коробочную версию MetaBot,
Или предложить свой функционал — пришлите описание в поддержку, и мы рассмотрим возможность включить расширения в официальный билд.Подробнее о тарифах: https://metabot24.ru/price/tarify-na-platformu/
На этом завершаем блок работы с JSON-схемой, импортом и серверной частью конструктора.
Часть 4. Визуальный редактор воронок на React/Next.js с помощью V0 или Cursor
Теперь, когда мы полностью разобрали JSON-формат, Mapper, Builder и серверную архитектуру импорта, мы можем перейти к самому вдохновляющему этапу — созданию визуального no-code редактора воронок, работающего поверх MetaBot.
Этот редактор позволяет:
рисовать схему так же, как в Miro;
редактировать каждый узел через popup-панель;
добавлять шаги, кнопки и связи;
импортировать/экспортировать JSON;
отправлять схему в Metabot;
подключать промпты и базу знаний КРОМЕ схем (расширение для продвинутых).
4.1. Общая концепция редактора
Наша реализация состоит из:
React/Next.js фронтенда
визуального канваса на React Flow
pop-up редактора узлов
панели инструментов
API-клиента, отправляющего JSON в Metabot
блока для редактирования промптов и базы знаний (не входит в урок, но есть в репозитории)
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-подхода и умеет генерировать чистый, аккуратный, минималистичный интерфейс, который отлично подходит для:
прототипирования: первые версии можно получить за минуты;
быстрой эволюции: внесли правку → обновили компонент;
единообразия UI: всё выглядит цельно, в единой системе;
лучшего старта проекта: он создаёт базовый стиль, который проще поддерживать.
Именно поэтому первые две версии визуального редактора были сделаны полностью в V0.
После того как каркас был собран, мы переехали в Cursor.В Cursor уже:
улучшали дизайн,
корректировали стиль,
дорабатывали интерфейс,
добавляли сложные компоненты,
интегрировали API,
формировали уже продакшен-ориентированную архитектуру.
Финальный вариант, который вы видите в репозитории, — это результат совместной работы V0 и Cursor, где:
V0 дал идеальный стартовый дизайн и структуру,
Cursor дал возможность довести проект до рабочего качества.
Что касается спецификаций:
В репозитории сохранены почти все спецификации V0, поскольку V0 работал строго по промптам и генерировал воспроизводимый код.
Спецификации Cursor восстановить не удалось: там происходило множество микро-итераций, локальных улучшений, полировки компонентов, дополнительных правок, устранения несовпадений между UI и реальным поведением.
Мы намеренно исключили из набора спецификаций пункт «исправления ошибок», потому что такие правки — это компетенция разработчика и не представляют особой обучающей пользы.
Таким образом:
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 — идеальная библиотека для быстрого прототипирования и небольших проектов.Но не идеальна для:
больших схем (лэйаут начинает тормозить),
100+ нод,
сложных графов.
Для полноценных продуктов рекомендуем подобрать более оптимизированную библиотеку.
Перед тем как завершить
Мы не углубляемся в детали разработки приложений на React/Next.js — это отдельная большая тема, и этот урок посвящён именно тому, как построить no-code редактор поверх ядра Metabot, а не фронтенд-разработке как таковой.
Если вы хотите разобраться глубже — есть два лучших пути:
Взять исходники и спецификации редактора, загрузить архив в ChatGPT, и пошагово изучить архитектуру, структуры данных, паттерны и логику.Это максимально практичный способ.
Взять спецификации из папки /specs и попробовать воссоздать редактор самостоятельно — используя V0, Cursor или любой другой генератор UI.Спеки в этом проекте очень детализированные и позволяют полностью повторить дизайнер с нуля.
Эти два подхода дадут вам гораздо больше, чем любой абстрактный учебник по React — потому что вы будете изучать реальный рабочий продукт, который уже интегрирован с Metabot.
🎉 Поздравляем — урок завершён!
Мы подошли к финалу большого и важного пути.Теперь у вас есть все инструменты, чтобы создавать собственные no-code редакторы поверх ядра Metabot, точно так же, как это делают разработчики самой платформы.
Давайте коротко зафиксируем, что именно вы теперь умеете.
✅ Что вы освоили в этом уроке
1. Поняли внутреннюю механику Metabot
Вы разобрались:
как Metabot хранит скрипты, команды и меню;
как работает низкоуровневый PHP-Builder;
почему Builder — это фундамент конструктора любого уровня сложности.
Теперь вы умеете работать с двигателем платформы, а не только с UI.
2. Создали собственный JSON-формат воронок
Вы:
спроектировали свою FSM-модель (автомат состояний),
добавили поля формата и версии,
определили свои типы шагов,
научились описывать сценарии как человекочитаемый DSL.
Этот JSON — основа любого no-code редактора.
3. Полностью разобрали Mapper
Вы увидели, как работает реальный JS-транслятор:
проверка формата и бота,
создание секции,
создание скриптов,
построение переходов,
генерация JS-кода для LLM, поиска по базе знаний и логгера,
обработка кнопок,
сборка карты как topology graph.
То есть вы освоили алгоритм импорта в Metabot от начала до конца.
Mapper — это ваш мост между визуальным редактором и low-code ядром.
4. Научились импортировать воронки через API
Вы теперь умеете:
создавать endpoint в Metabot,
передавать JSON,
полностью пересобирать карту,
реализовывать версионирование и namespaces.
Это полноценный backend-конвейер для вашего конструктора.
5. Построили визуальный редактор на React
Вы узнали:
почему мы начинали с V0 (создатели Next.js → чистый UI),
как V0 даёт быстрый skeleton редактора,
как затем довести продукт до ума в Cursor,
как устроены popup-формы, канва, панель инструментов, связи,
как связать редактор с JSON и Metabot API.
А самое главное — вы получили исходники редактора и спецификации, на которых его можно воспроизвести полностью.
6. Получили материалы для самостоятельного развития
В этом уроке вы получили:
JSON-формат и полное его описание;
Mapper.js — рабочий пример транслятора;
Builder.php — низкоуровневый API;
Спецификации редактора (/specs), созданные в V0;
Исходный код фронтенда (React + Next.js);
Полную схему связей «JSON → Mapper → Builder → Metabot DB».
Это достаточный набор, чтобы:
сделать свой ManyChat,
свой BotHelp,
свой AI-конструктор,
свой CJM-редактор,
свой бизнесовый продукт поверх Metabot.
🔧 Примечание
Если вам понадобится углублять функциональность Builder, создавать собственные PHP-плагины или расширять серверную логику конструктора — это можно делать:
на выделенном сервере,
или в коробочной версии Metabot.
На общем облаке установка PHP-плагинов отключена по безопасности.Если хотите предложить улучшение Builder — напишите нам, мы рассмотрим и, возможно, включим в официальный билд.
🚀 Напоследок
Мы сознательно не углублялись в React/Next.js как учебник — это не цель урока.Но у вас есть всё необходимое, чтобы:
загрузить архив со спецификациями в ChatGPT,
изучить архитектуру,
воспроизвести или улучшить редактор,
построить собственный инструмент с нуля.
Это лучший способ освоить реальный продуктовый подход.
🎉 Ещё раз поздравляем!
Теперь вы умеете создавать no-code редакторы поверх Metabot, понимаете, как писать свои команды, как превращать JSON в скрипты, как строить визуальные пайплайны и как надстраивать интерфейсы над low-code ядром.
Вы научились тому, что большинство разработчиков видит только как «магия» в интерфейсе — а вы теперь умеете реализовывать её сами.
И это уже уровень архитекторов платформ, а не просто разработчиков чат-ботов.
Удачи вам в ваших продуктах — и создавайте смело.Если нужно — мы рядом.