Віджети — 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 браузер і так не дасть мікрофон.

Агента перевіряють

З'єднання піднімається лише для наявного ідентифікатора агента.

Частоту обмежено

Видача доступу до розмови обмежена — від перебору та накрутки.

Токен сесії не осідає

Читається наново на кожен дзвінок і в нашу базу не пишеться.

Список дозволених доменів не є межею безпеки

Ця перевірка виконується в браузері. Вона зручна, щоб віджет не запустився через недогляд на чужій копії вашої сторінки, але не заважає звернутися до сервісу напряму. Вважайте публічним усе, що агент розповість анонімному співрозмовнику, а решту закривайте токеном сесії.

Що і де залишається

У браузері

Два записи в localStorage: мітка відвідувача і факт згоди

Звук

Потоком. Записується, лише якщо запис увімкнено в агента

Розшифровка

Зберігається разом із записом, доступна в кабінеті

Розміщення

Обробка і зберігання в ЄС

Обмеження, про які краще знати заздалегідь

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

Перевіряйте на localhost або захищеному домені

Safari вимагає жесту перед звуком

Дзвінок має починатися з натискання

Блокувальники та проксі можуть різати WebRTC

Залиште форму зворотного дзвінка як запасний шлях

У приватному вікні мітка не зберігається

Кожна розмова виглядає як перша — це нормально

React нижче 19 не передає об'єкти атрибутом

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

Серверний рендеринг ламається на прямому імпорті

Динамічний імпорт з ssr: false

Ваш телефон, який нарешті
працює на вас.

Почніть безкоштовно за 60 секунд. Без картки. Скасуйте будь-коли. Просто телефон, який сам піднімає слухавку.

Почати безкоштовно
Book a Meeting