Public API виджета
Коротко
Public API нужен, чтобы установить виджет на сайт и управлять им из JavaScript: открыть, закрыть, встроить в контейнер, включить выбор элемента и передать данные пользователя. Если нужен только обычный popup, достаточно базовой инициализации с channel_id.
Для подсказок по элементам сайта важна настройка features.element_selection: true. Она включает кнопку выбора элемента в виджете, но качество ответа все равно зависит от data-ai-* разметки на сайте и связанной Markdown-документации.
Базовая инициализация
Инициализация выполняется один раз после загрузки скрипта.
SenlerWidget.init({
channel_id: "xxx",
user: { external_id: "user_123" },
});
Для embedded-режима:
SenlerWidget.init({
channel_id: "xxx",
display_mode: "embedded",
container: "#senler-widget",
theme: {
height: 600,
},
user: { external_id: "user_123" },
});
Методы
SenlerWidget.open(config?)- показать popup- или embedded-wrapper; если переданconfig, сначала применить runtime-настройки.SenlerWidget.close()- скрыть wrapper в любом режиме, не удаляя iframe и состояние диалога.SenlerWidget.toggle()- переключить видимость wrapper в любом режиме.SenlerWidget.isOpen()- вернутьtrue, если wrapper сейчас открыт, иfalse, если он скрыт.SenlerWidget.selectDialog(dialogId)- выбрать существующий диалог без отправки сообщения.SenlerWidget.setPageContext(items)- заменить постоянный контекст текущей страницы.SenlerWidget.updateRuntime(config)- обновить часть runtime-настроек без пересоздания виджета и без сохранения этих значений в настройках канала.SenlerWidget.createInlineTextEdit(config)- создать headless-controller для inline-правок в поле сайта.SenlerWidget.destroy()- полностью удалить wrapper, iframe, кнопку и обработчики со страницы. Чтобы вернуть виджет послеdestroy(), нужен новыйSenlerWidget.init(...).
Для обычного скрытия используйте close(), а не destroy(): после close() достаточно вызвать open(), и текущий экземпляр продолжит работу.
Runtime-параметры
open(config?) и updateRuntime(config) принимают одинаковую runtime-конфигурацию:
| Параметр | Что меняет |
|---|---|
lang | Язык ru, en или auto. |
display_mode | Режим popup или embedded. Для перехода в embedded контейнер должен быть передан ещё при init; при переходе в popup wrapper переносится в document.body и остаётся закрытым до open(). |
theme_mode | Светлую, тёмную или автоматическую тему текущего экземпляра. |
border_radius | Скругление текущего экземпляра от 0 до 50. |
shell | Объект с collapse_button и mobile_edge_swipe. |
customActions | Доступные действия сайта и их browser-side handler. |
customActionsLanguage | Язык описаний действий: ru или en. |
autoExecuteCustomActionNames | Имена действий, которые разрешено автоматически выполнить в текущем сценарии. |
dialogId | Существующий диалог, который нужно выбрать. |
startNewDialog | Создать новый пустой диалог. |
focusInput | Поставить фокус в поле ввода. |
pageContextItems | Заменить постоянный контекст страницы; для обычной навигации понятнее использовать setPageContext(items). |
contextItems | Передать одноразовый контекст следующего сообщения. |
message | Подготовить или автоматически отправить сообщение. |
Если передан dialogId, виджет открывает или выбирает этот диалог; startNewDialog: true создаёт пустой новый диалог, а message.startNewDialog: true отправляет сообщение в новый диалог. autoExecuteCustomActionNames сбрасывается при выборе другого диалога или нового сценария без этого поля. Если обновляются customActions, loader отправляет в iframe их описания, а handler остаётся на странице сайта и заменяет прежний обработчик действия с тем же именем.
message принимает { text, requestId?, startNewDialog?, autoSend? }. text должен быть непустой строкой до 10 000 символов. requestId - непустая строка до 200 символов для корреляции runtime-сообщения с событием результата. startNewDialog: true начинает отправку без текущего dialog_id, а autoSend: true отправляет текст автоматически после готовности виджета. message, contextItems и pageContextItems относятся к runtime-обновлению: SenlerWidget.init принимает только contextProvider для начального контекста страницы.
В SPA виджет инициализируется один раз. Не вызывайте destroy() и повторный init() при каждом переходе: это сбрасывает состояние iframe и может выглядеть как перезагрузка чата. Для навигации обновляйте только контекст через SenlerWidget.setPageContext(items), а для точечного сценария вроде "открыть виджет по заказу" используйте SenlerWidget.open({ contextItems, message }).
Крестик в popup и embedded
В popup обычный крестик уже есть в шапке. Пока iframe загружается, loader также показывает крестик на рамке окна. Добавлять собственную кнопку поверх iframe не нужно: нажатие скрывает окно, а плавающая кнопка открывает его снова.
В embedded-режиме обычный крестик закрытия скрыт, потому что виджет занимает контейнер сайта. Чтобы показать крестик внутри шапки самого виджета, передайте shell.collapse_button: true. Нажатие сообщает сайту о намерении свернуть виджет, но само по себе не скрывает embedded-wrapper: сайт должен вызвать SenlerWidget.close() или скрыть более крупную область своего интерфейса.
SenlerWidget.init({
channel_id: "xxx",
display_mode: "embedded",
container: "#senler-widget",
shell: {
collapse_button: true,
},
onCollapse(detail) {
console.log("Пользователь нажал крестик", detail);
SenlerWidget.close();
},
});
После этого SenlerWidget.open() снова покажет тот же embedded-экземпляр и сохранённое состояние диалога. Если сайт скрывает не wrapper, а свою панель или модальное окно целиком, в onCollapse нужно закрыть эту область вместо вызова SenlerWidget.close().
Вместо onCollapse можно один раз подписаться на событие window:
window.addEventListener("senler-widget:collapse-request", (event) => {
if (event.detail.display_mode === "embedded") {
SenlerWidget.close();
}
});
В detail приходят channel_id и display_mode. Выберите один способ обработки: callback или событие, иначе один клик будет обработан дважды. Отдельного onClose для обычного popup-крестика нет.
Важно различать команды:
SenlerWidget.close()- публичный host-метод, который скрывает popup- или embedded-wrapper;SenlerWidget.destroy()- удаляет экземпляр и подходит только для полного завершения интеграции;- внутреннее сообщение
CLOSE_WIDGETприходит от обычного popup-крестика и игнорируется loader-ом в embedded-режиме; - внутреннее сообщение
COLLAPSE_WIDGETприходит от кнопкиshell.collapse_button, после чего loader вызываетonCollapse(detail)и отправляетsenler-widget:collapse-request.
CLOSE_WIDGET и COLLAPSE_WIDGET относятся к протоколу iframe. Сайт не должен отправлять их вручную через postMessage.
Жест от левого края
shell.mobile_edge_swipe: true включает распознавание свайпа от левого края внутри виджета. Loader не закрывает интерфейс автоматически, а отправляет на window событие senler-widget:mobile-edge-swipe.
window.addEventListener("senler-widget:mobile-edge-swipe", (event) => {
if (event.detail.side === "left") {
closeMobilePanel();
}
});
В detail приходят channel_id, текущий display_mode и side: "left". Это отдельный мобильный навигационный сигнал, а не замена onCollapse.
Жизненный цикл runtime-сообщения
Если host-страница передает message.requestId, loader отправляет в window событие senler-widget:runtime-message-result. Это событие нужно кастомным runtime-сценариям, где host-странице важно связать отправку сообщения с результатом без опроса истории диалога. Для inline-правок в текстовых полях используйте createInlineTextEdit(config): он уже отслеживает requestId, регистрирует нужный custom_action и возвращает готовый preview.
const requestId = crypto.randomUUID();
window.addEventListener("senler-widget:runtime-message-result", (event) => {
if (event.detail.request_id !== requestId) return;
if (event.detail.status === "sent") {
// event.detail.dialog_id можно сохранить, чтобы позже открыть этот диалог.
}
if (event.detail.status === "answered") {
// Ответ AI сформирован.
}
if (event.detail.status === "failed") {
// event.detail.error_message содержит причину, если она известна.
}
});
SenlerWidget.open({
contextItems,
message: {
text: "Сделай текст понятнее.",
requestId,
startNewDialog: true,
autoSend: true,
},
});
Статус sent означает, что сообщение принято и известен dialog_id. Статус answered приходит по streaming done или по финальному событию сообщения ассистента. Статус failed приходит при ошибке отправки или streaming. Если нужно открыть этот же диалог в большом виджете, вызовите SenlerWidget.open({ dialogId: event.detail.dialog_id }) или SenlerWidget.selectDialog(event.detail.dialog_id).
Inline-правки в полях сайта
createInlineTextEdit(config) дает сайту headless-controller для сценария "выделить текст → спросить AI → показать before/after". Loader не рисует dropdown и не меняет поле: внешний сайт сам показывает кнопку, меню быстрых действий, состояние загрузки и preview в своем стиле. Controller связывает выбранный текст, сообщение в виджет, кнопку применения senler.previewTextEdit, requestId и готовый preview.
const textarea = document.querySelector("#description");
const inlinePendingStatuses = new Set(["sending", "message_sent", "answered"]);
const inlineFailureStatuses = new Set([
"message_send_failed",
"message_answer_failed",
"preview_failed",
]);
const inlineEdit = SenlerWidget.createInlineTextEdit({
fieldId: "product:123:description",
fieldLabel: "Описание товара",
getValue: () => textarea.value,
getSelection: () => ({
text: textarea.value.slice(textarea.selectionStart, textarea.selectionEnd),
}),
getContextItems: () => [
{
id: "product:123",
kind: "product",
role: "business_context",
display: { label: "Товар #123" },
ref: { entity_type: "product", entity_id: "123", route: "/products/123" },
},
],
onStatusChange: ({ status, error }) => {
renderInlineLoading(inlinePendingStatuses.has(status));
if (inlineFailureStatuses.has(status)) {
renderInlineFallback(error?.message);
}
},
onPreview: (preview) => {
renderTextEditReview({
before: preview.sourceText,
after: preview.replacementText,
changedFrom: preview.selectedText,
changedTo: preview.selectedTextReplacement,
onAccept: () => {
textarea.value = preview.replacementText;
textarea.dispatchEvent(new Event("input", { bubbles: true }));
},
});
},
});
document.querySelector("#ai-improve").addEventListener("click", () => {
inlineEdit.ask("Сделай понятнее");
});
document.querySelector("#ai-chat").addEventListener("click", () => {
inlineEdit.openChat();
});
fieldId должен стабильно идентифицировать поле на странице. getValue() возвращает полный текущий текст поля. getSelection() возвращает выделение как строку или объект { text }; если сайт сам хранит выделение, можно передать его напрямую: inlineEdit.ask({ text: "Исправь ошибки", selectedText }). getContextItems() возвращает бизнес-контекст страницы: товар, заказ, проект или другую сущность. Controller сам добавляет теги для поля, выделенного текста и задачи ассистента.
Метод ask(text | { text, actionLabel?, selectedText? }) создает новый фоновый диалог, отправляет запрос AI и вызывает onPreview(preview), когда правка готова. Метод openChat({ selectedText? }) открывает большой виджет с тем же контекстом: если фоновый диалог уже создан, откроется он; иначе будет создан новый пустой диалог с фокусом в поле ввода. Метод getDialogId() возвращает последний известный диалог, а destroy() снимает обработчики controller-а.
Preview всегда содержит полный текст поля до и после изменения: sourceText, replacementText, selectedText, selectedTextReplacement, summary. Если пришла только замена выделенного фрагмента, loader соберет полный replacementText сам, но только когда исходный фрагмент найден в поле однозначно. Применение изменения остается на стороне сайта, потому что у разных редакторов разные события ввода, undo/redo, autosave и валидация.
Inline-controller использует свой набор статусов, а не общий sent/answered/failed из senler-widget:runtime-message-result:
sending- controller начал отправку запроса;message_sent- сообщение принято, фоновый диалог создан или известен;answered- ответ сформирован, но preview еще может ожидаться;preview_ready- правка готова,onPreviewполучает before/after;message_send_failed- не удалось отправить runtime-сообщение;message_answer_failed- сообщение ушло, но ответ не завершился успешно;preview_failed- ответ был, но preview-правку не удалось получить или разобрать.
Host-страница должна показывать свое состояние загрузки до preview_ready или ошибки, а при message_send_failed, message_answer_failed и preview_failed давать понятный fallback: повторить запрос или открыть чат через openChat(). openChat() использует уже известный фоновый диалог, если он был создан, поэтому пользователь видит тот же контекст, а не отдельную пустую переписку.
Эти статусы нужны разработчику интеграции. В обычной поддержке объясняйте состояние человеческими словами: AI готовит вариант, правку не удалось получить, можно повторить запрос или открыть чат.
Callback-параметры
В SenlerWidget.init можно передать callback-параметры:
contextProvider()- синхронно возвращает массив постоянного контекста текущей страницы при инициализации. Promise не поддерживается; при SPA-переходах обновляйте контекст черезSenlerWidget.setPageContext(items).onCollapse(detail)- вызывается при нажатии кнопки сворачивания, включённой черезshell.collapse_button. Вdetailприходятchannel_idиdisplay_mode. Callback может вызватьSenlerWidget.close()или закрыть внешнюю панель сайта.
Custom action кнопки обрабатываются не отдельным top-level callback, а через handler внутри customActions.
События страницы
Событие window | Когда приходит |
|---|---|
senler-widget:collapse-request | Пользователь нажал кнопку shell.collapse_button; содержит channel_id и display_mode. |
senler-widget:mobile-edge-swipe | Пользователь сделал разрешённый свайп от левого края; дополнительно содержит side: "left". |
senler-widget:runtime-message-result | Runtime-сообщение с requestId принято, завершено или завершилось ошибкой. |
senler-widget:stage | Изменился диагностический этап загрузки, соединения, отправки, истории или загрузки файла. Используйте для наблюдаемости, а не для бизнес-логики. |
Custom actions для кнопок в чате
Чтобы в ответе появилась кнопка, которую обработает сайт, host-страница должна явно объявить доступные customActions. В публичном loader API это объект, где ключ - имя действия, а значение содержит title, description, опциональную payloadSchema и обязательный handler. Виджет передает в AI только описание действия, а сам handler остается в браузере и вызывается при клике по кнопке custom_action.
Если title и description написаны только на одном языке, укажите рядом customActionsLanguage: "ru" или customActionsLanguage: "en". Это поле не переводит тексты, а задает язык описаний custom actions; если его не передать, используется двуязычный поиск по ru+en.
SenlerWidget.init({
channel_id: "xxx",
customActionsLanguage: "ru",
customActions: {
"site.openOrder": {
title: "Открыть заказ",
description: "Открывает карточку заказа по order_id.",
payloadSchema: {
type: "object",
properties: {
order_id: { type: "string" },
},
required: ["order_id"],
additionalProperties: false,
},
handler(detail) {
openOrder(detail.payload?.order_id);
},
},
},
});
Имя действия берется из ключа объекта customActions. Оно должно начинаться с латинской буквы и может содержать латинские буквы, цифры, _, ., : и -. В одном контексте можно объявить до 20 действий. title и description помогают понять, когда использовать действие. title ограничен 80 символами, description - 240 символами. payloadSchema описывает JSON object payload: корневая схема должна быть object, схема не должна быть глубже 8 уровней и сериализуется не больше чем в 1200 символов. Общий descriptor payload всех действий ограничен 6000 символами.
Когда в диалоге виджета включены кнопки, доступны только действия, которые host-страница явно передала через customActions. Если страница не передала действие, такую кнопку нельзя обещать или выполнять.
custom_action.name нельзя придумывать: используются только имена, явно переданные host-страницей через customActions. open_url в виджете обрабатывается host-страницей: target: "self" меняет текущую страницу, а без self ссылка открывается в новой вкладке. Для переходов внутри сайта лучше объявлять custom_action, а не открывать прямой URL через open_url.
Контекст сообщения
Host-страница может передать два вида контекста. Постоянный контекст страницы задается через contextProvider при SenlerWidget.init или через SenlerWidget.setPageContext(items) при переходах без перезагрузки. Одноразовый контекст следующего сообщения передается как contextItems через SenlerWidget.open({ contextItems, message }) или SenlerWidget.updateRuntime({ contextItems, message }). Это нужно, когда контекст известен без ручного выбора элемента: открытая карточка заказа, проект, рекомендация AI-сводки, цель действия или другая бизнес-сущность.
contextProvider должен вернуть уже готовый массив синхронно. Если название страницы или сущность загружаются позже, сначала можно вернуть базовый page item, а после загрузки данных вызвать SenlerWidget.setPageContext(items) с актуальной подписью, route и avatar_url, если он есть.
function buildPageContext() {
return [
{
id: `page:${window.location.pathname}`,
kind: "page",
role: "technical",
display: {
label: document.title || "Текущая страница",
subtitle: window.location.pathname,
},
ref: {
url: window.location.href,
route: window.location.pathname,
},
},
];
}
SenlerWidget.init({
channel_id: "xxx",
contextProvider: buildPageContext,
});
SenlerWidget.setPageContext(buildPageContext());
SenlerWidget.open({
contextItems: [
{
id: "order:123",
kind: "order",
role: "business_context",
display: { label: "Заказ #123", subtitle: "Открытая карточка" },
ref: { entity_type: "order", entity_id: "123", route: "/orders/123" },
},
],
message: {
text: "Опиши, что можно сделать с этим заказом.",
startNewDialog: true,
autoSend: true,
},
});
Каждый item должен иметь id, kind, role, display.label и может иметь ref, snapshot, payload. Роль может быть technical, user_selected, business_context или action_target. В одном сообщении можно передать до 12 items. Лимиты: id до 120 символов, kind до 80, display.label до 80, display.subtitle до 140, display.icon до 40, display.avatar_url до 500. ref, snapshot и payload должны быть объектами, если переданы.
Используйте стабильные id: например page:/orders/123, project:019..., order:123. Если один и тот же объект приходит из page context и draft contextItems, совпадающий id оставит один тег вместо дублей. Для разных сущностей используйте разные kind и разные id, даже если подписи похожи.
Выбранный элемент тоже показывается как тег контекста с типом selected_element. Поэтому отдельное поле выбранного элемента не нужно: нужный контекст уже находится в тегах рядом с сообщением.
В поле ввода постоянный page context, выбранный элемент и draft contextItems показываются вместе как теги и дедуплицируются по id. Пользователь может удалить любой тег перед отправкой; после отправки draft-контекст и выбранный элемент очищаются, а постоянный page context остается для следующих сообщений. В истории чата теги уже отправленного сообщения нельзя удалить: их можно только раскрыть, открыть ref.route/ref.url или посмотреть детали kind, role, ref, snapshot и payload.
После отправки итоговые теги сохраняются в истории диалога: страница, проект, выбранный элемент, цель действия или другая сущность. Если у item есть display.avatar_url, виджет показывает аватар в теге; если аватара нет, используется display.icon или стандартная иконка по kind.
Встроенные сценарии используют тот же формат. Виджет может добавить страницу, ассистент кабинета добавляет проект, а кнопка применения рекомендации AI-сводки добавляет целевого агента, выбранную рекомендацию и сценарий применения. Проектный item добавляется только когда текущий route проекта совпал с загруженным проектом; в display и snapshot передаются имя проекта, public_id и, если есть, avatar_url. В диалогах эти items отображаются как теги рядом с сообщением.
Служебные элементы виджета
Сам виджет может иметь служебные маркеры для тестов, аналитики и стабильной работы UI, но они не являются целями AI-подсказок. Подсветка указывает на элементы страницы сайта, кабинета или iframe диалогов; UI самого виджета пользователь читает как обычный чат.
Если нужно объяснить кнопку или состояние внутри виджета, пишите это словами в документации. Для подсветки на странице используйте разметку внешнего сайта или кабинета: data-ai-context-id, data-ai-label и data-ai-kb-query.
Встроенная история чатов
В шапке виджета может быть кнопка истории чатов. Она открывает компактное меню с поиском по истории чатов, кнопкой создания нового диалога и списком элементов истории чатов.
Поиск в истории включается от двух символов. Без поиска история загружается порциями и догружает следующие элементы при прокрутке. Если /init вернул текущий диалог, виджет старается открыть его при первом входе; иначе открывает первый доступный диалог.
Top-level параметры
Loader принимает только поддерживаемые ключи:
| Ключ | Назначение |
|---|---|
channel_id | Обязательный ID канала типа «Виджет». |
user | Данные посетителя и, при включённой привязке к авторизации, external_id с серверной подписью user_hash. |
theme | Оформление, размеры, тексты и плавающая кнопка. |
features | Доступные функции чата. |
lang | ru, en или auto. |
config_source | local для настроек в коде или remote для сохранённых настроек канала; по умолчанию remote. |
display_mode | popup или embedded; по умолчанию popup. |
button_only | Показать только неинтерактивный вид плавающей кнопки без iframe чата. Не совместим с embedded; методы чата в этом режиме не работают. |
container | CSS-селектор или DOM Element для embedded-wrapper. |
shell | Объект с collapse_button и mobile_edge_swipe. |
onCollapse | Callback запроса на сворачивание. |
contextProvider | Синхронный поставщик постоянного контекста страницы. |
customActions | Объект действий сайта. |
customActionsLanguage | Язык описаний действий: ru или en. |
debug | true включает дополнительные сообщения loader-а в консоли. |
Другие top-level ключи считаются ошибкой конфигурации.
Если виджет не запускается, сначала проверьте channel_id, доступность loader-скрипта, корректность container для embedded-режима и отсутствие неподдерживаемых top-level ключей.
Данные пользователя
Объект user поддерживает external_id, user_hash, email, phone, first_name, last_name, avatar_url и объект data. user_hash формируется только на сервере сайта с секретом канала; секрет нельзя помещать в браузерный код.
Shell
collapse_button: trueпоказывает кнопку сворачивания внутри шапки виджета;mobile_edge_swipe: trueвключает мобильный жест от левого края.
Popup и embedded
display_mode по умолчанию равен popup. Для embedded нужно передать container с CSS-селектором или использовать auto-init через элемент с data-senler-config.
Комбинация display_mode: "embedded" и button_only: true не поддерживается.
У popup есть плавающая кнопка и встроенное закрытие окна. Embedded-виджет занимает контейнер сайта и не использует плавающую кнопку. Публичные методы open(), close() и toggle() управляют wrapper-ом в обоих режимах. Крестик shell.collapse_button только запрашивает сворачивание: обработайте onCollapse или senler-widget:collapse-request.
Theme
В theme поддерживаются:
chat_title;default_dialog_title;theme_mode;position;width;height;border_radius;shadow_enabled;welcome_message;empty_state_message;button.
Локализованные тексты принимают объект { ru?: string, en?: string }, а не обычную строку.
theme.position управляет положением popup-окна и принимает только bottom-right, bottom-left, top-right, top-left.
theme.button.position управляет toggle-кнопкой и дополнительно поддерживает hidden. Чтобы скрыть кнопку, используйте theme.button.position = "hidden". Значение theme.position = "hidden" не поддерживается.
theme.button поддерживает position, а также цветовые объекты light и dark. В каждом цветовом объекте допустимы background и icon в формате #RRGGBB.
Скругление и источник настроек
theme: { border_radius: 18 }задаёт начальное скругление от0до50приconfig_source: "local";SenlerWidget.updateRuntime({ border_radius: 18 })сразу меняет wrapper и содержимое iframe только у текущего экземпляра и не сохраняет значение в канал;- при
config_source: "remote"постоянным источником темы служат сохранённые настройки канала. Не используйте несохранённыйtheme.border_radiusвinitкак замену серверной настройке: для временного изменения вызовитеupdateRuntime, для постоянного измените радиус в кабинете и сохраните; - предпросмотр в кабинете сразу применяет текущий черновик и выбранный режим темы. Это ещё не публикация: после сохранения remote-код подхватит значение с сервера, а для local-кода нужно скопировать обновлённый код на сайт.
Features
В features поддерживаются:
file_upload;voice_messages;emoji;split_view;element_selection.
element_selection по умолчанию выключен. Если включить features.element_selection: true, посетитель сайта сможет нажать в виджете "Выбрать элемент", кликнуть по элементу страницы и отправить вопрос вместе с контекстом этого элемента.
Для качественных ответов владельцу сайта стоит добавить data-ai-* разметку на важные кнопки, поля, карточки товаров, тарифы и шаги оформления. Подробная публичная инструкция: site-actions/website-data-ai-markup.md.
Где это в интерфейсе
Основные элементы раздела:
- список каналов
- пункт «Каналы» в боковом меню
- кнопка добавления канала
- кнопка выбора платформы Виджет
- страница настроек канала