@gitlab/ui 66.32.0 → 66.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,391 @@
1
+ import { nextTick } from 'vue';
2
+ import { shallowMount } from '@vue/test-utils';
3
+ import GlEmptyState from '../../../regions/empty_state/empty_state.vue';
4
+ import GlExperimentBadge from '../../experiment_badge/experiment_badge.vue';
5
+ import DuoChatLoader from './components/duo_chat_loader/duo_chat_loader.vue';
6
+ import DuoChatPredefinedPrompts from './components/duo_chat_predefined_prompts/duo_chat_predefined_prompts.vue';
7
+ import DuoChatConversation from './components/duo_chat_conversation/duo_chat_conversation.vue';
8
+ import GlDuoChat from './duo_chat.vue';
9
+
10
+ import { MESSAGE_MODEL_ROLES, CHAT_RESET_MESSAGE } from './constants';
11
+
12
+ describe('GlDuoChat', () => {
13
+ let wrapper;
14
+
15
+ const createComponent = ({ propsData = {}, data = {}, slots = {} } = {}) => {
16
+ jest.spyOn(DuoChatLoader.methods, 'computeTransitionWidth').mockImplementation();
17
+
18
+ wrapper = shallowMount(GlDuoChat, {
19
+ propsData,
20
+ data() {
21
+ return {
22
+ ...data,
23
+ };
24
+ },
25
+ slots,
26
+ stubs: {
27
+ DuoChatLoader,
28
+ },
29
+ });
30
+ };
31
+
32
+ const findChatComponent = () => wrapper.find('[data-testid="chat-component"]');
33
+ const findCloseButton = () => wrapper.find('[data-testid="chat-close-button"]');
34
+ const findChatConversations = () => wrapper.findAllComponents(DuoChatConversation);
35
+ const findCustomLoader = () => wrapper.findComponent(DuoChatLoader);
36
+ const findError = () => wrapper.find('[data-testid="chat-error"]');
37
+ const findFooter = () => wrapper.find('[data-testid="chat-footer"]');
38
+ const findPromptForm = () => wrapper.find('[data-testid="chat-prompt-form"]');
39
+ const findGeneratedByAI = () => wrapper.find('[data-testid="chat-legal-warning"]');
40
+ const findBadge = () => wrapper.findComponent(GlExperimentBadge);
41
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
42
+ const findPredefined = () => wrapper.findComponent(DuoChatPredefinedPrompts);
43
+ const findChatInput = () => wrapper.find('[data-testid="chat-prompt-input"]');
44
+ const findCloseChatButton = () => wrapper.find('[data-testid="chat-close-button"]');
45
+ const findLegalDisclaimer = () => wrapper.find('[data-testid="chat-legal-disclaimer"]');
46
+
47
+ beforeEach(() => {
48
+ createComponent();
49
+ });
50
+
51
+ const promptStr = 'foo';
52
+ const messages = [
53
+ {
54
+ role: MESSAGE_MODEL_ROLES.user,
55
+ content: promptStr,
56
+ },
57
+ ];
58
+
59
+ describe('rendering', () => {
60
+ it('does not fail if no messages are passed', () => {
61
+ createComponent({
62
+ propsData: { messages: null },
63
+ });
64
+ expect(findChatConversations()).toHaveLength(0);
65
+ expect(findEmptyState().exists()).toBe(true);
66
+ });
67
+
68
+ it.each`
69
+ desc | component | shouldRender
70
+ ${'renders root component'} | ${findChatComponent} | ${true}
71
+ ${'renders experimental label'} | ${findBadge} | ${true}
72
+ ${'renders empty state'} | ${findEmptyState} | ${true}
73
+ ${'renders predefined prompts'} | ${findPredefined} | ${true}
74
+ ${'does not render loading skeleton'} | ${findCustomLoader} | ${false}
75
+ ${'does not render chat error'} | ${findError} | ${false}
76
+ ${'does render chat input'} | ${findChatInput} | ${true}
77
+ ${'renders a generated by AI note'} | ${findGeneratedByAI} | ${true}
78
+ `('$desc', ({ component, shouldRender }) => {
79
+ expect(component().exists()).toBe(shouldRender);
80
+ });
81
+
82
+ describe('when messages exist', () => {
83
+ it('scrolls to the bottom on load', async () => {
84
+ createComponent({ propsData: { messages } });
85
+ const { element } = findChatComponent();
86
+ jest.spyOn(element, 'scrollHeight', 'get').mockReturnValue(200);
87
+
88
+ await nextTick();
89
+
90
+ expect(element.scrollTop).toEqual(200);
91
+ });
92
+ });
93
+
94
+ describe('conversations', () => {
95
+ it('renders one conversation when no reset message is present', () => {
96
+ const newMessages = [
97
+ {
98
+ role: MESSAGE_MODEL_ROLES.user,
99
+ content: 'How are you?',
100
+ },
101
+ {
102
+ role: MESSAGE_MODEL_ROLES.assistant,
103
+ content: 'Great!',
104
+ },
105
+ ];
106
+ createComponent({ propsData: { messages: newMessages } });
107
+
108
+ expect(findChatConversations().length).toEqual(1);
109
+ expect(findChatConversations().at(0).props('showDelimiter')).toEqual(false);
110
+ });
111
+
112
+ it('does not render conversations when no message is present', () => {
113
+ createComponent({ propsData: { messages: [] } });
114
+
115
+ expect(findChatConversations().length).toEqual(0);
116
+ });
117
+
118
+ it('splits it up into multiple conversations when reset message is present', () => {
119
+ const newMessages = [
120
+ {
121
+ role: MESSAGE_MODEL_ROLES.user,
122
+ content: 'Message 1',
123
+ },
124
+ {
125
+ role: MESSAGE_MODEL_ROLES.assistant,
126
+ content: 'Great!',
127
+ },
128
+ {
129
+ role: MESSAGE_MODEL_ROLES.user,
130
+ content: CHAT_RESET_MESSAGE,
131
+ },
132
+ ];
133
+ createComponent({ propsData: { messages: newMessages } });
134
+
135
+ expect(findChatConversations().length).toEqual(2);
136
+ expect(findChatConversations().at(0).props('showDelimiter')).toEqual(false);
137
+ expect(findChatConversations().at(1).props('showDelimiter')).toEqual(true);
138
+ });
139
+ });
140
+
141
+ describe('slots', () => {
142
+ const slotContent = 'As Gregor Samsa awoke one morning from uneasy dreams';
143
+
144
+ it.each`
145
+ slot | content | isChatAvailable
146
+ ${'hero'} | ${slotContent} | ${true}
147
+ ${'hero'} | ${slotContent} | ${false}
148
+ ${'subheader'} | ${slotContent} | ${false}
149
+ ${'subheader'} | ${slotContent} | ${true}
150
+ `(
151
+ 'renders the $content passed to the $slot slot when isChatAvailable is $isChatAvailable',
152
+ ({ slot, content, isChatAvailable }) => {
153
+ createComponent({
154
+ propsData: { isChatAvailable },
155
+ slots: { [slot]: content },
156
+ });
157
+ expect(wrapper.text()).toContain(content);
158
+ }
159
+ );
160
+ });
161
+
162
+ it('sets correct props on the Experiment badge', () => {
163
+ const experimentHelpPageUrl = 'https://foo.bar';
164
+ const containerId = 'chat-component';
165
+ createComponent({ propsData: { experimentHelpPageUrl } });
166
+ expect(findBadge().props('experimentHelpPageUrl')).toBe(experimentHelpPageUrl);
167
+ expect(findBadge().attributes('container-id')).toBe(containerId);
168
+ });
169
+ });
170
+
171
+ describe('chat', () => {
172
+ const clickSubmit = () =>
173
+ findPromptForm().vm.$emit('submit', {
174
+ preventDefault: jest.fn(),
175
+ stopPropagation: jest.fn(),
176
+ });
177
+
178
+ it('does render the prompt input by default', () => {
179
+ createComponent({ propsData: { messages } });
180
+ expect(findChatInput().exists()).toBe(true);
181
+ });
182
+
183
+ it('does not render the prompt input if `isChatAvailable` prop is `false`', () => {
184
+ createComponent({ propsData: { messages, isChatAvailable: false } });
185
+ expect(findChatInput().exists()).toBe(false);
186
+ });
187
+
188
+ it('renders the legal disclaimer if `isChatAvailable` prop is `true', () => {
189
+ createComponent({ propsData: { messages, isChatAvailable: true } });
190
+ expect(findLegalDisclaimer().exists()).toBe(true);
191
+ });
192
+
193
+ describe('submit', () => {
194
+ const ENTER = 'Enter';
195
+
196
+ it.each`
197
+ trigger | event | action | expectEmitted
198
+ ${() => clickSubmit()} | ${'Submit button click'} | ${'submit'} | ${[[promptStr]]}
199
+ ${() => findChatInput().trigger('keydown.enter', { code: ENTER })} | ${`Clicking ${ENTER}`} | ${'submit'} | ${[[promptStr]]}
200
+ ${() => findChatInput().trigger('keydown.enter', { code: ENTER, metaKey: true })} | ${`Clicking ${ENTER} + ⌘`} | ${'not submit'} | ${undefined}
201
+ ${() => findChatInput().trigger('keydown.enter', { code: ENTER, altKey: true })} | ${`Clicking ${ENTER} + ⎇`} | ${'not submit'} | ${undefined}
202
+ ${() => findChatInput().trigger('keydown.enter', { code: ENTER, shiftKey: true })} | ${`Clicking ${ENTER} + ⬆︎`} | ${'not submit'} | ${undefined}
203
+ ${() => findChatInput().trigger('keydown.enter', { code: ENTER, ctrlKey: true })} | ${`Clicking ${ENTER} + CTRL`} | ${'not submit'} | ${undefined}
204
+ `('$event should $action the prompt form', ({ trigger, expectEmitted } = {}) => {
205
+ createComponent({
206
+ propsData: { messages: [], isChatAvailable: true },
207
+ data: { prompt: promptStr },
208
+ });
209
+ trigger();
210
+ if (expectEmitted) {
211
+ expect(wrapper.emitted('send-chat-prompt')).toEqual(expectEmitted);
212
+ } else {
213
+ expect(wrapper.emitted('send-chat-prompt')).toBe(undefined);
214
+ }
215
+ });
216
+ });
217
+
218
+ describe('reset', () => {
219
+ it('emits the event with the reset prompt', () => {
220
+ createComponent({
221
+ propsData: { messages, isChatAvailable: true },
222
+ data: { prompt: CHAT_RESET_MESSAGE },
223
+ });
224
+ clickSubmit();
225
+
226
+ expect(wrapper.emitted('send-chat-prompt')).toEqual([[CHAT_RESET_MESSAGE]]);
227
+ expect(findChatConversations().length).toEqual(1);
228
+ });
229
+
230
+ it('reset does nothing when chat is loading', () => {
231
+ createComponent({
232
+ propsData: { messages, isChatAvailable: true, isLoading: true },
233
+ data: { prompt: CHAT_RESET_MESSAGE },
234
+ });
235
+ clickSubmit();
236
+
237
+ expect(wrapper.emitted('send-chat-prompt')).toBeUndefined();
238
+ expect(findChatConversations().length).toEqual(1);
239
+ });
240
+
241
+ it('reset does nothing when there are no messages', () => {
242
+ createComponent({
243
+ propsData: { messages: [], isChatAvailable: true },
244
+ data: { prompt: CHAT_RESET_MESSAGE },
245
+ });
246
+ clickSubmit();
247
+
248
+ expect(wrapper.emitted('send-chat-prompt')).toBeUndefined();
249
+ expect(findChatConversations().length).toEqual(0);
250
+ });
251
+
252
+ it('reset does nothing when last message was a reset message', () => {
253
+ const existingMessages = [
254
+ ...messages,
255
+ {
256
+ role: MESSAGE_MODEL_ROLES.user,
257
+ content: CHAT_RESET_MESSAGE,
258
+ },
259
+ ];
260
+ createComponent({
261
+ propsData: {
262
+ isLoading: false,
263
+ messages: existingMessages,
264
+ isChatAvailable: true,
265
+ },
266
+ data: { prompt: CHAT_RESET_MESSAGE },
267
+ });
268
+ clickSubmit();
269
+
270
+ expect(wrapper.emitted('send-chat-prompt')).toBeUndefined();
271
+
272
+ expect(findChatConversations().length).toEqual(2);
273
+ expect(findChatConversations().at(0).props('messages')).toEqual(messages);
274
+ expect(findChatConversations().at(1).props('messages')).toEqual([]);
275
+ });
276
+ });
277
+ });
278
+
279
+ describe('interaction', () => {
280
+ it('is hidden after the header button is clicked', async () => {
281
+ findCloseButton().vm.$emit('click');
282
+ await nextTick();
283
+ expect(findChatComponent().exists()).toBe(false);
284
+ });
285
+
286
+ it('resets the hidden status of the component on loading', async () => {
287
+ createComponent({ data: { isHidden: true } });
288
+ expect(findChatComponent().exists()).toBe(false);
289
+ // setProps is justified here because we are testing the component's
290
+ // reactive behavior which consistutes an exception
291
+ // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
292
+ wrapper.setProps({
293
+ isLoading: true,
294
+ });
295
+ await nextTick();
296
+ expect(findChatComponent().exists()).toBe(true);
297
+ });
298
+
299
+ it('resets the prompt when new messages are added', async () => {
300
+ const prompt = 'foo';
301
+ createComponent({ data: { prompt } });
302
+ expect(findChatInput().props('value')).toBe(prompt);
303
+ // setProps is justified here because we are testing the component's
304
+ // reactive behavior which consistutes an exception
305
+ // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
306
+ wrapper.setProps({
307
+ messages,
308
+ });
309
+ await nextTick();
310
+ expect(findChatInput().props('value')).toBe('');
311
+ });
312
+
313
+ it('renders custom loader when isLoading', () => {
314
+ createComponent({ propsData: { isLoading: true } });
315
+ expect(findCustomLoader().exists()).toBe(true);
316
+ });
317
+
318
+ it('renders alert if error', () => {
319
+ const errorMessage = 'Something went Wrong';
320
+ createComponent({ propsData: { error: errorMessage } });
321
+ expect(findError().text()).toBe(errorMessage);
322
+ });
323
+
324
+ it('hides the chat on button click and emits an event', async () => {
325
+ createComponent({ propsData: { messages } });
326
+ expect(findChatComponent().exists()).toBe(true);
327
+ findCloseChatButton().vm.$emit('click');
328
+ await nextTick();
329
+ expect(findChatComponent().exists()).toBe(false);
330
+ expect(wrapper.emitted('chat-hidden')).toBeDefined();
331
+ });
332
+
333
+ it('does not render the empty state when there are messages available', () => {
334
+ createComponent({ propsData: { messages } });
335
+ expect(findEmptyState().exists()).toBe(false);
336
+ });
337
+
338
+ describe('scrolling', () => {
339
+ let element;
340
+
341
+ beforeEach(() => {
342
+ createComponent({ propsData: { messages, isChatAvailable: true } });
343
+ element = findChatComponent().element;
344
+ });
345
+
346
+ it('when scrolling to the bottom it removes the scrim class', async () => {
347
+ jest.spyOn(element, 'scrollTop', 'get').mockReturnValue(100);
348
+ jest.spyOn(element, 'offsetHeight', 'get').mockReturnValue(100);
349
+ jest.spyOn(element, 'scrollHeight', 'get').mockReturnValue(200);
350
+
351
+ findChatComponent().trigger('scroll');
352
+
353
+ await nextTick();
354
+
355
+ expect(findFooter().classes()).not.toContain('gl-drawer-body-scrim-on-footer');
356
+ });
357
+
358
+ it('when scrolling up it adds the scrim class', async () => {
359
+ jest.spyOn(element, 'scrollTop', 'get').mockReturnValue(50);
360
+ jest.spyOn(element, 'offsetHeight', 'get').mockReturnValue(100);
361
+ jest.spyOn(element, 'scrollHeight', 'get').mockReturnValue(200);
362
+
363
+ findChatComponent().trigger('scroll');
364
+
365
+ await nextTick();
366
+
367
+ expect(findFooter().classes()).toContain('gl-drawer-body-scrim-on-footer');
368
+ });
369
+ });
370
+
371
+ describe('predefined prompts', () => {
372
+ const prompts = ['what is a fork'];
373
+
374
+ beforeEach(() => {
375
+ createComponent({ propsData: { predefinedPrompts: prompts } });
376
+ });
377
+
378
+ it('passes on predefined prompts', () => {
379
+ expect(findPredefined().props().prompts).toEqual(prompts);
380
+ });
381
+
382
+ it('listens to the click event and sends the predefined prompt', async () => {
383
+ findPredefined().vm.$emit('click', prompts[0]);
384
+
385
+ await nextTick();
386
+
387
+ expect(wrapper.emitted('send-chat-prompt')).toEqual([[prompts[0]]]);
388
+ });
389
+ });
390
+ });
391
+ });
@@ -0,0 +1,216 @@
1
+ import GlButton from '../../../base/button/button.vue';
2
+ import GlAlert from '../../../base/alert/alert.vue';
3
+ import { setStoryTimeout } from '../../../../utils/test_utils';
4
+ import { makeContainer } from '../../../../utils/story_decorators/container';
5
+ import GlDuoChat from './duo_chat.vue';
6
+ import readme from './duo_chat.md';
7
+ import {
8
+ MOCK_RESPONSE_MESSAGE,
9
+ MOCK_USER_PROMPT_MESSAGE,
10
+ generateMockResponseChunks,
11
+ } from './mock_data';
12
+
13
+ const renderMarkdown = (content) => content;
14
+ const renderGFM = () => {};
15
+
16
+ const defaultValue = (prop) =>
17
+ typeof GlDuoChat.props[prop].default === 'function'
18
+ ? GlDuoChat.props[prop].default()
19
+ : GlDuoChat.props[prop].default;
20
+
21
+ const generateProps = ({
22
+ title = defaultValue('title'),
23
+ messages = defaultValue('messages'),
24
+ error = defaultValue('error'),
25
+ isLoading = defaultValue('isLoading'),
26
+ isChatAvailable = defaultValue('isChatAvailable'),
27
+ predefinedPrompts = defaultValue('predefinedPrompts'),
28
+ experimentHelpPageUrl = defaultValue('experimentHelpPageUrl'),
29
+ toolName = defaultValue('toolName'),
30
+ } = {}) => ({
31
+ title,
32
+ messages,
33
+ error,
34
+ isLoading,
35
+ isChatAvailable,
36
+ predefinedPrompts,
37
+ experimentHelpPageUrl,
38
+ toolName,
39
+ });
40
+
41
+ export const Default = (args, { argTypes }) => ({
42
+ components: { GlDuoChat },
43
+ props: Object.keys(argTypes),
44
+ provide: {
45
+ renderMarkdown,
46
+ renderGFM,
47
+ },
48
+ template: `
49
+ <gl-duo-chat
50
+ :title="title"
51
+ :messages="messages"
52
+ :error="error"
53
+ :is-loading="isLoading"
54
+ :is-chat-available="isChatAvailable"
55
+ :predefined-prompts="predefinedPrompts"
56
+ :experiment-help-page-url="experimentHelpPageUrl"
57
+ :tool-name="toolName"
58
+ />`,
59
+ });
60
+ Default.args = generateProps({
61
+ messages: [MOCK_USER_PROMPT_MESSAGE, MOCK_RESPONSE_MESSAGE],
62
+ });
63
+ Default.decorators = [makeContainer({ height: '800px' })];
64
+
65
+ export const Interactive = (args, { argTypes }) => ({
66
+ components: { GlDuoChat, GlButton },
67
+ props: Object.keys(argTypes),
68
+ provide: {
69
+ renderMarkdown,
70
+ renderGFM,
71
+ },
72
+ data() {
73
+ return {
74
+ isHidden: false,
75
+ loggerInfo: '',
76
+ promptInFlight: false,
77
+ msgs: args.messages,
78
+ chunks: [],
79
+ timeout: null,
80
+ requestId: 1,
81
+ };
82
+ },
83
+ methods: {
84
+ onSendChatPrompt(prompt) {
85
+ const newPrompt = {
86
+ ...MOCK_USER_PROMPT_MESSAGE,
87
+ contentHtml: '',
88
+ content: prompt,
89
+ requestId: this.requestId,
90
+ };
91
+ this.loggerInfo += `New prompt: ${JSON.stringify(newPrompt)}\n\n`;
92
+ this.msgs.push(newPrompt);
93
+ this.promptInFlight = true;
94
+ },
95
+ onChatHidden() {
96
+ this.isHidden = true;
97
+ this.loggerInfo += `Chat closed\n\n`;
98
+ },
99
+ showChat() {
100
+ this.isHidden = false;
101
+ this.loggerInfo += `Chat opened\n\n`;
102
+ },
103
+ onResponseRequested() {
104
+ this.timeout = null;
105
+ this.chunks = generateMockResponseChunks(this.requestId);
106
+ this.mockResponseFromAi();
107
+ this.requestId += 1;
108
+ },
109
+ mockResponseFromAi() {
110
+ this.promptInFlight = false;
111
+ if (this.chunks.length) {
112
+ const newResponse = this.chunks.shift();
113
+ const existingMessageIndex = this.msgs.findIndex(
114
+ (msg) => msg.requestId === newResponse.requestId && msg.role === newResponse.role
115
+ );
116
+ const existingMessage = this.msgs[existingMessageIndex];
117
+ if (existingMessage) {
118
+ this.msgs.splice(existingMessageIndex, 1, {
119
+ ...existingMessage,
120
+ content: existingMessage.content + newResponse.content,
121
+ });
122
+ } else {
123
+ this.msgs.push(newResponse);
124
+ }
125
+ this.logerInfo += `New response: ${JSON.stringify(newResponse)}\n\n`;
126
+ this.timeout = setStoryTimeout(() => {
127
+ this.mockResponseFromAi();
128
+ }, Math.floor(Math.random() * 251) + 50);
129
+ }
130
+ },
131
+ },
132
+ template: `
133
+ <div style="height: 800px">
134
+ <div id="logger" class="gl-w-half">
135
+ <pre class="gl-font-sm" style="text-wrap: wrap">
136
+ <code>{{ loggerInfo }}</code>
137
+ </pre>
138
+ <gl-button v-if="promptInFlight" @click="onResponseRequested">Mock the response</gl-button>
139
+ </div>
140
+ <gl-button v-if="isHidden" @click="showChat">Show chat</gl-button>
141
+ <gl-duo-chat
142
+ v-if="!isHidden"
143
+ :title="title"
144
+ :messages="msgs"
145
+ :error="error"
146
+ :is-loading="promptInFlight"
147
+ :is-chat-available="isChatAvailable"
148
+ :predefined-prompts="predefinedPrompts"
149
+ :experiment-help-page-url="experimentHelpPageUrl"
150
+ :tool-name="toolName"
151
+ @send-chat-prompt="onSendChatPrompt"
152
+ @chat-hidden="onChatHidden"
153
+ />
154
+ </div>`,
155
+ });
156
+ Interactive.args = generateProps({});
157
+
158
+ export const Slots = (args, { argTypes }) => ({
159
+ components: { GlDuoChat, GlAlert },
160
+ props: Object.keys(argTypes),
161
+ provide: {
162
+ renderMarkdown,
163
+ renderGFM,
164
+ },
165
+ template: `
166
+ <div>
167
+ <gl-duo-chat
168
+ :title="title"
169
+ :messages="messages"
170
+ :error="error"
171
+ :is-loading="isLoading"
172
+ :is-chat-available="isChatAvailable"
173
+ :predefined-prompts="predefinedPrompts"
174
+ :experiment-help-page-url="experimentHelpPageUrl"
175
+ :tool-name="toolName">
176
+
177
+ <template #hero>
178
+ <pre class="code-block rounded code highlight gl-border-b gl-rounded-0! gl-mb-0 gl-overflow-y-auto solarized-light" style="max-height: 20rem; overflow-y: auto;">
179
+ if (firstUserPromptIndex >= 0 && lastUserPromptIndex > firstUserPromptIndex) {
180
+ messages.splice(firstUserPromptIndex, 1);
181
+ return truncateChatPrompt(messages);
182
+ }
183
+ </pre>
184
+ </template>
185
+ <template #subheader>
186
+ <gl-alert
187
+ :dismissible="false"
188
+ variant="warning"
189
+ class="gl-font-sm gl-border-t"
190
+ role="alert"
191
+ data-testid="chat-legal-warning-gitlab-usage"
192
+ primary-button-link="https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/legal_restrictions/"
193
+ primary-button-text="Read more"
194
+ >
195
+ <p>
196
+ You are not allowed to copy any part of this output into issues, comments, GitLab source code, commit messages, merge requests or any other user interface in the <code>/gitlab-org</code> or <code>/gitlab-com</code> groups.
197
+ </p>
198
+ </gl-alert>
199
+ </template>
200
+ </gl-duo-chat>
201
+ </div>
202
+ `,
203
+ });
204
+ Slots.decorators = [makeContainer({ height: '800px' })];
205
+
206
+ export default {
207
+ title: 'experimental/duo/chat/duo-chat',
208
+ component: GlDuoChat,
209
+ parameters: {
210
+ docs: {
211
+ description: {
212
+ component: readme,
213
+ },
214
+ },
215
+ },
216
+ };