From 759a9554744e42e98f8d3717dc702cdbcc54c331 Mon Sep 17 00:00:00 2001 From: Denys Mishunov <dmishunov@gitlab.com> Date: Thu, 19 Oct 2023 17:35:20 +0200 Subject: [PATCH] Swapped AIGenieChat with DuoChat component Changelog: changed MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134669 EE: true --- .../ai/tanuki_bot/components/app.vue | 48 ++++-- .../ai/tanuki_bot/components/app_spec.js | 143 +++++++----------- 2 files changed, 89 insertions(+), 102 deletions(-) diff --git a/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue b/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue index 59ad49a90c113..d630a08874d0b 100644 --- a/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue +++ b/ee/app/assets/javascripts/ai/tanuki_bot/components/app.vue @@ -1,15 +1,18 @@ <script> // eslint-disable-next-line no-restricted-imports import { mapActions, mapState } from 'vuex'; +import { GlDuoChat } from '@gitlab/ui'; import { v4 as uuidv4 } from 'uuid'; import { __, s__ } from '~/locale'; +import { renderMarkdown } from '~/notes/utils'; +import { renderGFM } from '~/behaviors/markdown/render_gfm'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { helpCenterState } from '~/super_sidebar/constants'; import aiResponseSubscription from 'ee/graphql_shared/subscriptions/ai_completion_response.subscription.graphql'; import getAiMessages from 'ee/ai/graphql/get_ai_messages.query.graphql'; import chatMutation from 'ee/ai/graphql/chat.mutation.graphql'; import Tracking from '~/tracking'; import { i18n, GENIE_CHAT_RESET_MESSAGE } from 'ee/ai/constants'; -import AiGenieChat from 'ee/ai/components/ai_genie_chat.vue'; import { TANUKI_BOT_TRACKING_EVENT_NAME } from '../constants'; export default { @@ -30,13 +33,15 @@ export default { __('How do I create a template?'), ], }, + experimentHelpPagePath: helpPagePath('policy/experiment-beta-support', { anchor: 'experiment' }), components: { - AiGenieChat, + GlDuoChat, }, mixins: [Tracking.mixin()], provide() { return { - trackingEventName: TANUKI_BOT_TRACKING_EVENT_NAME, + renderMarkdown, + renderGFM, }; }, props: { @@ -108,6 +113,7 @@ export default { return { helpCenterState, clientSubscriptionId: uuidv4(), + toolName: i18n.GITLAB_DUO, }; }, computed: { @@ -115,7 +121,7 @@ export default { }, methods: { ...mapActions(['addDuoChatMessage', 'setMessages', 'setLoading']), - sendMessage(question) { + onSendChatPrompt(question) { if (question !== GENIE_CHAT_RESET_MESSAGE) { this.setLoading(); } @@ -144,28 +150,38 @@ export default { }); }); }, - closeDrawer() { + onChatClose() { this.helpCenterState.showTanukiBotChatDrawer = false; }, + onTrackFeedback({ feedbackOptions, extendedFeedback } = {}) { + this.track(TANUKI_BOT_TRACKING_EVENT_NAME, { + action: 'click_button', + label: 'response_feedback', + property: feedbackOptions, + extra: { + extendedFeedback, + prompt_location: 'after_content', + }, + }); + }, }, }; </script> <template> <div> - <ai-genie-chat + <gl-duo-chat v-if="helpCenterState.showTanukiBotChatDrawer" - :is-loading="loading" + :title="$options.i18n.gitlabChat" :messages="messages" - :full-screen="true" + error="" + :is-loading="loading" :predefined-prompts="$options.i18n.predefinedPrompts" - is-chat-available - @send-chat-prompt="sendMessage" - @chat-hidden="closeDrawer" - > - <template #title> - {{ $options.i18n.gitlabChat }} - </template> - </ai-genie-chat> + :experiment-help-page-url="$options.experimentHelpPagePath" + :tool-name="toolName" + @send-chat-prompt="onSendChatPrompt" + @chat-hidden="onChatClose" + @track-feedback="onTrackFeedback" + /> </div> </template> diff --git a/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js b/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js index 318d6dc45a71b..2ca66def3a8aa 100644 --- a/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js +++ b/ee/spec/frontend/ai/tanuki_bot/components/app_spec.js @@ -1,15 +1,11 @@ -import { GlSprintf } from '@gitlab/ui'; +import { GlDuoChat } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import { v4 as uuidv4 } from 'uuid'; // eslint-disable-next-line no-restricted-imports import Vuex from 'vuex'; import VueApollo from 'vue-apollo'; import TanukiBotChatApp from 'ee/ai/tanuki_bot/components/app.vue'; -import AiGenieChat from 'ee/ai/components/ai_genie_chat.vue'; -import AiGenieChatConversation from 'ee/ai/components/ai_genie_chat_conversation.vue'; -import AiGenieChatMessage from 'ee/ai/components/ai_genie_chat_message.vue'; -import UserFeedback from 'ee/ai/components/user_feedback.vue'; -import { i18n, GENIE_CHAT_RESET_MESSAGE } from 'ee/ai/constants'; +import { GENIE_CHAT_RESET_MESSAGE } from 'ee/ai/constants'; import { TANUKI_BOT_TRACKING_EVENT_NAME } from 'ee/ai/tanuki_bot/constants'; import aiResponseSubscription from 'ee/graphql_shared/subscriptions/ai_completion_response.subscription.graphql'; import chatMutation from 'ee/ai/graphql/chat.mutation.graphql'; @@ -22,7 +18,6 @@ import waitForPromises from 'helpers/wait_for_promises'; import { helpCenterState } from '~/super_sidebar/constants'; import { MOCK_USER_MESSAGE, - MOCK_TANUKI_MESSAGE, MOCK_USER_ID, MOCK_RESOURCE_ID, MOCK_TANUKI_SUCCESS_RES, @@ -70,19 +65,10 @@ describe('GitLab Duo Chat', () => { store, apolloProvider, propsData, - stubs: { - AiGenieChat, - GlSprintf, - AiGenieChatConversation, - AiGenieChatMessage, - }, }); }; - const findWarning = () => wrapper.findByTestId('chat-legal-warning'); - const findGenieChat = () => wrapper.findComponent(AiGenieChat); - const findGeneratedByAI = () => wrapper.findByText(i18n.GENIE_CHAT_LEGAL_GENERATED_BY_AI); - const findAllUserFeedback = () => wrapper.findAllComponents(UserFeedback); + const findGlDuoChat = () => wrapper.findComponent(GlDuoChat); beforeEach(() => { uuidv4.mockImplementation(() => '123'); @@ -95,83 +81,57 @@ describe('GitLab Duo Chat', () => { expect(wrapper.vm.clientSubscriptionId).toBe('123'); }); + it('fetches the cached messages on mount', () => { + createComponent(); + expect(queryHandlerMock).toHaveBeenCalled(); + }); + describe('rendering', () => { beforeEach(() => { createComponent(); helpCenterState.showTanukiBotChatDrawer = true; }); - it('renders a legal info when rendered', () => { - expect(findWarning().exists()).toBe(true); - }); - - it('renders a generated by AI note', () => { - expect(findGeneratedByAI().exists()).toBe(true); - }); - - it('passes down the example prompts', () => { - expect(findGenieChat().props().predefinedPrompts).toEqual( - wrapper.vm.$options.i18n.predefinedPrompts, - ); + it('renders the DuoChat component', () => { + expect(findGlDuoChat().exists()).toBe(true); }); }); - describe('AiGenieChat interactions', () => { + describe('chat props', () => { beforeEach(() => { createComponent(); helpCenterState.showTanukiBotChatDrawer = true; }); - - it('closes the chat on @chat-hidden', async () => { - findGenieChat().vm.$emit('chat-hidden'); - await nextTick(); - expect(helpCenterState.showTanukiBotChatDrawer).toBe(false); - expect(findGenieChat().exists()).toBe(false); - }); }); - describe('Chat', () => { + describe('events handling', () => { beforeEach(() => { - createComponent({ loading: true }); + createComponent(); helpCenterState.showTanukiBotChatDrawer = true; }); - it('renders AiGenieChat component', () => { - expect(findGenieChat().exists()).toBe(true); - }); - - it('fetches the cached messages on mount', () => { - expect(queryHandlerMock).toHaveBeenCalled(); - }); - - it('renders the User Feedback component for every assistent mesage', () => { - createComponent({ - messages: [MOCK_USER_MESSAGE, MOCK_TANUKI_MESSAGE, MOCK_USER_MESSAGE, MOCK_TANUKI_MESSAGE], + describe('@chat-hidden', () => { + beforeEach(async () => { + findGlDuoChat().vm.$emit('chat-hidden'); + await nextTick(); }); - expect(findAllUserFeedback().length).toBe(2); - - findAllUserFeedback().wrappers.forEach((component) => { - expect(component.props('eventName')).toBe(TANUKI_BOT_TRACKING_EVENT_NAME); - expect(component.props('promptLocation')).toBe('after_content'); + it('closes the chat on @chat-hidden', () => { + expect(helpCenterState.showTanukiBotChatDrawer).toBe(false); + expect(findGlDuoChat().exists()).toBe(false); }); }); - describe('when input is submitted', () => { - beforeEach(() => { - findGenieChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.msg); + describe('@send-chat-prompt', () => { + it('does set loading to `true` for a message other than the reset one', () => { + findGlDuoChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.msg); + expect(actionSpies.setLoading).toHaveBeenCalled(); }); - - describe('loading state', () => { - it('does set loading to `true` for a message other than the reset one', () => { - expect(actionSpies.setLoading).toHaveBeenCalled(); - }); - it('does not set loading to `true` for a reset message', async () => { - actionSpies.setLoading.mockReset(); - findGenieChat().vm.$emit('send-chat-prompt', GENIE_CHAT_RESET_MESSAGE); - await nextTick(); - expect(actionSpies.setLoading).not.toHaveBeenCalled(); - }); + it('does not set loading to `true` for a reset message', async () => { + actionSpies.setLoading.mockReset(); + findGlDuoChat().vm.$emit('send-chat-prompt', GENIE_CHAT_RESET_MESSAGE); + await nextTick(); + expect(actionSpies.setLoading).not.toHaveBeenCalled(); }); describe.each` @@ -186,7 +146,7 @@ describe('GitLab Duo Chat', () => { 'calls correct GraphQL mutation with fallback to userId when input is submitted and feature flag is $isFlagEnabled', async ({ expectedMutation } = {}) => { createComponent({}, { userId: MOCK_USER_ID, resourceId }); - findGenieChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.msg); + findGlDuoChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.msg); await nextTick(); @@ -219,25 +179,36 @@ describe('GitLab Duo Chat', () => { MOCK_TANUKI_SUCCESS_RES.data.aiCompletionResponse, ); }); + }); + }); - describe('snowplow tracking', () => { - let trackingSpy; + describe('@track-feedback', () => { + let trackingSpy; - beforeEach(() => { - trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - }); + beforeEach(() => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }); - afterEach(() => { - unmockTracking(); - }); + afterEach(() => { + unmockTracking(); + }); - it('tracks the snowplow event on successful mutation for chat', async () => { - createComponent(); - findGenieChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.msg); + it('tracks the snowplow event on successful mutation for chat', async () => { + createComponent(); + findGlDuoChat().vm.$emit('track-feedback', { + feedbackOptions: ['foo', 'bar'], + extendedFeedback: 'baz', + }); - await waitForPromises(); - expect(trackingSpy).toHaveBeenCalled(); - }); + await waitForPromises(); + expect(trackingSpy).toHaveBeenCalledWith(undefined, TANUKI_BOT_TRACKING_EVENT_NAME, { + action: 'click_button', + label: 'response_feedback', + property: ['foo', 'bar'], + extra: { + extendedFeedback: 'baz', + prompt_location: 'after_content', + }, }); }); }); @@ -258,7 +229,7 @@ describe('GitLab Duo Chat', () => { helpCenterState.showTanukiBotChatDrawer = true; await nextTick(); - findGenieChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.msg); + findGlDuoChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.msg); }); it('once error arrives via GraphQL subscription calls addDuoChatMessage', () => { @@ -288,7 +259,7 @@ describe('GitLab Duo Chat', () => { helpCenterState.showTanukiBotChatDrawer = true; await nextTick(); - findGenieChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.msg); + findGlDuoChat().vm.$emit('send-chat-prompt', MOCK_USER_MESSAGE.msg); }); it('calls addDuoChatMessage', () => { -- GitLab