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 — check KonAI.version in 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 inputMode dropdown, currently set to "Voice" (VAD).
  • Avatar stage — the lipsync video, framed by videoFit and videoPosition.
  • 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.
KonAI widget mid-call: avatar video with the fullscreen, captions, text input and Voice input-mode buttons at the top-left, a transcript panel showing the exchange, and mic-mute plus end-call buttons at the bottom

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_domains via 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.

html
<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:

  1. A <div> on your page where the widget mounts.
  2. The CDN <script> tag.
  3. KonAI.init({...}) with an auth method and either agenticAvatarId or 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.

html
<!-- 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

javascript
KonAI.init({
    sessionEndpoint: '/api/widget-session',
    agenticAvatarId: 'af1250af-...',
});

// The widget POSTs { agenticAvatarId, audioOnly } to your endpoint
// and expects { sessionToken } back.

Backend - Generate Session Token

javascript
// 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.

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

javascript
// ⚠️ 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

OptionTypeDefaultNotes
apiKeystring⚠️ Development only
sessionEndpointstringRecommended for production
getSessionToken() => Promise<sessionData>Custom auth flows
agenticAvatarIdstringRequired with apiKey; optional with the other two

Layout & Positioning

OptionTypeDefaultNotes
containerIdstringID of a DOM element to mount inside. When set, position is ignored and the widget fills the container.
positionobject{ 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 by position.
  • Container mode (containerId set): the widget renders 100%×100% inside the given element — you control sizing via that element's CSS.

Video / Audio Mode

OptionTypeDefaultNotes
audioOnlybooleanfalseForce audio-only. Disables lipsync + WebRTC video, shows an animated circle with the avatar’s initials or image.
enableLipsyncbooleantrueRequest server-side lipsync. Falls back gracefully if unavailable.
enableWebRTCbooleantrueAttempt WebRTC / MediaSoup for lowest-latency A/V. Falls back to audio-only on failure.
usePlaceholderVideobooleantrueShow 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.
videoPositionstring"top"CSS object-position (e.g. "top", "center", "50% 20%"). Framing for portrait avatars.

Conversation Behavior

OptionTypeDefaultNotes
allowInterruptionbooleantrueWhen false: no barge-in, interrupt button hidden, text-send disabled while the avatar speaks, widget.interruptAI() is a no-op.
pushToTalkbooleanfalseEnables PTT mode — mic defaults muted, replaces the mute button with a "hold to talk" button, disables VAD-driven commit.

Third-party Keys & Advanced

OptionTypeDefaultNotes
googleMapsApiKeystringGoogle 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.
apiBaseUrlstring"https://api.konpro.ai"Override for a self-hosted / staging Konpro backend.

Theme

The theme option accepts an object:

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

javascript
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: true shows the defaults, false hides everything in the row, and an object merges with the defaults so you only pass overrides.
  • showTopButtons follows the same pattern.
  • captions and inputMode are opt-in — they default to hidden even when showTopButtons: true is used as shorthand.

Callbacks / Events

OptionSignatureNotes
onReady(widget) => voidFires when init completes. Receives the widget instance.
onError(Error) => voidFires on unrecoverable errors (auth failure, WS error, session failure, etc.).
onTranscription(text: string) => voidFires on the user’s final STT commit — not on partials.
onResponse(text: string) => voidFires when the AI completes a full response.
onAvatarStateChange(state: string) => voidFires 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.

EventDirectionEffect
{ type: 'display', mediaType: 'city', cityProjects, cityRenderer, cityMapBounds }Server → widgetMount the map
{ type: 'display', action: 'navigate', lat, lng, projectId }Server → widgetPan/zoom to a project
{ type: 'city_street_view', lat, lng, heading?, pitch? }Server → widgetSwap to StreetViewPanorama
{ type: 'city_project_tour', imageUrl }Server → widgetOpen the project’s immersive tour
{ type: 'city_navigated' | 'city_arrived', projectId }Widget → serverAck 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):

ButtonKeyDefaultNotes
Start / end callcallonGreen phone icon; goes red when connected.
Mute micmuteonToggles VAD mic mute. Replaced by the PTT button when pushToTalk: true.
Interrupt AIinterruptoffManually stop the avatar mid-response. Always visible on Firefox, where automatic voice barge-in is limited. Hidden entirely when allowInterruption: false.
Speaker muteaudioMuteoffSilence 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):

ButtonKeyDefaultNotes
FullscreenfullscreenonNative Fullscreen API where supported; automatic CSS fallback for PWA / iframe / old Android WebViews.
Captions (CC)captionsonRuntime toggle for transcript visibility. Hidden if showTranscript: false.
Text inputtextInputonReveals a floating text-input bar for typed messages. Only visible during an active call.
Input modeinputModeoffOpt-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.

javascript
// 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.

javascript
// 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.

javascript
// 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.

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

MethodNotes
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().

StateMeaning
idleSession live, waiting for speech.
listeningUser speech detected by VAD (mid-utterance).
thinkingUser’s speech committed; the LLM is generating a response.
speakingAI 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)

text
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) and onnxruntime-web load 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:

text
# 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:

text
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:

html
<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

SymptomCause & 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 fullscreenUsually 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 promptsServe 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 stopsUsually 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" turnsSTT 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 utteranceThe 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 renderingCheck 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 appearingThe 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:

javascript
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_domains when 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.

Table of Contents