Skip to content

Commit 70405ec

Browse files
feat(compass-generative-ai): Add plugin to generative ai package to show opt-in for project setting in DE COMPASS-8378 (#6489)
* moving prefs up nit from prev ticket * new modal and plugin * removing console log * review comments and checking for userPref * review comments and checking for userPref * PR comments * PR comments * reducer name change bug * fixing post request * fixing test setup failures * new test and sign in test fixes * test fixes and package.json fix * state name update * test and reducer bug fixes * commenting out errors for evg patch * fixing reducer type and entrypoint * npm check fix * removing ts-expect-errors * prettier fix * taking out duplicated function * addressing changes to modal * fixing flag for disabling opt in * nit: * nits and fixing optin modal/refctoring for projid * fixing projectID prop * optin modal test * test tweak * nit comment * fixing projectId
1 parent 8ab6813 commit 70405ec

File tree

17 files changed

+998
-91
lines changed

17 files changed

+998
-91
lines changed

packages/compass-generative-ai/src/atlas-ai-service.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import type { Document } from 'mongodb';
1010
import type { Logger } from '@mongodb-js/compass-logging';
1111
import { EJSON } from 'bson';
1212
import { signIntoAtlasWithModalPrompt } from './store/atlas-signin-reducer';
13-
import { getStore } from './store/atlas-signin-store';
13+
import { getStore } from './store/atlas-ai-store';
14+
import { optIntoGenAIWithModalPrompt } from './store/atlas-optin-reducer';
1415

1516
type GenerativeAiInput = {
1617
userInput: string;
@@ -329,6 +330,10 @@ export class AtlasAiService {
329330
async ensureAiFeatureAccess({ signal }: { signal?: AbortSignal } = {}) {
330331
// When the ai feature is attempted to be opened we make sure
331332
// the user is signed into Atlas and opted in.
333+
334+
if (this.apiURLPreset === 'cloud') {
335+
return getStore().dispatch(optIntoGenAIWithModalPrompt({ signal }));
336+
}
332337
return getStore().dispatch(signIntoAtlasWithModalPrompt({ signal }));
333338
}
334339

@@ -437,6 +442,26 @@ export class AtlasAiService {
437442
);
438443
}
439444

445+
// Performs a post request to atlas to set the user opt in preference to true.
446+
async optIntoGenAIFeaturesAtlas() {
447+
await this.atlasService.authenticatedFetch(
448+
this.atlasService.cloudEndpoint(
449+
'/settings/optInDataExplorerGenAIFeatures'
450+
),
451+
{
452+
method: 'POST',
453+
headers: {
454+
'Content-Type': 'application/x-www-form-urlencoded',
455+
Accept: 'application/json',
456+
},
457+
body: new URLSearchParams([['value', 'true']]),
458+
}
459+
);
460+
await this.preferences.savePreferences({
461+
optInDataExplorerGenAIFeatures: true,
462+
});
463+
}
464+
440465
private validateAIFeatureEnablementResponse(
441466
response: any
442467
): asserts response is AIFeatureEnablement {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const bannerStyles = css({
77
height: 263,
88
});
99

10-
export const AISignInImageBanner = () => {
10+
export const AiImageBanner = () => {
1111
return (
1212
<svg
1313
className={bannerStyles}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import React from 'react';
2+
import { render, screen, cleanup } from '@mongodb-js/testing-library-compass';
3+
import { expect } from 'chai';
4+
import { AIOptInModal } from './ai-optin-modal';
5+
import type { PreferencesAccess } from 'compass-preferences-model';
6+
import { createSandboxFromDefaultPreferences } from 'compass-preferences-model';
7+
import { PreferencesProvider } from 'compass-preferences-model/provider';
8+
9+
let mockPreferences: PreferencesAccess;
10+
11+
describe('AIOptInModal Component', function () {
12+
beforeEach(async function () {
13+
mockPreferences = await createSandboxFromDefaultPreferences();
14+
});
15+
16+
afterEach(function () {
17+
cleanup();
18+
});
19+
20+
it('should show the modal title', function () {
21+
render(
22+
<PreferencesProvider value={mockPreferences}>
23+
<AIOptInModal
24+
projectId="ab123"
25+
isOptInModalVisible={true}
26+
isOptInInProgress={false}
27+
onOptInModalClose={() => {}}
28+
onOptInClick={() => {}}
29+
></AIOptInModal>
30+
</PreferencesProvider>
31+
);
32+
expect(
33+
screen.getByRole('heading', {
34+
name: 'Use natural language to generate queries and pipelines',
35+
})
36+
).to.exist;
37+
});
38+
it('should show the cancel button', function () {
39+
render(
40+
<PreferencesProvider value={mockPreferences}>
41+
<AIOptInModal
42+
projectId="ab123"
43+
isOptInModalVisible={true}
44+
isOptInInProgress={false}
45+
onOptInModalClose={() => {}}
46+
onOptInClick={() => {}}
47+
>
48+
{' '}
49+
</AIOptInModal>
50+
</PreferencesProvider>
51+
);
52+
const button = screen.getByText('Cancel').closest('button');
53+
expect(button).to.not.match('disabled');
54+
});
55+
56+
it('should show the opt in button enabled when project AI setting is enabled', async function () {
57+
await mockPreferences.savePreferences({
58+
enableGenAIFeaturesAtlasProject: true,
59+
});
60+
render(
61+
<PreferencesProvider value={mockPreferences}>
62+
<AIOptInModal
63+
projectId="ab123"
64+
isOptInModalVisible={true}
65+
isOptInInProgress={false}
66+
onOptInModalClose={() => {}}
67+
onOptInClick={() => {}}
68+
>
69+
{' '}
70+
</AIOptInModal>
71+
</PreferencesProvider>
72+
);
73+
const button = screen.getByText('Use Natural Language').closest('button');
74+
expect(button?.getAttribute('aria-disabled')).to.equal('false');
75+
});
76+
77+
it('should disable the opt in button if project AI setting is disabled ', async function () {
78+
await mockPreferences.savePreferences({
79+
enableGenAIFeaturesAtlasProject: false,
80+
});
81+
render(
82+
<PreferencesProvider value={mockPreferences}>
83+
<AIOptInModal
84+
projectId="ab123"
85+
isOptInModalVisible={true}
86+
isOptInInProgress={false}
87+
onOptInModalClose={() => {}}
88+
onOptInClick={() => {}}
89+
>
90+
{' '}
91+
</AIOptInModal>
92+
</PreferencesProvider>
93+
);
94+
const button = screen.getByText('Use Natural Language').closest('button');
95+
expect(button?.getAttribute('aria-disabled')).to.equal('true');
96+
});
97+
});
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import React from 'react';
2+
import { connect } from 'react-redux';
3+
import {
4+
Banner,
5+
Body,
6+
Link,
7+
ConfirmationModal,
8+
SpinLoader,
9+
css,
10+
spacing,
11+
H3,
12+
palette,
13+
} from '@mongodb-js/compass-components';
14+
import { AiImageBanner } from './ai-image-banner';
15+
import { closeOptInModal, optIn } from '../store/atlas-optin-reducer';
16+
import type { RootState } from '../store/atlas-ai-store';
17+
import { usePreference } from 'compass-preferences-model/provider';
18+
19+
const GEN_AI_FAQ_LINK = 'https://www.mongodb.com/docs/generative-ai-faq/';
20+
21+
type OptInModalProps = {
22+
isOptInModalVisible: boolean;
23+
isOptInInProgress: boolean;
24+
onOptInModalClose: () => void;
25+
onOptInClick: () => void;
26+
projectId?: string;
27+
};
28+
29+
const titleStyles = css({
30+
marginBottom: spacing[400],
31+
marginTop: spacing[400],
32+
marginLeft: spacing[500],
33+
marginRight: spacing[500],
34+
textAlign: 'center',
35+
});
36+
37+
const bodyStyles = css({
38+
marginBottom: spacing[400],
39+
marginTop: spacing[400],
40+
marginLeft: spacing[300],
41+
marginRight: spacing[300],
42+
display: 'flex',
43+
flexDirection: 'column',
44+
alignItems: 'center',
45+
textAlign: 'center',
46+
});
47+
48+
const disclaimerStyles = css({
49+
color: palette.gray.dark1,
50+
marginTop: spacing[400],
51+
marginLeft: spacing[800],
52+
marginRight: spacing[800],
53+
});
54+
55+
const bannerStyles = css({
56+
padding: spacing[400],
57+
marginTop: spacing[400],
58+
textAlign: 'left',
59+
});
60+
const getButtonText = (isOptInInProgress: boolean) => {
61+
return (
62+
<>
63+
&nbsp;Use Natural Language
64+
{isOptInInProgress && (
65+
<>
66+
&nbsp;
67+
<SpinLoader darkMode={true}></SpinLoader>
68+
</>
69+
)}
70+
</>
71+
);
72+
};
73+
74+
export const AIOptInModal: React.FunctionComponent<OptInModalProps> = ({
75+
isOptInModalVisible,
76+
isOptInInProgress,
77+
onOptInModalClose,
78+
onOptInClick,
79+
projectId,
80+
}) => {
81+
const isProjectAIEnabled = usePreference('enableGenAIFeaturesAtlasProject');
82+
const PROJECT_SETTINGS_LINK = projectId
83+
? window.location.origin + '/v2/' + projectId + '#/settings/groupSettings'
84+
: null;
85+
86+
const onConfirmClick = () => {
87+
if (isOptInInProgress) {
88+
return;
89+
}
90+
onOptInClick();
91+
};
92+
return (
93+
<ConfirmationModal
94+
open={isOptInModalVisible}
95+
title=""
96+
confirmButtonProps={{
97+
children: getButtonText(isOptInInProgress),
98+
disabled: !isProjectAIEnabled,
99+
onClick: onConfirmClick,
100+
}}
101+
cancelButtonProps={{
102+
onClick: onOptInModalClose,
103+
}}
104+
>
105+
<Body className={bodyStyles}>
106+
<AiImageBanner></AiImageBanner>
107+
<H3 className={titleStyles}>
108+
Use natural language to generate queries and pipelines
109+
</H3>
110+
Atlas users can now quickly create queries and aggregations with
111+
MongoDB&apos;s&nbsp; intelligent AI-powered feature, available today.
112+
<Banner
113+
variant={isProjectAIEnabled ? 'info' : 'warning'}
114+
className={bannerStyles}
115+
>
116+
{isProjectAIEnabled
117+
? 'AI features are enabled for project users with data access.'
118+
: 'AI features are disabled for project users.'}{' '}
119+
Project Owners can change this setting in the{' '}
120+
{PROJECT_SETTINGS_LINK !== null ? (
121+
<Link href={PROJECT_SETTINGS_LINK} target="_blank">
122+
AI features
123+
</Link>
124+
) : (
125+
'AI features '
126+
)}
127+
section.
128+
</Banner>
129+
<div className={disclaimerStyles}>
130+
This is a feature powered by generative AI, and may give inaccurate
131+
responses. Please see our{' '}
132+
<Link hideExternalIcon={false} href={GEN_AI_FAQ_LINK} target="_blank">
133+
FAQ
134+
</Link>{' '}
135+
for more information.
136+
</div>
137+
</Body>
138+
</ConfirmationModal>
139+
);
140+
};
141+
142+
export default connect(
143+
(state: RootState) => {
144+
return {
145+
isOptInModalVisible: state.optIn.isModalOpen,
146+
isOptInInProgress: state.optIn.state === 'in-progress',
147+
};
148+
},
149+
{ onOptInModalClose: closeOptInModal, onOptInClick: optIn }
150+
)(AIOptInModal);
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
spacing,
1111
useDarkMode,
1212
} from '@mongodb-js/compass-components';
13-
import { AISignInImageBanner } from './ai-signin-banner-image';
14-
import type { AtlasSignInState } from '../../store/atlas-signin-reducer';
15-
import { closeSignInModal, signIn } from '../../store/atlas-signin-reducer';
13+
import { AiImageBanner } from './ai-image-banner';
14+
import { closeSignInModal, signIn } from '../store/atlas-signin-reducer';
15+
import type { RootState } from '../store/atlas-ai-store';
1616

1717
const GEN_AI_FAQ_LINK = 'https://www.mongodb.com/docs/generative-ai-faq/';
1818

@@ -30,7 +30,7 @@ const titleStyles = css({
3030
alignItems: 'center',
3131
});
3232

33-
const disclaimer = css({
33+
const disclaimerStyles = css({
3434
padding: `0 ${spacing[900]}px`,
3535
});
3636

@@ -46,7 +46,7 @@ const AISignInModal: React.FunctionComponent<SignInModalProps> = ({
4646
<MarketingModal
4747
darkMode={darkMode}
4848
disclaimer={
49-
<div className={disclaimer}>
49+
<div className={disclaimerStyles}>
5050
This is a feature powered by generative AI, and may give inaccurate
5151
responses. Please see our{' '}
5252
<Link hideExternalIcon={false} href={GEN_AI_FAQ_LINK} target="_blank">
@@ -55,7 +55,7 @@ const AISignInModal: React.FunctionComponent<SignInModalProps> = ({
5555
for more information.
5656
</div>
5757
}
58-
graphic={<AISignInImageBanner></AISignInImageBanner>}
58+
graphic={<AiImageBanner></AiImageBanner>}
5959
title={
6060
<div className={titleStyles}>
6161
Use natural language to generate queries and pipelines
@@ -100,10 +100,10 @@ const AISignInModal: React.FunctionComponent<SignInModalProps> = ({
100100
};
101101

102102
export default connect(
103-
(state: AtlasSignInState) => {
103+
(state: RootState) => {
104104
return {
105-
isSignInModalVisible: state.isModalOpen,
106-
isSignInInProgress: state.state === 'in-progress',
105+
isSignInModalVisible: state.signIn.isModalOpen,
106+
isSignInInProgress: state.signIn.state === 'in-progress',
107107
};
108108
},
109109
{ onSignInModalClose: closeSignInModal, onSignInClick: signIn }

packages/compass-generative-ai/src/components/atlas-signin/index.tsx

Lines changed: 0 additions & 11 deletions
This file was deleted.

packages/compass-generative-ai/src/components/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ export {
33
AIExperienceEntry,
44
createAIPlaceholderHTMLPlaceholder,
55
} from './ai-experience-entry';
6-
export { AtlasSignIn } from './atlas-signin';
6+
export { AtlasAiPlugin } from './plugin';
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React from 'react';
2+
import AISignInModal from './ai-signin-modal';
3+
import AIOptInModal from './ai-optin-modal';
4+
import { ConfirmationModalArea } from '@mongodb-js/compass-components';
5+
6+
export interface AtlasAiPluginProps {
7+
projectId?: string;
8+
}
9+
10+
export const AtlasAiPlugin: React.FunctionComponent<AtlasAiPluginProps> = ({
11+
projectId,
12+
}) => {
13+
return (
14+
<ConfirmationModalArea>
15+
<AISignInModal></AISignInModal>
16+
<AIOptInModal projectId={projectId}></AIOptInModal>
17+
</ConfirmationModalArea>
18+
);
19+
};

0 commit comments

Comments
 (0)