ruLog in to Senler

Widget Public API

In short

The Public API is used to install the widget on a site and control it from JavaScript: open, close, embed into a container, enable element selection, and pass user data. If you only need a regular popup, basic initialization with channel_id is enough.

For site element hints, the important setting is features.element_selection: true. It enables the element selection button in the widget, but answer quality still depends on data-ai-* markup on the site and linked Markdown documentation.

Basic initialization

Initialization runs once after the loader script is ready.

SenlerWidget.init({
  channel_id: "xxx",
  user: { external_id: "user_123" },
});

For embedded mode:

SenlerWidget.init({
  channel_id: "xxx",
  display_mode: "embedded",
  container: "#senler-widget",
  theme: {
    height: 600,
  },
  user: { external_id: "user_123" },
});

Methods

  • SenlerWidget.open(config?) - show the popup or embedded wrapper; if config is passed, apply runtime settings first.
  • SenlerWidget.close() - hide the wrapper in either mode without removing the iframe or dialog state.
  • SenlerWidget.toggle() - toggle wrapper visibility in either mode.
  • SenlerWidget.isOpen() - return true when the wrapper is open and false when it is hidden.
  • SenlerWidget.selectDialog(dialogId) - select an existing dialog without sending a message.
  • SenlerWidget.setPageContext(items) - replace persistent context for the current page.
  • SenlerWidget.updateRuntime(config) - update part of the runtime configuration without recreating the widget or saving those values to channel settings.
  • SenlerWidget.createInlineTextEdit(config) - create a headless controller for inline edits in a site field.
  • SenlerWidget.destroy() - completely remove the wrapper, iframe, button, and handlers from the page. Returning the widget after destroy() requires another SenlerWidget.init(...).

Use close() rather than destroy() for ordinary hiding: after close(), call open() and the current instance continues working.

Runtime parameters

open(config?) and updateRuntime(config) accept the same runtime configuration:

ParameterWhat it changes
langLanguage: ru, en, or auto.
display_modeMode: popup or embedded. Switching to embedded requires a container passed during init; switching to popup moves the wrapper to document.body and leaves it closed until open().
theme_modeLight, dark, or automatic theme for the current instance.
border_radiusRounding from 0 to 50 for the current instance.
shellAn object with collapse_button and mobile_edge_swipe.
customActionsAvailable site actions and their browser-side handler.
customActionsLanguageAction description language: ru or en.
autoExecuteCustomActionNamesAction names allowed to execute automatically in the current scenario.
dialogIdExisting dialog to select.
startNewDialogCreate an empty new dialog.
focusInputFocus the message input.
pageContextItemsReplace persistent page context; for normal navigation, setPageContext(items) is clearer.
contextItemsPass one-time context for the next message.
messagePrepare or automatically send a message.

If dialogId is passed, the widget opens or selects that dialog; startNewDialog: true creates an empty new dialog, while message.startNewDialog: true sends the message into a new dialog. autoExecuteCustomActionNames resets when another dialog or a new scenario is selected without this field. If customActions are updated, the loader sends their descriptors to the iframe, while each handler stays on the host page and replaces the previous handler for the same action name.

message accepts { text, requestId?, startNewDialog?, autoSend? }. text must be a non-empty string up to 10,000 characters. requestId is a non-empty string up to 200 characters used to correlate a runtime message with its result event. startNewDialog: true starts sending without the current dialog_id, and autoSend: true sends the text automatically after the widget is ready. message, contextItems, and pageContextItems belong to runtime updates: SenlerWidget.init only accepts contextProvider for initial page context.

In an SPA, initialize the widget once. Do not call destroy() and then init() again on every route change: that resets iframe state and can look like the chat is reloading. For navigation, update only the context through SenlerWidget.setPageContext(items); for a focused scenario like "open the widget for this order", use SenlerWidget.open({ contextItems, message }).

Close Button In Popup And Embedded Modes

Popup mode already has a regular close button in the header. While the iframe is loading, the loader also shows a close button on the window frame. You do not need to add your own button over the iframe: clicking it hides the window, and the floating button opens it again.

