Виджеты — Hanc.AI
5 форм1 тег скрипта14 языков

Голосовой агент на вашем сайте.
Один тег, без бэкенда.

Пять веб-компонентов, которые дают посетителю поговорить с вашим агентом — голосом, в браузере, ничего не устанавливая. Разговор идёт по WebRTC, ответ приходит примерно за секунду, а агент умеет открывать страницы и видеть, что на экране.

Эта сфера и есть виджет. Нажмите и говорите.

Пять форм

Выберите ту форму, которая нужна странице

Один агент, пять способов до него добраться. Смешивайте свободно — два тега с одним agent-id это обычная схема.

Сфера в потоке

Сфера в потоке

370 px по умолчанию, стоит прямо в потоке страницы. Задайте нужную ширину; на узком экране уменьшается сама.

<hanc-ai-inline-call>

Секция на посадочной, страница «Связаться» — там, где разговор и есть смысл блока.

Плавающая сфера

Плавающая сфера

120 px, прибита в 32 px от правого нижнего угла. Ещё три угла на выбор или static, чтобы поставить её в поток.

<hanc-ai-floating-call>

Доступна с любой страницы и не занимает места в вёрстке.

Кнопка-пилюля

Кнопка-пилюля

Горизонтальная кнопка: слева сфера 48 px, справа ваша подпись. Стоит в потоке, как любая другая кнопка.

<hanc-ai-pill-call>

В ряду кнопок, в шапке, внутри карточки.

Плавающая пилюля

Плавающая пилюля

Та же кнопка, прибитая в 32 px от правого нижнего угла. Те же четыре угла на выбор.

<hanc-ai-pill-floating-call>

Когда круглая сфера слишком заметна, но нужна под рукой на каждой странице.

Форма обратного звонка

Форма обратного звонка

Выбор страны, номер форматируется по мере ввода, одна кнопка. Сколько раз перезванивать — настраивается на агенте, а не в разметке.

<hanc-ai-callback>

Для тех, кто не станет говорить через браузер, и для мест, где микрофон неуместен.

Пять минут

Как поставить

Обязательный атрибут один: идентификатор агента из кабинета. Ключам API в разметке не место — всё, что попадает в HTML, видно посетителю.

  1. 1

    Возьмите идентификатор агента

    Строка вида 69d20781de6244c89509eb08, из кабинета.

  2. 2

    Подключите скрипт

    Одна строка в <head>. Через npm это один импорт — теги регистрируются сами.

  3. 3

    Поставьте тег

    Там, где нужен звонок. Обязателен только agent-id.

  4. 4

    Отдавайте по HTTPS

    На http:// браузер не даст микрофон. Пока разрабатываете, подойдёт localhost.

index.html
<!-- 1. the script, once, in <head> -->
<script src="https://unpkg.com/hanc-webrtc-widgets" async></script>

<!-- 2. the widget, wherever the call belongs -->
<hanc-ai-inline-call
  agent-id="69d20781de6244c89509eb08"
  size="320"
  theme="tangerine"
  button-start-text="Talk to us">
</hanc-ai-inline-call>
1 script tag 0 dependencies WebRTC

React

До 19-й версии неизвестные пропсы передаются строками, поэтому объекты так не отдать. Оберните через @lit/react — официальный способ, заодно даёт типизацию и события.

Next.js

Виджету нужен браузер: 'use client' и динамический импорт с ssr: false. Иначе сборка упадёт на серверном рендеринге.

Vue

Работает без обёрток. Скажите сборщику, что теги с префиксом hanc-ai- — пользовательские элементы.

Он управляет страницей

Агент умеет открывать страницы и видеть, что на экране

Голосом отвечать удобно не всегда — таблицу доставки вслух никто слушать не станет. Поэтому агент может отправить странице команду и спросить у неё, что сейчас смотрит посетитель. Решаете при этом вы: ваш код сам выбирает, что выполнять.

