Files
helix-engage/docs/superpowers/specs/2026-04-05-website-widget-design.md
saridsa2 82ec843c6c docs: website widget spec + implementation plan
- 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>
2026-04-06 06:49:59 +05:30

10 KiB
Raw Permalink Blame History

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:

  1. Key signature is valid (HMAC)
  2. siteId exists and is active
  3. Referer or Origin header matches allowedOrigins for this site key
  4. 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:

  1. Department — dropdown populated from /api/widget/doctors
  2. Doctor — dropdown filtered by department, shows visiting hours
  3. Date — date picker (min: today, max: 30 days)
  4. Time Slot — grid of available slots from /api/widget/slots
  5. Patient Details — name, phone, age, gender, chief complaint
  6. Captcha — invisible reCAPTCHA v3 on submit
  7. 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