In embedded mode, the regular close button is hidden because the widget occupies a host-site container. To show a close-shaped collapse button inside the widget header, pass shell.collapse_button: true. Clicking it tells the site that the user wants to collapse the widget, but does not hide the embedded wrapper by itself: the site must call SenlerWidget.close() or hide a larger host UI surface.

SenlerWidget.init({
  channel_id: "xxx",
  display_mode: "embedded",
  container: "#senler-widget",
  shell: {
    collapse_button: true,
  },
  onCollapse(detail) {
    console.log("The user clicked the close button", detail);
    SenlerWidget.close();
  },
});

After that, SenlerWidget.open() shows the same embedded instance again with its dialog state intact. If the site hides an entire panel or modal rather than the wrapper, close that host surface in onCollapse instead of calling SenlerWidget.close().

Instead of onCollapse, subscribe once to the window event:

window.addEventListener("senler-widget:collapse-request", (event) => {
  if (event.detail.display_mode === "embedded") {
    SenlerWidget.close();
  }
});

detail contains channel_id and display_mode. Choose one handling mechanism, either the callback or the event, so one click is not handled twice. There is no separate onClose callback for the regular popup close button.

Keep these commands distinct:

  • SenlerWidget.close() is the public host method that hides a popup or embedded wrapper;
  • SenlerWidget.destroy() removes the instance and is only for complete integration teardown;
  • the internal CLOSE_WIDGET message comes from the regular popup close button and is ignored by the loader in embedded mode;
  • the internal COLLAPSE_WIDGET message comes from the shell.collapse_button button, after which the loader calls onCollapse(detail) and dispatches senler-widget:collapse-request.

CLOSE_WIDGET and COLLAPSE_WIDGET belong to the iframe protocol. A host site must not send them manually through postMessage.

Left-edge gesture

shell.mobile_edge_swipe: true enables recognition of a swipe from the left edge inside the widget. The loader does not close the interface automatically; it dispatches senler-widget:mobile-edge-swipe on window.

window.addEventListener("senler-widget:mobile-edge-swipe", (event) => {
  if (event.detail.side === "left") {
    closeMobilePanel();
  }
});

detail contains channel_id, the current display_mode, and side: "left". This is a separate mobile navigation signal, not a replacement for onCollapse.

Runtime message lifecycle

If the host page passes message.requestId, the loader dispatches a senler-widget:runtime-message-result event on window. This event is for custom runtime scenarios where the host page needs to correlate a sent message with the result without polling dialog history. For inline edits in text fields, use createInlineTextEdit(config): it already tracks requestId, registers the required custom_action, and returns the ready 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 can be stored to open this dialog later.
  }

  if (event.detail.status === "answered") {
    // The AI response has been produced.
  }

  if (event.detail.status === "failed") {
    // event.detail.error_message contains the reason when it is known.
  }
});

SenlerWidget.open({
  contextItems,
  message: {
    text: "Make this text clearer.",
    requestId,
    startNewDialog: true,
    autoSend: true,
  },
});

Status sent means the message was accepted and dialog_id is known. Status answered is emitted from streaming done or from the final assistant message event. Status failed is emitted on send or streaming errors. To open the same dialog in the full widget, call SenlerWidget.open({ dialogId: event.detail.dialog_id }) or SenlerWidget.selectDialog(event.detail.dialog_id).

Inline Edits In Site Fields

