feat: AI chat claim confirmation, CDT alias learning, and eligibility auto-trigger fixes

- Claim flow: show green confirm card (patient, CDT codes, service date) before Selenium starts
- CDT lookup: add DIRECT_CODE_MAP + ALIAS_MAP with 60+ dental abbreviations from office fee schedule
  (2BW→D0272, 4BW→D0274, PA→D0220, FL→D1208, RCT codes, composite tooth#/surface parser, etc.)
- Composite filling parser: auto-map "#29 OB" → D2392 based on tooth# (front/back) and surface count
- Ask-and-learn: unknown CDT terms block claim and ask user; answer saved to DB alias map for future use
- Cancel on confirm card returns to chat (not full reset) so user can correct and retry
- Eligibility auto-trigger fix: reset autoTriggeredRef when autoTrigger resets to false so second
  chatbot eligibility check on same page visit fires correctly (all 5 provider buttons fixed)
- check_eligibility by name now returns eligibility_id_ready with correct siteKey for non-MH patients
- DDMA/CCA/United/Tufts fee schedules updated with office prices (single Price field, no age split)
- getCodeMap case-insensitive matching fix (ddma/cca/etc. now correctly selected)
- Family plan member disambiguation: insuranceId+DOB composite lookup prevents overwriting siblings
- AI chat date fix: send clientDate from browser to avoid UTC midnight rollover (EST→wrong day)
- AI prompt: appointmentDate extracted for claim_only intent when user says "today" or a date

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-06 21:11:58 -04:00
parent 4899ab8368
commit 86cf55aa4d
16 changed files with 1405 additions and 4913 deletions

View File

@@ -187,7 +187,8 @@ export function CCAEligibilityButton({
};
useEffect(() => {
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
if (!autoTrigger) { autoTriggeredRef.current = false; return; }
if (autoTriggeredRef.current || isFormIncomplete) return;
autoTriggeredRef.current = true;
onAutoTriggered?.();
handleStart();

View File

@@ -393,7 +393,8 @@ export function DdmaEligibilityButton({
};
useEffect(() => {
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
if (!autoTrigger) { autoTriggeredRef.current = false; return; }
if (autoTriggeredRef.current || isFormIncomplete) return;
autoTriggeredRef.current = true;
onAutoTriggered?.();
handleDdmaStart();

View File

@@ -327,7 +327,8 @@ export function DeltaInsEligibilityButton({
};
useEffect(() => {
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
if (!autoTrigger) { autoTriggeredRef.current = false; return; }
if (autoTriggeredRef.current || isFormIncomplete) return;
autoTriggeredRef.current = true;
onAutoTriggered?.();
handleStart();

View File

@@ -324,7 +324,8 @@ export function TuftsSCOEligibilityButton({
};
useEffect(() => {
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
if (!autoTrigger) { autoTriggeredRef.current = false; return; }
if (autoTriggeredRef.current || isFormIncomplete) return;
autoTriggeredRef.current = true;
onAutoTriggered?.();
handleStart();

View File

@@ -324,7 +324,8 @@ export function UnitedSCOEligibilityButton({
};
useEffect(() => {
if (!autoTrigger || autoTriggeredRef.current || isFormIncomplete) return;
if (!autoTrigger) { autoTriggeredRef.current = false; return; }
if (autoTriggeredRef.current || isFormIncomplete) return;
autoTriggeredRef.current = true;
onAutoTriggered?.();
handleStart();

View File

@@ -26,7 +26,9 @@ type Step =
| "eligibility-id-ready"
| "check-and-claim-ready"
| "need-insurance-clarification"
| "need-appointment-selection";
| "need-appointment-selection"
| "need-cdt-clarification"
| "claim-ready";
interface Message {
id: number;
@@ -126,6 +128,18 @@ export function ChatbotButton() {
matchedCodes: { code: string; description: string }[];
options: { label: string; appointmentId: number; serviceDate: string }[];
} | null>(null);
const [cdtClarificationData, setCdtClarificationData] = useState<{
unknownPhrases: string[];
codeInputs: Record<string, string>;
originalMessage: string;
} | null>(null);
const [claimReadyData, setClaimReadyData] = useState<{
patient: PatientResult | null;
matchedCodes: { code: string; description: string }[];
siteKey: string;
serviceDate: string;
appointmentId: number | null;
} | null>(null);
const [, setLocation] = useLocation();
const messagesEndRef = useRef<HTMLDivElement>(null);
const pasteRef = useRef<HTMLTextAreaElement>(null);
@@ -175,6 +189,8 @@ export function ChatbotButton() {
setCheckAndClaimData(null);
setClarificationData(null);
setApptSelectionData(null);
setCdtClarificationData(null);
setClaimReadyData(null);
};
// Full reset including message history and stored session
@@ -294,7 +310,9 @@ export function ChatbotButton() {
.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 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();
replaceLastMsg(data.reply ?? "Sorry, I couldn't process that.");
@@ -366,22 +384,25 @@ export function ChatbotButton() {
return;
}
if (data.action === "need_cdt_clarification" && data.actionData) {
const phrases: string[] = data.actionData.unknownPhrases ?? [];
const inputs: Record<string, string> = {};
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 } = 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);
setClaimReadyData({
patient: patient ?? null,
matchedCodes: matchedCodes ?? [],
siteKey,
serviceDate,
appointmentId: appointmentId ?? null,
});
setStep("claim-ready");
return;
}
@@ -672,6 +693,7 @@ export function ChatbotButton() {
setStep("ai-loading");
apiRequest("POST", "/api/ai/internal-chat", {
message: `check ${opt} for ${clarificationData.memberId}, ${clarificationData.dob}${clarificationData.procedureNames.length > 0 ? " and claim " + clarificationData.procedureNames.join(", ") : ""}`,
clientDate: (() => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`; })(),
})
.then((r) => r.json())
.then((data) => {
@@ -753,6 +775,124 @@ export function ChatbotButton() {
</div>
)}
{/* Claim ready — confirm before submitting */}
{step === "claim-ready" && claimReadyData && (() => {
const [sy, sm, sd] = (claimReadyData.serviceDate ?? "").split("-");
const dateLabel = sy ? `${sm}/${sd}/${sy}` : claimReadyData.serviceDate;
return (
<div className="bg-green-50 border border-green-200 rounded-xl p-3 space-y-2">
<p className="text-xs font-semibold text-green-800">Confirm Claim</p>
{claimReadyData.patient && (
<p className="text-xs text-green-700 font-medium">
{claimReadyData.patient.firstName} {claimReadyData.patient.lastName}
</p>
)}
<p className="text-xs text-gray-500">Service date: {dateLabel}</p>
{claimReadyData.matchedCodes.length > 0 && (
<div className="space-y-0.5 pt-0.5">
{claimReadyData.matchedCodes.map((c) => (
<p key={c.code} className="text-xs text-gray-700 pl-1">
<span className="font-medium">{c.code}</span> {c.description}
</p>
))}
</div>
)}
<div className="flex gap-2 pt-1">
<Button
size="sm"
className="flex-1 h-8 text-xs bg-green-600 hover:bg-green-700 text-white"
onClick={() => {
const { patient, matchedCodes, siteKey, serviceDate, appointmentId } = claimReadyData;
addMsg("user", "Confirm & submit claim");
addMsg("bot", "Opening claim...");
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);
}}
>
<FileText className="h-3 w-3 mr-1" />
Confirm &amp; Submit
</Button>
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={resetStep}>Cancel</Button>
</div>
</div>
);
})()}
{/* CDT clarification — unknown procedure terms */}
{step === "need-cdt-clarification" && cdtClarificationData && (
<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">Unknown procedure term{cdtClarificationData.unknownPhrases.length > 1 ? "s" : ""}</p>
<div className="space-y-2">
{cdtClarificationData.unknownPhrases.map((phrase) => (
<div key={phrase} className="flex items-center gap-2">
<span className="text-xs text-gray-700 font-medium shrink-0">"{phrase}"</span>
<input
type="text"
placeholder="D0272"
value={cdtClarificationData.codeInputs[phrase] ?? ""}
onChange={(e) => setCdtClarificationData((prev) => prev ? {
...prev,
codeInputs: { ...prev.codeInputs, [phrase]: e.target.value.toUpperCase() },
} : prev)}
className="flex-1 rounded border border-amber-300 bg-white px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-amber-400"
/>
</div>
))}
</div>
<div className="flex gap-2 pt-1">
<Button
size="sm"
className="flex-1 h-8 text-xs bg-amber-600 hover:bg-amber-700 text-white"
disabled={Object.values(cdtClarificationData.codeInputs).some((v) => !v.trim())}
onClick={async () => {
const entries = Object.entries(cdtClarificationData.codeInputs);
for (const [phrase, code] of entries) {
await apiRequest("POST", "/api/ai/cdt-aliases/add", { phrase, cdtCode: code.trim() });
}
const savedPairs = entries.map(([p, c]) => `"${p}" = ${c.trim()}`).join(", ");
addMsg("user", entries.map(([p, c]) => `${p} = ${c.trim()}`).join(", "));
addMsg("bot", `Got it! Saved ${savedPairs}. Retrying...`, true);
setStep("ai-loading");
setCdtClarificationData(null);
const origMsg = cdtClarificationData.originalMessage;
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 _cd = `${_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: origMsg, history, clientDate: _cd });
const data = await res.json();
replaceLastMsg(data.reply ?? "Sorry, I couldn't process that.");
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);
} else {
setStep("menu");
}
} catch {
replaceLastMsg("Sorry, something went wrong. Please try again.");
setStep("menu");
}
}}
>
Save &amp; Retry
</Button>
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={resetStep}>Cancel</Button>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>