@cognigy/chat-components-vue 0.1.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.
Files changed (81) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +178 -0
  3. package/dist/assets/svg/ArrowBackIcon.vue.d.ts +9 -0
  4. package/dist/assets/svg/AudioPauseIcon.vue.d.ts +2 -0
  5. package/dist/assets/svg/AudioPlayIcon.vue.d.ts +2 -0
  6. package/dist/assets/svg/CloseIcon.vue.d.ts +2 -0
  7. package/dist/assets/svg/DownloadIcon.vue.d.ts +2 -0
  8. package/dist/assets/svg/VideoPlayIcon.vue.d.ts +9 -0
  9. package/dist/assets/svg/index.d.ts +7 -0
  10. package/dist/chat-components-vue.css +1 -0
  11. package/dist/chat-components-vue.js +9858 -0
  12. package/dist/components/Message.vue.d.ts +11 -0
  13. package/dist/components/common/ActionButton.vue.d.ts +59 -0
  14. package/dist/components/common/ActionButtons.vue.d.ts +36 -0
  15. package/dist/components/common/ChatBubble.vue.d.ts +22 -0
  16. package/dist/components/common/ChatEvent.vue.d.ts +20 -0
  17. package/dist/components/common/LinkIcon.vue.d.ts +2 -0
  18. package/dist/components/common/TypingIndicator.vue.d.ts +21 -0
  19. package/dist/components/common/Typography.vue.d.ts +38 -0
  20. package/dist/components/messages/AdaptiveCard.vue.d.ts +2 -0
  21. package/dist/components/messages/AudioMessage.vue.d.ts +5 -0
  22. package/dist/components/messages/DatePicker.vue.d.ts +2 -0
  23. package/dist/components/messages/FileMessage.vue.d.ts +2 -0
  24. package/dist/components/messages/Gallery.vue.d.ts +2 -0
  25. package/dist/components/messages/GalleryItem.vue.d.ts +7 -0
  26. package/dist/components/messages/ImageMessage.vue.d.ts +5 -0
  27. package/dist/components/messages/List.vue.d.ts +2 -0
  28. package/dist/components/messages/ListItem.vue.d.ts +16 -0
  29. package/dist/components/messages/TextMessage.vue.d.ts +15 -0
  30. package/dist/components/messages/TextWithButtons.vue.d.ts +2 -0
  31. package/dist/components/messages/VideoMessage.vue.d.ts +5 -0
  32. package/dist/composables/useChannelPayload.d.ts +47 -0
  33. package/dist/composables/useCollation.d.ts +47 -0
  34. package/dist/composables/useImageContext.d.ts +13 -0
  35. package/dist/composables/useMessageContext.d.ts +18 -0
  36. package/dist/composables/useSanitize.d.ts +8 -0
  37. package/dist/index.d.ts +33 -0
  38. package/dist/types/index.d.ts +275 -0
  39. package/dist/utils/helpers.d.ts +56 -0
  40. package/dist/utils/matcher.d.ts +20 -0
  41. package/dist/utils/sanitize.d.ts +28 -0
  42. package/dist/utils/theme.d.ts +18 -0
  43. package/package.json +94 -0
  44. package/src/assets/svg/ArrowBackIcon.vue +30 -0
  45. package/src/assets/svg/AudioPauseIcon.vue +20 -0
  46. package/src/assets/svg/AudioPlayIcon.vue +19 -0
  47. package/src/assets/svg/CloseIcon.vue +10 -0
  48. package/src/assets/svg/DownloadIcon.vue +10 -0
  49. package/src/assets/svg/VideoPlayIcon.vue +25 -0
  50. package/src/assets/svg/index.ts +7 -0
  51. package/src/components/Message.vue +152 -0
  52. package/src/components/common/ActionButton.vue +354 -0
  53. package/src/components/common/ActionButtons.vue +170 -0
  54. package/src/components/common/ChatBubble.vue +109 -0
  55. package/src/components/common/ChatEvent.vue +84 -0
  56. package/src/components/common/LinkIcon.vue +34 -0
  57. package/src/components/common/TypingIndicator.vue +202 -0
  58. package/src/components/common/Typography.vue +196 -0
  59. package/src/components/messages/AdaptiveCard.vue +292 -0
  60. package/src/components/messages/AudioMessage.vue +391 -0
  61. package/src/components/messages/DatePicker.vue +135 -0
  62. package/src/components/messages/FileMessage.vue +195 -0
  63. package/src/components/messages/Gallery.vue +296 -0
  64. package/src/components/messages/GalleryItem.vue +214 -0
  65. package/src/components/messages/ImageMessage.vue +368 -0
  66. package/src/components/messages/List.vue +149 -0
  67. package/src/components/messages/ListItem.vue +344 -0
  68. package/src/components/messages/TextMessage.vue +203 -0
  69. package/src/components/messages/TextWithButtons.vue +119 -0
  70. package/src/components/messages/VideoMessage.vue +343 -0
  71. package/src/composables/useChannelPayload.ts +101 -0
  72. package/src/composables/useCollation.ts +163 -0
  73. package/src/composables/useImageContext.ts +27 -0
  74. package/src/composables/useMessageContext.ts +41 -0
  75. package/src/composables/useSanitize.ts +25 -0
  76. package/src/index.ts +71 -0
  77. package/src/types/index.ts +373 -0
  78. package/src/utils/helpers.ts +164 -0
  79. package/src/utils/matcher.ts +283 -0
  80. package/src/utils/sanitize.ts +133 -0
  81. package/src/utils/theme.ts +58 -0
