Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

Как сделать AI чат-виджет для WordPress: JavaScript + n8n + OpenAI

Сайтостроение и инструменты

На многих сайтах можно сделать автоматическую поддержку для пользователей. Проще всего использовать лёгкий виджет чата на чистом JavaScript, а всю умную часть (бота) вынести в n8n + OpenAI. Такой подход:

  • не требует тяжёлых и сложных плагинов под WordPress;
  • даёт полный контроль над тем, как бот отвечает;
  • позволяет использовать один backend для нескольких сайтов.

Ниже — простой контракт обмена данными, код виджета и базовый/расширенный вариант воркфлоу в n8n.

1. Взаимодействие между чат-виджетом и n8n

Фронтенд-виджет общается с backend только через один HTTP-адрес (Webhook) в n8n. Это упрощает настройку и позволяет легко менять логику внутри воркфлоу.

1.1. Что отправляет виджет в n8n

Виджет делает запрос по HTTP API (метод POST) на Webhook в n8n и передаёт в JSON только самое главное: текст пользователя и базовый контекст.

POST https://your-n8n-domain.com/webhook/your-webhook-id
Content-Type: application/json

{
  "message": "Текст пользователя",
  "siteLabel": "example.com",
  "pageUrl": "https://example.com/some-page/",
  "userAgent": "Mozilla/5.0 (...)"
}
  • message — текст сообщения пользователя;
  • siteLabel — метка сайта (обычно домен или короткое имя проекта);
  • pageUrl — точный URL страницы, где открыт виджет;
  • userAgent — строка браузера (можно использовать для простой аналитики или для ключа сессии).

1.2. Что должен вернуть n8n

Ответ n8n — простой JSON с единственным полем reply. Виджет показывает это поле как сообщение бота.

{
  "reply": "Ответ бота"
}

Лучше сделать ответ максимально простым. Всё дополнительное (логи, аналитика, CRM и т.п.) удобнее обрабатывать внутри n8n, не усложняя формат ответа для виджета.

2. Фронтенд: чат-виджет на JavaScript для WordPress

Фронтенд полностью живёт в одном JavaScript-сниппете. В WordPress его удобно подключать через плагин для сниппетов.

2.1. Создание JavaScript-сниппета в WordPress

Простой порядок действий:

  1. Установить и включить плагин для сниппетов (тип: JavaScript-код на фронтенде).
  2. Создать новый сниппет типа JavaScript (не PHP и не HTML).
  3. Вставить в него код виджета целиком.
  4. Выбрать, где показывать виджет: «На всех страницах» или по своим условиям.
  5. Сохранить и включить сниппет.

Если на сайте включено кэширование и минификация скриптов, проверьте, что сниппет не ломается при объединении и сжатии. При необходимости исключите этот скрипт из минификации.

2.2. Полный код JavaScript-виджета

Подключитесь к админке WordPress, создайте JavaScript-сниппет и вставьте код ниже. Обязательно замените N8N_WEBHOOK_URL на свой реальный Webhook из n8n.

