WEB-студия U11 | Блог эксперта по веб-разработке Сергея Майорова

🤖 Свой ИИ-ассистент на YandexGPT для Tilda и любого сайта — кейс разработчика

Свой ИИ-ассистент на YandexGPT для Tilda и любого сайта — кейс разработчика
Каждый второй разработчик сейчас предлагает клиентам «умный чат-бот с ИИ». Каждый второй такой бот на деле — виджет от стороннего SaaS-сервиса: чужой сервер, чужая база данных с перепиской ваших клиентов, ежемесячная подписка и ноль контроля над тем, что именно бот говорит от вашего имени.
Я пошёл другим путём и написал собственного ассистента с нуля — на PHP и SQLite, без единого внешнего фреймворка, с полным контролем над кодом, данными и логикой ответов. Он прямо сейчас работает на этом сайте — можете открыть чат в углу экрана и убедиться сами. В этой статье — как он устроен внутри, зачем я принял именно такие технические решения, и что происходит на сайте в момент, когда посетитель нажимает на кнопку чата.

📑 Содержание

  1. Одна строка — и виджет живёт на сайте 🔌
  2. Мозг ассистента — YandexGPT по API, без посредников 🧠
  3. Почему именно YandexGPT, а не что-то ещё 💸
  4. Что на самом деле происходит между вопросом и ответом 🛤️
  5. Контакты — не текстом, а токенами 🔗
  6. Что видит браузер посетителя — и что нет 🔒
  7. База данных — файл, а не дорогой сервер 💾
  8. Живая синхронизация — там, где обычно все останавливаются 🔄
  9. На стороне посетителя: тихая дозагрузка без единого лишнего клика 👤
  10. На стороне администратора: список диалогов 📋
  11. На стороне администратора: просмотр конкретного диалога 💬
  12. Плашка-приглашение — небольшая деталь, которая работает на конверсию 🚀
  13. Админ-панель — переписка под контролем, без сторонних сервисов 🛠️
  14. Поиск на кириллице — и грабли, на которые я наступил 🔎
  15. Восстановление пароля — письмо с доменной почты ✉️
  16. Про куки и что вообще хранится у посетителя 🍪
  17. Зачем всё это разработчику сайтов на Tilda 💡
  18. Заключение ⚡

🔌 Одна строка — и виджет живёт на сайте

Первое требование, которое я себе поставил: подключение не должно быть головной болью. Никаких билд-шагов, npm, сборщиков. Одна строка перед закрывающим тегом </body>:
<!-- html -->
<script src="https://my-domen.ru/public/embed.js"></script>
И всё. Скрипт сам рисует кнопку в правом нижнем углу, сам подгружает настройки (заголовок, приветствие, аватар), сам разворачивает окно чата при клике. На Tilda это буквально копипаст в настройках футера — без доступа к хостингу, без правки шаблонов.
Технически весь клиентский функционал — разметка, стили, обработчики событий, рендеринг сообщений, синхронизация с сервером — живёт в одном файле. Осознанное решение: раз скрипт подключается на сторонние сайты одним тегом, разбивать его на модули означало бы для каждого сайта дополнительный HTTP-запрос и лишнюю точку отказа. Простота встраивания важнее «архитектурной чистоты».
Вид виджета на сайте в закрытом виде

Рис. 1. Вид виджета на сайте в закрытом виде.

🧠 Мозг ассистента — YandexGPT по API, без посредников

Отвечает не «набор готовых фраз по ключевым словам», а настоящая большая языковая модель — YandexGPT, к которой сервер обращается напрямую по HTTPS-API. Никаких прослоек, никаких сторонних платформ-конструкторов ботов — я управляю запросом к модели полностью сам.
Вот упрощённый, но рабочий по сути код клиента, который отправляет запрос и разбирает ответ (ключ API, разумеется, замаскирован — показываю только логику):
/* php */
class YandexClient {
    private $apiKey;
    private $folderId;
    private $modelName;

