@cognigy/chat-components-vue 0.1.0 → 0.3.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 (40) hide show
  1. package/dist/chat-components-vue.css +1 -1
  2. package/dist/chat-components-vue.js +12386 -5741
  3. package/dist/components/Message.vue.d.ts +4 -0
  4. package/dist/components/common/Typography.vue.d.ts +1 -1
  5. package/dist/components/messages/AdaptiveCardRenderer.vue.d.ts +35 -0
  6. package/dist/components/messages/ListItem.vue.d.ts +1 -1
  7. package/dist/composables/useLiveRegion.d.ts +30 -0
  8. package/dist/index.d.ts +3 -2
  9. package/dist/types/index.d.ts +105 -1
  10. package/dist/utils/helpers.d.ts +3 -2
  11. package/dist/utils/matcher.d.ts +3 -3
  12. package/dist/utils/theme.d.ts +12 -1
  13. package/package.json +8 -3
  14. package/src/components/Message.vue +98 -55
  15. package/src/components/common/ActionButton.vue +16 -7
  16. package/src/components/common/ChatBubble.vue +8 -6
  17. package/src/components/common/ChatEvent.vue +5 -2
  18. package/src/components/common/TypingIndicator.vue +4 -1
  19. package/src/components/common/Typography.vue +56 -67
  20. package/src/components/messages/AdaptiveCard.vue +322 -225
  21. package/src/components/messages/AdaptiveCardRenderer.vue +260 -0
  22. package/src/components/messages/AudioMessage.vue +4 -1
  23. package/src/components/messages/DatePicker.vue +5 -27
  24. package/src/components/messages/FileMessage.vue +12 -3
  25. package/src/components/messages/Gallery.vue +96 -10
  26. package/src/components/messages/GalleryItem.vue +17 -5
  27. package/src/components/messages/ImageMessage.vue +20 -5
  28. package/src/components/messages/List.vue +56 -42
  29. package/src/components/messages/ListItem.vue +105 -68
  30. package/src/components/messages/TextMessage.vue +1 -1
  31. package/src/components/messages/TextWithButtons.vue +35 -11
  32. package/src/components/messages/VideoMessage.vue +35 -26
  33. package/src/composables/useCollation.ts +28 -45
  34. package/src/composables/useLiveRegion.ts +101 -0
  35. package/src/index.ts +4 -1
  36. package/src/types/index.ts +127 -2
  37. package/src/utils/helpers.ts +46 -24
  38. package/src/utils/matcher.ts +20 -6
  39. package/src/utils/sanitize.ts +1 -2
  40. package/src/utils/theme.ts +42 -1
@@ -16,7 +16,9 @@
16
16
 
17
17
  <!-- Regular list items -->
18
18
  <ul
19
- :aria-labelledby="headerElement ? `listHeader-header-${listTemplateId}` : undefined"
19
+ :aria-labelledby="
20
+ headerElement ? `listHeader-header-${listTemplateId}` : undefined
21
+ "
20
22
  :class="$style.list"
21
23
  >
22
24
  <ListItem
@@ -26,7 +28,9 @@
26
28
  :headingLevel="headerElement ? 'h5' : 'h4'"
27
29
  :id="`${listTemplateId}-${index}`"
28
30
  :dividerBefore="index > 0"
29
- :dividerAfter="Boolean(globalButton && index === regularElements.length - 1)"
31
+ :dividerAfter="
32
+ Boolean(globalButton && index === regularElements.length - 1)
33
+ "
30
34
  />
31
35
  </ul>
32
36
 
@@ -39,6 +43,7 @@
39
43
  :containerClassName="$style.mainButtonWrapper"
40
44
  :config="config"
41
45
  :dataMessageId="dataMessageId"
46
+ :openXAppOverlay="openXAppOverlay"
42
47
  :onEmitAnalytics="onEmitAnalytics"
43
48
  size="large"
44
49
  />
@@ -46,86 +51,94 @@
46
51
  </template>
47
52
 
48
53
  <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'
