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.
Lo stesso agente, cinque modi per raggiungerlo. Mescolali liberamente — due tag con un solo agent-id è un impianto del tutto normale.
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
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
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
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
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.
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.
Prendi l'id dell'agente
Una stringa come 69d20781de6244c89509eb08, dalla dashboard.
Aggiungi lo script
Una riga in <head>. Via npm è un solo import — i tag si registrano da soli.
Metti il tag
Dove la chiamata ha senso. Solo agent-id è obbligatorio.
Servilo in HTTPS
I browser non danno il microfono su http://. Mentre sviluppi, localhost funziona.
<!-- 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
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.
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.
navigate page_context cart_state 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.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.
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.
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")) 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.
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.
| Approccio | Falsificabile | Giudizio |
|---|---|---|
| Un id utente in chiaro | Banalmente | Inaccettabile |
| Un id utente cifrato | No, ma è riutilizzabile | Mezza soluzione |
| Il token del tuo provider di accesso | No: la firma la mette il provider | Funziona |
| Un tagliando server monouso | No, e nemmeno riutilizzabile | Il 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.
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 Undici temi, chiaro e scuro secondo l'impostazione di sistema o forzati, e altri quindici attributi circa per la sfera stessa.
| Attributo | Cosa imposta | Predefinito |
|---|---|---|
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.
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
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
Inizia gratis in 60 secondi. Senza carta. Disdici quando vuoi. Il telefono risponde da solo.