@gitlab/duo-ui 10.20.0 → 10.22.0

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/components/agentic_chat/agentic_duo_chat.js +18 -37
  3. package/dist/components/chat/components/duo_chat_header/duo_chat_header.js +4 -4
  4. package/dist/components/chat/components/duo_chat_message_tool_approval/components/create_commit_tool_params.js +148 -0
  5. package/dist/components/chat/components/duo_chat_message_tool_approval/components/create_issue_tool_params.js +88 -0
  6. package/dist/components/chat/components/duo_chat_message_tool_approval/components/create_merge_request_tool_params.js +83 -0
  7. package/dist/components/chat/components/duo_chat_message_tool_approval/components/pre_block.js +38 -0
  8. package/dist/components/chat/components/duo_chat_message_tool_approval/components/run_command_tool_params.js +62 -0
  9. package/dist/components/chat/components/duo_chat_message_tool_approval/message_tool_approval.js +46 -8
  10. package/dist/components/chat/duo_chat.js +35 -54
  11. package/dist/components/chat/mock_data.js +85 -1
  12. package/dist/components/ui/duo_layout/duo_layout.js +100 -0
  13. package/dist/components/ui/side_rail/side_rail.js +67 -0
  14. package/dist/components.css +1 -1
  15. package/dist/components.css.map +1 -1
  16. package/dist/index.js +2 -0
  17. package/dist/tailwind.css +1 -1
  18. package/dist/tailwind.css.map +1 -1
  19. package/dist/utils/object.js +9 -0
  20. package/package.json +5 -4
  21. package/src/components/agentic_chat/agentic_duo_chat.vue +210 -244
  22. package/src/components/chat/components/duo_chat_header/duo_chat_header.vue +24 -22
  23. package/src/components/chat/components/duo_chat_message_tool_approval/components/create_commit_tool_params.vue +155 -0
  24. package/src/components/chat/components/duo_chat_message_tool_approval/components/create_issue_tool_params.vue +80 -0
  25. package/src/components/chat/components/duo_chat_message_tool_approval/components/create_merge_request_tool_params.vue +74 -0
  26. package/src/components/chat/components/duo_chat_message_tool_approval/components/pre_block.vue +5 -0
  27. package/src/components/chat/components/duo_chat_message_tool_approval/components/run_command_tool_params.vue +30 -0
  28. package/src/components/chat/components/duo_chat_message_tool_approval/message_tool_approval.vue +143 -88
  29. package/src/components/chat/duo_chat.scss +1 -2
  30. package/src/components/chat/duo_chat.vue +214 -238
  31. package/src/components/chat/mock_data.js +99 -0
  32. package/src/components/ui/duo_layout/duo_layout.md +0 -0
  33. package/src/components/ui/duo_layout/duo_layout.vue +95 -0
  34. package/src/components/ui/side_rail/side_rail.vue +56 -0
  35. package/src/index.js +2 -0
  36. package/src/utils/object.js +4 -0
  37. package/translations.js +29 -6
@@ -1,16 +1,12 @@
1
1
  <script>
2
2
  import throttle from 'lodash/throttle';
3
- import VueResizable from 'vue-resizable';
4
3
 
5
4
  import {
6
5
  GlButton,
7
6
  GlDropdownItem,
8
7
  GlCard,
9
- GlAlert,
10
- GlFormInputGroup,
11
8
  GlFormTextarea,
12
9
  GlForm,
13
- GlExperimentBadge,
14
10
  GlSafeHtmlDirective as SafeHtml,
15
11
  } from '@gitlab/ui';
16
12
 
@@ -24,6 +20,8 @@ import {
24
20
  CHAT_NEW_MESSAGE,
25
21
  CHAT_INCLUDE_MESSAGE,
26
22
  MESSAGE_MODEL_ROLES,
23
+ MAX_PROMPT_LENGTH,
24
+ PROMPT_LENGTH_WARNING,
27
25
  } from './constants';
28
26
  import { VIEW_TYPES } from './components/duo_chat_header/constants';
