mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
- 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>
338 lines
10 KiB
Markdown
338 lines
10 KiB
Markdown
# 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
|