Skip to content

Commit 5401bcc

Browse files
feat: AI description - DB model + frontend + backend (fetch only) (#17651)
* feat: AI description - DB model + frontend + backend (fetch only) * fix types and add validation to backend * improve log * improve * import type * fix replexica error * fix * fix test * update replexica type * Renamed descriptionTranslations to fieldTranslations * Moved the eventTypeId column to 2nd --------- Co-authored-by: Keith Williams <[email protected]>
1 parent f9afca2 commit 5401bcc

File tree

20 files changed

+621
-9
lines changed

20 files changed

+621
-9
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,6 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
408408
# NEXT_PUBLIC_LOGGER_LEVEL=3 sets to log info, warn, error and fatal logs.
409409
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
410410
NEXT_PUBLIC_LOGGER_LEVEL=
411+
412+
# Used to use Replexica SDK, a tool for real-time AI-powered localization
413+
REPLEXICA_API_KEY=

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"@radix-ui/react-switch": "^1.0.0",
6565
"@radix-ui/react-toggle-group": "^1.0.0",
6666
"@radix-ui/react-tooltip": "^1.0.0",
67+
"@replexica/sdk": "^0.6.0",
6768
"@sentry/nextjs": "^8.8.0",
6869
"@stripe/react-stripe-js": "^1.10.0",
6970
"@stripe/stripe-js": "^1.35.0",

apps/web/public/static/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2791,6 +2791,7 @@
27912791
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
27922792
"salesforce_option": "Salesforce Option",
27932793
"lookup_field_name": "Lookup Field Name",
2794+
"translate_description_button": "Translate description to the visitor's browser language using AI",
27942795
"rr_distribution_method": "Distribution",
27952796
"rr_distribution_method_description": "Allows for optimising distribution for maximum availability or to aim for a more balanced assignment.",
27962797
"rr_distribution_method_availability_title": "Maximize availability",

apps/web/test/lib/handleChildrenEventTypes.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ describe("handleChildrenEventTypes", () => {
112112
lockTimeZoneToggleOnBookingPage,
113113
useEventTypeDestinationCalendarEmail,
114114
secondaryEmailId,
115+
autoTranslateDescriptionEnabled,
115116
...evType
116117
} = mockFindFirstEventType({
117118
id: 123,
@@ -182,7 +183,7 @@ describe("handleChildrenEventTypes", () => {
182183
bookingLimits: undefined,
183184
},
184185
});
185-
const { profileId, ...rest } = evType;
186+
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
186187
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
187188
data: {
188189
...rest,
@@ -265,6 +266,7 @@ describe("handleChildrenEventTypes", () => {
265266
lockTimeZoneToggleOnBookingPage,
266267
useEventTypeDestinationCalendarEmail,
267268
secondaryEmailId,
269+
autoTranslateDescriptionEnabled,
268270
...evType
269271
} = mockFindFirstEventType({
270272
id: 123,
@@ -337,7 +339,7 @@ describe("handleChildrenEventTypes", () => {
337339
length: 30,
338340
},
339341
});
340-
const { profileId, ...rest } = evType;
342+
const { profileId, autoTranslateDescriptionEnabled, ...rest } = evType;
341343
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
342344
data: {
343345
...rest,
@@ -378,6 +380,7 @@ describe("handleChildrenEventTypes", () => {
378380
lockTimeZoneToggleOnBookingPage,
379381
useEventTypeDestinationCalendarEmail,
380382
secondaryEmailId,
383+
autoTranslateDescriptionEnabled,
381384
...evType
382385
} = mockFindFirstEventType({
383386
metadata: { managedEventConfig: {} },

packages/features/bookings/Booker/components/EventMeta.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export const EventMeta = ({
4848
| "recurringEvent"
4949
| "price"
5050
| "isDynamic"
51+
| "fieldTranslations"
52+
| "autoTranslateDescriptionEnabled"
5153
> | null;
5254
isPending: boolean;
5355
isPlatform?: boolean;
@@ -101,6 +103,10 @@ export const EventMeta = ({
101103
: isHalfFull
102104
? "text-yellow-500"
103105
: "text-bookinghighlight";
106+
const browserLocale = navigator.language; // e.g. "en-US", "es-ES", "fr-FR"
107+
const translatedDescription = (event?.fieldTranslations ?? []).find((translation) =>
108+
browserLocale.startsWith(translation.targetLang)
109+
)?.translatedText;
104110

105111
return (
106112
<div className={`${classNames?.eventMetaContainer || ""} relative z-10 p-6`} data-testid="event-meta">
@@ -120,9 +126,9 @@ export const EventMeta = ({
120126
/>
121127
)}
122128
<EventTitle className={`${classNames?.eventMetaTitle} my-2`}>{event?.title}</EventTitle>
123-
{event.description && (
129+
{(event.description || translatedDescription) && (
124130
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
125-
<div dangerouslySetInnerHTML={{ __html: event.description }} />
131+
<div dangerouslySetInnerHTML={{ __html: translatedDescription ?? event.description }} />
126132
</EventMetaBlock>
127133
)}
128134
<div className="space-y-4 font-medium rtl:-mr-2">

packages/features/bookings/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ export type BookerEvent = Pick<
5151
| "bookingFields"
5252
| "seatsShowAvailabilityCount"
5353
| "isInstantEvent"
54+
| "fieldTranslations"
55+
| "autoTranslateDescriptionEnabled"
5456
> & { users: BookerEventUser[]; showInstantEventConnectNowModal: boolean } & { profile: BookerEventProfile };
5557

5658
export type ValidationErrors<T extends object> = { key: FieldPath<T>; error: ErrorOption }[];

packages/features/eventtypes/components/tabs/setup/EventSetupTab.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useSession } from "next-auth/react";
12
import { useState } from "react";
23
import { Controller, useFormContext } from "react-hook-form";
34
import type { UseFormGetValues, UseFormSetValue, Control, FormState } from "react-hook-form";
@@ -20,6 +21,7 @@ export type EventSetupTabProps = Pick<
2021
>;
2122
export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; hasOrgBranding: boolean }) => {
2223
const { t } = useLocale();
24+
const session = useSession();
2325
const isPlatform = useIsPlatform();
2426
const formMethods = useFormContext<FormValues>();
2527
const { eventType, team, urlPrefix, hasOrgBranding } = props;
@@ -29,6 +31,7 @@ export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; h
2931
const [firstRender, setFirstRender] = useState(true);
3032

3133
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
34+
const autoTranslateDescriptionEnabled = formMethods.watch("autoTranslateDescriptionEnabled");
3235

3336
const multipleDurationOptions = [
3437
5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 150, 180, 240, 300, 360, 420, 480,
@@ -95,6 +98,17 @@ export const EventSetupTab = (props: EventSetupTabProps & { urlPrefix: string; h
9598
</>
9699
)}
97100
</div>
101+
<div className="[&_label]:my-1 [&_label]:font-normal">
102+
<SettingsToggle
103+
title={t("translate_description_button")}
104+
checked={!!autoTranslateDescriptionEnabled}
105+
onCheckedChange={(value) => {
106+
formMethods.setValue("autoTranslateDescriptionEnabled", value, { shouldDirty: true });
107+
}}
108+
disabled={!session.data?.user.org?.id}
109+
tooltip={!session.data?.user.org?.id ? t("orgs_upgrade_to_enable_feature") : undefined}
110+
/>
111+
</div>
98112
<TextField
99113
required
100114
label={isPlatform ? "Slug" : t("URL")}

packages/features/eventtypes/lib/getPublicEvent.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
6666
metadata: true,
6767
lockTimeZoneToggleOnBookingPage: true,
6868
requiresConfirmation: true,
69+
autoTranslateDescriptionEnabled: true,
70+
fieldTranslations: {
71+
select: {
72+
translatedText: true,
73+
targetLang: true,
74+
},
75+
},
6976
requiresBookerEmailVerification: true,
7077
recurringEvent: true,
7178
price: true,
@@ -293,6 +300,8 @@ export const getPublicEvent = async (
293300
},
294301
isInstantEvent: false,
295302
showInstantEventConnectNowModal: false,
303+
autoTranslateDescriptionEnabled: false,
304+
fieldTranslations: [],
296305
};
297306
}
298307

packages/features/eventtypes/lib/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { z } from "zod";
22

33
import type { EventLocationType } from "@calcom/core/location";
44
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
5+
import type { EventTypeTranslation } from "@calcom/prisma/client";
56
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
67
import type { BookerLayoutSettings, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
78
import type { customInputSchema } from "@calcom/prisma/zod-utils";
@@ -109,6 +110,8 @@ export type FormValues = {
109110
seatsShowAttendees: boolean | null;
110111
seatsShowAvailabilityCount: boolean | null;
111112
seatsPerTimeSlotEnabled: boolean;
113+
autoTranslateDescriptionEnabled: boolean;
114+
fieldTranslations: EventTypeTranslation[];
112115
scheduleName: string;
113116
minimumBookingNotice: number;
114117
minimumBookingNoticeInDurationType: number;

packages/lib/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ export const CALCOM_PRIVATE_API_ROUTE = process.env.CALCOM_PRIVATE_API_ROUTE ||
168168
export const WEBSITE_PRIVACY_POLICY_URL =
169169
process.env.NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL || "https://cal.com/privacy";
170170
export const WEBSITE_TERMS_URL = process.env.NEXT_PUBLIC_WEBSITE_TERMS_URL || "https://cal.com/terms";
171+
export const REPLEXICA_API_KEY = process.env.REPLEXICA_API_KEY;
171172

172173
/**
173174
* The maximum number of days we should check for if we don't find all required bookable days
@@ -190,3 +191,4 @@ export const RECORDING_DEFAULT_ICON = IS_PRODUCTION
190191
export const RECORDING_IN_PROGRESS_ICON = IS_PRODUCTION
191192
? `${WEBAPP_URL}/stop-recording.svg`
192193
: `https://app.cal.com/stop-recording.svg`;
194+

0 commit comments

Comments
 (0)