54
+ import { computed, onMounted, useCssModule } from "vue";
55
+ import ListItem from "./ListItem.vue";
56
+ import ActionButtons from "../common/ActionButtons.vue";
57
+ import { useMessageContext } from "../../composables/useMessageContext";
58
+ import { getChannelPayload } from "../../utils/matcher";
59
+ import { getRandomId } from "../../utils/helpers";
60
+ import type { IWebchatTemplateAttachment } from "../../types";
56
61
 
57
- const $style = useCssModule()
62
+ const $style = useCssModule();
58
63
 
59
64
  // Message context
60
- const { message, config, action, onEmitAnalytics } = useMessageContext()
61
- const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
65
+ const { message, config, action, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext();
66
+ const dataMessageId = window.__TEST_MESSAGE_ID__; // For testing
62
67
 
63
68
  // Get list data from message payload
64
- const payload = computed(() => getChannelPayload(message, config))
65
- const attachment = computed(() => payload.value?.message?.attachment as IWebchatTemplateAttachment | undefined)
69
+ const payload = computed(() => getChannelPayload(message, config));
70
+ const attachment = computed(
71
+ () =>
72
+ payload.value?.message?.attachment as IWebchatTemplateAttachment | undefined
73
+ );
66
74
 
67
75
  // Extract list elements and configuration
68
76
  const elements = computed(() => {
69
- return attachment.value?.payload?.elements || []
70
- })
77
+ return attachment.value?.payload?.elements || [];
78
+ });
71
79
 
72
80
  const topElementStyle = computed(() => {
73
- return attachment.value?.payload?.top_element_style
74
- })
81
+ return attachment.value?.payload?.top_element_style;
82
+ });
75
83
 
76
84
  const showTopElementLarge = computed(() => {
77
- return topElementStyle.value === 'large' || topElementStyle.value === true
78
- })
85
+ return topElementStyle.value === "large" || topElementStyle.value === true;
86
+ });
79
87
 
80
88
  // Split elements into header and regular items
81
89
  const headerElement = computed(() => {
82
- return showTopElementLarge.value ? elements.value[0] : null
83
- })
90
+ return showTopElementLarge.value ? elements.value[0] : null;
91
+ });
84
92
 
85
93
  const regularElements = computed(() => {
86
- return showTopElementLarge.value ? elements.value.slice(1) : elements.value
87
- })
94
+ return showTopElementLarge.value ? elements.value.slice(1) : elements.value;
95
+ });
88
96
 
89
97
  // Global button (first button in buttons array)
90
98
  const globalButton = computed(() => {
91
- const buttons = attachment.value?.payload?.buttons
92
- return buttons?.[0]
93
- })
99
+ const buttons = attachment.value?.payload?.buttons;
100
+ return buttons?.[0];
101
+ });
94
102
 
95
103
  // Should buttons be disabled
96
104
  const shouldBeDisabled = computed(() => {
97
- // TODO: Add conversation ended check when messageParams available
98
- return false
105
+ return messageParams?.isConversationEnded ?? false
99
106
  })
100
107
 
101
108
  // Generate unique ID for list
102
- const listTemplateId = getRandomId('webchatListTemplateRoot')
109
+ const listTemplateId = getRandomId("webchatListTemplateRoot");
103
110
 
104
111
  // Auto-focus first focusable element on mount
105
112
  onMounted(() => {
106
- if (!config?.settings?.widgetSettings?.enableAutoFocus) return
113
+ if (!config?.settings?.widgetSettings?.enableAutoFocus) return;
107
114
 
108
- const chatHistory = document.getElementById('webchatChatHistoryWrapperLiveLogPanel')
109
- if (!chatHistory?.contains(document.activeElement)) return
115
+ const chatHistory = document.getElementById(
116
+ "webchatChatHistoryWrapperLiveLogPanel"
117
+ );
118
+ if (!chatHistory?.contains(document.activeElement)) return;
110
119
 
111
120
  setTimeout(() => {
112
- const listTemplateRoot = document.getElementById(listTemplateId)
121
+ const listTemplateRoot = document.getElementById(listTemplateId);
113
122
  // Get the first focusable element within the list and add focus
114
123
  const focusable = listTemplateRoot?.querySelectorAll(
115
124
  'button, [href], [tabindex]:not([tabindex="-1"])'
116
- )
117
- const firstFocusable = focusable?.[0] as HTMLElement
118
- firstFocusable?.focus()
119
- }, 200)
120
- })
125
+ );
126
+ const firstFocusable = focusable?.[0] as HTMLElement;
127
+ firstFocusable?.focus();
128
+ }, 200);
129
+ });
121
130
  </script>