createInlineTextEdit(config) gives the site a headless controller for the "select text → ask AI → show before/after" scenario. The loader does not render a dropdown and does not change the field: the external site shows the button, quick action menu, loading state, and preview in its own style. The controller connects the selected text, widget message, senler.previewTextEdit apply button, requestId, and ready 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: "Product description",
  getValue: () => textarea.value,
  getSelection: () => ({
    text: textarea.value.slice(textarea.selectionStart, textarea.selectionEnd),
  }),
  getContextItems: () => [
    {
      id: "product:123",
      kind: "product",
      role: "business_context",
      display: { label: "Product #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("Make this clearer");
});

document.querySelector("#ai-chat").addEventListener("click", () => {
  inlineEdit.openChat();
});

fieldId must identify the field on the page consistently. getValue() returns the full current field text. getSelection() returns the selection as a string or an object { text }; if the site stores the selection itself, pass it directly: inlineEdit.ask({ text: "Fix mistakes", selectedText }). getContextItems() returns the page business context: product, order, project, or another entity. The controller adds tags for the field, selected text, and assistant task itself.

The ask(text | { text, actionLabel?, selectedText? }) method creates a new background dialog, sends the request to AI, and calls onPreview(preview) when the edit is ready. The openChat({ selectedText? }) method opens the full widget with the same context: if a background dialog already exists, it opens that dialog; otherwise it creates a new empty dialog and focuses the input. getDialogId() returns the last known dialog, and destroy() removes the controller handlers.

The preview always contains the full field text before and after the edit: sourceText, replacementText, selectedText, selectedTextReplacement, and summary. If only the selected fragment replacement is returned, the loader builds the full replacementText itself, but only when the original fragment is found in the field unambiguously. Applying the change stays on the site side because editors differ in input events, undo/redo, autosave, and validation.

The inline controller uses its own status set, not the generic sent/answered/failed values from senler-widget:runtime-message-result:

  • sending - the controller started sending the request;
  • message_sent - the message was accepted, and the background dialog was created or is known;
  • answered - the answer is ready, but preview may still be pending;
  • preview_ready - the edit is ready, and onPreview receives before/after;
  • message_send_failed - the runtime message could not be sent;
  • message_answer_failed - the message was sent, but the response did not finish successfully;
  • preview_failed - there was a response, but the preview edit could not be received or parsed.

The host page should keep its own loading state until preview_ready or an error, and for message_send_failed, message_answer_failed, and preview_failed show a clear fallback: retry the request or open the chat through openChat(). openChat() uses the already known background dialog when one was created, so the user sees the same context instead of a separate empty conversation.

These statuses are for integration developers. In regular support, explain the state in human-facing words: AI is preparing a variant, the edit could not be received, the user can retry the request or open the chat.

Callback Parameters

SenlerWidget.init can receive callback parameters:

  • contextProvider() - synchronously returns the persistent context array for the current page during initialization. Promises are not supported; on SPA route changes, update context through SenlerWidget.setPageContext(items).
  • onCollapse(detail) - called when the user clicks the collapse button enabled through shell.collapse_button. detail contains channel_id and display_mode. The callback may call SenlerWidget.close() or close an outer host-site panel.

Custom action buttons are handled through the handler inside customActions, not by a separate top-level callback.

Page events

window eventWhen it is dispatched
senler-widget:collapse-requestThe user clicked the shell.collapse_button button; contains channel_id and display_mode.
senler-widget:mobile-edge-swipeThe user completed an enabled left-edge swipe; also contains side: "left".
senler-widget:runtime-message-resultA runtime message with a requestId was accepted, completed, or failed.
senler-widget:stageA diagnostic loading, connection, sending, history, or upload stage changed. Use it for observability, not business logic.

Custom actions for chat buttons

For an answer to include a button that the site will handle, the host page must explicitly declare the available customActions. In the public loader API this is an object where the key is the action name and the value contains title, description, optional payloadSchema, and required handler. The widget passes only the action description to AI, while the handler stays in the browser and is called when the user clicks a custom_action button.

If title and description are written in only one language, pass customActionsLanguage: "ru" or customActionsLanguage: "en" next to them. This field does not translate texts; it sets the language for custom action descriptions. If it is not passed, bilingual ru+en search is used.

SenlerWidget.init({
  channel_id: "xxx",
  customActionsLanguage: "en",
  customActions: {
    "site.openOrder": {
      title: "Open order",
      description: "Opens the order card by order_id.",
      payloadSchema: {
        type: "object",
        properties: {
          order_id: { type: "string" },
        },
        required: ["order_id"],
        additionalProperties: false,
      },
      handler(detail) {
        openOrder(detail.payload?.order_id);
      },
    },
  },
});

The action name is taken from the customActions object key. It must start with a Latin letter and may contain Latin letters, digits, _, ., :, and -. Up to 20 actions can be declared in one context. title and description help clarify when to use the action. title is limited to 80 characters, and description to 240 characters. payloadSchema describes a JSON object payload: the root schema must be an object, the schema depth must not exceed 8 levels, and the serialized schema must be at most 1200 characters. The total descriptor payload for all actions is limited to 6000 characters.

When buttons are enabled in a widget dialog, only actions explicitly passed by the host page through customActions are available. If the page did not pass an action, that button must not be promised or performed.

Do not invent custom_action.name: only names explicitly passed by the host page through customActions are used. In the widget, open_url is handled by the host page: target: "self" changes the current page, while links without self open in a new tab. For navigation inside the site, declare a custom_action instead of opening a direct URL through open_url.

Message Context

The host page can pass two kinds of context. Persistent page context is set through contextProvider during SenlerWidget.init or through SenlerWidget.setPageContext(items) on route changes without a reload. One-time context for the next message is passed as contextItems through SenlerWidget.open({ contextItems, message }) or SenlerWidget.updateRuntime({ contextItems, message }). This is used when context is known without manual element selection: an open order card, project, AI summary recommendation, action target, or another business entity.

contextProvider must return a ready array synchronously. If the page title or entity is loaded later, it is fine to return a basic page item first and call SenlerWidget.setPageContext(items) after the data is available, with the current label, route, and avatar_url when one exists.

function buildPageContext() {
  return [
    {
      id: `page:${window.location.pathname}`,
      kind: "page",
      role: "technical",
      display: {
        label: document.title || "Current page",
        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: "Order #123", subtitle: "Open card" },
      ref: { entity_type: "order", entity_id: "123", route: "/orders/123" },
    },
  ],
  message: {
    text: "Describe what can be done with this order.",
    startNewDialog: true,
    autoSend: true,
  },
});

