@gitlab/ui 78.1.0 → 78.1.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 +7 -0
- package/dist/components/experimental/duo/chat/duo_chat.js +28 -9
- package/dist/components/experimental/duo/chat/mock_data.js +41 -14
- 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 +1 -1
- package/src/components/experimental/duo/chat/duo_chat.spec.js +35 -7
- package/src/components/experimental/duo/chat/duo_chat.stories.js +39 -20
- package/src/components/experimental/duo/chat/duo_chat.vue +26 -11
- package/src/components/experimental/duo/chat/mock_data.js +28 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [78.1.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v78.1.0...v78.1.1) (2024-03-05)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **GlDuoChat:** allow typing prompt while streaming ([bc107bf](https://gitlab.com/gitlab-org/gitlab-ui/commit/bc107bf7b4e84ec787142ed5b43f299ec6c7ea0b))
|
|
7
|
+
|
|
1
8
|
# [78.1.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v78.0.0...v78.1.0) (2024-03-05)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -201,12 +201,22 @@ var script = {
|
|
|
201
201
|
return acc;
|
|
202
202
|
}, [[]]);
|
|
203
203
|
},
|
|
204
|
+
lastMessage() {
|
|
205
|
+
return this.messages[this.messages.length - 1];
|
|
206
|
+
},
|
|
204
207
|
resetDisabled() {
|
|
208
|
+
var _this$lastMessage;
|
|
205
209
|
if (this.isLoading || !this.hasMessages) {
|
|
206
210
|
return true;
|
|
207
211
|
}
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
return ((_this$lastMessage = this.lastMessage) === null || _this$lastMessage === void 0 ? void 0 : _this$lastMessage.content) === CHAT_RESET_MESSAGE;
|
|
213
|
+
},
|
|
214
|
+
submitDisabled() {
|
|
215
|
+
return this.isLoading || this.isStreaming;
|
|
216
|
+
},
|
|
217
|
+
isStreaming() {
|
|
218
|
+
var _this$lastMessage2, _this$lastMessage2$ch, _this$lastMessage3;
|
|
219
|
+
return Boolean(((_this$lastMessage2 = this.lastMessage) === null || _this$lastMessage2 === void 0 ? void 0 : (_this$lastMessage2$ch = _this$lastMessage2.chunks) === null || _this$lastMessage2$ch === void 0 ? void 0 : _this$lastMessage2$ch.length) > 0 && !((_this$lastMessage3 = this.lastMessage) !== null && _this$lastMessage3 !== void 0 && _this$lastMessage3.content));
|
|
210
220
|
},
|
|
211
221
|
filteredSlashCommands() {
|
|
212
222
|
const caseInsensitivePrompt = this.prompt.toLowerCase();
|
|
@@ -227,12 +237,13 @@ var script = {
|
|
|
227
237
|
}
|
|
228
238
|
},
|
|
229
239
|
watch: {
|
|
230
|
-
isLoading() {
|
|
240
|
+
isLoading(newVal) {
|
|
231
241
|
this.isHidden = false;
|
|
232
242
|
this.scrollToBottom();
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
243
|
+
if (newVal) {
|
|
244
|
+
// We reset the prompt when we start getting the response and focus in the prompt field
|
|
245
|
+
this.setPromptAndFocus();
|
|
246
|
+
}
|
|
236
247
|
}
|
|
237
248
|
},
|
|
238
249
|
created() {
|
|
@@ -250,6 +261,9 @@ var script = {
|
|
|
250
261
|
this.$emit('chat-hidden');
|
|
251
262
|
},
|
|
252
263
|
sendChatPrompt() {
|
|
264
|
+
if (this.submitDisabled) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
253
267
|
if (this.prompt) {
|
|
254
268
|
if (this.prompt === CHAT_RESET_MESSAGE && this.resetDisabled) {
|
|
255
269
|
return;
|
|
@@ -324,14 +338,19 @@ var script = {
|
|
|
324
338
|
this.activeCommandIndex = 0;
|
|
325
339
|
}
|
|
326
340
|
},
|
|
341
|
+
async setPromptAndFocus() {
|
|
342
|
+
let prompt = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
|
|
343
|
+
this.prompt = prompt;
|
|
344
|
+
await this.$nextTick();
|
|
345
|
+
this.$refs.prompt.$el.focus();
|
|
346
|
+
},
|
|
327
347
|
selectSlashCommand(index) {
|
|
328
348
|
const command = this.filteredSlashCommands[index];
|
|
329
349
|
if (command.shouldSubmit) {
|
|
330
350
|
this.prompt = command.name;
|
|
331
351
|
this.sendChatPrompt();
|
|
332
352
|
} else {
|
|
333
|
-
this.
|
|
334
|
-
this.$refs.prompt.$el.focus();
|
|
353
|
+
this.setPromptAndFocus(`${command.name} `);
|
|
335
354
|
}
|
|
336
355
|
}
|
|
337
356
|
},
|
|
@@ -347,7 +366,7 @@ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=
|
|
|
347
366
|
{
|
|
348
367
|
'gl-h-full': !_vm.hasMessages,
|
|
349
368
|
'force-scroll-bar': _vm.hasMessages,
|
|
350
|
-
} ],attrs:{"tag":"section","name":"message"}},[_vm._l((_vm.conversations),function(conversation,index){return _c('gl-duo-chat-conversation',{key:("conversation-" + index),attrs:{"messages":conversation,"show-delimiter":index > 0},on:{"track-feedback":_vm.onTrackFeedback}})}),_vm._v(" "),(!_vm.hasMessages && !_vm.isLoading)?[_c('gl-empty-state',{key:"empty-state",staticClass:"gl-flex-grow gl-justify-content-center",attrs:{"svg-path":_vm.$options.emptySvg,"svg-height":145,"title":_vm.emptyStateTitle,"description":_vm.emptyStateDescription}}),_vm._v(" "),_c('gl-duo-chat-predefined-prompts',{key:"predefined-prompts",attrs:{"prompts":_vm.predefinedPrompts},on:{"click":_vm.sendPredefinedPrompt}})]:_vm._e(),_vm._v(" "),(_vm.isLoading)?_c('gl-duo-chat-loader',{key:"loader",attrs:{"tool-name":_vm.toolName}}):_vm._e(),_vm._v(" "),_c('div',{key:"anchor",ref:"anchor",staticClass:"scroll-anchor"})],2)],1),_vm._v(" "),(_vm.isChatAvailable)?_c('footer',{staticClass:"gl-drawer-footer gl-drawer-footer-sticky gl-p-5 gl-border-t gl-bg-gray-10",class:{ 'gl-drawer-body-scrim-on-footer': !_vm.scrolledToBottom },attrs:{"data-testid":"chat-footer"}},[_c('gl-form',{attrs:{"data-testid":"chat-prompt-form"},on:{"submit":function($event){$event.stopPropagation();$event.preventDefault();return _vm.sendChatPrompt.apply(null, arguments)}}},[_c('gl-form-input-group',{scopedSlots:_vm._u([{key:"append",fn:function(){return [_c('gl-button',{staticClass:"!gl-absolute gl-bottom-2 gl-right-2 gl-rounded-base!",attrs:{"icon":"paper-airplane","category":"primary","variant":"confirm","type":"submit","aria-label":_vm.$options.i18n.CHAT_SUBMIT_LABEL,"disabled":_vm.
|
|
369
|
+
} ],attrs:{"tag":"section","name":"message"}},[_vm._l((_vm.conversations),function(conversation,index){return _c('gl-duo-chat-conversation',{key:("conversation-" + index),attrs:{"messages":conversation,"show-delimiter":index > 0},on:{"track-feedback":_vm.onTrackFeedback}})}),_vm._v(" "),(!_vm.hasMessages && !_vm.isLoading)?[_c('gl-empty-state',{key:"empty-state",staticClass:"gl-flex-grow gl-justify-content-center",attrs:{"svg-path":_vm.$options.emptySvg,"svg-height":145,"title":_vm.emptyStateTitle,"description":_vm.emptyStateDescription}}),_vm._v(" "),_c('gl-duo-chat-predefined-prompts',{key:"predefined-prompts",attrs:{"prompts":_vm.predefinedPrompts},on:{"click":_vm.sendPredefinedPrompt}})]:_vm._e(),_vm._v(" "),(_vm.isLoading)?_c('gl-duo-chat-loader',{key:"loader",attrs:{"tool-name":_vm.toolName}}):_vm._e(),_vm._v(" "),_c('div',{key:"anchor",ref:"anchor",staticClass:"scroll-anchor"})],2)],1),_vm._v(" "),(_vm.isChatAvailable)?_c('footer',{staticClass:"gl-drawer-footer gl-drawer-footer-sticky gl-p-5 gl-border-t gl-bg-gray-10",class:{ 'gl-drawer-body-scrim-on-footer': !_vm.scrolledToBottom },attrs:{"data-testid":"chat-footer"}},[_c('gl-form',{attrs:{"data-testid":"chat-prompt-form"},on:{"submit":function($event){$event.stopPropagation();$event.preventDefault();return _vm.sendChatPrompt.apply(null, arguments)}}},[_c('gl-form-input-group',{scopedSlots:_vm._u([{key:"append",fn:function(){return [_c('gl-button',{staticClass:"!gl-absolute gl-bottom-2 gl-right-2 gl-rounded-base!",attrs:{"icon":"paper-airplane","category":"primary","variant":"confirm","type":"submit","data-testid":"chat-prompt-submit-button","aria-label":_vm.$options.i18n.CHAT_SUBMIT_LABEL,"disabled":_vm.submitDisabled}})]},proxy:true}],null,false,3132459889)},[_c('div',{staticClass:"duo-chat-input gl-flex-grow-1 gl-vertical-align-top gl-max-w-full gl-min-h-8 gl-inset-border-1-gray-400 gl-rounded-base gl-bg-white",attrs:{"data-value":_vm.prompt}},[(_vm.shouldShowSlashCommands)?_c('gl-card',{ref:"commands",staticClass:"slash-commands !gl-absolute gl-translate-y-n100 gl-list-style-none gl-pl-0 gl-w-full gl-shadow-md",attrs:{"body-class":"gl-p-2!"}},_vm._l((_vm.filteredSlashCommands),function(command,index){return _c('gl-dropdown-item',{key:command.name,class:{ 'active-command': index === _vm.activeCommandIndex },on:{"click":function($event){return _vm.selectSlashCommand(index)}},nativeOn:{"mouseenter":function($event){_vm.activeCommandIndex = index;}}},[_c('span',{staticClass:"gl-display-flex gl-justify-content-space-between"},[_c('span',{staticClass:"gl-display-block"},[_vm._v(_vm._s(command.name))]),_vm._v(" "),_c('small',{staticClass:"gl-text-gray-500 gl-font-style-italic gl-text-right gl-pl-3"},[_vm._v(_vm._s(command.description))])])])}),1):_vm._e(),_vm._v(" "),_c('gl-form-textarea',{ref:"prompt",staticClass:"gl-absolute gl-h-full! gl-py-4! gl-bg-transparent! gl-rounded-top-right-none gl-rounded-bottom-right-none gl-shadow-none!",class:{ 'gl-text-truncate': !_vm.prompt },attrs:{"data-testid":"chat-prompt-input","placeholder":_vm.inputPlaceholder,"autofocus":""},nativeOn:{"keydown":function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"enter",13,$event.key,"Enter")){ return null; }if($event.ctrlKey||$event.shiftKey||$event.altKey||$event.metaKey){ return null; }$event.preventDefault();},"keyup":function($event){return _vm.onInputKeyup.apply(null, arguments)}},model:{value:(_vm.prompt),callback:function ($$v) {_vm.prompt=$$v;},expression:"prompt"}})],1)]),_vm._v(" "),_c('gl-form-text',{staticClass:"gl-text-gray-400 gl-line-height-20 gl-mt-3",attrs:{"data-testid":"chat-legal-disclaimer"}},[_vm._v(_vm._s(_vm.$options.i18n.CHAT_LEGAL_DISCLAIMER))])],1)],1):_vm._e()]):_vm._e()};
|
|
351
370
|
var __vue_staticRenderFns__ = [];
|
|
352
371
|
|
|
353
372
|
/* style */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setStoryTimeout } from '../../../../utils/test_utils';
|
|
1
2
|
import { DOCUMENTATION_SOURCE_TYPES, MESSAGE_MODEL_ROLES } from './constants';
|
|
2
3
|
|
|
3
4
|
const MOCK_SOURCES = [{
|
|
@@ -57,21 +58,47 @@ const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
|
|
|
57
58
|
errors: [],
|
|
58
59
|
timestamp: '2021-04-21T12:00:00.000Z'
|
|
59
60
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
|
|
62
|
+
// Utility function for delay
|
|
63
|
+
async function delayRandom() {
|
|
64
|
+
let min = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 16;
|
|
65
|
+
let max = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 267;
|
|
66
|
+
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
67
|
+
// eslint-disable-next-line no-promise-executor-return
|
|
68
|
+
return new Promise(resolve => setStoryTimeout(resolve, delay));
|
|
69
|
+
}
|
|
70
|
+
function generateMockResponseChunks() {
|
|
71
|
+
try {
|
|
72
|
+
let requestId = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
|
|
73
|
+
return async function* () {
|
|
74
|
+
const chunkSize = 5;
|
|
75
|
+
const contentLength = MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.length;
|
|
76
|
+
const chunkCount = Math.ceil(contentLength / chunkSize);
|
|
77
|
+
for (let chunkId = 0; chunkId < chunkCount; chunkId += 1) {
|
|
78
|
+
const start = chunkId * chunkSize;
|
|
79
|
+
const end = Math.min((chunkId + 1) * chunkSize, contentLength);
|
|
80
|
+
const chunk = {
|
|
81
|
+
...MOCK_RESPONSE_MESSAGE_FOR_STREAMING,
|
|
82
|
+
requestId,
|
|
83
|
+
content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.substring(start, end),
|
|
84
|
+
chunkId
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// eslint-disable-next-line no-await-in-loop
|
|
88
|
+
await delayRandom();
|
|
89
|
+
yield chunk;
|
|
90
|
+
}
|
|
91
|
+
yield {
|
|
92
|
+
...MOCK_RESPONSE_MESSAGE_FOR_STREAMING,
|
|
93
|
+
requestId,
|
|
94
|
+
content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content,
|
|
95
|
+
chunkId: null
|
|
96
|
+
};
|
|
97
|
+
}();
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return Promise.reject(e);
|
|
72
100
|
}
|
|
73
|
-
|
|
74
|
-
};
|
|
101
|
+
}
|
|
75
102
|
const MOCK_USER_PROMPT_MESSAGE = {
|
|
76
103
|
id: '456',
|
|
77
104
|
content: 'How to create a new template?',
|
package/dist/tokens/js/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import DuoChatLoader from './components/duo_chat_loader/duo_chat_loader.vue';
|
|
|
8
8
|
import DuoChatPredefinedPrompts from './components/duo_chat_predefined_prompts/duo_chat_predefined_prompts.vue';
|
|
9
9
|
import DuoChatConversation from './components/duo_chat_conversation/duo_chat_conversation.vue';
|
|
10
10
|
import GlDuoChat from './duo_chat.vue';
|
|
11
|
+
import { MOCK_RESPONSE_MESSAGE, MOCK_USER_PROMPT_MESSAGE } from './mock_data';
|
|
11
12
|
|
|
12
13
|
import { MESSAGE_MODEL_ROLES, CHAT_RESET_MESSAGE } from './constants';
|
|
13
14
|
|
|
@@ -339,11 +340,38 @@ describe('GlDuoChat', () => {
|
|
|
339
340
|
data: { prompt: promptStr },
|
|
340
341
|
});
|
|
341
342
|
trigger();
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
343
|
+
expect(wrapper.emitted('send-chat-prompt')).toEqual(expectEmitted);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it.each`
|
|
347
|
+
desc | msgs
|
|
348
|
+
${''} | ${[]}
|
|
349
|
+
${'with just a user message'} | ${[MOCK_USER_PROMPT_MESSAGE]}
|
|
350
|
+
${'with a user message, and a complete response'} | ${[MOCK_USER_PROMPT_MESSAGE, MOCK_RESPONSE_MESSAGE]}
|
|
351
|
+
`('prevents submission when loading $desc', ({ msgs } = {}) => {
|
|
352
|
+
createComponent({
|
|
353
|
+
propsData: { isChatAvailable: true, isLoading: true, messages: msgs },
|
|
354
|
+
data: { prompt: promptStr },
|
|
355
|
+
});
|
|
356
|
+
clickSubmit();
|
|
357
|
+
expect(wrapper.emitted('send-chat-prompt')).toBe(undefined);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it.each([
|
|
361
|
+
[[{ ...MOCK_RESPONSE_MESSAGE, content: undefined, chunks: [''] }]],
|
|
362
|
+
[
|
|
363
|
+
[
|
|
364
|
+
MOCK_USER_PROMPT_MESSAGE,
|
|
365
|
+
{ ...MOCK_RESPONSE_MESSAGE, content: undefined, chunks: [''] },
|
|
366
|
+
],
|
|
367
|
+
],
|
|
368
|
+
])('prevents submission when streaming (messages = "%o")', (msgs = []) => {
|
|
369
|
+
createComponent({
|
|
370
|
+
propsData: { isChatAvailable: true, messages: msgs },
|
|
371
|
+
data: { prompt: promptStr },
|
|
372
|
+
});
|
|
373
|
+
clickSubmit();
|
|
374
|
+
expect(wrapper.emitted('send-chat-prompt')).toBe(undefined);
|
|
347
375
|
});
|
|
348
376
|
});
|
|
349
377
|
|
|
@@ -428,7 +456,7 @@ describe('GlDuoChat', () => {
|
|
|
428
456
|
expect(findChatComponent().exists()).toBe(true);
|
|
429
457
|
});
|
|
430
458
|
|
|
431
|
-
it('resets the prompt when
|
|
459
|
+
it('resets the prompt when a message is loaded', async () => {
|
|
432
460
|
const prompt = 'foo';
|
|
433
461
|
createComponent({ data: { prompt } });
|
|
434
462
|
expect(findChatInput().props('value')).toBe(prompt);
|
|
@@ -436,7 +464,7 @@ describe('GlDuoChat', () => {
|
|
|
436
464
|
// reactive behavior which consistutes an exception
|
|
437
465
|
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
438
466
|
wrapper.setProps({
|
|
439
|
-
|
|
467
|
+
isLoading: true,
|
|
440
468
|
});
|
|
441
469
|
await nextTick();
|
|
442
470
|
expect(findChatInput().props('value')).toBe('');
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import GlButton from '../../../base/button/button.vue';
|
|
2
2
|
import GlAlert from '../../../base/alert/alert.vue';
|
|
3
|
-
import { setStoryTimeout } from '../../../../utils/test_utils';
|
|
4
3
|
import { makeContainer } from '../../../../utils/story_decorators/container';
|
|
5
4
|
import GlDuoChat from './duo_chat.vue';
|
|
6
5
|
import readme from './duo_chat.md';
|
|
@@ -132,32 +131,52 @@ export const Interactive = (args, { argTypes }) => ({
|
|
|
132
131
|
this.isHidden = false;
|
|
133
132
|
this.loggerInfo += `Chat opened\n\n`;
|
|
134
133
|
},
|
|
135
|
-
onResponseRequested() {
|
|
134
|
+
async onResponseRequested() {
|
|
136
135
|
this.timeout = null;
|
|
137
|
-
this.
|
|
138
|
-
this.mockResponseFromAi();
|
|
136
|
+
await this.mockResponseFromAi();
|
|
139
137
|
this.requestId += 1;
|
|
140
138
|
},
|
|
141
|
-
mockResponseFromAi() {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
async mockResponseFromAi() {
|
|
140
|
+
const generator = generateMockResponseChunks(this.requestId);
|
|
141
|
+
|
|
142
|
+
for await (const result of generator) {
|
|
143
|
+
const { chunkId, content, ...messageAttributes } = result;
|
|
145
144
|
const existingMessageIndex = this.msgs.findIndex(
|
|
146
|
-
(msg) => msg.requestId ===
|
|
145
|
+
(msg) => msg.requestId === result.requestId && msg.role === result.role
|
|
147
146
|
);
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
this.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
147
|
+
|
|
148
|
+
if (existingMessageIndex === -1) {
|
|
149
|
+
this.addNewMessage(messageAttributes, content);
|
|
150
|
+
} else {
|
|
151
|
+
this.updateExistingMessage(existingMessageIndex, content, chunkId);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
addNewMessage(messageAttributes, content) {
|
|
156
|
+
this.promptInFlight = false;
|
|
157
|
+
this.$set(this.msgs, this.msgs.length, {
|
|
158
|
+
...messageAttributes,
|
|
159
|
+
chunks: [content],
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
updateExistingMessage(index, content, chunkId) {
|
|
163
|
+
const message = this.msgs[index];
|
|
164
|
+
|
|
165
|
+
if (chunkId != null) {
|
|
166
|
+
// Ensure the chunks array exists
|
|
167
|
+
if (!message.chunks) {
|
|
168
|
+
this.$set(message, 'chunks', []);
|
|
154
169
|
} else {
|
|
155
|
-
this.
|
|
170
|
+
this.$set(message.chunks, chunkId, content);
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
// Update for final message
|
|
174
|
+
this.$set(message, 'content', content);
|
|
175
|
+
|
|
176
|
+
// Remove chunks if they are not needed anymore
|
|
177
|
+
if (message.chunks) {
|
|
178
|
+
this.$delete(message, 'chunks');
|
|
156
179
|
}
|
|
157
|
-
this.logerInfo += `New response: ${JSON.stringify(newResponse)}\n\n`;
|
|
158
|
-
this.timeout = setStoryTimeout(() => {
|
|
159
|
-
this.mockResponseFromAi();
|
|
160
|
-
}, Math.floor(Math.random() * 251) + 16);
|
|
161
180
|
}
|
|
162
181
|
},
|
|
163
182
|
},
|
|
@@ -212,13 +212,20 @@ export default {
|
|
|
212
212
|
[[]]
|
|
213
213
|
);
|
|
214
214
|
},
|
|
215
|
+
lastMessage() {
|
|
216
|
+
return this.messages[this.messages.length - 1];
|
|
217
|
+
},
|
|
215
218
|
resetDisabled() {
|
|
216
219
|
if (this.isLoading || !this.hasMessages) {
|
|
217
220
|
return true;
|
|
218
221
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
return this.lastMessage?.content === CHAT_RESET_MESSAGE;
|
|
223
|
+
},
|
|
224
|
+
submitDisabled() {
|
|
225
|
+
return this.isLoading || this.isStreaming;
|
|
226
|
+
},
|
|
227
|
+
isStreaming() {
|
|
228
|
+
return Boolean(this.lastMessage?.chunks?.length > 0 && !this.lastMessage?.content);
|
|
222
229
|
},
|
|
223
230
|
filteredSlashCommands() {
|
|
224
231
|
const caseInsensitivePrompt = this.prompt.toLowerCase();
|
|
@@ -246,12 +253,13 @@ export default {
|
|
|
246
253
|
},
|
|
247
254
|
},
|
|
248
255
|
watch: {
|
|
249
|
-
isLoading() {
|
|
256
|
+
isLoading(newVal) {
|
|
250
257
|
this.isHidden = false;
|
|
251
258
|
this.scrollToBottom();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
if (newVal) {
|
|
260
|
+
// We reset the prompt when we start getting the response and focus in the prompt field
|
|
261
|
+
this.setPromptAndFocus();
|
|
262
|
+
}
|
|
255
263
|
},
|
|
256
264
|
},
|
|
257
265
|
created() {
|
|
@@ -269,6 +277,9 @@ export default {
|
|
|
269
277
|
this.$emit('chat-hidden');
|
|
270
278
|
},
|
|
271
279
|
sendChatPrompt() {
|
|
280
|
+
if (this.submitDisabled) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
272
283
|
if (this.prompt) {
|
|
273
284
|
if (this.prompt === CHAT_RESET_MESSAGE && this.resetDisabled) {
|
|
274
285
|
return;
|
|
@@ -336,14 +347,18 @@ export default {
|
|
|
336
347
|
this.activeCommandIndex = 0;
|
|
337
348
|
}
|
|
338
349
|
},
|
|
350
|
+
async setPromptAndFocus(prompt = '') {
|
|
351
|
+
this.prompt = prompt;
|
|
352
|
+
await this.$nextTick();
|
|
353
|
+
this.$refs.prompt.$el.focus();
|
|
354
|
+
},
|
|
339
355
|
selectSlashCommand(index) {
|
|
340
356
|
const command = this.filteredSlashCommands[index];
|
|
341
357
|
if (command.shouldSubmit) {
|
|
342
358
|
this.prompt = command.name;
|
|
343
359
|
this.sendChatPrompt();
|
|
344
360
|
} else {
|
|
345
|
-
this.
|
|
346
|
-
this.$refs.prompt.$el.focus();
|
|
361
|
+
this.setPromptAndFocus(`${command.name} `);
|
|
347
362
|
}
|
|
348
363
|
},
|
|
349
364
|
},
|
|
@@ -494,7 +509,6 @@ export default {
|
|
|
494
509
|
class="gl-absolute gl-h-full! gl-py-4! gl-bg-transparent! gl-rounded-top-right-none gl-rounded-bottom-right-none gl-shadow-none!"
|
|
495
510
|
:class="{ 'gl-text-truncate': !prompt }"
|
|
496
511
|
:placeholder="inputPlaceholder"
|
|
497
|
-
:disabled="isLoading"
|
|
498
512
|
autofocus
|
|
499
513
|
@keydown.enter.exact.native.prevent
|
|
500
514
|
@keyup.native="onInputKeyup"
|
|
@@ -507,8 +521,9 @@ export default {
|
|
|
507
521
|
variant="confirm"
|
|
508
522
|
class="!gl-absolute gl-bottom-2 gl-right-2 gl-rounded-base!"
|
|
509
523
|
type="submit"
|
|
524
|
+
data-testid="chat-prompt-submit-button"
|
|
510
525
|
:aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
|
|
511
|
-
:disabled="
|
|
526
|
+
:disabled="submitDisabled"
|
|
512
527
|
/>
|
|
513
528
|
</template>
|
|
514
529
|
</gl-form-input-group>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setStoryTimeout } from '../../../../utils/test_utils';
|
|
1
2
|
import { DOCUMENTATION_SOURCE_TYPES, MESSAGE_MODEL_ROLES } from './constants';
|
|
2
3
|
|
|
3
4
|
const MOCK_SOURCES = [
|
|
@@ -66,24 +67,39 @@ export const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
|
|
|
66
67
|
timestamp: '2021-04-21T12:00:00.000Z',
|
|
67
68
|
};
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
// Utility function for delay
|
|
71
|
+
async function delayRandom(min = 16, max = 267) {
|
|
72
|
+
const delay = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
73
|
+
// eslint-disable-next-line no-promise-executor-return
|
|
74
|
+
return new Promise((resolve) => setStoryTimeout(resolve, delay));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function* generateMockResponseChunks(requestId = 1) {
|
|
71
78
|
const chunkSize = 5;
|
|
72
|
-
const
|
|
73
|
-
|
|
79
|
+
const contentLength = MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.length;
|
|
80
|
+
const chunkCount = Math.ceil(contentLength / chunkSize);
|
|
81
|
+
|
|
82
|
+
for (let chunkId = 0; chunkId < chunkCount; chunkId += 1) {
|
|
83
|
+
const start = chunkId * chunkSize;
|
|
84
|
+
const end = Math.min((chunkId + 1) * chunkSize, contentLength);
|
|
74
85
|
const chunk = {
|
|
75
86
|
...MOCK_RESPONSE_MESSAGE_FOR_STREAMING,
|
|
76
87
|
requestId,
|
|
77
|
-
content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.substring(
|
|
78
|
-
|
|
79
|
-
(i + 1) * chunkSize
|
|
80
|
-
),
|
|
81
|
-
chunkId: i,
|
|
88
|
+
content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.substring(start, end),
|
|
89
|
+
chunkId,
|
|
82
90
|
};
|
|
83
|
-
|
|
91
|
+
|
|
92
|
+
// eslint-disable-next-line no-await-in-loop
|
|
93
|
+
await delayRandom();
|
|
94
|
+
yield chunk;
|
|
84
95
|
}
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
yield {
|
|
97
|
+
...MOCK_RESPONSE_MESSAGE_FOR_STREAMING,
|
|
98
|
+
requestId,
|
|
99
|
+
content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content,
|
|
100
|
+
chunkId: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
87
103
|
|
|
88
104
|
export const MOCK_USER_PROMPT_MESSAGE = {
|
|
89
105
|
id: '456',
|