Widgets — Hanc.AI
5 formas1 etiqueta de script14 idiomas

Un agente de voz en tu web.
Una etiqueta, sin backend.

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.

Cinco formas

Elige la forma que necesita la página

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

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

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

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

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

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.

Cinco minutos

Instalarlo

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.

  1. 1

    Toma el id del agente

    Una cadena como 69d20781de6244c89509eb08, desde el panel.

  2. 2

    Añade el script

    Una línea en <head>. Por npm es un solo import — las etiquetas se registran solas.

  3. 3

    Coloca la etiqueta

    Donde corresponda la llamada. Solo agent-id es obligatorio.

  4. 4

    Sírvelo por HTTPS

    Los navegadores no dan micrófono en http://. Mientras desarrollas, localhost funciona.

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

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.

Conduce la página

El agente puede abrir páginas y leer lo que hay en pantalla

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.

Hanc.AI
navigate
page_context
cart_state
El widget
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.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',
    });
  }
});

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.

Apps móviles

El mismo widget dentro de tu app

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.

El mismo widget dentro de tu app
En producción en la app 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"))
Quién está en la llamada

Deja que el agente vea al usuario identificado — y solo a él

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.

Tu app
El widget
Hanc.AI
Tu MCP
Tus datos

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.

EnfoqueFalsificableVeredicto
Un id de usuario en claroTrivialmenteInaceptable
Un id de usuario cifradoNo, pero se puede reutilizarMedia solución
El token de tu proveedor de accesoNo: lo firma el proveedorFunciona
Un vale de servidor de un solo usoNo, y tampoco se puede reutilizarLo 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.

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
Aspecto

Debe parecerse a tu web, no a la nuestra

Once temas, claro y oscuro según el ajuste del sistema o forzados, y alrededor de quince atributos más para la propia esfera.

default
emerald
rose
amber
cyan
purple
blue
white
black
tangerine
ember
AtributoQué definePor 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.

Respuestas claras

Qué te protege y qué no

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

Límites que conviene conocer antes de empezar

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

Su teléfono, por fin
trabajando para usted.

Empiece gratis en 60 segundos. Sin tarjeta. Cancele cuando quiera. El teléfono se contesta solo.

Empezar gratis
Book a Meeting