Widget — Hanc.AI
5 forme1 tag di script14 lingue

Un agente vocale sul tuo sito.
Un tag, nessun backend.

Cinque componenti web che permettono a un visitatore di parlare con il tuo agente — a voce, nel browser, senza installare nulla. La conversazione viaggia su WebRTC, la risposta arriva in circa un secondo, e l'agente può aprire pagine e leggere quello che c'è sullo schermo.

Questa sfera è il widget. Premila e parla.

Cinque forme

Scegli la forma che serve alla pagina

Lo stesso agente, cinque modi per raggiungerlo. Mescolali liberamente — due tag con un solo agent-id è un impianto del tutto normale.

Sfera in linea

Sfera in linea

370 px di default, nel flusso della pagina. Dagli la larghezza che vuoi; su schermo stretto si riduce da sola.

<hanc-ai-inline-call>

Una sezione di una landing, una pagina contatti — dove la chiamata è il senso del blocco.

Sfera flottante

Sfera flottante

120 px, fissata a 32 px dall'angolo in basso a destra. Altri tre angoli a scelta, oppure static per rimetterla nel flusso.

<hanc-ai-floating-call>

Raggiungibile da qualsiasi pagina senza occupare spazio nel layout.

Pillola

Pillola

Un pulsante orizzontale: a sinistra una sfera da 48 px, a destra la tua etichetta. Sta nel flusso come qualsiasi altro pulsante.

<hanc-ai-pill-call>

In una fila di pulsanti, nell'header, dentro una card.

Pillola flottante

Pillola flottante

Lo stesso pulsante, fissato a 32 px dall'angolo in basso a destra. Gli stessi quattro angoli a scelta.

<hanc-ai-pill-floating-call>

Quando una sfera tonda attira troppo l'attenzione ma la vuoi sempre a portata di mano.

Modulo di richiamata

Modulo di richiamata

Selettore del paese, numero formattato mentre si digita, e un pulsante. Quanti tentativi fare si imposta sull'agente, non nel markup.

<hanc-ai-callback>

Per chi non parlerà attraverso il browser — e ovunque un microfono sia scomodo.

Cinque minuti

Installarlo

Un solo attributo obbligatorio: l'id dell'agente, dalla tua dashboard. Nessuna chiave API va nel markup — tutto ciò che sta nell'HTML è visibile al visitatore.

  1. 1

    Prendi l'id dell'agente

    Una stringa come 69d20781de6244c89509eb08, dalla dashboard.

  2. 2

    Aggiungi lo script

    Una riga in <head>. Via npm è un solo import — i tag si registrano da soli.

  3. 3

    Metti il tag

    Dove la chiamata ha senso. Solo agent-id è obbligatorio.

  4. 4

    Servilo in HTTPS

    I browser non danno il microfono su http://. Mentre sviluppi, localhost funziona.

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

Sotto la v19 le prop sconosciute vengono passate come stringhe, quindi gli oggetti non si possono consegnare così. Avvolgi con @lit/react — la via ufficiale, e in più ti dà tipizzazione ed eventi.

Next.js

Il widget ha bisogno di un browser: 'use client' e un import dinamico con ssr: false. Altrimenti la build si rompe sul rendering lato server.

Vue

Funziona senza wrapper. Di' al bundler che i tag hanc-ai- sono custom element.

Guida la pagina

L'agente può aprire pagine e leggere cosa c'è sullo schermo

Una risposta parlata spesso è il mezzo sbagliato — nessuno vuole sentirsi leggere una tabella di spedizione. Perciò l'agente può inviare un comando alla pagina e chiederle cosa sta guardando il visitatore. Il controllo resta al tuo codice: è lui a decidere cosa eseguire.

Hanc.AI
navigate
page_context
cart_state
Il widget
navigate

L'agente apre una pagina del tuo sito — un prodotto, una sezione, un modulo già compilato.

page_context

La pagina risponde dov'è il visitatore e cosa viene mostrato, così l'agente parla di questo prodotto e non in generale.

cart_state

La pagina risponde cosa c'è nel carrello e quanto costa, così l'agente può dire il totale ad alta voce.

Ogni comando arriva come evento DOM annullabile agent-command. Chiamare preventDefault() significa «gestito»; respond(data) rimanda una risposta all'agente. Se ignori l'evento, all'agente viene detto che non è stato gestito e torna a rispondere a voce.

I comandi sono richieste, non ordini: valida ogni percorso prima di seguirlo. L'esempio rifiuta tutto ciò che non è un percorso interno al sito.

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',
    });
  }
});

In produzione su

milotec.com.ua

L'agente accompagna il visitatore fino a un prodotto, gli dice cosa c'è nella pagina in cui si trova e gli rilegge il carrello.

App mobili

Lo stesso widget dentro la tua app

iOS e Android lo mostrano entrambi in una web view. Costruisci una piccola pagina HTML nel codice e caricala con una base HTTPS — senza un'origine sicura il motore non consegna il microfono alla pagina.

L'agente non si sente

Una web view non riproduce l'audio WebRTC finché la sessione audio dell'app resta in playback. Passala a una modalità conversazionale con uscita in altoparlante per la durata della chiamata e rimettila com'era dopo.

