import { useState, useRef, useEffect } from "react"; import { Bot, X, ChevronRight, Stethoscope, Calendar, FileText, MessageSquare, Send, Loader2, } 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"; type Step = | "menu" | "eligibility-input" | "eligibility-confirm" | "ai-loading" | "patient-found"; 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; } 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 INITIAL_MESSAGES: Message[] = [ 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(INITIAL_MESSAGES); const [pasteInput, setPasteInput] = useState(""); const [parseError, setParseError] = useState(""); const [eligibilityData, setEligibilityData] = useState(null); const [freeTextInput, setFreeTextInput] = useState(""); const [patientResult, setPatientResult] = useState(null); const [, setLocation] = useLocation(); const messagesEndRef = useRef(null); const pasteRef = useRef(null); const freeTextRef = useRef(null); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages, step]); 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; }); const reset = () => { setStep("menu"); setMessages([makeMsg("bot", "Hi! What can I help you with today?")]); setPasteInput(""); setParseError(""); setEligibilityData(null); setFreeTextInput(""); setPatientResult(null); }; const handleClose = () => { setOpen(false); reset(); }; 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); reset(); }, 600); } else if (option === "claims") { addMsg("user", "View claims"); addMsg("bot", "Opening the claims page..."); setTimeout(() => { setLocation("/claims"); setOpen(false); reset(); }, 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); reset(); }, 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); reset(); }, 600); }; 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 res = await apiRequest("POST", "/api/ai/internal-chat", { message: text }); const data = await res.json(); replaceLastMsg(data.reply ?? "Sorry, I couldn't process that."); if (data.action === "navigate" && data.actionData?.url) { setTimeout(() => { setLocation(data.actionData.url); setOpen(false); reset(); }, 800); return; } if ( (data.action === "check_eligibility_prefill" || data.action === "show_patient") && data.actionData?.patient ) { setPatientResult(data.actionData.patient); setStep("patient-found"); 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"; 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" && (