feat: cal.ai enterprise phone calls (#14100)

* added empty layout

* init

* yarn lock due to package changes

* added retell ai to .env

* removed ton of glue code

* nit

* Discard changes to package.json

* Upgrades lucide-react

* minor UI fixes

* nit

* nit

* feat: save progress

* feat: v1

* fix: type error

* feat: change schema

* fix: type error and testr

* chore: update agent

* chore: change default prompt

* feat: feedback and improvements

* fix: type error

* fix: type error

* hidden for now while in trial

* added i18n and removed some comments

* feat: add cal api key

* fix: type error

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
This commit is contained in:
Peer Richelsen
2024-03-28 13:59:01 +00:00
committed by GitHub
co-authored by Omar López Udit Takkar Udit Takkar Joe Au-Yeung
parent eec76ecfe2
commit 1025238eee
42 changed files with 724 additions and 59 deletions
+6 -1
View File
@@ -345,4 +345,9 @@ SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
NEXT_PUBLIC_API_V2_URL="http://localhost:5555/api/v2"
# Ratelimiting via unkey
UNKEY_ROOT_KEY=
UNKEY_ROOT_KEY=
# Used for Cal.ai Enterprise Voice AI Agents
# https://retellai.com
RETELL_AI_KEY=
@@ -122,7 +122,7 @@ export const InstallAppButtonChild = ({
data-testid={team.isUser ? "install-app-button-personal" : "anything else"}
key={team.id}
disabled={isInstalled}
StartIcon={(props) => (
StartIcon={(props: { className?: string }) => (
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
@@ -0,0 +1,279 @@
import { useSession } from "next-auth/react";
import type { EventTypeSetup } from "pages/event-types/[type]";
import { useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import { z } from "zod";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
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,
Label,
EmptyScreen,
SettingsToggle,
Divider,
TextField,
TextAreaField,
PhoneInput,
showToast,
} from "@calcom/ui";
import { Sparkles } from "@calcom/ui/components/icon";
type AIEventControllerProps = {
eventType: EventTypeSetup;
isTeamEvent: boolean;
};
export default function AIEventController({ eventType, isTeamEvent }: AIEventControllerProps) {
const { t } = useLocale();
const session = useSession();
const [aiEventState, setAIEventState] = useState<boolean>(eventType?.aiPhoneCallConfig?.enabled ?? false);
const formMethods = useFormContext<FormValues>();
const isOrg = !!session.data?.user?.org?.id;
if (session.status === "loading") return <></>;
return (
<LicenseRequired>
<div className="block items-start sm:flex">
{!isOrg || !isTeamEvent ? (
<EmptyScreen
headline={t("Cal.ai")}
Icon={Sparkles}
description={t("upgrade_to_cal_ai_phone_number_description")}
buttonRaw={<Button href="/enterprise">{t("upgrade")}</Button>}
/>
) : (
<div className="w-full">
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName={classNames(
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
aiEventState && "rounded-b-none"
)}
childrenClassName="lg:ml-0"
title={t("Cal.ai")}
description={t("use_cal_ai_to_make_call_description")}
checked={aiEventState}
data-testid="instant-event-check"
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("aiPhoneCallConfig.enabled", false, {
shouldDirty: true,
});
setAIEventState(false);
} else {
formMethods.setValue("aiPhoneCallConfig.enabled", true, {
shouldDirty: true,
});
setAIEventState(true);
}
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
{aiEventState && <AISettings eventType={eventType} />}
</div>
</SettingsToggle>
</div>
)}
</div>
</LicenseRequired>
);
}
const AISettings = ({ eventType }: { eventType: EventTypeSetup }) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const [calApiKey, setCalApiKey] = useState("");
const createCallMutation = trpc.viewer.organizations.createPhoneCall.useMutation({
onSuccess: (data) => {
if (!!data?.call_id) {
showToast("Phone Call Created successfully", "success");
}
},
onError: (err) => {
showToast(t("something_went_wrong"), "error");
},
});
// const [createModalOpen, setCreateModalOpen] = useState(false);
// const NewPhoneButton = () => {
// const { t } = useLocale();
// return (
// <Button
// color="primary"
// data-testid="new_phone_number"
// StartIcon={Plus}
// onClick={() => setCreateModalOpen(true)}>
// {t("New Phone number")}
// </Button>
// );
// };
// 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 data = await AIPhoneSettingSchema.parseAsync({
...values,
eventTypeId: eventType.id,
calApiKey,
});
createCallMutation.mutate(data);
} catch (err) {
if (err instanceof z.ZodError) {
const fieldName = err.issues?.[0]?.path?.[0];
const message = err.issues?.[0]?.message;
showToast(`Error on ${fieldName}: ${message} `, "error");
} else {
showToast(t("something_went_wrong"), "error");
}
}
};
return (
<div>
<div className="space-y-4">
<>
<Label>{t("your_phone_number")}</Label>
<Controller
name="aiPhoneCallConfig.yourPhoneNumber"
render={({ field: { onChange, value } }) => {
return (
<PhoneInput
required
placeholder={t("your_phone_number")}
id="aiPhoneCallConfig.yourPhoneNumber"
name="aiPhoneCallConfig.yourPhoneNumber"
value={value}
onChange={(val) => {
onChange(val);
}}
/>
);
}}
/>
<Label>{t("number_to_call")}</Label>
<Controller
name="aiPhoneCallConfig.numberToCall"
render={({ field: { onChange, value } }) => {
return (
<PhoneInput
required
placeholder={t("phone_number")}
id="aiPhoneCallConfig.numberToCall"
name="aiPhoneCallConfig.numberToCall"
value={value}
onChange={(val) => {
onChange(val);
}}
/>
);
}}
/>
<Divider />
</>
<TextField
type="text"
hint="Variable: {name}"
label={t("guest_name")}
placeholder="Jane Doe"
{...formMethods.register("aiPhoneCallConfig.guestName")}
/>
<TextField
type="text"
hint="For eg:- cal_live_0123.."
label={t("provide_api_key")}
name="calApiKey"
placeholder="Cal API Key"
value={calApiKey}
onChange={(e) => {
setCalApiKey(e.target.value);
}}
/>
<Divider />
<TextAreaField
rows={3}
required
placeholder={t("general_prompt")}
label={t("general_prompt")}
{...formMethods.register("aiPhoneCallConfig.generalPrompt")}
onChange={(e) => {
formMethods.setValue("aiPhoneCallConfig.generalPrompt", e.target.value, { shouldDirty: true });
}}
/>
<TextAreaField
rows={3}
placeholder={t("begin_message")}
label={t("begin_message")}
{...formMethods.register("aiPhoneCallConfig.beginMessage")}
onChange={(e) => {
formMethods.setValue("aiPhoneCallConfig.beginMessage", e.target.value, { shouldDirty: true });
}}
/>
<Button
disabled={createCallMutation.isPending}
loading={createCallMutation.isPending}
onClick={handleSubmit}>
{t("make_a_call")}
</Button>
{/* TODO:<small className="block opacity-60">
Want to automate outgoing phone calls? Read our{" "}
<Link className="underline" href="https://cal.com/docs">
API docs
</Link>{" "}
and learn how to build workflows.
</small> */}
</div>
{/* TODO:
<>
<EmptyScreen
Icon={Phone}
headline={t("Create your phone number")}
description={t(
"This phone number can be called by guests but can also do proactive outbound calls by the AI agent."
)}
buttonRaw={
<div className="flex justify-between gap-2">
<NewPhoneButton />
<Button color="secondary">{t("learn_more")}</Button>
</div>
}
/>
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
<DialogContent
enableOverflow
title={t("Create phone number")}
description={t("This number can later be called or can do proactive outbound calls")}>
<div className="mb-12 mt-4">
<TextField placeholder="+415" hint="Area Code" />
</div>
</DialogContent>
</Dialog>
</>
*/}
</div>
);
};
@@ -0,0 +1,10 @@
import type { EventTypeSetupProps } from "pages/event-types/[type]";
import AIEventController from "./AIEventController";
export const EventAITab = ({
eventType,
isTeamEvent,
}: Pick<EventTypeSetupProps, "eventType"> & { isTeamEvent: boolean }) => {
return <AIEventController eventType={eventType} isTeamEvent={isTeamEvent} />;
};
@@ -44,6 +44,7 @@ import {
Repeat,
Grid,
Zap,
Sparkles,
Users,
ExternalLink,
Code,
@@ -283,6 +284,15 @@ function EventTypeSingleLayout({
info: `${activeWebhooksNumber} ${t("active")}`,
});
}
const hidden = true; // hidden while in alpha trial. you can access it with tabName=ai
if (team && hidden) {
navigation.push({
name: "Cal.ai",
href: `/event-types/${eventType.id}?tabName=ai`,
icon: Sparkles,
info: "cal_ai_event_tab_description", // todo `cal_ai_event_tab_description`,
});
}
return navigation;
}, [
t,
+3 -1
View File
@@ -1,9 +1,11 @@
import React from "react";
import type { LucideIcon } from "@calcom/ui/components/icon";
import type { SVGComponent } from "@lib/types/SVGComponent";
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
Icon: SVGComponent;
Icon: SVGComponent | LucideIcon;
}
export default function LinkIconButton(props: LinkIconButtonProps) {
@@ -34,6 +34,11 @@ import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayo
import { type PageProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
const DEFAULT_PROMPT_VALUE =
"## You are helping user set up a call with the support team. The appointment is 15 min long. You are a pleasant and friendly.\n\n## Style Guardrails\nBe Concise: Respond succinctly, addressing one topic at most.\nEmbrace Variety: Use diverse language and rephrasing to enhance clarity without repeating content.\nBe Conversational: Use everyday language, making the chat feel like talking to a friend.\nBe Proactive: Lead the conversation, often wrapping up with a question or next-step suggestion.\nAvoid multiple questions in a single response.\nGet clarity: If the user only partially answers a question, or if the answer is unclear, keep asking to get clarity.\nUse a colloquial way of referring to the date (like Friday, Jan 14th, or Tuesday, Jan 12th, 2024 at 8am).\nIf you are saying a time like 8:00 AM, just say 8 AM and emit the trailing zeros.\n\n## Response Guideline\nAdapt and Guess: Try to understand transcripts that may contain transcription errors. Avoid mentioning \"transcription error\" in the response.\nStay in Character: Keep conversations within your role'''s scope, guiding them back creatively without repeating.\nEnsure Fluid Dialogue: Respond in a role-appropriate, direct manner to maintain a smooth conversation flow.\n\n## Schedule Rule\nCurrent time is {{current_time}}. You only schedule time in current calendar year, you cannot schedule time that'''s in the past.\n\n## Task Steps\n1. I am here to learn more about your issue and help schedule an appointment with our support team.\n2. Ask for user name and email. Confirm the name and email with user by reading it back to user.\n3. Ask user for \"When would you want to meet with one of our representive\".\n4. Call function check_availability to check for availability in the user provided time range.\n - if availability exists, inform user about the availability range (do not repeat the detailed available slot) and ask user to choose from it. Make sure user chose a slot within detailed available slot.\n - if availability does not exist, ask user to select another time range for the appointment, repeat this step 3.\n4. Confirm the date and time selected by user: \"Just to confirm, you want to book the appointment at ...\".\n6. Once confirmed, call function book_appointment to book the appointment.\n - if booking returned booking detail, it means booking is successful, proceed to step 7.\n - if booking returned error message, let user know why the booking was not successful, and maybe start over with step 3.\n7. Inform the user booking is successful, and ask if user have any questions. Answer them if there are any.\n8. After all questions answered, call function end_call to hang up.";
const DEFAULT_BEGIN_MESSAGE = "Hi. How are you doing?";
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings;
const EventSetupTab = dynamic(() =>
import("@components/eventtype/EventSetupTab").then((mod) => mod.EventSetupTab)
@@ -73,6 +78,8 @@ const EventWebhooksTab = dynamic(() =>
import("@components/eventtype/EventWebhooksTab").then((mod) => mod.EventWebhooksTab)
);
const EventAITab = dynamic(() => import("@components/eventtype/EventAITab").then((mod) => mod.EventAITab));
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
export type Host = { isFixed: boolean; userId: number; priority: number };
@@ -92,6 +99,7 @@ const querySchema = z.object({
"advanced",
"workflows",
"webhooks",
"ai",
])
.optional()
.default("setup"),
@@ -243,6 +251,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
})),
seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
assignAllTeamMembers: eventType.assignAllTeamMembers,
aiPhoneCallConfig: {
generalPrompt: eventType.aiPhoneCallConfig?.generalPrompt ?? DEFAULT_PROMPT_VALUE,
enabled: eventType.aiPhoneCallConfig?.enabled,
beginMessage: eventType.aiPhoneCallConfig?.beginMessage ?? DEFAULT_BEGIN_MESSAGE,
guestName: eventType.aiPhoneCallConfig?.guestName,
yourPhoneNumber: eventType.aiPhoneCallConfig?.yourPhoneNumber,
numberToCall: eventType.aiPhoneCallConfig?.numberToCall,
},
};
}, [eventType, periodDates, metadata]);
const formMethods = useForm<FormValues>({
@@ -375,6 +391,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
/>
),
webhooks: <EventWebhooksTab eventType={eventType} />,
ai: <EventAITab eventType={eventType} isTeamEvent={!!team} />,
} as const;
const isObject = <T,>(value: T): boolean => {
return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -81,6 +81,8 @@
"payment": "Payment",
"missing_card_fields": "Missing card fields",
"pay_now": "Pay now",
"general_prompt": "General Prompt",
"begin_message":"Begin Message",
"codebase_has_to_stay_opensource": "The codebase has to stay open source, whether it was modified or not",
"cannot_repackage_codebase": "You can not repackage or sell the codebase",
"acquire_license": "Acquire a commercial license to remove these terms by emailing",
@@ -207,6 +209,7 @@
"2fa_confirm_current_password": "Confirm your current password to get started.",
"2fa_scan_image_or_use_code": "Scan the image below with the authenticator app on your phone or manually enter the text code instead.",
"text": "Text",
"your_phone_number":"Your Phone Number",
"multiline_text": "Multiline Text",
"number": "Number",
"checkbox": "Checkbox",
@@ -693,6 +696,7 @@
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
"minutes": "Minutes",
"use_cal_ai_to_make_call_description": "Use Cal.ai to get an AI powered phone number or make calls to guests.",
"round_robin": "Round Robin",
"round_robin_description": "Cycle meetings between multiple team members.",
"managed_event": "Managed Event",
@@ -1277,6 +1281,7 @@
"upgrade": "Upgrade",
"upgrade_to_access_recordings_title": "Upgrade to access recordings",
"upgrade_to_access_recordings_description": "Recordings are only available as part of our teams plan. Upgrade to start recording your calls",
"upgrade_to_cal_ai_phone_number_description":"Upgrade to Enterprise to generate an AI Agent phone number that can call guests to schedule calls",
"recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan",
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
@@ -2249,6 +2254,8 @@
"troubleshooter_tooltip": "Open the troubleshooter and figure out what is wrong with your schedule",
"need_help": "Need help?",
"troubleshooter": "Troubleshooter",
"number_to_call": "Number to Call",
"guest_name": "Guest Name",
"please_install_a_calendar": "Please install a calendar",
"instant_tab_title": "Instant Booking",
"instant_event_tab_description": "Let people book immediately",
@@ -2256,6 +2263,7 @@
"dont_want_to_wait": "Don't want to wait?",
"meeting_started": "Meeting Started",
"pay_and_book": "Pay to book",
"cal_ai_event_tab_description":"Let AI Agents book you",
"booking_not_found_error": "Could not find booking",
"booking_seats_full_error": "Booking seats are full",
"missing_payment_credential_error": "Missing payment credentials",
@@ -2330,5 +2338,6 @@
"lock_org_users_eventtypes": "Lock individual event type creation",
"lock_org_users_eventtypes_description": "Prevent members from creating their own event types.",
"cookie_consent_checkbox": "I consent to our privacy policy and cookie usage",
"make_a_call": "Make a Call",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+1 -1
View File
@@ -109,7 +109,7 @@
},
"dependencies": {
"eslint": "^8.34.0",
"lucide-react": "^0.171.0",
"lucide-react": "^0.363.0",
"turbo": "^1.10.1"
},
"resolutions": {
@@ -6,6 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import { Alert, Button, Dialog, DialogClose, DialogContent, DialogFooter, Input } from "@calcom/ui";
import { Link, Search } from "@calcom/ui/components/icon";
import type { LucideIcon } from "@calcom/ui/components/icon";
interface ISearchDialog {
isOpenDialog: boolean;
@@ -85,7 +86,7 @@ export const SearchDialog = (props: ISearchDialog) => {
return null;
};
const renderTab = (Icon: SVGComponent, text: string, mode: Mode) => (
const renderTab = (Icon: SVGComponent | LucideIcon, text: string, mode: Mode) => (
<div
className={classNames(
"flex cursor-pointer items-center border-b-2 p-2 text-sm ",
@@ -6,6 +6,7 @@ import classNames from "@calcom/lib/classNames";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Clock, CheckSquare, RefreshCcw } from "@calcom/ui/components/icon";
import type { LucideIcon } from "@calcom/ui/components/icon";
import type { PublicEvent } from "../../types";
import { EventDetailBlocks } from "../../types";
@@ -34,7 +35,7 @@ type EventDetailCustomBlock = {
type EventDetailsProps = EventDetailsPropsBase & (EventDetailDefaultBlock | EventDetailCustomBlock);
interface EventMetaProps {
icon?: React.FC<{ className: string }> | string;
icon?: React.FC<{ className: string }> | string | LucideIcon;
children: React.ReactNode;
// Emphasises the text in the block. For now only
// applying in dark mode.
@@ -1,5 +1,7 @@
import { CreditCard, Zap } from "lucide-react";
export function getPayIcon(currency: string): React.FC<{ className: string }> | string {
import type { LucideIcon } from "@calcom/ui/components/icon";
export function getPayIcon(currency: string): React.FC<{ className: string }> | string | LucideIcon {
return currency !== "BTC" ? CreditCard : Zap;
}
@@ -1,7 +1,8 @@
import { CreditCard } from "lucide-react";
import type { LucideIcon } from "@calcom/ui/components/icon";
import { SatSymbol } from "@calcom/ui/components/icon/SatSymbol";
export function getPriceIcon(currency: string): React.FC<{ className: string }> | string {
export function getPriceIcon(currency: string): React.FC<{ className: string }> | string | LucideIcon {
return currency !== "BTC" ? CreditCard : (SatSymbol as React.FC<{ className: string }>);
}
@@ -6,9 +6,10 @@ import { trpc } from "@calcom/trpc/react";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import { CreateButtonWithTeamsList, EmptyScreen as ClassicEmptyScreen, showToast } from "@calcom/ui";
import { Mail, Smartphone, Zap } from "@calcom/ui/components/icon";
import type { LucideIcon } from "@calcom/ui/components/icon";
type WorkflowExampleType = {
Icon: SVGComponent;
Icon: SVGComponent | LucideIcon;
text: string;
};
@@ -54,6 +54,7 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
eventName: true,
slug: true,
isInstantEvent: true,
aiPhoneCallConfig: true,
schedulingType: true,
length: true,
locations: true,
@@ -302,6 +303,7 @@ export const getPublicEvent = async (
},
isDynamic: false,
isInstantEvent: eventWithUserProfiles.isInstantEvent,
aiPhoneCallConfig: eventWithUserProfiles.aiPhoneCallConfig,
assignAllTeamMembers: event.assignAllTeamMembers,
};
};
@@ -58,6 +58,14 @@ export type FormValues = {
credentialId?: number;
teamName?: string;
}[];
aiPhoneCallConfig: {
generalPrompt: string;
enabled: boolean;
beginMessage: string;
yourPhoneNumber: string;
numberToCall: string;
guestName: string;
};
customInputs: CustomInputParsed[];
schedule: number | null;
periodType: PeriodType;
@@ -8,11 +8,12 @@ import {
DropdownMenuTrigger,
Tooltip,
} from "@calcom/ui";
import type { LucideIcon } from "@calcom/ui/components/icon";
import { Plus, Link, User, Check } from "@calcom/ui/components/icon";
import { useFilterContext } from "../context/provider";
type Option = { value: "event-type" | "user"; label: string; StartIcon?: SVGComponent };
type Option = { value: "event-type" | "user"; label: string; StartIcon?: SVGComponent | LucideIcon };
export const FilterType = () => {
const { t } = useLocale();
+15 -6
View File
@@ -79,6 +79,7 @@ import {
Tooltip,
useCalcomTheme,
} from "@calcom/ui";
import type { LucideIcon } from "@calcom/ui/components/icon";
import {
ArrowLeft,
ArrowRight,
@@ -100,6 +101,7 @@ import {
Settings,
User as UserIcon,
Users,
Sparkles,
Zap,
Check,
} from "@calcom/ui/components/icon";
@@ -504,7 +506,7 @@ function UserDropdown({ small }: UserDropdownProps) {
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => (
StartIcon={(props: { className?: string }) => (
<UserIcon className={classNames("text-default", props.className)} aria-hidden="true" />
)}
href="/settings/my-account/profile">
@@ -514,7 +516,7 @@ function UserDropdown({ small }: UserDropdownProps) {
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => (
StartIcon={(props: { className?: string }) => (
<Settings className={classNames("text-default", props.className)} aria-hidden="true" />
)}
href="/settings/my-account/general">
@@ -524,7 +526,7 @@ function UserDropdown({ small }: UserDropdownProps) {
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => (
StartIcon={(props: { className?: string }) => (
<Moon className={classNames("text-default", props.className)} aria-hidden="true" />
)}
href="/settings/my-account/out-of-office">
@@ -549,7 +551,9 @@ function UserDropdown({ small }: UserDropdownProps) {
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => <HelpCircle aria-hidden="true" {...props} />}
StartIcon={(props: { className?: string }) => (
<HelpCircle aria-hidden="true" {...props} />
)}
onClick={() => setHelpOpen(true)}>
{t("help")}
</DropdownItem>
@@ -565,7 +569,7 @@ function UserDropdown({ small }: UserDropdownProps) {
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={(props) => <LogOut aria-hidden="true" {...props} />}
StartIcon={(props: { className?: string }) => <LogOut aria-hidden="true" {...props} />}
onClick={() => signOut({ callbackUrl: "/auth/logout" })}>
{t("sign_out")}
</DropdownItem>
@@ -585,7 +589,7 @@ export type NavigationItemType = {
onClick?: React.MouseEventHandler<HTMLAnchorElement | HTMLButtonElement>;
target?: HTMLAnchorElement["target"];
badge?: React.ReactNode;
icon?: SVGComponent;
icon?: SVGComponent | LucideIcon;
child?: NavigationItemType[];
pro?: true;
onlyMobile?: boolean;
@@ -670,6 +674,11 @@ const navigation: NavigationItemType[] = [
icon: FileText,
isCurrent: ({ pathname }) => pathname?.startsWith("/apps/routing-forms/") ?? false,
},
{
name: "Cal.ai",
href: "/ai",
icon: Sparkles,
},
{
name: "workflows",
href: "/workflows",
@@ -84,6 +84,7 @@ export const getEventTypeById = async ({
description: true,
length: true,
isInstantEvent: true,
aiPhoneCallConfig: true,
offsetStart: true,
hidden: true,
locations: true,
+1
View File
@@ -40,6 +40,7 @@ export const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
slotInterval: true,
successRedirectUrl: true,
isInstantEvent: true,
aiPhoneCallConfig: true,
assignAllTeamMembers: true,
recurringEvent: true,
});
+1 -1
View File
@@ -47,7 +47,7 @@
"@tanstack/react-query": "^5.17.15",
"class-variance-authority": "^0.4.0",
"clsx": "^2.0.0",
"lucide-react": "^0.171.0",
"lucide-react": "^0.363.0",
"react-use": "^17.4.2",
"tailwind-merge": "^1.13.2",
"tailwindcss": "^3.3.3",
@@ -45,6 +45,15 @@ export type Event = {
eventName: string;
slug: string;
isInstantEvent: boolean;
aiPhoneCallConfig: {
eventTypeId: number;
enabled: boolean;
generalPrompt: string;
beginMessage: string | null;
yourPhoneNumber: string;
numberToCall: string;
guestName: string;
};
schedulingType: string;
length: number;
locations: string[]; // Define more specifically if possible
@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "AIPhoneCallConfiguration" (
"id" SERIAL NOT NULL,
"eventTypeId" INTEGER NOT NULL,
"generalPrompt" TEXT NOT NULL,
"yourPhoneNumber" TEXT NOT NULL,
"numberToCall" TEXT NOT NULL,
"guestName" TEXT NOT NULL,
"enabled" BOOLEAN NOT NULL DEFAULT false,
"beginMessage" TEXT,
"llmId" TEXT,
CONSTRAINT "AIPhoneCallConfiguration_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "AIPhoneCallConfiguration_eventTypeId_idx" ON "AIPhoneCallConfiguration"("eventTypeId");
-- CreateIndex
CREATE UNIQUE INDEX "AIPhoneCallConfiguration_eventTypeId_key" ON "AIPhoneCallConfiguration"("eventTypeId");
-- AddForeignKey
ALTER TABLE "AIPhoneCallConfiguration" ADD CONSTRAINT "AIPhoneCallConfiguration_eventTypeId_fkey" FOREIGN KEY ("eventTypeId") REFERENCES "EventType"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+38 -21
View File
@@ -76,7 +76,7 @@ model EventType {
profileId Int?
profile Profile? @relation(fields: [profileId], references: [id], onDelete: Cascade)
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId Int?
hashedLink HashedLink?
bookings Booking[]
@@ -86,40 +86,40 @@ model EventType {
eventName String?
customInputs EventTypeCustomInput[]
parentId Int?
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
children EventType[] @relation("managed_eventtype")
parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade)
children EventType[] @relation("managed_eventtype")
/// @zod.custom(imports.eventTypeBookingFields)
bookingFields Json?
timeZone String?
periodType PeriodType @default(UNLIMITED)
periodType PeriodType @default(UNLIMITED)
/// @zod.custom(imports.coerceToDate)
periodStartDate DateTime?
/// @zod.custom(imports.coerceToDate)
periodEndDate DateTime?
periodDays Int?
periodCountCalendarDays Boolean?
lockTimeZoneToggleOnBookingPage Boolean @default(false)
requiresConfirmation Boolean @default(false)
requiresBookerEmailVerification Boolean @default(false)
lockTimeZoneToggleOnBookingPage Boolean @default(false)
requiresConfirmation Boolean @default(false)
requiresBookerEmailVerification Boolean @default(false)
/// @zod.custom(imports.recurringEventType)
recurringEvent Json?
disableGuests Boolean @default(false)
hideCalendarNotes Boolean @default(false)
disableGuests Boolean @default(false)
hideCalendarNotes Boolean @default(false)
/// @zod.min(0)
minimumBookingNotice Int @default(120)
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
minimumBookingNotice Int @default(120)
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
seatsPerTimeSlot Int?
onlyShowFirstAvailableSlot Boolean @default(false)
seatsShowAttendees Boolean? @default(false)
seatsShowAvailabilityCount Boolean? @default(true)
onlyShowFirstAvailableSlot Boolean @default(false)
seatsShowAttendees Boolean? @default(false)
seatsShowAvailabilityCount Boolean? @default(true)
schedulingType SchedulingType?
schedule Schedule? @relation(fields: [scheduleId], references: [id])
schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?
// price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column.
price Int @default(0)
price Int @default(0)
// currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column.
currency String @default("usd")
currency String @default("usd")
slotInterval Int?
/// @zod.custom(imports.EventTypeMetaDataSchema)
metadata Json?
@@ -130,9 +130,10 @@ model EventType {
bookingLimits Json?
/// @zod.custom(imports.intervalLimitsType)
durationLimits Json?
isInstantEvent Boolean @default(false)
assignAllTeamMembers Boolean @default(false)
useEventTypeDestinationCalendarEmail Boolean @default(false)
isInstantEvent Boolean @default(false)
assignAllTeamMembers Boolean @default(false)
useEventTypeDestinationCalendarEmail Boolean @default(false)
aiPhoneCallConfig AIPhoneCallConfiguration?
secondaryEmailId Int?
secondaryEmail SecondaryEmail? @relation(fields: [secondaryEmailId], references: [id], onDelete: Cascade)
@@ -906,6 +907,22 @@ model Workflow {
@@index([teamId])
}
model AIPhoneCallConfiguration {
id Int @id @default(autoincrement())
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
generalPrompt String
yourPhoneNumber String
numberToCall String
guestName String
enabled Boolean @default(false)
beginMessage String?
llmId String?
@@unique([eventTypeId])
@@index([eventTypeId])
}
model WorkflowsOnEventTypes {
id Int @id @default(autoincrement())
workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade)
+18
View File
@@ -1,5 +1,6 @@
import type { Prisma } from "@prisma/client";
import type { UnitTypeLongPlural } from "dayjs";
import { isValidPhoneNumber } from "libphonenumber-js";
import type { TFunction } from "next-i18next";
import z, { ZodNullable, ZodObject, ZodOptional } from "zod";
import type {
@@ -591,6 +592,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
title: true,
description: true,
isInstantEvent: true,
aiPhoneCallConfig: true,
currency: true,
periodDays: true,
position: true,
@@ -688,3 +690,19 @@ export const bookingSeatDataSchema = z.object({
description: z.string().optional(),
responses: bookingResponses,
});
export const AIPhoneSettingSchema = z.object({
yourPhoneNumber: z.string().refine((val) => isValidPhoneNumber(val)),
numberToCall: z.string().refine((val) => isValidPhoneNumber(val)),
guestName: z.string().trim().min(1, {
message: "Please enter Guest Name",
}),
generalPrompt: z.string().trim().min(1, {
message: "Please enter prompt",
}),
beginMessage: z.string().nullable(),
eventTypeId: z.number(),
calApiKey: z.string().trim().min(1, {
message: "Please enter CAL API Key",
}),
});
@@ -8,6 +8,17 @@ export const EventTypeUpdateInput = _EventTypeModel
/** Optional fields */
.extend({
isInstantEvent: z.boolean().optional(),
aiPhoneCallConfig: z
.object({
generalPrompt: z.string(),
enabled: z.boolean(),
beginMessage: z.string().nullable(),
yourPhoneNumber: z.string().default(""),
numberToCall: z.string().default(""),
guestName: z.string().default(""),
})
.optional(),
calAiPhoneScript: z.string().optional(),
customInputs: z.array(customInputSchema).optional(),
destinationCalendar: _DestinationCalendarModel
.pick({
@@ -49,12 +49,21 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
bookingFields,
offsetStart,
secondaryEmailId,
aiPhoneCallConfig,
...rest
} = input;
const eventType = await ctx.prisma.eventType.findUniqueOrThrow({
where: { id },
select: {
aiPhoneCallConfig: {
select: {
generalPrompt: true,
beginMessage: true,
enabled: true,
llmId: true,
},
},
children: {
select: {
userId: true,
@@ -345,6 +354,27 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}
}
if (aiPhoneCallConfig) {
if (aiPhoneCallConfig.enabled) {
await ctx.prisma.aIPhoneCallConfiguration.upsert({
where: {
eventTypeId: id,
},
update: aiPhoneCallConfig,
create: {
...aiPhoneCallConfig,
eventTypeId: id,
},
});
} else if (!aiPhoneCallConfig.enabled && eventType.aiPhoneCallConfig) {
await ctx.prisma.aIPhoneCallConfiguration.delete({
where: {
eventTypeId: id,
},
});
}
}
const updatedEventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
slug: true,
schedulingType: true,
@@ -1,4 +1,5 @@
import { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
import { AIPhoneSettingSchema } from "@calcom/prisma/zod-utils";
import authedProcedure, {
authedAdminProcedure,
@@ -141,4 +142,11 @@ export const viewerOrganizationsRouter = router({
const handler = await importHandler(namespaced("adminDelete"), () => import("./adminDelete.handler"));
return handler(opts);
}),
createPhoneCall: authedProcedure.input(AIPhoneSettingSchema).mutation(async (opts) => {
const handler = await importHandler(
namespaced("createPhoneCall"),
() => import("./createPhoneCall.handler")
);
return handler(opts);
}),
});
@@ -0,0 +1,180 @@
import { z } from "zod";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { handleErrorsJson } from "@calcom/lib/errors";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import type { AIPhoneSettingSchema } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
type CreatePhoneCallProps = {
ctx: {
user: NonNullable<TrpcSessionUser>;
prisma: PrismaClient;
};
input: z.infer<typeof AIPhoneSettingSchema>;
};
const createRetellLLMSchema = z
.object({
llm_id: z.string(),
llm_websocket_url: z.string(),
})
.passthrough();
const getRetellLLMSchema = z
.object({
general_prompt: z.string(),
begin_message: z.string().nullable(),
})
.passthrough();
const createPhoneSchema = z
.object({
call_id: z.string(),
agent_id: z.string(),
})
.passthrough();
const getPhoneNumberSchema = z
.object({
agent_id: z.string(),
})
.passthrough();
export const fetcher = async (endpoint: string, init?: RequestInit | undefined) => {
return fetch(`https://api.retellai.com${endpoint}`, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.RETELL_AI_KEY}`,
"Content-Type": "application/json",
...init?.headers,
},
...init,
}).then(handleErrorsJson);
};
const createPhoneCallHandler = async ({ input, ctx }: CreatePhoneCallProps) => {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: `createPhoneCall:${ctx.user.id}`,
});
const { yourPhoneNumber, numberToCall, guestName, eventTypeId, beginMessage, generalPrompt, calApiKey } =
input;
const aiPhoneCallConfig = await ctx.prisma.aIPhoneCallConfiguration.upsert({
where: {
eventTypeId,
},
update: {
beginMessage,
generalPrompt,
enabled: true,
guestName,
numberToCall,
yourPhoneNumber,
},
create: {
eventTypeId,
beginMessage,
generalPrompt,
enabled: true,
guestName,
numberToCall,
yourPhoneNumber,
},
});
let llmWebSocketUrlToBeUpdated = null;
if (!aiPhoneCallConfig.llmId) {
const createdRetellLLM = await fetcher("/create-retell-llm", {
method: "POST",
body: JSON.stringify({
general_prompt: generalPrompt,
begin_message: beginMessage,
general_tools: [
{
type: "end_call",
name: "end_call",
description: "Hang up the call, triggered only after appointment successfully scheduled.",
},
{
type: "check_availability_cal",
name: "check_availability",
cal_api_key: calApiKey,
event_type_id: eventTypeId,
timezone: ctx.user.timeZone,
},
{
type: "book_appointment_cal",
name: "book_appointment",
cal_api_key: calApiKey,
event_type_id: eventTypeId,
timezone: ctx.user.timeZone,
},
],
}),
}).then(createRetellLLMSchema.parse);
await ctx.prisma.aIPhoneCallConfiguration.update({
where: {
eventTypeId,
},
data: {
llmId: createdRetellLLM.llm_id,
},
});
llmWebSocketUrlToBeUpdated = createdRetellLLM.llm_websocket_url;
} else {
const retellLLM = await fetcher(`/get-retell-llm/${aiPhoneCallConfig.llmId}`).then(
getRetellLLMSchema.parse
);
if (retellLLM.general_prompt !== generalPrompt || retellLLM.begin_message !== beginMessage) {
const updatedRetellLLM = await fetcher(`/update-retell-llm/${aiPhoneCallConfig.llmId}`, {
method: "PATCH",
body: JSON.stringify({
general_prompt: generalPrompt,
}),
}).then(getRetellLLMSchema.parse);
logger.debug("updated Retell LLM", updatedRetellLLM);
llmWebSocketUrlToBeUpdated = updatedRetellLLM.llm_websocket_url;
}
}
if (llmWebSocketUrlToBeUpdated) {
const getPhoneNumberDetails = await fetcher(`/get-phone-number/${yourPhoneNumber}`).then(
getPhoneNumberSchema.parse
);
const updated = await fetcher(`/update-agent/${getPhoneNumberDetails.agent_id}`, {
method: "PATCH",
body: JSON.stringify({
llm_websocket_url: llmWebSocketUrlToBeUpdated,
}),
});
logger.debug("updated Retell Agent", updated);
}
// Create Phone Call
const createPhoneCallRes = await fetcher("/create-phone-call", {
method: "POST",
body: JSON.stringify({
from_number: yourPhoneNumber,
to_number: numberToCall,
retell_llm_dynamic_variables: { name: guestName },
}),
}).then(createPhoneSchema.parse);
logger.debug("Create Call Response", createPhoneCallRes);
return createPhoneCallRes;
};
export default createPhoneCallHandler;
+2 -1
View File
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
import { forwardRef } from "react";
import type { IconType } from "react-icons";
import type { LucideIcon } from "@calcom/ui/components/icon";
import { CheckCircle2, Info, XCircle, AlertTriangle } from "@calcom/ui/components/icon";
export interface AlertProps {
@@ -15,7 +16,7 @@ export interface AlertProps {
iconClassName?: string;
// @TODO: Success and info shouldn't exist as per design?
severity: "success" | "warning" | "error" | "info" | "neutral" | "green";
CustomIcon?: IconType;
CustomIcon?: IconType | LucideIcon;
customIconColor?: string;
}
export const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
+1 -1
View File
@@ -273,7 +273,7 @@ const InstallAppButtonChild = ({
type="button"
disabled={isInstalledTeamOrUser}
key={team.id}
StartIcon={(props) => (
StartIcon={(props: { className?: string }) => (
<Avatar
alt={team.logo || ""}
imageSrc={team.logo || `${WEBAPP_URL}/${team.logo}/avatar.png`} // if no image, use default avatar
+2 -1
View File
@@ -5,6 +5,7 @@ import { GoPrimitiveDot } from "react-icons/go";
import classNames from "@calcom/lib/classNames";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import type { LucideIcon } from "@calcom/ui/components/icon";
export const badgeStyles = cva("font-medium inline-flex items-center justify-center rounded gap-x-1", {
variants: {
@@ -36,7 +37,7 @@ type InferredBadgeStyles = VariantProps<typeof badgeStyles>;
type IconOrDot =
| {
startIcon?: SVGComponent;
startIcon?: SVGComponent | LucideIcon;
withDot?: unknown;
}
| { startIcon?: unknown; withDot?: boolean };
+3 -2
View File
@@ -6,6 +6,7 @@ import React, { forwardRef } from "react";
import classNames from "@calcom/lib/classNames";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import type { LucideIcon } from "@calcom/ui/components/icon";
import { Plus } from "../icon";
import { Tooltip } from "../tooltip";
@@ -17,9 +18,9 @@ export type ButtonBaseProps = {
/** Action that happens when the button is clicked */
onClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
/**Left aligned icon*/
StartIcon?: SVGComponent | React.ElementType;
StartIcon?: SVGComponent | React.ElementType | LucideIcon;
/**Right aligned icon */
EndIcon?: SVGComponent;
EndIcon?: SVGComponent | LucideIcon;
shallow?: boolean;
/**Tool tip used when icon size is set to small */
tooltip?: string;
@@ -1,9 +1,10 @@
import React from "react";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import type { LucideIcon } from "@calcom/ui/components/icon";
interface LinkIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
Icon: SVGComponent;
Icon: SVGComponent | LucideIcon;
}
export default function LinkIconButton(props: LinkIconButtonProps) {
@@ -113,7 +113,7 @@ export function CreateButton(props: CreateBtnProps) {
<DropdownItem
type="button"
data-testid={`option${option.teamId ? "-team" : ""}-${idx}`}
StartIcon={(props) => (
StartIcon={(props: { className?: string }) => (
<Avatar alt={option.label || ""} imageSrc={option.image} size="sm" {...props} />
)}
onClick={() =>
+2 -1
View File
@@ -9,6 +9,7 @@ import classNames from "@calcom/lib/classNames";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import type { LucideIcon } from "@calcom/ui/components/icon";
import type { ButtonProps } from "../../components/button";
import { Button } from "../../components/button";
@@ -85,7 +86,7 @@ type DialogContentProps = React.ComponentProps<(typeof DialogPrimitive)["Content
description?: string | JSX.Element | null;
closeText?: string;
actionDisabled?: boolean;
Icon?: SVGComponent;
Icon?: SVGComponent | LucideIcon;
enableOverflow?: boolean;
};
@@ -6,6 +6,7 @@ import { forwardRef } from "react";
import { classNames } from "@calcom/lib";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import { CheckCircle } from "@calcom/ui/components/icon";
import type { LucideIcon } from "@calcom/ui/components/icon";
import type { ButtonColor } from "../../button/Button";
@@ -105,8 +106,8 @@ DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem";
type DropdownItemProps = {
children: React.ReactNode;
color?: ButtonColor;
StartIcon?: SVGComponent;
EndIcon?: SVGComponent;
StartIcon?: SVGComponent | LucideIcon;
EndIcon?: SVGComponent | LucideIcon;
href?: string;
disabled?: boolean;
childrenClassName?: string;
@@ -4,6 +4,7 @@ import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useUrlMatchesCurrentUrl } from "@calcom/lib/hooks/useUrlMatchesCurrentUrl";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import type { LucideIcon } from "@calcom/ui/components/icon";
import { Avatar } from "../../avatar";
import { SkeletonText } from "../../skeleton";
@@ -16,7 +17,7 @@ export type HorizontalTabItemProps = {
href: string;
linkShallow?: boolean;
linkScroll?: boolean;
icon?: SVGComponent;
icon?: SVGComponent | LucideIcon;
avatar?: string;
};
@@ -5,6 +5,7 @@ import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useUrlMatchesCurrentUrl } from "@calcom/lib/hooks/useUrlMatchesCurrentUrl";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import type { LucideIcon } from "@calcom/ui/components/icon";
import { ChevronRight, ExternalLink } from "../../icon";
import { Skeleton } from "../../skeleton";
@@ -12,7 +13,7 @@ import { Skeleton } from "../../skeleton";
export type VerticalTabItemProps = {
name: string;
info?: string;
icon?: SVGComponent;
icon?: SVGComponent | LucideIcon;
disabled?: boolean;
children?: VerticalTabItemProps[];
textClassNames?: string;
+1 -1
View File
@@ -42,7 +42,7 @@
"cmdk": "^0.2.0",
"cmk": "^0.1.1",
"downshift": "^6.1.9",
"lucide-react": "^0.171.0",
"lucide-react": "^0.363.0",
"next": "^13.5.4",
"next-seo": "^6.0.0",
"react": "^18.2.0",
+1
View File
@@ -340,6 +340,7 @@
"RAILWAY_STATIC_URL",
"RENDER_EXTERNAL_URL",
"RESERVED_SUBDOMAINS",
"RETELL_AI_KEY",
"SALESFORCE_CONSUMER_KEY",
"SALESFORCE_CONSUMER_SECRET",
"SAML_ADMINS",
+8 -8
View File
@@ -4038,7 +4038,7 @@ __metadata:
"@vitejs/plugin-react": ^2.2.0
class-variance-authority: ^0.4.0
clsx: ^2.0.0
lucide-react: ^0.171.0
lucide-react: ^0.363.0
react-use: ^17.4.2
rollup-plugin-node-builtins: ^2.1.2
tailwind-merge: ^1.13.2
@@ -5168,7 +5168,7 @@ __metadata:
cmdk: ^0.2.0
cmk: ^0.1.1
downshift: ^6.1.9
lucide-react: ^0.171.0
lucide-react: ^0.363.0
next: ^13.5.4
next-seo: ^6.0.0
react: ^18.2.0
@@ -5446,7 +5446,7 @@ __metadata:
i18n-unused: ^0.13.0
iframe-resizer-react: ^1.1.0
keen-slider: ^6.8.0
lucide-react: ^0.171.0
lucide-react: ^0.363.0
micro: ^10.0.1
next: ^14.1
next-auth: ^4.22.1
@@ -20073,7 +20073,7 @@ __metadata:
jest-diff: ^29.5.0
jsdom: ^22.0.0
lint-staged: ^12.5.0
lucide-react: ^0.171.0
lucide-react: ^0.363.0
mailhog: ^4.16.0
next-router-mock: ^0.9.12
node-ical: ^0.16.1
@@ -32846,12 +32846,12 @@ __metadata:
languageName: node
linkType: hard
"lucide-react@npm:^0.171.0":
version: 0.171.0
resolution: "lucide-react@npm:0.171.0"
"lucide-react@npm:^0.363.0":
version: 0.363.0
resolution: "lucide-react@npm:0.363.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
checksum: 768ffe368c52a518ee339203d86ff4479989ab4d79c0716f721900c4bb7392ef6ff7a14807f6a685abd74d27f4c1778170bff77a0ab4c3e06c17944b557d8300
checksum: abe8fad469a2f14181eca6f3682403c5d53a4a354f333b0f7d3b575471d451c6f3fd36f59d96745a5a823e4cc55eeb2dcda70774a6fe438834ea3fb6fa5b75af
languageName: node
linkType: hard