@gitlab/ui 66.32.0 → 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.
@@ -0,0 +1,372 @@
1
+ <script>
2
+ import throttle from 'lodash/throttle';
3
+ import emptySvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-activity-md.svg';
4
+ import GlEmptyState from '../../../regions/empty_state/empty_state.vue';
5
+ import GlButton from '../../../base/button/button.vue';
6
+ import GlAlert from '../../../base/alert/alert.vue';
7
+ import GlFormInputGroup from '../../../base/form/form_input_group/form_input_group.vue';
8
+ import GlFormTextarea from '../../../base/form/form_textarea/form_textarea.vue';
9
+ import GlForm from '../../../base/form/form.vue';
10
+ import GlFormText from '../../../base/form/form_text/form_text.vue';
11
+ import GlExperimentBadge from '../../experiment_badge/experiment_badge.vue';
12
+ import { SafeHtmlDirective as SafeHtml } from '../../../../directives/safe_html/safe_html';
13
+ import GlDuoChatLoader from './components/duo_chat_loader/duo_chat_loader.vue';
14
+ import GlDuoChatPredefinedPrompts from './components/duo_chat_predefined_prompts/duo_chat_predefined_prompts.vue';
15
+ import GlDuoChatConversation from './components/duo_chat_conversation/duo_chat_conversation.vue';
16
+ import { CHAT_RESET_MESSAGE } from './constants';
17
+
18
+ export 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:
27
+ "May provide inappropriate responses not representative of GitLab's views. Do not input personal data.",
28
+ CHAT_DEFAULT_PREDEFINED_PROMPTS: [
29
+ 'How do I change my password in GitLab?',
30
+ 'How do I fork a project?',
31
+ 'How do I clone a repository?',
32
+ 'How do I create a template?',
33
+ ],
34
+ };
35
+
36
+ const isMessage = (item) => Boolean(item) && item?.role;
37
+
38
+ const itemsValidator = (items) => items.every(isMessage);
39
+
40
+ export default {
41
+ name: 'GlDuoChat',
42
+ components: {
43
+ GlEmptyState,
44
+ GlButton,
45
+ GlAlert,
46
+ GlFormInputGroup,
47
+ GlFormTextarea,
48
+ GlForm,
49
+ GlFormText,
50
+ GlExperimentBadge,
51
+ GlDuoChatLoader,
52
+ GlDuoChatPredefinedPrompts,
53
+ GlDuoChatConversation,
54
+ },
55
+ directives: {
56
+ SafeHtml,
57
+ },
58
+ props: {
59
+ /**
60
+ * The title of the chat/feature.
61
+ */
62
+ title: {
63
+ type: String,
64
+ required: false,
65
+ default: i18n.CHAT_DEFAULT_TITLE,
66
+ },
67
+ /**
68
+ * Array of messages to display in the chat.
69
+ */
70
+ messages: {
71
+ type: Array,
72
+ required: false,
73
+ default: () => [],
74
+ validator: itemsValidator,
75
+ },
76
+ /**
77
+ * A non-recoverable error message to display in the chat.
78
+ */
79
+ error: {
80
+ type: String,
81
+ required: false,
82
+ default: '',
83
+ },
84
+ /**
85
+ * Whether the chat is currently fetching a response from AI.
86
+ */
87
+ isLoading: {
88
+ type: Boolean,
89
+ required: false,
90
+ default: false,
91
+ },
92
+ /**
93
+ * Whether the conversational interfaces should be enabled.
94
+ */
95
+ isChatAvailable: {
96
+ type: Boolean,
97
+ required: false,
98
+ default: true,
99
+ },
100
+ /**
101
+ * Array of predefined prompts to display in the chat to start a conversation.
102
+ */
103
+ predefinedPrompts: {
104
+ type: Array,
105
+ required: false,
106
+ default: () => i18n.CHAT_DEFAULT_PREDEFINED_PROMPTS,
107
+ },
108
+ /**
109
+ * URL to the experiment help page. This is passed down to the `GlExperimentBadge` component. Refer that component for more information.
110
+ */
111
+ experimentHelpPageUrl: {
112
+ type: String,
113
+ required: false,
114
+ default: '',
115
+ },
116
+ /**
117
+ * 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.
118
+ */
119
+ toolName: {
120
+ type: String,
121
+ required: false,
122
+ default: i18n.CHAT_DEFAULT_TITLE,
123
+ },
124
+ },
125
+ data() {
126
+ return {
127
+ isHidden: false,
128
+ prompt: '',
129
+ scrolledToBottom: true,
130
+ };
131
+ },
132
+ computed: {
133
+ hasMessages() {
134
+ return this.messages?.length > 0;
135
+ },
136
+ conversations() {
137
+ if (!this.hasMessages) return [];
138
+
139
+ return this.messages.reduce(
140
+ (acc, message) => {
141
+ if (message.content === CHAT_RESET_MESSAGE) {
142
+ acc.push([]);
143
+ } else {
144
+ acc[acc.length - 1].push(message);
145
+ }
146
+ return acc;
147
+ },
148
+ [[]]
149
+ );
150
+ },
151
+ resetDisabled() {
152
+ if (this.isLoading || !this.hasMessages) {
153
+ return true;
154
+ }
155
+
156
+ const lastMessage = this.messages[this.messages.length - 1];
157
+ return lastMessage.content === CHAT_RESET_MESSAGE;
158
+ },
159
+ },
160
+ watch: {
161
+ isLoading() {
162
+ this.isHidden = false;
163
+ this.scrollToBottom();
164
+ },
165
+ messages() {
166
+ this.prompt = '';
167
+ },
168
+ },
169
+ created() {
170
+ this.handleScrollingTrottled = throttle(this.handleScrolling, 200); // Assume a 200ms throttle for example
171
+ },
172
+ mounted() {
173
+ this.scrollToBottom();
174
+ },
175
+ methods: {
176
+ hideChat() {
177
+ this.isHidden = true;
178
+ /**
179
+ * Emitted when clicking the cross in the title and the chat gets closed.
180
+ */
181
+ this.$emit('chat-hidden');
182
+ },
183
+ sendChatPrompt() {
184
+ if (this.prompt) {
185
+ if (this.prompt === CHAT_RESET_MESSAGE && this.resetDisabled) {
186
+ return;
187
+ }
188
+ /**
189
+ * Emitted when a new user prompt should be sent out.
190
+ *
191
+ * @param {String} prompt The user prompt to send.
192
+ */
193
+ this.$emit('send-chat-prompt', this.prompt);
194
+ }
195
+ },
196
+ sendPredefinedPrompt(prompt) {
197
+ this.prompt = prompt;
198
+ this.sendChatPrompt();
199
+ },
200
+ handleScrolling() {
201
+ const { scrollTop, offsetHeight, scrollHeight } = this.$refs.drawer;
202
+ this.scrolledToBottom = scrollTop + offsetHeight >= scrollHeight;
203
+ },
204
+ async scrollToBottom() {
205
+ await this.$nextTick();
206
+
207
+ if (this.$refs.drawer) {
208
+ this.$refs.drawer.scrollTop = this.$refs.drawer.scrollHeight;
209
+ }
210
+ },
211
+ onTrackFeedback(event) {
212
+ /**
213
+ * Notify listeners about the feedback form submission on a response message.
214
+ * @param {*} event An event, containing the feedback choices and the extended feedback text.
215
+ */
216
+ this.$emit('track-feedback', event);
217
+ },
218
+ },
219
+ i18n,
220
+ emptySvg,
221
+ };
222
+ </script>
223
+ <template>
224
+ <aside
225
+ v-if="!isHidden"
226
+ id="chat-component"
227
+ ref="drawer"
228
+ class="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"
229
+ role="complementary"
230
+ data-testid="chat-component"
231
+ @scroll="handleScrollingTrottled"
232
+ >
233
+ <header class="gl-drawer-header gl-drawer-header-sticky gl-z-index-200 gl-p-0! gl-border-b-0">
234
+ <div
235
+ class="drawer-title gl-display-flex gl-justify-content-start gl-align-items-center gl-p-5"
236
+ >
237
+ <h3 class="gl-my-0 gl-font-size-h2">{{ title }}</h3>
238
+ <gl-experiment-badge
239
+ :experiment-help-page-url="experimentHelpPageUrl"
240
+ container-id="chat-component"
241
+ />
242
+ <gl-button
243
+ category="tertiary"
244
+ variant="default"
245
+ icon="close"
246
+ size="small"
247
+ class="gl-p-0! gl-ml-auto"
248
+ data-testid="chat-close-button"
249
+ :aria-label="$options.i18n.CHAT_CLOSE_LABEL"
250
+ @click="hideChat"
251
+ />
252
+ </div>
253
+
254
+ <gl-alert
255
+ :dismissible="false"
256
+ variant="tip"
257
+ :show-icon="false"
258
+ class="gl-text-center gl-border-t gl-p-4 gl-text-gray-700 gl-bg-gray-50 legal-warning gl-max-w-full"
259
+ role="alert"
260
+ data-testid="chat-legal-warning"
261
+ >{{ $options.i18n.CHAT_LEGAL_GENERATED_BY_AI }}</gl-alert
262
+ >
263
+
264
+ <!--
265
+ @slot Subheader to be rendered right after the title. It is sticky and stays on top of the chat no matter the number of messages.
266
+ -->
267
+ <slot name="subheader"></slot>
268
+ </header>
269
+
270
+ <div class="gl-drawer-body gl-display-flex gl-flex-direction-column">
271
+ <!-- @slot 'Hero' information to be rendered at the top of the chat before any message. It gets pushed away from the view by incomming messages
272
+ -->
273
+ <slot name="hero"></slot>
274
+
275
+ <gl-alert
276
+ v-if="error"
277
+ key="error"
278
+ :dismissible="false"
279
+ variant="danger"
280
+ class="gl-mb-0 gl-pl-9!"
281
+ role="alert"
282
+ data-testid="chat-error"
283
+ ><span v-safe-html="error"></span
284
+ ></gl-alert>
285
+
286
+ <section
287
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-end gl-flex-grow-1 gl-border-b-0 gl-bg-gray-10"
288
+ >
289
+ <transition-group
290
+ tag="div"
291
+ name="message"
292
+ class="gl-display-flex gl-flex-direction-column gl-justify-content-end"
293
+ :class="[
294
+ {
295
+ 'gl-h-full': !hasMessages,
296
+ 'gl-h-auto': hasMessages,
297
+ },
298
+ ]"
299
+ >
300
+ <gl-duo-chat-conversation
301
+ v-for="(conversation, index) in conversations"
302
+ :key="`conversation-${index}`"
303
+ :messages="conversation"
304
+ :show-delimiter="index > 0"
305
+ @track-feedback="onTrackFeedback"
306
+ />
307
+ <template v-if="!hasMessages && !isLoading">
308
+ <div key="empty-state" class="gl-display-flex gl-flex-grow-1 gl-mr-auto gl-ml-auto">
309
+ <gl-empty-state
310
+ :svg-path="$options.emptySvg"
311
+ :svg-height="145"
312
+ :title="$options.i18n.CHAT_EMPTY_STATE_TITLE"
313
+ :description="$options.i18n.CHAT_EMPTY_STATE_DESC"
314
+ class="gl-align-self-center"
315
+ />
316
+ </div>
317
+ <gl-duo-chat-predefined-prompts
318
+ key="predefined-prompts"
319
+ :prompts="predefinedPrompts"
320
+ @click="sendPredefinedPrompt"
321
+ />
322
+ </template>
323
+ </transition-group>
324
+ <transition name="loader">
325
+ <gl-duo-chat-loader v-if="isLoading" :tool-name="toolName" />
326
+ </transition>
327
+ </section>
328
+ </div>
329
+ <footer
330
+ v-if="isChatAvailable"
331
+ data-testid="chat-footer"
332
+ class="gl-drawer-footer gl-drawer-footer-sticky gl-p-5 gl-border-t gl-bg-gray-10"
333
+ :class="{ 'gl-drawer-body-scrim-on-footer': !scrolledToBottom }"
334
+ >
335
+ <gl-form data-testid="chat-prompt-form" @submit.stop.prevent="sendChatPrompt">
336
+ <gl-form-input-group>
337
+ <div
338
+ class="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"
339
+ :data-value="prompt"
340
+ >
341
+ <gl-form-textarea
342
+ v-model="prompt"
343
+ data-testid="chat-prompt-input"
344
+ class="gl-absolute gl-h-full! gl-py-4! gl-bg-transparent! gl-rounded-top-right-none gl-rounded-bottom-right-none gl-shadow-none!"
345
+ :class="{ 'gl-text-truncate': !prompt }"
346
+ :placeholder="$options.i18n.CHAT_PROMPT_PLACEHOLDER"
347
+ :disabled="isLoading"
348
+ autofocus
349
+ @keydown.enter.exact.native.prevent="sendChatPrompt"
350
+ />
351
+ </div>
352
+ <template #append>
353
+ <gl-button
354
+ icon="paper-airplane"
355
+ category="primary"
356
+ variant="confirm"
357
+ class="gl-absolute! gl-bottom-2 gl-right-2 gl-rounded-base!"
358
+ type="submit"
359
+ :aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
360
+ :disabled="isLoading"
361
+ />
362
+ </template>
363
+ </gl-form-input-group>
364
+ <gl-form-text
365
+ class="gl-text-gray-400 gl-line-height-20 gl-mt-3"
366
+ data-testid="chat-legal-disclaimer"
367
+ >{{ $options.i18n.CHAT_LEGAL_DISCLAIMER }}</gl-form-text
368
+ >
369
+ </gl-form>
370
+ </footer>
371
+ </aside>
372
+ </template>
@@ -65,6 +65,25 @@ export const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
65
65
  timestamp: '2021-04-21T12:00:00.000Z',