122
131
 
123
132
  <style module>
124
133
  .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));
134
+ max-width: 250px;
135
+ border-radius: var(--cc-bubble-border-radius, 16px);
136
+ overflow: hidden;
128
137
  background-color: var(--cc-white, #ffffff);
138
+ color: rgba(0, 0, 0, 0.8);
139
+ box-shadow: var(--cc-message-shadow, rgba(151, 124, 156, 0.1) 0px 5px 9px 0px,
140
+ rgba(203, 195, 212, 0.1) 0px 5px 16px 0px,
141
+ rgba(216, 212, 221, 0.1) 0px 8px 20px 0px);
129
142
  }
130
143
 
131
144
  .wrapper .listItemRoot {
@@ -137,6 +150,7 @@ onMounted(() => {
137
150
  list-style: none;
138
151
  padding-inline-start: 0;
139
152
  margin-block: 0;
153
+ margin-left: 0;
140
154
  }
141
155
 
142
156
  .list .listItemRoot {
@@ -2,7 +2,10 @@
2
2
  <component
3
3
  :is="componentTag"
4
4
  :class="[isHeaderElement && $style.headerRoot, $style.listItemRoot]"
5
- :style="{ backgroundImage: isHeaderElement && element.image_url ? backgroundImage : undefined }"
5
+ :style="{
6
+ backgroundImage:
7
+ isHeaderElement && element.image_url ? backgroundImage : undefined,
8
+ }"
6
9
  :data-testid="isHeaderElement ? 'header-image' : 'list-item'"
7
10
  :id="id"
8
11
  >
@@ -13,17 +16,25 @@
13
16
  <div
14
17
  :class="contentClasses"
15
18
  :role="defaultActionUrl ? 'link' : undefined"
16
- :aria-label="defaultActionUrl ? `${titleHtml}. ${opensInNewTabLabel}` : undefined"
19
+ :aria-label="
20
+ defaultActionUrl ? `${titleHtml}. ${opensInNewTabLabel}` : undefined
21
+ "
17
22
  :aria-describedby="element.subtitle ? subtitleId : undefined"
18
23
  :tabindex="defaultActionUrl ? 0 : -1"
19
- :style="defaultActionUrl && !shouldBeDisabled ? { cursor: 'pointer' } : {}"
24
+ :style="
25
+ defaultActionUrl && !shouldBeDisabled ? { cursor: 'pointer' } : {}
26
+ "
20
27
  @click="handleClick"
21
28
  @keydown="handleKeyDown"
22
29
  >
23
30
  <!-- Header element content -->
24
31
  <div
25
32
  v-if="isHeaderElement"
26
- :class="['webchat-list-template-header-content', $style.headerContent, button && $style.headerContentWithButton]"
33
+ :class="[
34
+ 'webchat-list-template-header-content',
35
+ $style.headerContent,
36
+ button && $style.headerContentWithButton,
37
+ ]"
27
38
  >
28
39
  <!-- Title and subtitle -->
29
40
  <Typography
@@ -31,8 +42,10 @@
31
42
  :variant="isHeaderElement ? 'h2-semibold' : 'title1-semibold'"
32
43
  :component="headingLevel"
33
44
  :class="[
34
- isHeaderElement ? 'webchat-list-template-header-title' : 'webchat-list-template-element-title',
35
- subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle
45
+ isHeaderElement
46
+ ? 'webchat-list-template-header-title'
47
+ : 'webchat-list-template-element-title',
48
+ subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle,
36
49
  ]"
