@gitlab/ui 78.9.0 → 78.10.1
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 +15 -0
- package/dist/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.js +51 -14
- package/dist/components/experimental/duo/chat/components/duo_chat_message/utils.js +8 -0
- package/dist/components/experimental/duo/chat/duo_chat.js +2 -2
- package/dist/components/experimental/duo/chat/mock_data.js +18 -4
- package/dist/index.css +1 -1
- 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/package.json +2 -2
- package/src/components/base/label/label.scss +1 -1
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js +258 -138
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue +46 -17
- package/src/components/experimental/duo/chat/components/duo_chat_message/utils.js +9 -0
- package/src/components/experimental/duo/chat/components/duo_chat_message/utils.spec.js +24 -0
- package/src/components/experimental/duo/chat/duo_chat.spec.js +1 -0
- package/src/components/experimental/duo/chat/duo_chat.stories.js +14 -31
- package/src/components/experimental/duo/chat/duo_chat.vue +4 -1
- package/src/components/experimental/duo/chat/mock_data.js +18 -3
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue
CHANGED
|
@@ -8,6 +8,7 @@ import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_s
|
|
|
8
8
|
// eslint-disable-next-line no-restricted-imports
|
|
9
9
|
import { renderDuoChatMarkdownPreview } from '../../markdown_renderer';
|
|
10
10
|
import { CopyCodeElement } from './copy_code_element';
|
|
11
|
+
import { concatUntilEmpty } from './utils';
|
|
11
12
|
|
|
12
13
|
export const i18n = {
|
|
13
14
|
MODAL: {
|
|
@@ -21,16 +22,6 @@ export const i18n = {
|
|
|
21
22
|
},
|
|
22
23
|
};
|
|
23
24
|
|
|
24
|
-
const concatUntilEmpty = (arr) => {
|
|
25
|
-
if (!arr) return '';
|
|
26
|
-
|
|
27
|
-
let end = arr.findIndex((el) => !el);
|
|
28
|
-
|
|
29
|
-
if (end < 0) end = arr.length;
|
|
30
|
-
|
|
31
|
-
return arr.slice(0, end).join('');
|
|
32
|
-
};
|
|
33
|
-
|
|
34
25
|
export default {
|
|
35
26
|
name: 'GlDuoChatMessage',
|
|
36
27
|
safeHtmlConfigExtension: {
|
|
@@ -80,9 +71,14 @@ export default {
|
|
|
80
71
|
return {
|
|
81
72
|
didWhat: '',
|
|
82
73
|
improveWhat: '',
|
|
74
|
+
messageWatcher: null, // imperatively set up watcher on message
|
|
75
|
+
messageChunks: [],
|
|
83
76
|
};
|
|
84
77
|
},
|
|
85
78
|
computed: {
|
|
79
|
+
isChunk() {
|
|
80
|
+
return typeof this.message.chunkId === 'number';
|
|
81
|
+
},
|
|
86
82
|
isAssistantMessage() {
|
|
87
83
|
return this.message.role.toLowerCase() === MESSAGE_MODEL_ROLES.assistant;
|
|
88
84
|
},
|
|
@@ -95,7 +91,7 @@ export default {
|
|
|
95
91
|
hasFeedback() {
|
|
96
92
|
return this.message.extras?.hasFeedback;
|
|
97
93
|
},
|
|
98
|
-
|
|
94
|
+
defaultContent() {
|
|
99
95
|
if (this.message.errors.length > 0)
|
|
100
96
|
return this.renderMarkdown(this.message.errors.join('; '));
|
|
101
97
|
|
|
@@ -103,7 +99,13 @@ export default {
|
|
|
103
99
|
return this.message.contentHtml;
|
|
104
100
|
}
|
|
105
101
|
|
|
106
|
-
return this.renderMarkdown(this.message.content
|
|
102
|
+
return this.renderMarkdown(this.message.content);
|
|
103
|
+
},
|
|
104
|
+
messageContent() {
|
|
105
|
+
if (this.isAssistantMessage && this.isChunk) {
|
|
106
|
+
return this.renderMarkdown(concatUntilEmpty(this.messageChunks));
|
|
107
|
+
}
|
|
108
|
+
return this.defaultContent || this.renderMarkdown(concatUntilEmpty(this.message.chunks));
|
|
107
109
|
},
|
|
108
110
|
},
|
|
109
111
|
beforeCreate() {
|
|
@@ -112,15 +114,36 @@ export default {
|
|
|
112
114
|
}
|
|
113
115
|
},
|
|
114
116
|
mounted() {
|
|
115
|
-
|
|
117
|
+
if (this.isAssistantMessage) {
|
|
118
|
+
// The watcher has to be created imperatively here
|
|
119
|
+
// to give an opportunity to remove it after
|
|
120
|
+
// the complete message has arrived
|
|
121
|
+
this.messageWatcher = this.$watch('message', this.manageMessageUpdate);
|
|
122
|
+
}
|
|
123
|
+
this.setChunks();
|
|
124
|
+
this.hydrateContentWithGFM();
|
|
116
125
|
},
|
|
117
126
|
updated() {
|
|
118
|
-
this
|
|
127
|
+
this.hydrateContentWithGFM();
|
|
119
128
|
},
|
|
120
129
|
methods: {
|
|
121
|
-
|
|
122
|
-
if (this.
|
|
123
|
-
this.
|
|
130
|
+
setChunks() {
|
|
131
|
+
if (this.isChunk) {
|
|
132
|
+
const { chunkId, content } = this.message;
|
|
133
|
+
this.$set(this.messageChunks, chunkId - 1, content);
|
|
134
|
+
} else {
|
|
135
|
+
this.messageChunks = [];
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
stopWatchingMessage() {
|
|
139
|
+
if (this.messageWatcher) {
|
|
140
|
+
this.messageWatcher(); // Stop watching the message prop
|
|
141
|
+
this.messageWatcher = null; // Ensure the watcher can't be stopped multiple times
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
hydrateContentWithGFM() {
|
|
145
|
+
if (!this.isChunk) {
|
|
146
|
+
this.$nextTick(this.renderGFM(this.$refs.content));
|
|
124
147
|
}
|
|
125
148
|
},
|
|
126
149
|
logEvent(e) {
|
|
@@ -131,6 +154,12 @@ export default {
|
|
|
131
154
|
message: this.message,
|
|
132
155
|
});
|
|
133
156
|
},
|
|
157
|
+
manageMessageUpdate() {
|
|
158
|
+
this.setChunks();
|
|
159
|
+
if (!this.isChunk) {
|
|
160
|
+
this.stopWatchingMessage();
|
|
161
|
+
}
|
|
162
|
+
},
|
|
134
163
|
},
|
|
135
164
|
i18n,
|
|
136
165
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { concatUntilEmpty } from './utils';
|
|
2
|
+
|
|
3
|
+
describe('concatUntilEmpty utility', () => {
|
|
4
|
+
it('returns empty string if input array is falsy', () => {
|
|
5
|
+
expect(concatUntilEmpty()).toEqual('');
|
|
6
|
+
expect(concatUntilEmpty(null)).toEqual('');
|
|
7
|
+
expect(concatUntilEmpty(undefined)).toEqual('');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('concatenates array elements until first falsy element', () => {
|
|
11
|
+
const arr = ['a', 'b', undefined, 'c'];
|
|
12
|
+
expect(concatUntilEmpty(arr)).toEqual('ab');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('concatenates all array elements if none are falsy', () => {
|
|
16
|
+
const arr = ['a', 'b', 'c'];
|
|
17
|
+
expect(concatUntilEmpty(arr)).toEqual('abc');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns empty string if first element is falsy', () => {
|
|
21
|
+
const arr = [undefined, 'b', 'c'];
|
|
22
|
+
expect(concatUntilEmpty(arr)).toEqual('');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -357,6 +357,7 @@ describe('GlDuoChat', () => {
|
|
|
357
357
|
{ ...MOCK_RESPONSE_MESSAGE, content: undefined, chunks: [''] },
|
|
358
358
|
],
|
|
359
359
|
],
|
|
360
|
+
[[{ ...MOCK_RESPONSE_MESSAGE, chunkId: 1 }]],
|
|
360
361
|
])('prevents submission when streaming (messages = "%o")', (msgs = []) => {
|
|
361
362
|
createComponent({
|
|
362
363
|
propsData: { isChatAvailable: true, messages: msgs },
|
|
@@ -125,45 +125,28 @@ export const Interactive = (args, { argTypes }) => ({
|
|
|
125
125
|
async mockResponseFromAi() {
|
|
126
126
|
const generator = generateMockResponseChunks(this.requestId);
|
|
127
127
|
|
|
128
|
-
for await (const
|
|
129
|
-
const { chunkId, content, ...messageAttributes } = result;
|
|
128
|
+
for await (const newResponse of generator) {
|
|
130
129
|
const existingMessageIndex = this.msgs.findIndex(
|
|
131
|
-
(msg) => msg.requestId ===
|
|
130
|
+
(msg) => msg.requestId === newResponse.requestId && msg.role === newResponse.role
|
|
132
131
|
);
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
this.
|
|
132
|
+
const existingMessage = this.msgs[existingMessageIndex];
|
|
133
|
+
if (existingMessage) {
|
|
134
|
+
this.updateExistingMessage(newResponse, existingMessageIndex);
|
|
136
135
|
} else {
|
|
137
|
-
this.
|
|
136
|
+
this.addNewMessage(newResponse);
|
|
138
137
|
}
|
|
139
138
|
}
|
|
140
139
|
},
|
|
141
|
-
addNewMessage(
|
|
140
|
+
addNewMessage(msg) {
|
|
142
141
|
this.promptInFlight = false;
|
|
143
|
-
this.$set(this.msgs, this.msgs.length,
|
|
144
|
-
...messageAttributes,
|
|
145
|
-
chunks: [content],
|
|
146
|
-
});
|
|
142
|
+
this.$set(this.msgs, this.msgs.length, msg);
|
|
147
143
|
},
|
|
148
|
-
updateExistingMessage(
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.$set(message, 'chunks', []);
|
|
155
|
-
} else {
|
|
156
|
-
this.$set(message.chunks, chunkId, content);
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
// Update for final message
|
|
160
|
-
this.$set(message, 'content', content);
|
|
161
|
-
|
|
162
|
-
// Remove chunks if they are not needed anymore
|
|
163
|
-
if (message.chunks) {
|
|
164
|
-
this.$delete(message, 'chunks');
|
|
165
|
-
}
|
|
166
|
-
}
|
|
144
|
+
updateExistingMessage(newResponse, existingMessageIndex) {
|
|
145
|
+
const existingMessage = this.msgs[existingMessageIndex];
|
|
146
|
+
this.$set(this.msgs, existingMessageIndex, {
|
|
147
|
+
...existingMessage,
|
|
148
|
+
...newResponse,
|
|
149
|
+
});
|
|
167
150
|
},
|
|
168
151
|
},
|
|
169
152
|
template: `
|
|
@@ -225,7 +225,10 @@ export default {
|
|
|
225
225
|
return this.isLoading || this.isStreaming;
|
|
226
226
|
},
|
|
227
227
|
isStreaming() {
|
|
228
|
-
return Boolean(
|
|
228
|
+
return Boolean(
|
|
229
|
+
(this.lastMessage?.chunks?.length > 0 && !this.lastMessage?.content) ||
|
|
230
|
+
typeof this.lastMessage?.chunkId === 'number'
|
|
231
|
+
);
|
|
229
232
|
},
|
|
230
233
|
filteredSlashCommands() {
|
|
231
234
|
const caseInsensitivePrompt = this.prompt.toLowerCase();
|
|
@@ -41,6 +41,21 @@ export const MOCK_RESPONSE_MESSAGE = {
|
|
|
41
41
|
timestamp: '2021-04-21T12:00:00.000Z',
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
export const generateSeparateChunks = (n) => {
|
|
45
|
+
const res = [];
|
|
46
|
+
for (let i = 1; i <= n; i += 1) {
|
|
47
|
+
res.push({
|
|
48
|
+
chunkId: i,
|
|
49
|
+
content: `chunk #${i}`,
|
|
50
|
+
role: MESSAGE_MODEL_ROLES.assistant,
|
|
51
|
+
requestId: '987',
|
|
52
|
+
errors: [],
|
|
53
|
+
timestamp: '2021-04-21T12:00:00.000Z',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return res;
|
|
57
|
+
};
|
|
58
|
+
|
|
44
59
|
export const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
|
|
45
60
|
id: '123',
|
|
46
61
|
content: `To change your password in GitLab:
|
|
@@ -64,7 +79,6 @@ export const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
|
|
|
64
79
|
~~~
|
|
65
80
|
which is rendered while streaming.
|
|
66
81
|
`,
|
|
67
|
-
contentHtml: '',
|
|
68
82
|
role: 'assistant',
|
|
69
83
|
extras: {},
|
|
70
84
|
requestId: '987',
|
|
@@ -91,7 +105,7 @@ export async function* generateMockResponseChunks(requestId = 1) {
|
|
|
91
105
|
...MOCK_RESPONSE_MESSAGE_FOR_STREAMING,
|
|
92
106
|
requestId,
|
|
93
107
|
content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.substring(start, end),
|
|
94
|
-
chunkId,
|
|
108
|
+
chunkId: chunkId + 1,
|
|
95
109
|
};
|
|
96
110
|
|
|
97
111
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -108,8 +122,9 @@ export async function* generateMockResponseChunks(requestId = 1) {
|
|
|
108
122
|
|
|
109
123
|
export const MOCK_USER_PROMPT_MESSAGE = {
|
|
110
124
|
id: '456',
|
|
125
|
+
chunkId: null,
|
|
111
126
|
content: 'How to create a new template?',
|
|
112
|
-
contentHtml: '',
|
|
127
|
+
contentHtml: '<p>How to create a new template?</p>',
|
|
113
128
|
role: MESSAGE_MODEL_ROLES.user,
|
|
114
129
|
requestId: '987',
|
|
115
130
|
errors: [],
|