(function () {
  // Адрес webhook n8n (замените на свой)
  const N8N_WEBHOOK_URL = "https://your-n8n-domain.com/webhook/your-webhook-id";

  function initChatWidget() {
    // Защита от повторного запуска
    if (document.getElementById("chatWidget") || document.getElementById("chatToggleBtn")) {
      return;
    }

    // 1) Стили
    const style = document.createElement("style");
    style.textContent = `
      .chat-toggle-btn {
        position: fixed;
        right: 20px;
        bottom: 20px;
        width: 56px;
        height: 56px;
        border-radius: 50%;
        border: none;
        cursor: pointer;
        font-size: 14px;
        font-weight: 600;
        background: #4d3bfe;
        color: #fff;
        box-shadow: 0 6px 20px rgba(0,0,0,0.2);
        z-index: 9999;
      }

      .chat-widget {
        position: fixed;
        right: 20px;
        bottom: 90px;
        width: 320px;
        max-height: 480px;
        background: #ffffff;
        border-radius: 16px;
        box-shadow: 0 12px 30px rgba(0,0,0,0.15);
        display: flex;
        flex-direction: column;
        overflow: hidden;
        font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
        z-index: 9999;
      }

      .chat-hidden {
        display: none;
      }

      .chat-header {
        padding: 12px 16px;
        background: #4d3bfe;
        color: #fff;
        font-size: 14px;
        font-weight: 600;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }

      .chat-header button {
        border: none;
        background: transparent;
        color: #fff;
        font-size: 18px;
        cursor: pointer;
        padding: 0;
        line-height: 1;
      }

      .chat-messages {
        padding: 10px 12px;
        overflow-y: auto;
        flex: 1;
        font-size: 13px;
        background: #f7f7fb;
      }

      .chat-message {
        margin-bottom: 8px;
        max-width: 90%;
        padding: 8px 10px;
        border-radius: 10px;
        line-height: 1.4;
        word-wrap: break-word;
      }

      .chat-message.user {
        margin-left: auto;
        background: #4d3bfe;
        color: #fff;
        border-bottom-right-radius: 2px;
      }

      .chat-message.bot {
        margin-right: auto;
        background: #ffffff;
        color: #111827;
        border-bottom-left-radius: 2px;
        box-shadow: 0 1px 3px rgba(0,0,0,0.08);
      }

      .chat-status {
        font-size: 11px;
        color: #6b7280;
        padding: 4px 12px;
        min-height: 16px;
      }

      .chat-input-area {
        border-top: 1px solid #e5e7eb;
        padding: 8px;
        background: #fff;
        display: flex;
        gap: 6px;
      }

      .chat-input-area input[type="text"] {
        flex: 1;
        border-radius: 999px;
        border: 1px solid #d1d5db;
        padding: 8px 12px;
        font-size: 13px;
        outline: none;
      }

      .chat-input-area input[type="text"]:focus {
        border-color: #4d3bfe;
        box-shadow: 0 0 0 1px rgba(77,59,254,0.25);
      }

      .chat-input-area button {
        border-radius: 999px;
        border: none;
        padding: 0 14px;
        font-size: 13px;
        cursor: pointer;
        background: #4d3bfe;
        color: #fff;
        white-space: nowrap;
      }

      .chat-input-area button:disabled {
        opacity: 0.6;
        cursor: default;
      }
    `;
    document.head.appendChild(style);

    // 2) HTML виджета
    const widgetHTML = `
      <button class="chat-toggle-btn" id="chatToggleBtn" aria-label="Открыть чат">Чат</button>

      <div class="chat-widget chat-hidden" id="chatWidget">
        <div class="chat-header">
          <span>Чат-помощник</span>
          <button id="chatCloseBtn" aria-label="Закрыть чат">×</button>
        </div>

        <div class="chat-messages" id="chatMessages"></div>

        <div class="chat-status" id="chatStatus"></div>

        <form class="chat-input-area" id="chatForm">
          <input
            type="text"
            id="chatInput"
            placeholder="Напишите сообщение..."
            autocomplete="off"
            required
          />
          <button type="submit" id="chatSendBtn">Отправить</button>
        </form>
      </div>
    `;
    document.body.insertAdjacentHTML("beforeend", widgetHTML);

    // 3) Логика
    const chatWidget    = document.getElementById("chatWidget");
    const chatToggleBtn = document.getElementById("chatToggleBtn");
    const chatCloseBtn  = document.getElementById("chatCloseBtn");
    const chatMessages  = document.getElementById("chatMessages");
    const chatStatus    = document.getElementById("chatStatus");
    const chatForm      = document.getElementById("chatForm");
    const chatInput     = document.getElementById("chatInput");
    const chatSendBtn   = document.getElementById("chatSendBtn");

    function addMessage(text, sender = "bot") {
      const msg = document.createElement("div");
      msg.classList.add("chat-message", sender);
      msg.textContent = text;
      chatMessages.appendChild(msg);
      chatMessages.scrollTop = chatMessages.scrollHeight;
    }

    async function sendMessageToN8N(messageText) {
      const payload = {
        message: messageText,
        siteLabel: window.location.hostname,
        pageUrl: window.location.href,
        userAgent: navigator.userAgent
      };

      try {
        chatStatus.textContent = "Отправка...";
        chatSendBtn.disabled = true;
        chatInput.disabled = true;

        const response = await fetch(N8N_WEBHOOK_URL, {
          method: "POST",
          headers: {
            "Content-Type": "application/json"
          },
          body: JSON.stringify(payload)
        });

        if (!response.ok) {
          throw new Error("Ошибка сети: " + response.status);
        }

        const data = await response.json();

        if (data && typeof data.reply === "string") {
          addMessage(data.reply, "bot");
          chatStatus.textContent = "";
        } else {
          addMessage("Извините, сервер вернул непонятный ответ.", "bot");
          chatStatus.textContent = "Некорректный формат ответа сервера.";
        }
      } catch (error) {
        console.error("Ошибка при запросе к n8n:", error);
        addMessage("Не удалось отправить сообщение. Попробуйте позже.", "bot");
        chatStatus.textContent = "Ошибка отправки.";
      } finally {
        chatSendBtn.disabled = false;
        chatInput.disabled = false;
        chatInput.focus();
      }
    }

    chatToggleBtn.addEventListener("click", () => {
      chatWidget.classList.toggle("chat-hidden");
      if (!chatWidget.classList.contains("chat-hidden")) {
        chatInput.focus();
      }
    });

    chatCloseBtn.addEventListener("click", () => {
      chatWidget.classList.add("chat-hidden");
    });

    chatForm.addEventListener("submit", (event) => {
      event.preventDefault();
      const text = chatInput.value.trim();
      if (!text) return;

      addMessage(text, "user");
      chatInput.value = "";
      sendMessageToN8N(text);
    });

    addMessage("Здравствуйте! Задайте вопрос, и чат-помощник постарается помочь.", "bot");
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", initChatWidget);
  } else {
    initChatWidget();
  }
})();