    public function __construct() {
        // Ключ и ID каталога читаются из переменных окружения,
        // а не хранятся прямо в коде — это отдельный секретный файл,
        // недоступный из браузера ни при каком URL.
        $this->apiKey    = getenv('YANDEX_API_KEY');   // Токен Яндекс: AQVN••••••••••••••••3f8k
        $this->folderId  = getenv('YANDEX_FOLDER_ID'); // b1g••••••••••••••01u
        $this->modelName = getenv('YANDEX_MODEL_NAME');
    }

    public function sendMessage($messages) {
        $url = 'https://llm.api.cloud.yandex.net/foundationModels/v1/completion';
        $modelUri = "gpt://{$this->folderId}/{$this->modelName}/latest";

        $data = [
            'modelUri' => $modelUri,
            'completionOptions' => [
                'stream'      => false,
                'temperature' => 0.6,   // баланс "живости" и предсказуемости ответа
                'maxTokens'   => '2000' // жёсткий потолок длины одного ответа
            ],
            'messages' => $messages
        ];

        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            "Authorization: Api-Key {$this->apiKey}"
        ]);
        curl_setopt($ch, CURLOPT_TIMEOUT, 25);

        $response = curl_exec($ch);
        $result = json_decode($response, true);

        return $result['result']['alternatives'][0]['message']['text'];
    }
}

💸 Почему именно YandexGPT, а не что-то ещё

Выбор модели — не случайность, а результат вполне прагматичного расчёта.
Во-первых, на старте у Yandex Cloud есть пробный период: новому платёжному аккаунту даётся грант на 4 000 рублей на 60 дней — этого гранта хватает, чтобы полноценно обкатать бота на реальном трафике сайта, снять метрики по расходу токенов и понять экономику проекта ещё до того, как начнёт списываться реальная оплата.
Во-вторых — уже по деньгам после пробного периода. Для задачи «консультант на сайте», где каждый ответ — это несколько абзацев текста, а не многостраничная аналитика, YandexGPT-lite остаётся одним из самых дешёвых по стоимости токена вариантов на рынке. Для такого объёма это означает: даже при активном общении посетителей стоимость обслуживания чата остаётся копеечной по сравнению со стоимостью привлечения самого посетителя на сайт (реклама, SEO) — экономика легко сходится даже без оптимизации промпта под минимальный расход токенов.
В-третьих — данные остаются в российской юрисдикции, что для клиентского сервиса на российском рынке снимает лишние вопросы про хранение персональных данных за рубежом.

🛤️ Что на самом деле происходит между вопросом и ответом

Разберу весь путь одного сообщения по шагам — от нажатия Enter посетителем до текста, который он видит на экране.

✍️ Шаг 1. Клиент собирает и отправляет вопрос.

embed.js берёт текст из поля ввода и отправляет его на сервер вместе с идентификатором сессии — сам текст переписки на клиенте нигде не хранится, только этот id.

🗄️ Шаг 2. Сервер пишет вопрос в базу данных ещё до обращения к модели.

Это важно: если YandexGPT вдруг ответит с ошибкой или таймаутом, вопрос пользователя всё равно не потеряется — он уже сохранён.

🧩 Шаг 3. Сервер собирает контекст для модели — три источника, склеенные в один системный промпт.

/* php */
$systemPrompt = file_get_contents('private/prompt-rules.md')  // как отвечать: тон, формат
              . file_get_contents('private/knowledge.md')      // что известно о бизнесе
              . renderContactsAsTokenList(getContacts());       // какие сейчас есть токены контактов

🕰️ Шаг 4. К системному промпту добавляется история диалога.

Если с последнего сообщения этой сессии прошло меньше 24 часов — сессия считается «живой», и модель получает предыдущие реплики целиком (до 30 последних сообщений), чтобы помнить контекст. Если сессия «остыла» — контекст начинается заново, как будто это новый разговор.

📡 Шаг 5. Собранный массив сообщений уходит в YandexGPT.

system + история + новый вопрос отправляются тем самым методом sendMessage() выше. Модель не знает заранее, что ответит — генерирует текст токен за токеном на основе всего, что получила в контексте.

