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
@@ -24,6 +24,7 @@ import {
Send,
X,
MoonStar,
Bot,
} from "lucide-react";
import { SmsTemplateDialog } from "@/components/patient-connection/sms-template-diaog";
import { MessageThread } from "@/components/patient-connection/message-thread";
@@ -103,6 +104,32 @@ export default function PatientConnectionPage() {
},
});
const makeAiCallMutation = useMutation({
mutationFn: async ({
to,
patientId,
}: {
to: string;
patientId: number;
}) => {
return apiRequest("POST", "/api/twilio/make-ai-call", { to, patientId });
},
onSuccess: () => {
toast({
title: "AI Call Started",
description: "Lisa is calling the patient now.",
});
},
onError: (error: any) => {
toast({
title: "AI Call Failed",
description:
error.message || "Unable to place the AI call. Please try again.",
variant: "destructive",
});
},
});
// Fetch all patients from database
const { data: patients = [], isLoading } = useQuery<Patient[]>({
queryKey: ["/api/patients"],
@@ -139,6 +166,22 @@ export default function PatientConnectionPage() {
});
};
// Handle AI-driven call via Twilio
const handleAiCall = (patient: Patient) => {
if (!patient.phone?.trim()) {
toast({
title: "No Phone Number",
description: "This patient does not have a phone number on file.",
variant: "destructive",
});
return;
}
makeAiCallMutation.mutate({
to: patient.phone,
patientId: Number(patient.id),
});
};
// Handle sending SMS
const handleSMS = (patient: Patient) => {
setSelectedPatient(patient);
@@ -343,6 +386,15 @@ export default function PatientConnectionPage() {
<Phone className="h-4 w-4 mr-1" />
Call
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleAiCall(patient)}
data-testid={`button-ai-call-${patient.id}`}
>
<Bot className="h-4 w-4 mr-1" />
AI Call
</Button>
<Button
variant="outline"
size="sm"