@gitlab/ui 78.0.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 +14 -0
- package/dist/components/experimental/duo/chat/duo_chat.js +30 -11
- package/dist/components/experimental/duo/chat/mock_data.js +41 -14
- package/dist/components/experimental/experiment_badge/constants.js +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 +1 -1
- package/src/components/experimental/duo/chat/duo_chat.spec.js +44 -7
- package/src/components/experimental/duo/chat/duo_chat.stories.js +39 -20
- package/src/components/experimental/duo/chat/duo_chat.vue +28 -12
- package/src/components/experimental/duo/chat/mock_data.js +28 -12
- package/src/components/experimental/experiment_badge/constants.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
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
|
+
|
|
8
|
+
# [78.1.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v78.0.0...v78.1.0) (2024-03-05)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **GlDuoChat:** explicitly disable Experimental Badge ([d701cc9](https://gitlab.com/gitlab-org/gitlab-ui/commit/d701cc95e9af956ce17c3b4c75ea4d481cadde59))
|
|
14
|
+
|
|
1
15
|
# [78.0.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v77.7.0...v78.0.0) (2024-03-05)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -119,7 +119,7 @@ var script = {
|
|
|
119
119
|
* The type of the badge. This is passed down to the `GlExperimentBadge` component. Refer that component for more information.
|
|
120
120
|
*/
|
|
121
121
|
badgeType: {
|
|
122
|
-
type: String,
|
|
122
|
+
type: String || null,
|
|
123
123
|
required: false,
|
|
124
124
|
default: badgeTypes[0],
|
|
125
125
|
validator: badgeTypeValidator
|
|
@@ -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
|
},
|
|
@@ -343,11 +362,11 @@ var script = {
|
|
|
343
362
|
const __vue_script__ = script;
|
|
344
363
|
|
|
345
364
|
/* template */
|
|
346
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (!_vm.isHidden)?_c('aside',{staticClass:"markdown-code-block gl-drawer gl-max-h-full gl-bottom-0 gl-shadow-none gl-border-l gl-border-t duo-chat",attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"}},[(_vm.showHeader)?_c('header',{staticClass:"gl-drawer-header gl-drawer-header-sticky gl-z-index-200 gl-p-0! gl-border-b-0",attrs:{"data-testid":"chat-header"}},[_c('div',{staticClass:"drawer-title gl-display-flex gl-justify-content-start gl-align-items-center gl-p-5"},[_c('h3',{staticClass:"gl-my-0 gl-font-size-h2"},[_vm._v(_vm._s(_vm.title))]),_vm._v(" "),_c('gl-experiment-badge',{attrs:{"help-page-url":_vm.badgeHelpPageUrl,"type":_vm.badgeType,"container-id":"chat-component"}}),_vm._v(" "),_c('gl-button',{staticClass:"gl-p-0! gl-ml-auto",attrs:{"category":"tertiary","variant":"default","icon":"close","size":"small","data-testid":"chat-close-button","aria-label":_vm.$options.i18n.CHAT_CLOSE_LABEL},on:{"click":_vm.hideChat}})],1),_vm._v(" "),_c('gl-alert',{staticClass:"gl-text-center gl-border-t gl-p-4 gl-text-gray-700 gl-bg-gray-50 legal-warning gl-max-w-full",attrs:{"dismissible":false,"variant":"tip","show-icon":false,"role":"alert","data-testid":"chat-legal-warning"}},[_vm._v(_vm._s(_vm.$options.i18n.CHAT_LEGAL_GENERATED_BY_AI))]),_vm._v(" "),_vm._t("subheader"),_vm._v(" "),(_vm.error)?_c('gl-alert',{key:"error",staticClass:"gl-pl-9!",attrs:{"dismissible":false,"variant":"danger","role":"alert","data-testid":"chat-error"}},[_c('span',{directives:[{name:"safe-html",rawName:"v-safe-html",value:(_vm.error),expression:"error"}]})]):_vm._e()],2):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-drawer-body",attrs:{"data-testid":"chat-history"},on:{"scroll":_vm.handleScrollingTrottled}},[_c('transition-group',{staticClass:"duo-chat-history gl-display-flex gl-flex-direction-column gl-justify-content-end gl-bg-gray-10",class:[
|
|
365
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (!_vm.isHidden)?_c('aside',{staticClass:"markdown-code-block gl-drawer gl-max-h-full gl-bottom-0 gl-shadow-none gl-border-l gl-border-t duo-chat",attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"}},[(_vm.showHeader)?_c('header',{staticClass:"gl-drawer-header gl-drawer-header-sticky gl-z-index-200 gl-p-0! gl-border-b-0",attrs:{"data-testid":"chat-header"}},[_c('div',{staticClass:"drawer-title gl-display-flex gl-justify-content-start gl-align-items-center gl-p-5"},[_c('h3',{staticClass:"gl-my-0 gl-font-size-h2"},[_vm._v(_vm._s(_vm.title))]),_vm._v(" "),(_vm.badgeType)?_c('gl-experiment-badge',{attrs:{"help-page-url":_vm.badgeHelpPageUrl,"type":_vm.badgeType,"container-id":"chat-component"}}):_vm._e(),_vm._v(" "),_c('gl-button',{staticClass:"gl-p-0! gl-ml-auto",attrs:{"category":"tertiary","variant":"default","icon":"close","size":"small","data-testid":"chat-close-button","aria-label":_vm.$options.i18n.CHAT_CLOSE_LABEL},on:{"click":_vm.hideChat}})],1),_vm._v(" "),_c('gl-alert',{staticClass:"gl-text-center gl-border-t gl-p-4 gl-text-gray-700 gl-bg-gray-50 legal-warning gl-max-w-full",attrs:{"dismissible":false,"variant":"tip","show-icon":false,"role":"alert","data-testid":"chat-legal-warning"}},[_vm._v(_vm._s(_vm.$options.i18n.CHAT_LEGAL_GENERATED_BY_AI))]),_vm._v(" "),_vm._t("subheader"),_vm._v(" "),(_vm.error)?_c('gl-alert',{key:"error",staticClass:"gl-pl-9!",attrs:{"dismissible":false,"variant":"danger","role":"alert","data-testid":"chat-error"}},[_c('span',{directives:[{name:"safe-html",rawName:"v-safe-html",value:(_vm.error),expression:"error"}]})]):_vm._e()],2):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-drawer-body",attrs:{"data-testid":"chat-history"},on:{"scroll":_vm.handleScrollingTrottled}},[_c('transition-group',{staticClass:"duo-chat-history gl-display-flex gl-flex-direction-column gl-justify-content-end gl-bg-gray-10",class:[
|
|
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
|
|
|
@@ -126,6 +127,15 @@ describe('GlDuoChat', () => {
|
|
|
126
127
|
expect(component().exists()).toBe(shouldRender);
|
|
127
128
|
});
|
|
128
129
|
|
|
130
|
+
it('does not render the experimental label if it is explicitely disabled', () => {
|
|
131
|
+
createComponent({
|
|
132
|
+
propsData: {
|
|
133
|
+
badgeType: null,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
expect(findBadge().exists()).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
129
139
|
describe('when messages exist', () => {
|
|
130
140
|
it('scrolls to the bottom on load', async () => {
|
|
131
141
|
const scrollIntoViewMock = jest.fn();
|
|
@@ -330,11 +340,38 @@ describe('GlDuoChat', () => {
|
|
|
330
340
|
data: { prompt: promptStr },
|
|
331
341
|
});
|
|
332
342
|
trigger();
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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);
|
|
338
375
|
});
|
|
339
376
|
});
|
|
340
377
|
|
|
@@ -419,7 +456,7 @@ describe('GlDuoChat', () => {
|
|
|
419
456
|
expect(findChatComponent().exists()).toBe(true);
|
|
420
457
|
});
|
|
421
458
|
|
|
422
|
-
it('resets the prompt when
|
|
459
|
+
it('resets the prompt when a message is loaded', async () => {
|
|
423
460
|
const prompt = 'foo';
|
|
424
461
|
createComponent({ data: { prompt } });
|
|
425
462
|
expect(findChatInput().props('value')).toBe(prompt);
|
|
@@ -427,7 +464,7 @@ describe('GlDuoChat', () => {
|
|
|
427
464
|
// reactive behavior which consistutes an exception
|
|
428
465
|
// See https://docs.gitlab.com/ee/development/fe_guide/style/vue.html#setting-component-state
|
|
429
466
|
wrapper.setProps({
|
|
430
|
-
|
|
467
|
+
isLoading: true,
|
|
431
468
|
});
|
|
432
469
|
await nextTick();
|
|
433
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
|
},
|
|
@@ -127,7 +127,7 @@ export default {
|
|
|
127
127
|
* The type of the badge. This is passed down to the `GlExperimentBadge` component. Refer that component for more information.
|
|
128
128
|
*/
|
|
129
129
|
badgeType: {
|
|
130
|
-
type: String,
|
|
130
|
+
type: String || null,
|
|
131
131
|
required: false,
|
|
132
132
|
default: badgeTypes[0],
|
|
133
133
|
validator: badgeTypeValidator,
|
|
@@ -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
|
},
|
|
@@ -369,6 +384,7 @@ export default {
|
|
|
369
384
|
>
|
|
370
385
|
<h3 class="gl-my-0 gl-font-size-h2">{{ title }}</h3>
|
|
371
386
|
<gl-experiment-badge
|
|
387
|
+
v-if="badgeType"
|
|
372
388
|
:help-page-url="badgeHelpPageUrl"
|
|
373
389
|
:type="badgeType"
|
|
374
390
|
container-id="chat-component"
|
|
@@ -493,7 +509,6 @@ export default {
|
|
|
493
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!"
|
|
494
510
|
:class="{ 'gl-text-truncate': !prompt }"
|
|
495
511
|
:placeholder="inputPlaceholder"
|
|
496
|
-
:disabled="isLoading"
|
|
497
512
|
autofocus
|
|
498
513
|
@keydown.enter.exact.native.prevent
|
|
499
514
|
@keyup.native="onInputKeyup"
|
|
@@ -506,8 +521,9 @@ export default {
|
|
|
506
521
|
variant="confirm"
|
|
507
522
|
class="!gl-absolute gl-bottom-2 gl-right-2 gl-rounded-base!"
|
|
508
523
|
type="submit"
|
|
524
|
+
data-testid="chat-prompt-submit-button"
|
|
509
525
|
:aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
|
|
510
|
-
:disabled="
|
|
526
|
+
:disabled="submitDisabled"
|
|
511
527
|
/>
|
|
512
528
|
</template>
|
|
513
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',
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const badgeTypes = ['experiment', 'beta'];
|
|
1
|
+
export const badgeTypes = ['experiment', 'beta', null];
|
|
2
2
|
export const badgeTypeValidator = (value) => badgeTypes.includes(value);
|