Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
5cac626
feat: cal.ai self serve
Udit-takkar Jun 15, 2025
e38f713
chore: progress
Udit-takkar Jun 16, 2025
f52c497
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jun 24, 2025
b6be1fd
fix: form
Udit-takkar Jun 24, 2025
e7a61c4
fix: links[endpoint] error
Udit-takkar Jun 25, 2025
d2ad1cf
chore: form
Udit-takkar Jun 26, 2025
ca041da
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jun 26, 2025
049c1bb
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 2, 2025
3b9cf80
feat: finish setup
Udit-takkar Jul 3, 2025
def1186
chore: fix
Udit-takkar Jul 3, 2025
8a5967b
fix: trpc hang bug
Udit-takkar Jul 4, 2025
b1c0524
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 4, 2025
436301c
chore: save progress
Udit-takkar Jul 4, 2025
e9c3c05
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 8, 2025
254a2e0
feat: add phone number billing
Udit-takkar Jul 9, 2025
807d515
feat: retell ai webhook
Udit-takkar Jul 9, 2025
6771fa7
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 9, 2025
39cd886
fix: type error
Udit-takkar Jul 9, 2025
a27a800
feat: remove api key input
Udit-takkar Jul 9, 2025
9676ef3
feat: add delete
Udit-takkar Jul 10, 2025
fb852c2
feat: add delete logic and assign phone number
Udit-takkar Jul 10, 2025
6d06d74
refactor: use design pattern
Udit-takkar Jul 10, 2025
7106fca
chore: remove comment
Udit-takkar Jul 10, 2025
bf7b9c9
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 11, 2025
610bd75
fix: type errror
Udit-takkar Jul 11, 2025
95f82aa
refactor: decouple retell ai
Udit-takkar Jul 11, 2025
b79c4b5
fix: name
Udit-takkar Jul 14, 2025
130e38c
chore: comment unued
Udit-takkar Jul 14, 2025
7ff4667
fix: type errors
Udit-takkar Jul 14, 2025
d29eeb9
fix: type error
Udit-takkar Jul 14, 2025
e6700d2
feat: import phone number functionality
Udit-takkar Jul 15, 2025
47d4ad7
Merge branch 'main' into feat/cal-ai-self-serve
Udit-takkar Jul 16, 2025
cdaeae4
feat: wwebhook handler
Udit-takkar Jul 17, 2025
8791002
fix: type errors
Udit-takkar Jul 17, 2025
d9baa63
fix: type error
Udit-takkar Jul 17, 2025
443fa77
fix: 404 new trpc endpoint bug
Udit-takkar Jul 17, 2025
82cca61
fix: impor
Udit-takkar Jul 18, 2025
b1b9aac
chore
Udit-takkar Jul 18, 2025
9be39b8
feat: new version (wip)
Udit-takkar Jul 28, 2025
0754d7b
chore: improvements
Udit-takkar Jul 29, 2025
2701675
tests: add unit tests
Udit-takkar Jul 29, 2025
470f034
fix: types
Udit-takkar Jul 30, 2025
20bcd41
feat: new desing
Udit-takkar Jul 31, 2025
0b9f2b8
refactor: move everything to service
Udit-takkar Jul 31, 2025
351c4db
tests: add unit tests for service and client
Udit-takkar Jul 31, 2025
0ad600a
fix: deleting cal ai action and workflow
Udit-takkar Jul 31, 2025
b0d50a7
chore: update
Udit-takkar Jul 31, 2025
e7a804d
refactor: improve code
Udit-takkar Aug 1, 2025
9b9ef44
chore: use new app route
Udit-takkar Aug 1, 2025
326621a
fix: infinite rendering bug
Udit-takkar Aug 1, 2025
feb6236
feat: add team support completely
Udit-takkar Aug 1, 2025
1df875e
chore: remove old design and unused
Udit-takkar Aug 4, 2025
3146566
perf: improve agent repository query
Udit-takkar Aug 4, 2025
f50049e
perf: improve query
Udit-takkar Aug 4, 2025
faebeb1
chore: improvements and rate limiting
Udit-takkar Aug 4, 2025
4c62e65
fix: credit reposiotory
Udit-takkar Aug 4, 2025
8d8ff2d
fix: update unit tests and type error
Udit-takkar Aug 4, 2025
62bbd3f
fix: type
Udit-takkar Aug 4, 2025
f9886a2
chore: types
Udit-takkar Aug 4, 2025
5253927
fix: use types from retell sdk
Udit-takkar Aug 4, 2025
9301519
chore: remove PhoneData
Udit-takkar Aug 4, 2025
81f0145
chore: code improvements
Udit-takkar Aug 4, 2025
e97d2ed
chore: code improvements
Udit-takkar Aug 5, 2025
c1791b8
fix: tests and types
Udit-takkar Aug 5, 2025
b34588e
chore: improve UI
Udit-takkar Aug 5, 2025
a34a25e
chore: type error
Udit-takkar Aug 5, 2025
fe7ec30
fix: improvements and i18n
Udit-takkar Aug 5, 2025
69f1ec1
chore: remove unused
Udit-takkar Aug 5, 2025
c153c8b
chore: more i18n
Udit-takkar Aug 5, 2025
b9c2de2
chore: i18n
Udit-takkar Aug 5, 2025
fcfb4f6
fix: type err
Udit-takkar Aug 5, 2025
13ba93a
chore: missing check
Udit-takkar Aug 5, 2025
990c9c8
chore: update docs
Udit-takkar Aug 5, 2025
85878ec
nit
Udit-takkar Aug 5, 2025
8d0d762
chore: i18n, other improvements
Udit-takkar Aug 5, 2025
265e536
fix: formatting
Udit-takkar Aug 5, 2025
70c18b8
fix: type
Udit-takkar Aug 5, 2025
953001f
refactor: stripe related webhooks
Udit-takkar Aug 6, 2025
49bd22a
Merge branch 'main' into feat/cal-ai-self-serve
emrysal Aug 6, 2025
2cdbfa7
chore: imporovements
Udit-takkar Aug 7, 2025
2f52003
fix: types
Udit-takkar Aug 7, 2025
15ddc3d
chore
Udit-takkar Aug 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const getTabs = (orgBranding: OrganizationBranding | null) => {
{ name: "appearance", href: "/settings/my-account/appearance" },
{ name: "out_of_office", href: "/settings/my-account/out-of-office" },
{ name: "push_notifications", href: "/settings/my-account/push-notifications" },
{ name: "phone_numbers", href: "/settings/my-account/phone-numbers" },
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Create smaller PRs

// TODO
// { name: "referrals", href: "/settings/my-account/referrals" },
],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { _generateMetadata } from "app/_utils";
import { unstable_cache } from "next/cache";
import { revalidatePath } from "next/cache";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";

