@gitlab/duo-ui 10.21.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.
- package/CHANGELOG.md +7 -0
- package/dist/components/agentic_chat/agentic_duo_chat.js +18 -37
- package/dist/components/chat/components/duo_chat_header/duo_chat_header.js +4 -4
- package/dist/components/chat/duo_chat.js +35 -54
- package/dist/components/ui/duo_layout/duo_layout.js +100 -0
- package/dist/components/ui/side_rail/side_rail.js +67 -0
- package/dist/components.css +1 -1
- package/dist/components.css.map +1 -1
- package/dist/index.js +2 -0
- package/dist/tailwind.css +1 -1
- package/dist/tailwind.css.map +1 -1
- package/package.json +2 -1
- package/src/components/agentic_chat/agentic_duo_chat.vue +210 -244
- package/src/components/chat/components/duo_chat_header/duo_chat_header.vue +24 -22
- package/src/components/chat/duo_chat.scss +1 -2
- package/src/components/chat/duo_chat.vue +214 -238
- package/src/components/ui/duo_layout/duo_layout.md +0 -0
- package/src/components/ui/duo_layout/duo_layout.vue +95 -0
- package/src/components/ui/side_rail/side_rail.vue +56 -0
- package/src/index.js +2 -0
- package/translations.js +5 -4
|
@@ -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
|
-
'
|
|
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
|
-
<
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
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
|
-
<
|
|
726
|
-
v-if="
|
|
727
|
-
|
|
728
|
-
:
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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-
|
|
737
|
-
v-if="
|
|
738
|
-
|
|
739
|
-
:
|
|
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
|
-
<
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
<
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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-
|
|
840
|
-
v-
|
|
841
|
-
|
|
842
|
-
class="
|
|
843
|
-
|
|
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-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
</
|
|
859
|
-
</
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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>
|
|
File without changes
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import VueResizable from 'vue-resizable';
|
|
3
|
+
import SideRail from '../side_rail/side_rail.vue';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: 'DuoLayout',
|
|
7
|
+
components: {
|
|
8
|
+
VueResizable,
|
|
9
|
+
SideRail,
|
|
10
|
+
},
|
|
11
|
+
props: {
|
|
12
|
+
/**
|
|
13
|
+
* Determines if the component should be resizable. When true, it renders inside
|
|
14
|
+
* a `vue-resizable` wrapper; otherwise, a standard `div` is used.
|
|
15
|
+
*/
|
|
16
|
+
shouldRenderResizable: {
|
|
17
|
+
type: Boolean,
|
|
18
|
+
required: false,
|
|
19
|
+
default: false,
|
|
20
|
+
},
|
|
21
|
+
/**
|
|
22
|
+
* Defines the dimensions of the layout container when resizable.
|
|
23
|
+
* By default, the height is set to match the height of the browser window,
|
|
24
|
+
* and the width is fixed at 400px. The `top` position is left undefined,
|
|
25
|
+
* allowing it to be dynamically adjusted if needed.
|
|
26
|
+
*/
|
|
27
|
+
dimensions: {
|
|
28
|
+
type: Object,
|
|
29
|
+
required: false,
|
|
30
|
+
default: () => ({
|
|
31
|
+
width: undefined,
|
|
32
|
+
height: undefined,
|
|
33
|
+
top: undefined,
|
|
34
|
+
left: undefined,
|
|
35
|
+
maxWidth: undefined,
|
|
36
|
+
minWidth: 400,
|
|
37
|
+
maxHeight: undefined,
|
|
38
|
+
minHeight: 400,
|
|
39
|
+
}),
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* Whether the layout is hidden
|
|
43
|
+
*/
|
|
44
|
+
isHidden: {
|
|
45
|
+
type: Boolean,
|
|
46
|
+
required: false,
|
|
47
|
+
default: false,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
methods: {
|
|
51
|
+
updateSize(e) {
|
|
52
|
+
/**
|
|
53
|
+
* Emitted when the layout is resized
|
|
54
|
+
* @param {Object} e The resize event data
|
|
55
|
+
*/
|
|
56
|
+
this.$emit('resize', e);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
</script>
|
|
61
|
+
<template>
|
|
62
|
+
<component
|
|
63
|
+
:is="shouldRenderResizable ? 'vue-resizable' : 'div'"
|
|
64
|
+
v-if="!isHidden"
|
|
65
|
+
:width="shouldRenderResizable ? dimensions.width : null"
|
|
66
|
+
:height="shouldRenderResizable ? dimensions.height : null"
|
|
67
|
+
:max-width="shouldRenderResizable ? dimensions.maxWidth : null"
|
|
68
|
+
:max-height="shouldRenderResizable ? dimensions.maxHeight : null"
|
|
69
|
+
:min-width="shouldRenderResizable ? dimensions.minWidth : null"
|
|
70
|
+
:left="shouldRenderResizable ? dimensions.left : null"
|
|
71
|
+
:top="shouldRenderResizable ? dimensions.top : null"
|
|
72
|
+
:fit-parent="true"
|
|
73
|
+
:min-height="shouldRenderResizable ? dimensions.minHeight : null"
|
|
74
|
+
:class="{
|
|
75
|
+
'duo-chat-resizable': shouldRenderResizable,
|
|
76
|
+
'non-resizable-wrapper': !shouldRenderResizable,
|
|
77
|
+
}"
|
|
78
|
+
:active="shouldRenderResizable ? ['l', 't', 'lt'] : null"
|
|
79
|
+
data-testid="duo-layout-component"
|
|
80
|
+
@resize:end="updateSize"
|
|
81
|
+
>
|
|
82
|
+
<aside
|
|
83
|
+
class="markdown-code-block duo-chat gl-align-items gl-bottom-0 gl-flex gl-h-full gl-max-h-full gl-flex-row gl-bg-strong gl-py-3 gl-pl-3"
|
|
84
|
+
>
|
|
85
|
+
<main
|
|
86
|
+
class="content flex-none gl-h-full gl-min-w-0 gl-grow gl-overflow-y-auto gl-rounded-[1rem] gl-bg-neutral-0"
|
|
87
|
+
>
|
|
88
|
+
<slot name="mainview"></slot>
|
|
89
|
+
</main>
|
|
90
|
+
<aside class="gl-flex gl-h-full gl-min-h-full gl-bg-strong gl-px-3 gl-py-2">
|
|
91
|
+
<slot name="siderail"></slot>
|
|
92
|
+
</aside>
|
|
93
|
+
</aside>
|
|
94
|
+
</component>
|
|
95
|
+
</template>
|