@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,292 @@
1
+ <template>
2
+ <ChatBubble
3
+ v-if="hasAdaptiveCard"
4
+ :class="['adaptivecard-wrapper', 'internal', $style.wrapper]"
5
+ data-testid="adaptive-card-message"
6
+ >
7
+ <div :class="$style.card">
8
+ <!-- Card icon/indicator -->
9
+ <div :class="$style.icon">
10
+ <svg
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ viewBox="0 0 24 24"
13
+ :width="24"
14
+ :height="24"
15
+ fill="none"
16
+ stroke="currentColor"
17
+ stroke-width="2"
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ >
21
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
22
+ <line x1="9" y1="9" x2="15" y2="9"></line>
23
+ <line x1="9" y1="15" x2="15" y2="15"></line>
24
+ </svg>
25
+ </div>
26
+
27
+ <!-- Card content -->
28
+ <div :class="$style.content">
29
+ <Typography
30
+ variant="title1-semibold"
31
+ component="div"
32
+ :class="$style.title"
33
+ >
34
+ {{ cardTitle }}
35
+ </Typography>
36
+
37
+ <Typography
38
+ v-if="cardBody"
39
+ variant="body-regular"
40
+ component="div"
41
+ :class="$style.body"
42
+ >
43
+ {{ cardBody }}
44
+ </Typography>
45
+
46
+ <div v-if="hasActions" :class="$style.actions">
47
+ <Typography variant="copy-medium" component="span" :class="$style.actionsLabel">
48
+ {{ actionsLabel }}
49
+ </Typography>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </ChatBubble>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ import { computed } from 'vue'
58
+ import ChatBubble from '../common/ChatBubble.vue'
59
+ import Typography from '../common/Typography.vue'
60
+ import { useMessageContext } from '../../composables/useMessageContext'
61
+ import { isAdaptiveCardPayload } from '../../types'
62
+
63
+ /**
64
+ * Simplified adaptive card structure for our rendering needs.
65
+ * The full IAdaptiveCard type from 'adaptivecards' package is complex,
66
+ * but we only need a subset for display purposes.
67
+ */
68
+ interface AdaptiveCardData {
69
+ title?: string
70
+ body?: unknown[]
71
+ actions?: unknown[]
72
+ speak?: string
73
+ }
74
+
75
+ /**
76
+ * Adaptive card sources from different payload locations
77
+ */
78
+ interface AdaptiveCardSources {
79
+ webchat: AdaptiveCardData | undefined
80
+ defaultPreview: AdaptiveCardData | undefined
81
+ plugin: AdaptiveCardData | undefined
82
+ }
83
+
84
+ /**
85
+ * Extract adaptive card from various message payload locations.
86
+ *
87
+ * Note on types: The socket-client types have limitations:
88
+ * - _defaultPreview is typed as `any`
89
+ * - _plugin.data is `any` for adaptivecards type
90
+ * We use runtime type guards to safely access these properties.
91
+ */
92
+ function getAdaptiveCardSources(message: ReturnType<typeof useMessageContext>['message']): AdaptiveCardSources {
93
+ const cognigyData = message?.data?._cognigy
94
+ const pluginData = message?.data?._plugin
95
+
96
+ // _webchat can be IWebchatMessage | IAdaptiveCardMessage - use type guard
97
+ const webchatPayload = cognigyData?._webchat
98
+ const webchat = isAdaptiveCardPayload(webchatPayload)
99
+ ? (webchatPayload.adaptiveCard as AdaptiveCardData)
100
+ : undefined
101
+
102
+ // _defaultPreview is typed as `any` in socket-client (upstream limitation)
103
+ // We safely check for adaptiveCard property
104
+ const defaultPreviewPayload = cognigyData?._defaultPreview
105
+ const defaultPreview = isAdaptiveCardPayload(defaultPreviewPayload)
106
+ ? (defaultPreviewPayload.adaptiveCard as AdaptiveCardData)
107
+ : undefined
108
+
109
+ // Plugin data can come in two formats:
110
+ // 1. Typed format: { type: 'adaptivecards', data: cardData }
111
+ // 2. Legacy format: { payload: cardData }
112
+ let plugin: AdaptiveCardData | undefined
113
+ if (pluginData) {
114
+ if (pluginData.type === 'adaptivecards' && 'data' in pluginData) {
115
+ plugin = pluginData.data as AdaptiveCardData
116
+ } else if ('payload' in pluginData) {
117
+ plugin = (pluginData as { payload?: unknown }).payload as AdaptiveCardData
118
+ }
119
+ }
120
+
121
+ return { webchat, defaultPreview, plugin }
122
+ }
123
+
124
+ // Message context
125
+ const { message, config } = useMessageContext()
126
+
127
+ // Get all adaptive card sources
128
+ const cardSources = computed(() => getAdaptiveCardSources(message))
129
+
130
+ // Check if this message has an adaptive card
131
+ const hasAdaptiveCard = computed(() => {
132
+ const { webchat, defaultPreview, plugin } = cardSources.value
133
+ return !!(webchat || defaultPreview || plugin)
134
+ })
135
+
136
+ // Get card payload based on configuration
137
+ const cardPayload = computed((): AdaptiveCardData | undefined => {
138
+ const { webchat, defaultPreview, plugin } = cardSources.value
139
+ const defaultPreviewEnabled = config?.settings?.widgetSettings?.enableDefaultPreview
140
+
141
+ if (webchat && defaultPreview && !defaultPreviewEnabled) {
142
+ return webchat
143
+ }
144
+ if (defaultPreview && defaultPreviewEnabled) {
145
+ return defaultPreview
146
+ }
147
+ return plugin || webchat
148
+ })
149
+
150
+ /**
151
+ * Adaptive card body element (simplified type for our use case)
152
+ */
153
+ interface AdaptiveCardElement {
154
+ type: string
155
+ text?: string
156
+ size?: string
157
+ }
158
+
159
+ /**
160
+ * Check if an item is a TextBlock element
161
+ */
162
+ function isTextBlock(item: unknown): item is AdaptiveCardElement & { type: 'TextBlock'; text: string } {
163
+ return (
164
+ typeof item === 'object' &&
165
+ item !== null &&
166
+ (item as AdaptiveCardElement).type === 'TextBlock' &&
167
+ typeof (item as AdaptiveCardElement).text === 'string'
168
+ )
169
+ }
170
+
171
+ // Extract card title
172
+ const cardTitle = computed(() => {
173
+ const card = cardPayload.value
174
+ if (!card) return 'Adaptive Card'
175
+
176
+ // Try to get title from card property (custom extension)
177
+ if ('title' in card && typeof card.title === 'string') {
178
+ return card.title
179
+ }
180
+
181
+ // Try to get from body elements
182
+ const body = card.body
183
+ if (body && Array.isArray(body)) {
184
+ // Look for large text block (likely a title)
185
+ const titleElement = body.find(
186
+ (item): item is AdaptiveCardElement =>
187
+ isTextBlock(item) && item.size === 'large'
188
+ )
189
+ if (titleElement?.text) {
190
+ return titleElement.text
191
+ }
192
+
193
+ // Fallback to first TextBlock
194
+ const firstText = body.find(isTextBlock)
195
+ if (firstText?.text) {
196
+ return firstText.text
197
+ }
198
+ }
199
+
200
+ // Fallback to speak text or generic title
201
+ if (card.speak) {
202
+ return card.speak.substring(0, 50)
203
+ }
204
+
205
+ return 'Adaptive Card'
206
+ })
207
+
208
+ // Extract card body text
209
+ const cardBody = computed(() => {
210
+ const body = cardPayload.value?.body
211
+ if (!body || !Array.isArray(body)) {
212
+ return null
213
+ }
214
+
215
+ // Get text from body elements (skip the title)
216
+ const titleText = cardTitle.value
217
+ const bodyTexts = body
218
+ .filter((item): item is AdaptiveCardElement & { text: string } =>
219
+ isTextBlock(item) && item.text !== titleText
220
+ )
221
+ .map(item => item.text)
222
+ .slice(0, 2) // Limit to first 2 text blocks
223
+
224
+ return bodyTexts.length > 0 ? bodyTexts.join(' ') : null
225
+ })
226
+
227
+ // Check if card has actions
228
+ const hasActions = computed(() => {
229
+ return cardPayload.value?.actions && Array.isArray(cardPayload.value.actions) && cardPayload.value.actions.length > 0
230
+ })
231
+
232
+ // Actions label
233
+ const actionsLabel = computed(() => {
234
+ const actions = cardPayload.value?.actions
235
+ if (!actions || !Array.isArray(actions) || actions.length === 0) return ''
236
+
237
+ const count = actions.length
238
+ return count === 1 ? '1 action available' : `${count} actions available`
239
+ })
240
+ </script>
241
+
242
+ <style module>
243
+ .wrapper {
244
+ max-width: 400px;
245
+ }
246
+
247
+ .card {
248
+ display: flex;
249
+ gap: 12px;
250
+ padding: 16px;
251
+ background-color: var(--cc-white, #ffffff);
252
+ border-radius: var(--cc-bubble-border-radius, 15px);
253
+ }
254
+
255
+ .icon {
256
+ flex-shrink: 0;
257
+ color: var(--cc-primary-color, #1976d2);
258
+ display: flex;
259
+ align-items: flex-start;
260
+ padding-top: 2px;
261
+ }
262
+
263
+ .content {
264
+ flex: 1;
265
+ display: flex;
266
+ flex-direction: column;
267
+ gap: 8px;
268
+ min-width: 0;
269
+ }
270
+
271
+ .title {
272
+ color: var(--cc-black-10, rgba(0, 0, 0, 0.1));
273
+ margin: 0;
274
+ }
275
+
276
+ .body {
277
+ color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
278
+ margin: 0;
279
+ overflow-wrap: break-word;
280
+ }
281
+
282
+ .actions {
283
+ margin-top: 4px;
284
+ padding-top: 8px;
285
+ border-top: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
286
+ }
287
+
288
+ .actionsLabel {
289
+ color: var(--cc-black-40, rgba(0, 0, 0, 0.4));
290
+ font-style: italic;
291
+ }
292
+ </style>
@@ -0,0 +1,391 @@
1
+ <template>
2
+ <div v-if="audioData.url" :class="[$style.wrapper, 'webchat-media-template-audio']" data-testid="audio-message">
3
+ <!-- Hidden audio element -->
4
+ <audio
5
+ ref="audioRef"
6
+ :src="audioData.url"
7
+ @play="handlePlay"
8
+ @pause="handlePause"
9
+ @timeupdate="handleTimeUpdate"
10
+ @loadedmetadata="handleLoadedMetadata"
11
+ @ended="handleEnded"
12
+ style="display: none"
13
+ />
14
+
15
+ <!-- Custom controls -->
16
+ <div :class="$style.audioWrapper" data-testid="audio-controls">
17
+ <div :class="$style.controls">
18
+ <!-- Time remaining -->
19
+ <div class="duration">
20
+ <time>{{ formattedTime }}</time>
21
+ </div>
22
+
23
+ <!-- Progress bar -->
24
+ <div :class="$style.progressBar">
25
+ <input
26
+ type="range"
27
+ min="0"
28
+ max="0.999999"
29
+ step="any"
30
+ :value="progress"
31
+ :aria-valuetext="audioTimeValueText"
32
+ :aria-label="audioPlaybackProgressLabel"
33
+ :style="progressBarStyle"
34
+ @mousedown="handleSeekStart"
35
+ @touchstart="handleSeekStart"
36
+ @input="handleSeekChange"
37
+ @mouseup="handleSeekEnd"
38
+ @touchend="handleSeekEnd"
39
+ />
40
+ </div>
41
+
42
+ <!-- Play/pause button -->
43
+ <div class="buttons">
44
+ <button
45
+ :class="$style.playButton"
46
+ :aria-label="playing ? pauseAudioLabel : playAudioLabel"
47
+ @click="togglePlayPause"
48
+ >
49
+ <AudioPauseIcon v-if="playing" />
50
+ <AudioPlayIcon v-else />
51
+ </button>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Download transcript button -->
56
+ <button
57
+ v-if="audioData.altText"
58
+ :class="$style.downloadButton"
59
+ :aria-label="downloadTranscriptLabel"
60
+ data-testid="download-transcript-button"
61
+ @click="downloadTranscript"
62
+ >
63
+ <DownloadIcon />
64
+ </button>
65
+ <a
66
+ v-if="audioData.altText"
67
+ ref="downloadLinkRef"
68
+ :href="transcriptDataUrl"
69
+ download="audio-transcript.txt"
70
+ style="display: none"
71
+ aria-hidden="true"
72
+ />
73
+ </div>
74
+ </div>
75
+ </template>
76
+
77
+ <script setup lang="ts">
78
+ import { ref, computed, onMounted, useCssModule } from 'vue'
79
+ import { useMessageContext } from '../../composables/useMessageContext'
80
+ import { getChannelPayload } from '../../utils/matcher'
81
+ import { interpolateString } from '../../utils/helpers'
82
+ import { DownloadIcon, AudioPlayIcon, AudioPauseIcon } from '../../assets/svg'
83
+ import type { IWebchatAudioAttachment } from '../../types'
84
+
85
+ const { message, config } = useMessageContext()
86
+
87
+ const $style = useCssModule()
88
+
89
+ // Refs
90
+ const audioRef = ref<HTMLAudioElement>()
91
+ const downloadLinkRef = ref<HTMLAnchorElement>()
92
+
93
+ // State
94
+ const playing = ref(false)
95
+ const progress = ref(0)
96
+ const duration = ref(0)
97
+ const currentTime = ref(0)
98
+
99
+ // Get audio data from message payload
100
+ const payload = computed(() => getChannelPayload(message, config))
101
+ const audioData = computed(() => {
102
+ const attachment = payload.value?.message?.attachment as IWebchatAudioAttachment
103
+ return {
104
+ url: attachment?.payload?.url || '',
105
+ altText: attachment?.payload?.altText,
106
+ }
107
+ })
108
+
109
+ // Format time display (time remaining)
110
+ const formattedTime = computed(() => {
111
+ const padString = (num: number) => {
112
+ return ('0' + num).toString().slice(-2)
113
+ }
114
+
115
+ const seconds = duration.value * (1 - Math.min(1, progress.value))
116
+ const date = new Date(seconds * 1000)
117
+ const hh = date.getUTCHours()
118
+ const mm = date.getUTCMinutes()
119
+ const ss = padString(date.getUTCSeconds())
120
+
121
+ if (hh) {
122
+ return `${hh}:${padString(mm)}:${ss}`
123
+ }
124
+ return `${mm}:${ss}`
125
+ })
126
+
127
+ // Convert formatted time to readable text for screen readers
128
+ const timeToText = (time: string) => {
129
+ let timeStr = time
130
+ if (timeStr.length < 6) {
131
+ timeStr = `00:${timeStr}`
132
+ }
133
+ const [hours, minutes, seconds] = timeStr.split(':').map(Number)
134
+ const hoursText = hours ? `${hours} hours ` : ''
135
+ const minutesText = minutes ? `${minutes} minutes ` : ''
136
+ const secondsText = `${seconds} seconds`
137
+ return `${hoursText}${minutesText}${secondsText}`
138
+ }
139
+
140
+ // Progress bar style (linear gradient)
141
+ const progressBarStyle = computed(() => ({
142
+ background: `linear-gradient(to right, var(--cc-primary-color-focus) ${
143
+ progress.value * 100
144
+ }%, var(--cc-black-80) ${progress.value * 100}%)`,
145
+ }))
146
+
147
+ // Transcript data URL for download
148
+ const transcriptDataUrl = computed(() => {
149
+ const text = audioData.value.altText || ''
150
+ return `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`
151
+ })
152
+
153
+ // Translations
154
+ const audioPlaybackProgressLabel = computed(() =>
155
+ config?.settings?.customTranslations?.ariaLabels?.audioPlaybackProgress || 'Audio playback progress'
156
+ )
157
+ const playAudioLabel = computed(() =>
158
+ config?.settings?.customTranslations?.ariaLabels?.playAudio || 'Play audio'
159
+ )
160
+ const pauseAudioLabel = computed(() =>
161
+ config?.settings?.customTranslations?.ariaLabels?.pauseAudio || 'Pause audio'
162
+ )
163
+ const downloadTranscriptLabel = computed(() =>
164
+ config?.settings?.customTranslations?.ariaLabels?.downloadTranscript || 'Download transcript'
165
+ )
166
+ const audioTimeRemainingLabel = computed(() =>
167
+ config?.settings?.customTranslations?.ariaLabels?.audioTimeRemaining ?? '{time} remaining'
168
+ )
169
+
170
+ // Audio time value text for ARIA
171
+ const audioTimeValueText = computed(() =>
172
+ interpolateString(audioTimeRemainingLabel.value, {
173
+ time: timeToText(formattedTime.value),
174
+ })
175
+ )
176
+
177
+ // Handlers
178
+ const handlePlay = () => {
179
+ playing.value = true
180
+ }
181
+
182
+ const handlePause = () => {
183
+ playing.value = false
184
+ }
185
+
186
+ const handleTimeUpdate = () => {
187
+ if (audioRef.value && duration.value > 0) {
188
+ currentTime.value = audioRef.value.currentTime
189
+ progress.value = currentTime.value / duration.value
190
+ }
191
+ }
192
+
193
+ const handleLoadedMetadata = () => {
194
+ if (audioRef.value) {
195
+ duration.value = audioRef.value.duration
196
+ }
197
+ }
198
+
199
+ const handleEnded = () => {
200
+ playing.value = false
201
+ progress.value = 0
202
+ if (audioRef.value) {
203
+ audioRef.value.currentTime = 0
204
+ }
205
+ }
206
+
207
+ const togglePlayPause = () => {
208
+ if (audioRef.value) {
209
+ if (playing.value) {
210
+ audioRef.value.pause()
211
+ } else {
212
+ audioRef.value.play()
213
+ }
214
+ }
215
+ }
216
+
217
+ const handleSeekStart = () => {
218
+ if (audioRef.value && playing.value) {
219
+ audioRef.value.pause()
220
+ }
221
+ }
222
+
223
+ const handleSeekChange = (event: Event) => {
224
+ const target = event.target as HTMLInputElement
225
+ const newProgress = parseFloat(target.value)
226
+ progress.value = newProgress
227
+
228
+ if (audioRef.value && duration.value > 0) {
229
+ audioRef.value.currentTime = newProgress * duration.value
230
+ }
231
+ }
232
+
233
+ const handleSeekEnd = () => {
234
+ if (audioRef.value && !playing.value) {
235
+ audioRef.value.play()
236
+ }
237
+ }
238
+
239
+ const downloadTranscript = () => {
240
+ downloadLinkRef.value?.click()
241
+ }
242
+
243
+ // Auto-focus audio on mount if configured
244
+ onMounted(() => {
245
+ if (!config?.settings?.widgetSettings?.enableAutoFocus) return
246
+
247
+ const chatHistory = document.getElementById('webchatChatHistoryWrapperLiveLogPanel')
248
+ if (!chatHistory?.contains(document.activeElement)) return
249
+
250
+ setTimeout(() => {
251
+ audioRef.value?.focus()
252
+ }, 100)
253
+ })
254
+ </script>
255
+
256
+ <style module>
257
+ .wrapper {
258
+ border-radius: var(--cc-bubble-border-radius, 15px);
259
+ max-width: 295px;
260
+ position: relative;
261
+ display: flex;
262
+ }
263
+
264
+ .audioWrapper {
265
+ display: flex;
266
+ align-items: center;
267
+ justify-content: space-between;
268
+ height: 52px;
269
+ width: 100%;
270
+ gap: 10px;
271
+ background-color: var(--cc-white, #ffffff);
272
+ border-radius: var(--cc-bubble-border-radius, 15px);
273
+ border: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
274
+ padding: 0px 12px;
275
+ }
276
+
277
+ .audioWrapper .controls {
278
+ display: flex;
279
+ flex-direction: row;
280
+ align-items: center;
281
+ justify-content: space-between;
282
+ gap: 16px;
283
+ width: 100%;
284
+ }
285
+
286
+ .audioWrapper .downloadButton {
287
+ background-color: transparent;
288
+ border: none;
289
+ outline: none;
290
+ cursor: pointer;
291
+ width: 30px;
292
+ height: 30px;
293
+ }
294
+
295
+ .audioWrapper .downloadButton:hover :deep(svg path) {
296
+ fill-opacity: 0.85;
297
+ }
298
+
299
+ .audioWrapper .downloadButton:focus-visible {
300
+ outline: 2px solid var(--cc-primary-color-focus, #1976d2);
301
+ outline-offset: 2px;
302
+ }
303
+
304
+ button.playButton {
305
+ all: unset;
306
+ outline: revert;
307
+ line-height: 0;
308
+ display: block;
309
+ cursor: pointer;
310
+ }
311
+
312
+ button.playButton:hover,
313
+ button.playButton:focus {
314
+ outline: none;
315
+ }
316
+
317
+ button.playButton:focus-visible {
318
+ outline: 2px solid var(--cc-primary-color-focus, #1976d2);
319
+ outline-offset: 2px;
320
+ }
321
+
322
+ button.playButton:focus :deep(svg circle),
323
+ button.playButton:hover :deep(svg circle) {
324
+ fill-opacity: 0.85;
325
+ }
326
+
327
+ /* Custom range input */
328
+ .progressBar {
329
+ display: flex;
330
+ width: 100%;
331
+ }
332
+
333
+ .progressBar input[type="range"] {
334
+ -webkit-appearance: none;
335
+ appearance: none;
336
+ width: 100%;
337
+ cursor: pointer;
338
+ outline: none;
339
+ border-radius: var(--cc-bubble-border-radius, 15px);
340
+ height: 3px;
341
+ background: var(--cc-black-80, rgba(0, 0, 0, 0.8));
342
+ }
343
+
344
+ /* Thumb: Webkit */
345
+ .progressBar input[type="range"]::-webkit-slider-thumb {
346
+ -webkit-appearance: none;
347
+ appearance: none;
348
+ height: 13px;
349
+ width: 13px;
350
+ background-color: var(--cc-primary-color-focus, #1976d2);
351
+ border-radius: 50%;
352
+ border: none;
353
+ transition: 0.2s ease-in-out;
354
+ }
355
+
356
+ /* Thumb: Firefox */
357
+ .progressBar input[type="range"]::-moz-range-thumb {
358
+ height: 13px;
359
+ width: 13px;
360
+ background-color: var(--cc-primary-color-focus, #1976d2);
361
+ border-radius: 50%;
362
+ border: none;
363
+ transition: 0.2s ease-in-out;
364
+ }
365
+
366
+ /* Hover, active & focus Thumb: Webkit */
367
+ .progressBar input[type="range"]::-webkit-slider-thumb:hover {
368
+ box-shadow: 0 0 0 7px var(--cc-primary-color-opacity-10, rgba(25, 118, 210, 0.1));
369
+ }
370
+
371
+ .progressBar input[type="range"]:active::-webkit-slider-thumb {
372
+ box-shadow: 0 0 0 9px var(--cc-primary-color-opacity-20, rgba(25, 118, 210, 0.2));
373
+ }
374
+
375
+ .progressBar input[type="range"]:focus::-webkit-slider-thumb {
376
+ box-shadow: 0 0 0 9px var(--cc-primary-color-opacity-20, rgba(25, 118, 210, 0.2));
377
+ }
378
+
379
+ /* Hover, active & focus Thumb: Firefox */
380
+ .progressBar input[type="range"]::-moz-range-thumb:hover {
381
+ box-shadow: 0 0 0 7px var(--cc-primary-color-opacity-10, rgba(25, 118, 210, 0.1));
382
+ }
383
+
384
+ .progressBar input[type="range"]:active::-moz-range-thumb {
385
+ box-shadow: 0 0 0 9px var(--cc-primary-color-opacity-20, rgba(25, 118, 210, 0.2));
386
+ }
387
+
388
+ .progressBar input[type="range"]:focus::-moz-range-thumb {
389
+ box-shadow: 0 0 0 9px var(--cc-primary-color-opacity-20, rgba(25, 118, 210, 0.2));
390
+ }
391
+ </style>