Skip to main content

widget — Embeddable Visitor Widget

The lightweight Vue 3 app that customers install on their sites. It launches chat/calls, talks to the AI agents, and captures the rrweb recording.

  • Stack: Vue 3 + TS + Vite, Pinia, axios, rrweb, vue-tel-input, ElevenLabs client, Twilio Video. Deliberately lean (no heavy dashboard deps).
  • Output: a static SPA/iframe bundle, served by nginx (Docker) and fetched fresh by customer sites — no auto-updater (unlike desktop).

Embedding

A loader snippet on the customer page mounts the widget in an iframe (#app). Install methods (script tag, GTM, WordPress, Shopify) are covered in the customer docs; internally the key points are:

  • The widget holds a persisted user_session_id (localStorage UUID, survives refreshes) and the company_id.
  • It communicates with the host page via postMessage (services/postMessage.ts) — resize, user-interaction signals (to suppress auto-trigger), and the page summary used for page rules.
  • Agent K landing: stores/widget.ts adopts a session id from a /k/<id> path (survives GHL redirects) or ?knock_id= param, so an Agent K click continues the known lead's session.
  • Consent gating lives in the host-page loader (backend/public/widget.js), not this repo — see Consent Gating.

Headless SDK

backend/public/kk-sdk.js (window.KnockKnockSDK) embeds the widget iframe with &headless=1AI chat + AI voice only, no widget UI/video/human escalation. Bridge: src/services/headlessBridge.ts; postMessage protocol prefixed knockknock:sdk_ — commands sdk_chat_send, sdk_voice_start/end/mute; events sdk_ready, sdk_ack, sdk_error, sdk_chat_message/typing/seen, sdk_voice_status/transcript/level/mute_state. requestCall(phone, opts) posts POST /ai-outbound/sdk-trigger (E.164 required; secret stays server-side; rejects outbound_unavailable / rate_limited / request_failed).

Structure (src/)

main.ts create app, init Pinia; rrweb (self-hosted 2.1.1 from
/widget/rrweb-all.min.js) lazy-loaded only when the parent
sends the cross-origin-record message (~350KB saved)
stores/widget.ts the main store — most widget state & lifecycle, page rules,
auto-triggers, Agent K session adoption, spoken-link extraction
services/
api.ts fetch-based HTTP client
socket.ts Socket.IO connections (backend, ms-communication; the
ms-sessions socket is no longer opened — it had no consumer)
call.ts Twilio video call setup & permissions
aiCall.ts AI voice call; @elevenlabs/client + Ultravox dynamically
imported at call start
headlessBridge.ts headless-SDK postMessage bridge
postMessage.ts parent-window IPC
sound.ts ringtones/notifications
components/ Widget (launcher), ChatWidget, IncomingCallScreen,
AICallTakeoverScreen, ProductSlider
components/widgets/ launcher variants + AIVoiceWidgetSwitch → AIVoiceWidget
(modern) / AIVoiceWidgetClassic (frozen snapshot)
composables/ useChatLogic, useI18n
utils/sanitizeMessageHtml.ts chat HTML sanitizer (see below)
i18n/messages.ts single-file locales — en | sv | ar (RTL via [dir='rtl'] CSS)

What it captures & sends

  • Session recording: rrweb events emitted to the parent and streamed to ms-sessions (widget:event).
  • Sockets (keyed by company_id + user_session_id):
    • backend — company config, session lifecycle.
    • ms-communication — chat messages, AI chat streaming, chatbot flow, and server-pushed Nox auto-engage (nox-auto-engage-message).
  • REST: /companies/{id}, /files/{id}/get, /user-session/{id}/, calculate-lead-auto-connect-message.
  • Events: open/close, call start/end, chat expand, user-interaction, permission prompts.

Call & chat UX

  • Video: Twilio Video (WebRTC) with mute toggles.
  • Voice UI designs: components/widgets/AIVoiceWidgetSwitch.vue picks by company.ai_calling.call_interface.design'modern'AIVoiceWidget.vue, else AIVoiceWidgetClassic.vue (pre-redesign snapshot; don't evolve it). Modern adds connect_messages rotating headlines, provider-agnostic setSpeakerMuted(), a type-instead mid-call chat mode, a ringing tone while connecting, and ~20 new i18n keys (en/sv/ar). ai-call-client-log events ship client-side call diagnostics.
  • AI voice: ElevenLabs realtime — streaming transcripts, frequency visualization, one/two-way modes, AI-call takeover screen, agent language auto-switch (language_detection). Spoken-link extraction (stores/widget.ts): phonetic URL reconstruction from transcripts ("dot"/"slash" tokens), TLD allowlist for bare hosts, "at"-preceded hosts treated as emails not URLs, links auto-dismiss after 10s (VOICE_CALL_LINKS_TTL_MS).
  • Chat: Socket.IO message stream supporting human agents, an AI chatbot (multi-turn flow with options/products/booking), and hybrid escalation (AI → human queue with a ~3-min fallback). Phone inputs use vue-tel-input (E.164) in pre-chat/chat/voice screens.
  • Chat sanitization: src/utils/sanitizeMessageHtml.ts — allow-list of formatting tags, all attributes stripped except safe hrefs; disallowed tags are escaped (rendered visibly), not removed; FIFO memo (400 entries).
  • Chat page attribution: message and reset-chat emits carry page_url from getStartingPageUrl() so transcripts know the chat's starting URL.
  • Auto-triggers: ai_auto_trigger.allow_second_message (default true) lets the auto-connect agent send a follow-up on intent: 'other'; Nox admin-forced engage bypasses heuristic suppressors (cooldowns, 48h agent-connected window, URL rules) but not hard blockers — setAutoTrigger refuses with tab_hidden | user_interacted | call_active. An accepted CALL trigger message becomes the ElevenLabs first_message (Ultravox gets it via system prompt); chat-trigger replies respect the pre-chat form (pendingAutoTriggerMessage parks the visitor reply until the form completes); messages render multi-line. Delivery receipts: ai-auto-trigger-delivery reports displayed/blocked_reason onto AIAutoTriggerMessageModel → admin broadcast visitor-engage-result + backend POST /internal/engage-receipt; log-auto-trigger records client-side URL-rule triggers.
  • Multi-calendar booking: widget receives server-computed boolean company.multi_calendar_enabled (the go_high_level subdoc is never sent to the widget). When on, booking step 1 is a calendar picker (cal.purpose); the get-available-slots socket emit carries calendar_id and the reply echoes it (so a slow default reply can't clobber a picked calendar); the pick reaches the backend book endpoint as calendar_booking_meta.calendar_id.
  • GA4/GTM: sendTrackEvent() posts knockknock:track to the parent page, forwarded to gtag/dataLayer; legacy event names kept (KnockKnock_open_chat, KnockKnock_open_chatbot). openChat({track:false}) for programmatic (non-visitor) opens.
  • Products/booking: inline product cards (Shopify/custom) and calendar booking (Calendly/Google/GHL) during chat or AI call. Launcher style is now resolved per-device (mobile_launcher_style).

Versioning & deployment

npm run version:bump (--minor/--major) bumps package.json. npm run build type-checks and produces the dist/ bundle. Multi-stage Docker (Node 22 builder → nginx Alpine) pushed to ECR via buildspec.yml. Customers always pull the latest bundle from the CDN/domain.

2026-08/09 changes

  • Blocked visitors — one applyBlocked() in stores/widget.ts: ends any call in flight, then asks the host page to drop the iframe; block-user is listened for on all three sockets; runs once per page load. Session tracking and recoded_session.js recording continue.
  • Agent takeover of a visitor call (0a8e82f) — IncomingCallCallbacks.isOnVisitorCall() and EndCallOptions.replacedByAgentCall → the end-call payload carries replaced_by_agent_call: true; TakeoverOptions.replacesVisitorCall auto-joins the agent's room instead of answering "user on another call". ms-comm preserves call_status and sends no missed-call email.
  • CalendarBookingMeta.booking_type gains 'jobber' (mapped from calendar_type containing jobber).
  • Audio: ScriptProcessor buffer is rate-dependent — sampleRate > 24000 ? 8192 : 4096 — so chunk duration stays ~170–186 ms at 44.1/48k (aiCall.ts).
  • Reschedule panel (66e095c): the step v-if chains were merged (step 0 rendered two panels; Confirm submitted with no date), the intent regex covers "re schedule"/"rebook"/bare "reschedule", and Google's 'primary' is no longer passed as a calendar override.
  • Powered by is one poweredByUrl computed: whitelabel override URL → affiliate link → knockknockapp.ai, only for direct (non-agency) companies with the override off; override-on-with-empty-URL removes the link.
  • Served scripts are obfuscated + white-labelled — see Widget installation.