66
66
  };
67
67
 
68
+ export const generateMockResponseChunks = (requestId) => {
69
+ const chunks = [];
70
+ const chunkSize = 5;
71
+ const chunkCount = Math.ceil(MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.length / chunkSize);
72
+ for (let i = 0; i < chunkCount; i += 1) {
73
+ const chunk = {
74
+ ...MOCK_RESPONSE_MESSAGE_FOR_STREAMING,
75
+ requestId,
76
+ content: MOCK_RESPONSE_MESSAGE_FOR_STREAMING.content.substring(
77
+ i * chunkSize,
78
+ (i + 1) * chunkSize
79
+ ),
80
+ chunkId: i,
81
+ };
82
+ chunks.push(chunk);
83
+ }
84
+ return chunks;
85
+ };
86
+
68
87
  export const MOCK_USER_PROMPT_MESSAGE = {
69
88
  id: '456',
70
89
  content: 'How to create a new template?',
package/src/index.js CHANGED
@@ -99,6 +99,7 @@ export { default as GlCarouselSlide } from './components/base/carousel/carousel_
99
99
  // Experimental
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
+ export { default as GlDuoChat } from './components/experimental/duo/chat/duo_chat.vue';
102
103
 
103
104
  // Utilities
104
105
  export { default as GlAnimatedNumber } from './components/utilities/animated_number/animated_number.vue';
@@ -3,6 +3,7 @@
3
3
  // @import '../components/base/dropdown/dropdown'
4
4
  //
5
5
  // ADD COMPONENT IMPORTS - needed for yarn generate:component. Do not remove
6
+ @import '../components/experimental/duo/chat/duo_chat';
6
7
  @import '../components/experimental/duo/chat/components/duo_chat_message/duo_chat_message';
7
8
  @import '../components/experimental/duo/chat/components/duo_chat_loader/duo_chat_loader';
8
9
  @import '../components/base/new_dropdowns/disclosure/disclosure_dropdown';
@@ -13,18 +13,34 @@ Usage:
13
13
  */
14
14
  font-display: block;
15
15
  font-style: normal;
16
- font-named-instance: 'Regular'; /* stylelint-disable property-no-unknown */
16
+ /* stylelint-disable-next-line property-no-unknown */
17
+ font-named-instance: 'Regular';
17
18
  src: url('../../static/fonts/GitLabSans.woff2') format('woff2');
18
19
  }
