# 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

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

```json
{
  "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:
```css
: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
