@gitlab/ui 74.3.1 → 74.5.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 +19 -0
- package/dist/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.js +17 -1
- package/dist/components/experimental/duo/chat/duo_chat.js +2 -2
- package/dist/components/experimental/duo/chat/markdown_renderer.js +18 -0
- package/dist/components/experimental/duo/chat/mock_data.js +19 -9
- 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/package.json +16 -14
- package/src/components/base/card/card.stories.js +27 -18
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.md +5 -1
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js +172 -8
- package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue +18 -1
- package/src/components/experimental/duo/chat/duo_chat.spec.js +0 -2
- package/src/components/experimental/duo/chat/duo_chat.stories.js +2 -14
- package/src/components/experimental/duo/chat/duo_chat.vue +7 -9
- package/src/components/experimental/duo/chat/markdown_renderer.js +20 -0
- package/src/components/experimental/duo/chat/markdown_renderer.spec.js +55 -0
- package/src/components/experimental/duo/chat/mock_data.js +19 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
# [74.5.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v74.4.0...v74.5.0) (2024-02-12)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **DuoChat:** Move global error to header ([7d026ef](https://gitlab.com/gitlab-org/gitlab-ui/commit/7d026effca6cf9fb9a352e09784da401936285c5))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **DuoChat:** Remove unused hero header ([30ab50c](https://gitlab.com/gitlab-org/gitlab-ui/commit/30ab50c30e72f10cca8600d21b5f71238a4dd2c4))
|
|
12
|
+
|
|
13
|
+
# [74.4.0](https://gitlab.com/gitlab-org/gitlab-ui/compare/v74.3.1...v74.4.0) (2024-02-09)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **DuoChat:** Add default markdown renderer for streaming messages ([07fea1c](https://gitlab.com/gitlab-org/gitlab-ui/commit/07fea1ce41a0a42be2860b5652e5e8b35330ae83))
|
|
19
|
+
|
|
1
20
|
## [74.3.1](https://gitlab.com/gitlab-org/gitlab-ui/compare/v74.3.0...v74.3.1) (2024-02-08)
|
|
2
21
|
|
|
3
22
|
|
package/dist/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.js
CHANGED
|
@@ -2,6 +2,7 @@ import GlDuoUserFeedback from '../../../user_feedback/user_feedback';
|
|
|
2
2
|
import { SafeHtmlDirective } from '../../../../../../directives/safe_html/safe_html';
|
|
3
3
|
import { MESSAGE_MODEL_ROLES } from '../../constants';
|
|
4
4
|
import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources';
|
|
5
|
+
import { renderDuoChatMarkdownPreview } from '../../markdown_renderer';
|
|
5
6
|
import { CopyCodeElement } from './copy_code_element';
|
|
6
7
|
import __vue_normalize__ from 'vue-runtime-helpers/dist/normalize-component.js';
|
|
7
8
|
|
|
@@ -23,7 +24,22 @@ var script = {
|
|
|
23
24
|
directives: {
|
|
24
25
|
SafeHtml: SafeHtmlDirective
|
|
25
26
|
},
|
|
26
|
-
inject:
|
|
27
|
+
inject: {
|
|
28
|
+
// Note, we likely might move away from Provide/Inject for this
|
|
29
|
+
// and only ship the versions that are currently in the default
|
|
30
|
+
// See https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3953#note_1762834219
|
|
31
|
+
// for more context.
|
|
32
|
+
renderGFM: {
|
|
33
|
+
from: 'renderGFM',
|
|
34
|
+
default: () => element => {
|
|
35
|
+
element.classList.add('gl-markdown', 'gl-compact-markdown');
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
renderMarkdown: {
|
|
39
|
+
from: 'renderMarkdown',
|
|
40
|
+
default: () => renderDuoChatMarkdownPreview
|
|
41
|
+
}
|
|
42
|
+
},
|
|
27
43
|
props: {
|
|
28
44
|
/**
|
|
29
45
|
* A message object
|
|
@@ -343,11 +343,11 @@ var script = {
|
|
|
343
343
|
const __vue_script__ = script;
|
|
344
344
|
|
|
345
345
|
/* template */
|
|
346
|
-
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (!_vm.isHidden)?_c('aside',{ref:"drawer",staticClass:"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",attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"},on:{"scroll":_vm.handleScrollingTrottled}},[(_vm.showHeader)?_c('header',{staticClass:"gl-drawer-header gl-drawer-header-sticky gl-z-index-200 gl-p-0! gl-border-b-0",attrs:{"data-testid":"chat-header"}},[_c('div',{staticClass:"drawer-title gl-display-flex gl-justify-content-start gl-align-items-center gl-p-5"},[_c('h3',{staticClass:"gl-my-0 gl-font-size-h2"},[_vm._v(_vm._s(_vm.title))]),_vm._v(" "),_c('gl-experiment-badge',{attrs:{"help-page-url":_vm.badgeHelpPageUrl,"type":_vm.badgeType,"container-id":"chat-component"}}),_vm._v(" "),_c('gl-button',{staticClass:"gl-p-0! gl-ml-auto",attrs:{"category":"tertiary","variant":"default","icon":"close","size":"small","data-testid":"chat-close-button","aria-label":_vm.$options.i18n.CHAT_CLOSE_LABEL},on:{"click":_vm.hideChat}})],1),_vm._v(" "),_c('gl-alert',{staticClass:"gl-text-center gl-border-t gl-p-4 gl-text-gray-700 gl-bg-gray-50 legal-warning gl-max-w-full",attrs:{"dismissible":false,"variant":"tip","show-icon":false,"role":"alert","data-testid":"chat-legal-warning"}},[_vm._v(_vm._s(_vm.$options.i18n.CHAT_LEGAL_GENERATED_BY_AI))]),_vm._v(" "),_vm._t("subheader")
|
|
346
|
+
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return (!_vm.isHidden)?_c('aside',{ref:"drawer",staticClass:"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",attrs:{"id":"chat-component","role":"complementary","data-testid":"chat-component"},on:{"scroll":_vm.handleScrollingTrottled}},[(_vm.showHeader)?_c('header',{staticClass:"gl-drawer-header gl-drawer-header-sticky gl-z-index-200 gl-p-0! gl-border-b-0",attrs:{"data-testid":"chat-header"}},[_c('div',{staticClass:"drawer-title gl-display-flex gl-justify-content-start gl-align-items-center gl-p-5"},[_c('h3',{staticClass:"gl-my-0 gl-font-size-h2"},[_vm._v(_vm._s(_vm.title))]),_vm._v(" "),_c('gl-experiment-badge',{attrs:{"help-page-url":_vm.badgeHelpPageUrl,"type":_vm.badgeType,"container-id":"chat-component"}}),_vm._v(" "),_c('gl-button',{staticClass:"gl-p-0! gl-ml-auto",attrs:{"category":"tertiary","variant":"default","icon":"close","size":"small","data-testid":"chat-close-button","aria-label":_vm.$options.i18n.CHAT_CLOSE_LABEL},on:{"click":_vm.hideChat}})],1),_vm._v(" "),_c('gl-alert',{staticClass:"gl-text-center gl-border-t gl-p-4 gl-text-gray-700 gl-bg-gray-50 legal-warning gl-max-w-full",attrs:{"dismissible":false,"variant":"tip","show-icon":false,"role":"alert","data-testid":"chat-legal-warning"}},[_vm._v(_vm._s(_vm.$options.i18n.CHAT_LEGAL_GENERATED_BY_AI))]),_vm._v(" "),_vm._t("subheader"),_vm._v(" "),(_vm.error)?_c('gl-alert',{key:"error",staticClass:"gl-pl-9!",attrs:{"dismissible":false,"variant":"danger","role":"alert","data-testid":"chat-error"}},[_c('span',{directives:[{name:"safe-html",rawName:"v-safe-html",value:(_vm.error),expression:"error"}]})]):_vm._e()],2):_vm._e(),_vm._v(" "),_c('div',{staticClass:"gl-drawer-body gl-display-flex gl-flex-direction-column"},[_c('section',{staticClass:"duo-chat-history gl-display-flex gl-flex-direction-column gl-justify-content-end gl-flex-grow-1 gl-border-b-0 gl-bg-gray-10"},[_c('transition-group',{staticClass:"gl-display-flex gl-flex-direction-column gl-justify-content-end",class:[
|
|
347
347
|
{
|
|
348
348
|
'gl-h-full': !_vm.hasMessages,
|
|
349
349
|
'gl-h-auto': _vm.hasMessages,
|
|
350
|
-
} ],attrs:{"tag":"div","name":"message"}},[_vm._l((_vm.conversations),function(conversation,index){return _c('gl-duo-chat-conversation',{key:("conversation-" + index),attrs:{"messages":conversation,"show-delimiter":index > 0},on:{"track-feedback":_vm.onTrackFeedback}})}),_vm._v(" "),(!_vm.hasMessages && !_vm.isLoading)?[_c('div',{key:"empty-state",staticClass:"gl-display-flex gl-flex-grow-1 gl-mr-auto gl-ml-auto"},[_c('gl-empty-state',{staticClass:"gl-align-self-center",attrs:{"svg-path":_vm.$options.emptySvg,"svg-height":145,"title":_vm.emptyStateTitle,"description":_vm.emptyStateDescription}})],1),_vm._v(" "),_c('gl-duo-chat-predefined-prompts',{key:"predefined-prompts",attrs:{"prompts":_vm.predefinedPrompts},on:{"click":_vm.sendPredefinedPrompt}})]:_vm._e()],2),_vm._v(" "),_c('transition',{attrs:{"name":"loader"}},[(_vm.isLoading)?_c('gl-duo-chat-loader',{staticClass:"gl-px-0!",attrs:{"tool-name":_vm.toolName}}):_vm._e()],1),_vm._v(" "),_c('div',{ref:"anchor",staticClass:"scroll-anchor"})],1)]
|
|
350
|
+
} ],attrs:{"tag":"div","name":"message"}},[_vm._l((_vm.conversations),function(conversation,index){return _c('gl-duo-chat-conversation',{key:("conversation-" + index),attrs:{"messages":conversation,"show-delimiter":index > 0},on:{"track-feedback":_vm.onTrackFeedback}})}),_vm._v(" "),(!_vm.hasMessages && !_vm.isLoading)?[_c('div',{key:"empty-state",staticClass:"gl-display-flex gl-flex-grow-1 gl-mr-auto gl-ml-auto"},[_c('gl-empty-state',{staticClass:"gl-align-self-center",attrs:{"svg-path":_vm.$options.emptySvg,"svg-height":145,"title":_vm.emptyStateTitle,"description":_vm.emptyStateDescription}})],1),_vm._v(" "),_c('gl-duo-chat-predefined-prompts',{key:"predefined-prompts",attrs:{"prompts":_vm.predefinedPrompts},on:{"click":_vm.sendPredefinedPrompt}})]:_vm._e()],2),_vm._v(" "),_c('transition',{attrs:{"name":"loader"}},[(_vm.isLoading)?_c('gl-duo-chat-loader',{staticClass:"gl-px-0!",attrs:{"tool-name":_vm.toolName}}):_vm._e()],1),_vm._v(" "),_c('div',{ref:"anchor",staticClass:"scroll-anchor"})],1)]),_vm._v(" "),(_vm.isChatAvailable)?_c('footer',{staticClass:"gl-drawer-footer gl-drawer-footer-sticky gl-p-5 gl-border-t gl-bg-gray-10",class:{ 'gl-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('gl-form-input-group',{scopedSlots:_vm._u([{key:"append",fn:function(){return [_c('gl-button',{staticClass:"gl-absolute! gl-bottom-2 gl-right-2 gl-rounded-base!",attrs:{"icon":"paper-airplane","category":"primary","variant":"confirm","type":"submit","aria-label":_vm.$options.i18n.CHAT_SUBMIT_LABEL,"disabled":_vm.isLoading}})]},proxy:true}],null,false,2232229068)},[_c('div',{staticClass:"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",attrs:{"data-value":_vm.prompt}},[(_vm.shouldShowSlashCommands)?_c('gl-card',{ref:"commands",staticClass:"slash-commands gl-absolute! gl-translate-y-n100 gl-list-style-none gl-pl-0 gl-w-full 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-display-flex gl-justify-content-space-between"},[_c('span',{staticClass:"gl-display-block"},[_vm._v(_vm._s(command.name))]),_vm._v(" "),_c('small',{staticClass:"gl-text-gray-500 gl-font-style-italic gl-text-right gl-pl-3"},[_vm._v(_vm._s(command.description))])])])}),1):_vm._e(),_vm._v(" "),_c('gl-form-textarea',{ref:"prompt",staticClass:"gl-absolute gl-h-full! gl-py-4! gl-bg-transparent! gl-rounded-top-right-none gl-rounded-bottom-right-none gl-shadow-none!",class:{ 'gl-text-truncate': !_vm.prompt },attrs:{"data-testid":"chat-prompt-input","placeholder":_vm.inputPlaceholder,"disabled":_vm.isLoading,"autofocus":""},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)}},model:{value:(_vm.prompt),callback:function ($$v) {_vm.prompt=$$v;},expression:"prompt"}})],1)]),_vm._v(" "),_c('gl-form-text',{staticClass:"gl-text-gray-400 gl-line-height-20 gl-mt-3",attrs:{"data-testid":"chat-legal-disclaimer"}},[_vm._v(_vm._s(_vm.$options.i18n.CHAT_LEGAL_DISCLAIMER))])],1)],1):_vm._e()]):_vm._e()};
|
|
351
351
|
var __vue_staticRenderFns__ = [];
|
|
352
352
|
|
|
353
353
|
/* style */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Marked } from 'marked';
|
|
2
|
+
import markedBidi from 'marked-bidi';
|
|
3
|
+
|
|
4
|
+
// eslint-disable-next-line no-restricted-imports
|
|
5
|
+
const duoMarked = new Marked([{
|
|
6
|
+
async: false,
|
|
7
|
+
breaks: false,
|
|
8
|
+
gfm: false
|
|
9
|
+
}, markedBidi()]);
|
|
10
|
+
function renderDuoChatMarkdownPreview(md) {
|
|
11
|
+
try {
|
|
12
|
+
return md ? duoMarked.parse(md.toString()) : '';
|
|
13
|
+
} catch {
|
|
14
|
+
return md;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { renderDuoChatMarkdownPreview };
|
|
@@ -31,15 +31,25 @@ const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
|
|
|
31
31
|
id: '123',
|
|
32
32
|
content: `To change your password in GitLab:
|
|
33
33
|
|
|
34
|
-
Log in to your GitLab account.
|
|
35
|
-
Select your avatar in the top right corner and choose Edit profile.
|
|
36
|
-
On the left sidebar, select Password.
|
|
37
|
-
Enter your current password in the Current password field.
|
|
38
|
-
Enter your new password in the New password and Password confirmation fields.
|
|
39
|
-
Select Save password.
|
|
40
|
-
If you don't know your current password, select the I forgot my password link to reset it.
|
|
41
|
-
|
|
42
|
-
GitLab enforces password requirements when you choose a new password
|
|
34
|
+
1. Log in to your GitLab account.
|
|
35
|
+
2. Select your avatar in the top right corner and choose Edit profile.
|
|
36
|
+
3. On the left sidebar, select Password.
|
|
37
|
+
4. Enter your current password in the Current password field.
|
|
38
|
+
5. Enter your new password in the New password and Password confirmation fields.
|
|
39
|
+
6. Select Save password.
|
|
40
|
+
7. If you don't know your current password, select the I forgot my password link to reset it.
|
|
41
|
+
|
|
42
|
+
GitLab enforces password requirements when you choose a new password.
|
|
43
|
+
|
|
44
|
+
~~~yaml
|
|
45
|
+
# And here is a
|
|
46
|
+
# code block
|
|
47
|
+
everyone:
|
|
48
|
+
likes:
|
|
49
|
+
yaml: true
|
|
50
|
+
~~~
|
|
51
|
+
which is rendered while streaming.
|
|
52
|
+
`,
|
|
43
53
|
contentHtml: '',
|
|
44
54
|
role: 'assistant',
|
|
45
55
|
extras: {},
|
package/dist/tokens/js/tokens.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/ui",
|
|
3
|
-
"version": "74.
|
|
3
|
+
"version": "74.5.0",
|
|
4
4
|
"description": "GitLab UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
"echarts": "^5.3.2",
|
|
73
73
|
"iframe-resizer": "^4.3.2",
|
|
74
74
|
"lodash": "^4.17.20",
|
|
75
|
+
"marked": "^12.0.0",
|
|
76
|
+
"marked-bidi": "^1.0.8",
|
|
75
77
|
"portal-vue": "^2.1.6",
|
|
76
78
|
"vue-runtime-helpers": "^1.1.2"
|
|
77
79
|
},
|
|
@@ -99,25 +101,25 @@
|
|
|
99
101
|
"@cypress/grep": "^4.0.1",
|
|
100
102
|
"@gitlab/eslint-plugin": "19.4.0",
|
|
101
103
|
"@gitlab/fonts": "^1.3.0",
|
|
102
|
-
"@gitlab/stylelint-config": "
|
|
104
|
+
"@gitlab/stylelint-config": "6.1.0",
|
|
103
105
|
"@gitlab/svgs": "3.83.0",
|
|
104
106
|
"@rollup/plugin-commonjs": "^11.1.0",
|
|
105
107
|
"@rollup/plugin-node-resolve": "^7.1.3",
|
|
106
108
|
"@rollup/plugin-replace": "^2.3.2",
|
|
107
|
-
"@storybook/addon-a11y": "7.6.
|
|
108
|
-
"@storybook/addon-docs": "7.6.
|
|
109
|
-
"@storybook/addon-essentials": "7.6.
|
|
110
|
-
"@storybook/addon-interactions": "7.6.
|
|
111
|
-
"@storybook/addon-viewport": "7.6.
|
|
112
|
-
"@storybook/builder-webpack5": "7.6.
|
|
109
|
+
"@storybook/addon-a11y": "7.6.13",
|
|
110
|
+
"@storybook/addon-docs": "7.6.13",
|
|
111
|
+
"@storybook/addon-essentials": "7.6.13",
|
|
112
|
+
"@storybook/addon-interactions": "7.6.13",
|
|
113
|
+
"@storybook/addon-viewport": "7.6.13",
|
|
114
|
+
"@storybook/builder-webpack5": "7.6.13",
|
|
113
115
|
"@storybook/jest": "0.2.3",
|
|
114
116
|
"@storybook/test-runner": "0.16.0",
|
|
115
117
|
"@storybook/testing-library": "0.2.2",
|
|
116
|
-
"@storybook/theming": "7.6.
|
|
117
|
-
"@storybook/vue": "7.6.
|
|
118
|
-
"@storybook/vue-webpack5": "7.6.
|
|
119
|
-
"@storybook/vue3": "7.6.
|
|
120
|
-
"@storybook/vue3-webpack5": "7.6.
|
|
118
|
+
"@storybook/theming": "7.6.13",
|
|
119
|
+
"@storybook/vue": "7.6.13",
|
|
120
|
+
"@storybook/vue-webpack5": "7.6.13",
|
|
121
|
+
"@storybook/vue3": "7.6.13",
|
|
122
|
+
"@storybook/vue3-webpack5": "7.6.13",
|
|
121
123
|
"@types/jest": "^29.5.11",
|
|
122
124
|
"@types/jest-image-snapshot": "^6.4.0",
|
|
123
125
|
"@vue/compat": "^3.2.40",
|
|
@@ -170,7 +172,7 @@
|
|
|
170
172
|
"sass-loader": "^10.2.0",
|
|
171
173
|
"sass-true": "^6.1.0",
|
|
172
174
|
"start-server-and-test": "^1.10.6",
|
|
173
|
-
"storybook": "7.6.
|
|
175
|
+
"storybook": "7.6.13",
|
|
174
176
|
"storybook-dark-mode": "3.0.3",
|
|
175
177
|
"style-dictionary": "^3.8.0",
|
|
176
178
|
"stylelint": "15.10.2",
|
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
import readme from './card.md';
|
|
2
2
|
import GlCard from './card.vue';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<gl-card>
|
|
10
|
-
<template #header>
|
|
11
|
-
<h3 class="gl-my-0 gl-font-weight-bold gl-font-lg">This is a custom header</h3>
|
|
12
|
-
</template>
|
|
13
|
-
<template #default>
|
|
14
|
-
Hello World
|
|
15
|
-
</template>
|
|
16
|
-
<template #footer>
|
|
17
|
-
<span>This is a custom footer</span>
|
|
18
|
-
</template>
|
|
19
|
-
</gl-card>`;
|
|
4
|
+
const generateProps = ({ headerClass, bodyClass, footerClass } = {}) => ({
|
|
5
|
+
headerClass,
|
|
6
|
+
bodyClass,
|
|
7
|
+
footerClass,
|
|
8
|
+
});
|
|
20
9
|
|
|
21
10
|
const Template = (args, { argTypes }) => ({
|
|
11
|
+
components: { GlCard },
|
|
22
12
|
props: Object.keys(argTypes),
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
template: `
|
|
14
|
+
<gl-card :header-class="headerClass" :body-class="bodyClass" :footer-class="footerClass">
|
|
15
|
+
<template #header>
|
|
16
|
+
<h3 class="gl-my-0 gl-font-weight-bold gl-font-lg">This is a custom header</h3>
|
|
17
|
+
</template>
|
|
18
|
+
<template #default>
|
|
19
|
+
Hello World
|
|
20
|
+
</template>
|
|
21
|
+
<template #footer>
|
|
22
|
+
<span>This is a custom footer</span>
|
|
23
|
+
</template>
|
|
24
|
+
</gl-card>`,
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
export const Default = Template.bind({});
|
|
28
|
+
Default.args = generateProps();
|
|
28
29
|
|
|
29
30
|
export default {
|
|
30
31
|
title: 'base/card',
|
|
@@ -36,4 +37,12 @@ export default {
|
|
|
36
37
|
},
|
|
37
38
|
},
|
|
38
39
|
},
|
|
40
|
+
argTypes: {
|
|
41
|
+
headerClass: { control: 'text' },
|
|
42
|
+
bodyClass: { control: 'text' },
|
|
43
|
+
footerClass: { control: 'text' },
|
|
44
|
+
header: { control: { disable: true } },
|
|
45
|
+
default: { control: { disable: true } },
|
|
46
|
+
footer: { control: { disable: true } },
|
|
47
|
+
},
|
|
39
48
|
};
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.md
CHANGED
|
@@ -11,7 +11,11 @@ The component represents a Duo Chat message.
|
|
|
11
11
|
There are two ways of pretty-rendering a message's content in the component:
|
|
12
12
|
|
|
13
13
|
- dependency injection, providing functions to convert raw markdown into HTML,
|
|
14
|
-
- sending `contentHtml` prop as part of the `message` property
|
|
14
|
+
- sending `contentHtml` prop as part of the `message` property
|
|
15
|
+
|
|
16
|
+
The component ships a default markdown renderer based on `marked`. It should produce
|
|
17
|
+
reasonably well-looking results while streaming messages. The implementation can be found
|
|
18
|
+
[here](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/src/components/experimental/duo/chat/markdown_renderer.js).
|
|
15
19
|
|
|
16
20
|
### Injecting functions
|
|
17
21
|
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.spec.js
CHANGED
|
@@ -19,14 +19,14 @@ describe('DuoChatMessage', () => {
|
|
|
19
19
|
|
|
20
20
|
const componentFactory = ({ message = MOCK_USER_PROMPT_MESSAGE, options = {} } = {}) => {
|
|
21
21
|
return shallowMount(GlDuoChatMessage, {
|
|
22
|
-
...options,
|
|
23
|
-
propsData: {
|
|
24
|
-
message,
|
|
25
|
-
},
|
|
26
22
|
provide: {
|
|
27
23
|
renderMarkdown,
|
|
28
24
|
renderGFM,
|
|
29
25
|
},
|
|
26
|
+
...options,
|
|
27
|
+
propsData: {
|
|
28
|
+
message,
|
|
29
|
+
},
|
|
30
30
|
});
|
|
31
31
|
};
|
|
32
32
|
|
|
@@ -115,7 +115,7 @@ describe('DuoChatMessage', () => {
|
|
|
115
115
|
|
|
116
116
|
describe('message output', () => {
|
|
117
117
|
it('outputs errors if they are present', async () => {
|
|
118
|
-
const errors = ['
|
|
118
|
+
const errors = ['error1', 'error2', 'error3'];
|
|
119
119
|
|
|
120
120
|
createComponent({
|
|
121
121
|
message: {
|
|
@@ -129,9 +129,10 @@ describe('DuoChatMessage', () => {
|
|
|
129
129
|
|
|
130
130
|
await nextTick();
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
expect(
|
|
134
|
-
expect(
|
|
132
|
+
const contentText = findContent().text();
|
|
133
|
+
expect(contentText).toContain(errors[0]);
|
|
134
|
+
expect(contentText).toContain(errors[1]);
|
|
135
|
+
expect(contentText).toContain(errors[2]);
|
|
135
136
|
});
|
|
136
137
|
|
|
137
138
|
it('outputs contentHtml if it is present', async () => {
|
|
@@ -228,4 +229,167 @@ describe('DuoChatMessage', () => {
|
|
|
228
229
|
expect(renderGFM).toHaveBeenCalled();
|
|
229
230
|
});
|
|
230
231
|
});
|
|
232
|
+
|
|
233
|
+
describe('default renderers', () => {
|
|
234
|
+
it('outputs errors if they are present', async () => {
|
|
235
|
+
const errors = ['error1', 'error2', 'error3'];
|
|
236
|
+
|
|
237
|
+
createComponent({
|
|
238
|
+
options: {
|
|
239
|
+
provide: null,
|
|
240
|
+
},
|
|
241
|
+
message: {
|
|
242
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
243
|
+
errors,
|
|
244
|
+
contentHtml: 'fooHtml barHtml',
|
|
245
|
+
content: 'foo bar',
|
|
246
|
+
chunks: ['a', 'b', 'c'],
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await nextTick();
|
|
251
|
+
|
|
252
|
+
const contentText = findContent().text();
|
|
253
|
+
expect(contentText).toContain(errors[0]);
|
|
254
|
+
expect(contentText).toContain(errors[1]);
|
|
255
|
+
expect(contentText).toContain(errors[2]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('outputs contentHtml if it is present', async () => {
|
|
259
|
+
createComponent({
|
|
260
|
+
options: {
|
|
261
|
+
provide: null,
|
|
262
|
+
},
|
|
263
|
+
message: {
|
|
264
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
265
|
+
errors: [],
|
|
266
|
+
contentHtml: 'fooHtml barHtml',
|
|
267
|
+
content: 'foo bar',
|
|
268
|
+
chunks: ['a', 'b', 'c'],
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
await nextTick();
|
|
273
|
+
|
|
274
|
+
expect(findContent().html()).toBe(
|
|
275
|
+
'<div class="gl-markdown gl-compact-markdown">fooHtml barHtml</div>'
|
|
276
|
+
);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('outputs markdown content if there is no contentHtml', async () => {
|
|
280
|
+
createComponent({
|
|
281
|
+
options: {
|
|
282
|
+
provide: null,
|
|
283
|
+
},
|
|
284
|
+
message: {
|
|
285
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
286
|
+
errors: [],
|
|
287
|
+
contentHtml: '',
|
|
288
|
+
content: 'foo bar',
|
|
289
|
+
chunks: ['a', 'b', 'c'],
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await nextTick();
|
|
294
|
+
|
|
295
|
+
expect(findContent().html()).toBe('<div>\n <p>foo bar</p>\n</div>');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('outputs chunks if there is no content', async () => {
|
|
299
|
+
createComponent({
|
|
300
|
+
options: {
|
|
301
|
+
provide: null,
|
|
302
|
+
},
|
|
303
|
+
message: {
|
|
304
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
305
|
+
errors: [],
|
|
306
|
+
contentHtml: '',
|
|
307
|
+
content: '',
|
|
308
|
+
chunks: ['a', 'b', 'c'],
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
await nextTick();
|
|
313
|
+
|
|
314
|
+
expect(findContent().html()).toBe('<div>\n <p>abc</p>\n</div>');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('sanitizes html produced by errors', async () => {
|
|
318
|
+
createComponent({
|
|
319
|
+
options: {
|
|
320
|
+
provide: null,
|
|
321
|
+
},
|
|
322
|
+
message: {
|
|
323
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
324
|
+
errors: ['[click here](javascript:prompt(1))'],
|
|
325
|
+
contentHtml: '',
|
|
326
|
+
content: '',
|
|
327
|
+
chunks: [],
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await nextTick();
|
|
332
|
+
|
|
333
|
+
expect(findContent().html()).toBe('<div>\n <p><a>click here</a></p>\n</div>');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('sanitizes html produced by content', async () => {
|
|
337
|
+
createComponent({
|
|
338
|
+
options: {
|
|
339
|
+
provide: null,
|
|
340
|
+
},
|
|
341
|
+
message: {
|
|
342
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
343
|
+
errors: [],
|
|
344
|
+
contentHtml: '',
|
|
345
|
+
content: '[click here](javascript:prompt(1))',
|
|
346
|
+
chunks: [],
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
await nextTick();
|
|
351
|
+
|
|
352
|
+
expect(findContent().html()).toBe('<div>\n <p><a>click here</a></p>\n</div>');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('sanitizes html produced by chunks', async () => {
|
|
356
|
+
createComponent({
|
|
357
|
+
options: {
|
|
358
|
+
provide: null,
|
|
359
|
+
},
|
|
360
|
+
message: {
|
|
361
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
362
|
+
errors: [],
|
|
363
|
+
contentHtml: '',
|
|
364
|
+
content: '',
|
|
365
|
+
chunks: ['[click here]', '(javascript:prompt(1))'],
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await nextTick();
|
|
370
|
+
|
|
371
|
+
expect(findContent().html()).toBe('<div>\n <p><a>click here</a></p>\n</div>');
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('sanitizes contentHtml', async () => {
|
|
375
|
+
createComponent({
|
|
376
|
+
options: {
|
|
377
|
+
provide: null,
|
|
378
|
+
},
|
|
379
|
+
message: {
|
|
380
|
+
...MOCK_USER_PROMPT_MESSAGE,
|
|
381
|
+
errors: [],
|
|
382
|
+
contentHtml: `<a href="javascript:prompt(1)">click here</a>`,
|
|
383
|
+
content: '',
|
|
384
|
+
chunks: [],
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
await nextTick();
|
|
389
|
+
|
|
390
|
+
expect(findContent().html()).toBe(
|
|
391
|
+
'<div class="gl-markdown gl-compact-markdown"><a>click here</a></div>'
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
231
395
|
});
|
package/src/components/experimental/duo/chat/components/duo_chat_message/duo_chat_message.vue
CHANGED
|
@@ -3,6 +3,8 @@ import GlDuoUserFeedback from '../../../user_feedback/user_feedback.vue';
|
|
|
3
3
|
import { SafeHtmlDirective as SafeHtml } from '../../../../../../directives/safe_html/safe_html';
|
|
4
4
|
import { MESSAGE_MODEL_ROLES } from '../../constants';
|
|
5
5
|
import DocumentationSources from '../duo_chat_message_sources/duo_chat_message_sources.vue';
|
|
6
|
+
// eslint-disable-next-line no-restricted-imports
|
|
7
|
+
import { renderDuoChatMarkdownPreview } from '../../markdown_renderer';
|
|
6
8
|
import { CopyCodeElement } from './copy_code_element';
|
|
7
9
|
|
|
8
10
|
const concatUntilEmpty = (arr) => {
|
|
@@ -27,7 +29,22 @@ export default {
|
|
|
27
29
|
directives: {
|
|
28
30
|
SafeHtml,
|
|
29
31
|
},
|
|
30
|
-
inject:
|
|
32
|
+
inject: {
|
|
33
|
+
// Note, we likely might move away from Provide/Inject for this
|
|
34
|
+
// and only ship the versions that are currently in the default
|
|
35
|
+
// See https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3953#note_1762834219
|
|
36
|
+
// for more context.
|
|
37
|
+
renderGFM: {
|
|
38
|
+
from: 'renderGFM',
|
|
39
|
+
default: () => (element) => {
|
|
40
|
+
element.classList.add('gl-markdown', 'gl-compact-markdown');
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
renderMarkdown: {
|
|
44
|
+
from: 'renderMarkdown',
|
|
45
|
+
default: () => renderDuoChatMarkdownPreview,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
31
48
|
props: {
|
|
32
49
|
/**
|
|
33
50
|
* A message object
|
|
@@ -191,8 +191,6 @@ describe('GlDuoChat', () => {
|
|
|
191
191
|
|
|
192
192
|
it.each`
|
|
193
193
|
slot | content | isChatAvailable
|
|
194
|
-
${'hero'} | ${slotContent} | ${true}
|
|
195
|
-
${'hero'} | ${slotContent} | ${false}
|
|
196
194
|
${'subheader'} | ${slotContent} | ${false}
|
|
197
195
|
${'subheader'} | ${slotContent} | ${true}
|
|
198
196
|
`(
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
MOCK_USER_PROMPT_MESSAGE,
|
|
10
10
|
generateMockResponseChunks,
|
|
11
11
|
renderGFM,
|
|
12
|
-
renderMarkdown,
|
|
13
12
|
} from './mock_data';
|
|
14
13
|
|
|
15
14
|
const slashCommands = [
|
|
@@ -72,7 +71,6 @@ export const Default = (args, { argTypes }) => ({
|
|
|
72
71
|
components: { GlDuoChat },
|
|
73
72
|
props: Object.keys(argTypes),
|
|
74
73
|
provide: {
|
|
75
|
-
renderMarkdown,
|
|
76
74
|
renderGFM,
|
|
77
75
|
},
|
|
78
76
|
template: `
|
|
@@ -101,7 +99,6 @@ export const Interactive = (args, { argTypes }) => ({
|
|
|
101
99
|
components: { GlDuoChat, GlButton },
|
|
102
100
|
props: Object.keys(argTypes),
|
|
103
101
|
provide: {
|
|
104
|
-
renderMarkdown,
|
|
105
102
|
renderGFM,
|
|
106
103
|
},
|
|
107
104
|
data() {
|
|
@@ -160,7 +157,7 @@ export const Interactive = (args, { argTypes }) => ({
|
|
|
160
157
|
this.logerInfo += `New response: ${JSON.stringify(newResponse)}\n\n`;
|
|
161
158
|
this.timeout = setStoryTimeout(() => {
|
|
162
159
|
this.mockResponseFromAi();
|
|
163
|
-
}, Math.floor(Math.random() * 251) +
|
|
160
|
+
}, Math.floor(Math.random() * 251) + 16);
|
|
164
161
|
}
|
|
165
162
|
},
|
|
166
163
|
},
|
|
@@ -199,7 +196,7 @@ export const Slots = (args, { argTypes }) => ({
|
|
|
199
196
|
components: { GlDuoChat, GlAlert },
|
|
200
197
|
props: Object.keys(argTypes),
|
|
201
198
|
provide: {
|
|
202
|
-
renderMarkdown
|
|
199
|
+
renderMarkdown: (md) => `THIS IS ALTERED MARKDOWN: ${md}`,
|
|
203
200
|
renderGFM,
|
|
204
201
|
},
|
|
205
202
|
template: `
|
|
@@ -218,15 +215,6 @@ export const Slots = (args, { argTypes }) => ({
|
|
|
218
215
|
:empty-state-title="emptyStateTitle"
|
|
219
216
|
:empty-state-description="emptyStateDescription"
|
|
220
217
|
:chat-prompt-placeholder="chatPromptPlaceholder">
|
|
221
|
-
|
|
222
|
-
<template #hero>
|
|
223
|
-
<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;">
|
|
224
|
-
if (firstUserPromptIndex >= 0 && lastUserPromptIndex > firstUserPromptIndex) {
|
|
225
|
-
messages.splice(firstUserPromptIndex, 1);
|
|
226
|
-
return truncateChatPrompt(messages);
|
|
227
|
-
}
|
|
228
|
-
</pre>
|
|
229
|
-
</template>
|
|
230
218
|
<template #subheader>
|
|
231
219
|
<gl-alert
|
|
232
220
|
:dismissible="false"
|
|
@@ -401,24 +401,22 @@ export default {
|
|
|
401
401
|
@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.
|
|
402
402
|
-->
|
|
403
403
|
<slot name="subheader"></slot>
|
|
404
|
-
</header>
|
|
405
|
-
|
|
406
|
-
<div class="gl-drawer-body gl-display-flex gl-flex-direction-column">
|
|
407
|
-
<!-- @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
|
|
408
|
-
-->
|
|
409
|
-
<slot name="hero"></slot>
|
|
410
404
|
|
|
405
|
+
<!-- Ensure that the global error is not scrolled away -->
|
|
411
406
|
<gl-alert
|
|
412
407
|
v-if="error"
|
|
413
408
|
key="error"
|
|
414
409
|
:dismissible="false"
|
|
415
410
|
variant="danger"
|
|
416
|
-
class="gl-
|
|
411
|
+
class="gl-pl-9!"
|
|
417
412
|
role="alert"
|
|
418
413
|
data-testid="chat-error"
|
|
419
|
-
|
|
420
|
-
|
|
414
|
+
>
|
|
415
|
+
<span v-safe-html="error"></span>
|
|
416
|
+
</gl-alert>
|
|
417
|
+
</header>
|
|
421
418
|
|
|
419
|
+
<div class="gl-drawer-body gl-display-flex gl-flex-direction-column">
|
|
422
420
|
<section
|
|
423
421
|
class="duo-chat-history gl-display-flex gl-flex-direction-column gl-justify-content-end gl-flex-grow-1 gl-border-b-0 gl-bg-gray-10"
|
|
424
422
|
>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// eslint-disable-next-line no-restricted-imports
|
|
2
|
+
import { Marked } from 'marked';
|
|
3
|
+
import markedBidi from 'marked-bidi';
|
|
4
|
+
|
|
5
|
+
const duoMarked = new Marked([
|
|
6
|
+
{
|
|
7
|
+
async: false,
|
|
8
|
+
breaks: false,
|
|
9
|
+
gfm: false,
|
|
10
|
+
},
|
|
11
|
+
markedBidi(),
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function renderDuoChatMarkdownPreview(md) {
|
|
15
|
+
try {
|
|
16
|
+
return md ? duoMarked.parse(md.toString()) : '';
|
|
17
|
+
} catch {
|
|
18
|
+
return md;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Parser } from 'marked';
|
|
2
|
+
import { renderDuoChatMarkdownPreview } from './markdown_renderer';
|
|
3
|
+
|
|
4
|
+
describe('Duo Chat Markdown renderer', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
jest.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('renders a few edge cases', () => {
|
|
10
|
+
expect(renderDuoChatMarkdownPreview('')).toEqual('');
|
|
11
|
+
expect(renderDuoChatMarkdownPreview(null)).toEqual('');
|
|
12
|
+
expect(renderDuoChatMarkdownPreview(undefined)).toEqual('');
|
|
13
|
+
expect(renderDuoChatMarkdownPreview(5)).toEqual('<p>5</p>\n');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders a simple paragraph', () => {
|
|
17
|
+
expect(renderDuoChatMarkdownPreview('Hello world')).toEqual('<p>Hello world</p>\n');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('auto-closes an open code block', () => {
|
|
21
|
+
expect(renderDuoChatMarkdownPreview('```yaml\n# comment')).toEqual(
|
|
22
|
+
'<pre><code class="language-yaml"># comment\n</code></pre>\n'
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders standard markdown syntax', () => {
|
|
27
|
+
expect(renderDuoChatMarkdownPreview('*italic*')).toEqual(`<p><em>italic</em></p>\n`);
|
|
28
|
+
expect(renderDuoChatMarkdownPreview('_italic_')).toEqual(`<p><em>italic</em></p>\n`);
|
|
29
|
+
expect(renderDuoChatMarkdownPreview('**bold**')).toEqual(`<p><strong>bold</strong></p>\n`);
|
|
30
|
+
expect(renderDuoChatMarkdownPreview('~~strike~~')).toEqual(`<p><del>strike</del></p>\n`);
|
|
31
|
+
expect(renderDuoChatMarkdownPreview('https://example.org')).toEqual(
|
|
32
|
+
`<p><a href="https://example.org">https://example.org</a></p>\n`
|
|
33
|
+
);
|
|
34
|
+
expect(renderDuoChatMarkdownPreview('[example](https://example.org)')).toEqual(
|
|
35
|
+
`<p><a href="https://example.org">example</a></p>\n`
|
|
36
|
+
);
|
|
37
|
+
expect(renderDuoChatMarkdownPreview('1. first\n2. second')).toEqual(
|
|
38
|
+
`<ol>\n<li>first</li>\n<li>second</li>\n</ol>\n`
|
|
39
|
+
);
|
|
40
|
+
expect(renderDuoChatMarkdownPreview('- first\n- second')).toEqual(
|
|
41
|
+
`<ul>\n<li>first</li>\n<li>second</li>\n</ul>\n`
|
|
42
|
+
);
|
|
43
|
+
expect(renderDuoChatMarkdownPreview('* first\n* second')).toEqual(
|
|
44
|
+
`<ul>\n<li>first</li>\n<li>second</li>\n</ul>\n`
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns content as-is if marked throws', () => {
|
|
49
|
+
jest.spyOn(Parser.prototype, 'parse').mockImplementationOnce(() => {
|
|
50
|
+
throw new Error('I am a broken parser');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(renderDuoChatMarkdownPreview('Hello world')).toEqual('Hello world');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -39,15 +39,25 @@ export const MOCK_RESPONSE_MESSAGE_FOR_STREAMING = {
|
|
|
39
39
|
id: '123',
|
|
40
40
|
content: `To change your password in GitLab:
|
|
41
41
|
|
|
42
|
-
Log in to your GitLab account.
|
|
43
|
-
Select your avatar in the top right corner and choose Edit profile.
|
|
44
|
-
On the left sidebar, select Password.
|
|
45
|
-
Enter your current password in the Current password field.
|
|
46
|
-
Enter your new password in the New password and Password confirmation fields.
|
|
47
|
-
Select Save password.
|
|
48
|
-
If you don't know your current password, select the I forgot my password link to reset it.
|
|
49
|
-
|
|
50
|
-
GitLab enforces password requirements when you choose a new password
|
|
42
|
+
1. Log in to your GitLab account.
|
|
43
|
+
2. Select your avatar in the top right corner and choose Edit profile.
|
|
44
|
+
3. On the left sidebar, select Password.
|
|
45
|
+
4. Enter your current password in the Current password field.
|
|
46
|
+
5. Enter your new password in the New password and Password confirmation fields.
|
|
47
|
+
6. Select Save password.
|
|
48
|
+
7. If you don't know your current password, select the I forgot my password link to reset it.
|
|
49
|
+
|
|
50
|
+
GitLab enforces password requirements when you choose a new password.
|
|
51
|
+
|
|
52
|
+
~~~yaml
|
|
53
|
+
# And here is a
|
|
54
|
+
# code block
|
|
55
|
+
everyone:
|
|
56
|
+
likes:
|
|
57
|
+
yaml: true
|
|
58
|
+
~~~
|
|
59
|
+
which is rendered while streaming.
|
|
60
|
+
`,
|
|
51
61
|
contentHtml: '',
|
|
52
62
|
role: 'assistant',
|
|
53
63
|
extras: {},
|