docs: website widget operations guide + archive widget source

- Comprehensive docs: embed snippet, key management, API endpoints,
  chat/booking/contact flows, lead dedup, reCAPTCHA, branding, deploy
  checklist, troubleshooting
- Widget Preact source archived in packages/widget-src/ (was only on
  local machine, not tracked in any repo)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 06:39:23 +05:30
parent 014b27cf90
commit 9cb4d1c122
14 changed files with 1078 additions and 0 deletions

239
docs/website-widget.md Normal file
View File

@@ -0,0 +1,239 @@
# Website Chat Widget — Operations Guide
## Overview
A floating chat/booking/contact widget that hospitals embed on their website via a single `<script>` tag. Visitors can:
- **Chat** with an AI assistant (powered by OpenAI/Anthropic)
- **Book** appointments (department → doctor → date → slot)
- **Contact** the hospital (name, phone, interest — creates a lead in the CRM)
All interactions create or update leads in Helix Engage, so CC agents see the full visitor journey when they call back.
---
## Embed Snippet
Add this to any page on the hospital website (before `</body>`):
```html
<script src="https://ramaiah.engage.healix360.net/widget.js"
data-key="956018d178194fb9.313657fbc8a912b9cf8c93b9a51dfb209022fcd9910bd5abc7aa16dfaacf98a3">
</script>
```
| Parameter | Description |
|---|---|
| `src` | The sidecar URL + `/widget.js` |
| `data-key` | Site key — generated and rotatable from the admin portal |
The widget renders in a **shadow DOM** — its styles don't leak into or get affected by the host website's CSS.
---
## Admin Configuration
### Settings Page
URL: `https://ramaiah.engage.healix360.net/settings/widget` (login as admin/supervisor)
| Setting | Description |
|---|---|
| **Enabled** | Master kill switch — when off, widget.js no-ops |
| **Site Key** | Read-only HMAC-signed key. Copy-to-clipboard for the embed snippet |
| **Site ID** | Read-only identifier |
| **Rotate Key** | Generates a new key, invalidates the old embed snippet |
| **Hosting URL** | Public base URL for widget.js. Leave blank to use same origin as sidecar |
| **Allowed Origins** | Whitelist of domains allowed to embed. Empty = any origin (test mode) |
| **Show on Login Page** | Toggle to display widget on the Helix Engage login page |
### Configuration API
```bash
# Read config (public — no auth)
curl https://ramaiah.engage.healix360.net/api/config/widget
# Read full config (admin)
curl https://ramaiah.engage.healix360.net/api/config/widget/admin
# Update config
curl -X PUT https://ramaiah.engage.healix360.net/api/config/widget \
-H "Content-Type: application/json" \
-d '{"enabled": true, "allowedOrigins": ["https://ramaiahmedical.com"]}'
# Rotate site key (invalidates old embeds)
curl -X POST https://ramaiah.engage.healix360.net/api/config/widget/rotate-key
```
---
## Widget Key Security
- Key format: `{siteId}.{hmacSignature}`
- HMAC computed as: `sha256(siteId, secret=WIDGET_SECRET env var)`
- Validated with `timingSafeEqual()` to prevent timing attacks
- Origin checked against `allowedOrigins` whitelist
- Empty whitelist = test mode (any origin allowed)
### For Production
Before going live on a real hospital website:
1. Set `allowedOrigins` to the hospital's domain(s): `["https://ramaiahmedical.com", "https://www.ramaiahmedical.com"]`
2. Ensure `WIDGET_SECRET` env var is set in the sidecar (auto-generated on first run if missing)
---
## Widget API Endpoints
All endpoints require `WidgetKeyGuard` (key as query param `?key=...` or header `X-Widget-Key`).
| Endpoint | Method | Purpose |
|---|---|---|
| `/api/widget/init` | GET | Returns brand name, logo, colors, reCAPTCHA key |
| `/api/widget/doctors` | GET | All doctors with departments, specialties, fees, visit slots |
| `/api/widget/slots?doctorId=X&date=YYYY-MM-DD` | GET | Available time slots for a doctor on a date |
| `/api/widget/book` | POST | Book appointment (requires captcha token) |
| `/api/widget/lead` | POST | Create lead from contact form (requires captcha token) |
| `/api/widget/chat-start` | POST | Start chat session — body: `{name, phone}`, returns `{leadId}` |
| `/api/widget/chat` | POST | Stream AI reply — body: `{leadId, messages[], branch?}`, returns SSE stream |
---
## How the Chat Flow Works
1. Visitor opens widget → Chat tab shows name + phone form
2. Visitor enters name + phone → `POST /api/widget/chat-start` → returns `leadId`
- Creates or finds existing lead by phone (deduplication)
3. Visitor types a message → `POST /api/widget/chat` with `leadId` + message history
- AI streams a reply via Server-Sent Events
- AI has tools: branch selection, doctor search, slot lookup, booking suggestions
4. After conversation ends, transcript is saved to lead activity timeline
5. CC agent sees the WhatsApp/chat history when calling the patient back
---
## How the Booking Flow Works
1. Visitor opens widget → Book tab
2. Selects department → fetches doctor list
3. Selects doctor → fetches available dates/slots
4. Enters patient name, phone, chief complaint
5. `POST /api/widget/book` with captcha token
6. Creates patient + lead + appointment in the platform
7. Shows confirmation with reference number
---
## How Lead Capture Works
1. Visitor opens widget → Contact tab
2. Enters name, phone, interest, optional message
3. `POST /api/widget/lead` with captcha token
4. Creates lead with source "Website Widget"
5. Shows success confirmation
---
## Lead Deduplication
All three flows (chat, book, contact) use `CallerResolutionService` to find existing leads by phone number. If a visitor chats, then books, then contacts — all within 24 hours — they create ONE lead, not three. Activities are appended to the same lead.
---
## reCAPTCHA Protection
- Booking and lead endpoints use reCAPTCHA v3 (invisible — no user friction)
- Chat endpoints do NOT use reCAPTCHA (session already verified by name+phone gate)
- reCAPTCHA site key returned by `/api/widget/init`
- Server validates tokens via Google reCAPTCHA API
- Captcha can be bypassed for webhooks using `captchaToken: "webhook-bypass"`
---
## Branding & Theming
The widget pulls branding from the sidecar theme config:
- **Hospital name** — displayed in the widget header
- **Logo** — shown in the header
- **Brand color** — applied to buttons, links, active states
- **Location** — shown under the hospital name
Configure via: `https://ramaiah.engage.healix360.net/settings` → Theme section
---
## Widget Source Code
| Location | Description |
|---|---|
| `packages/widget-src/` | Preact source (chat.tsx, booking.tsx, contact.tsx, api.ts, etc.) |
| `public/widget.js` | Compiled IIFE bundle (served by NestJS static assets) |
| `src/widget/` | Backend API (controller, service, chat service, key guard) |
| `src/config/widget-keys.service.ts` | Key generation + HMAC validation |
| `src/config/widget-config.service.ts` | Config file management (data/widget.json) |
### Rebuilding widget.js
```bash
cd packages/widget-src
npm install
npm run build
# Output goes to ../../public/widget.js (configured in vite.config.ts)
```
After rebuilding:
1. Commit `public/widget.js`
2. Build Docker image and deploy sidecar
3. Widget auto-updates on next page load (1h cache)
---
## Deployment Checklist
### First-time setup on a new tenant:
1. **Sidecar serves widget.js** — verify `https://{tenant}.engage.healix360.net/widget.js` returns JS, not HTML
2. **Caddy routing**`/widget.js` must route to sidecar, not frontend. Add to the `@api path` matcher in Caddyfile:
```
@api path /api/* /widget.js /graphql /auth/* ...
```
3. **Widget config exists** — `GET /api/config/widget` should return `{enabled: true, key: "..."}`
4. **Generate key if needed** — `POST /api/widget/keys/generate`
5. **Set allowed origins** (for production) — `PUT /api/config/widget` with `allowedOrigins: ["https://hospital.com"]`
6. **Test embed** — paste the `<script>` tag into a test HTML page and verify the widget appears
7. **Verify AI** — start a chat, confirm AI responds (requires `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in sidecar env)
### Current deployment (Ramaiah):
| Item | Value |
|---|---|
| Widget URL | `https://ramaiah.engage.healix360.net/widget.js` |
| Site Key | `956018d178194fb9.313657fbc8a912b9cf8c93b9a51dfb209022fcd9910bd5abc7aa16dfaacf98a3` |
| Site ID | `956018d178194fb9` |
| Allowed Origins | Empty (test mode — any origin) |
| Login Page Widget | Enabled |
| reCAPTCHA | Configured (key returned by `/api/widget/init`) |
---
## Troubleshooting
### Widget doesn't appear
1. Check browser console for errors
2. Verify `VITE_API_URL` in the frontend build points to the sidecar URL (not `localhost`)
3. Verify `/api/config/widget` returns `enabled: true` and `embed.loginPage: true`
4. Verify `/widget.js` returns actual JavaScript (not HTML from the frontend catch-all)
### "leadId required" error
The chat requires a `chat-start` call first (name + phone → leadId). If the widget skips this step, it's using an old `widget.js`. Clear browser cache or deploy the correct version from commit `aa41a2a`.
### Chat returns "AI not configured"
Missing `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in sidecar environment. Check with:
```bash
docker exec sidecar-ramaiah env | grep -i 'OPENAI\|ANTHROPIC\|AI_PROVIDER'
```
### CORS errors
The widget key guard validates the request origin against `allowedOrigins`. If empty, any origin is allowed. If set, the host website's domain must be in the list.
### Widget shows on login page but not on hospital website
The login page injection code is in `helix-engage/src/pages/login.tsx`. For external hospital websites, the embed snippet must be manually added to their HTML. There's no automatic injection for third-party sites.