@gitlab/duo-ui 10.23.1 → 11.0.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,658 @@
1
+ import throttle from 'lodash/throttle';
2
+ import { GlButton, GlFormTextarea, GlForm, GlCard, GlDropdownItem, GlSafeHtmlDirective } from '@gitlab/ui';
3
+ import { translate } from '../../utils/i18n';
4
+ import { badgeTypes, badgeTypeValidator, MAX_PROMPT_LENGTH, PROMPT_LENGTH_WARNING, CHAT_RESET_MESSAGE, CHAT_INCLUDE_MESSAGE, MESSAGE_MODEL_ROLES, CHAT_CLEAR_MESSAGE, CHAT_NEW_MESSAGE } from './constants';
5
+ import { VIEW_TYPES } from './components/duo_chat_header/constants';
6
+ import DuoChatLoader from './components/duo_chat_loader/duo_chat_loader';
7
+ import DuoChatPredefinedPrompts from './components/duo_chat_predefined_prompts/duo_chat_predefined_prompts';
8
+ import DuoChatConversation from './components/duo_chat_conversation/duo_chat_conversation';
9
+ import WebDuoChatHeader from './components/duo_chat_header/web_duo_chat_header';
10
+ import DuoChatThreads from './components/duo_chat_threads/duo_chat_threads';
11
+ import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
12
+
13
+ const i18n = {
14
+ CHAT_DEFAULT_TITLE: translate('WebDuoChat.chatDefaultTitle', 'GitLab Duo Chat'),
15
+ CHAT_HISTORY_TITLE: translate('WebDuoChat.chatHistoryTitle', 'Chat history'),
16
+ CHAT_DISCLAMER: translate('WebDuoChat.chatDisclamer', 'Responses may be inaccurate. Verify before use.'),
17
+ CHAT_EMPTY_STATE_TITLE: translate('WebDuoChat.chatEmptyStateTitle', '👋 I am GitLab Duo Chat, your personal AI-powered assistant. How can I help you today?'),
18
+ CHAT_PROMPT_PLACEHOLDER_DEFAULT: translate('WebDuoChat.chatPromptPlaceholderDefault', "Let's work through this together..."),
19
+ CHAT_PROMPT_PLACEHOLDER_WITH_COMMANDS: translate('WebDuoChat.chatPromptPlaceholderWithCommands', 'Type /help to learn more'),
20
+ CHAT_SUBMIT_LABEL: translate('WebDuoChat.chatSubmitLabel', 'Send chat message.'),
21
+ CHAT_CANCEL_LABEL: translate('WebDuoChat.chatCancelLabel', 'Cancel'),
22
+ CHAT_MODEL_PLACEHOLDER: translate('WebDuoChat.chatModelPlaceholder', 'GitLab Duo Chat'),
23
+ CHAT_DEFAULT_PREDEFINED_PROMPTS: [translate('WebDuoChat.chatDefaultPredefinedPromptsChangePassword', 'How do I change my password in GitLab?'), translate('WebDuoChat.chatDefaultPredefinedPromptsForkProject', 'How do I fork a project?'), translate('WebDuoChat.chatDefaultPredefinedPromptsCloneRepository', 'How do I clone a repository?'), translate('WebDuoChat.chatDefaultPredefinedPromptsCreateTemplate', 'How do I create a template?')]
24
+ };
25
+ const isMessage = item => Boolean(item) && (item === null || item === void 0 ? void 0 : item.role);
26
+ const isSlashCommand = command => Boolean(command) && (command === null || command === void 0 ? void 0 : command.name) && command.description;
27
+
28
+ // eslint-disable-next-line unicorn/no-array-callback-reference
29
+ const itemsValidator = items => items.every(isMessage);
30
+ // eslint-disable-next-line unicorn/no-array-callback-reference
31
+ const slashCommandsValidator = commands => commands.every(isSlashCommand);
32
+ const isThread = thread => typeof thread === 'object' && typeof thread.id === 'string' && typeof thread.lastUpdatedAt === 'string' && typeof thread.createdAt === 'string' && typeof thread.conversationType === 'string' && (thread.title === null || typeof thread.title === 'string');
33
+
34
+ // eslint-disable-next-line unicorn/no-array-callback-reference
35
+ const threadListValidator = threads => threads.every(isThread);
36
+ const localeValidator = value => {
37
+ try {
38
+ Intl.getCanonicalLocales(value);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ };
44
+ var script = {
45
+ name: 'DuoChat',
46
+ components: {
47
+ GlButton,
48
+ GlFormTextarea,
49
+ GlForm,
50
+ DuoChatLoader,
51
+ DuoChatPredefinedPrompts,
52
+ DuoChatConversation,
53
+ WebDuoChatHeader,
54
+ DuoChatThreads,
55
+ GlCard,
56
+ GlDropdownItem
57
+ },
58
+ directives: {
59
+ SafeHtml: GlSafeHtmlDirective
60
+ },
61
+ props: {
62
+ /**
63
+ * The title of the chat/feature.
64
+ */
65
+ title: {
66
+ type: String,
67
+ required: false,
68
+ default: i18n.CHAT_DEFAULT_TITLE
69
+ },
70
+ /**
71
+ * Array of messages to display in the chat.
72
+ */
73
+ messages: {
74
+ type: Array,
75
+ required: false,
76
+ default: () => [],
77
+ validator: itemsValidator
78
+ },
79
+ /**
80
+ * The ID of the active thread (if any).
81
+ */
82
+ activeThreadId: {
83
+ type: String,
84
+ required: false,
85
+ default: () => ''
86
+ },
87
+ /**
88
+ * The chat page that should be shown.
89
+ */
90
+ multiThreadedView: {
91
+ type: String,
92
+ required: false,
93
+ default: VIEW_TYPES.LIST,
94
+ validator: value => [VIEW_TYPES.LIST, VIEW_TYPES.CHAT].includes(value)
95
+ },
96
+ /**
97
+ * Array of RequestIds that have been canceled.
98
+ */
99
+ canceledRequestIds: {
100
+ type: Array,
101
+ required: false,
102
+ default: () => []
103
+ },
104
+ /**
105
+ * A non-recoverable error message to display in the chat.
106
+ */
107
+ error: {
108
+ type: String,
109
+ required: false,
110
+ default: ''
111
+ },
112
+ /**
113
+ * Array of messages to display in the chat.
114
+ */
115
+ threadList: {
116
+ type: Array,
117
+ required: false,
118
+ default: () => [],
119
+ validator: threadListValidator
120
+ },
121
+ /**
122
+ * Whether the chat is currently fetching a response from AI.
123
+ */
124
+ isLoading: {
125
+ type: Boolean,
126
+ required: false,
127
+ default: false
128
+ },
129
+ /**
130
+ * Whether the conversational interfaces should be enabled.
131
+ */
132
+ isChatAvailable: {
133
+ type: Boolean,
134
+ required: false,
135
+ default: true
136
+ },
137
+ /**
138
+ * Whether the insertCode feature should be available.
139
+ */
140
+ enableCodeInsertion: {
141
+ type: Boolean,
142
+ required: false,
143
+ default: false
144
+ },
145
+ /**
146
+ * Array of predefined prompts to display in the chat to start a conversation.
147
+ */
148
+ predefinedPrompts: {
149
+ type: Array,
150
+ required: false,
151
+ default: () => i18n.CHAT_DEFAULT_PREDEFINED_PROMPTS
152
+ },
153
+ /**
154
+ * The type of the badge. This is passed down to the `GlExperimentBadge` component. Refer that component for more information.
155
+ */
156
+ badgeType: {
157
+ type: String || null,
158
+ required: false,
159
+ default: badgeTypes[0],
160
+ validator: badgeTypeValidator
161
+ },
162
+ /**
163
+ * The current tool's name to display in the loading message while waiting for a response from AI. Refer the `DuoChatLoader` component for more information.
164
+ */
165
+ toolName: {
166
+ type: String,
167
+ required: false,
168
+ default: i18n.CHAT_DEFAULT_TITLE
169
+ },
170
+ /**
171
+ * Array of slash commands to display in the chat.
172
+ */
173
+ slashCommands: {
174
+ type: Array,
175
+ required: false,
176
+ default: () => [],
177
+ validator: slashCommandsValidator
178
+ },
179
+ /**
180
+ * Whether the header should be displayed.
181
+ */
182
+ showHeader: {
183
+ type: Boolean,
184
+ required: false,
185
+ default: true
186
+ },
187
+ /**
188
+ * Override the default empty state title text.
189
+ */
190
+ emptyStateTitle: {
191
+ type: String,
192
+ required: false,
193
+ default: i18n.CHAT_EMPTY_STATE_TITLE
194
+ },
195
+ /**
196
+ * Override the default chat prompt placeholder text.
197
+ */
198
+ chatPromptPlaceholder: {
199
+ type: String,
200
+ required: false,
201
+ default: ''
202
+ },
203
+ /**
204
+ * Whether the chat is running in multi-threaded mode
205
+ */
206
+ isMultithreaded: {
207
+ type: Boolean,
208
+ required: false,
209
+ default: false
210
+ },
211
+ /**
212
+ * Base URL of the GitLab instance.
213
+ */
214
+ trustedUrls: {
215
+ type: Array,
216
+ required: false,
217
+ default: () => []
218
+ },
219
+ /*
220
+ * The preferred locale for the chat interface.
221
+ * Follows BCP 47 language tag format (e.g., 'en-US', 'fr-FR', 'es-ES').
222
+ */
223
+ preferredLocale: {
224
+ type: Array,
225
+ required: false,
226
+ default: () => ['en-US', 'en'],
227
+ validator: localeValidator
228
+ },
229
+ shouldRenderResizable: {
230
+ type: Boolean,
231
+ required: false,
232
+ default: false
233
+ }
234
+ },
235
+ data() {
236
+ return {
237
+ prompt: '',
238
+ scrolledToBottom: true,
239
+ activeCommandIndex: 0,
240
+ displaySubmitButton: true,
241
+ compositionJustEnded: false,
242
+ contextItemsMenuIsOpen: false,
243
+ contextItemMenuRef: null,
244
+ currentView: this.multiThreadedView,
245
+ maxPromptLength: MAX_PROMPT_LENGTH,
246
+ maxPromptLengthWarning: PROMPT_LENGTH_WARNING,
247
+ promptLengthWarningCount: MAX_PROMPT_LENGTH - PROMPT_LENGTH_WARNING
248
+ };
249
+ },
250
+ computed: {
251
+ shouldShowThreadList() {
252
+ return this.isMultithreaded && this.currentView === VIEW_TYPES.LIST;
253
+ },
254
+ withSlashCommands() {
255
+ return this.slashCommands.length > 0;
256
+ },
257
+ hasMessages() {
258
+ var _this$messages;
259
+ return ((_this$messages = this.messages) === null || _this$messages === void 0 ? void 0 : _this$messages.length) > 0;
260
+ },
261
+ conversations() {
262
+ if (!this.hasMessages) return [];
263
+ return this.messages.reduce((acc, message) => {
264
+ if (message.content === CHAT_RESET_MESSAGE) {
265
+ acc.push([]);
266
+ } else {
267
+ acc[acc.length - 1].push(message);
268
+ }
269
+ return acc;
270
+ }, [[]]);
271
+ },
272
+ lastMessage() {
273
+ var _this$messages2;
274
+ return (_this$messages2 = this.messages) === null || _this$messages2 === void 0 ? void 0 : _this$messages2[this.messages.length - 1];
275
+ },
276
+ caseInsensitivePrompt() {
277
+ return this.prompt.toLowerCase().trim();
278
+ },
279
+ isPromptEmpty() {
280
+ return this.caseInsensitivePrompt.length === 0;
281
+ },
282
+ isStreaming() {
283
+ var _this$lastMessage, _this$lastMessage2, _this$lastMessage2$ch, _this$lastMessage3, _this$lastMessage4;
284
+ if (this.canceledRequestIds.includes((_this$lastMessage = this.lastMessage) === null || _this$lastMessage === void 0 ? void 0 : _this$lastMessage.requestId)) {
285
+ return false;
286
+ }
287
+ return Boolean(((_this$lastMessage2 = this.lastMessage) === null || _this$lastMessage2 === void 0 ? void 0 : (_this$lastMessage2$ch = _this$lastMessage2.chunks) === null || _this$lastMessage2$ch === void 0 ? void 0 : _this$lastMessage2$ch.length) > 0 && !((_this$lastMessage3 = this.lastMessage) !== null && _this$lastMessage3 !== void 0 && _this$lastMessage3.content) || typeof ((_this$lastMessage4 = this.lastMessage) === null || _this$lastMessage4 === void 0 ? void 0 : _this$lastMessage4.chunkId) === 'number');
288
+ },
289
+ filteredSlashCommands() {
290
+ return this.slashCommands.filter(c => c.name.toLowerCase().startsWith(this.caseInsensitivePrompt)).filter(c => {
291
+ if (c.name === CHAT_INCLUDE_MESSAGE) {
292
+ return this.hasContextItemSelectionMenu;
293
+ }
294
+ return true;
295
+ });
296
+ },
297
+ shouldShowSlashCommands() {
298
+ if (!this.withSlashCommands || this.contextItemsMenuIsOpen) return false;
299
+ const startsWithSlash = this.caseInsensitivePrompt.startsWith('/');
300
+ const startsWithSlashCommand = this.slashCommands.some(c => this.caseInsensitivePrompt.startsWith(c.name));
301
+ return startsWithSlash && this.filteredSlashCommands.length && !startsWithSlashCommand;
302
+ },
303
+ shouldShowContextItemSelectionMenu() {
304
+ if (!this.hasContextItemSelectionMenu) {
305
+ return false;
306
+ }
307
+ const isSlash = this.caseInsensitivePrompt === '/';
308
+ if (!this.caseInsensitivePrompt || isSlash) {
309
+ // 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
310
+ return false;
311
+ }
312
+ return CHAT_INCLUDE_MESSAGE.startsWith(this.caseInsensitivePrompt);
313
+ },
314
+ inputPlaceholder() {
315
+ if (this.chatPromptPlaceholder) {
316
+ return this.chatPromptPlaceholder;
317
+ }
318
+ return this.withSlashCommands ? i18n.CHAT_PROMPT_PLACEHOLDER_WITH_COMMANDS : i18n.CHAT_PROMPT_PLACEHOLDER_DEFAULT;
319
+ },
320
+ hasContextItemSelectionMenu() {
321
+ return Boolean(this.contextItemMenuRef);
322
+ },
323
+ activeThread() {
324
+ return this.activeThreadId ? this.threadList.find(thread => thread.id === this.activeThreadId) : null;
325
+ },
326
+ activeThreadTitle() {
327
+ var _this$activeThread;
328
+ return (_this$activeThread = this.activeThread) === null || _this$activeThread === void 0 ? void 0 : _this$activeThread.title;
329
+ },
330
+ activeThreadTitleForView() {
331
+ return this.currentView === VIEW_TYPES.CHAT && this.activeThreadTitle || '';
332
+ }
333
+ },
334
+ watch: {
335
+ multiThreadedView(newView) {
336
+ this.currentView = newView;
337
+ },
338
+ isLoading(newVal) {
339
+ if (!newVal && !this.isStreaming) {
340
+ this.displaySubmitButton = true; // Re-enable submit button when loading stops
341
+ }
342
+ },
343
+ isStreaming(newVal) {
344
+ if (!newVal && !this.isLoading) {
345
+ this.displaySubmitButton = true; // Re-enable submit button when streaming stops
346
+ }
347
+ },
348
+ lastMessage(newMessage) {
349
+ if (this.scrolledToBottom || (newMessage === null || newMessage === void 0 ? void 0 : newMessage.role.toLowerCase()) === MESSAGE_MODEL_ROLES.user) {
350
+ // only scroll to bottom on new message if the user hasn't explicitly scrolled up to view an earlier message
351
+ // or if the user has just submitted a new message
352
+ this.scrollToBottom();
353
+ }
354
+ },
355
+ shouldShowSlashCommands(shouldShow) {
356
+ if (shouldShow) {
357
+ this.onShowSlashCommands();
358
+ }
359
+ }
360
+ },
361
+ created() {
362
+ this.handleScrollingThrottled = throttle(this.handleScrolling, 200); // Assume a 200ms throttle for example
363
+ },
364
+ mounted() {
365
+ this.scrollToBottom();
366
+ },
367
+ methods: {
368
+ onGoBack() {
369
+ this.$emit('back-to-list');
370
+ },
371
+ onGoBackToChat() {
372
+ this.$emit('back-to-chat');
373
+ },
374
+ onNewChat() {
375
+ this.$emit('new-chat');
376
+ this.$nextTick(() => {
377
+ this.focusChatInput();
378
+ });
379
+ },
380
+ compositionEnd() {
381
+ this.compositionJustEnded = true;
382
+ },
383
+ hideChat() {
384
+ /**
385
+ * Emitted when clicking the cross in the title and the chat gets closed.
386
+ */
387
+ this.$emit('chat-hidden');
388
+ },
389
+ cancelPrompt() {
390
+ /**
391
+ * Emitted when user clicks the stop button in the textarea
392
+ */
393
+
394
+ this.displaySubmitButton = true;
395
+ this.$emit('chat-cancel');
396
+ this.setPromptAndFocus();
397
+ },
398
+ sendChatPrompt() {
399
+ if (!this.displaySubmitButton || this.contextItemsMenuIsOpen) {
400
+ return;
401
+ }
402
+ if (this.prompt) {
403
+ if (this.caseInsensitivePrompt.startsWith(CHAT_INCLUDE_MESSAGE) && this.hasContextItemSelectionMenu) {
404
+ this.contextItemsMenuIsOpen = true;
405
+ return;
406
+ }
407
+ if (![CHAT_RESET_MESSAGE, CHAT_CLEAR_MESSAGE, CHAT_NEW_MESSAGE].includes(this.caseInsensitivePrompt)) {
408
+ this.displaySubmitButton = false;
409
+ }
410
+
411
+ /**
412
+ * Emitted when a new user prompt should be sent out.
413
+ *
414
+ * @param {String} prompt The user prompt to send.
415
+ */
416
+ this.$emit('send-chat-prompt', this.prompt.trim());
417
+ this.setPromptAndFocus();
418
+ }
419
+ },
420
+ sendPredefinedPrompt(prompt) {
421
+ this.contextItemsMenuIsOpen = false;
422
+ this.prompt = prompt;
423
+ this.sendChatPrompt();
424
+ },
425
+ handleScrolling(event) {
426
+ const {
427
+ scrollTop,
428
+ offsetHeight,
429
+ scrollHeight
430
+ } = event.target;
431
+ this.scrolledToBottom = scrollTop + offsetHeight >= scrollHeight;
432
+ },
433
+ async scrollToBottom() {
434
+ var _this$$refs$anchor, _this$$refs$anchor$sc;
435
+ await this.$nextTick();
436
+ (_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);
437
+ },
438
+ focusChatInput() {
439
+ var _this$$refs$prompt, _this$$refs$prompt$$e, _this$$refs$prompt$$e2, _this$$refs$prompt$$e3;
440
+ // This method is also called directly by consumers of this component
441
+ // https://gitlab.com/gitlab-org/gitlab-vscode-extension/-/blob/dae2d4669ab4da327921492a2962beae8a05c290/webviews/vue2/gitlab_duo_chat/src/App.vue#L109
442
+ (_this$$refs$prompt = this.$refs.prompt) === null || _this$$refs$prompt === void 0 ? void 0 : (_this$$refs$prompt$$e = _this$$refs$prompt.$el) === null || _this$$refs$prompt$$e === void 0 ? void 0 : (_this$$refs$prompt$$e2 = _this$$refs$prompt$$e.querySelector) === null || _this$$refs$prompt$$e2 === void 0 ? void 0 : (_this$$refs$prompt$$e3 = _this$$refs$prompt$$e2.call(_this$$refs$prompt$$e, 'textarea')) === null || _this$$refs$prompt$$e3 === void 0 ? void 0 : _this$$refs$prompt$$e3.focus();
443
+ },
444
+ onTrackFeedback(event) {
445
+ /**
446
+ * Notify listeners about the feedback form submission on a response message.
447
+ * @param {*} event An event, containing the feedback choices and the extended feedback text.
448
+ */
449
+ this.$emit('track-feedback', event);
450
+ },
451
+ onShowSlashCommands() {
452
+ /**
453
+ * Emitted when user opens the slash commands menu
454
+ */
455
+ this.$emit('chat-slash');
456
+ },
457
+ sendChatPromptOnEnter(e) {
458
+ const {
459
+ metaKey,
460
+ ctrlKey,
461
+ altKey,
462
+ shiftKey,
463
+ isComposing
464
+ } = e;
465
+ const isModifierKey = metaKey || ctrlKey || altKey || shiftKey;
466
+ return !(isModifierKey || isComposing || this.compositionJustEnded);
467
+ },
468
+ onInputKeyup(e) {
469
+ const {
470
+ key
471
+ } = e;
472
+ if (this.contextItemsMenuIsOpen) {
473
+ var _this$contextItemMenu;
474
+ if (!this.shouldShowContextItemSelectionMenu) {
475
+ this.contextItemsMenuIsOpen = false;
476
+ }
477
+ (_this$contextItemMenu = this.contextItemMenuRef) === null || _this$contextItemMenu === void 0 ? void 0 : _this$contextItemMenu.handleKeyUp(e);
478
+ return;
479
+ }
480
+ if (this.caseInsensitivePrompt === CHAT_INCLUDE_MESSAGE) {
481
+ this.contextItemsMenuIsOpen = true;
482
+ return;
483
+ }
484
+ if (this.shouldShowSlashCommands) {
485
+ e.preventDefault();
486
+ if (key === 'Enter') {
487
+ this.selectSlashCommand(this.activeCommandIndex);
488
+ } else if (key === 'ArrowUp') {
489
+ this.prevCommand();
490
+ } else if (key === 'ArrowDown') {
491
+ this.nextCommand();
492
+ } else {
493
+ this.activeCommandIndex = 0;
494
+ }
495
+ } else if (key === 'Enter' && this.sendChatPromptOnEnter(e)) {
496
+ e.preventDefault();
497
+ this.sendChatPrompt();
498
+ }
499
+ this.compositionJustEnded = false;
500
+ },
501
+ prevCommand() {
502
+ this.activeCommandIndex -= 1;
503
+ this.wrapCommandIndex();
504
+ },
505
+ nextCommand() {
506
+ this.activeCommandIndex += 1;
507
+ this.wrapCommandIndex();
508
+ },
509
+ wrapCommandIndex() {
510
+ if (this.activeCommandIndex < 0) {
511
+ this.activeCommandIndex = this.filteredSlashCommands.length - 1;
512
+ } else if (this.activeCommandIndex >= this.filteredSlashCommands.length) {
513
+ this.activeCommandIndex = 0;
514
+ }
515
+ },
516
+ async setPromptAndFocus() {
517
+ let prompt = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
518
+ this.prompt = prompt;
519
+ await this.$nextTick();
520
+ this.focusChatInput();
521
+ },
522
+ selectSlashCommand(index) {
523
+ const command = this.filteredSlashCommands[index];
524
+ if (command.shouldSubmit) {
525
+ this.prompt = command.name;
526
+ this.sendChatPrompt();
527
+ } else {
528
+ this.setPromptAndFocus(`${command.name} `);
529
+ if (command.name === CHAT_INCLUDE_MESSAGE && this.hasContextItemSelectionMenu) {
530
+ this.contextItemsMenuIsOpen = true;
531
+ }
532
+ }
533
+ },
534
+ onInsertCodeSnippet(e) {
535
+ /**
536
+ * Emit insert-code-snippet event that clients can use to interact with a suggested code.
537
+ * @param {*} event An event containing code string in the "detail.code" field.
538
+ */
539
+ this.$emit('insert-code-snippet', e);
540
+ },
541
+ onCopyCodeSnippet(e) {
542
+ /**
543
+ * Emit copy-code-snippet event that clients can use to interact with a suggested code.
544
+ * @param {*} event An event containing code string in the "detail.code" field.
545
+ */
546
+ this.$emit('copy-code-snippet', e);
547
+ },
548
+ onCopyMessage(e) {
549
+ /**
550
+ * Emit copy-message event that clients can use to copy chat message content.
551
+ * @param {*} event An event containing code string in the "detail.message" field.
552
+ */
553
+ this.$emit('copy-message', e);
554
+ },
555
+ onGetContextItemContent(event) {
556
+ /**
557
+ * Emit get-context-item-content event that tells clients to load the full file content for a selected context item.
558
+ * The fully hydrated context item should be updated in the chat message context item.
559
+ * @param {*} event An event containing the message ID and context item to hydrate
560
+ */
561
+ this.$emit('get-context-item-content', event);
562
+ },
563
+ closeContextItemsMenuOpen() {
564
+ this.contextItemsMenuIsOpen = false;
565
+ this.setPromptAndFocus();
566
+ },
567
+ setContextItemsMenuRef(ref) {
568
+ this.contextItemMenuRef = ref;
569
+ },
570
+ onSelectThread(thread) {
571
+ /**
572
+ * Emitted when a thread is selected from the history.
573
+ * @param {Object} thread The selected thread object
574
+ */
575
+ this.$emit('thread-selected', thread);
576
+ },
577
+ onDeleteThread(threadId) {
578
+ /**
579
+ * Emitted when a thread is deleted from the history.
580
+ * @param {String} threadId The ID of the thread to delete
581
+ */
582
+ this.$emit('delete-thread', threadId);
583
+ },
584
+ onOpenFilePath(filePath) {
585
+ /**
586
+ * Emitted when a file path link is clicked in a chat message.
587
+ * @param {String} filePath The file path to open
588
+ */
589
+ this.$emit('open-file-path', filePath);
590
+ },
591
+ handleUndo(event) {
592
+ event.preventDefault();
593
+ document.execCommand('undo');
594
+ },
595
+ handleRedo(event) {
596
+ event.preventDefault();
597
+ document.execCommand('redo');
598
+ },
599
+ remainingCharacterCountMessage(count) {
600
+ return `${count} characters remaining`;
601
+ },
602
+ overLimitCharacterCountMessage(count) {
603
+ return `${Math.abs(count)} characters over limit`;
604
+ }
605
+ },
606
+ i18n
607
+ };
608
+
609
+ /* script */
610
+ const __vue_script__ = script;
611
+
612
+ /* template */
613
+ var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{staticClass:"markdown-code-block duo-chat gl-bottom-0 gl-flex gl-max-h-full gl-flex-col",attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"}},[(_vm.showHeader)?_c('web-duo-chat-header',{ref:"header",attrs:{"active-thread-id":_vm.activeThreadId,"title":_vm.isMultithreaded && _vm.currentView === 'list' ? _vm.$options.i18n.CHAT_HISTORY_TITLE : _vm.title,"subtitle":_vm.activeThreadTitleForView,"is-multithreaded":_vm.isMultithreaded,"current-view":_vm.currentView,"should-render-resizable":_vm.shouldRenderResizable,"badge-type":_vm.isMultithreaded ? null : _vm.badgeType},on:{"go-back":_vm.onGoBack,"go-back-to-chat":_vm.onGoBackToChat,"new-chat":_vm.onNewChat,"close":_vm.hideChat},scopedSlots:_vm._u([{key:"subheader",fn:function(){return [_vm._t("subheader")]},proxy:true}],null,true)}):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-flex-1 gl-flex-grow gl-flex-col gl-overflow-y-auto gl-overscroll-contain gl-bg-inherit",attrs:{"data-testid":"chat-history"},on:{"scroll":_vm.handleScrollingThrottled}},[(_vm.shouldShowThreadList)?_c('duo-chat-threads',{attrs:{"threads":_vm.threadList,"preferred-locale":_vm.preferredLocale},on:{"new-chat":_vm.onNewChat,"select-thread":_vm.onSelectThread,"delete-thread":_vm.onDeleteThread,"close":_vm.hideChat}}):_c('transition-group',{staticClass:"duo-chat-history gl-mt-auto gl-p-5",attrs:{"mode":"out-in","tag":"section","name":"message"}},[_vm._l((_vm.conversations),function(conversation,index){return _c('duo-chat-conversation',{key:("conversation-" + index),attrs:{"enable-code-insertion":_vm.enableCodeInsertion,"messages":conversation,"canceled-request-ids":_vm.canceledRequestIds,"show-delimiter":index > 0,"trusted-urls":_vm.trustedUrls},on:{"track-feedback":_vm.onTrackFeedback,"insert-code-snippet":_vm.onInsertCodeSnippet,"copy-code-snippet":_vm.onCopyCodeSnippet,"copy-message":_vm.onCopyMessage,"get-context-item-content":_vm.onGetContextItemContent,"open-file-path":_vm.onOpenFilePath}})}),_vm._v(" "),(!_vm.hasMessages && !_vm.isLoading)?[_c('div',{key:"empty-state-message",staticClass:"duo-chat-message gl-rounded-bl-none gl-p-4 gl-leading-20 gl-text-gray-900 gl-break-anywhere",attrs:{"data-testid":"gl-duo-chat-empty-state"}},[(_vm.emptyStateTitle)?_c('p',{staticClass:"gl-m-0",attrs:{"data-testid":"gl-duo-chat-empty-state-title"}},[_vm._v("\n "+_vm._s(_vm.emptyStateTitle)+"\n ")]):_vm._e(),_vm._v(" "),_c('duo-chat-predefined-prompts',{key:"predefined-prompts",attrs:{"prompts":_vm.predefinedPrompts},on:{"click":_vm.sendPredefinedPrompt}})],1)]:_vm._e(),_vm._v(" "),(_vm.isLoading)?_c('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 && !_vm.shouldShowThreadList)?_c('footer',{staticClass:"duo-chat-drawer-footer gl-relative gl-z-2 gl-shrink-0 gl-border-0 gl-bg-default gl-pb-3",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('div',{staticClass:"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"},[_c('div',{staticClass:"gl-flex gl-justify-between gl-border-0 gl-border-b-1 gl-border-solid gl-border-[#DCDCDE] gl-px-4 gl-py-4"},[_c('div',[_vm._v(_vm._s(_vm.$options.i18n.CHAT_MODEL_PLACEHOLDER))]),_vm._v(" "),_c('div',[_vm._t("agentic-switch")],2)]),_vm._v(" "),_c('div',{staticClass:"gl-h-[40px] gl-grow",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-subtle"},[_vm._v(_vm._s(command.description))])])])}),1):_vm._e(),_vm._v(" "),_c('gl-form-textarea',{ref:"prompt",attrs:{"data-testid":"chat-prompt-input","textarea-classes":[
614
+ '!gl-h-full',
615
+ '!gl-bg-transparent',
616
+ '!gl-py-4',
617
+ '!gl-shadow-none',
618
+ 'form-control',
619
+ 'gl-form-input',
620
+ 'gl-form-textarea',
621
+ { 'gl-truncate': !_vm.prompt } ],"placeholder":_vm.inputPlaceholder,"character-count-limit":_vm.maxPromptLength,"autofocus":""},on:{"keydown":[function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"z",undefined,$event.key,undefined)){ return null; }if(!$event.ctrlKey){ return null; }if($event.shiftKey||$event.altKey||$event.metaKey){ return null; }return _vm.handleUndo.apply(null, arguments)},function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"z",undefined,$event.key,undefined)){ return null; }if(!$event.metaKey){ return null; }if($event.ctrlKey||$event.shiftKey||$event.altKey){ return null; }return _vm.handleUndo.apply(null, arguments)},function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"z",undefined,$event.key,undefined)){ return null; }if(!$event.ctrlKey){ return null; }if(!$event.shiftKey){ return null; }return _vm.handleRedo.apply(null, arguments)},function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"z",undefined,$event.key,undefined)){ return null; }if(!$event.metaKey){ return null; }if(!$event.shiftKey){ return null; }return _vm.handleRedo.apply(null, arguments)},function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"y",undefined,$event.key,undefined)){ return null; }if(!$event.ctrlKey){ return null; }return _vm.handleRedo.apply(null, arguments)},function($event){if(!$event.type.indexOf('key')&&_vm._k($event.keyCode,"y",undefined,$event.key,undefined)){ return null; }if(!$event.metaKey){ return null; }return _vm.handleRedo.apply(null, arguments)}],"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)}},scopedSlots:_vm._u([{key:"remaining-character-count-text",fn:function(ref){
622
+ var count = ref.count;
623
+ return [(count <= _vm.promptLengthWarningCount)?_c('span',{staticClass:"gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3"},[_vm._v("\n "+_vm._s(_vm.remainingCharacterCountMessage(count))+"\n ")]):_vm._e()]}},{key:"character-count-over-limit-text",fn:function(ref){
624
+ var count = ref.count;
625
+ return [_c('span',{staticClass:"gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3"},[_vm._v(_vm._s(_vm.overLimitCharacterCountMessage(count)))])]}}],null,false,839584904),model:{value:(_vm.prompt),callback:function ($$v) {_vm.prompt=$$v;},expression:"prompt"}})],1),_vm._v(" "),_c('div',{staticClass:"gl-flex gl-justify-end gl-px-3 gl-pb-3"},[(_vm.displaySubmitButton)?_c('gl-button',{staticClass:"gl-bottom-2 gl-right-2 gl-ml-auto !gl-rounded-full",attrs:{"icon":"paper-airplane","category":"primary","variant":"confirm","type":"submit","data-testid":"chat-prompt-submit-button","disabled":_vm.isPromptEmpty,"aria-label":_vm.$options.i18n.CHAT_SUBMIT_LABEL}}):_c('gl-button',{staticClass:"gl-bottom-2 gl-right-2 gl-ml-auto !gl-rounded-full",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}})],1)])]),_vm._v(" "),_vm._t("footer-controls"),_vm._v(" "),_c('p',{staticClass:"gl-mb-0 gl-mt-3 gl-px-4 gl-text-sm gl-text-secondary"},[_vm._v("\n "+_vm._s(_vm.$options.i18n.CHAT_DISCLAMER)+"\n ")])],2):_vm._e()],1)};
626
+ var __vue_staticRenderFns__ = [];
627
+
628
+ /* style */
629
+ const __vue_inject_styles__ = undefined;
630
+ /* scoped */
631
+ const __vue_scope_id__ = undefined;
632
+ /* module identifier */
633
+ const __vue_module_identifier__ = undefined;
634
+ /* functional template */
635
+ const __vue_is_functional_template__ = false;
636
+ /* style inject */
637
+
638
+ /* style inject SSR */
639
+
640
+ /* style inject shadow dom */
641
+
642
+
643
+
644
+ const __vue_component__ = __vue_normalize__(
645
+ { render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
646
+ __vue_inject_styles__,
647
+ __vue_script__,
648
+ __vue_scope_id__,
649
+ __vue_is_functional_template__,
650
+ __vue_module_identifier__,
651
+ false,
652
+ undefined,
653
+ undefined,
654
+ undefined
655
+ );
656
+
657
+ export default __vue_component__;
658
+ export { i18n };