37
50
  :id="isHeaderElement ? `listHeader-${id}` : `listItemHeader-${id}`"
38
51
  v-html="titleHtml"
@@ -42,8 +55,10 @@
42
55
  v-if="subtitleHtml"
43
56
  variant="body-regular"
44
57
  :class="[
45
- isHeaderElement ? 'webchat-list-template-header-subtitle' : 'webchat-list-template-element-subtitle',
46
- $style.itemSubtitle
58
+ isHeaderElement
59
+ ? 'webchat-list-template-header-subtitle'
60
+ : 'webchat-list-template-element-subtitle',
61
+ $style.itemSubtitle,
47
62
  ]"
48
63
  :id="subtitleId"
49
64
  v-html="subtitleHtml"
@@ -53,7 +68,10 @@
53
68
  <!-- Regular list item content -->
54
69
  <div
55
70
  v-else
56
- :class="['webchat-list-template-element-content', $style.listItemContent]"
71
+ :class="[
72
+ 'webchat-list-template-element-content',
73
+ $style.listItemContent,
74
+ ]"
57
75
  >
58
76
  <div :class="$style.listItemText">
59
77
  <!-- Title and subtitle -->
@@ -63,7 +81,7 @@
63
81
  :component="headingLevel"
64
82
  :class="[
65
83
  'webchat-list-template-element-title',
66
- subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle
84
+ subtitleHtml ? $style.itemTitleWithSubtitle : $style.itemTitle,
67
85
  ]"
68
86
  :id="`listItemHeader-${id}`"
69
87
  v-html="titleHtml"
@@ -72,7 +90,10 @@
72
90
  <Typography
73
91
  v-if="subtitleHtml"
74
92
  variant="body-regular"
75
- :class="['webchat-list-template-element-subtitle', $style.itemSubtitle]"
93
+ :class="[
94
+ 'webchat-list-template-element-subtitle',
95
+ $style.itemSubtitle,
96
+ ]"
76
97
  :id="subtitleId"
77
98
  v-html="subtitleHtml"
78
99
  />
@@ -95,10 +116,19 @@
95
116
  v-if="button"
96
117
  :payload="[button]"
97
118
  :action="shouldBeDisabled ? undefined : action"
98
- :buttonClassName="isHeaderElement ? 'webchat-list-template-header-button' : 'webchat-list-template-element-button'"
99
- :containerClassName="isHeaderElement ? $style.listHeaderButtonWrapper : $style.listItemButtonWrapper"
119
+ :buttonClassName="
120
+ isHeaderElement
121
+ ? 'webchat-list-template-header-button'
122
+ : 'webchat-list-template-element-button'
123
+ "
124
+ :containerClassName="
125
+ isHeaderElement
126
+ ? $style.listHeaderButtonWrapper
127
+ : $style.listItemButtonWrapper
128
+ "
100
129
  :config="config"
101
130
  :dataMessageId="dataMessageId"
131
+ :openXAppOverlay="openXAppOverlay"
102
132
  :onEmitAnalytics="onEmitAnalytics"
103
133
  size="large"
104
134
  />
@@ -109,104 +139,106 @@
109
139
  </template>
110
140
 
111
141
  <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'
142
+ import { computed, useCssModule } from "vue";
143
+ import Typography from "../common/Typography.vue";
144
+ import ActionButtons from "../common/ActionButtons.vue";
145
+ import { useMessageContext } from "../../composables/useMessageContext";
146
+ import { useSanitize } from "../../composables/useSanitize";
147
+ import { getRandomId, getBackgroundImage } from "../../utils/helpers";
148
+ import { sanitizeUrl } from "@braintree/sanitize-url";
149
+ import type { IWebchatAttachmentElement } from "../../types";
120
150
 
