Skip to content

Commit

Permalink
feat(compass-generative-ai): Add plugin to generative ai package to s…
Browse files Browse the repository at this point in the history
…how 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
  • Loading branch information
ruchitharajaghatta authored Nov 22, 2024
1 parent 8ab6813 commit 70405ec
Show file tree
Hide file tree
Showing 17 changed files with 998 additions and 91 deletions.
27 changes: 26 additions & 1 deletion packages/compass-generative-ai/src/atlas-ai-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import type { Document } from 'mongodb';
import type { Logger } from '@mongodb-js/compass-logging';
import { EJSON } from 'bson';
import { signIntoAtlasWithModalPrompt } from './store/atlas-signin-reducer';
import { getStore } from './store/atlas-signin-store';
import { getStore } from './store/atlas-ai-store';
import { optIntoGenAIWithModalPrompt } from './store/atlas-optin-reducer';

type GenerativeAiInput = {
userInput: string;
Expand Down Expand Up @@ -329,6 +330,10 @@ export class AtlasAiService {
async ensureAiFeatureAccess({ signal }: { signal?: AbortSignal } = {}) {
// 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(signIntoAtlasWithModalPrompt({ signal }));
}

Expand Down Expand Up @@ -437,6 +442,26 @@ export class AtlasAiService {
);
}

// Performs a post request to atlas to set the user opt in preference to true.
async optIntoGenAIFeaturesAtlas() {
await this.atlasService.authenticatedFetch(
this.atlasService.cloudEndpoint(
'/settings/optInDataExplorerGenAIFeatures'
),
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
body: new URLSearchParams([['value', 'true']]),
}
);
await this.preferences.savePreferences({
optInDataExplorerGenAIFeatures: true,
});
}

