@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,119 @@
1
+ <template>
2
+ <div :class="`webchat-${classType}-template-root`">
3
+ <!-- Text content -->
4
+ <TextMessage
5
+ v-if="text"
6
+ :content="text"
7
+ :className="`webchat-${classType}-template-header`"
8
+ :id="webchatButtonTemplateTextId"
9
+ ignoreLiveRegion
10
+ />
11
+
12
+ <!-- Buttons -->
13
+ <ActionButtons
14
+ v-if="buttons.length > 0"
15
+ :payload="buttons"
16
+ :action="modifiedAction"
17
+ :buttonClassName="buttonClassName"
18
+ :containerClassName="containerClassName"
19
+ :containerStyle="containerStyle"
20
+ :config="config"
21
+ :onEmitAnalytics="onEmitAnalytics"
22
+ :templateTextId="webchatButtonTemplateTextId"
23
+ showUrlIcon
24
+ />
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ import { computed, useCssModule } from 'vue'
30
+ import TextMessage from './TextMessage.vue'
31
+ import ActionButtons from '../common/ActionButtons.vue'
32
+ import { useMessageContext } from '../../composables/useMessageContext'
33
+ import { getChannelPayload } from '../../utils/matcher'
34
+ import { getRandomId } from '../../utils/helpers'
35
+ import type { IWebchatTemplateAttachment, IWebchatButton, IWebchatQuickReply } from '../../types'
36
+
37
+ // Message context
38
+ const { message, config, action, onEmitAnalytics } = useMessageContext()
39
+
40
+ const $style = useCssModule()
41
+
42
+ // Get payload data
43
+ const payload = computed(() => getChannelPayload(message, config))
44
+
45
+ // Get attachment data
46
+ const attachment = computed(() => payload.value?.message?.attachment as IWebchatTemplateAttachment | undefined)
47
+
48
+ // Get text content
49
+ const text = computed(() => {
50
+ return attachment.value?.payload?.text || payload.value?.message?.text || ''
51
+ })
52
+
53
+ // Get buttons (either from buttons or quick_replies)
54
+ const buttons = computed(() => {
55
+ const payloadButtons = attachment.value?.payload?.buttons
56
+ const quickReplies = payload.value?.message?.quick_replies
57
+ return (payloadButtons || quickReplies || []) as (IWebchatButton | IWebchatQuickReply)[]
58
+ })
59
+
60
+ // Determine if this is quick replies
61
+ const isQuickReplies = computed(() => {
62
+ return payload.value?.message?.quick_replies && payload.value.message.quick_replies.length > 0
63
+ })
64
+
65
+ // Determine class type
66
+ const classType = computed(() => {
67
+ return isQuickReplies.value ? 'quick-reply' : 'buttons'
68
+ })
69
+
70
+ // For quick replies, disable if there's already a reply
71
+ // Note: In the React version, this uses messageParams.hasReply
72
+ // For now, we'll just pass the action as-is since we don't have messageParams in Vue yet
73
+ const modifiedAction = computed(() => {
74
+ // TODO: Implement disabling for quick replies when there's a user reply
75
+ // This would require tracking conversation state
76
+ return action
77
+ })
78
+
79
+ // Generate unique ID for accessibility
80
+ const webchatButtonTemplateTextId = getRandomId('webchatButtonTemplateHeader')
81
+
82
+ // Get bot output max width
83
+ const botOutputMaxWidthPercentage = config?.settings?.layout?.botOutputMaxWidthPercentage
84
+ const isBotMessage = message.source === 'bot'
85
+ const isEngagementMessage = message.source === 'engagement'
86
+
87
+ // Container style for max width
88
+ const containerStyle = computed(() => {
89
+ if ((isBotMessage || isEngagementMessage) && botOutputMaxWidthPercentage) {
90
+ return { maxWidth: `${botOutputMaxWidthPercentage}%` }
91
+ }
92
+ return {}
93
+ })
94
+
95
+ // Button class name
96
+ const buttonClassName = computed(() => {
97
+ return `${$style.button} webchat-${classType.value}-template-button`
98
+ })
99
+
100
+ // Container class name
101
+ const containerClassName = computed(() => {
102
+ return `${$style.buttons} webchat-${classType.value}-template-replies-container`
103
+ })
104
+ </script>
105
+
106
+ <style module>
107
+ .buttons {
108
+ align-items: flex-start;
109
+ display: flex;
110
+ flex-direction: row;
111
+ flex-wrap: wrap;
112
+ margin-top: 10px;
113
+ max-width: 295px;
114
+ }
115
+
116
+ .button {
117
+ /* Button styles are inherited from ActionButton component */
118
+ }
119
+ </style>
@@ -0,0 +1,343 @@
1
+ <template>
2
+ <div v-if="videoData.url" :class="wrapperClasses">
3
+ <div
4
+ :class="[$style.playerWrapper, 'webchat-media-template-video']"
5
+ :role="showLightMode ? 'button' : undefined"
6
+ :tabindex="showLightMode ? 0 : -1"
7
+ :aria-label="showLightMode ? playVideoLabel : undefined"
8
+ data-testid="video-message"
9
+ @keydown="handleKeyDown"
10
+ >
11
+ <!-- Light mode overlay with play button -->
12
+ <div
13
+ v-if="showLightMode"
14
+ :class="$style.lightOverlay"
15
+ @click="startPlaying"
16
+ >
17
+ <VideoPlayIcon width="35" height="35" />
18
+ </div>
19
+
20
+ <!-- YouTube embed -->
21
+ <iframe
22
+ v-if="videoType === 'youtube'"
23
+ ref="videoRef"
24
+ :class="$style.player"
25
+ :src="youtubeEmbedUrl"
26
+ frameborder="0"
27
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
28
+ allowfullscreen
29
+ :title="videoData.altText || 'Video player'"
30
+ />
31
+
32
+ <!-- Vimeo embed -->
33
+ <iframe
34
+ v-else-if="videoType === 'vimeo'"
35
+ ref="videoRef"
36
+ :class="$style.player"
37
+ :src="vimeoEmbedUrl"
38
+ frameborder="0"
39
+ allow="autoplay; fullscreen; picture-in-picture"
40
+ allowfullscreen
41
+ :title="videoData.altText || 'Video player'"
42
+ />
43
+
44
+ <!-- Direct video file -->
45
+ <video
46
+ v-else
47
+ ref="videoRef"
48
+ :class="$style.player"
49
+ controls
50
+ :crossorigin="videoData.captionsUrl ? 'anonymous' : undefined"
51
+ @play="handlePlay"
52
+ @pause="handlePause"
53
+ >
54
+ <source :src="videoData.url" />
55
+ <track
56
+ v-if="videoData.captionsUrl"
57
+ kind="subtitles"
58
+ :src="videoData.captionsUrl"
59
+ srclang="en-US"
60
+ label="English"
61
+ default
62
+ />
63
+ Your browser does not support the video tag.
64
+ </video>
65
+ </div>
66
+
67
+ <!-- Download transcript button -->
68
+ <div v-if="videoData.altText" :class="$style.downloadButtonWrapper">
69
+ <button
70
+ :class="[$style.downloadButton, 'webchat-buttons-template-button-video']"
71
+ @click="downloadTranscript"
72
+ >
73
+ <DownloadIcon :class="$style.downloadIcon" />
74
+ Download Transcript
75
+ </button>
76
+ <a
77
+ ref="downloadLinkRef"
78
+ :href="transcriptDataUrl"
79
+ download="video-transcript.txt"
80
+ style="display: none"
81
+ aria-hidden="true"
82
+ />
83
+ </div>
84
+ </div>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ import { ref, computed, onMounted, useCssModule } from 'vue'
89
+ import { useMessageContext } from '../../composables/useMessageContext'
90
+ import { getChannelPayload } from '../../utils/matcher'
91
+ import { DownloadIcon, VideoPlayIcon } from '../../assets/svg'
92
+ import type { IWebchatVideoAttachment } from '../../types'
93
+
94
+ const { message, config } = useMessageContext()
95
+
96
+ const $style = useCssModule()
97
+
98
+ // Refs
99
+ const videoRef = ref<HTMLVideoElement | HTMLIFrameElement>()
100
+ const downloadLinkRef = ref<HTMLAnchorElement>()
101
+
102
+ // State
103
+ const playing = ref(false)
104
+ const hasStarted = ref(false)
105
+
106
+ // Get video data from message payload
107
+ const payload = computed(() => getChannelPayload(message, config))
108
+ const videoData = computed(() => {
109
+ const attachment = payload.value?.message?.attachment as IWebchatVideoAttachment
110
+ return {
111
+ url: attachment?.payload?.url || '',
112
+ altText: attachment?.payload?.altText,
113
+ captionsUrl: attachment?.payload?.captionsUrl,
114
+ }
115
+ })
116
+
117
+ // Detect video type (YouTube, Vimeo, or direct)
118
+ const videoType = computed(() => {
119
+ const url = videoData.value.url
120
+ if (!url) return 'direct'
121
+
122
+ if (url.includes('youtube.com') || url.includes('youtu.be')) {
123
+ return 'youtube'
124
+ }
125
+ if (url.includes('vimeo.com')) {
126
+ return 'vimeo'
127
+ }
128
+ return 'direct'
129
+ })
130
+
131
+ // Extract YouTube video ID
132
+ const youtubeVideoId = computed(() => {
133
+ const url = videoData.value.url
134
+ const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/
135
+ const match = url.match(regExp)
136
+ return match && match[7].length === 11 ? match[7] : null
137
+ })
138
+
139
+ // YouTube embed URL
140
+ const youtubeEmbedUrl = computed(() => {
141
+ if (!youtubeVideoId.value) return ''
142
+ return `https://www.youtube.com/embed/${youtubeVideoId.value}?enablejsapi=1`
143
+ })
144
+
145
+ // Extract Vimeo video ID
146
+ const vimeoVideoId = computed(() => {
147
+ const url = videoData.value.url
148
+ const regExp = /vimeo.com\/(\d+)/
149
+ const match = url.match(regExp)
150
+ return match ? match[1] : null
151
+ })
152
+
153
+ // Vimeo embed URL
154
+ const vimeoEmbedUrl = computed(() => {
155
+ if (!vimeoVideoId.value) return ''
156
+ return `https://player.vimeo.com/video/${vimeoVideoId.value}`
157
+ })
158
+
159
+ // Show light mode (preview) only for direct videos before they start
160
+ const showLightMode = computed(() => {
161
+ if (videoType.value !== 'direct') return false
162
+ return !hasStarted.value
163
+ })
164
+
165
+ // Wrapper classes
166
+ const wrapperClasses = computed(() => {
167
+ return {
168
+ [$style.wrapper]: true,
169
+ [$style.wrapperWithButton]: !!videoData.value.altText,
170
+ }
171
+ })
172
+
173
+ // Transcript data URL for download
174
+ const transcriptDataUrl = computed(() => {
175
+ const text = videoData.value.altText || ''
176
+ return `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`
177
+ })
178
+
179
+ // Translations
180
+ const playVideoLabel = computed(() =>
181
+ config?.settings?.customTranslations?.ariaLabels?.playVideo || 'Play video'
182
+ )
183
+
184
+ // Handlers
185
+ const startPlaying = () => {
186
+ hasStarted.value = true
187
+ if (videoRef.value && 'play' in videoRef.value) {
188
+ (videoRef.value as HTMLVideoElement).play()
189
+ }
190
+ }
191
+
192
+ const handlePlay = () => {
193
+ playing.value = true
194
+ hasStarted.value = true
195
+ }
196
+
197
+ const handlePause = () => {
198
+ playing.value = false
199
+ }
200
+
201
+ const handleKeyDown = (event: KeyboardEvent) => {
202
+ if (showLightMode.value && (event.key === 'Enter' || event.key === ' ')) {
203
+ event.preventDefault()
204
+ event.stopPropagation()
205
+ startPlaying()
206
+ }
207
+ }
208
+
209
+ const downloadTranscript = () => {
210
+ downloadLinkRef.value?.click()
211
+ }
212
+
213
+ // Auto-focus video on mount if configured
214
+ onMounted(() => {
215
+ if (!config?.settings?.widgetSettings?.enableAutoFocus) return
216
+
217
+ const chatHistory = document.getElementById('webchatChatHistoryWrapperLiveLogPanel')
218
+ if (!chatHistory?.contains(document.activeElement)) return
219
+
220
+ setTimeout(() => {
221
+ if (videoRef.value && 'focus' in videoRef.value) {
222
+ (videoRef.value as HTMLVideoElement).focus()
223
+ }
224
+ }, 100)
225
+ })
226
+ </script>
227
+
228
+ <style module>
229
+ .wrapper {
230
+ border-radius: var(--cc-bubble-border-radius, 15px);
231
+ max-width: 295px;
232
+ position: relative;
233
+ }
234
+
235
+ .player {
236
+ aspect-ratio: 16/9;
237
+ max-width: 295px;
238
+ object-fit: cover;
239
+ object-position: left;
240
+ overflow: hidden;
241
+ border-radius: var(--cc-bubble-border-radius, 15px);
242
+ width: 100%;
243
+ display: block;
244
+ }
245
+
246
+ .player video {
247
+ max-width: 295px;
248
+ object-fit: cover;
249
+ object-position: left;
250
+ overflow: hidden;
251
+ }
252
+
253
+ .wrapperWithButton {
254
+ border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
255
+ }
256
+
257
+ .wrapperWithButton .player {
258
+ border-bottom-left-radius: 0px;
259
+ border-bottom-right-radius: 0px;
260
+ }
261
+
262
+ .downloadButtonWrapper {
263
+ padding: 16px;
264
+ background-color: var(--cc-white, #ffffff);
265
+ border-bottom-left-radius: var(--cc-bubble-border-radius, 15px);
266
+ border-bottom-right-radius: var(--cc-bubble-border-radius, 15px);
267
+ }
268
+
269
+ .downloadButton {
270
+ display: flex;
271
+ align-items: center;
272
+ justify-content: center;
273
+ gap: 10px;
274
+ width: 100%;
275
+ padding: 12px;
276
+ background-color: var(--cc-primary-color, #1976d2);
277
+ color: var(--cc-primary-contrast-color, #ffffff);
278
+ border: none;
279
+ border-radius: 8px;
280
+ cursor: pointer;
281
+ font-size: 14px;
282
+ font-weight: 600;
283
+ transition: background-color 0.2s ease;
284
+ }
285
+
286
+ .downloadButton:hover {
287
+ background-color: var(--cc-primary-color-hover, #1565c0);
288
+ }
289
+
290
+ .downloadButton:focus-visible {
291
+ outline: 2px solid var(--cc-primary-color-focus, #1976d2);
292
+ outline-offset: 2px;
293
+ }
294
+
295
+ .downloadIcon {
296
+ width: 12px;
297
+ height: 12px;
298
+ }
299
+
300
+ .downloadIcon :deep(path) {
301
+ fill: var(--cc-primary-contrast-color, #ffffff);
302
+ }
303
+
304
+ .playerWrapper {
305
+ position: relative;
306
+ }
307
+
308
+ .playerWrapper:focus .player,
309
+ .player:focus-within {
310
+ outline: 2px solid var(--cc-primary-color-focus, #1976d2);
311
+ }
312
+
313
+ .playerWrapper:focus,
314
+ .player video:focus {
315
+ outline: none;
316
+ }
317
+
318
+ /* Light mode overlay */
319
+ .lightOverlay {
320
+ position: absolute;
321
+ top: 0;
322
+ left: 0;
323
+ width: 100%;
324
+ height: 100%;
325
+ background-color: var(--cc-black-10, #1a1a1a);
326
+ border-radius: var(--cc-bubble-border-radius, 15px);
327
+ display: flex;
328
+ align-items: center;
329
+ justify-content: center;
330
+ cursor: pointer;
331
+ z-index: 1;
332
+ }
333
+
334
+ .lightOverlay:hover,
335
+ .lightOverlay:focus {
336
+ opacity: 0.85;
337
+ }
338
+
339
+ .lightOverlay:hover svg circle,
340
+ .lightOverlay:focus svg circle {
341
+ fill-opacity: 1;
342
+ }
343
+ </style>
@@ -0,0 +1,101 @@
1
+ /**
2
+ * useChannelPayload composable
3
+ * Reactive wrapper for extracting the correct channel payload from a message
4
+ *
5
+ * The payload is determined by checking (in order):
6
+ * 1. _defaultPreview (if enableDefaultPreview is true)
7
+ * 2. _facebook (if strictMessengerSync is enabled and syncWebchatWithFacebook is true)
8
+ * 3. _webchat (default)
9
+ * 4. _facebook (fallback)
10
+ */
11
+
12
+ import { computed, type ComputedRef, toValue, type MaybeRefOrGetter } from 'vue'
13
+ import type { IMessage } from '@cognigy/socket-client'
14
+ import type { ChatConfig, IWebchatChannelPayload } from '../types'
15
+ import { getChannelPayload } from '../utils/matcher'
16
+
17
+ export interface UseChannelPayloadReturn {
18
+ /**
19
+ * The resolved channel payload (_webchat, _defaultPreview, or _facebook)
20
+ */
21
+ payload: ComputedRef<IWebchatChannelPayload | undefined>
22
+
23
+ /**
24
+ * Whether the message has any channel payload
25
+ */
26
+ hasPayload: ComputedRef<boolean>
27
+
28
+ /**
29
+ * The message content from the payload
30
+ */
31
+ message: ComputedRef<IWebchatChannelPayload['message'] | undefined>
32
+
33
+ /**
34
+ * Quick check for quick replies
35
+ */
36
+ hasQuickReplies: ComputedRef<boolean>
37
+
38
+ /**
39
+ * Quick check for attachment
40
+ */
41
+ hasAttachment: ComputedRef<boolean>
42
+
43
+ /**
44
+ * The attachment type if present
45
+ */
46
+ attachmentType: ComputedRef<string | undefined>
47
+ }
48
+
49
+ /**
50
+ * Reactive composable for accessing channel-specific payload from a message
51
+ *
52
+ * @param message - The message (can be ref, getter, or plain value)
53
+ * @param config - Optional chat config (can be ref, getter, or plain value)
54
+ * @returns Reactive payload accessors
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const { payload, hasQuickReplies, attachmentType } = useChannelPayload(message, config)
59
+ *
60
+ * // In template or computed
61
+ * if (hasQuickReplies.value) {
62
+ * // Render quick reply buttons
63
+ * }
64
+ * ```
65
+ */
66
+ export function useChannelPayload(
67
+ message: MaybeRefOrGetter<IMessage>,
68
+ config?: MaybeRefOrGetter<ChatConfig | undefined>
69
+ ): UseChannelPayloadReturn {
70
+ const payload = computed(() => {
71
+ const msg = toValue(message)
72
+ const cfg = toValue(config)
73
+ return getChannelPayload(msg, cfg)
74
+ })
75
+
76
+ const hasPayload = computed(() => payload.value !== undefined)
77
+
78
+ const payloadMessage = computed(() => payload.value?.message)
79
+
80
+ const hasQuickReplies = computed(() => {
81
+ const quickReplies = payloadMessage.value?.quick_replies
82
+ return Array.isArray(quickReplies) && quickReplies.length > 0
83
+ })
84
+
85
+ const hasAttachment = computed(() => {
86
+ return payloadMessage.value?.attachment !== undefined
87
+ })
88
+
89
+ const attachmentType = computed(() => {
90
+ return payloadMessage.value?.attachment?.type
91
+ })
92
+
93
+ return {
94
+ payload,
95
+ hasPayload,
96
+ message: payloadMessage,
97
+ hasQuickReplies,
98
+ hasAttachment,
99
+ attachmentType,
100
+ }
101
+ }