@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.
- package/LICENSE +20 -0
- package/README.md +178 -0
- package/dist/assets/svg/ArrowBackIcon.vue.d.ts +9 -0
- package/dist/assets/svg/AudioPauseIcon.vue.d.ts +2 -0
- package/dist/assets/svg/AudioPlayIcon.vue.d.ts +2 -0
- package/dist/assets/svg/CloseIcon.vue.d.ts +2 -0
- package/dist/assets/svg/DownloadIcon.vue.d.ts +2 -0
- package/dist/assets/svg/VideoPlayIcon.vue.d.ts +9 -0
- package/dist/assets/svg/index.d.ts +7 -0
- package/dist/chat-components-vue.css +1 -0
- package/dist/chat-components-vue.js +9858 -0
- package/dist/components/Message.vue.d.ts +11 -0
- package/dist/components/common/ActionButton.vue.d.ts +59 -0
- package/dist/components/common/ActionButtons.vue.d.ts +36 -0
- package/dist/components/common/ChatBubble.vue.d.ts +22 -0
- package/dist/components/common/ChatEvent.vue.d.ts +20 -0
- package/dist/components/common/LinkIcon.vue.d.ts +2 -0
- package/dist/components/common/TypingIndicator.vue.d.ts +21 -0
- package/dist/components/common/Typography.vue.d.ts +38 -0
- package/dist/components/messages/AdaptiveCard.vue.d.ts +2 -0
- package/dist/components/messages/AudioMessage.vue.d.ts +5 -0
- package/dist/components/messages/DatePicker.vue.d.ts +2 -0
- package/dist/components/messages/FileMessage.vue.d.ts +2 -0
- package/dist/components/messages/Gallery.vue.d.ts +2 -0
- package/dist/components/messages/GalleryItem.vue.d.ts +7 -0
- package/dist/components/messages/ImageMessage.vue.d.ts +5 -0
- package/dist/components/messages/List.vue.d.ts +2 -0
- package/dist/components/messages/ListItem.vue.d.ts +16 -0
- package/dist/components/messages/TextMessage.vue.d.ts +15 -0
- package/dist/components/messages/TextWithButtons.vue.d.ts +2 -0
- package/dist/components/messages/VideoMessage.vue.d.ts +5 -0
- package/dist/composables/useChannelPayload.d.ts +47 -0
- package/dist/composables/useCollation.d.ts +47 -0
- package/dist/composables/useImageContext.d.ts +13 -0
- package/dist/composables/useMessageContext.d.ts +18 -0
- package/dist/composables/useSanitize.d.ts +8 -0
- package/dist/index.d.ts +33 -0
- package/dist/types/index.d.ts +275 -0
- package/dist/utils/helpers.d.ts +56 -0
- package/dist/utils/matcher.d.ts +20 -0
- package/dist/utils/sanitize.d.ts +28 -0
- package/dist/utils/theme.d.ts +18 -0
- package/package.json +94 -0
- package/src/assets/svg/ArrowBackIcon.vue +30 -0
- package/src/assets/svg/AudioPauseIcon.vue +20 -0
- package/src/assets/svg/AudioPlayIcon.vue +19 -0
- package/src/assets/svg/CloseIcon.vue +10 -0
- package/src/assets/svg/DownloadIcon.vue +10 -0
- package/src/assets/svg/VideoPlayIcon.vue +25 -0
- package/src/assets/svg/index.ts +7 -0
- package/src/components/Message.vue +152 -0
- package/src/components/common/ActionButton.vue +354 -0
- package/src/components/common/ActionButtons.vue +170 -0
- package/src/components/common/ChatBubble.vue +109 -0
- package/src/components/common/ChatEvent.vue +84 -0
- package/src/components/common/LinkIcon.vue +34 -0
- package/src/components/common/TypingIndicator.vue +202 -0
- package/src/components/common/Typography.vue +196 -0
- package/src/components/messages/AdaptiveCard.vue +292 -0
- package/src/components/messages/AudioMessage.vue +391 -0
- package/src/components/messages/DatePicker.vue +135 -0
- package/src/components/messages/FileMessage.vue +195 -0
- package/src/components/messages/Gallery.vue +296 -0
- package/src/components/messages/GalleryItem.vue +214 -0
- package/src/components/messages/ImageMessage.vue +368 -0
- package/src/components/messages/List.vue +149 -0
- package/src/components/messages/ListItem.vue +344 -0
- package/src/components/messages/TextMessage.vue +203 -0
- package/src/components/messages/TextWithButtons.vue +119 -0
- package/src/components/messages/VideoMessage.vue +343 -0
- package/src/composables/useChannelPayload.ts +101 -0
- package/src/composables/useCollation.ts +163 -0
- package/src/composables/useImageContext.ts +27 -0
- package/src/composables/useMessageContext.ts +41 -0
- package/src/composables/useSanitize.ts +25 -0
- package/src/index.ts +71 -0
- package/src/types/index.ts +373 -0
- package/src/utils/helpers.ts +164 -0
- package/src/utils/matcher.ts +283 -0
- package/src/utils/sanitize.ts +133 -0
- 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
|
+
}
|