enВойти в Senler

Inline-правки текста через виджет

SenlerWidget.createInlineTextEdit(config) возвращает контроллер — объект с методами для сценария «выбрать текст или поле → запросить изменение у AI → показать предварительный вариант → применить его после подтверждения». Виджет связывает запрос, отдельный диалог и результат, а сайт рисует кнопку, загрузку, сравнение и подтверждение в своём стиле.

Контроллер не меняет поле автоматически. Применение остаётся на стороне сайта, потому что редакторы по-разному обрабатывают события ввода, отмену изменений, автосохранение и валидацию.

В кабинете нет отдельного экрана настройки inline-правок и у них нет единого внешнего вида: кнопки и окно сравнения создаёт ваш сайт. Senler предоставляет контроллер и события, описанные ниже.

Сначала выберите область правки

  • scope: "selection" изменяет выделенный фрагмент. Для запуска нужен непустой selectedText либо результат getSelection().
  • scope: "field" предлагает замену всего поля и работает даже с пустым значением.

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

Создайте контроллер

const textarea = document.querySelector("#description");

const pendingStatuses = new Set(["sending", "message_sent", "answered"]);
const failureStatuses = 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(pendingStatuses.has(status));
    if (failureStatuses.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({ text: "Сделай понятнее", scope: "selection" });
});

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

renderInlineLoading, renderInlineFallback и renderTextEditReview в примере обозначают компоненты вашего сайта. Замените их собственной загрузкой, сообщением об ошибке и окном подтверждения; скрипт-загрузчик не добавляет эти функции в window.

Передайте данные поля

ПараметрОбязательныйНазначение
fieldIdДаСтабильный непустой ID поля на странице, не длиннее 88 символов. Контроллер добавляет к нему служебные префиксы, а итоговый ID элемента контекста ограничен 120 символами.
fieldLabelДаПонятное агенту непустое название поля длиной до 80 символов.
getValue()ДаВозвращает полный актуальный текст строкой.
onPreview(preview)ДаПоказывает результат и применяет его только после подтверждения пользователя.
getSelection()Для selection, если текст не передан в askВозвращает строку или объект { text }.
contextItemsНетПостоянный для этого контроллера массив дополнительного контекста.
getContextItems()НетВозвращает актуальный массив перед каждым запросом. Если переданы и функция, и contextItems, используется функция.
onStatusChange(detail)НетПолучает состояние, а также применимые requestId, dialogId, preview или error.
onDialogIdChange(dialogId)НетСообщает ID созданного фонового диалога.
onError(error)НетПолучает ошибку отправки, ответа или проверки предварительного варианта.

В сообщении виджета может быть не более 12 элементов контекста. Контроллер сам добавляет три элемента для ask() и два, когда openChat() создаёт диалог. Поэтому для ask() собственные contextItems контроллера вместе с постоянным контекстом страницы должны оставлять три свободных места: после удаления дубликатов их суммарное количество не должно превышать 9.

Дополнительно можно изменить подписи и техническую инструкцию через selectedTextLabel, fieldScopeLabel, taskLabel, taskSubtitle и taskInstruction. selectedTextLabel, fieldScopeLabel и taskLabel должны быть не длиннее 80 символов, taskSubtitle — 140. Обычно достаточно значений по умолчанию. Если меняете taskInstruction, сохраните требование вызвать senler.previewTextEdit с полями результата, перечисленными ниже.

В каждый запрос агенту передаётся полный результат getValue(), выбранный фрагмент и настроенные context items. Не подключайте inline-правки к паролям, платёжным данным и другим полям, содержимое которых нельзя отправлять агенту.

Если сайт сам хранит выделение, передайте его при вызове: inlineEdit.ask({ text: "Исправь ошибки", selectedText, scope: "selection" }).

Запустите правку или откройте чат

  • ask(text | { text, actionLabel?, selectedText?, scope? }) создаёт новый диалог и отправляет запрос; onPreview вызывается, когда правка готова;
  • openChat({ selectedText?, scope? }) открывает обычный виджет с тем же контекстом и уже созданным диалогом;
  • getDialogId() возвращает ID последнего известного диалога или null;
  • destroy() снимает обработчики контроллера.

В объекте запроса text — инструкция пользователя, actionLabel — её короткая подпись в техническом контексте, selectedText — сохранённое выделение, а scope выбирает выделенный фрагмент или всё поле. В строковой форме ask("Сделай понятнее") текст одновременно используется как инструкция и подпись, а областью считается selection.

После загрузки полной версии скрипта ask() возвращает строковый requestId. Если метод вызван, пока bootstrap-загрузчик ещё ставит команды в очередь, он возвращает undefined; созданный позже ID всё равно придёт в onStatusChange. Не стройте бизнес-логику только на синхронном возвращаемом значении.

Каждый ask() запускает новый диалог и автоматически подключает внутреннее действие senler.previewTextEdit, которое формирует preview. Если диалога ещё нет, openChat() создаёт пустой диалог с контекстом и ставит фокус в поле ввода, но ничего не отправляет.

Покажите предварительный вариант и примените его

Объект preview содержит:

  • fieldId — поле, к которому относится результат;
  • sourceText — полный исходный текст поля;
  • replacementText — полный предложенный текст;
  • selectedText — исходный выделенный фрагмент;
  • selectedTextReplacement — предложенная замена фрагмента;
  • summary — необязательное краткое описание изменения.

Контроллер не записывает replacementText в поле. В onPreview покажите сравнение и подтверждение, а только затем обновите состояние редактора, вызовите его событие ввода и сохранение по правилам вашего сайта.

Для selection скрипт-загрузчик собирает полный replacementText, только если исходный фрагмент всё ещё находится в поле однозначно; сравнение допускает различия в пробелах. Для field результат отклоняется, если содержимое изменилось после отправки запроса. Так запоздалый ответ не перезаписывает более новую правку.

Обработайте состояния

  • sending — контроллер начал отправку;
  • message_sent — сообщение принято и фоновый диалог известен;
  • answered — ответ сформирован, но предварительный вариант ещё может ожидаться;
  • preview_readyonPreview получил готовый before/after;
  • message_send_failed — runtime-сообщение не отправлено;
  • message_answer_failed — ответ не завершился успешно;
  • preview_failed — ответ получен, но правку не удалось сформировать или разобрать.

Сайт должен показывать загрузку до preview_ready или ошибки. onStatusChange получает все состояния, onError — ошибку, а onDialogIdChange сообщает ID созданного диалога. При ошибке можно повторить запрос или открыть тот же диалог через openChat().

Жизненный цикл

Для одного fieldId одновременно действует один контроллер: создание нового удаляет предыдущий. Один контроллер отслеживает один активный ask(), поэтому дождитесь предварительного варианта или ошибки, прежде чем запускать следующий запрос для того же поля. Вызывайте destroy(), когда поле окончательно удаляется со страницы. После SenlerWidget.destroy() создайте и виджет, и контроллер заново. В режиме button_only inline-правки недоступны.

Где смотреть полный контракт

Методы SenlerWidget, временные параметры и события результата собраны в справочнике Public API. Используйте событие напрямую для собственных сценариев автоматической отправки; для правки поля контроллер уже выполняет эту связку и повторять её вручную не нужно.