feat: add AI-driven phone calls to patient connection

Adds an "AI Call" option alongside Call/SMS/Chat that places an outbound
call where Lisa converses live with the patient via Twilio speech
gather + TTS, using the same conversational AI as chat. Includes a
separate, user-editable Call Template (Settings > AI Chat/Call Settings)
so the call greeting can be customized independently of SMS templates.

Also fixes the outbound-call webhook URL to always use the fixed public
Twilio hostname (CLOUDFLARE_HOST) instead of deriving it from the
triggering request, since staff-browser requests arrive over the
LAN-only hostname, which Twilio can never reach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ff
2026-07-31 20:14:09 -04:00
co-authored by Claude Sonnet 5
parent ed5a8af835
commit 089e94ba88
9 changed files with 364 additions and 6 deletions
@@ -311,7 +311,7 @@ export function Sidebar() {
icon: <Bot className="h-4 w-4 text-gray-400" />,
},
{
name: "AI Chat Settings",
name: "AI Chat/Call Settings",
path: "/settings/aichat",
icon: <Bot className="h-4 w-4 text-gray-400" />,
},
@@ -7,7 +7,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient";
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus, Zap, SlidersHorizontal, BookMarked, ChevronDown, ChevronUp } from "lucide-react";
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus, Zap, SlidersHorizontal, BookMarked, ChevronDown, ChevronUp, PhoneCall } from "lucide-react";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
// ─── Types ────────────────────────────────────────────────────────────────────
@@ -18,6 +18,7 @@ type AiChatTemplates = {
reminderGreeting: string;
newPatientGreeting: string;
generalFallback: string;
callTemplate: string;
};
type OfficeContact = {
@@ -36,6 +37,8 @@ const DEFAULTS = {
"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?",
callTemplate:
"Hi {firstName}, this is Lisa, the AI assistant at {officeName}. How can I help you today?",
};
const DEFAULT_SMS_TEMPLATES = [
@@ -1009,6 +1012,7 @@ export function AiChatSettingsCard() {
const [reminderGreeting, setReminderGreeting] = useState(DEFAULTS.reminderGreeting);
const [newPatientGreeting, setNewPatientGreeting] = useState(DEFAULTS.newPatientGreeting);
const [generalFallback, setGeneralFallback] = useState(DEFAULTS.generalFallback);
const [callTemplate, setCallTemplate] = useState(DEFAULTS.callTemplate);
const initialized = useRef(false);
const [openPhoneReply, setOpenPhoneReply] = useState(false);
@@ -1087,6 +1091,7 @@ export function AiChatSettingsCard() {
setReminderGreeting(templates.reminderGreeting || DEFAULTS.reminderGreeting);
setNewPatientGreeting(templates.newPatientGreeting || DEFAULTS.newPatientGreeting);
setGeneralFallback(templates.generalFallback || DEFAULTS.generalFallback);
setCallTemplate(templates.callTemplate || DEFAULTS.callTemplate);
}
}, [templates]);
@@ -1144,6 +1149,7 @@ export function AiChatSettingsCard() {
reminderGreeting: reminderGreeting.trim() || DEFAULTS.reminderGreeting,
newPatientGreeting: newPatientGreeting.trim() || DEFAULTS.newPatientGreeting,
generalFallback: generalFallback.trim() || DEFAULTS.generalFallback,
callTemplate: callTemplate.trim() || DEFAULTS.callTemplate,
});
};
@@ -1191,6 +1197,15 @@ export function AiChatSettingsCard() {
onChange: setGeneralFallback,
placeholder: DEFAULTS.generalFallback,
},
{
key: "call",
icon: <PhoneCall className="h-4 w-4 text-primary" />,
label: "Call Template",
description: "Lisa's opening line when placing an AI Call to a patient. Separate from the SMS chat templates above — use {firstName} and {officeName} as placeholders.",
value: callTemplate,
onChange: setCallTemplate,
placeholder: DEFAULTS.callTemplate,
},
];
return (
@@ -1393,6 +1408,7 @@ export function AiChatSettingsCard() {
setReminderGreeting(DEFAULTS.reminderGreeting);
setNewPatientGreeting(DEFAULTS.newPatientGreeting);
setGeneralFallback(DEFAULTS.generalFallback);
setCallTemplate(DEFAULTS.callTemplate);
}}
>
Reset to defaults