Этот виджет не зависит от темы WordPress и работает там, где можно запустить пользовательский JavaScript на фронтенде.

3. Базовый воркфлоу в n8n: рабочий чат без истории

На стороне backend достаточно простого воркфлоу из четырёх шагов. Он уже даёт рабочий AI-чат, но без памяти о предыдущих сообщениях.

3.1. Структура воркфлоу

  1. Webhook — принимает запрос от виджета;
  2. OpenAI Chat (или HTTP Request к OpenAI API) — создаёт ответ;
  3. Function — приводит ответ к формату { reply: "..." };
  4. Respond to Webhook — отправляет JSON обратно виджету.

3.2. Настройка Webhook-ноды

Создайте новый воркфлоу и добавьте ноду Webhook:

  • HTTP Method: POST;
  • Response Mode: Using "Respond to Webhook";
  • Path: путь вида /webhook/your-webhook-id (или свой).

Webhook обычно доступен из интернета. В продакшене важно ограничить доступ по IP, токену или использовать прокси с авторизацией. API-ключи OpenAI храните только в переменных окружения n8n.

3.3. Нода OpenAI Chat

Далее добавьте ноду OpenAI Chat (или HTTP Request к OpenAI API). Задача простая: взять текст пользователя из body.message и передать его в messages.

Пример JSON для поля messages (формат может чуть отличаться, но идея одна):

[
  {
    "role": "system",
    "content": "Ты — помощник сайта. Отвечай коротко, по делу и простым техническим языком."
  },
  {
    "role": "user",
    "content": "={{ $json.body.message }}"
  }
]

3.4. Формирование ответа в Function-node

После ноды OpenAI добавьте ноду Function. В простом варианте OpenAI возвращает структуру с choices[0].message.content. Нужно вынести этот текст в поле reply.

const answer = $json.choices[0].message.content;

return [
  {
    json: {
      reply: answer
    }
  }
];

3.5. Нода Respond to Webhook

В конце цепочки подключите ноду Respond to Webhook, настроенную в режиме JSON-ответа. В теле ответа укажите:

{
  "reply": "={{ $json.reply }}"
}

В результате виджет всегда получает предсказуемый JSON вида { "reply": "..." } и показывает это сообщение в чате.

4. Расширение: история диалога и сессии

Если нужен «умный» чат с памятью, удобно хранить историю сообщений в Data Store или внешней базе. Ключ сессии можно собрать из домена, userAgent и других простых идентификаторов.

4.1. Идея архитектуры сессии

  1. Определить ключ сессии (например, CHAT:<siteLabel>:<userAgent>).
  2. Прочитать историю сообщений по этому ключу из Data Store.
  3. Добавить новое сообщение пользователя к истории.
  4. Отправить всю историю (обрезанную по длине) в OpenAI.
  5. Добавить ответ ассистента в историю и сохранить её в Data Store.
  6. Вернуть только последнее сообщение ассистента в поле reply.

4.2. Function-нода для подготовки сессии

Сразу после Webhook-ноды можно добавить Function-ноду, которая аккуратно разберёт вход и создаст ключ сессии.

const body = $json.body || {};

const message   = body.message || "";
const siteLabel = body.siteLabel || "unknown";
const pageUrl   = body.pageUrl || "";
const userAgent = body.userAgent || "";

// Простой вариант ключа сессии
const sessionKey = `CHAT:${siteLabel}:${userAgent}`;

return [
  {
    json: {
      message,
      siteLabel,
      pageUrl,
      userAgent,
      sessionKey
    }
  }
];

