import { zodResolver } from "@hookform/resolvers/zod"; import { isValidPhoneNumber } from "libphonenumber-js"; import { useState, useRef, useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; import type { UseFormReturn } from "react-hook-form"; import { z } from "zod"; import { Dialog } from "@calcom/features/components/controlled-dialog"; import type { Language } from "@calcom/features/calAIPhone/providers/retellAI/types"; import { CAL_AI_PHONE_NUMBER_MONTHLY_PRICE } from "@calcom/lib/constants"; import { formatPhoneNumber } from "@calcom/lib/formatPhoneNumber"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { PhoneNumberSubscriptionStatus } from "@calcom/prisma/enums"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import { Alert } from "@calcom/ui/components/alert"; import { Badge } from "@calcom/ui/components/badge"; import { Button } from "@calcom/ui/components/button"; import { DialogContent, DialogHeader, DialogFooter as BaseDialogFooter } from "@calcom/ui/components/dialog"; import { ConfirmationDialogContent } from "@calcom/ui/components/dialog"; import { Dialog as UIDialog } from "@calcom/ui/components/dialog"; import { Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@calcom/ui/components/dropdown"; import { AddVariablesDropdown } from "@calcom/ui/components/editor"; import { ToggleGroup, Switch } from "@calcom/ui/components/form"; import { Label, TextArea, Input, TextField, Form, Select } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetBody, SheetFooter, } from "@calcom/ui/components/sheet"; import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants"; import type { FormValues } from "../pages/workflow"; import { TestPhoneCallDialog } from "./TestPhoneCallDialog"; import { VoiceSelectionDialog } from "./VoiceSelectionDialog"; import { WebCallDialog } from "./WebCallDialog"; // Utility functions for prompt display const cleanPromptForDisplay = (prompt: string): string => { if (!prompt) return prompt; const cleanedPrompt = prompt .replace(/check_availability_\{\{eventTypeId\}\}/g, "check_availability") .replace(/book_appointment_\{\{eventTypeId\}\}/g, "book_appointment") .replace(/check_availability_\d+/g, "check_availability") .replace(/book_appointment_\d+/g, "book_appointment"); return cleanedPrompt; }; const restorePromptComplexity = (prompt: string): string => { if (!prompt) return prompt; const restoredPrompt = prompt .replace(/\bcheck_availability\b/g, "check_availability_{{eventTypeId}}") .replace(/\bbook_appointment\b/g, "book_appointment_{{eventTypeId}}"); return restoredPrompt; }; // Language options for the dropdown const LANGUAGE_OPTIONS = [ { value: "en-US", label: "English (US)" }, { value: "en-IN", label: "English (India)" }, { value: "en-GB", label: "English (UK)" }, { value: "en-AU", label: "English (Australia)" }, { value: "en-NZ", label: "English (New Zealand)" }, { value: "de-DE", label: "German" }, { value: "es-ES", label: "Spanish (Spain)" }, { value: "es-419", label: "Spanish (Latin America)" }, { value: "hi-IN", label: "Hindi" }, { value: "fr-FR", label: "French (France)" }, { value: "fr-CA", label: "French (Canada)" }, { value: "ja-JP", label: "Japanese" }, { value: "pt-PT", label: "Portuguese (Portugal)" }, { value: "pt-BR", label: "Portuguese (Brazil)" }, { value: "zh-CN", label: "Chinese (Simplified)" }, { value: "ru-RU", label: "Russian" }, { value: "it-IT", label: "Italian" }, { value: "ko-KR", label: "Korean" }, { value: "nl-NL", label: "Dutch (Netherlands)" }, { value: "nl-BE", label: "Dutch (Belgium)" }, { value: "pl-PL", label: "Polish" }, { value: "tr-TR", label: "Turkish" }, { value: "th-TH", label: "Thai" }, { value: "vi-VN", label: "Vietnamese" }, { value: "ro-RO", label: "Romanian" }, { value: "bg-BG", label: "Bulgarian" }, { value: "ca-ES", label: "Catalan" }, { value: "da-DK", label: "Danish" }, { value: "fi-FI", label: "Finnish" }, { value: "el-GR", label: "Greek" }, { value: "hu-HU", label: "Hungarian" }, { value: "id-ID", label: "Indonesian" }, { value: "no-NO", label: "Norwegian" }, { value: "sk-SK", label: "Slovak" }, { value: "sv-SE", label: "Swedish" }, { value: "multi", label: "Multilingual" }, ]; const agentSchema = z.object({ generalPrompt: z.string().min(1, "General prompt is required"), beginMessage: z.string().min(1, "Begin message is required"), numberToCall: z.string().optional(), language: z.string().optional(), voiceId: z.string().optional(), generalTools: z .array( z.object({ type: z.string(), name: z.string(), description: z.string().nullish().default(null), cal_api_key: z.string().nullish().default(null), event_type_id: z.number().nullish().default(null), timezone: z.string().nullish().default(null), }) ) .optional(), }); const phoneNumberFormSchema = z.object({ phoneNumber: z.string().refine((val) => isValidPhoneNumber(val)), terminationUri: z.string().min(1, "Termination URI is required"), sipTrunkAuthUsername: z.string().optional(), sipTrunkAuthPassword: z.string().optional(), nickname: z.string().optional(), }); type AgentFormValues = z.infer; type PhoneNumberFormValues = z.infer; // type RetellData = RouterOutputs["viewer"]["ai"]["get"]["retellData"]; // type ToolDraft = { // type: string; // name: string; // description: string | null; // cal_api_key: string | null; // event_type_id: number | null; // timezone: string; // }; type AgentConfigurationSheetProps = { open: boolean; onOpenChange: (open: boolean) => void; agentId?: string | null; agentData?: RouterOutputs["viewer"]["aiVoiceAgent"]["get"]; onUpdate: (data: AgentFormValues) => void; readOnly?: boolean; teamId?: number; isOrganization?: boolean; workflowId?: string; workflowStepId?: number; activeTab?: "prompt" | "phoneNumber"; form: UseFormReturn; }; export function AgentConfigurationSheet({ open, activeTab: _activeTab, onOpenChange, agentId, agentData, onUpdate, readOnly = false, teamId, isOrganization = false, workflowId, workflowStepId: _workflowStepId, form, }: AgentConfigurationSheetProps) { const { t } = useLocale(); const utils = trpc.useUtils(); const [activeTab, setActiveTab] = useState<"prompt" | "phoneNumber">(_activeTab ?? "prompt"); const [isBuyDialogOpen, setIsBuyDialogOpen] = useState(false); const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); const [showAdvancedFields, setShowAdvancedFields] = useState(false); const [isTestAgentDialogOpen, setIsTestAgentDialogOpen] = useState(false); const [isWebCallDialogOpen, setIsWebCallDialogOpen] = useState(false); const [isVoiceDialogOpen, setIsVoiceDialogOpen] = useState(false); const [cancellingNumberId, setCancellingNumberId] = useState(null); const [numberToDelete, setNumberToDelete] = useState(null); // const [toolDialogOpen, setToolDialogOpen] = useState(false); // const [editingToolIndex, setEditingToolIndex] = useState(null); // const [toolDraft, setToolDraft] = useState(null); const generalPromptRef = useRef(null); const agentForm = useForm({ resolver: zodResolver(agentSchema), defaultValues: { generalPrompt: "", beginMessage: "", numberToCall: "", language: "en-US", voiceId: "", generalTools: [], }, }); useEffect(() => { if (agentData?.retellData) { const retellData = agentData.retellData; agentForm.reset({ generalPrompt: cleanPromptForDisplay(retellData.generalPrompt || ""), beginMessage: retellData.beginMessage || "", numberToCall: "", language: retellData.language || "en-US", voiceId: retellData.voiceId || "", generalTools: retellData.generalTools || [], }); } }, [agentData, agentForm]); const phoneNumberForm = useForm({ resolver: zodResolver(phoneNumberFormSchema), defaultValues: { phoneNumber: "", terminationUri: "", sipTrunkAuthUsername: "", sipTrunkAuthPassword: "", nickname: "", }, }); // const { // fields: toolFields, // append: appendTool, // remove: removeTool, // update: updateTool, // } = useFieldArray({ // control: agentForm.control, // name: "generalTools", // }); const buyNumberMutation = trpc.viewer.phoneNumber.buy.useMutation({ onSuccess: async (data: { checkoutUrl?: string; message?: string; phoneNumber?: unknown }) => { if (data.checkoutUrl) { window.location.href = data.checkoutUrl; } else if (data.phoneNumber) { showToast(t("phone_number_purchased_successfully"), "success"); await utils.viewer.me.get.invalidate(); setIsBuyDialogOpen(false); if (agentId) { utils.viewer.aiVoiceAgent.get.invalidate({ id: agentId }); } } else { showToast(data.message || t("something_went_wrong"), "error"); } }, onError: (error: { message: string }) => { showToast(error.message, "error"); }, }); const importNumberMutation = trpc.viewer.phoneNumber.import.useMutation({ onSuccess: async (_data: unknown) => { showToast(t("phone_number_imported_successfully"), "success"); setIsImportDialogOpen(false); phoneNumberForm.reset(); await utils.viewer.me.get.invalidate(); if (agentId) { await utils.viewer.aiVoiceAgent.get.invalidate({ id: agentId }); } }, onError: (error: { message: string }) => { showToast(error.message, "error"); }, }); const cancelSubscriptionMutation = trpc.viewer.phoneNumber.cancel.useMutation({ onSuccess: async (data: { message?: string }) => { showToast(data.message || t("phone_number_subscription_cancelled_successfully"), "success"); setCancellingNumberId(null); await utils.viewer.me.get.invalidate(); if (agentId) { await utils.viewer.aiVoiceAgent.get.invalidate({ id: agentId }); } }, onError: (error: { message: string }) => { showToast(error.message, "error"); setCancellingNumberId(null); }, }); const deletePhoneNumberMutation = trpc.viewer.phoneNumber.delete.useMutation({ onSuccess: async () => { showToast(t("phone_number_deleted_successfully"), "success"); setNumberToDelete(null); if (agentId) { await utils.viewer.aiVoiceAgent.get.invalidate({ id: agentId }); } }, onError: (error: { message: string }) => { showToast(error.message, "error"); setNumberToDelete(null); }, }); const agentQuery = trpc.viewer.aiVoiceAgent.get.useQuery( { id: agentId || "" }, { enabled: !!agentId, refetchOnMount: false, } ); const updateAgentMutation = trpc.viewer.aiVoiceAgent.update.useMutation({ onSuccess: () => { if (agentId) { agentQuery.refetch(); } }, onError: (error: { message: string }) => { showToast(error.message, "error"); }, }); const handleAgentUpdate = async (data: AgentFormValues) => { if (!agentId) return; // Only send prompt-related fields, not tools const updatePayload = { generalPrompt: restorePromptComplexity(data.generalPrompt), beginMessage: data.beginMessage, language: data.language as Language, voiceId: data.voiceId, // Don't include generalTools - they are managed by the backend }; await updateAgentMutation.mutateAsync({ id: agentId, teamId: teamId, ...updatePayload, }); onUpdate(updatePayload); }; // const openAddToolDialog = () => { // setEditingToolIndex(null); // setToolDraft({ // type: "check_availability_cal", // name: "", // description: "", // cal_api_key: null, // event_type_id: null, // timezone: "", // }); // setToolDialogOpen(true); // }; // const openEditToolDialog = (idx: number) => { // const tool = toolFields[idx]; // if (tool) { // setEditingToolIndex(idx); // setToolDraft({ ...tool }); // setToolDialogOpen(true); // } // }; // const handleToolDialogSave = () => { // if (!toolDraft?.name || !toolDraft?.type) return; // if (toolDraft.type === "check_availability_cal" || toolDraft.type === "book_appointment_cal") { // if (!toolDraft.cal_api_key) { // showToast(t("API Key is required for Cal.com tools"), "error"); // return; // } // if (!toolDraft.event_type_id) { // showToast(t("Event Type ID is required for Cal.com tools"), "error"); // return; // } // if (!toolDraft.timezone) { // showToast(t("Timezone is required for Cal.com tools"), "error"); // return; // } // } // if (editingToolIndex !== null) { // updateTool(editingToolIndex, toolDraft); // } else { // appendTool(toolDraft); // } // setToolDialogOpen(false); // setToolDraft(null); // setEditingToolIndex(null); // }; // const handleToolDelete = (idx: number) => { // removeTool(idx); // }; const handleImportPhoneNumber = (values: PhoneNumberFormValues) => { if (!agentId) { showToast(t("agent_required_for_import"), "error"); return; } const mutationPayload = { ...values, workflowId: workflowId, agentId: agentId, teamId: teamId, }; importNumberMutation.mutate(mutationPayload); }; const handleCancelSubscription = (phoneNumberId: number) => { setCancellingNumberId(phoneNumberId); }; const handleDeletePhoneNumber = (phoneNumber: string) => { setNumberToDelete(phoneNumber); }; const confirmCancelSubscription = () => { if (cancellingNumberId) { cancelSubscriptionMutation.mutate({ phoneNumberId: cancellingNumberId }); } }; const confirmDeletePhoneNumber = () => { if (numberToDelete) { deletePhoneNumberMutation.mutate({ phoneNumber: numberToDelete }); } }; const addVariableToGeneralPrompt = (variable: string) => { if (generalPromptRef.current) { const currentPrompt = generalPromptRef.current.value || ""; const cursorPosition = generalPromptRef.current.selectionStart || currentPrompt.length; const variableName = `{${variable.toUpperCase().replace(/ /g, "_")}}`; const newPrompt = `${currentPrompt.substring( 0, cursorPosition )}${variableName}${currentPrompt.substring(cursorPosition)}`; agentForm.setValue("generalPrompt", newPrompt, { shouldDirty: true, shouldTouch: true }); requestAnimationFrame(() => { if (generalPromptRef.current) { generalPromptRef.current.focus(); generalPromptRef.current.setSelectionRange( cursorPosition + variableName.length, cursorPosition + variableName.length ); } }); } }; return ( <> {t("cal_ai_agent_configuration")} { setActiveTab((val || "prompt") as "prompt" | "phoneNumber"); }} value={activeTab} options={[ { value: "prompt", label: t("prompt") }, { value: "phoneNumber", label: t("phone_number") }, ]} isFullWidth={true} /> {activeTab === "prompt" && (
(

{t("general_prompt_description")}

{!readOnly && ( )}