Skip to content

Commit 26291d1

Browse files
committed
chore(compass-assistant): add confirmation step to explain plan entry point COMPASS-9836
1 parent c4721ff commit 26291d1

File tree

7 files changed

+599
-44
lines changed

7 files changed

+599
-44
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
showConfirmation,
99
spacing,
1010
} from '@mongodb-js/compass-components';
11-
import { AssistantChat } from './assistant-chat';
11+
import { AssistantChat } from './components/assistant-chat';
1212
import {
1313
ASSISTANT_DRAWER_ID,
1414
AssistantActionsContext,

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

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ export type AssistantMessage = UIMessage & {
4040
* Used for warning messages in cases like using non-genuine MongoDB.
4141
*/
4242
isPermanent?: boolean;
43+
/** Information for confirmation messages. */
44+
confirmation?: {
45+
description: string;
46+
state: 'confirmed' | 'rejected' | 'pending';
47+
};
4348
};
4449
};
4550

@@ -174,7 +179,17 @@ export const AssistantProvider: React.FunctionComponent<
174179

175180
const { prompt, displayText } = builder(props);
176181
void assistantActionsContext.current.ensureOptInAndSend(
177-
{ text: prompt, metadata: { displayText } },
182+
{
183+
text: prompt,
184+
metadata: {
185+
displayText,
186+
confirmation: {
187+
description:
188+
'Explain plan metadata, including the original query, may be used to process your request',
189+
state: 'pending',
190+
},
191+
},
192+
},
178193
{},
179194
() => {
180195
openDrawer(ASSISTANT_DRAWER_ID);
@@ -185,17 +200,17 @@ export const AssistantProvider: React.FunctionComponent<
185200
}
186201
);
187202
};
188-
});
203+
}).current;
189204
const assistantActionsContext = useRef<AssistantActionsContextType>({
190-
interpretExplainPlan: createEntryPointHandler.current(
205+
interpretExplainPlan: createEntryPointHandler(
191206
'explain plan',
192207
buildExplainPlanPrompt
193208
),
194-
interpretConnectionError: createEntryPointHandler.current(
209+
interpretConnectionError: createEntryPointHandler(
195210
'connection error',
196211
buildConnectionErrorPrompt
197212
),
198-
tellMoreAboutInsight: createEntryPointHandler.current(
213+
tellMoreAboutInsight: createEntryPointHandler(
199214
'performance insights',
200215
buildProactiveInsightsPrompt
201216
),
@@ -220,6 +235,10 @@ export const AssistantProvider: React.FunctionComponent<
220235
// place to do tracking.
221236
callback();
222237

238+
if (chat.status === 'streaming') {
239+
await chat.stop();
240+
}
241+
223242
await chat.sendMessage(message, options);
224243
},
225244
});

packages/compass-assistant/src/assistant-chat.spec.tsx renamed to packages/compass-assistant/src/components/assistant-chat.spec.tsx

Lines changed: 234 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import {
77
} from '@mongodb-js/testing-library-compass';
88
import { AssistantChat } from './assistant-chat';
99
import { expect } from 'chai';
10-
import { createMockChat } from '../test/utils';
10+
import { createMockChat } from '../../test/utils';
1111
import type { ConnectionInfo } from '@mongodb-js/connection-info';
1212
import {
1313
AssistantActionsContext,
1414
type AssistantMessage,
15-
} from './compass-assistant-provider';
15+
} from '../compass-assistant-provider';
1616
import sinon from 'sinon';
17+
import type { TextPart } from 'ai';
1718

1819
describe('AssistantChat', function () {
1920
const mockMessages: AssistantMessage[] = [
@@ -527,4 +528,235 @@ describe('AssistantChat', function () {
527528
expect(screen.queryByLabelText('Thumbs Down Icon')).to.not.exist;
528529
});
529530
});
531+
532+
describe('messages with confirmation', function () {
533+
let mockConfirmationMessage: AssistantMessage;
534+
535+
beforeEach(function () {
536+
mockConfirmationMessage = {
537+
id: 'confirmation-test',
538+
role: 'assistant',
539+
parts: [{ type: 'text', text: 'This is a confirmation message.' }],
540+
metadata: {
541+
confirmation: {
542+
state: 'pending',
543+
description: 'Are you sure you want to proceed with this action?',
544+
},
545+
},
546+
};
547+
});
548+
549+
it('renders confirmation message when message has confirmation metadata', function () {
550+
renderWithChat([mockConfirmationMessage]);
551+
552+
expect(screen.getByText('Please confirm your request')).to.exist;
553+
expect(
554+
screen.getByText('Are you sure you want to proceed with this action?')
555+
).to.exist;
556+
expect(screen.getByText('Confirm')).to.exist;
557+
expect(screen.getByText('Cancel')).to.exist;
558+
});
559+
560+
it('does not render regular message content when confirmation metadata exists', function () {
561+
renderWithChat([mockConfirmationMessage]);
562+
563+
// Should not show the message text content when confirmation is present
564+
expect(screen.queryByText('This is a confirmation message.')).to.not
565+
.exist;
566+
});
567+
568+
it('shows confirmation as pending when it is the last message', function () {
569+
renderWithChat([mockConfirmationMessage]);
570+
571+
expect(screen.getByText('Confirm')).to.exist;
572+
expect(screen.getByText('Cancel')).to.exist;
573+
expect(screen.queryByText('Request confirmed')).to.not.exist;
574+
expect(screen.queryByText('Request cancelled')).to.not.exist;
575+
});
576+
577+
it('shows confirmation as rejected when it is not the last message', function () {
578+
const messages: AssistantMessage[] = [
579+
mockConfirmationMessage,
580+
{
581+
id: 'newer-message',
582+
role: 'user' as const,
583+
parts: [{ type: 'text', text: 'Another message' }],
584+
},
585+
];
586+
587+
renderWithChat(messages);
588+
589+
// The confirmation message (first one) should show as rejected since it's not the last
590+
expect(screen.queryByText('Confirm')).to.not.exist;
591+
expect(screen.queryByText('Cancel')).to.not.exist;
592+
expect(screen.getByText('Request cancelled')).to.exist;
593+
});
594+
595+
it('adds new confirmed message when confirmation is confirmed', function () {
596+
const { chat, ensureOptInAndSendStub } = renderWithChat([
597+
mockConfirmationMessage,
598+
]);
599+
600+
const confirmButton = screen.getByText('Confirm');
601+
userEvent.click(confirmButton);
602+
603+
// Should add a new message without confirmation metadata
604+
expect(chat.messages).to.have.length(2);
605+
const newMessage = chat.messages[1];
606+
expect(newMessage.id).to.equal('confirmation-test-confirmed');
607+
expect(newMessage.metadata?.confirmation).to.be.undefined;
608+
expect(newMessage.parts).to.deep.equal(mockConfirmationMessage.parts);
609+
610+
// Should call ensureOptInAndSend to send the new message
611+
expect(ensureOptInAndSendStub.calledOnce).to.be.true;
612+
});
613+
614+
it('updates confirmation state to confirmed and adds a new message when confirm button is clicked', function () {
615+
const { chat } = renderWithChat([mockConfirmationMessage]);
616+
617+
const confirmButton = screen.getByText('Confirm');
618+
userEvent.click(confirmButton);
619+
620+
// Original message should have updated confirmation state
621+
const originalMessage = chat.messages[0];
622+
expect(originalMessage.metadata?.confirmation?.state).to.equal(
623+
'confirmed'
624+
);
625+
626+
expect(chat.messages).to.have.length(2);
627+
628+
expect(
629+
screen.getByText((mockConfirmationMessage.parts[0] as TextPart).text)
630+
).to.exist;
631+
});
632+
633+
it('updates confirmation state to rejected and does not add a new message when cancel button is clicked', function () {
634+
const { chat, ensureOptInAndSendStub } = renderWithChat([
635+
mockConfirmationMessage,
636+
]);
637+
638+
const cancelButton = screen.getByText('Cancel');
639+
userEvent.click(cancelButton);
640+
641+
// Original message should have updated confirmation state
642+
const originalMessage = chat.messages[0];
643+
expect(originalMessage.metadata?.confirmation?.state).to.equal(
644+
'rejected'
645+
);
646+
647+
// Should not add a new message
648+
expect(chat.messages).to.have.length(1);
649+
650+
// Should not call ensureOptInAndSend
651+
expect(ensureOptInAndSendStub.notCalled).to.be.true;
652+
});
653+
654+
it('shows confirmed status after confirmation is confirmed', function () {
655+
const { chat } = renderWithChat([mockConfirmationMessage]);
656+
657+
// Verify buttons are initially present
658+
expect(screen.getByText('Confirm')).to.exist;
659+
expect(screen.getByText('Cancel')).to.exist;
660+
661+
const confirmButton = screen.getByText('Confirm');
662+
userEvent.click(confirmButton);
663+
664+
// The state update should be immediate - check the chat messages
665+
const updatedMessage = chat.messages[0];
666+
expect(updatedMessage.metadata?.confirmation?.state).to.equal(
667+
'confirmed'
668+
);
669+
});
670+
671+
it('shows cancelled status after confirmation is rejected', function () {
672+
const { chat } = renderWithChat([mockConfirmationMessage]);
673+
674+
// Verify buttons are initially present
675+
expect(screen.getByText('Confirm')).to.exist;
676+
expect(screen.getByText('Cancel')).to.exist;
677+
678+
const cancelButton = screen.getByText('Cancel');
679+
userEvent.click(cancelButton);
680+
681+
// The state update should be immediate - check the chat messages
682+
const updatedMessage = chat.messages[0];
683+
expect(updatedMessage.metadata?.confirmation?.state).to.equal('rejected');
684+
});
685+
686+
it('handles multiple confirmation messages correctly', function () {
687+
const confirmationMessage1: AssistantMessage = {
688+
id: 'confirmation-1',
689+
role: 'assistant',
690+
parts: [{ type: 'text', text: 'First confirmation' }],
691+
metadata: {
692+
confirmation: {
693+
state: 'pending',
694+
description: 'First confirmation description',
695+
},
696+
},
697+
};
698+
699+
const confirmationMessage2: AssistantMessage = {
700+
id: 'confirmation-2',
701+
role: 'assistant',
702+
parts: [{ type: 'text', text: 'Second confirmation' }],
703+
metadata: {
704+
confirmation: {
705+
state: 'pending',
706+
description: 'Second confirmation description',
707+
},
708+
},
709+
};
710+
711+
renderWithChat([confirmationMessage1, confirmationMessage2]);
712+
713+
expect(screen.getAllByText('Request cancelled')).to.have.length(1);
714+
715+
expect(screen.getAllByText('Confirm')).to.have.length(1);
716+
expect(screen.getAllByText('Cancel')).to.have.length(1);
717+
expect(screen.getByText('Second confirmation description')).to.exist;
718+
});
719+
720+
it('preserves other metadata when creating confirmed message', function () {
721+
const messageWithExtraMetadata: AssistantMessage = {
722+
id: 'confirmation-with-metadata',
723+
role: 'assistant',
724+
parts: [{ type: 'text', text: 'Message with extra metadata' }],
725+
metadata: {
726+
confirmation: {
727+
state: 'pending',
728+
description: 'Confirmation description',
729+
},
730+
displayText: 'Custom display text',
731+
isPermanent: true,
732+
},
733+
};
734+
735+
const { chat } = renderWithChat([messageWithExtraMetadata]);
736+
737+
const confirmButton = screen.getByText('Confirm');
738+
userEvent.click(confirmButton);
739+
740+
// New confirmed message should preserve other metadata
741+
const newMessage = chat.messages[1];
742+
expect(newMessage.metadata?.displayText).to.equal('Custom display text');
743+
expect(newMessage.metadata?.isPermanent).to.equal(true);
744+
expect(newMessage.metadata?.confirmation).to.be.undefined;
745+
});
746+
747+
it('does not render confirmation component for regular messages', function () {
748+
const regularMessage: AssistantMessage = {
749+
id: 'regular',
750+
role: 'assistant',
751+
parts: [{ type: 'text', text: 'This is a regular message' }],
752+
};
753+
754+
renderWithChat([regularMessage]);
755+
756+
expect(screen.queryByText('Please confirm your request')).to.not.exist;
757+
expect(screen.queryByText('Confirm')).to.not.exist;
758+
expect(screen.queryByText('Cancel')).to.not.exist;
759+
expect(screen.getByText('This is a regular message')).to.exist;
760+
});
761+
});
530762
});

0 commit comments

Comments
 (0)