Hanc.AI
navigate
page_context
cart_state
Виджет
navigate

Агент открывает страницу у вас на сайте — товар, раздел, заранее заполненную форму.

page_context

Страница отвечает, где находится посетитель и что показано, и агент говорит про этот товар, а не вообще.

cart_state

Страница отвечает, что лежит в корзине и сколько стоит, и агент называет сумму вслух.

Каждая команда приходит отменяемым DOM-событием agent-command. Вызов preventDefault() означает «обработал», respond(data) отправляет ответ обратно агенту. Проигнорируете событие — агент узнает, что команду не выполнили, и вернётся к разговору.

Команда — это просьба, а не приказ: проверяйте каждый путь, прежде чем по нему пойти. В примере отвергается всё, что не является путём внутри сайта.

widget.js
widget.addEventListener('agent-command', (event) => {
  const command = event.detail;

  if (command.type === 'navigate') {
    const path = command.payload?.path;
    // Only same-site paths. Never follow an absolute URL.
    if (!path?.startsWith('/') || path.includes('://')) return;
    event.preventDefault();          // "handled" — the ACK protocol
    router.push(path);
  }

  if (command.type === 'cart_state') {
    event.preventDefault();
    command.respond({                // answer travels back to the agent
      items: cart.map(i => ({ name: i.name, price: i.price })),
      total: cart.total,
      currency: 'UAH',
    });
  }
});

Работает на

milotec.com.ua

Агент доводит посетителя до товара, рассказывает, что на текущей странице, и зачитывает корзину.

Мобильные приложения

Тот же виджет внутри вашего приложения

И iOS, и Android показывают его в веб-вью. Соберите маленькую HTML-страницу в коде и загрузите её с HTTPS-базой — без защищённого источника движок не выдаст странице микрофон.

Собеседника не слышно

Веб-вью не выведет звук WebRTC, пока аудиосессия приложения стоит в режиме воспроизведения. Переключите её на разговорный режим с выводом в динамик на время звонка и верните обратно при закрытии.

Агент отвечает не на том языке

Виджет берёт язык из navigator.language, а в веб-вью это язык системы, а не выбранный в приложении. Подмените его на старте документа — и пропатчите fetch, дописав browser_language в запрос комнаты: встраиваемый виджет сам это поле не шлёт.

Микрофону нужны обе половины

Системное разрешение приложению и разрешение странице внутри веб-вью. На Android спрашивайте системное до открытия экрана, на iOS выдайте запрос страницы в делегате.

Тот же виджет внутри вашего приложения
Работает в приложении RunOrJog
SupportView.swift
let html = """
<hanc-ai-inline-call
    agent-id="\(agentId)"
    support-session-token="\(sessionToken)"></hanc-ai-inline-call>
<script src="https://unpkg.com/hanc-webrtc-widgets" async></script>
"""

// An HTTPS base is required — without a secure origin
// the engine will not hand the page a microphone.
webView.loadHTMLString(html, baseURL: URL(string: "https://hanc.ai"))
Кто на звонке

Дайте агенту увидеть вошедшего пользователя — и только его

Передайте токен в support-session-token. Виджет прикладывает его к звонку как есть, мы отдаём его вашей системе заголовком X-Hanc-Support-Token, а ваш MCP-сервер решает, кто это. Для нас токен непрозрачен: мы его не разбираем, не проверяем и не храним.

Ваше приложение
Виджет
Hanc.AI
Ваш MCP
Ваши данные

Никогда не доверяйте идентификатору, который пришёл внутри токена как заявление. Спросите у того, кто выдал токен, кто это, и работайте с ответом.

ПодходПодделатьОценка
Открытый идентификаторТривиальноНедопустимо
Зашифрованный идентификаторНельзя, но можно предъявить повторноПоловина решения
Токен вашего провайдера входаНельзя: подпись ставит провайдерРабочий вариант
Разовый серверный талонНельзя, и переиспользовать тожеЛучший вариант

