Widget Integration
Embed a real-time AI avatar (voice + optional lipsync video) into any web page. This guide covers every configuration option, feature flag, event, and integration pattern the KonAI widget supports.
⚠️ Important Security Notice
- • Never use your API key directly in client-side code - This exposes your key to anyone who views your page source
- • API keys in JavaScript are NOT secure and not recommended for production
- • Always use server-side session tokens for widget authentication in production environments
For production deployments, create a backend endpoint that generates session tokens, call it from your frontend, and use the session token to initialize the widget. Never expose your actual API key to the client.
Overview
The widget is delivered as a self-contained IIFE bundle that exposes a global KonAI object. It handles the session WebSocket, microphone capture, voice activity detection, lipsync video, and the entire on-screen UI.
- CDN bundle:
https://cdn.konpro.ai/widget/index.min.js - Global:
window.KonAI— checkKonAI.versionin the browser console for the bundle version. - Bundle size: ~340KB minified. Panorama and Google Maps support are lazy-loaded on first use.
What the widget looks like
The widget mid-call, with most of the optional chrome switched on:
- Top-left button row — fullscreen, captions (highlighted while the transcript is showing), text input, and the opt-in
inputModedropdown, currently set to "Voice" (VAD). - Avatar stage — the lipsync video, framed by
videoFitandvideoPosition. - Transcript panel — the scrolling user/AI exchange overlaid across the bottom of the stage.
- Bottom control row — mic mute, and the call button in its red connected state.