📥 Шаг 6. Сырой ответ модели сохраняется в базу как есть, без обработки.

Именно как есть — если модель вставила {{contacts_block}}, в базе останется этот технический токен, а не готовый развёрнутый номер телефона. Это принципиально: на следующем ходу диалога модель должна увидеть в истории свой же вызов токена, а не готовый текст с цифрами — иначе она начинает пересказывать номер своими словами, путая цифры.

🎨 Шаг 7. Только перед показом ответ проходит цепочку обработки.

Снимаются случайные обратные кавычки вокруг токенов → markdown превращается в безопасный HTML (Parsedown в safe-режиме) → токены контактов разворачиваются в реальные кликабельные ссылки → уже в браузере ответ ещё раз прогоняется через DOMPurify перед вставкой в страницу — второй независимый барьер защиты от вредоносного кода в тексте.

📤 Шаг 8. Готовый HTML уходит клиенту.

embed.js вставляет его в окно чата — и вот тот самый ответ, который видит посетитель.

🔗 Контакты — не текстом, а токенами

Вот деталь, которой я особенно горжусь с инженерной точки зрения. Модель никогда не пишет номер телефона или ссылку на Telegram сама — вместо этого она вставляет в ответ технический токен вида {{contacts_block}} или {{phone}}, а уже сервер разворачивает его в готовую кликабельную ссылку:
/* php */
function expandContactTokens(string $html, array $contacts): string {
    // {{contacts_block}} → полный список каналов связи,
    // {{phone}}, {{telegram}}, {{whatsapp}} → отдельные ссылки
    return str_replace(
        array_keys($contacts),
        array_values($contacts),
        $html
    );
}
Зачем так сложно? Потому что LLM время от времени «доизобретает» цифры в номерах телефонов или путает ссылки — известная особенность любых генеративных моделей. Если контакты меняются (новый номер, другой мессенджер), я правлю их в одном файле — и бот сразу во всех будущих ответах отдаёт актуальные данные, ему физически неоткуда взять другие.

🔒 Что видит браузер посетителя — и что нет

Проект аккуратно разделён на две зоны:
Публичная часть (public/) — то, что реально отдаётся браузеру: сам скрипт виджета, картинки, два лёгких API-эндпоинта.
Приватная часть (private/) — системный промпт, база знаний, ключ API, вся логика работы с базой данных. Эта папка физически недоступна из браузера ни при каком URL — на уровне веб-сервера стоит правило Require all denied. Даже если кто-то узнает точный путь к файлу с промптом, сервер ответит «403 Доступ запрещён», а не отдаст содержимое.
Это стандартная практика для серьёзных бэкенд-проектов, но именно в мире «быстрых чат-ботов на коленке» её часто пропускают — секреты и системные инструкции оставляют лежать рядом с публичными файлами.

💾 База данных — файл, а не дорогой сервер

Вся переписка хранится в SQLite — облегчённой базе данных, которая живёт в одном обычном файле, без отдельного сервера СУБД. Для проекта такого масштаба это осознанный плюс: никаких лишних движущихся частей, резервная копия — это просто скопировать один файл.
Маленькая деталь для тех, кто ценит защиту в глубину: файл базы данных называется conversations.db.php, а не conversations.db. Расширение .php — не потому что это программный код, а ровно наоборот: если бы кто-то узнал прямой путь к файлу, веб-сервер попытался бы исполнить его как PHP-скрипт (получит пустоту) вместо того, чтобы просто отдать сырые байты базы данных на скачивание. Простой, но эффективный трюк.

🔄 Живая синхронизация — там, где обычно все останавливаются

Вот сценарий, который легко упустить при проектировании: посетитель открыл сайт в двух вкладках браузера одновременно (так бывает чаще, чем кажется) — обе используют одну и ту же сессию диалога. Он написал вопрос в одной вкладке, а окно чата во второй — так и не узнало об этом, даже спустя долгое время.
Я закрыл этот пробел сразу в трёх местах проекта — на стороне посетителя и на стороне администратора, и у каждого своя логика поведения.

