import { useState, useRef, useEffect } from "react"; import { Bot, X, ChevronRight, Stethoscope, Calendar, FileText, MessageSquare, Send, Loader2, RotateCcw, Paperclip, Image as ImageIcon, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useLocation } from "wouter"; import { cn } from "@/lib/utils"; import { apiRequest } from "@/lib/queryClient"; import { setChatbotPendingFiles } from "@/lib/chatbotFileStore"; import { ServerAttachmentsPickerModal } from "@/components/file-upload/server-attachments-picker-modal"; type Step = | "menu" | "eligibility-input" | "eligibility-confirm" | "ai-loading" | "patient-found" | "eligibility-id-ready" | "batch-eligibility-ready" | "check-and-claim-ready" | "need-insurance-clarification" | "need-appointment-selection" | "need-cdt-clarification" | "claim-ready" | "batch-claim-ready" | "multi-claim-ready" | "batch-check-and-claim-ready" | "preauth-ready"; interface Message { id: number; role: "bot" | "user"; text: string; isLoading?: boolean; } interface PatientResult { id: number; firstName: string; lastName: string; insuranceId: string | null; insuranceProvider: string | null; dateOfBirth: string | null; } interface EligibilityData { memberId: string; dob: string; dobISO: string; } interface CheckAndClaimData { patient: PatientResult | null; memberId: string; dob: string; // ISO YYYY-MM-DD siteKey: string; autoCheck: string; matchedCodes: { code: string; description: string }[]; serviceDate?: string | null; renderingProvider?: string | null; } let msgCounter = 0; function makeMsg(role: "bot" | "user", text: string, isLoading = false): Message { return { id: ++msgCounter, role, text, isLoading }; } // eligibilityIdData.dob is stored/sent as ISO ("YYYY-MM-DD") everywhere downstream (patient // sync, sessionStorage prefill, matching) — these only convert for display/editing. function isoToMDY(iso: string): string { const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})$/); if (!m) return iso; return `${m[2]}/${m[3]}/${m[1]}`; } function mdyToISO(mdy: string): string { const m = mdy.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/); if (!m) return mdy; return `${m[3]}-${m[1]!.padStart(2, "0")}-${m[2]!.padStart(2, "0")}`; } const INSURANCE_TYPE_OPTIONS: { value: string; label: string }[] = [ { value: "mh", label: "MassHealth" }, { value: "cmsp", label: "CMSP" }, { value: "ddma", label: "DDMA" }, { value: "delta-ins", label: "Delta Dental" }, { value: "united-sco", label: "United SCO" }, { value: "tufts-sco", label: "Tufts SCO" }, { value: "cca", label: "CCA" }, ]; function getAutoCheck(dobISO: string): "mh" | "cmsp" { const [y, m, d] = dobISO.split("-").map(Number); const today = new Date(); let age = today.getFullYear() - (y ?? 0); const monthDiff = today.getMonth() + 1 - (m ?? 0); if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < (d ?? 0))) age--; return age >= 21 ? "mh" : "cmsp"; } function parseEligibilityInput( raw: string ): { memberId: string; display: string; iso: string } | null { const dateMatch = raw.match(/\b(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})\b/); if (!dateMatch) return null; const m = dateMatch[1]; const d = dateMatch[2]; const y = dateMatch[3]; if (!m || !d || !y) return null; const month = parseInt(m, 10); const day = parseInt(d, 10); const year = parseInt(y, 10); if (month < 1 || month > 12 || day < 1 || day > 31 || year < 1900) return null; const withoutDate = raw.replace(dateMatch[0], ""); const memberId = (withoutDate.match(/[a-zA-Z0-9]/g) ?? []).join(""); if (!memberId) return null; return { memberId, display: `${m.padStart(2, "0")}/${d.padStart(2, "0")}/${y}`, iso: `${y}-${m.padStart(2, "0")}-${d.padStart(2, "0")}`, }; } const CHAT_STORAGE_KEY = "chatbot_messages"; const CHATBOT_JOB_TS_KEY = "chatbot_job_started_at"; function markJobStarted() { try { sessionStorage.setItem(CHATBOT_JOB_TS_KEY, String(Date.now())); } catch {} } function shouldAutoReset(): boolean { try { const ts = sessionStorage.getItem(CHATBOT_JOB_TS_KEY); if (!ts) return false; return Date.now() - Number(ts) > 60_000; } catch { return false; } } function loadSavedMessages(): Message[] { try { const raw = sessionStorage.getItem(CHAT_STORAGE_KEY); if (raw) return JSON.parse(raw) as Message[]; } catch {} return [makeMsg("bot", "Hi! What can I help you with today?")]; } const WELCOME = [makeMsg("bot", "Hi! What can I help you with today?")]; let saveTimer: ReturnType | null = null; function saveChatHistoryToServer(msgs: Message[]) { if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(() => { const saveable = msgs.filter((m) => !m.isLoading); fetch("/api/ai/chat-history", { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.getItem("token") ?? ""}` }, body: JSON.stringify({ messages: saveable }), }).catch(() => {}); }, 500); } function clearChatHistoryOnServer() { fetch("/api/ai/chat-history", { method: "DELETE", headers: { Authorization: `Bearer ${localStorage.getItem("token") ?? ""}` }, }).catch(() => {}); } export function ChatbotButton() { const [open, setOpen] = useState(false); const [step, setStep] = useState("menu"); const [messages, setMessages] = useState(loadSavedMessages); const [pasteInput, setPasteInput] = useState(""); const [parseError, setParseError] = useState(""); const [eligibilityData, setEligibilityData] = useState(null); const [freeTextInput, setFreeTextInput] = useState(""); const [patientResult, setPatientResult] = useState(null); // Snapshot of insuranceId/dateOfBirth as originally fetched, so we can tell whether the // user hand-edited them in the patient-found card and only PUT to the DB when they did. const originalPatientResultRef = useRef<{ insuranceId: string | null; dateOfBirth: string | null } | null>(null); const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | null } | null>(null); const [batchEligibilityData, setBatchEligibilityData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null }[] | null>(null); const [checkAndClaimData, setCheckAndClaimData] = useState(null); const [clarificationData, setClarificationData] = useState<{ memberId: string; dob: string; patient: PatientResult | null; procedureNames: string[]; options: string[] } | null>(null); const [apptSelectionData, setApptSelectionData] = useState<{ patient: PatientResult; siteKey: string; matchedCodes: { code: string; description: string }[]; options: { label: string; appointmentId: number; serviceDate: string }[]; } | null>(null); const [cdtClarificationData, setCdtClarificationData] = useState<{ unknownPhrases: string[]; codeInputs: Record; originalMessage: string; } | null>(null); const [claimReadyData, setClaimReadyData] = useState<{ patient: PatientResult | null; matchedCodes: { code: string; description: string }[]; siteKey: string; serviceDate: string; appointmentId: number | null; renderingProvider: string | null; } | null>(null); const [batchCheckAndClaimData, setBatchCheckAndClaimData] = useState<{ queue: { memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null }[]; matchedCodes: { code: string; description: string; toothNumber?: string; toothSurface?: string; quad?: string }[]; renderingProvider: string | null; } | null>(null); const [batchClaimData, setBatchClaimData] = useState<{ queue: { patient: PatientResult; siteKey: string; serviceDate: string | null; appointmentId: number | null }[]; matchedCodes: { code: string; description: string; toothNumber?: string; toothSurface?: string; quad?: string }[]; renderingProvider: string | null; } | null>(null); const [preauthReadyData, setPreauthReadyData] = useState<{ patient: PatientResult | null; matchedCodes: { code: string; description: string; toothNumber?: string }[]; siteKey: string; serviceDate: string; renderingProvider: string | null; } | null>(null); const [pendingFiles, setPendingFiles] = useState([]); const [isServerPickerOpen, setIsServerPickerOpen] = useState(false); const [, setLocation] = useLocation(); const messagesEndRef = useRef(null); const pasteRef = useRef(null); const freeTextRef = useRef(null); const fileInputRef = useRef(null); // Set by the Copy Agent page's "AI Detect & Check Eligibility" flow when the screenshots were // saved under a placeholder folder name; renamed to the real patient name once the user confirms. const pendingFolderRenameRef = useRef<{ patientId: number; patientName: string } | null>(null); // Load chat history from server on first mount (server is source of truth) const serverLoaded = useRef(false); useEffect(() => { if (serverLoaded.current) return; serverLoaded.current = true; const token = localStorage.getItem("token"); if (!token) return; fetch("/api/ai/chat-history", { headers: { Authorization: `Bearer ${token}` }, }) .then((r) => r.json()) .then((data) => { if (Array.isArray(data.messages) && data.messages.length > 0) { setMessages(data.messages); try { sessionStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(data.messages)); } catch {} } }) .catch(() => {}); }, []); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, step]); // Opened externally by the Copy Agent page after it saves screenshots and runs AI // Member ID/DOB extraction — lands the chatbot directly on the confirm step. useEffect(() => { function handleOpenEligibilityIdReady(e: Event) { const detail = (e as CustomEvent).detail as { memberId: string | null; dob: string | null; autoCheck: string; patient: PatientResult | null; patientId?: number; pendingFolderPatientName?: string; }; if (detail.patientId && detail.pendingFolderPatientName) { pendingFolderRenameRef.current = { patientId: detail.patientId, patientName: detail.pendingFolderPatientName, }; } if (shouldAutoReset()) resetStep(); setOpen(true); if (detail.memberId && detail.dob) { setEligibilityIdData({ memberId: detail.memberId, dob: detail.dob, siteKey: "", autoCheck: detail.autoCheck || "mh", patient: detail.patient, appointmentDate: null, }); addMsg( "bot", `I read this from your screenshots — ready to check ${detail.autoCheck === "mh" ? "MassHealth" : "eligibility"} for ${ detail.patient ? `${detail.patient.firstName} ${detail.patient.lastName}` : detail.memberId }. Confirm to proceed.` ); setStep("eligibility-id-ready"); } else { setPasteInput([detail.memberId, detail.dob].filter(Boolean).join(" ")); addMsg( "bot", "I saved your screenshots but couldn't fully read the Member ID/DOB. Please review and complete the fields below:" ); setStep("eligibility-input"); } } window.addEventListener("chatbot:open-eligibility-id-ready", handleOpenEligibilityIdReady); return () => window.removeEventListener("chatbot:open-eligibility-id-ready", handleOpenEligibilityIdReady); }, []); // Persist messages to sessionStorage AND server useEffect(() => { try { const saveable = messages.filter((m) => !m.isLoading); sessionStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(saveable)); saveChatHistoryToServer(messages); } catch {} }, [messages]); useEffect(() => { if (step === "eligibility-input") { setTimeout(() => pasteRef.current?.focus(), 50); } if (step === "menu") { setTimeout(() => freeTextRef.current?.focus(), 50); } }, [step]); const addMsg = (role: "bot" | "user", text: string, isLoading = false) => setMessages((prev) => [...prev, makeMsg(role, text, isLoading)]); const replaceLastMsg = (text: string) => setMessages((prev) => { const next = [...prev]; const last = next[next.length - 1]; if (last) next[next.length - 1] = { ...last, text, isLoading: false }; return next; }); // Resets step/data only — keeps message history const resetStep = () => { setStep("menu"); setPasteInput(""); setParseError(""); setEligibilityData(null); setFreeTextInput(""); setPatientResult(null); setEligibilityIdData(null); setBatchEligibilityData(null); setCheckAndClaimData(null); setClarificationData(null); setApptSelectionData(null); setCdtClarificationData(null); setClaimReadyData(null); setBatchClaimData(null); setBatchCheckAndClaimData(null); setPreauthReadyData(null); pendingFolderRenameRef.current = null; }; // Full reset including message history and stored session const reset = () => { resetStep(); setPendingFiles([]); const fresh = [makeMsg("bot", "Hi! What can I help you with today?")]; try { sessionStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(fresh)); } catch {} clearChatHistoryOnServer(); setMessages(fresh); }; const handleClose = () => { setOpen(false); resetStep(); }; const handleOptionSelect = (option: "eligibility" | "schedule" | "claims") => { if (option === "schedule") { addMsg("user", "Schedule an appointment"); addMsg("bot", "Opening the appointments page..."); setTimeout(() => { setLocation("/appointments"); setOpen(false); resetStep(); }, 600); } else if (option === "claims") { addMsg("user", "View claims"); addMsg("bot", "Opening the claims page..."); setTimeout(() => { setLocation("/claims"); setOpen(false); resetStep(); }, 600); } else if (option === "eligibility") { addMsg("user", "Check Eligibility"); addMsg("bot", "Please enter the patient's Member ID and Date of Birth:"); setStep("eligibility-input"); } }; const handleEligibilitySubmit = () => { const parsed = parseEligibilityInput(pasteInput); if (!parsed) { setParseError("Couldn't find both a Member ID and a date. Try: 123456789 01/15/1985"); return; } setParseError(""); const data: EligibilityData = { memberId: parsed.memberId, dob: parsed.display, dobISO: parsed.iso }; setEligibilityData(data); addMsg("user", pasteInput.trim()); addMsg("bot", `Ready to check MassHealth eligibility for:\n• Member ID: ${parsed.memberId}\n• Date of Birth: ${parsed.display}\n\nShall I proceed?`); setStep("eligibility-confirm"); }; const handleConfirm = () => { if (!eligibilityData) return; addMsg("user", "Yes, check now"); addMsg("bot", "Opening the eligibility check page with this patient..."); sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId: eligibilityData.memberId, dob: eligibilityData.dobISO, autoCheck: getAutoCheck(eligibilityData.dobISO), })); window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill")); setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600); }; // If the user hand-edited Member ID / DOB on the patient-found card, save the correction // back to the patient record so it doesn't revert to the stale value next time. const persistPatientEditsIfChanged = async (patient: PatientResult) => { const original = originalPatientResultRef.current; if (!original) return; const changed = patient.insuranceId !== original.insuranceId || patient.dateOfBirth !== original.dateOfBirth; if (!changed) return; try { await apiRequest("PUT", `/api/patients/${patient.id}`, { insuranceId: patient.insuranceId, dateOfBirth: patient.dateOfBirth, }); originalPatientResultRef.current = { insuranceId: patient.insuranceId, dateOfBirth: patient.dateOfBirth }; } catch { // Non-fatal — the edited values are still used for this eligibility check below. } }; const handleEligibilityFromPatient = async () => { if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return; addMsg("user", "Check eligibility now"); addMsg("bot", "Opening the eligibility check page..."); await persistPatientEditsIfChanged(patientResult); prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth)); }; const handleEligibilityAndAppointmentFromPatient = async () => { if (!patientResult?.insuranceId || !patientResult?.dateOfBirth) return; addMsg("user", "Check eligibility & add to schedule (today)"); addMsg("bot", "Running eligibility check — will add to today's schedule after..."); await persistPatientEditsIfChanged(patientResult); // Create the appointment only after eligibility is confirmed, not before — the eligibility // page's tryAppointmentFromChatbot picks this up once the check running below completes. sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: patientResult.insuranceId, date: null })); prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth)); }; const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string) => { sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId, dob: dobISO, autoCheck })); window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill")); markJobStarted(); if (pendingFolderRenameRef.current) { const { patientId, patientName } = pendingFolderRenameRef.current; pendingFolderRenameRef.current = null; apiRequest("PATCH", `/api/claims/patient-folder/${patientId}/confirm-name`, { patientName }).catch(() => {}); } setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600); }; // If the user hand-edited the Member ID/DOB on the "eligibility-id-ready" card (e.g. fixing a // misread digit), push the correction to the patient record too — otherwise the real eligibility // check (which matches by insuranceId+dob) would create a second, disconnected patient instead // of updating this one. Surface a failure in the chat rather than swallowing it, since a silent // failure here means the check proceeds against a stale record and silently creates a duplicate. const syncEditedPatientFields = async (memberId: string, dob: string, patientId?: number): Promise => { if (!patientId) return true; try { await apiRequest("PUT", `/api/patients/${patientId}`, { insuranceId: memberId, dateOfBirth: dob }); return true; } catch (err: any) { console.error("Failed to sync edited Member ID/DOB to patient record:", err); addMsg( "bot", `⚠️ Couldn't save the edited Member ID/DOB to the patient record (${err?.message ?? "unknown error"}). The eligibility check may create a duplicate patient — check the Patients page after.` ); return false; } }; const handleEligibilityIdRun = async () => { if (!eligibilityIdData) return; addMsg("user", "Check eligibility now"); addMsg("bot", "Opening the eligibility check page..."); await syncEditedPatientFields(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.patient?.id); prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck); }; const handleBatchEligibilityRun = () => { if (!batchEligibilityData || batchEligibilityData.length === 0) return; addMsg("user", `Check all ${batchEligibilityData.length} patients`); addMsg("bot", `Checking ${batchEligibilityData.length} patients one by one...`); const [first, ...rest] = batchEligibilityData; if (rest.length > 0) { sessionStorage.setItem("chatbot_eligibility_queue", JSON.stringify(rest)); } prefillAndNavigate(first!.memberId, first!.dob, first!.autoCheck); }; const handleBatchEligibilityAndAppointment = () => { if (!batchEligibilityData || batchEligibilityData.length === 0) return; addMsg("user", `Check all & appointment today (${batchEligibilityData.length} patients)`); addMsg("bot", `Checking ${batchEligibilityData.length} patients — appointments will be created after each check...`); sessionStorage.setItem("chatbot_batch_appt_after_eligibility", "true"); const [first, ...rest] = batchEligibilityData; if (rest.length > 0) { sessionStorage.setItem("chatbot_eligibility_queue", JSON.stringify(rest)); } prefillAndNavigate(first!.memberId, first!.dob, first!.autoCheck); }; const handleEligibilityAndAppointment = async (targetDate?: string) => { if (!eligibilityIdData) return; const dateLabel = targetDate ? new Date(targetDate + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "today"; addMsg("user", `Check eligibility & add to schedule (${dateLabel})`); await syncEditedPatientFields(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.patient?.id); addMsg("bot", `Running eligibility check — will add${eligibilityIdData.patient ? "" : " patient and"} to schedule for ${dateLabel} after...`); // Create the appointment only after eligibility is confirmed, not before — the eligibility // page's tryAppointmentFromChatbot picks this up once the check running below completes. sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: eligibilityIdData.memberId, date: targetDate ?? null })); prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck); }; const handleCheckAndClaimRun = () => { if (!checkAndClaimData) return; addMsg("user", "Run check & claim"); addMsg("bot", "Opening the eligibility check page..."); // Store claim codes so the eligibility page can offer auto-claim after ACTIVE result sessionStorage.setItem( "chatbot_claim_codes", JSON.stringify({ codes: checkAndClaimData.matchedCodes, siteKey: checkAndClaimData.siteKey, patientId: checkAndClaimData.patient?.id ?? null, memberId: checkAndClaimData.memberId, dob: checkAndClaimData.dob, serviceDate: checkAndClaimData.serviceDate ?? null, renderingProvider: checkAndClaimData.renderingProvider ?? null, }) ); setChatbotPendingFiles(pendingFiles); prefillAndNavigate(checkAndClaimData.memberId, checkAndClaimData.dob, checkAndClaimData.autoCheck); }; const handleFreeTextSubmit = async () => { const text = freeTextInput.trim(); if (!text || step === "ai-loading") return; setFreeTextInput(""); addMsg("user", text); addMsg("bot", "Thinking...", true); setStep("ai-loading"); try { const history = messages .filter((m) => !m.isLoading) .slice(-15) .map((m) => ({ role: m.role === "user" ? "user" : "assistant", text: m.text })); const d = new Date(); const clientDate = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; const res = await apiRequest("POST", "/api/ai/internal-chat", { message: text, history, clientDate }); const data = await res.json(); const claimActions = new Set(["claim_only_ready", "check_and_claim_ready", "need_appointment_selection"]); const attachmentSuffix = pendingFiles.length > 0 && claimActions.has(data.action) ? ` (📎 ${pendingFiles.length} attachment${pendingFiles.length > 1 ? "s" : ""} will be included)` : ""; replaceLastMsg((data.reply ?? "Sorry, I couldn't process that.") + attachmentSuffix); if (data.action === "navigate" && data.actionData?.url) { setTimeout(() => { setLocation(data.actionData.url); setOpen(false); resetStep(); }, 800); return; } if ( (data.action === "check_eligibility_prefill" || data.action === "show_patient") && data.actionData?.patient ) { setPatientResult(data.actionData.patient); originalPatientResultRef.current = { insuranceId: data.actionData.patient.insuranceId, dateOfBirth: data.actionData.patient.dateOfBirth, }; setStep("patient-found"); return; } if (data.action === "batch_eligibility_ready" && data.actionData?.queue) { setBatchEligibilityData(data.actionData.queue); setStep("batch-eligibility-ready"); return; } if (data.action === "eligibility_id_ready" && data.actionData) { setEligibilityIdData({ memberId: data.actionData.memberId, dob: data.actionData.dob, siteKey: data.actionData.siteKey, autoCheck: data.actionData.autoCheck, patient: data.actionData.patient ?? null, appointmentDate: data.actionData.appointmentDate ?? null, }); setStep("eligibility-id-ready"); return; } if (data.action === "check_and_claim_ready" && data.actionData) { setCheckAndClaimData({ patient: data.actionData.patient ?? null, memberId: data.actionData.memberId, dob: data.actionData.dob, siteKey: data.actionData.siteKey, autoCheck: data.actionData.autoCheck, matchedCodes: data.actionData.matchedCodes ?? [], serviceDate: data.actionData.serviceDate ?? null, renderingProvider: data.actionData.renderingProvider ?? null, }); setStep("check-and-claim-ready"); return; } if (data.action === "need_insurance_clarification" && data.actionData) { setClarificationData({ memberId: data.actionData.memberId, dob: data.actionData.dob, patient: data.actionData.patient ?? null, procedureNames: data.actionData.procedureNames ?? [], options: data.actionData.options ?? [], }); setStep("need-insurance-clarification"); return; } if (data.action === "appointment_created") { setStep("menu"); return; } if (data.action === "need_appointment_selection" && data.actionData) { setApptSelectionData({ patient: data.actionData.patient, siteKey: data.actionData.siteKey, matchedCodes: data.actionData.matchedCodes ?? [], options: data.actionData.options ?? [], }); setStep("need-appointment-selection"); return; } if (data.action === "need_cdt_clarification" && data.actionData) { const phrases: string[] = data.actionData.unknownPhrases ?? []; const inputs: Record = {}; for (const p of phrases) inputs[p] = ""; setCdtClarificationData({ unknownPhrases: phrases, codeInputs: inputs, originalMessage: text }); setStep("need-cdt-clarification"); return; } if (data.action === "batch_check_and_claim_ready" && data.actionData) { setBatchCheckAndClaimData({ queue: data.actionData.queue ?? [], matchedCodes: data.actionData.matchedCodes ?? [], renderingProvider: data.actionData.renderingProvider ?? null, }); setStep("batch-check-and-claim-ready"); return; } if (data.action === "batch_claim_ready" && data.actionData) { setBatchClaimData({ queue: data.actionData.queue ?? [], matchedCodes: data.actionData.matchedCodes ?? [], renderingProvider: data.actionData.renderingProvider ?? null, }); setStep("batch-claim-ready"); return; } if (data.action === "multi_claim_ready" && data.actionData) { setBatchClaimData({ queue: data.actionData.queue ?? [], matchedCodes: [], renderingProvider: data.actionData.renderingProvider ?? null, }); setStep("multi-claim-ready"); return; } if (data.action === "claim_only_ready" && data.actionData) { const { patient, matchedCodes, siteKey, serviceDate, appointmentId, renderingProvider } = data.actionData; setClaimReadyData({ patient: patient ?? null, matchedCodes: matchedCodes ?? [], siteKey, serviceDate, appointmentId: appointmentId ?? null, renderingProvider: renderingProvider ?? null, }); setStep("claim-ready"); return; } if (data.action === "preauth_ready" && data.actionData) { const { patient, matchedCodes, siteKey, serviceDate, renderingProvider } = data.actionData; setPreauthReadyData({ patient: patient ?? null, matchedCodes: matchedCodes ?? [], siteKey, serviceDate, renderingProvider: renderingProvider ?? null, }); setStep("preauth-ready"); return; } setStep("menu"); } catch { replaceLastMsg("Sorry, something went wrong. Please try again."); setStep("menu"); } }; const handleFreeTextKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleFreeTextSubmit(); } }; const showFreeTextInput = step === "menu" || step === "ai-loading" || step === "patient-found" || step === "eligibility-id-ready" || step === "batch-eligibility-ready" || step === "check-and-claim-ready" || step === "batch-claim-ready" || step === "multi-claim-ready" || step === "batch-check-and-claim-ready" || step === "need-insurance-clarification" || step === "need-appointment-selection"; return ( <> {open && ( <> {/* Backdrop */}
{/* Chat panel */}
{/* Header */}
Assistant
{/* Messages */}
{messages.map((msg) => (
{msg.isLoading ? ( Thinking... ) : ( msg.text )}
))} {/* Quick-action buttons (menu step) */} {step === "menu" && (
)} {/* Eligibility manual input */} {step === "eligibility-input" && (