feat: AI chat scheduling, claim automation, and session improvements
- Internal AI chat: schedule_appointment intent books earliest available slot in Column A using office hours; claim_only intent looks up latest past appointment for service date, asks user when two appointments are within 7 days, auto-triggers correct Selenium worker with mapped prices - Gemini model updated to gemini-flash-latest; conversation history (15 messages) passed for pronoun/reference resolution; history trimmed to start with user turn so Gemini doesn't reject the context - Insurance alias file (insuranceAliases.json) replaces hardcoded siteKey matching; "tufs" now resolves to TUFTS_SCO - DOB format normalized (MM/DD/YYYY → YYYY-MM-DD) before parseLocalDate; autoCheck now fires for all insurance types, not just MH/CMSP - Claim form auto-submit: all handlers (MH, CCA, DDMA, UnitedDH, Tufts) accept formToUse and receive fee-schedule-priced form; prefillDone set after chatbot code prefill so autoSubmit gate opens correctly - Chatbot: chat history persisted in sessionStorage, cleared on logout and auto-logout; Clear button writes fresh state synchronously; message history window increased to 15 - DentaQuest/TuftsSCO Selenium: "Remember me" checkbox clicked before sign-in to persist OTP trust cookie across sessions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
MessageSquare,
|
||||
Send,
|
||||
Loader2,
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -24,7 +25,8 @@ type Step =
|
||||
| "patient-found"
|
||||
| "eligibility-id-ready"
|
||||
| "check-and-claim-ready"
|
||||
| "need-insurance-clarification";
|
||||
| "need-insurance-clarification"
|
||||
| "need-appointment-selection";
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
@@ -96,14 +98,20 @@ function parseEligibilityInput(
|
||||
};
|
||||
}
|
||||
|
||||
const INITIAL_MESSAGES: Message[] = [
|
||||
makeMsg("bot", "Hi! What can I help you with today?"),
|
||||
];
|
||||
const CHAT_STORAGE_KEY = "chatbot_messages";
|
||||
|
||||
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<Step>("menu");
|
||||
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
|
||||
const [messages, setMessages] = useState<Message[]>(loadSavedMessages);
|
||||
const [pasteInput, setPasteInput] = useState("");
|
||||
const [parseError, setParseError] = useState("");
|
||||
const [eligibilityData, setEligibilityData] = useState<EligibilityData | null>(null);
|
||||
@@ -112,6 +120,12 @@ export function ChatbotButton() {
|
||||
const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null } | null>(null);
|
||||
const [checkAndClaimData, setCheckAndClaimData] = useState<CheckAndClaimData | null>(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 [, setLocation] = useLocation();
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const pasteRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -121,6 +135,14 @@ export function ChatbotButton() {
|
||||
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);
|
||||
@@ -141,9 +163,9 @@ export function ChatbotButton() {
|
||||
return next;
|
||||
});
|
||||
|
||||
const reset = () => {
|
||||
// Resets step/data only — keeps message history
|
||||
const resetStep = () => {
|
||||
setStep("menu");
|
||||
setMessages([makeMsg("bot", "Hi! What can I help you with today?")]);
|
||||
setPasteInput("");
|
||||
setParseError("");
|
||||
setEligibilityData(null);
|
||||
@@ -152,22 +174,33 @@ export function ChatbotButton() {
|
||||
setEligibilityIdData(null);
|
||||
setCheckAndClaimData(null);
|
||||
setClarificationData(null);
|
||||
setApptSelectionData(null);
|
||||
};
|
||||
|
||||
// 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);
|
||||
reset();
|
||||
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); reset(); }, 600);
|
||||
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); reset(); }, 600);
|
||||
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:");
|
||||
@@ -199,7 +232,7 @@ export function ChatbotButton() {
|
||||
autoCheck: getAutoCheck(eligibilityData.dobISO),
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); reset(); }, 600);
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600);
|
||||
};
|
||||
|
||||
const handleEligibilityFromPatient = () => {
|
||||
@@ -214,13 +247,13 @@ export function ChatbotButton() {
|
||||
}));
|
||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||
}
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); reset(); }, 600);
|
||||
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"));
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); reset(); }, 600);
|
||||
setTimeout(() => { setLocation("/insurance-status"); setOpen(false); resetStep(); }, 600);
|
||||
};
|
||||
|
||||
const handleEligibilityIdRun = () => {
|
||||
@@ -257,13 +290,17 @@ export function ChatbotButton() {
|
||||
setStep("ai-loading");
|
||||
|
||||
try {
|
||||
const res = await apiRequest("POST", "/api/ai/internal-chat", { message: text });
|
||||
const history = messages
|
||||
.filter((m) => !m.isLoading)
|
||||
.slice(-15)
|
||||
.map((m) => ({ role: m.role === "user" ? "user" : "assistant", text: m.text }));
|
||||
const res = await apiRequest("POST", "/api/ai/internal-chat", { message: text, history });
|
||||
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);
|
||||
setTimeout(() => { setLocation(data.actionData.url); setOpen(false); resetStep(); }, 800);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -313,6 +350,41 @@ export function ChatbotButton() {
|
||||
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 === "claim_only_ready" && data.actionData) {
|
||||
const { patient, matchedCodes, siteKey, serviceDate, appointmentId } = data.actionData;
|
||||
if (patient?.id && matchedCodes?.length > 0) {
|
||||
sessionStorage.setItem(
|
||||
"chatbot_claim_prefill",
|
||||
JSON.stringify({ codes: matchedCodes, siteKey, serviceDate, autoSubmit: true })
|
||||
);
|
||||
}
|
||||
const url = appointmentId
|
||||
? `/claims?appointmentId=${appointmentId}`
|
||||
: `/claims?newPatient=${patient?.id}`;
|
||||
setTimeout(() => {
|
||||
setLocation(url);
|
||||
setOpen(false);
|
||||
resetStep();
|
||||
}, 600);
|
||||
return;
|
||||
}
|
||||
|
||||
setStep("menu");
|
||||
} catch {
|
||||
replaceLastMsg("Sorry, something went wrong. Please try again.");
|
||||
@@ -333,7 +405,8 @@ export function ChatbotButton() {
|
||||
step === "patient-found" ||
|
||||
step === "eligibility-id-ready" ||
|
||||
step === "check-and-claim-ready" ||
|
||||
step === "need-insurance-clarification";
|
||||
step === "need-insurance-clarification" ||
|
||||
step === "need-appointment-selection";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -360,13 +433,24 @@ export function ChatbotButton() {
|
||||
<Bot className="h-4 w-4" />
|
||||
<span className="font-semibold text-sm">Assistant</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="hover:opacity-70 transition-opacity rounded"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className="flex items-center gap-1 text-xs hover:opacity-70 transition-opacity rounded px-1.5 py-0.5 border border-white/30 hover:bg-white/10"
|
||||
title="Clear chat history"
|
||||
>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="hover:opacity-70 transition-opacity rounded"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
@@ -631,6 +715,44 @@ export function ChatbotButton() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Appointment selection */}
|
||||
{step === "need-appointment-selection" && apptSelectionData && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-3 space-y-2">
|
||||
<p className="text-xs font-semibold text-amber-800">Which appointment date?</p>
|
||||
<div className="flex flex-col gap-1.5 pt-1">
|
||||
{apptSelectionData.options.map((opt) => (
|
||||
<button
|
||||
key={opt.serviceDate}
|
||||
className="text-left text-xs px-3 py-2 rounded-lg border border-amber-300 hover:bg-amber-100 transition-colors font-medium"
|
||||
onClick={() => {
|
||||
addMsg("user", opt.label);
|
||||
addMsg("bot", `Using ${opt.label} as the service date.`);
|
||||
sessionStorage.setItem(
|
||||
"chatbot_claim_prefill",
|
||||
JSON.stringify({
|
||||
codes: apptSelectionData.matchedCodes,
|
||||
siteKey: apptSelectionData.siteKey,
|
||||
serviceDate: opt.serviceDate,
|
||||
autoSubmit: true,
|
||||
})
|
||||
);
|
||||
setTimeout(() => {
|
||||
setLocation(`/claims?appointmentId=${opt.appointmentId}`);
|
||||
setOpen(false);
|
||||
resetStep();
|
||||
}, 600);
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Button size="sm" variant="ghost" className="h-7 text-xs w-full" onClick={resetStep}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user