@gitlab/ui 66.25.0 → 66.26.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 +14 -0
- package/dist/components/charts/legend/legend.js +1 -1
- package/dist/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.js +128 -0
- package/dist/components/experimental/duo/chat/mock_data.js +41 -3
- package/dist/index.css +2 -2
- package/dist/index.css.map +1 -1
- package/dist/tokens/css/tokens.css +1 -1
- package/dist/tokens/css/tokens.dark.css +1 -1
- package/dist/tokens/js/tokens.dark.js +1 -1
- package/dist/tokens/js/tokens.js +1 -1
- package/dist/tokens/scss/_tokens.dark.scss +1 -1
- package/dist/tokens/scss/_tokens.scss +1 -1
- package/dist/utils/charts/constants.js +1 -1
- package/package.json +15 -15
- package/src/components/charts/legend/legend.scss +34 -19
- package/src/components/charts/legend/legend.stories.js +41 -4
- package/src/components/charts/legend/legend.vue +1 -1
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.md +60 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.scss +18 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js +381 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.stories.js +45 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue +107 -0
- package/src/components/experimental/duo/chat/mock_data.js +45 -2
- package/src/scss/components.scss +1 -0
- package/src/utils/charts/constants.js +2 -1
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { nextTick } from 'vue';
|
|
2
|
+
import { shallowMount } from '@vue/test-utils';
|
|
3
|
+
import { GlDuoUserFeedback } from '../../../../../../index';
|
|
4
|
+
import {
|
|
5
|
+
MOCK_USER_PROMPT_MESSAGE,
|
|
6
|
+
MOCK_RESPONSE_MESSAGE,
|
|
7
|
+
MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
8
|
+
} from '../../mock_data';
|
|
9
|
+
import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
|
|
10
|
+
import GlDuoChatMessage from './duo_chat_message.vue';
|
|
11
|
+
|
|
12
|
+
describe('DuoChatMessage', () => {
|
|
13
|
+
let wrapper;
|
|
14
|
+
|
|
15
|
+
const findContent = () => wrapper.findComponent({ ref: 'content' });
|
|
16
|
+
const findDocumentSources = () => wrapper.findComponent(DocumentationSources);
|
|
17
|
+
const findUserFeedback = () => wrapper.findComponent(GlDuoUserFeedback);
|
|
18
|
+
const mockMarkdownContent = 'foo **bar**';
|
|
19
|
+
|
|
20
|
+
let renderMarkdown;
|
|
21
|
+
let renderGFM;
|
|
22
|
+
|
|
23
|
+
const componentFactory = ({ message = MOCK_USER_PROMPT_MESSAGE, options = {} } = {}) => {
|
|
24
|
+
return shallowMount(GlDuoChatMessage, {
|
|
25
|
+
...options,
|
|
26
|
+
propsData: {
|
|
27
|
+
message,
|
|
28
|
+
},
|
|
29
|
+
provide: {
|
|
30
|
+
renderMarkdown,
|
|
31
|
+
renderGFM,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const createComponent = (args) => {
|
|
37
|
+
wrapper = componentFactory(args);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
renderMarkdown = jest.fn().mockImplementation((val) => val);
|
|
42
|
+
renderGFM = jest.fn();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
jest.clearAllMocks();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('rendering', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
renderMarkdown.mockImplementation(() => mockMarkdownContent);
|
|
52
|
+
createComponent();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('converts the message `content` to Markdown', () => {
|
|
56
|
+
expect(renderMarkdown).toHaveBeenCalledWith(MOCK_USER_PROMPT_MESSAGE.content);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renders message content', () => {
|
|
60
|
+
expect(wrapper.text()).toBe(mockMarkdownContent);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('user message', () => {
|
|
64
|
+
it('does not render the documentation sources component', () => {
|
|
65
|
+
expect(findDocumentSources().exists()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('does not render the user feedback component', () => {
|
|
69
|
+
expect(findUserFeedback().exists()).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('rendering - with assistant message', () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
createComponent({
|
|
77
|
+
message: MOCK_RESPONSE_MESSAGE,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('renders the documentation sources component by default', () => {
|
|
82
|
+
expect(findDocumentSources().exists()).toBe(true);
|
|
83
|
+
expect(findDocumentSources().props('sources')).toEqual(MOCK_RESPONSE_MESSAGE.extras.sources);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it.each([null, undefined, ''])(
|
|
87
|
+
'does not render sources component when `sources` is %s',
|
|
88
|
+
(sources) => {
|
|
89
|
+
createComponent({
|
|
90
|
+
message: {
|
|
91
|
+
...MOCK_RESPONSE_MESSAGE,
|
|
92
|
+
extras: {
|
|
93
|
+
sources,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
expect(findDocumentSources().exists()).toBe(false);
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
it('renders the user feedback component', () => {
|
|
102
|
+
expect(findUserFeedback().exists()).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('proxies the emitted event from the User Feedback component', () => {
|
|
106
|
+
findUserFeedback().vm.$emit('feedback', 'foo');
|
|
107
|
+
expect(wrapper.emitted('track-feedback')).toEqual([['foo']]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('message output', () => {
|
|
112
|
+
it('hydrates the message with GLFM when mounting the component', () => {
|
|
113
|
+
createComponent();
|
|
114
|
+
expect(renderGFM).toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('outputs errors if message has no content', async () => {
|
|
118
|
+
const errors = ['foo', 'bar', 'baz'];
|
|
119
|
+
|
|
120
|
+
createComponent({
|
|
121
|
+
message: {
|
|
122
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
123
|
+
contentHtml: '',
|
|
124
|
+
content: '',
|
|
125
|
+
errors,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await nextTick();
|
|
130
|
+
|
|
131
|
+
expect(findContent().text()).toContain(errors[0]);
|
|
132
|
+
expect(findContent().text()).toContain(errors[1]);
|
|
133
|
+
expect(findContent().text()).toContain(errors[2]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('message updates watcher', () => {
|
|
137
|
+
const newContent = 'new foo content';
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
createComponent();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('listens to the message changes', async () => {
|
|
143
|
+
expect(findContent().text()).toContain(MOCK_USER_PROMPT_MESSAGE.content);
|
|
144
|
+
// setProps is justified here because we are testing the component's
|
|
145
|
+
// reactive behavior which consistutes an exception
|
|
146
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
147
|
+
wrapper.setProps({
|
|
148
|
+
message: {
|
|
149
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
150
|
+
contentHtml: `<p>${newContent}</p>`,
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
await nextTick();
|
|
154
|
+
expect(findContent().text()).not.toContain(MOCK_USER_PROMPT_MESSAGE.content);
|
|
155
|
+
expect(findContent().text()).toContain(newContent);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('prioritises the output of contentHtml over content', async () => {
|
|
159
|
+
// setProps is justified here because we are testing the component's
|
|
160
|
+
// reactive behavior which consistutes an exception
|
|
161
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
162
|
+
wrapper.setProps({
|
|
163
|
+
message: {
|
|
164
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
165
|
+
contentHtml: `<p>${MOCK_USER_PROMPT_MESSAGE.content}</p>`,
|
|
166
|
+
content: newContent,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
await nextTick();
|
|
170
|
+
expect(findContent().text()).not.toContain(newContent);
|
|
171
|
+
expect(findContent().text()).toContain(MOCK_USER_PROMPT_MESSAGE.content);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('outputs errors if message has no content', async () => {
|
|
175
|
+
// setProps is justified here because we are testing the component's
|
|
176
|
+
// reactive behavior which consistutes an exception
|
|
177
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
178
|
+
wrapper.setProps({
|
|
179
|
+
message: {
|
|
180
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
181
|
+
contentHtml: '',
|
|
182
|
+
content: '',
|
|
183
|
+
errors: ['error'],
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
await nextTick();
|
|
187
|
+
expect(findContent().text()).not.toContain(newContent);
|
|
188
|
+
expect(findContent().text()).not.toContain(MOCK_USER_PROMPT_MESSAGE.content);
|
|
189
|
+
expect(findContent().text()).toContain('error');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('merges all the errors for output', async () => {
|
|
193
|
+
const errors = ['foo', 'bar', 'baz'];
|
|
194
|
+
// setProps is justified here because we are testing the component's
|
|
195
|
+
// reactive behavior which consistutes an exception
|
|
196
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
197
|
+
wrapper.setProps({
|
|
198
|
+
message: {
|
|
199
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
200
|
+
contentHtml: '',
|
|
201
|
+
content: '',
|
|
202
|
+
errors,
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
await nextTick();
|
|
206
|
+
expect(findContent().text()).toContain(errors[0]);
|
|
207
|
+
expect(findContent().text()).toContain(errors[1]);
|
|
208
|
+
expect(findContent().text()).toContain(errors[2]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('hydrates the output message with GLFM if its not a chunk', async () => {
|
|
212
|
+
// setProps is justified here because we are testing the component's
|
|
213
|
+
// reactive behavior which consistutes an exception
|
|
214
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
215
|
+
wrapper.setProps({
|
|
216
|
+
message: {
|
|
217
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
218
|
+
contentHtml: `<p>${newContent}</p>`,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
await nextTick();
|
|
222
|
+
expect(renderGFM).toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('updates to the message', () => {
|
|
228
|
+
const content1 = 'chunk #1';
|
|
229
|
+
const content2 = ' chunk #2';
|
|
230
|
+
const content3 = ' chunk #3';
|
|
231
|
+
const chunk1 = {
|
|
232
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
233
|
+
content: content1,
|
|
234
|
+
chunkId: 1,
|
|
235
|
+
};
|
|
236
|
+
const chunk2 = {
|
|
237
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
238
|
+
content: content2,
|
|
239
|
+
chunkId: 2,
|
|
240
|
+
};
|
|
241
|
+
const chunk3 = {
|
|
242
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
243
|
+
content: content3,
|
|
244
|
+
chunkId: 3,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
beforeEach(() => {
|
|
248
|
+
createComponent();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('does not fail if the message has no chunkId', async () => {
|
|
252
|
+
// setProps is justified here because we are testing the component's
|
|
253
|
+
// reactive behavior which consistutes an exception
|
|
254
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
255
|
+
wrapper.setProps({
|
|
256
|
+
message: {
|
|
257
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
258
|
+
content: content1,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
await nextTick();
|
|
262
|
+
expect(findContent().text()).toBe(content1);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('renders chunks correctly when the chunks arrive out of order', async () => {
|
|
266
|
+
// setProps is justified here because we are testing the component's
|
|
267
|
+
// reactive behavior which consistutes an exception
|
|
268
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
269
|
+
wrapper.setProps({
|
|
270
|
+
message: chunk2,
|
|
271
|
+
});
|
|
272
|
+
await nextTick();
|
|
273
|
+
expect(findContent().text()).toBe('');
|
|
274
|
+
|
|
275
|
+
wrapper.setProps({
|
|
276
|
+
message: chunk1,
|
|
277
|
+
});
|
|
278
|
+
await nextTick();
|
|
279
|
+
expect(findContent().text()).toBe(content1 + content2);
|
|
280
|
+
|
|
281
|
+
wrapper.setProps({
|
|
282
|
+
message: chunk3,
|
|
283
|
+
});
|
|
284
|
+
await nextTick();
|
|
285
|
+
expect(findContent().text()).toBe(content1 + content2 + content3);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('renders the chunks as they arrive', async () => {
|
|
289
|
+
const consolidatedContent = content1 + content2;
|
|
290
|
+
|
|
291
|
+
// setProps is justified here because we are testing the component's
|
|
292
|
+
// reactive behavior which consistutes an exception
|
|
293
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
294
|
+
wrapper.setProps({
|
|
295
|
+
message: chunk1,
|
|
296
|
+
});
|
|
297
|
+
await nextTick();
|
|
298
|
+
expect(findContent().text()).toBe(content1);
|
|
299
|
+
|
|
300
|
+
wrapper.setProps({
|
|
301
|
+
message: chunk2,
|
|
302
|
+
});
|
|
303
|
+
await nextTick();
|
|
304
|
+
expect(findContent().text()).toBe(consolidatedContent);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('treats the initial message content as chunk if message has chunkId', async () => {
|
|
308
|
+
createComponent({
|
|
309
|
+
message: chunk1,
|
|
310
|
+
});
|
|
311
|
+
await nextTick();
|
|
312
|
+
expect(findContent().text()).toBe(content1);
|
|
313
|
+
|
|
314
|
+
// setProps is justified here because we are testing the component's
|
|
315
|
+
// reactive behavior which consistutes an exception
|
|
316
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
317
|
+
wrapper.setProps({
|
|
318
|
+
message: chunk2,
|
|
319
|
+
});
|
|
320
|
+
await nextTick();
|
|
321
|
+
expect(findContent().text()).toBe(content1 + content2);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('does not hydrate the chunk messages with GLFM', async () => {
|
|
325
|
+
createComponent({
|
|
326
|
+
propsData: {
|
|
327
|
+
message: chunk1,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
renderGFM.mockClear();
|
|
331
|
+
expect(renderGFM).not.toHaveBeenCalled();
|
|
332
|
+
|
|
333
|
+
// setProps is justified here because we are testing the component's
|
|
334
|
+
// reactive behavior which consistutes an exception
|
|
335
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
336
|
+
wrapper.setProps({
|
|
337
|
+
message: chunk2,
|
|
338
|
+
});
|
|
339
|
+
await nextTick();
|
|
340
|
+
expect(renderGFM).not.toHaveBeenCalled();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it.each`
|
|
344
|
+
content | contentHtml | errors | expectedContent
|
|
345
|
+
${'alpha'} | ${'beta'} | ${['foo', 'bar']} | ${'beta'}
|
|
346
|
+
${'alpha'} | ${'beta'} | ${[]} | ${'beta'}
|
|
347
|
+
${'alpha'} | ${''} | ${['foo', 'bar']} | ${'alpha'}
|
|
348
|
+
${'alpha'} | ${''} | ${[]} | ${'alpha'}
|
|
349
|
+
${''} | ${'beta'} | ${['foo', 'bar']} | ${'beta'}
|
|
350
|
+
${''} | ${'beta'} | ${[]} | ${'beta'}
|
|
351
|
+
${''} | ${''} | ${['foo', 'bar']} | ${'foo; bar'}
|
|
352
|
+
`(
|
|
353
|
+
'outputs "$expectedContent" and hydrates this content when content is "$content", contentHtml is "$contentHtml" and errors is "$errors" with "chunkId: null"',
|
|
354
|
+
async ({ content, contentHtml, errors, expectedContent } = {}) => {
|
|
355
|
+
createComponent({
|
|
356
|
+
propsData: {
|
|
357
|
+
message: chunk1,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
renderGFM.mockClear();
|
|
361
|
+
expect(renderGFM).not.toHaveBeenCalled();
|
|
362
|
+
|
|
363
|
+
// setProps is justified here because we are testing the component's
|
|
364
|
+
// reactive behavior which consistutes an exception
|
|
365
|
+
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
366
|
+
wrapper.setProps({
|
|
367
|
+
message: {
|
|
368
|
+
...MOCK_CHUNK_RESPONSE_MESSAGE,
|
|
369
|
+
chunkId: null,
|
|
370
|
+
content,
|
|
371
|
+
contentHtml,
|
|
372
|
+
errors,
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
await nextTick();
|
|
376
|
+
expect(renderGFM).toHaveBeenCalled();
|
|
377
|
+
expect(findContent().text()).toBe(expectedContent);
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
});
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.stories.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MOCK_RESPONSE_MESSAGE, MOCK_USER_PROMPT_MESSAGE } from '../../mock_data';
|
|
2
|
+
import GlDuoChatMessage from './duo_chat_message.vue';
|
|
3
|
+
import readme from './duo_chat_message.md';
|
|
4
|
+
|
|
5
|
+
const renderMarkdown = (content) => content;
|
|
6
|
+
const renderGFM = () => {};
|
|
7
|
+
|
|
8
|
+
const generateProps = ({ message = MOCK_RESPONSE_MESSAGE } = {}) => ({
|
|
9
|
+
message,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const Template = (args, { argTypes }) => ({
|
|
13
|
+
components: { GlDuoChatMessage },
|
|
14
|
+
props: Object.keys(argTypes),
|
|
15
|
+
provide: {
|
|
16
|
+
renderMarkdown,
|
|
17
|
+
renderGFM,
|
|
18
|
+
},
|
|
19
|
+
template: `
|
|
20
|
+
<gl-duo-chat-message :message="message" />
|
|
21
|
+
`,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const User = Template.bind({});
|
|
25
|
+
User.args = generateProps({
|
|
26
|
+
message: MOCK_USER_PROMPT_MESSAGE,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const Response = Template.bind({});
|
|
30
|
+
Response.args = generateProps({
|
|
31
|
+
message: MOCK_RESPONSE_MESSAGE,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export default {
|
|
35
|
+
title: 'experimental/duo/chat/duo-chat-message',
|
|
36
|
+
component: GlDuoChatMessage,
|
|
37
|
+
parameters: {
|
|
38
|
+
docs: {
|
|
39
|
+
description: {
|
|
40
|
+
component: readme,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
argTypes: {},
|
|
45
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { GlDuoUserFeedback } from '../../../../../../index';
|
|
3
|
+
import { SafeHtmlDirective as SafeHtml } from '../../../../../../directives/safe_html/safe_html';
|
|
4
|
+
import { MESSAGE_MODEL_ROLES } from '../../constants';
|
|
5
|
+
import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
|
|
6
|
+
|
|
7
|
+
const concatIndicesUntilEmpty = (arr) => {
|
|
8
|
+
const start = arr.findIndex((el) => el);
|
|
9
|
+
if (start === -1 || start !== 1) return ''; // If there are no non-empty elements
|
|
10
|
+
|
|
11
|
+
const end = arr.slice(start).findIndex((el) => !el);
|
|
12
|
+
return end > 0 ? arr.slice(start, end).join('') : arr.slice(start).join('');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default {
|
|
16
|
+
name: 'GlDuoChatMessage',
|
|
17
|
+
components: {
|
|
18
|
+
DocumentationSources,
|
|
19
|
+
GlDuoUserFeedback,
|
|
20
|
+
},
|
|
21
|
+
directives: {
|
|
22
|
+
SafeHtml,
|
|
23
|
+
},
|
|
24
|
+
inject: ['renderGFM', 'renderMarkdown'],
|
|
25
|
+
props: {
|
|
26
|
+
/**
|
|
27
|
+
* A message object
|
|
28
|
+
*/
|
|
29
|
+
message: {
|
|
30
|
+
type: Object,
|
|
31
|
+
required: true,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
data() {
|
|
35
|
+
return {
|
|
36
|
+
messageContent: '',
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
computed: {
|
|
40
|
+
isAssistantMessage() {
|
|
41
|
+
return this.message.role.toLowerCase() === MESSAGE_MODEL_ROLES.assistant;
|
|
42
|
+
},
|
|
43
|
+
isUserMessage() {
|
|
44
|
+
return this.message.role.toLowerCase() === MESSAGE_MODEL_ROLES.user;
|
|
45
|
+
},
|
|
46
|
+
sources() {
|
|
47
|
+
return this.message.extras?.sources;
|
|
48
|
+
},
|
|
49
|
+
content() {
|
|
50
|
+
return (
|
|
51
|
+
this.message.contentHtml ||
|
|
52
|
+
this.renderMarkdown(this.message.content || this.message.errors.join('; '))
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
watch: {
|
|
57
|
+
message: {
|
|
58
|
+
handler() {
|
|
59
|
+
const { chunkId, content } = this.message;
|
|
60
|
+
if (!chunkId) {
|
|
61
|
+
this.messageChunks = [];
|
|
62
|
+
this.messageContent = this.content;
|
|
63
|
+
this.renderGFM(this.$refs.content);
|
|
64
|
+
} else {
|
|
65
|
+
this.messageChunks[chunkId] = content;
|
|
66
|
+
this.messageContent = this.renderMarkdown(concatIndicesUntilEmpty(this.messageChunks));
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
deep: true,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
beforeCreate() {
|
|
73
|
+
/**
|
|
74
|
+
* Keeps cache of previous chunks used for rerendering the AI response when streaming.
|
|
75
|
+
* Is intentionally non-reactive
|
|
76
|
+
*/
|
|
77
|
+
this.messageChunks = [];
|
|
78
|
+
},
|
|
79
|
+
mounted() {
|
|
80
|
+
this.messageContent = this.content;
|
|
81
|
+
if (this.message.chunkId) {
|
|
82
|
+
this.messageChunks[this.message.chunkId] = this.message.content;
|
|
83
|
+
}
|
|
84
|
+
this.renderGFM(this.$refs.content);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
</script>
|
|
88
|
+
<template>
|
|
89
|
+
<div
|
|
90
|
+
class="gl-p-4 gl-mb-4 gl-rounded-lg gl-line-height-20 gl-word-break-word duo-chat-message"
|
|
91
|
+
:class="{
|
|
92
|
+
'gl-ml-auto gl-bg-blue-100 gl-text-blue-900 gl-rounded-bottom-right-none': isUserMessage,
|
|
93
|
+
'gl-rounded-bottom-left-none gl-text-gray-900 gl-bg-white gl-border-1 gl-border-solid gl-border-gray-50':
|
|
94
|
+
isAssistantMessage,
|
|
95
|
+
}"
|
|
96
|
+
>
|
|
97
|
+
<div ref="content" v-safe-html="messageContent"></div>
|
|
98
|
+
|
|
99
|
+
<template v-if="isAssistantMessage">
|
|
100
|
+
<documentation-sources v-if="sources" :sources="sources" />
|
|
101
|
+
|
|
102
|
+
<div class="gl-display-flex gl-align-items-flex-end gl-mt-4">
|
|
103
|
+
<gl-duo-user-feedback @feedback="$emit('track-feedback', $event)" />
|
|
104
|
+
</div>
|
|
105
|
+
</template>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
@@ -22,8 +22,10 @@ const MOCK_SOURCES = [
|
|
|
22
22
|
|
|
23
23
|
export const MOCK_RESPONSE_MESSAGE = {
|
|
24
24
|
id: '123',
|
|
25
|
-
content:
|
|
26
|
-
|
|
25
|
+
content:
|
|
26
|
+
'To create a new template:\n\n1. Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/` or `.gitlab/merge_request_templates/` directory in your repository.\n2. Commit the template file to your default branch.\n\nTo check if this has worked correctly, create a new issue or merge request and see if you can find your template in the **Choose a template** dropdown list.',
|
|
27
|
+
contentHtml:
|
|
28
|
+
'\u003cp data-sourcepos="1:1-1:25" dir="auto"\u003eTo create a new template:\u003c/p\u003e\n\u003col data-sourcepos="3:1-5:0" dir="auto"\u003e\n\u003cli data-sourcepos="3:1-3:143"\u003eCreate a new Markdown (\u003ccode\u003e.md\u003c/code\u003e) file inside the \u003ccode\u003e.gitlab/issue_templates/\u003c/code\u003e or \u003ccode\u003e.gitlab/merge_request_templates/\u003c/code\u003e directory in your repository.\u003c/li\u003e\n\u003cli data-sourcepos="4:1-5:0"\u003eCommit the template file to your default branch.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp data-sourcepos="6:1-6:156" dir="auto"\u003eTo check if this has worked correctly, create a new issue or merge request and see if you can find your template in the \u003cstrong\u003eChoose a template\u003c/strong\u003e dropdown list.\u003c/p\u003e',
|
|
27
29
|
role: MESSAGE_MODEL_ROLES.assistant,
|
|
28
30
|
extras: {
|
|
29
31
|
sources: MOCK_SOURCES,
|
|
@@ -32,3 +34,44 @@ export const MOCK_RESPONSE_MESSAGE = {
|
|
|
32
34
|
errors: [],
|
|
33
35
|
timestamp: '2021-04-21T12:00:00.000Z',
|
|
34
36
|
};
|
|
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
|
+
export const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
|
|
48
|
+
id: '123',
|
|
49
|
+
content: `To change your password in GitLab:
|
|
50
|
+
|
|
51
|
+
Log in to your GitLab account.
|
|
52
|
+
Select your avatar in the top right corner and choose Edit profile.
|
|
53
|
+
On the left sidebar, select Password.
|
|
54
|
+
Enter your current password in the Current password field.
|
|
55
|
+
Enter your new password in the New password and Password confirmation fields.
|
|
56
|
+
Select Save password.
|
|
57
|
+
If you don't know your current password, select the I forgot my password link to reset it.
|
|
58
|
+
|
|
59
|
+
GitLab enforces password requirements when you choose a new password.`,
|
|
60
|
+
contentHtml: '',
|
|
61
|
+
role: 'assistant',
|
|
62
|
+
extras: {},
|
|
63
|
+
requestId: '987',
|
|
64
|
+
errors: [],
|
|
65
|
+
timestamp: '2021-04-21T12:00:00.000Z',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const MOCK_USER_PROMPT_MESSAGE = {
|
|
69
|
+
id: '456',
|
|
70
|
+
content: 'How to create a new template?',
|
|
71
|
+
contentHtml: '',
|
|
72
|
+
role: MESSAGE_MODEL_ROLES.user,
|
|
73
|
+
requestId: '987',
|
|
74
|
+
errors: [],
|
|
75
|
+
timestamp: '2021-04-21T12:00:00.000Z',
|
|
76
|
+
extras: null,
|
|
77
|
+
};
|
package/src/scss/components.scss
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// @import '../components/base/dropdown/dropdown'
|
|
4
4
|
//
|
|
5
5
|
// ADD COMPONENT IMPORTS - needed for yarn generate:component. Do not remove
|
|
6
|
+
@import '../components/experimental/duo/chat/components/duo_chat_message/duo_chat_message';
|
|
6
7
|
@import '../components/experimental/duo/chat/components/duo_chat_loader/duo_chat_loader';
|
|
7
8
|
@import '../components/base/new_dropdowns/disclosure/disclosure_dropdown';
|
|
8
9
|
@import '../components/base/keyset_pagination/keyset_pagination';
|
|
@@ -65,5 +65,6 @@ export const CHART_TYPE_BAR = 'bar';
|
|
|
65
65
|
export const CHART_TYPE_LINE = 'line';
|
|
66
66
|
|
|
67
67
|
// Constants for height "auto"
|
|
68
|
-
export const HEIGHT_AUTO_CLASSES =
|
|
68
|
+
export const HEIGHT_AUTO_CLASSES =
|
|
69
|
+
'gl-chart-h-auto gl-display-flex gl-flex-direction-column gl-h-full';
|
|
69
70
|
export const HEIGHT_AUTO_HORIZONTAL_LAYOUT_CLASSES = 'gl-display-flex gl-h-full';
|