* add trigger * small fixes * add missing workflow DTOs * small fixes * use activeOnWithChildren * fix active on when switching trigger type * remove add variable dropdown * feat: lang support * fix: type errors * feat: select voice agent * refactor: address feedback * refactor: address feedback * refactor: missing import * fix: types * add getAllWorkflowsFromRoutingForm to WorkflowService * fix error caused by undefined evt * fix type error * fix type error * fix tests * feat: add inbound calls * chore: formatting * chore * feat: finish inbound call * chore: formatting * fix: update bug * fix: types * code clean up * final fixes and clean up * remove console.log * remove template text form from triggers * add routing form repoditory function * refactor: Agent Configuration Sheet (#23930) * refactor: agent configuration sheet * chore: use default phone numbre * refactor: improvements * refactor: improvements * fix: types * fix: feedback * fix bug with key * chore: * fix: feedback * fix: prompt * add comments * fix: review * fix: review * refactor: class * refactor: class * fix test * allow cal ai action on form triggers * move any reusable code to scheduleAIPhoneCall * add missing await * use predefined FormSubmissionData type * add .trim() to sms message * pass contextData instead * finish base setup * add missing trigger in update-workflow.input.ts * allow cal.ai action for form triggers in handler * chore: add support for form workflows on api v2 * fixup! chore: add support for form workflows on api v2 * ai phone call on form submissions (WIP) * use existing type for Option array * pass chosen event type id * refactor: rename * Update apps/web/public/static/locales/en/common.json * Update apps/web/public/static/locales/en/common.json * add missing imports * chore: update set value * fix: remove index * fix: type error * fix: update tetss * use only repository functions in update handler * move all prisma queries from list.handler * review suggestions * fix: use logger * chore: handle workflows api v2 * chore: handle workflows api v2, split in 2 endpoints * fix workflow step creation * remove connect agent and fixes types * add type to workflow * chore: use workflow type in apiv2 WorkflowsOutputService * update worklfow type on update * chore: use workflow type in apiv2 WorkflowsOutputService * fix template body for torm trigger * some UI fixes for email subject/body * resetting email body when changing form triggers * use type field to query workflows * clean up all old active on values * remove responseId from all funciton calls * remove undefined from updateTemplate * refactor: don't use static * fix: type * refactor: split routing form and event-type workflows code * refactor: split routing form and event-type workflows code * fix template text when adding action * chore: don't rename WorkflowActivationDto to avoid ci blocking * refine update schedule to use only allowed actions * fix type error * don't allow whatsapp action with form trigger * fix type error * return early if activeOn array is empty * fix: from step type in BaseFormWorkflowStepDto * fixup! fix: from step type in BaseFormWorkflowStepDto * api v2 updates * move all prisma calls to repository (service/workflows.ts) * use FORM_TRIGGER_WORKFLOW_EVENTS for form queries * use userRepository * use FORM_TRIGGER_WORKFLOW_EVENTS in isFormTrigger * code clean up * code clean up * use repository functions in formSubmissionValidation.ts * fix: schema * refactor: * remove action check in update handler * add event type selection * event type selector improvements * adjust update.handler * set outboundEventTypeId * add back trpc import * fix agent repository functions * clean up * fix bugs caused by merge * pass eventTypeId to updateToolsFromAgentId * add migration for outboundEventTypeId * add SMS actions to allowed form action constants * add cal ai to allowed form actions * pick correct event type for web call * pass correct routed event type id * remove unsued import * fixes for offset api v2 * add missing responseId * fix failing test * fix failing test * improve error message * remove unused imports * chore: handle sms step action for form worklfow in dtos * fix typo * missing missing newStep * minor fixes * remove changes * add routedEventTypeId * fix type error * fix type error * fix typ error in executAPIPhoneCall.tsx * add back inboundEventTypeId * remove console.log * remove outdated code * small fixes * don't throw error for missing phone number * add back filtered triggerOptions * fix eventTypeId in testCall handler * fix type error * update migration * fix trigger is not defined * convert eventTypeId to string * only use outboundEventTypeId for FORM_SUBMITTED trigger * show toast when no event type selected * fix type errors * add missing translation * fix type error * remove callType * fix tests * small fixes * clean up AgentConfigurationSheet * remove EventTypeSelector file * code clean up * clean up * clean up * use resusable function for TestPhoneCallDialog and WebCallDialog * rename result * fix types for event type id * use repository runction in workflowReminder.ts * fix type error * pass eventTypeIds correctly * fix typo * Update apps/web/public/static/locales/en/common.json Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> * use watch instead of getValues * change to z.record(z.unknown()) instead of any() * fix type of eventTypeId * check permissinon for outBoundEventTypeId * add isNaN check * improve function name * update tools when outbound agent event type id changes * pass missing outboundEventTypeId * update migration * fix test * remove cal-ai step from test --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Udit Takkar <udit222001@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: cal.com <morgan@cal.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
390 lines
13 KiB
TypeScript
390 lines
13 KiB
TypeScript
import { useState } from "react";
|
|
import type { UseFormReturn } from "react-hook-form";
|
|
|
|
import type { Language } from "@calcom/features/calAIPhone/providers/retellAI/types";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
import { Button } from "@calcom/ui/components/button";
|
|
import {
|
|
Dropdown,
|
|
DropdownItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@calcom/ui/components/dropdown";
|
|
import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui/components/form";
|
|
import { ToggleGroup } from "@calcom/ui/components/form";
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
SheetBody,
|
|
SheetFooter,
|
|
} from "@calcom/ui/components/sheet";
|
|
import { showToast } from "@calcom/ui/components/toast";
|
|
|
|
import type { FormValues } from "../../pages/workflow";
|
|
import { WebCallDialog } from "../WebCallDialog";
|
|
import { IncomingCallsTab } from "./components/tabs/IncomingCallsTab";
|
|
import { OutgoingCallsTab } from "./components/tabs/OutgoingCallsTab";
|
|
import { PhoneNumberTab } from "./components/tabs/PhoneNumberTab";
|
|
import { useAgentForms } from "./hooks/useAgentForms";
|
|
import type { AgentFormValues } from "./types/schemas";
|
|
import { restorePromptComplexity } from "./utils/promptUtils";
|
|
|
|
type AgentConfigurationSheetProps = {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
agentId?: string | null;
|
|
inboundAgentId?: string | null;
|
|
agentData?: RouterOutputs["viewer"]["aiVoiceAgent"]["get"];
|
|
inboundAgentData?: RouterOutputs["viewer"]["aiVoiceAgent"]["get"];
|
|
onUpdate: (data: AgentFormValues & { id: string }) => void;
|
|
readOnly?: boolean;
|
|
teamId?: number;
|
|
isOrganization?: boolean;
|
|
workflowId?: string;
|
|
workflowStepId?: number;
|
|
activeTab?: "outgoingCalls" | "phoneNumber" | "incomingCalls";
|
|
form: UseFormReturn<FormValues>;
|
|
eventTypeOptions?: Option[];
|
|
};
|
|
|
|
export function AgentConfigurationSheet({
|
|
open,
|
|
activeTab: _activeTab,
|
|
onOpenChange,
|
|
agentId,
|
|
inboundAgentId,
|
|
agentData,
|
|
inboundAgentData,
|
|
readOnly = false,
|
|
teamId,
|
|
isOrganization = false,
|
|
workflowId,
|
|
workflowStepId,
|
|
form,
|
|
eventTypeOptions = [],
|
|
}: AgentConfigurationSheetProps) {
|
|
const { t } = useLocale();
|
|
const utils = trpc.useUtils();
|
|
|
|
const [activeTab, setActiveTab] = useState<"outgoingCalls" | "phoneNumber" | "incomingCalls">(
|
|
_activeTab ?? "outgoingCalls"
|
|
);
|
|
const [isWebCallDialogOpen, setIsWebCallDialogOpen] = useState(false);
|
|
|
|
const activeAgentId = activeTab === "incomingCalls" ? inboundAgentId : agentId;
|
|
|
|
const { outboundAgentForm, inboundAgentForm } = useAgentForms({ agentData, inboundAgentData });
|
|
|
|
const trigger = form.watch("trigger");
|
|
|
|
const updateAgentMutation = trpc.viewer.aiVoiceAgent.update.useMutation({
|
|
onSuccess: async () => {
|
|
if (activeAgentId) {
|
|
await utils.viewer.aiVoiceAgent.get.invalidate({ id: activeAgentId });
|
|
}
|
|
},
|
|
onError: (error: { message: string }) => {
|
|
showToast(error.message, "error");
|
|
},
|
|
});
|
|
|
|
const handleAgentUpdate = async (data: AgentFormValues) => {
|
|
if (!agentId) return;
|
|
if (trigger === "FORM_SUBMITTED" && !data.outboundEventTypeId) {
|
|
showToast(t("select_event_type_to_schedule_calls"), "error");
|
|
return;
|
|
}
|
|
|
|
const updatePayload = {
|
|
generalPrompt: restorePromptComplexity(data.generalPrompt),
|
|
beginMessage: data.beginMessage,
|
|
language: data.language as Language,
|
|
voiceId: data.voiceId,
|
|
outboundEventTypeId: data.outboundEventTypeId,
|
|
};
|
|
|
|
await updateAgentMutation.mutateAsync({
|
|
id: agentId,
|
|
teamId: teamId,
|
|
...updatePayload,
|
|
});
|
|
};
|
|
|
|
const handleInboundAgentUpdate = async (data: AgentFormValues) => {
|
|
if (!inboundAgentId) return;
|
|
|
|
const updatePayload = {
|
|
generalPrompt: restorePromptComplexity(data.generalPrompt),
|
|
beginMessage: data.beginMessage,
|
|
language: data.language as Language,
|
|
voiceId: data.voiceId,
|
|
};
|
|
|
|
await updateAgentMutation.mutateAsync({
|
|
id: inboundAgentId,
|
|
teamId: teamId,
|
|
...updatePayload,
|
|
});
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
<SheetContent className="sm:max-w-xl">
|
|
<SheetHeader showCloseButton={false} className="w-full">
|
|
<SheetTitle className="mb-6">{t("cal_ai_agent_configuration")}</SheetTitle>
|
|
<ToggleGroup
|
|
onValueChange={(val) => {
|
|
setActiveTab((val || "outgoingCalls") as "outgoingCalls" | "phoneNumber" | "incomingCalls");
|
|
}}
|
|
value={activeTab}
|
|
options={[
|
|
{ value: "outgoingCalls", label: t("outgoing_calls") },
|
|
{ value: "phoneNumber", label: t("phone_number") },
|
|
{ value: "incomingCalls", label: t("incoming_calls") },
|
|
]}
|
|
isFullWidth={true}
|
|
/>
|
|
</SheetHeader>
|
|
<SheetBody className="px-0">
|
|
{activeTab === "outgoingCalls" && (
|
|
<OutgoingCallsTab
|
|
outboundAgentForm={outboundAgentForm}
|
|
readOnly={readOnly}
|
|
eventTypeOptions={eventTypeOptions}
|
|
trigger={trigger}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === "phoneNumber" && (
|
|
<PhoneNumberTab
|
|
agentData={agentData}
|
|
agentId={agentId}
|
|
readOnly={readOnly}
|
|
teamId={teamId}
|
|
workflowId={workflowId}
|
|
isOrganization={isOrganization}
|
|
form={form}
|
|
eventTypeIds={eventTypeOptions?.map((opt) => parseInt(opt.value, 10))}
|
|
/>
|
|
)}
|
|
|
|
{activeTab === "incomingCalls" && (
|
|
<IncomingCallsTab
|
|
agentData={agentData}
|
|
inboundAgentData={inboundAgentData}
|
|
inboundAgentId={inboundAgentId}
|
|
inboundAgentForm={inboundAgentForm}
|
|
form={form}
|
|
eventTypeOptions={eventTypeOptions}
|
|
readOnly={readOnly}
|
|
teamId={teamId}
|
|
workflowStepId={workflowStepId}
|
|
/>
|
|
)}
|
|
</SheetBody>
|
|
<SheetFooter>
|
|
<div className="mr-auto">
|
|
<Dropdown>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
color="secondary"
|
|
className="rounded-[10px]"
|
|
disabled={readOnly}
|
|
EndIcon="chevron-down">
|
|
{t("test_agent")}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent>
|
|
<DropdownMenuItem>
|
|
<DropdownItem
|
|
type="button"
|
|
StartIcon="monitor"
|
|
onClick={() => {
|
|
setIsWebCallDialogOpen(true);
|
|
}}>
|
|
{t("web_call")}
|
|
</DropdownItem>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</Dropdown>
|
|
</div>
|
|
<Button
|
|
className="justify-center"
|
|
type="button"
|
|
color="secondary"
|
|
onClick={() => onOpenChange(false)}>
|
|
{t("cancel")}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
className="justify-center"
|
|
onClick={() => {
|
|
if (activeTab === "incomingCalls" && inboundAgentId) {
|
|
inboundAgentForm.handleSubmit(handleInboundAgentUpdate)();
|
|
} else if (activeTab === "outgoingCalls" && agentId) {
|
|
outboundAgentForm.handleSubmit(handleAgentUpdate)();
|
|
}
|
|
}}
|
|
disabled={
|
|
readOnly ||
|
|
updateAgentMutation.isPending ||
|
|
(activeTab === "outgoingCalls" && !outboundAgentForm.formState.isDirty) ||
|
|
(activeTab === "incomingCalls" && !inboundAgentForm.formState.isDirty) ||
|
|
(activeTab === "outgoingCalls" && !agentId) ||
|
|
(activeTab === "incomingCalls" && !inboundAgentId)
|
|
}
|
|
loading={updateAgentMutation.isPending}>
|
|
{t("save")}
|
|
</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
|
|
{activeAgentId && (
|
|
<WebCallDialog
|
|
open={isWebCallDialogOpen}
|
|
onOpenChange={setIsWebCallDialogOpen}
|
|
agentId={activeAgentId}
|
|
teamId={teamId}
|
|
isOrganization={isOrganization}
|
|
form={form}
|
|
eventTypeIds={eventTypeOptions?.map((opt) => parseInt(opt.value, 10))}
|
|
outboundEventTypeId={agentData?.outboundEventTypeId}
|
|
/>
|
|
)}
|
|
|
|
{/* <ToolsDialog
|
|
open={toolDialogOpen}
|
|
onOpenChange={setToolDialogOpen}
|
|
toolDraft={toolDraft}
|
|
setToolDraft={setToolDraft}
|
|
onSave={handleToolDialogSave}
|
|
isEditing={editingToolIndex !== null}
|
|
/> */}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// const ToolsDialog = ({
|
|
// open,
|
|
// onOpenChange,
|
|
// toolDraft,
|
|
// setToolDraft,
|
|
// onSave,
|
|
// isEditing,
|
|
// }: {
|
|
// open: boolean;
|
|
// onOpenChange: (open: boolean) => void;
|
|
// toolDraft: ToolDraft | null;
|
|
// setToolDraft: (draft: ToolDraft) => void;
|
|
// onSave: () => void;
|
|
// isEditing: boolean;
|
|
// }) => {
|
|
// const { t } = useLocale();
|
|
|
|
// const TOOL_TYPES = [
|
|
// { label: t("Check Availability"), value: "check_availability_cal" },
|
|
// { label: t("Book Appointment"), value: "book_appointment_cal" },
|
|
// { label: t("End Call"), value: "end_call" },
|
|
// ];
|
|
|
|
// return (
|
|
// <Dialog open={open} onOpenChange={onOpenChange}>
|
|
// <DialogContent
|
|
// enableOverflow
|
|
// title={isEditing ? t("Edit Function") : t("Add Function")}
|
|
// type="creation">
|
|
// <div className="flex flex-col gap-4">
|
|
// <div>
|
|
// <Label>{t("Type")}</Label>
|
|
// <Select
|
|
// options={TOOL_TYPES}
|
|
// value={TOOL_TYPES.find((opt) => opt.value === toolDraft?.type) || null}
|
|
// onChange={(option) => setToolDraft((d: ToolDraft) => ({ ...d, type: option?.value || "" }))}
|
|
// placeholder={t("Select function type")}
|
|
// />
|
|
// </div>
|
|
// <div>
|
|
// <Label>{t("Name")}</Label>
|
|
// <TextField
|
|
// required
|
|
// value={toolDraft?.name || ""}
|
|
// onChange={(e) => setToolDraft((d: ToolDraft) => ({ ...d, name: e.target.value }))}
|
|
// placeholder={t("Enter function name")}
|
|
// />
|
|
// </div>
|
|
// <div>
|
|
// <Label>{t("Description")}</Label>
|
|
// <TextArea
|
|
// value={toolDraft?.description || ""}
|
|
// onChange={(e) => setToolDraft((d: ToolDraft) => ({ ...d, description: e.target.value }))}
|
|
// placeholder={t("Enter description (optional)")}
|
|
// />
|
|
// </div>
|
|
// {(toolDraft?.type === "check_availability_cal" || toolDraft?.type === "book_appointment_cal") && (
|
|
// <>
|
|
// <div>
|
|
// <Label>
|
|
// {t("API Key (Cal.com)")}
|
|
// <span className="text-red-500"> *</span>
|
|
// </Label>
|
|
// <TextField
|
|
// required
|
|
// value={toolDraft?.cal_api_key || ""}
|
|
// onChange={(e) => setToolDraft((d: ToolDraft) => ({ ...d, cal_api_key: e.target.value }))}
|
|
// placeholder={t("Enter Cal.com API key")}
|
|
// />
|
|
// </div>
|
|
// <div>
|
|
// <Label>
|
|
// {t("Event Type ID (Cal.com)")}
|
|
// <span className="text-red-500"> *</span>
|
|
// </Label>
|
|
// <TextField
|
|
// required
|
|
// value={toolDraft?.event_type_id || ""}
|
|
// onChange={(e) =>
|
|
// setToolDraft((d: ToolDraft) => ({
|
|
// ...d,
|
|
// event_type_id: e.target.value ? Number(e.target.value) : null,
|
|
// }))
|
|
// }
|
|
// placeholder={t("Enter Event Type ID")}
|
|
// type="number"
|
|
// />
|
|
// </div>
|
|
// <div>
|
|
// <Label>
|
|
// {t("Timezone")}
|
|
// <span className="text-red-500"> *</span>
|
|
// </Label>
|
|
// <TextField
|
|
// required
|
|
// value={toolDraft?.timezone || ""}
|
|
// onChange={(e) => setToolDraft((d: ToolDraft) => ({ ...d, timezone: e.target.value }))}
|
|
// placeholder={t("America/Los_Angeles")}
|
|
// />
|
|
// <p className="mt-1 text-xs text-subtle">{t("Required for Cal.com calendar integration")}</p>
|
|
// </div>
|
|
// </>
|
|
// )}
|
|
// </div>
|
|
// <DialogFooter showDivider>
|
|
// <Button type="button" color="secondary" onClick={() => onOpenChange(false)}>
|
|
// {t("Cancel")}
|
|
// </Button>
|
|
// <Button type="button" onClick={onSave}>
|
|
// {t("Save")}
|
|
// </Button>
|
|
// </DialogFooter>
|
|
// </DialogContent>
|
|
// </Dialog>
|
|
// );
|
|
// };
|