private validateAIFeatureEnablementResponse(
response: any
): asserts response is AIFeatureEnablement {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const bannerStyles = css({
height: 263,
});

export const AISignInImageBanner = () => {
export const AiImageBanner = () => {
return (
<svg
className={bannerStyles}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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';

let mockPreferences: PreferencesAccess;

describe('AIOptInModal Component', function () {
beforeEach(async function () {
mockPreferences = await createSandboxFromDefaultPreferences();
});

afterEach(function () {
cleanup();
});

it('should show the modal title', function () {
render(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal
projectId="ab123"
isOptInModalVisible={true}
isOptInInProgress={false}
onOptInModalClose={() => {}}
onOptInClick={() => {}}
></AIOptInModal>
</PreferencesProvider>
);
expect(
screen.getByRole('heading', {
name: 'Use natural language to generate queries and pipelines',
})
).to.exist;
});
it('should show the cancel button', function () {
render(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal
projectId="ab123"
isOptInModalVisible={true}
isOptInInProgress={false}
onOptInModalClose={() => {}}
onOptInClick={() => {}}
>
{' '}
</AIOptInModal>
</PreferencesProvider>
);
const button = screen.getByText('Cancel').closest('button');
expect(button).to.not.match('disabled');
});

it('should show the opt in button enabled when project AI setting is enabled', async function () {
await mockPreferences.savePreferences({
enableGenAIFeaturesAtlasProject: true,
});
render(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal
projectId="ab123"
isOptInModalVisible={true}
isOptInInProgress={false}
onOptInModalClose={() => {}}
onOptInClick={() => {}}
>
{' '}
</AIOptInModal>
</PreferencesProvider>
);
const button = screen.getByText('Use Natural Language').closest('button');
expect(button?.getAttribute('aria-disabled')).to.equal('false');
});

it('should disable the opt in button if project AI setting is disabled ', async function () {
await mockPreferences.savePreferences({
enableGenAIFeaturesAtlasProject: false,
});
render(
<PreferencesProvider value={mockPreferences}>
<AIOptInModal
projectId="ab123"
isOptInModalVisible={true}
isOptInInProgress={false}
onOptInModalClose={() => {}}
onOptInClick={() => {}}
>
{' '}
</AIOptInModal>
</PreferencesProvider>
);
const button = screen.getByText('Use Natural Language').closest('button');
expect(button?.getAttribute('aria-disabled')).to.equal('true');
});
});
150 changes: 150 additions & 0 deletions packages/compass-generative-ai/src/components/ai-optin-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React from 'react';
import { connect } from 'react-redux';
import {
Banner,
Body,
Link,
ConfirmationModal,
SpinLoader,
css,
spacing,
H3,
palette,
} from '@mongodb-js/compass-components';
import { AiImageBanner } from './ai-image-banner';
import { closeOptInModal, optIn } from '../store/atlas-optin-reducer';
import type { RootState } from '../store/atlas-ai-store';
import { usePreference } from 'compass-preferences-model/provider';

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

type OptInModalProps = {
isOptInModalVisible: boolean;
isOptInInProgress: 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],
});

const bannerStyles = css({
padding: spacing[400],
marginTop: spacing[400],
textAlign: 'left',
});
const getButtonText = (isOptInInProgress: boolean) => {
return (
<>
&nbsp;Use Natural Language
{isOptInInProgress && (
<>
&nbsp;
<SpinLoader darkMode={true}></SpinLoader>
</>
)}
</>
);
};

export const AIOptInModal: React.FunctionComponent<OptInModalProps> = ({
isOptInModalVisible,
isOptInInProgress,
onOptInModalClose,
onOptInClick,
projectId,
}) => {
const isProjectAIEnabled = usePreference('enableGenAIFeaturesAtlasProject');
const PROJECT_SETTINGS_LINK = projectId
? window.location.origin + '/v2/' + projectId + '#/settings/groupSettings'
: null;

const onConfirmClick = () => {
if (isOptInInProgress) {
return;
}
onOptInClick();
};
return (
<ConfirmationModal
open={isOptInModalVisible}
title=""
confirmButtonProps={{
children: getButtonText(isOptInInProgress),
disabled: !isProjectAIEnabled,
onClick: onConfirmClick,
}}
cancelButtonProps={{
onClick: onOptInModalClose,
}}
>
<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 change this setting in the{' '}
{PROJECT_SETTINGS_LINK !== null ? (
<Link href={PROJECT_SETTINGS_LINK} target="_blank">
AI features
</Link>
) : (
'AI features '
)}
section.
</Banner>
<div className={disclaimerStyles}>
This is a feature powered by generative AI, and may give inaccurate
responses. Please see our{' '}
<Link hideExternalIcon={false} href={GEN_AI_FAQ_LINK} target="_blank">
FAQ
</Link>{' '}
for more information.
</div>
</Body>
</ConfirmationModal>
);
};

export default connect(
(state: RootState) => {
return {
isOptInModalVisible: state.optIn.isModalOpen,
isOptInInProgress: state.optIn.state === 'in-progress',
};
},
{ onOptInModalClose: closeOptInModal, onOptInClick: optIn }
)(AIOptInModal);
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
spacing,
useDarkMode,
} from '@mongodb-js/compass-components';
import { AISignInImageBanner } from './ai-signin-banner-image';
import type { AtlasSignInState } from '../../store/atlas-signin-reducer';
import { closeSignInModal, signIn } from '../../store/atlas-signin-reducer';
import { AiImageBanner } from './ai-image-banner';
import { closeSignInModal, signIn } from '../store/atlas-signin-reducer';
import type { RootState } from '../store/atlas-ai-store';

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

Expand All @@ -30,7 +30,7 @@ const titleStyles = css({
alignItems: 'center',
});

const disclaimer = css({
const disclaimerStyles = css({
padding: `0 ${spacing[900]}px`,
});

Expand All @@ -46,7 +46,7 @@ const AISignInModal: React.FunctionComponent<SignInModalProps> = ({
<MarketingModal
darkMode={darkMode}
disclaimer={
<div className={disclaimer}>
<div className={disclaimerStyles}>
This is a feature powered by generative AI, and may give inaccurate
responses. Please see our{' '}
<Link hideExternalIcon={false} href={GEN_AI_FAQ_LINK} target="_blank">
Expand All @@ -55,7 +55,7 @@ const AISignInModal: React.FunctionComponent<SignInModalProps> = ({
for more information.
</div>
}
graphic={<AISignInImageBanner></AISignInImageBanner>}
graphic={<AiImageBanner></AiImageBanner>}
title={
<div className={titleStyles}>
Use natural language to generate queries and pipelines
Expand Down Expand Up @@ -100,10 +100,10 @@ const AISignInModal: React.FunctionComponent<SignInModalProps> = ({
};

export default connect(
(state: AtlasSignInState) => {
(state: RootState) => {
return {
isSignInModalVisible: state.isModalOpen,
isSignInInProgress: state.state === 'in-progress',
isSignInModalVisible: state.signIn.isModalOpen,
isSignInInProgress: state.signIn.state === 'in-progress',
};
},
{ onSignInModalClose: closeSignInModal, onSignInClick: signIn }
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion packages/compass-generative-ai/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ export {
AIExperienceEntry,
createAIPlaceholderHTMLPlaceholder,
} from './ai-experience-entry';
export { AtlasSignIn } from './atlas-signin';
export { AtlasAiPlugin } from './plugin';
19 changes: 19 additions & 0 deletions packages/compass-generative-ai/src/components/plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import AISignInModal from './ai-signin-modal';
import AIOptInModal from './ai-optin-modal';
import { ConfirmationModalArea } from '@mongodb-js/compass-components';

export interface AtlasAiPluginProps {
projectId?: string;
}

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

0 comments on commit 70405ec

Please sign in to comment.