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 } from "lucide-react"; type AiChatTemplates = { reminderGreeting: string; newPatientGreeting: string; generalFallback: string; }; type OfficeContact = { officeName?: string | null; }; 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. How can I help you today?", 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 preview(text: string, officeName: string) { return text.replace(/\{officeName\}/g, officeName || "your dental office"); } export function AiChatTemplatesCard() { 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({ 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, // never silently refetch and overwrite user edits refetchOnWindowFocus: false, }); // Seed state from server on first successful 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 templates_list = [ { key: "reminder" as const, icon: , label: "Appointment Reminder Reply", description: "Sent when the AI introduces itself after the office sends an appointment reminder.", value: reminderGreeting, onChange: setReminderGreeting, placeholder: DEFAULTS.reminderGreeting, }, { key: "newPatient" as const, icon: , 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" as const, icon: , 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 ( {/* Header */}

AI Chat Templates

Customize how your AI assistant introduces itself and responds to patients. Use{" "} {"{officeName}"}{" "} as a placeholder — it will be replaced automatically with your dental office name.

{/* Office name hint */} {officeName && (
{"{officeName}"} will display as{" "} "{officeName}"
)} {isLoading ? (

Loading templates...

) : (
{templates_list.map((t) => (
{t.icon} {t.label}

{t.description}