fix: correct SMS template defaults and add unsupported-variable warning

- reminderSms default: remove {officeAddress} (never replaced by backend) to prevent
  patients receiving literal '{officeAddress}' in reminder texts
- reminderGreeting default: fix typo 'reply you message' → 'reply to your message'
- rescheduleGreeting default: remove duplicate AI intro (intro is now sent separately
  as MSG 1; fallback text should only contain the intent response)
- Add unsupportedVars() detector: highlights any {variable} in the SMS template that
  the backend does not replace, with an amber warning showing the supported list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-14 12:51:55 -04:00
parent c40ebae979
commit fd8e664e7b

View File

@@ -19,23 +19,35 @@ type OfficeContact = {
officeName?: string | null; officeName?: string | null;
}; };
const SUPPORTED_SMS_VARS = [
"{firstName}", "{officeName}", "{appointmentDate}", "{appointmentTime}", "{date}", "{time}",
] as const;
const DEFAULTS = { const DEFAULTS = {
reminderSms: reminderSms:
"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!", "Hi {firstName}, this is a reminder from {officeName}. You have an appointment on {appointmentDate} at {appointmentTime}. Please reply YES to confirm or NO to reschedule. Thank you!",
reminderGreeting: 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.", "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 to your message at any time you need.",
newPatientGreeting: 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?", "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: generalFallback:
"Hi! My name is Lisa, the dedicated AI assistant at {officeName}. How can I help you today?", "Hi! My name is Lisa, the dedicated AI assistant at {officeName}. How can I help you today?",
rescheduleGreeting: rescheduleGreeting:
"Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you find a new appointment time that works for you. Would you like to reschedule your appointment?", "It is understandable! When would you like to reschedule your appointment?",
}; };
function preview(text: string, officeName: string) { function preview(text: string, officeName: string) {
return text.replace(/\{officeName\}/g, officeName || "your dental office"); return text.replace(/\{officeName\}/g, officeName || "your dental office");
} }
/** Returns any {variable} tokens in `text` that are not in the supported list. */
function unsupportedVars(text: string): string[] {
const found = text.match(/\{[^}]+\}/g) ?? [];
return [...new Set(found)].filter(
(v) => !(SUPPORTED_SMS_VARS as readonly string[]).includes(v)
);
}
export function AiChatTemplatesCard() { export function AiChatTemplatesCard() {
const { toast } = useToast(); const { toast } = useToast();
@@ -115,7 +127,7 @@ export function AiChatTemplatesCard() {
key: "reminderSms" as const, key: "reminderSms" as const,
icon: <CalendarCheck className="h-4 w-4 text-primary" />, icon: <CalendarCheck className="h-4 w-4 text-primary" />,
label: "Reminder SMS Text", label: "Reminder SMS Text",
description: "Outgoing text sent from the Schedule page. Supports: {firstName}, {officeName}, {appointmentDate}, {appointmentTime}.", description: "Outgoing text sent from the Schedule page. Supported variables: {firstName}, {officeName}, {appointmentDate}, {appointmentTime}, {date}, {time}. Any other {variable} will be sent as plain text.",
value: reminderSms, value: reminderSms,
onChange: setReminderSms, onChange: setReminderSms,
placeholder: DEFAULTS.reminderSms, placeholder: DEFAULTS.reminderSms,
@@ -190,28 +202,42 @@ export function AiChatTemplatesCard() {
<p className="text-sm text-muted-foreground">Loading templates...</p> <p className="text-sm text-muted-foreground">Loading templates...</p>
) : ( ) : (
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
{templates_list.map((t) => ( {templates_list.map((t) => {
<div key={t.key} className="space-y-2"> const badVars = t.key === "reminderSms" ? unsupportedVars(t.value) : [];
<div className="flex items-center gap-2"> return (
{t.icon} <div key={t.key} className="space-y-2">
<span className="text-sm font-medium">{t.label}</span> <div className="flex items-center gap-2">
{t.icon}
<span className="text-sm font-medium">{t.label}</span>
</div>
<p className="text-xs text-muted-foreground">{t.description}</p>
<Textarea
value={t.value}
onChange={(e) => t.onChange(e.target.value)}
placeholder={t.placeholder}
rows={3}
className={`text-sm resize-none ${badVars.length ? "border-amber-400 focus-visible:ring-amber-400" : ""}`}
/>
{/* Unsupported variable warning */}
{badVars.length > 0 && (
<div className="flex items-start gap-2 rounded-md bg-amber-50 border border-amber-300 px-3 py-2 text-xs text-amber-800">
<span className="mt-0.5"></span>
<span>
<strong>Unsupported variable{badVars.length > 1 ? "s" : ""}:</strong>{" "}
{badVars.join(", ")} {badVars.length > 1 ? "these" : "this"} will be sent as plain text to patients.
Supported: {SUPPORTED_SMS_VARS.join(", ")}.
</span>
</div>
)}
{/* Live preview */}
{officeName && t.value.includes("{officeName}") && (
<p className="text-xs text-muted-foreground italic pl-1">
Preview: {preview(t.value, officeName)}
</p>
)}
</div> </div>
<p className="text-xs text-muted-foreground">{t.description}</p> );
<Textarea })}
value={t.value}
onChange={(e) => t.onChange(e.target.value)}
placeholder={t.placeholder}
rows={3}
className="text-sm resize-none"
/>
{/* Live preview */}
{officeName && t.value.includes("{officeName}") && (
<p className="text-xs text-muted-foreground italic pl-1">
Preview: {preview(t.value, officeName)}
</p>
)}
</div>
))}
<div className="flex items-center gap-3 pt-2"> <div className="flex items-center gap-3 pt-2">
<Button <Button