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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user