@@ -0,0 +1,751 @@
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 { Textarea } from "@/components/ui/textarea" ;
import { useToast } from "@/hooks/use-toast" ;
import { apiRequest , queryClient } from "@/lib/queryClient" ;
import { Bot , CalendarCheck , UserPlus , MessageCircle , Info , GitFork } from "lucide-react" ;
import { Tabs , TabsList , TabsTrigger , TabsContent } from "@/components/ui/tabs" ;
// ─── Types ────────────────────────────────────────────────────────────────────
type AiChatTemplates = {
reminderGreeting : string ;
newPatientGreeting : string ;
generalFallback : string ;
} ;
type OfficeContact = {
officeName? : string | null ;
} ;
// ─── Defaults ─────────────────────────────────────────────────────────────────
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 your 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 : "How can I help you today?" ,
} ;
function previewTemplate ( text : string , officeName : string ) {
return text . replace ( /\{officeName\}/g , officeName || "your dental office" ) ;
}
// ─── LangGraph flow diagram (SVG) ─────────────────────────────────────────────
function LangGraphFlow() {
const W = 620 ;
const cx = W / 2 ; // 310
const nodeW = 200 ;
const nx = cx - nodeW / 2 ; // 210
// sequential node y positions & heights
const n1y = 16 , n1h = 58 ;
const n2y = 116 , n2h = 58 ;
const n3y = 216 , n3h = 78 ;
// fork geometry
const forkHY = n3y + n3h + 22 ; // 316
const lcx = 150 , rcx = 470 ; // branch centers (balanced around 310)
// main branch nodes
const branchY = forkHY + 58 ; // 374
const branchW = 220 ;
const branchH = 88 ;
const branchBottom = branchY + branchH ; // 462
// ── Rescheduling sub-tree (below NO/left branch at lcx=150) ──────────────
const rsForkHY = branchBottom + 28 ; // 490 patient says YES/NO to reschedule
const rsYesCx = 90 ;
const rsNoCx = 215 ;
const rsNodeY = rsForkHY + 32 ; // 522
const rsNodeH = 54 ;
// ASAP-or-next-week fork (below rsYesCx=90)
const prefForkHY = rsNodeY + rsNodeH + 22 ; // 598
const asakCx = 48 ;
const nwkCx = 138 ;
const prefNodeY = prefForkHY + 32 ; // 630
const prefNodeH = 62 ;
const totalH = prefNodeY + prefNodeH + 54 + 20 + 54 + 22 ; // time node + DB node + padding
return (
< svg
viewBox = { ` 0 0 ${ W } ${ totalH } ` }
className = "w-full max-w-xl mx-auto"
role = "img"
aria-label = "LangGraph conversation flow diagram"
>
< defs >
{ /* Arrowhead marker */ }
< marker
id = "ah"
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 >
{ /* ── Node 1: Office sends reminder ─────────────────────────── */ }
< rect x = { nx } y = { n1y } width = { nodeW } height = { n1h } rx = { 8 }
fill = "#EFF6FF" stroke = "#3B82F6" strokeWidth = { 1.5 } / >
< text x = { cx } y = { n1y + 24 } textAnchor = "middle" fontSize = { 13 }
fontWeight = "600" fill = "#1D4ED8" > Office sends reminder < / text >
< text x = { cx } y = { n1y + 42 } textAnchor = "middle" fontSize = { 10 }
fill = "#93C5FD" > Staff triggers the SMS < / text >
{ /* Arrow 1 → 2 */ }
< line x1 = { cx } y1 = { n1y + n1h } x2 = { cx } y2 = { n2y - 2 }
stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
{ /* ── Node 2: Patient replies ───────────────────────────────── */ }
< rect x = { nx } y = { n2y } width = { nodeW } height = { n2h } rx = { 8 }
fill = "#F9FAFB" stroke = "#D1D5DB" strokeWidth = { 1.5 } / >
< text x = { cx } y = { n2y + 24 } textAnchor = "middle" fontSize = { 13 }
fontWeight = "600" fill = "#374151" > Patient replies < / text >
< text x = { cx } y = { n2y + 42 } textAnchor = "middle" fontSize = { 10 }
fill = "#9CA3AF" > Any message triggers the AI < / text >
{ /* Arrow 2 → 3 */ }
< line x1 = { cx } y1 = { n2y + n2h } x2 = { cx } y2 = { n3y - 2 }
stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
{ /* ── Node 3: AI introduces itself ──────────────────────────── */ }
< rect x = { nx } y = { n3y } width = { nodeW } height = { n3h } rx = { 8 }
fill = "#ECFDF5" stroke = "#10B981" strokeWidth = { 1.5 } / >
< text x = { cx } y = { n3y + 24 } textAnchor = "middle" fontSize = { 13 }
fontWeight = "600" fill = "#065F46" > AI introduces itself < / text >
< text x = { cx } y = { n3y + 43 } textAnchor = "middle" fontSize = { 9 }
fill = "#6B7280" fontStyle = "italic" > "Hi! My name is Lisa, the dedicated</text>
<text x={cx} y={n3y + 57} textAnchor=" middle " fontSize={9}
fill=" # 6 B7280 " fontStyle=" italic ">AI assistant at {" { officeName } "}..." < / text >
{ /* ── Fork connectors ───────────────────────────────────────── */ }
{ /* Vertical down from N3 */ }
< line x1 = { cx } y1 = { n3y + n3h } x2 = { cx } y2 = { forkHY }
stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
{ /* Horizontal fork line */ }
< line x1 = { lcx } y1 = { forkHY } x2 = { rcx } y2 = { forkHY }
stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
{ /* Left drop → NO branch */ }
< line x1 = { lcx } y1 = { forkHY } x2 = { lcx } y2 = { branchY - 2 }
stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
{ /* Right drop → YES branch */ }
< line x1 = { rcx } y1 = { forkHY } x2 = { rcx } y2 = { branchY - 2 }
stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
{ /* ── YES / NO badges on the fork ───────────────────────────── */ }
{ /* NO badge — left junction */ }
< rect x = { lcx - 28 } y = { forkHY - 12 } width = { 56 } height = { 24 } rx = { 12 }
fill = "#FFF7ED" stroke = "#F97316" strokeWidth = { 1.5 } / >
< text x = { lcx } y = { forkHY + 5 } textAnchor = "middle" fontSize = { 11 }
fontWeight = "700" fill = "#EA580C" > NO < / text >
{ /* YES badge — right junction */ }
< rect x = { rcx - 24 } y = { forkHY - 12 } width = { 48 } height = { 24 } rx = { 12 }
fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { rcx } y = { forkHY + 5 } textAnchor = "middle" fontSize = { 11 }
fontWeight = "700" fill = "#15803D" > YES < / text >
{ /* ── Left branch: NO → "Would you like to reschedule?" ───────── */ }
< rect x = { lcx - branchW / 2 } y = { branchY } width = { branchW } height = { branchH }
rx = { 8 } fill = "#FFF7ED" stroke = "#F97316" strokeWidth = { 1.5 } / >
< text x = { lcx } y = { branchY + 22 } textAnchor = "middle" fontSize = { 12 } fontWeight = "600" fill = "#C2410C" > It is understandable ! < / text >
< text x = { lcx } y = { branchY + 40 } textAnchor = "middle" fontSize = { 12 } fontWeight = "600" fill = "#C2410C" > Would you like to < / text >
< text x = { lcx } y = { branchY + 58 } textAnchor = "middle" fontSize = { 12 } fontWeight = "600" fill = "#C2410C" > reschedule ? < / text >
{ /* ── Right branch: YES → Confirm ───────────────────────────── */ }
< rect x = { rcx - branchW / 2 } y = { branchY } width = { branchW } height = { branchH }
rx = { 8 } fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { rcx } y = { branchY + 22 } textAnchor = "middle" fontSize = { 12 } fontWeight = "600" fill = "#15803D" > Thank you for < / text >
< text x = { rcx } y = { branchY + 40 } textAnchor = "middle" fontSize = { 12 } fontWeight = "600" fill = "#15803D" > confirming ! < / text >
< text x = { rcx } y = { branchY + 60 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > "See you on [date & time]" < / text >
{ /* ══════════ RESCHEDULING SUB-TREE ══════════════════════════════ */ }
{ /* Vertical from NO node bottom */ }
< line x1 = { lcx } y1 = { branchBottom } x2 = { lcx } y2 = { rsForkHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
{ /* Horizontal YES/NO fork */ }
< line x1 = { rsYesCx } y1 = { rsForkHY } x2 = { rsNoCx } y2 = { rsForkHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { rsYesCx } y1 = { rsForkHY } x2 = { rsYesCx } y2 = { rsNodeY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
< line x1 = { rsNoCx } y1 = { rsForkHY } x2 = { rsNoCx } y2 = { rsNodeY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
{ /* YES / NO badges */ }
< rect x = { rsYesCx - 20 } y = { rsForkHY - 11 } width = { 40 } height = { 22 } rx = { 11 } fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { rsYesCx } y = { rsForkHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#15803D" > YES < / text >
< rect x = { rsNoCx - 18 } y = { rsForkHY - 11 } width = { 36 } height = { 22 } rx = { 11 } fill = "#F9FAFB" stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< text x = { rsNoCx } y = { rsForkHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#6B7280" > NO < / text >
{ /* YES → "ASAP or next week?" node */ }
< rect x = { rsYesCx - 52 } y = { rsNodeY } width = { 104 } height = { rsNodeH } rx = { 8 } fill = "#EFF6FF" stroke = "#3B82F6" strokeWidth = { 1.5 } / >
< text x = { rsYesCx } y = { rsNodeY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#1D4ED8" > ASAP or < / text >
< text x = { rsYesCx } y = { rsNodeY + 32 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#1D4ED8" > next week ? < / text >
< text x = { rsYesCx } y = { rsNodeY + 46 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > ( if Mon – Thu appt ) < / text >
{ /* NO → polite close */ }
< rect x = { rsNoCx - 46 } y = { rsNodeY } width = { 92 } height = { rsNodeH } rx = { 8 } fill = "#F9FAFB" stroke = "#D1D5DB" strokeWidth = { 1.5 } / >
< text x = { rsNoCx } y = { rsNodeY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#374151" > No problem ! < / text >
< text x = { rsNoCx } y = { rsNodeY + 36 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > Conversation ends < / text >
{ /* Vertical from "ASAP or next week?" down to preference fork */ }
< line x1 = { rsYesCx } y1 = { rsNodeY + rsNodeH } x2 = { rsYesCx } y2 = { prefForkHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
{ /* ASAP / Next week fork */ }
< line x1 = { asakCx } y1 = { prefForkHY } x2 = { nwkCx } y2 = { prefForkHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { asakCx } y1 = { prefForkHY } x2 = { asakCx } y2 = { prefNodeY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
< line x1 = { nwkCx } y1 = { prefForkHY } x2 = { nwkCx } y2 = { prefNodeY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
{ /* ASAP badge */ }
< rect x = { asakCx - 22 } y = { prefForkHY - 11 } width = { 44 } height = { 22 } rx = { 11 } fill = "#EFF6FF" stroke = "#3B82F6" strokeWidth = { 1.5 } / >
< text x = { asakCx } y = { prefForkHY + 4 } textAnchor = "middle" fontSize = { 9 } fontWeight = "700" fill = "#1D4ED8" > ASAP < / text >
{ /* Next week badge */ }
< rect x = { nwkCx - 30 } y = { prefForkHY - 11 } width = { 60 } height = { 22 } rx = { 11 } fill = "#F5F3FF" stroke = "#8B5CF6" strokeWidth = { 1.5 } / >
< text x = { nwkCx } y = { prefForkHY + 4 } textAnchor = "middle" fontSize = { 9 } fontWeight = "700" fill = "#6D28D9" > Next week < / text >
{ /* ASAP → "Can you come tomorrow?" */ }
< rect x = { asakCx - 44 } y = { prefNodeY } width = { 88 } height = { prefNodeH } rx = { 8 } fill = "#EFF6FF" stroke = "#3B82F6" strokeWidth = { 1.5 } / >
< text x = { asakCx } y = { prefNodeY + 16 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#1D4ED8" > Can you come < / text >
< text x = { asakCx } y = { prefNodeY + 30 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#1D4ED8" > tomorrow ? < / text >
< text x = { asakCx } y = { prefNodeY + 48 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > YES → ask time < / text >
{ /* Next week → "Mon, Tue, or Wed?" */ }
< rect x = { nwkCx - 46 } y = { prefNodeY } width = { 92 } height = { prefNodeH } rx = { 8 } fill = "#F5F3FF" stroke = "#8B5CF6" strokeWidth = { 1.5 } / >
< text x = { nwkCx } y = { prefNodeY + 16 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#5B21B6" > Mon , Tue , < / text >
< text x = { nwkCx } y = { prefNodeY + 30 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#5B21B6" > or Wed ? < / text >
< text x = { nwkCx } y = { prefNodeY + 48 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > → ask time < / text >
{ /* ── Time-slot node (shared by both paths) ─────────────────── */ }
{ /* Left vertical from ASAP node */ }
< line x1 = { asakCx } y1 = { prefNodeY + prefNodeH } x2 = { asakCx } y2 = { prefNodeY + prefNodeH + 18 }
stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
{ /* Right vertical from NextWeek node */ }
< line x1 = { nwkCx } y1 = { prefNodeY + prefNodeH } x2 = { nwkCx } y2 = { prefNodeY + prefNodeH + 18 }
stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
{ /* Horizontal converge line */ }
< line x1 = { asakCx } y1 = { prefNodeY + prefNodeH + 18 } x2 = { nwkCx } y2 = { prefNodeY + prefNodeH + 18 }
stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
{ /* Drop to time node */ }
< line x1 = { rsYesCx } y1 = { prefNodeY + prefNodeH + 18 } x2 = { rsYesCx } y2 = { prefNodeY + prefNodeH + 36 }
stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
{ /* Time-slot node */ }
< rect x = { rsYesCx - 52 } y = { prefNodeY + prefNodeH + 40 } width = { 104 } height = { 54 } rx = { 8 }
fill = "#ECFDF5" stroke = "#10B981" strokeWidth = { 1.5 } / >
< text x = { rsYesCx } y = { prefNodeY + prefNodeH + 58 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#065F46" > Morning or < / text >
< text x = { rsYesCx } y = { prefNodeY + prefNodeH + 72 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#065F46" > afternoon ? < / text >
{ /* DB update node (dashed) */ }
< line x1 = { rsYesCx } y1 = { prefNodeY + prefNodeH + 94 } x2 = { rsYesCx } y2 = { prefNodeY + prefNodeH + 112 }
stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#ah)" / >
< rect x = { rsYesCx - 52 } y = { prefNodeY + prefNodeH + 116 } width = { 104 } height = { 54 } rx = { 8 }
fill = "#F0F9FF" stroke = "#0EA5E9" strokeWidth = { 1.5 } strokeDasharray = "5 3" / >
< rect x = { rsYesCx - 38 } y = { prefNodeY + prefNodeH + 108 } width = { 76 } height = { 20 } rx = { 10 }
fill = "#E0F2FE" stroke = "#0EA5E9" strokeWidth = { 1.5 } / >
< text x = { rsYesCx } y = { prefNodeY + prefNodeH + 122 } textAnchor = "middle" fontSize = { 9 } fontWeight = "700" fill = "#0369A1" > DB Update < / text >
< text x = { rsYesCx } y = { prefNodeY + prefNodeH + 138 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#0369A1" > Appt . moved ! < / text >
< text x = { rsYesCx } y = { prefNodeY + prefNodeH + 154 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > "See you on [day] at [time]" < / text >
< / svg >
) ;
}
// ─── New Patient / After-Hours flow diagram ───────────────────────────────────
function NewPatientFlow() {
// ── Canvas ───────────────────────────────────────────────────────────────
const W = 760 ;
const cx = 380 ; // top-node horizontal center
const nW = 220 ;
const nx = cx - nW / 2 ; // 270
// ── Sequential top nodes ─────────────────────────────────────────────────
const n1y = 15 , n1h = 58 ;
const n2y = 115 , n2h = 58 ;
const n3y = 215 , n3h = 58 ;
const n4y = 315 , n4h = 60 ;
// ── Main fork ────────────────────────────────────────────────────────────
const forkHY = n4y + n4h + 20 ; // 395
const lcx = 165 ; // New Patient branch center
const rcx = 590 ; // Existing Patient branch center
const brW = 200 ;
// ── Main branch nodes ─────────────────────────────────────────────────────
const brY = forkHY + 50 ; // 445
const brH = 62 ;
// ── Sub-forks ─────────────────────────────────────────────────────────────
const sfHY = brY + brH + 18 ; // 525
const llcx = 100 ; const lrcx = 230 ; // New Patient sub-branches (centered on 165)
const rlcx = 510 ; const rrcx = 660 ; // Existing Patient sub-branches
// ── Leaf nodes ────────────────────────────────────────────────────────────
const leafY = sfHY + 46 ; // 571
const leafW = 118 ;
const leafH = 80 ;
// ── New patient: Selenium chain (below LL at cx=100) ─────────────────────
const npSelY = leafY + leafH + 34 ; // 685 — Selenium node top
const npSelW = 140 ;
const npSelH = 56 ;
const npResHY = npSelY + npSelH + 16 ; // 757 — result fork y
const npActCx = 52 ; // ACTIVE result center (100-48)
const npInaCx = 148 ; // INACTIVE result center (100+48)
const npResY = npResHY + 34 ; // 791
const npResW = 80 ;
const npResH = 72 ;
// ── Existing patient: YES → MassHealth auto-check (below RL at cx=510) ──
const exForkHY = leafY + leafH + 26 ; // 677
const exMhCx = 445 ; // MassHealth sub-branch center
const exOtherCx = 558 ; // Other insurance center
const exCheckY = exForkHY + 34 ; // 711
const exCheckW = 116 ;
const exCheckH = 52 ;
const exSelY = exCheckY + exCheckH + 18 ; // 781
const exSelW = 128 ;
const exSelH = 52 ;
const exResHY = exSelY + exSelH + 16 ; // 849
const exActCx = 401 ; // (445-44)
const exInaCx = 489 ; // (445+44)
const exLeafY = exResHY + 34 ; // 883
const exLeafW = 80 ;
const exLeafH = 62 ;
const totalH = Math . max ( npResY + npResH , exLeafY + exLeafH ) + 22 ;
return (
< svg
viewBox = { ` 0 0 ${ W } ${ totalH } ` }
className = "w-full max-w-2xl mx-auto"
role = "img"
aria-label = "New patient conversation flow diagram"
>
< defs >
< marker id = "nph" 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 >
{ /* ═══════════ TOP SEQUENCE ═══════════════════════════════════ */ }
< rect x = { nx } y = { n1y } width = { nW } height = { n1h } rx = { 8 } fill = "#EFF6FF" stroke = "#3B82F6" strokeWidth = { 1.5 } / >
< text x = { cx } y = { n1y + 22 } textAnchor = "middle" fontSize = { 12 } fontWeight = "600" fill = "#1D4ED8" > Patient texts after hours < / text >
< text x = { cx } y = { n1y + 38 } textAnchor = "middle" fontSize = { 10 } fill = "#93C5FD" > or staff selects < / text >
< text x = { cx } y = { n1y + 50 } textAnchor = "middle" fontSize = { 10 } fill = "#93C5FD" > "Schedule a New Patient" < / text >
< line x1 = { cx } y1 = { n1y + n1h } x2 = { cx } y2 = { n2y - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { nx } y = { n2y } width = { nW } height = { n2h } rx = { 8 } fill = "#ECFDF5" stroke = "#10B981" strokeWidth = { 1.5 } / >
< text x = { cx } y = { n2y + 22 } textAnchor = "middle" fontSize = { 12 } fontWeight = "600" fill = "#065F46" > AI sends New Patient Greeting < / text >
< text x = { cx } y = { n2y + 40 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > "Hi! My name is Lisa..." < / text >
< line x1 = { cx } y1 = { n2y + n2h } x2 = { cx } y2 = { n3y - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { nx } y = { n3y } width = { nW } height = { n3h } rx = { 8 } fill = "#F9FAFB" stroke = "#D1D5DB" strokeWidth = { 1.5 } / >
< text x = { cx } y = { n3y + 22 } textAnchor = "middle" fontSize = { 12 } fontWeight = "600" fill = "#374151" > Patient replies < / text >
< text x = { cx } y = { n3y + 40 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > "I want an appointment / cleaning" < / text >
< line x1 = { cx } y1 = { n3y + n3h } x2 = { cx } y2 = { n4y - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { nx } y = { n4y } width = { nW } height = { n4h } rx = { 8 } fill = "#ECFDF5" stroke = "#10B981" strokeWidth = { 1.5 } / >
< text x = { cx } y = { n4y + 24 } textAnchor = "middle" fontSize = { 12 } fontWeight = "600" fill = "#065F46" > AI : New or existing patient ? < / text >
< text x = { cx } y = { n4y + 42 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > "Are you a new or existing patient?" < / text >
{ /* ═══════════ MAIN FORK ══════════════════════════════════════ */ }
< line x1 = { cx } y1 = { n4y + n4h } 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 = { brY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< line x1 = { rcx } y1 = { forkHY } x2 = { rcx } y2 = { brY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { lcx - 42 } y = { forkHY - 13 } width = { 84 } 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 - 50 } y = { forkHY - 13 } width = { 100 } 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 >
{ /* ═══════════ LEFT BRANCH — NEW PATIENT ══════════════════════ */ }
< rect x = { lcx - brW / 2 } y = { brY } width = { brW } height = { brH } rx = { 8 } fill = "#EEF2FF" stroke = "#6366F1" strokeWidth = { 1.5 } / >
< text x = { lcx } y = { brY + 22 } textAnchor = "middle" fontSize = { 11 } fontWeight = "600" fill = "#3730A3" > Do you have any < / text >
< text x = { lcx } y = { brY + 38 } textAnchor = "middle" fontSize = { 11 } fontWeight = "600" fill = "#3730A3" > dental insurance ? < / text >
< line x1 = { lcx } y1 = { brY + brH } x2 = { lcx } y2 = { sfHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { llcx } y1 = { sfHY } x2 = { lrcx } y2 = { sfHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { llcx } y1 = { sfHY } x2 = { llcx } y2 = { leafY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< line x1 = { lrcx } y1 = { sfHY } x2 = { lrcx } y2 = { leafY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { llcx - 36 } y = { sfHY - 11 } width = { 72 } height = { 22 } rx = { 11 } fill = "#ECFDF5" stroke = "#10B981" strokeWidth = { 1.5 } / >
< text x = { llcx } y = { sfHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#065F46" > MassHealth < / text >
< rect x = { lrcx - 32 } y = { sfHY - 11 } width = { 64 } height = { 22 } rx = { 11 } fill = "#FFF7ED" stroke = "#F97316" strokeWidth = { 1.5 } / >
< text x = { lrcx } y = { sfHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#C2410C" > No ins . < / text >
{ /* NP: MassHealth leaf — ask for ID+DOB */ }
< rect x = { llcx - leafW / 2 } y = { leafY } width = { leafW } height = { leafH } rx = { 8 } fill = "#ECFDF5" stroke = "#10B981" strokeWidth = { 1.5 } / >
< text x = { llcx } y = { leafY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#065F46" > Check MassHealth < / text >
< text x = { llcx } y = { leafY + 34 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > "Text me your</text>
<text x={llcx} y={leafY+48} textAnchor=" middle " fontSize={9} fill=" # 9 CA3AF " fontStyle=" italic ">Member ID & DOB" < / text >
{ /* NP: No insurance leaf */ }
< rect x = { lrcx - leafW / 2 } y = { leafY } width = { leafW } height = { leafH } rx = { 8 } fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { lrcx } y = { leafY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#15803D" > Schedule < / text >
< text x = { lrcx } y = { leafY + 34 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > "When would</text>
<text x={lrcx} y={leafY+48} textAnchor=" middle " fontSize={9} fill=" # 9 CA3AF " fontStyle=" italic ">you like to come?" < / text >
{ /* NP: Selenium check (patient texts ID+DOB) */ }
< line x1 = { llcx } y1 = { leafY + leafH } x2 = { llcx } y2 = { npSelY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { llcx - npSelW / 2 } y = { npSelY } width = { npSelW } height = { npSelH } rx = { 8 }
fill = "#F0F9FF" stroke = "#0EA5E9" strokeWidth = { 1.5 } strokeDasharray = "5 3" / >
< rect x = { llcx - 40 } y = { npSelY - 11 } width = { 80 } height = { 22 } rx = { 11 } fill = "#E0F2FE" stroke = "#0EA5E9" strokeWidth = { 1.5 } / >
< text x = { llcx } y = { npSelY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#0369A1" > Selenium < / text >
< text x = { llcx } y = { npSelY + 20 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#0369A1" > MassHealth Portal < / text >
< text x = { llcx } y = { npSelY + 36 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > Auto - checks eligibility < / text >
< text x = { llcx } y = { npSelY + 48 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > → result SMS to patient < / text >
{ /* NP: Post-Selenium ACTIVE/INACTIVE fork */ }
< line x1 = { llcx } y1 = { npSelY + npSelH } x2 = { llcx } 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(#nph)" / >
< line x1 = { npInaCx } y1 = { npResHY } x2 = { npInaCx } y2 = { npResY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< 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 >
< rect x = { npActCx - npResW / 2 } y = { npResY } width = { npResW } height = { npResH } rx = { 8 } fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { npActCx } y = { npResY + 16 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#15803D" > Check - up or < / text >
< text x = { npActCx } y = { npResY + 30 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#15803D" > tooth problem ? < / text >
< text x = { npActCx } y = { npResY + 48 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > → book appt < / text >
< rect x = { npInaCx - npResW / 2 } y = { npResY } width = { npResW } height = { npResH } rx = { 8 } fill = "#FFF7ED" stroke = "#F97316" strokeWidth = { 1.5 } / >
< text x = { npInaCx } y = { npResY + 16 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#C2410C" > Inactive . < / text >
< text x = { npInaCx } y = { npResY + 30 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#C2410C" > Self - pay ? < / text >
< text x = { npInaCx } y = { npResY + 50 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > YES / NO < / text >
{ /* ═══════════ RIGHT BRANCH — EXISTING PATIENT ════════════════ */ }
< rect x = { rcx - brW / 2 } y = { brY } width = { brW } height = { brH } rx = { 8 } fill = "#F5F3FF" stroke = "#8B5CF6" strokeWidth = { 1.5 } / >
< text x = { rcx } y = { brY + 22 } textAnchor = "middle" fontSize = { 11 } fontWeight = "600" fill = "#5B21B6" > Do you still have < / text >
< text x = { rcx } y = { brY + 38 } textAnchor = "middle" fontSize = { 11 } fontWeight = "600" fill = "#5B21B6" > the same insurance ? < / text >
< line x1 = { rcx } y1 = { brY + brH } x2 = { rcx } y2 = { sfHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { rlcx } y1 = { sfHY } x2 = { rrcx } y2 = { sfHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { rlcx } y1 = { sfHY } x2 = { rlcx } y2 = { leafY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< line x1 = { rrcx } y1 = { sfHY } x2 = { rrcx } y2 = { leafY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { rlcx - 20 } y = { sfHY - 11 } width = { 40 } height = { 22 } rx = { 11 } fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { rlcx } y = { sfHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#15803D" > YES < / text >
< rect x = { rrcx - 20 } y = { sfHY - 11 } width = { 40 } height = { 22 } rx = { 11 } fill = "#FFF7ED" stroke = "#F97316" strokeWidth = { 1.5 } / >
< text x = { rrcx } y = { sfHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#C2410C" > NO < / text >
{ /* EP: YES — "Same insurance confirmed" */ }
< rect x = { rlcx - leafW / 2 } y = { leafY } width = { leafW } height = { leafH } rx = { 8 } fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { rlcx } y = { leafY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#15803D" > Same insurance < / text >
< text x = { rlcx } y = { leafY + 34 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > MassHealth → auto - check < / text >
< text x = { rlcx } y = { leafY + 48 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > Other → schedule < / text >
{ /* EP: NO — Transfer */ }
< rect x = { rrcx - leafW / 2 } y = { leafY } width = { leafW } height = { leafH } rx = { 8 } fill = "#FFF7ED" stroke = "#F97316" strokeWidth = { 1.5 } / >
< text x = { rrcx } y = { leafY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#C2410C" > Transfer < / text >
< text x = { rrcx } y = { leafY + 34 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > "Our staff will</text>
<text x={rrcx} y={leafY+48} textAnchor=" middle " fontSize={9} fill=" # 9 CA3AF " fontStyle=" italic ">assist you" < / text >
{ /* EP: YES → MassHealth sub-fork */ }
< line x1 = { rlcx } y1 = { leafY + leafH } x2 = { rlcx } y2 = { exForkHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { exMhCx } y1 = { exForkHY } x2 = { exOtherCx } y2 = { exForkHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { exMhCx } y1 = { exForkHY } x2 = { exMhCx } y2 = { exCheckY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< line x1 = { exOtherCx } y1 = { exForkHY } x2 = { exOtherCx } y2 = { exCheckY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { exMhCx - 36 } y = { exForkHY - 11 } width = { 72 } height = { 22 } rx = { 11 } fill = "#ECFDF5" stroke = "#10B981" strokeWidth = { 1.5 } / >
< text x = { exMhCx } y = { exForkHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#065F46" > MassHealth < / text >
< rect x = { exOtherCx - 32 } y = { exForkHY - 11 } width = { 64 } height = { 22 } rx = { 11 } fill = "#E0F2FE" stroke = "#0EA5E9" strokeWidth = { 1.5 } / >
< text x = { exOtherCx } y = { exForkHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#0369A1" > Other ins . < / text >
{ /* EP: MassHealth — "Checking 30-60s" node */ }
< rect x = { exMhCx - exCheckW / 2 } y = { exCheckY } width = { exCheckW } height = { exCheckH } rx = { 8 } fill = "#ECFDF5" stroke = "#10B981" strokeWidth = { 1.5 } / >
< text x = { exMhCx } y = { exCheckY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#065F46" > Wait 30 - 60 secs … < / text >
< text x = { exMhCx } y = { exCheckY + 34 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > Using saved ID & amp ; DOB < / text >
{ /* EP: Other insurance — schedule directly */ }
< rect x = { exOtherCx - exCheckW / 2 } y = { exCheckY } width = { exCheckW } height = { exCheckH } rx = { 8 } fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { exOtherCx } y = { exCheckY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#15803D" > Schedule < / text >
< text x = { exOtherCx } y = { exCheckY + 34 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > "When to come in?" < / text >
{ /* EP: MassHealth → Selenium */ }
< line x1 = { exMhCx } y1 = { exCheckY + exCheckH } x2 = { exMhCx } y2 = { exSelY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { exMhCx - exSelW / 2 } y = { exSelY } width = { exSelW } height = { exSelH } rx = { 8 }
fill = "#F0F9FF" stroke = "#0EA5E9" strokeWidth = { 1.5 } strokeDasharray = "5 3" / >
< rect x = { exMhCx - 38 } y = { exSelY - 11 } width = { 76 } height = { 22 } rx = { 11 } fill = "#E0F2FE" stroke = "#0EA5E9" strokeWidth = { 1.5 } / >
< text x = { exMhCx } y = { exSelY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#0369A1" > Selenium < / text >
< text x = { exMhCx } y = { exSelY + 20 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#0369A1" > MassHealth Portal < / text >
< text x = { exMhCx } y = { exSelY + 36 } textAnchor = "middle" fontSize = { 9 } fill = "#6B7280" fontStyle = "italic" > Auto - checks eligibility < / text >
{ /* EP: ACTIVE / INACTIVE fork */ }
< line x1 = { exMhCx } y1 = { exSelY + exSelH } x2 = { exMhCx } y2 = { exResHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { exActCx } y1 = { exResHY } x2 = { exInaCx } y2 = { exResHY } stroke = "#9CA3AF" strokeWidth = { 1.5 } / >
< line x1 = { exActCx } y1 = { exResHY } x2 = { exActCx } y2 = { exLeafY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< line x1 = { exInaCx } y1 = { exResHY } x2 = { exInaCx } y2 = { exLeafY - 2 } stroke = "#9CA3AF" strokeWidth = { 1.5 } markerEnd = "url(#nph)" / >
< rect x = { exActCx - 28 } y = { exResHY - 11 } width = { 56 } height = { 22 } rx = { 11 } fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { exActCx } y = { exResHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#15803D" > ACTIVE < / text >
< rect x = { exInaCx - 34 } y = { exResHY - 11 } width = { 68 } height = { 22 } rx = { 11 } fill = "#FFF7ED" stroke = "#F97316" strokeWidth = { 1.5 } / >
< text x = { exInaCx } y = { exResHY + 4 } textAnchor = "middle" fontSize = { 10 } fontWeight = "700" fill = "#C2410C" > INACTIVE < / text >
< rect x = { exActCx - exLeafW / 2 } y = { exLeafY } width = { exLeafW } height = { exLeafH } rx = { 8 } fill = "#F0FDF4" stroke = "#22C55E" strokeWidth = { 1.5 } / >
< text x = { exActCx } y = { exLeafY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#15803D" > When to < / text >
< text x = { exActCx } y = { exLeafY + 32 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#15803D" > come in ? < / text >
< text x = { exActCx } y = { exLeafY + 50 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > → book appt < / text >
< rect x = { exInaCx - exLeafW / 2 } y = { exLeafY } width = { exLeafW } height = { exLeafH } rx = { 8 } fill = "#FFF7ED" stroke = "#F97316" strokeWidth = { 1.5 } / >
< text x = { exInaCx } y = { exLeafY + 18 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#C2410C" > Inactive . < / text >
< text x = { exInaCx } y = { exLeafY + 32 } textAnchor = "middle" fontSize = { 10 } fontWeight = "600" fill = "#C2410C" > Self - pay ? < / text >
< text x = { exInaCx } y = { exLeafY + 50 } textAnchor = "middle" fontSize = { 9 } fill = "#9CA3AF" fontStyle = "italic" > YES / NO < / text >
< / svg >
) ;
}
// ─── 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 { data : officeContact } = useQuery < OfficeContact | null > ( {
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 < AiChatTemplates > ( {
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 ,
} ) ;
// Seed local state from server on first load only
useEffect ( ( ) = > {
if ( templates && ! initialized . current ) {
initialized . current = true ;
setReminderGreeting ( templates . reminderGreeting || DEFAULTS . reminderGreeting ) ;
setNewPatientGreeting ( templates . newPatientGreeting || DEFAULTS . newPatientGreeting ) ;
setGeneralFallback ( templates . generalFallback || DEFAULTS . generalFallback ) ;
}
} , [ templates ] ) ;
const saveMutation = useMutation ( {
mutationFn : async ( data : AiChatTemplates ) = > {
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 : any ) = > {
toast ( { title : "Error" , description : err?.message , variant : "destructive" } ) ;
} ,
} ) ;
const handleSubmit = ( e : React.FormEvent ) = > {
e . preventDefault ( ) ;
saveMutation . mutate ( {
reminderGreeting : reminderGreeting.trim ( ) || DEFAULTS . reminderGreeting ,
newPatientGreeting : newPatientGreeting.trim ( ) || DEFAULTS . newPatientGreeting ,
generalFallback : generalFallback.trim ( ) || DEFAULTS . generalFallback ,
} ) ;
} ;
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" >
{ /* ── 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" >
Appointment Reminder Flow
< / TabsTrigger >
< TabsTrigger value = "new_patient" className = "flex-1 text-xs" >
New Patient / After - Hours 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 >
< / div >
) ;
}