- Widget design spec (embeddable AI chat + booking + lead capture) - Implementation plan (6 tasks) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 KiB
Website Widget — Embeddable AI Chat + Appointment Booking
Date: 2026-04-05 Status: Draft
Overview
A single JavaScript file that hospitals embed on their website via a <script> tag. Renders a floating chat bubble that opens to an AI chatbot (hospital knowledge base), appointment booking flow, and lead capture form. Themed to match the hospital's branding. All write endpoints are captcha-gated.
Embed Code
<script src="https://engage-api.srv1477139.hstgr.cloud/widget.js"
data-key="a8f3e2b1.7c4d9e6f2a1b8c3d5e0f4a2b6c8d1e3f7a9b0c2d4e6f8a1b3c5d7e9f0a2b"></script>
The data-key is an HMAC-signed token: {siteId}.{hmacSignature}. Cannot be guessed or forged without the server-side secret.
Architecture
Hospital Website (any tech stack)
└─ <script data-key="xxx"> loads widget.js from sidecar
└─ Widget initializes:
1. GET /api/widget/init?key=xxx → validates key, returns theme + config
2. Renders shadow DOM (CSS-isolated from host page)
3. All interactions go to /api/widget/* endpoints
Sidecar (helix-engage-server):
└─ src/widget/
├── widget.controller.ts — REST endpoints for the widget
├── widget.service.ts — lead creation, appointment booking, key validation
├── widget.guard.ts — HMAC key validation + origin check
├── captcha.guard.ts — reCAPTCHA/Turnstile verification
└── widget-keys.service.ts — generate/validate site keys
Widget Bundle:
└─ packages/helix-engage-widget/
├── src/
│ ├── main.ts — entry point, reads data-key, initializes
│ ├── widget.ts — shadow DOM mount, theming, tab routing
│ ├── chat.ts — AI chatbot (streaming)
│ ├── booking.ts — appointment booking flow
│ ├── contact.ts — lead capture form
│ ├── captcha.ts — captcha integration
│ ├── api.ts — HTTP client for widget endpoints
│ └── styles.ts — CSS-in-JS (injected into shadow DOM)
├── vite.config.ts — library mode, single IIFE bundle
└── package.json
Sidecar Endpoints
All prefixed with /api/widget/. Public endpoints validate the site key. Write endpoints require captcha.
| Method | Path | Auth | Captcha | Description |
|---|---|---|---|---|
| GET | /init |
Key | No | Returns theme, config, captcha site key |
| POST | /chat |
Key | Yes (first message only) | AI chat stream (same knowledge base as agent AI) |
| GET | /doctors |
Key | No | Department + doctor list with visiting hours |
| GET | /slots |
Key | No | Available time slots for a doctor + date |
| POST | /book |
Key | Yes | Create appointment + lead + patient |
| POST | /lead |
Key | Yes | Create lead (contact form submission) |
| POST | /keys/generate |
Admin JWT | No | Generate a new site key for a hospital |
| GET | /keys |
Admin JWT | No | List all site keys |
| DELETE | /keys/:siteId |
Admin JWT | No | Revoke a site key |
Site Key System
Generation
siteId = uuid v4 (random)
payload = siteId
signature = HMAC-SHA256(payload, SERVER_SECRET)
key = `${siteId}.${signature}`
The SERVER_SECRET is an environment variable on the sidecar. Never leaves the server.
Validation
input = "a8f3e2b1.7c4d9e6f2a1b8c3d5e0f4a2b6c8d1e3f7a9b0c2d4e6f8a1b3c5d7e9f0a2b"
[siteId, signature] = input.split('.')
expectedSignature = HMAC-SHA256(siteId, SERVER_SECRET)
valid = timingSafeEqual(signature, expectedSignature)
Storage
Site keys are stored in Redis (already running in the stack):
Key: widget:keys:{siteId}
Value: JSON { hospitalName, allowedOrigins, active, createdAt }
TTL: none (persistent until revoked)
Example:
widget:keys:a8f3e2b1 → {
"hospitalName": "Global Hospital",
"allowedOrigins": ["https://globalhospital.com", "https://www.globalhospital.com"],
"createdAt": "2026-04-05T10:00:00Z",
"active": true
}
CRUD via SessionService (getCache/setCache/deleteCache/scanKeys) — same pattern as caller cache and agent names.
Origin Validation
On every widget request, the sidecar checks:
- Key signature is valid (HMAC)
siteIdexists and is activeRefererorOriginheader matchesallowedOriginsfor this site key- If origin doesn't match → 403
Widget UI
Collapsed State (Floating Bubble)
- Position: fixed bottom-right, 20px margin
- Size: 56px circle
- Shows hospital logo (from theme)
- Pulse animation on first load
- Click → expands panel
- Z-index: 999999 (above host page content)
Expanded State (Panel)
- Size: 380px wide × 520px tall
- Anchored bottom-right
- Shadow DOM container (CSS isolation from host page)
- Header: hospital logo + name + close button
- Three tabs: Chat (default) | Book | Contact
- All styled with brand colors from theme
Chat Tab (Default)
- AI chatbot interface
- Streaming responses (same endpoint as agent AI, but with widget system prompt)
- Quick action chips: "Doctor availability", "Clinic timings", "Book appointment", "Treatment packages"
- If AI detects it can't help → shows: "An agent will call you shortly" + lead capture fields (name, phone)
- First message triggers captcha verification (invisible reCAPTCHA v3)
Book Tab
Step-by-step appointment booking:
- Department — dropdown populated from
/api/widget/doctors - Doctor — dropdown filtered by department, shows visiting hours
- Date — date picker (min: today, max: 30 days)
- Time Slot — grid of available slots from
/api/widget/slots - Patient Details — name, phone, age, gender, chief complaint
- Captcha — invisible reCAPTCHA v3 on submit
- Confirmation — "Appointment booked! Reference: ABC123. We'll send a confirmation SMS."
On successful booking:
- Creates patient (if new phone number)
- Creates lead with
source: 'WEBSITE' - Creates appointment linked to patient + doctor
- Rules engine scores the lead
- Pushes to agent worklist
- Real-time notification to agents
Contact Tab
Simple lead capture form:
- Name (required)
- Phone (required)
- Interest / Department (dropdown, optional)
- Message (textarea, optional)
- Captcha on submit
- Success: "Thank you! An agent will call you shortly."
On submit:
- Creates lead with
source: 'WEBSITE',interestedService: interest - Rules engine scores it
- Pushes to agent worklist + notification
Theming
Widget fetches theme from /api/widget/init:
{
"brand": { "name": "Global Hospital", "logo": "https://..." },
"colors": {
"primary": "rgb(29 78 216)",
"primaryLight": "rgb(219 234 254)",
"text": "rgb(15 23 42)",
"textLight": "rgb(100 116 139)"
},
"captchaSiteKey": "6Lc..."
}
Colors are injected as CSS variables inside the shadow DOM:
:host {
--widget-primary: rgb(29 78 216);
--widget-primary-light: rgb(219 234 254);
--widget-text: rgb(15 23 42);
--widget-text-light: rgb(100 116 139);
}
All widget elements reference these variables. Changing the theme API → widget auto-updates on next load.
Widget System Prompt (AI Chat)
Different from the agent AI prompt — tailored for website visitors:
You are a virtual assistant for {hospitalName}.
You help website visitors with:
- Doctor availability and visiting hours
- Clinic locations and timings
- Health packages and pricing
- Booking appointments
- General hospital information
RULES:
1. Be friendly and welcoming — this is the hospital's first impression
2. If someone wants to book an appointment, guide them to the Book tab
3. If you can't answer a question, say "I'd be happy to have our team call you" and ask for their name and phone number
4. Never give medical advice
5. Keep responses under 80 words — visitors are scanning, not reading
6. Always mention the hospital name naturally in first response
KNOWLEDGE BASE:
{same KB as agent AI — clinics, doctors, packages, insurance}
Captcha
- Provider: Google reCAPTCHA v3 (invisible) or Cloudflare Turnstile
- When: On first chat message, appointment booking submit, lead form submit
- How: Widget loads captcha script, gets token, sends with request. Sidecar validates via provider API before processing.
- Fallback: If captcha fails to load (ad blocker), show a simple challenge or allow with rate limiting
Widget Bundle
Tech Stack
- Preact — 3KB, React-compatible API, sufficient for the widget UI
- Vite — library mode build, outputs single IIFE bundle
- CSS-in-JS — styles injected into shadow DOM (no external CSS files)
- Target: ~60KB gzipped (Preact + UI + styles)
Build Output
dist/
└── widget.js — single IIFE bundle, self-contained
Serving
Sidecar serves widget.js as a static file:
GET /widget.js → serves dist/widget.js with Cache-Control: public, max-age=3600
Lead Flow (all channels)
Widget submit (chat/book/contact)
→ POST /api/widget/lead or /api/widget/book
→ captcha validation
→ key + origin validation
→ create patient (if new phone)
→ create lead (source: WEBSITE, channel metadata)
→ rules engine scores lead (source weight, campaign weight)
→ push to agent worklist
→ WebSocket notification to agents (bell + toast)
→ response to widget: success + reference number
Rate Limiting
| Endpoint | Limit |
|---|---|
/init |
60/min per IP |
/chat |
10/min per IP |
/doctors, /slots |
30/min per IP |
/book |
5/min per IP |
/lead |
5/min per IP |
Scope
In scope:
- Widget JS bundle (Preact + shadow DOM + theming)
- Sidecar widget endpoints (init, chat, doctors, slots, book, lead)
- Site key generation + validation (HMAC)
- Captcha integration (reCAPTCHA v3)
- Lead creation with worklist integration
- Appointment booking end-to-end
- Origin validation
- Rate limiting
- Widget served from sidecar
Out of scope:
- Live agent chat in widget (shows "agent will call you" instead)
- Widget analytics/tracking dashboard
- A/B testing widget variations
- Multi-language widget UI
- File upload in widget
- Payment integration in widget