Skip to content

chore(compass-generative-ai): update Gen AI opt-in modal COMPASS-9593 #7164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/compass-generative-ai/src/atlas-ai-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
}
Expand Down
803 changes: 244 additions & 559 deletions packages/compass-generative-ai/src/components/ai-image-banner.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand All @@ -20,37 +29,23 @@ describe('AIOptInModal Component', function () {
it('should show the modal title', function () {
render(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal
projectId="ab123"
isOptInModalVisible={true}
isOptInInProgress={false}
onOptInModalClose={() => {}}
onOptInClick={() => {}}
></AIOptInModal>
<AIOptInModal {...baseProps}></AIOptInModal>
</PreferencesProvider>
);
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(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal
projectId="ab123"
isOptInModalVisible={true}
isOptInInProgress={false}
onOptInModalClose={() => {}}
onOptInClick={() => {}}
>
{' '}
</AIOptInModal>
<AIOptInModal {...baseProps}></AIOptInModal>
</PreferencesProvider>
);
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 () {
Expand All @@ -59,19 +54,34 @@ describe('AIOptInModal Component', function () {
});
render(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal
projectId="ab123"
isOptInModalVisible={true}
isOptInInProgress={false}
onOptInModalClose={() => {}}
onOptInClick={() => {}}
>
{' '}
</AIOptInModal>
<AIOptInModal {...baseProps}></AIOptInModal>
</PreferencesProvider>
);
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(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal {...baseProps} isCloudOptIn={true}></AIOptInModal>
</PreferencesProvider>
);
const banner = screen.getByTestId('ai-optin-cloud-banner');
expect(banner).to.exist;
});

it('should not show a banner in non-cloud environment', function () {
render(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal {...baseProps} isCloudOptIn={false}></AIOptInModal>
</PreferencesProvider>
);
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 () {
Expand All @@ -80,18 +90,10 @@ describe('AIOptInModal Component', function () {
});
render(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal
projectId="ab123"
isOptInModalVisible={true}
isOptInInProgress={false}
onOptInModalClose={() => {}}
onOptInClick={() => {}}
>
{' '}
</AIOptInModal>
<AIOptInModal {...baseProps}></AIOptInModal>
</PreferencesProvider>
);
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;
});
});
156 changes: 85 additions & 71 deletions packages/compass-generative-ai/src/components/ai-optin-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seemed like the best solution, I can open a LG ticket and link it here if this sounds good.

button: {
opacity: 0.5,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would we want to try to match the LG disabled styles, as in https://github.com/mongodb/leafygreen-ui/blob/f9f9ed34f21b561986c46c502cf13ef82a0e7c3c/packages/button/src/Button/Button.styles.ts#L360?

Using opacity here seems a bit odd and probably makes the button look a bit off compared to other disabled buttons?

pointerEvents: 'none',
cursor: 'not-allowed',
},
});
const getButtonText = (isOptInInProgress: boolean) => {

const getButtonText = ({
isOptInInProgress,
darkMode,
}: {
isOptInInProgress: boolean;
darkMode: boolean | undefined;
}) => {
return (
<>
&nbsp;Use Natural Language
Opt-in AI features
{isOptInInProgress && (
<>
&nbsp;
<SpinLoader darkMode={true}></SpinLoader>
<SpinLoader darkMode={darkMode}></SpinLoader>
</>
)}
</>
Expand All @@ -75,12 +67,14 @@ const getButtonText = (isOptInInProgress: boolean) => {
export const AIOptInModal: React.FunctionComponent<OptInModalProps> = ({
isOptInModalVisible,
isOptInInProgress,
isCloudOptIn,
onOptInModalClose,
onOptInClick,
projectId,
}) => {
const isProjectAIEnabled = usePreference('enableGenAIFeaturesAtlasProject');
Copy link
Contributor Author

@gagik gagik Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enableGenAIFeaturesAtlasProject will eventually converge with the new Compass opt-in preference in #7129

const track = useTelemetry();
const darkMode = useDarkMode();
const PROJECT_SETTINGS_LINK = projectId
? window.location.origin + '/v2/' + projectId + '#/settings/groupSettings'
: null;
Expand All @@ -92,7 +86,7 @@ export const AIOptInModal: React.FunctionComponent<OptInModalProps> = ({
}, [isOptInModalVisible, track]);

const onConfirmClick = () => {
if (isOptInInProgress) {
if (isOptInInProgress || !isProjectAIEnabled) {
return;
}
onOptInClick();
Expand All @@ -104,43 +98,22 @@ export const AIOptInModal: React.FunctionComponent<OptInModalProps> = ({
}, [track, onOptInModalClose]);

return (
<ConfirmationModal
open={isOptInModalVisible}
title=""
confirmButtonProps={{
children: getButtonText(isOptInInProgress),
disabled: !isProjectAIEnabled,
onClick: onConfirmClick,
}}
cancelButtonProps={{
onClick: handleModalClose,
}}
>
<Body className={bodyStyles}>
<AiImageBanner></AiImageBanner>
<H3 className={titleStyles}>
Use natural language to generate queries and pipelines
</H3>
Atlas users can now quickly create queries and aggregations with
MongoDB&apos;s&nbsp; intelligent AI-powered feature, available today.
<Banner
variant={isProjectAIEnabled ? 'info' : 'warning'}
className={bannerStyles}
>
{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 ? (
<Link href={PROJECT_SETTINGS_LINK} target="_blank">
Project Settings
</Link>
) : (
'Project Settings'
)}
.
</Banner>
<MarketingModal
open={true}
onClose={handleModalClose}
className={isCloudOptIn ? disableOptInButtonStyles : undefined}
onButtonClick={onConfirmClick}
title="Opt-in Gen AI-Powered features"
// @ts-expect-error - buttonText expects a string but supports ReactNode as well.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also maybe a LG ticket for this?

buttonText={getButtonText({
isOptInInProgress,
darkMode,
})}
linkText="Not now"
onLinkClick={handleModalClose}
graphic={<AiImageBanner />}
darkMode={darkMode}
disclaimer={
<div className={disclaimerStyles}>
This is a feature powered by generative AI, and may give inaccurate
responses. Please see our{' '}
Expand All @@ -149,8 +122,49 @@ export const AIOptInModal: React.FunctionComponent<OptInModalProps> = ({
</Link>{' '}
for more information.
</div>
}
>
<Body
style={{
textAlign: 'left',
marginTop: spacing[300],
}}
>
Opt in now MongoDB’s intelligent AI-powered features:
<ul
style={{
textAlign: 'left',
marginTop: spacing[100],
listStyle: 'disc',
}}
>
<li>AI Assistant allows you to ask questions across connections</li>
<li>Natural Language Bar to create queries and aggregations</li>
{/* <li>Mock Data Generator to create AI powered sample data</li> */}
</ul>
{isCloudOptIn && (
<Banner
data-testid="ai-optin-cloud-banner"
variant={isProjectAIEnabled ? 'info' : 'warning'}
style={{ marginTop: spacing[300] }}
>
{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 ? (
<Link href={PROJECT_SETTINGS_LINK} target="_blank">
Project Settings
</Link>
) : (
'Project Settings'
)}
.
</Banner>
)}
</Body>
</ConfirmationModal>
</MarketingModal>
);
};

Expand Down
7 changes: 6 additions & 1 deletion packages/compass-generative-ai/src/components/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@ import { ConfirmationModalArea } from '@mongodb-js/compass-components';

export interface AtlasAiPluginProps {
projectId?: string;
isCloudOptIn: boolean;
}

export const AtlasAiPlugin: React.FunctionComponent<AtlasAiPluginProps> = ({
projectId,
isCloudOptIn,
}) => {
return (
<ConfirmationModalArea>
<AISignInModal></AISignInModal>
<AIOptInModal projectId={projectId}></AIOptInModal>
<AIOptInModal
isCloudOptIn={isCloudOptIn}
projectId={projectId}
></AIOptInModal>
</ConfirmationModalArea>
);
};
Loading
Loading