👤 На стороне посетителя: тихая дозагрузка без единого лишнего клика

Пока окно чата открыто, скрипт раз в 15 секунд незаметно сверяется с сервером — не появилось ли новых сообщений в этой же сессии из другой вкладки:
/* javascript */
// Общая сверка с сервером — вызывается и при открытии окна, и по
// таймеру, пока окно остаётся открытым.
async function syncWithServer() {
    try {
        const res = await fetch(`${CONFIG_URL}?sessionId=${encodeURIComponent(sessionId)}`);
        const data = await res.json();

        if (data.totalCount > knownMessageCount) {
            const missingCount = data.totalCount - knownMessageCount;
            const newTail = data.history.slice(-missingCount);

            // Автоскролл вниз — только если пользователь и так был
            // у нижнего края чата. Если он листает историю выше —
            // его текущий вид не трогаем.
            const wasAtBottom =
                messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight < 40;

            appendHistoryMessages(newTail);
            if (wasAtBottom) messagesEl.scrollTop = messagesEl.scrollHeight;

            knownMessageCount = data.totalCount;
        }
    } catch (e) {
        // Тихо игнорируем — не критично, просто не дозагрузили в этот раз.
    }
}

// Таймер живёт, только пока окно чата открыто
let pollIntervalId = setInterval(syncWithServer, 15000);

// И гарантированно гасится при закрытии окна —
// незачем слать запросы, пока чат никто не смотрит
function stopPolling() {
    clearInterval(pollIntervalId);
    pollIntervalId = null;
}
Новые сообщения просто тихо появляются в конце переписки — как в обычном мессенджере, никакого баннера, никакого «нажмите, чтобы обновить». Для посетителя это и должно выглядеть максимально естественно.
Автоматическая дозагрузка диалога без единого лишнего клика

Рис. 2. Автоматическая дозагрузка диалога без единого лишнего клика.

📋 На стороне администратора: список диалогов

В админ-панели логика уже другая — намеренно. Администратор часто читает или копирует текст переписки в моменте, и внезапная подмена контента у него на экране может сбить с толку. Поэтому вместо тихой дозагрузки — ненавязчивый баннер с приглашением обновить вручную:
/* javascript */
const initial = { sessionCount: <?= $initialMarker['sessionCount'] ?>,
                   latestAt: <?= json_encode($initialMarker['latestAt']) ?> };

async function checkForUpdates() {
    try {
        const res = await fetch('check-updates.php');
        const data = await res.json();

        // Сравниваем со снимком состояния на момент загрузки страницы
        if (data.sessionCount !== initial.sessionCount || data.latestAt !== initial.latestAt) {
            document.getElementById('u11-update-banner').style.display = 'block';
        }
    } catch (e) {
        // Тихо игнорируем — следующая попытка через 25 секунд
    }
}

setInterval(checkForUpdates, 25000);
Серверная часть этого опроса — намеренно «дешёвый» запрос, который не тянет за собой всю переписку, а лишь две цифры:
/* php */
// check-updates.php
function getActivityMarker() {
    $stmt = getDb()->query(
        'SELECT COUNT(DISTINCT session_id) AS session_count,
                MAX(created_at) AS latest_at
         FROM messages'
    );
    return $stmt->fetch(PDO::FETCH_ASSOC);
}

echo json_encode(getActivityMarker());
Вид на стороне администратора: список диалого

Рис. 4. Вид на стороне администратора: список диалогов.

💬 На стороне администратора: просмотр конкретного диалога

Отдельная и более точная версия того же механизма — когда админ открыл переписку одного конкретного посетителя. Здесь опрос не сканирует всю базу целиком, а считает сообщения только этой сессии:
/* php */
// check-updates.php — с параметром ?session=...
$sessionId = $_GET['session'] ?? null;