4.3. Чтение истории из Data Store

Далее добавьте ноду Data Store → Get:

  • Key: ={{ $json.sessionKey }};
  • Если данных нет — считайте, что результат это пустой массив [].

После этого можно использовать ещё одну Function-ноду, чтобы собрать историю и подготовить массив messages для OpenAI.

const prev = $json.value || []; // value из Data Store
const { message, sessionKey, siteLabel, pageUrl, userAgent } = $items(0, 0).json;

// ограничиваем историю, чтобы она не разрасталась
const MAX_MESSAGES = 20;
const trimmed = prev.slice(-MAX_MESSAGES);

// добавляем новое сообщение пользователя
trimmed.push({
  role: "user",
  content: message
});

// готовим messages для OpenAI с system-промптом
const messages = [
  {
    role: "system",
    content: "Ты — помощник сайта. Отвечай коротко, по делу и простым техническим языком."
  },
  ...trimmed
];

return [
  {
    json: {
      sessionKey,
      siteLabel,
      pageUrl,
      userAgent,
      history: trimmed,
      messages
    }
  }
];

4.4. Вызов OpenAI с историей

В ноде OpenAI Chat достаточно использовать поле messages из предыдущей Function-ноды, например:

={{ $json.messages }}

Так модель видит не только последнее сообщение, но и короткую историю диалога.

4.5. Добавление ответа ассистента в историю

После ноды OpenAI добавьте ещё одну Function-ноду, которая добавляет ответ ассистента в историю и одновременно формирует финальный reply для виджета.

const history = $json.history || [];
const answer = $json.choices[0].message.content;

history.push({
  role: "assistant",
  content: answer
});

return [
  {
    json: {
      sessionKey: $json.sessionKey,
      history,
      reply: answer
    }
  }
];

4.6. Сохранение истории и ответ виджету

Сохраните историю через Data Store → Set:

  • Key: ={{ $json.sessionKey }};
  • Value: ={{ $json.history }}.

Далее используйте ноду Respond to Webhook с тем же JSON, что и в базовом варианте:

{
  "reply": "={{ $json.reply }}"
}

Обычно достаточно хранить 10–20 последних сообщений. Это экономит токены и ускоряет ответы, но у пользователя остаётся ощущение живого диалога.

5. Документация для клиента или партнёра

Ниже — пример простого описания сервиса, который можно отдать владельцу сайта в виде инструкции (например, в Notion или PDF).

5.1. Что это за чат-виджет

Это лёгкий виджет чата для поддержки пользователей, который подключается на сайт одной строкой кода и общается через n8n + OpenAI. Вся логика бота — на стороне n8n. На сайте только внешний JavaScript-файл.

5.2. Как подключить на WordPress

  1. Установите плагин для JavaScript-сниппетов.
  2. Создайте новый сниппет типа JavaScript.
  3. Вставьте предоставленный код виджета и укажите адрес Webhook.
  4. Включите сниппет и выберите, где показывать виджет (на всех страницах или только в нужных местах).

5.3. Как работает обмен с backend

  1. Пользователь открывает виджет и пишет сообщение.
  2. Виджет отправляет запрос POST в n8n:
{
  "message": "Текст пользователя",
  "siteLabel": "<домен вашего сайта>",
  "pageUrl": "<полный URL страницы>",
  "userAgent": "<браузер пользователя>"
}
  1. n8n:
    • получает сообщение через Webhook;
    • при необходимости подгружает историю диалога;
    • обращается к OpenAI за ответом;
    • формирует ответ в виде JSON.
  2. Виджет показывает поле reply как ответ бота:
{
  "reply": "Ответ бота"
}

5.4. Что можно делать с логами

Дополнительно доступны поля:

  • siteLabel — домен или идентификатор сайта, на котором стоит виджет;
  • pageUrl — страница, на которой был задан вопрос;
  • userAgent — тип устройства и браузера.

Эти данные можно использовать для:

  • аналитики по страницам и типам трафика;
  • улучшения ответов (например, отдельные промпты для разных разделов сайта);
  • отправки обращений в CRM или helpdesk.

Заключение

Подход с лёгким JavaScript-виджетом и backend на n8n + OpenAI даёт простой в поддержке и расширяемый AI-чат для WordPress без тяжёлых плагинов. Контракт в виде простого JSON облегчает интеграцию, а хранение истории в Data Store позволяет добавить память и аналитику по мере развития проекта.

Оцените статью
ctrllife.ru
Подписаться
Уведомить о
guest
0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x