@gitlab/duo-ui 10.23.1 → 11.0.1
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 +25 -0
- package/dist/components/agentic_chat/web_agentic_duo_chat.js +725 -0
- package/dist/components/chat/components/duo_chat_header/web_duo_chat_header.js +161 -0
- package/dist/components/chat/web_duo_chat.js +658 -0
- package/dist/components/ui/duo_layout/duo_layout.js +100 -0
- package/dist/components/ui/side_rail/side_rail.js +67 -0
- package/dist/index.js +4 -0
- package/dist/tailwind.css +1 -1
- package/dist/tailwind.css.map +1 -1
- package/package.json +2 -1
- package/src/components/agentic_chat/web_agentic_duo_chat.vue +978 -0
- package/src/components/chat/components/duo_chat_header/web_duo_chat_header.vue +274 -0
- package/src/components/chat/web_duo_chat.vue +891 -0
- 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 +57 -0
- package/src/index.js +5 -0
- package/translations.js +42 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import throttle from 'lodash/throttle';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
GlButton,
|
|
6
|
+
GlDropdownItem,
|
|
7
|
+
GlCard,
|
|
8
|
+
GlFormTextarea,
|
|
9
|
+
GlForm,
|
|
10
|
+
GlSafeHtmlDirective as SafeHtml,
|
|
11
|
+
} from '@gitlab/ui';
|
|
12
|
+
|
|
13
|
+
import { sprintf, translate, translatePlural } from '@gitlab/ui/dist/utils/i18n';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
badgeTypes,
|
|
17
|
+
badgeTypeValidator,
|
|
18
|
+
CHAT_RESET_MESSAGE,
|
|
19
|
+
CHAT_INCLUDE_MESSAGE,
|
|
20
|
+
CHAT_BASE_COMMANDS,
|
|
21
|
+
MESSAGE_MODEL_ROLES,
|
|
22
|
+
MAX_PROMPT_LENGTH,
|
|
23
|
+
PROMPT_LENGTH_WARNING,
|
|
24
|
+
} from '../chat/constants';
|
|
25
|
+
import { VIEW_TYPES } from '../chat/components/duo_chat_header/constants';
|
|
26
|
+
import DuoChatLoader from '../chat/components/duo_chat_loader/duo_chat_loader.vue';
|
|
27
|
+
import DuoChatPredefinedPrompts from '../chat/components/duo_chat_predefined_prompts/duo_chat_predefined_prompts.vue';
|
|
28
|
+
import DuoChatConversation from '../chat/components/duo_chat_conversation/duo_chat_conversation.vue';
|
|
29
|
+
import WebDuoChatHeader from '../chat/components/duo_chat_header/web_duo_chat_header.vue';
|
|
30
|
+
import DuoChatThreads from '../chat/components/duo_chat_threads/duo_chat_threads.vue';
|
|
31
|
+
|
|
32
|
+
export const i18n = {
|
|
33
|
+
CHAT_DEFAULT_TITLE: translate('WebAgenticDuoChat.chatDefaultTitle', 'GitLab Duo Agentic Chat'),
|
|
34
|
+
CHAT_HISTORY_TITLE: translate('WebAgenticDuoChat.chatHistoryTitle', 'Chat history'),
|
|
35
|
+
CHAT_DISCLAMER: translate(
|
|
36
|
+
'WebAgenticDuoChat.chatDisclamer',
|
|
37
|
+
'Responses may be inaccurate. Verify before use.'
|
|
38
|
+
),
|
|
39
|
+
CHAT_EMPTY_STATE_TITLE: translate(
|
|
40
|
+
'WebAgenticDuoChat.chatEmptyStateTitle',
|
|
41
|
+
'👋 I am GitLab Duo Agentic Chat, your personal AI-powered assistant. How can I help you today?'
|
|
42
|
+
),
|
|
43
|
+
CHAT_PROMPT_PLACEHOLDER_DEFAULT: translate(
|
|
44
|
+
'WebAgenticDuoChat.chatPromptPlaceholderDefault',
|
|
45
|
+
"Let's work through this together..."
|
|
46
|
+
),
|
|
47
|
+
CHAT_MODEL_PLACEHOLDER: translate(
|
|
48
|
+
'WebAgenticDuoChat.chatModelPlaceholder',
|
|
49
|
+
'GitLab Duo Agentic Chat'
|
|
50
|
+
),
|
|
51
|
+
CHAT_PROMPT_PLACEHOLDER_WITH_COMMANDS: translate(
|
|
52
|
+
'WebAgenticDuoChat.chatPromptPlaceholderWithCommands',
|
|
53
|
+
'Type /help to learn more'
|
|
54
|
+
),
|
|
55
|
+
CHAT_SUBMIT_LABEL: translate('WebAgenticDuoChat.chatSubmitLabel', 'Send chat message.'),
|
|
56
|
+
CHAT_CANCEL_LABEL: translate('WebAgenticDuoChat.chatCancelLabel', 'Cancel'),
|
|
57
|
+
CHAT_DEFAULT_PREDEFINED_PROMPTS: [
|
|
58
|
+
translate(
|
|
59
|
+
'WebAgenticDuoChat.chatDefaultPredefinedPromptsChangePassword',
|
|
60
|
+
'How do I change my password in GitLab?'
|
|
61
|
+
),
|
|
62
|
+
translate(
|
|
63
|
+
'WebAgenticDuoChat.chatDefaultPredefinedPromptsForkProject',
|
|
64
|
+
'How do I fork a project?'
|
|
65
|
+
),
|
|
66
|
+
translate(
|
|
67
|
+
'WebAgenticDuoChat.chatDefaultPredefinedPromptsCloneRepository',
|
|
68
|
+
'How do I clone a repository?'
|
|
69
|
+
),
|
|
70
|
+
translate(
|
|
71
|
+
'WebAgenticDuoChat.chatDefaultPredefinedPromptsCreateTemplate',
|
|
72
|
+
'How do I create a template?'
|
|
73
|
+
),
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const isMessage = (item) => Boolean(item) && item?.role;
|
|
78
|
+
const isSlashCommand = (command) => Boolean(command) && command?.name && command.description;
|
|
79
|
+
|
|
80
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
81
|
+
const itemsValidator = (items) => items.every(isMessage);
|
|
82
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
83
|
+
const slashCommandsValidator = (commands) => commands.every(isSlashCommand);
|
|
84
|
+
|
|
85
|
+
const isThread = (thread) =>
|
|
86
|
+
(typeof thread === 'object' &&
|
|
87
|
+
typeof thread.id === 'string' &&
|
|
88
|
+
typeof thread.lastUpdatedAt === 'string') ||
|
|
89
|
+
(typeof thread.updatedAt === 'string' &&
|
|
90
|
+
(thread.title === null || typeof thread.title === 'string' || typeof thread.goal === 'string'));
|
|
91
|
+
|
|
92
|
+
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
93
|
+
const threadListValidator = (threads) => threads.every(isThread);
|
|
94
|
+
|
|
95
|
+
const localeValidator = (value) => {
|
|
96
|
+
try {
|
|
97
|
+
Intl.getCanonicalLocales(value);
|
|
98
|
+
return true;
|
|
99
|
+
} catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export default {
|
|
105
|
+
name: 'DuoChat',
|
|
106
|
+
components: {
|
|
107
|
+
GlButton,
|
|
108
|
+
GlFormTextarea,
|
|
109
|
+
GlForm,
|
|
110
|
+
DuoChatLoader,
|
|
111
|
+
DuoChatPredefinedPrompts,
|
|
112
|
+
DuoChatConversation,
|
|
113
|
+
WebDuoChatHeader,
|
|
114
|
+
DuoChatThreads,
|
|
115
|
+
GlCard,
|
|
116
|
+
GlDropdownItem,
|
|
117
|
+
},
|
|
118
|
+
directives: {
|
|
119
|
+
SafeHtml,
|
|
120
|
+
},
|
|
121
|
+
props: {
|
|
122
|
+
/**
|
|
123
|
+
* Determines if the component should be resizable. When true, it renders inside
|
|
124
|
+
* a `vue-resizable` wrapper; otherwise, a standard `div` is used.
|
|
125
|
+
*/
|
|
126
|
+
shouldRenderResizable: {
|
|
127
|
+
type: Boolean,
|
|
128
|
+
required: false,
|
|
129
|
+
default: false,
|
|
130
|
+
},
|
|
131
|
+
/**
|
|
132
|
+
* Defines the dimensions of the chat container when resizable.
|
|
133
|
+
* By default, the height is set to match the height of the browser window,
|
|
134
|
+
* and the width is fixed at 400px. The `top` position is left undefined,
|
|
135
|
+
* allowing it to be dynamically adjusted if needed.
|
|
136
|
+
*/
|
|
137
|
+
dimensions: {
|
|
138
|
+
type: Object,
|
|
139
|
+
required: false,
|
|
140
|
+
default: () => ({
|
|
141
|
+
width: undefined,
|
|
142
|
+
height: undefined,
|
|
143
|
+
top: undefined,
|
|
144
|
+
left: undefined,
|
|
145
|
+
maxWidth: undefined,
|
|
146
|
+
minWidth: 400,
|
|
147
|
+
maxHeight: undefined,
|
|
148
|
+
minHeight: 400,
|
|
149
|
+
}),
|
|
150
|
+
},
|
|
151
|
+
agents: {
|
|
152
|
+
type: Array,
|
|
153
|
+
|
|
154
|
+
required: false,
|
|
155
|
+
default: () => [],
|
|
156
|
+
},
|
|
157
|
+
/**
|
|
158
|
+
* The title of the chat/feature.
|
|
159
|
+
*/
|
|
160
|
+
title: {
|
|
161
|
+
type: String,
|
|
162
|
+
required: false,
|
|
163
|
+
default: i18n.CHAT_DEFAULT_TITLE,
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* Array of messages to display in the chat.
|
|
167
|
+
*/
|
|
168
|
+
messages: {
|
|
169
|
+
type: Array,
|
|
170
|
+
required: false,
|
|
171
|
+
default: () => [],
|
|
172
|
+
validator: itemsValidator,
|
|
173
|
+
},
|
|
174
|
+
/**
|
|
175
|
+
* The ID of the active thread (if any).
|
|
176
|
+
*/
|
|
177
|
+
activeThreadId: {
|
|
178
|
+
type: String,
|
|
179
|
+
required: false,
|
|
180
|
+
default: () => '',
|
|
181
|
+
},
|
|
182
|
+
/**
|
|
183
|
+
* The chat page that should be shown.
|
|
184
|
+
*/
|
|
185
|
+
multiThreadedView: {
|
|
186
|
+
type: String,
|
|
187
|
+
required: false,
|
|
188
|
+
default: VIEW_TYPES.LIST,
|
|
189
|
+
validator: (value) => [VIEW_TYPES.LIST, VIEW_TYPES.CHAT].includes(value),
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A non-recoverable error message to display in the chat.
|
|
194
|
+
*/
|
|
195
|
+
error: {
|
|
196
|
+
type: String,
|
|
197
|
+
required: false,
|
|
198
|
+
default: '',
|
|
199
|
+
},
|
|
200
|
+
/**
|
|
201
|
+
* Array of messages to display in the chat.
|
|
202
|
+
*/
|
|
203
|
+
threadList: {
|
|
204
|
+
type: Array,
|
|
205
|
+
required: false,
|
|
206
|
+
default: () => [],
|
|
207
|
+
validator: threadListValidator,
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Whether the chat is currently fetching a response from AI.
|
|
212
|
+
*/
|
|
213
|
+
isLoading: {
|
|
214
|
+
type: Boolean,
|
|
215
|
+
required: false,
|
|
216
|
+
default: false,
|
|
217
|
+
},
|
|
218
|
+
/**
|
|
219
|
+
* Whether the conversational interfaces should be enabled.
|
|
220
|
+
*/
|
|
221
|
+
isChatAvailable: {
|
|
222
|
+
type: Boolean,
|
|
223
|
+
required: false,
|
|
224
|
+
default: true,
|
|
225
|
+
},
|
|
226
|
+
/**
|
|
227
|
+
* Whether the insertCode feature should be available.
|
|
228
|
+
*/
|
|
229
|
+
enableCodeInsertion: {
|
|
230
|
+
type: Boolean,
|
|
231
|
+
required: false,
|
|
232
|
+
default: false,
|
|
233
|
+
},
|
|
234
|
+
/**
|
|
235
|
+
* Array of predefined prompts to display in the chat to start a conversation.
|
|
236
|
+
*/
|
|
237
|
+
predefinedPrompts: {
|
|
238
|
+
type: Array,
|
|
239
|
+
required: false,
|
|
240
|
+
default: () => i18n.CHAT_DEFAULT_PREDEFINED_PROMPTS,
|
|
241
|
+
},
|
|
242
|
+
/**
|
|
243
|
+
* The type of the badge. This is passed down to the `GlExperimentBadge` component. Refer that component for more information.
|
|
244
|
+
*/
|
|
245
|
+
badgeType: {
|
|
246
|
+
type: String || null,
|
|
247
|
+
required: false,
|
|
248
|
+
default: badgeTypes[0],
|
|
249
|
+
validator: badgeTypeValidator,
|
|
250
|
+
},
|
|
251
|
+
/**
|
|
252
|
+
* 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.
|
|
253
|
+
*/
|
|
254
|
+
toolName: {
|
|
255
|
+
type: String,
|
|
256
|
+
required: false,
|
|
257
|
+
default: i18n.CHAT_DEFAULT_TITLE,
|
|
258
|
+
},
|
|
259
|
+
/**
|
|
260
|
+
* Array of slash commands to display in the chat.
|
|
261
|
+
*/
|
|
262
|
+
slashCommands: {
|
|
263
|
+
type: Array,
|
|
264
|
+
required: false,
|
|
265
|
+
default: () => [],
|
|
266
|
+
validator: slashCommandsValidator,
|
|
267
|
+
},
|
|
268
|
+
/**
|
|
269
|
+
* Whether the header should be displayed.
|
|
270
|
+
*/
|
|
271
|
+
showHeader: {
|
|
272
|
+
type: Boolean,
|
|
273
|
+
required: false,
|
|
274
|
+
default: true,
|
|
275
|
+
},
|
|
276
|
+
/**
|
|
277
|
+
* Override the default empty state title text.
|
|
278
|
+
*/
|
|
279
|
+
emptyStateTitle: {
|
|
280
|
+
type: String,
|
|
281
|
+
required: false,
|
|
282
|
+
default: i18n.CHAT_EMPTY_STATE_TITLE,
|
|
283
|
+
},
|
|
284
|
+
/**
|
|
285
|
+
* Override the default chat prompt placeholder text.
|
|
286
|
+
*/
|
|
287
|
+
chatPromptPlaceholder: {
|
|
288
|
+
type: String,
|
|
289
|
+
required: false,
|
|
290
|
+
default: '',
|
|
291
|
+
},
|
|
292
|
+
/**
|
|
293
|
+
* Whether the chat is running in multi-threaded mode
|
|
294
|
+
*/
|
|
295
|
+
isMultithreaded: {
|
|
296
|
+
type: Boolean,
|
|
297
|
+
required: false,
|
|
298
|
+
default: false,
|
|
299
|
+
},
|
|
300
|
+
/**
|
|
301
|
+
* The preferred locale for the chat interface.
|
|
302
|
+
* Follows BCP 47 language tag format (e.g., 'en-US', 'fr-FR', 'es-ES').
|
|
303
|
+
*/
|
|
304
|
+
preferredLocale: {
|
|
305
|
+
type: Array,
|
|
306
|
+
required: false,
|
|
307
|
+
default: () => ['en-US', 'en'],
|
|
308
|
+
validator: localeValidator,
|
|
309
|
+
},
|
|
310
|
+
/**
|
|
311
|
+
* Whether the chat should show the feedback link on the assistant messages.
|
|
312
|
+
*/
|
|
313
|
+
withFeedback: {
|
|
314
|
+
type: Boolean,
|
|
315
|
+
required: false,
|
|
316
|
+
default: true,
|
|
317
|
+
},
|
|
318
|
+
/**
|
|
319
|
+
* Whether the tool call is currently being processed.
|
|
320
|
+
*/
|
|
321
|
+
isToolApprovalProcessing: {
|
|
322
|
+
type: Boolean,
|
|
323
|
+
required: false,
|
|
324
|
+
default: false,
|
|
325
|
+
},
|
|
326
|
+
/**
|
|
327
|
+
* Optional parameter to pass in the working directory - needed for MessageMap Tool type
|
|
328
|
+
*/
|
|
329
|
+
workingDirectory: {
|
|
330
|
+
type: String,
|
|
331
|
+
required: false,
|
|
332
|
+
default: '',
|
|
333
|
+
},
|
|
334
|
+
/**
|
|
335
|
+
* Optional parameter to expose the workflow/session ID in the Agentic Chat UI via a copy Session ID dropdown.
|
|
336
|
+
*/
|
|
337
|
+
sessionId: {
|
|
338
|
+
type: String,
|
|
339
|
+
required: false,
|
|
340
|
+
default: () => '',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
data() {
|
|
344
|
+
return {
|
|
345
|
+
prompt: '',
|
|
346
|
+
scrolledToBottom: true,
|
|
347
|
+
activeCommandIndex: 0,
|
|
348
|
+
canSubmit: true,
|
|
349
|
+
hasValidPrompt: true,
|
|
350
|
+
compositionJustEnded: false,
|
|
351
|
+
contextItemsMenuIsOpen: false,
|
|
352
|
+
contextItemMenuRef: null,
|
|
353
|
+
currentView: this.multiThreadedView,
|
|
354
|
+
maxPromptLength: MAX_PROMPT_LENGTH,
|
|
355
|
+
maxPromptLengthWarning: PROMPT_LENGTH_WARNING,
|
|
356
|
+
promptLengthWarningCount: MAX_PROMPT_LENGTH - PROMPT_LENGTH_WARNING,
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
computed: {
|
|
360
|
+
shouldShowThreadList() {
|
|
361
|
+
return this.isMultithreaded && this.currentView === VIEW_TYPES.LIST;
|
|
362
|
+
},
|
|
363
|
+
withSlashCommands() {
|
|
364
|
+
return this.slashCommands.length > 0;
|
|
365
|
+
},
|
|
366
|
+
hasMessages() {
|
|
367
|
+
return this.messages?.length > 0;
|
|
368
|
+
},
|
|
369
|
+
conversations() {
|
|
370
|
+
if (!this.hasMessages) return [];
|
|
371
|
+
|
|
372
|
+
return this.messages.reduce(
|
|
373
|
+
(acc, message) => {
|
|
374
|
+
if (message.content === CHAT_RESET_MESSAGE) {
|
|
375
|
+
acc.push([]);
|
|
376
|
+
} else {
|
|
377
|
+
acc[acc.length - 1].push(message);
|
|
378
|
+
}
|
|
379
|
+
return acc;
|
|
380
|
+
},
|
|
381
|
+
[[]]
|
|
382
|
+
);
|
|
383
|
+
},
|
|
384
|
+
lastMessage() {
|
|
385
|
+
return this.messages?.[this.messages.length - 1];
|
|
386
|
+
},
|
|
387
|
+
caseInsensitivePrompt() {
|
|
388
|
+
return this.prompt.toLowerCase().trim();
|
|
389
|
+
},
|
|
390
|
+
isPromptEmpty() {
|
|
391
|
+
return this.caseInsensitivePrompt.length === 0;
|
|
392
|
+
},
|
|
393
|
+
isStreaming() {
|
|
394
|
+
return Boolean(
|
|
395
|
+
(this.lastMessage?.chunks?.length > 0 && !this.lastMessage?.content) ||
|
|
396
|
+
typeof this.lastMessage?.chunkId === 'number'
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
filteredSlashCommands() {
|
|
400
|
+
return this.slashCommands
|
|
401
|
+
.filter((c) => c.name.toLowerCase().startsWith(this.caseInsensitivePrompt))
|
|
402
|
+
.filter((c) => {
|
|
403
|
+
if (c.name === CHAT_INCLUDE_MESSAGE) {
|
|
404
|
+
return this.hasContextItemSelectionMenu;
|
|
405
|
+
}
|
|
406
|
+
return true;
|
|
407
|
+
});
|
|
408
|
+
},
|
|
409
|
+
shouldShowSlashCommands() {
|
|
410
|
+
if (!this.withSlashCommands || this.contextItemsMenuIsOpen) return false;
|
|
411
|
+
const startsWithSlash = this.caseInsensitivePrompt.startsWith('/');
|
|
412
|
+
const startsWithSlashCommand = this.slashCommands.some((c) =>
|
|
413
|
+
this.caseInsensitivePrompt.startsWith(c.name)
|
|
414
|
+
);
|
|
415
|
+
return startsWithSlash && this.filteredSlashCommands.length && !startsWithSlashCommand;
|
|
416
|
+
},
|
|
417
|
+
shouldShowContextItemSelectionMenu() {
|
|
418
|
+
if (!this.hasContextItemSelectionMenu) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const isSlash = this.caseInsensitivePrompt === '/';
|
|
423
|
+
if (!this.caseInsensitivePrompt || isSlash) {
|
|
424
|
+
// 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
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return CHAT_INCLUDE_MESSAGE.startsWith(this.caseInsensitivePrompt);
|
|
429
|
+
},
|
|
430
|
+
inputPlaceholder() {
|
|
431
|
+
if (this.chatPromptPlaceholder) {
|
|
432
|
+
return this.chatPromptPlaceholder;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return this.withSlashCommands
|
|
436
|
+
? i18n.CHAT_PROMPT_PLACEHOLDER_WITH_COMMANDS
|
|
437
|
+
: i18n.CHAT_PROMPT_PLACEHOLDER_DEFAULT;
|
|
438
|
+
},
|
|
439
|
+
hasContextItemSelectionMenu() {
|
|
440
|
+
return Boolean(this.contextItemMenuRef);
|
|
441
|
+
},
|
|
442
|
+
activeThread() {
|
|
443
|
+
return this.activeThreadId
|
|
444
|
+
? this.threadList.find((thread) => thread.id === this.activeThreadId)
|
|
445
|
+
: null;
|
|
446
|
+
},
|
|
447
|
+
activeThreadTitle() {
|
|
448
|
+
return this.activeThread?.title;
|
|
449
|
+
},
|
|
450
|
+
activeThreadTitleForView() {
|
|
451
|
+
return (this.currentView === VIEW_TYPES.CHAT && this.activeThreadTitle) || '';
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
watch: {
|
|
455
|
+
multiThreadedView(newView) {
|
|
456
|
+
this.currentView = newView;
|
|
457
|
+
},
|
|
458
|
+
isLoading(loading) {
|
|
459
|
+
if (!loading && !this.isStreaming) {
|
|
460
|
+
this.canSubmit = true; // Re-enable submit button when loading stops
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
isStreaming(streaming) {
|
|
464
|
+
if (!streaming && !this.isLoading) {
|
|
465
|
+
this.canSubmit = true; // Re-enable submit button when streaming stops
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
lastMessage(newMessage) {
|
|
469
|
+
if (this.scrolledToBottom || newMessage?.role.toLowerCase() === MESSAGE_MODEL_ROLES.user) {
|
|
470
|
+
// only scroll to bottom on new message if the user hasn't explicitly scrolled up to view an earlier message
|
|
471
|
+
// or if the user has just submitted a new message
|
|
472
|
+
this.scrollToBottom();
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
shouldShowSlashCommands(shouldShow) {
|
|
476
|
+
if (shouldShow) {
|
|
477
|
+
this.onShowSlashCommands();
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
prompt(newPrompt) {
|
|
481
|
+
this.hasValidPrompt = newPrompt?.length < MAX_PROMPT_LENGTH + 1;
|
|
482
|
+
},
|
|
483
|
+
},
|
|
484
|
+
created() {
|
|
485
|
+
this.handleScrollingThrottled = throttle(this.handleScrolling, 200); // Assume a 200ms throttle for example
|
|
486
|
+
},
|
|
487
|
+
mounted() {
|
|
488
|
+
this.scrollToBottom();
|
|
489
|
+
},
|
|
490
|
+
|
|
491
|
+
methods: {
|
|
492
|
+
onGoBack() {
|
|
493
|
+
this.$emit('back-to-list');
|
|
494
|
+
},
|
|
495
|
+
onNewChat(agent) {
|
|
496
|
+
this.$emit('new-chat', agent);
|
|
497
|
+
|
|
498
|
+
this.$nextTick(() => {
|
|
499
|
+
this.focusChatInput();
|
|
500
|
+
});
|
|
501
|
+
},
|
|
502
|
+
compositionEnd() {
|
|
503
|
+
this.compositionJustEnded = true;
|
|
504
|
+
},
|
|
505
|
+
hideChat() {
|
|
506
|
+
/**
|
|
507
|
+
* Emitted when clicking the cross in the title and the chat gets closed.
|
|
508
|
+
*/
|
|
509
|
+
this.$emit('chat-hidden');
|
|
510
|
+
},
|
|
511
|
+
cancelPrompt() {
|
|
512
|
+
/**
|
|
513
|
+
* Emitted when user clicks the stop button in the textarea
|
|
514
|
+
*/
|
|
515
|
+
this.canSubmit = true;
|
|
516
|
+
this.$emit('chat-cancel');
|
|
517
|
+
this.setPromptAndFocus();
|
|
518
|
+
},
|
|
519
|
+
async sendChatPrompt() {
|
|
520
|
+
if (!this.canSubmit || this.contextItemsMenuIsOpen) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (this.prompt) {
|
|
525
|
+
// Store these before any async operations that might clear the prompt
|
|
526
|
+
const trimmedPrompt = this.prompt.trim();
|
|
527
|
+
const lowerCasePrompt = this.prompt.toLowerCase().trim();
|
|
528
|
+
|
|
529
|
+
if (lowerCasePrompt.startsWith(CHAT_INCLUDE_MESSAGE) && this.hasContextItemSelectionMenu) {
|
|
530
|
+
this.contextItemsMenuIsOpen = true;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Emitted when a new user prompt should be sent out.
|
|
536
|
+
*
|
|
537
|
+
* @param {String} prompt The user prompt to send.
|
|
538
|
+
*/
|
|
539
|
+
this.$emit('send-chat-prompt', trimmedPrompt);
|
|
540
|
+
|
|
541
|
+
// Always clear the prompt after sending, regardless of the command type
|
|
542
|
+
await this.setPromptAndFocus();
|
|
543
|
+
// Check if it was a special command using the stored value (before clearing)
|
|
544
|
+
if (!CHAT_BASE_COMMANDS.includes(lowerCasePrompt)) {
|
|
545
|
+
// Wait for all reactive updates to complete before setting canSubmit
|
|
546
|
+
await this.$nextTick();
|
|
547
|
+
this.canSubmit = false;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
sendPredefinedPrompt(prompt) {
|
|
552
|
+
this.contextItemsMenuIsOpen = false;
|
|
553
|
+
this.prompt = prompt;
|
|
554
|
+
this.sendChatPrompt();
|
|
555
|
+
},
|
|
556
|
+
handleScrolling(event) {
|
|
557
|
+
const { scrollTop, offsetHeight, scrollHeight } = event.target;
|
|
558
|
+
this.scrolledToBottom = scrollTop + offsetHeight >= scrollHeight;
|
|
559
|
+
},
|
|
560
|
+
async scrollToBottom() {
|
|
561
|
+
await this.$nextTick();
|
|
562
|
+
|
|
563
|
+
this.$refs.anchor?.scrollIntoView?.();
|
|
564
|
+
},
|
|
565
|
+
focusChatInput() {
|
|
566
|
+
this.$refs.prompt?.$el?.querySelector?.('textarea')?.focus();
|
|
567
|
+
},
|
|
568
|
+
onTrackFeedback(event) {
|
|
569
|
+
/**
|
|
570
|
+
* Notify listeners about the feedback form submission on a response message.
|
|
571
|
+
* @param {*} event An event, containing the feedback choices and the extended feedback text.
|
|
572
|
+
*/
|
|
573
|
+
this.$emit('track-feedback', event);
|
|
574
|
+
},
|
|
575
|
+
onShowSlashCommands() {
|
|
576
|
+
/**
|
|
577
|
+
* Emitted when user opens the slash commands menu
|
|
578
|
+
*/
|
|
579
|
+
this.$emit('chat-slash');
|
|
580
|
+
},
|
|
581
|
+
sendChatPromptOnEnter(e) {
|
|
582
|
+
const { metaKey, ctrlKey, altKey, shiftKey, isComposing } = e;
|
|
583
|
+
const isModifierKey = metaKey || ctrlKey || altKey || shiftKey;
|
|
584
|
+
|
|
585
|
+
return !(isModifierKey || isComposing || this.compositionJustEnded);
|
|
586
|
+
},
|
|
587
|
+
onInputKeyup(e) {
|
|
588
|
+
const { key } = e;
|
|
589
|
+
if (this.contextItemsMenuIsOpen) {
|
|
590
|
+
if (!this.shouldShowContextItemSelectionMenu) {
|
|
591
|
+
this.contextItemsMenuIsOpen = false;
|
|
592
|
+
}
|
|
593
|
+
this.contextItemMenuRef?.handleKeyUp(e);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (this.caseInsensitivePrompt === CHAT_INCLUDE_MESSAGE) {
|
|
597
|
+
this.contextItemsMenuIsOpen = true;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (this.shouldShowSlashCommands) {
|
|
602
|
+
e.preventDefault();
|
|
603
|
+
|
|
604
|
+
if (key === 'Enter') {
|
|
605
|
+
this.selectSlashCommand(this.activeCommandIndex);
|
|
606
|
+
} else if (key === 'ArrowUp') {
|
|
607
|
+
this.prevCommand();
|
|
608
|
+
} else if (key === 'ArrowDown') {
|
|
609
|
+
this.nextCommand();
|
|
610
|
+
} else {
|
|
611
|
+
this.activeCommandIndex = 0;
|
|
612
|
+
}
|
|
613
|
+
} else if (key === 'Enter' && this.sendChatPromptOnEnter(e)) {
|
|
614
|
+
e.preventDefault();
|
|
615
|
+
|
|
616
|
+
this.sendChatPrompt();
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
this.compositionJustEnded = false;
|
|
620
|
+
},
|
|
621
|
+
prevCommand() {
|
|
622
|
+
this.activeCommandIndex -= 1;
|
|
623
|
+
this.wrapCommandIndex();
|
|
624
|
+
},
|
|
625
|
+
nextCommand() {
|
|
626
|
+
this.activeCommandIndex += 1;
|
|
627
|
+
this.wrapCommandIndex();
|
|
628
|
+
},
|
|
629
|
+
wrapCommandIndex() {
|
|
630
|
+
if (this.activeCommandIndex < 0) {
|
|
631
|
+
this.activeCommandIndex = this.filteredSlashCommands.length - 1;
|
|
632
|
+
} else if (this.activeCommandIndex >= this.filteredSlashCommands.length) {
|
|
633
|
+
this.activeCommandIndex = 0;
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
async setPromptAndFocus(prompt = '') {
|
|
637
|
+
this.prompt = prompt;
|
|
638
|
+
await this.$nextTick();
|
|
639
|
+
this.focusChatInput();
|
|
640
|
+
},
|
|
641
|
+
selectSlashCommand(index) {
|
|
642
|
+
const command = this.filteredSlashCommands[index];
|
|
643
|
+
if (command.shouldSubmit) {
|
|
644
|
+
this.prompt = command.name;
|
|
645
|
+
this.sendChatPrompt();
|
|
646
|
+
} else {
|
|
647
|
+
this.setPromptAndFocus(`${command.name} `);
|
|
648
|
+
|
|
649
|
+
if (command.name === CHAT_INCLUDE_MESSAGE && this.hasContextItemSelectionMenu) {
|
|
650
|
+
this.contextItemsMenuIsOpen = true;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
onInsertCodeSnippet(e) {
|
|
655
|
+
/**
|
|
656
|
+
* Emit insert-code-snippet event that clients can use to interact with a suggested code.
|
|
657
|
+
* @param {*} event An event containing code string in the "detail.code" field.
|
|
658
|
+
*/
|
|
659
|
+
this.$emit('insert-code-snippet', e);
|
|
660
|
+
},
|
|
661
|
+
onCopyCodeSnippet(e) {
|
|
662
|
+
/**
|
|
663
|
+
* Emit copy-code-snippet event that clients can use to interact with a suggested code.
|
|
664
|
+
* @param {*} event An event containing code string in the "detail.code" field.
|
|
665
|
+
*/
|
|
666
|
+
this.$emit('copy-code-snippet', e);
|
|
667
|
+
},
|
|
668
|
+
onCopyMessage(e) {
|
|
669
|
+
/**
|
|
670
|
+
* Emit copy-message event that clients can use to copy chat message content.
|
|
671
|
+
* @param {*} event An event containing code string in the "detail.message" field.
|
|
672
|
+
*/
|
|
673
|
+
this.$emit('copy-message', e);
|
|
674
|
+
},
|
|
675
|
+
onGetContextItemContent(event) {
|
|
676
|
+
/**
|
|
677
|
+
* Emit get-context-item-content event that tells clients to load the full file content for a selected context item.
|
|
678
|
+
* The fully hydrated context item should be updated in the chat message context item.
|
|
679
|
+
* @param {*} event An event containing the message ID and context item to hydrate
|
|
680
|
+
*/
|
|
681
|
+
this.$emit('get-context-item-content', event);
|
|
682
|
+
},
|
|
683
|
+
closeContextItemsMenuOpen() {
|
|
684
|
+
this.contextItemsMenuIsOpen = false;
|
|
685
|
+
this.setPromptAndFocus();
|
|
686
|
+
},
|
|
687
|
+
setContextItemsMenuRef(ref) {
|
|
688
|
+
this.contextItemMenuRef = ref;
|
|
689
|
+
},
|
|
690
|
+
onSelectThread(thread) {
|
|
691
|
+
/**
|
|
692
|
+
* Emitted when a thread is selected from the history.
|
|
693
|
+
* @param {Object} thread The selected thread object
|
|
694
|
+
*/
|
|
695
|
+
this.$emit('thread-selected', thread);
|
|
696
|
+
},
|
|
697
|
+
onDeleteThread(threadId) {
|
|
698
|
+
/**
|
|
699
|
+
* Emitted when a thread is deleted from the history.
|
|
700
|
+
* @param {String} threadId The ID of the thread to delete
|
|
701
|
+
*/
|
|
702
|
+
this.$emit('delete-thread', threadId);
|
|
703
|
+
},
|
|
704
|
+
onApproveToolCall() {
|
|
705
|
+
/**
|
|
706
|
+
* Emitted when a user approves a tool call.
|
|
707
|
+
*/
|
|
708
|
+
this.$emit('approve-tool');
|
|
709
|
+
},
|
|
710
|
+
onDenyToolCall(reason) {
|
|
711
|
+
/**
|
|
712
|
+
* Emitted when a user denies a tool call.
|
|
713
|
+
* @param {String} reason The reason for denying the tool call.
|
|
714
|
+
*/
|
|
715
|
+
this.$emit('deny-tool', reason);
|
|
716
|
+
},
|
|
717
|
+
onOpenFilePath(filePath) {
|
|
718
|
+
/**
|
|
719
|
+
* Emitted when a file path link is clicked in a chat message.
|
|
720
|
+
* @param {String} filePath The file path to open
|
|
721
|
+
*/
|
|
722
|
+
this.$emit('open-file-path', filePath);
|
|
723
|
+
},
|
|
724
|
+
handleUndo(event) {
|
|
725
|
+
event.preventDefault();
|
|
726
|
+
document.execCommand?.('undo');
|
|
727
|
+
},
|
|
728
|
+
handleRedo(event) {
|
|
729
|
+
event.preventDefault();
|
|
730
|
+
document.execCommand?.('redo');
|
|
731
|
+
},
|
|
732
|
+
remainingCharacterCountMessage(count) {
|
|
733
|
+
return sprintf(
|
|
734
|
+
translatePlural(
|
|
735
|
+
'WebAgenticDuoChat.remainingCharacterCountMessage',
|
|
736
|
+
'%{count} character remaining.',
|
|
737
|
+
'%{count} characters remaining.'
|
|
738
|
+
)(count),
|
|
739
|
+
{
|
|
740
|
+
count,
|
|
741
|
+
}
|
|
742
|
+
);
|
|
743
|
+
},
|
|
744
|
+
overLimitCharacterCountMessage(count) {
|
|
745
|
+
return sprintf(
|
|
746
|
+
translatePlural(
|
|
747
|
+
'WebAgenticDuoChat.overLimitCharacterCountMessage',
|
|
748
|
+
'%{count} character over limit.',
|
|
749
|
+
'%{count} characters over limit.'
|
|
750
|
+
)(count),
|
|
751
|
+
{
|
|
752
|
+
count,
|
|
753
|
+
}
|
|
754
|
+
);
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
i18n,
|
|
758
|
+
};
|
|
759
|
+
</script>
|
|
760
|
+
<template>
|
|
761
|
+
<div
|
|
762
|
+
id="chat-component"
|
|
763
|
+
class="markdown-code-block duo-chat gl-bottom-0 gl-flex gl-max-h-full gl-flex-grow gl-flex-col"
|
|
764
|
+
role="complementary"
|
|
765
|
+
data-testid="chat-component"
|
|
766
|
+
>
|
|
767
|
+
<web-duo-chat-header
|
|
768
|
+
v-if="showHeader"
|
|
769
|
+
ref="header"
|
|
770
|
+
:active-thread-id="activeThreadId"
|
|
771
|
+
:title="isMultithreaded && currentView === 'list' ? $options.i18n.CHAT_HISTORY_TITLE : title"
|
|
772
|
+
:subtitle="activeThreadTitleForView"
|
|
773
|
+
:error="error"
|
|
774
|
+
:is-multithreaded="isMultithreaded"
|
|
775
|
+
:current-view="currentView"
|
|
776
|
+
:should-render-resizable="shouldRenderResizable"
|
|
777
|
+
:badge-type="isMultithreaded ? null : badgeType"
|
|
778
|
+
:session-id="sessionId"
|
|
779
|
+
:agents="agents"
|
|
780
|
+
@go-back="onGoBack"
|
|
781
|
+
@new-chat="onNewChat"
|
|
782
|
+
@close="hideChat"
|
|
783
|
+
>
|
|
784
|
+
<template #subheader>
|
|
785
|
+
<slot name="subheader"></slot>
|
|
786
|
+
</template>
|
|
787
|
+
</web-duo-chat-header>
|
|
788
|
+
|
|
789
|
+
<div
|
|
790
|
+
class="gl-flex gl-flex-1 gl-flex-grow gl-flex-col gl-overflow-y-auto gl-overscroll-contain gl-bg-inherit"
|
|
791
|
+
data-testid="chat-history"
|
|
792
|
+
@scroll="handleScrollingThrottled"
|
|
793
|
+
>
|
|
794
|
+
<duo-chat-threads
|
|
795
|
+
v-if="shouldShowThreadList"
|
|
796
|
+
:threads="threadList"
|
|
797
|
+
:preferred-locale="preferredLocale"
|
|
798
|
+
@new-chat="onNewChat"
|
|
799
|
+
@select-thread="onSelectThread"
|
|
800
|
+
@delete-thread="onDeleteThread"
|
|
801
|
+
@close="hideChat"
|
|
802
|
+
/>
|
|
803
|
+
<transition-group
|
|
804
|
+
v-else
|
|
805
|
+
mode="out-in"
|
|
806
|
+
tag="section"
|
|
807
|
+
name="message"
|
|
808
|
+
class="duo-chat-history gl-mt-auto gl-p-5"
|
|
809
|
+
>
|
|
810
|
+
<duo-chat-conversation
|
|
811
|
+
v-for="(conversation, index) in conversations"
|
|
812
|
+
:key="`conversation-${index}`"
|
|
813
|
+
:enable-code-insertion="enableCodeInsertion"
|
|
814
|
+
:messages="conversation"
|
|
815
|
+
:show-delimiter="index > 0"
|
|
816
|
+
:with-feedback="withFeedback"
|
|
817
|
+
:is-tool-approval-processing="isToolApprovalProcessing"
|
|
818
|
+
:working-directory="workingDirectory"
|
|
819
|
+
@track-feedback="onTrackFeedback"
|
|
820
|
+
@insert-code-snippet="onInsertCodeSnippet"
|
|
821
|
+
@copy-code-snippet="onCopyCodeSnippet"
|
|
822
|
+
@copy-message="onCopyMessage"
|
|
823
|
+
@get-context-item-content="onGetContextItemContent"
|
|
824
|
+
@approve-tool="onApproveToolCall"
|
|
825
|
+
@deny-tool="onDenyToolCall"
|
|
826
|
+
@open-file-path="onOpenFilePath"
|
|
827
|
+
/>
|
|
828
|
+
<template v-if="!hasMessages && !isLoading">
|
|
829
|
+
<div
|
|
830
|
+
key="empty-state-message"
|
|
831
|
+
class="duo-chat-message gl-rounded-bl-none gl-leading-20 gl-text-gray-900 gl-break-anywhere"
|
|
832
|
+
data-testid="gl-duo-chat-empty-state"
|
|
833
|
+
>
|
|
834
|
+
<p v-if="emptyStateTitle" data-testid="gl-duo-chat-empty-state-title" class="gl-m-0">
|
|
835
|
+
{{ emptyStateTitle }}
|
|
836
|
+
</p>
|
|
837
|
+
<duo-chat-predefined-prompts
|
|
838
|
+
key="predefined-prompts"
|
|
839
|
+
:prompts="predefinedPrompts"
|
|
840
|
+
@click="sendPredefinedPrompt"
|
|
841
|
+
/>
|
|
842
|
+
</div>
|
|
843
|
+
</template>
|
|
844
|
+
<duo-chat-loader v-if="isLoading" key="loader" :tool-name="toolName" />
|
|
845
|
+
<div key="anchor" ref="anchor" class="scroll-anchor"></div>
|
|
846
|
+
</transition-group>
|
|
847
|
+
</div>
|
|
848
|
+
<footer
|
|
849
|
+
v-if="isChatAvailable && !shouldShowThreadList"
|
|
850
|
+
data-testid="chat-footer"
|
|
851
|
+
class="duo-chat-drawer-footer gl-relative gl-z-2 gl-shrink-0 gl-border-0 gl-bg-default gl-pb-3"
|
|
852
|
+
:class="{ 'duo-chat-drawer-body-scrim-on-footer': !scrolledToBottom }"
|
|
853
|
+
>
|
|
854
|
+
<gl-form data-testid="chat-prompt-form" @submit.stop.prevent="sendChatPrompt">
|
|
855
|
+
<div class="gl-relative gl-max-w-full">
|
|
856
|
+
<!--
|
|
857
|
+
@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" ...`
|
|
858
|
+
-->
|
|
859
|
+
<slot
|
|
860
|
+
name="context-items-menu"
|
|
861
|
+
:is-open="contextItemsMenuIsOpen"
|
|
862
|
+
:on-close="closeContextItemsMenuOpen"
|
|
863
|
+
:set-ref="setContextItemsMenuRef"
|
|
864
|
+
:focus-prompt="focusChatInput"
|
|
865
|
+
></slot>
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
<div
|
|
869
|
+
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"
|
|
870
|
+
>
|
|
871
|
+
<div
|
|
872
|
+
class="gl-flex gl-justify-between gl-border-0 gl-border-b-1 gl-border-solid gl-border-strong gl-px-4 gl-py-4"
|
|
873
|
+
>
|
|
874
|
+
<div>{{ $options.i18n.CHAT_MODEL_PLACEHOLDER }}</div>
|
|
875
|
+
<div><slot name="agentic-switch"></slot></div>
|
|
876
|
+
</div>
|
|
877
|
+
<div :data-value="prompt" class="gl-h-[40px] gl-grow">
|
|
878
|
+
<gl-card
|
|
879
|
+
v-if="shouldShowSlashCommands"
|
|
880
|
+
ref="commands"
|
|
881
|
+
class="slash-commands !gl-absolute gl-w-full -gl-translate-y-full gl-list-none gl-pl-0 gl-shadow-md"
|
|
882
|
+
body-class="!gl-p-2"
|
|
883
|
+
>
|
|
884
|
+
<gl-dropdown-item
|
|
885
|
+
v-for="(command, index) in filteredSlashCommands"
|
|
886
|
+
:key="command.name"
|
|
887
|
+
:class="{ 'active-command': index === activeCommandIndex }"
|
|
888
|
+
@mouseenter.native="activeCommandIndex = index"
|
|
889
|
+
@click="selectSlashCommand(index)"
|
|
890
|
+
>
|
|
891
|
+
<span class="gl-flex gl-justify-between">
|
|
892
|
+
<span class="gl-block">{{ command.name }}</span>
|
|
893
|
+
<small class="gl-pl-3 gl-text-right gl-italic gl-text-subtle">{{
|
|
894
|
+
command.description
|
|
895
|
+
}}</small>
|
|
896
|
+
</span>
|
|
897
|
+
</gl-dropdown-item>
|
|
898
|
+
</gl-card>
|
|
899
|
+
|
|
900
|
+
<gl-form-textarea
|
|
901
|
+
ref="prompt"
|
|
902
|
+
v-model="prompt"
|
|
903
|
+
:disabled="!canSubmit"
|
|
904
|
+
data-testid="chat-prompt-input"
|
|
905
|
+
:placeholder="inputPlaceholder"
|
|
906
|
+
:character-count-limit="maxPromptLength"
|
|
907
|
+
:textarea-classes="[
|
|
908
|
+
'!gl-h-full',
|
|
909
|
+
'!gl-bg-transparent',
|
|
910
|
+
'!gl-py-4',
|
|
911
|
+
'!gl-shadow-none',
|
|
912
|
+
'form-control',
|
|
913
|
+
'gl-form-input',
|
|
914
|
+
'gl-form-textarea',
|
|
915
|
+
{ 'gl-truncate': !prompt },
|
|
916
|
+
]"
|
|
917
|
+
aria-label="Chat prompt input"
|
|
918
|
+
autofocus
|
|
919
|
+
@keydown.enter.exact.native.prevent
|
|
920
|
+
@keydown.ctrl.z.exact="handleUndo"
|
|
921
|
+
@keydown.meta.z.exact="handleUndo"
|
|
922
|
+
@keydown.ctrl.shift.z.exact="handleRedo"
|
|
923
|
+
@keydown.meta.shift.z.exact="handleRedo"
|
|
924
|
+
@keydown.ctrl.y.exact="handleRedo"
|
|
925
|
+
@keydown.meta.y.exact="handleRedo"
|
|
926
|
+
@keyup.native="onInputKeyup"
|
|
927
|
+
@compositionend="compositionEnd"
|
|
928
|
+
>
|
|
929
|
+
<template #remaining-character-count-text="{ count }">
|
|
930
|
+
<span
|
|
931
|
+
v-if="count <= promptLengthWarningCount"
|
|
932
|
+
class="gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3"
|
|
933
|
+
>
|
|
934
|
+
{{ remainingCharacterCountMessage(count) }}
|
|
935
|
+
</span>
|
|
936
|
+
</template>
|
|
937
|
+
<template #character-count-over-limit-text="{ count }">
|
|
938
|
+
<span class="gl-absolute gl-bottom-[-25px] gl-right-px gl-pr-3">{{
|
|
939
|
+
overLimitCharacterCountMessage(count)
|
|
940
|
+
}}</span>
|
|
941
|
+
</template>
|
|
942
|
+
</gl-form-textarea>
|
|
943
|
+
</div>
|
|
944
|
+
<div class="gl-flex gl-justify-end gl-px-3 gl-pb-3">
|
|
945
|
+
<gl-button
|
|
946
|
+
v-if="canSubmit"
|
|
947
|
+
icon="paper-airplane"
|
|
948
|
+
category="primary"
|
|
949
|
+
variant="confirm"
|
|
950
|
+
class="gl-bottom-2 gl-right-2 gl-ml-auto !gl-rounded-full"
|
|
951
|
+
type="submit"
|
|
952
|
+
:disabled="isPromptEmpty || !hasValidPrompt"
|
|
953
|
+
data-testid="chat-prompt-submit-button"
|
|
954
|
+
:aria-label="$options.i18n.CHAT_SUBMIT_LABEL"
|
|
955
|
+
/>
|
|
956
|
+
<gl-button
|
|
957
|
+
v-else
|
|
958
|
+
icon="stop"
|
|
959
|
+
category="primary"
|
|
960
|
+
variant="default"
|
|
961
|
+
class="gl-bottom-2 gl-right-2 !gl-rounded-full"
|
|
962
|
+
data-testid="chat-prompt-cancel-button"
|
|
963
|
+
:aria-label="$options.i18n.CHAT_CANCEL_LABEL"
|
|
964
|
+
@click="cancelPrompt"
|
|
965
|
+
/>
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
</gl-form>
|
|
969
|
+
<slot name="footer-controls"></slot>
|
|
970
|
+
<p
|
|
971
|
+
class="gl-mb-0 gl-mt-3 gl-px-4 gl-text-sm gl-text-secondary"
|
|
972
|
+
:class="{ 'gl-mt-6 sm:gl-mt-3 sm:gl-max-w-1/2': prompt.length >= maxPromptLengthWarning }"
|
|
973
|
+
>
|
|
974
|
+
{{ $options.i18n.CHAT_DISCLAMER }}
|
|
975
|
+
</p>
|
|
976
|
+
</footer>
|
|
977
|
+
</div>
|
|
978
|
+
</template>
|