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.

Toque para hablar

Esta esfera es el widget. Púlsala y habla.

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.

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.

  • Esfera en línea

    Esfera en línea

    <hanc-ai-inline-call>

    370 px por defecto, dentro del flujo de la página. Dale el ancho que quieras; en pantalla estrecha se reduce sola.

    Una sección de una landing, una página de contacto, donde la llamada sea el sentido del bloque.

  • Esfera flotante

    Esfera flotante

    <hanc-ai-floating-call>

    120 px, fijada a 32 px de la esquina inferior derecha. Otras tres esquinas a elegir, o static para meterla en el flujo.

    Accesible desde cualquier página sin ocupar sitio en la maquetación.

  • Píldora

    Píldora

    <hanc-ai-pill-call>

    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.

    En una fila de botones, en la cabecera, dentro de una tarjeta.

  • Píldora flotante

    Píldora flotante

    <hanc-ai-pill-floating-call>

    El mismo botón, fijado a 32 px de la esquina inferior derecha. Las mismas cuatro esquinas a elegir.

    Cuando una esfera redonda llama demasiado la atención pero la quieres siempre a mano.

  • Formulario de rellamada

    Formulario de rellamada

    <hanc-ai-callback>

    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.

    Para quien no va a hablar por el navegador, y para cualquier sitio donde un micrófono resulta incómodo.

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. Toma el id del agente Una cadena como 69d20781de6244c89509eb08, desde el panel.
  2. Añade el script Una línea en <head>. Por npm es un solo import, las etiquetas se registran solas.
  3. Coloca la etiqueta Donde corresponda la llamada. Solo agent-id es obligatorio.
  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>
  • 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.

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.

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.

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.

  1. 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.

  2. 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.

  3. 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.

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"))
En producción en la app RunOrJog
En producción en la app RunOrJog

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.

  1. Tu app
  2. El widget
  3. Hanc.AI
  4. Tu MCP
  5. 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.

Deja que el agente vea al usuario identificado, y solo a él
Enfoque Falsificable Veredicto
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
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

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.

Debe parecerse a tu web, no a la nuestra

11 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
Debe parecerse a tu web, no a la nuestra
Atributo Qué define Por defecto
agent-idQué agente responde. El único obligatorio
themeUno de los 11 temas de colordefault
sizeTamaño en píxeles; se reduce solo en pantallas estrechas370
button-start-textEl texto del botónTry to call
positionEsquina para las formas flotantesbottom-right
glow-intensityIntensidad del brillo, 0-20.8
audio-reactivityCuánto responde la esfera a la voz3.0
terms-enabledPanel de consentimiento antes de la llamadafalse
sound-enabledSonidos de inicio y fintrue
support-session-tokenEl 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.

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.

Seis empleados IA de Hanc con auriculares, juntos bajo un anillo de luz naranja y rosa