diff --git a/package-lock.json b/package-lock.json index b5fee62c049..3d4ba390496 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47012,6 +47012,7 @@ "@mongodb-js/compass-app-stores": "^7.57.0", "@mongodb-js/compass-components": "^1.49.0", "@mongodb-js/compass-connections": "^1.71.0", + "@mongodb-js/compass-generative-ai": "^0.51.0", "@mongodb-js/compass-logging": "^1.7.12", "@mongodb-js/compass-telemetry": "^1.14.0", "@mongodb-js/compass-workspaces": "^0.52.0", @@ -60268,6 +60269,7 @@ "@mongodb-js/compass-app-stores": "^7.57.0", "@mongodb-js/compass-components": "^1.49.0", "@mongodb-js/compass-connections": "^1.71.0", + "@mongodb-js/compass-generative-ai": "^0.51.0", "@mongodb-js/compass-logging": "^1.7.12", "@mongodb-js/compass-telemetry": "^1.14.0", "@mongodb-js/compass-workspaces": "^0.52.0", diff --git a/packages/compass-collection/package.json b/packages/compass-collection/package.json index 9d18ed0513e..1f9ae664228 100644 --- a/packages/compass-collection/package.json +++ b/packages/compass-collection/package.json @@ -52,6 +52,7 @@ "@mongodb-js/compass-app-stores": "^7.57.0", "@mongodb-js/compass-components": "^1.49.0", "@mongodb-js/compass-connections": "^1.71.0", + "@mongodb-js/compass-generative-ai": "^0.51.0", "@mongodb-js/compass-logging": "^1.7.12", "@mongodb-js/compass-telemetry": "^1.14.0", "@mongodb-js/compass-workspaces": "^0.52.0", diff --git a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx index 6cc4d4da7bc..0a7b3118ce8 100644 --- a/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx +++ b/packages/compass-collection/src/components/collection-header/collection-header.spec.tsx @@ -26,7 +26,7 @@ function renderCollectionHeader( const mockStore = createStore(() => ({ mockDataGenerator: { isModalOpen: false, - currentStep: MockDataGeneratorStep.AI_DISCLAIMER, + currentStep: MockDataGeneratorStep.SCHEMA_CONFIRMATION, }, })); diff --git a/packages/compass-collection/src/components/collection-header/collection-header.tsx b/packages/compass-collection/src/components/collection-header/collection-header.tsx index 415d3c6901b..6af9fea8dd7 100644 --- a/packages/compass-collection/src/components/collection-header/collection-header.tsx +++ b/packages/compass-collection/src/components/collection-header/collection-header.tsx @@ -20,7 +20,7 @@ import { useConnectionInfo } from '@mongodb-js/compass-connections/provider'; import { getConnectionTitle } from '@mongodb-js/connection-info'; import MockDataGeneratorModal from '../mock-data-generator-modal/mock-data-generator-modal'; import { connect } from 'react-redux'; -import { mockDataGeneratorModalOpened } from '../../modules/collection-tab'; +import { openMockDataGeneratorModal } from '../../modules/collection-tab'; const collectionHeaderStyles = css({ padding: spacing[400], @@ -182,7 +182,7 @@ const CollectionHeader: React.FunctionComponent = ({ }; const ConnectedCollectionHeader = connect(undefined, { - onOpenMockDataModal: mockDataGeneratorModalOpened, + onOpenMockDataModal: openMockDataGeneratorModal, })(CollectionHeader); export default ConnectedCollectionHeader; diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts b/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts index 5057ee5946a..942d9f905a0 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/constants.ts @@ -1,7 +1,6 @@ import { MockDataGeneratorStep } from './types'; export const StepButtonLabelMap = { - [MockDataGeneratorStep.AI_DISCLAIMER]: 'Use Natural Language', [MockDataGeneratorStep.SCHEMA_CONFIRMATION]: 'Confirm', [MockDataGeneratorStep.SCHEMA_EDITOR]: 'Next', [MockDataGeneratorStep.DOCUMENT_COUNT]: 'Next', diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx index 25550d79a8e..9caf475f8a0 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx +++ b/packages/compass-collection/src/components/mock-data-generator-modal/mock-data-generator-modal.spec.tsx @@ -23,7 +23,7 @@ describe('MockDataGeneratorModal', () => { function renderModal({ isOpen = true, - currentStep = MockDataGeneratorStep.AI_DISCLAIMER, + currentStep = MockDataGeneratorStep.SCHEMA_CONFIRMATION, } = {}) { return render( diff --git a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts index 6394dc25e2d..c5a45271504 100644 --- a/packages/compass-collection/src/components/mock-data-generator-modal/types.ts +++ b/packages/compass-collection/src/components/mock-data-generator-modal/types.ts @@ -1,5 +1,4 @@ export enum MockDataGeneratorStep { - AI_DISCLAIMER = 'AI_DISCLAIMER', SCHEMA_CONFIRMATION = 'SCHEMA_CONFIRMATION', SCHEMA_EDITOR = 'SCHEMA_EDITOR', DOCUMENT_COUNT = 'DOCUMENT_COUNT', diff --git a/packages/compass-collection/src/index.ts b/packages/compass-collection/src/index.ts index 90f8281e51c..5ba6370bc36 100644 --- a/packages/compass-collection/src/index.ts +++ b/packages/compass-collection/src/index.ts @@ -18,6 +18,7 @@ import { CollectionWorkspaceTitle, CollectionPluginTitleComponent, } from './plugin-tab-title'; +import { atlasAiServiceLocator } from '@mongodb-js/compass-generative-ai/provider'; export const WorkspaceTab: WorkspacePlugin = { name: CollectionWorkspaceTitle, @@ -37,6 +38,7 @@ export const WorkspaceTab: WorkspacePlugin = { connectionInfoRef: connectionInfoRefLocator, logger: createLoggerLocator('COMPASS-COLLECTION'), preferences: preferencesLocator, + atlasAiService: atlasAiServiceLocator, } ), content: CollectionTab, diff --git a/packages/compass-collection/src/modules/collection-tab.ts b/packages/compass-collection/src/modules/collection-tab.ts index 874e4026035..746632abe9e 100644 --- a/packages/compass-collection/src/modules/collection-tab.ts +++ b/packages/compass-collection/src/modules/collection-tab.ts @@ -10,6 +10,8 @@ import type { DataService } from '@mongodb-js/compass-connections/provider'; import type { experimentationServiceLocator } from '@mongodb-js/compass-telemetry/provider'; import { type Logger, mongoLogId } from '@mongodb-js/compass-logging/provider'; import { type PreferencesAccess } from 'compass-preferences-model/provider'; +import type { AtlasAiService } from '@mongodb-js/compass-generative-ai/provider'; + import { isInternalFieldPath } from 'hadron-document'; import toNS from 'mongodb-ns'; import { @@ -24,11 +26,11 @@ import { import { calculateSchemaDepth } from '../calculate-schema-depth'; import { processSchema } from '../transform-schema-to-field-info'; import type { Document, MongoError } from 'mongodb'; +import { MockDataGeneratorStep } from '../components/mock-data-generator-modal/types'; const DEFAULT_SAMPLE_SIZE = 100; const NO_DOCUMENTS_ERROR = 'No documents found in the collection to analyze.'; -import { MockDataGeneratorStep } from '../components/mock-data-generator-modal/types'; function isAction( action: AnyAction, @@ -65,6 +67,7 @@ type CollectionThunkAction = ThunkAction< experimentationServices: ReturnType; logger: Logger; preferences: PreferencesAccess; + atlasAiService: AtlasAiService; }, A >; @@ -148,7 +151,7 @@ const reducer: Reducer = ( }, mockDataGenerator: { isModalOpen: false, - currentStep: MockDataGeneratorStep.AI_DISCLAIMER, + currentStep: MockDataGeneratorStep.SCHEMA_CONFIRMATION, }, }, action @@ -236,7 +239,7 @@ const reducer: Reducer = ( mockDataGenerator: { ...state.mockDataGenerator, isModalOpen: true, - currentStep: MockDataGeneratorStep.AI_DISCLAIMER, + currentStep: MockDataGeneratorStep.SCHEMA_CONFIRMATION, }, }; } @@ -266,9 +269,6 @@ const reducer: Reducer = ( let nextStep: MockDataGeneratorStep; switch (currentStep) { - case MockDataGeneratorStep.AI_DISCLAIMER: - nextStep = MockDataGeneratorStep.SCHEMA_CONFIRMATION; - break; case MockDataGeneratorStep.SCHEMA_CONFIRMATION: nextStep = MockDataGeneratorStep.SCHEMA_EDITOR; break; @@ -305,7 +305,8 @@ const reducer: Reducer = ( switch (currentStep) { case MockDataGeneratorStep.SCHEMA_CONFIRMATION: - previousStep = MockDataGeneratorStep.AI_DISCLAIMER; + // TODO: Decide with product what we want behavior to be: close modal? Re-open disclaimer modal, if possible? + previousStep = MockDataGeneratorStep.SCHEMA_CONFIRMATION; break; case MockDataGeneratorStep.SCHEMA_EDITOR: previousStep = MockDataGeneratorStep.SCHEMA_CONFIRMATION; @@ -372,6 +373,27 @@ export const selectTab = ( }; }; +export const openMockDataGeneratorModal = (): CollectionThunkAction< + Promise +> => { + return async (dispatch, _getState, { atlasAiService, logger }) => { + try { + if (process.env.COMPASS_E2E_SKIP_ATLAS_SIGNIN !== 'true') { + await atlasAiService.ensureAiFeatureAccess(); + } + dispatch(mockDataGeneratorModalOpened()); + } catch (error) { + // if failed or user canceled we just don't show the modal + logger.log.error( + mongoLogId(1_001_000_364), + 'Collections', + 'Failed to ensure AI feature access and open mock data generator modal', + error + ); + } + }; +}; + export const analyzeCollectionSchema = (): CollectionThunkAction< Promise > => { diff --git a/packages/compass-collection/src/stores/collection-tab.spec.ts b/packages/compass-collection/src/stores/collection-tab.spec.ts index d200d24b0d9..4813d13bc66 100644 --- a/packages/compass-collection/src/stores/collection-tab.spec.ts +++ b/packages/compass-collection/src/stores/collection-tab.spec.ts @@ -63,6 +63,7 @@ describe('Collection Tab Content store', function () { .stub(collectionTabModule, 'analyzeCollectionSchema') .returns(async () => {}); const dataService = {} as any; + const atlasAiService = {} as any; let store: ReturnType['store']; let deactivate: ReturnType['deactivate']; @@ -106,6 +107,7 @@ describe('Collection Tab Content store', function () { connectionInfoRef: connectionInfoRef as any, logger, preferences, + atlasAiService, }, { on() {}, cleanup() {} } as any )); diff --git a/packages/compass-collection/src/stores/collection-tab.ts b/packages/compass-collection/src/stores/collection-tab.ts index 68e20c0dd4e..db718545ec9 100644 --- a/packages/compass-collection/src/stores/collection-tab.ts +++ b/packages/compass-collection/src/stores/collection-tab.ts @@ -22,6 +22,7 @@ import { } from 'compass-preferences-model/provider'; import { ExperimentTestName } from '@mongodb-js/compass-telemetry/provider'; import { SCHEMA_ANALYSIS_STATE_INITIAL } from '../schema-analysis-types'; +import type { AtlasAiService } from '@mongodb-js/compass-generative-ai/provider'; export type CollectionTabOptions = { /** @@ -48,6 +49,7 @@ export type CollectionTabServices = { connectionInfoRef: ReturnType; logger: Logger; preferences: PreferencesAccess; + atlasAiService: AtlasAiService; }; export function activatePlugin( @@ -67,6 +69,7 @@ export function activatePlugin( connectionInfoRef, logger, preferences, + atlasAiService, } = services; if (!collectionModel) { @@ -87,7 +90,7 @@ export function activatePlugin( }, mockDataGenerator: { isModalOpen: false, - currentStep: MockDataGeneratorStep.AI_DISCLAIMER, + currentStep: MockDataGeneratorStep.SCHEMA_CONFIRMATION, }, }, applyMiddleware( @@ -98,6 +101,7 @@ export function activatePlugin( experimentationServices, logger, preferences, + atlasAiService, }) ) ); diff --git a/packages/compass-components/src/components/modals/marketing-modal.tsx b/packages/compass-components/src/components/modals/marketing-modal.tsx new file mode 100644 index 00000000000..6277d545861 --- /dev/null +++ b/packages/compass-components/src/components/modals/marketing-modal.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { + Body, + MarketingModal as LeafyGreenMarketingModal, +} from '../leafygreen'; +import { withStackedComponentStyles } from '../../hooks/use-stacked-component'; + +function MarketingModal({ + children, + ...props +}: React.ComponentProps): React.ReactElement { + return ( + + {children} + + ); +} + +export default withStackedComponentStyles(MarketingModal); diff --git a/packages/compass-components/src/index.ts b/packages/compass-components/src/index.ts index d43122604d6..7e4431ab956 100644 --- a/packages/compass-components/src/index.ts +++ b/packages/compass-components/src/index.ts @@ -14,6 +14,7 @@ export { cache, } from '@leafygreen-ui/emotion'; import ConfirmationModal from './components/modals/confirmation-modal'; +import MarketingModal from './components/modals/marketing-modal'; import type { ElectronFileDialogOptions, ElectronShowFileDialogProvider, @@ -131,6 +132,7 @@ export { defaultSidebarWidth, createElectronFileInputBackend, createJSDomFileInputDummyBackend, + MarketingModal, }; export { useFocusState, diff --git a/packages/compass-generative-ai/src/atlas-ai-service.ts b/packages/compass-generative-ai/src/atlas-ai-service.ts index 8c5a39b1aa1..fe3788d3b96 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.ts @@ -278,14 +278,18 @@ export class AtlasAiService { async ensureAiFeatureAccess({ signal }: { signal?: AbortSignal } = {}) { if (this.preferences.getPreferences().enableUnauthenticatedGenAI) { - return getStore().dispatch(optIntoGenAIWithModalPrompt({ signal })); + return getStore().dispatch( + optIntoGenAIWithModalPrompt({ signal, isCloudOptIn: false }) + ); } // When the ai feature is attempted to be opened we make sure // 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..2b2b3f11165 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,452 @@ import { css } from '@mongodb-js/compass-components'; const bannerStyles = css({ display: 'block', - width: 462, - height: 263, + width: 270, + height: 180, }); 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..8e624f935c3 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 @@ -1,56 +1,113 @@ +import { render, screen } from '@mongodb-js/testing-library-compass'; import React from 'react'; -import { render, screen, cleanup } from '@mongodb-js/testing-library-compass'; import { expect } from 'chai'; import { AIOptInModal } from './ai-optin-modal'; import type { PreferencesAccess } from 'compass-preferences-model'; import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; import { PreferencesProvider } from 'compass-preferences-model/provider'; +import Sinon from 'sinon'; let mockPreferences: PreferencesAccess; describe('AIOptInModal Component', function () { + const sandbox = Sinon.createSandbox(); + const onOptInClickStub = sandbox.stub(); + + const baseProps = { + projectId: 'ab123', + isCloudOptIn: true, + isOptInModalVisible: true, + isOptInInProgress: false, + onOptInModalClose: () => {}, + onOptInClick: onOptInClickStub, + }; + beforeEach(async function () { mockPreferences = await createSandboxFromDefaultPreferences(); }); afterEach(function () { - cleanup(); + sandbox.restore(); }); - it('should show the modal title', function () { - render( - - {}} - onOptInClick={() => {}} - > - - ); - expect( - screen.getByRole('heading', { - name: 'Use natural language to generate queries and pipelines', - }) - ).to.exist; + describe('with cloud opt-in environment', function () { + it('should show the correct modal title and description', function () { + render( + + + + ); + expect( + screen.getByRole('heading', { + name: 'Use AI Features in Data Explorer', + }) + ).to.exist; + expect( + screen.getByText( + 'AI-powered features in Data Explorer supply users with an intelligent toolset to build faster and smarter with MongoDB.' + ) + ).to.exist; + }); + + it('should show an info banner', async function () { + await mockPreferences.savePreferences({ + enableGenAIFeaturesAtlasProject: true, + }); + + render( + + + + ); + + const banner = screen.getByTestId('ai-optin-cloud-banner'); + expect(banner).to.exist; + }); + + it('should show the Use AI Features and Not now buttons', function () { + render( + + + + ); + expect(screen.getByText('Use AI Features')).to.exist; + expect(screen.getByText('Not now')).to.exist; + }); }); - it('should show the cancel button', function () { - render( - - {}} - onOptInClick={() => {}} - > - {' '} - - - ); - const button = screen.getByText('Cancel').closest('button'); - expect(button).to.not.match('disabled'); + + describe('with non-cloud opt-in environment', function () { + it('should show the correct modal title and not show the banner', function () { + render( + + + + ); + expect(screen.getByText('Use AI Features in Compass')).to.exist; + expect( + screen.getByText( + 'AI-powered features in Compass supply users with an intelligent toolset to build faster and smarter with MongoDB.' + ) + ).to.exist; + }); + + it('should not show the banner', function () { + render( + + + + ); + expect(screen.queryByTestId('ai-optin-cloud-banner')).to.not.exist; + }); + + it('should show the Use AI Features and Not now buttons', function () { + render( + + + + ); + expect(screen.getByText('Use AI Features')).to.exist; + expect(screen.getByText('Not now')).to.exist; + }); }); it('should show the opt in button enabled when project AI setting is enabled', async function () { @@ -59,39 +116,104 @@ describe('AIOptInModal Component', function () { }); render( - {}} - onOptInClick={() => {}} - > - {' '} - + ); - const button = screen.getByText('Use Natural Language').closest('button'); - expect(button?.getAttribute('aria-disabled')).to.equal('false'); + const button = screen.getByText('Use AI Features').closest('button'); + expect(button?.style.cursor).to.not.equal('not-allowed'); }); - it('should disable the opt in button if project AI setting is disabled ', async function () { - await mockPreferences.savePreferences({ - enableGenAIFeaturesAtlasProject: false, + describe('conditional banner messages', function () { + it('should show warning banner when AI features are disabled', async function () { + await mockPreferences.savePreferences({ + enableGenAIFeaturesAtlasProject: false, + enableGenAISampleDocumentPassingOnAtlasProject: false, + }); + render( + + + + ); + expect( + screen.getByText( + /AI features are disabled for project users with data access/ + ) + ).to.exist; + expect( + screen.getByText(/Project Owners can enable Data Explorer AI features/) + ).to.exist; + }); + + it('should show info banner with correct copy when only the "Sending Sample Field Values in DE Gen AI Features" setting is disabled', async function () { + await mockPreferences.savePreferences({ + enableGenAIFeaturesAtlasProject: true, + enableGenAISampleDocumentPassingOnAtlasProject: false, + }); + render( + + + + ); + expect( + screen.getByText( + /AI features are enabled for project users with data access/ + ) + ).to.exist; + expect( + screen.getByText( + /enable sending sample field values in Data Explorer AI features/ + ) + ).to.exist; + }); + + it('should show info banner with correct copy when both project settings are enabled', async function () { + await mockPreferences.savePreferences({ + enableGenAIFeaturesAtlasProject: true, + enableGenAISampleDocumentPassingOnAtlasProject: true, + }); + render( + + + + ); + expect( + screen.getByText( + /AI features are enabled for project users with data access/ + ) + ).to.exist; + expect( + screen.getByText(/Project Owners can disable Data Explorer AI features/) + ).to.exist; + }); + }); + + describe('button click behavior', function () { + it('should not call onOptInClick when main AI features are disabled', async function () { + await mockPreferences.savePreferences({ + enableGenAIFeaturesAtlasProject: false, + }); + render( + + + + ); + const button = screen.getByText('Use AI Features'); + button.click(); + expect(onOptInClickStub).not.to.have.been.called; + }); + + it('should call onOptInClick when main AI features are enabled', async function () { + await mockPreferences.savePreferences({ + enableGenAIFeaturesAtlasProject: true, + }); + render( + + + + ); + const button = screen.getByText('Use AI Features'); + button.click(); + expect(onOptInClickStub).to.have.been.calledOnce; }); - render( - - {}} - onOptInClick={() => {}} - > - {' '} - - - ); - const button = screen.getByText('Use Natural Language').closest('button'); - expect(button?.getAttribute('aria-disabled')).to.equal('true'); }); }); 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..562a7f2e971 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,13 @@ import { Banner, Body, Link, - ConfirmationModal, - SpinLoader, css, spacing, - H3, palette, + Theme, + useDarkMode, + MarketingModal, + cx, } from '@mongodb-js/compass-components'; import { AiImageBanner } from './ai-image-banner'; import { closeOptInModal, optIn } from '../store/atlas-optin-reducer'; @@ -22,21 +23,13 @@ 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], @@ -46,6 +39,14 @@ const bodyStyles = css({ textAlign: 'center', }); +const bodyLightThemeStyles = css({ + color: palette.gray.dark1, +}); + +const bodyDarkThemeStyles = css({ + color: palette.gray.light2, +}); + const disclaimerStyles = css({ color: palette.gray.dark1, marginTop: spacing[400], @@ -54,20 +55,100 @@ const disclaimerStyles = css({ }); const bannerStyles = css({ + width: '480px', padding: spacing[400], marginTop: spacing[400], textAlign: 'left', }); -const getButtonText = (isOptInInProgress: boolean) => { + +// TODO: The LG MarketingModal does not provide a way to disable the button +// so this is a temporary workaround to make the button look disabled. +const leafyGreenButtonSelector = + 'button[data-lgid="lg-button"]:not([aria-label="Close modal"])'; +const focusSelector = `&:focus-visible, &[data-focus="true"]`; +const hoverSelector = `&:hover, &[data-hover="true"]`; +const activeSelector = `&:active, &[data-active="true"]`; +const focusBoxShadow = (color: string) => ` + 0 0 0 2px ${color}, + 0 0 0 4px ${palette.blue.light1}; +`; +const disabledButtonStyles: Record = { + [Theme.Light]: css({ + [leafyGreenButtonSelector]: { + [`&, ${hoverSelector}, ${activeSelector}`]: { + backgroundColor: palette.gray.light2, + borderColor: palette.gray.light1, + color: palette.gray.base, + boxShadow: 'none', + cursor: 'not-allowed', + }, + + [focusSelector]: { + color: palette.gray.base, + boxShadow: focusBoxShadow(palette.white), + }, + }, + }), + + [Theme.Dark]: css({ + [leafyGreenButtonSelector]: { + [`&, ${hoverSelector}, ${activeSelector}`]: { + backgroundColor: palette.gray.dark3, + borderColor: palette.gray.dark2, + color: palette.gray.dark1, + boxShadow: 'none', + cursor: 'not-allowed', + }, + + [focusSelector]: { + color: palette.gray.dark1, + boxShadow: focusBoxShadow(palette.black), + }, + }, + }), +}; + +const CloudAIOptInBannerContent: React.FunctionComponent<{ + isProjectAIEnabled: boolean; + isSampleDocumentPassingEnabled: boolean; + projectId?: string; +}> = ({ isProjectAIEnabled, isSampleDocumentPassingEnabled, projectId }) => { + const projectSettingsLink = projectId ? ( + + Project Settings + + ) : ( + 'Project Settings' + ); + if (!isProjectAIEnabled) { + // Both disabled case (main AI features disabled) + return ( + <> + AI features are disabled for project users with data access. Project + Owners can enable Data Explorer AI features in {projectSettingsLink}. + + ); + } else if (!isSampleDocumentPassingEnabled) { + // Only sample values disabled case + return ( + <> + AI features are enabled for project users with data access. Project + Owners can disable these features or enable sending sample field values + in Data Explorer AI features to improve their accuracy in{' '} + {projectSettingsLink}. + + ); + } return ( <> -  Use Natural Language - {isOptInInProgress && ( - <> -   - - - )} + AI features are enabled for project users with data access. Project Owners + can disable Data Explorer AI features in {projectSettingsLink}. ); }; @@ -75,15 +156,19 @@ const getButtonText = (isOptInInProgress: boolean) => { export const AIOptInModal: React.FunctionComponent = ({ isOptInModalVisible, isOptInInProgress, + isCloudOptIn, onOptInModalClose, onOptInClick, projectId, }) => { const isProjectAIEnabled = usePreference('enableGenAIFeaturesAtlasProject'); + const isSampleDocumentPassingEnabled = usePreference( + 'enableGenAISampleDocumentPassingOnAtlasProject' + ); const track = useTelemetry(); - const PROJECT_SETTINGS_LINK = projectId - ? window.location.origin + '/v2/' + projectId + '#/settings/groupSettings' - : null; + const darkMode = useDarkMode(); + const currentDisabledButtonStyles = + disabledButtonStyles[darkMode ? Theme.Dark : Theme.Light]; useEffect(() => { if (isOptInModalVisible) { @@ -92,7 +177,7 @@ export const AIOptInModal: React.FunctionComponent = ({ }, [isOptInModalVisible, track]); const onConfirmClick = () => { - if (isOptInInProgress) { + if (isOptInInProgress || !isProjectAIEnabled) { return; } onOptInClick(); @@ -104,43 +189,20 @@ 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' - )} - . - + onClose={handleModalClose} + // TODO Button Disabling + className={!isProjectAIEnabled ? currentDisabledButtonStyles : undefined} + buttonText="Use AI Features" + onButtonClick={onConfirmClick} + linkText="Not now" + onLinkClick={onOptInModalClose} + graphic={} + disclaimer={
This is a feature powered by generative AI, and may give inaccurate responses. Please see our{' '} @@ -149,8 +211,32 @@ export const AIOptInModal: React.FunctionComponent = ({ {' '} for more information.
+ } + > + + AI-powered features in {isCloudOptIn ? 'Data Explorer' : 'Compass'}{' '} + supply users with an intelligent toolset to build faster and smarter + with MongoDB. + {isCloudOptIn && ( + + + + )} -
+ ); }; 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 947842f8054..7cebbb2fe9d 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 @@ -66,7 +66,9 @@ describe('atlasOptInReducer', function () { 'state', 'initial' ); - void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {}); + void store + .dispatch(optIntoGenAIWithModalPrompt({ isCloudOptIn: true })) + .catch(() => {}); await store.dispatch(optIn()); expect(mockAtlasAiService.optIntoGenAIFeatures).to.have.been.calledOnce; expect(store.getState().optIn).to.have.nested.property( @@ -97,7 +99,9 @@ describe('atlasOptInReducer', function () { 'state', 'initial' ); - void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {}); + void store + .dispatch(optIntoGenAIWithModalPrompt({ isCloudOptIn: true })) + .catch(() => {}); await store.dispatch(optIn()); expect(mockAtlasAiService.optIntoGenAIFeatures).to.have.been.calledOnce; expect(store.getState().optIn).to.have.nested.property( @@ -117,7 +121,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(() => {}); @@ -164,7 +170,9 @@ describe('atlasOptInReducer', function () { preferences: mockPreferences, }); - void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {}); + void store + .dispatch(optIntoGenAIWithModalPrompt({ isCloudOptIn: true })) + .catch(() => {}); await Promise.all([ store.dispatch(optIn()), @@ -188,7 +196,9 @@ describe('atlasOptInReducer', function () { preferences: mockPreferences, }); - const optInPromise = store.dispatch(optIntoGenAIWithModalPrompt()); + const optInPromise = store.dispatch( + optIntoGenAIWithModalPrompt({ isCloudOptIn: true }) + ); await store.dispatch(optIn()); await optInPromise; @@ -205,7 +215,9 @@ describe('atlasOptInReducer', function () { preferences: mockPreferences, }); - const optInPromise = store.dispatch(optIntoGenAIWithModalPrompt()); + const optInPromise = store.dispatch( + optIntoGenAIWithModalPrompt({ isCloudOptIn: true }) + ); await store.dispatch(optIn()); try { @@ -228,7 +240,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 { @@ -253,7 +267,7 @@ 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 f653f815eb4..a2268e13ad5 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 = { @@ -77,6 +79,7 @@ export type AtlasOptInCancelAction = { type: AtlasOptInActions.Cancel }; const INITIAL_STATE = { 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 = ( diff --git a/packages/compass-web/src/entrypoint.tsx b/packages/compass-web/src/entrypoint.tsx index d224c38426c..ce30909482e 100644 --- a/packages/compass-web/src/entrypoint.tsx +++ b/packages/compass-web/src/entrypoint.tsx @@ -429,7 +429,10 @@ const CompassWeb = ({ > - + diff --git a/packages/compass/src/app/components/home.tsx b/packages/compass/src/app/components/home.tsx index da6b6562f34..763a4dc9a5a 100644 --- a/packages/compass/src/app/components/home.tsx +++ b/packages/compass/src/app/components/home.tsx @@ -121,7 +121,9 @@ function Home({ - +