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.

Touchez pour parler

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

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.

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.

  • Sphère intégrée

    Sphère intégrée

    <hanc-ai-inline-call>

    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.

    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

    <hanc-ai-floating-call>

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

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

  • Pilule

    Pilule

    <hanc-ai-pill-call>

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

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

  • Pilule flottante

    Pilule flottante

    <hanc-ai-pill-floating-call>

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

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

  • Formulaire de rappel

    Formulaire de rappel

    <hanc-ai-callback>

    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.

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

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. Récupérez l'identifiant de l'agent Une chaîne du type 69d20781de6244c89509eb08, depuis le tableau de bord.
  2. Ajoutez le script Une ligne dans <head>. Via npm c'est un seul import, les balises s'enregistrent seules.
  3. Placez la balise Là où l'appel a sa place. Seul agent-id est obligatoire.
  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>
  • 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.

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.

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.

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.

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

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

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

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 production dans l'app RunOrJog
En production dans l'app RunOrJog

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.

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

Laissez l'agent voir l'utilisateur connecté, et lui seul
Approche Falsifiable Verdict
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
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

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.

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

11 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
Il doit ressembler à votre site, pas au nôtre
Attribut Ce qu'il définit Par défaut
agent-idQuel agent répond. Le seul obligatoire
themeL'un des 11 thèmes de couleurdefault
sizeTaille en pixels ; se réduit seule sur écran étroit370
button-start-textLe libellé du boutonTry to call
positionCoin pour les formes flottantesbottom-right
glow-intensityIntensité de la lueur, 0-20.8
audio-reactivityRéactivité de la sphère à la voix3.0
terms-enabledPanneau de consentement avant l'appelfalse
sound-enabledSons de début et de fintrue
support-session-tokenLe 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.

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.

Six employés IA de Hanc avec casque-micro, réunis sous un anneau lumineux orange et rose