if ($sessionId) {
    function countSessionMessages($sessionId) {
        $stmt = getDb()->prepare('SELECT COUNT(*) FROM messages WHERE session_id = ?');
        $stmt->execute([$sessionId]);
        return (int) $stmt->fetchColumn();
    }

    echo json_encode(['messageCount' => countSessionMessages($sessionId)]);
}
/* javascript */
const sessionId = <?= json_encode($viewingSessionId) ?>;
const initialMessageCount = <?= (int) $initialMessageCount ?>;

async function checkForSessionUpdates() {
    const res = await fetch('check-updates.php?session=' + encodeURIComponent(sessionId));
    const data = await res.json();

    if (data.messageCount > initialMessageCount) {
        document.getElementById('u11-session-update-banner').style.display = 'block';
    }
}

setInterval(checkForSessionUpdates, 15000);
Интересная деталь: два таймера админки — 25 секунд для списка и 15 секунд для конкретного диалога — никогда не тикают одновременно. Это не результат специальной блокировки, а просто следствие того, что это обычная многостраничная навигация: при переходе со списка в диалог браузер полностью перезагружает страницу, а вместе с ней — уничтожает предыдущий JS-таймер.
Вид на стороне администратора: просмотр конкретного диалога

Рис. 5. Вид на стороне администратора: просмотр конкретного диалога.

🚀 Плашка-приглашение — небольшая деталь, которая работает на конверсию

Кнопка чата в углу экрана — это хорошо, но люди не всегда её замечают. Поэтому рядом с кнопкой через несколько секунд после появления показывается ненавязчивая плашка-приглашение:
«Помочь с проектом? 🚀»
Она не мигает и не преследует посетителя — появляется один раз за сутки на устройство, «живёт» рядом с кнопкой около 10 секунд и мягко гаснет, если на неё не среагировали. Клик по ней сразу открывает окно чата — как если бы кликнули по самой кнопке.
Логика показа продумана так, чтобы плашка не раздражала при повторных заходах на сайт — весь цикл держится на нескольких константах и одном localStorage-флаге, общем для всех вкладок и страниц сайта:
/* javascript */
const BUBBLE_COOLDOWN_MS = 24 * 60 * 60 * 1000; // не чаще раза в сутки
const BUBBLE_SHOW_DELAY_MS = 3000;              // задержка перед показом
const BUBBLE_VISIBLE_WINDOW_MS = 10000;         // сколько "живёт" на экране

let bubbleShownAt = null;
let bubblePendingShow = false;
let bubblePermanentlyClosed = false;

function isBubbleEligible() {
    const lastShown = Number(localStorage.getItem('u11_bubble_last_shown') || 0);
    return !bubblePermanentlyClosed && (Date.now() - lastShown) > BUBBLE_COOLDOWN_MS;
}

function showBubbleNow() {
    bubble.classList.add('visible');
    bubbleShownAt = Date.now();
    localStorage.setItem('u11_bubble_last_shown', String(bubbleShownAt));

    // Плашка обязана "прожить" 10 секунд с момента ПЕРВОГО показа,
    // даже если пользователь несколько раз проскроллит вверх-вниз
    setTimeout(() => {
        bubble.classList.remove('visible'); // плавное угасание по CSS-transition
        bubblePermanentlyClosed = true;
    }, BUBBLE_VISIBLE_WINDOW_MS);
}

function closeBubbleInstantly() {
    // Мгновенно, БЕЗ fade-анимации — иначе при клике на 9-й секунде
    // плашка "доигрывала" бы исчезновение уже поверх открытого чата
    bubble.style.transition = 'none';
    bubble.classList.remove('visible');
    requestAnimationFrame(() => { bubble.style.transition = ''; });
    bubblePermanentlyClosed = true;
}

function handleScroll() {
    const buttonVisible = window.scrollY > SCROLL_THRESHOLD;
    btn.classList.toggle('visible', buttonVisible);

    if (buttonVisible && isBubbleEligible() && !bubbleShownAt) {
        setTimeout(() => {
            if (buttonVisible) showBubbleNow();
            else bubblePendingShow = true; // покажем, как только кнопка снова станет видна
        }, BUBBLE_SHOW_DELAY_MS);
    }
}

