@gitlab/ui 66.32.1 → 66.33.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 CHANGED
@@ -1,3 +1,10 @@
1
+ # [66.33.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v66.32.1...v66.33.0) (2023-10-17)
2
+
3
+
4
+ ### Features
5
+
6
+ * **GlDuoChat:** Implemented component ([ce8a3b4](https://gitlab.com/gitlab-org/gitlab-ui/commit/ce8a3b417882d068a147719a4b7dfdeb91e7bb3b))
7
+
1
8
  ## [66.32.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v66.32.0...v66.32.1) (2023-10-17)
2
9
 
3
10
 
@@ -1,3 +1,4 @@
1
+ const CHAT_RESET_MESSAGE = '/reset';
1
2
  const LOADING_TRANSITION_DURATION = 7500;
2
3
  const DOCUMENTATION_SOURCE_TYPES = {
3
4
  HANDBOOK: {
@@ -19,4 +20,4 @@ const MESSAGE_MODEL_ROLES = {
19
20
  assistant: 'assistant'
20
21
  };
21
22
 
22
- export { DOCUMENTATION_SOURCE_TYPES, LOADING_TRANSITION_DURATION, MESSAGE_MODEL_ROLES };
23
+ export { CHAT_RESET_MESSAGE, DOCUMENTATION_SOURCE_TYPES, LOADING_TRANSITION_DURATION, MESSAGE_MODEL_ROLES };
@@ -0,0 +1,255 @@
1
+ import throttle from 'lodash/throttle';
2
+ import emptySvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-activity-md.svg';
3
+ import GlEmptyState from '../../../regions/empty_state/empty_state';
4
+ import GlButton from '../../../base/button/button';
5
+ import GlAlert from '../../../base/alert/alert';
6
+ import GlFormInputGroup from '../../../base/form/form_input_group/form_input_group';
7
+ import GlFormTextarea from '../../../base/form/form_textarea/form_textarea';
8
+ import GlForm from '../../../base/form/form';
9
+ import GlFormText from '../../../base/form/form_text/form_text';
10
+ import GlExperimentBadge from '../../experiment_badge/experiment_badge';
11
+ import { SafeHtmlDirective } from '../../../../directives/safe_html/safe_html';
12
+ import GlDuoChatLoader from './components/duo_chat_loader/duo_chat_loader';
13
+ import GlDuoChatPredefinedPrompts from './components/duo_chat_predefined_prompts/duo_chat_predefined_prompts';
14
+ import GlDuoChatConversation from './components/duo_chat_conversation/duo_chat_conversation';
15
+ import { CHAT_RESET_MESSAGE } from './constants';
16
+ import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
17
+
18
+ const i18n = {
19
+ CHAT_DEFAULT_TITLE: 'GitLab Duo Chat',
20
+ CHAT_CLOSE_LABEL: 'Close the Code Explanation',
21
+ CHAT_LEGAL_GENERATED_BY_AI: 'Responses generated by AI',
22
+ CHAT_EMPTY_STATE_TITLE: 'Ask a question',
23
+ CHAT_EMPTY_STATE_DESC: 'AI generated explanations will appear here.',
24
+ CHAT_PROMPT_PLACEHOLDER: 'GitLab Duo Chat',
25
+ CHAT_SUBMIT_LABEL: 'Send chat message.',
26
+ CHAT_LEGAL_DISCLAIMER: "May provide inappropriate responses not representative of GitLab's views. Do not input personal data.",
27
+ CHAT_DEFAULT_PREDEFINED_PROMPTS: ['How do I change my password in GitLab?', 'How do I fork a project?', 'How do I clone a repository?', 'How do I create a template?']
28
+ };
29
+ const isMessage = item => Boolean(item) && (item === null || item === void 0 ? void 0 : item.role);
30
+ const itemsValidator = items => items.every(isMessage);
31
+ var script = {
32
+ name: 'GlDuoChat',
33
+ components: {
34
+ GlEmptyState,
35
+ GlButton,
36
+ GlAlert,
37
+ GlFormInputGroup,
38
+ GlFormTextarea,
39
+ GlForm,
40
+ GlFormText,
41
+ GlExperimentBadge,
42
+ GlDuoChatLoader,
43
+ GlDuoChatPredefinedPrompts,
44
+ GlDuoChatConversation
45
+ },
46
+ directives: {
47
+ SafeHtml: SafeHtmlDirective
48
+ },
49
+ props: {
50
+ /**
51
+ * The title of the chat/feature.
52
+ */
53
+ title: {
54
+ type: String,
55
+ required: false,
56
+ default: i18n.CHAT_DEFAULT_TITLE
57
+ },
58
+ /**
59
+ * Array of messages to display in the chat.
60
+ */
61
+ messages: {
62
+ type: Array,
63
+ required: false,
64
+ default: () => [],
65
+ validator: itemsValidator
66
+ },
67
+ /**
68
+ * A non-recoverable error message to display in the chat.
69
+ */
70
+ error: {
71
+ type: String,
72
+ required: false,
73
+ default: ''
74
+ },
75
+ /**
76
+ * Whether the chat is currently fetching a response from AI.
77
+ */
78
+ isLoading: {
79
+ type: Boolean,
80
+ required: false,
81
+ default: false
82
+ },
83
+ /**
84
+ * Whether the conversational interfaces should be enabled.
85
+ */
86
+ isChatAvailable: {
87
+ type: Boolean,
88
+ required: false,
89
+ default: true
90
+ },
91
+ /**
92
+ * Array of predefined prompts to display in the chat to start a conversation.
93
+ */
94
+ predefinedPrompts: {
95
+ type: Array,
96
+ required: false,
97
+ default: () => i18n.CHAT_DEFAULT_PREDEFINED_PROMPTS
98
+ },
99
+ /**
100
+ * URL to the experiment help page. This is passed down to the `GlExperimentBadge` component. Refer that component for more information.
101
+ */
102
+ experimentHelpPageUrl: {
103
+ type: String,
104
+ required: false,
105
+ default: ''
106
+ },
107
+ /**
108
+ * The current tool's name to display in the loading message while waiting for a response from AI. Refer the `GlDuoChatLoader` component for more information.
109
+ */
110
+ toolName: {
111
+ type: String,
112
+ required: false,
113
+ default: i18n.CHAT_DEFAULT_TITLE
114
+ }
115
+ },
116
+ data() {
117
+ return {
118
+ isHidden: false,
119
+ prompt: '',
120
+ scrolledToBottom: true
121
+ };
122
+ },
123
+ computed: {
124
+ hasMessages() {
125
+ var _this$messages;
126
+ return ((_this$messages = this.messages) === null || _this$messages === void 0 ? void 0 : _this$messages.length) > 0;
127
+ },
128
+ conversations() {
129
+ if (!this.hasMessages) return [];
130
+ return this.messages.reduce((acc, message) => {
131
+ if (message.content === CHAT_RESET_MESSAGE) {
132
+ acc.push([]);
133
+ } else {
134
+ acc[acc.length - 1].push(message);
135
+ }
136
+ return acc;
137
+ }, [[]]);
138
+ },
139
+ resetDisabled() {
140
+ if (this.isLoading || !this.hasMessages) {
141
+ return true;
142
+ }
143
+ const lastMessage = this.messages[this.messages.length - 1];
144
+ return lastMessage.content === CHAT_RESET_MESSAGE;
145
+ }
146
+ },
147
+ watch: {
148
+ isLoading() {
149
+ this.isHidden = false;
150
+ this.scrollToBottom();
151
+ },
152
+ messages() {
153
+ this.prompt = '';
154
+ }
155
+ },
156
+ created() {
157
+ this.handleScrollingTrottled = throttle(this.handleScrolling, 200); // Assume a 200ms throttle for example
158
+ },
159
+
160
+ mounted() {
161
+ this.scrollToBottom();
162
+ },
163
+ methods: {
164
+ hideChat() {
165
+ this.isHidden = true;
166
+ /**
167
+ * Emitted when clicking the cross in the title and the chat gets closed.
168
+ */
169
+ this.$emit('chat-hidden');
170
+ },
171
+ sendChatPrompt() {
172
+ if (this.prompt) {
173
+ if (this.prompt === CHAT_RESET_MESSAGE && this.resetDisabled) {
174
+ return;
175
+ }
176
+ /**
177
+ * Emitted when a new user prompt should be sent out.
178
+ *
179
+ * @param {String} prompt The user prompt to send.
180
+ */
181
+ this.$emit('send-chat-prompt', this.prompt);
182
+ }
183
+ },
184
+ sendPredefinedPrompt(prompt) {
185
+ this.prompt = prompt;
186
+ this.sendChatPrompt();
187
+ },
188
+ handleScrolling() {
189
+ const {
190
+ scrollTop,
191
+ offsetHeight,
192
+ scrollHeight
193
+ } = this.$refs.drawer;
194
+ this.scrolledToBottom = scrollTop + offsetHeight >= scrollHeight;
195
+ },
196
+ async scrollToBottom() {
197
+ await this.$nextTick();
198
+ if (this.$refs.drawer) {
199
+ this.$refs.drawer.scrollTop = this.$refs.drawer.scrollHeight;
200
+ }
201
+ },
202
+ onTrackFeedback(event) {
203
+ /**
204
+ * Notify listeners about the feedback form submission on a response message.
205
+ * @param {*} event An event, containing the feedback choices and the extended feedback text.
206
+ */
207
+ this.$emit('track-feedback', event);
208
+ }
209
+ },
210
+ i18n,
211
+ emptySvg
212
+ };
213
+
214
+ /* script */
215
+ const __vue_script__ = script;
216
+
217
+ /* template */
218
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (!_vm.isHidden)?_c('aside',{ref:"drawer",staticClass:"markdown-code-block gl-drawer gl-drawer-default gl-max-h-full gl-bottom-0 gl-shadow-none gl-border-l gl-border-t duo-chat gl-h-auto",attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"},on:{"scroll":_vm.handleScrollingTrottled}},[_c('header',{staticClass:"gl-drawer-header gl-drawer-header-sticky gl-z-index-200 gl-p-0! gl-border-b-0"},[_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:{"experiment-help-page-url":_vm.experimentHelpPageUrl,"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")],2),_vm._v(" "),_c('div',{staticClass:"gl-drawer-body gl-display-flex gl-flex-direction-column"},[_vm._t("hero"),_vm._v(" "),(_vm.error)?_c('gl-alert',{key:"error",staticClass:"gl-mb-0 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(),_vm._v(" "),_c('section',{staticClass:"gl-display-flex gl-flex-direction-column gl-justify-content-end gl-flex-grow-1 gl-border-b-0 gl-bg-gray-10"},[_c('transition-group',{staticClass:"gl-display-flex gl-flex-direction-column gl-justify-content-end",class:[
219
+ {
220
+ 'gl-h-full': !_vm.hasMessages,
221
+ 'gl-h-auto': _vm.hasMessages,
222
+ } ],attrs:{"tag":"div","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('div',{key:"empty-state",staticClass:"gl-display-flex gl-flex-grow-1 gl-mr-auto gl-ml-auto"},[_c('gl-empty-state',{staticClass:"gl-align-self-center",attrs:{"svg-path":_vm.$options.emptySvg,"svg-height":145,"title":_vm.$options.i18n.CHAT_EMPTY_STATE_TITLE,"description":_vm.$options.i18n.CHAT_EMPTY_STATE_DESC}})],1),_vm._v(" "),_c('gl-duo-chat-predefined-prompts',{key:"predefined-prompts",attrs:{"prompts":_vm.predefinedPrompts},on:{"click":_vm.sendPredefinedPrompt}})]:_vm._e()],2),_vm._v(" "),_c('transition',{attrs:{"name":"loader"}},[(_vm.isLoading)?_c('gl-duo-chat-loader',{attrs:{"tool-name":_vm.toolName}}):_vm._e()],1)],1)],2),_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,2232229068)},[_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}},[_c('gl-form-textarea',{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.$options.i18n.CHAT_PROMPT_PLACEHOLDER,"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();return _vm.sendChatPrompt.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()};
223
+ var __vue_staticRenderFns__ = [];
224
+
225
+ /* style */
226
+ const __vue_inject_styles__ = undefined;
227
+ /* scoped */
228
+ const __vue_scope_id__ = undefined;
229
+ /* module identifier */
230
+ const __vue_module_identifier__ = undefined;
231
+ /* functional template */
232
+ const __vue_is_functional_template__ = false;
233
+ /* style inject */
234
+
235
+ /* style inject SSR */
236
+
237
+ /* style inject shadow dom */
238
+
239
+
240
+
241
+ const __vue_component__ = __vue_normalize__(
242
+ { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
243
+ __vue_inject_styles__,
244
+ __vue_script__,
245
+ __vue_scope_id__,
246
+ __vue_is_functional_template__,
247
+ __vue_module_identifier__,
248
+ false,
249
+ undefined,
250
+ undefined,
251
+ undefined
252
+ );
253
+
254
+ export default __vue_component__;
255
+ export { i18n };
@@ -55,6 +55,21 @@ const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
55
55
  errors: [],
56
56
  timestamp: '2021-04-21T12:00:00.000Z'
57
57
  };
58
+ const generateMockResponseChunks = requestId => {
59
+ const chunks = [];
60
+ const chunkSize = 5;
61
+ const chunkCount = Math.ceil(MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.length / chunkSize);
62
+ for (let i = 0; i < chunkCount; i += 1) {
63
+ const chunk = {
64
+ ...MOCK_RESPONSE_MESSAGE_FOR_STREAMING,
65
+ requestId,
66
+ content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.substring(i * chunkSize, (i + 1) * chunkSize),
67
+ chunkId: i
68
+ };
69
+ chunks.push(chunk);
70
+ }
71
+ return chunks;
72
+ };
58
73
  const MOCK_USER_PROMPT_MESSAGE = {
59
74
  id: '456',
60
75
  content: 'How to create a new template?',
@@ -66,4 +81,4 @@ const MOCK_USER_PROMPT_MESSAGE = {
66
81
  extras: null
67
82
  };
68
83
 
69
- export { MOCK_CHUNK_RESPONSE_MESSAGE, MOCK_RESPONSE_MESSAGE, MOCK_RESPONSE_MESSAGE_FOR_STREAMING, MOCK_USER_PROMPT_MESSAGE };
84
+ export { MOCK_CHUNK_RESPONSE_MESSAGE, MOCK_RESPONSE_MESSAGE, MOCK_RESPONSE_MESSAGE_FOR_STREAMING, MOCK_USER_PROMPT_MESSAGE, generateMockResponseChunks };