Включите на подключении «требуется опознанный собеседник» — и на анонимном звонке ваши инструменты вообще не поднимутся: агент не расскажет про чужой аккаунт, даже если в вашем коде однажды появится ошибка.

Звонок без токена не получает доступа к аккаунту вовсе. Это поведение по умолчанию, а не настройка.

your_mcp_server.py
async def verified_user_id(ctx) -> str:
    token = ctx.request.headers.get("x-hanc-support-token", "").strip()
    if not token:
        raise ValueError("No signed-in user on this call.")

    # Ask the issuer who this is. Never read the id out of the token.
    resp = await http.get(f"{AUTH_URL}/auth/v1/user",
                          headers={"Authorization": f"Bearer {token}"})
    if resp.status_code != 200:
        raise ValueError("Token invalid or expired.")

    return resp.json()["id"]        # ← the trusted identity
Внешний вид

Он должен выглядеть как ваш сайт, а не как наш

Одиннадцать тем, светлая и тёмная по системной настройке или принудительно, и ещё около пятнадцати атрибутов для самой сферы.

default
emerald
rose
amber
cyan
purple
blue
white
black
tangerine
ember
АтрибутЧто задаётПо умолчанию
agent-id Какой агент отвечает. Единственный обязательный
theme Одна из одиннадцати цветовых тем default
size Размер в пикселях; на узких экранах уменьшается сам 370
button-start-text Надпись на кнопке Try to call
position Угол для плавающих форм bottom-right
glow-intensity Сила свечения, 0–2 0.8
audio-reactivity Насколько сфера отзывается на голос 3.0
terms-enabled Окно согласия перед звонком false
sound-enabled Звуки начала и конца true
support-session-token Токен вошедшего пользователя

События для вашего кода

Ни одно из событий не несёт данных: оба — обычный Event, то есть вы получаете сам факт и момент времени. Содержимого разговора нет ни в том, ни в другом — расшифровки и записи живут в кабинете и в API.

Честно

Что защищает, а что защитой не является

Канал зашифрован

Управляющие запросы по HTTPS, звук по WSS и SRTP. Без HTTPS браузер и так не даст микрофон.

Агент проверяется

Соединение поднимается только для существующего идентификатора агента.

Частота ограничена

Выдача доступа к разговору throttled — от перебора и накрутки.

Токен сессии не оседает

Читается заново на каждый звонок и в нашу базу не пишется.

Список разрешённых доменов не является границей безопасности

Эта проверка выполняется в браузере. Она удобна, чтобы виджет не запустился по недосмотру на чужой копии вашей страницы, но не мешает обратиться к сервису напрямую. Считайте публичным всё, что агент расскажет анонимному собеседнику, а остальное закрывайте токеном сессии.

Что и где остаётся

В браузере

Две записи в localStorage: метка посетителя и факт согласия

Звук

Потоком. Записывается, только если запись включена у агента

Расшифровка

Сохраняется вместе с записью, доступна в кабинете

Размещение

Обработка и хранение в ЕС

Ограничения, о которых лучше знать заранее

Без HTTPS микрофона не будет

Проверяйте на localhost или защищённом домене

Safari требует жеста перед звуком

Звонок должен начинаться по нажатию

Блокировщики и прокси могут резать WebRTC

Оставьте форму обратного звонка как запасной путь

В приватном окне метка не сохраняется

Каждый разговор выглядит как первый — это нормально

React ниже 19 не передаёт объекты атрибутом

Обёртка через @lit/react

Серверный рендеринг ломается на прямом импорте

Динамический импорт с ssr: false

Ваш телефон, который наконец
работает на вас.

Начните бесплатно за 60 секунд. Без карты. Отмена в любой момент. Просто телефон, который сам берёт трубку.

Начать бесплатно
Book a Meeting