121
151
  interface Props {
122
- element: IWebchatAttachmentElement
123
- isHeaderElement?: boolean
124
- headingLevel?: 'h4' | 'h5'
125
- id: string
126
- dividerBefore?: boolean
127
- dividerAfter?: boolean
152
+ element: IWebchatAttachmentElement;
153
+ isHeaderElement?: boolean;
154
+ headingLevel?: "h4" | "h5";
155
+ id: string;
156
+ dividerBefore?: boolean;
157
+ dividerAfter?: boolean;
128
158
  }
129
159
 
130
160
  const props = withDefaults(defineProps<Props>(), {
131
161
  isHeaderElement: false,
132
- headingLevel: 'h4',
162
+ headingLevel: "h4",
133
163
  dividerBefore: false,
134
164
  dividerAfter: false,
135
- })
165
+ });
136
166
 
137
- const $style = useCssModule()
167
+ const $style = useCssModule();
138
168
 
139
169
  // Context
140
- const { action, config, onEmitAnalytics } = useMessageContext()
141
- const dataMessageId = window.__TEST_MESSAGE_ID__ // For testing
170
+ const { action, config, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext();
171
+ const dataMessageId = window.__TEST_MESSAGE_ID__; // For testing
142
172
 
143
173
  // Sanitize HTML
144
- const { processHTML } = useSanitize()
145
- const titleHtml = computed(() => processHTML(props.element.title || ''))
146
- const subtitleHtml = computed(() => processHTML(props.element.subtitle || ''))
174
+ const { processHTML } = useSanitize();
175
+ const titleHtml = computed(() => processHTML(props.element.title || ""));
176
+ const subtitleHtml = computed(() => processHTML(props.element.subtitle || ""));
147
177
 
148
178
  // IDs for accessibility
149
- const subtitleId = getRandomId('webchatListTemplateHeaderSubtitle')
179
+ const subtitleId = getRandomId("webchatListTemplateHeaderSubtitle");
150
180
 
151
181
  // Background image
152
182
  const backgroundImage = computed(() => {
153
- if (!props.element.image_url) return undefined
154
- return getBackgroundImage(props.element.image_url)
155
- })
183
+ if (!props.element.image_url) return undefined;
184
+ return getBackgroundImage(props.element.image_url);
185
+ });
156
186
 
157
187
  // Button (only first button is used)
158
188
  const button = computed(() => {
159
- return props.element.buttons?.[0]
160
- })
189
+ return props.element.buttons?.[0];
190
+ });
161
191
 
162
192
  // Default action URL (clickable item)
163
193
  const defaultActionUrl = computed(() => {
164
- return props.element.default_action?.url
165
- })
194
+ return props.element.default_action?.url;
195
+ });
166
196
 
167
197
  // Should buttons be disabled
168
198
  const shouldBeDisabled = computed(() => {
169
- // TODO: Add conversation ended check when messageParams available
170
- return false
199
+ return messageParams?.isConversationEnded ?? false
171
200
  })
172
201
 
173
202
  // Translations
174
203
  const opensInNewTabLabel = computed(() => {
175
- return config?.settings?.customTranslations?.ariaLabels?.opensInNewTab || 'Opens in new tab'
176
- })
204
+ return (
205
+ config?.settings?.customTranslations?.ariaLabels?.opensInNewTab ||
206
+ "Opens in new tab"
207
+ );
208
+ });
177
209
 
178
210
  // Component tag (div for header, li for regular items)
179
211
  const componentTag = computed(() => {
180
- return props.isHeaderElement ? 'div' : 'li'
181
- })
212
+ return props.isHeaderElement ? "div" : "li";
213
+ });
182
214
 
183
215
  // Content classes
184
216
  const contentClasses = computed(() => {
185
217
  return props.isHeaderElement
186
- ? ['webchat-list-template-header', $style.headerContentWrapper]
187
- : ['webchat-list-template-element', $style.listItemWrapper]
188
- })
218
+ ? ["webchat-list-template-header", $style.headerContentWrapper]
219
+ : ["webchat-list-template-element", $style.listItemWrapper];
220
+ });
189
221
 
