@gitlab/ui 74.3.0 → 74.4.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.
Files changed (25) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.js +17 -1
  3. package/dist/components/experimental/duo/chat/duo_chat.js +4 -5
  4. package/dist/components/experimental/duo/chat/markdown_renderer.js +18 -0
  5. package/dist/components/experimental/duo/chat/mock_data.js +19 -9
  6. package/dist/index.css +1 -1
  7. package/dist/index.css.map +1 -1
  8. package/dist/tokens/css/tokens.css +1 -1
  9. package/dist/tokens/css/tokens.dark.css +1 -1
  10. package/dist/tokens/js/tokens.dark.js +1 -1
  11. package/dist/tokens/js/tokens.js +1 -1
  12. package/dist/tokens/scss/_tokens.dark.scss +1 -1
  13. package/dist/tokens/scss/_tokens.scss +1 -1
  14. package/package.json +3 -1
  15. package/src/components/base/card/card.stories.js +27 -18
  16. package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.md +5 -1
  17. package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js +172 -8
  18. package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue +18 -1
  19. package/src/components/experimental/duo/chat/duo_chat.scss +28 -1
  20. package/src/components/experimental/duo/chat/duo_chat.spec.js +5 -3
  21. package/src/components/experimental/duo/chat/duo_chat.stories.js +2 -5
  22. package/src/components/experimental/duo/chat/duo_chat.vue +3 -4
  23. package/src/components/experimental/duo/chat/markdown_renderer.js +20 -0
  24. package/src/components/experimental/duo/chat/markdown_renderer.spec.js +55 -0
  25. package/src/components/experimental/duo/chat/mock_data.js +19 -9
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Thu, 08 Feb 2024 04:02:28 GMT
3
+ * Generated on Fri, 09 Feb 2024 13:52:54 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Thu, 08 Feb 2024 04:02:28 GMT
3
+ * Generated on Fri, 09 Feb 2024 13:52:54 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, 08 Feb 2024 04:02:28 GMT
3
+ * Generated on Fri, 09 Feb 2024 13:52:54 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, 08 Feb 2024 04:02:28 GMT
3
+ * Generated on Fri, 09 Feb 2024 13:52:54 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, 08 Feb 2024 04:02:28 GMT
3
+ // Generated on Fri, 09 Feb 2024 13:52:54 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, 08 Feb 2024 04:02:28 GMT
3
+ // Generated on Fri, 09 Feb 2024 13:52:54 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": "74.3.0",
3
+ "version": "74.4.0",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -72,6 +72,8 @@
72
72
  "echarts": "^5.3.2",
73
73
  "iframe-resizer": "^4.3.2",
74
74
  "lodash": "^4.17.20",
75
+ "marked": "^12.0.0",
76
+ "marked-bidi": "^1.0.8",
75
77
  "portal-vue": "^2.1.6",
76
78
  "vue-runtime-helpers": "^1.1.2"
77
79
  },
@@ -1,30 +1,31 @@
1
1
  import readme from './card.md';
2
2
  import GlCard from './card.vue';
3
3
 
4
- const components = {
5
- GlCard,
6
- };
7
-
8
- const template = `
9
- <gl-card>
10
- <template #header>
11
- <h3 class="gl-my-0 gl-font-weight-bold gl-font-lg">This is a custom header</h3>
12
- </template>
13
- <template #default>
14
- Hello World
15
- </template>
16
- <template #footer>
17
- <span>This is a custom footer</span>
18
- </template>
19
- </gl-card>`;
4
+ const generateProps = ({ headerClass, bodyClass, footerClass } = {}) => ({
5
+ headerClass,
6
+ bodyClass,
7
+ footerClass,
8
+ });
20
9
 
21
10
  const Template = (args, { argTypes }) => ({
11
+ components: { GlCard },
22
12
  props: Object.keys(argTypes),
23
- components,
24
- template,
13
+ template: `
14
+ <gl-card :header-class="headerClass" :body-class="bodyClass" :footer-class="footerClass">
15
+ <template #header>
16
+ <h3 class="gl-my-0 gl-font-weight-bold gl-font-lg">This is a custom header</h3>
17
+ </template>
18
+ <template #default>
19
+ Hello World
20
+ </template>
21
+ <template #footer>
22
+ <span>This is a custom footer</span>
23
+ </template>
24
+ </gl-card>`,
25
25
  });
