Cinco componentes web que permiten a un visitante hablar con tu agente — en voz alta, en el navegador, sin instalar nada. La conversación va por WebRTC, la respuesta llega en aproximadamente un segundo, y el agente puede abrir páginas y leer lo que hay en pantalla.
Esta esfera es el widget. Púlsala y habla.
El mismo agente, cinco maneras de llegar a él. Mézclalas libremente — dos etiquetas con un mismo agent-id es un montaje normal.
Esfera en línea
370 px por defecto, dentro del flujo de la página. Dale el ancho que quieras; en pantalla estrecha se reduce sola.
<hanc-ai-inline-call> Una sección de una landing, una página de contacto — donde la llamada sea el sentido del bloque.
Esfera flotante
120 px, fijada a 32 px de la esquina inferior derecha. Otras tres esquinas a elegir, o static para meterla en el flujo.
<hanc-ai-floating-call> Accesible desde cualquier página sin ocupar sitio en la maquetación.
Píldora
Un botón horizontal: a la izquierda una esfera de 48 px, a la derecha tu propia etiqueta. Va en el flujo como cualquier botón.
<hanc-ai-pill-call> En una fila de botones, en la cabecera, dentro de una tarjeta.
Píldora flotante
El mismo botón, fijado a 32 px de la esquina inferior derecha. Las mismas cuatro esquinas a elegir.
<hanc-ai-pill-floating-call> Cuando una esfera redonda llama demasiado la atención pero la quieres siempre a mano.
Formulario de rellamada
Selector de país, número formateado a medida que se escribe, y un botón. Cuántos reintentos hacer se configura en el agente, no en el marcado.
<hanc-ai-callback> Para quien no va a hablar por el navegador — y para cualquier sitio donde un micrófono resulta incómodo.
Un solo atributo obligatorio: el id del agente, desde tu panel. Ninguna clave de API va en el marcado — todo lo que está en el HTML lo ve el visitante.
Toma el id del agente
Una cadena como 69d20781de6244c89509eb08, desde el panel.
Añade el script
Una línea en <head>. Por npm es un solo import — las etiquetas se registran solas.
Coloca la etiqueta
Donde corresponda la llamada. Solo agent-id es obligatorio.
Sírvelo por HTTPS
Los navegadores no dan micrófono en http://. Mientras desarrollas, localhost funciona.
<!-- 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
Por debajo de la v19 las props desconocidas se pasan como cadenas, así que los objetos no se pueden entregar por ahí. Envuélvelo con @lit/react — la vía oficial, y además te da tipado y eventos.
Next.js
El widget necesita un navegador: 'use client' y un import dinámico con ssr: false. Si no, la compilación se rompe en el renderizado de servidor.
Vue
Funciona sin envoltorio. Dile al empaquetador que las etiquetas hanc-ai- son elementos personalizados.
Una respuesta hablada a menudo es el medio equivocado — nadie quiere que le lean una tabla de envíos. Por eso el agente puede enviar un comando a la página y preguntarle qué está mirando el visitante. Tu código sigue al mando: decide qué atiende.
navigate page_context cart_state navigate El agente abre una página de tu sitio — un producto, una sección, un formulario ya rellenado.
page_context La página responde dónde está el visitante y qué se muestra, para que el agente hable de este producto y no en general.
cart_state La página responde qué hay en el carrito y cuánto cuesta, para que el agente sume en voz alta.
Cada comando llega como un evento DOM cancelable agent-command. Llamar a preventDefault() significa «atendido»; respond(data) devuelve una respuesta al agente. Si ignoras el evento, al agente se le informa de que no se atendió y vuelve a responder hablando.
Los comandos son peticiones, no órdenes: valida cada ruta antes de seguirla. El ejemplo rechaza todo lo que no sea una ruta del propio sitio.
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',
});
}
}); En producción en
milotec.com.ua
El agente lleva al visitante hasta un producto, le cuenta qué hay en la página en la que está y le lee el carrito.
iOS y Android lo muestran en una vista web. Construye una pequeña página HTML en código y cárgala con una base HTTPS — sin un origen seguro el motor no entrega micrófono a la página.
No se oye al agente
Una vista web no reproduce audio WebRTC mientras la sesión de audio de la app está en modo reproducción. Cámbiala a un modo conversacional con salida por altavoz mientras dure la llamada y devuélvela después.
El agente responde en el idioma equivocado
El widget toma el idioma de navigator.language, que en una vista web es el del sistema, no el elegido en tu app. Sobrescríbelo al inicio del documento — y parchea fetch para añadir browser_language a la petición de sala, que el widget incrustado no envía por sí mismo.
El micrófono necesita las dos mitades
El permiso del sistema para la app y el permiso para la página dentro de la vista web. En Android pide el del sistema antes de abrir la pantalla; en iOS concede la petición de la página en el delegado.
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")) Pasa un token en support-session-token. El widget lo adjunta a la llamada tal cual, nosotros lo reenviamos a tu sistema en la cabecera X-Hanc-Support-Token, y tu servidor MCP decide quién es. El token nos resulta opaco: no lo analizamos, ni lo verificamos, ni lo guardamos.
Nunca confíes en un identificador que llega dentro de un token como afirmación. Pregunta a quien emitió el token quién es, y trabaja con la respuesta.
| Enfoque | Falsificable | Veredicto |
|---|---|---|
| Un id de usuario en claro | Trivialmente | Inaceptable |
| Un id de usuario cifrado | No, pero se puede reutilizar | Media solución |
| El token de tu proveedor de acceso | No: lo firma el proveedor | Funciona |
| Un vale de servidor de un solo uso | No, y tampoco se puede reutilizar | Lo mejor |
Activa «requiere interlocutor identificado» en la conexión y una llamada anónima no levantará tus herramientas en absoluto — el agente no podrá hablar de la cuenta de otro ni aunque más adelante aparezca un fallo en tu código.
Una llamada sin token no obtiene acceso alguno a la cuenta. Es el comportamiento por defecto, no una opción.
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 Once temas, claro y oscuro según el ajuste del sistema o forzados, y alrededor de quince atributos más para la propia esfera.
| Atributo | Qué define | Por defecto |
|---|---|---|
agent-id | Qué agente responde. El único obligatorio | — |
theme | Uno de los once temas de color | default |
size | Tamaño en píxeles; se reduce solo en pantallas estrechas | 370 |
button-start-text | El texto del botón | Try to call |
position | Esquina para las formas flotantes | bottom-right |
glow-intensity | Intensidad del brillo, 0–2 | 0.8 |
audio-reactivity | Cuánto responde la esfera a la voz | 3.0 |
terms-enabled | Panel de consentimiento antes de la llamada | false |
sound-enabled | Sonidos de inicio y fin | true |
support-session-token | El token del usuario identificado | — |
Eventos para tu propio código
Ninguno de los dos eventos lleva datos: ambos son un Event simple, así que obtienes el hecho y el momento. En ninguno hay contenido de la conversación — las transcripciones y grabaciones viven en el panel y en la API.
El canal está cifrado
Peticiones de control por HTTPS, audio por WSS y SRTP. Sin HTTPS el navegador no da micrófono, de entrada.
Se comprueba el agente
Solo se levanta una llamada para un id de agente que existe.
El acceso está limitado por tasa
La entrega de acceso a la conversación está limitada, contra fuerza bruta y abuso.
El token de sesión no se queda
Se lee de nuevo en cada llamada y nunca se escribe en nuestra base de datos.
La lista de dominios permitidos no es una frontera de seguridad
Esa comprobación se ejecuta en el navegador. Sirve para que el widget no arranque en una copia perdida de tu página, pero no impide llamar al servicio directamente. Trata como público todo lo que el agente le contaría a un interlocutor anónimo, y protege el resto con un token de sesión.
Qué se guarda y dónde
En el navegador
Dos entradas en localStorage: una marca de visitante y el hecho del consentimiento
Audio
En streaming. Se graba solo si la grabación está activada para el agente
Transcripción
Se guarda con la grabación y es legible en el panel
Ubicación
Procesamiento y almacenamiento en la UE
Sin HTTPS no hay micrófono
Prueba en localhost o en un dominio seguro
Safari quiere un gesto antes del audio
La llamada tiene que arrancar con un clic
Bloqueadores y proxies pueden cortar WebRTC
Deja el formulario de rellamada como alternativa
Una ventana privada no guarda la marca de visitante
Cada conversación parece la primera — es normal
React por debajo de 19 no pasa objetos como atributos
Envuélvelo con @lit/react
El renderizado de servidor se rompe con un import directo
Import dinámico con ssr: false
Empiece gratis en 60 segundos. Sin tarjeta. Cancele cuando quiera. El teléfono se contesta solo.