Where the avatar itself is configured
This page documents the widget — how the avatar is embedded and how the on-screen UI behaves. The avatar behind it (persona, prompt, voice, knowledge base, display library, tools) is configured in one of two places, and both drive the same agent:
- KonPro Studio — build and tune the agent visually at studio.konpro.ai. Pick or upload an avatar, assign a stock or cloned voice, write the persona, attach a knowledge base, fill the Display Library, and enable tools on the Tools tab.
- API / SDK — do the same programmatically when you need to provision agents per customer or per tenant: manage avatars and sessions through the KonPro API, set limits and
allowed_domainsvia Create Widget Session, or build a custom front end with the KonPro JS SDK when the widget isn't the shape you need.
For a guide-level walkthrough of embedding an agent, see Embed an Agent in Your Site.
Quick Start
The minimum viable embed. Everything else on this page is optional configuration.
<div id="konpro-avatar"></div>
<script src="https://cdn.konpro.ai/widget/index.min.js"></script>
<script>
KonAI.init({
sessionEndpoint: '/api/widget-session',
agenticAvatarId: 'af1250af-5de6-4730-a079-32d96705f8c1',
containerId: 'konpro-avatar',
});
</script>Three moving parts:
- A
<div>on your page where the widget mounts. - The CDN
<script>tag. KonAI.init({...})with an auth method and eitheragenticAvatarIdor a session endpoint that knows the avatar internally.
Installation
Load the bundle from the CDN. Once the script runs, the global KonAI is available and KonAI.init(config) returns the widget instance — hold on to it if you want to call any of the later.
<!-- Add to the <head> section or before the closing </body> tag -->
<script src="https://cdn.konpro.ai/widget/index.min.js"></script>
<script>
const widget = KonAI.init({ /* config */ });
</script>Authentication
The widget needs a short-lived session token to open the WebSocket. Pick exactly one of three methods — passing zero throws at init().
Option A — sessionEndpoint (Recommended for production)
Your backend mints a session token from your API key. The widget POSTs to your endpoint, gets back a token, then connects. Your API key never touches the browser.
Frontend - Point at Your Endpoint
KonAI.init({
sessionEndpoint: '/api/widget-session',
agenticAvatarId: 'af1250af-...',
});
// The widget POSTs { agenticAvatarId, audioOnly } to your endpoint
// and expects { sessionToken } back.Backend - Generate Session Token
// backend/api.js (Node.js / Express example)
app.post('/api/widget-session', async (req, res) => {
const response = await fetch('https://api.konpro.ai/v1/widget/sessions', {
method: 'POST',
headers: {
'x-api-key': process.env.KONPRO_API_KEY, // Secure server-side
'Content-Type': 'application/json'
},
body: JSON.stringify({
agentic_avatar_id: req.body.agenticAvatarId,
settings: {
max_duration: 3600,
allowed_domains: ['yourdomain.com']
}
})
});
const data = await response.json();
res.json({ sessionToken: data.session_token });
});Option B — getSessionToken (Custom auth flow)
A Promise-returning callback you control. Use this when you need custom headers, alternate auth flows (Cognito, Auth0, etc.), or a token from an existing session.
KonAI.init({
getSessionToken: async () => {
const res = await myApiClient.post('/create-avatar-session', {
avatarId: 'af1250af-...',
});
// { sessionToken, sessionId, websocketUrl, userId, avatar, ... }
return res.data;
},
});Option C — apiKey (Development only)
⚠️ Warning: This method hardcodes your Konpro API key in the browser and exposes it to anyone who views the page source. Only use it for local prototyping or a walled internal demo.
// ⚠️ NEVER DO THIS IN PRODUCTION - API KEY EXPOSED
KonAI.init({
apiKey: 'YOUR_KEY', // NOT SECURE - Development only
agenticAvatarId: 'af1250af-...',
});agenticAvatarId is required with apiKey. With sessionEndpoint or getSessionToken, your backend can pick the avatar server-side.
Configuration Reference
Every option accepted by KonAI.init({...}), grouped by purpose.
Authentication
| Option | Type | Default | Notes |
|---|---|---|---|
| apiKey | string | — | ⚠️ Development only |
| sessionEndpoint | string | — | Recommended for production |
| getSessionToken | () => Promise<sessionData> | — | Custom auth flows |
| agenticAvatarId | string | — | Required with apiKey; optional with the other two |
Layout & Positioning
| Option | Type | Default | Notes |
|---|---|---|---|
| containerId | string | — | ID of a DOM element to mount inside. When set, position is ignored and the widget fills the container. |
| position | object | { position:'fixed', bottom:'20px', right:'20px', width:'400px', height:'600px' } | Floating-mode CSS. Only used when containerId is omitted. |
- Floating mode (no
containerId): the widget floats over the host page at the corner set byposition. - Container mode (
containerIdset): the widget renders 100%×100% inside the given element — you control sizing via that element's CSS.
Video / Audio Mode
| Option | Type | Default | Notes |
|---|---|---|---|
| audioOnly | boolean | false | Force audio-only. Disables lipsync + WebRTC video, shows an animated circle with the avatar’s initials or image. |
| enableLipsync | boolean | true | Request server-side lipsync. Falls back gracefully if unavailable. |
| enableWebRTC | boolean | true | Attempt WebRTC / MediaSoup for lowest-latency A/V. Falls back to audio-only on failure. |
| usePlaceholderVideo | boolean | true | Show the avatar’s idle-loop video (from session data) between responses. |
| videoFit | "contain" | "cover" | "contain" | CSS object-fit for the avatar video and placeholder image. |
| videoPosition | string | "top" | CSS object-position (e.g. "top", "center", "50% 20%"). Framing for portrait avatars. |
Conversation Behavior
| Option | Type | Default | Notes |
|---|---|---|---|
| allowInterruption | boolean | true | When false: no barge-in, interrupt button hidden, text-send disabled while the avatar speaks, widget.interruptAI() is a no-op. |
| pushToTalk | boolean | false | Enables PTT mode — mic defaults muted, replaces the mute button with a "hold to talk" button, disables VAD-driven commit. |
Third-party Keys & Advanced
| Option | Type | Default | Notes |
|---|---|---|---|
| googleMapsApiKey | string | — | Google Maps JS API key. Required for the city display type (3D city map + Street View). Needs Maps JavaScript API + Map Tiles API enabled and billing on the GCP project. |
| apiBaseUrl | string | "https://api.konpro.ai" | Override for a self-hosted / staging Konpro backend. |
Theme
The theme option accepts an object:
theme: {
primaryColor: '#667eea', // start of the header/avatar/call-btn gradient
secondaryColor: '#764ba2', // end of the gradient
iconSize: 18, // control-button icon px (mute icon renders at iconSize − 2)
}UI Details
uiDetails controls what gets rendered. Passing false for any group hides it.
uiDetails: {
showHeader: true, // top bar with avatar name + "Online"
showStatus: true, // Live / Listening / Speaking / Processing status pill
showTranscript: true, // scrolling transcript panel + gates the captions toggle
showAvatar: true, // whole avatar display area
showControls: { // bottom control row
call: true,
mute: true,
interrupt: false, // barge-in button (always on when Firefox is detected)
audioMute: false, // speaker mute (WebRTC audio out only)
},
showTopButtons: { // top-left button row (top-right in fullscreen)
fullscreen: true,
captions: true, // hidden if showTranscript === false
textInput: true, // only visible during an active call
inputMode: false, // dropdown to switch VAD ↔ PTT at runtime (opt-in)
},
}showControls:trueshows the defaults,falsehides everything in the row, and an object merges with the defaults so you only pass overrides.showTopButtonsfollows the same pattern.captionsandinputModeare opt-in — they default to hidden even whenshowTopButtons: trueis used as shorthand.
Callbacks / Events
| Option | Signature | Notes |
|---|---|---|
| onReady | (widget) => void | Fires when init completes. Receives the widget instance. |
| onError | (Error) => void | Fires on unrecoverable errors (auth failure, WS error, session failure, etc.). |
| onTranscription | (text: string) => void | Fires on the user’s final STT commit — not on partials. |
| onResponse | (text: string) => void | Fires when the AI completes a full response. |
| onAvatarStateChange | (state: string) => void | Fires on every state change: idle, listening, thinking, speaking. |
Tools
Server-driven avatar capabilities
The LLM invokes tools via tool calls, the server pushes events over the WebSocket, and the widget renders the resulting UI. You don't call these directly — you configure them on the avatar in the studio and they appear when the LLM decides to use them. Each tool is a distinct visual mode that overlays the avatar; they are mutually exclusive, and the active tool takes over the stage while the avatar shrinks to a picture-in-picture corner.
Display tool — images, videos, galleries, slideshows
The most general tool (show_display). The avatar shows visual media while it talks. Handles the image and video media types, in three layouts:
- Single image or video: full-stage card, avatar as PiP top-left.
- Multi-item gallery (2+ items, non-sequential): bottom-band tile grid; the avatar joins the grid as the first tile.
- Sequential slideshow: full-stage, cross-fades between items paced to speech. Per-slide dwell is caption-length weighted (5–12.5s per item).
Configuration lives on the avatar in the studio (Display Library) — nothing widget-side to enable it. Images can be static uploads or links; videos autoplay muted and looped.
360° Panorama
mediaType: 'panorama' — an interactive 360° equirectangular photo the guest can drag to look around. The widget lazy-loads Photo Sphere Viewer (~800KB) from jsDelivr on first use, starts fully zoomed out, and shows a "Drag to look around" hint that fades after ~2s.
The panorama image must be:
- HTTPS (enforced by the widget)
- Equirectangular (2:1 aspect ratio)
- CORS-enabled on its origin (required for the WebGL texture load)
Panoramas are sticky by design — the guest explores until the server explicitly dismisses the tour, with a 10-minute hard cap as a safety net. There is no auto-dismiss when the avatar stops speaking.
Virtual tour
mediaType: 'tour' — an interactive 3D walkthrough (studio.konpro.ai/t/<tourId>) rendered in a sandboxed iframe.
- Sandbox:
allow-scripts allow-same-origin allow-popups - Feature policy:
fullscreen; gyroscope; accelerometer; magnetometer(mobile look-around) - postMessage bridge for voice-driven navigation ("take me to the lobby") plus activity keep-alives
- Sticky like a panorama — server-driven dismissal only
Prerequisite: the tour's origin (typically studio.konpro.ai) must include your host page's origin in its frame-ancestors CSP, or the iframe won't render. Coordinate with Konpro to get your host origin allowlisted.
Google Map — 3D city + Street View
mediaType: 'city' — an interactive Google Maps 3D city surface with markers for projects or buildings, plus an optional Street View drop-in. Requires googleMapsApiKey in the widget config, and the GCP project behind that key must have billing, the Maps JavaScript API, and the Map Tiles API enabled.
| Event | Direction | Effect |
|---|---|---|
{ type: 'display', mediaType: 'city', cityProjects, cityRenderer, cityMapBounds } | Server → widget | Mount the map |
{ type: 'display', action: 'navigate', lat, lng, projectId } | Server → widget | Pan/zoom to a project |
{ type: 'city_street_view', lat, lng, heading?, pitch? } | Server → widget | Swap to StreetViewPanorama |
{ type: 'city_project_tour', imageUrl } | Server → widget | Open the project’s immersive tour |
{ type: 'city_navigated' | 'city_arrived', projectId } | Widget → server | Ack after a successful pan |
The Google Maps JS API is lazy-loaded on first use, so it doesn't bloat the base bundle. Sticky like a panorama, with the same 10-minute cap.
Kiosk / concierge menu
A 4-button on-screen menu that scopes the avatar to a specific domain for the exchange — turnkey for lobby and exhibition avatars. It appears automatically when the server session reports kioskEnabled: true in its ready message; no widget config is needed.
- Variants (the server picks):
'hotel'(Check-in / Spa / Restaurant / Concierge) or'concierge'(Maintenance / Complaints / Leasing / Concierge). - Flow: the menu appears → the guest taps a button → the widget sends
{ type: 'kiosk:set_domain', domain }→ the server scopes the avatar's prompt and tools to that domain and greets in-domain. - When the guest wraps up, the avatar calls
kiosk_return_to_menu, the server pushes{ type: 'kiosk:return_to_menu' }, and the widget clears the domain and re-shows the buttons. The same call stays alive throughout.
CV / document upload QR
A small QR code card in the bottom-right that lets the guest scan-to-upload a document with their phone. It appears automatically when the server sends { type: 'cv_upload_qr_show', qrUrl, expiresAt }. The widget delays showing it by 7s so the avatar's lead-in plays first, then fades it in. It dismisses on Escape, the ✕ button, or a server-driven cv_upload_qr_dismiss. Uses the bundled qrcode library — no extra hosts to allow in your CSP.
UI Controls
The chrome around the avatar — buttons, indicators, and overlays. All of it is configurable via uiDetails and can be individually shown or hidden per integration.
Bottom control row
Small circular buttons at the bottom of the widget area (uiDetails.showControls):
| Button | Key | Default | Notes |
|---|---|---|---|
| Start / end call | call | on | Green phone icon; goes red when connected. |
| Mute mic | mute | on | Toggles VAD mic mute. Replaced by the PTT button when pushToTalk: true. |
| Interrupt AI | interrupt | off | Manually stop the avatar mid-response. Always visible on Firefox, where automatic voice barge-in is limited. Hidden entirely when allowInterruption: false. |
| Speaker mute | audioMute | off | Silence the avatar’s audio output only (mic still works). WebRTC mode only. |
Top button row
Small circular buttons anchored top-left, or top-right in fullscreen (uiDetails.showTopButtons):
| Button | Key | Default | Notes |
|---|---|---|---|
| Fullscreen | fullscreen | on | Native Fullscreen API where supported; automatic CSS fallback for PWA / iframe / old Android WebViews. |
| Captions (CC) | captions | on | Runtime toggle for transcript visibility. Hidden if showTranscript: false. |
| Text input | textInput | on | Reveals a floating text-input bar for typed messages. Only visible during an active call. |
| Input mode | inputMode | off | Opt-in dropdown to switch between VAD ("Voice") and PTT ("Push-to-talk") at runtime. |
Status, transcript, and header
- Status indicator (
showStatus): a live status pill in the top-right — Live / Listening / Speaking / Processing / Muted. - Transcript panel (
showTranscript): a scrolling text panel at the bottom showing user and AI text. Auto-hides during display tools that would overlap it (galleries, QR card, kiosk menu). - Header (
showHeader): the top bar with the avatar's name and "Online" subtitle.
Conversation Modes
These control how the guest talks to the avatar, not what gets rendered.
Voice (VAD) — default
Always-listening voice activity detection. The guest speaks whenever they want; VAD detects speech, commits utterances, and handles voice barge-in over the avatar. No button hold required. Best for normal conversational interfaces and quiet-environment demos.
Push-to-Talk (PTT)
Set pushToTalk: true, or enable runtime switching via uiDetails.showTopButtons.inputMode: true. The mic defaults muted and the guest holds the walkie-talkie button to talk; the button's release is the utterance boundary, so VAD-driven commit is suppressed to avoid double-commits. Best for exhibitions, kiosks, and noisy environments where always-listening VAD produces false positives.
Barge-in / interruption control
allowInterruption: true (default) lets the guest interrupt the avatar mid-response by speaking or via the interrupt button. Setting it to false enforces strict turn-taking:
- Voice barge-in blocked
- Interrupt button hidden
- Text-send disabled while the avatar is speaking
widget.interruptAI()becomes a no-op- Outgoing mic audio is zero-filled during avatar speech, so server-side STT can't back-door an interrupt either
Best for scripted demos, interviews, and compliance flows where the avatar must finish each turn.
Customization Examples
Minimalist mode
No chrome at all — just the avatar and the call button.
// Minimalist mode - no chrome, just the avatar + call button
KonAI.init({
sessionEndpoint: '/api/widget-session',
agenticAvatarId: '...',
uiDetails: {
showHeader: false,
showStatus: false,
showTranscript: false,
showTopButtons: false,
showControls: { call: true, mute: false, interrupt: false, audioMute: false },
},
});Exhibition kiosk
Portrait, push-to-talk, strict turn-taking, no chrome.
// Exhibition kiosk - portrait, push-to-talk, no chrome
KonAI.init({
sessionEndpoint: '/api/widget-session',
agenticAvatarId: '...',
pushToTalk: true,
allowInterruption: false, // avatar must finish each turn
videoFit: 'cover', // full-bleed portrait
videoPosition: 'top',
uiDetails: {
showHeader: false,
showStatus: false,
showTopButtons: { fullscreen: true, captions: false, textInput: false },
},
});Full-featured customer support widget
Every control enabled, themed to your brand, with analytics hooks.
// Full-featured customer support widget
KonAI.init({
sessionEndpoint: '/api/widget-session',
agenticAvatarId: '...',
theme: { primaryColor: '#0ea5e9', secondaryColor: '#6366f1' },
uiDetails: {
showControls: {
call: true, mute: true, interrupt: true, audioMute: true,
},
showTopButtons: {
fullscreen: true, captions: true, textInput: true, inputMode: true,
},
},
onTranscription: text => analytics.track('user_said', { text }),
onResponse: text => analytics.track('avatar_said', { text }),
});React Integration
Load the bundle in an effect, mount into a container element, and call destroy() on cleanup.
import { useEffect, useRef } from 'react';
export function KonProAvatar({ avatarId }) {
const widgetRef = useRef(null);
useEffect(() => {
let widget;
const script = document.createElement('script');
script.src = 'https://cdn.konpro.ai/widget/index.min.js';
script.onload = () => {
widget = window.KonAI.init({
sessionEndpoint: '/api/widget-session',
agenticAvatarId: avatarId,
containerId: 'konpro-avatar',
});
widgetRef.current = widget;
};
document.head.appendChild(script);
return () => widget?.destroy();
}, [avatarId]);
return <div id="konpro-avatar" style={{ width: 400, height: 600 }} />;
}Instance Methods
Call these on the widget instance returned from KonAI.init(...) or handed to onReady.
| Method | Notes |
|---|---|
| widget.init() | Fetches the session and mounts the UI. KonAI.init() calls this for you. |
| widget.destroy() | Tear down everything — call this before removing the container from the DOM. |
| widget.toggleCall() | Start / end the voice call programmatically. |
| widget.toggleMute() | Mute / unmute the mic. |
| widget.toggleAudioMute() | Mute / unmute speaker output (WebRTC only). |
| widget.toggleTextInput() | Show / hide the floating text-input bar. The call must be active. |
| widget.toggleCaptions() | Show / hide the transcript at runtime. |
| widget.toggleFullscreen() | Enter / exit fullscreen. |
| widget.interruptAI() | Interrupt the AI’s current response. No-op when allowInterruption: false. |
| widget.sendTextMessage(text) | Programmatically send text as if the user typed it. |
| widget.setInputMode(mode) | Switch between 'vad' and 'ptt' at runtime. |
| widget.getAvatarState() | Returns "idle" / "listening" / "thinking" / "speaking". |
| widget.getModeInfo() | Returns { audioOnly, usingLipsync, webrtcConnected, version, ... }. |
| widget.getSessionInfo() | Returns session metadata from the ready event (session ID, quotas, durations). |
| widget.isWebRTCConnected() | Boolean — true when WebRTC A/V is active. |
| widget.isAudioOnlyMode() | Boolean — true when in audio-only fallback. |
Avatar States
Emitted via onAvatarStateChange and available from widget.getAvatarState().
| State | Meaning |
|---|---|
| idle | Session live, waiting for speech. |
| listening | User speech detected by VAD (mid-utterance). |
| thinking | User’s speech committed; the LLM is generating a response. |
| speaking | AI audio/video output is playing. |
Content Security Policy (CSP)
If your host page has a strict CSP, allow these origins for the widget's runtime dependencies.
Baseline (voice + video, always needed)
script-src 'self' https://cdn.konpro.ai https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline';
connect-src 'self'
wss://api.konpro.ai
https://api.konpro.ai
https://inference.konpro.ai
https://cdn.jsdelivr.net;
img-src 'self' data: https:;
media-src 'self' blob:;
worker-src 'self' blob:;cdn.konpro.ai— the widget bundle.cdn.jsdelivr.net— VAD (@ricky0123/vad-web) andonnxruntime-webload at runtime via script-tag injection.wss://api.konpro.ai— the session WebSocket.https://api.konpro.ai— session REST calls.https://inference.konpro.ai— MediaSoup HTTP endpoints (session/connect, transport/connect, consumer/resume).style-src 'unsafe-inline'— the widget injects its stylesheet as a<style>block.media-src blob:—<video srcObject>MediaStream binding.worker-src blob:— ONNX Runtime spins up a Web Worker.img-src https:— avatar images and any display content.
Feature-gated additions
Add only what your avatar actually uses:
# Panorama (mediaType: 'panorama')
script-src ... https://cdn.jsdelivr.net;
connect-src ... https://cdn.jsdelivr.net https://<panorama-image-hosts>;
img-src ... https://<panorama-image-hosts>;
# Virtual tour (mediaType: 'tour')
frame-src https://studio.konpro.ai;
# Google Map (mediaType: 'city')
script-src ... https://maps.googleapis.com https://maps.gstatic.com;
connect-src ... https://*.googleapis.com https://*.google.com https://*.gstatic.com;
img-src ... https://maps.googleapis.com https://maps.gstatic.com https://*.googleusercontent.com;
style-src ... https://fonts.googleapis.com;
font-src ... https://fonts.gstatic.com;What NOT to allow
script-src 'unsafe-eval'— not required by the widget.script-src 'unsafe-inline'— not required. All widget JS loads via external<script src=...>.
To debug, load your page with the CSP applied and watch the console for Refused to load ... messages. Each blocked resource surfaces as a distinct violation; add the origin to the appropriate directive until the console is clean.
Permissions Policy (microphone / camera)
⚠️ CSP does not control device permissions — those are governed by the Permissions Policy header. Many hardened sites disable microphone access by default; if yours does, the widget's getUserMedia call silently rejects with no console error and the guest sees a stuck "Loading…" state.
If the widget is loaded directly on your page, the top-level document's Permissions Policy applies. Include at minimum:
Permissions-Policy: microphone=(self), camera=(self), autoplay=(self), fullscreen=(self)microphone— required for voice input.camera— only if you plan to use camera-based features; the widget doesn't currently consume the camera, so it's safe to omit.autoplay— the avatar's audio/video element auto-plays after the guest's tap-to-call gesture.fullscreen— powers the fullscreen button.
If the widget is embedded inside an <iframe>, the parent frame delegates permissions via the allow attribute. Both the parent's Permissions-Policy header and the iframe's allow attribute must grant microphone — the intersection wins. To verify, open DevTools → Application → Permissions Policy, or check document.featurePolicy.allowsFeature('microphone') in the console.
Deployment
PWA / standalone install contexts
The widget auto-detects standalone PWA mode and switches its fullscreen behavior from the native Fullscreen API to a CSS-class fallback. This covers Neat Frame kiosk devices, Android PWAs, and iOS home-screen installs — anywhere the native API is unreliable or missing. No config needed.
iframe embeds
If you embed the widget inside an <iframe> rather than including the script directly, the iframe needs:
<iframe
src="..."
allow="microphone; camera; autoplay; fullscreen"
allowfullscreen
></iframe>Without allow="microphone", getUserMedia will silently reject.
Old Android WebViews & bundle size
The widget uses the vendor-prefixed Fullscreen API (webkitRequestFullscreen) as a fallback for pre-M71 Chromium, and expands inset: 0 into individual longhands in its CSS fullscreen fallback (pre-M87 didn't parse the shorthand), so it should work on typical kiosk hardware. The base bundle is ~340KB minified; panorama support (~800KB) and Google Maps (~500KB) are lazy-loaded on first use and don't affect initial page load if unused.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| "Refused to display in a frame because…" | The tour iframe is blocked by the tour page’s frame-ancestors policy. Get your host origin allowlisted on studio.konpro.ai’s tour route. |
| White screen when clicking fullscreen | Usually a PWA / kiosk WebView issue. The widget auto-falls back to CSS fullscreen there, but the fallback relies on injecting !important overrides — ensure your CSP allows style-src ‘unsafe-inline’. |
| Mic permission never prompts | Serve the page over HTTPS (localhost also works), verify allow="microphone" if in an iframe, and check for a system-wide microphone privacy toggle. |
| Avatar starts talking then abruptly stops | Usually a stale CDN bundle. The current widget guards against double-interrupts and STT hallucinations — verify KonAI.version matches the latest release and cache-bust the CDN URL. |
| Phantom "Yea" / "Yes" / "OK" turns | STT hallucinations on ambient audio. Fixed in the current widget by requiring both an RMS noise floor and a VAD speech-probability gate on outgoing mic frames — verify you are on the latest bundle. |
| Double interrupt for a single utterance | The jitter-buffer flush after a real barge-in used to cause a phantom AI-restart. Fixed in the current widget via a suppression flag around the pause+resume cycle. Confirm the bundle version. |
| Google Map not rendering | Check the console for errors such as BillingNotEnabledMapError, verify the Maps JavaScript API and Map Tiles API are enabled on the GCP project, and confirm billing is enabled. |
| Kiosk buttons not appearing | The kiosk menu is server-driven. The server must include kioskEnabled: true in the ready WebSocket message — no widget-side config forces it on. |
Full Config Schema
Every supported option in one glance:
KonAI.init({
// ── Auth (pick ONE) ─────────────────────────────
sessionEndpoint: '/api/widget-session',
// apiKey: 'YOUR_KEY',
// getSessionToken: async () => ({ sessionToken, ... }),
agenticAvatarId: 'af1250af-...',
// ── Layout ─────────────────────────────────────
containerId: 'konpro-avatar', // omit to float
position: { // only used when floating
position: 'fixed',
bottom: '20px',
right: '20px',
width: '400px',
height: '600px',
},
// ── Video / Audio ──────────────────────────────
audioOnly: false,
enableLipsync: true,
enableWebRTC: true,
usePlaceholderVideo: true,
videoFit: 'contain', // 'contain' | 'cover'
videoPosition: 'top',
// ── Conversation behavior ──────────────────────
allowInterruption: true, // false = strict turn-taking
pushToTalk: false, // true = walkie-talkie mode
// ── Third-party integrations ───────────────────
googleMapsApiKey: 'AIza...', // only if using city display
// ── UI visibility ──────────────────────────────
uiDetails: {
showHeader: true,
showStatus: true,
showTranscript: true,
showAvatar: true,
showControls: {
call: true,
mute: true,
interrupt: false,
audioMute: false,
},
showTopButtons: {
fullscreen: true,
captions: true,
textInput: true,
inputMode: false,
},
},
// ── Theme ──────────────────────────────────────
theme: {
primaryColor: '#667eea',
secondaryColor: '#764ba2',
iconSize: 18,
},
// ── Advanced ───────────────────────────────────
apiBaseUrl: 'https://api.konpro.ai',
// ── Callbacks ──────────────────────────────────
onReady: (w) => console.log('ready', w),
onError: (err) => console.error(err),
onTranscription: (text) => console.log('user:', text),
onResponse: (text) => console.log('avatar:', text),
onAvatarStateChange: (state) => console.log('state:', state),
});Notes
- Always use session tokens in production - never expose your API key in client-side code.
- Session tokens are generated using the Create Widget Session endpoint.
- Configure
allowed_domainswhen creating sessions to restrict widget usage to specific domains. - Tools (display, panorama, tour, city, kiosk, QR upload) are configured on the avatar in the studio — the widget renders whatever the server pushes.
- Use the callbacks in
KonAI.init()to track user interactions and integrate with your analytics. - For more information about widget sessions, see Create Widget Session, Get Widget Session, and Delete Widget Session.