П'ять вебкомпонентів, які дають відвідувачу поговорити з вашим агентом — голосом, у браузері, нічого не встановлюючи. Розмова йде через 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, видно відвідувачу.
Візьміть ідентифікатор агента
Рядок на кшталт 69d20781de6244c89509eb08, з кабінету.
Підключіть скрипт
Один рядок у <head>. Через npm це один імпорт — теги реєструються самі.
Поставте тег
Там, де потрібен дзвінок. Обов'язковий лише agent-id.
Віддавайте через HTTPS
На http:// браузер не дасть мікрофон. Поки розробляєте, підійде localhost.
<!-- 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 navigate Агент відкриває сторінку у вас на сайті — товар, розділ, заздалегідь заповнену форму.
page_context Сторінка відповідає, де перебуває відвідувач і що показано, і агент говорить про цей товар, а не взагалі.
cart_state Сторінка відповідає, що лежить у кошику і скільки коштує, і агент називає суму вголос.
Кожна команда приходить скасовуваною DOM-подією agent-command. Виклик preventDefault() означає «обробив», respond(data) надсилає відповідь назад агентові. Проігноруєте подію — агент дізнається, що команду не виконали, і повернеться до розмови.
Команда — це прохання, а не наказ: перевіряйте кожен шлях, перш ніж ним іти. У прикладі відкидається все, що не є шляхом усередині сайту.
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 видайте запит сторінки в делегаті.
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-сервер вирішує, хто це. Для нас токен непрозорий: ми його не розбираємо, не перевіряємо і не зберігаємо.
Ніколи не довіряйте ідентифікатору, який прийшов усередині токена як заява. Запитайте в того, хто видав токен, хто це, і працюйте з відповіддю.
| Підхід | Підробити | Оцінка |
|---|---|---|
| Відкритий ідентифікатор | Тривіально | Неприпустимо |
| Зашифрований ідентифікатор | Не можна, але можна пред'явити повторно | Половина рішення |
| Токен вашого провайдера входу | Не можна: підпис ставить провайдер | Робочий варіант |
| Разовий серверний талон | Не можна, і перевикористати теж | Найкращий варіант |
Увімкніть на підключенні «потрібен упізнаний співрозмовник» — і на анонімному дзвінку ваші інструменти взагалі не піднімуться: агент не розповість про чужий акаунт, навіть якщо у вашому коді колись з'явиться помилка.
Дзвінок без токена не отримує доступу до акаунта взагалі. Це поведінка за замовчуванням, а не налаштування.
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 Одинадцять тем, світла й темна за системним налаштуванням або примусово, і ще близько п'ятнадцяти атрибутів для самої сфери.
| Атрибут | Що задає | За замовчуванням |
|---|---|---|
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 секунд. Без картки. Скасуйте будь-коли. Просто телефон, який сам піднімає слухавку.