@gitlab/duo-ui 15.3.0 → 15.4.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 +7 -0
- package/dist/components/agentic_chat/agentic_duo_chat.js +1 -1
- package/dist/components/agentic_chat/web_agentic_duo_chat.js +1 -1
- package/dist/components/chat/components/duo_chat_message/duo_chat_message.js +21 -28
- package/dist/components/chat/components/duo_chat_message/markdown_renderer.js +102 -0
- package/dist/components/chat/components/duo_chat_message/message_types/message_agent.js +5 -6
- package/dist/components/chat/components/duo_chat_message/message_types/message_base.js +27 -14
- package/dist/components/chat/components/duo_chat_message/message_types/message_tool_kv_section.js +1 -1
- package/dist/components/chat/components/duo_chat_message_tool_approval/components/base_tool_params.js +1 -1
- package/dist/components/chat/components/duo_chat_message_tool_approval/components/create_commit_tool_params.js +9 -3
- package/dist/components/chat/components/duo_chat_message_tool_approval/components/pre_block.js +11 -6
- package/dist/components/chat/components/duo_chat_message_tool_approval/components/tool_params_json_view.js +1 -1
- package/dist/components/chat/duo_chat.js +1 -1
- package/dist/components/chat/markdown_renderer.js +31 -5
- package/dist/components/chat/mock_data.js +28 -2
- package/dist/components/chat/web_duo_chat.js +1 -1
- package/dist/components/ui/duo_layout/duo_layout.js +1 -1
- package/dist/components.css +1 -1
- package/dist/components.css.map +1 -1
- package/dist/utils/highlight.js +483 -0
- package/package.json +3 -2
- package/src/components/agentic_chat/agentic_duo_chat.scss +2 -1
- package/src/components/agentic_chat/agentic_duo_chat.vue +1 -1
- package/src/components/agentic_chat/web_agentic_duo_chat.vue +1 -1
- package/src/components/chat/components/duo_chat_message/duo_chat_message.scss +15 -19
- package/src/components/chat/components/duo_chat_message/duo_chat_message.vue +30 -28
- package/src/components/chat/components/duo_chat_message/markdown_renderer.vue +71 -0
- package/src/components/chat/components/duo_chat_message/message_types/message_agent.vue +8 -8
- package/src/components/chat/components/duo_chat_message/message_types/message_base.vue +24 -14
- package/src/components/chat/components/duo_chat_message/message_types/message_tool_kv_section.vue +1 -1
- package/src/components/chat/components/duo_chat_message_tool_approval/components/base_tool_params.vue +1 -1
- package/src/components/chat/components/duo_chat_message_tool_approval/components/create_commit_tool_params.vue +7 -2
- package/src/components/chat/components/duo_chat_message_tool_approval/components/pre_block.vue +11 -6
- package/src/components/chat/components/duo_chat_message_tool_approval/components/tool_params_json_view.vue +1 -1
- package/src/components/chat/duo_chat.vue +1 -1
- package/src/components/chat/markdown_renderer.js +37 -6
- package/src/components/chat/mock_data.js +30 -1
- package/src/components/chat/web_duo_chat.vue +1 -1
- package/src/components/ui/duo_layout/duo_layout.vue +1 -1
- package/src/utils/highlight.js +562 -0
- package/dist/components/chat/components/duo_chat_message_tool_approval/services/highlight.js +0 -21
- package/src/components/chat/components/duo_chat_message_tool_approval/services/highlight.js +0 -18
|
@@ -14,9 +14,9 @@ import { MESSAGE_MODEL_ROLES, SELECTED_CONTEXT_ITEMS_DEFAULT_COLLAPSED } from '.
|
|
|
14
14
|
|
|
15
15
|
import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
|
|
16
16
|
// eslint-disable-next-line no-restricted-imports
|
|
17
|
-
import { renderDuoChatMarkdownPreview } from '../../markdown_renderer';
|
|
18
17
|
import { copyToClipboard, concatUntilEmpty } from '../utils';
|
|
19
18
|
import MessageFeedback from './message_feedback.vue';
|
|
19
|
+
import MarkdownRenderer from './markdown_renderer.vue';
|
|
20
20
|
import { CopyCodeElement } from './copy_code_element';
|
|
21
21
|
import { InsertCodeSnippetElement } from './insert_code_snippet_element';
|
|
22
22
|
|
|
@@ -48,6 +48,7 @@ export default {
|
|
|
48
48
|
GlIcon,
|
|
49
49
|
GlAnimatedLoaderIcon,
|
|
50
50
|
GlButton,
|
|
51
|
+
MarkdownRenderer,
|
|
51
52
|
},
|
|
52
53
|
directives: {
|
|
53
54
|
SafeHtml,
|
|
@@ -64,10 +65,6 @@ export default {
|
|
|
64
65
|
element.classList.add('duo-chat-markdown', 'duo-chat-compact-markdown');
|
|
65
66
|
},
|
|
66
67
|
},
|
|
67
|
-
renderMarkdown: {
|
|
68
|
-
from: 'renderMarkdown',
|
|
69
|
-
default: () => renderDuoChatMarkdownPreview,
|
|
70
|
-
},
|
|
71
68
|
},
|
|
72
69
|
props: {
|
|
73
70
|
/**
|
|
@@ -147,31 +144,25 @@ export default {
|
|
|
147
144
|
hasFeedback() {
|
|
148
145
|
return Boolean(this.message.extras?.hasFeedback);
|
|
149
146
|
},
|
|
147
|
+
hasContentHtml() {
|
|
148
|
+
return this.message.contentHtml?.length > 0;
|
|
149
|
+
},
|
|
150
150
|
defaultContent() {
|
|
151
|
-
if (this.
|
|
151
|
+
if (this.hasContentHtml) {
|
|
152
152
|
return this.message.contentHtml;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
return this.
|
|
155
|
+
return this.message.content;
|
|
156
156
|
},
|
|
157
157
|
messageContent() {
|
|
158
158
|
if (this.isAssistantMessage && this.isChunk) {
|
|
159
|
-
return
|
|
160
|
-
trustedUrls: this.trustedUrls,
|
|
161
|
-
});
|
|
159
|
+
return concatUntilEmpty(this.messageChunks);
|
|
162
160
|
}
|
|
163
161
|
|
|
164
|
-
return (
|
|
165
|
-
this.renderMarkdown(this.defaultContent, { trustedUrls: this.trustedUrls }) ||
|
|
166
|
-
this.renderMarkdown(concatUntilEmpty(this.message.chunks), {
|
|
167
|
-
trustedUrls: this.trustedUrls,
|
|
168
|
-
})
|
|
169
|
-
);
|
|
162
|
+
return this.defaultContent || concatUntilEmpty(this.message.chunks);
|
|
170
163
|
},
|
|
171
164
|
renderedError() {
|
|
172
|
-
return this.
|
|
173
|
-
trustedUrls: this.trustedUrls,
|
|
174
|
-
});
|
|
165
|
+
return this.message.errors?.join('; ');
|
|
175
166
|
},
|
|
176
167
|
error() {
|
|
177
168
|
return Boolean(this.message?.errors?.length) && this.message.errors.join('; ');
|
|
@@ -255,12 +246,14 @@ export default {
|
|
|
255
246
|
this.messageWatcher = null; // Ensure the watcher can't be stopped multiple times
|
|
256
247
|
}
|
|
257
248
|
},
|
|
258
|
-
hydrateContentWithGFM() {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
async hydrateContentWithGFM() {
|
|
250
|
+
await this.$nextTick();
|
|
251
|
+
if (this.isDoneStreaming) {
|
|
252
|
+
if (this.$refs.content) {
|
|
253
|
+
this.renderGFM(this.$refs.content.$el);
|
|
254
|
+
}
|
|
263
255
|
}
|
|
256
|
+
|
|
264
257
|
this.detectScrollableCodeBlocks();
|
|
265
258
|
},
|
|
266
259
|
logEvent(e) {
|
|
@@ -339,6 +332,7 @@ export default {
|
|
|
339
332
|
:class="{
|
|
340
333
|
'gl-pr-7': isAssistantMessage,
|
|
341
334
|
'gl-justify-end gl-pl-7': isUserMessage,
|
|
335
|
+
'duo-chat-message-complete': isDoneStreaming,
|
|
342
336
|
}"
|
|
343
337
|
@insert-code-snippet="onInsertCodeSnippet"
|
|
344
338
|
@copy-code-snippet="onCopyCodeSnippet"
|
|
@@ -356,7 +350,13 @@ export default {
|
|
|
356
350
|
class="error-icon !gl-mt-1 gl-mr-3 gl-shrink-0 gl-text-danger"
|
|
357
351
|
data-testid="error"
|
|
358
352
|
/>
|
|
359
|
-
<
|
|
353
|
+
<markdown-renderer
|
|
354
|
+
v-if="error"
|
|
355
|
+
ref="error-message"
|
|
356
|
+
class="duo-chat-message-error"
|
|
357
|
+
:markdown="renderedError"
|
|
358
|
+
:trusted-urls="trustedUrls"
|
|
359
|
+
/>
|
|
360
360
|
</div>
|
|
361
361
|
<template v-else-if="isAssistantMessage || isUserMessage">
|
|
362
362
|
<div class="gl-sr-only">
|
|
@@ -372,14 +372,16 @@ export default {
|
|
|
372
372
|
@get-content="onGetContextItemContent"
|
|
373
373
|
/>
|
|
374
374
|
|
|
375
|
-
<
|
|
375
|
+
<markdown-renderer
|
|
376
376
|
ref="content"
|
|
377
|
-
v-safe-html:[$options.safeHtmlConfigExtension]="messageContent"
|
|
378
377
|
class="duo-chat-message"
|
|
379
378
|
:class="{
|
|
380
379
|
'gl-bg-feedback-info gl-p-4 gl-text-feedback-info': isUserMessage,
|
|
381
380
|
}"
|
|
382
|
-
|
|
381
|
+
:is-html="hasContentHtml"
|
|
382
|
+
:markdown="messageContent"
|
|
383
|
+
:trusted-urls="trustedUrls"
|
|
384
|
+
/>
|
|
383
385
|
|
|
384
386
|
<documentation-sources v-if="sources" :sources="sources" />
|
|
385
387
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
|
3
|
+
// eslint-disable-next-line no-restricted-imports
|
|
4
|
+
import { renderDuoChatMarkdownPreview } from '../../markdown_renderer';
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
name: 'MarkdownRenderer',
|
|
8
|
+
directives: {
|
|
9
|
+
SafeHtml,
|
|
10
|
+
},
|
|
11
|
+
inject: {
|
|
12
|
+
// Note, we likely might move away from Provide/Inject for this
|
|
13
|
+
// and only ship the versions that are currently in the default
|
|
14
|
+
// See https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3953#note_1762834219
|
|
15
|
+
// for more context.
|
|
16
|
+
renderGFM: {
|
|
17
|
+
from: 'renderGFM',
|
|
18
|
+
default: () => (element) => {
|
|
19
|
+
element.classList.add('gl-markdown', 'gl-compact-markdown', 'gl-text-sm');
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
renderMarkdown: {
|
|
23
|
+
from: 'renderMarkdown',
|
|
24
|
+
default: () => renderDuoChatMarkdownPreview,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
props: {
|
|
28
|
+
markdown: {
|
|
29
|
+
type: String,
|
|
30
|
+
required: false,
|
|
31
|
+
default: '',
|
|
32
|
+
},
|
|
33
|
+
isHtml: {
|
|
34
|
+
type: Boolean,
|
|
35
|
+
required: false,
|
|
36
|
+
default: false,
|
|
37
|
+
},
|
|
38
|
+
trustedUrls: {
|
|
39
|
+
type: Array,
|
|
40
|
+
required: false,
|
|
41
|
+
default: () => [],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
data() {
|
|
45
|
+
return {
|
|
46
|
+
renderedMarkdown: '',
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
watch: {
|
|
50
|
+
async markdown() {
|
|
51
|
+
this.render();
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
created() {
|
|
55
|
+
this.render();
|
|
56
|
+
},
|
|
57
|
+
methods: {
|
|
58
|
+
async render() {
|
|
59
|
+
this.renderedMarkdown = this.isHtml
|
|
60
|
+
? this.markdown
|
|
61
|
+
: await this.renderMarkdown(this.markdown, this.trustedUrls);
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
safeHtmlConfigExtension: {
|
|
65
|
+
ADD_TAGS: ['copy-code', 'insert-code-snippet'],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
</script>
|
|
69
|
+
<template>
|
|
70
|
+
<div ref="content" v-safe-html:[$options.safeHtmlConfigExtension]="renderedMarkdown"></div>
|
|
71
|
+
</template>
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { GlIcon
|
|
2
|
+
import { GlIcon } from '@gitlab/ui';
|
|
3
3
|
import MessageFeedback from '../message_feedback.vue';
|
|
4
|
+
import MarkdownRenderer from '../markdown_renderer.vue';
|
|
4
5
|
import BaseMessage from './message_base.vue';
|
|
5
6
|
|
|
6
7
|
export default {
|
|
7
8
|
name: 'DuoAgentMessage',
|
|
8
9
|
components: {
|
|
9
10
|
BaseMessage,
|
|
10
|
-
MessageFeedback,
|
|
11
11
|
GlIcon,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
SafeHtml,
|
|
12
|
+
MessageFeedback,
|
|
13
|
+
MarkdownRenderer,
|
|
15
14
|
},
|
|
16
15
|
safeHtmlConfigExtension: {
|
|
17
16
|
ADD_TAGS: ['copy-code', 'insert-code-snippet'],
|
|
@@ -57,12 +56,13 @@ export default {
|
|
|
57
56
|
</div>
|
|
58
57
|
</div>
|
|
59
58
|
<slot name="message" v-bind="slotProps">
|
|
60
|
-
<
|
|
59
|
+
<markdown-renderer
|
|
61
60
|
ref="content"
|
|
62
|
-
|
|
61
|
+
:is-html="slotProps.isHtml"
|
|
62
|
+
:markdown="slotProps.content"
|
|
63
63
|
@insert-code-snippet="onInsertCodeSnippet"
|
|
64
64
|
@copy-code-snippet="onCopyCodeSnippet"
|
|
65
|
-
|
|
65
|
+
/>
|
|
66
66
|
</slot>
|
|
67
67
|
|
|
68
68
|
<message-feedback v-if="withFeedback" @feedback="$emit('feedback', $event)" />
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { GlIcon, GlTooltipDirective
|
|
3
|
-
|
|
4
|
-
import { renderDuoChatMarkdownPreview } from '../../../markdown_renderer';
|
|
2
|
+
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
|
3
|
+
import MarkdownRenderer from '../markdown_renderer.vue';
|
|
5
4
|
|
|
6
5
|
export default {
|
|
7
6
|
name: 'DuoBaseMessage',
|
|
8
7
|
components: {
|
|
9
8
|
GlIcon,
|
|
9
|
+
MarkdownRenderer,
|
|
10
10
|
},
|
|
11
11
|
directives: {
|
|
12
|
-
SafeHtml,
|
|
13
12
|
GlTooltip: GlTooltipDirective,
|
|
14
13
|
},
|
|
15
14
|
inject: {
|
|
@@ -23,10 +22,6 @@ export default {
|
|
|
23
22
|
element.classList.add('gl-markdown', 'gl-compact-markdown', 'gl-text-sm');
|
|
24
23
|
},
|
|
25
24
|
},
|
|
26
|
-
renderMarkdown: {
|
|
27
|
-
from: 'renderMarkdown',
|
|
28
|
-
default: () => renderDuoChatMarkdownPreview,
|
|
29
|
-
},
|
|
30
25
|
},
|
|
31
26
|
props: {
|
|
32
27
|
/**
|
|
@@ -41,11 +36,25 @@ export default {
|
|
|
41
36
|
},
|
|
42
37
|
},
|
|
43
38
|
computed: {
|
|
39
|
+
isDoneStreaming() {
|
|
40
|
+
// Agentic chat format
|
|
41
|
+
if (Object.hasOwn(this.message, 'status')) {
|
|
42
|
+
return this.message.status === 'success';
|
|
43
|
+
}
|
|
44
|
+
// Classic chat format
|
|
45
|
+
return !this.isChunk;
|
|
46
|
+
},
|
|
47
|
+
isChunk() {
|
|
48
|
+
return typeof this.message.chunkId === 'number';
|
|
49
|
+
},
|
|
50
|
+
hasContentHtml() {
|
|
51
|
+
return this.message?.contentHtml?.length > 0;
|
|
52
|
+
},
|
|
44
53
|
messageContent() {
|
|
45
|
-
return this.
|
|
54
|
+
return this.hasContentHtml ? this.message.contentHtml : this.message?.content;
|
|
46
55
|
},
|
|
47
56
|
renderedError() {
|
|
48
|
-
return this.
|
|
57
|
+
return this.message.errors?.join('; ') || '';
|
|
49
58
|
},
|
|
50
59
|
hasError() {
|
|
51
60
|
return Boolean(this.message?.errors?.length);
|
|
@@ -61,7 +70,7 @@ export default {
|
|
|
61
70
|
hydrateContentWithGFM() {
|
|
62
71
|
if (this.$refs.content) {
|
|
63
72
|
this.$nextTick(() => {
|
|
64
|
-
this.renderGFM(this.$refs.content, this.message.role);
|
|
73
|
+
this.renderGFM(this.$refs.content.$el, this.message.role);
|
|
65
74
|
});
|
|
66
75
|
}
|
|
67
76
|
},
|
|
@@ -73,6 +82,7 @@ export default {
|
|
|
73
82
|
class="duo-chat-message gl-w-full gl-flex-grow gl-leading-20 gl-break-anywhere"
|
|
74
83
|
:class="{
|
|
75
84
|
'gl-items-top gl-flex': hasError,
|
|
85
|
+
'duo-chat-message-complete': isDoneStreaming,
|
|
76
86
|
}"
|
|
77
87
|
>
|
|
78
88
|
<gl-icon
|
|
@@ -83,10 +93,10 @@ export default {
|
|
|
83
93
|
data-testid="error"
|
|
84
94
|
/>
|
|
85
95
|
<div ref="content-wrapper" :class="{ 'has-error': hasError }">
|
|
86
|
-
<
|
|
96
|
+
<markdown-renderer v-if="hasError" ref="error-message" :markdown="renderedError" />
|
|
87
97
|
<div v-else>
|
|
88
|
-
<slot name="message" v-bind="{ content:
|
|
89
|
-
<
|
|
98
|
+
<slot name="message" v-bind="{ content: messageContent, isHtml: hasContentHtml }">
|
|
99
|
+
<markdown-renderer ref="content" :is-html="hasContentHtml" :markdown="messageContent" />
|
|
90
100
|
</slot>
|
|
91
101
|
</div>
|
|
92
102
|
</div>
|
|
@@ -169,7 +169,7 @@ export default {
|
|
|
169
169
|
|
|
170
170
|
<gl-accordion v-if="message || description" class="-gl-ml-2" :header-level="3">
|
|
171
171
|
<gl-accordion-item v-if="description" class="gl-mb-3" :title="accordionTitle">
|
|
172
|
-
<pre-block
|
|
172
|
+
<pre-block :languages="['markdown']">{{ description }}</pre-block>
|
|
173
173
|
</gl-accordion-item>
|
|
174
174
|
<gl-accordion-item v-if="withJsonView" :title="$options.i18n.EXPAND_JSON_VIEW_TITLE">
|
|
175
175
|
<tool-params-json-view :tool-params="toolParams" />
|
|
@@ -70,6 +70,9 @@ export default {
|
|
|
70
70
|
return sprintf(this.$options.i18n.UNKNOWN_FILE_ACTION_LABEL, { filePath });
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
|
+
getActionFileExtension({ file_path: filePath }) {
|
|
74
|
+
return filePath.split('.').pop() || 'plaintext';
|
|
75
|
+
},
|
|
73
76
|
getActionContent({
|
|
74
77
|
action,
|
|
75
78
|
content,
|
|
@@ -168,7 +171,7 @@ export default {
|
|
|
168
171
|
class="gl-mb-3"
|
|
169
172
|
:title="$options.i18n.READ_COMMIT_MESSAGE"
|
|
170
173
|
>
|
|
171
|
-
<pre-block
|
|
174
|
+
<pre-block :languages="['markdown']">{{ commitMessage }}</pre-block>
|
|
172
175
|
</gl-accordion-item>
|
|
173
176
|
<gl-accordion-item v-if="actionsCount" :title="$options.i18n.EXPAND_CHANGES">
|
|
174
177
|
<gl-accordion :header-level="4">
|
|
@@ -178,7 +181,9 @@ export default {
|
|
|
178
181
|
class="gl-mb-3"
|
|
179
182
|
:title="getActionTitle(action)"
|
|
180
183
|
>
|
|
181
|
-
<pre-block
|
|
184
|
+
<pre-block :languages="[getActionFileExtension(action), 'diff']">{{
|
|
185
|
+
getActionContent(action)
|
|
186
|
+
}}</pre-block>
|
|
182
187
|
</gl-accordion-item>
|
|
183
188
|
</gl-accordion>
|
|
184
189
|
</gl-accordion-item>
|
package/src/components/chat/components/duo_chat_message_tool_approval/components/pre_block.vue
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
|
|
3
3
|
import { translate } from '../../../../../utils/i18n';
|
|
4
|
+
import { highlightElement } from '../../../../../utils/highlight';
|
|
4
5
|
import { copyToClipboard } from '../../utils';
|
|
5
|
-
import { highlightElement } from '../services/highlight';
|
|
6
6
|
|
|
7
7
|
export default {
|
|
8
8
|
name: 'PreBlock',
|
|
@@ -13,10 +13,15 @@ export default {
|
|
|
13
13
|
GlButton,
|
|
14
14
|
},
|
|
15
15
|
props: {
|
|
16
|
-
|
|
17
|
-
type:
|
|
16
|
+
languages: {
|
|
17
|
+
type: Array,
|
|
18
18
|
required: false,
|
|
19
|
-
default:
|
|
19
|
+
default: () => [],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
computed: {
|
|
23
|
+
languageClasses() {
|
|
24
|
+
return this.languages.map((lang) => `language-${lang}`);
|
|
20
25
|
},
|
|
21
26
|
},
|
|
22
27
|
async mounted() {
|
|
@@ -30,7 +35,7 @@ export default {
|
|
|
30
35
|
const codeElement = this.$el.querySelector('code');
|
|
31
36
|
|
|
32
37
|
if (codeElement) {
|
|
33
|
-
highlightElement(codeElement);
|
|
38
|
+
highlightElement(codeElement, this.languages);
|
|
34
39
|
}
|
|
35
40
|
} catch (error) {
|
|
36
41
|
// Silently fail if highlight.js is not available
|
|
@@ -77,5 +82,5 @@ export default {
|
|
|
77
82
|
/>
|
|
78
83
|
</span>
|
|
79
84
|
|
|
80
|
-
<code ref="codeElement" :class="
|
|
85
|
+
<code ref="codeElement" :class="languageClasses" class="!gl-leading-20 gl-break-all"><slot></slot></code></pre>
|
|
81
86
|
</template>
|
|
@@ -32,7 +32,7 @@ export default {
|
|
|
32
32
|
<figcaption class="gl-text-subtle">
|
|
33
33
|
{{ $options.i18n.REQUEST_TEXT }}
|
|
34
34
|
</figcaption>
|
|
35
|
-
<pre-block v-if="hasToolParams"
|
|
35
|
+
<pre-block v-if="hasToolParams" :languages="['json']" data-testid="tool-parameters">{{
|
|
36
36
|
JSON.stringify(toolParams, null, 2)
|
|
37
37
|
}}</pre-block>
|
|
38
38
|
<span v-else class="gl-text-sm gl-text-gray-500" data-testid="no-parameters-message">
|
|
@@ -732,7 +732,7 @@ export default {
|
|
|
732
732
|
'resizable-content': shouldRenderResizable,
|
|
733
733
|
'duo-chat-drawer': !shouldRenderResizable,
|
|
734
734
|
}"
|
|
735
|
-
class="
|
|
735
|
+
class="duo-chat gl-bottom-0 gl-flex gl-max-h-full gl-flex-col"
|
|
736
736
|
role="complementary"
|
|
737
737
|
data-testid="chat-component"
|
|
738
738
|
>
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
// eslint-disable-next-line no-restricted-imports
|
|
2
|
-
import { Marked } from 'marked';
|
|
2
|
+
import { Marked, Renderer } from 'marked';
|
|
3
3
|
import markedBidi from 'marked-bidi';
|
|
4
|
+
import { markedHighlight } from 'marked-highlight';
|
|
4
5
|
import DOMPurify from 'dompurify';
|
|
6
|
+
import { highlightCode } from '../../utils/highlight';
|
|
5
7
|
|
|
8
|
+
const baseOptions = {
|
|
9
|
+
async: true,
|
|
10
|
+
breaks: false,
|
|
11
|
+
gfm: false,
|
|
12
|
+
};
|
|
13
|
+
const baseRenderer = new Renderer({ ...baseOptions });
|
|
14
|
+
const customCodeRenderer = {
|
|
15
|
+
code(...args) {
|
|
16
|
+
const baseCode = baseRenderer.code(...args);
|
|
17
|
+
|
|
18
|
+
// Insert copy-code elements inside the <pre> tag
|
|
19
|
+
return baseCode
|
|
20
|
+
.replace(
|
|
21
|
+
'<pre>',
|
|
22
|
+
'<div class="gl-relative gl-flex js-markdown-code duo-message-pre-block !focus:gl-focus" tabindex="0">\n<copy-code></copy-code>\n<insert-code-snippet></insert-code-snippet>\n<pre class="code code-syntax-highlight-theme duo-message-pre-block gl-grow gl-overflow-auto gl-grid">'
|
|
23
|
+
)
|
|
24
|
+
.replace('</pre>', '</pre></div>');
|
|
25
|
+
},
|
|
26
|
+
};
|
|
6
27
|
const duoMarked = new Marked([
|
|
7
28
|
{
|
|
8
|
-
|
|
9
|
-
breaks: false,
|
|
10
|
-
gfm: false,
|
|
29
|
+
...baseOptions,
|
|
11
30
|
},
|
|
12
31
|
markedBidi(),
|
|
13
32
|
]);
|
|
@@ -134,13 +153,13 @@ function isHtml(markup) {
|
|
|
134
153
|
return Array.from(doc.body.childNodes).some((n) => n.nodeType === Node.ELEMENT_NODE);
|
|
135
154
|
}
|
|
136
155
|
|
|
137
|
-
export function renderDuoChatMarkdownPreview(md, { trustedUrls = [] } = {}) {
|
|
156
|
+
export async function renderDuoChatMarkdownPreview(md, { trustedUrls = [] } = {}) {
|
|
138
157
|
if (!md) return '';
|
|
139
158
|
|
|
140
159
|
DOMPurify.addHook('beforeSanitizeElements', handleImageElements);
|
|
141
160
|
DOMPurify.addHook('afterSanitizeAttributes', sanitizeLinksHook(trustedUrls));
|
|
142
161
|
|
|
143
|
-
const parsedMarkdown = isHtml(md) ? md : duoMarked.parse(md.toString());
|
|
162
|
+
const parsedMarkdown = isHtml(md) ? md : await duoMarked.parse(md.toString());
|
|
144
163
|
|
|
145
164
|
const sanitized = DOMPurify.sanitize(parsedMarkdown, config);
|
|
146
165
|
|
|
@@ -149,6 +168,18 @@ export function renderDuoChatMarkdownPreview(md, { trustedUrls = [] } = {}) {
|
|
|
149
168
|
return sanitized;
|
|
150
169
|
}
|
|
151
170
|
|
|
171
|
+
duoMarked.use(
|
|
172
|
+
markedHighlight({
|
|
173
|
+
async: true,
|
|
174
|
+
langPrefix: 'hljs language-',
|
|
175
|
+
highlight(code, lang) {
|
|
176
|
+
return highlightCode(code, [lang]);
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
duoMarked.use({ renderer: customCodeRenderer });
|
|
182
|
+
|
|
152
183
|
export function addDuoMarkdownPlugin(plugin) {
|
|
153
184
|
duoMarked.use(plugin);
|
|
154
185
|
}
|
|
@@ -91,7 +91,7 @@ export const MOCK_TOOL_MESSAGE = {
|
|
|
91
91
|
|
|
92
92
|
export const MOCK_AGENT_MESSAGE = {
|
|
93
93
|
id: '123',
|
|
94
|
-
|
|
94
|
+
contentHtml: `I'll write a simple Python function with comments for you. Here's an example:\n\n<div class="gl-relative markdown-code-block js-markdown-code"><pre><code class="language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">factorial</span>(<span class="hljs-params">n</span>):
|
|
95
95
|
<span class="hljs-string">"""
|
|
96
96
|
Calculate the factorial of a non-negative integer.
|
|
97
97
|
|
|
@@ -118,6 +118,35 @@ export const MOCK_AGENT_MESSAGE = {
|
|
|
118
118
|
message_type: MESSAGE_MODEL_ROLES.agent,
|
|
119
119
|
};
|
|
120
120
|
|
|
121
|
+
export const MOCK_AGENT_MESSAGE_WITH_MARKDOWN = {
|
|
122
|
+
status: 'success',
|
|
123
|
+
content:
|
|
124
|
+
"The `GENERATE_SERIES` function in PostgreSQL creates a set of values from a start to an end point, which is incredibly useful for generating sequences, filling gaps in data, or creating test datasets.\n\n#### Basic Syntax\n\n```sql\n-- Generate integers from 1 to 10\nSELECT * FROM GENERATE_SERIES(1, 10);\n\n-- Generate with a step value (every 2)\nSELECT * FROM GENERATE_SERIES(1, 10, 2);\n-- Returns: 1, 3, 5, 7, 9\n```\n\n#### Common Use Cases\n\n**1. Generate date ranges:**\n```sql\n-- Generate all dates in January 2025\nSELECT GENERATE_SERIES(\n '2025-01-01'::timestamp,\n '2025-01-31'::timestamp,\n '1 day'::interval\n) AS date;\n```\n\n**2. Fill missing data gaps:**\n```sql\n-- Find missing IDs in a sequence\nSELECT missing_id\nFROM GENERATE_SERIES(1, 100) AS missing_id\nWHERE missing_id NOT IN (SELECT id FROM your_table);\n```\n\n**3. Create test data:**\n```sql\n-- Generate 1000 rows of test data\nSELECT \n id,\n 'User ' || id AS username,\n NOW() - (id || ' days')::interval AS created_at\nFROM GENERATE_SERIES(1, 1000) AS id;\n```\n\n**4. Time-based aggregations:**\n```sql\n-- Group data by hour, even for hours with no data\nSELECT \n hour_slot,\n COUNT(orders.id) AS order_count\nFROM GENERATE_SERIES(\n '2025-01-01 00:00'::timestamp,\n '2025-01-01 23:00'::timestamp,\n '1 hour'::interval\n) AS hour_slot\nLEFT JOIN orders ON DATE_TRUNC('hour', orders.created_at) = hour_slot\nGROUP BY hour_slot\nORDER BY hour_slot;\n```\n\n**Key points:**\n- Works with integers, bigints, numeric types, and timestamps\n- The step parameter is optional (defaults to 1 for numbers, 1 day for timestamps)\n- For descending sequences, use a negative step: `GENERATE_SERIES(10, 1, -1)`\n- Can be used in `FROM` clauses, joins, or subqueries",
|
|
125
|
+
timestamp: '2025-11-29T13:32:50.545772+00:00',
|
|
126
|
+
tool_info: null,
|
|
127
|
+
message_type: 'agent',
|
|
128
|
+
correlation_id: null,
|
|
129
|
+
message_sub_type: null,
|
|
130
|
+
additional_context: null,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const MOCK_AGENT_MESSAGE_WITH_LONG_CODE_BLOCK = {
|
|
134
|
+
status: 'success',
|
|
135
|
+
content: `
|
|
136
|
+
\`\`\`javascript
|
|
137
|
+
${Array.from({ length: 500 })
|
|
138
|
+
.map(() => 'console.log("Hello world")')
|
|
139
|
+
.join('\n\n')}
|
|
140
|
+
\`\`\`
|
|
141
|
+
`,
|
|
142
|
+
timestamp: '2025-11-29T13:32:50.545772+00:00',
|
|
143
|
+
tool_info: null,
|
|
144
|
+
message_type: 'agent',
|
|
145
|
+
correlation_id: null,
|
|
146
|
+
message_sub_type: null,
|
|
147
|
+
additional_context: null,
|
|
148
|
+
};
|
|
149
|
+
|
|
121
150
|
export const MOCK_TOOL_MESSAGE_WITH_LINK = {
|
|
122
151
|
id: '123',
|
|
123
152
|
content: "Search for 'duo.*chat.*message' in directory",
|
|
@@ -738,7 +738,7 @@ export default {
|
|
|
738
738
|
<template>
|
|
739
739
|
<div
|
|
740
740
|
id="chat-component"
|
|
741
|
-
class="
|
|
741
|
+
class="duo-chat web-only gl-bottom-0 gl-flex gl-max-h-full gl-flex-col"
|
|
742
742
|
role="complementary"
|
|
743
743
|
data-testid="chat-component"
|
|
744
744
|
>
|
|
@@ -81,7 +81,7 @@ export default {
|
|
|
81
81
|
@resize:end="updateSize"
|
|
82
82
|
>
|
|
83
83
|
<aside
|
|
84
|
-
class="
|
|
84
|
+
class="duo-chat gl-align-items gl-bottom-0 gl-flex gl-h-full gl-max-h-full gl-flex-row gl-border-subtle"
|
|
85
85
|
>
|
|
86
86
|
<main
|
|
87
87
|
class="content flex-none gl-border-r gl-flex gl-h-full gl-min-w-0 gl-grow gl-overflow-y-auto gl-border-subtle"
|