26
26
 
27
27
  export const Default = Template.bind({});
28
+ Default.args = generateProps();
28
29
 
29
30
  export default {
30
31
  title: 'base/card',
@@ -36,4 +37,12 @@ export default {
36
37
  },
37
38
  },
38
39
  },
40
+ argTypes: {
41
+ headerClass: { control: 'text' },
42
+ bodyClass: { control: 'text' },
43
+ footerClass: { control: 'text' },
44
+ header: { control: { disable: true } },
45
+ default: { control: { disable: true } },
46
+ footer: { control: { disable: true } },
47
+ },
39
48
  };
@@ -11,7 +11,11 @@ The component represents a Duo Chat message.
11
11
  There are two ways of pretty-rendering a message's content in the component:
12
12
 
13
13
  - dependency injection, providing functions to convert raw markdown into HTML,
14
- - sending `contentHtml` prop as part of the `message` property,
14
+ - sending `contentHtml` prop as part of the `message` property
15
+
16
+ The component ships a default markdown renderer based on `marked`. It should produce
17
+ reasonably well-looking results while streaming messages. The implementation can be found
18
+ [here](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/src/components/experimental/duo/chat/markdown_renderer.js).
15
19
 
16
20
  ### Injecting functions
17
21
 
@@ -19,14 +19,14 @@ describe('DuoChatMessage', () => {
19
19
 
20
20
  const componentFactory = ({ message = MOCK_USER_PROMPT_MESSAGE, options = {} } = {}) => {
21
21
  return shallowMount(GlDuoChatMessage, {
22
- ...options,
23
- propsData: {
24
- message,
25
- },
26
22
  provide: {
27
23
  renderMarkdown,
28
24
  renderGFM,
29
25
  },
26
+ ...options,
27
+ propsData: {
28
+ message,
29
+ },
30
30
  });
31
31
  };
32
32
 
@@ -115,7 +115,7 @@ describe('DuoChatMessage', () => {
115
115
 
116
116
  describe('message output', () => {
117
117
  it('outputs errors if they are present', async () => {
118
- const errors = ['foo', 'bar', 'baz'];
118
+ const errors = ['error1', 'error2', 'error3'];
119
119
 
120
120
  createComponent({
121
121
  message: {
@@ -129,9 +129,10 @@ describe('DuoChatMessage', () => {
129
129
 
130
130
  await nextTick();
131
131
 
132
- expect(findContent().text()).toContain(errors[0]);
133
- expect(findContent().text()).toContain(errors[1]);
134
- expect(findContent().text()).toContain(errors[2]);
132
+ const contentText = findContent().text();
133
+ expect(contentText).toContain(errors[0]);
134
+ expect(contentText).toContain(errors[1]);
135
+ expect(contentText).toContain(errors[2]);
135
136
  });
136
137
 
137
138
  it('outputs contentHtml if it is present', async () => {
@@ -228,4 +229,167 @@ describe('DuoChatMessage', () => {
228
229
  expect(renderGFM).toHaveBeenCalled();
229
230
  });
230
231
  });
232
+
233
+ describe('default renderers', () => {
234
+ it('outputs errors if they are present', async () => {
235
+ const errors = ['error1', 'error2', 'error3'];
236
+
237
+ createComponent({
238
+ options: {
239
+ provide: null,
240
+ },
241
+ message: {
242
+ ...MOCK_USER_PROMPT_MESSAGE,
243
+ errors,
244
+ contentHtml: 'fooHtml barHtml',
245
+ content: 'foo bar',
246
+ chunks: ['a', 'b', 'c'],
247
+ },
248
+ });
249
+
250
+ await nextTick();
251
+
252
+ const contentText = findContent().text();
253
+ expect(contentText).toContain(errors[0]);
254
+ expect(contentText).toContain(errors[1]);
255
+ expect(contentText).toContain(errors[2]);
256
+ });
257
+
258
+ it('outputs contentHtml if it is present', async () => {
259
+ createComponent({
260
+ options: {
261
+ provide: null,
262
+ },
263
+ message: {
264
+ ...MOCK_USER_PROMPT_MESSAGE,
265
+ errors: [],
266
+ contentHtml: 'fooHtml barHtml',
267
+ content: 'foo bar',
268
+ chunks: ['a', 'b', 'c'],
269
+ },
270
+ });
271
+
272
+ await nextTick();
273
+
274
+ expect(findContent().html()).toBe(
275
+ '<div class="gl-markdown gl-compact-markdown">fooHtml barHtml</div>'
276
+ );
277
+ });
278
+
279
+ it('outputs markdown content if there is no contentHtml', async () => {
280
+ createComponent({
281
+ options: {
282
+ provide: null,
283
+ },
284
+ message: {
285
+ ...MOCK_USER_PROMPT_MESSAGE,
286
+ errors: [],
287
+ contentHtml: '',
288
+ content: 'foo bar',
289
+ chunks: ['a', 'b', 'c'],
290
+ },
291
+ });
292
+
293
+ await nextTick();
294
+
295
+ expect(findContent().html()).toBe('<div>\n <p>foo bar</p>\n</div>');
296
+ });
297
+
298
+ it('outputs chunks if there is no content', async () => {
299
+ createComponent({
300
+ options: {
301
+ provide: null,
302
+ },
303
+ message: {
304
+ ...MOCK_USER_PROMPT_MESSAGE,
305
+ errors: [],
306
+ contentHtml: '',
307
+ content: '',
308
+ chunks: ['a', 'b', 'c'],
309
+ },
310
+ });
311
+
312
+ await nextTick();
313
+
314
+ expect(findContent().html()).toBe('<div>\n <p>abc</p>\n</div>');
315
+ });
316
+
317
+ it('sanitizes html produced by errors', async () => {
318
+ createComponent({
319
+ options: {
320
+ provide: null,
321
+ },
322
+ message: {
323
+ ...MOCK_USER_PROMPT_MESSAGE,
324
+ errors: ['[click here](javascript:prompt(1))'],
325
+ contentHtml: '',
326
+ content: '',
327
+ chunks: [],
328
+ },
329
+ });
330
+
331
+ await nextTick();
332
+
333
+ expect(findContent().html()).toBe('<div>\n <p><a>click here</a></p>\n</div>');
334
+ });
335
+
336
+ it('sanitizes html produced by content', async () => {
337
+ createComponent({
338
+ options: {
339
+ provide: null,
340
+ },
341
+ message: {
342
+ ...MOCK_USER_PROMPT_MESSAGE,
343
+ errors: [],
344
+ contentHtml: '',
345
+ content: '[click here](javascript:prompt(1))',
346
+ chunks: [],
347
+ },
348
+ });
349
+
350
+ await nextTick();
351
+
352
+ expect(findContent().html()).toBe('<div>\n <p><a>click here</a></p>\n</div>');
353
+ });
354
+
355
+ it('sanitizes html produced by chunks', async () => {
356
+ createComponent({
357
+ options: {
358
+ provide: null,
359
+ },
360
+ message: {
361
+ ...MOCK_USER_PROMPT_MESSAGE,
362
+ errors: [],
363
+ contentHtml: '',
364
+ content: '',
365
+ chunks: ['[click here]', '(javascript:prompt(1))'],
366
+ },
367
+ });
368
+
369
+ await nextTick();
370
+
371
+ expect(findContent().html()).toBe('<div>\n <p><a>click here</a></p>\n</div>');
372
+ });
373
+
374
+ it('sanitizes contentHtml', async () => {
375
+ createComponent({
376
+ options: {
377
+ provide: null,
378
+ },
379
+ message: {
380
+ ...MOCK_USER_PROMPT_MESSAGE,
381
+ errors: [],
382
+ contentHtml: `<a href="javascript:prompt(1)">click here</a>`,
383
+ content: '',
384
+ chunks: [],
385
+ },
386
+ });
387
+
388
+ await nextTick();
389
+
390
+ expect(findContent().html()).toBe(
391
+ '<div class="gl-markdown gl-compact-markdown"><a>click here</a></div>'
392
+ );
393
+ });
394
+ });
231
395
  });
@@ -3,6 +3,8 @@ import GlDuoUserFeedback from '../../../user_feedback/user_feedback.vue';
3
3
  import { SafeHtmlDirective as SafeHtml } from '../../../../../../directives/safe_html/safe_html';
4
4
  import { MESSAGE_MODEL_ROLES } from '../../constants';
5
5
  import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
6
+ // eslint-disable-next-line no-restricted-imports
7
+ import { renderDuoChatMarkdownPreview } from '../../markdown_renderer';
6
8
  import { CopyCodeElement } from './copy_code_element';
7
9
 
8
10
  const concatUntilEmpty = (arr) => {
@@ -27,7 +29,22 @@ export default {
27
29
  directives: {
28
30
  SafeHtml,
29
31
  },
30
- inject: ['renderGFM', 'renderMarkdown'],
32
+ inject: {
33
+ // Note, we likely might move away from Provide/Inject for this
34
+ // and only ship the versions that are currently in the default
35
+ // See https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3953#note_1762834219
36
+ // for more context.
37
+ renderGFM: {
38
+ from: 'renderGFM',
39
+ default: () => (element) => {
40
+ element.classList.add('gl-markdown', 'gl-compact-markdown');
41
+ },
42
+ },
43
+ renderMarkdown: {
44
+ from: 'renderMarkdown',
45
+ default: () => renderDuoChatMarkdownPreview,
46
+ },
47
+ },
31
48
  props: {
32
49
  /**
33
50
  * A message object
@@ -33,6 +33,33 @@
33
33
  }
34
34
  }
35
35
 
36
+ .duo-chat-history {
37
+ scroll-behavior: smooth;
38
+
39
+ /*
40
+ Browsers a are pretty good at keeping the focus on an element while
41
+ the parent element grows in size. With this we mark all child elements
42
+ of the chat history as "non" anchors.
43
+ https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-anchor
44
+ */
45
+ * {
46
+ overflow-anchor: none;
47
+ }
48
+
49
+ /*
50
+ Right at the bottom of the chat history we add a scroll-anchor element.
51
+ This scroll-anchor element is the only "possible" anchor. The beauty of it:
52
+ It only will be used as an anchor _if_ it is currently inside the view port.
53
+ So if the user manually scrolls up while a chunked message is coming in,
54
+ it won't stick to the bottom while the message still loads.
55
+ */
56
+ .scroll-anchor {
57
+ overflow-anchor: auto;
58
+ height: 1px;
59
+ margin-top: -1px; // In order to not add 1px vertically, we add a negative margin
60
+ }
61
+ }
62
+
36
63
  .duo-chat-input {
37
64
  @include gl-display-flex;
38
65
  @include gl-flex-direction-column;
@@ -61,7 +88,7 @@
61
88
  .slash-commands {
62
89
  @include gl-mt-n2;
63
90
 
64
- .active-command{
91
+ .active-command {
65
92
  @include gl-bg-gray-50;
66
93
  @include gl-rounded-base;
67
94
  }
@@ -127,13 +127,15 @@ describe('GlDuoChat', () => {
127
127
 
128
128
  describe('when messages exist', () => {
129
129
  it('scrolls to the bottom on load', async () => {
130
+ const scrollIntoViewMock = jest.fn();
131
+ window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
132
+
130
133
  createComponent({ propsData: { messages } });
131
- const { element } = findChatComponent();
132
- jest.spyOn(element, 'scrollHeight', 'get').mockReturnValue(200);
133
134
 
134
135
  await nextTick();
135
136
 
136
- expect(element.scrollTop).toEqual(200);
137
+ expect(scrollIntoViewMock).toHaveBeenCalledTimes(1);
138
+ window.HTMLElement.prototype.scrollIntoView = undefined;
137
139
  });
138
140
  });
139
141
 
@@ -9,7 +9,6 @@ import {
9
9
  MOCK_USER_PROMPT_MESSAGE,
10
10
  generateMockResponseChunks,
11
11
  renderGFM,
12
- renderMarkdown,
13
12
  } from './mock_data';
14
13
 
15
14
  const slashCommands = [
@@ -72,7 +71,6 @@ export const Default = (args, { argTypes }) => ({
72
71
  components: { GlDuoChat },
73
72
  props: Object.keys(argTypes),
74
73
  provide: {
75
- renderMarkdown,
76
74
  renderGFM,
77
75
  },
78
76
  template: `
@@ -101,7 +99,6 @@ export const Interactive = (args, { argTypes }) => ({
101
99
  components: { GlDuoChat, GlButton },
102
100
  props: Object.keys(argTypes),
103
101
  provide: {
104
- renderMarkdown,
105
102
  renderGFM,
106
103
  },
107
104
  data() {
@@ -160,7 +157,7 @@ export const Interactive = (args, { argTypes }) => ({
160
157
  this.logerInfo += `New response: ${JSON.stringify(newResponse)}\n\n`;
161
158
  this.timeout = setStoryTimeout(() => {
162
159
  this.mockResponseFromAi();
163
- }, Math.floor(Math.random() * 251) + 50);
160
+ }, Math.floor(Math.random() * 251) + 16);
164
161
  }
165
162
  },
166
163
  },
@@ -199,7 +196,7 @@ export const Slots = (args, { argTypes }) => ({
199
196
  components: { GlDuoChat, GlAlert },
200
197
  props: Object.keys(argTypes),
201
198
  provide: {
202
- renderMarkdown,
199
+ renderMarkdown: (md) => `THIS IS ALTERED MARKDOWN: ${md}`,
203
200
  renderGFM,
204
201
  },
205
202
  template: `
@@ -292,9 +292,7 @@ export default {
292
292
  async scrollToBottom() {
293
293
  await this.$nextTick();
294
294
 
295
- if (this.$refs.drawer) {
296
- this.$refs.drawer.scrollTop = this.$refs.drawer.scrollHeight;
297
- }
295
+ this.$refs.anchor?.scrollIntoView?.();
298
296
  },
299
297
  onTrackFeedback(event) {
300
298
  /**
@@ -422,7 +420,7 @@ export default {
422
420
  ></gl-alert>
423
421
 
424
422
  <section
425
- class="gl-display-flex gl-flex-direction-column gl-justify-content-end gl-flex-grow-1 gl-border-b-0 gl-bg-gray-10"
423
+ class="duo-chat-history gl-display-flex gl-flex-direction-column gl-justify-content-end gl-flex-grow-1 gl-border-b-0 gl-bg-gray-10"
426
424
  >
427
425
  <transition-group
428
426
  tag="div"
@@ -462,6 +460,7 @@ export default {
462
460
  <transition name="loader">
463
461
  <gl-duo-chat-loader v-if="isLoading" :tool-name="toolName" class="gl-px-0!" />
464
462
  </transition>
463
+ <div ref="anchor" class="scroll-anchor"></div>
465
464
  </section>
466
465
  </div>
467
466
  <footer
@@ -0,0 +1,20 @@
1
+ // eslint-disable-next-line no-restricted-imports
2
+ import { Marked } from 'marked';
3
+ import markedBidi from 'marked-bidi';
4
+
5
+ const duoMarked = new Marked([
6
+ {
7
+ async: false,
8
+ breaks: false,
9
+ gfm: false,
10
+ },
11
+ markedBidi(),
12
+ ]);
13
+
14
+ export function renderDuoChatMarkdownPreview(md) {
15
+ try {
16
+ return md ? duoMarked.parse(md.toString()) : '';
17
+ } catch {
18
+ return md;
19
+ }
20
+ }
@@ -0,0 +1,55 @@
1
+ import { Parser } from 'marked';
2
+ import { renderDuoChatMarkdownPreview } from './markdown_renderer';
3
+
4
+ describe('Duo Chat Markdown renderer', () => {
5
+ afterEach(() => {
6
+ jest.restoreAllMocks();
7
+ });
8
+
9
+ it('renders a few edge cases', () => {
10
+ expect(renderDuoChatMarkdownPreview('')).toEqual('');
11
+ expect(renderDuoChatMarkdownPreview(null)).toEqual('');
12
+ expect(renderDuoChatMarkdownPreview(undefined)).toEqual('');
13
+ expect(renderDuoChatMarkdownPreview(5)).toEqual('<p>5</p>\n');
14
+ });
15
+
16
+ it('renders a simple paragraph', () => {
17
+ expect(renderDuoChatMarkdownPreview('Hello world')).toEqual('<p>Hello world</p>\n');
18
+ });
19
+
20
+ it('auto-closes an open code block', () => {
21
+ expect(renderDuoChatMarkdownPreview('```yaml\n# comment')).toEqual(
22
+ '<pre><code class="language-yaml"># comment\n</code></pre>\n'
23
+ );
24
+ });
25
+
26
+ it('renders standard markdown syntax', () => {
27
+ expect(renderDuoChatMarkdownPreview('*italic*')).toEqual(`<p><em>italic</em></p>\n`);
28
+ expect(renderDuoChatMarkdownPreview('_italic_')).toEqual(`<p><em>italic</em></p>\n`);
29
+ expect(renderDuoChatMarkdownPreview('**bold**')).toEqual(`<p><strong>bold</strong></p>\n`);
30
+ expect(renderDuoChatMarkdownPreview('~~strike~~')).toEqual(`<p><del>strike</del></p>\n`);
31
+ expect(renderDuoChatMarkdownPreview('https://example.org')).toEqual(
32
+ `<p><a href="https://example.org">https://example.org</a></p>\n`
33
+ );
34
+ expect(renderDuoChatMarkdownPreview('[example](https://example.org)')).toEqual(
35
+ `<p><a href="https://example.org">example</a></p>\n`
36
+ );
37
+ expect(renderDuoChatMarkdownPreview('1. first\n2. second')).toEqual(
38
+ `<ol>\n<li>first</li>\n<li>second</li>\n</ol>\n`
39
+ );
40
+ expect(renderDuoChatMarkdownPreview('- first\n- second')).toEqual(
41
+ `<ul>\n<li>first</li>\n<li>second</li>\n</ul>\n`
42
+ );
43
+ expect(renderDuoChatMarkdownPreview('* first\n* second')).toEqual(
44
+ `<ul>\n<li>first</li>\n<li>second</li>\n</ul>\n`
45
+ );
46
+ });
47
+
48
+ it('returns content as-is if marked throws', () => {
49
+ jest.spyOn(Parser.prototype, 'parse').mockImplementationOnce(() => {
50
+ throw new Error('I am a broken parser');
51
+ });
52
+
53
+ expect(renderDuoChatMarkdownPreview('Hello world')).toEqual('Hello world');
54
+ });
55
+ });
@@ -39,15 +39,25 @@ export const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
39
39
  id: '123',
40
40
  content: `To change your password in GitLab:
41
41
 
42
- Log in to your GitLab account.
43
- Select your avatar in the top right corner and choose Edit profile.
44
- On the left sidebar, select Password.
45
- Enter your current password in the Current password field.
46
- Enter your new password in the New password and Password confirmation fields.
47
- Select Save password.
48
- If you don't know your current password, select the I forgot my password link to reset it.
49
-
50
- GitLab enforces password requirements when you choose a new password.`,
42
+ 1. Log in to your GitLab account.
43
+ 2. Select your avatar in the top right corner and choose Edit profile.
44
+ 3. On the left sidebar, select Password.
45
+ 4. Enter your current password in the Current password field.
46
+ 5. Enter your new password in the New password and Password confirmation fields.
47
+ 6. Select Save password.
48
+ 7. If you don't know your current password, select the I forgot my password link to reset it.
49
+
50
+ GitLab enforces password requirements when you choose a new password.
51
+
52
+ ~~~yaml
53
+ # And here is a
54
+ # code block
55
+ everyone:
56
+ likes:
57
+ yaml: true
58
+ ~~~
59
+ which is rendered while streaming.
60
+ `,
51
61
  contentHtml: '',
52
62
  role: 'assistant',
53
63
  extras: {},