diff --git a/packages/compass-generative-ai/src/atlas-ai-service.ts b/packages/compass-generative-ai/src/atlas-ai-service.ts index d504c1fcc9a..c3529d7cc64 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.ts @@ -281,7 +281,9 @@ export class AtlasAiService { // the user is signed into Atlas and opted in. if (this.apiURLPreset === 'cloud') { - return getStore().dispatch(optIntoGenAIWithModalPrompt({ signal })); + return getStore().dispatch( + optIntoGenAIWithModalPrompt({ signal, isCloudOptIn: true }) + ); } return getStore().dispatch(signIntoAtlasWithModalPrompt({ signal })); } diff --git a/packages/compass-generative-ai/src/components/ai-image-banner.tsx b/packages/compass-generative-ai/src/components/ai-image-banner.tsx index cfba44e0f6e..689aca2cf44 100644 --- a/packages/compass-generative-ai/src/components/ai-image-banner.tsx +++ b/packages/compass-generative-ai/src/components/ai-image-banner.tsx @@ -3,577 +3,262 @@ import { css } from '@mongodb-js/compass-components'; const bannerStyles = css({ display: 'block', - width: 462, - height: 263, + width: 200, + height: 151, }); export const AiImageBanner = () => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + + + + + + + ); }; diff --git a/packages/compass-generative-ai/src/components/ai-optin-modal.spec.tsx b/packages/compass-generative-ai/src/components/ai-optin-modal.spec.tsx index e196bb8c626..dfb47c9cda4 100644 --- a/packages/compass-generative-ai/src/components/ai-optin-modal.spec.tsx +++ b/packages/compass-generative-ai/src/components/ai-optin-modal.spec.tsx @@ -9,6 +9,15 @@ import { PreferencesProvider } from 'compass-preferences-model/provider'; let mockPreferences: PreferencesAccess; describe('AIOptInModal Component', function () { + const baseProps = { + projectId: 'ab123', + isCloudOptIn: true, + isOptInModalVisible: true, + isOptInInProgress: false, + onOptInModalClose: () => {}, + onOptInClick: () => {}, + }; + beforeEach(async function () { mockPreferences = await createSandboxFromDefaultPreferences(); }); @@ -20,37 +29,23 @@ describe('AIOptInModal Component', function () { it('should show the modal title', function () { render( - {}} - onOptInClick={() => {}} - > + ); expect( screen.getByRole('heading', { - name: 'Use natural language to generate queries and pipelines', + name: 'Opt-in Gen AI-Powered features', }) ).to.exist; }); - it('should show the cancel button', function () { + it('should show the not now link', function () { render( - {}} - onOptInClick={() => {}} - > - {' '} - + ); - const button = screen.getByText('Cancel').closest('button'); - expect(button).to.not.match('disabled'); + const link = screen.getByText('Not now'); + expect(link).to.exist; }); it('should show the opt in button enabled when project AI setting is enabled', async function () { @@ -59,19 +54,34 @@ describe('AIOptInModal Component', function () { }); render( - {}} - onOptInClick={() => {}} - > - {' '} - + + + ); + const button = screen.getByText('Opt-in AI features'); + expect(button).to.exist; + }); + + it('should show an info banner in a cloud opt-in', async function () { + await mockPreferences.savePreferences({ + enableGenAIFeaturesAtlasProject: true, + }); + render( + + + + ); + const banner = screen.getByTestId('ai-optin-cloud-banner'); + expect(banner).to.exist; + }); + + it('should not show a banner in non-cloud environment', function () { + render( + + ); - const button = screen.getByText('Use Natural Language').closest('button'); - expect(button?.getAttribute('aria-disabled')).to.equal('false'); + const banner = screen.queryByTestId('ai-optin-cloud-banner'); + expect(banner).to.not.exist; }); it('should disable the opt in button if project AI setting is disabled ', async function () { @@ -80,18 +90,10 @@ describe('AIOptInModal Component', function () { }); render( - {}} - onOptInClick={() => {}} - > - {' '} - + ); - const button = screen.getByText('Use Natural Language').closest('button'); - expect(button?.getAttribute('aria-disabled')).to.equal('true'); + const button = screen.getByText('AI features disabled'); + expect(button).to.exist; }); }); diff --git a/packages/compass-generative-ai/src/components/ai-optin-modal.tsx b/packages/compass-generative-ai/src/components/ai-optin-modal.tsx index 13244680e73..cd79b795c54 100644 --- a/packages/compass-generative-ai/src/components/ai-optin-modal.tsx +++ b/packages/compass-generative-ai/src/components/ai-optin-modal.tsx @@ -4,12 +4,12 @@ import { Banner, Body, Link, - ConfirmationModal, - SpinLoader, + MarketingModal, css, spacing, - H3, palette, + useDarkMode, + SpinLoader, } from '@mongodb-js/compass-components'; import { AiImageBanner } from './ai-image-banner'; import { closeOptInModal, optIn } from '../store/atlas-optin-reducer'; @@ -22,50 +22,42 @@ const GEN_AI_FAQ_LINK = 'https://www.mongodb.com/docs/generative-ai-faq/'; type OptInModalProps = { isOptInModalVisible: boolean; isOptInInProgress: boolean; + isCloudOptIn: boolean; onOptInModalClose: () => void; onOptInClick: () => void; projectId?: string; }; -const titleStyles = css({ - marginBottom: spacing[400], - marginTop: spacing[400], - marginLeft: spacing[500], - marginRight: spacing[500], - textAlign: 'center', -}); - -const bodyStyles = css({ - marginBottom: spacing[400], - marginTop: spacing[400], - marginLeft: spacing[300], - marginRight: spacing[300], - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - textAlign: 'center', -}); - const disclaimerStyles = css({ color: palette.gray.dark1, - marginTop: spacing[400], - marginLeft: spacing[800], - marginRight: spacing[800], + paddingLeft: spacing[800], + paddingRight: spacing[800], }); -const bannerStyles = css({ - padding: spacing[400], - marginTop: spacing[400], - textAlign: 'left', +// TODO: The LG MarketingModal does not provide a way to disable a button +// so this is a temporary workaround to make the button look disabled. +const disableOptInButtonStyles = css({ + button: { + opacity: 0.5, + pointerEvents: 'none', + cursor: 'not-allowed', + }, }); -const getButtonText = (isOptInInProgress: boolean) => { + +const getButtonText = ({ + isOptInInProgress, + darkMode, +}: { + isOptInInProgress: boolean; + darkMode: boolean | undefined; +}) => { return ( <> -  Use Natural Language + Opt-in AI features {isOptInInProgress && ( <>   - + )} @@ -75,12 +67,14 @@ const getButtonText = (isOptInInProgress: boolean) => { export const AIOptInModal: React.FunctionComponent = ({ isOptInModalVisible, isOptInInProgress, + isCloudOptIn, onOptInModalClose, onOptInClick, projectId, }) => { const isProjectAIEnabled = usePreference('enableGenAIFeaturesAtlasProject'); const track = useTelemetry(); + const darkMode = useDarkMode(); const PROJECT_SETTINGS_LINK = projectId ? window.location.origin + '/v2/' + projectId + '#/settings/groupSettings' : null; @@ -92,7 +86,7 @@ export const AIOptInModal: React.FunctionComponent = ({ }, [isOptInModalVisible, track]); const onConfirmClick = () => { - if (isOptInInProgress) { + if (isOptInInProgress || !isProjectAIEnabled) { return; } onOptInClick(); @@ -104,43 +98,22 @@ export const AIOptInModal: React.FunctionComponent = ({ }, [track, onOptInModalClose]); return ( - - - -

- Use natural language to generate queries and pipelines -

- Atlas users can now quickly create queries and aggregations with - MongoDB's  intelligent AI-powered feature, available today. - - {isProjectAIEnabled - ? 'AI features are enabled for project users with data access.' - : 'AI features are disabled for project users.'}{' '} - Project Owners can {isProjectAIEnabled ? 'disable' : 'enable'} Data - Explorer AI features in the{' '} - {PROJECT_SETTINGS_LINK !== null ? ( - - Project Settings - - ) : ( - 'Project Settings' - )} - . - + } + darkMode={darkMode} + disclaimer={
This is a feature powered by generative AI, and may give inaccurate responses. Please see our{' '} @@ -149,8 +122,49 @@ export const AIOptInModal: React.FunctionComponent = ({ {' '} for more information.
+ } + > + + Opt in now MongoDB’s intelligent AI-powered features: +
    +
  • AI Assistant allows you to ask questions across connections
  • +
  • Natural Language Bar to create queries and aggregations
  • + {/*
  • Mock Data Generator to create AI powered sample data
  • */} +
+ {isCloudOptIn && ( + + {isProjectAIEnabled + ? 'AI features are enabled for project users with data access.' + : 'AI features are disabled for project users.'}{' '} + Project Owners can {isProjectAIEnabled ? 'disable' : 'enable'} Data + Explorer AI features in the{' '} + {PROJECT_SETTINGS_LINK !== null ? ( + + Project Settings + + ) : ( + 'Project Settings' + )} + . + + )} -
+ ); }; diff --git a/packages/compass-generative-ai/src/components/plugin.tsx b/packages/compass-generative-ai/src/components/plugin.tsx index d4795d42dc6..fd306399db8 100644 --- a/packages/compass-generative-ai/src/components/plugin.tsx +++ b/packages/compass-generative-ai/src/components/plugin.tsx @@ -5,15 +5,20 @@ import { ConfirmationModalArea } from '@mongodb-js/compass-components'; export interface AtlasAiPluginProps { projectId?: string; + isCloudOptIn: boolean; } export const AtlasAiPlugin: React.FunctionComponent = ({ projectId, + isCloudOptIn, }) => { return ( - + ); }; diff --git a/packages/compass-generative-ai/src/store/atlas-optin-reducer.spec.ts b/packages/compass-generative-ai/src/store/atlas-optin-reducer.spec.ts index 9829e033833..65c84c50210 100644 --- a/packages/compass-generative-ai/src/store/atlas-optin-reducer.spec.ts +++ b/packages/compass-generative-ai/src/store/atlas-optin-reducer.spec.ts @@ -67,7 +67,9 @@ describe('atlasOptInReducer', function () { 'state', 'initial' ); - void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {}); + void store + .dispatch(optIntoGenAIWithModalPrompt({ isCloudOptIn: true })) + .catch(() => {}); await store.dispatch(optIn()); expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).to.have.been .calledOnce; @@ -99,7 +101,9 @@ describe('atlasOptInReducer', function () { 'state', 'initial' ); - void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {}); + void store + .dispatch(optIntoGenAIWithModalPrompt({ isCloudOptIn: true })) + .catch(() => {}); await store.dispatch(optIn()); expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).to.have.been .calledOnce; @@ -122,7 +126,9 @@ describe('atlasOptInReducer', function () { preferences: mockPreferences, }); - void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {}); + void store + .dispatch(optIntoGenAIWithModalPrompt({ isCloudOptIn: true })) + .catch(() => {}); const optInPromise = store.dispatch(optIn()); // Avoid unhandled rejections. AttemptStateMap.get(attemptId)?.promise.catch(() => {}); @@ -170,7 +176,9 @@ describe('atlasOptInReducer', function () { preferences: mockPreferences, }); - void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {}); + void store + .dispatch(optIntoGenAIWithModalPrompt({ isCloudOptIn: true })) + .catch(() => {}); await Promise.all([ store.dispatch(optIn()), @@ -194,7 +202,9 @@ describe('atlasOptInReducer', function () { preferences: mockPreferences, }); - const optInPromise = store.dispatch(optIntoGenAIWithModalPrompt()); + const optInPromise = store.dispatch( + optIntoGenAIWithModalPrompt({ isCloudOptIn: true }) + ); await store.dispatch(optIn()); await optInPromise; @@ -211,7 +221,9 @@ describe('atlasOptInReducer', function () { preferences: mockPreferences, }); - const optInPromise = store.dispatch(optIntoGenAIWithModalPrompt()); + const optInPromise = store.dispatch( + optIntoGenAIWithModalPrompt({ isCloudOptIn: true }) + ); await store.dispatch(optIn()); try { @@ -234,7 +246,9 @@ describe('atlasOptInReducer', function () { preferences: mockPreferences, }); - const optInPromise = store.dispatch(optIntoGenAIWithModalPrompt()); + const optInPromise = store.dispatch( + optIntoGenAIWithModalPrompt({ isCloudOptIn: true }) + ); store.dispatch(closeOptInModal(new Error('This operation was aborted'))); try { @@ -259,7 +273,10 @@ describe('atlasOptInReducer', function () { const c = new AbortController(); const optInPromise = store.dispatch( - optIntoGenAIWithModalPrompt({ signal: c.signal }) + optIntoGenAIWithModalPrompt({ + signal: c.signal, + isCloudOptIn: true, + }) ); c.abort(new Error('Aborted from outside')); diff --git a/packages/compass-generative-ai/src/store/atlas-optin-reducer.ts b/packages/compass-generative-ai/src/store/atlas-optin-reducer.ts index 1378c26a093..4ea5ab12b17 100644 --- a/packages/compass-generative-ai/src/store/atlas-optin-reducer.ts +++ b/packages/compass-generative-ai/src/store/atlas-optin-reducer.ts @@ -16,6 +16,7 @@ type AttemptState = { export type AtlasOptInState = { error: string | null; + isCloudOptIn: boolean; isModalOpen: boolean; attemptId: number | null; state: 'initial' | 'in-progress' | 'error' | 'canceled' | 'optin-success'; @@ -62,6 +63,7 @@ export type AtlasOptInAttemptEndAction = { export type AtlasOptInStartAction = { type: AtlasOptInActions.Start; + isCloudOptIn: boolean; }; export type AtlasOptInSuccessAction = { @@ -75,8 +77,9 @@ export type AtlasOptInErrorAction = { export type AtlasOptInCancelAction = { type: AtlasOptInActions.Cancel }; -const INITIAL_STATE = { +const INITIAL_STATE: AtlasOptInState = { state: 'initial' as const, + isCloudOptIn: true, error: null, isModalOpen: false, attemptId: null, @@ -142,7 +145,11 @@ const optInReducer: Reducer = ( } if (isAction(action, AtlasOptInActions.Start)) { - return { ...state, state: 'in-progress' }; + return { + ...state, + state: 'in-progress', + isCloudOptIn: action.isCloudOptIn, + }; } if ( @@ -220,9 +227,11 @@ const startAttempt = ( export const optIntoGenAIWithModalPrompt = ({ signal, -}: { signal?: AbortSignal } = {}): GenAIAtlasOptInThunkAction< - Promise -> => { + isCloudOptIn, +}: { + signal?: AbortSignal; + isCloudOptIn: boolean; +}): GenAIAtlasOptInThunkAction> => { return (dispatch, getState, { preferences }) => { // Nothing to do if we already opted in. const { state } = getState().optIn; @@ -235,7 +244,7 @@ export const optIntoGenAIWithModalPrompt = ({ } const attempt = dispatch( startAttempt(() => { - dispatch(openOptInModal()); + dispatch(openOptInModal({ isCloudOptIn })); }) ); signal?.addEventListener('abort', () => { @@ -261,6 +270,7 @@ export const optIn = (): GenAIAtlasOptInThunkAction> => { } = getAttempt(getState().optIn.attemptId); dispatch({ type: AtlasOptInActions.Start, + isCloudOptIn: getState().optIn.isCloudOptIn, }); try { @@ -281,8 +291,8 @@ export const optIn = (): GenAIAtlasOptInThunkAction> => { }; }; -export const openOptInModal = () => { - return { type: AtlasOptInActions.OpenOptInModal }; +export const openOptInModal = ({ isCloudOptIn }: { isCloudOptIn: boolean }) => { + return { type: AtlasOptInActions.OpenOptInModal, isCloudOptIn }; }; export const closeOptInModal = (