btn.addEventListener('click', closeBubbleInstantly);
bubble.addEventListener('click', () => { closeBubbleInstantly(); openChat(); });
Небольшой психологический приём («предложить конкретное действие», а не просто показать иконку) — и заметный прирост CTR по кнопке чата на практике.
Плашка-приглашение виджета — небольшая деталь, которая работает на конверсию

Рис. 6. Плашка-приглашение виджета — небольшая деталь, которая работает на конверсию.

🛠️ Админ-панель — переписка под контролем, без сторонних сервисов

Отдельная закрытая панель для меня как владельца:
•список всех диалогов с поиском по содержимому (в том числе на кириллице, регистронезависимо);
•просмотр полной переписки конкретного посетителя;
•ненавязчивый баннер «Появились новые сообщения» — тоже живая синхронизация, только уже на моей стороне: не нужно вручную обновлять страницу, чтобы увидеть, продолжил ли клиент диалог;
•управление доступом других администраторов (с ролями и лимитом аккаунтов — на случай, если в будущем к проекту подключатся коллеги).
Вид админ-панели с пользователями

Рис. 7. Вид админ-панели с пользователями.

🔎 Поиск на кириллице — и грабли, на которые я наступил

Казалось бы, простая задача — «искать по переписке» — на деле принесла пару неочевидных нюансов.
Первый: встроенная в SQLite функция LOWER() умеет приводить к нижнему регистру только латиницу — «Тест» и «тест» для неё две разные строки. Решение — хранить рядом с каждым сообщением ещё одну колонку, content_lower, уже приведённую к нижнему регистру средствами PHP (mb_strtolower(), который умеет в UTF-8 корректно), и искать именно по ней, тоже предварительно приводя поисковый запрос к нижнему регистру.
Второй нюанс оказался интереснее и всплыл только на реальных данных: в первой версии поиска запрос напрямую выбирал строки из таблицы сообщений — а если посетитель в одном диалоге упоминал искомое слово несколько раз (например, дважды написал «сайт»), тот же самый диалог дублировался в списке результатов несколько раз подряд. Внешне выглядело как баг с базой данных, а на деле — просто отсутствие группировки по диалогу:
/* php */
// Так БЫЛО — один диалог мог попасть в результаты
// столько раз, сколько раз в нём встретилось слово:
$stmt = $db->prepare(
    'SELECT session_id, created_at FROM messages
     WHERE content_lower LIKE ?'
);

// Так СТАЛО — сначала находим уникальные session_id,
// затем уже собираем сводку по каждому диалогу одним рядом:
$stmt = $db->prepare(
    "SELECT session_id, MIN(created_at) AS started_at,
            MAX(created_at) AS last_message_at,
            COUNT(*) AS messages_count
     FROM messages
     WHERE session_id IN (
         SELECT DISTINCT session_id FROM messages WHERE content_lower LIKE ?
     )
     GROUP BY session_id
     ORDER BY last_message_at DESC"
);
Простое исправление, но полезный урок: с базами данных первая «очевидная» версия запроса не всегда учитывает связи между строками — GROUP BY и DISTINCT здесь не формальность, а необходимость.

✉️ Восстановление пароля — письмо с доменной почты

Вход в админку защищён паролем, и на случай, если я его забуду — есть восстановление по email со ссылкой, ограниченной по времени действия. Сама отправка письма — через встроенную функцию PHP mail(), без подключения платных сервисов рассылок:
/* php */
$fromAddress = 'no-reply@' . $domainHost; // например, no-reply@u11.ru

$headers = "From: {$fromAddress}\r\nContent-Type: text/plain; charset=UTF-8\r\n";

// Envelope sender задаётся ЯВНО через параметр -f — без этого письма
// не доходили даже при корректно настроенных SPF и DKIM в DNS: сервер
// подставлял свой технический адрес в служебный MAIL FROM на уровне
// SMTP-диалога, и почтовые провайдеры проверяли SPF именно по нему,
// а не по видимому заголовку «От кого» в самом письме.
$envelopeSender = '-f' . $fromAddress;