190
222
  // Handle item click (default action)
191
223
  const handleClick = () => {
192
- if (shouldBeDisabled.value || !defaultActionUrl.value) return
224
+ if (shouldBeDisabled.value || !defaultActionUrl.value) return;
193
225
 
194
226
  const url = config?.settings?.layout?.disableUrlButtonSanitization
195
227
  ? defaultActionUrl.value
196
- : sanitizeUrl(defaultActionUrl.value)
228
+ : sanitizeUrl(defaultActionUrl.value);
197
229
 
198
230
  // Prevent no-ops from sending you to a blank page
199
- if (url === 'about:blank') return
231
+ if (url === "about:blank") return;
200
232
 
201
- window.open(url)
202
- }
233
+ window.open(url);
234
+ };
203
235
 
204
236
  // Handle keyboard navigation
205
237
  const handleKeyDown = (event: KeyboardEvent) => {
206
- if (defaultActionUrl.value && event.key === 'Enter') {
207
- handleClick()
238
+ if (defaultActionUrl.value && event.key === "Enter") {
239
+ handleClick();
208
240
  }
209
- }
241
+ };
210
242
  </script>
211
243
 
212
244
  <style module>
@@ -215,7 +247,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
215
247
  }
216
248
 
217
249
  .divider {
218
- border-top: 1px solid var(--cc-black-80, rgba(0, 0, 0, 0.8));
250
+ border-bottom: 1px solid var(--cc-list-divider-color, var(--cc-border-media-card, var(--cc-black-80, rgba(0, 0, 0, 0.8))));
219
251
  }
220
252
 
221
253
  /* Header element styles */
@@ -277,13 +309,19 @@ const handleKeyDown = (event: KeyboardEvent) => {
277
309
  }
278
310
 
279
311
  .itemTitleWithSubtitle {
280
- margin-top: 0px;
281
- margin-bottom: 8px;
312
+ margin-top: 2px;
313
+ margin-bottom: 5px;
314
+ font-size: 0.9375rem; /* 15px */
315
+ font-weight: bold;
316
+ line-height: 1.45em; /* 23.2px */
282
317
  }
283
318
 
284
319
  .itemSubtitle {
285
- margin-top: 0px;
286
- margin-bottom: 0px;
320
+ margin: 0;
321
+ color: rgba(0, 0, 0, 0.54);
322
+ font-size: 0.8125rem; /* 13px */
323
+ white-space: pre-line;
324
+ line-height: 1.45em; /* 14.5px */
287
325
  }
288
326
 
289
327
  /* Regular list item styles */
@@ -302,7 +340,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
302
340
  }
303
341
 
304
342
  .listItemContent {
305
- padding: 16px 16px 12px 16px;
343
+ padding: 10px;
306
344
  overflow-wrap: break-word;
307
345
  display: flex;
308
346
  -webkit-box-pack: justify;
@@ -310,7 +348,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
310
348
  align-items: center;
311
349
  gap: 16px;
312
350
  width: 100%;
313
- color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
314
351
  }
315
352
 
