feat: chatbot screenshot-only eligibility detect, Sun Life/DentaQuest auto-check, chatbot attachment handoff to preauth; fix TuftsSCO preauth Selenium reliability

- Chatbot: submitting with only a screenshot attached (no text) now triggers the same
  "AI Detect & Check Eligibility" flow as the Copy Agent page
- detect-eligibility-info now also extracts the visible insurance payer name and picks
  Tufts SCO auto-check when both Sun Life and DentaQuest are detected, else MassHealth
- Chatbot-attached files are now handed off to both claim and preauth forms unconditionally,
  not just claims
- TuftsSCO preauth Selenium worker: verify-and-retry the Tooth field (typing was silently
  getting reset by duplicate-procedure-code warning banners), add a final re-verification
  pass across all rows, fix acknowledgement-checkbox targeting/verification, and verify
  the "Next step" click actually advances the wizard instead of trusting a blind click
This commit is contained in:
2026-07-25 00:10:05 -04:00
parent 6b9b3c41f2
commit 870bda5950
8 changed files with 237 additions and 724 deletions
@@ -535,16 +535,18 @@ export function ClaimForm({
};
}, [appointmentId, serviceDate, existingClaimId]);
// Attach any file the user uploaded/screenshotted in the chatbot to this claim/preauth,
// regardless of whether the user typed anything like "with the attachment" — the mere
// act of attaching it in chat is enough intent.
useEffect(() => {
const chatbotFiles = takeChatbotPendingFiles();
if (chatbotFiles.length === 0) return;
setForm((prev) => ({ ...prev, uploadedFiles: chatbotFiles }));
}, []);
// Prefill service lines (and optional service date) from chatbot claim_only flow
useEffect(() => {
const raw = sessionStorage.getItem("chatbot_claim_prefill");
const chatbotFiles = takeChatbotPendingFiles();
if (!raw && chatbotFiles.length === 0) return;
if (chatbotFiles.length > 0) {
setForm((prev) => ({ ...prev, uploadedFiles: chatbotFiles }));
}
if (!raw) return;
try {
const { codes, serviceDate } = JSON.parse(raw) as {
@@ -398,7 +398,7 @@ export function ChatbotButton() {
});
addMsg(
"bot",
`I read this from your screenshots — ready to check ${detail.autoCheck === "mh" ? "MassHealth" : "eligibility"} for ${
`I read this from your screenshots — ready to check ${detail.autoCheck === "tufts-sco" ? "Tufts SCO" : detail.autoCheck === "mh" ? "MassHealth" : "eligibility"} for ${
detail.patient ? `${detail.patient.firstName} ${detail.patient.lastName}` : detail.memberId
}. Confirm to proceed.`
);
@@ -663,9 +663,56 @@ export function ChatbotButton() {
prefillAndNavigate(checkAndClaimData.memberId, checkAndClaimData.dob, checkAndClaimData.autoCheck);
};
// Screenshot(s) attached with no typed text: same intent as the Copy Agent's
// "AI Detect & Check Eligibility" button — read Member ID/DOB off the image(s).
const handleScreenshotOnlyDetect = async () => {
const files = pendingFiles;
if (!files.length || step === "ai-loading") return;
setPendingFiles([]);
addMsg("user", `📎 ${files.length} screenshot${files.length > 1 ? "s" : ""}`);
addMsg("bot", "Reading your screenshot...", true);
setStep("ai-loading");
try {
const formData = new FormData();
for (const file of files) formData.append("files", file);
const res = await apiRequest("POST", "/api/ai/detect-eligibility-info", formData);
const json = await res.json();
if (json?.error) throw new Error(json.message || "Detection failed");
const memberId: string | null = json?.data?.memberId ?? null;
const dob: string | null = json?.data?.dob ?? null;
const autoCheck: string = json?.data?.autoCheck ?? "mh";
if (memberId && dob) {
setEligibilityIdData({
memberId,
dob,
siteKey: "",
autoCheck,
patient: null,
appointmentDate: null,
});
replaceLastMsg(`I read this from your screenshot — ready to check ${autoCheck === "tufts-sco" ? "Tufts SCO" : "MassHealth"} for ${memberId}. Confirm to proceed.`);
setStep("eligibility-id-ready");
} else {
setPasteInput([memberId, dob].filter(Boolean).join(" "));
replaceLastMsg("I saved your screenshot but couldn't fully read the Member ID/DOB. Please review and complete the fields below:");
setStep("eligibility-input");
}
} catch {
replaceLastMsg("Sorry, I couldn't read that screenshot. Please try again.");
setStep("menu");
}
};
const handleFreeTextSubmit = async () => {
const text = freeTextInput.trim();
if (!text || step === "ai-loading") return;
if (step === "ai-loading") return;
if (!text) {
if (pendingFiles.length > 0) return handleScreenshotOnlyDetect();
return;
}
setFreeTextInput("");
addMsg("user", text);
addMsg("bot", "Thinking...", true);
@@ -1785,7 +1832,7 @@ export function ChatbotButton() {
size="sm"
className="h-9 w-9 p-0 shrink-0"
onClick={handleFreeTextSubmit}
disabled={!freeTextInput.trim() || step === "ai-loading"}
disabled={(!freeTextInput.trim() && pendingFiles.length === 0) || step === "ai-loading"}
>
{step === "ai-loading" ? (
<Loader2 className="h-4 w-4 animate-spin" />
@@ -301,13 +301,14 @@ export default function AiCopyAgentPage() {
const memberId: string | null = detectResult.value?.data?.memberId ?? null;
const dob: string | null = detectResult.value?.data?.dob ?? null;
const autoCheck: string = detectResult.value?.data?.autoCheck ?? "mh";
window.dispatchEvent(
new CustomEvent("chatbot:open-eligibility-id-ready", {
detail: {
memberId,
dob,
autoCheck: "mh",
autoCheck,
patient: {
id: patient.id,
firstName: patient.firstName,
@@ -336,11 +337,11 @@ export default function AiCopyAgentPage() {
try {
const files = await itemsToFiles(items);
let detected: { memberId: string | null; dob: string | null };
let detected: { memberId: string | null; dob: string | null; autoCheck: string };
try {
const json = await detectFromFiles(files);
if (json?.error) throw new Error(json.message || "Detection failed");
detected = { memberId: json?.data?.memberId ?? null, dob: json?.data?.dob ?? null };
detected = { memberId: json?.data?.memberId ?? null, dob: json?.data?.dob ?? null, autoCheck: json?.data?.autoCheck ?? "mh" };
} catch (error) {
console.error("Error detecting Member ID/DOB:", error);
toast({
@@ -351,7 +352,7 @@ export default function AiCopyAgentPage() {
return;
}
const { memberId, dob } = detected;
const { memberId, dob, autoCheck } = detected;
if (memberId && dob) {
let tempPatient: TempPatient;
@@ -395,7 +396,7 @@ export default function AiCopyAgentPage() {
detail: {
memberId,
dob,
autoCheck: "mh",
autoCheck,
patient: {
id: tempPatient.id,
firstName: tempPatient.firstName,
@@ -424,7 +425,7 @@ export default function AiCopyAgentPage() {
window.dispatchEvent(
new CustomEvent("chatbot:open-eligibility-id-ready", {
detail: { memberId, dob, autoCheck: "mh", patient: null },
detail: { memberId, dob, autoCheck, patient: null },
})
);
}