Widgets — Hanc.AI
5 formes1 balise de script14 langues

Un agent vocal sur votre site.
Une balise, aucun backend.

Cinq composants web qui permettent à un visiteur de parler à votre agent — à voix haute, dans le navigateur, sans rien installer. La conversation passe par WebRTC, la réponse arrive en une seconde environ, et l'agent peut ouvrir des pages et lire ce qui est à l'écran.

Cette sphère est le widget. Appuyez et parlez.

Cinq formes

Choisissez la forme dont la page a besoin

Le même agent, cinq façons de l'atteindre. Mélangez librement — deux balises avec un seul agent-id est un montage tout à fait normal.

Sphère intégrée

Sphère intégrée

370 px par défaut, dans le flux de la page. Donnez-lui la largeur voulue ; sur écran étroit elle se réduit d'elle-même.

<hanc-ai-inline-call>

Une section de landing, une page de contact — là où l'appel est la raison d'être du bloc.

Sphère flottante

Sphère flottante

120 px, fixée à 32 px du coin inférieur droit. Trois autres coins au choix, ou static pour la remettre dans le flux.

<hanc-ai-floating-call>

Accessible depuis n'importe quelle page sans prendre de place dans la mise en page.

Pilule

Pilule

Un bouton horizontal : à gauche une sphère de 48 px, à droite votre propre libellé. Se place dans le flux comme n'importe quel bouton.

<hanc-ai-pill-call>

Dans une rangée de boutons, dans l'en-tête, dans une carte.

Pilule flottante

Pilule flottante

Le même bouton, fixé à 32 px du coin inférieur droit. Les mêmes quatre coins au choix.

<hanc-ai-pill-floating-call>

Quand une sphère ronde attire trop l'attention mais qu'on la veut toujours sous la main.

Formulaire de rappel

Formulaire de rappel

Sélecteur de pays, numéro formaté à la frappe, et un bouton. Le nombre de tentatives se règle sur l'agent, pas dans le balisage.

<hanc-ai-callback>

Pour ceux qui ne parleront pas via le navigateur — et partout où un micro est malvenu.

Cinq minutes

L'installer

Un seul attribut obligatoire : l'identifiant de l'agent, depuis votre tableau de bord. Aucune clé d'API n'a sa place dans le balisage — tout ce qui est dans le HTML est visible par le visiteur.

  1. 1

    Récupérez l'identifiant de l'agent

    Une chaîne du type 69d20781de6244c89509eb08, depuis le tableau de bord.

  2. 2

    Ajoutez le script

    Une ligne dans <head>. Via npm c'est un seul import — les balises s'enregistrent seules.

  3. 3

    Placez la balise

    Là où l'appel a sa place. Seul agent-id est obligatoire.

  4. 4

    Servez en HTTPS

    Les navigateurs ne donnent pas de micro en http://. Pendant le développement, localhost fonctionne.

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

En dessous de la v19, les props inconnues sont passées en chaînes : impossible de transmettre des objets ainsi. Encapsulez avec @lit/react — la voie officielle, avec typage et événements en prime.

Next.js

Le widget a besoin d'un navigateur : 'use client' et un import dynamique avec ssr: false. Sinon la compilation casse au rendu serveur.

Vue

Fonctionne sans encapsulation. Indiquez au bundler que les balises hanc-ai- sont des éléments personnalisés.

Il pilote la page

L'agent peut ouvrir des pages et lire ce qui est à l'écran

Une réponse à l'oral est souvent le mauvais support — personne ne veut se faire lire un tableau de livraison. L'agent peut donc envoyer une commande à la page et lui demander ce que le visiteur regarde. Votre code garde la main : c'est lui qui décide ce qu'il honore.

Hanc.AI
navigate
page_context
cart_state
Le widget
navigate

L'agent ouvre une page de votre site — un produit, une section, un formulaire déjà rempli.

page_context

La page répond où se trouve le visiteur et ce qui est affiché, pour que l'agent parle de ce produit-là plutôt qu'en général.

cart_state

La page répond ce qu'il y a dans le panier et ce que ça coûte, pour que l'agent annonce le total à voix haute.

Chaque commande arrive sous forme d'événement DOM annulable agent-command. Appeler preventDefault() signifie « traité » ; respond(data) renvoie une réponse à l'agent. Ignorez l'événement et l'agent est informé qu'il n'a pas été traité : il repasse alors par la parole.

Les commandes sont des demandes, pas des ordres : validez chaque chemin avant de le suivre. L'exemple refuse tout ce qui n'est pas un chemin interne au site.

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 production sur

milotec.com.ua

L'agent conduit le visiteur jusqu'à un produit, lui dit ce qui figure sur la page où il se trouve, et lui relit le panier.

Applications mobiles

Le même widget dans votre application

iOS et Android l'affichent tous deux dans une vue web. Construisez une petite page HTML dans le code et chargez-la avec une base HTTPS — sans origine sécurisée, le moteur ne donnera pas de micro à la page.

On n'entend pas l'agent

Une vue web ne restitue pas l'audio WebRTC tant que la session audio de l'application reste en lecture. Basculez-la en mode conversation avec sortie haut-parleur pendant l'appel, puis remettez-la ensuite.

