({
SidePanelTopBarRightCornerIcon: () => null,
}));
+jest.mock('@/side-panel/components/SidePanelExpandAiChatButton', () => ({
+ SidePanelExpandAiChatButton: () => null,
+}));
+
const mockCloseSidePanelMenu = jest.fn();
jest.mock('@/side-panel/hooks/useSidePanelContextChips', () => ({
diff --git a/packages/twenty-front/src/modules/side-panel/hooks/useOpenAskAiPageInSidePanel.ts b/packages/twenty-front/src/modules/side-panel/hooks/useOpenAskAiPageInSidePanel.ts
index 55c86bfd5f..3ba6258973 100644
--- a/packages/twenty-front/src/modules/side-panel/hooks/useOpenAskAiPageInSidePanel.ts
+++ b/packages/twenty-front/src/modules/side-panel/hooks/useOpenAskAiPageInSidePanel.ts
@@ -1,6 +1,8 @@
+import { hasAgentChatBeenOpenedState } from '@/ai/states/hasAgentChatBeenOpenedState';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { t } from '@lingui/core/macro';
import { useCallback } from 'react';
import { SidePanelPages } from 'twenty-shared/types';
@@ -10,6 +12,9 @@ import { v4 } from 'uuid';
export const useOpenAskAiPageInSidePanel = () => {
const { navigateSidePanelMenu } = useSidePanelMenu();
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
+ const setHasAgentChatBeenOpened = useSetAtomState(
+ hasAgentChatBeenOpenedState,
+ );
const openAskAiPage = useCallback(
({
@@ -22,6 +27,8 @@ export const useOpenAskAiPageInSidePanel = () => {
? resetNavigationStack
: isSidePanelOpened;
+ setHasAgentChatBeenOpened(true);
+
navigateSidePanelMenu({
page: SidePanelPages.AskAI,
pageTitle: t`Ask AI`,
@@ -30,7 +37,7 @@ export const useOpenAskAiPageInSidePanel = () => {
resetNavigationStack: shouldReset,
});
},
- [navigateSidePanelMenu, isSidePanelOpened],
+ [navigateSidePanelMenu, isSidePanelOpened, setHasAgentChatBeenOpened],
);
return {
diff --git a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx
index 499e1fee34..d6ee76c6fc 100644
--- a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx
+++ b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx
@@ -1,13 +1,10 @@
import { SubTitle } from '@/auth/components/SubTitle';
-import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingAnimatedReveal } from '@/onboarding/components/OnboardingAnimatedReveal';
import { OnboardingVerifyLayout } from '@/onboarding/components/OnboardingVerifyLayout';
import { useOnboardingMotionTransition } from '@/onboarding/hooks/useOnboardingMotionTransition';
import { useShowWelcomeAnimationAfterOnboardingCheckout } from '@/onboarding/hooks/useShowWelcomeAnimationAfterOnboardingCheckout';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
-import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
-import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
-import { useLazyQuery } from '@apollo/client/react';
+import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
import { t } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { AnimatePresence, motion } from 'framer-motion';
@@ -15,7 +12,6 @@ import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { MainButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-import { GetCurrentUserDocument } from '~/generated-metadata/graphql';
const SUBSCRIPTION_CONFIRMATION_POLL_INTERVAL_MS = 2000;
const SUBSCRIPTION_CONFIRMATION_MAX_ATTEMPTS = 30;
@@ -26,11 +22,7 @@ const StyledRetryButtonContainer = styled.div`
`;
export const PaymentSuccess = () => {
- const subscriptionStatus = useSubscriptionStatus();
- const [getCurrentUser] = useLazyQuery(GetCurrentUserDocument, {
- fetchPolicy: 'network-only',
- });
- const setCurrentUser = useSetAtomState(currentUserState);
+ const { loadCurrentUser } = useLoadCurrentUser();
const showWelcomeAnimationAfterOnboardingCheckout =
useShowWelcomeAnimationAfterOnboardingCheckout();
const { enqueueErrorSnackBar } = useSnackBar();
@@ -47,23 +39,18 @@ export const PaymentSuccess = () => {
return;
}
- if (isDefined(subscriptionStatus)) {
- showWelcomeAnimationAfterOnboardingCheckout();
- return;
- }
-
- const result = await getCurrentUser();
+ const refreshedWorkspace = await loadCurrentUser()
+ .then(({ workspace }) => workspace)
+ .catch(() => null);
if (cancelled) {
return;
}
- const currentUser = result.data?.currentUser;
const refreshedSubscriptionStatus =
- currentUser?.currentWorkspace?.currentBillingSubscription?.status;
+ refreshedWorkspace?.currentBillingSubscription?.status;
- if (isDefined(currentUser) && isDefined(refreshedSubscriptionStatus)) {
- setCurrentUser(currentUser);
+ if (isDefined(refreshedSubscriptionStatus)) {
showWelcomeAnimationAfterOnboardingCheckout();
return;
}
@@ -93,7 +80,7 @@ export const PaymentSuccess = () => {
}
};
// oxlint-disable-next-line react-hooks/exhaustive-deps
- }, [confirmationRunIndex, subscriptionStatus]);
+ }, [confirmationRunIndex]);
const handleRetry = () => {
setHasTimedOut(false);
diff --git a/packages/twenty-front/src/pages/onboarding/WorkspaceSetup.tsx b/packages/twenty-front/src/pages/onboarding/WorkspaceSetup.tsx
new file mode 100644
index 0000000000..e640945a9f
--- /dev/null
+++ b/packages/twenty-front/src/pages/onboarding/WorkspaceSetup.tsx
@@ -0,0 +1,68 @@
+import { styled } from '@linaria/react';
+import { useLingui } from '@lingui/react/macro';
+import { Navigate } from 'react-router-dom';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+
+import { AiChatMessageListPreambleContext } from '@/ai/contexts/AiChatMessageListPreambleContext';
+import { AiChatTab } from '@/ai/components/AiChatTab';
+import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
+import { WorkspaceSetupChatPreamble } from '@/onboarding/components/WorkspaceSetupChatPreamble';
+import { WorkspaceSetupHeader } from '@/onboarding/components/WorkspaceSetupHeader';
+import { shouldOpenAiChatAfterOnboardingState } from '@/onboarding/states/shouldOpenAiChatAfterOnboardingState';
+import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
+import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
+import { FeatureFlagKey } from '~/generated-metadata/graphql';
+
+const PANEL_CORNER_RADIUS_DERIVED_FROM_THEME_SCALE = `calc(${themeCssVariables.border.radius.md} + ${themeCssVariables.spacing[1]})`;
+
+const StyledPanel = styled.div`
+ background: ${themeCssVariables.background.primary};
+ border-left: 1px solid ${themeCssVariables.border.color.medium};
+ border-radius: ${PANEL_CORNER_RADIUS_DERIVED_FROM_THEME_SCALE} 0 0
+ ${PANEL_CORNER_RADIUS_DERIVED_FROM_THEME_SCALE};
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-width: 0;
+ overflow: hidden;
+`;
+
+const StyledContent = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ min-height: 0;
+ overflow-x: clip;
+ width: 100%;
+`;
+
+export const WorkspaceSetup = () => {
+ const { t } = useLingui();
+ const { defaultHomePagePath } = useDefaultHomePagePath();
+ const isOnboardingAiChatEnabled = useIsFeatureEnabled(
+ FeatureFlagKey.IS_ONBOARDING_AI_CHAT_ENABLED,
+ );
+ const shouldOpenAiChatAfterOnboarding = useAtomStateValue(
+ shouldOpenAiChatAfterOnboardingState,
+ );
+
+ if (!isOnboardingAiChatEnabled) {
+ return ;
+ }
+
+ const title = shouldOpenAiChatAfterOnboarding ? t`Onboarding` : t`Ask AI`;
+ const preamble = shouldOpenAiChatAfterOnboarding ? (
+
+ ) : null;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/onboarding/__tests__/WorkspaceSetup.test.tsx b/packages/twenty-front/src/pages/onboarding/__tests__/WorkspaceSetup.test.tsx
new file mode 100644
index 0000000000..59b3998244
--- /dev/null
+++ b/packages/twenty-front/src/pages/onboarding/__tests__/WorkspaceSetup.test.tsx
@@ -0,0 +1,113 @@
+import { i18n } from '@lingui/core';
+import { I18nProvider } from '@lingui/react';
+import { render } from '@testing-library/react';
+import { Provider as JotaiProvider } from 'jotai';
+import { type ReactNode } from 'react';
+import { SOURCE_LOCALE } from 'twenty-shared/translations';
+
+import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
+import { shouldOpenAiChatAfterOnboardingState } from '@/onboarding/states/shouldOpenAiChatAfterOnboardingState';
+import {
+ jotaiStore,
+ resetJotaiStore,
+} from '@/ui/utilities/state/jotai/jotaiStore';
+import { FeatureFlagKey } from '~/generated-metadata/graphql';
+import { messages } from '~/locales/generated/en';
+import { WorkspaceSetup } from '~/pages/onboarding/WorkspaceSetup';
+import { mockCurrentWorkspace } from '~/testing/mock-data/users';
+
+i18n.load({ [SOURCE_LOCALE]: messages });
+i18n.activate(SOURCE_LOCALE);
+
+const defaultHomePagePath = '/objects/companies';
+
+jest.mock('@/navigation/hooks/useDefaultHomePagePath', () => ({
+ useDefaultHomePagePath: () => ({ defaultHomePagePath }),
+}));
+
+jest.mock('@/ai/components/AiChatTab', () => {
+ const { useContext } = jest.requireActual('react');
+ const { AiChatMessageListPreambleContext } = jest.requireActual(
+ '@/ai/contexts/AiChatMessageListPreambleContext',
+ );
+ return {
+ AiChatTab: () => (
+
+ {useContext(AiChatMessageListPreambleContext)}
+
+ ),
+ };
+});
+
+jest.mock('@/onboarding/components/WorkspaceSetupHeader', () => ({
+ WorkspaceSetupHeader: ({ title }: { title: string }) => (
+ {title}
+ ),
+}));
+
+jest.mock('@/onboarding/components/WorkspaceSetupChatPreamble', () => ({
+ WorkspaceSetupChatPreamble: () => ,
+}));
+
+const mockNavigate = jest.fn();
+jest.mock('react-router-dom', () => ({
+ Navigate: (props: { to: string }) => {
+ mockNavigate(props.to);
+ return ;
+ },
+}));
+
+const setOnboardingAiChatFeatureFlag = (value: boolean) => {
+ jotaiStore.set(currentWorkspaceState.atom, {
+ ...mockCurrentWorkspace,
+ featureFlags: [
+ { key: FeatureFlagKey.IS_ONBOARDING_AI_CHAT_ENABLED, value },
+ ],
+ });
+};
+
+const Wrapper = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+);
+
+describe('WorkspaceSetup', () => {
+ beforeEach(() => {
+ sessionStorage.clear();
+ resetJotaiStore();
+ mockNavigate.mockClear();
+ });
+
+ it('should dress the chat for onboarding when the post-onboarding hint is set', () => {
+ setOnboardingAiChatFeatureFlag(true);
+ jotaiStore.set(shouldOpenAiChatAfterOnboardingState.atom, true);
+
+ const { getByTestId } = render(, { wrapper: Wrapper });
+
+ expect(getByTestId('ai-chat-tab')).toBeInTheDocument();
+ expect(getByTestId('preamble')).toBeInTheDocument();
+ expect(getByTestId('header-title')).toHaveTextContent('Onboarding');
+ });
+
+ it('should render a plain chat when the post-onboarding hint is not set', () => {
+ setOnboardingAiChatFeatureFlag(true);
+
+ const { getByTestId, queryByTestId } = render(, {
+ wrapper: Wrapper,
+ });
+
+ expect(getByTestId('ai-chat-tab')).toBeInTheDocument();
+ expect(queryByTestId('preamble')).not.toBeInTheDocument();
+ expect(getByTestId('header-title')).toHaveTextContent('Ask AI');
+ });
+
+ it('should redirect home when the onboarding ai chat feature flag is disabled', () => {
+ setOnboardingAiChatFeatureFlag(false);
+
+ const { queryByTestId } = render(, { wrapper: Wrapper });
+
+ expect(queryByTestId('ai-chat-tab')).not.toBeInTheDocument();
+ expect(mockNavigate).toHaveBeenCalledWith(defaultHomePagePath);
+ });
+});
diff --git a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
index 0c66c3bb01..4b4de3dc59 100644
--- a/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
+++ b/packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.spec.ts
@@ -247,6 +247,7 @@ describe('WorkspaceEntityManager', () => {
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED: false,
IS_SETTINGS_DISCOVERY_HERO_ENABLED: false,
IS_WORKFLOW_VERSION_IN_CORE_ENABLED: false,
+ IS_ONBOARDING_AI_CHAT_ENABLED: false,
},
userWorkspaceRoleMap: {},
apiKeyRoleMap: {},
diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
index 13f94e9218..88fa3e7399 100644
--- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts
@@ -50,6 +50,11 @@ export const seedFeatureFlags = async ({
workspaceId: workspaceId,
value: false,
},
+ {
+ key: FeatureFlagKey.IS_ONBOARDING_AI_CHAT_ENABLED,
+ workspaceId: workspaceId,
+ value: false,
+ },
])
.execute();
};
diff --git a/packages/twenty-shared/src/types/AppPath.ts b/packages/twenty-shared/src/types/AppPath.ts
index e407e178ab..561eb7a974 100644
--- a/packages/twenty-shared/src/types/AppPath.ts
+++ b/packages/twenty-shared/src/types/AppPath.ts
@@ -17,6 +17,7 @@ export enum AppPath {
BookCall = '/book-call',
// Onboarded
+ WorkspaceSetup = '/workspace-setup',
Index = '/',
TasksPage = '/objects/tasks',
OpportunitiesPage = '/objects/opportunities',
diff --git a/packages/twenty-shared/src/types/FeatureFlagKey.ts b/packages/twenty-shared/src/types/FeatureFlagKey.ts
index 2e441a870f..3ccc7d2707 100644
--- a/packages/twenty-shared/src/types/FeatureFlagKey.ts
+++ b/packages/twenty-shared/src/types/FeatureFlagKey.ts
@@ -7,6 +7,7 @@ export enum FeatureFlagKey {
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
IS_REST_METADATA_API_NEW_FORMAT_DIRECT = 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT',
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED = 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED',
+ IS_ONBOARDING_AI_CHAT_ENABLED = 'IS_ONBOARDING_AI_CHAT_ENABLED',
IS_SETTINGS_DISCOVERY_HERO_ENABLED = 'IS_SETTINGS_DISCOVERY_HERO_ENABLED',
IS_WORKFLOW_VERSION_IN_CORE_ENABLED = 'IS_WORKFLOW_VERSION_IN_CORE_ENABLED',
}