feat: Plain chat (#18284)

* plain custom desin

* No display on card

* dynamic plain chat component and hmac hash

* re-route users to plain.com chat instead of intercom chat when going to /support

* provider errors

* yarn lock fix

* plain chat removed unneeded hmac

* fix ts error

* error handling improved

* remove intercome provider from app-dir

* Create getting-started.mdx (#18342)

* Delete help directory (#18343)

* chore: moved docs/help to /help (#18345)

* chore: Delete unused guides directory (#18346)

* feat: booking filters (#18303)

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>

* chore: Remove `HeadSeo` components where no longer needed + improve app router metadata logic (#18348)

* remove HeadSeo for already migrated pages and refactor prepareMetadata

* create _generateMetadataWithoutImage and refactor _generateMetadata

* chore: app router - /bookings status page (#18183)

* chore: app router - /bookings page

* remove env vars

* fix

* Update middleware.ts

* revert unneeded change

* refactor for the better

* fix

* cache i18n instances (#18309)

* feat: virtual queues tab in insights (#18260)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>

* feat: update translations via @replexica (#18361)

Co-authored-by: Replexica <support@replexica.com>

* update OOO e2e tests to remove flakiness (#18367)

* fix: metadata is overwritten for child managed eventType when updating parent (#18059)

* fix: metadata is overwirten

* Update

* type error

* Update

* fix test

* fix type error

* chore: added routing support link (#18369)

* Added routing form support link

* small change

* Type fix

---------

Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>

* chore: refactor handling logic for embeds in app router (#18362)

* refactor handling logic for embeds in app router

* fix type checks

* add test for withEmbedSsrAppDir

* fix

* review changes

* yarn lock

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Calcom Bot <109866826+calcom-bot@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Replexica <support@replexica.com>
Co-authored-by: Vijay <vijayraghav22@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Nizzy
2024-12-26 19:07:08 +00:00
committed by GitHub
co-authored by Syed Ali Shahbaz CarinaWolli sean-brydon sean-brydon Peer Richelsen Omar López Replexica Joe Au-Yeung Calcom Bot Udit Takkar Benny Joo Carina Wollendorfer Vijay Anik Dhabal Babu Alex van Andel
parent 3994929a51
commit 225313e391
12 changed files with 7767 additions and 210 deletions
+2
View File
@@ -112,6 +112,8 @@ NEXT_PUBLIC_POSTHOG_HOST=
PLAIN_API_KEY=
PLAIN_API_URL=https://api.plain.com/v1
PLAIN_HMAC_SECRET_KEY=
PLAIN_CHAT_ID=
PLAIN_CHAT_HMAC_SECRET_KEY=
# Zendesk Config
NEXT_PUBLIC_ZENDESK_KEY=
+5 -24
View File
@@ -26,6 +26,7 @@ export async function handler(request: Request) {
// HMAC verification
const incomingSignature = headersList.get("plain-request-signature");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const expectedSignature = createHmac("sha-256", process.env.PLAIN_HMAC_SECRET_KEY!)
.update(JSON.stringify(requestBody))
.digest("hex");
@@ -88,18 +89,8 @@ export async function handler(request: Request) {
rowAsideContent: [
{
componentBadge: {
badgeLabel:
customer.emailVerified === undefined
? "Unknown"
: customer.emailVerified
? "Yes"
: "No",
badgeColor:
customer.emailVerified === undefined
? "YELLOW"
: customer.emailVerified
? "GREEN"
: "RED",
badgeLabel: customer.emailVerified ? "Yes" : "No",
badgeColor: customer.emailVerified ? "GREEN" : "RED",
},
},
],
@@ -195,18 +186,8 @@ export async function handler(request: Request) {
rowAsideContent: [
{
componentBadge: {
badgeLabel:
customer.twoFactorEnabled === undefined
? "Unknown"
: customer.twoFactorEnabled
? "Yes"
: "No",
badgeColor:
customer.twoFactorEnabled === undefined
? "YELLOW"
: customer.twoFactorEnabled
? "GREEN"
: "RED",
badgeLabel: customer.twoFactorEnabled ? "Yes" : "No",
badgeColor: customer.twoFactorEnabled ? "GREEN" : "RED",
},
},
],
+49
View File
@@ -0,0 +1,49 @@
import { createHmac } from "crypto";
import { NextResponse } from "next/server";
import { z } from "zod";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { apiRouteMiddleware } from "@calcom/lib/server/apiRouteMiddleware";
const responseSchema = z.object({
hash: z.string(),
email: z.string().email(),
shortName: z.string(),
appId: z.string(),
fullName: z.string(),
chatAvatarUrl: z.string(),
});
async function handler(request: Request) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const session = await getServerSession({ req: request as any });
if (!session?.user?.email) {
return new Response("Unauthorized - No session email found", { status: 401 });
}
const secret = process.env.PLAIN_CHAT_HMAC_SECRET_KEY;
if (!secret) {
return new Response("Missing Plain Chat secret", { status: 500 });
}
const hmac = createHmac("sha256", secret);
hmac.update(session.user.email.toLowerCase().trim());
const hash = hmac.digest("hex");
const shortName =
(session.user.name?.split(" ")[0] || session.user.email).charAt(0).toUpperCase() +
(session.user.name?.split(" ")[0] || session.user.email).slice(1) || "User";
const response = responseSchema.parse({
hash,
email: session.user.email || "user@example.com",
shortName,
appId: process.env.PLAIN_CHAT_ID,
fullName: session.user.name || "User",
chatAvatarUrl: session.user.avatarUrl || "",
});
return NextResponse.json(response);
}
export const POST = apiRouteMiddleware(handler);
+7 -5
View File
@@ -17,12 +17,12 @@ import CacheProvider from "react-inlinesvg/provider";
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
import { useFlags } from "@calcom/features/flags/hooks";
import { MetaProvider } from "@calcom/ui";
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
import PlainChat from "@lib/plain/plainChat";
import type { WithLocaleProps } from "@lib/withLocale";
import type { WithNonceProps } from "@lib/withNonce";
@@ -265,9 +265,11 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) {
const AppProviders = (props: PageWrapperProps) => {
// No need to have intercom on public pages - Good for Page Performance
const isBookingPage = useIsBookingPage();
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider>
<PlainChat />
<CustomI18nextProvider i18n={props.i18n}>
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
@@ -301,11 +303,11 @@ const AppProviders = (props: PageWrapperProps) => {
}
return (
<DynamicHelpscoutProvider>
<DynamicIntercomProvider>
<>
<DynamicHelpscoutProvider>
<DynamicPostHogProvider>{Hydrated}</DynamicPostHogProvider>
</DynamicIntercomProvider>
</DynamicHelpscoutProvider>
</DynamicHelpscoutProvider>
</>
);
};
+6 -7
View File
@@ -16,12 +16,12 @@ import CacheProvider from "react-inlinesvg/provider";
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
import { FeatureProvider } from "@calcom/features/flags/context/provider";
import { useFlags } from "@calcom/features/flags/hooks";
import { MetaProvider } from "@calcom/ui";
import useIsBookingPage from "@lib/hooks/useIsBookingPage";
import PlainChat from "@lib/plain/plainChat";
import type { WithLocaleProps } from "@lib/withLocale";
import type { WithNonceProps } from "@lib/withNonce";
@@ -282,7 +282,6 @@ function OrgBrandProvider({ children }: { children: React.ReactNode }) {
}
const AppProviders = (props: AppPropsWithChildren) => {
// No need to have intercom on public pages - Good for Page Performance
const isBookingPage = useIsBookingPage();
const { pageProps, ...rest } = props;
@@ -298,9 +297,9 @@ const AppProviders = (props: AppPropsWithChildren) => {
const RemainingProviders = (
<EventCollectionProvider options={{ apiPath: "/api/collect-events" }}>
<SessionProvider session={pageProps.session ?? undefined}>
<PlainChat />
<CustomI18nextProvider {...propsWithoutNonce}>
<TooltipProvider>
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
<CalcomThemeProvider
themeBasis={props.pageProps.themeBasis}
nonce={props.pageProps.nonce}
@@ -327,14 +326,14 @@ const AppProviders = (props: AppPropsWithChildren) => {
}
return (
<DynamicHelpscoutProvider>
<DynamicIntercomProvider>
<>
<DynamicHelpscoutProvider>
<DynamicPostHogProvider>
<PostHogPageView />
{RemainingProviders}
</DynamicPostHogProvider>
</DynamicIntercomProvider>
</DynamicHelpscoutProvider>
</DynamicHelpscoutProvider>
</>
);
};
+247
View File
@@ -0,0 +1,247 @@
"use client";
import { useSession } from "next-auth/react";
import { usePathname, useSearchParams } from "next/navigation";
import Script from "next/script";
import { useEffect, useState } from "react";
declare global {
interface Window {
Plain?: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
init: (config: any) => void;
open: () => void;
};
plainScriptLoaded?: () => void;
__PLAIN_CONFIG__?: PlainChatConfig;
}
}
interface PlainChatConfig {
appId: string;
customerDetails: {
email: string;
emailHash: string;
fullName: string;
shortName: string;
chatAvatarUrl: string;
};
links: Array<{
icon: string;
text: string;
url: string;
}>;
chatButtons: Array<{
icon: string;
text: string;
type: string;
form?: {
fields: Array<{
type: string;
placeholder: string;
options: Array<{
icon: string;
text: string;
threadDetails: {
severity: string;
labelTypeIds: Array<string>;
issueType: string;
priority: string;
};
}>;
}>;
};
}>;
entryPoint: {
type: string;
};
hideBranding: boolean;
theme: string;
style: {
brandColor: string;
launcherBackgroundColor: string;
launcherIconColor: string;
};
position: {
bottom: string;
right: string;
};
}
const PlainChat = () => {
const [config, setConfig] = useState<PlainChatConfig | null>(null);
const { data: session } = useSession();
const pathname = usePathname();
const searchParams = useSearchParams();
useEffect(() => {
const initConfig = async () => {
if (!session?.user?.email) return;
try {
const response = await fetch("/api/plain-hash", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Failed to generate hash: ${errorText}`);
}
const data = await response.json();
if (!data.hash || !data.email || !data.appId) {
throw new Error("Missing required fields in API response");
}
const plainChatConfig: PlainChatConfig = {
appId: data.appId,
customerDetails: {
email: data.email,
shortName: data.shortName,
fullName: data.fullName,
emailHash: data.hash,
chatAvatarUrl: data.chatAvatarUrl,
},
links: [
{
icon: "book",
text: "Documentation",
url: "https://cal.com/docs",
},
{
icon: "chat",
text: "Ask the community",
url: "https://github.com/calcom/cal.com/discussions",
},
],
chatButtons: [
{
icon: "chat",
text: "Ask a question",
type: "primary",
},
{
icon: "bulb",
text: "Send feedback",
type: "default",
},
{
icon: "error",
text: "Report an issue",
type: "default",
form: {
fields: [
{
type: "dropdown",
placeholder: "Select severity...",
options: [
{
icon: "support",
text: "I'm unable to use the app",
threadDetails: {
severity: "critical",
issueType: "critical",
labelTypeIds: ["lt_01JFJWNWAC464N8DZ6YE71YJRF"],
priority: "u",
},
},
{
icon: "error",
text: "Major functionality degraded",
threadDetails: {
severity: "major",
issueType: "major",
labelTypeIds: ["lt_01JFJWP3KECF1YQES6XF212RFW"],
priority: "h",
},
},
{
icon: "bug",
text: "Minor annoyance",
threadDetails: {
severity: "minor",
issueType: "minor",
labelTypeIds: ["lt_01JFJWPC8ADW0PK28JHMJR6NSS"],
priority: "l",
},
},
],
},
],
},
},
],
entryPoint: {
type: "chat",
},
hideBranding: true,
theme: "auto",
style: {
brandColor: "#FFFFFF",
launcherBackgroundColor: "#262626",
launcherIconColor: "#FFFFFF",
},
position: {
bottom: "20px",
right: "20px",
},
};
if (process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test") {
window.__PLAIN_CONFIG__ = plainChatConfig;
}
setConfig(plainChatConfig);
if (pathname === "/event-types" && searchParams?.has("openPlain")) {
const timer = setTimeout(() => {
if (window.Plain) {
window.Plain.open();
}
}, 100);
return () => clearTimeout(timer);
}
} catch (error) {
console.error("Failed to initialize Plain Chat:", error);
}
};
initConfig();
}, [session, pathname, searchParams]);
const plainChatScript = `
window.plainScriptLoaded = function() {
if (window.Plain && ${Boolean(config)}) {
try {
Plain.init(${config ? JSON.stringify(config) : null});
} catch (error) {
console.error("Failed to initialize Plain:", error);
}
}
}
`;
if (!config) return null;
return (
<>
<Script
id="plain-chat"
src="https://chat.cdn-plain.com/index.js"
strategy="afterInteractive"
onLoad={() => window.plainScriptLoaded?.()}
/>
<Script
id="plain-chat-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{ __html: plainChatScript }}
/>
</>
);
};
export default PlainChat;
+1 -1
View File
@@ -594,7 +594,7 @@ const nextConfig = {
value: orgHostPath,
},
],
destination: "/event-types?openIntercom=true",
destination: "/event-types?openPlain=true",
permanent: true,
},
{
@@ -55,8 +55,8 @@ export default (
rowAsideContent: [
{
componentBadge: {
badgeLabel: emailVerified === null ? "Unknown" : emailVerified ? "Yes" : "No",
badgeColor: emailVerified === null ? "YELLOW" : emailVerified ? "GREEN" : "RED",
badgeLabel: emailVerified === null ? "No" : emailVerified ? "Yes" : "No",
badgeColor: emailVerified === null ? "RED" : emailVerified ? "GREEN" : "RED",
},
},
],
@@ -152,8 +152,8 @@ export default (
rowAsideContent: [
{
componentBadge: {
badgeLabel: twoFactorEnabled === null ? "Unknown" : twoFactorEnabled ? "Yes" : "No",
badgeColor: twoFactorEnabled === null ? "YELLOW" : twoFactorEnabled ? "GREEN" : "RED",
badgeLabel: twoFactorEnabled === null ? "No" : twoFactorEnabled ? "Yes" : "No",
badgeColor: twoFactorEnabled === null ? "RED" : twoFactorEnabled ? "GREEN" : "RED",
},
},
],
@@ -2216,8 +2216,8 @@
"org_no_teams_yet": "This organization has no teams yet",
"org_no_teams_yet_description": "If you are an administrator, be sure to create teams to be shown here.",
"set_up": "Set up",
"my_profile": "My Profile",
"my_settings": "My Settings",
"my_profile": "My profile",
"my_settings": "My settings",
"crm": "CRM",
"messaging": "Messaging",
"sender_id_info": "Name or number shown as the sender of an SMS (some countries do not allow alphanumeric sender IDs)",
@@ -2,9 +2,8 @@ import { signOut } from "next-auth/react";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem";
import { classNames } from "@calcom/lib";
import { JOIN_COMMUNITY, ROADMAP, DESKTOP_APP_LINK } from "@calcom/lib/constants";
import { ROADMAP, DESKTOP_APP_LINK } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import {
@@ -23,6 +22,16 @@ import { useGetUserAttributes } from "@calcom/web/components/settings/platform/h
import FreshChatProvider from "../../ee/support/lib/freshchat/FreshChatProvider";
declare global {
interface Window {
Plain?: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
init: (config: any) => void;
open: () => void;
};
}
}
interface UserDropdownProps {
small?: boolean;
}
@@ -44,11 +53,12 @@ export function UserDropdown({ small }: UserDropdownProps) {
});
});
const [helpOpen, setHelpOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const onHelpItemSelect = () => {
setHelpOpen(false);
const handleHelpClick = () => {
if (window.Plain) {
window.Plain.open();
}
setMenuOpen(false);
};
@@ -108,110 +118,88 @@ export function UserDropdown({ small }: UserDropdownProps) {
align="start"
onInteractOutside={() => {
setMenuOpen(false);
setHelpOpen(false);
}}
className="group overflow-hidden rounded-md">
{helpOpen ? (
<HelpMenuItem onHelpItemSelect={() => onHelpItemSelect()} />
) : (
<>
{!isPlatformPages && (
<>
<DropdownMenuItem>
<DropdownItem
type="button"
CustomStartIcon={
<Icon name="user" className="text-default h-4 w-4" aria-hidden="true" />
}
href="/settings/my-account/profile">
{t("my_profile")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
CustomStartIcon={
<Icon name="settings" className="text-default h-4 w-4" aria-hidden="true" />
}
href="/settings/my-account/general">
{t("my_settings")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
CustomStartIcon={
<Icon name="moon" className="text-default h-4 w-4" aria-hidden="true" />
}
href="/settings/my-account/out-of-office">
{t("out_of_office")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem>
<DropdownItem
StartIcon="messages-square"
target="_blank"
rel="noreferrer"
href={JOIN_COMMUNITY}>
{t("join_our_community")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem StartIcon="map" target="_blank" href={ROADMAP}>
{t("visit_roadmap")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon="circle-help"
aria-hidden="true"
onClick={() => setHelpOpen(true)}>
{t("help")}
</DropdownItem>
</DropdownMenuItem>
{!isPlatformPages && (
<DropdownMenuItem className="todesktop:hidden hidden lg:flex">
<>
{!isPlatformPages && (
<>
<DropdownMenuItem>
<DropdownItem
StartIcon="download"
target="_blank"
rel="noreferrer"
href={DESKTOP_APP_LINK}>
{t("download_desktop_app")}
type="button"
CustomStartIcon={
<Icon name="user" className="text-default h-4 w-4" aria-hidden="true" />
}
href="/settings/my-account/profile">
{t("my_profile")}
</DropdownItem>
</DropdownMenuItem>
)}
{!isPlatformPages && isPlatformUser && (
<DropdownMenuItem className="todesktop:hidden hidden lg:flex">
<DropdownMenuItem>
<DropdownItem
StartIcon="blocks"
target="_blank"
rel="noreferrer"
href="/settings/platform">
Platform
type="button"
CustomStartIcon={
<Icon name="settings" className="text-default h-4 w-4" aria-hidden="true" />
}
href="/settings/my-account/general">
{t("my_settings")}
</DropdownItem>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownItem
type="button"
CustomStartIcon={
<Icon name="moon" className="text-default h-4 w-4" aria-hidden="true" />
}
href="/settings/my-account/out-of-office">
{t("out_of_office")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon="log-out"
aria-hidden="true"
onClick={() => {
signOut({ callbackUrl: "/auth/logout" });
}}>
{t("sign_out")}
<DropdownMenuItem>
<DropdownItem StartIcon="map" target="_blank" href={ROADMAP}>
{t("visit_roadmap")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon="circle-help"
aria-hidden="true"
onClick={handleHelpClick}>
{t("help")}
</DropdownItem>
</DropdownMenuItem>
{!isPlatformPages && (
<DropdownMenuItem className="todesktop:hidden hidden lg:flex">
<DropdownItem StartIcon="download" target="_blank" rel="noreferrer" href={DESKTOP_APP_LINK}>
{t("download_desktop_app")}
</DropdownItem>
</DropdownMenuItem>
</>
)}
)}
{!isPlatformPages && isPlatformUser && (
<DropdownMenuItem className="todesktop:hidden hidden lg:flex">
<DropdownItem StartIcon="blocks" target="_blank" rel="noreferrer" href="/settings/platform">
Platform
</DropdownItem>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon="log-out"
aria-hidden="true"
onClick={() => {
signOut({ callbackUrl: "/auth/logout" });
}}>
{t("sign_out")}
</DropdownItem>
</DropdownMenuItem>
</>
</DropdownMenuContent>
</FreshChatProvider>
</DropdownMenuPortal>
+3 -1
View File
@@ -117,7 +117,9 @@
"MOTIFLAND_DOCS_PROJECT_ID",
"PLAIN_HMAC_SECRET_KEY",
"PLAIN_API_KEY",
"PLAIN_API_URL"
"PLAIN_API_URL",
"PLAIN_CHAT_ID",
"PLAIN_CHAT_HMAC_SECRET_KEY"
]
},
"build": {
+7355 -68
View File
File diff suppressed because it is too large Load Diff