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

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

Нажмите, чтобы говорить

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

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

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

Разговор идёт по WebRTC, ответ приходит примерно за секунду, а агент умеет открывать страницы и видеть, что на экране.

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

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

    <hanc-ai-inline-call>

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

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

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

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

    <hanc-ai-floating-call>

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

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

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

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

    <hanc-ai-pill-call>

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

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

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

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

    <hanc-ai-pill-floating-call>

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

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

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

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

    <hanc-ai-callback>

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

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

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

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

  1. Возьмите идентификатор агента Строка вида 69d20781de6244c89509eb08, из кабинета.
  2. Подключите скрипт Одна строка в <head>. Через npm это один импорт, теги регистрируются сами.
  3. Поставьте тег Там, где нужен звонок. Обязателен только agent-id.
  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>
  • React

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

  • Next.js

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

  • Vue

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

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

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

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-базой, без защищённого источника движок не выдаст странице микрофон.

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

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

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

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

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

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

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"))
Работает в приложении RunOrJog
Работает в приложении RunOrJog

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

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

  1. Ваше приложение
  2. Виджет
  3. Hanc.AI
  4. Ваш MCP
  5. Ваши данные

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

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

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

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

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

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

  • default
  • emerald
  • rose
  • amber
  • cyan
  • purple
  • blue
  • white
  • black
  • tangerine
  • ember
Он должен выглядеть как ваш сайт, а не как наш
Атрибут Что задаёт По умолчанию
agent-idКакой агент отвечает. Единственный обязательный
themeОдна из 11 цветовых темdefault
sizeРазмер в пикселях; на узких экранах уменьшается сам370
button-start-textНадпись на кнопкеTry to call
positionУгол для плавающих формbottom-right
glow-intensityСила свечения, 0-20.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

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

Шесть AI-сотрудников Hanc в гарнитурах стоят вместе под оранжево-розовым кольцом света