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