L'agent répond dans la mauvaise langue

Le widget prend sa langue dans navigator.language, qui dans une vue web est celle du système, pas celle choisie dans votre application. Surchargez-la au démarrage du document — et patchez fetch pour ajouter browser_language à la requête de salle, que le widget intégré n'envoie pas de lui-même.

Le micro exige les deux moitiés

L'autorisation système pour l'application et l'autorisation pour la page dans la vue web. Sur Android, demandez la première avant l'ouverture de l'écran ; sur iOS, accordez la demande de la page dans le délégué.

Le même widget dans votre application
En production dans l'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 est en ligne

Laissez l'agent voir l'utilisateur connecté — et lui seul

Transmettez un jeton dans support-session-token. Le widget l'attache à l'appel tel quel, nous le transmettons à votre système dans l'en-tête X-Hanc-Support-Token, et votre serveur MCP décide de qui il s'agit. Le jeton nous est opaque : nous ne l'analysons pas, ne le vérifions pas et ne le stockons pas.

Votre app
Le widget
Hanc.AI
Votre MCP
Vos données

Ne faites jamais confiance à un identifiant qui arrive dans un jeton comme une affirmation. Demandez à l'émetteur du jeton de qui il s'agit, et travaillez avec sa réponse.

ApprocheFalsifiableVerdict
Un identifiant en clairTrivialementInacceptable
Un identifiant chiffréNon, mais rejouableMoitié de solution
Le jeton de votre fournisseur d'identitéNon : c'est le fournisseur qui signeFonctionne
Un ticket serveur à usage uniqueNon, et non rejouable non plusLe meilleur

Activez « interlocuteur identifié requis » sur la connexion et un appel anonyme ne montera pas vos outils du tout — l'agent ne pourra pas parler du compte d'autrui, même si une erreur se glisse plus tard dans votre code.

Un appel sans jeton n'obtient aucun accès au compte. C'est le comportement par défaut, pas une option.

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
Apparence

Il doit ressembler à votre site, pas au nôtre

Onze thèmes, clair et sombre selon le réglage système ou forcés, et une quinzaine d'attributs supplémentaires pour la sphère elle-même.

default
emerald
rose
amber
cyan
purple
blue
white
black
tangerine
ember
AttributCe qu'il définitPar défaut
agent-id Quel agent répond. Le seul obligatoire
theme L'un des onze thèmes de couleur default
size Taille en pixels ; se réduit seule sur écran étroit 370
button-start-text Le libellé du bouton Try to call
position Coin pour les formes flottantes bottom-right
glow-intensity Intensité de la lueur, 0–2 0.8
audio-reactivity Réactivité de la sphère à la voix 3.0
terms-enabled Panneau de consentement avant l'appel false
sound-enabled Sons de début et de fin true
support-session-token Le jeton de l'utilisateur connecté

Événements pour votre propre code

Aucun des deux événements ne transporte de données : ce sont de simples Event, vous obtenez donc le fait et l'instant. Aucun contenu de conversation dans l'un ni l'autre — transcriptions et enregistrements vivent dans le tableau de bord et l'API.

Réponses franches

Ce qui vous protège, et ce qui ne protège pas

Le canal est chiffré

Requêtes de contrôle en HTTPS, audio en WSS et SRTP. Sans HTTPS, le navigateur ne donne de toute façon pas de micro.

L'agent est vérifié

Un appel n'est monté que pour un identifiant d'agent qui existe.

L'accès est limité en débit

L'attribution de l'accès à la conversation est bridée, contre la force brute et l'abus.

Le jeton de session ne se dépose pas

Il est relu à chaque appel et n'est jamais écrit dans notre base.

La liste des domaines autorisés n'est pas une frontière de sécurité

Cette vérification s'exécute dans le navigateur. Elle est utile pour que le widget ne démarre pas par mégarde sur une copie de votre page, mais elle n'empêche personne d'appeler le service directement. Considérez comme public tout ce que l'agent dirait à un interlocuteur anonyme, et protégez le reste par un jeton de session.

Ce qui reste, et où

Dans le navigateur

Deux entrées dans localStorage : un marqueur de visiteur et le fait du consentement

Audio

En flux. Enregistré uniquement si l'enregistrement est activé pour l'agent

Transcription

Conservée avec l'enregistrement, consultable dans le tableau de bord

Localisation

Traitement et stockage dans l'UE

Limites à connaître avant de commencer

Pas de micro sans HTTPS

Testez sur localhost ou un domaine sécurisé

Safari veut un geste avant le son

L'appel doit démarrer sur un clic

Bloqueurs et proxys peuvent couper WebRTC

Gardez le formulaire de rappel comme voie de secours

Une fenêtre privée ne garde pas le marqueur

Chaque conversation semble être la première — c'est normal

React sous 19 ne passe pas d'objets en attribut

Encapsulez avec @lit/react

Le rendu serveur casse sur un import direct

Import dynamique avec ssr: false

Votre téléphone, enfin
à votre service.

Démarrez gratuitement en 60 secondes. Sans carte. Annulable. Le téléphone décroche tout seul.

Commencer
Book a Meeting