@@ -0,0 +1,149 @@
1
+ <template>
2
+ <div
3
+ v-if="elements.length > 0"
4
+ :class="['webchat-list-template-root', $style.wrapper]"
5
+ :id="listTemplateId"
6
+ data-testid="list-message"
7
+ >
8
+ <!-- Header element (first element if top_element_style is large) -->
9
+ <ListItem
10
+ v-if="headerElement"
11
+ :element="headerElement"
12
+ isHeaderElement
13
+ headingLevel="h4"
14
+ :id="`header-${listTemplateId}`"
15
+ />
16
+
17
+ <!-- Regular list items -->
18
+ <ul
19
+ :aria-labelledby="headerElement ? `listHeader-header-${listTemplateId}` : undefined"
20
+ :class="$style.list"
21
+ >
22
+ <ListItem
23
+ v-for="(element, index) in regularElements"
24
+ :key="index"
25
+ :element="element"
26
+ :headingLevel="headerElement ? 'h5' : 'h4'"
27
+ :id="`${listTemplateId}-${index}`"
28
+ :dividerBefore="index > 0"
29
+ :dividerAfter="Boolean(globalButton && index === regularElements.length - 1)"
30
+ />
31
+ </ul>
32
+
33
+ <!-- Global button at bottom -->
34
+ <ActionButtons
35
+ v-if="globalButton"
36
+ :payload="[globalButton]"
37
+ :action="shouldBeDisabled ? undefined : action"
38
+ buttonClassName="webchat-list-template-global-button"
39
+ :containerClassName="$style.mainButtonWrapper"
40
+ :config="config"
41
+ :dataMessageId="dataMessageId"
42
+ :onEmitAnalytics="onEmitAnalytics"
43
+ size="large"
44
+ />
45
+ </div>
46
+ </template>
47
+
48
+ <script setup lang="ts">
49
+ import { computed, onMounted, useCssModule } from 'vue'
50
+ import ListItem from './ListItem.vue'
51
+ import ActionButtons from '../common/ActionButtons.vue'
52
+ import { useMessageContext } from '../../composables/useMessageContext'
53
+ import { getChannelPayload } from '../../utils/matcher'
54
+ import { getRandomId } from '../../utils/helpers'
55
+ import type { IWebchatTemplateAttachment } from '../../types'
56
+
57
+ const $style = useCssModule()
58
+
59
+ // Message context
60
+ const { message, config, action, onEmitAnalytics } = useMessageContext()
61
+ const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
62
+
63
+ // Get list data from message payload
64
+ const payload = computed(() => getChannelPayload(message, config))
65
+ const attachment = computed(() => payload.value?.message?.attachment as IWebchatTemplateAttachment | undefined)
66
+
67
+ // Extract list elements and configuration
68
+ const elements = computed(() => {
69
+ return attachment.value?.payload?.elements || []
70
+ })
71
+
72
+ const topElementStyle = computed(() => {
73
+ return attachment.value?.payload?.top_element_style
74
+ })
75
+
76
+ const showTopElementLarge = computed(() => {
77
+ return topElementStyle.value === 'large' || topElementStyle.value === true
78
+ })
79
+
80
+ // Split elements into header and regular items
81
+ const headerElement = computed(() => {
82
+ return showTopElementLarge.value ? elements.value[0] : null
83
+ })
84
+
85
+ const regularElements = computed(() => {
86
+ return showTopElementLarge.value ? elements.value.slice(1) : elements.value
87
+ })
88
+
89
+ // Global button (first button in buttons array)
90
+ const globalButton = computed(() => {
91
+ const buttons = attachment.value?.payload?.buttons
92
+ return buttons?.[0]
93
+ })
94
+
95
+ // Should buttons be disabled
96
+ const shouldBeDisabled = computed(() => {
97
+ // TODO: Add conversation ended check when messageParams available
98
+ return false
99
+ })
100
+
101
+ // Generate unique ID for list
102
+ const listTemplateId = getRandomId('webchatListTemplateRoot')
103
+
104
+ // Auto-focus first focusable element on mount
105
+ onMounted(() => {
106
+ if (!config?.settings?.widgetSettings?.enableAutoFocus) return
107
+
108
+ const chatHistory = document.getElementById('webchatChatHistoryWrapperLiveLogPanel')
109
+ if (!chatHistory?.contains(document.activeElement)) return
110
+
111
+ setTimeout(() => {
112
+ const listTemplateRoot = document.getElementById(listTemplateId)
113
+ // Get the first focusable element within the list and add focus
114
+ const focusable = listTemplateRoot?.querySelectorAll(
115
+ 'button, [href], [tabindex]:not([tabindex="-1"])'
116
+ )
117
+ const firstFocusable = focusable?.[0] as HTMLElement
118
+ firstFocusable?.focus()
119
+ }, 200)
120
+ })
121
+ </script>
122
+
123
+ <style module>
124
+ .wrapper {
125
+ max-width: 295px;
126
+ border-radius: var(--cc-bubble-border-radius, 15px);
127
+ border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
128
+ background-color: var(--cc-white, #ffffff);
129
+ }
130
+
131
+ .wrapper .listItemRoot {
132
+ border-top-right-radius: var(--cc-bubble-border-radius, 15px);
133
+ border-top-left-radius: var(--cc-bubble-border-radius, 15px);
134
+ }
135
+
136
+ .list {
137
+ list-style: none;
138
+ padding-inline-start: 0;
139
+ margin-block: 0;
140
+ }
141
+
142
+ .list .listItemRoot {
143
+ list-style: none;
144
+ }
145
+
146
+ .mainButtonWrapper {
147
+ padding: 16px;
148
+ }
149
+ </style>
@@ -0,0 +1,344 @@
1
+ <template>
2
+ <component
3
+ :is="componentTag"
4
+ :class="[isHeaderElement && $style.headerRoot, $style.listItemRoot]"
5
+ :style="{ backgroundImage: isHeaderElement && element.image_url ? backgroundImage : undefined }"
6
+ :data-testid="isHeaderElement ? 'header-image' : 'list-item'"
7
+ :id="id"
8
+ >
9
+ <!-- Divider before item -->
10
+ <div v-if="!isHeaderElement && dividerBefore" :class="$style.divider" />
11
+
12
+ <!-- Item content wrapper -->
13
+ <div
14
+ :class="contentClasses"
15
+ :role="defaultActionUrl ? 'link' : undefined"
16
+ :aria-label="defaultActionUrl ? `${titleHtml}. ${opensInNewTabLabel}` : undefined"
17
+ :aria-describedby="element.subtitle ? subtitleId : undefined"
18
+ :tabindex="defaultActionUrl ? 0 : -1"
19
+ :style="defaultActionUrl && !shouldBeDisabled ? { cursor: 'pointer' } : {}"
20
+ @click="handleClick"
21
+ @keydown="handleKeyDown"
22
+ >
23
+ <!-- Header element content -->
24
+ <div
25
+ v-if="isHeaderElement"
26
+ :class="['webchat-list-template-header-content', $style.headerContent, button && $style.headerContentWithButton]"
27
+ >
28
+ <!-- Title and subtitle -->
29
+ <Typography
30
+ v-if="titleHtml"
31
+ :variant="isHeaderElement ? 'h2-semibold' : 'title1-semibold'"
32
+ :component="headingLevel"
33
+ :class="[
34
+ isHeaderElement ? 'webchat-list-template-header-title' : 'webchat-list-template-element-title',
35
+ subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle
36
+ ]"
37
+ :id="isHeaderElement ? `listHeader-${id}` : `listItemHeader-${id}`"
38
+ v-html="titleHtml"
39
+ />
40
+
41
+ <Typography
42
+ v-if="subtitleHtml"
43
+ variant="body-regular"
44
+ :class="[
45
+ isHeaderElement ? 'webchat-list-template-header-subtitle' : 'webchat-list-template-element-subtitle',
46
+ $style.itemSubtitle
47
+ ]"
48
+ :id="subtitleId"
49
+ v-html="subtitleHtml"
50
+ />
51
+ </div>
52
+
53
+ <!-- Regular list item content -->
54
+ <div
55
+ v-else
56
+ :class="['webchat-list-template-element-content', $style.listItemContent]"
57
+ >
58
+ <div :class="$style.listItemText">
59
+ <!-- Title and subtitle -->
60
+ <Typography
61
+ v-if="titleHtml"
62
+ variant="title1-semibold"
63
+ :component="headingLevel"
64
+ :class="[
65
+ 'webchat-list-template-element-title',
66
+ subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle
67
+ ]"
68
+ :id="`listItemHeader-${id}`"
69
+ v-html="titleHtml"
70
+ />
71
+
72
+ <Typography
73
+ v-if="subtitleHtml"
74
+ variant="body-regular"
75
+ :class="['webchat-list-template-element-subtitle', $style.itemSubtitle]"
76
+ :id="subtitleId"
77
+ v-html="subtitleHtml"
78
+ />
79
+ </div>
80
+
81
+ <!-- Image thumbnail for regular items -->
82
+ <div
83
+ v-if="element.image_url"
84
+ :class="$style.listItemImage"
85
+ :style="{ backgroundImage }"
86
+ data-testid="regular-image"
87
+ >
88
+ <span role="img" :aria-label="element.image_alt_text || ''" />
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- Button for item -->
94
+ <ActionButtons
95
+ v-if="button"
96
+ :payload="[button]"
97
+ :action="shouldBeDisabled ? undefined : action"
98
+ :buttonClassName="isHeaderElement ? 'webchat-list-template-header-button' : 'webchat-list-template-element-button'"
99
+ :containerClassName="isHeaderElement ? $style.listHeaderButtonWrapper : $style.listItemButtonWrapper"
100
+ :config="config"
101
+ :dataMessageId="dataMessageId"
102
+ :onEmitAnalytics="onEmitAnalytics"
103
+ size="large"
104
+ />
105
+
106
+ <!-- Divider after item -->
107
+ <div v-if="!isHeaderElement && dividerAfter" :class="$style.divider" />
108
+ </component>
109
+ </template>
110
+
111
+ <script setup lang="ts">
112
+ import { computed, useCssModule } from 'vue'
113
+ import Typography from '../common/Typography.vue'
114
+ import ActionButtons from '../common/ActionButtons.vue'
115
+ import { useMessageContext } from '../../composables/useMessageContext'
116
+ import { useSanitize } from '../../composables/useSanitize'
117
+ import { getRandomId, getBackgroundImage } from '../../utils/helpers'
118
+ import { sanitizeUrl } from '@braintree/sanitize-url'
119
+ import type { IWebchatAttachmentElement } from '../../types'
120
+
121
+ interface Props {
122
+ element: IWebchatAttachmentElement
123
+ isHeaderElement?: boolean
124
+ headingLevel?: 'h4' | 'h5'
125
+ id: string
126
+ dividerBefore?: boolean
127
+ dividerAfter?: boolean
128
+ }
129
+
130
+ const props = withDefaults(defineProps<Props>(), {
131
+ isHeaderElement: false,
132
+ headingLevel: 'h4',
133
+ dividerBefore: false,
134
+ dividerAfter: false,
135
+ })
136
+
137
+ const $style = useCssModule()
138
+
139
+ // Context
140
+ const { action, config, onEmitAnalytics } = useMessageContext()
141
+ const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
142
+
143
+ // Sanitize HTML
144
+ const { processHTML } = useSanitize()
145
+ const titleHtml = computed(() => processHTML(props.element.title || ''))
146
+ const subtitleHtml = computed(() => processHTML(props.element.subtitle || ''))
147
+
148
+ // IDs for accessibility
149
+ const subtitleId = getRandomId('webchatListTemplateHeaderSubtitle')
150
+
151
+ // Background image
152
+ const backgroundImage = computed(() => {
153
+ if (!props.element.image_url) return undefined
154
+ return getBackgroundImage(props.element.image_url)
155
+ })
156
+
157
+ // Button (only first button is used)
158
+ const button = computed(() => {
159
+ return props.element.buttons?.[0]
160
+ })
161
+
162
+ // Default action URL (clickable item)
163
+ const defaultActionUrl = computed(() => {
164
+ return props.element.default_action?.url
165
+ })
166
+
167
+ // Should buttons be disabled
168
+ const shouldBeDisabled = computed(() => {
169
+ // TODO: Add conversation ended check when messageParams available
170
+ return false
171
+ })
172
+
173
+ // Translations
174
+ const opensInNewTabLabel = computed(() => {
175
+ return config?.settings?.customTranslations?.ariaLabels?.opensInNewTab || 'Opens in new tab'
176
+ })
177
+
178
+ // Component tag (div for header, li for regular items)
179
+ const componentTag = computed(() => {
180
+ return props.isHeaderElement ? 'div' : 'li'
181
+ })
182
+
183
+ // Content classes
184
+ const contentClasses = computed(() => {
185
+ return props.isHeaderElement
186
+ ? ['webchat-list-template-header', $style.headerContentWrapper]
187
+ : ['webchat-list-template-element', $style.listItemWrapper]
188
+ })
189
+
190
+ // Handle item click (default action)
191
+ const handleClick = () => {
192
+ if (shouldBeDisabled.value || !defaultActionUrl.value) return
193
+
194
+ const url = config?.settings?.layout?.disableUrlButtonSanitization
195
+ ? defaultActionUrl.value
196
+ : sanitizeUrl(defaultActionUrl.value)
197
+
198
+ // Prevent no-ops from sending you to a blank page
199
+ if (url === 'about:blank') return
200
+
201
+ window.open(url)
202
+ }
203
+
204
+ // Handle keyboard navigation
205
+ const handleKeyDown = (event: KeyboardEvent) => {
206
+ if (defaultActionUrl.value && event.key === 'Enter') {
207
+ handleClick()
208
+ }
209
+ }
210
+ </script>
211
+
212
+ <style module>
213
+ .listItemRoot {
214
+ list-style: none;
215
+ }
216
+
217
+ .divider {
218
+ border-top: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
219
+ }
220
+
221
+ /* Header element styles */
222
+ .headerRoot {
223
+ aspect-ratio: 16/9;
224
+ background-size: cover;
225
+ background-position: center center;
226
+ position: relative;
227
+ display: flex;
228
+ flex-direction: column;
229
+ border-top-right-radius: var(--cc-bubble-border-radius, 15px);
230
+ border-top-left-radius: var(--cc-bubble-border-radius, 15px);
231
+ }
232
+
233
+ .headerRoot::before {
234
+ content: "";
235
+ position: absolute;
236
+ top: 0;
237
+ left: 0;
238
+ width: 100%;
239
+ height: 100%;
240
+ background-color: hsla(0, 0%, 0%, 0.4); /* image overlay */
241
+ border-radius: inherit;
242
+ }
243
+
244
+ .headerRoot:focus {
245
+ opacity: 0.6;
246
+ }
247
+
248
+ .headerContentWrapper {
249
+ border-radius: inherit;
250
+ position: relative;
251
+ flex-grow: 1;
252
+ align-content: flex-end;
253
+ }
254
+
255
+ .headerContentWrapper:focus-visible {
256
+ outline: 2px solid var(--cc-primary-color-focus, #1976d2);
257
+ box-shadow: inset 0 0 0 2px var(--cc-white, #ffffff);
258
+ }
259
+
260
+ .headerContent {
261
+ padding: 16px 16px 12px 16px;
262
+ color: var(--cc-white, #ffffff);
263
+ }
264
+
265
+ .headerContent > * {
266
+ color: var(--cc-white, #ffffff);
267
+ }
268
+
269
+ .headerContentWithButton {
270
+ padding-bottom: 72px; /* 12px headerContent bottom padding + 44px button height + 16px button bottom padding */
271
+ }
272
+
273
+ /* Title and subtitle styles */
274
+ .itemTitle {
275
+ margin-top: 0px;
276
+ margin-bottom: 0px;
277
+ }
278
+
279
+ .itemTitleWithSubtitle {
280
+ margin-top: 0px;
281
+ margin-bottom: 8px;
282
+ }
283
+
284
+ .itemSubtitle {
285
+ margin-top: 0px;
286
+ margin-bottom: 0px;
287
+ }
288
+
289
+ /* Regular list item styles */
290
+ .listItemWrapper {
291
+ position: relative;
292
+ display: flex;
293
+ }
294
+
295
+ .listItemWrapper:focus {
296
+ outline: none;
297
+ }
298
+
299
+ .listItemWrapper:focus-visible {
300
+ outline: 2px solid var(--cc-primary-color-focus, #1976d2);
301
+ outline-offset: -10px;
302
+ }
303
+
304
+ .listItemContent {
305
+ padding: 16px 16px 12px 16px;
306
+ overflow-wrap: break-word;
307
+ display: flex;
308
+ -webkit-box-pack: justify;
309
+ justify-content: space-between;
310
+ align-items: center;
311
+ gap: 16px;
312
+ width: 100%;
313
+ color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
314
+ }
315
+
316
+ .listItemText {
317
+ width: 100%;
318
+ }
319
+
320
+ .listItemText > * {
321
+ color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
322
+ }
323
+
324
+ .listItemImage {
325
+ background-size: cover;
326
+ background-position: center center;
327
+ border-radius: 10px;
328
+ width: 86px;
329
+ height: 102px;
330
+ flex-shrink: 0;
331
+ }
332
+
333
+ /* Button container styles */
334
+ .listHeaderButtonWrapper {
335
+ position: absolute;
336
+ bottom: 16px;
337
+ right: 16px;
338
+ left: 16px;
339
+ }
340
+
341
+ .listItemButtonWrapper {
342
+ padding: 0 16px 16px 16px;
343
+ }
344
+ </style>
@@ -0,0 +1,203 @@
1
+ <script setup lang="ts">
2
+ import { computed, useCssModule } from 'vue'
3
+ import { useMessageContext } from '../../composables/useMessageContext'
4
+ import { useSanitize } from '../../composables/useSanitize'
5
+ import ChatBubble from '../common/ChatBubble.vue'
6
+ import { replaceUrlsWithHTMLanchorElem } from '../../utils/helpers'
7
+ import MarkdownIt from 'markdown-it'
8
+
9
+ interface Props {
10
+ content?: string | string[]
11
+ className?: string
12
+ markdownClassName?: string
13
+ id?: string
14
+ ignoreLiveRegion?: boolean
15
+ }
16
+
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ content: undefined,
19
+ className: '',
20
+ markdownClassName: '',
21
+ id: undefined,
22
+ ignoreLiveRegion: false,
23
+ })
24
+
25
+ const styles = useCssModule()
26
+ const { message, config } = useMessageContext()
27
+ const { processHTML } = useSanitize()
28
+
29
+ // Initialize markdown-it with HTML support and GFM tables
30
+ const md = new MarkdownIt({
31
+ html: true,
32
+ linkify: true,
33
+ breaks: true,
34
+ })
35
+
36
+ // Get text content
37
+ const text = computed(() => message?.text)
38
+ const source = computed(() => message?.source)
39
+
40
+ // Use prop content or message text
41
+ const content = computed(() => {
42
+ const contentValue = props.content || text.value || ''
43
+ return Array.isArray(contentValue) ? contentValue.join('') : contentValue
44
+ })
45
+
46
+ // Determine if markdown should be rendered
47
+ const renderMarkdown = computed(() => {
48
+ return config?.settings?.behavior?.renderMarkdown &&
49
+ (source.value === 'bot' || source.value === 'engagement')
50
+ })
51
+
52
+ // Optionally transform URL strings into clickable links
53
+ const enhancedURLsText = computed(() => {
54
+ if (config?.settings?.widgetSettings?.disableRenderURLsAsLinks) {
55
+ return content.value
56
+ }
57
+ return replaceUrlsWithHTMLanchorElem(content.value)
58
+ })
59
+
60
+ // Determine if sanitization should be ignored
61
+ const ignoreSanitization = computed(() => {
62
+ return source.value === 'user' && config?.settings?.widgetSettings?.disableTextInputSanitization
63
+ })
64
+
65
+ // Process content: sanitize HTML if needed
66
+ const processedContent = computed(() => {
67
+ if (ignoreSanitization.value) {
68
+ return enhancedURLsText.value
69
+ }
70
+ return processHTML(enhancedURLsText.value)
71
+ })
72
+
73
+ // Render markdown if enabled
74
+ const markdownContent = computed(() => {
75
+ if (!renderMarkdown.value) return ''
76
+
77
+ // Render markdown to HTML
78
+ let html = md.render(processedContent.value || content.value)
79
+
80
+ // Make all links open in new tab
81
+ html = html.replace(/<a /g, '<a target="_blank" rel="noreferrer" ')
82
+
83
+ return html
84
+ })
85
+
86
+ // Compute CSS classes
87
+ const textClasses = computed(() => {
88
+ const classes = [props.className]
89
+ if (!renderMarkdown.value) {
90
+ classes.push(styles.text)
91
+ }
92
+ return classes.filter(Boolean)
93
+ })
94
+
95
+ const markdownClasses = computed(() => {
96
+ const classes = [styles.markdown, props.markdownClassName]
97
+ return classes.filter(Boolean)
98
+ })
99
+ </script>
100
+
101
+ <template>
102
+ <ChatBubble>
103
+ <div
104
+ v-if="renderMarkdown"
105
+ :class="markdownClasses"
106
+ v-html="markdownContent"
107
+ />
108
+ <p
109
+ v-else
110
+ :id="id"
111
+ :class="textClasses"
112
+ v-html="processedContent"
113
+ />
114
+ </ChatBubble>
115
+ </template>
116
+
117
+ <style module>
118
+ .text {
119
+ white-space: pre-wrap;
120
+ overflow-wrap: break-word;
121
+ display: inline;
122
+ }
123
+
124
+ .text a:focus-visible {
125
+ outline: 2px solid var(--cc-primary-color-focus);
126
+ outline-offset: 2px;
127
+ }
128
+
129
+ .text img {
130
+ max-width: 100%;
131
+ }
132
+
133
+ .markdown {
134
+ display: inline;
135
+ white-space: normal;
136
+ }
137
+
138
+ .markdown > p:only-child {
139
+ margin: 0;
140
+ }
141
+
142
+ .markdown *:first-child {
143
+ margin-top: 0;
144
+ }
145
+
146
+ .markdown *:last-child {
147
+ margin-bottom: 0;
148
+ }
149
+
150
+ .markdown p:last-child {
151
+ display: inline;
152
+ }
153
+
154
+ .markdown table {
155
+ border-collapse: separate;
156
+ border-spacing: 0;
157
+ margin-bottom: 4px;
158
+ margin-top: 4px;
159
+ }
160
+
161
+ .markdown th {
162
+ text-align: left;
163
+ border-top-width: 1px;
164
+ border-bottom-width: 1px;
165
+ border-left-width: 1px;
166
+ border-right-width: 0px;
167
+ background-color: var(--cc-black-90);
168
+ border-style: solid;
169
+ border-color: var(--cc-black-80);
170
+ padding: 4px 12px;
171
+ }
172
+
173
+ .markdown th:first-child {
174
+ border-top-left-radius: 6px;
175
+ }
176
+
177
+ .markdown th:last-child {
178
+ border-top-right-radius: 6px;
179
+ border-right-width: 1px;
180
+ }
181
+
182
+ .markdown td {
183
+ border-style: solid;
184
+ border-bottom-width: 1px;
185
+ border-left-width: 1px;
186
+ border-top-width: 0px;
187
+ border-right-width: 0px;
188
+ border-color: var(--cc-black-80);
189
+ padding: 4px 12px;
190
+ }
191
+
192
+ .markdown td:last-child {
193
+ border-right-width: 1px;
194
+ }
195
+
196
+ .markdown tbody tr:last-child td:first-child {
197
+ border-bottom-left-radius: 6px;
198
+ }
199
+
200
+ .markdown tbody tr:last-child td:last-child {
201
+ border-bottom-right-radius: 6px;
202
+ }
203
+ </style>