@gitlab/ui 92.0.0 → 92.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
+ ## [92.1.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v92.1.0...v92.1.1) (2024-09-11)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * ensure chat scrolls to bottom with new message ([3d048f8](https://gitlab.com/gitlab-org/gitlab-ui/commit/3d048f82c586cd3860a7cf5c86d0572ee44d0dc6))
7
+
8
+ # [92.1.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v92.0.0...v92.1.0) (2024-09-10)
9
+
10
+
11
+ ### Features
12
+
13
+ * **GlDuoChat:** add /include command, integrate context-item-menu ([3f664f8](https://gitlab.com/gitlab-org/gitlab-ui/commit/3f664f89a35aa426bb043f1563cd2bdb9722878c))
14
+
1
15
  # [92.0.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v91.15.0...v92.0.0) (2024-09-10)
2
16
 
3
17
 
@@ -179,6 +179,11 @@ var script = {
179
179
  return;
180
180
  }
181
181
  this.selectedCategory = null;
182
+
183
+ /**
184
+ * Emitted when the parent GlDuoChat component should refocus on the main prompt input
185
+ */
186
+ this.$emit('focus-prompt');
182
187
  break;
183
188
  }
184
189
  },
@@ -82,7 +82,7 @@ var script = {
82
82
  const __vue_script__ = script;
83
83
 
84
84
  /* template */
85
- var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"gl-mb-3 gl-flex gl-flex-col"},[_c('button',{staticClass:"gl-flex gl-w-full gl-items-center gl-border-0 gl-bg-transparent gl-p-0 gl-text-left gl-text-xs gl-lowercase gl-text-gray-500",attrs:{"data-testid":"chat-context-selections-title"},on:{"click":_vm.toggleCollapse}},[_c('gl-icon',{attrs:{"name":_vm.collapseIconName,"data-testid":"chat-context-collapse-icon"}}),_vm._v(" "+_vm._s(_vm.title)+"\n ")],1),_vm._v(" "),_c('div',{directives:[{name:"show",rawName:"v-show",value:(!_vm.isCollapsed),expression:"!isCollapsed"}],staticClass:"gl-mt-1 gl-flex gl-grow gl-flex-wrap",attrs:{"data-testid":"chat-context-tokens-wrapper"}},_vm._l((_vm.selections),function(contextItem){return _c('gl-token',{key:contextItem.id,staticClass:"gl-mb-2 gl-mr-2",attrs:{"view-only":!_vm.removable,"variant":"default"},on:{"close":function($event){return _vm.onRemoveItem(contextItem)}}},[_c('div',{staticClass:"gl-flex gl-items-center",attrs:{"id":("context-item-" + (contextItem.id) + "-" + _vm.selectionsId)}},[_c('gl-icon',{staticClass:"gl-mr-1",attrs:{"name":_vm.getIconName(contextItem.type),"size":12}}),_vm._v("\n "+_vm._s(contextItem.metadata.name)+"\n ")],1),_vm._v(" "),_c('gl-duo-chat-context-item-popover',{attrs:{"context-item":contextItem,"target":("context-item-" + (contextItem.id) + "-" + _vm.selectionsId),"placement":"bottom"}})],1)}),1)])};
85
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"gl-mb-3 gl-flex gl-flex-col"},[_c('button',{staticClass:"gl-flex gl-w-full gl-items-center gl-border-0 gl-bg-transparent gl-p-0 gl-text-left gl-text-xs gl-lowercase gl-text-gray-500",attrs:{"data-testid":"chat-context-selections-title","type":"button"},on:{"click":_vm.toggleCollapse}},[_c('gl-icon',{attrs:{"name":_vm.collapseIconName,"data-testid":"chat-context-collapse-icon"}}),_vm._v(" "+_vm._s(_vm.title)+"\n ")],1),_vm._v(" "),_c('div',{directives:[{name:"show",rawName:"v-show",value:(!_vm.isCollapsed),expression:"!isCollapsed"}],staticClass:"gl-mt-1 gl-flex gl-grow gl-flex-wrap",attrs:{"data-testid":"chat-context-tokens-wrapper"}},_vm._l((_vm.selections),function(contextItem){return _c('gl-token',{key:contextItem.id,staticClass:"gl-mb-2 gl-mr-2",attrs:{"view-only":!_vm.removable,"variant":"default"},on:{"close":function($event){return _vm.onRemoveItem(contextItem)}}},[_c('div',{staticClass:"gl-flex gl-items-center",attrs:{"id":("context-item-" + (contextItem.id) + "-" + _vm.selectionsId)}},[_c('gl-icon',{staticClass:"gl-mr-1",attrs:{"name":_vm.getIconName(contextItem.type),"size":12}}),_vm._v("\n "+_vm._s(contextItem.metadata.name)+"\n ")],1),_vm._v(" "),_c('gl-duo-chat-context-item-popover',{attrs:{"context-item":contextItem,"target":("context-item-" + (contextItem.id) + "-" + _vm.selectionsId),"placement":"bottom"}})],1)}),1)])};
86
86
  var __vue_staticRenderFns__ = [];
87
87
 
88
88
  /* style */
@@ -1,6 +1,7 @@
1
1
  const CHAT_RESET_MESSAGE = '/reset';