316
353
  .listItemText {
@@ -318,7 +355,7 @@ const handleKeyDown = (event: KeyboardEvent) => {
318
355
  }
319
356
 
320
357
  .listItemText > * {
321
- color: var(--cc-black-20, rgba(0, 0, 0, 0.2));
358
+ color: inherit;
322
359
  }
323
360
 
324
361
  .listItemImage {
@@ -122,7 +122,7 @@ const markdownClasses = computed(() => {
122
122
  }
123
123
 
124
124
  .text a:focus-visible {
125
- outline: 2px solid var(--cc-primary-color-focus);
125
+ outline: 2px solid var(--cc-primary-color-focus, var(--cc-primary-color));
126
126
  outline-offset: 2px;
127
127
  }
128
128
 
@@ -9,9 +9,9 @@
9
9
  ignoreLiveRegion
10
10
  />
11
11
 
12
- <!-- Buttons -->
12
+ <!-- Buttons (hidden while progressive rendering is animating) -->
13
13
  <ActionButtons
14
- v-if="buttons.length > 0"
14
+ v-if="buttons.length > 0 && !isStillAnimating"
15
15
  :payload="buttons"
16
16
  :action="modifiedAction"
17
17
  :buttonClassName="buttonClassName"
@@ -20,6 +20,7 @@
20
20
  :config="config"
21
21
  :onEmitAnalytics="onEmitAnalytics"
22
22
  :templateTextId="webchatButtonTemplateTextId"
23
+ :openXAppOverlay="openXAppOverlay"
23
24
  showUrlIcon
24
25
  />
25
26
  </div>
@@ -32,10 +33,10 @@ import ActionButtons from '../common/ActionButtons.vue'
32
33
  import { useMessageContext } from '../../composables/useMessageContext'
33
34
  import { getChannelPayload } from '../../utils/matcher'
34
35
  import { getRandomId } from '../../utils/helpers'
35
- import type { IWebchatTemplateAttachment, IWebchatButton, IWebchatQuickReply } from '../../types'
36
+ import type { IWebchatTemplateAttachment, IWebchatButton, IWebchatQuickReply, IStreamingMessage } from '../../types'
36
37
 
37
38
  // Message context
38
- const { message, config, action, onEmitAnalytics } = useMessageContext()
39
+ const { message, config, action, onEmitAnalytics, messageParams, openXAppOverlay } = useMessageContext()
39
40
 
40
41
  const $style = useCssModule()
41
42
 
@@ -67,13 +68,12 @@ const classType = computed(() => {
67
68
  return isQuickReplies.value ? 'quick-reply' : 'buttons'
68
69
  })
69
70
 
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
71
+ // For quick replies, disable if there's already a reply or the conversation has ended
73
72
  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
73
+ const shouldBeDisabled =
74
+ (isQuickReplies.value && messageParams?.hasReply) ||
75
+ messageParams?.isConversationEnded
76
+ return shouldBeDisabled ? undefined : action
77
77
  })
78
78
 
79
79
  // Generate unique ID for accessibility
@@ -92,6 +92,15 @@ const containerStyle = computed(() => {
92
92
  return {}
93
93
  })
94
94
 
95
+ // Direction mapping for button alignment
96
+ const directionMapping = config?.settings?.widgetSettings?.sourceDirectionMapping
97
+ const messageDirection = computed(() => {
98
+ if (message.source === 'user') return directionMapping?.user || 'outgoing'
99
+ if (message.source === 'bot') return directionMapping?.bot || 'incoming'
100
+ if (message.source === 'agent') return directionMapping?.agent || 'incoming'
101
+ return 'incoming'
102
+ })
103
+
95
104
  // Button class name
96
105
  const buttonClassName = computed(() => {
97
106
  return `${$style.button} webchat-${classType.value}-template-button`
@@ -99,7 +108,14 @@ const buttonClassName = computed(() => {
99
108
 
100
109
  // Container class name
101
110
  const containerClassName = computed(() => {
102
- return `${$style.buttons} webchat-${classType.value}-template-replies-container`
111
+ return `${$style.buttons} ${$style[messageDirection.value]} webchat-${classType.value}-template-replies-container`
112
+ })
113
+
114
+ // Progressive message rendering — hide buttons while message text is still animating
115
+ const isStillAnimating = computed(() => {
116
+ if (!config?.settings?.behavior?.progressiveMessageRendering) return false
117
+ const animState = (message as IStreamingMessage).animationState
118
+ return animState === 'animating' || animState === 'start'
103
119
  })
104
120
  </script>
105
121
 
@@ -116,4 +132,12 @@ const containerClassName = computed(() => {
116
132
  .button {
117
133
  /* Button styles are inherited from ActionButton component */
118
134
  }
135
+
136
+ .incoming {
137
+ justify-content: flex-start;
138
+ }
139
+
140
+ .outgoing {
141
+ justify-content: flex-end;
142
+ }
119
143
  </style>