mail($toEmail, $subject, $body, $headers, $envelopeSender);
Отдельно стоит сказать: одного правильного кода недостаточно. Чтобы письма с доменной почты не улетали в спам (а часто — вообще не доставлялись), в DNS-записях домена обязательно должны быть настроены SPF (список серверов, которым разрешено отправлять почту от имени домена) и DKIM (цифровая подпись письма, подтверждающая, что оно не подделано по дороге). Без этих двух записей даже идеальный код отправки будет проигрывать спам-фильтрам получателя.
По самой ссылке восстановления — не буду приводить код проверки токена: он специально устроен так, чтобы ссылка была одноразовой, ограниченной по времени и непредсказуемой, и раскрывать логику её генерации и валидации в публичной статье — плохая идея с точки зрения безопасности. Важно, что сам функционал есть и работает надёжно.
Вход защищён полноценно: cookie-сессии с истечением, блокировка при переборе пароля, восстановление по email, CSRF-защита форм. Это не игрушечная админка «на один пароль в коде» — а такой же серьёзный уровень защиты, какой я закладываю в проекты клиентов.
Восстановление пароля через письмо с доменной почты

Рис. 8. Восстановление пароля через письмо с доменной почты.

🍪 Про куки и что вообще хранится у посетителя

Прозрачность — часть экспертной репутации, поэтому коротко закрою и этот вопрос. У виджета на стороне браузера хранится только техническое необходимое:
•идентификатор текущей сессии диалога (чтобы бот помнил контекст разговора при перезагрузке страницы);
•отметка о том, что плашка-приглашение уже показывалась сегодня (чтобы не показывать её повторно и не раздражать).
Никакой личной информации, никакой рекламной рассылки, никакой продажи данных третьим лицам — это моя собственная инфраструктура, а не чужой сервис, зарабатывающий на данных пользователей.

💡 Зачем всё это разработчику сайтов на Tilda

Я мог бы подключить готовый виджет от стороннего сервиса за 10 минут. Но тогда я не смог бы объяснить клиенту ни одной технической детали того, как он работает — просто пересказал бы маркетинговую страницу разработчика. Написав ассистента самостоятельно, я точно знаю, что происходит с каждым словом, которое говорит бот от лица моей студии, и могу так же спроектировать индивидуальное решение для клиента — с его собственной базой знаний, его тоном общения, его правилами.
Если вам интересно похожее решение для вашего сайта — обычный чат-бот, консультант по каталогу, квалификация заявок перед звонком менеджера — пишите, обсудим, что имеет смысл именно для вашего бизнеса.

⚡ Заключение

ИИ-ассистент на сайте в 2026 году — это не модная игрушка «для галочки», а инструмент, который либо снимает часть нагрузки с менеджера и доводит посетителя до заявки, либо просто висит иконкой в углу экрана без дела. Разница между этими двумя сценариями редко в самом факте наличия чат-бота — чаще в том, кто его настроил: сторонний сервис по шаблону или разработчик, который контролирует каждую строку промпта и знает, что происходит с данными Ваших клиентов.
Если Вы сомневаетесь, нужен ли Вам ИИ-ассистент или его стоит доработать под конкретные задачи — начните с честного разбора того, что у Вас есть сейчас. Иногда решение не в подключении очередного SaaS-виджета, а в написании собственного бота под Вашу базу знаний и Ваш тон общения.
Помогу настроить своего ИИ-ассистента на YandexGPT — с закрытой базой данных, админ-панелью и без ежемесячной платы за чужой сервис.
Готов бесплатно разобрать Ваш сайт и показать, где именно теряются заявки — возможно, ИИ-ассистент решит эту задачу быстрее, чем кажется. Написать можно через u11.ru или в блоге u11.ru/blog, где я разбираю подобные кейсы регулярно.
Можете также пройти опрос или заполнить форму обратной связи ниже. Я обязательно отвечу удобным для Вас способом в течение 15 минут в рабочее время.