@gitlab/ui 71.11.0 → 72.0.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/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ # [72.0.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v71.11.1...v72.0.0) (2023-12-18)
2
+
3
+
4
+ * Merge branch 'force-release' into 'main' ([7df9bbe](https://gitlab.com/gitlab-org/gitlab-ui/commit/7df9bbececf3e79513be7d022269fc84864cc7a2))
5
+
6
+
7
+ ### Bug Fixes
8
+
9
+ * Force a release ([090865a](https://gitlab.com/gitlab-org/gitlab-ui/commit/090865a92acbefa8b1891331651bfb27e430e0d8))
10
+
11
+
12
+ ### BREAKING CHANGES
13
+
14
+ * This changes the API the GitLab duo chat component expects
15
+
16
+ See merge request https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3868
17
+
18
+ Merged-by: Lukas 'Eipi' Eipert <leipert@gitlab.com>
19
+ Approved-by: Lukas 'Eipi' Eipert <leipert@gitlab.com>
20
+ Co-authored-by: Pavel Shutsin <pshutsin@gitlab.com>
21
+
22
+ ## [71.11.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v71.11.0...v71.11.1) (2023-12-15)
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * **Collapse:** explicitly pass visible prop ([afcae2b](https://gitlab.com/gitlab-org/gitlab-ui/commit/afcae2b257c44003a1776b6e403fe35c97638626))
28
+
1
29
  # [71.11.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v71.10.0...v71.11.0) (2023-12-14)
2
30
 
3
31
 
@@ -10,6 +10,13 @@ var script = {
10
10
  model: {
11
11
  prop: 'visible',
12
12
  event: 'input'
13
+ },
14
+ props: {
15
+ visible: {
16
+ type: Boolean,
17
+ default: false,
18
+ required: false
19
+ }
13
20
  }
14
21
  };
15
22
 
@@ -17,7 +24,7 @@ var script = {
17
24
  const __vue_script__ = script;
18
25
 
19
26
  /* template */
20
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-collapse',_vm._g(_vm._b({},'b-collapse',_vm.$attrs,false),_vm.$listeners),[_vm._t("default")],2)};
27
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('b-collapse',_vm._g(_vm._b({attrs:{"visible":_vm.visible}},'b-collapse',_vm.$attrs,false),_vm.$listeners),[_vm._t("default")],2)};
21
28
  var __vue_staticRenderFns__ = [];
22
29
 
23
30
  /* style */
@@ -5,12 +5,11 @@ import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_s
5
5
  import { CopyCodeElement } from './copy_code_element';
6
6
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
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
11
-
12
- const end = arr.slice(start).findIndex(el => !el);
13
- return end > 0 ? arr.slice(start, end).join('') : arr.slice(start).join('');
8
+ const concatUntilEmpty = arr => {
9
+ if (!arr) return '';
10
+ let end = arr.findIndex(el => !el);
11
+ if (end < 0) end = arr.length;
12
+ return arr.slice(0, end).join('');
14
13
  };
15
14
  var script = {
16
15
  name: 'GlDuoChatMessage',
@@ -34,12 +33,6 @@ var script = {
34
33
  required: true
35
34
  }
36
35
  },
37
- data() {
38
- return {
39
- messageContent: '',
40
- messageWatcher: null
41
- };
42
- },
43
36
  computed: {
44
37
  isAssistantMessage() {
45
38
  return this.message.role.toLowerCase() === MESSAGE_MODEL_ROLES.assistant;
@@ -51,50 +44,29 @@ var script = {
51
44
  var _this$message$extras;
52
45
  return (_this$message$extras = this.message.extras) === null || _this$message$extras === void 0 ? void 0 : _this$message$extras.sources;
53
46
  },
54
- content() {
55
- return this.message.contentHtml || this.renderMarkdown(this.message.content || this.message.errors.join('; '));
47
+ messageContent() {
48
+ if (this.message.errors.length > 0) return this.renderMarkdown(this.message.errors.join('; '));
49
+ if (this.message.contentHtml) {
50
+ return this.message.contentHtml;
51
+ }
52
+ return this.renderMarkdown(this.message.content || concatUntilEmpty(this.message.chunks));
56
53
  }
57
54
  },
58
- created() {
59
- this.messageWatcher = this.$watch('message', this.messageUpdateHandler, {
60
- deep: true
61
- });
62
- },
63
55
  beforeCreate() {
64
- /**
65
- * Keeps cache of previous chunks used for rerendering the AI response when streaming.
66
- * Is intentionally non-reactive
67
- */
68
- this.messageChunks = [];
69
56
  if (!customElements.get('copy-code')) {
70
57
  customElements.define('copy-code', CopyCodeElement);
71
58
  }
72
59
  },
73
60
  mounted() {
74
- this.messageContent = this.content;
75
- if (this.message.chunkId) {
76
- this.messageChunks[this.message.chunkId] = this.message.content;
77
- }
78
- this.hydrateContentWithGFM();
61
+ this.$nextTick(this.hydrateContentWithGFM);
62
+ },
63
+ updated() {
64
+ this.$nextTick(this.hydrateContentWithGFM);
79
65
  },
80
66
  methods: {
81
67
  async hydrateContentWithGFM() {
82
- await this.$nextTick();
83
- this.renderGFM(this.$refs.content);
84
- },
85
- async messageUpdateHandler() {
86
- const {
87
- chunkId,
88
- content
89
- } = this.message;
90
- if (!chunkId) {
91
- this.messageChunks = [];
92
- this.messageContent = this.content;
93
- this.messageWatcher();
94
- this.hydrateContentWithGFM();
95
- } else {
96
- this.messageChunks[chunkId] = content;
97
- this.messageContent = this.renderMarkdown(concatIndicesUntilEmpty(this.messageChunks));
68
+ if (this.message.contentHtml) {
69
+ this.renderGFM(this.$refs.content);
98
70
  }
99
71
  }
100
72
  }
@@ -27,14 +27,6 @@ const MOCK_RESPONSE_MESSAGE = {
27
27
  errors: [],
28
28
  timestamp: '2021-04-21T12:00:00.000Z'
29
29
  };
30
- const MOCK_CHUNK_RESPONSE_MESSAGE = {
31
- chunkId: 1,
32
- content: 'chunk',
33
- role: MESSAGE_MODEL_ROLES.assistant,
34
- requestId: '987',
35
- errors: [],
36
- timestamp: '2021-04-21T12:00:00.000Z'
37
- };
38
30
  const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
39
31
  id: '123',
40
32
  content: `To change your password in GitLab:
@@ -88,4 +80,4 @@ const renderGFM = el => {
88
80
  });
89
81
  };
90
82
 
91
- export { MOCK_CHUNK_RESPONSE_MESSAGE, MOCK_RESPONSE_MESSAGE, MOCK_RESPONSE_MESSAGE_FOR_STREAMING, MOCK_USER_PROMPT_MESSAGE, generateMockResponseChunks, renderGFM, renderMarkdown };
83
+ export { MOCK_RESPONSE_MESSAGE, MOCK_RESPONSE_MESSAGE_FOR_STREAMING, MOCK_USER_PROMPT_MESSAGE, generateMockResponseChunks, renderGFM, renderMarkdown };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Thu, 14 Dec 2023 19:07:24 GMT
3
+ * Generated on Mon, 18 Dec 2023 16:16:22 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Thu, 14 Dec 2023 19:07:24 GMT
3
+ * Generated on Mon, 18 Dec 2023 16:16:22 GMT
4
4
  */
5
5
 
6
6
  :root.gl-dark {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Thu, 14 Dec 2023 19:07:24 GMT
3
+ * Generated on Mon, 18 Dec 2023 16:16:22 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#133a03";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Thu, 14 Dec 2023 19:07:24 GMT
3
+ * Generated on Mon, 18 Dec 2023 16:16:22 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#ddfab7";
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Thu, 14 Dec 2023 19:07:24 GMT
3
+ // Generated on Mon, 18 Dec 2023 16:16:22 GMT
4
4
 
5
5
  $red-950: #fff4f3;
6
6
  $red-900: #fcf1ef;
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Thu, 14 Dec 2023 19:07:24 GMT
3
+ // Generated on Mon, 18 Dec 2023 16:16:22 GMT
4
4
 
5
5
  $gl-line-height-52: 3.25rem;
6
6
  $gl-line-height-44: 2.75rem;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "71.11.0",
3
+ "version": "72.0.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -87,8 +87,8 @@
87
87
  },
88
88
  "devDependencies": {
89
89
  "@arkweid/lefthook": "0.7.7",
90
- "@babel/core": "^7.23.5",
91
- "@babel/preset-env": "^7.23.5",
90
+ "@babel/core": "^7.23.6",
91
+ "@babel/preset-env": "^7.23.6",
92
92
  "@babel/preset-react": "^7.23.3",
93
93
  "@cypress/grep": "^4.0.1",
94
94
  "@gitlab/eslint-plugin": "19.2.0",
@@ -11,11 +11,18 @@ export default {
11
11
  prop: 'visible',
12
12
  event: 'input',
13
13
  },
14
+ props: {
15
+ visible: {
16
+ type: Boolean,
17
+ default: false,
18
+ required: false,
19
+ },
20
+ },
14
21
  };
15
22
  </script>
16
23
 
17
24
  <template>
18
- <b-collapse v-bind="$attrs" v-on="$listeners">
25
+ <b-collapse :visible="visible" v-bind="$attrs" v-on="$listeners">
19
26
  <!-- @slot The content to show/hide. -->
20
27
  <slot></slot>
21
28
  </b-collapse>
@@ -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: