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;
};
const SUPPORTED_SMS_VARS = [
"{firstName}", "{officeName}", "{appointmentDate}", "{appointmentTime}", "{date}", "{time}",
] as const;
const DEFAULTS = {
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:
"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:
"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?",
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) {
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() {
const { toast } = useToast();
@@ -115,7 +127,7 @@ export function AiChatTemplatesCard() {
key: "reminderSms" as const,
icon: <CalendarCheck className="h-4 w-4 text-primary" />,
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,
onChange: setReminderSms,
placeholder: DEFAULTS.reminderSms,
@@ -190,28 +202,42 @@ export function AiChatTemplatesCard() {
<p className="text-sm text-muted-foreground">Loading templates...</p>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
{templates_list.map((t) => (
<div key={t.key} className="space-y-2">
<div className="flex items-center gap-2">
{t.icon}
<span className="text-sm font-medium">{t.label}</span>
{templates_list.map((t) => {
const badVars = t.key === "reminderSms" ? unsupportedVars(t.value) : [];
return (
<div key={t.key} className="space-y-2">
<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>
<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">
<Button