- Add all new Frontend source files (pages, components, hooks, utils) - Add selenium_MHBatchPaymentCheckWorker.py and MHSinglePaymentCheckWorker.py - Add install-steps-5-13.sh setup script - Update .gitignore to exclude runtime/sensitive data (backups, uploads, chat-history, keys, downloads, generated .d.ts files) while keeping folders - Add .gitkeep to preserve empty runtime folders in git Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1259 lines
78 KiB
React
1259 lines
78 KiB
React
import { useState, useEffect, useRef } from "react";
|
||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||
import { Card, CardContent } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
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, Zap, SlidersHorizontal, BookMarked, ChevronDown, ChevronUp } from "lucide-react";
|
||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||
// ─── Defaults ─────────────────────────────────────────────────────────────────
|
||
const DEFAULT_REMINDER_SMS = "Hi {firstName}, this is a reminder from {officeName}. You have an appointment on {appointmentDate} at {appointmentTime}. Please come to our office at {officeAddress}. Please reply YES to confirm or NO to reschedule. Thank you!";
|
||
const DEFAULTS = {
|
||
reminderGreeting: "Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can confirm or reschedule your appointment and answer general questions 24/7. I will reply you message at any time you need.",
|
||
newPatientGreeting: "Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7. How can I help you today?",
|
||
generalFallback: "Hi! My name is Lisa, the dedicated AI assistant at {officeName}. How can I help you today?",
|
||
};
|
||
const DEFAULT_SMS_TEMPLATES = [
|
||
{
|
||
id: "default-appt-reminder",
|
||
name: "Appointment Reminder SMS",
|
||
body: DEFAULT_REMINDER_SMS,
|
||
},
|
||
{
|
||
id: "default-follow-up",
|
||
name: "Follow up reminder",
|
||
body: "Hi {firstName}, this is a follow-up from {officeName}. We wanted to check in with you after your recent appointment. Please don't hesitate to call us if you have any questions.",
|
||
},
|
||
];
|
||
function previewTemplate(text, officeName) {
|
||
return text.replace(/\{officeName\}/g, officeName || "your dental office");
|
||
}
|
||
function newId() {
|
||
return Date.now().toString(36) + Math.random().toString(36).slice(2);
|
||
}
|
||
// ─── Combined Reminder & Reschedule flow diagram (SVG) ────────────────────────
|
||
function LangGraphFlow() {
|
||
const W = 700;
|
||
const cx = 350;
|
||
const nW = 210;
|
||
const nx = cx - nW / 2; // 245
|
||
// ── Dual entry nodes ──────────────────────────────────────────────────────
|
||
const entryW = 192;
|
||
const lEntryCx = 155;
|
||
const rEntryCx = 545;
|
||
const e1y = 14, e1h = 56;
|
||
// Merge connector (horizontal line where both branches meet)
|
||
const mergeY = e1y + e1h + 14; // 84
|
||
// ── Center sequence ───────────────────────────────────────────────────────
|
||
const n2y = mergeY + 14, n2h = 52; // Patient replies y=98
|
||
const n3y = n2y + n2h + 14, n3h = 84; // AI classifies y=164
|
||
const forkHY = n3y + n3h + 22; // 270
|
||
// ── Branch centers ────────────────────────────────────────────────────────
|
||
const lcx = 140; // YES (left)
|
||
const rcx = 490; // NO / Reschedule (right)
|
||
// ── YES branch ────────────────────────────────────────────────────────────
|
||
const yesW = 195;
|
||
const yes1y = forkHY + 50, yes1h = 88; // y=320
|
||
const yes2y = yes1y + yes1h + 12, yes2h = 52; // y=460
|
||
// ── NO / Reschedule branch ────────────────────────────────────────────────
|
||
const noW = 208;
|
||
const failX = rcx + noW / 2 + 8; // 602
|
||
const failW = 92;
|
||
const no1y = forkHY + 50, no1h = 84; // y=320 ask date / shortcut
|
||
const no2y = no1y + no1h + 14, no2h = 82; // y=418 day check
|
||
const no3y = no2y + no2h + 14, no3h = 64; // y=514 ask time
|
||
const no4y = no3y + no3h + 14, no4h = 82; // y=592 time check
|
||
const no5y = no4y + no4h + 14, no5h = 64; // y=688 confirm datetime
|
||
const no6y = no5y + no5h + 14, no6h = 74; // y=766 patient YES/NO
|
||
const no7y = no6y + no6h + 14, no7h = 82; // y=854 slot check
|
||
const no8y = no7y + no7h + 14, no8h = 80; // y=950 move appointment
|
||
const no9y = no8y + no8h + 14, no9h = 54; // y=1044 patient thanks
|
||
const totalH = Math.max(yes2y + yes2h, no9y + no9h) + 28;
|
||
return (<svg viewBox={`0 0 ${W} ${totalH}`} className="w-full max-w-2xl mx-auto" role="img" aria-label="LangGraph Reminder and Reschedule conversation flow">
|
||
<defs>
|
||
<marker id="rg2ah" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto" markerUnits="userSpaceOnUse">
|
||
<polygon points="0 0,10 3.5,0 7" fill="#9CA3AF"/>
|
||
</marker>
|
||
<marker id="rg2fail" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto" markerUnits="userSpaceOnUse">
|
||
<polygon points="0 0,8 3,0 6" fill="#EF4444"/>
|
||
</marker>
|
||
<marker id="rg2short" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto" markerUnits="userSpaceOnUse">
|
||
<polygon points="0 0,8 3,0 6" fill="#047857"/>
|
||
</marker>
|
||
</defs>
|
||
|
||
{/* ══ DUAL ENTRY NODES ════════════════════════════════════════════ */}
|
||
|
||
{/* Left: Reminder SMS */}
|
||
<rect x={lEntryCx - entryW / 2} y={e1y} width={entryW} height={e1h} rx={8} fill="#EFF6FF" stroke="#3B82F6" strokeWidth={1.5}/>
|
||
<text x={lEntryCx} y={e1y + 22} textAnchor="middle" fontSize={12} fontWeight="600" fill="#1D4ED8">Office sends Reminder SMS</text>
|
||
<text x={lEntryCx} y={e1y + 40} textAnchor="middle" fontSize={9} fill="#93C5FD">Staff triggers batch send</text>
|
||
|
||
{/* Right: Reschedule by Office SMS */}
|
||
<rect x={rEntryCx - entryW / 2} y={e1y} width={entryW} height={e1h} rx={8} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={rEntryCx} y={e1y + 22} textAnchor="middle" fontSize={12} fontWeight="600" fill="#C2410C">Office sends Reschedule SMS</text>
|
||
<text x={rEntryCx} y={e1y + 40} textAnchor="middle" fontSize={9} fill="#FDBA74">"Reschedule by Office" template</text>
|
||
|
||
{/* Converging lines → horizontal merge → Patient replies */}
|
||
<line x1={lEntryCx} y1={e1y + e1h} x2={lEntryCx} y2={mergeY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={rEntryCx} y1={e1y + e1h} x2={rEntryCx} y2={mergeY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={lEntryCx} y1={mergeY} x2={rEntryCx} y2={mergeY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={cx} y1={mergeY} x2={cx} y2={n2y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* ══ CENTER SEQUENCE ══════════════════════════════════════════════ */}
|
||
|
||
{/* N2: Patient replies */}
|
||
<rect x={nx} y={n2y} width={nW} height={n2h} rx={8} fill="#F9FAFB" stroke="#D1D5DB" strokeWidth={1.5}/>
|
||
<text x={cx} y={n2y + 22} textAnchor="middle" fontSize={13} fontWeight="600" fill="#374151">Patient replies</text>
|
||
<text x={cx} y={n2y + 40} textAnchor="middle" fontSize={10} fill="#9CA3AF">Any SMS triggers the AI</text>
|
||
<line x1={cx} y1={n2y + n2h} x2={cx} y2={n3y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* N3: AI classifies */}
|
||
<rect x={nx} y={n3y} width={nW} height={n3h} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={cx} y={n3y + 20} textAnchor="middle" fontSize={12} fontWeight="700" fill="#065F46">Google AI classifies YES / NO</text>
|
||
<text x={cx} y={n3y + 36} textAnchor="middle" fontSize={10} fontWeight="600" fill="#1D4ED8">Reminder: intro (MSG 1) + reply (MSG 2)</text>
|
||
<text x={cx} y={n3y + 52} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Reschedule: reply sent directly</text>
|
||
<text x={cx} y={n3y + 70} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Date in reply → AI skips to Day Check</text>
|
||
|
||
{/* Fork lines */}
|
||
<line x1={cx} y1={n3y + n3h} x2={cx} y2={forkHY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={lcx} y1={forkHY} x2={rcx} y2={forkHY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={lcx} y1={forkHY} x2={lcx} y2={yes1y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
<line x1={rcx} y1={forkHY} x2={rcx} y2={no1y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* YES / NO badges */}
|
||
<rect x={lcx - 26} y={forkHY - 12} width={52} height={24} rx={12} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={lcx} y={forkHY + 5} textAnchor="middle" fontSize={11} fontWeight="700" fill="#15803D">YES</text>
|
||
<rect x={rcx - 24} y={forkHY - 12} width={48} height={24} rx={12} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={rcx} y={forkHY + 5} textAnchor="middle" fontSize={11} fontWeight="700" fill="#EA580C">NO</text>
|
||
|
||
{/* ══ YES BRANCH ══════════════════════════════════════════════════ */}
|
||
|
||
<rect x={lcx - yesW / 2} y={yes1y} width={yesW} height={yes1h} rx={8} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={lcx} y={yes1y + 18} textAnchor="middle" fontSize={10} fontWeight="600" fill="#15803D">Reminder: Thank you for confirming!</text>
|
||
<text x={lcx} y={yes1y + 34} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Reschedule: What day & time?</text>
|
||
<text x={lcx} y={yes1y + 52} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">"See you on [date & time]"</text>
|
||
<text x={lcx} y={yes1y + 70} textAnchor="middle" fontSize={8} fill="#047857">date in reply → Day Check ↘</text>
|
||
<line x1={lcx} y1={yes1y + yes1h} x2={lcx} y2={yes2y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* Yes2: Patient thanks */}
|
||
<rect x={lcx - yesW / 2} y={yes2y} width={yesW} height={yes2h} rx={8} fill="#F9FAFB" stroke="#D1D5DB" strokeWidth={1.5}/>
|
||
<text x={lcx} y={yes2y + 20} textAnchor="middle" fontSize={11} fontWeight="600" fill="#374151">Patient: "Thank you / OK"</text>
|
||
<text x={lcx} y={yes2y + 38} textAnchor="middle" fontSize={9} fill="#9CA3AF" fontStyle="italic">AI: "Thank you for choosing us! See you on [date]"</text>
|
||
|
||
{/* Dashed shortcut: YES with date → Day Check */}
|
||
<line x1={lcx + yesW / 2} y1={yes1y + 52} x2={rcx - noW / 2 - 2} y2={no2y + 34} stroke="#047857" strokeWidth={1} strokeDasharray="4 3" markerEnd="url(#rg2short)"/>
|
||
|
||
{/* ══ NO / RESCHEDULE BRANCH ══════════════════════════════════════ */}
|
||
|
||
{/* no1: Understandable / ask date (with date-shortcut note) */}
|
||
<rect x={rcx - noW / 2} y={no1y} width={noW} height={no1h} rx={8} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={rcx} y={no1y + 18} textAnchor="middle" fontSize={11} fontWeight="600" fill="#C2410C">MSG 2: It is understandable!</text>
|
||
<text x={rcx} y={no1y + 34} textAnchor="middle" fontSize={11} fontWeight="600" fill="#C2410C">When would you like to reschedule?</text>
|
||
<text x={rcx} y={no1y + 52} textAnchor="middle" fontSize={9} fill="#9CA3AF" fontStyle="italic">Patient replies with a preferred date</text>
|
||
<text x={rcx} y={no1y + 70} textAnchor="middle" fontSize={8} fill="#047857" fontStyle="italic">date in reply → skips to Day Check ↓</text>
|
||
<line x1={rcx} y1={no1y + no1h} x2={rcx} y2={no2y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* no2: Day check */}
|
||
<rect x={rcx - noW / 2} y={no2y} width={noW} height={no2h} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={rcx} y={no2y + 18} textAnchor="middle" fontSize={11} fontWeight="700" fill="#065F46">Check: Is office open on that day?</text>
|
||
<text x={rcx} y={no2y + 34} textAnchor="middle" fontSize={9} fill="#6B7280">(reads Office Hours settings)</text>
|
||
<text x={rcx} y={no2y + 52} textAnchor="middle" fontSize={9} fill="#15803D" fontWeight="600">✓ Open → ask what time</text>
|
||
<text x={rcx} y={no2y + 68} textAnchor="middle" fontSize={9} fill="#DC2626" fontWeight="600">✗ Closed → "Office closed on [day]. Choose another?"</text>
|
||
<line x1={rcx + noW / 2} y1={no2y + no2h - 16} x2={failX - 1} y2={no2y + no2h - 16} stroke="#EF4444" strokeWidth={1} strokeDasharray="3 2" markerEnd="url(#rg2fail)"/>
|
||
<rect x={failX} y={no2y + no2h - 28} width={failW} height={24} rx={5} fill="#FEF2F2" stroke="#FCA5A5" strokeWidth={1}/>
|
||
<text x={failX + failW / 2} y={no2y + no2h - 12} textAnchor="middle" fontSize={8} fill="#DC2626">↩ loops back to ask date</text>
|
||
<line x1={rcx} y1={no2y + no2h} x2={rcx} y2={no3y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* no3: What time on [date]? */}
|
||
<rect x={rcx - noW / 2} y={no3y} width={noW} height={no3h} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={rcx} y={no3y + 20} textAnchor="middle" fontSize={11} fontWeight="600" fill="#065F46">AI: "What time do you prefer</text>
|
||
<text x={rcx} y={no3y + 36} textAnchor="middle" fontSize={11} fontWeight="600" fill="#065F46">on [date]?"</text>
|
||
<text x={rcx} y={no3y + 52} textAnchor="middle" fontSize={9} fill="#9CA3AF" fontStyle="italic">Patient replies with a time (e.g. "1 pm")</text>
|
||
<line x1={rcx} y1={no3y + no3h} x2={rcx} y2={no4y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* no4: Time check */}
|
||
<rect x={rcx - noW / 2} y={no4y} width={noW} height={no4h} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={rcx} y={no4y + 18} textAnchor="middle" fontSize={11} fontWeight="700" fill="#065F46">Check: Is time within office hours?</text>
|
||
<text x={rcx} y={no4y + 34} textAnchor="middle" fontSize={9} fill="#6B7280">(e.g. not during lunch 12–1pm)</text>
|
||
<text x={rcx} y={no4y + 52} textAnchor="middle" fontSize={9} fill="#15803D" fontWeight="600">✓ In hours → ask to confirm</text>
|
||
<text x={rcx} y={no4y + 68} textAnchor="middle" fontSize={9} fill="#DC2626" fontWeight="600">✗ → "Not available. Our hours: [AM]–[PM]. Other time?"</text>
|
||
<line x1={rcx + noW / 2} y1={no4y + no4h - 16} x2={failX - 1} y2={no4y + no4h - 16} stroke="#EF4444" strokeWidth={1} strokeDasharray="3 2" markerEnd="url(#rg2fail)"/>
|
||
<rect x={failX} y={no4y + no4h - 28} width={failW} height={24} rx={5} fill="#FEF2F2" stroke="#FCA5A5" strokeWidth={1}/>
|
||
<text x={failX + failW / 2} y={no4y + no4h - 12} textAnchor="middle" fontSize={8} fill="#DC2626">↩ loops back to ask time</text>
|
||
<line x1={rcx} y1={no4y + no4h} x2={rcx} y2={no5y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* no5: Just to confirm */}
|
||
<rect x={rcx - noW / 2} y={no5y} width={noW} height={no5h} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={rcx} y={no5y + 20} textAnchor="middle" fontSize={11} fontWeight="600" fill="#065F46">AI: "Just to confirm —</text>
|
||
<text x={rcx} y={no5y + 36} textAnchor="middle" fontSize={11} fontWeight="600" fill="#065F46">do you prefer [date at time]?"</text>
|
||
<text x={rcx} y={no5y + 52} textAnchor="middle" fontSize={9} fill="#9CA3AF" fontStyle="italic">Patient replies YES or NO</text>
|
||
<line x1={rcx} y1={no5y + no5h} x2={rcx} y2={no6y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* no6: Patient YES/NO */}
|
||
<rect x={rcx - noW / 2} y={no6y} width={noW} height={no6h} rx={8} fill="#F9FAFB" stroke="#D1D5DB" strokeWidth={1.5}/>
|
||
<text x={rcx} y={no6y + 18} textAnchor="middle" fontSize={11} fontWeight="600" fill="#374151">Patient confirms or corrects</text>
|
||
<text x={rcx} y={no6y + 36} textAnchor="middle" fontSize={9} fill="#15803D" fontWeight="600">✓ YES → check slot availability</text>
|
||
<text x={rcx} y={no6y + 52} textAnchor="middle" fontSize={9} fill="#DC2626" fontWeight="600">✗ NO → "No problem! What day/time?"</text>
|
||
<text x={rcx} y={no6y + 66} textAnchor="middle" fontSize={8} fill="#9CA3AF" fontStyle="italic">↩ returns to date step</text>
|
||
<line x1={rcx} y1={no6y + no6h} x2={rcx} y2={no7y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* no7: Slot availability check */}
|
||
<rect x={rcx - noW / 2} y={no7y} width={noW} height={no7h} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={rcx} y={no7y + 18} textAnchor="middle" fontSize={11} fontWeight="700" fill="#065F46">Check: Is slot available for staff?</text>
|
||
<text x={rcx} y={no7y + 34} textAnchor="middle" fontSize={9} fill="#6B7280">(no overlapping appointments)</text>
|
||
<text x={rcx} y={no7y + 52} textAnchor="middle" fontSize={9} fill="#15803D" fontWeight="600">✓ Free → move appointment</text>
|
||
<text x={rcx} y={no7y + 68} textAnchor="middle" fontSize={9} fill="#DC2626" fontWeight="600">✗ Taken → "[date] not available. Other time?"</text>
|
||
<line x1={rcx + noW / 2} y1={no7y + no7h - 16} x2={failX - 1} y2={no7y + no7h - 16} stroke="#EF4444" strokeWidth={1} strokeDasharray="3 2" markerEnd="url(#rg2fail)"/>
|
||
<rect x={failX} y={no7y + no7h - 28} width={failW} height={24} rx={5} fill="#FEF2F2" stroke="#FCA5A5" strokeWidth={1}/>
|
||
<text x={failX + failW / 2} y={no7y + no7h - 12} textAnchor="middle" fontSize={8} fill="#DC2626">↩ loops back to ask date</text>
|
||
<line x1={rcx} y1={no7y + no7h} x2={rcx} y2={no8y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* no8: DB move + AI badge */}
|
||
<rect x={rcx - noW / 2} y={no8y} width={noW} height={no8h} rx={8} fill="#F0F9FF" stroke="#0EA5E9" strokeWidth={1.5} strokeDasharray="5 3"/>
|
||
<rect x={rcx - 46} y={no8y - 11} width={92} height={22} rx={11} fill="#E0F2FE" stroke="#0EA5E9" strokeWidth={1.5}/>
|
||
<text x={rcx} y={no8y + 3} textAnchor="middle" fontSize={9} fontWeight="700" fill="#0369A1">DB Update</text>
|
||
<text x={rcx} y={no8y + 22} textAnchor="middle" fontSize={11} fontWeight="600" fill="#0369A1">Appointment moved!</text>
|
||
<text x={rcx} y={no8y + 38} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">"Appt moved to [date at time].</text>
|
||
<text x={rcx} y={no8y + 52} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Receptionist will confirm tomorrow."</text>
|
||
<text x={rcx} y={no8y + 68} textAnchor="middle" fontSize={9} fill="#0369A1" fontWeight="600">🤖 AI badge shown on schedule grid</text>
|
||
<line x1={rcx} y1={no8y + no8h} x2={rcx} y2={no9y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#rg2ah)"/>
|
||
|
||
{/* no9: Patient thanks */}
|
||
<rect x={rcx - noW / 2} y={no9y} width={noW} height={no9h} rx={8} fill="#F9FAFB" stroke="#D1D5DB" strokeWidth={1.5}/>
|
||
<text x={rcx} y={no9y + 20} textAnchor="middle" fontSize={11} fontWeight="600" fill="#374151">Patient: "Thank you / OK"</text>
|
||
<text x={rcx} y={no9y + 38} textAnchor="middle" fontSize={9} fill="#9CA3AF" fontStyle="italic">AI: "Thank you for choosing us! See you on [date]"</text>
|
||
</svg>);
|
||
}
|
||
// ─── New Patient flow diagram (updated) ──────────────────────────────────────
|
||
function NewPatientFlow() {
|
||
// ── Canvas ───────────────────────────────────────────────────────────────
|
||
const W = 900;
|
||
const cx = 450;
|
||
// ── Coordinates ──────────────────────────────────────────────────────────
|
||
const nW = 240;
|
||
const nx = cx - nW / 2;
|
||
// Top sequence
|
||
const n1y = 14;
|
||
const n1h = 84;
|
||
const n2y = n1y + n1h + 14;
|
||
const n2h = 52;
|
||
const forkHY = n2y + n2h + 18;
|
||
// Main branch centres
|
||
const lcx = 210; // New Patient
|
||
const rcx = 690; // Existing Patient
|
||
const brW = 210;
|
||
// ── NEW PATIENT layout ────────────────────────────────────────────────────
|
||
// insurance question
|
||
const npInsY = forkHY + 44;
|
||
const npInsH = 52;
|
||
// sub-fork 1: has-ins / no-ins
|
||
const npSf1Y = npInsY + npInsH + 18;
|
||
const lhcx = 140; // has insurance
|
||
const lncx = 325; // no insurance
|
||
// no-ins leaf
|
||
const npNoInsY = npSf1Y + 20;
|
||
const npNoInsH = 76;
|
||
const npNoInsW = 130;
|
||
// what kind?
|
||
const npWkY = npSf1Y + 20;
|
||
const npWkH = 52;
|
||
const npWkW = 192;
|
||
// sub-fork 2: masshealth / other
|
||
const npSf2Y = npWkY + npWkH + 18;
|
||
const mhcx = 82; // masshealth
|
||
const othcx = 210; // other
|
||
// other ins leaf
|
||
const npOthY = npSf2Y + 20;
|
||
const npOthH = 76;
|
||
const npOthW = 130;
|
||
// masshealth consent node
|
||
const npMhY = npSf2Y + 20;
|
||
const npMhH = 76;
|
||
const npMhW = 155;
|
||
// selenium
|
||
const npSelY = npMhY + npMhH + 18;
|
||
const npSelH = 66;
|
||
const npSelW = 148;
|
||
// active / inactive fork
|
||
const npResHY = npSelY + npSelH + 14;
|
||
const npActCx = 40;
|
||
const npInaCx = 140;
|
||
const npResY = npResHY + 22;
|
||
// active result
|
||
const npActH = 76;
|
||
const npActW = 110;
|
||
// inactive → other insurance?
|
||
const npInaH = 52;
|
||
const npInaW = 110;
|
||
// other-insurance-after-inactive
|
||
const npOins2Y = npResY + Math.max(npActH, npInaH) + 16;
|
||
const npOins2H = 52;
|
||
const npOins2W = 150;
|
||
// final: yes=transfer / no=contact info
|
||
const npFinalY = npOins2Y + npOins2H + 18;
|
||
const npTxCx = npInaCx - 44;
|
||
const npCiCx = npInaCx + 46;
|
||
const npTxH = 44;
|
||
const npTxW = 86;
|
||
const npCiH = 64;
|
||
const npCiW = 132;
|
||
const totalH_np = npFinalY + npCiH + 18;
|
||
// ── EXISTING PATIENT layout ───────────────────────────────────────────────
|
||
const epInsY = forkHY + 44;
|
||
const epInsH = 64;
|
||
const epInsW = 210;
|
||
const epSf1Y = epInsY + epInsH + 18;
|
||
const epSameCx = 638;
|
||
const epDiffCx = 760;
|
||
// no-same (transfer)
|
||
const epDiffY = epSf1Y + 20;
|
||
const epDiffH = 52;
|
||
const epDiffW = 130;
|
||
// same confirmed
|
||
const epSameY = epSf1Y + 20;
|
||
const epSameH = 52;
|
||
const epSameW = 192;
|
||
// sub-fork 2: masshealth / other
|
||
const epSf2Y = epSameY + epSameH + 18;
|
||
const epMhCx = 586;
|
||
const epOthCx = 706;
|
||
// other ins leaf
|
||
const epOthY = epSf2Y + 20;
|
||
const epOthH = 76;
|
||
const epOthW = 130;
|
||
// mh auto-check
|
||
const epMhY = epSf2Y + 20;
|
||
const epMhH = 62;
|
||
const epMhW = 175;
|
||
// selenium
|
||
const epSelY = epMhY + epMhH + 18;
|
||
const epSelH = 66;
|
||
const epSelW = 148;
|
||
// active / inactive
|
||
const epResHY = epSelY + epSelH + 14;
|
||
const epActCx = 534;
|
||
const epInaCx = 648;
|
||
const epResY = epResHY + 22;
|
||
const epActH = 76;
|
||
const epActW = 110;
|
||
const epInaH = 64;
|
||
const epInaW = 120;
|
||
const totalH_ep = epResY + Math.max(epActH, epInaH) + 18;
|
||
const totalH = Math.max(totalH_np, totalH_ep) + 10;
|
||
return (<svg viewBox={`0 0 ${W} ${totalH}`} className="w-full max-w-3xl mx-auto" role="img" aria-label="New patient AI conversation flow diagram">
|
||
<defs>
|
||
<marker id="nph2" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto" markerUnits="userSpaceOnUse">
|
||
<polygon points="0 0,10 3.5,0 7" fill="#9CA3AF"/>
|
||
</marker>
|
||
</defs>
|
||
|
||
{/* ═══════════ ENTRY — 3-message flow ═════════════════════════ */}
|
||
|
||
<rect x={nx} y={n1y} width={nW} height={n1h} rx={8} fill="#EFF6FF" stroke="#3B82F6" strokeWidth={1.5}/>
|
||
<text x={cx} y={n1y + 18} textAnchor="middle" fontSize={12} fontWeight="600" fill="#1D4ED8">Patient texts the office number</text>
|
||
<text x={cx} y={n1y + 34} textAnchor="middle" fontSize={10} fontWeight="600" fill="#3B82F6">AI sends 3 messages (TwiML, guaranteed order):</text>
|
||
<text x={cx} y={n1y + 50} textAnchor="middle" fontSize={9} fill="#6B7280">① Self-intro (New Patient Greeting template)</text>
|
||
<text x={cx} y={n1y + 64} textAnchor="middle" fontSize={9} fill="#6B7280">② Empathetic ack · ③ "New or existing patient?"</text>
|
||
|
||
<line x1={cx} y1={n1y + n1h} x2={cx} y2={n2y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
|
||
<rect x={nx} y={n2y} width={nW} height={n2h} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={cx} y={n2y + 20} textAnchor="middle" fontSize={12} fontWeight="600" fill="#065F46">AI classifies patient reply</text>
|
||
<text x={cx} y={n2y + 38} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">"new patient" / "existing" / "old patient" / …</text>
|
||
|
||
{/* ═══════════ MAIN FORK ══════════════════════════════════════ */}
|
||
|
||
<line x1={cx} y1={n2y + n2h} x2={cx} y2={forkHY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={lcx} y1={forkHY} x2={rcx} y2={forkHY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={lcx} y1={forkHY} x2={lcx} y2={npInsY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<line x1={rcx} y1={forkHY} x2={rcx} y2={epInsY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
|
||
<rect x={lcx - 44} y={forkHY - 13} width={88} height={24} rx={12} fill="#EEF2FF" stroke="#6366F1" strokeWidth={1.5}/>
|
||
<text x={lcx} y={forkHY + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#4338CA">New Patient</text>
|
||
<rect x={rcx - 52} y={forkHY - 13} width={104} height={24} rx={12} fill="#F5F3FF" stroke="#8B5CF6" strokeWidth={1.5}/>
|
||
<text x={rcx} y={forkHY + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#6D28D9">Existing Patient</text>
|
||
|
||
{/* ═══════════ NEW PATIENT BRANCH ═════════════════════════════ */}
|
||
|
||
{/* Insurance? */}
|
||
<rect x={lcx - brW / 2} y={npInsY} width={brW} height={npInsH} rx={8} fill="#EEF2FF" stroke="#6366F1" strokeWidth={1.5}/>
|
||
<text x={lcx} y={npInsY + 20} textAnchor="middle" fontSize={11} fontWeight="600" fill="#3730A3">Do you have dental insurance?</text>
|
||
<text x={lcx} y={npInsY + 38} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">"Yes" / "No" / "MassHealth" / …</text>
|
||
|
||
{/* Sub-fork 1 */}
|
||
<line x1={lcx} y1={npInsY + npInsH} x2={lcx} y2={npSf1Y} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={lhcx} y1={npSf1Y} x2={lncx} y2={npSf1Y} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={lhcx} y1={npSf1Y} x2={lhcx} y2={npWkY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<line x1={lncx} y1={npSf1Y} x2={lncx} y2={npNoInsY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
|
||
<rect x={lhcx - 20} y={npSf1Y - 11} width={40} height={22} rx={11} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={lhcx} y={npSf1Y + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#065F46">Yes</text>
|
||
<rect x={lncx - 24} y={npSf1Y - 11} width={48} height={22} rx={11} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={lncx} y={npSf1Y + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#C2410C">No ins.</text>
|
||
|
||
{/* No insurance → schedule */}
|
||
<rect x={lncx - npNoInsW / 2} y={npNoInsY} width={npNoInsW} height={npNoInsH} rx={8} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={lncx} y={npNoInsY + 16} textAnchor="middle" fontSize={10} fontWeight="600" fill="#15803D">No insurance</text>
|
||
<text x={lncx} y={npNoInsY + 32} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Ask preferred date & time</text>
|
||
<text x={lncx} y={npNoInsY + 48} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Office hours check</text>
|
||
<text x={lncx} y={npNoInsY + 64} textAnchor="middle" fontSize={9} fill="#047857" fontWeight="600">→ Appointment created</text>
|
||
|
||
{/* What kind? */}
|
||
<rect x={lhcx - npWkW / 2} y={npWkY} width={npWkW} height={npWkH} rx={8} fill="#EEF2FF" stroke="#6366F1" strokeWidth={1.5}/>
|
||
<text x={lhcx} y={npWkY + 20} textAnchor="middle" fontSize={11} fontWeight="600" fill="#3730A3">What kind of insurance?</text>
|
||
<text x={lhcx} y={npWkY + 38} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">"MassHealth" / "Blue Cross" / …</text>
|
||
|
||
{/* Sub-fork 2 */}
|
||
<line x1={lhcx} y1={npWkY + npWkH} x2={lhcx} y2={npSf2Y} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={mhcx} y1={npSf2Y} x2={othcx} y2={npSf2Y} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={mhcx} y1={npSf2Y} x2={mhcx} y2={npMhY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<line x1={othcx} y1={npSf2Y} x2={othcx} y2={npOthY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
|
||
<rect x={mhcx - 36} y={npSf2Y - 11} width={72} height={22} rx={11} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={mhcx} y={npSf2Y + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#065F46">MassHealth</text>
|
||
<rect x={othcx - 28} y={npSf2Y - 11} width={56} height={22} rx={11} fill="#E0F2FE" stroke="#0EA5E9" strokeWidth={1.5}/>
|
||
<text x={othcx} y={npSf2Y + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#0369A1">Other</text>
|
||
|
||
{/* Other ins → schedule */}
|
||
<rect x={othcx - npOthW / 2} y={npOthY} width={npOthW} height={npOthH} rx={8} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={othcx} y={npOthY + 16} textAnchor="middle" fontSize={10} fontWeight="600" fill="#15803D">Other insurance</text>
|
||
<text x={othcx} y={npOthY + 32} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Ask preferred date & time</text>
|
||
<text x={othcx} y={npOthY + 48} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Office hours check</text>
|
||
<text x={othcx} y={npOthY + 64} textAnchor="middle" fontSize={9} fill="#047857" fontWeight="600">→ Appointment created</text>
|
||
|
||
{/* MassHealth consent + ID+DOB */}
|
||
<rect x={mhcx - npMhW / 2} y={npMhY} width={npMhW} height={npMhH} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={mhcx} y={npMhY + 16} textAnchor="middle" fontSize={10} fontWeight="600" fill="#065F46">Check MassHealth now?</text>
|
||
<text x={mhcx} y={npMhY + 30} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Yes → send Member ID</text>
|
||
<text x={mhcx} y={npMhY + 44} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">+ DOB (MM/DD/YYYY)</text>
|
||
<text x={mhcx} y={npMhY + 62} textAnchor="middle" fontSize={8} fill="#9CA3AF" fontStyle="italic">No → ask date preference</text>
|
||
|
||
{/* NP Selenium */}
|
||
<line x1={mhcx} y1={npMhY + npMhH} x2={mhcx} y2={npSelY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<rect x={mhcx - npSelW / 2} y={npSelY} width={npSelW} height={npSelH} rx={8} fill="#F0F9FF" stroke="#0EA5E9" strokeWidth={1.5} strokeDasharray="5 3"/>
|
||
<rect x={mhcx - 38} y={npSelY - 11} width={76} height={22} rx={11} fill="#E0F2FE" stroke="#0EA5E9" strokeWidth={1.5}/>
|
||
<text x={mhcx} y={npSelY + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#0369A1">Selenium</text>
|
||
<text x={mhcx} y={npSelY + 20} textAnchor="middle" fontSize={10} fontWeight="600" fill="#0369A1">MassHealth Portal</text>
|
||
<text x={mhcx} y={npSelY + 36} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Auto-checks eligibility</text>
|
||
<text x={mhcx} y={npSelY + 52} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Result sent via SMS</text>
|
||
|
||
{/* NP ACTIVE / INACTIVE fork */}
|
||
<line x1={mhcx} y1={npSelY + npSelH} x2={mhcx} y2={npResHY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={npActCx} y1={npResHY} x2={npInaCx} y2={npResHY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={npActCx} y1={npResHY} x2={npActCx} y2={npResY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<line x1={npInaCx} y1={npResHY} x2={npInaCx} y2={npResY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
|
||
<rect x={npActCx - 28} y={npResHY - 11} width={56} height={22} rx={11} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={npActCx} y={npResHY + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#15803D">ACTIVE</text>
|
||
<rect x={npInaCx - 34} y={npResHY - 11} width={68} height={22} rx={11} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={npInaCx} y={npResHY + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#C2410C">INACTIVE</text>
|
||
|
||
{/* NP ACTIVE result */}
|
||
<rect x={npActCx - npActW / 2} y={npResY} width={npActW} height={npActH} rx={8} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={npActCx} y={npResY + 16} textAnchor="middle" fontSize={10} fontWeight="600" fill="#15803D">MassHealth active!</text>
|
||
<text x={npActCx} y={npResY + 30} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Ask date & time</text>
|
||
<text x={npActCx} y={npResY + 46} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Office hours check</text>
|
||
<text x={npActCx} y={npResY + 62} textAnchor="middle" fontSize={9} fill="#047857" fontWeight="600">→ Appt created</text>
|
||
|
||
{/* NP INACTIVE → other insurance? */}
|
||
<rect x={npInaCx - npInaW / 2} y={npResY} width={npInaW} height={npInaH} rx={8} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={npInaCx} y={npResY + 18} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Coverage inactive</text>
|
||
<text x={npInaCx} y={npResY + 36} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">or not verified</text>
|
||
|
||
<line x1={npInaCx} y1={npResY + npInaH} x2={npInaCx} y2={npOins2Y - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<rect x={npInaCx - npOins2W / 2} y={npOins2Y} width={npOins2W} height={npOins2H} rx={8} fill="#FEF9C3" stroke="#CA8A04" strokeWidth={1.5}/>
|
||
<text x={npInaCx} y={npOins2Y + 18} textAnchor="middle" fontSize={10} fontWeight="600" fill="#92400E">Other insurance?</text>
|
||
<text x={npInaCx} y={npOins2Y + 36} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">"Yes" or "No"</text>
|
||
|
||
{/* Final fork: yes=transfer, no=contact info */}
|
||
<line x1={npInaCx} y1={npOins2Y + npOins2H} x2={npInaCx} y2={npFinalY - 16} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={npTxCx} y1={npFinalY - 16} x2={npCiCx} y2={npFinalY - 16} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={npTxCx} y1={npFinalY - 16} x2={npTxCx} y2={npFinalY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<line x1={npCiCx} y1={npFinalY - 16} x2={npCiCx} y2={npFinalY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
|
||
<rect x={npTxCx - 22} y={npFinalY - 28} width={44} height={22} rx={11} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={npTxCx} y={npFinalY - 13} textAnchor="middle" fontSize={10} fontWeight="700" fill="#15803D">Yes</text>
|
||
<rect x={npCiCx - 22} y={npFinalY - 28} width={44} height={22} rx={11} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={npCiCx} y={npFinalY - 13} textAnchor="middle" fontSize={10} fontWeight="700" fill="#C2410C">No</text>
|
||
|
||
<rect x={npTxCx - npTxW / 2} y={npFinalY} width={npTxW} height={npTxH} rx={8} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={npTxCx} y={npFinalY + 16} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Transfer</text>
|
||
<text x={npTxCx} y={npFinalY + 32} textAnchor="middle" fontSize={9} fill="#9CA3AF" fontStyle="italic">to staff</text>
|
||
|
||
<rect x={npCiCx - npCiW / 2} y={npFinalY} width={npCiW} height={npCiH} rx={8} fill="#F9FAFB" stroke="#D1D5DB" strokeWidth={1.5}/>
|
||
<text x={npCiCx} y={npFinalY + 16} textAnchor="middle" fontSize={10} fontWeight="600" fill="#374151">Leave name & phone</text>
|
||
<text x={npCiCx} y={npFinalY + 32} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Receptionist will</text>
|
||
<text x={npCiCx} y={npFinalY + 48} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">contact you shortly</text>
|
||
|
||
{/* ═══════════ EXISTING PATIENT BRANCH ════════════════════════ */}
|
||
|
||
{/* Same insurance? */}
|
||
<rect x={rcx - epInsW / 2} y={epInsY} width={epInsW} height={epInsH} rx={8} fill="#F5F3FF" stroke="#8B5CF6" strokeWidth={1.5}/>
|
||
<text x={rcx} y={epInsY + 18} textAnchor="middle" fontSize={11} fontWeight="600" fill="#5B21B6">Got it — existing patient!</text>
|
||
<text x={rcx} y={epInsY + 34} textAnchor="middle" fontSize={11} fontWeight="600" fill="#5B21B6">Same insurance on file?</text>
|
||
<text x={rcx} y={epInsY + 52} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">"Yes" / "No" / "Changed"</text>
|
||
|
||
{/* Sub-fork 1 */}
|
||
<line x1={rcx} y1={epInsY + epInsH} x2={rcx} y2={epSf1Y} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={epSameCx} y1={epSf1Y} x2={epDiffCx} y2={epSf1Y} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={epSameCx} y1={epSf1Y} x2={epSameCx} y2={epSameY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<line x1={epDiffCx} y1={epSf1Y} x2={epDiffCx} y2={epDiffY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
|
||
<rect x={epSameCx - 22} y={epSf1Y - 11} width={44} height={22} rx={11} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={epSameCx} y={epSf1Y + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#15803D">YES</text>
|
||
<rect x={epDiffCx - 22} y={epSf1Y - 11} width={44} height={22} rx={11} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={epDiffCx} y={epSf1Y + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#C2410C">NO</text>
|
||
|
||
{/* Changed → transfer */}
|
||
<rect x={epDiffCx - epDiffW / 2} y={epDiffY} width={epDiffW} height={epDiffH} rx={8} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={epDiffCx} y={epDiffY + 18} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Insurance changed</text>
|
||
<text x={epDiffCx} y={epDiffY + 36} textAnchor="middle" fontSize={9} fill="#9CA3AF" fontStyle="italic">Transfer to staff</text>
|
||
|
||
{/* Same confirmed */}
|
||
<rect x={epSameCx - epSameW / 2} y={epSameY} width={epSameW} height={epSameH} rx={8} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={epSameCx} y={epSameY + 20} textAnchor="middle" fontSize={11} fontWeight="600" fill="#15803D">Insurance confirmed</text>
|
||
<text x={epSameCx} y={epSameY + 38} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">MassHealth or Other?</text>
|
||
|
||
{/* Sub-fork 2 */}
|
||
<line x1={epSameCx} y1={epSameY + epSameH} x2={epSameCx} y2={epSf2Y} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={epMhCx} y1={epSf2Y} x2={epOthCx} y2={epSf2Y} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={epMhCx} y1={epSf2Y} x2={epMhCx} y2={epMhY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<line x1={epOthCx} y1={epSf2Y} x2={epOthCx} y2={epOthY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
|
||
<rect x={epMhCx - 36} y={epSf2Y - 11} width={72} height={22} rx={11} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={epMhCx} y={epSf2Y + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#065F46">MassHealth</text>
|
||
<rect x={epOthCx - 28} y={epSf2Y - 11} width={56} height={22} rx={11} fill="#E0F2FE" stroke="#0EA5E9" strokeWidth={1.5}/>
|
||
<text x={epOthCx} y={epSf2Y + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#0369A1">Other</text>
|
||
|
||
{/* Other ins → schedule */}
|
||
<rect x={epOthCx - epOthW / 2} y={epOthY} width={epOthW} height={epOthH} rx={8} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={epOthCx} y={epOthY + 16} textAnchor="middle" fontSize={10} fontWeight="600" fill="#15803D">Other insurance</text>
|
||
<text x={epOthCx} y={epOthY + 32} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Ask preferred date & time</text>
|
||
<text x={epOthCx} y={epOthY + 48} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Office hours check</text>
|
||
<text x={epOthCx} y={epOthY + 64} textAnchor="middle" fontSize={9} fill="#047857" fontWeight="600">→ Appointment created</text>
|
||
|
||
{/* EP MassHealth auto-check */}
|
||
<rect x={epMhCx - epMhW / 2} y={epMhY} width={epMhW} height={epMhH} rx={8} fill="#ECFDF5" stroke="#10B981" strokeWidth={1.5}/>
|
||
<text x={epMhCx} y={epMhY + 18} textAnchor="middle" fontSize={10} fontWeight="600" fill="#065F46">MassHealth auto-check</text>
|
||
<text x={epMhCx} y={epMhY + 34} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Using stored ID + DOB</text>
|
||
<text x={epMhCx} y={epMhY + 50} textAnchor="middle" fontSize={8} fill="#9CA3AF" fontStyle="italic">Please wait 30-60 secs…</text>
|
||
|
||
{/* EP Selenium */}
|
||
<line x1={epMhCx} y1={epMhY + epMhH} x2={epMhCx} y2={epSelY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<rect x={epMhCx - epSelW / 2} y={epSelY} width={epSelW} height={epSelH} rx={8} fill="#F0F9FF" stroke="#0EA5E9" strokeWidth={1.5} strokeDasharray="5 3"/>
|
||
<rect x={epMhCx - 38} y={epSelY - 11} width={76} height={22} rx={11} fill="#E0F2FE" stroke="#0EA5E9" strokeWidth={1.5}/>
|
||
<text x={epMhCx} y={epSelY + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#0369A1">Selenium</text>
|
||
<text x={epMhCx} y={epSelY + 20} textAnchor="middle" fontSize={10} fontWeight="600" fill="#0369A1">MassHealth Portal</text>
|
||
<text x={epMhCx} y={epSelY + 36} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Auto-checks eligibility</text>
|
||
<text x={epMhCx} y={epSelY + 52} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Result sent via SMS</text>
|
||
|
||
{/* EP ACTIVE / INACTIVE */}
|
||
<line x1={epMhCx} y1={epSelY + epSelH} x2={epMhCx} y2={epResHY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={epActCx} y1={epResHY} x2={epInaCx} y2={epResHY} stroke="#9CA3AF" strokeWidth={1.5}/>
|
||
<line x1={epActCx} y1={epResHY} x2={epActCx} y2={epResY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
<line x1={epInaCx} y1={epResHY} x2={epInaCx} y2={epResY - 2} stroke="#9CA3AF" strokeWidth={1.5} markerEnd="url(#nph2)"/>
|
||
|
||
<rect x={epActCx - 28} y={epResHY - 11} width={56} height={22} rx={11} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={epActCx} y={epResHY + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#15803D">ACTIVE</text>
|
||
<rect x={epInaCx - 34} y={epResHY - 11} width={68} height={22} rx={11} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={epInaCx} y={epResHY + 4} textAnchor="middle" fontSize={10} fontWeight="700" fill="#C2410C">INACTIVE</text>
|
||
|
||
<rect x={epActCx - epActW / 2} y={epResY} width={epActW} height={epActH} rx={8} fill="#F0FDF4" stroke="#22C55E" strokeWidth={1.5}/>
|
||
<text x={epActCx} y={epResY + 16} textAnchor="middle" fontSize={10} fontWeight="600" fill="#15803D">Coverage active!</text>
|
||
<text x={epActCx} y={epResY + 30} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Ask date & time</text>
|
||
<text x={epActCx} y={epResY + 46} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Office hours check</text>
|
||
<text x={epActCx} y={epResY + 62} textAnchor="middle" fontSize={9} fill="#047857" fontWeight="600">→ Appt created</text>
|
||
|
||
<rect x={epInaCx - epInaW / 2} y={epResY} width={epInaW} height={epInaH} rx={8} fill="#FFF7ED" stroke="#F97316" strokeWidth={1.5}/>
|
||
<text x={epInaCx} y={epResY + 18} textAnchor="middle" fontSize={10} fontWeight="600" fill="#C2410C">Coverage inactive</text>
|
||
<text x={epInaCx} y={epResY + 34} textAnchor="middle" fontSize={9} fill="#6B7280" fontStyle="italic">Self-pay offer?</text>
|
||
<text x={epInaCx} y={epResY + 50} textAnchor="middle" fontSize={8} fill="#9CA3AF" fontStyle="italic">YES → schedule / NO → close</text>
|
||
</svg>);
|
||
}
|
||
// ─── Users AI Chat (internal staff assistant) config ─────────────────────────
|
||
const INTERNAL_CHAT_PLACEHOLDER = `Examples of custom context you can add:
|
||
• "This office primarily treats MassHealth patients — always prefer MassHealth eligibility checks."
|
||
• "When a patient name is typed, also show their upcoming appointment if available."
|
||
• "Default provider is Mary Scannell."`;
|
||
function InternalChatSettingsCard() {
|
||
const { toast } = useToast();
|
||
const [systemPrompt, setSystemPrompt] = useState("");
|
||
const initialized = useRef(false);
|
||
const { data, isLoading } = useQuery({
|
||
queryKey: ["/api/ai/internal-chat-settings"],
|
||
queryFn: async () => {
|
||
const res = await apiRequest("GET", "/api/ai/internal-chat-settings");
|
||
if (!res.ok)
|
||
return { systemPrompt: "" };
|
||
return res.json();
|
||
},
|
||
staleTime: Infinity,
|
||
refetchOnWindowFocus: false,
|
||
});
|
||
useEffect(() => {
|
||
if (data && !initialized.current) {
|
||
initialized.current = true;
|
||
setSystemPrompt(data.systemPrompt ?? "");
|
||
}
|
||
}, [data]);
|
||
const saveMutation = useMutation({
|
||
mutationFn: async (prompt) => {
|
||
const res = await apiRequest("PUT", "/api/ai/internal-chat-settings", { systemPrompt: prompt });
|
||
if (!res.ok)
|
||
throw new Error("Failed to save");
|
||
return res.json();
|
||
},
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ["/api/ai/internal-chat-settings"] });
|
||
toast({ title: "Users AI Chat settings saved" });
|
||
},
|
||
onError: () => {
|
||
toast({ title: "Error", description: "Failed to save settings.", variant: "destructive" });
|
||
},
|
||
});
|
||
return (<Card>
|
||
<CardContent className="py-6 space-y-5">
|
||
<div className="flex items-center gap-2">
|
||
<SlidersHorizontal className="h-5 w-5 text-primary"/>
|
||
<h3 className="text-lg font-semibold">Users AI Chat</h3>
|
||
</div>
|
||
|
||
<p className="text-sm text-muted-foreground">
|
||
Configure the AI assistant available to staff via the{" "}
|
||
<span className="font-medium text-foreground">bot icon</span> in the top-right corner.
|
||
Staff can type natural language commands such as{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">"check MARIA GONZALES"</code>{" "}
|
||
to search for a patient and pre-fill their eligibility check, or{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">"open claims"</code>{" "}
|
||
to navigate directly to a page.
|
||
</p>
|
||
|
||
{/* Built-in workflows */}
|
||
<div className="space-y-2">
|
||
<p className="text-xs font-semibold text-foreground uppercase tracking-wide">Built-in Workflows</p>
|
||
<div className="divide-y rounded-lg border overflow-hidden text-xs">
|
||
|
||
{/* Eligibility by name */}
|
||
<div className="flex items-start gap-3 p-3 bg-background">
|
||
<span className="mt-0.5 shrink-0 text-base">🏥</span>
|
||
<div className="space-y-0.5">
|
||
<p className="font-medium text-foreground">Eligibility by patient name</p>
|
||
<p className="text-muted-foreground">
|
||
Looks up the patient in the database, resolves their insurance, and opens the eligibility page pre-filled.
|
||
</p>
|
||
<div className="flex flex-wrap gap-1 pt-1">
|
||
{["check Maria Jesus", "verify insurance for John Smith"].map((ex) => (<code key={ex} className="bg-muted px-1.5 py-0.5 rounded font-mono">{ex}</code>))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Eligibility by member ID */}
|
||
<div className="flex items-start gap-3 p-3 bg-muted/20">
|
||
<span className="mt-0.5 shrink-0 text-base">🔢</span>
|
||
<div className="space-y-0.5">
|
||
<p className="font-medium text-foreground">Eligibility by Member ID + DOB</p>
|
||
<p className="text-muted-foreground">
|
||
Provide a member ID and date of birth. Insurance is resolved from the patient record, or from what you state in the message.
|
||
</p>
|
||
<div className="flex flex-wrap gap-1 pt-1">
|
||
{["check masshealth for 100xxxx, 10/10/1988"].map((ex) => (<code key={ex} className="bg-muted px-1.5 py-0.5 rounded font-mono">{ex}</code>))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Check & Claim */}
|
||
<div className="flex items-start gap-3 p-3 bg-background">
|
||
<span className="mt-0.5 shrink-0 text-base">⚡</span>
|
||
<div className="space-y-0.5">
|
||
<p className="font-medium text-foreground">Check eligibility + claim procedures</p>
|
||
<p className="text-muted-foreground">
|
||
Resolves the patient, maps procedure names to CDT codes using the fee schedule, and opens the eligibility page ready to check and claim. Procedure names or CDT codes both work.
|
||
</p>
|
||
<div className="flex flex-wrap gap-1 pt-1">
|
||
{[
|
||
"check masshealth for 100xxxx, 10/10/1988 and claim perio exam and adult cleaning",
|
||
"check Maria Jesus and claim D0120 D1110",
|
||
].map((ex) => (<code key={ex} className="bg-muted px-1.5 py-0.5 rounded font-mono block">{ex}</code>))}
|
||
</div>
|
||
<p className="text-muted-foreground pt-1">
|
||
CDT codes are looked up from the fee schedule — no AI translation needed.
|
||
If insurance is unknown the assistant will ask which one to use.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Navigation */}
|
||
<div className="flex items-start gap-3 p-3 bg-muted/20">
|
||
<span className="mt-0.5 shrink-0 text-base">🗺️</span>
|
||
<div className="space-y-0.5">
|
||
<p className="font-medium text-foreground">Navigation</p>
|
||
<p className="text-muted-foreground">Opens any page in the app.</p>
|
||
<div className="flex flex-wrap gap-1 pt-1">
|
||
{["open claims", "go to schedule", "find patient GONZALES"].map((ex) => (<code key={ex} className="bg-muted px-1.5 py-0.5 rounded font-mono">{ex}</code>))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
{/* CDT Aliases */}
|
||
<CdtAliasesCard />
|
||
|
||
{/* Additional context / system prompt */}
|
||
<div className="space-y-1.5">
|
||
<div className="flex items-center gap-2">
|
||
<Bot className="h-4 w-4 text-primary"/>
|
||
<span className="text-sm font-medium">Additional AI Context</span>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">
|
||
Optional instructions injected into the AI's system prompt. Use this to describe your
|
||
practice's preferences, common shortcuts, or patient demographics.
|
||
</p>
|
||
{isLoading ? (<p className="text-sm text-muted-foreground">Loading...</p>) : (<Textarea value={systemPrompt} onChange={(e) => setSystemPrompt(e.target.value)} placeholder={INTERNAL_CHAT_PLACEHOLDER} rows={5} className="text-sm resize-none"/>)}
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3 pt-1">
|
||
<Button disabled={saveMutation.isPending || isLoading} className="bg-teal-600 hover:bg-teal-700 text-white" onClick={() => saveMutation.mutate(systemPrompt)}>
|
||
{saveMutation.isPending ? "Saving..." : "Save"}
|
||
</Button>
|
||
{systemPrompt && (<Button variant="ghost" className="text-xs text-muted-foreground" onClick={() => setSystemPrompt("")}>
|
||
Clear
|
||
</Button>)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>);
|
||
}
|
||
// ─── CDT Aliases card ────────────────────────────────────────────────────────
|
||
const BUILTIN_ALIASES = [
|
||
{ phrase: "perio exam", cdtCode: "D0120" },
|
||
{ phrase: "periodic exam", cdtCode: "D0120" },
|
||
{ phrase: "adult cleaning", cdtCode: "D1110" },
|
||
{ phrase: "adult prophy", cdtCode: "D1110" },
|
||
{ phrase: "child cleaning", cdtCode: "D1120" },
|
||
{ phrase: "full mouth xray", cdtCode: "D0210" },
|
||
{ phrase: "fmx", cdtCode: "D0210" },
|
||
{ phrase: "pano", cdtCode: "D0330" },
|
||
{ phrase: "comp exam", cdtCode: "D0150" },
|
||
{ phrase: "limited exam", cdtCode: "D0140" },
|
||
{ phrase: "scaling root planing", cdtCode: "D4341" },
|
||
{ phrase: "srp", cdtCode: "D4341" },
|
||
{ phrase: "perio maintenance", cdtCode: "D4910" },
|
||
];
|
||
function CdtAliasesCard() {
|
||
const { toast } = useToast();
|
||
const [aliases, setAliases] = useState([]);
|
||
const [newPhrase, setNewPhrase] = useState("");
|
||
const [newCode, setNewCode] = useState("");
|
||
const [showBuiltin, setShowBuiltin] = useState(false);
|
||
const initialized = useRef(false);
|
||
const { data, isLoading } = useQuery({
|
||
queryKey: ["/api/ai/cdt-aliases"],
|
||
queryFn: async () => {
|
||
const res = await apiRequest("GET", "/api/ai/cdt-aliases");
|
||
if (!res.ok)
|
||
return [];
|
||
return res.json();
|
||
},
|
||
staleTime: Infinity,
|
||
refetchOnWindowFocus: false,
|
||
});
|
||
useEffect(() => {
|
||
if (data && !initialized.current) {
|
||
initialized.current = true;
|
||
setAliases(data);
|
||
}
|
||
}, [data]);
|
||
const saveMutation = useMutation({
|
||
mutationFn: async (list) => {
|
||
const res = await apiRequest("PUT", "/api/ai/cdt-aliases", list);
|
||
if (!res.ok)
|
||
throw new Error("Failed to save");
|
||
return res.json();
|
||
},
|
||
onSuccess: (saved) => {
|
||
setAliases(saved);
|
||
queryClient.invalidateQueries({ queryKey: ["/api/ai/cdt-aliases"] });
|
||
toast({ title: "CDT aliases saved" });
|
||
},
|
||
onError: () => {
|
||
toast({ title: "Error", description: "Failed to save aliases.", variant: "destructive" });
|
||
},
|
||
});
|
||
const handleAdd = () => {
|
||
const phrase = newPhrase.trim().toLowerCase();
|
||
const code = newCode.trim().toUpperCase();
|
||
if (!phrase || !code)
|
||
return;
|
||
if (aliases.some((a) => a.phrase === phrase)) {
|
||
toast({ title: "Duplicate", description: `"${phrase}" already exists.`, variant: "destructive" });
|
||
return;
|
||
}
|
||
const updated = [...aliases, { phrase, cdtCode: code }];
|
||
setAliases(updated);
|
||
saveMutation.mutate(updated);
|
||
setNewPhrase("");
|
||
setNewCode("");
|
||
};
|
||
const handleDelete = (idx) => {
|
||
const updated = aliases.filter((_, i) => i !== idx);
|
||
setAliases(updated);
|
||
saveMutation.mutate(updated);
|
||
};
|
||
return (<Card>
|
||
<CardContent className="py-6 space-y-4">
|
||
<div className="flex items-center gap-2">
|
||
<BookMarked className="h-5 w-5 text-primary"/>
|
||
<h3 className="text-base font-semibold">CDT Aliases</h3>
|
||
</div>
|
||
|
||
<p className="text-xs text-muted-foreground">
|
||
Map your own shorthand phrases to CDT codes. These override the built-in aliases when
|
||
staff type procedure names in the chat (e.g.{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded font-mono">"check & claim perio exam"</code>).
|
||
Phrases are matched case-insensitively.
|
||
</p>
|
||
|
||
{/* Custom alias list */}
|
||
{isLoading ? (<p className="text-sm text-muted-foreground">Loading...</p>) : aliases.length === 0 ? (<p className="text-xs text-muted-foreground italic">No custom aliases yet — add one below.</p>) : (<div className="rounded-lg border overflow-hidden">
|
||
<table className="w-full text-xs">
|
||
<thead className="bg-muted/50">
|
||
<tr>
|
||
<th className="text-left px-3 py-2 font-medium text-muted-foreground">Phrase (what staff type)</th>
|
||
<th className="text-left px-3 py-2 font-medium text-muted-foreground">CDT Code</th>
|
||
<th className="w-8"/>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y">
|
||
{aliases.map((a, i) => (<tr key={i} className="bg-background">
|
||
<td className="px-3 py-2">
|
||
<code className="font-mono">{a.phrase}</code>
|
||
</td>
|
||
<td className="px-3 py-2 font-mono font-semibold text-primary">{a.cdtCode}</td>
|
||
<td className="px-2 py-1">
|
||
<Button size="sm" variant="ghost" className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive" onClick={() => handleDelete(i)} disabled={saveMutation.isPending}>
|
||
<Trash2 className="h-3.5 w-3.5"/>
|
||
</Button>
|
||
</td>
|
||
</tr>))}
|
||
</tbody>
|
||
</table>
|
||
</div>)}
|
||
|
||
{/* Add row */}
|
||
<div className="flex gap-2 items-end">
|
||
<div className="flex-1 space-y-1">
|
||
<p className="text-xs text-muted-foreground">Phrase</p>
|
||
<Input value={newPhrase} onChange={(e) => setNewPhrase(e.target.value)} placeholder='e.g. "cleaning adult"' className="h-8 text-xs" onKeyDown={(e) => { if (e.key === "Enter")
|
||
handleAdd(); }}/>
|
||
</div>
|
||
<div className="w-28 space-y-1">
|
||
<p className="text-xs text-muted-foreground">CDT Code</p>
|
||
<Input value={newCode} onChange={(e) => setNewCode(e.target.value)} placeholder="D1110" className="h-8 text-xs font-mono" onKeyDown={(e) => { if (e.key === "Enter")
|
||
handleAdd(); }}/>
|
||
</div>
|
||
<Button size="sm" className="h-8 gap-1.5 shrink-0" onClick={handleAdd} disabled={!newPhrase.trim() || !newCode.trim() || saveMutation.isPending}>
|
||
<Plus className="h-3.5 w-3.5"/>
|
||
Add
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Built-in aliases reference (collapsible) */}
|
||
<div className="border rounded-lg overflow-hidden">
|
||
<button className="w-full flex items-center justify-between px-3 py-2 text-xs font-medium text-muted-foreground hover:bg-muted/40 transition-colors" onClick={() => setShowBuiltin((v) => !v)}>
|
||
<span>View built-in aliases ({BUILTIN_ALIASES.length})</span>
|
||
{showBuiltin ? <ChevronUp className="h-3.5 w-3.5"/> : <ChevronDown className="h-3.5 w-3.5"/>}
|
||
</button>
|
||
{showBuiltin && (<table className="w-full text-xs border-t">
|
||
<tbody className="divide-y">
|
||
{BUILTIN_ALIASES.map((a) => (<tr key={a.phrase} className="bg-muted/20">
|
||
<td className="px-3 py-1.5">
|
||
<code className="font-mono text-muted-foreground">{a.phrase}</code>
|
||
</td>
|
||
<td className="px-3 py-1.5 font-mono font-semibold text-muted-foreground">{a.cdtCode}</td>
|
||
<td className="px-3 py-1.5 text-muted-foreground/60 italic text-[10px]">built-in</td>
|
||
</tr>))}
|
||
</tbody>
|
||
</table>)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>);
|
||
}
|
||
// ─── Main component ───────────────────────────────────────────────────────────
|
||
export function AiChatSettingsCard() {
|
||
const { toast } = useToast();
|
||
const [reminderGreeting, setReminderGreeting] = useState(DEFAULTS.reminderGreeting);
|
||
const [newPatientGreeting, setNewPatientGreeting] = useState(DEFAULTS.newPatientGreeting);
|
||
const [generalFallback, setGeneralFallback] = useState(DEFAULTS.generalFallback);
|
||
const initialized = useRef(false);
|
||
const [openPhoneReply, setOpenPhoneReply] = useState(false);
|
||
const { data: advancedSettings } = useQuery({
|
||
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) => {
|
||
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([]);
|
||
const smsInitialized = useRef(false);
|
||
const { data: officeContact } = useQuery({
|
||
queryKey: ["/api/office-contact"],
|
||
queryFn: async () => {
|
||
const res = await apiRequest("GET", "/api/office-contact");
|
||
if (!res.ok)
|
||
return null;
|
||
return res.json();
|
||
},
|
||
staleTime: 60_000,
|
||
});
|
||
const { data: templates, isLoading } = useQuery({
|
||
queryKey: ["/api/ai/chat-templates"],
|
||
queryFn: async () => {
|
||
const res = await apiRequest("GET", "/api/ai/chat-templates");
|
||
if (!res.ok)
|
||
throw new Error("Failed to load templates");
|
||
return res.json();
|
||
},
|
||
staleTime: Infinity,
|
||
refetchOnWindowFocus: false,
|
||
});
|
||
const { data: smsTemplateListData, isLoading: smsLoading } = useQuery({
|
||
queryKey: ["/api/twilio/sms-template-list"],
|
||
queryFn: async () => {
|
||
const res = await apiRequest("GET", "/api/twilio/sms-template-list");
|
||
if (!res.ok)
|
||
return [];
|
||
return res.json();
|
||
},
|
||
staleTime: Infinity,
|
||
refetchOnWindowFocus: false,
|
||
});
|
||
// Seed AI chat templates
|
||
useEffect(() => {
|
||
if (templates && !initialized.current) {
|
||
initialized.current = true;
|
||
setReminderGreeting(templates.reminderGreeting || DEFAULTS.reminderGreeting);
|
||
setNewPatientGreeting(templates.newPatientGreeting || DEFAULTS.newPatientGreeting);
|
||
setGeneralFallback(templates.generalFallback || DEFAULTS.generalFallback);
|
||
}
|
||
}, [templates]);
|
||
// Seed SMS template list — fall back to the saved reminderSms if list is empty
|
||
useEffect(() => {
|
||
if (smsTemplateListData && !smsInitialized.current) {
|
||
smsInitialized.current = true;
|
||
if (smsTemplateListData.length > 0) {
|
||
setSmsTemplates(smsTemplateListData);
|
||
}
|
||
else {
|
||
setSmsTemplates(DEFAULT_SMS_TEMPLATES);
|
||
}
|
||
}
|
||
}, [smsTemplateListData, templates]);
|
||
const saveMutation = useMutation({
|
||
mutationFn: async (data) => {
|
||
const res = await apiRequest("PUT", "/api/ai/chat-templates", data);
|
||
if (!res.ok) {
|
||
const err = await res.json().catch(() => null);
|
||
throw new Error(err?.message || "Failed to save templates");
|
||
}
|
||
return res.json();
|
||
},
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ["/api/ai/chat-templates"] });
|
||
toast({ title: "Templates saved", description: "AI chat templates have been updated." });
|
||
},
|
||
onError: (err) => {
|
||
toast({ title: "Error", description: err?.message, variant: "destructive" });
|
||
},
|
||
});
|
||
const saveSmsListMutation = useMutation({
|
||
mutationFn: async (list) => {
|
||
const res = await apiRequest("PUT", "/api/twilio/sms-template-list", list);
|
||
if (!res.ok) {
|
||
const err = await res.json().catch(() => null);
|
||
throw new Error(err?.message || "Failed to save SMS templates");
|
||
}
|
||
return res.json();
|
||
},
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ["/api/twilio/sms-template-list"] });
|
||
toast({ title: "SMS template saved" });
|
||
},
|
||
onError: (err) => {
|
||
toast({ title: "Error", description: err?.message, variant: "destructive" });
|
||
},
|
||
});
|
||
const handleSubmit = (e) => {
|
||
e.preventDefault();
|
||
saveMutation.mutate({
|
||
reminderGreeting: reminderGreeting.trim() || DEFAULTS.reminderGreeting,
|
||
newPatientGreeting: newPatientGreeting.trim() || DEFAULTS.newPatientGreeting,
|
||
generalFallback: generalFallback.trim() || DEFAULTS.generalFallback,
|
||
});
|
||
};
|
||
const updateSmsTemplate = (idx, field, value) => {
|
||
setSmsTemplates((prev) => prev.map((t, i) => i === idx ? { ...t, [field]: value } : t));
|
||
};
|
||
const deleteSmsTemplate = (idx) => {
|
||
const updated = smsTemplates.filter((_, i) => i !== idx);
|
||
setSmsTemplates(updated);
|
||
saveSmsListMutation.mutate(updated);
|
||
};
|
||
const addSmsTemplate = () => {
|
||
setSmsTemplates((prev) => [...prev, { id: newId(), name: "", body: "" }]);
|
||
};
|
||
const officeName = officeContact?.officeName?.trim() || "";
|
||
const templateFields = [
|
||
{
|
||
key: "reminder",
|
||
icon: <CalendarCheck className="h-4 w-4 text-primary"/>,
|
||
label: "Appointment Reminder Reply",
|
||
description: "Sent when the AI first introduces itself after an appointment reminder.",
|
||
value: reminderGreeting,
|
||
onChange: setReminderGreeting,
|
||
placeholder: DEFAULTS.reminderGreeting,
|
||
},
|
||
{
|
||
key: "newPatient",
|
||
icon: <UserPlus className="h-4 w-4 text-primary"/>,
|
||
label: "New Patient Greeting",
|
||
description: "Sent when a new patient texts in for the first time.",
|
||
value: newPatientGreeting,
|
||
onChange: setNewPatientGreeting,
|
||
placeholder: DEFAULTS.newPatientGreeting,
|
||
},
|
||
{
|
||
key: "general",
|
||
icon: <MessageCircle className="h-4 w-4 text-primary"/>,
|
||
label: "General Fallback",
|
||
description: "Used when the AI cannot determine the context of the patient's message.",
|
||
value: generalFallback,
|
||
onChange: setGeneralFallback,
|
||
placeholder: DEFAULTS.generalFallback,
|
||
},
|
||
];
|
||
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">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<MessageSquare className="h-5 w-5 text-primary"/>
|
||
<h3 className="text-lg font-semibold">SMS Templates</h3>
|
||
</div>
|
||
<Button type="button" size="sm" variant="outline" className="gap-1.5 text-xs" onClick={addSmsTemplate}>
|
||
<Plus className="h-3.5 w-3.5"/>
|
||
Add Template
|
||
</Button>
|
||
</div>
|
||
|
||
<p className="text-sm text-muted-foreground">
|
||
Available variables:{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{firstName}"}</code>{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{officeName}"}</code>{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{officeAddress}"}</code>{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono" title="Office call-in phone number">{"{officePhone}"}</code>{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono" title="Twilio SMS number patients text back to">{"{twilioPhone}"}</code>{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{appointmentDate}"}</code>{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{appointmentTime}"}</code>
|
||
</p>
|
||
|
||
{officeName && (<div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 rounded px-3 py-2">
|
||
<Info className="h-3.5 w-3.5 flex-shrink-0"/>
|
||
<span>
|
||
<span className="font-medium">{"{officeName}"}</span> will display as{" "}
|
||
<span className="font-medium text-foreground">"{officeName}"</span>
|
||
</span>
|
||
</div>)}
|
||
|
||
{smsLoading ? (<p className="text-sm text-muted-foreground">Loading...</p>) : smsTemplates.length === 0 ? (<p className="text-sm text-muted-foreground italic">
|
||
No templates yet. Click "Add Template" to create one.
|
||
</p>) : (<div className="space-y-4">
|
||
{smsTemplates.map((tpl, idx) => (<div key={tpl.id} className="border rounded-lg p-4 space-y-3">
|
||
{/* Name row */}
|
||
<div className="flex items-center gap-2">
|
||
{idx === 0 && (<span className="text-xs bg-teal-100 text-teal-700 font-medium px-2 py-0.5 rounded-full whitespace-nowrap">
|
||
Batch reminder
|
||
</span>)}
|
||
<input type="text" value={tpl.name} onChange={(e) => updateSmsTemplate(idx, "name", e.target.value)} className="flex-1 p-2 border rounded text-sm font-medium min-w-0" placeholder="Template name"/>
|
||
<Button type="button" variant="ghost" size="sm" className="text-red-500 hover:text-red-700 hover:bg-red-50 flex-shrink-0" onClick={() => deleteSmsTemplate(idx)}>
|
||
<Trash2 className="h-4 w-4"/>
|
||
</Button>
|
||
</div>
|
||
|
||
{/* Body */}
|
||
<Textarea value={tpl.body} onChange={(e) => updateSmsTemplate(idx, "body", e.target.value)} rows={3} className="text-sm resize-none" placeholder="Template content…"/>
|
||
|
||
{/* Live preview */}
|
||
{officeName && tpl.body.includes("{officeName}") && (<p className="text-xs text-muted-foreground italic pl-1">
|
||
Preview: {previewTemplate(tpl.body, officeName)}
|
||
</p>)}
|
||
|
||
{/* Save button */}
|
||
<Button type="button" size="sm" disabled={saveSmsListMutation.isPending} className="bg-teal-600 hover:bg-teal-700 text-white" onClick={() => saveSmsListMutation.mutate(smsTemplates)}>
|
||
{saveSmsListMutation.isPending ? "Saving…" : "Save"}
|
||
</Button>
|
||
</div>))}
|
||
</div>)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* ── Section 1: Chat Templates ────────────────────────────── */}
|
||
<Card>
|
||
<CardContent className="py-6 space-y-5">
|
||
<div className="flex items-center gap-2">
|
||
<Bot className="h-5 w-5 text-primary"/>
|
||
<h3 className="text-lg font-semibold">Chat Templates</h3>
|
||
</div>
|
||
<p className="text-sm text-muted-foreground">
|
||
Customize how the AI assistant introduces itself to patients. Use{" "}
|
||
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{"{officeName}"}</code>{" "}
|
||
as a placeholder — it will be replaced with your dental office name automatically.
|
||
</p>
|
||
|
||
{officeName && (<div className="flex items-center gap-2 text-xs text-muted-foreground bg-muted/50 rounded px-3 py-2">
|
||
<Info className="h-3.5 w-3.5 flex-shrink-0"/>
|
||
<span>
|
||
<span className="font-medium">{"{officeName}"}</span> will display as{" "}
|
||
<span className="font-medium text-foreground">"{officeName}"</span>
|
||
</span>
|
||
</div>)}
|
||
|
||
{isLoading ? (<p className="text-sm text-muted-foreground">Loading templates...</p>) : (<form onSubmit={handleSubmit} className="space-y-5">
|
||
{templateFields.map((f) => (<div key={f.key} className="space-y-1.5">
|
||
<div className="flex items-center gap-2">
|
||
{f.icon}
|
||
<span className="text-sm font-medium">{f.label}</span>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">{f.description}</p>
|
||
<Textarea value={f.value} onChange={(e) => f.onChange(e.target.value)} placeholder={f.placeholder} rows={3} className="text-sm resize-none"/>
|
||
{officeName && f.value.includes("{officeName}") && (<p className="text-xs text-muted-foreground italic pl-1">
|
||
Preview: {previewTemplate(f.value, officeName)}
|
||
</p>)}
|
||
</div>))}
|
||
|
||
<div className="flex items-center gap-3 pt-1">
|
||
<Button type="submit" disabled={saveMutation.isPending} className="bg-teal-600 hover:bg-teal-700 text-white">
|
||
{saveMutation.isPending ? "Saving..." : "Save Templates"}
|
||
</Button>
|
||
<Button type="button" variant="ghost" className="text-xs text-muted-foreground" onClick={() => {
|
||
setReminderGreeting(DEFAULTS.reminderGreeting);
|
||
setNewPatientGreeting(DEFAULTS.newPatientGreeting);
|
||
setGeneralFallback(DEFAULTS.generalFallback);
|
||
}}>
|
||
Reset to defaults
|
||
</Button>
|
||
</div>
|
||
</form>)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* ── Section 2: LangGraph Flow ────────────────────────────── */}
|
||
<Card>
|
||
<CardContent className="py-6 space-y-5">
|
||
<div className="flex items-center gap-2">
|
||
<GitFork className="h-5 w-5 text-primary"/>
|
||
<h3 className="text-lg font-semibold">LangGraph Settings</h3>
|
||
</div>
|
||
<p className="text-sm text-muted-foreground">
|
||
Visual diagrams of each AI conversation graph — select a flow below to inspect its node sequence.
|
||
</p>
|
||
|
||
{/* Shared legend */}
|
||
<div className="flex flex-wrap gap-3 text-xs">
|
||
{[
|
||
{ bg: "bg-blue-100", border: "border-blue-400", label: "Staff action" },
|
||
{ bg: "bg-gray-100", border: "border-gray-400", label: "Patient action" },
|
||
{ bg: "bg-emerald-50", border: "border-emerald-400", label: "AI node" },
|
||
{ bg: "bg-indigo-50", border: "border-indigo-400", label: "New patient branch" },
|
||
{ bg: "bg-violet-50", border: "border-violet-400", label: "Existing patient" },
|
||
{ bg: "bg-green-50", border: "border-green-400", label: "Confirmed / Schedule" },
|
||
{ bg: "bg-orange-50", border: "border-orange-400", label: "Reschedule / Transfer" },
|
||
].map((l) => (<span key={l.label} className="flex items-center gap-1.5">
|
||
<span className={`inline-block w-3 h-3 rounded-sm ${l.bg} border ${l.border}`}/>
|
||
{l.label}
|
||
</span>))}
|
||
</div>
|
||
|
||
<Tabs defaultValue="reminder">
|
||
<TabsList className="w-full">
|
||
<TabsTrigger value="reminder" className="flex-1 text-xs">
|
||
Reminder & Reschedule Flow
|
||
</TabsTrigger>
|
||
<TabsTrigger value="new_patient" className="flex-1 text-xs">
|
||
New Patient Flow
|
||
</TabsTrigger>
|
||
</TabsList>
|
||
|
||
<TabsContent value="reminder">
|
||
<div className="overflow-x-auto rounded-lg border bg-gray-50/50 p-4 mt-3">
|
||
<LangGraphFlow />
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="new_patient">
|
||
<div className="overflow-x-auto rounded-lg border bg-gray-50/50 p-4 mt-3">
|
||
<NewPatientFlow />
|
||
</div>
|
||
<p className="text-xs text-muted-foreground mt-2 pl-1">
|
||
Any reply not matching a known intent returns: "Our office staff will assist you shortly."
|
||
</p>
|
||
</TabsContent>
|
||
</Tabs>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* ── Section 3: Users AI Chat ─────────────────────────────── */}
|
||
<InternalChatSettingsCard />
|
||
|
||
</div>);
|
||
}
|