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>
76 lines
2.7 KiB
TypeScript
76 lines
2.7 KiB
TypeScript
import type { WidgetConfig, Doctor, TimeSlot } from './types';
|
|
|
|
let baseUrl = '';
|
|
let widgetKey = '';
|
|
|
|
export const initApi = (url: string, key: string) => {
|
|
baseUrl = url;
|
|
widgetKey = key;
|
|
};
|
|
|
|
const headers = () => ({
|
|
'Content-Type': 'application/json',
|
|
'X-Widget-Key': widgetKey,
|
|
});
|
|
|
|
export const fetchInit = async (): Promise<WidgetConfig> => {
|
|
const res = await fetch(`${baseUrl}/api/widget/init?key=${widgetKey}`);
|
|
if (!res.ok) throw new Error('Widget init failed');
|
|
return res.json();
|
|
};
|
|
|
|
export const fetchDoctors = async (): Promise<Doctor[]> => {
|
|
const res = await fetch(`${baseUrl}/api/widget/doctors?key=${widgetKey}`);
|
|
if (!res.ok) throw new Error('Failed to load doctors');
|
|
return res.json();
|
|
};
|
|
|
|
export const fetchSlots = async (doctorId: string, date: string): Promise<TimeSlot[]> => {
|
|
const res = await fetch(`${baseUrl}/api/widget/slots?key=${widgetKey}&doctorId=${doctorId}&date=${date}`);
|
|
if (!res.ok) throw new Error('Failed to load slots');
|
|
return res.json();
|
|
};
|
|
|
|
export const submitBooking = async (data: any): Promise<{ appointmentId: string; reference: string }> => {
|
|
const res = await fetch(`${baseUrl}/api/widget/book?key=${widgetKey}`, {
|
|
method: 'POST', headers: headers(), body: JSON.stringify(data),
|
|
});
|
|
if (!res.ok) throw new Error('Booking failed');
|
|
return res.json();
|
|
};
|
|
|
|
export const submitLead = async (data: any): Promise<{ leadId: string }> => {
|
|
const res = await fetch(`${baseUrl}/api/widget/lead?key=${widgetKey}`, {
|
|
method: 'POST', headers: headers(), body: JSON.stringify(data),
|
|
});
|
|
if (!res.ok) throw new Error('Submission failed');
|
|
return res.json();
|
|
};
|
|
|
|
export const startChatSession = async (name: string, phone: string): Promise<{ leadId: string }> => {
|
|
const res = await fetch(`${baseUrl}/api/widget/chat-start?key=${widgetKey}`, {
|
|
method: 'POST', headers: headers(),
|
|
body: JSON.stringify({ name, phone }),
|
|
});
|
|
if (!res.ok) throw new Error('Chat start failed');
|
|
return res.json();
|
|
};
|
|
|
|
// Send the simplified {role, content: string}[] history to the backend.
|
|
// Backend responds with an SSE stream of UIMessageChunk events.
|
|
// branch (when set) is injected into the system prompt so the AI scopes
|
|
// tool calls to that branch.
|
|
type OutboundMessage = { role: 'user' | 'assistant'; content: string };
|
|
export const streamChat = async (
|
|
leadId: string,
|
|
messages: OutboundMessage[],
|
|
branch: string | null,
|
|
): Promise<ReadableStream<Uint8Array>> => {
|
|
const res = await fetch(`${baseUrl}/api/widget/chat?key=${widgetKey}`, {
|
|
method: 'POST', headers: headers(),
|
|
body: JSON.stringify({ leadId, messages, branch }),
|
|
});
|
|
if (!res.ok || !res.body) throw new Error('Chat failed');
|
|
return res.body;
|
|
};
|