@gitlab/ui 71.11.1 → 72.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "71.11.1",
3
+ "version": "72.1.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -1,11 +1,7 @@
1
1
  import { nextTick } from 'vue';
2
2
  import { shallowMount } from '@vue/test-utils';
3
3
  import GlDuoUserFeedback from '../../../user_feedback/user_feedback.vue';
4
- import {
5
- MOCK_USER_PROMPT_MESSAGE,
6
- MOCK_RESPONSE_MESSAGE,
7
- MOCK_CHUNK_RESPONSE_MESSAGE,
8
- } from '../../mock_data';
4
+ import { MOCK_USER_PROMPT_MESSAGE, MOCK_RESPONSE_MESSAGE } from '../../mock_data';
9
5
  import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
10
6
  import GlDuoChatMessage from './duo_chat_message.vue';
11
7
 
@@ -39,7 +35,7 @@ describe('DuoChatMessage', () => {
39
35
  };
40
36
 
41
37
  beforeEach(() => {
42
- renderMarkdown = jest.fn().mockImplementation((val) => val);
38
+ renderMarkdown = jest.fn().mockImplementation((val) => `markdown: ${val}`);
43
39
  renderGFM = jest.fn();
44
40
  });
45
41
 
@@ -53,7 +49,7 @@ describe('DuoChatMessage', () => {
53
49
  expect(customElements.get('copy-code')).toBeDefined();
54
50
  });
55
51
 
56
- describe('rendering', () => {
52
+ describe('rendering with user message', () => {
57
53
  beforeEach(() => {
58
54
  renderMarkdown.mockImplementation(() => mockMarkdownContent);
59
55
  createComponent();
@@ -67,18 +63,16 @@ describe('DuoChatMessage', () => {
67
63
  expect(wrapper.text()).toBe(mockMarkdownContent);
68
64
  });
69
65
 
70
- describe('user message', () => {
71
- it('does not render the documentation sources component', () => {
72
- expect(findDocumentSources().exists()).toBe(false);
73
- });
66
+ it('does not render the documentation sources component', () => {
67
+ expect(findDocumentSources().exists()).toBe(false);
68
+ });
74
69
 
75
- it('does not render the user feedback component', () => {
76
- expect(findUserFeedback().exists()).toBe(false);
77
- });
70
+ it('does not render the user feedback component', () => {
71
+ expect(findUserFeedback().exists()).toBe(false);
78
72
  });
79
73
  });
80
74
 
81
- describe('rendering - with assistant message', () => {
75
+ describe('rendering with assistant message', () => {
82
76
  beforeEach(() => {
83
77
  createComponent({
84
78
  message: MOCK_RESPONSE_MESSAGE,
@@ -120,21 +114,16 @@ describe('DuoChatMessage', () => {
120
114
  });
121
115
 
122
116
  describe('message output', () => {
123
- it('hydrates the message with GLFM when mounting the component', async () => {
124
- createComponent();
125
- await nextTick();
126
- expect(renderGFM).toHaveBeenCalled();
127
- });
128
-
129
- it('outputs errors if message has no content', async () => {
117
+ it('outputs errors if they are present', async () => {
130
118
  const errors = ['foo', 'bar', 'baz'];
131
119
 
132
120
  createComponent({
133
121
  message: {
134
122
  ...MOCK_USER_PROMPT_MESSAGE,
135
- contentHtml: '',
136
- content: '',
137
123
  errors,
124
+ contentHtml: 'fooHtml barHtml',
125
+ content: 'foo bar',
126
+ chunks: ['a', 'b', 'c'],
138
127
  },
139
128
  });
140
129
 
@@ -145,277 +134,98 @@ describe('DuoChatMessage', () => {
145
134
  expect(findContent().text()).toContain(errors[2]);
146
135
  });
147
136
 
148
- describe('message updates watcher', () => {
149
- const newContent = 'new foo content';
150
- beforeEach(() => {
151
- createComponent();
152
- });
153
-
154
- it('listens to the message changes', async () => {
155
- expect(findContent().text()).toContain(MOCK_USER_PROMPT_MESSAGE.content);
156
- // setProps is justified here because we are testing the component's
157
- // reactive behavior which consistutes an exception
158
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
159
- wrapper.setProps({
160
- message: {
161
- ...MOCK_USER_PROMPT_MESSAGE,
162
- contentHtml: `<p>${newContent}</p>`,
163
- },
164
- });
165
- await nextTick();
166
- expect(findContent().text()).not.toContain(MOCK_USER_PROMPT_MESSAGE.content);
167
- expect(findContent().text()).toContain(newContent);
168
- });
169
-
170
- it('prioritises the output of contentHtml over content', async () => {
171
- // setProps is justified here because we are testing the component's
172
- // reactive behavior which consistutes an exception
173
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
174
- wrapper.setProps({
175
- message: {
176
- ...MOCK_USER_PROMPT_MESSAGE,
177
- contentHtml: `<p>${MOCK_USER_PROMPT_MESSAGE.content}</p>`,
178
- content: newContent,
179
- },
180
- });
181
- await nextTick();
182
- expect(findContent().text()).not.toContain(newContent);
183
- expect(findContent().text()).toContain(MOCK_USER_PROMPT_MESSAGE.content);
184
- });
185
-
186
- it('outputs errors if message has no content', async () => {
187
- // setProps is justified here because we are testing the component's
188
- // reactive behavior which consistutes an exception
189
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
190
- wrapper.setProps({
191
- message: {
192
- ...MOCK_USER_PROMPT_MESSAGE,
193
- contentHtml: '',
194
- content: '',
195
- errors: ['error'],
196
- },
197
- });
198
- await nextTick();
199
- expect(findContent().text()).not.toContain(newContent);
200
- expect(findContent().text()).not.toContain(MOCK_USER_PROMPT_MESSAGE.content);
201
- expect(findContent().text()).toContain('error');
202
- });
203
-
204
- it('merges all the errors for output', async () => {
205
- const errors = ['foo', 'bar', 'baz'];
206
- // setProps is justified here because we are testing the component's
207
- // reactive behavior which consistutes an exception
208
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
209
- wrapper.setProps({
210
- message: {
211
- ...MOCK_USER_PROMPT_MESSAGE,
212
- contentHtml: '',
213
- content: '',
214
- errors,
215
- },
216
- });
217
- await nextTick();
218
- expect(findContent().text()).toContain(errors[0]);
219
- expect(findContent().text()).toContain(errors[1]);
220
- expect(findContent().text()).toContain(errors[2]);
221
- });
222
-
223
- it('hydrates the output message with GLFM if its not a chunk', async () => {
224
- // setProps is justified here because we are testing the component's
225
- // reactive behavior which consistutes an exception
226
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
227
- wrapper.setProps({
228
- message: {
229
- ...MOCK_USER_PROMPT_MESSAGE,
230
- contentHtml: `<p>${newContent}</p>`,
231
- },
232
- });
233
- await nextTick();
234
- expect(renderGFM).toHaveBeenCalled();
137
+ it('outputs contentHtml if it is present', async () => {
138
+ createComponent({
139
+ message: {
140
+ ...MOCK_USER_PROMPT_MESSAGE,
141
+ errors: [],
142
+ contentHtml: 'fooHtml barHtml',
143
+ content: 'foo bar',
144
+ chunks: ['a', 'b', 'c'],
145
+ },
235
146
  });
236
- });
237
- });
238
147
 
239
- describe('updates to the message', () => {
240
- const content1 = 'chunk #1';
241
- const content2 = ' chunk #2';
242
- const content3 = ' chunk #3';
243
- const chunk1 = {
244
- ...MOCK_CHUNK_RESPONSE_MESSAGE,
245
- content: content1,
246
- chunkId: 1,
247
- };
248
- const chunk2 = {
249
- ...MOCK_CHUNK_RESPONSE_MESSAGE,
250
- content: content2,
251
- chunkId: 2,
252
- };
253
- const chunk3 = {
254
- ...MOCK_CHUNK_RESPONSE_MESSAGE,
255
- content: content3,
256
- chunkId: 3,
257
- };
148
+ await nextTick();
258
149
 
259
- beforeEach(() => {
260
- createComponent();
150
+ expect(findContent().text()).toContain('fooHtml barHtml');
261
151
  });
262
152
 
263
- it('does not fail if the message has no chunkId', async () => {
264
- // setProps is justified here because we are testing the component's
265
- // reactive behavior which consistutes an exception
266
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
267
- wrapper.setProps({
153
+ it('outputs markdown content if there is no contentHtml', async () => {
154
+ createComponent({
268
155
  message: {
269
- ...MOCK_CHUNK_RESPONSE_MESSAGE,
270
- content: content1,
156
+ ...MOCK_USER_PROMPT_MESSAGE,
157
+ errors: [],
158
+ contentHtml: '',
159
+ content: 'foo bar',
160
+ chunks: ['a', 'b', 'c'],
271
161
  },
272
162
  });
273
- await nextTick();
274
- expect(findContent().text()).toBe(content1);
275
- });
276
163
 
277
- it('renders chunks correctly when the chunks arrive out of order', async () => {
278
- // setProps is justified here because we are testing the component's
279
- // reactive behavior which consistutes an exception
280
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
281
- wrapper.setProps({
282
- message: chunk2,
283
- });
284
164
  await nextTick();
285
- expect(findContent().text()).toBe('');
286
165
 
287
- wrapper.setProps({
288
- message: chunk1,
289
- });
290
- await nextTick();
291
- expect(findContent().text()).toBe(content1 + content2);
292
-
293
- wrapper.setProps({
294
- message: chunk3,
295
- });
296
- await nextTick();
297
- expect(findContent().text()).toBe(content1 + content2 + content3);
166
+ expect(findContent().text()).toContain('markdown: foo bar');
298
167
  });
299
168
 
300
- it('renders the chunks as they arrive', async () => {
301
- const consolidatedContent = content1 + content2;
302
-
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
- message: chunk1,
169
+ it('outputs chunks if there is no content', async () => {
170
+ createComponent({
171
+ message: {
172
+ ...MOCK_USER_PROMPT_MESSAGE,
173
+ errors: [],
174
+ contentHtml: '',
175
+ content: '',
176
+ chunks: ['a', 'b', 'c'],
177
+ },
308
178
  });
309
- await nextTick();
310
- expect(findContent().text()).toBe(content1);
311
179
 
312
- wrapper.setProps({
313
- message: chunk2,
314
- });
315
180
  await nextTick();
316
- expect(findContent().text()).toBe(consolidatedContent);
181
+
182
+ expect(findContent().text()).toContain('markdown: abc');
317
183
  });
318
184
 
319
- it('treats the initial message content as chunk if message has chunkId', async () => {
185
+ it('outputs chunks until first undefined', async () => {
320
186
  createComponent({
321
- message: chunk1,
187
+ message: {
188
+ ...MOCK_USER_PROMPT_MESSAGE,
189
+ errors: [],
190
+ contentHtml: '',
191
+ content: '',
192
+ chunks: ['a', undefined, 'c'],
193
+ },
322
194
  });
323
- await nextTick();
324
- expect(findContent().text()).toBe(content1);
325
195
 
326
- // setProps is justified here because we are testing the component's
327
- // reactive behavior which consistutes an exception
328
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
329
- wrapper.setProps({
330
- message: chunk2,
331
- });
332
196
  await nextTick();
333
- expect(findContent().text()).toBe(content1 + content2);
197
+
198
+ expect(findContent().text()).toContain('markdown: a');
334
199
  });
335
200
 
336
- it('does not hydrate the chunk messages with GLFM', async () => {
201
+ it('hydrates the message with GFM when mounting with contentHtml', async () => {
337
202
  createComponent({
338
- propsData: {
339
- message: chunk1,
203
+ message: {
204
+ ...MOCK_USER_PROMPT_MESSAGE,
205
+ contentHtml: 'foo bar',
340
206
  },
341
207
  });
342
208
  await nextTick();
343
- renderGFM.mockClear();
344
- expect(renderGFM).not.toHaveBeenCalled();
345
-
346
- // setProps is justified here because we are testing the component's
347
- // reactive behavior which consistutes an exception
348
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
349
- wrapper.setProps({
350
- message: chunk2,
351
- });
352
- await nextTick();
353
- expect(renderGFM).not.toHaveBeenCalled();
209
+ expect(renderGFM).toHaveBeenCalled();
354
210
  });
355
211
 
356
- it('does not re-render when chunk is received after final message', async () => {
357
- const finalMessageContent = content1 + content2;
358
-
359
- // setProps is justified here because we are testing the component's
360
- // reactive behavior which consistutes an exception
361
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
362
- await wrapper.setProps({
363
- message: chunk1,
212
+ it('hydrates the message with GFM when updating with contentHtml', async () => {
213
+ createComponent({
214
+ message: {
215
+ ...MOCK_USER_PROMPT_MESSAGE,
216
+ contentHtml: '',
217
+ },
364
218
  });
365
- expect(findContent().text()).toBe(content1);
366
219
 
367
- await wrapper.setProps({
220
+ wrapper.setProps({
368
221
  message: {
369
- ...MOCK_RESPONSE_MESSAGE,
370
- content: finalMessageContent,
371
- contentHtml: finalMessageContent,
372
- chunkId: null,
222
+ ...MOCK_USER_PROMPT_MESSAGE,
223
+ contentHtml: 'foo bar',
373
224
  },
374
225
  });
375
- expect(findContent().text()).toBe(finalMessageContent);
376
226
 
377
- await wrapper.setProps({
378
- message: chunk2,
379
- });
380
- expect(findContent().text()).toBe(finalMessageContent);
227
+ await nextTick();
228
+ expect(renderGFM).toHaveBeenCalled();
381
229
  });
382
-
383
- it.each`
384
- content | contentHtml | errors | expectedContent
385
- ${'alpha'} | ${'beta'} | ${['foo', 'bar']} | ${'beta'}
386
- ${'alpha'} | ${'beta'} | ${[]} | ${'beta'}
387
- ${'alpha'} | ${''} | ${['foo', 'bar']} | ${'alpha'}
388
- ${'alpha'} | ${''} | ${[]} | ${'alpha'}
389
- ${''} | ${'beta'} | ${['foo', 'bar']} | ${'beta'}
390
- ${''} | ${'beta'} | ${[]} | ${'beta'}
391
- ${''} | ${''} | ${['foo', 'bar']} | ${'foo; bar'}
392
- `(
393
- 'outputs "$expectedContent" and hydrates this content when content is "$content", contentHtml is "$contentHtml" and errors is "$errors" with "chunkId: null"',
394
- async ({ content, contentHtml, errors, expectedContent } = {}) => {
395
- createComponent({
396
- propsData: {
397
- message: chunk1,
398
- },
399
- });
400
- renderGFM.mockClear();
401
- expect(renderGFM).not.toHaveBeenCalled();
402
-
403
- // setProps is justified here because we are testing the component's
404
- // reactive behavior which consistutes an exception
405
- // See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
406
- wrapper.setProps({
407
- message: {
408
- ...MOCK_CHUNK_RESPONSE_MESSAGE,
409
- chunkId: null,
410
- content,
411
- contentHtml,
412
- errors,
413
- },
414
- });
415
- await nextTick();
416
- expect(renderGFM).toHaveBeenCalled();
417
- expect(findContent().text()).toBe(expectedContent);
418
- }
419
- );
420
230
  });
421
231
  });
@@ -5,12 +5,14 @@ import { MESSAGE_MODEL_ROLES } from '../../constants';
5
5
  import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
6
6
  import { CopyCodeElement } from './copy_code_element';
7
7
 
8
- const concatIndicesUntilEmpty = (arr) => {
9
- const start = arr.findIndex((el) => el);
10
- if (start === -1 || start !== 1) return ''; // If there are no non-empty elements
8
+ const concatUntilEmpty = (arr) => {
9
+ if (!arr) return '';
11
10
 
12
- const end = arr.slice(start).findIndex((el) => !el);
13
- return end > 0 ? arr.slice(start, end).join('') : arr.slice(start).join('');
11
+ let end = arr.findIndex((el) => !el);
12
+
13
+ if (end < 0) end = arr.length;
14
+
15
+ return arr.slice(0, end).join('');
14
16
  };
15
17
 
16
18
  export default {
@@ -35,12 +37,6 @@ export default {
35
37
  required: true,
36
38
  },
37
39
  },
38
- data() {
39
- return {
40
- messageContent: '',
41
- messageWatcher: null,
42
- };
43
- },
44
40
  computed: {
45
41
  isAssistantMessage() {
46
42
  return this.message.role.toLowerCase() === MESSAGE_MODEL_ROLES.assistant;
@@ -51,49 +47,32 @@ export default {
51
47
  sources() {
52
48
  return this.message.extras?.sources;
53
49
  },
54
- content() {
55
- return (
56
- this.message.contentHtml ||
57
- this.renderMarkdown(this.message.content || this.message.errors.join('; '))
58
- );
50
+ messageContent() {
51
+ if (this.message.errors.length > 0)
52
+ return this.renderMarkdown(this.message.errors.join('; '));
53
+
54
+ if (this.message.contentHtml) {
55
+ return this.message.contentHtml;
56
+ }
57
+
58
+ return this.renderMarkdown(this.message.content || concatUntilEmpty(this.message.chunks));
59
59
  },
60
60
  },
61
- created() {
62
- this.messageWatcher = this.$watch('message', this.messageUpdateHandler, { deep: true });
63
- },
64
61
  beforeCreate() {
65
- /**
66
- * Keeps cache of previous chunks used for rerendering the AI response when streaming.
67
- * Is intentionally non-reactive
68
- */
69
- this.messageChunks = [];
70
62
  if (!customElements.get('copy-code')) {
71
63
  customElements.define('copy-code', CopyCodeElement);
72
64
  }
73
65
  },
74
66
  mounted() {
75
- this.messageContent = this.content;
76
- if (this.message.chunkId) {
77
- this.messageChunks[this.message.chunkId] = this.message.content;
78
- }
79
- this.hydrateContentWithGFM();
67
+ this.$nextTick(this.hydrateContentWithGFM);
68
+ },
69
+ updated() {
70
+ this.$nextTick(this.hydrateContentWithGFM);
80
71
  },
81
72
  methods: {
82
73
  async hydrateContentWithGFM() {
83
- await this.$nextTick();
84
- this.renderGFM(this.$refs.content);
85
- },
86
- async messageUpdateHandler() {
87
- const { chunkId, content } = this.message;
88
- if (!chunkId) {
89
- this.messageChunks = [];
90
- this.messageContent = this.content;
91
-
92
- this.messageWatcher();
93
- this.hydrateContentWithGFM();
94
- } else {
95
- this.messageChunks[chunkId] = content;
96
- this.messageContent = this.renderMarkdown(concatIndicesUntilEmpty(this.messageChunks));
74
+ if (this.message.contentHtml) {
75
+ this.renderGFM(this.$refs.content);
97
76
  }
98
77
  },
99
78
  },
@@ -35,15 +35,6 @@ export const MOCK_RESPONSE_MESSAGE = {
35
35
  timestamp: '2021-04-21T12:00:00.000Z',
36
36
  };
37
37
 
38
- export const MOCK_CHUNK_RESPONSE_MESSAGE = {
39
- chunkId: 1,
40
- content: 'chunk',
41
- role: MESSAGE_MODEL_ROLES.assistant,
42
- requestId: '987',
43
- errors: [],
44
- timestamp: '2021-04-21T12:00:00.000Z',
45
- };
46
-
47
38
  export const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
48
39
  id: '123',
49
40
  content: `To change your password in GitLab:
@@ -3938,6 +3938,14 @@ $gl-animate-skeleton-loader-max-width: 64 * $grid-size;
3938
3938
  }
3939
3939
  }
3940
3940
 
3941
+ .gl-align-self-baseline {
3942
+ align-self: baseline;
3943
+ }
3944
+
3945
+ .gl-align-self-baseline\! {
3946
+ align-self: baseline !important;
3947
+ }
3948
+
3941
3949
  .gl-order-n1 {
3942
3950
  order: -1;
3943
3951
  }
@@ -5047,6 +5055,14 @@ $gl-animate-skeleton-loader-max-width: 64 * $grid-size;
5047
5055
  height: $gl-spacing-scale-9 !important;
5048
5056
  }
5049
5057
 
5058
+ .gl-h-10 {
5059
+ height: $gl-spacing-scale-10;
5060
+ }
5061
+
5062
+ .gl-h-10\! {
5063
+ height: $gl-spacing-scale-10 !important;
5064
+ }
5065
+
5050
5066
  .gl-h-11 {
5051
5067
  height: $gl-spacing-scale-11;
5052
5068
  }
@@ -321,6 +321,10 @@
321
321
  }
322
322
  }
323
323
 
324
+ @mixin gl-align-self-baseline {
325
+ align-self: baseline;
326
+ }
327
+
324
328
  /**
325
329
  * Order utilities
326
330
  *
@@ -208,6 +208,10 @@
208
208
  height: $gl-spacing-scale-9;
209
209
  }
210
210
 
211
+ @mixin gl-h-10 {
212
+ height: $gl-spacing-scale-10;
213
+ }
214
+
211
215
  @mixin gl-h-11 {
212
216
  height: $gl-spacing-scale-11;
213
217
  }