feat: enhance new-patient AI chat flow with full scheduling and eligibility

- Add 3-message intro (self-intro → empathetic ack → new/existing question) via single TwiML response to guarantee delivery order
- Detect reschedule intent from first message; look up existing appointment date
- New patient flow: ask insurance type → MassHealth consent → member ID + DOB → Selenium eligibility check
- Post-eligibility: active → ask appointment date/time with office-hours validation; inactive → ask other insurance or collect contact info
- Date/time collection mirrors reschedule flow: check office day open, ask time, validate against office hours
- Auto-create appointment in schedule for known patients on confirmation; use first available staff member
- Add openPhoneReply toggle (Settings → AI Chat) to respond to any number at any time
- Add 5-minute inactivity timeout: reset conversation to initial stage and clear pending state
- Normalize MassHealth DOB to zero-padded MM/DD/YYYY before Selenium submission
- Expand isExistingPatient classifier to recognize "old patient", "old", "previous", "prior"
- Existing patient confirmation message now acknowledges patient type before asking about insurance

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-15 00:00:56 -04:00
parent c1f55778ca
commit c71624f7e7
53 changed files with 1078 additions and 124 deletions

View File

@@ -3,9 +3,10 @@ import { useQuery, useMutation } from "@tanstack/react-query";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus } from "lucide-react";
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus, Zap } from "lucide-react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -546,6 +547,39 @@ export function AiChatSettingsCard() {
const [generalFallback, setGeneralFallback] = useState(DEFAULTS.generalFallback);
const initialized = useRef(false);
const [openPhoneReply, setOpenPhoneReply] = useState(false);
const { data: advancedSettings } = useQuery<{ openPhoneReply: boolean }>({
queryKey: ["/api/ai/advanced-settings"],
queryFn: async () => {
const res = await apiRequest("GET", "/api/ai/advanced-settings");
if (!res.ok) return { openPhoneReply: false };
return res.json();
},
staleTime: Infinity,
refetchOnWindowFocus: false,
});
useEffect(() => {
if (advancedSettings) setOpenPhoneReply(advancedSettings.openPhoneReply ?? false);
}, [advancedSettings]);
const openPhoneReplyMutation = useMutation({
mutationFn: async (enabled: boolean) => {
const res = await apiRequest("PUT", "/api/ai/advanced-settings", { openPhoneReply: enabled });
if (!res.ok) throw new Error("Failed to save");
return res.json();
},
onSuccess: (data) => {
setOpenPhoneReply(data.openPhoneReply);
queryClient.invalidateQueries({ queryKey: ["/api/ai/advanced-settings"] });
toast({ title: data.openPhoneReply ? "Open reply enabled" : "Open reply disabled" });
},
onError: () => {
toast({ title: "Error", description: "Failed to save setting.", variant: "destructive" });
},
});
// ── SMS template list ──────────────────────────────────────────
const [smsTemplates, setSmsTemplates] = useState<SmsTemplate[]>([]);
const smsInitialized = useRef(false);
@@ -698,6 +732,29 @@ export function AiChatSettingsCard() {
return (
<div className="space-y-6">
{/* ── Advanced: Open Phone Reply toggle ───────────────────── */}
<Card>
<CardContent className="py-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-start gap-3">
<Zap className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
<div>
<p className="text-sm font-semibold leading-snug">Respond to any phone number at any time</p>
<p className="text-xs text-muted-foreground mt-0.5">
When enabled, the AI will reply to all incoming texts including unknown numbers not yet in your patient list 24/7 regardless of office hours. When disabled, the AI only responds during after-hours.
</p>
</div>
</div>
<Switch
checked={openPhoneReply}
disabled={openPhoneReplyMutation.isPending}
onCheckedChange={(val) => openPhoneReplyMutation.mutate(val)}
className="flex-shrink-0"
/>
</div>
</CardContent>
</Card>
{/* ── Section 0: SMS Templates ─────────────────────────────── */}
<Card>
<CardContent className="py-6 space-y-5">