import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { PhoneNumberRepository } from "@calcom/lib/server/repository/phoneNumber";

import { buildLegacyRequest } from "@lib/buildLegacyCtx";

import PhoneNumbersQueryView from "~/settings/my-account/phone-numbers-view";

export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("cal_ai_phone_numbers"),
(t) => t("cal_ai_phone_numbers_description"),
undefined,
undefined,
"/settings/my-account/phone-numbers"
);

const getCachedPhoneNumbers = unstable_cache(
async (userId: number) => {
return await PhoneNumberRepository.findPhoneNumbersFromUserId({ userId });
},
undefined,
{ revalidate: 3600, tags: ["viewer.phoneNumbers.list"] } // Cache for 1 hour
);

const Page = async () => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });

if (!session) {
redirect("/auth/login?callbackUrl=/settings/my-account/phone-numbers");
}

const userId = session.user.id;
const revalidatePage = async () => {
"use server";
revalidatePath("settings/my-account/phone-numbers");
};

const cachedNumbers = await getCachedPhoneNumbers(userId);

return <PhoneNumbersQueryView revalidatePage={revalidatePage} cachedNumbers={cachedNumbers} />;
};

export default Page;
133 changes: 133 additions & 0 deletions apps/web/modules/settings/my-account/phone-numbers-view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use client";

import { useState } from "react";

