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:
@@ -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 & 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 & Retry
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="h-8 text-xs" onClick={resetStep}>Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user