mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
- Streaming AI chat via Vercel AI SDK v6 UI message stream — tool-based
generative UI (pick_branch, list_departments, show_clinic_timings,
show_doctors, show_doctor_slots, suggest_booking). Typing indicator,
markdown suppressed, text parts hidden when widgets are rendered.
- Centralized Preact store (store.tsx) for visitor, leadId, captchaToken,
bookingPrefill, doctors roster, branches, selectedBranch — replaces prop
drilling across chat/book/contact tabs.
- Cloudflare Turnstile captcha gate rendered via light-DOM portal so it
renders correctly inside the shadow DOM (Turnstile CSS doesn't cross
shadow boundaries).
- Lead dedup helper (findOrCreateLeadByPhone, 24h phone window) shared
across chat-start / book / contact so one visitor == one lead. Booking
upgrades existing lead status NEW → APPOINTMENT_SET via updateLeadStatus.
- Pre-chat name+phone form captures the visitor; chat transcript logged
to leadActivity records after each stream.
- Booking wizard gains a branch step 0 (skipped for single-branch
hospitals); departments + doctors filtered by selectedBranch. Chat slot
picks prefill the booking details step and lock the branch.
- Window-level captcha gate, modal maximize mode, header badge showing
selected branch, widget font inherits from host page (fix :host { all:
initial } override).
- 23 FA Pro 7.1 duotone icons bundled — medical departments, nav, actions,
hospital/location-dot for branch context.
- main.ts: resolve public/ from process.cwd() so widget.js serves in both
dev and prod. tsconfig: exclude widget-src/public/data from server tsc.
- captcha.guard: switch from reCAPTCHA v3 to Cloudflare Turnstile verify.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
51 lines
1.7 KiB
TypeScript
51 lines
1.7 KiB
TypeScript
import { render } from 'preact';
|
|
import { initApi, fetchInit } from './api';
|
|
import { loadTurnstile } from './captcha';
|
|
import { Widget } from './widget';
|
|
import type { WidgetConfig } from './types';
|
|
|
|
const init = async () => {
|
|
const script = document.querySelector('script[data-key]') as HTMLScriptElement | null;
|
|
if (!script) { console.error('[HelixWidget] Missing data-key attribute'); return; }
|
|
|
|
const key = script.getAttribute('data-key') ?? '';
|
|
const baseUrl = script.src.replace(/\/widget\.js.*$/, '');
|
|
|
|
initApi(baseUrl, key);
|
|
|
|
let config: WidgetConfig;
|
|
try {
|
|
config = await fetchInit();
|
|
} catch (err) {
|
|
console.error('[HelixWidget] Init failed:', err);
|
|
return;
|
|
}
|
|
|
|
// Preload Turnstile script so the captcha gate renders quickly on first open.
|
|
// No-op if siteKey is empty (dev mode — backend fails open).
|
|
if (config.captchaSiteKey) {
|
|
loadTurnstile().catch(() => {
|
|
console.warn('[HelixWidget] Turnstile preload failed — gate will retry on open');
|
|
});
|
|
}
|
|
|
|
// Create shadow DOM host
|
|
// No font-family here — we want the widget to inherit from the host page.
|
|
const host = document.createElement('div');
|
|
host.id = 'helix-widget-host';
|
|
host.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:999999;';
|
|
document.body.appendChild(host);
|
|
|
|
const shadow = host.attachShadow({ mode: 'open' });
|
|
const mountPoint = document.createElement('div');
|
|
shadow.appendChild(mountPoint);
|
|
|
|
render(<Widget config={config} shadow={shadow} />, mountPoint);
|
|
};
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|