@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 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
- const lastMessage = this.messages[this.messages.length - 1];
209
- return lastMessage.content === CHAT_RESET_MESSAGE;
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
- messages() {
235
- this.prompt = '';
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.prompt = `${command.name} `;
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.isLoading}})]},proxy:true}],null,false,3929529036)},[_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,"disabled":_vm.isLoading,"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()};
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
- const generateMockResponseChunks = requestId => {
61
- const chunks = [];
62
- const chunkSize = 5;
63
- const chunkCount = Math.ceil(MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.length / chunkSize);
64
- for (let i = 0; i < chunkCount; i += 1) {
65
- const chunk = {
66
- ...MOCK_RESPONSE_MESSAGE_FOR_STREAMING,
67
- requestId,
68
- content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.substring(i * chunkSize, (i + 1) * chunkSize),
69
- chunkId: i
70
- };
71
- chunks.push(chunk);
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
- return chunks;
74
- };
101
+ }
75
102
  const MOCK_USER_PROMPT_MESSAGE = {
76
103
  id: '456',
77
104
  content: 'How to create a new template?',
@@ -1,4 +1,4 @@
1
- const badgeTypes = ['experiment', 'beta'];
1
+ const badgeTypes = ['experiment', 'beta', null];
2
2
  const badgeTypeValidator = value => badgeTypes.includes(value);
3
3
 
4
4
  export { badgeTypeValidator, badgeTypes };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 05 Mar 2024 10:22:05 GMT
3
+ * Generated on Tue, 05 Mar 2024 16:29:47 GMT
4
4
  */
5
5
 
6
6
  :root {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 05 Mar 2024 10:22:05 GMT
3
+ * Generated on Tue, 05 Mar 2024 16:29:47 GMT
4
4
  */
5
5
 
6
6
  :root.gl-dark {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 05 Mar 2024 10:22:05 GMT
3
+ * Generated on Tue, 05 Mar 2024 16:29:47 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#133a03";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Do not edit directly
3
- * Generated on Tue, 05 Mar 2024 10:22:05 GMT
3
+ * Generated on Tue, 05 Mar 2024 16:29:47 GMT
4
4
  */
5
5
 
6
6
  export const DATA_VIZ_GREEN_50 = "#ddfab7";
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Tue, 05 Mar 2024 10:22:06 GMT
3
+ // Generated on Tue, 05 Mar 2024 16:29:47 GMT
4
4
 
5
5
  $gl-text-tertiary: #737278 !default;
6
6
  $gl-text-secondary: #89888d !default;
@@ -1,6 +1,6 @@
1
1
 
2
2
  // Do not edit directly
3
- // Generated on Tue, 05 Mar 2024 10:22:05 GMT
3
+ // Generated on Tue, 05 Mar 2024 16:29:47 GMT
4
4
 
5
5
  $gl-text-tertiary: #89888d !default;
6
6
  $gl-text-secondary: #737278 !default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "78.0.0",
3
+ "version": "78.1.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -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
- if (expectEmitted) {
334
- expect(wrapper.emitted('send-chat-prompt')).toEqual(expectEmitted);
335
- } else {
336
- expect(wrapper.emitted('send-chat-prompt')).toBe(undefined);
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 new messages are added', async () => {
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
- messages,
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.chunks = generateMockResponseChunks(this.requestId);
138
- this.mockResponseFromAi();
136
+ await this.mockResponseFromAi();
139
137
  this.requestId += 1;
140
138
  },
141
- mockResponseFromAi() {
142
- this.promptInFlight = false;
143
- if (this.chunks.length) {
144
- const newResponse = this.chunks.shift();
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 === newResponse.requestId && msg.role === newResponse.role
145
+ (msg) => msg.requestId === result.requestId && msg.role === result.role
147
146
  );
148
- const existingMessage = this.msgs[existingMessageIndex];
149
- if (existingMessage) {
150
- this.msgs.splice(existingMessageIndex, 1, {
151
- ...existingMessage,
152
- content: existingMessage.content + newResponse.content,
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.msgs.push(newResponse);
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
- const lastMessage = this.messages[this.messages.length - 1];
221
- return lastMessage.content === CHAT_RESET_MESSAGE;
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
- messages() {
254
- this.prompt = '';
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.prompt = `${command.name} `;
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="isLoading"
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
- export const generateMockResponseChunks = (requestId) => {
70
- const chunks = [];
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 chunkCount = Math.ceil(MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.length / chunkSize);
73
- for (let i = 0; i < chunkCount; i += 1) {
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
- i * chunkSize,
79
- (i + 1) * chunkSize
80
- ),
81
- chunkId: i,
88
+ content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.substring(start, end),
89
+ chunkId,
82
90
  };
83
- chunks.push(chunk);
91
+
92
+ // eslint-disable-next-line no-await-in-loop
93
+ await delayRandom();
94
+ yield chunk;
84
95
  }
85
- return chunks;
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);