import { Dialog } from "@calcom/features/components/controlled-dialog";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
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 { DialogContent } from "@calcom/ui/components/dialog";
import { DialogFooter } from "@calcom/ui/components/dialog";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Icon } from "@calcom/ui/components/icon";
import { SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
import { showToast } from "@calcom/ui/components/toast";

const SkeletonLoader = () => {
return (
<SkeletonContainer>
<div className="border-subtle space-y-6 rounded-b-xl border border-t-0 px-4 py-8 sm:px-6">
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonText className="h-8 w-full" />
<SkeletonButton className="ml-auto h-8 w-20 rounded-md p-5" />
</div>
</SkeletonContainer>
);
};

function PhoneNumbersView({
numbers = [],
revalidatePage,
}: {
numbers?: RouterOutputs["viewer"]["phoneNumbers"]["list"];
revalidatePage: () => Promise<void>;
}) {
const { t } = useLocale();
const [isBuyDialogOpen, setIsBuyDialogOpen] = useState(false);
const utils = trpc.useContext();

const buyNumberMutation = trpc.viewer.phoneNumbers.buy.useMutation({
onSuccess: async () => {
showToast(t("phone_number_purchased_successfully"), "success");
await utils.viewer.phoneNumbers.list.invalidate();
await utils.viewer.me.invalidate();
await revalidatePage();
setIsBuyDialogOpen(false);
},
onError: (error) => {
console.log("error", error);
showToast(error.message, "error");
},
});

const BuyNumberButton = (props: React.ComponentProps<typeof Button>) => (
<Button {...props} color="secondary" StartIcon={Icon.Plus} onClick={() => setIsBuyDialogOpen(true)}>
{t("buy_number")}
</Button>
);

return (
<>
<SettingsHeader
title={t("cal_ai_phone_numbers")}
description={t("cal_ai_phone_numbers_description")}
CTA={<BuyNumberButton />}
borderInShellHeader={true}>
<div>
{numbers.length > 0 ? (
<div className="border-subtle rounded-b-lg border border-t-0">
{numbers.map((number, index) => (
<div
key={number.id}
className={`flex items-center justify-between p-6 ${
numbers.length !== index + 1 ? "border-subtle border-b" : ""
}`}>
<div className="flex items-center">
<Icon.Phone className="h-6 w-6 text-gray-400" />
<span className="text-emphasis ml-4 text-sm font-medium">{number.phoneNumber}</span>
</div>
</div>
))}
</div>
) : (
<EmptyScreen
Icon={Icon.Phone}
headline={t("no_phone_numbers_yet")}
description={t("buy_your_first_phone_number")}
className="rounded-b-lg rounded-t-none border-t-0"
buttonRaw={<BuyNumberButton />}
/>
)}
</div>
</SettingsHeader>
<Dialog open={isBuyDialogOpen} onOpenChange={setIsBuyDialogOpen}>
<DialogContent type="creation">
<div className="flex flex-col">
<div className="mb-4">
<h3 className="text-emphasis text-lg font-bold">{t("buy_new_number")}</h3>
<p className="text-default text-sm">{t("buy_number_cost_50_credits")}</p>
</div>
<DialogFooter showDivider className="relative">
<Button onClick={() => setIsBuyDialogOpen(false)} color="secondary">
{t("cancel")}
</Button>
<Button onClick={() => buyNumberMutation.mutate()} loading={buyNumberMutation.isPending}>
{t("buy_number_with_credits")}
</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
</>
);
}

export default function PhoneNumbersQueryView({
cachedNumbers,
revalidatePage,
}: {
cachedNumbers: RouterOutputs["viewer"]["phoneNumbers"]["list"];
revalidatePage: () => Promise<void>;
}) {
const { t } = useLocale();
const { data: numbers, isPending } = trpc.viewer.phoneNumbers.list.useQuery(undefined, {
suspense: false,
});

if (isPending && !cachedNumbers) return <SkeletonLoader />;

return <PhoneNumbersView numbers={numbers || cachedNumbers} revalidatePage={revalidatePage} />;
}
25 changes: 25 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,24 @@
"verify_email_subject": "{{appName}}: Verify your account",
"verify_email_subject_verifying_email": "{{appName}}: Verify your email",
"check_your_email": "Check your email",
"assigned_phone_number": "Assigned Phone Number",
"this_is_the_number_your_agent_will_use": "This is the number your agent will use",
"buy_and_assign_number": "Buy and Assign Number",
"configure_agent": "Configure Agent",
"configure_agent_subtitle": "Configure your agent to make outbound calls",
"save_and_test_call": "Save and Test Call",
"old_email_address": "Old Email",
"new_email_address": "New Email",
"verify_email_page_body": "We've sent an email to {{email}}. It is important to verify your email address to guarantee the best email and calendar deliverability from {{appName}}.",
"verify_email_banner_body": "Verify your email address to guarantee the best email and calendar deliverability",
"verify_email_email_header": "Verify your email address",
"verify_email_button": "Verify email",
"cal_ai_assistant": "Cal AI Assistant",
"setup_ai_phone_assistant": "Setup AI Phone Assistant",
"provide_api_key_and_timezone_to_setup": "Provide a Cal.com API key and agent's timezone to setup your AI Phone Assistant",
"cal_api_key": "Cal.com API key",
"agent_timezone": "Agent's Timezone",
"agent_timezone_description": "The timezone of the agent. This is used to schedule events in the agent's timezone.",
"send_cal_video_transcription_emails": "Send Cal Video Transcription Emails",
"description_send_cal_video_transcription_emails": "Send emails with the transcription of the Cal Video after the meeting ends. (Requires a paid plan)",
"verify_email_change_description": "You have recently requested to change the email address you use to log into your {{appName}} account. Please click the button below to confirm your new email address.",
Expand Down Expand Up @@ -1255,6 +1266,7 @@
"make_setup_instructions_3": "Select Cal.com as your Trigger app. Also choose a Trigger event.",
"make_setup_instructions_4": "Choose your account and then enter your Unique API Key.",
"make_setup_instructions_5": "Test your Trigger.",
"choose_phone_number": "Choose a phone number",
"make_setup_instructions_6": "You're set!",
"install_zapier_app": "Please first install the Zapier App in the app store.",
"install_make_app": "Please first install the Make App in the app store.",
Expand Down Expand Up @@ -1707,6 +1719,11 @@
"booker_booking_limit_description": "Limit the number of active bookings a booker can make for this event type",
"recurring_event_doesnt_support_booker_booking_limit": "Recurring events don't support booker booking limit",
"add_limit": "Add Limit",
"phone_numbers": "Phone Numbers",
"cal_ai_phone_numbers": "Cal AI Phone Numbers",
"cal_ai_phone_numbers_description": "Manage your Cal AI Phone Numbers",
"ai_self_serve_tab_title": "AI Self Serve",
"ai_self_serve_tab_description": "Create and manage AI-powered event types with ease",
"team_name_required": "Team name required",
"show_attendees": "Share attendee information between guests",
"show_available_seats_count": "Show the number of available seats",
Expand Down Expand Up @@ -3043,8 +3060,16 @@
"desc": "Desc",
"verify_email_change": "Verify email change",
"buy_credits": "Buy Credits",
"buy_number": "Buy Number",
"no_phone_numbers_yet": "No phone numbers yet",
"buy_your_first_phone_number": "Buy your first phone number to get started",
"credits": "Credits",
"view_and_manage_credits": "View and manage credits",
"buy_new_number": "Buy New Number",
"buy_number_cost_50_credits": "Buying a phone number costs 50 credits. You can buy additional credits in the <0>Credits</0> section.",
"area_code_optional": "Area Code (Optional)",
"buy_number_with_credits": "Buy Number with Credits",
"phone_number_purchased_successfully": "Phone number purchased successfully",
"view_and_manage_credits_description": "View and manage credits for sending SMS messages. One credit is worth 1¢ (USD). <0>Learn more</0>",
"buy_additional_credits": "Buy additional credits ($0.01 per credit)",
"overview": "Overview",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createSelfServePhoneCall } from "@calcom/features/ee/cal-ai-phone/retellAIService";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { prisma } from "@calcom/prisma";

import { TRPCError } from "@trpc/server";

export const handleCreateSelfServePhoneCall = async ({
userId,
eventTypeId,
numberToCall,
}: {
userId: number;
eventTypeId: number;
numberToCall: string;
}) => {
await checkRateLimitAndThrowError({
rateLimitingType: "core",
identifier: `create-self-serve-phone-call:${userId}`,
});

const config = await prisma.aISelfServeConfiguration.findFirst({
where: {
eventTypeId: eventTypeId,
eventType: {
userId: userId,
},
},
include: {
yourPhoneNumber: true,
},
});

console.log("config", config);
if (!config || !config.yourPhoneNumber) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "AI not configured for this event type or no phone number is assigned.",
});
}

const fromNumber = config.yourPhoneNumber.phoneNumber;
const call = await createSelfServePhoneCall(fromNumber, numberToCall);

return call;
};
Loading
Loading