diff --git a/apps/Frontend/src/components/layout/chatbot.tsx b/apps/Frontend/src/components/layout/chatbot.tsx new file mode 100644 index 00000000..f95d4d07 --- /dev/null +++ b/apps/Frontend/src/components/layout/chatbot.tsx @@ -0,0 +1,360 @@ +import { useState, useRef, useEffect } from "react"; +import { + Bot, + X, + ChevronRight, + Stethoscope, + Calendar, + FileText, + MessageSquare, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { useLocation } from "wouter"; +import { cn } from "@/lib/utils"; + +type Step = "menu" | "eligibility-input" | "eligibility-confirm"; + +interface Message { + id: number; + role: "bot" | "user"; + text: string; +} + +interface EligibilityData { + memberId: string; + dob: string; // MM/DD/YYYY display format + dobISO: string; // YYYY-MM-DD for storage +} + +let msgCounter = 0; +function makeMsg(role: "bot" | "user", text: string): Message { + return { id: ++msgCounter, role, text }; +} + +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 { + // Find a date anywhere in the text (MM/DD/YYYY or MM-DD-YYYY) + 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; + + // Remove the matched date, then collect remaining alphanumeric chars as member ID + 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 [, setLocation] = useLocation(); + const messagesEndRef = useRef(null); + const pasteRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages, step]); + + useEffect(() => { + if (step === "eligibility-input") { + setTimeout(() => pasteRef.current?.focus(), 50); + } + }, [step]); + + const addMsg = (role: "bot" | "user", text: string) => + setMessages((prev) => [...prev, makeMsg(role, text)]); + + const reset = () => { + setStep("menu"); + setMessages([makeMsg("bot", "Hi! What can I help you with today?")]); + setPasteInput(""); + setParseError(""); + setEligibilityData(null); + }; + + const handleClose = () => { + setOpen(false); + reset(); + }; + + const handleOptionSelect = ( + option: "eligibility" | "schedule" | "claims" | "chat" + ) => { + 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 === "chat") { + addMsg("user", "General chat"); + addMsg( + "bot", + "This feature is coming soon! For now, use the quick options below to navigate." + ); + } 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); + }; + + return ( + <> + + + {open && ( + <> + {/* Backdrop */} +
+ + {/* Chat panel */} +
+ {/* Header */} +
+
+ + Assistant +
+ +
+ + {/* Messages + controls */} +
+ {messages.map((msg) => ( +
+
+ {msg.text} +
+
+ ))} + + {/* Step: menu */} + {step === "menu" && ( +
+ + + + +
+ )} + + {/* Step: eligibility input */} + {step === "eligibility-input" && ( +
+ +