Each item must have id, kind, role, display.label, and may have ref, snapshot, and payload. Role can be technical, user_selected, business_context, or action_target. Up to 12 items can be sent in one message. Limits: id up to 120 characters, kind up to 80, display.label up to 80, display.subtitle up to 140, display.icon up to 40, display.avatar_url up to 500. ref, snapshot, and payload must be objects if passed.

Use stable id values, for example page:/orders/123, project:019..., or order:123. If the same object arrives from page context and draft contextItems, a matching id leaves one tag instead of duplicates. For different entities, use different kind and different id, even when labels look similar.

The selected element is also shown as a context tag with the selected_element type. Therefore, no separate selected-element field is needed: the required context is already in the tags next to the message.

In the composer, persistent page context, selected element, and draft contextItems are shown together as tags and deduplicated by id. The user can remove any tag before sending; after sending, draft context and selected element are cleared, while persistent page context remains for later messages. In chat history, tags on an already sent message cannot be removed: they can only be expanded, opened through ref.route/ref.url, or inspected for kind, role, ref, snapshot, and payload.

After sending, the final tags are saved in dialog history: page, project, selected element, action target, or another entity. If an item has display.avatar_url, the widget shows the avatar in the tag; if there is no avatar, it uses display.icon or the default icon for kind.

Built-in flows use the same format. The widget may add the page, the cabinet assistant adds the project, and the AI summary recommendation apply button adds the target agent, selected recommendation, and apply scenario. The project item is added only when the current project route matches the loaded project; display and snapshot include the project name, public_id, and, when available, avatar_url. In dialogs these items are shown as tags next to the message.

Widget service elements

The widget may have service markers for tests, analytics, and stable UI behavior, but they are not targets for page-element actions. Highlighting points to elements on the website page, the cabinet, or the dialogs iframe; the widget UI itself is read by the user as a normal chat.

If you need to explain a button or state inside the widget, describe it in words in the documentation. For highlighting on the page, use markup on the external site or cabinet: data-ai-context-id, data-ai-label, and data-ai-kb-query.

Built-in chat history

