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 thecompany_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.tsadopts 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=1 — AI 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.vuepicks bycompany.ai_calling.call_interface.design—'modern'→AIVoiceWidget.vue, elseAIVoiceWidgetClassic.vue(pre-redesign snapshot; don't evolve it). Modern addsconnect_messagesrotating headlines, provider-agnosticsetSpeakerMuted(), a type-instead mid-call chat mode, a ringing tone while connecting, and ~20 new i18n keys (en/sv/ar).ai-call-client-logevents 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_urlfromgetStartingPageUrl()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 onintent: 'other'; Nox admin-forced engage bypasses heuristic suppressors (cooldowns, 48h agent-connected window, URL rules) but not hard blockers —setAutoTriggerrefuses withtab_hidden | user_interacted | call_active. An accepted CALL trigger message becomes the ElevenLabsfirst_message(Ultravox gets it via system prompt); chat-trigger replies respect the pre-chat form (pendingAutoTriggerMessageparks the visitor reply until the form completes); messages render multi-line. Delivery receipts:ai-auto-trigger-deliveryreportsdisplayed/blocked_reasonontoAIAutoTriggerMessageModel→ admin broadcastvisitor-engage-result+ backendPOST /internal/engage-receipt;log-auto-triggerrecords client-side URL-rule triggers. - Multi-calendar booking: widget receives server-computed boolean
company.multi_calendar_enabled(thego_high_levelsubdoc is never sent to the widget). When on, booking step 1 is a calendar picker (cal.purpose); theget-available-slotssocket emit carriescalendar_idand the reply echoes it (so a slow default reply can't clobber a picked calendar); the pick reaches the backend book endpoint ascalendar_booking_meta.calendar_id. - GA4/GTM:
sendTrackEvent()postsknockknock:trackto the parent page, forwarded togtag/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()instores/widget.ts: ends any call in flight, then asks the host page to drop the iframe;block-useris listened for on all three sockets; runs once per page load. Session tracking andrecoded_session.jsrecording continue. - Agent takeover of a visitor call (
0a8e82f) —IncomingCallCallbacks.isOnVisitorCall()andEndCallOptions.replacedByAgentCall→ theend-callpayload carriesreplaced_by_agent_call: true;TakeoverOptions.replacesVisitorCallauto-joins the agent's room instead of answering "user on another call". ms-comm preservescall_statusand sends no missed-call email. CalendarBookingMeta.booking_typegains'jobber'(mapped fromcalendar_typecontainingjobber).- 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 stepv-ifchains 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
poweredByUrlcomputed: 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.