L'agente risponde nella lingua sbagliata

Il widget prende la lingua da navigator.language, che in una web view è quella di sistema, non quella scelta nella tua app. Sovrascrivila all'avvio del documento — e applica una patch a fetch per aggiungere browser_language alla richiesta di stanza, che il widget incorporato non invia da solo.

Il microfono vuole entrambe le metà

Il permesso di sistema per l'app e il permesso per la pagina dentro la web view. Su Android chiedi quello di sistema prima di aprire la schermata; su iOS concedi la richiesta della pagina nel delegate.

Lo stesso widget dentro la tua app
In produzione nell'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"))
Chi è in chiamata

Fai vedere all'agente l'utente autenticato — e solo lui

Passa un token in support-session-token. Il widget lo allega alla chiamata così com'è, noi lo inoltriamo al tuo sistema nell'header X-Hanc-Support-Token, e il tuo server MCP decide chi è. Per noi il token è opaco: non lo analizziamo, non lo verifichiamo e non lo conserviamo.

La tua app
Il widget
Hanc.AI
Il tuo MCP
I tuoi dati

Non fidarti mai di un identificativo che arriva dentro un token come affermazione. Chiedi a chi ha emesso il token di chi si tratta, e lavora con la risposta.

ApproccioFalsificabileGiudizio
Un id utente in chiaroBanalmenteInaccettabile
Un id utente cifratoNo, ma è riutilizzabileMezza soluzione
Il token del tuo provider di accessoNo: la firma la mette il providerFunziona
Un tagliando server monousoNo, e nemmeno riutilizzabileIl migliore

Attiva «richiede un interlocutore identificato» sulla connessione e una chiamata anonima non solleverà affatto i tuoi strumenti — l'agente non potrà parlare dell'account di qualcun altro nemmeno se più avanti nel tuo codice comparisse un errore.

Una chiamata senza token non ottiene alcun accesso all'account. È il comportamento predefinito, non un'opzione.

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
Aspetto

Deve assomigliare al tuo sito, non al nostro

Undici temi, chiaro e scuro secondo l'impostazione di sistema o forzati, e altri quindici attributi circa per la sfera stessa.

default
emerald
rose
amber
cyan
purple
blue
white
black
tangerine
ember
AttributoCosa impostaPredefinito
agent-id Quale agente risponde. L'unico obbligatorio
theme Uno degli undici temi di colore default
size Dimensione in pixel; su schermi stretti si riduce da sé 370
button-start-text L'etichetta del pulsante Try to call
position Angolo per le forme flottanti bottom-right
glow-intensity Intensità del bagliore, 0–2 0.8
audio-reactivity Quanto la sfera reagisce alla voce 3.0
terms-enabled Pannello di consenso prima della chiamata false
sound-enabled Suoni di inizio e fine true
support-session-token Il token dell'utente autenticato

Eventi per il tuo codice

Nessuno dei due eventi porta dati: sono entrambi un Event semplice, quindi ottieni il fatto e il momento. In nessuno dei due c'è il contenuto della conversazione — trascrizioni e registrazioni vivono nella dashboard e nell'API.

Risposte chiare

Cosa ti protegge e cosa no

Il canale è cifrato

Richieste di controllo su HTTPS, audio su WSS e SRTP. Senza HTTPS il browser non dà comunque il microfono.

L'agente viene verificato

Una chiamata viene aperta solo per un id agente che esiste.

L'accesso è limitato in frequenza

La concessione dell'accesso alla conversazione è limitata, contro forza bruta e abuso.

Il token di sessione non si deposita

Viene riletto a ogni chiamata e non finisce mai nel nostro database.

L'elenco dei domini consentiti non è un confine di sicurezza

Quel controllo gira nel browser. Serve perché il widget non parta su una copia sbagliata della tua pagina, ma non impedisce a nessuno di chiamare il servizio direttamente. Considera pubblico tutto ciò che l'agente direbbe a un interlocutore anonimo, e proteggi il resto con un token di sessione.

Cosa resta e dove

Nel browser

Due voci in localStorage: un contrassegno del visitatore e il fatto del consenso

Audio

In streaming. Registrato solo se la registrazione è attiva per l'agente

Trascrizione

Conservata con la registrazione, leggibile nella dashboard

Collocazione

Elaborazione e conservazione nell'UE

Limiti da conoscere prima di iniziare

Senza HTTPS niente microfono

Prova su localhost o su un dominio sicuro

Safari vuole un gesto prima dell'audio

La chiamata deve partire da un clic

Blocker e proxy possono tagliare WebRTC

Tieni il modulo di richiamata come via alternativa

Una finestra privata non conserva il contrassegno

Ogni conversazione sembra la prima — è normale

React sotto la 19 non passa oggetti come attributi

Avvolgi con @lit/react

Il rendering lato server si rompe con un import diretto

Import dinamico con ssr: false

Il tuo telefono, che finalmente
lavora per te.

Inizia gratis in 60 secondi. Senza carta. Disdici quando vuoi. Il telefono risponde da solo.

Inizia gratis
Book a Meeting