From 199d95b35f7309dc5a325105e8693aae5d99dd8d Mon Sep 17 00:00:00 2001
From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Date: Tue, 30 Jul 2024 16:39:12 +0530
Subject: [PATCH] feat: cal ai templates (#15391)
* chore: save progress
* feat: add schema and fieds
* chore: update code
* chore: save
* refactor: code and type error
* feat: use facade + command design pattern
* chore: missing param
* feat: add custom template feature
* refactor: code
* chore: move the default prompt
* fix: type err
* fix: code and ux improvements
* chore: change default template
* fix: ui and typo
* fix: add validation check for my phone number
---------
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
---
.../eventtype/AIEventController.tsx | 274 ++++++++++--------
.../views/event-types-single-view.tsx | 42 +--
.../api/get-inbound-dynamic-variables.ts | 6 +-
apps/web/public/static/locales/en/common.json | 4 +
.../cal-ai-phone/getTemplateFieldsSchema.ts | 6 +
.../ee/cal-ai-phone/promptTemplates.ts | 62 ++++
.../ee/cal-ai-phone/retellAIService.ts | 227 +++++++++++++++
.../ee/cal-ai-phone/template-fields-map.ts | 54 ++++
.../features/ee/cal-ai-phone/zod-utils.ts | 171 +++++++++++
packages/features/eventtypes/lib/types.ts | 8 +-
.../sdk/src/endpoints/events/types.ts | 8 +-
.../migration.sql | 5 +
packages/prisma/schema.prisma | 6 +-
packages/prisma/zod-utils.ts | 62 ----
.../server/routers/viewer/eventTypes/types.ts | 8 +-
.../routers/viewer/organizations/_router.tsx | 4 +-
.../organizations/createPhoneCall.handler.ts | 152 ++++------
17 files changed, 759 insertions(+), 340 deletions(-)
create mode 100644 packages/features/ee/cal-ai-phone/getTemplateFieldsSchema.ts
create mode 100644 packages/features/ee/cal-ai-phone/promptTemplates.ts
create mode 100644 packages/features/ee/cal-ai-phone/retellAIService.ts
create mode 100644 packages/features/ee/cal-ai-phone/template-fields-map.ts
create mode 100644 packages/features/ee/cal-ai-phone/zod-utils.ts
create mode 100644 packages/prisma/migrations/20240610084425_add_cal_ai_template/migration.sql
diff --git a/apps/web/components/eventtype/AIEventController.tsx b/apps/web/components/eventtype/AIEventController.tsx
index 783161abb3..9d9b78c1bf 100644
--- a/apps/web/components/eventtype/AIEventController.tsx
+++ b/apps/web/components/eventtype/AIEventController.tsx
@@ -1,14 +1,18 @@
+import * as RadioGroup from "@radix-ui/react-radio-group";
import { useSession } from "next-auth/react";
import type { EventTypeSetup } from "pages/event-types/[type]";
-import { useState } from "react";
+import React, { useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import { z } from "zod";
+import { getTemplateFieldsSchema } from "@calcom/features/ee/cal-ai-phone/getTemplateFieldsSchema";
+import { TEMPLATES_FIELDS } from "@calcom/features/ee/cal-ai-phone/template-fields-map";
+import type { TemplateType } from "@calcom/features/ee/cal-ai-phone/zod-utils";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
+import { ComponentForField } from "@calcom/features/form-builder/FormBuilderField";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
-import { AIPhoneSettingSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import {
Button,
@@ -17,9 +21,9 @@ import {
SettingsToggle,
Divider,
TextField,
- TextAreaField,
PhoneInput,
showToast,
+ Icon,
} from "@calcom/ui";
type AIEventControllerProps = {
@@ -85,10 +89,64 @@ export default function AIEventController({ eventType, isTeamEvent }: AIEventCon
);
}
+const ErrorMessage = ({ fieldName, message }: { fieldName: string; message: string }) => {
+ const { t } = useLocale();
+ return (
+
+
+
{t(message || "invalid_input")}
+
+ );
+};
+
+const TemplateFields = () => {
+ const { t } = useLocale();
+ const formMethods = useFormContext();
+ const { control, watch } = formMethods;
+
+ const templateType = watch("aiPhoneCallConfig.templateType");
+ const fields = TEMPLATES_FIELDS[templateType as TemplateType];
+
+ return (
+
+ {fields?.map((field) => (
+
+
{
+ const { variableName, ...restField } = field;
+ const variableInfo = !!variableName ? `: ${t("variable")} {{${variableName}}}` : "";
+ return (
+
+ {
+ onChange(val);
+ }}
+ />
+ {error?.message && }
+
+ );
+ }}
+ />
+
+ ))}
+
+ );
+};
+
const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const formMethods = useFormContext();
+
const [calApiKey, setCalApiKey] = useState("");
const createCallMutation = trpc.viewer.organizations.createPhoneCall.useMutation({
@@ -98,43 +156,25 @@ const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
}
},
onError: (err) => {
- showToast(t("something_went_wrong"), "error");
+ const message = err?.message ? err.message : t("something_went_wrong");
+ showToast(message, "error");
},
});
- // const [createModalOpen, setCreateModalOpen] = useState(false);
-
- // const NewPhoneButton = () => {
- // const { t } = useLocale();
- // return (
- //
- // );
- // };
-
- // v1 will require the user to log in to Retellai.com to create a phone number, and an agent and
- // authorize it with the Cal.com API key / OAuth
- // const retellAuthorized = true; // TODO: call retellAPI here
-
const handleSubmit = async () => {
try {
const values = formMethods.getValues("aiPhoneCallConfig");
+ const { templateType } = values;
- const data = await AIPhoneSettingSchema.parseAsync({
- generalPrompt: values.generalPrompt,
- beginMessage: values.beginMessage,
- enabled: values.enabled,
- guestName: values.guestName,
- guestEmail: values.guestEmail.trim().length ? values.guestEmail : undefined,
- guestCompany: values.guestCompany.trim().length ? values.guestCompany : undefined,
+ const schema = getTemplateFieldsSchema({ templateType });
+
+ const data = schema.parse({
+ ...values,
+ guestEmail: values.guestEmail && values.guestEmail.trim().length ? values.guestEmail : undefined,
+ guestCompany:
+ values.guestCompany && values.guestCompany.trim().length ? values.guestCompany : undefined,
+ guestName: values.guestName && values.guestName.trim().length ? values.guestName : undefined,
eventTypeId: eventType.id,
- numberToCall: values.numberToCall,
- yourPhoneNumber: values.yourPhoneNumber,
calApiKey,
});
@@ -144,6 +184,15 @@ const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
const fieldName = err.issues?.[0]?.path?.[0];
const message = err.issues?.[0]?.message;
showToast(`Error on ${fieldName}: ${message} `, "error");
+
+ const issues = err.issues;
+ for (const issue of issues) {
+ const fieldName = `aiPhoneCallConfig.${issue.path[0]}` as unknown as keyof FormValues;
+ formMethods.setError(fieldName, {
+ type: "custom",
+ message: issue.message,
+ });
+ }
} else {
showToast(t("something_went_wrong"), "error");
}
@@ -157,68 +206,89 @@ const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
{
+ render={({ field: { onChange, value, name }, fieldState: { error } }) => {
return (
- {
- onChange(val);
- }}
- />
+
+
{
+ onChange(val);
+ }}
+ />
+ {error?.message && }
+
);
}}
/>
+ <>
+ {
+ formMethods.setValue("aiPhoneCallConfig.templateType", val, { shouldDirty: true });
+ }}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
{
+ render={({ field: { onChange, value, name }, fieldState: { error } }) => {
return (
- {
- onChange(val);
- }}
- />
+
+
{
+ onChange(val);
+ }}
+ />
+ {error?.message && }
+
);
}}
/>
+
+
>
-
-
-
-
-
-
{
/>
- {
- formMethods.setValue("aiPhoneCallConfig.generalPrompt", e.target.value, { shouldDirty: true });
- }}
- />
-
- {
- formMethods.setValue("aiPhoneCallConfig.beginMessage", e.target.value, { shouldDirty: true });
- }}
- />