Skip to content

Commit 95fdb4e

Browse files
authored
feat(compass-assistant): ensure opt-in before sending the first message COMPASS-9759 (#7263)
* ensure opt-in before sending the first message * don't use useAtlasAiServiceContext * circular dep, inferred troubles * don't include ensureOptInAndSend in useAssistantActions() * also don't expose clearChat * deps * tests * factor out common type * function is optional * fix the cached feature flag * add options * only open the drawer once
1 parent 333c116 commit 95fdb4e

File tree

9 files changed

+329
-100
lines changed

9 files changed

+329
-100
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass-assistant/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"@mongodb-js/compass-telemetry": "^1.14.0",
5757
"@mongodb-js/connection-info": "^0.17.1",
5858
"@mongodb-js/compass-logging": "^1.7.12",
59+
"@mongodb-js/compass-generative-ai": "^0.51.0",
5960
"mongodb-connection-string-url": "^3.0.1",
6061
"ai": "^5.0.26",
6162
"compass-preferences-model": "^2.51.0",

packages/compass-assistant/src/assistant-chat.spec.tsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ import {
88
import { AssistantChat } from './assistant-chat';
99
import { expect } from 'chai';
1010
import { createMockChat } from '../test/utils';
11-
import type { AssistantMessage } from './compass-assistant-provider';
11+
import {
12+
AssistantActionsContext,
13+
type AssistantMessage,
14+
} from './compass-assistant-provider';
15+
import sinon from 'sinon';
1216

1317
describe('AssistantChat', function () {
1418
const mockMessages: AssistantMessage[] = [
@@ -38,10 +42,30 @@ describe('AssistantChat', function () {
3842
} = {}
3943
) {
4044
const chat = createMockChat({ messages, status });
41-
const result = render(<AssistantChat chat={chat} />);
45+
46+
// The chat component does not use chat.sendMessage() directly, it uses
47+
// ensureOptInAndSend() via the AssistantActionsContext.
48+
const ensureOptInAndSendStub = sinon
49+
.stub()
50+
.callsFake(async (message, options, callback) => {
51+
// call the callback so we can test the tracking
52+
callback();
53+
54+
await chat.sendMessage(message, options);
55+
});
56+
57+
const assistantActionsContext = {
58+
ensureOptInAndSend: ensureOptInAndSendStub,
59+
};
60+
const result = render(
61+
<AssistantActionsContext.Provider value={assistantActionsContext as any}>
62+
<AssistantChat chat={chat} />
63+
</AssistantActionsContext.Provider>
64+
);
4265
return {
4366
result,
4467
chat,
68+
ensureOptInAndSendStub,
4569
};
4670
}
4771

@@ -156,8 +180,8 @@ describe('AssistantChat', function () {
156180
);
157181
});
158182

159-
it('calls sendMessage when form is submitted', async function () {
160-
const { chat, result } = renderWithChat([]);
183+
it('calls ensureOptInAndSend when form is submitted', async function () {
184+
const { ensureOptInAndSendStub, result } = renderWithChat([]);
161185
const { track } = result;
162186
const inputField = screen.getByPlaceholderText(
163187
'Ask MongoDB Assistant a question'
@@ -167,8 +191,7 @@ describe('AssistantChat', function () {
167191
userEvent.type(inputField, 'What is aggregation?');
168192
userEvent.click(sendButton);
169193

170-
expect(chat.sendMessage.calledWith({ text: 'What is aggregation?' })).to.be
171-
.true;
194+
expect(ensureOptInAndSendStub.called).to.be.true;
172195

173196
await waitFor(() => {
174197
expect(track).to.have.been.calledWith('Assistant Prompt Submitted', {
@@ -193,7 +216,7 @@ describe('AssistantChat', function () {
193216
});
194217

195218
it('trims whitespace from input before sending', async function () {
196-
const { chat, result } = renderWithChat([]);
219+
const { ensureOptInAndSendStub, result } = renderWithChat([]);
197220
const { track } = result;
198221

199222
const inputField = screen.getByPlaceholderText(
@@ -203,8 +226,7 @@ describe('AssistantChat', function () {
203226
userEvent.type(inputField, ' What is sharding? ');
204227
userEvent.click(screen.getByLabelText('Send message'));
205228

206-
expect(chat.sendMessage.calledWith({ text: 'What is sharding?' })).to.be
207-
.true;
229+
expect(ensureOptInAndSendStub.called).to.be.true;
208230

209231
await waitFor(() => {
210232
expect(track).to.have.been.calledWith('Assistant Prompt Submitted', {
@@ -213,8 +235,8 @@ describe('AssistantChat', function () {
213235
});
214236
});
215237

216-
it('does not call sendMessage when input is empty or whitespace-only', function () {
217-
const { chat } = renderWithChat([]);
238+
it('does not call ensureOptInAndSend when input is empty or whitespace-only', function () {
239+
const { ensureOptInAndSendStub } = renderWithChat([]);
218240

219241
const inputField = screen.getByPlaceholderText(
220242
'Ask MongoDB Assistant a question'
@@ -223,12 +245,12 @@ describe('AssistantChat', function () {
223245

224246
// Test empty input
225247
userEvent.click(chatForm);
226-
expect(chat.sendMessage.notCalled).to.be.true;
248+
expect(ensureOptInAndSendStub.notCalled).to.be.true;
227249

228250
// Test whitespace-only input
229251
userEvent.type(inputField, ' ');
230252
userEvent.click(chatForm);
231-
expect(chat.sendMessage.notCalled).to.be.true;
253+
expect(ensureOptInAndSendStub.notCalled).to.be.true;
232254
});
233255

234256
it('displays user and assistant messages with different styling', function () {

packages/compass-assistant/src/assistant-chat.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useContext } from 'react';
22
import type { AssistantMessage } from './compass-assistant-provider';
3+
import { AssistantActionsContext } from './compass-assistant-provider';
34
import type { Chat } from './@ai-sdk/react/chat-react';
45
import { useChat } from './@ai-sdk/react/use-chat';
56
import {
@@ -129,7 +130,8 @@ export const AssistantChat: React.FunctionComponent<AssistantChatProps> = ({
129130
}) => {
130131
const track = useTelemetry();
131132
const darkMode = useDarkMode();
132-
const { messages, sendMessage, status, error, clearError } = useChat({
133+
const { ensureOptInAndSend } = useContext(AssistantActionsContext);
134+
const { messages, status, error, clearError } = useChat({
133135
chat,
134136
onError: (error) => {
135137
track('Assistant Response Failed', () => ({
@@ -157,13 +159,14 @@ export const AssistantChat: React.FunctionComponent<AssistantChatProps> = ({
157159
(messageBody: string) => {
158160
const trimmedMessageBody = messageBody.trim();
159161
if (trimmedMessageBody) {
160-
track('Assistant Prompt Submitted', {
161-
user_input_length: trimmedMessageBody.length,
162+
void ensureOptInAndSend?.({ text: trimmedMessageBody }, {}, () => {
163+
track('Assistant Prompt Submitted', {
164+
user_input_length: trimmedMessageBody.length,
165+
});
162166
});
163-
void sendMessage({ text: trimmedMessageBody });
164167
}
165168
},
166-
[sendMessage, track]
169+
[track, ensureOptInAndSend]
167170
);
168171

169172
const handleFeedback = useCallback(

0 commit comments

Comments
 (0)