Виджеты для сайта
Встраивайте голосовых агентов на сайт с настраиваемыми виджетами
Встраивайте голосового агента на любой сайт, чтобы посетители могли начать голосовой разговор прямо из браузера. Вкладка «Виджеты» даёт коды встраивания, настройки внешнего вида и ограничения доменов.
Виджет обратного звонка работает совсем иначе, чем виджеты на этой странице — вместо старта голосового звонка в браузере он собирает телефонный номер посетителя, и ваш агент сам перезванивает. См. отдельную страницу Виджет обратного звонка.
Типы виджетов
Говорило предлагает четыре типа браузерных виджетов плюс отдельный Виджет обратного звонка для телефонных callback:
| Виджет | Имя тега | Описание | Лучше всего для |
|---|---|---|---|
| Плавающий виджет | govorilo-floating-call | Orb-кнопка, плавающая над вашей страницей | Всегда видимый призыв к действию |
| Pill-виджет | govorilo-pill-call | Компактная пилюлевидная кнопка, размещённая inline в вашем контенте | Минимальный footprint внутри существующего макета |
| Плавающий Pill-виджет | govorilo-pill-floating-call | Та же компактная форма pill, что и у Pill, но плавает со страницей, как виджет Floating | Когда нужна эстетика pill, следующая за посетителем при скролле |
| Инлайн-виджет | govorilo-inline-call | Полноразмерная кнопка вызова, встроенная в контент страницы | Выделенные секции «Поговорить с нами» |
| Виджет обратного звонка | govorilo-callback | Форма для номера телефона; агент перезванивает посетителю | Лидген-страницы, high-intent лендинги — см. Виджет обратного звонка |
В кабинете плавающая «пилюля» — это код Pill, переключённый в режим Float. Её тег — govorilo-pill-floating-call: проверьте, что в скопированном коде стоит именно он.
Ссылка «Посмотреть пример» на вкладке «Виджеты» открывает живую демо-страницу с виджетом агента.
Общие атрибуты
Все типы виджетов поддерживают эти ключевые атрибуты:
| Атрибут | Обязательно | Описание | По умолчанию |
|---|---|---|---|
agent-id | Да | Уникальный идентификатор агента | — |
voice-service-url | Нет | Переопределить URL голосового сервиса | Автоопределение |
api-base-url | Нет | Переопределить базовый URL API | Автоопределение |
support-session-token | Нет | Короткоживущий токен, который ваш сервер выдаёт вошедшему пользователю; передаётся вашим MCP-серверам. Только Floating, Pill, Pill Floating и Inline — см. Опознание вошедшего пользователя | — |
Атрибуты отображения
| Атрибут | Описание | Значения | По умолчанию |
|---|---|---|---|
position | Позиция виджета на странице | bottom-right, bottom-left, top-right, top-left, static | bottom-right |
size | Размер виджета в пикселях | Число | 120 (Floating), 370 (Inline; на маленьких экранах уменьшается) |
orb-size | Диаметр сферы в виджетах Pill | Число | 48 |
theme | Имя цветовой темы | См. Цветовые темы | default |
Атрибуты текста кнопки
| Атрибут | Описание | По умолчанию |
|---|---|---|
button-start-text | Текст на кнопке до звонка | "Call" (Floating), "Start Call" (Inline), "Talk to AI Agent" (Pill) |
button-connecting-text | Текст при соединении | "Connecting..." |
button-end-text | Текст во время звонка | "End call" (Pill) |
Подписи по умолчанию — на английском; для русскоязычного сайта задайте свои, например button-start-text="Позвонить".
Атрибуты сферы
Трёхмерная сфера реагирует на голос. Самые полезные настройки:
| Атрибут | Описание | По умолчанию |
|---|---|---|
glow-intensity | Сила свечения, 0–2 | 0.8 |
audio-reactivity | Насколько сильно сфера реагирует на звук | 3.0 |
theme-mode | auto (как в системе), dark или light | auto |
Остальные (morph-strength, noise-speed, rotation-speed и другие) перечислены в README пакета.
Атрибуты Terms
| Атрибут | Описание | По умолчанию |
|---|---|---|
terms-enabled | Включить диалог согласия до звонка | false |
terms-content | Markdown-форматированный текст согласия | "" |
terms-url | Ссылка на вашу страницу с условиями использования | "https://example.ru/terms" |
privacy-url | Ссылка на вашу страницу с политикой конфиденциальности | "https://example.ru/privacy" |
Terms-атрибуты, заданные на HTML-элементе, переопределяются настройками виджета агента, полученными из API, если только skip-fetch не выставлен в true.
Атрибуты звука
| Атрибут | Описание | По умолчанию |
|---|---|---|
sound-enabled | Включить звуки старта/окончания звонка | true |
sound-volume | Громкость звуковых эффектов | 0.25 |
sound-preset | Идентификатор пресета звука | "1" |
Цветовые темы
Кастомизируйте внешний вид виджета с помощью 11 встроенных цветовых тем:
| Тема | Значение |
|---|---|
| Default | default |
| Purple | purple |
| Blue | blue |
| Cyan | cyan |
| Emerald | emerald |
| Amber | amber |
| Tangerine | tangerine |
| Rose | rose |
| Ember | ember |
| Black | black |
| White | white |
У каждой темы есть тёмный и светлый варианты. Задайте тему через атрибут theme в embed-коде или сконфигурируйте её в настройках виджета агента.
Примеры встраивания
Плавающий виджет
<govorilo-floating-call agent-id="YOUR_AGENT_ID"></govorilo-floating-call>
<script src="https://app.govorilo.tech/widget/govorilo-widget.js" async></script>
Плавающий виджет с темой и позицией
<govorilo-floating-call
agent-id="YOUR_AGENT_ID"
theme="emerald"
position="bottom-left"
size="140"
></govorilo-floating-call>
<script src="https://app.govorilo.tech/widget/govorilo-widget.js" async></script>
Pill-виджет
<govorilo-pill-call agent-id="YOUR_AGENT_ID"></govorilo-pill-call>
<script src="https://app.govorilo.tech/widget/govorilo-widget.js" async></script>
Инлайн-виджет
<govorilo-inline-call agent-id="YOUR_AGENT_ID"></govorilo-inline-call>
<script src="https://app.govorilo.tech/widget/govorilo-widget.js" async></script>
Плавающий Pill-виджет
<govorilo-pill-floating-call agent-id="YOUR_AGENT_ID"></govorilo-pill-floating-call>
<script src="https://app.govorilo.tech/widget/govorilo-widget.js" async></script>
Фреймворки и приложения
Виджеты — обычные веб-компоненты и работают в любом стеке. Несколько замечаний:
| Стек | Что сделать |
|---|---|
| React ниже 19 | Подключите скрипт виджета, как показано выше, — он сам регистрирует теги govorilo-…. React ниже 19 передаёт веб-компонентам только строки: объекты и обработчики событий задавайте через ref или оберните тег в createComponent из @lit/react с классом customElements.get('govorilo-…'). React 19 поддерживает веб-компоненты сам |
| Next.js | Загружайте виджет только на клиенте: dynamic(..., { ssr: false }) вокруг обёртки @lit/react |
| Vue | Подключите скрипт виджета, как показано выше, — теги govorilo-… зарегистрируются сами; используйте их в шаблоне (в compilerOptions.isCustomElement отметьте теги, начинающиеся с govorilo-) |
| WebView на iOS / Android | Приложение должно дать WebView доступ к микрофону и разрешить WebRTC, иначе звонок не начнётся |
Скрипт по адресу https://app.govorilo.tech/widget/govorilo-widget.js всегда последней версии: исправления и новые возможности приходят сами, код на сайте менять не нужно.
События виджета
Виджеты генерируют события, которые можно слушать в JavaScript:
| Событие | Описание |
|---|---|
status-changed | Срабатывает при смене статуса звонка |
connecting | Звонок устанавливается |
connected | Звонок активен |
idle | Нет активного звонка |
error | Произошла ошибка |
audio-track | Получен удалённый аудиотрек (для визуализации) |
local-audio-track | Локальный аудиотрек микрофона (для визуализации) |
microphone-enabled | Микрофон был включён |
microphone-disabled | Микрофон был выключен |
call-start | Срабатывает при успешном старте звонка |
call-end | Срабатывает при завершении звонка |
Пример: слушаем события
const widget = document.querySelector('govorilo-floating-call');
widget.addEventListener('call-start', () => {
console.log('Call started');
});
widget.addEventListener('call-end', () => {
console.log('Call ended');
});
Как разрешить агенту открывать страницы
Готовый код обработчика показан в блоке «Навигация по сайту» на вкладке «Виджеты». Во время звонка из браузера агент может попросить ваш сайт открыть страницу — «давайте покажу тарифы» — и посетитель видит это, не прерывая разговор.
Решение остаётся за вашей страницей. Агент присылает запрос, ваш код решает, что он означает. Открыть адрес, переключить вкладку, прокрутить к разделу и раскрыть аккордеон — всё это допустимые ответы.
Нужны три шага, и по отдельности ни один не работает.
Шаг 1 — Скажите агенту, какие страницы есть
Агент не видит карту вашего сайта. Он просит только тот путь, который вы ему дали, — перечислите их в промпте агента или в базе знаний:
Страницы сайта:
/pricing — тарифы и цены
/contact — форма связи и телефон
/product/crm — CRM
Пропустите этот шаг — и агенту нечего просить, он просто не попытается.
Шаг 2 — Обработайте запрос на своей странице
Добавьте это один раз, где угодно после скрипта виджета. Событие всплывает по документу, так что document — подходящее место:
<script>
document.addEventListener('agent-command', (event) => {
const { type, payload } = event.detail;
if (type === 'navigate') {
// SPA: переход без перезагрузки — звонок продолжается.
router.push(payload.path);
// Обычный сайт: открыть вторую вкладку, эта (и звонок) останется жива.
// window.open(payload.path, '_blank');
event.preventDefault(); // ← это и говорит агенту «принято»
}
});
</script>
Звонок живёт в этой странице. window.location.href = … выгружает документ, и разговор уходит вместе с ним — посетителя обрывает на полуслове. Если есть роутер, переходите без перезагрузки; иначе открывайте страницу в новой вкладке. Переподключения нет: перезагрузку не переживает ничто.
preventDefault() обязателенЭто единственный способ сказать «я обработал». Без него агенту сообщат, что сайт не поддерживает навигацию: он перестанет пытаться до конца звонка и вернётся к объяснениям, куда нажать. В консоли ничего не появится — страница просто выглядит так, будто проигнорировала запрос, потому что так и есть.
Шаг 3 — Проверьте
Позвоните своему агенту с сайта и попросите открыть страницу по названию. Должно произойти два события: страница открылась, и агент сказал что-то вроде «вот цены», а не «найдите их в меню».
Как отвечать агенту данными
Некоторые команды — это вопросы, а не указания. Приходят они так же, но отвечать нужно через respond():
| Команда | О чём спрашивает агент | Чем отвечаете |
|---|---|---|
navigate | «открой этот путь» | ничем — достаточно preventDefault() |
page_context | «что сейчас перед посетителем?» | чем угодно полезным: путь, заголовок, товар |
cart_state | «что у него в корзине?» | позиции, суммы, валюта |
<script>
document.addEventListener('agent-command', (event) => {
const { type, respond } = event.detail;
if (type === 'page_context') {
event.preventDefault();
respond({ path: location.pathname, title: document.title });
}
});
</script>
На ответ после preventDefault() есть около секунды, так что await допустим, а медленный запрос к API — нет. Отвечайте тем, что уже под рукой.
Что агент может просить, а что нет
- Только пути внутри вашего сайта. Путь обязан начинаться с
/. Всё, что уводит с домена —//evil.com,https://…, обратные слэши — отсекается до того, как дойдёт до страницы. Увести посетителя на чужой сайт агент не может. - Только звонки из браузера. На телефонном звонке открывать нечего, поэтому там таких команд нет.
- Одного отказа достаточно. Если страница не подтвердила первый запрос, агент перестаёт спрашивать до конца звонка. Он не будет пытаться снова и — что важнее — не скажет посетителю, что открыл то, чего не открывал.
Выводите каждую команду до того, как фильтруете: document.addEventListener('agent-command', e => console.log(e.detail)). Видите в консоли navigate — агент свою часть сделал, значит дело в preventDefault() или в вашем обработчике. Не видите ничего — агенту не сказали, что такой путь существует; вернитесь к шагу 1.
Опознание вошедшего пользователя
Если посетители входят на вашем сайте в личный кабинет, агент может работать с их аккаунтом — заказами, записями, обращениями — через ваши MCP-серверы. Сам виджет не знает, кто вошёл: это сообщает ему ваша страница с помощью токена.
Как это работает
- Ваш сервер выдаёт отдельный короткоживущий токен (не дольше 15 минут, audience — ваш MCP-сервер), а страница передаёт его в атрибут виджета
support-session-token. Атрибут работает в виджетах Floating, Pill, Pill Floating и Inline; виджет обратного звонка (Callback) его не поддерживает. - Говорило передаёт токен без изменений каждому MCP-подключению агента в HTTP-заголовке
X-Govorilo-Support-Token. - Говорило токен не разбирает, не проверяет и не хранит. Подпись и срок действия проверяет ваш MCP-сервер — он и решает, кто этот пользователь.
Этот заголовок получает каждое MCP-подключение агента. Никогда не передавайте токен сессии или cookie вашего сайта.
Если на странице никто не вошёл или атрибут не задан, заголовок просто не приходит — а инструменты ваших MCP-подключений агенту всё равно доступны. Поэтому MCP-сервер должен решать сам: если X-Govorilo-Support-Token нет или токен не прошёл проверку, отклоняйте любой запрос к данным аккаунта — возвращайте ошибку или только те данные, которые и так общедоступны.
Какой токен передавать
Не передавайте то, что посетитель может угадать или подставить от чужого имени, — например, открытый ID пользователя. Передавайте то, что может подтвердить только выдавший токен, и на своём сервере спрашивайте у него, кто этот пользователь.
| Подход | Можно подделать? | Оценка |
|---|---|---|
| Открытый идентификатор пользователя | Тривиально | Недопустимо |
| Зашифрованный идентификатор | Нет, но его можно повторять бесконечно | Недопустимо |
| Токен вашего провайдера входа (JWT) | Нет, но его получает каждое MCP-подключение и может повторить | Недопустимо |
| Разовый короткоживущий токен, выданный вашим сервером | Нельзя, и повторно предъявить тоже | Лучший вариант |
Встраивание виджета с токеном
Подставьте токен в страницу на своём сервере:
<govorilo-floating-call
agent-id="YOUR_AGENT_ID"
support-session-token="{{ support_token }}"
></govorilo-floating-call>
<script src="https://app.govorilo.tech/widget/govorilo-widget.js" async></script>
Или задайте его из JavaScript после входа пользователя, до начала звонка:
const res = await fetch('/api/support-token'); // ваш эндпоинт, выдаёт короткоживущий токен
const { token } = await res.json();
document
.querySelector('govorilo-floating-call')
.setAttribute('support-session-token', token);
В приложении для iOS или Android загружайте ту же страницу в WebView после входа, чтобы атрибут уже содержал токен пользователя.
Проверка токена в MCP-сервере
Набросок на Node.js с библиотекой jose: инструмент с данными аккаунта читает заголовок, проверяет токен, выданный вашим сервером, — подпись, issuer, audience и срок действия — и отказывает, если заголовка нет или токен неверный. Как обработчик инструмента получает HTTP-запрос, зависит от вашего MCP-фреймворка.
import { createRemoteJWKSet, jwtVerify } from 'jose';
// Публичные ключи вашего сервера, который выдаёт токены поддержки
const JWKS = createRemoteJWKSet(new URL('https://example.com/.well-known/jwks.json'));
async function signedInUser(headers: Record<string, string | undefined>): Promise<string | null> {
const token = headers['x-govorilo-support-token'];
if (!token) return null; // на странице никто не вошёл
try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: 'https://example.com/',
audience: 'https://mcp.example.com/',
});
return payload.sub ?? null; // это подписал ваш сервер, значению можно доверять
} catch {
return null; // неверная подпись или истёк срок
}
}
// MCP-инструмент, который отдаёт данные аккаунта
async function getMyOrders(headers: Record<string, string | undefined>) {
const userId = await signedInUser(headers);
if (!userId) {
return {
isError: true,
content: [{ type: 'text', text: 'Собеседник не вошёл в аккаунт. Данные аккаунта недоступны.' }],
};
}
const orders = await db.orders.findByUser(userId);
return { content: [{ type: 'text', text: JSON.stringify(orders) }] };
}
Если вы выдаёте разовые токены сами, замените jwtVerify поиском в своём хранилище: найдите токен, проверьте, что он не истёк и не использован, отметьте его использованным и возьмите пользователя из этой записи.
Технические требования
Виджеты требуют, чтобы браузер посетителя поддерживал:
- WebGL 2.0 — для отрисовки
- Web Audio API — для обработки звука
- WebRTC — для реал-тайм голосовой связи
Все современные браузеры (Chrome, Firefox, Safari, Edge) поддерживают эти технологии.
Ограничения доменов
Контролируйте, какие сайты могут встраивать виджет вашего агента.
Всегда разрешённые домены
Следующие домены всегда разрешены, независимо от конфигурации:
govorilo.tech(и поддомены)localhost
Allow All Domains
По умолчанию ваш виджет можно встраивать на любой сайт. Переключите «Allow all domains» в настройках виджета, чтобы ограничить это.
Ограничение конкретными доменами
При ограничении добавьте каждый домен, которому должно быть разрешено встраивание:
- Вводите имена доменов без
https://(например,example.com) - Поддомены нужно добавлять отдельными записями (например,
www.example.com,shop.example.com) - Порты можно указывать (например,
localhost:3000) - Максимум 50 доменов можно добавить в whitelist
Список не даёт другим сайтам встраивать ваш виджет в браузере, но это не контроль доступа: скрипт вне браузера всё равно может обратиться к агенту. Не полагайтесь на него для защиты данных — давайте агенту только те инструменты и знания, которые можно анонимному посетителю, и пусть ваши MCP-серверы проверяют, кто звонит, — см. Опознание вошедшего пользователя.
Условия использования
Включите диалог согласия до того, как звонящие смогут начать разговор.
Конфигурация
| Настройка | Описание |
|---|---|
| Enable Terms | Переключатель диалога terms |
| Terms Content | Markdown-форматированный текст согласия, показываемый пользователям (макс. 5000 символов) |
| URL условий | Ссылка на вашу полную страницу с условиями использования |
| URL политики конфиденциальности | Ссылка на вашу страницу с политикой конфиденциальности |
Когда включено:
- Пользователи видят диалог согласия до старта звонка
- Они должны нажать «Agree», чтобы продолжить
- Согласие хранится локально в браузере
- Кнопка Reset Consent очищает сохранённое согласие для тестирования
Форматирование контента
Контент terms поддерживает форматирование Markdown:
- Используйте
####для заголовков - Используйте
**bold**для акцента - Используйте переводы строк для читаемости
Что виджет хранит в браузере
Виджет держит несколько небольших значений в localStorage посетителя:
| Ключ | Что хранит |
|---|---|
user_identity | Случайный ID браузера посетителя, чтобы повторные звонки из того же браузера узнавались |
privacy_accepted | Что посетитель принял условия использования — окно больше не показывается |
govorilo_callback_timestamps | Когда посетитель запрашивал обратные звонки — для ограничения частоты в форме обратного звонка |
Содержимое звонков в браузере не хранится.
Связанное
- Виджет обратного звонка — телефонные обратные звонки для посетителей, которым не хочется говорить в браузере
- Обзор голосовых агентов
- Настройки — настройки агента, включая конфигурацию виджета
- Интеграции — API-ключи и настройка телефонных номеров