From a8a3e48465eeb4276a6a4fc69d8701ab6e2e20c1 Mon Sep 17 00:00:00 2001 From: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Date: Wed, 10 Sep 2025 21:11:10 +0530 Subject: [PATCH] feat: add web phone call for cal ai (#23527) * feat: add web phone call for cal ai * fix: web call * refactor: improvements * chore * refactor: web call improvements * chore: error message * refactor: improvements * fix: err * refactor: feedback and improvements * fix: type errors * refactor: hasAvailable * chore: use orgid * fix: types --------- Co-authored-by: Peer Richelsen --- apps/web/package.json | 1 + apps/web/public/icons/sprite.svg | 22 + apps/web/public/static/locales/en/common.json | 25 + .../interfaces/AIPhoneService.interface.ts | 15 + .../retellAI/RetellAIPhoneServiceProvider.ts | 14 + .../providers/retellAI/RetellAIService.ts | 10 + .../providers/retellAI/RetellSDKClient.ts | 14 + .../retellAI/services/CallService.ts | 87 ++++ .../calAIPhone/providers/retellAI/types.ts | 8 + .../components/AgentConfigurationSheet.tsx | 59 ++- ...gentDialog.tsx => TestPhoneCallDialog.tsx} | 7 +- .../ee/workflows/components/WebCallDialog.tsx | 477 ++++++++++++++++++ .../components/WorkflowStepContainer.tsx | 87 +++- .../routers/viewer/aiVoiceAgent/_router.ts | 11 + .../aiVoiceAgent/createWebCall.handler.ts | 58 +++ .../aiVoiceAgent/createWebCall.schema.ts | 9 + .../server/routers/viewer/credits/_router.tsx | 19 + .../credits/hasAvailableCredits.handler.ts | 25 + .../credits/hasAvailableCredits.schema.ts | 7 + packages/ui/components/icon/icon-list.mjs | 4 + packages/ui/components/icon/icon-names.ts | 4 + yarn.lock | 124 ++++- 22 files changed, 1043 insertions(+), 44 deletions(-) rename packages/features/ee/workflows/components/{TestAgentDialog.tsx => TestPhoneCallDialog.tsx} (91%) create mode 100644 packages/features/ee/workflows/components/WebCallDialog.tsx create mode 100644 packages/trpc/server/routers/viewer/aiVoiceAgent/createWebCall.handler.ts create mode 100644 packages/trpc/server/routers/viewer/aiVoiceAgent/createWebCall.schema.ts create mode 100644 packages/trpc/server/routers/viewer/credits/hasAvailableCredits.handler.ts create mode 100644 packages/trpc/server/routers/viewer/credits/hasAvailableCredits.schema.ts diff --git a/apps/web/package.json b/apps/web/package.json index 94909a2899..064c1c75b9 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -139,6 +139,7 @@ "react-use-intercom": "1.5.1", "recoil": "^0.7.7", "remove-markdown": "^0.5.0", + "retell-client-js-sdk": "^2.0.0", "retell-sdk": "^4.40.0", "rrule": "^2.7.1", "sanitize-html": "^2.10.0", diff --git a/apps/web/public/icons/sprite.svg b/apps/web/public/icons/sprite.svg index 76b7741690..beefee2cdd 100644 --- a/apps/web/public/icons/sprite.svg +++ b/apps/web/public/icons/sprite.svg @@ -478,6 +478,24 @@ + + + + + + + + + + + + + + + + + + @@ -498,6 +516,10 @@ + + + + diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index d90113fd23..10c83be67e 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -860,6 +860,7 @@ "phone_number_for_ai_call": "Phone number (for our agent to call)", "buy_a_phone_number_or_import_one_you_already_have": "Buy a phone number or import one you already have on twilio", "test_agent": "Test Agent", + "test_web_call": "Test Web Call", "phone_numbers": "Phone Numbers", "unsubscribe": "Unsubscribe", "delete_workflow_step": "Delete Workflow Step", @@ -1479,6 +1480,24 @@ "app_removed_successfully": "App removed successfully", "error_removing_app": "Error removing app", "web_conference": "Web conference", + "web_call": "Web call", + "start_web_call": "Start web call", + "end_call": "End call", + "test_your_agent_with_web_call": "Test your agent with a web call without requiring a phone number", + "phone_call": "Phone call", + "connecting_to_agent": "Connecting to agent...", + "call_active": "Call active", + "call_ended": "Call ended", + "call_error": "Call error", + "ready_to_start_call": "Ready to start call", + "failed_to_start_web_call_try_again": "Failed to start web call. Please try again.", + "call_encountered_error_try_again": "Call encountered an error. Please try again.", + "failed_initialize_web_call_microphone_permissions": "Failed to initialize web call. Please ensure microphone permissions are granted.", + "start_call_to_see_conversation": "Start a call to see the conversation", + "waiting_for_conversation": "Waiting for conversation...", + "ai_agent": "AI Agent", + "live": "Live", + "connecting": "Connecting...", "requires_confirmation": "Requires confirmation", "requires_confirmation_threshold": "Requires confirmation if booked with < {{time}} $t({{unit}}_timeUnit) notice", "may_require_confirmation": "May require confirmation", @@ -1714,6 +1733,7 @@ "transcription_enabled": "Transcriptions are enabled now", "transcription_stopped": "Transcriptions are stopped now", "download_transcript": "Download Transcript", + "transcript": "Transcript", "recording_from_your_recent_call": "A recording from your recent call on {{appName}} is ready for download", "transcript_from_previous_call": "Transcript from your recent call on {{appName}} is ready to download. Links are valid only for 1 Hour", "you_can_download_transcript_from_attachments": "You can also download transcript from attachments", @@ -3622,6 +3642,11 @@ "send_another_message": "Send another message", "sending": "Sending", "no_variables_found": "No variables found", + "credits_required": "Credits Required", + "web_call_credits_info": "Web calls consume credits. Make sure you have enough credits to start a call.", + "web_call_no_credits": "Web calls require credits. You have 0 credits remaining.", + "purchase_credits": "Purchase Credits", + "credits_required_tooltip": "This action requires credits to proceed", "api_key_name_too_long": "Name must not exceed {{max}} characters", "auto_charge_for_last_minute_cancellation": "Auto charge no-show fee for last minute cancellations", "before_scheduled_start_time": "Before scheduled start time", diff --git a/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts b/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts index 86f0f7b46c..fb0bc3ae60 100644 --- a/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts +++ b/packages/features/calAIPhone/interfaces/AIPhoneService.interface.ts @@ -327,6 +327,21 @@ export interface AIPhoneServiceProvider; + /** + * Create a web call + */ + createWebCall(params: { + agentId: string; + userId: number; + teamId?: number; + timeZone: string; + eventTypeId: number; + }): Promise<{ + callId: string; + accessToken: string; + agentId: string; + }>; + /** * Update tools from event type ID */ diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts index 5b5474da5c..00784dd7e1 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellAIPhoneServiceProvider.ts @@ -266,6 +266,20 @@ export class RetellAIPhoneServiceProvider return await this.service.createTestCall(params); } + async createWebCall(params: { + agentId: string; + userId: number; + teamId?: number; + timeZone: string; + eventTypeId: number; + }): Promise<{ + callId: string; + accessToken: string; + agentId: string; + }> { + return await this.service.createWebCall(params); + } + async updateToolsFromAgentId( agentId: string, data: { eventTypeId: number | null; timeZone: string; userId: number | null; teamId?: number | null } diff --git a/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts b/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts index 4852357cba..8390436ec0 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellAIService.ts @@ -220,6 +220,16 @@ export class RetellAIService { return this.callService.createTestCall(params); } + async createWebCall(params: { + agentId: string; + userId: number; + teamId?: number; + timeZone: string; + eventTypeId: number; + }) { + return this.callService.createWebCall(params); + } + async generatePhoneNumberCheckoutSession(params: { userId: number; teamId?: number; diff --git a/packages/features/calAIPhone/providers/retellAI/RetellSDKClient.ts b/packages/features/calAIPhone/providers/retellAI/RetellSDKClient.ts index 2f88b98c02..be1526051e 100644 --- a/packages/features/calAIPhone/providers/retellAI/RetellSDKClient.ts +++ b/packages/features/calAIPhone/providers/retellAI/RetellSDKClient.ts @@ -12,6 +12,7 @@ import type { RetellAgent, CreatePhoneNumberParams, CreatePhoneCallParams, + CreateWebCallParams, ImportPhoneNumberParams, } from "./types"; @@ -234,4 +235,17 @@ export class RetellSDKClient implements RetellAIRepository { throw error; } } + + async createWebCall(data: CreateWebCallParams) { + try { + const response = await this.client.call.createWebCall({ + agent_id: data.agentId, + retell_llm_dynamic_variables: data.dynamicVariables, + }); + return response; + } catch (error) { + this.logger.error("Failed to create web call", { error }); + throw error; + } + } } diff --git a/packages/features/calAIPhone/providers/retellAI/services/CallService.ts b/packages/features/calAIPhone/providers/retellAI/services/CallService.ts index 232696d9b5..966d4d0a1d 100644 --- a/packages/features/calAIPhone/providers/retellAI/services/CallService.ts +++ b/packages/features/calAIPhone/providers/retellAI/services/CallService.ts @@ -166,6 +166,93 @@ export class CallService { }; } + async createWebCall({ + agentId, + userId, + teamId, + timeZone, + eventTypeId, + }: { + agentId: string; + userId: number; + teamId?: number; + timeZone: string; + eventTypeId: number; + }) { + if (!agentId?.trim()) { + throw new HttpError({ + statusCode: 400, + message: "Agent ID is required and cannot be empty", + }); + } + + await this.validateCreditsForTestCall({ userId, teamId }); + + await checkRateLimitAndThrowError({ + rateLimitingType: "core", + identifier: `web-call:${userId}`, + }); + + const agent = await this.agentRepository.findByIdWithCallAccess({ + id: agentId, + userId, + }); + + if (!agent) { + throw new HttpError({ + statusCode: 404, + message: "Agent not found or you don't have permission to use it.", + }); + } + + if (!agent.providerAgentId) { + throw new HttpError({ + statusCode: 400, + message: "Agent provider ID not found.", + }); + } + + const dynamicVariables = { + EVENT_NAME: "Web Call Test with Agent", + EVENT_DATE: "Monday, January 15, 2025", + EVENT_TIME: "2:00 PM", + EVENT_END_TIME: "2:30 PM", + TIMEZONE: timeZone, + LOCATION: "Web Call", + ORGANIZER_NAME: "Cal.com AI Agent", + ATTENDEE_NAME: "Test User", + ATTENDEE_FIRST_NAME: "Test", + ATTENDEE_LAST_NAME: "User", + ATTENDEE_EMAIL: "testuser@example.com", + ATTENDEE_TIMEZONE: timeZone, + ADDITIONAL_NOTES: "This is a test web call to verify the AI phone agent", + EVENT_START_TIME_IN_ATTENDEE_TIMEZONE: "2:00 PM", + EVENT_END_TIME_IN_ATTENDEE_TIMEZONE: "2:30 PM", + eventTypeId: eventTypeId.toString(), + }; + + try { + const webCall = await this.retellRepository.createWebCall({ + agentId: agent.providerAgentId, + dynamicVariables, + }); + return { + callId: webCall.call_id, + accessToken: webCall.access_token, + agentId: webCall.agent_id, + }; + } catch (error) { + this.logger.error("Failed to create web call in external AI service", { + agentId: agent.providerAgentId, + error, + }); + throw new HttpError({ + statusCode: 500, + message: "Failed to create web call", + }); + } + } + private async validateCreditsForTestCall({ userId, teamId }: { userId: number; teamId?: number }) { try { const { CreditService } = await import("@calcom/features/ee/billing/credit-service"); diff --git a/packages/features/calAIPhone/providers/retellAI/types.ts b/packages/features/calAIPhone/providers/retellAI/types.ts index f49a427e2d..9cc07ccd30 100644 --- a/packages/features/calAIPhone/providers/retellAI/types.ts +++ b/packages/features/calAIPhone/providers/retellAI/types.ts @@ -86,6 +86,11 @@ export type CreatePhoneCallParams = { toNumber: string; dynamicVariables?: RetellDynamicVariables; }; + +export type CreateWebCallParams = { + agentId: string; + dynamicVariables?: RetellDynamicVariables; +}; export type UpdatePhoneNumberParams = Retell.PhoneNumberUpdateParams; export type ImportPhoneNumberParams = Retell.PhoneNumberImportParams; export type RetellLLMGeneralTools = Retell.LlmCreateParams["general_tools"]; @@ -171,4 +176,7 @@ export interface RetellAIRepository { // Call operations createPhoneCall(data: CreatePhoneCallParams): Promise; + createWebCall( + data: CreateWebCallParams + ): Promise<{ call_id: string; access_token: string; agent_id: string }>; } diff --git a/packages/features/ee/workflows/components/AgentConfigurationSheet.tsx b/packages/features/ee/workflows/components/AgentConfigurationSheet.tsx index f2404c9408..18cdc59e87 100644 --- a/packages/features/ee/workflows/components/AgentConfigurationSheet.tsx +++ b/packages/features/ee/workflows/components/AgentConfigurationSheet.tsx @@ -42,7 +42,8 @@ import { Tooltip } from "@calcom/ui/components/tooltip"; import { DYNAMIC_TEXT_VARIABLES } from "../lib/constants"; import type { FormValues } from "../pages/workflow"; -import { TestAgentDialog } from "./TestAgentDialog"; +import { TestPhoneCallDialog } from "./TestPhoneCallDialog"; +import { WebCallDialog } from "./WebCallDialog"; // Utility functions for prompt display const cleanPromptForDisplay = (prompt: string): string => { @@ -114,6 +115,7 @@ type AgentConfigurationSheetProps = { onUpdate: (data: AgentFormValues) => void; readOnly?: boolean; teamId?: number; + isOrganization?: boolean; workflowId?: string; workflowStepId?: number; activeTab?: "prompt" | "phoneNumber"; @@ -129,6 +131,7 @@ export function AgentConfigurationSheet({ onUpdate, readOnly = false, teamId, + isOrganization = false, workflowId, workflowStepId: _workflowStepId, form, @@ -141,6 +144,7 @@ export function AgentConfigurationSheet({ const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); const [showAdvancedFields, setShowAdvancedFields] = useState(false); const [isTestAgentDialogOpen, setIsTestAgentDialogOpen] = useState(false); + const [isWebCallDialogOpen, setIsWebCallDialogOpen] = useState(false); const [cancellingNumberId, setCancellingNumberId] = useState(null); const [numberToDelete, setNumberToDelete] = useState(null); // const [toolDialogOpen, setToolDialogOpen] = useState(false); @@ -593,13 +597,39 @@ export function AgentConfigurationSheet({
- + + + + + + + { + setIsTestAgentDialogOpen(true); + }}> + {t("phone_call")} + + + + { + setIsWebCallDialogOpen(true); + }}> + {t("web_call")} + + + + + )} +
+ +
+ {transcript.length === 0 ? ( +
+
+ +

+ {callStatus === "idle" + ? t("start_call_to_see_conversation") + : t("waiting_for_conversation")} +

+
+
+ ) : ( + transcript.map((entry, index) => ( +
+
+
+ + {entry.speaker === "agent" ? t("ai_agent") : t("you")} + +
+

{entry.text}

+
+
+ )) + )} +
+
+
+ + {error && callStatus === "error" && ( +
+
+ + {error} +
+
+ )} + + +
+ {callStatus === "active" && ( + + )} + + {callStatus === "idle" || + callStatus === "error" || + callStatus === "ended" || + callStatus === "connecting" ? ( + + + + ) : ( + + )} +
+
+ + + ); +} diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx index 3381e46ade..cb3e5d130c 100644 --- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx +++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx @@ -64,8 +64,9 @@ import emailRatingTemplate from "../lib/reminders/templates/emailRatingTemplate" import emailReminderTemplate from "../lib/reminders/templates/emailReminderTemplate"; import type { FormValues } from "../pages/workflow"; import { AgentConfigurationSheet } from "./AgentConfigurationSheet"; -import { TestAgentDialog } from "./TestAgentDialog"; +import { TestPhoneCallDialog } from "./TestPhoneCallDialog"; import { TimeTimeUnitInput } from "./TimeTimeUnitInput"; +import { WebCallDialog } from "./WebCallDialog"; type User = RouterOutputs["viewer"]["me"]["get"]; @@ -209,6 +210,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { const verifiedEmails = _verifiedEmails || []; const [isAdditionalInputsDialogOpen, setIsAdditionalInputsDialogOpen] = useState(false); const [isTestAgentDialogOpen, setIsTestAgentDialogOpen] = useState(false); + const [isWebCallDialogOpen, setIsWebCallDialogOpen] = useState(false); const [isUnsubscribeDialogOpen, setIsUnsubscribeDialogOpen] = useState(false); const [isDeleteStepDialogOpen, setIsDeleteStepDialogOpen] = useState(false); @@ -800,26 +802,57 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{arePhoneNumbersActive.length > 0 ? ( - + + + + + + + setIsTestAgentDialogOpen(true)}> + {t("phone_call")} + + + + setIsWebCallDialogOpen(true)}> + {t("web_call")} + + + + ) : ( - + <> + + + )} @@ -1447,6 +1480,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { }} readOnly={props.readOnly} teamId={teamId} + isOrganization={props.isOrganization} workflowId={params?.workflow as string} workflowStepId={step?.id} form={form} @@ -1454,7 +1488,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) { )} {stepAgentId && ( - )} + {stepAgentId && ( + + )} + {/* Unsubscribe Confirmation Dialog */} diff --git a/packages/trpc/server/routers/viewer/aiVoiceAgent/_router.ts b/packages/trpc/server/routers/viewer/aiVoiceAgent/_router.ts index 86e39156bc..e159726872 100644 --- a/packages/trpc/server/routers/viewer/aiVoiceAgent/_router.ts +++ b/packages/trpc/server/routers/viewer/aiVoiceAgent/_router.ts @@ -1,6 +1,8 @@ import authedProcedure from "../../../procedures/authedProcedure"; import { router } from "../../../trpc"; +import { eventOwnerProcedure } from "../eventTypes/util"; import { ZCreateInputSchema } from "./create.schema"; +import { ZCreateWebCallInputSchema } from "./createWebCall.schema"; import { ZDeleteInputSchema } from "./delete.schema"; import { ZGetInputSchema } from "./get.schema"; import { ZListInputSchema } from "./list.schema"; @@ -61,4 +63,13 @@ export const aiVoiceAgentRouter = router({ input, }); }), + + createWebCall: eventOwnerProcedure.input(ZCreateWebCallInputSchema).mutation(async ({ ctx, input }) => { + const { createWebCallHandler } = await import("./createWebCall.handler"); + + return createWebCallHandler({ + ctx, + input, + }); + }), }); diff --git a/packages/trpc/server/routers/viewer/aiVoiceAgent/createWebCall.handler.ts b/packages/trpc/server/routers/viewer/aiVoiceAgent/createWebCall.handler.ts new file mode 100644 index 0000000000..0e7fb6521f --- /dev/null +++ b/packages/trpc/server/routers/viewer/aiVoiceAgent/createWebCall.handler.ts @@ -0,0 +1,58 @@ +import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone"; +import logger from "@calcom/lib/logger"; + +import { TRPCError } from "@trpc/server"; + +import type { TrpcSessionUser } from "../../../types"; +import type { TCreateWebCallInputSchema } from "./createWebCall.schema"; + +type CreateWebCallHandlerOptions = { + ctx: { + user: NonNullable; + }; + input: TCreateWebCallInputSchema; +}; + +export const createWebCallHandler = async ({ ctx, input }: CreateWebCallHandlerOptions) => { + const timeZone = ctx.user.timeZone ?? "Europe/London"; + const eventTypeId = input.eventTypeId; + + const aiService = createDefaultAIPhoneServiceProvider(); + + const agent = await aiService.getAgentWithDetails({ + id: input.agentId, + userId: ctx.user.id, + teamId: input.teamId, + }); + + if (!agent?.providerAgentId) { + logger.error(`Agent not found or access denied for agentId ${input.agentId}, userId ${ctx.user.id}`); + throw new TRPCError({ + code: "FORBIDDEN", + message: "Agent not found or you don't have permission to access this agent", + }); + } + + try { + await aiService.updateToolsFromAgentId(agent.providerAgentId, { + eventTypeId, + timeZone, + userId: ctx.user.id, + teamId: input.teamId, + }); + } catch (error) { + logger.error(`Failed to update tools for agent ${agent.providerAgentId}: ${error}`); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to configure agent for web call", + }); + } + + return await aiService.createWebCall({ + agentId: input.agentId, + userId: ctx.user.id, + teamId: input.teamId, + timeZone, + eventTypeId, + }); +}; diff --git a/packages/trpc/server/routers/viewer/aiVoiceAgent/createWebCall.schema.ts b/packages/trpc/server/routers/viewer/aiVoiceAgent/createWebCall.schema.ts new file mode 100644 index 0000000000..a7af5a2af7 --- /dev/null +++ b/packages/trpc/server/routers/viewer/aiVoiceAgent/createWebCall.schema.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const ZCreateWebCallInputSchema = z.object({ + agentId: z.string().min(1, "Agent ID is required"), + teamId: z.number().optional(), + eventTypeId: z.number().min(1, "Event Type ID is required"), +}); + +export type TCreateWebCallInputSchema = z.infer; diff --git a/packages/trpc/server/routers/viewer/credits/_router.tsx b/packages/trpc/server/routers/viewer/credits/_router.tsx index d871672039..267897a0f5 100644 --- a/packages/trpc/server/routers/viewer/credits/_router.tsx +++ b/packages/trpc/server/routers/viewer/credits/_router.tsx @@ -4,11 +4,13 @@ import { router } from "../../../trpc"; import { ZBuyCreditsSchema } from "./buyCredits.schema"; import { ZDownloadExpenseLogSchema } from "./downloadExpenseLog.schema"; import { ZGetAllCreditsSchema } from "./getAllCredits.schema"; +import { ZHasAvailableCreditsSchema } from "./hasAvailableCredits.schema"; type CreditsCache = { getAllCredits?: typeof import("./getAllCredits.handler").getAllCreditsHandler; buyCredits?: typeof import("./buyCredits.handler").buyCreditsHandler; downloadExpenseLog?: typeof import("./downloadExpenseLog.handler").downloadExpenseLogHandler; + hasAvailableCredits?: typeof import("./hasAvailableCredits.handler").hasAvailableCreditsHandler; }; const UNSTABLE_HANDLER_CACHE: CreditsCache = {}; @@ -31,6 +33,23 @@ export const creditsRouter = router({ input, }); }), + hasAvailableCredits: authedProcedure.input(ZHasAvailableCreditsSchema).query(async ({ input, ctx }) => { + if (!UNSTABLE_HANDLER_CACHE.hasAvailableCredits) { + UNSTABLE_HANDLER_CACHE.hasAvailableCredits = await import("./hasAvailableCredits.handler").then( + (mod) => mod.hasAvailableCreditsHandler + ); + } + + // Unreachable code but required for type safety + if (!UNSTABLE_HANDLER_CACHE.hasAvailableCredits) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.hasAvailableCredits({ + ctx, + input, + }); + }), buyCredits: authedProcedure.input(ZBuyCreditsSchema).mutation(async ({ input, ctx }) => { if (!UNSTABLE_HANDLER_CACHE.buyCredits) { UNSTABLE_HANDLER_CACHE.buyCredits = await import("./buyCredits.handler").then( diff --git a/packages/trpc/server/routers/viewer/credits/hasAvailableCredits.handler.ts b/packages/trpc/server/routers/viewer/credits/hasAvailableCredits.handler.ts new file mode 100644 index 0000000000..548a89812b --- /dev/null +++ b/packages/trpc/server/routers/viewer/credits/hasAvailableCredits.handler.ts @@ -0,0 +1,25 @@ +import type { TrpcSessionUser } from "@calcom/trpc/server/types"; + +import type { THasAvailableCreditsSchema } from "./hasAvailableCredits.schema"; + +type HasAvailableCreditsOptions = { + ctx: { + user: NonNullable; + }; + input: THasAvailableCreditsSchema; +}; + +export const hasAvailableCreditsHandler = async ({ ctx, input }: HasAvailableCreditsOptions) => { + const { teamId } = input; + + const orgId = ctx.user.organization?.id; + + const { CreditService } = await import("@calcom/features/ee/billing/credit-service"); + const creditService = new CreditService(); + + const hasCredits = await creditService.hasAvailableCredits({ + userId: ctx.user.id, + teamId: teamId ?? orgId, + }); + return hasCredits; +}; diff --git a/packages/trpc/server/routers/viewer/credits/hasAvailableCredits.schema.ts b/packages/trpc/server/routers/viewer/credits/hasAvailableCredits.schema.ts new file mode 100644 index 0000000000..fbf29dfccd --- /dev/null +++ b/packages/trpc/server/routers/viewer/credits/hasAvailableCredits.schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ZHasAvailableCreditsSchema = z.object({ + teamId: z.number().optional(), +}); + +export type THasAvailableCreditsSchema = z.infer; diff --git a/packages/ui/components/icon/icon-list.mjs b/packages/ui/components/icon/icon-list.mjs index 51c3428629..694ba7a401 100644 --- a/packages/ui/components/icon/icon-list.mjs +++ b/packages/ui/components/icon/icon-list.mjs @@ -148,4 +148,8 @@ export const lucideIconList = new Set([ "bookmark", "square-pen", "list-filter", + "phone-off", + "mic-off", + "mic", + "monitor", ]); diff --git a/packages/ui/components/icon/icon-names.ts b/packages/ui/components/icon/icon-names.ts index ff6139851c..accd3795ef 100644 --- a/packages/ui/components/icon/icon-names.ts +++ b/packages/ui/components/icon/icon-names.ts @@ -98,11 +98,15 @@ export type IconName = | "menu" | "message-circle" | "messages-square" + | "mic-off" + | "mic" + | "monitor" | "moon" | "paintbrush" | "paperclip" | "pencil" | "phone-call" + | "phone-off" | "phone" | "plus" | "refresh-ccw" diff --git a/yarn.lock b/yarn.lock index cee6bd69bb..6c05002c5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2640,6 +2640,13 @@ __metadata: languageName: node linkType: hard +"@bufbuild/protobuf@npm:^1.10.0": + version: 1.10.1 + resolution: "@bufbuild/protobuf@npm:1.10.1" + checksum: 403838ad278d504e33e72ec0f64ce1bac9f5025ee6396253382e821a1fe0c371a58ffe45a0e8f23306205b6890c5f83e85828168a35dae118915cd4c3d091177 + languageName: node + linkType: hard + "@bundled-es-modules/cookie@npm:^2.0.1": version: 2.0.1 resolution: "@bundled-es-modules/cookie@npm:2.0.1" @@ -4358,6 +4365,7 @@ __metadata: react-use-intercom: 1.5.1 recoil: ^0.7.7 remove-markdown: ^0.5.0 + retell-client-js-sdk: ^2.0.0 retell-sdk: ^4.40.0 rrule: ^2.7.1 sanitize-html: ^2.10.0 @@ -8988,6 +8996,22 @@ __metadata: languageName: node linkType: hard +"@livekit/mutex@npm:1.1.1": + version: 1.1.1 + resolution: "@livekit/mutex@npm:1.1.1" + checksum: 44a31eb7a913357ffb57d04eaa20f7507c0a659638c6dfaba9a413c21a3397aa351497f7c77bca3c06a29ac2cbe83698a5f96b9230012a24b86ac8366e9b8666 + languageName: node + linkType: hard + +"@livekit/protocol@npm:1.39.3": + version: 1.39.3 + resolution: "@livekit/protocol@npm:1.39.3" + dependencies: + "@bufbuild/protobuf": ^1.10.0 + checksum: 72aa3b9e82203ccec039047518d841d65b10dcc11ab9d661f269eab8a8672aed7b2d072f4f09cc6fb94a2579542b0a9216d7f66476d34cd1675c13e1ebcc119f + languageName: node + linkType: hard + "@ljharb/through@npm:^2.3.11": version: 2.3.13 resolution: "@ljharb/through@npm:2.3.13" @@ -33757,6 +33781,25 @@ __metadata: languageName: node linkType: hard +"livekit-client@npm:^2.5.1": + version: 2.15.6 + resolution: "livekit-client@npm:2.15.6" + dependencies: + "@livekit/mutex": 1.1.1 + "@livekit/protocol": 1.39.3 + events: ^3.3.0 + loglevel: ^1.9.2 + sdp-transform: ^2.15.0 + ts-debounce: ^4.0.0 + tslib: 2.8.1 + typed-emitter: ^2.1.0 + webrtc-adapter: ^9.0.1 + peerDependencies: + "@types/dom-mediacapture-record": ^1 + checksum: 3dda194dbb5c9ff83038fafa8ead353e9abea84bdd7fdb4c65e164bcfab2d48ccec04caf6df2bace164c4475c4a6ae753c6154654abca984fe96384d885721af + languageName: node + linkType: hard + "load-bmfont@npm:^1.3.1, load-bmfont@npm:^1.4.0": version: 1.4.1 resolution: "load-bmfont@npm:1.4.1" @@ -34102,6 +34145,13 @@ __metadata: languageName: node linkType: hard +"loglevel@npm:^1.9.2": + version: 1.9.2 + resolution: "loglevel@npm:1.9.2" + checksum: 896c67b90a507bfcfc1e9a4daa7bf789a441dd70d95cd13b998d6dd46233a3bfadfb8fadb07250432bbfb53bf61e95f2520f9b11f9d3175cc460e5c251eca0af + languageName: node + linkType: hard + "long-timeout@npm:0.1.1": version: 0.1.1 resolution: "long-timeout@npm:0.1.1" @@ -42186,6 +42236,16 @@ __metadata: languageName: node linkType: hard +"retell-client-js-sdk@npm:^2.0.0": + version: 2.0.7 + resolution: "retell-client-js-sdk@npm:2.0.7" + dependencies: + eventemitter3: ^5.0.1 + livekit-client: ^2.5.1 + checksum: 8c0f0a0ed95b79a7c0e4b9dad1f1b0369c8e05bc6e0219945ad271426e5348d644d113b94d2c9a35eff62919748a3f638c35e76e3ddb413fa5062295b4de17e4 + languageName: node + linkType: hard + "retell-sdk@npm:^4.40.0": version: 4.41.0 resolution: "retell-sdk@npm:4.41.0" @@ -42667,6 +42727,15 @@ __metadata: languageName: node linkType: hard +"rxjs@npm:*, rxjs@npm:^7.8.2": + version: 7.8.2 + resolution: "rxjs@npm:7.8.2" + dependencies: + tslib: ^2.1.0 + checksum: 2f233d7c832a6c255dabe0759014d7d9b1c9f1cb2f2f0d59690fd11c883c9826ea35a51740c06ab45b6ade0d9087bde9192f165cba20b6730d344b831ef80744 + languageName: node + linkType: hard + "rxjs@npm:7.8.1, rxjs@npm:^7.8.1": version: 7.8.1 resolution: "rxjs@npm:7.8.1" @@ -42685,15 +42754,6 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:^7.8.2": - version: 7.8.2 - resolution: "rxjs@npm:7.8.2" - dependencies: - tslib: ^2.1.0 - checksum: 2f233d7c832a6c255dabe0759014d7d9b1c9f1cb2f2f0d59690fd11c883c9826ea35a51740c06ab45b6ade0d9087bde9192f165cba20b6730d344b831ef80744 - languageName: node - linkType: hard - "safe-array-concat@npm:^1.0.1": version: 1.1.0 resolution: "safe-array-concat@npm:1.1.0" @@ -42939,6 +42999,22 @@ __metadata: languageName: node linkType: hard +"sdp-transform@npm:^2.15.0": + version: 2.15.0 + resolution: "sdp-transform@npm:2.15.0" + bin: + sdp-verify: checker.js + checksum: 63a21fdb3f97adc31019c7fb896e77076bdd2405d9e0eb6bd991b6f51973552bdd99409878aae8f8375f516af162d5a4176e102e090e24c870accafb9f73886f + languageName: node + linkType: hard + +"sdp@npm:^3.2.0": + version: 3.2.1 + resolution: "sdp@npm:3.2.1" + checksum: 88c24ceb46bf29f7ad3ece249db6353668a7c970b5831cbf1f4f371d8960ae90a4b0e3b9b05aa499a5038fc145662578456530c7b3efcaf623fdb0ef8ab243a0 + languageName: node + linkType: hard + "section-matter@npm:^1.0.0": version: 1.0.0 resolution: "section-matter@npm:1.0.0" @@ -45943,6 +46019,13 @@ __metadata: languageName: node linkType: hard +"ts-debounce@npm:^4.0.0": + version: 4.0.0 + resolution: "ts-debounce@npm:4.0.0" + checksum: e1e509632c5aa09c40d3fa315b3a95b2c2e8813ccc706a400aa08e41f691e658061f34b42a1e8a578a043540d6db198e6ecf3ce26a5356a02a0940985fb1e379 + languageName: node + linkType: hard + "ts-dedent@npm:^2.0.0": version: 2.2.0 resolution: "ts-dedent@npm:2.2.0" @@ -46250,7 +46333,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2, tslib@npm:^2.0.3, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1": +"tslib@npm:2, tslib@npm:2.8.1, tslib@npm:^2.0.3, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a @@ -46740,6 +46823,18 @@ __metadata: languageName: node linkType: hard +"typed-emitter@npm:^2.1.0": + version: 2.1.0 + resolution: "typed-emitter@npm:2.1.0" + dependencies: + rxjs: "*" + dependenciesMeta: + rxjs: + optional: true + checksum: 95821a9e05784b972cc9d152891fd12a56cb4b1a7c57e768c02bea6a8984da7aff8f19404a7b69eea11fae2a3b6c0c510a4c510f575f50162c759ae9059f2520 + languageName: node + linkType: hard + "typedarray-to-buffer@npm:~1.0.0": version: 1.0.4 resolution: "typedarray-to-buffer@npm:1.0.4" @@ -48305,6 +48400,15 @@ __metadata: languageName: node linkType: hard +"webrtc-adapter@npm:^9.0.1": + version: 9.0.3 + resolution: "webrtc-adapter@npm:9.0.3" + dependencies: + sdp: ^3.2.0 + checksum: 2b29e6e6ed1018f62b2077fffd4f6d7fa9993f98091b7af459261092c2c05125bfa146b4d0dd76ed9e667703a1391743e0bb76a3754239c7d20acb034026eae3 + languageName: node + linkType: hard + "websocket-driver@npm:>=0.5.1": version: 0.7.4 resolution: "websocket-driver@npm:0.7.4"