diff --git a/packages/compass-e2e-tests/tests/atlas-cloud/collection-ai-query.test.ts b/packages/compass-e2e-tests/tests/atlas-cloud/collection-ai-query.test.ts index efbddc5477d..374c50d9d9e 100644 --- a/packages/compass-e2e-tests/tests/atlas-cloud/collection-ai-query.test.ts +++ b/packages/compass-e2e-tests/tests/atlas-cloud/collection-ai-query.test.ts @@ -49,7 +49,7 @@ describe('Collection ai query', function () { true ); await browser.setFeature('enableGenAIFeaturesAtlasOrg', true); - await browser.setFeature('optInDataExplorerGenAIFeatures', true); + await browser.setFeature('optInGenAIFeatures', true); }); describe('on the documents tab', function () { @@ -170,7 +170,7 @@ describe('Collection ai query', function () { true ); await browser.setFeature('enableGenAIFeaturesAtlasOrg', false); - await browser.setFeature('optInDataExplorerGenAIFeatures', true); + await browser.setFeature('optInGenAIFeatures', true); }); it('should not show the gen ai intro button', async function () { diff --git a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts index 46e190b742b..193a437acff 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.spec.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.spec.ts @@ -321,6 +321,54 @@ describe('AtlasAiService', function () { }); }); }); + + describe('optIntoGenAIFeatures', function () { + beforeEach(async function () { + // Reset preferences + await preferences.savePreferences({ + optInGenAIFeatures: false, + enableUnauthenticatedGenAI: true, + }); + }); + + afterEach(async function () { + await preferences.savePreferences({ + enableUnauthenticatedGenAI: false, + }); + }); + + it('should save preference when cloud preset', async function () { + const fetchStub = sandbox.stub().resolves(makeResponse({})); + global.fetch = fetchStub; + + await atlasAiService.optIntoGenAIFeatures(); + + // In Data Explorer, make a POST request to cloud endpoint and save preference + if (apiURLPreset === 'cloud') { + // Verify fetch was called with correct parameters + expect(fetchStub).to.have.been.calledOnce; + + expect(fetchStub).to.have.been.calledWith( + '/cloud/settings/optInDataExplorerGenAIFeatures', + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + body: new URLSearchParams([['value', 'true']]), + } + ); + } else { + // In Compass, no fetch is made, only stored locally + expect(fetchStub).to.not.have.been.called; + } + + // Verify preference was saved + const currentPreferences = preferences.getPreferences(); + expect(currentPreferences.optInGenAIFeatures).to.equal(true); + }); + }); }); } }); diff --git a/packages/compass-generative-ai/src/atlas-ai-service.ts b/packages/compass-generative-ai/src/atlas-ai-service.ts index d504c1fcc9a..8c5a39b1aa1 100644 --- a/packages/compass-generative-ai/src/atlas-ai-service.ts +++ b/packages/compass-generative-ai/src/atlas-ai-service.ts @@ -9,9 +9,9 @@ import type { ConnectionInfo } from '@mongodb-js/compass-connections/provider'; 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-ai-store'; import { optIntoGenAIWithModalPrompt } from './store/atlas-optin-reducer'; +import { signIntoAtlasWithModalPrompt } from './store/atlas-signin-reducer'; type GenerativeAiInput = { userInput: string; @@ -277,6 +277,10 @@ export class AtlasAiService { } async ensureAiFeatureAccess({ signal }: { signal?: AbortSignal } = {}) { + if (this.preferences.getPreferences().enableUnauthenticatedGenAI) { + return getStore().dispatch(optIntoGenAIWithModalPrompt({ signal })); + } + // When the ai feature is attempted to be opened we make sure // the user is signed into Atlas and opted in. @@ -391,23 +395,25 @@ 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']]), - } - ); + async optIntoGenAIFeatures() { + if (this.apiURLPreset === 'cloud') { + // Performs a post request to Atlas to set the user opt in preference to true. + 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, + optInGenAIFeatures: true, }); } 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..947842f8054 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 @@ -20,7 +20,7 @@ describe('atlasOptInReducer', function () { beforeEach(async function () { mockPreferences = await createSandboxFromDefaultPreferences(); await mockPreferences.savePreferences({ - optInDataExplorerGenAIFeatures: false, + optInGenAIFeatures: false, }); }); @@ -31,7 +31,7 @@ describe('atlasOptInReducer', function () { describe('optIn', function () { it('should check state and set state to success if already opted in', async function () { const mockAtlasAiService = { - optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }), + optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }), }; const store = configureStore({ atlasAuthService: {} as any, @@ -45,8 +45,7 @@ describe('atlasOptInReducer', function () { ); void store.dispatch(atlasAiServiceOptedIn()); await store.dispatch(optIn()); - expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).not.to.have.been - .called; + expect(mockAtlasAiService.optIntoGenAIFeatures).not.to.have.been.called; expect(store.getState().optIn).to.have.nested.property( 'state', 'optin-success' @@ -55,7 +54,7 @@ describe('atlasOptInReducer', function () { it('should start opt in, and set state to success', async function () { const mockAtlasAiService = { - optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }), + optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }), }; const store = configureStore({ atlasAuthService: {} as any, @@ -69,8 +68,7 @@ describe('atlasOptInReducer', function () { ); void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {}); await store.dispatch(optIn()); - expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).to.have.been - .calledOnce; + expect(mockAtlasAiService.optIntoGenAIFeatures).to.have.been.calledOnce; expect(store.getState().optIn).to.have.nested.property( 'state', 'optin-success' @@ -81,13 +79,13 @@ describe('atlasOptInReducer', function () { beforeEach(async function () { await mockPreferences.savePreferences({ enableGenAIFeaturesAtlasProject: false, - optInDataExplorerGenAIFeatures: true, + optInGenAIFeatures: true, }); }); it('should start the opt in flow', async function () { const mockAtlasAiService = { - optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }), + optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }), }; const store = configureStore({ atlasAuthService: {} as any, @@ -101,8 +99,7 @@ describe('atlasOptInReducer', function () { ); void store.dispatch(optIntoGenAIWithModalPrompt()).catch(() => {}); await store.dispatch(optIn()); - expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).to.have.been - .calledOnce; + expect(mockAtlasAiService.optIntoGenAIFeatures).to.have.been.calledOnce; expect(store.getState().optIn).to.have.nested.property( 'state', 'optin-success' @@ -112,9 +109,7 @@ describe('atlasOptInReducer', function () { it('should fail opt in if opt in failed', async function () { const mockAtlasAiService = { - optIntoGenAIFeaturesAtlas: sandbox - .stub() - .rejects(new Error('Whooops!')), + optIntoGenAIFeatures: sandbox.stub().rejects(new Error('Whooops!')), }; const store = configureStore({ atlasAuthService: {} as any, @@ -127,8 +122,7 @@ describe('atlasOptInReducer', function () { // Avoid unhandled rejections. AttemptStateMap.get(attemptId)?.promise.catch(() => {}); await optInPromise; - expect(mockAtlasAiService.optIntoGenAIFeaturesAtlas).to.have.been - .calledOnce; + expect(mockAtlasAiService.optIntoGenAIFeatures).to.have.been.calledOnce; expect(store.getState().optIn).to.have.nested.property('state', 'error'); }); }); @@ -153,7 +147,7 @@ describe('atlasOptInReducer', function () { it('should cancel opt in if opt in is in progress', async function () { const mockAtlasAiService = { - optIntoGenAIFeaturesAtlas: sandbox + optIntoGenAIFeatures: sandbox .stub() .callsFake(({ signal }: { signal: AbortSignal }) => { return new Promise((resolve, reject) => { @@ -183,10 +177,10 @@ describe('atlasOptInReducer', function () { }); }); - describe('optIntoAtlasWithModalPrompt', function () { + describe('optIntoGenAIWithModalPrompt', function () { it('should resolve when user finishes opt in with prompt flow', async function () { const mockAtlasAiService = { - optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }), + optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }), }; const store = configureStore({ atlasAuthService: {} as any, @@ -203,7 +197,7 @@ describe('atlasOptInReducer', function () { it('should reject if opt in flow fails', async function () { const mockAtlasAiService = { - optIntoGenAIFeaturesAtlas: sandbox.stub().rejects(new Error('Whoops!')), + optIntoGenAIFeatures: sandbox.stub().rejects(new Error('Whoops!')), }; const store = configureStore({ atlasAuthService: {} as any, @@ -226,7 +220,7 @@ describe('atlasOptInReducer', function () { it('should reject if user dismissed the modal', async function () { const mockAtlasAiService = { - optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }), + optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }), }; const store = configureStore({ atlasAuthService: {} as any, @@ -249,7 +243,7 @@ describe('atlasOptInReducer', function () { it('should reject if provided signal was aborted', async function () { const mockAtlasAiService = { - optIntoGenAIFeaturesAtlas: sandbox.stub().resolves({ sub: '1234' }), + optIntoGenAIFeatures: sandbox.stub().resolves({ sub: '1234' }), }; const store = configureStore({ atlasAuthService: {} as any, 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..f653f815eb4 100644 --- a/packages/compass-generative-ai/src/store/atlas-optin-reducer.ts +++ b/packages/compass-generative-ai/src/store/atlas-optin-reducer.ts @@ -228,7 +228,7 @@ export const optIntoGenAIWithModalPrompt = ({ const { state } = getState().optIn; if ( (state === 'optin-success' || - preferences.getPreferences().optInDataExplorerGenAIFeatures) && + preferences.getPreferences().optInGenAIFeatures) && preferences.getPreferences().enableGenAIFeaturesAtlasProject ) { return Promise.resolve(); @@ -265,7 +265,7 @@ export const optIn = (): GenAIAtlasOptInThunkAction> => { try { throwIfAborted(signal); - await atlasAiService.optIntoGenAIFeaturesAtlas(); + await atlasAiService.optIntoGenAIFeatures(); dispatch(atlasAiServiceOptedIn()); resolve(); } catch (err) { diff --git a/packages/compass-preferences-model/src/compass-web-preferences-access.ts b/packages/compass-preferences-model/src/compass-web-preferences-access.ts index 259390ea180..05b9095a3db 100644 --- a/packages/compass-preferences-model/src/compass-web-preferences-access.ts +++ b/packages/compass-preferences-model/src/compass-web-preferences-access.ts @@ -7,7 +7,7 @@ import { getActiveUser } from './utils'; const editablePreferences: (keyof UserPreferences)[] = [ // Value can change from false to true during allocation / checking - 'optInDataExplorerGenAIFeatures', + 'optInGenAIFeatures', 'cloudFeatureRolloutAccess', // TODO(COMPASS-9353): Provide a standard for updating Compass preferences in web 'enableIndexesGuidanceExp', diff --git a/packages/compass-preferences-model/src/feature-flags.ts b/packages/compass-preferences-model/src/feature-flags.ts index ca4d0fe0b2d..b45be8e0e61 100644 --- a/packages/compass-preferences-model/src/feature-flags.ts +++ b/packages/compass-preferences-model/src/feature-flags.ts @@ -28,6 +28,7 @@ export type FeatureFlags = { showIndexesGuidanceVariant: boolean; enableContextMenus: boolean; enableSearchActivationProgramP1: boolean; + enableUnauthenticatedGenAI: boolean; }; export const featureFlags: Required<{ @@ -150,6 +151,13 @@ export const featureFlags: Required<{ }, }, + enableUnauthenticatedGenAI: { + stage: 'development', + description: { + short: 'Enable GenAI for unauthenticated users', + }, + }, + /** * Feature flag for CLOUDP-308952. */ diff --git a/packages/compass-preferences-model/src/preferences-schema.tsx b/packages/compass-preferences-model/src/preferences-schema.tsx index 93b5a6540d0..fdeed50452c 100644 --- a/packages/compass-preferences-model/src/preferences-schema.tsx +++ b/packages/compass-preferences-model/src/preferences-schema.tsx @@ -85,7 +85,7 @@ export type UserConfigurablePreferences = PermanentFeatureFlags & | 'web-sandbox-atlas-dev' | 'web-sandbox-atlas-qa' | 'web-sandbox-atlas'; - optInDataExplorerGenAIFeatures: boolean; + optInGenAIFeatures: boolean; // Features that are enabled by default in Compass, but are disabled in Data // Explorer enableExplainPlan: boolean; @@ -810,17 +810,16 @@ export const storedUserPreferencesProps: Required<{ .default('atlas'), type: 'string', }, - optInDataExplorerGenAIFeatures: { + optInGenAIFeatures: { ui: true, cli: false, global: false, description: { - short: 'User Opt-in for Data Explorer Gen AI Features', + short: 'User or Client Opt-in for Gen AI Features', }, - validator: z.boolean().default(true), + validator: z.boolean().default(false), type: 'boolean', }, - enableAtlasSearchIndexes: { ui: true, cli: true, diff --git a/packages/compass-web/sandbox/index.tsx b/packages/compass-web/sandbox/index.tsx index b5bb034c0f3..17f676edf2b 100644 --- a/packages/compass-web/sandbox/index.tsx +++ b/packages/compass-web/sandbox/index.tsx @@ -48,7 +48,7 @@ const App = () => { enableGenAIFeaturesAtlasProject, enableGenAISampleDocumentPassingOnAtlasProject, enableGenAIFeaturesAtlasOrg, - optInDataExplorerGenAIFeatures, + optInGenAIFeatures, } = projectParams ?? {}; const atlasServiceSandboxBackendVariant = @@ -135,8 +135,7 @@ const App = () => { isAtlas && !!enableGenAISampleDocumentPassingOnAtlasProject, enableGenAIFeaturesAtlasOrg: isAtlas && !!enableGenAIFeaturesAtlasOrg, - optInDataExplorerGenAIFeatures: - isAtlas && !!optInDataExplorerGenAIFeatures, + optInGenAIFeatures: isAtlas && !!optInGenAIFeatures, enableDataModeling: true, }} onTrack={sandboxTelemetry.track} diff --git a/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx b/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx index fd6c5322273..c1c257881c7 100644 --- a/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx +++ b/packages/compass-web/sandbox/sandbox-atlas-sign-in.tsx @@ -19,7 +19,7 @@ type ProjectParams = { enableGenAIFeaturesAtlasProject: boolean; enableGenAISampleDocumentPassingOnAtlasProject: boolean; enableGenAIFeaturesAtlasOrg: boolean; - optInDataExplorerGenAIFeatures: boolean; + optInGenAIFeatures: boolean; }; type AtlasLoginReturnValue = @@ -129,8 +129,7 @@ export function useAtlasProxySignIn(): AtlasLoginReturnValue { projectId, csrfToken, csrfTime, - optInDataExplorerGenAIFeatures: - isOptedIntoDataExplorerGenAIFeatures, + optInGenAIFeatures: isOptedIntoDataExplorerGenAIFeatures, enableGenAIFeaturesAtlasOrg: genAIFeaturesEnabled, enableGenAISampleDocumentPassingOnAtlasProject: groupEnabledFeatureFlags.includes( diff --git a/packages/compass-web/src/preferences.tsx b/packages/compass-web/src/preferences.tsx index 13cdb1060cb..7347cdd53b4 100644 --- a/packages/compass-web/src/preferences.tsx +++ b/packages/compass-web/src/preferences.tsx @@ -55,7 +55,7 @@ export function useCompassWebPreferences( enableShell: false, enableCreatingNewConnections: false, enableGlobalWrites: false, - optInDataExplorerGenAIFeatures: false, + optInGenAIFeatures: false, enableConnectInNewWindow: false, ...initialPreferences, })