The widget header may contain a chat history button. It opens a compact menu with chat history search, a new-dialog button, and chat history items.

History search starts from two characters. Without search, history loads in pages and loads more items on scroll. If /init returns the current dialog, the widget tries to open it on first entry; otherwise it opens the first available dialog.

Top-level parameters

Loader only accepts supported keys:

KeyPurpose
channel_idRequired ID of a Widget channel.
userVisitor data and, when authentication linking is enabled, external_id with the server-generated user_hash.
themeAppearance, dimensions, texts, and floating button.
featuresAvailable chat features.
langru, en, or auto.
config_sourcelocal for settings in code or remote for saved channel settings; defaults to remote.
display_modepopup or embedded; defaults to popup.
button_onlyRender only a non-interactive floating-button preview without a chat iframe. Incompatible with embedded; chat methods do not work in this mode.
containerA CSS selector or DOM Element for the embedded wrapper.
shellAn object with collapse_button and mobile_edge_swipe.
onCollapseCollapse-request callback.
contextProviderSynchronous provider of persistent page context.
customActionsSite action object.
customActionsLanguageAction description language: ru or en.
debugtrue enables additional loader messages in the console.

Other top-level keys are considered a configuration error.

If the widget does not start, first check channel_id, loader script availability, the correct container for embedded mode, and the absence of unsupported top-level keys.

User data

The user object supports external_id, user_hash, email, phone, first_name, last_name, avatar_url, and the data object. Generate user_hash only on the site server with the channel secret; never put that secret in browser code.

Shell

  • collapse_button: true shows a collapse button inside the widget header;
  • mobile_edge_swipe: true enables the mobile left-edge gesture.

display_mode defaults to popup. For embedded you need to pass container with a CSS selector or use auto-init through an element with data-senler-config. The combination of display_mode: "embedded" and button_only: true is not supported.

Popup mode has a floating button and built-in window closing. An embedded widget occupies its host-site container and does not use the floating button. The public open(), close(), and toggle() methods control the wrapper in both modes. The shell.collapse_button button only requests collapsing: handle either onCollapse or senler-widget:collapse-request.

Theme

theme supports:

  • chat_title;
  • default_dialog_title;
  • theme_mode;
  • position;
  • width;
  • height;
  • border_radius;
  • shadow_enabled;
  • welcome_message;
  • empty_state_message;
  • button.

Localized texts accept a { ru?: string, en?: string } object rather than a regular string. theme.position controls the position of the popup window and accepts only bottom-right, bottom-left, top-right, top-left.

theme.button.position controls the toggle button and additionally supports hidden. To hide a button, use theme.button.position = "hidden". The value theme.position = "hidden" is not supported.

theme.button supports position and the light and dark color objects. Each color object accepts background and icon in #RRGGBB format.

Rounding and configuration source

  • theme: { border_radius: 18 } sets initial rounding from 0 to 50 with config_source: "local";
  • SenlerWidget.updateRuntime({ border_radius: 18 }) immediately changes the wrapper and iframe content only for the current instance and does not save the value to the channel;
  • with config_source: "remote", saved channel settings are the persistent theme source. Do not use unsaved theme.border_radius in init as a replacement for the server setting: call updateRuntime for a temporary change, or change the radius in the cabinet and save it for a persistent change;
  • cabinet preview immediately applies the current draft and selected theme mode. This is not publication: after saving, remote code reads the value from the server, while local code must be copied to the site again.

Features

features supports:

  • file_upload;
  • voice_messages;
  • emoji;
  • split_view;
  • element_selection.

element_selection is disabled by default. If you enable features.element_selection: true, a site visitor will be able to click on the "Select Element" widget, click on a page element and submit a question along with the context of that element. For high-quality answers, the site owner should add data-ai-* markup to important buttons, fields, product cards, tariffs and registration steps. Detailed public instructions: site-actions/website-data-ai-markup.md.

Where this is in the interface

Main elements in this section:

  • channel list
  • Channels item in the side menu
  • Add channel button
  • Widget platform selection button
  • channel settings page