@gitlab/ui 66.32.1 → 66.34.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 +14 -0
- package/dist/components/experimental/duo/chat/constants.js +2 -1
- package/dist/components/experimental/duo/chat/duo_chat.js +255 -0
- package/dist/components/experimental/duo/chat/mock_data.js +16 -1
- package/dist/index.css +1 -1
- package/dist/index.css.map +1 -1
- package/dist/index.js +1 -0
- package/dist/tokens/css/tokens.css +1 -1
- package/dist/tokens/css/tokens.dark.css +1 -1
- package/dist/tokens/js/tokens.dark.js +1 -1
- package/dist/tokens/js/tokens.js +1 -1
- package/dist/tokens/scss/_tokens.dark.scss +1 -1
- package/dist/tokens/scss/_tokens.scss +1 -1
- package/dist/utility_classes.css +1 -1
- package/dist/utility_classes.css.map +1 -1
- package/package.json +1 -1
- package/src/components/experimental/duo/chat/constants.js +2 -0
- package/src/components/experimental/duo/chat/duo_chat.md +143 -0
- package/src/components/experimental/duo/chat/duo_chat.scss +50 -0
- package/src/components/experimental/duo/chat/duo_chat.spec.js +391 -0
- package/src/components/experimental/duo/chat/duo_chat.stories.js +216 -0
- package/src/components/experimental/duo/chat/duo_chat.vue +372 -0
- package/src/components/experimental/duo/chat/mock_data.js +19 -0
- package/src/index.js +1 -0
- package/src/scss/components.scss +1 -0
- package/src/scss/utilities.scss +8 -0
- package/src/scss/utility-mixins/z-index.scss +4 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import GlButton from '../../../base/button/button.vue';
|
|
2
|
+
import GlAlert from '../../../base/alert/alert.vue';
|
|
3
|
+
import { setStoryTimeout } from '../../../../utils/test_utils';
|
|
4
|
+
import { makeContainer } from '../../../../utils/story_decorators/container';
|
|
5
|
+
import GlDuoChat from './duo_chat.vue';
|
|
6
|
+
import readme from './duo_chat.md';
|
|
7
|
+
import {
|
|
8
|
+
MOCK_RESPONSE_MESSAGE,
|
|
9
|
+
MOCK_USER_PROMPT_MESSAGE,
|
|
10
|
+
generateMockResponseChunks,
|
|
11
|
+
} from './mock_data';
|
|
12
|
+
|
|
13
|
+
const renderMarkdown = (content) => content;
|
|
14
|
+
const renderGFM = () => {};
|
|
15
|
+
|
|
16
|
+
const defaultValue = (prop) =>
|
|
17
|
+
typeof GlDuoChat.props[prop].default === 'function'
|
|
18
|
+
? GlDuoChat.props[prop].default()
|
|
19
|
+
: GlDuoChat.props[prop].default;
|
|
20
|
+
|
|
21
|
+
const generateProps = ({
|
|
22
|
+
title = defaultValue('title'),
|
|
23
|
+
messages = defaultValue('messages'),
|
|
24
|
+
error = defaultValue('error'),
|
|
25
|
+
isLoading = defaultValue('isLoading'),
|
|
26
|
+
isChatAvailable = defaultValue('isChatAvailable'),
|
|
27
|
+
predefinedPrompts = defaultValue('predefinedPrompts'),
|
|
28
|
+
experimentHelpPageUrl = defaultValue('experimentHelpPageUrl'),
|
|
29
|
+
toolName = defaultValue('toolName'),
|
|
30
|
+
} = {}) => ({
|
|
31
|
+
title,
|
|
32
|
+
messages,
|
|
33
|
+
error,
|
|
34
|
+
isLoading,
|
|
35
|
+
isChatAvailable,
|
|
36
|
+
predefinedPrompts,
|
|
37
|
+
experimentHelpPageUrl,
|
|
38
|
+
toolName,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const Default = (args, { argTypes }) => ({
|
|
42
|
+
components: { GlDuoChat },
|
|
43
|
+
props: Object.keys(argTypes),
|
|
44
|
+
provide: {
|
|
45
|
+
renderMarkdown,
|
|
46
|
+
renderGFM,
|
|
47
|
+
},
|
|
48
|
+
template: `
|
|
49
|
+
<gl-duo-chat
|
|
50
|
+
:title="title"
|
|
51
|
+
:messages="messages"
|
|
52
|
+
:error="error"
|
|
53
|
+
:is-loading="isLoading"
|
|
54
|
+
:is-chat-available="isChatAvailable"
|
|
55
|
+
:predefined-prompts="predefinedPrompts"
|
|
56
|
+
:experiment-help-page-url="experimentHelpPageUrl"
|
|
57
|
+
:tool-name="toolName"
|
|
58
|
+
/>`,
|
|
59
|
+
});
|
|
60
|
+
Default.args = generateProps({
|
|
61
|
+
messages: [MOCK_USER_PROMPT_MESSAGE, MOCK_RESPONSE_MESSAGE],
|
|
62
|
+
});
|
|
63
|
+
Default.decorators = [makeContainer({ height: '800px' })];
|
|
64
|
+
|
|
65
|
+
export const Interactive = (args, { argTypes }) => ({
|
|
66
|
+
components: { GlDuoChat, GlButton },
|
|
67
|
+
props: Object.keys(argTypes),
|
|
68
|
+
provide: {
|
|
69
|
+
renderMarkdown,
|
|
70
|
+
renderGFM,
|
|
71
|
+
},
|
|
72
|
+
data() {
|
|
73
|
+
return {
|
|
74
|
+
isHidden: false,
|
|
75
|
+
loggerInfo: '',
|
|
76
|
+
promptInFlight: false,
|
|
77
|
+
msgs: args.messages,
|
|
78
|
+
chunks: [],
|
|
79
|
+
timeout: null,
|
|
80
|
+
requestId: 1,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
methods: {
|
|
84
|
+
onSendChatPrompt(prompt) {
|
|
85
|
+
const newPrompt = {
|
|
86
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
87
|
+
contentHtml: '',
|
|
88
|
+
content: prompt,
|
|
89
|
+
requestId: this.requestId,
|
|
90
|
+
};
|
|
91
|
+
this.loggerInfo += `New prompt: ${JSON.stringify(newPrompt)}\n\n`;
|
|
92
|
+
this.msgs.push(newPrompt);
|
|
93
|
+
this.promptInFlight = true;
|
|
94
|
+
},
|
|
95
|
+
onChatHidden() {
|
|
96
|
+
this.isHidden = true;
|
|
97
|
+
this.loggerInfo += `Chat closed\n\n`;
|
|
98
|
+
},
|
|
99
|
+
showChat() {
|
|
100
|
+
this.isHidden = false;
|
|
101
|
+
this.loggerInfo += `Chat opened\n\n`;
|
|
102
|
+
},
|
|
103
|
+
onResponseRequested() {
|
|
104
|
+
this.timeout = null;
|
|
105
|
+
this.chunks = generateMockResponseChunks(this.requestId);
|
|
106
|
+
this.mockResponseFromAi();
|
|
107
|
+
this.requestId += 1;
|
|
108
|
+
},
|
|
109
|
+
mockResponseFromAi() {
|
|
110
|
+
this.promptInFlight = false;
|
|
111
|
+
if (this.chunks.length) {
|
|
112
|
+
const newResponse = this.chunks.shift();
|
|
113
|
+
const existingMessageIndex = this.msgs.findIndex(
|
|
114
|
+
(msg) => msg.requestId === newResponse.requestId && msg.role === newResponse.role
|
|
115
|
+
);
|
|
116
|
+
const existingMessage = this.msgs[existingMessageIndex];
|
|
117
|
+
if (existingMessage) {
|
|
118
|
+
this.msgs.splice(existingMessageIndex, 1, {
|
|
119
|
+
...existingMessage,
|
|
120
|
+
content: existingMessage.content + newResponse.content,
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
this.msgs.push(newResponse);
|
|
124
|
+
}
|
|
125
|
+
this.logerInfo += `New response: ${JSON.stringify(newResponse)}\n\n`;
|
|
126
|
+
this.timeout = setStoryTimeout(() => {
|
|
127
|
+
this.mockResponseFromAi();
|
|
128
|
+
}, Math.floor(Math.random() * 251) + 50);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
template: `
|
|
133
|
+
<div style="height: 800px">
|
|
134
|
+
<div id="logger" class="gl-w-half">
|
|
135
|
+
<pre class="gl-font-sm" style="text-wrap: wrap">
|
|
136
|
+
<code>{{ loggerInfo }}</code>
|
|
137
|
+
</pre>
|
|
138
|
+
<gl-button v-if="promptInFlight" @click="onResponseRequested">Mock the response</gl-button>
|
|
139
|
+
</div>
|
|
140
|
+
<gl-button v-if="isHidden" @click="showChat">Show chat</gl-button>
|
|
141
|
+
<gl-duo-chat
|
|
142
|
+
v-if="!isHidden"
|
|
143
|
+
:title="title"
|
|
144
|
+
:messages="msgs"
|
|
145
|
+
:error="error"
|
|
146
|
+
:is-loading="promptInFlight"
|
|
147
|
+
:is-chat-available="isChatAvailable"
|
|
148
|
+
:predefined-prompts="predefinedPrompts"
|
|
149
|
+
:experiment-help-page-url="experimentHelpPageUrl"
|
|
150
|
+
:tool-name="toolName"
|
|
151
|
+
@send-chat-prompt="onSendChatPrompt"
|
|
152
|
+
@chat-hidden="onChatHidden"
|
|
153
|
+
/>
|
|
154
|
+
</div>`,
|
|
155
|
+
});
|
|
156
|
+
Interactive.args = generateProps({});
|
|
157
|
+
|
|
158
|
+
export const Slots = (args, { argTypes }) => ({
|
|
159
|
+
components: { GlDuoChat, GlAlert },
|
|
160
|
+
props: Object.keys(argTypes),
|
|
161
|
+
provide: {
|
|
162
|
+
renderMarkdown,
|
|
163
|
+
renderGFM,
|
|
164
|
+
},
|
|
165
|
+
template: `
|
|
166
|
+
<div>
|
|
167
|
+
<gl-duo-chat
|
|
168
|
+
:title="title"
|
|
169
|
+
:messages="messages"
|
|
170
|
+
:error="error"
|
|
171
|
+
:is-loading="isLoading"
|
|
172
|
+
:is-chat-available="isChatAvailable"
|
|
173
|
+
:predefined-prompts="predefinedPrompts"
|
|
174
|
+
:experiment-help-page-url="experimentHelpPageUrl"
|
|
175
|
+
:tool-name="toolName">
|
|
176
|
+
|
|
177
|
+
<template #hero>
|
|
178
|
+
<pre class="code-block rounded code highlight gl-border-b gl-rounded-0! gl-mb-0 gl-overflow-y-auto solarized-light" style="max-height: 20rem; overflow-y: auto;">
|
|
179
|
+
if (firstUserPromptIndex >= 0 && lastUserPromptIndex > firstUserPromptIndex) {
|
|
180
|
+
messages.splice(firstUserPromptIndex, 1);
|
|
181
|
+
return truncateChatPrompt(messages);
|
|
182
|
+
}
|
|
183
|
+
</pre>
|
|
184
|
+
</template>
|
|
185
|
+
<template #subheader>
|
|
186
|
+
<gl-alert
|
|
187
|
+
:dismissible="false"
|
|
188
|
+
variant="warning"
|
|
189
|
+
class="gl-font-sm gl-border-t"
|
|
190
|
+
role="alert"
|
|
191
|
+
data-testid="chat-legal-warning-gitlab-usage"
|
|
192
|
+
primary-button-link="https://internal-handbook.gitlab.io/handbook/product/ai-strategy/ai-integration-effort/legal_restrictions/"
|
|
193
|
+
primary-button-text="Read more"
|
|
194
|
+
>
|
|
195
|
+
<p>
|
|
196
|
+
You are not allowed to copy any part of this output into issues, comments, GitLab source code, commit messages, merge requests or any other user interface in the <code>/gitlab-org</code> or <code>/gitlab-com</code> groups.
|
|
197
|
+
</p>
|
|
198
|
+
</gl-alert>
|
|
199
|
+
</template>
|
|
200
|
+
</gl-duo-chat>
|
|
201
|
+
</div>
|
|
202
|
+
`,
|
|
203
|
+
});
|
|
204
|
+
Slots.decorators = [makeContainer({ height: '800px' })];
|
|
205
|
+
|
|
206
|
+
export default {
|
|
207
|
+
title: 'experimental/duo/chat/duo-chat',
|
|
208
|
+
component: GlDuoChat,
|
|
209
|
+
parameters: {
|
|
210
|
+
docs: {
|
|
211
|
+
description: {
|
|
212
|
+
component: readme,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
};
|
|
@@ -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';
|
package/src/scss/components.scss
CHANGED
|
@@ -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';
|
package/src/scss/utilities.scss
CHANGED
|
@@ -9305,6 +9305,14 @@ $gl-animate-skeleton-loader-max-width: 64 * $grid-size;
|
|
|
9305
9305
|
z-index: 200 !important
|
|
9306
9306
|
}
|
|
9307
9307
|
|
|
9308
|
+
.gl-z-index-999 {
|
|
9309
|
+
z-index: 999
|
|
9310
|
+
}
|
|
9311
|
+
|
|
9312
|
+
.gl-z-index-999\! {
|
|
9313
|
+
z-index: 999 !important
|
|
9314
|
+
}
|
|
9315
|
+
|
|
9308
9316
|
.gl-z-index-9999 {
|
|
9309
9317
|
z-index: 9999
|
|
9310
9318
|
}
|