Fünf Web-Komponenten, mit denen Besucher mit Ihrem Agenten sprechen — laut, im Browser, ohne Installation. Das Gespräch läuft über WebRTC, die Antwort kommt in etwa einer Sekunde, und der Agent kann Seiten öffnen und lesen, was auf dem Bildschirm steht.
Diese Sphäre ist das Widget. Drücken und sprechen.
Derselbe Agent, fünf Wege zu ihm. Kombinieren Sie frei — zwei Tags mit einer agent-id sind ein ganz normaler Aufbau.
Inline-Sphäre
370 px standardmäßig, im Fluss der Seite. Geben Sie ihr die Breite, die Sie wollen; auf schmalem Bildschirm verkleinert sie sich selbst.
<hanc-ai-inline-call> Ein Abschnitt auf einer Landingpage, eine Kontaktseite — überall dort, wo das Gespräch der Sinn des Blocks ist.
Schwebende Sphäre
120 px, fix 32 px von der Ecke rechts unten. Drei weitere Ecken zur Wahl, oder static, um sie in den Fluss zu setzen.
<hanc-ai-floating-call> Von jeder Seite erreichbar, ohne Platz im Layout zu belegen.
Pill
Ein horizontaler Button: links eine 48-px-Sphäre, rechts Ihre eigene Beschriftung. Steht im Fluss wie jeder andere Button.
<hanc-ai-pill-call> In einer Button-Reihe, im Header, in einer Karte.
Schwebende Pill
Derselbe Button, fix 32 px von der Ecke rechts unten. Dieselben vier Ecken zur Wahl.
<hanc-ai-pill-floating-call> Wenn eine runde Sphäre zu viel Aufmerksamkeit zieht, sie aber trotzdem immer griffbereit sein soll.
Rückruf-Formular
Länderauswahl, eine Nummer, die beim Tippen formatiert wird, und ein Button. Wie oft es erneut versucht wird, steht am Agenten, nicht im Markup.
<hanc-ai-callback> Für alle, die nicht über den Browser sprechen — und für jede Lage, in der ein Mikrofon unpassend ist.
Ein Pflichtattribut: die Agent-ID aus Ihrem Dashboard. Ein API-Schlüssel gehört nicht ins Markup — alles im HTML ist für Besucher sichtbar.
Agent-ID holen
Eine Zeichenfolge wie 69d20781de6244c89509eb08, aus dem Dashboard.
Script einbinden
Eine Zeile im <head>. Über npm ist es ein Import — die Tags registrieren sich selbst.
Tag platzieren
Dort, wo das Gespräch hingehört. Pflicht ist nur agent-id.
Über HTTPS ausliefern
Browser geben auf http:// kein Mikrofon frei. Beim Entwickeln funktioniert localhost.
<!-- 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
Unter v19 werden unbekannte Props als Strings übergeben, Objekte lassen sich so nicht durchreichen. Mit @lit/react umschließen — der offizielle Weg, inklusive Typisierung und Events.
Next.js
Das Widget braucht einen Browser: 'use client' und ein dynamischer Import mit ssr: false. Sonst bricht der Build beim Server-Rendering ab.
Vue
Funktioniert ohne Wrapper. Sagen Sie dem Bundler, dass hanc-ai- Tags Custom Elements sind.
Eine gesprochene Antwort ist oft das falsche Medium — niemand will eine Liefertabelle vorgelesen bekommen. Deshalb kann der Agent der Seite einen Befehl schicken und die Seite fragen, was der Besucher gerade ansieht. Die Kontrolle bleibt bei Ihrem Code: er entscheidet, was er ausführt.
navigate page_context cart_state navigate Der Agent öffnet eine Seite bei Ihnen — ein Produkt, einen Abschnitt, ein bereits ausgefülltes Formular.
page_context Die Seite antwortet, wo der Besucher ist und was gezeigt wird, damit der Agent über dieses Produkt spricht statt allgemein.
cart_state Die Seite antwortet, was im Warenkorb liegt und was es kostet, damit der Agent die Summe laut nennen kann.
Jeder Befehl kommt als abbrechbares agent-command DOM-Event an. preventDefault() bedeutet „erledigt“; respond(data) schickt eine Antwort zurück an den Agenten. Wird ein Event ignoriert, erfährt der Agent, dass es nicht bearbeitet wurde, und weicht auf Sprache aus.
Befehle sind Bitten, keine Anweisungen — prüfen Sie jeden Pfad, bevor Sie ihm folgen. Das Beispiel verweigert alles, was kein seiteninterner Pfad ist.
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',
});
}
}); Live auf
milotec.com.ua
Der Agent führt Besucher zu einem Produkt, erzählt, was auf der aktuellen Seite steht, und liest den Warenkorb vor.
iOS und Android zeigen es beide in einer Web-View. Bauen Sie eine kleine HTML-Seite im Code und laden Sie sie mit HTTPS-Basis — ohne sicheren Ursprung gibt die Engine der Seite kein Mikrofon.
Der Agent ist nicht zu hören
Eine Web-View gibt kein WebRTC-Audio aus, solange die Audio-Session der App auf Playback steht. Schalten Sie sie für die Dauer des Gesprächs auf einen Gesprächsmodus mit Lautsprecher und danach zurück.
Der Agent antwortet in der falschen Sprache
Das Widget nimmt die Sprache aus navigator.language, und das ist in einer Web-View die Systemsprache, nicht die in Ihrer App gewählte. Überschreiben Sie sie beim Dokumentstart — und patchen Sie fetch, um browser_language an die Raum-Anfrage anzuhängen, was das eingebettete Widget von sich aus nicht sendet.
Das Mikrofon braucht beide Hälften
Die Systemberechtigung für die App und die Berechtigung für die Seite in der Web-View. Auf Android vor dem Öffnen des Bildschirms anfragen, auf iOS die Anfrage der Seite im Delegate freigeben.
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")) Übergeben Sie ein Token in support-session-token. Das Widget hängt es unverändert an das Gespräch, wir reichen es im Header X-Hanc-Support-Token an Ihr System weiter, und Ihr MCP-Server entscheidet, wer das ist. Für uns ist das Token undurchsichtig: wir zerlegen, prüfen und speichern es nicht.
Vertrauen Sie niemals einer Kennung, die als Behauptung im Token ankommt. Fragen Sie den Aussteller des Tokens, wer das ist, und arbeiten Sie mit der Antwort.
| Ansatz | Fälschbar | Bewertung |
|---|---|---|
| Offene Nutzer-ID | Trivial | Nicht akzeptabel |
| Verschlüsselte Nutzer-ID | Nein — aber wiederverwendbar | Halbe Lösung |
| Token Ihres Login-Anbieters | Nein: der Anbieter signiert | Funktioniert |
| Einmal-Ticket vom Server | Nein, und auch nicht wiederverwendbar | Am besten |
Aktivieren Sie an der Verbindung „identifizierter Anrufer erforderlich“, und bei einem anonymen Gespräch werden Ihre Tools gar nicht erst hochgefahren — der Agent kann nichts über ein fremdes Konto sagen, selbst wenn sich später ein Fehler in Ihren Code einschleicht.
Ein Gespräch ohne Token bekommt überhaupt keinen Kontozugriff. Das ist der Standard, keine Option.
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 Elf Themes, hell und dunkel nach Systemeinstellung oder erzwungen, und rund fünfzehn weitere Attribute für die Sphäre selbst.
| Attribut | Wofür | Standard |
|---|---|---|
agent-id | Welcher Agent antwortet. Das einzige Pflichtattribut | — |
theme | Eines von elf Farbthemes | default |
size | Größe in Pixeln; verkleinert sich auf schmalen Bildschirmen selbst | 370 |
button-start-text | Die Beschriftung des Buttons | Try to call |
position | Ecke für die schwebenden Formen | bottom-right |
glow-intensity | Stärke des Leuchtens, 0–2 | 0.8 |
audio-reactivity | Wie stark die Sphäre auf die Stimme reagiert | 3.0 |
terms-enabled | Einwilligungsfenster vor dem Gespräch | false |
sound-enabled | Töne am Anfang und Ende | true |
support-session-token | Token des angemeldeten Nutzers | — |
Events für Ihren eigenen Code
Keines der beiden Events trägt Daten: beide sind ein einfaches Event, Sie erhalten also die Tatsache und den Zeitpunkt. Gesprächsinhalte stehen in keinem von beiden — Transkripte und Aufnahmen liegen im Dashboard und in der API.
Der Kanal ist verschlüsselt
Steuer-Anfragen über HTTPS, Audio über WSS und SRTP. Ohne HTTPS gibt der Browser ohnehin kein Mikrofon frei.
Der Agent wird geprüft
Ein Gespräch wird nur für eine existierende Agent-ID aufgebaut.
Zugriff ist ratenbegrenzt
Die Vergabe von Gesprächszugängen ist gedrosselt — gegen Brute Force und Missbrauch.
Das Session-Token bleibt nicht liegen
Es wird bei jedem Gespräch neu gelesen und nie in unsere Datenbank geschrieben.
Die Liste erlaubter Domains ist keine Sicherheitsgrenze
Diese Prüfung läuft im Browser. Sie ist nützlich, damit das Widget nicht versehentlich auf einer fremden Kopie Ihrer Seite startet, hindert aber niemanden daran, den Dienst direkt aufzurufen. Behandeln Sie alles, was der Agent einem anonymen Gegenüber erzählen würde, als öffentlich, und sichern Sie den Rest mit einem Session-Token.
Was wo bleibt
Im Browser
Zwei Einträge im localStorage: eine Besuchermarkierung und die erteilte Einwilligung
Audio
Als Stream. Aufgezeichnet nur, wenn die Aufnahme für den Agenten aktiviert ist
Transkript
Wird mit der Aufnahme gespeichert und ist im Dashboard einsehbar
Standort
Verarbeitung und Speicherung in der EU
Kein Mikrofon ohne HTTPS
Auf localhost oder einer sicheren Domain testen
Safari will eine Geste vor dem Ton
Das Gespräch muss per Klick starten
Blocker und Proxies können WebRTC abschneiden
Das Rückruf-Formular als Ausweichweg lassen
Im privaten Fenster bleibt keine Markierung
Jedes Gespräch wirkt wie das erste — das ist normal
React unter 19 kann keine Objekte als Attribut übergeben
Mit @lit/react umschließen
Server-Rendering bricht beim direkten Import ab
Dynamischer Import mit ssr: false
In 60 Sekunden kostenlos starten. Keine Kreditkarte. Jederzeit kündbar. Das Telefon nimmt sich selbst ab.