2
2
  const CHAT_CLEAN_MESSAGE = '/clean';
3
3
  const CHAT_CLEAR_MESSAGE = '/clear';
4
+ const CHAT_INCLUDE_MESSAGE = '/include';
4
5
  const LOADING_TRANSITION_DURATION = 7500;
5
6
  const DOCUMENTATION_SOURCE_TYPES = {
6
7
  HANDBOOK: {
@@ -22,4 +23,4 @@ const MESSAGE_MODEL_ROLES = {
22
23
  assistant: 'assistant'
23
24
  };
24
25
 
25
- export { CHAT_CLEAN_MESSAGE, CHAT_CLEAR_MESSAGE, CHAT_RESET_MESSAGE, DOCUMENTATION_SOURCE_TYPES, LOADING_TRANSITION_DURATION, MESSAGE_MODEL_ROLES };
26
+ export { CHAT_CLEAN_MESSAGE, CHAT_CLEAR_MESSAGE, CHAT_INCLUDE_MESSAGE, CHAT_RESET_MESSAGE, DOCUMENTATION_SOURCE_TYPES, LOADING_TRANSITION_DURATION, MESSAGE_MODEL_ROLES };
@@ -14,7 +14,8 @@ import { SafeHtmlDirective } from '../../../../directives/safe_html/safe_html';
14
14
  import GlDuoChatLoader from './components/duo_chat_loader/duo_chat_loader';
15
15
  import GlDuoChatPredefinedPrompts from './components/duo_chat_predefined_prompts/duo_chat_predefined_prompts';
16
16
  import GlDuoChatConversation from './components/duo_chat_conversation/duo_chat_conversation';
17
- import { CHAT_RESET_MESSAGE, CHAT_CLEAN_MESSAGE, CHAT_CLEAR_MESSAGE } from './constants';
17
+ import { CHAT_RESET_MESSAGE, CHAT_INCLUDE_MESSAGE, MESSAGE_MODEL_ROLES, CHAT_CLEAN_MESSAGE, CHAT_CLEAR_MESSAGE } from './constants';
18
+ import { INCLUDE_SLASH_COMMAND } from './mock_data';
18
19
  import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
19
20
 
20
21
  const i18n = {
@@ -205,7 +206,9 @@ var script = {
205
206
  scrolledToBottom: true,
206
207
  activeCommandIndex: 0,
207
208
  displaySubmitButton: true,
208
- compositionJustEnded: false
209
+ compositionJustEnded: false,
210
+ contextItemsMenuIsOpen: false,
211
+ contextItemMenuRef: null
209
212
  };
210
213
  },
211
214
  computed: {
@@ -231,6 +234,9 @@ var script = {
231
234
  var _this$messages2;
232
235
  return (_this$messages2 = this.messages) === null || _this$messages2 === void 0 ? void 0 : _this$messages2[this.messages.length - 1];
233
236
  },
237
+ caseInsensitivePrompt() {
238
+ return this.prompt.toLowerCase().trim();
239
+ },
234
240
  resetDisabled() {
235
241
  var _this$lastMessage;
236
242
  if (this.isLoading || !this.hasMessages) {
@@ -246,21 +252,38 @@ var script = {
246
252
  return Boolean(((_this$lastMessage3 = this.lastMessage) === null || _this$lastMessage3 === void 0 ? void 0 : (_this$lastMessage3$ch = _this$lastMessage3.chunks) === null || _this$lastMessage3$ch === void 0 ? void 0 : _this$lastMessage3$ch.length) > 0 && !((_this$lastMessage4 = this.lastMessage) !== null && _this$lastMessage4 !== void 0 && _this$lastMessage4.content) || typeof ((_this$lastMessage5 = this.lastMessage) === null || _this$lastMessage5 === void 0 ? void 0 : _this$lastMessage5.chunkId) === 'number');
247
253
  },
248
254
  filteredSlashCommands() {
249
- const caseInsensitivePrompt = this.prompt.toLowerCase();
250
- return this.slashCommands.filter(c => c.name.toLowerCase().startsWith(caseInsensitivePrompt));
255
+ return this.slashCommands.filter(c => c.name.toLowerCase().startsWith(this.caseInsensitivePrompt)).filter(c => {
256
+ if (c.name === CHAT_INCLUDE_MESSAGE) {
257
+ return this.hasContextItemSelectionMenu;
258
+ }
259
+ return true;
260
+ });
251
261
  },
252
262
  shouldShowSlashCommands() {
253
- if (!this.withSlashCommands) return false;
254
- const caseInsensitivePrompt = this.prompt.toLowerCase();
255
- const startsWithSlash = caseInsensitivePrompt.startsWith('/');
256
- const startsWithSlashCommand = this.slashCommands.some(c => caseInsensitivePrompt.startsWith(c.name));
263
+ if (!this.withSlashCommands || this.contextItemsMenuIsOpen) return false;
264
+ const startsWithSlash = this.caseInsensitivePrompt.startsWith('/');
265
+ const startsWithSlashCommand = this.slashCommands.some(c => this.caseInsensitivePrompt.startsWith(c.name));
257
266
  return startsWithSlash && this.filteredSlashCommands.length && !startsWithSlashCommand;
258
267
  },
268
+ shouldShowContextItemSelectionMenu() {
269
+ if (!this.hasContextItemSelectionMenu) {
270
+ return false;
271
+ }
272
+ const isSlash = this.caseInsensitivePrompt === '/';
273
+ if (!this.caseInsensitivePrompt || isSlash) {
274
+ // if user has removed entire command (or whole command except for '/') we should close context item menu and allow slash command menu to show again
275
+ return false;
276
+ }
277
+ return INCLUDE_SLASH_COMMAND.name.startsWith(this.caseInsensitivePrompt);
278
+ },
259
279
  inputPlaceholder() {
260
280
  if (this.chatPromptPlaceholder) {
261
281
  return this.chatPromptPlaceholder;
262
282
  }
263
283
  return this.withSlashCommands ? i18n.CHAT_PROMPT_PLACEHOLDER_WITH_COMMANDS : i18n.CHAT_PROMPT_PLACEHOLDER_DEFAULT;
284
+ },
285
+ hasContextItemSelectionMenu() {
286
+ return Boolean(this.contextItemMenuRef);
264
287
  }
265
288
  },
266
289
  watch: {
@@ -269,12 +292,18 @@ var script = {
269
292
  this.displaySubmitButton = true; // Re-enable submit button when loading stops
270
293
  }
271
294
  this.isHidden = false;
272
- this.scrollToBottom();
273
295
  },
274
296
  isStreaming(newVal) {
275
297
  if (!newVal && !this.isLoading) {
276
298
  this.displaySubmitButton = true; // Re-enable submit button when streaming stops
277
299
  }
300
+ },
301
+ lastMessage(newMessage) {
302
+ if (this.scrolledToBottom || newMessage.role.toLowerCase() === MESSAGE_MODEL_ROLES.user) {
303
+ // only scroll to bottom on new message if the user hasn't explicitly scrolled up to view an earlier message
304
+ // or if the user has just submitted a new message
305
+ this.scrollToBottom();
306
+ }
278
307
  }
279
308
  },
280
309
  created() {
@@ -304,27 +333,32 @@ var script = {
304
333
  this.setPromptAndFocus();
305
334
  },
306
335
  sendChatPrompt() {
307
- if (!this.displaySubmitButton) {
336
+ if (!this.displaySubmitButton || this.contextItemsMenuIsOpen) {
308
337
  return;
309
338
  }
310
339
  if (this.prompt) {
311
- if (this.prompt === CHAT_RESET_MESSAGE && this.resetDisabled) {
340
+ if (this.caseInsensitivePrompt === CHAT_RESET_MESSAGE && this.resetDisabled) {
312
341
  return;
313
342
  }
343
+ if (this.caseInsensitivePrompt.startsWith(CHAT_INCLUDE_MESSAGE) && this.hasContextItemSelectionMenu) {
344
+ this.contextItemsMenuIsOpen = true;
345
+ return;
346
+ }
347
+ if (![CHAT_RESET_MESSAGE, CHAT_CLEAN_MESSAGE, CHAT_CLEAR_MESSAGE].includes(this.caseInsensitivePrompt)) {
348
+ this.displaySubmitButton = false;
349
+ }
350
+
314
351
  /**
315
352
  * Emitted when a new user prompt should be sent out.
316
353
  *
317
354
  * @param {String} prompt The user prompt to send.
318
355
  */
319
-
320
- if (![CHAT_RESET_MESSAGE, CHAT_CLEAN_MESSAGE, CHAT_CLEAR_MESSAGE].includes(this.prompt)) {
321
- this.displaySubmitButton = false;
322
- }
323
356
  this.$emit('send-chat-prompt', this.prompt.trim());
324
357
  this.setPromptAndFocus();
325
358
  }
326
359
  },
327
360
  sendPredefinedPrompt(prompt) {
361
+ this.contextItemsMenuIsOpen = false;
328
362
  this.prompt = prompt;
329
363
  this.sendChatPrompt();
330
364
  },
@@ -338,8 +372,6 @@ var script = {
338
372
  },
339
373
  async scrollToBottom() {
340
374
  var _this$$refs$anchor, _this$$refs$anchor$sc;
341
- // This method is also called directly by consumers of this component
342
- // https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/3269f6200dc3821c62a3992b40c971dd9ee55d87/webviews/vue2/gitlab_duo_chat/src/App.vue#L97
343
375
  await this.$nextTick();
344
376
  (_this$$refs$anchor = this.$refs.anchor) === null || _this$$refs$anchor === void 0 ? void 0 : (_this$$refs$anchor$sc = _this$$refs$anchor.scrollIntoView) === null || _this$$refs$anchor$sc === void 0 ? void 0 : _this$$refs$anchor$sc.call(_this$$refs$anchor);
345
377
  },
@@ -371,6 +403,18 @@ var script = {
371
403
  const {
372
404
  key
373
405
  } = e;
406
+ if (this.contextItemsMenuIsOpen) {
407
+ var _this$contextItemMenu;
408
+ if (!this.shouldShowContextItemSelectionMenu) {
409
+ this.contextItemsMenuIsOpen = false;
410
+ }
411
+ (_this$contextItemMenu = this.contextItemMenuRef) === null || _this$contextItemMenu === void 0 ? void 0 : _this$contextItemMenu.handleKeyUp(e);
412
+ return;
413
+ }
414
+ if (this.caseInsensitivePrompt === INCLUDE_SLASH_COMMAND.name) {
415
+ this.contextItemsMenuIsOpen = true;
416
+ return;
417
+ }
374
418
  if (this.shouldShowSlashCommands) {
375
419
  e.preventDefault();
376
420
  if (key === 'Enter') {
@@ -416,6 +460,9 @@ var script = {
416
460
  this.sendChatPrompt();
417
461
  } else {
418
462
  this.setPromptAndFocus(`${command.name} `);
463
+ if (command.name === CHAT_INCLUDE_MESSAGE && this.hasContextItemSelectionMenu) {
464
+ this.contextItemsMenuIsOpen = true;
465
+ }
419
466
  }
420
467
  },
421
468
  onInsertCodeSnippet(e) {
@@ -424,6 +471,13 @@ var script = {
424
471
  * @param {*} event An event containing code string in the "detail.code" field.
425
472
  */
426
473
  this.$emit('insert-code-snippet', e);
474
+ },
475
+ closeContextItemsMenuOpen() {
476
+ this.contextItemsMenuIsOpen = false;
477
+ this.setPromptAndFocus();
478
+ },
479
+ setContextItemsMenuRef(ref) {
480
+ this.contextItemMenuRef = ref;
427
481
  }
428
482
  },
429
483
  i18n,
@@ -438,7 +492,7 @@ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=
438
492
  {
439
493
  'gl-h-full': !_vm.hasMessages,
440
494
  'force-scroll-bar': _vm.hasMessages,
441
- } ],attrs:{"tag":"section","name":"message"}},[_vm._l((_vm.conversations),function(conversation,index){return _c('gl-duo-chat-conversation',{key:("conversation-" + index),attrs:{"enable-code-insertion":_vm.enableCodeInsertion,"messages":conversation,"canceled-request-ids":_vm.canceledRequestIds,"show-delimiter":index > 0},on:{"track-feedback":_vm.onTrackFeedback,"insert-code-snippet":_vm.onInsertCodeSnippet}})}),_vm._v(" "),(!_vm.hasMessages && !_vm.isLoading)?[_c('gl-empty-state',{key:"empty-state",staticClass:"gl-flex-grow gl-justify-center",attrs:{"svg-path":_vm.$options.emptySvg,"svg-height":145,"title":_vm.emptyStateTitle},scopedSlots:_vm._u([{key:"description",fn:function(){return [_c('p',{staticClass:"gl-mb-3",attrs:{"data-testid":"gl-duo-chat-empty-state-description"}},[_vm._v("\n "+_vm._s(_vm.emptyStateDescription)+"\n ")]),_vm._v(" "),_c('p',{staticClass:"gl-mt-3 gl-text-sm gl-text-subtle",attrs:{"data-testid":"gl-duo-chat-empty-state-secondary-description"}},[_vm._v("\n "+_vm._s(_vm.emptyStateSecondaryDescription)+"\n ")])]},proxy:true}],null,false,460840487)}),_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:"duo-chat-drawer-footer duo-chat-drawer-footer-sticky gl-border-t gl-bg-gray-10 gl-p-5",class:{ 'duo-chat-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 [(_vm.displaySubmitButton)?_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}}):_c('gl-button',{staticClass:"!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-base",attrs:{"icon":"stop","category":"primary","variant":"default","data-testid":"chat-prompt-cancel-button","aria-label":_vm.$options.i18n.CHAT_CANCEL_LABEL},on:{"click":_vm.cancelPrompt}})]},proxy:true}],null,false,677611116)},[_c('div',{staticClass:"duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-rounded-base gl-bg-white gl-align-top gl-shadow-inner-1-gray-400",attrs:{"data-value":_vm.prompt}},[(_vm.shouldShowSlashCommands)?_c('gl-card',{ref:"commands",staticClass:"slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 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-flex gl-justify-between"},[_c('span',{staticClass:"gl-block"},[_vm._v(_vm._s(command.name))]),_vm._v(" "),_c('small',{staticClass:"gl-pl-3 gl-text-right gl-italic gl-text-gray-500"},[_vm._v(_vm._s(command.description))])])])}),1):_vm._e(),_vm._v(" "),_c('gl-form-textarea',{ref:"prompt",staticClass:"gl-absolute !gl-h-full gl-rounded-br-none gl-rounded-tr-none !gl-bg-transparent !gl-py-4 !gl-shadow-none",class:{ 'gl-truncate': !_vm.prompt },attrs:{"data-testid":"chat-prompt-input","placeholder":_vm.inputPlaceholder,"autofocus":""},on:{"compositionend":_vm.compositionEnd},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('p',{staticClass:"gl-mb-0 gl-mt-3 gl-text-sm gl-leading-20 gl-text-subtle",attrs:{"data-testid":"chat-legal-disclaimer"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.CHAT_LEGAL_DISCLAIMER)+"\n ")])],1)],1):_vm._e()]):_vm._e()};
495
+ } ],attrs:{"tag":"section","name":"message"}},[_vm._l((_vm.conversations),function(conversation,index){return _c('gl-duo-chat-conversation',{key:("conversation-" + index),attrs:{"enable-code-insertion":_vm.enableCodeInsertion,"messages":conversation,"canceled-request-ids":_vm.canceledRequestIds,"show-delimiter":index > 0},on:{"track-feedback":_vm.onTrackFeedback,"insert-code-snippet":_vm.onInsertCodeSnippet}})}),_vm._v(" "),(!_vm.hasMessages && !_vm.isLoading)?[_c('gl-empty-state',{key:"empty-state",staticClass:"gl-flex-grow gl-justify-center",attrs:{"svg-path":_vm.$options.emptySvg,"svg-height":145,"title":_vm.emptyStateTitle},scopedSlots:_vm._u([{key:"description",fn:function(){return [_c('p',{staticClass:"gl-mb-3",attrs:{"data-testid":"gl-duo-chat-empty-state-description"}},[_vm._v("\n "+_vm._s(_vm.emptyStateDescription)+"\n ")]),_vm._v(" "),_c('p',{staticClass:"gl-mt-3 gl-text-sm gl-text-subtle",attrs:{"data-testid":"gl-duo-chat-empty-state-secondary-description"}},[_vm._v("\n "+_vm._s(_vm.emptyStateSecondaryDescription)+"\n ")])]},proxy:true}],null,false,460840487)}),_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:"duo-chat-drawer-footer duo-chat-drawer-footer-sticky gl-border-t gl-bg-gray-10 gl-p-5",class:{ 'duo-chat-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('div',{staticClass:"gl-relative gl-max-w-full"},[_vm._t("context-items-menu",null,{"isOpen":_vm.contextItemsMenuIsOpen,"onClose":_vm.closeContextItemsMenuOpen,"setRef":_vm.setContextItemsMenuRef,"focusPrompt":_vm.focusChatInput})],2),_vm._v(" "),_c('gl-form-input-group',{scopedSlots:_vm._u([{key:"append",fn:function(){return [(_vm.displaySubmitButton)?_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}}):_c('gl-button',{staticClass:"!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-base",attrs:{"icon":"stop","category":"primary","variant":"default","data-testid":"chat-prompt-cancel-button","aria-label":_vm.$options.i18n.CHAT_CANCEL_LABEL},on:{"click":_vm.cancelPrompt}})]},proxy:true}],null,false,677611116)},[_c('div',{staticClass:"duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-rounded-base gl-bg-white gl-align-top gl-shadow-inner-1-gray-400",attrs:{"data-value":_vm.prompt}},[(_vm.shouldShowSlashCommands)?_c('gl-card',{ref:"commands",staticClass:"slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 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-flex gl-justify-between"},[_c('span',{staticClass:"gl-block"},[_vm._v(_vm._s(command.name))]),_vm._v(" "),_c('small',{staticClass:"gl-pl-3 gl-text-right gl-italic gl-text-gray-500"},[_vm._v(_vm._s(command.description))])])])}),1):_vm._e(),_vm._v(" "),_c('gl-form-textarea',{ref:"prompt",staticClass:"gl-absolute !gl-h-full gl-rounded-br-none gl-rounded-tr-none !gl-bg-transparent !gl-py-4 !gl-shadow-none",class:{ 'gl-truncate': !_vm.prompt },attrs:{"data-testid":"chat-prompt-input","placeholder":_vm.inputPlaceholder,"autofocus":""},on:{"compositionend":_vm.compositionEnd},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('p',{staticClass:"gl-mb-0 gl-mt-3 gl-text-sm gl-leading-20 gl-text-subtle",attrs:{"data-testid":"chat-legal-disclaimer"}},[_vm._v("\n "+_vm._s(_vm.$options.i18n.CHAT_LEGAL_DISCLAIMER)+"\n ")])],1)],1):_vm._e()]):_vm._e()};
442
496
  var __vue_staticRenderFns__ = [];
443
497
 
444
498
  /* style */
@@ -1,5 +1,5 @@
1
1
  import { setStoryTimeout } from '../../../../utils/test_utils';
2
- import { DOCUMENTATION_SOURCE_TYPES, MESSAGE_MODEL_ROLES, CHAT_RESET_MESSAGE, CHAT_CLEAN_MESSAGE } from './constants';
2
+ import { DOCUMENTATION_SOURCE_TYPES, MESSAGE_MODEL_ROLES, CHAT_RESET_MESSAGE, CHAT_CLEAN_MESSAGE, CHAT_INCLUDE_MESSAGE } from './constants';
3
3
  import { getMockContextItems } from './components/duo_chat_context/mock_context_data';
4
4
 
5
5
  const MOCK_SOURCES = [{
@@ -154,5 +154,9 @@ const SLASH_COMMANDS = [{
154
154
  name: '/explain',
155
155
  description: 'Explain the selected snippet.'
156
156
  }];
157
+ const INCLUDE_SLASH_COMMAND = {
158
+ name: CHAT_INCLUDE_MESSAGE,
159
+ description: 'Include additional context in the conversation.'
160
+ };
157
161
 
158
- export { MOCK_RESPONSE_MESSAGE, MOCK_RESPONSE_MESSAGE_FOR_STREAMING, MOCK_USER_PROMPT_MESSAGE, SLASH_COMMANDS, generateMockResponseChunks, generateSeparateChunks, renderGFM, renderMarkdown };
162
+ export { INCLUDE_SLASH_COMMAND, MOCK_RESPONSE_MESSAGE, MOCK_RESPONSE_MESSAGE_FOR_STREAMING, MOCK_USER_PROMPT_MESSAGE, SLASH_COMMANDS, generateMockResponseChunks, generateSeparateChunks, renderGFM, renderMarkdown };
package/dist/index.js CHANGED
@@ -89,6 +89,7 @@ export { default as GlAccordionItem } from './components/base/accordion/accordio
89
89
  export { default as GlExperimentBadge } from './components/experimental/experiment_badge/experiment_badge';
90
90
  export { default as GlDuoUserFeedback } from './components/experimental/duo/user_feedback/user_feedback';
91
91
  export { default as GlDuoChat } from './components/experimental/duo/chat/duo_chat';
92
+ export { default as GlDuoChatContextItemMenu } from './components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu';
92
93
  export { default as GlAnimatedNumber } from './components/utilities/animated_number/animated_number';
93
94
  export { default as GlFriendlyWrap } from './components/utilities/friendly_wrap/friendly_wrap';
94
95
  export { default as GlIntersperse } from './components/utilities/intersperse/intersperse';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitlab/ui",
3
- "version": "92.0.0",
3
+ "version": "92.1.1",
4
4
  "description": "GitLab UI Components",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -183,6 +183,11 @@ export default {
183
183
  }
184
184
 
185
185
  this.selectedCategory = null;
186
+
187
+ /**
188
+ * Emitted when the parent GlDuoChat component should refocus on the main prompt input
189
+ */
190
+ this.$emit('focus-prompt');
186
191
  break;
187
192
  default:
188
193
  break;
@@ -88,6 +88,7 @@ export default {
88
88
  <button
89
89
  class="gl-flex gl-w-full gl-items-center gl-border-0 gl-bg-transparent gl-p-0 gl-text-left gl-text-xs gl-lowercase gl-text-gray-500"
90
90
  data-testid="chat-context-selections-title"
91
+ type="button"
91
92
  @click="toggleCollapse"
92
93
  >
93
94
  <gl-icon :name="collapseIconName" data-testid="chat-context-collapse-icon" /> {{ title }}
@@ -1,6 +1,7 @@
1
1
  export const CHAT_RESET_MESSAGE = '/reset';
2
2
  export const CHAT_CLEAN_MESSAGE = '/clean';
3
3
  export const CHAT_CLEAR_MESSAGE = '/clear';
4
+ export const CHAT_INCLUDE_MESSAGE = '/include';
4
5
 
5
6
  export const LOADING_TRANSITION_DURATION = 7500;
6
7
 
@@ -15,7 +15,14 @@ import { SafeHtmlDirective as SafeHtml } from '../../../../directives/safe_html/
15
15
  import GlDuoChatLoader from './components/duo_chat_loader/duo_chat_loader.vue';
16
16
  import GlDuoChatPredefinedPrompts from './components/duo_chat_predefined_prompts/duo_chat_predefined_prompts.vue';
17
17
  import GlDuoChatConversation from './components/duo_chat_conversation/duo_chat_conversation.vue';
18
- import { CHAT_CLEAN_MESSAGE, CHAT_RESET_MESSAGE, CHAT_CLEAR_MESSAGE } from './constants';
18
+ import {
19
+ CHAT_CLEAN_MESSAGE,
20
+ CHAT_RESET_MESSAGE,
21
+ CHAT_CLEAR_MESSAGE,
22
+ CHAT_INCLUDE_MESSAGE,
23
+ MESSAGE_MODEL_ROLES,
24
+ } from './constants';
25
+ import { INCLUDE_SLASH_COMMAND } from './mock_data';
19
26
 
20
27
  export const i18n = {
21
28
  CHAT_DEFAULT_TITLE: 'GitLab Duo Chat',
@@ -214,6 +221,8 @@ export default {
214
221
  activeCommandIndex: 0,
215
222
  displaySubmitButton: true,
216
223
  compositionJustEnded: false,
224
+ contextItemsMenuIsOpen: false,
225
+ contextItemMenuRef: null,
217
226
  };
218
227
  },
219
228
  computed: {
@@ -241,6 +250,9 @@ export default {
241
250
  lastMessage() {
242
251
  return this.messages?.[this.messages.length - 1];
243
252
  },
253
+ caseInsensitivePrompt() {
254
+ return this.prompt.toLowerCase().trim();
255
+ },
244
256
  resetDisabled() {
245
257
  if (this.isLoading || !this.hasMessages) {
246
258
  return true;
@@ -257,20 +269,36 @@ export default {
257
269
  );
258
270
  },
259
271
  filteredSlashCommands() {
260
- const caseInsensitivePrompt = this.prompt.toLowerCase();
261
- return this.slashCommands.filter((c) =>
262
- c.name.toLowerCase().startsWith(caseInsensitivePrompt)
263
- );
272
+ return this.slashCommands
273
+ .filter((c) => c.name.toLowerCase().startsWith(this.caseInsensitivePrompt))
274
+ .filter((c) => {
275
+ if (c.name === CHAT_INCLUDE_MESSAGE) {
276
+ return this.hasContextItemSelectionMenu;
277
+ }
278
+ return true;
279
+ });
264
280
  },
265
281
  shouldShowSlashCommands() {
266
- if (!this.withSlashCommands) return false;
267
- const caseInsensitivePrompt = this.prompt.toLowerCase();
268
- const startsWithSlash = caseInsensitivePrompt.startsWith('/');
282
+ if (!this.withSlashCommands || this.contextItemsMenuIsOpen) return false;
283
+ const startsWithSlash = this.caseInsensitivePrompt.startsWith('/');
269
284
  const startsWithSlashCommand = this.slashCommands.some((c) =>
270
- caseInsensitivePrompt.startsWith(c.name)
285
+ this.caseInsensitivePrompt.startsWith(c.name)
271
286
  );
272
287
  return startsWithSlash && this.filteredSlashCommands.length && !startsWithSlashCommand;
273
288
  },
289
+ shouldShowContextItemSelectionMenu() {
290
+ if (!this.hasContextItemSelectionMenu) {
291
+ return false;
292
+ }
293
+
294
+ const isSlash = this.caseInsensitivePrompt === '/';
295
+ if (!this.caseInsensitivePrompt || isSlash) {
296
+ // if user has removed entire command (or whole command except for '/') we should close context item menu and allow slash command menu to show again
297
+ return false;
298
+ }
299
+
300
+ return INCLUDE_SLASH_COMMAND.name.startsWith(this.caseInsensitivePrompt);
301
+ },
274
302
  inputPlaceholder() {
275
303
  if (this.chatPromptPlaceholder) {
276
304
  return this.chatPromptPlaceholder;
@@ -280,6 +308,9 @@ export default {
280
308
  ? i18n.CHAT_PROMPT_PLACEHOLDER_WITH_COMMANDS
281
309
  : i18n.CHAT_PROMPT_PLACEHOLDER_DEFAULT;
282
310
  },
311
+ hasContextItemSelectionMenu() {
312
+ return Boolean(this.contextItemMenuRef);
313
+ },
283
314
  },
284
315
  watch: {
285
316
  isLoading(newVal) {
@@ -287,13 +318,19 @@ export default {
287
318
  this.displaySubmitButton = true; // Re-enable submit button when loading stops
288
319
  }
289
320
  this.isHidden = false;
290
- this.scrollToBottom();
291
321
  },
292
322
  isStreaming(newVal) {
293
323
  if (!newVal && !this.isLoading) {
294
324
  this.displaySubmitButton = true; // Re-enable submit button when streaming stops
295
325
  }
296
326
  },
327
+ lastMessage(newMessage) {
328
+ if (this.scrolledToBottom || newMessage.role.toLowerCase() === MESSAGE_MODEL_ROLES.user) {
329
+ // only scroll to bottom on new message if the user hasn't explicitly scrolled up to view an earlier message
330
+ // or if the user has just submitted a new message
331
+ this.scrollToBottom();
332
+ }
333
+ },
297
334
  },
298
335
  created() {
299
336
  this.handleScrollingTrottled = throttle(this.handleScrolling, 200); // Assume a 200ms throttle for example
@@ -322,28 +359,42 @@ export default {
322
359
  this.setPromptAndFocus();
323
360
  },
324
361
  sendChatPrompt() {
325
- if (!this.displaySubmitButton) {
362
+ if (!this.displaySubmitButton || this.contextItemsMenuIsOpen) {
326
363
  return;
327
364
  }
328
365
  if (this.prompt) {
329
- if (this.prompt === CHAT_RESET_MESSAGE && this.resetDisabled) {
366
+ if (this.caseInsensitivePrompt === CHAT_RESET_MESSAGE && this.resetDisabled) {
330
367
  return;
331
368
  }
369
+
370
+ if (
371
+ this.caseInsensitivePrompt.startsWith(CHAT_INCLUDE_MESSAGE) &&
372
+ this.hasContextItemSelectionMenu
373
+ ) {
374
+ this.contextItemsMenuIsOpen = true;
375
+ return;
376
+ }
377
+
378
+ if (
379
+ ![CHAT_RESET_MESSAGE, CHAT_CLEAN_MESSAGE, CHAT_CLEAR_MESSAGE].includes(
380
+ this.caseInsensitivePrompt
381
+ )
382
+ ) {
383
+ this.displaySubmitButton = false;
384
+ }
385
+
332
386
  /**
333
387
  * Emitted when a new user prompt should be sent out.
334
388
  *
335
389
  * @param {String} prompt The user prompt to send.
336
390
  */
337
-
338
- if (![CHAT_RESET_MESSAGE, CHAT_CLEAN_MESSAGE, CHAT_CLEAR_MESSAGE].includes(this.prompt)) {
339
- this.displaySubmitButton = false;
340
- }
341
391
  this.$emit('send-chat-prompt', this.prompt.trim());
342
392
 
343
393
  this.setPromptAndFocus();
344
394
  }
345
395
  },
346
396
  sendPredefinedPrompt(prompt) {
397
+ this.contextItemsMenuIsOpen = false;
347
398
  this.prompt = prompt;
348
399
  this.sendChatPrompt();
349
400
  },
@@ -352,8 +403,6 @@ export default {
352
403
  this.scrolledToBottom = scrollTop + offsetHeight >= scrollHeight;
353
404
  },
354
405
  async scrollToBottom() {
355
- // This method is also called directly by consumers of this component
356
- // https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/3269f6200dc3821c62a3992b40c971dd9ee55d87/webviews/vue2/gitlab_duo_chat/src/App.vue#L97
357
406
  await this.$nextTick();
358
407
 
359
408
  this.$refs.anchor?.scrollIntoView?.();
@@ -379,6 +428,18 @@ export default {
379
428
  onInputKeyup(e) {
380
429
  const { key } = e;
381
430
 
431
+ if (this.contextItemsMenuIsOpen) {
432
+ if (!this.shouldShowContextItemSelectionMenu) {
433
+ this.contextItemsMenuIsOpen = false;
434
+ }
435
+ this.contextItemMenuRef?.handleKeyUp(e);
436
+ return;
437
+ }
438
+ if (this.caseInsensitivePrompt === INCLUDE_SLASH_COMMAND.name) {
439
+ this.contextItemsMenuIsOpen = true;
440
+ return;
441
+ }
442
+
382
443
  if (this.shouldShowSlashCommands) {
383
444
  e.preventDefault();
384
445
 
@@ -426,6 +487,10 @@ export default {
426
487
  this.sendChatPrompt();
427
488
  } else {
428
489
  this.setPromptAndFocus(`${command.name} `);
490
+
491
+ if (command.name === CHAT_INCLUDE_MESSAGE && this.hasContextItemSelectionMenu) {
492
+ this.contextItemsMenuIsOpen = true;
493
+ }
429
494
  }
430
495
  },
431
496
  onInsertCodeSnippet(e) {
@@ -435,6 +500,13 @@ export default {
435
500
  */
436
501
  this.$emit('insert-code-snippet', e);
437
502
  },
503
+ closeContextItemsMenuOpen() {
504
+ this.contextItemsMenuIsOpen = false;
505
+ this.setPromptAndFocus();
506
+ },
507
+ setContextItemsMenuRef(ref) {
508
+ this.contextItemMenuRef = ref;
509
+ },
438
510
  },
439
511
  i18n,
440
512
  emptySvg,
@@ -561,6 +633,19 @@ export default {
561
633
  :class="{ 'duo-chat-drawer-body-scrim-on-footer': !scrolledToBottom }"
562
634
  >
563
635
  <gl-form data-testid="chat-prompt-form" @submit.stop.prevent="sendChatPrompt">
636
+ <div class="gl-relative gl-max-w-full">
637
+ <!--
638
+ @slot For integrating `<gl-context-items-menu>` component if pinned-context should be available. The following scopedSlot properties are provided: `isOpen`, `onClose`, `setRef`, `focusPrompt`, which should be passed to the `<gl-context-items-menu>` component when rendering, e.g. `<template #context-items-menu="{ isOpen, onClose, setRef, focusPrompt }">` `<gl-duo-chat-context-item-menu :ref="setRef" :open="isOpen" @close="onClose" @focus-prompt="focusPrompt" ...`
639
+ -->
640
+ <slot
641
+ name="context-items-menu"
642
+ :is-open="contextItemsMenuIsOpen"
643
+ :on-close="closeContextItemsMenuOpen"
644
+ :set-ref="setContextItemsMenuRef"
645
+ :focus-prompt="focusChatInput"
646
+ ></slot>
647
+ </div>
648
+
564
649
  <gl-form-input-group>
565
650
  <div
566
651
  class="duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-rounded-base gl-bg-white gl-align-top gl-shadow-inner-1-gray-400"
@@ -4,6 +4,7 @@ import {
4
4
  MESSAGE_MODEL_ROLES,
5
5
  CHAT_RESET_MESSAGE,
6
6
  CHAT_CLEAN_MESSAGE,
7
+ CHAT_INCLUDE_MESSAGE,
7
8
  } from './constants';
8
9
  import { getMockContextItems } from './components/duo_chat_context/mock_context_data';
9
10
 
@@ -170,3 +171,8 @@ export const SLASH_COMMANDS = [
170
171
  description: 'Explain the selected snippet.',
171
172
  },
172
173
  ];
174
+
175
+ export const INCLUDE_SLASH_COMMAND = {
176
+ name: CHAT_INCLUDE_MESSAGE,
177
+ description: 'Include additional context in the conversation.',
178
+ };
package/src/index.js CHANGED
@@ -100,6 +100,7 @@ export { default as GlAccordionItem } from './components/base/accordion/accordio
100
100
  export { default as GlExperimentBadge } from './components/experimental/experiment_badge/experiment_badge.vue';
101
101
  export { default as GlDuoUserFeedback } from './components/experimental/duo/user_feedback/user_feedback.vue';
102
102
  export { default as GlDuoChat } from './components/experimental/duo/chat/duo_chat.vue';
103
+ export { default as GlDuoChatContextItemMenu } from './components/experimental/duo/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.vue';
103
104
 
104
105
  // Utilities
105
106
  export { default as GlAnimatedNumber } from './components/utilities/animated_number/animated_number.vue';