19
20
 
21
+ @font-face {
22
+ font-family: 'GitLab Sans';
23
+ font-weight: 100 900;
24
+ /**
25
+ * Applications should use a less aggressive font-display value than this.
26
+ * This is done to make sure Storybook Previews load the font.
27
+ */
28
+ font-display: block;
29
+ font-style: italic;
30
+ /* stylelint-disable-next-line property-no-unknown */
31
+ font-named-instance: 'Regular';
32
+ src: url('../../static/fonts/GitLabSans-Italic.woff2') format('woff2');
33
+ }
34
+
20
35
  /* -------------------------------------------------------
21
36
  Monospaced font: GitLab Mono.
22
37
 
23
38
  Usage:
24
- html { font-family: 'GitLab Mono', sans-serif; }
39
+ html { font-family: 'GitLab Mono', monospace; }
25
40
  */
26
41
  @font-face {
27
42
  font-family: 'GitLab Mono';
43
+ font-weight: 100 900;
28
44
  /**
29
45
  * Applications should use a less aggressive font-display value than this.
30
46
  * This is done to make sure Storybook Previews load the font.
@@ -33,3 +49,19 @@ Usage:
33
49
  font-style: normal;
34
50
  src: url('../../static/fonts/GitLabMono.woff2') format('woff2');
35
51
  }
52
+
53
+ @font-face {
54
+ font-family: 'GitLab Mono';
55
+ font-weight: 100 900;
56
+ /**
57
+ * Applications should use a less aggressive font-display value than this.
58
+ * This is done to make sure Storybook Previews load the font.
59
+ */
60
+ font-display: block;
61
+ font-style: italic;
62
+ src: url('../../static/fonts/GitLabMono-Italic.woff2') format('woff2');
63
+ }
64
+
65
+ * {
66
+ font-synthesis: none;
67
+ }
@@ -15,6 +15,7 @@
15
15
 
16
16
  @import 'components';
17
17
 
18
+ @import '../../dist/tokens/css/tokens';
18
19
  @import 'storybook_dark_mode';
19
20
 
20
21
  /**
@@ -1,4 +1,5 @@
1
1
  @import 'variables';
2
+ @import '../../dist/tokens/css/tokens.dark';
2
3
 
3
4
  // conditional overrides for dark mode for use in storybook.
4
5
  // Because we only use application.css from gitlab (and not
@@ -15,7 +16,6 @@
15
16
  // Note that we are assigning variables with different values due to the way GitLab inverts variables in dark mode.
16
17
  // e.g. text color is usually $gray-900, but in dark mode $gray-900 variable gets inverted to $gray-50
17
18
  --gl-text-color: #{$gray-50};
18
- --gray-600: #{$gray-300};
19
19
 
20
20
  color-scheme: dark;
21
21