10. Web UI
Web-интерфейсы, пользовательские компоненты и визуальная интеграция с платформой.
- Web-формы в чат-боте и Web Apps
- Введение. Виды форм. Принцип работы.
- Создание HTML формы.
- Универсальная форма в виде ссылки (для любого мессенджера)
- Web App форма c использованием inline-кнопки (только для Telegram)
- Web App форма c использованием keyboard-кнопки (только для Telegram)
- Методология
- Методика разработки дизайна
- Стандарты Web UI
- Типовые ошибки в дизайне интерфейсов при переходе от “рисования” к реальной UI/UX-разработке
- Тренды в UI/UX: перенос архитектуры на этап дизайна
- Как понимать контекст проекта
- Уроки
Web-формы в чат-боте и Web Apps
Введение. Виды форм. Принцип работы.
Теоретическая часть для разработчика сайта (разработчика HTML-формы) и для разработчика бота.
Веб-формы расширяют канал коммуникаций с ботом и позволяют в текстовом боте использовать все возможности HTML и JavaScript, таким образом бот, по функционалу становится полноценной заменой любому веб-сайту. Бот теперь не ограничивает пользователя в коммуникациях только обменом текстом, аудио или файлами.
Актуальный исходный код веб-формы реализующий все три вида форм смотрите по ссылке: go-to-the-mars.html
Пример работы веб-формы приведенной выше смотрите в Telegram боте https://t.me/metabot_test_form_bot
Разработка веб-формы, расширяющей диалоговую коммуникацию происходит в несколько шагов:
- Перед началом разработки необходимо выбрать один из вариантов реализации формы и согласовать с разработчиком веб-формы и разработчиком чат-бота (если форма и чат-бот разрабатываются параллельно). Также необходимо согласовать формат обмена данными.
- После того как вы определились с видом формы, необходимо разработать 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 бота и закрытия страницы
<script>
function closeForm() {
location.href="tg://resolve?domain=metabot_test_form_bot";
window.close();
}
</script>
После заполнения формы, данные введенные пользователем необходимо отправить на бэк вашего сайта, а затем в Metabot API используя токен бота (чтобы токен бота не фигурировал в коде HTML-формы). Далее страницу браузера необходимо автоматически закрыть, с помощью JS-кода встраиваемого в HTML-форму. Рекомендуется отправлять данные на бэк с помощью REST API, для того чтобы после отправки не выполнять дополнительный рендеринг страницы (чтобы потом ее просто закрыть). Т.е. отправляем данные на бэк, убеждаемся что ответ от API - 200, т.е. все ОК и сразу закрываем форму, пользователь возвращается в бота. При этом желательно учесть в коде HTML-формы варианты, если что-то пошло не так, или пользователь остался на форме, а хэш код лида для формы истек (если вы генерируете токен со сроком действия), в этих случаях можно вывести сообщение о том что форма устарела, что пользователю необходимо вернуться в бота и запросить форму повторно, также дополнительно можно уведомить Metabot по API, чтобы бот автоматически выслал ссылку на новую форму.
Есть нюанс: перед закрытием формы можно вызвать открытие ссылки для перехода в мессенджер (на случай если после закрытия страницы не будет открывается мессенджер). Но, для PC, например, открытие ссылки с Telegram ботом не срабатывает автоматически и у пользователя остается открытой другая вкладка в браузере (если у него включено открытие ссылок мессенджера во внешнем браузере и в браузере открыто несколько вкладок).
Cсылка может быть отправлена в бота в виде простого текста или inline-кнопки Telegram, cодержащей ссылку на форму. Но в любом случае, для рассматриваемого варианта формы, нажатие на ссылку или кнопку приведет к открытию странице в браузере и после заполнения формы страницу необходимо автоматически закрывать. Здесь речь идет именно об обычной inline-кнопке c ссылкой для Telegram, а не о Telegram WebApp-форме, на основе inline-кнопки которая открывает форму в Web App (во всплывающем окне, на котором расположен WebView). Описание вариантов форм с WebApp смотрите ниже.
Информация для разработчика бота:
Отличие между Inline Web App Button и обычной inline кнопки в виде ссылки заключается в том, что Inline Web App Button формируется передачей параметров web_app и url а для обычная inline-кнопка в виде ссылки просто передачей параметра url. Детали можно найти в описании API Telegram.
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
Исходный код примера веб-формы
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Заявка для полета на Марс</title>
<!-- Подключаем Telegram Web App -->
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<!-- Подключаем JQuery и JQ suggestions для dadata -->
<!-- PS: JQuery подключать не обязательно, у вас могут быть свои библиотеки для работы с формой и отпарвки ajax запросов -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://app.metabot24.com/lib/jquery.suggestions/js/jquery.suggestions.min.js"></script>
<link href="https://app.metabot24.com/lib/jquery.suggestions/css/suggestions.min.css" rel="stylesheet"/>
<script language="JavaScript">
let botId = ID_ВАШЕГО_БОТА
let botToken = 'ТОКЕН_ВАШЕГО_БОТА'
let dadataToken = 'ТОКЕН_DADATA'
// URL для POST запроса на который будет отправлять данные с формы
// В качестве примера мы отправляем напрямую в бота
// Но в вашем production боте вы должны отправлять на данные на бэк
// А с бэка уже пересылать в бота, чтобы, например "не светить" токен бота в коде html-формы
let sendUrl = '/api/v1/bots/' + botId + '/call/submit-form'
let sendBody = {}
let sendHeaders = {}
let mode = null
let tgWebApp = null
$(function () {
/* Определяем режим работы формы
Режим передается в query url (GET параметр mode=)
Режимы текущей html:
- '' - пустая стркоа или null, когда в чат-боте ссылка на форму приходит в виде ссылки
это универсальный вариант который будет работать в любом мессенджере
- 'tg_inline' - для Telegram чат-бота, когда ссылка на форму приходит в виде inline-кнопки
- 'tg_keyboard' - для Telegram чат-бота, когда ссылка на форму приходит в виде keyboard-кнопки
Для вашего бота может быть достаточно одного из режимов
В качестве примера просто приведены 3 варианта, чтобы вы могли выбрать подходящий и понять разницу
*/
if (typeof (urlParams["mode"]) === 'string') {
mode = urlParams["mode"]
}
// Переменная для доступа к Telegram Web App, чтобы не писать везде window.Telegram.WebApp
if (mode === 'tg_inline' || mode === 'tg_keyboard') {
if (window.Telegram && window.Telegram.WebApp) {
tgWebApp = window.Telegram.WebApp
}
}
// Инициалиця Dadata для автокомплита поля с адресом
$(".dadata-suggestion").suggestions({
token: dadataToken,
type: "ADDRESS"
})
// Получаем хэш-код лида из request url (GET параметр q=)
$('#q').val(urlParams["q"])
// Событие нажатия на кнопку "Отправить данные"
$(document).on('click', '#submit-mars-form', (e) => {
let form = $('#form-mars')
if (!form[0].checkValidity()) {
form[0].reportValidity()
return
}
let formData = getFormData(form)
formData['tg_query_id'] = ''
formData['mode'] = mode
if (mode === 'tg_inline') {
if (tgWebApp && tgWebApp.initDataUnsafe && tgWebApp.initDataUnsafe.query_id) {
formData['tg_query_id'] = tgWebApp.initDataUnsafe.query_id
}
}
if (tgWebApp) {
// https://core.telegram.org/bots/webapps#initializing-web-apps
tgWebApp.expand() // необязательно
tgWebApp.ready() // необязательно
}
// Для универсального режима или режима tg_inline
if (mode !== 'tg_keyboard') {
// Отправляем данные внутри script_request_params и указываем токен и др заголовки для Metabot API
// Но в вашем production, здесь вы должны просто отправить данные на ваш бэк, а с бэка уже в Metabot API
// отправлять необзяталеьно через JSON API, можно делать это просто через ACTION формы SUBMIT кнопку на форме
//
// Если вы реализуете универсальный режим и выполняете отправку через action forma(сабмит без REST API),
// то ваша форма разрывается на два шага
// - заполнение формы клиентом
// - получаем данные на бэке
// и после приема данных рендерим опять HTML в коде которого размещаем JavaScript который закроет страницу (вызовет метод closeForm())
// Поэтому проще данные на бэк отправить по REST API и сразу же закрыть форму
// Именно такой вариант и реализован в данной HTML форме
sendBody = {"script_request_params": formData} //form.serializeArray()
sendHeaders = {
"Authorization": "Bearer " + botToken,
'Content-Type': 'application/json',
'Accept': 'application/json',
}
// Отправка запроса с помощью библиотеки JQuery
sendJQueryRequest('POST', sendUrl, sendBody, sendHeaders, function (response, isError, jqXHR, textStatus, errorThrown) {
if (!isError) {
// Запрос завершён. Здесь можно обрабатывать результат.
//console.log(response)
closeForm()
} else {
// Произошла ошибка
alert("Ошибка обработки API запроса")
console.log(jqXHR)
}
})
// Отправка запроса без дополнительных библиотек (с помощью XHR)
// Могут быть проблемы с отправкой, возможно нужна корректировка кода под ваш бэкенд вашего сайта
/*sendXhrRequest('POST', sendUrl, sendBody, sendHeaders, function(xhr) {
//https://developer.mozilla.org/ru/docs/Web/API/XMLHttpRequest/send#%D0%BF%D1%80%D0%B8%D0%BC%D0%B5%D1%80_get
if(xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
// Запрос завершён. Здесь можно обрабатывать результат.
console.log(xhr)
closeForm()
} else {
// Произошла ошибка
alert("Ошибка обработки API запроса")
console.log(xhr)
}
})*/
} else {
// Если это режим tg_keyboard
// Внимание! Лимит строки для sendData - 4096 байт !
// Поэтому такой режим менее универсален, хотя на первый взгляд проще и не требует бэкенда
// Но в будущем могут возникнуть проблемы, если форма будет усложнена
// Если необходимо вывести сообщение (например для отладки),
// тк обычные alert и console.log для telegram Web App не сработают
//tgWebApp.showAlert('Все ок!')
tgWebApp.sendData(JSON.stringify(formData));
// Если не выполняем sendData, то закрываем форму сами
//closeForm()
}
})
})
/**
* Метод для закрытия формы
*/
function closeForm() {
if (mode === null || mode === '') {
location.href = "tg://resolve?domain=metabot_test_form_bot"
window.close()
} else {
tgWebApp.close()
}
}
// https://stackoverflow.com/questions/11338774/serialize-form-data-to-json
function getFormData($form) {
var unindexed_array = $form.serializeArray();
var indexed_array = {};
$.map(unindexed_array, function (n, i) {
indexed_array[n['name']] = n['value'];
});
return indexed_array;
}
//https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
var urlParams = (function (a) {
if (a == "") return {}
var b = {}
for (var i = 0; i < a.length; ++i) {
var p = a[i].split('=', 2)
if (p.length == 1)
b[p[0]] = ""
else
b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "))
}
return b
})(window.location.search.substr(1).split('&'))
// Отправка POST запроса по API с помощью JQuery
// https://reqbin.com/code/javascript/wzp2hxwh/javascript-post-request-example
function sendJQueryRequest(method, url, data, jsonHeaders, callback) {
$.ajax({
url: url,
type: method,
contentType: 'application/json; charset=utf-8',
dataType: 'json',
async: false,
headers: jsonHeaders,
data: JSON.stringify(data),
success: function (response) {
callback(response, false)
},
error: function (jqXHR, textStatus, errorThrown) {
//alert("Произошла ошибка при обработке API запроса")
callback(null, true, jqXHR, textStatus, errorThrown)
}
})
}
// Отправка POST запроса по API с помощью XHR
// https://reqbin.com/code/javascript/wzp2hxwh/javascript-post-request-example
function sendXhrRequest(method, url, data, jsonHeaders, callback) {
let xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.setRequestHeader("Accept", "application/json")
xhr.setRequestHeader("Content-Type", "application/json")
if (typeof (jsonHeaders) != "undefined") {
for (let key in jsonHeaders) {
xhr.setRequestHeader(key, jsonHeaders[key])
}
}
xhr.onload = () => console.log(xhr.responseText)
xhr.onload = function () {
// Запрос завершён. Здесь можно обрабатывать результат.
callback(xhr)
};
//Вызывает функцию при смене состояния.
//xhr.onreadystatechange = function() {
// callback(xhr)
//}
xhr.send(JSON.stringify(data))
}
</script>
<!-- Стили формы, в вашей релизации будет свой блок кода, подлючаемый в виде css файла -->
<style>
body {
background-color: #070619;
/*width: 100%;
height: 100%;*/
font-size: 18px;
}
.main-container {
width: 95%;
height: 95%;
}
form {
color: #fff;
border: 1px solid silver;
border-radius: 10px;
padding: 10px;
margin: 10px auto 0 auto;
width: 95%;
max-width: 500px;
height: 100%;
}
form .form-title {
font-size: 20px;
text-align: center;
font-weight: bold;
}
form .form-group {
margin: 10px;
}
form .form-group input {
font-size: 18px;
}
form .form-group input[type=checkbox] {
width: 20px;
height: 20px;
}
form .form-group input.dadata-suggestion {
max-width: 75%;
}
form .form-group select {
font-size: 18px;
}
.suggestions-suggestions {
color: #000
}
form .form-button {
text-align: center;
margin-bottom: 10px;
}
form .send-button {
font-size: 18px;
border: 1px solid #fff;
border-radius: 3px;
background-color: #fff;
padding: 5px;
font-weight: bold;
text-decoration: none;
}
form .form-button button {
font-size: 18px;
font-weight: bold;
}
.mars {
position: absolute;
left: calc(55vw);
/*right: 0;*/
top: 0;
z-index: -10;
opacity: 0.8;
}
</style>
</head>
<body>
<!-- HTML КОД формы -->
<div class="main-container">
<!--Для отправки с помощью submit укажите аттрибут action данной формы-->
<form id="form-mars" autocomplete="off">
<div class="form-title">Заявка для полета на Марс</div>
<input type="hidden" id="q" name="q" value="">
<div class="form-group">
<label for="name">
Ваше имя:
</label>
<div>
<input type="text" name="name" id="name" placeholder="Юрий Гагарин" required autofocus>
</div>
</div>
<div class="form-group">
<label for="email">
Почта:
</label>
<div>
<input type="email" name="email" id="email" placeholder="yuri@gagarin.ru">
</div>
</div>
<div class="form-group">
<label for="age">
Возраст:
</label>
<div>
<input type="number" name="age" id="age" min=12 max=777 step=1>
</div>
</div>
<div class="form-group">
<label for="specialization">
Профессия:
</label>
<div>
<select name="specialization" id="specialization" required>
<option value="engineer" selected>Инженер</option>
<option value="scientist">Учёный</option>
<option value="psychologist">Психолог</option>
<option value="other">Другая</option>
</select>
</div>
</div>
<div class="form-group">
<label for="address">
Ваш адрес проживания:
</label>
<input class="dadata-suggestion" type="text" name="address" id="address" placeholder="" required>
</div>
<div class="form-group">
<label for="is_qualified">
Прошел курсы в Центре<br>подготовки космонавтов
<input type="checkbox" name="is_qualified" id="is_qualified" value="1">
</label>
</div>
<div class="form-group">
<label for="has_experience">
Я уже летал в космос (имею опыт)
<input type="checkbox" name="has_experience" id="has_experience" value="1">
</label>
</div>
<!-- Если нужна отправка фото -->
<!--<div class="form-group">
<label>
Фото:
<input type="file" accept="image/jpeg" name="photo" required>
</label>
</div>-->
<div class="form-button">
<!--<button type="submit">Отправить заявку</button>--> <!-- Для отправки с помощью submit формы -->
<a class="send-button" id="submit-mars-form" href="#">Отправить заявку</a>
</div>
</form>
<img class="mars" src="./mars1.gif"/>
</div>
</body>
</html>
При использовании данного примера замените в следующих строках данные на актуальные для вашего бота:
- 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) — это инструмент визуализации стилистического направления проекта. Он представляет собой подборку изображений, шрифтов, цветовых сочетаний и композиционных решений, отражающих предполагаемое настроение и атмосферу будущего продукта.
Создание мудборда необходимо для:
-
согласования визуального направления с заказчиком до начала детальной проработки макетов;
-
минимизации количества правок на последующих этапах;
-
формирования общего понимания эстетики проекта.
Рекомендуемые материалы:
2.2. Практические рекомендации
-
Подготовьте несколько вариантов мудбордов, отражающих разные стилистические решения.
-
Обсудите и согласуйте выбранное направление напрямую с заказчиком.
-
После утверждения мудборда зафиксируйте его как основу визуальной концепции проекта.
3. Типографика
Типографика определяет структуру и читаемость интерфейса. Правильный выбор шрифтов и их иерархия обеспечивают удобство восприятия информации пользователями.
Рекомендуемые материалы:
Рекомендации:
-
Используйте не более двух-трёх шрифтовых пар в одном проекте.
-
Формируйте чёткую иерархию заголовков, подзаголовков и основного текста.
-
Проверяйте читаемость на различных разрешениях экранов.
4. Сетки и структурирование контента
Сетка является основой композиционной организации макета. Она обеспечивает визуальный порядок и согласованность элементов.
Рекомендуемые материалы:
Рекомендации:
-
Используйте модульные и колоночные сетки.
-
Соблюдайте пропорциональность и единообразие отступов.
-
Структура должна быть адаптивной для различных устройств.
5. Цветовое решение
Цветовая палитра формирует эмоциональный отклик и визуальный баланс интерфейса. Цвет должен подбираться осознанно, с учётом целевой аудитории и контекста бренда.
Рекомендуемые материалы:
Рекомендации:
-
Используйте ограниченную палитру: 1–2 основных цвета и 1 акцентный.
-
Проверяйте контрастность и читаемость текста.
-
При наличии брендбука следуйте корпоративным цветовым стандартам.
6. Композиция
Композиция определяет визуальный ритм, баланс и иерархию элементов интерфейса.
Рекомендуемые материалы:
Рекомендации:
-
Используйте принципы визуального равновесия и акцентирования.
-
Избегайте перегруженности экрана второстепенными элементами.
-
Соблюдайте целостность восприятия интерфейса.
7. UI Kit (дизайн-система проекта)
UI Kit — это систематизированный набор компонентов интерфейса (кнопки, поля ввода, карточки, иконки, стили текста и пр.), который обеспечивает единообразие визуальных решений.
Рекомендуемые материалы:
Рекомендации:
-
Создавайте компоненты со всеми необходимыми состояниями.
-
Настраивайте связь UI Kit с автолейаутами.
-
Используйте UI Kit как единый источник истины по стилю проекта.
8. Автолейауты
Автолейаут (Auto Layout) — инструмент Figma, позволяющий создавать гибкие и адаптивные интерфейсы.
Рекомендуемые материалы:
Рекомендации:
-
Используйте автоотступы и выравнивание для стабильности макета.
-
Комбинируйте автолейауты с компонентами и вариантами.
-
Применяйте для подготовки кликабельных прототипов.
9. Кликабельные прототипы
Кликабельный прототип имитирует поведение готового продукта и позволяет наглядно продемонстрировать структуру и логику интерфейса заказчику.
Рекомендуемый ресурс:
Рекомендации:
-
Обязательно предоставляйте заказчику кликабельный прототип перед финальным утверждением.
-
Прототип помогает выявить 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 Состояния
Каждый компонент обязан иметь необходимые стейты:
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. Формирует уникальные коды скриптов
Это ключевой момент.
Мы склеиваем:
<format>:<version>:<сjm_funnel_code>:<step_code>
Например:
cjm:1.0:quiz_lead_magnet_tags:entry_point_main
cjm:1.0:quiz_lead_magnet_tags:hello_intro
cjm:1.0:quiz_lead_magnet_tags:send_pdf
cjm:1.0:quiz_lead_magnet_tags:log_action_2ec8cbbe
Так обеспечивается:
-
уникальность внутри всего бота
-
читаемость
-
возможность переимпорта
-
возможность отладки
✔ 5. Внутрь каждого скрипта складывается набор команд (phrases)
Например, шаг «Send Text» превратится в:
-
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)
- 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 ядром.
Вы научились тому, что большинство разработчиков видит только как «магия» в интерфейсе — а вы теперь умеете реализовывать её сами.
И это уже уровень архитекторов платформ, а не просто разработчиков чат-ботов.
Удачи вам в ваших продуктах — и создавайте смело.
Если нужно — мы рядом.