29
27
  import DuoChatLoader from './components/duo_chat_loader/duo_chat_loader.vue';
@@ -45,7 +43,7 @@ export const i18n = {
45
43
  ),
46
44
  CHAT_PROMPT_PLACEHOLDER_DEFAULT: translate(
47
45
  'DuoChat.chatPromptPlaceholderDefault',
48
- 'GitLab Duo Chat'
46
+ "Let's work through this together..."
49
47
  ),
50
48
  CHAT_PROMPT_PLACEHOLDER_WITH_COMMANDS: translate(
51
49
  'DuoChat.chatPromptPlaceholderWithCommands',
@@ -53,6 +51,7 @@ export const i18n = {
53
51
  ),
54
52
  CHAT_SUBMIT_LABEL: translate('DuoChat.chatSubmitLabel', 'Send chat message.'),
55
53
  CHAT_CANCEL_LABEL: translate('DuoChat.chatCancelLabel', 'Cancel'),
54
+ CHAT_MODEL_PLACEHOLDER: translate('DuoChat.chatModelPlaceholder', 'GitLab Duo Chat'),
56
55
  CHAT_DEFAULT_PREDEFINED_PROMPTS: [
57
56
  translate(
58
57
  'DuoChat.chatDefaultPredefinedPromptsChangePassword',
@@ -99,11 +98,8 @@ export default {
99
98
  name: 'DuoChat',
100
99
  components: {
101
100
  GlButton,
102
- GlAlert,
103
- GlFormInputGroup,
104
101
  GlFormTextarea,
105
102
  GlForm,
106
- GlExperimentBadge,
107
103
  DuoChatLoader,
108
104
  DuoChatPredefinedPrompts,
109
105
  DuoChatConversation,
@@ -111,41 +107,11 @@ export default {
111
107
  DuoChatThreads,
112
108
  GlCard,
113
109
  GlDropdownItem,
114
- VueResizable,
115
110
  },
116
111
  directives: {
117
112
  SafeHtml,
118
113
  },
119
114
  props: {
120
- /**
121
- * Determines if the component should be resizable. When true, it renders inside
122
- * a `vue-resizable` wrapper; otherwise, a standard `div` is used.
123
- */
124
- shouldRenderResizable: {
125
- type: Boolean,
126
- required: false,
127
- default: false,
128
- },
129
- /**
130
- * Defines the dimensions of the chat container when resizable.
131
- * By default, the height is set to match the height of the browser window,
132
- * and the width is fixed at 400px. The `top` position is left undefined,
133
- * allowing it to be dynamically adjusted if needed.
134
- */
135
- dimensions: {
136
- type: Object,
137
- required: false,
138
- default: () => ({
139
- width: undefined,
140
- height: undefined,
141
- top: undefined,
142
- left: undefined,
143
- maxWidth: undefined,
144
- minWidth: 400,
145
- maxHeight: undefined,
146
- minHeight: 400,
147
- }),
148
- },
149
115
  /**
150
116
  * The title of the chat/feature.
151
117
  */
@@ -314,10 +280,14 @@ export default {
314
280
  default: () => ['en-US', 'en'],
315
281
  validator: localeValidator,
316
282
  },
283
+ shouldRenderResizable: {
284
+ type: Boolean,
285
+ required: false,
286
+ default: false,
287
+ },
317
288
  },
318
289
  data() {
319
290
  return {
320
- isHidden: false,
321
291
  prompt: '',
322
292
  scrolledToBottom: true,
323
293
  activeCommandIndex: 0,
@@ -326,6 +296,9 @@ export default {
326
296
  contextItemsMenuIsOpen: false,
327
297
  contextItemMenuRef: null,
328
298
  currentView: this.multiThreadedView,
299
+ maxPromptLength: MAX_PROMPT_LENGTH,
300
+ maxPromptLengthWarning: PROMPT_LENGTH_WARNING,
301
+ promptLengthWarningCount: MAX_PROMPT_LENGTH - PROMPT_LENGTH_WARNING,
329
302
  };
330
303
  },
331
304
  computed: {
@@ -434,7 +407,6 @@ export default {
434
407
  if (!newVal && !this.isStreaming) {
435
408
  this.displaySubmitButton = true; // Re-enable submit button when loading stops
436
409
  }
437
- this.isHidden = false;
438
410
  },
439
411
  isStreaming(newVal) {
440
412
  if (!newVal && !this.isLoading) {
@@ -475,14 +447,10 @@ export default {
475
447
  this.focusChatInput();
476
448
  });
477
449
  },
478
- updateSize(e) {
479
- this.$emit('chat-resize', e);
480
- },
481
450
  compositionEnd() {
482
451
  this.compositionJustEnded = true;
483
452
  },
484
453
  hideChat() {
485
- this.isHidden = true;
486
454
  /**
487
455
  * Emitted when clicking the cross in the title and the chat gets closed.
488
456
  */
@@ -545,7 +513,7 @@ export default {
545
513
  focusChatInput() {
546
514
  // This method is also called directly by consumers of this component
547
515
  // https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/dae2d4669ab4da327921492a2962beae8a05c290/webviews/vue2/gitlab_duo_chat/src/App.vue#L109
548
- this.$refs.prompt?.$el?.focus();
516
+ this.$refs.prompt?.$el?.querySelector?.('textarea')?.focus();
549
517
  },
550
518
  onTrackFeedback(event) {
551
519
  /**
@@ -699,214 +667,222 @@ export default {
699
667
  event.preventDefault();
700
668
  document.execCommand('redo');
701
669
  },
670
+ remainingCharacterCountMessage(count) {
671
+ return `${count} characters remaining`;
672
+ },
673
+ overLimitCharacterCountMessage(count) {
674
+ return `${Math.abs(count)} characters over limit`;
675
+ },
702
676
  },
703
677
  i18n,
704
678
  };
705
679
  </script>
706
680
  <template>
707
- <component
708
- :is="shouldRenderResizable ? 'vue-resizable' : 'div'"
709
- :width="shouldRenderResizable ? dimensions.width : null"
710
- :height="shouldRenderResizable ? dimensions.height : null"
711
- :max-width="shouldRenderResizable ? dimensions.maxWidth : null"
712
- :max-height="shouldRenderResizable ? dimensions.maxHeight : null"
713
- :min-width="shouldRenderResizable ? dimensions.minWidth : null"
714
- :left="shouldRenderResizable ? dimensions.left : null"
715
- :top="shouldRenderResizable ? dimensions.top : null"
716
- :fit-parent="true"
717
- :min-height="shouldRenderResizable ? dimensions.minHeight : null"
718
- :class="{
719
- 'duo-chat-resizable': shouldRenderResizable,
720
- 'non-resizable-wrapper': !shouldRenderResizable,
721
- }"
722
- :active="shouldRenderResizable ? ['l', 't', 'lt'] : null"
723
- @resize:end="updateSize"
681
+ <div
682
+ id="chat-component"
683
+ class="markdown-code-block duo-chat gl-bottom-0 gl-flex gl-max-h-full gl-flex-col"
684
+ role="complementary"
685
+ data-testid="chat-component"
724
686
  >
725
- <aside
726
- v-if="!isHidden"
727
- id="chat-component"
728
- :class="{
729
- 'resizable-content': shouldRenderResizable,
730
- 'duo-chat-drawer': !shouldRenderResizable,
731
- }"
732
- class="markdown-code-block duo-chat gl-bottom-0 gl-flex gl-max-h-full gl-flex-col"
733
- role="complementary"
734
- data-testid="chat-component"
687
+ <duo-chat-header
688
+ v-if="showHeader"
689
+ ref="header"
690
+ :active-thread-id="activeThreadId"
691
+ :title="isMultithreaded && currentView === 'list' ? $options.i18n.CHAT_HISTORY_TITLE : title"
692
+ :subtitle="activeThreadTitleForView"
693
+ :is-multithreaded="isMultithreaded"
694
+ :current-view="currentView"
695
+ :should-render-resizable="shouldRenderResizable"
696
+ :badge-type="isMultithreaded ? null : badgeType"
697
+ @go-back="onGoBack"
698
+ @go-back-to-chat="onGoBackToChat"
699
+ @new-chat="onNewChat"
700
+ @close="hideChat"
701
+ >
702
+ <template #subheader>
703
+ <slot name="subheader"></slot>
704
+ </template>
705
+ </duo-chat-header>
706
+
707
+ <div
708
+ class="gl-flex gl-flex-1 gl-flex-grow gl-flex-col gl-overflow-y-auto gl-overscroll-contain gl-bg-inherit"
709
+ data-testid="chat-history"
710
+ @scroll="handleScrollingThrottled"
735
711
  >
736
- <duo-chat-header
737
- v-if="showHeader"
738
- ref="header"
739
- :active-thread-id="activeThreadId"
740
- :title="
741
- isMultithreaded && currentView === 'list' ? $options.i18n.CHAT_HISTORY_TITLE : title
742
- "
743
- :subtitle="activeThreadTitleForView"
744
- :is-multithreaded="isMultithreaded"
745
- :current-view="currentView"
746
- :should-render-resizable="shouldRenderResizable"
747
- :badge-type="isMultithreaded ? null : badgeType"
748
- @go-back="onGoBack"
749
- @go-back-to-chat="onGoBackToChat"
712
+ <duo-chat-threads
713
+ v-if="shouldShowThreadList"
714
+ :threads="threadList"
715
+ :preferred-locale="preferredLocale"
750
716
  @new-chat="onNewChat"
717
+ @select-thread="onSelectThread"
718
+ @delete-thread="onDeleteThread"
751
719
  @close="hideChat"
720
+ />
721
+ <transition-group
722
+ v-else
723
+ mode="out-in"
724
+ tag="section"
725
+ name="message"
726
+ class="duo-chat-history gl-mt-auto gl-p-5"
752
727
  >
753
- <template #subheader>
754
- <slot name="subheader"></slot>
755
- </template>
756
- </duo-chat-header>
757
-
758
- <div
759
- class="gl-flex gl-flex-1 gl-flex-grow gl-flex-col gl-overflow-y-auto gl-overscroll-contain gl-bg-inherit"
760
- data-testid="chat-history"
761
- @scroll="handleScrollingThrottled"
762
- >
763
- <duo-chat-threads
764
- v-if="shouldShowThreadList"
765
- :threads="threadList"
766
- :preferred-locale="preferredLocale"
767
- @new-chat="onNewChat"
768
- @select-thread="onSelectThread"
769
- @delete-thread="onDeleteThread"
770
- @close="hideChat"
728
+ <duo-chat-conversation
729
+ v-for="(conversation, index) in conversations"
730
+ :key="`conversation-${index}`"
731
+ :enable-code-insertion="enableCodeInsertion"
732
+ :messages="conversation"
733
+ :canceled-request-ids="canceledRequestIds"
734
+ :show-delimiter="index > 0"
735
+ :trusted-urls="trustedUrls"
736
+ @track-feedback="onTrackFeedback"
737
+ @insert-code-snippet="onInsertCodeSnippet"
738
+ @copy-code-snippet="onCopyCodeSnippet"
739
+ @copy-message="onCopyMessage"
740
+ @get-context-item-content="onGetContextItemContent"
741
+ @open-file-path="onOpenFilePath"
771
742
  />
772
- <transition-group
773
- v-else
774
- mode="out-in"
775
- tag="section"
776
- name="message"
777
- class="duo-chat-history gl-mt-auto gl-p-5"
778
- >
779
- <duo-chat-conversation
780
- v-for="(conversation, index) in conversations"
781
- :key="`conversation-${index}`"
782
- :enable-code-insertion="enableCodeInsertion"
783
- :messages="conversation"
784
- :canceled-request-ids="canceledRequestIds"
785
- :show-delimiter="index > 0"
786
- :trusted-urls="trustedUrls"
787
- @track-feedback="onTrackFeedback"
788
- @insert-code-snippet="onInsertCodeSnippet"
789
- @copy-code-snippet="onCopyCodeSnippet"
790
- @copy-message="onCopyMessage"
791
- @get-context-item-content="onGetContextItemContent"
792
- @open-file-path="onOpenFilePath"
793
- />
794
- <template v-if="!hasMessages && !isLoading">
795
- <div
796
- key="empty-state-message"
797
- class="duo-chat-message gl-rounded-bl-none gl-p-4 gl-leading-20 gl-text-gray-900 gl-break-anywhere"
798
- data-testid="gl-duo-chat-empty-state"
799
- >
800
- <p v-if="emptyStateTitle" data-testid="gl-duo-chat-empty-state-title" class="gl-m-0">
801
- {{ emptyStateTitle }}
802
- </p>
803
- <duo-chat-predefined-prompts
804
- key="predefined-prompts"
805
- :prompts="predefinedPrompts"
806
- @click="sendPredefinedPrompt"
807
- />
808
- </div>
809
- </template>
810
- <duo-chat-loader v-if="isLoading" key="loader" :tool-name="toolName" />
811
- <div key="anchor" ref="anchor" class="scroll-anchor"></div>
812
- </transition-group>
813
- </div>
814
- <footer
815
- v-if="isChatAvailable && !shouldShowThreadList"
816
- data-testid="chat-footer"
817
- class="duo-chat-drawer-footer gl-relative gl-z-2 gl-shrink-0 gl-border-0 gl-bg-default gl-pb-3"
818
- :class="{ 'duo-chat-drawer-body-scrim-on-footer': !scrolledToBottom }"
819
- >
820
- <gl-form data-testid="chat-prompt-form" @submit.stop.prevent="sendChatPrompt">
821
- <div class="gl-relative gl-max-w-full">
822
- <!--
743
+ <template v-if="!hasMessages && !isLoading">
744
+ <div
745
+ key="empty-state-message"
746
+ class="duo-chat-message gl-rounded-bl-none gl-p-4 gl-leading-20 gl-text-gray-900 gl-break-anywhere"
747
+ data-testid="gl-duo-chat-empty-state"
748
+ >
749
+ <p v-if="emptyStateTitle" data-testid="gl-duo-chat-empty-state-title" class="gl-m-0">
750
+ {{ emptyStateTitle }}
751
+ </p>
752
+ <duo-chat-predefined-prompts
753
+ key="predefined-prompts"
754
+ :prompts="predefinedPrompts"
755
+ @click="sendPredefinedPrompt"
756
+ />
757
+ </div>
758
+ </template>
759
+ <duo-chat-loader v-if="isLoading" key="loader" :tool-name="toolName" />
760
+ <div key="anchor" ref="anchor" class="scroll-anchor"></div>
761
+ </transition-group>
762
+ </div>
763
+ <footer
764
+ v-if="isChatAvailable && !shouldShowThreadList"
765
+ data-testid="chat-footer"
766
+ class="duo-chat-drawer-footer gl-relative gl-z-2 gl-shrink-0 gl-border-0 gl-bg-default gl-pb-3"
767
+ :class="{ 'duo-chat-drawer-body-scrim-on-footer': !scrolledToBottom }"
768
+ >
769
+ <gl-form data-testid="chat-prompt-form" @submit.stop.prevent="sendChatPrompt">
770
+ <div class="gl-relative gl-max-w-full">
771
+ <!--
823
772
  @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 }">` `<duo-chat-context-item-menu :ref="setRef" :open="isOpen" @close="onClose" @focus-prompt="focusPrompt" ...`
824
773
  -->
825
- <slot
826
- name="context-items-menu"
827
- :is-open="contextItemsMenuIsOpen"
828
- :on-close="closeContextItemsMenuOpen"
829
- :set-ref="setContextItemsMenuRef"
830
- :focus-prompt="focusChatInput"
831
- ></slot>
774
+ <slot
775
+ name="context-items-menu"
776
+ :is-open="contextItemsMenuIsOpen"
777
+ :on-close="closeContextItemsMenuOpen"
778
+ :set-ref="setContextItemsMenuRef"
779
+ :focus-prompt="focusChatInput"
780
+ ></slot>
781
+ </div>
782
+
783
+ <div
784
+ class="duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-flex-col gl-rounded-bl-[12px] gl-rounded-br-[18px] gl-rounded-tl-[12px] gl-rounded-tr-[12px] gl-align-top"
785
+ >
786
+ <div
787
+ class="gl-flex gl-justify-between gl-border-0 gl-border-b-1 gl-border-solid gl-border-[#DCDCDE] gl-px-4 gl-py-4"
788
+ >
789
+ <div>{{ $options.i18n.CHAT_MODEL_PLACEHOLDER }}</div>
790
+ <div><slot name="agentic-switch"></slot></div>
832
791
  </div>
833
-
834
- <gl-form-input-group>
835
- <div
836
- class="duo-chat-input gl-min-h-8 gl-max-w-full gl-grow gl-align-top"
837
- :data-value="prompt"
792
+ <div class="gl-h-[40px] gl-grow" :data-value="prompt">
793
+ <gl-card
794
+ v-if="shouldShowSlashCommands"
795
+ ref="commands"
796
+ class="slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 gl-shadow-md"
797
+ body-class="!gl-p-2"
838
798
  >
839
- <gl-card
840
- v-if="shouldShowSlashCommands"
841
- ref="commands"
842
- class="slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 gl-shadow-md"
843
- body-class="!gl-p-2"
799
+ <gl-dropdown-item
800
+ v-for="(command, index) in filteredSlashCommands"
801
+ :key="command.name"
802
+ :class="{ 'active-command': index === activeCommandIndex }"
803
+ @mouseenter.native="activeCommandIndex = index"
804
+ @click="selectSlashCommand(index)"
844
805
  >
845
- <gl-dropdown-item
846
- v-for="(command, index) in filteredSlashCommands"
847
- :key="command.name"
848
- :class="{ 'active-command': index === activeCommandIndex }"
849
- @mouseenter.native="activeCommandIndex = index"
850
- @click="selectSlashCommand(index)"
806
+ <span class="gl-flex gl-justify-between">
807
+ <span class="gl-block">{{ command.name }}</span>
808
+ <small class="gl-pl-3 gl-text-right gl-italic gl-text-subtle">{{
809
+ command.description
810
+ }}</small>
811
+ </span>
812
+ </gl-dropdown-item>
813
+ </gl-card>
814
+
815
+ <gl-form-textarea
816
+ ref="prompt"
817
+ v-model="prompt"
818
+ data-testid="chat-prompt-input"
819
+ :textarea-classes="[
820
+ '!gl-h-full',
821
+ '!gl-bg-transparent',
822
+ '!gl-py-4',
823
+ '!gl-shadow-none',
824
+ 'form-control',
825
+ 'gl-form-input',
826
+ 'gl-form-textarea',
827
+ { 'gl-truncate': !prompt },
828
+ ]"
829
+ :placeholder="inputPlaceholder"
830
+ :character-count-limit="maxPromptLength"
831
+ autofocus
832
+ @keydown.enter.exact.native.prevent
833
+ @keydown.ctrl.z.exact="handleUndo"
834
+ @keydown.meta.z.exact="handleUndo"
835
+ @keydown.ctrl.shift.z="handleRedo"
836
+ @keydown.meta.shift.z="handleRedo"
837
+ @keydown.ctrl.y="handleRedo"
838
+ @keydown.meta.y="handleRedo"
839
+ @keyup.native="onInputKeyup"
840
+ @compositionend="compositionEnd"
841
+ >
842
+ <template #remaining-character-count-text="{ count }">
843
+ <span
844
+ v-if="count <= promptLengthWarningCount"
845
+ class="gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3"
851
846
  >
852
- <span class="gl-flex gl-justify-between">
853
- <span class="gl-block">{{ command.name }}</span>
854
- <small class="gl-pl-3 gl-text-right gl-italic gl-text-subtle">{{
855
- command.description
856
- }}</small>
857
- </span>
858
- </gl-dropdown-item>
859
- </gl-card>
860
-
861
- <gl-form-textarea
862
- ref="prompt"
863
- v-model="prompt"
864
- data-testid="chat-prompt-input"
865
- class="gl-absolute !gl-h-full gl-rounded-br-none gl-rounded-tr-none !gl-bg-transparent !gl-py-4 !gl-shadow-none"
866
- :class="{ 'gl-truncate': !prompt }"
867
- :placeholder="inputPlaceholder"
868
- autofocus
869
- @keydown.enter.exact.native.prevent
870
- @keydown.ctrl.z.exact="handleUndo"
871
- @keydown.meta.z.exact="handleUndo"
872
- @keydown.ctrl.shift.z="handleRedo"
873
- @keydown.meta.shift.z="handleRedo"
874
- @keydown.ctrl.y="handleRedo"
875
- @keydown.meta.y="handleRedo"
876
- @keyup.native="onInputKeyup"
877
- @compositionend="compositionEnd"
878
- />
879
- </div>
880
- <template #append>
881
- <gl-button
882
- v-if="displaySubmitButton"
883
- icon="paper-airplane"
884
- category="primary"
885
- variant="confirm"
886
- class="!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full"
887
- type="submit"
888
- data-testid="chat-prompt-submit-button"
889
- :disabled="isPromptEmpty"
890
- :aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
891
- />
892
- <gl-button
893
- v-else
894
- icon="stop"
895
- category="primary"
896
- variant="default"
897
- class="!gl-absolute gl-bottom-2 gl-right-2 !gl-rounded-full"
898
- data-testid="chat-prompt-cancel-button"
899
- :aria-label="$options.i18n.CHAT_CANCEL_LABEL"
900
- @click="cancelPrompt"
901
- />
902
- </template>
903
- </gl-form-input-group>
904
- </gl-form>
905
- <slot name="footer-controls"></slot>
906
- <p class="gl-mb-0 gl-mt-3 gl-px-4 gl-text-sm gl-text-secondary">
907
- {{ $options.i18n.CHAT_DISCLAMER }}
908
- </p>
909
- </footer>
910
- </aside>
911
- </component>
847
+ {{ remainingCharacterCountMessage(count) }}
848
+ </span>
849
+ </template>
850
+ <template #character-count-over-limit-text="{ count }">
851
+ <span class="gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3">{{
852
+ overLimitCharacterCountMessage(count)
853
+ }}</span>
854
+ </template>
855
+ </gl-form-textarea>
856
+ </div>
857
+ <div class="gl-flex gl-justify-end gl-px-3 gl-pb-3">
858
+ <gl-button
859
+ v-if="displaySubmitButton"
860
+ icon="paper-airplane"
861
+ category="primary"
862
+ variant="confirm"
863
+ class="gl-bottom-2 gl-right-2 gl-ml-auto !gl-rounded-full"
864
+ type="submit"
865
+ data-testid="chat-prompt-submit-button"
866
+ :disabled="isPromptEmpty"
867
+ :aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
868
+ />
869
+ <gl-button
870
+ v-else
871
+ icon="stop"
872
+ category="primary"
873
+ variant="default"
874
+ class="gl-bottom-2 gl-right-2 gl-ml-auto !gl-rounded-full"
875
+ data-testid="chat-prompt-cancel-button"
876
+ :aria-label="$options.i18n.CHAT_CANCEL_LABEL"
877
+ @click="cancelPrompt"
878
+ />
879
+ </div>
880
+ </div>
881
+ </gl-form>
882
+ <slot name="footer-controls"></slot>
883
+ <p class="gl-mb-0 gl-mt-3 gl-px-4 gl-text-sm gl-text-secondary">
884
+ {{ $options.i18n.CHAT_DISCLAMER }}
885
+ </p>
886
+ </footer>
887
+ </div>
912
888
  </template>