import { useState, useRef, useEffect } from "react"; import { Bot, X, ChevronRight, Stethoscope, Calendar, FileText, MessageSquare, Send, Loader2, RotateCcw, Paperclip, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { useLocation } from "wouter"; import { cn } from "@/lib/utils"; import { apiRequest } from "@/lib/queryClient"; import { setChatbotPendingFiles } from "@/lib/chatbotFileStore"; type Step = | "menu" | "eligibility-input" | "eligibility-confirm" | "ai-loading" | "patient-found" | "eligibility-id-ready" | "check-and-claim-ready" | "need-insurance-clarification" | "need-appointment-selection" | "need-cdt-clarification" | "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 }; } 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?")]; } 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); const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | 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 [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 [, setLocation] = useLocation(); const messagesEndRef = useRef(null); const pasteRef = useRef(null); const freeTextRef = useRef(null); const fileInputRef = useRef(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, step]); // Persist messages across navigation (cleared on logout) useEffect(() => { try { const saveable = messages.filter((m) => !m.isLoading); sessionStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(saveable)); } 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); setCheckAndClaimData(null); setClarificationData(null); setApptSelectionData(null); setCdtClarificationData(null); setClaimReadyData(null); setPreauthReadyData(null); setPendingFiles([]); }; // Full reset including message history and stored session const reset = () => { resetStep(); const fresh = [makeMsg("bot", "Hi! What can I help you with today?")]; try { sessionStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(fresh)); } catch {} 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); }; const handleEligibilityFromPatient = () => { if (!patientResult) return; addMsg("user", "Check eligibility now"); addMsg("bot", "Opening the eligibility check page..."); if (patientResult.insuranceId && patientResult.dateOfBirth) { sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId: patientResult.insuranceId, dob: patientResult.dateOfBirth, autoCheck: getAutoCheck(patientResult.dateOfBirth), })); window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill")); } setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600); }; 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(); setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600); }; const handleEligibilityIdRun = () => { if (!eligibilityIdData) return; addMsg("user", "Check eligibility now"); addMsg("bot", "Opening the eligibility check page..."); prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.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})`); if (!eligibilityIdData.patient) { addMsg("bot", `Running eligibility check — will add patient and create appointment for ${dateLabel} after...`); sessionStorage.setItem("chatbot_appt_after_eligibility", JSON.stringify({ memberId: eligibilityIdData.memberId, date: targetDate ?? null })); prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck); return; } addMsg("bot", `Creating appointment for ${dateLabel}...`, true); try { const res = await apiRequest("POST", "/api/ai/create-appointment-today", { patientId: eligibilityIdData.patient.id, date: targetDate ?? undefined, }); const data = await res.json(); if (!res.ok) { replaceLastMsg(data.message ?? "Could not create appointment."); return; } replaceLastMsg(`Appointment added at ${data.startTime} (${data.column ?? "Column A"}) for ${data.dateLabel} — opening eligibility check page...`); prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck); } catch { replaceLastMsg("Could not create appointment. Please try again."); } }; 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, }) ); 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); setStep("patient-found"); 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 === "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 === "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" && (