@nyaruka/temba-components 0.135.9 → 0.136.1

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 (144) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/demo/components/webchat/example.html +4 -2
  3. package/dist/static/svg/index.svg +1 -1
  4. package/dist/temba-components.js +1351 -322
  5. package/dist/temba-components.js.map +1 -1
  6. package/out-tsc/src/Icons.js +2 -1
  7. package/out-tsc/src/Icons.js.map +1 -1
  8. package/out-tsc/src/display/FloatingTab.js +2 -6
  9. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  10. package/out-tsc/src/flow/CanvasNode.js +29 -1
  11. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  12. package/out-tsc/src/flow/Editor.js +229 -5
  13. package/out-tsc/src/flow/Editor.js.map +1 -1
  14. package/out-tsc/src/flow/Plumber.js +320 -1
  15. package/out-tsc/src/flow/Plumber.js.map +1 -1
  16. package/out-tsc/src/interfaces.js +1 -0
  17. package/out-tsc/src/interfaces.js.map +1 -1
  18. package/out-tsc/src/layout/FloatingWindow.js +30 -8
  19. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  20. package/out-tsc/src/simulator/Simulator.js +1861 -0
  21. package/out-tsc/src/simulator/Simulator.js.map +1 -0
  22. package/out-tsc/src/store/AppState.js +66 -0
  23. package/out-tsc/src/store/AppState.js.map +1 -1
  24. package/out-tsc/src/utils.js +48 -0
  25. package/out-tsc/src/utils.js.map +1 -1
  26. package/out-tsc/temba-modules.js +2 -0
  27. package/out-tsc/temba-modules.js.map +1 -1
  28. package/out-tsc/test/temba-appstate-node-sorting.test.js +430 -0
  29. package/out-tsc/test/temba-appstate-node-sorting.test.js.map +1 -0
  30. package/out-tsc/test/temba-floating-tab.test.js +0 -9
  31. package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
  32. package/out-tsc/test/temba-flow-editor.test.js +262 -1
  33. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  34. package/out-tsc/test/temba-flow-plumber-connections.test.js +3 -1
  35. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  36. package/out-tsc/test/temba-flow-plumber.test.js +3 -1
  37. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  38. package/out-tsc/test/temba-simulator.test.js +642 -0
  39. package/out-tsc/test/temba-simulator.test.js.map +1 -0
  40. package/out-tsc/test/utils.test.js +1 -1
  41. package/out-tsc/test/utils.test.js.map +1 -1
  42. package/package.json +1 -1
  43. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  44. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  45. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  46. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  47. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  48. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  49. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  50. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  51. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  52. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  53. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  54. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  55. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  56. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  57. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  58. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  59. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  60. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  61. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  62. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  63. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  64. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  65. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  66. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  67. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  68. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  69. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  70. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  71. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  72. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  73. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  74. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  75. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  76. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  77. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  78. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  79. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  80. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  81. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  82. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  83. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  84. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  85. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  86. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  87. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  88. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  89. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  90. package/screenshots/truth/floating-tab/gray.png +0 -0
  91. package/screenshots/truth/floating-tab/green.png +0 -0
  92. package/screenshots/truth/floating-tab/purple.png +0 -0
  93. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  94. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  95. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  96. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  97. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  98. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  99. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  100. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  101. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  102. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  103. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  104. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  105. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  106. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  107. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  108. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  109. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  110. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  111. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  112. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  113. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  114. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  115. package/screenshots/truth/simulator/after-message-sent.png +0 -0
  116. package/screenshots/truth/simulator/after-reset.png +0 -0
  117. package/screenshots/truth/simulator/attachment-menu.png +0 -0
  118. package/screenshots/truth/simulator/context-expanded.png +0 -0
  119. package/screenshots/truth/simulator/context-explorer-open.png +0 -0
  120. package/screenshots/truth/simulator/event-info.png +0 -0
  121. package/screenshots/truth/simulator/image-attachment.png +0 -0
  122. package/screenshots/truth/simulator/open-initial.png +0 -0
  123. package/screenshots/truth/simulator/quick-replies.png +0 -0
  124. package/src/Icons.ts +2 -1
  125. package/src/display/FloatingTab.ts +2 -7
  126. package/src/flow/CanvasNode.ts +30 -1
  127. package/src/flow/Editor.ts +246 -4
  128. package/src/flow/Plumber.ts +371 -2
  129. package/src/interfaces.ts +2 -1
  130. package/src/layout/FloatingWindow.ts +37 -12
  131. package/src/simulator/Simulator.ts +2061 -0
  132. package/src/store/AppState.ts +109 -0
  133. package/src/utils.ts +53 -0
  134. package/static/svg/index.svg +1 -1
  135. package/static/svg/work/traced/route.svg +1 -0
  136. package/static/svg/work/used/route.svg +3 -0
  137. package/temba-modules.ts +2 -0
  138. package/test/temba-appstate-node-sorting.test.ts +506 -0
  139. package/test/temba-floating-tab.test.ts +0 -11
  140. package/test/temba-flow-editor.test.ts +298 -1
  141. package/test/temba-flow-plumber-connections.test.ts +4 -1
  142. package/test/temba-flow-plumber.test.ts +4 -1
  143. package/test/temba-simulator.test.ts +866 -0
  144. package/test/utils.test.ts +1 -1
@@ -0,0 +1,1861 @@
1
+ import { __decorate } from "tslib";
2
+ import { html } from 'lit-html';
3
+ import { RapidElement } from '../RapidElement';
4
+ import { css } from 'lit';
5
+ import { property } from 'lit/decorators.js';
6
+ import { postJSON, fromCookie } from '../utils';
7
+ import { getStore } from '../store/Store';
8
+ import { CustomEventType } from '../interfaces';
9
+ // test attachment URLs
10
+ const TEST_IMAGES = [
11
+ 'https://s3.amazonaws.com/floweditor-assets.temba.io/simulator/sim_image_a.jpg',
12
+ 'https://s3.amazonaws.com/floweditor-assets.temba.io/simulator/sim_image_b.jpg',
13
+ 'https://s3.amazonaws.com/floweditor-assets.temba.io/simulator/sim_image_c.jpg',
14
+ 'https://s3.amazonaws.com/floweditor-assets.temba.io/simulator/sim_image_d.jpg'
15
+ ];
16
+ const TEST_VIDEOS = [
17
+ 'https://s3.amazonaws.com/floweditor-assets.temba.io/simulator/sim_video_a.mp4'
18
+ ];
19
+ const TEST_AUDIO = [
20
+ 'https://s3.amazonaws.com/floweditor-assets.temba.io/simulator/sim_audio_a.mp3'
21
+ ];
22
+ const TEST_LOCATIONS = [
23
+ 'geo:47.6062,-122.3321', // Seattle
24
+ 'geo:-0.1807,-78.4678', // Quito
25
+ 'geo:-2.9001,-79.0059', // Cuenca
26
+ 'geo:-1.9536,30.0606' // Kigali
27
+ ];
28
+ const SIMULATOR_SIZES = {
29
+ small: {
30
+ phoneWidth: 270,
31
+ phoneHeight: 576,
32
+ phoneTotalHeight: 576,
33
+ phoneScreenHeight: 376,
34
+ contextWidth: 336,
35
+ contextHeight: 416,
36
+ contextOffset: 48,
37
+ optionPaneWidth: 44,
38
+ optionPaneGap: 10,
39
+ windowPadding: 24,
40
+ cutoutHeight: 32,
41
+ cutoutPadding: 12,
42
+ cutoutFontSize: 10,
43
+ cutoutIslandWidth: 80,
44
+ cutoutIslandHeight: 20,
45
+ cutoutIslandTop: 6
46
+ },
47
+ medium: {
48
+ phoneWidth: 300,
49
+ phoneHeight: 720,
50
+ phoneTotalHeight: 720,
51
+ phoneScreenHeight: 470,
52
+ contextWidth: 420,
53
+ contextHeight: 520,
54
+ contextOffset: 60,
55
+ optionPaneWidth: 44,
56
+ optionPaneGap: 12,
57
+ windowPadding: 30,
58
+ cutoutHeight: 40,
59
+ cutoutPadding: 16,
60
+ cutoutFontSize: 12,
61
+ cutoutIslandWidth: 100,
62
+ cutoutIslandHeight: 24,
63
+ cutoutIslandTop: 8
64
+ },
65
+ large: {
66
+ phoneWidth: 360,
67
+ phoneHeight: 864,
68
+ phoneTotalHeight: 864,
69
+ phoneScreenHeight: 564,
70
+ contextWidth: 504,
71
+ contextHeight: 624,
72
+ contextOffset: 72,
73
+ optionPaneWidth: 44,
74
+ optionPaneGap: 14,
75
+ windowPadding: 36,
76
+ cutoutHeight: 50,
77
+ cutoutPadding: 20,
78
+ cutoutFontSize: 14,
79
+ cutoutIslandWidth: 120,
80
+ cutoutIslandHeight: 30,
81
+ cutoutIslandTop: 10
82
+ }
83
+ };
84
+ export class Simulator extends RapidElement {
85
+ constructor() {
86
+ super(...arguments);
87
+ this.flow = '';
88
+ this.endpoint = '';
89
+ this.animationTime = 200;
90
+ this.events = [];
91
+ this.previousEventCount = 0;
92
+ this.session = null;
93
+ this.context = null;
94
+ this.contact = {
95
+ uuid: 'fb3787ab-2eda-48a0-a2bc-e2ddadec1286',
96
+ urns: ['tel:+12065551212'],
97
+ fields: {},
98
+ groups: [],
99
+ language: 'eng',
100
+ status: 'active',
101
+ created_on: new Date().toISOString()
102
+ };
103
+ this.sprinting = false;
104
+ this.inputValue = '';
105
+ this.expandedPaths = new Set();
106
+ this.copiedExpression = '';
107
+ this.toastMessage = '';
108
+ this.showAllKeys = true;
109
+ this.previousWindowWidth = 0;
110
+ this.currentQuickReplies = [];
111
+ this.isVisible = false;
112
+ this.attachmentMenuOpen = false;
113
+ this.boundClickOutsideHandler = null;
114
+ // attachment cycling indices - initialized randomly
115
+ this.imageIndex = Math.floor(Math.random() * TEST_IMAGES.length);
116
+ this.videoIndex = Math.floor(Math.random() * TEST_VIDEOS.length);
117
+ this.audioIndex = Math.floor(Math.random() * TEST_AUDIO.length);
118
+ this.locationIndex = Math.floor(Math.random() * TEST_LOCATIONS.length);
119
+ }
120
+ static get styles() {
121
+ return css `
122
+ :host {
123
+ /* size-specific dimensions are set dynamically via inline styles */
124
+ --phone-width: 300px;
125
+ --phone-total-height: 720px;
126
+ --context-width: 420px;
127
+ --context-offset: 60px;
128
+ --option-pane-width: 44px;
129
+ --option-pane-gap: 12px;
130
+ --window-padding: 30px;
131
+ --phone-screen-height: 470px;
132
+ --context-height: 520px;
133
+ --context-closed-left: 332px;
134
+ --animation-time: 200ms;
135
+ }
136
+
137
+ .phone-simulator {
138
+ padding-left: calc(var(--context-width) + var(--context-offset));
139
+ padding-top: var(--window-padding);
140
+ padding-bottom: var(--window-padding);
141
+ position: relative;
142
+ display: flex;
143
+ align-items: flex-start;
144
+ }
145
+
146
+ .option-pane {
147
+ margin-top: var(--window-padding);
148
+ margin-left: var(--option-pane-gap);
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 6px;
152
+ padding: 6px;
153
+ background: rgba(0, 0, 0, 0.7);
154
+ backdrop-filter: blur(10px);
155
+ border-radius: 16px;
156
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
157
+ pointer-events: all;
158
+ }
159
+ .option-btn {
160
+ background: rgba(255, 255, 255, 0.1);
161
+ border: none;
162
+ border-radius: 12px;
163
+ width: 32px;
164
+ height: 32px;
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ cursor: pointer;
169
+ transition: all var(--animation-time) ease;
170
+ color: white;
171
+ }
172
+ .option-btn:hover {
173
+ background: rgba(255, 255, 255, 0.2);
174
+ transform: scale(1.05);
175
+ }
176
+ .option-btn:active {
177
+ transform: scale(0.95);
178
+ }
179
+ .option-btn.active {
180
+ background: var(--color-primary-dark);
181
+ color: white;
182
+ }
183
+ .option-btn.active:hover {
184
+ background: var(--color-primary-dark);
185
+ }
186
+
187
+ .phone-frame {
188
+ width: var(--phone-width);
189
+ border-radius: 40px;
190
+ border: 6px solid #1f2937;
191
+ box-shadow: 0 0px 30px rgba(0, 0, 0, 0.4);
192
+ background: #000;
193
+ position: relative;
194
+ overflow: hidden;
195
+ z-index: 2;
196
+ }
197
+
198
+ .context-explorer {
199
+ width: var(--context-width);
200
+ height: var(--context-height);
201
+ border-top-left-radius: 16px;
202
+ border-bottom-left-radius: 16px;
203
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
204
+ position: absolute;
205
+ left: var(--context-closed-left);
206
+ top: calc(var(--window-padding) + 40px);
207
+ z-index: 1;
208
+ font-size: 13px;
209
+ color: #374151;
210
+ transition: left calc(var(--animation-time) * 1.5) ease-out,
211
+ opacity calc(var(--animation-time) * 1.5) ease-out;
212
+ opacity: 0;
213
+ pointer-events: none;
214
+ background: rgba(0, 0, 0, 0.7);
215
+ backdrop-filter: blur(10px);
216
+ display: flex;
217
+ flex-direction: column;
218
+ padding: 12px;
219
+ }
220
+
221
+ .context-gutter {
222
+ background: rgba(0, 0, 0, 0.3);
223
+ border-radius: 6px;
224
+
225
+ display: flex;
226
+ flex-direction: row;
227
+ align-items: center;
228
+ padding: 4px;
229
+ margin-right: 32px;
230
+ margin-top: 8px;
231
+ flex-shrink: 0;
232
+ }
233
+
234
+ .context-gutter-btn {
235
+ width: 14px;
236
+ height: 14px;
237
+ display: flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ cursor: pointer;
241
+ border-radius: 6px;
242
+ transition: background var(--animation-time) ease;
243
+ color: rgba(255, 255, 255, 0.6);
244
+ padding: 4px;
245
+ }
246
+
247
+ .context-gutter-btn:hover {
248
+ background: rgba(255, 255, 255, 0.1);
249
+ color: rgba(255, 255, 255, 0.9);
250
+ }
251
+
252
+ .context-gutter-btn.active {
253
+ color: #c084fc;
254
+ }
255
+
256
+ .context-gutter-spacer {
257
+ flex: 1;
258
+ }
259
+
260
+ .context-explorer-scroll {
261
+ scrollbar-color: rgba(255, 255, 255, 0.3) #4a4a4a;
262
+ scrollbar-width: thin;
263
+ height: 100%;
264
+ overflow-y: scroll;
265
+ padding-right: 10px;
266
+ margin-right: 30px;
267
+ flex-grow: 1;
268
+ }
269
+
270
+ .context-explorer-bleed {
271
+ height: 100%;
272
+ width: 0px;
273
+ }
274
+
275
+ .context-explorer-scroll::-webkit-scrollbar {
276
+ width: 18px;
277
+ }
278
+
279
+ .context-explorer-scroll::-webkit-scrollbar-track {
280
+ background: rgba(0, 0, 0, 0.3);
281
+ border-radius: 4px;
282
+ }
283
+
284
+ .context-explorer-scroll::-webkit-scrollbar-thumb {
285
+ background: rgba(255, 255, 255, 0.3);
286
+ border-radius: 4px;
287
+ }
288
+
289
+ .context-explorer-scroll::-webkit-scrollbar-thumb:hover {
290
+ background: rgba(255, 255, 255, 0.5);
291
+ }
292
+
293
+ .context-explorer.open {
294
+ left: var(--context-offset);
295
+ opacity: 1;
296
+ pointer-events: auto;
297
+ }
298
+
299
+ .context-item {
300
+ display: flex;
301
+ align-items: flex-start;
302
+ padding: 2px 4px;
303
+ cursor: pointer;
304
+ user-select: none;
305
+ }
306
+
307
+ .context-item:hover {
308
+ background: rgba(0, 0, 0, 0.05);
309
+ }
310
+
311
+ .context-item-expandable {
312
+ display: flex;
313
+ align-items: center;
314
+ }
315
+
316
+ .context-expand-icon {
317
+ width: 16px;
318
+ display: inline-block;
319
+ text-align: center;
320
+ flex-shrink: 0;
321
+ transition: transform var(--animation-time) ease;
322
+ color: #ffffff;
323
+ }
324
+
325
+ .context-expand-icon.expanded {
326
+ transform: rotate(90deg);
327
+ }
328
+
329
+ .context-key {
330
+ color: #ffffff;
331
+ flex-shrink: 0;
332
+ margin-right: 8px;
333
+ display: flex;
334
+ }
335
+
336
+ .context-key.has-value {
337
+ color: #e8b5e8;
338
+ }
339
+
340
+ .context-value {
341
+ color: #aaa;
342
+ flex: 1;
343
+ text-align: right;
344
+ overflow: hidden;
345
+ text-overflow: ellipsis;
346
+ white-space: nowrap;
347
+ }
348
+
349
+ .context-children {
350
+ margin-left: 16px;
351
+ }
352
+
353
+ .context-copy-icon {
354
+ opacity: 0;
355
+ margin-left: 4px;
356
+ transition: opacity var(--animation-time) ease;
357
+ cursor: pointer;
358
+ color: #ccc;
359
+ }
360
+
361
+ .context-item:hover .context-copy-icon {
362
+ opacity: 1;
363
+ }
364
+
365
+ .context-toast {
366
+ position: absolute;
367
+ bottom: 60px;
368
+ left: 50%;
369
+ transform: translateX(-50%);
370
+ background: #666;
371
+ color: white;
372
+ padding: 12px 12px;
373
+ border-radius: 8px;
374
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
375
+ font-size: 13px;
376
+ z-index: 10;
377
+ animation: slideInUp var(--animation-time) ease-out;
378
+ }
379
+
380
+ .context-toast .expression {
381
+ color: #e8b5e8;
382
+ font-weight: 600;
383
+ }
384
+
385
+ @keyframes slideInUp {
386
+ from {
387
+ opacity: 0;
388
+ transform: translateX(-50%) translateY(20px);
389
+ }
390
+ to {
391
+ opacity: 1;
392
+ transform: translateX(-50%) translateY(0);
393
+ }
394
+ }
395
+
396
+ .phone-top {
397
+ position: absolute;
398
+ top: 0;
399
+ left: 0;
400
+ right: 0;
401
+ z-index: 10;
402
+ cursor: grab;
403
+ }
404
+ .phone-notch {
405
+ background: transparent;
406
+ height: var(--cutout-height);
407
+ position: relative;
408
+ display: flex;
409
+ align-items: center;
410
+ justify-content: center;
411
+ padding: 0 var(--cutout-padding);
412
+ }
413
+ .phone-notch::before {
414
+ content: '';
415
+ position: absolute;
416
+ top: 0;
417
+ left: 0;
418
+ right: 0;
419
+ height: 100%;
420
+ background: linear-gradient(
421
+ to bottom,
422
+ rgba(0, 0, 0, 0.3) 0%,
423
+ rgba(0, 0, 0, 0.2) 50%,
424
+ transparent 100%
425
+ );
426
+ z-index: -1;
427
+ }
428
+ .dynamic-island {
429
+ top: var(--cutout-island-top);
430
+ left: 50%;
431
+
432
+ width: var(--cutout-island-width);
433
+ height: var(--cutout-island-height);
434
+ background: #000;
435
+ border-radius: calc(var(--cutout-island-height) / 1.5);
436
+ z-index: 1;
437
+ }
438
+ .phone-notch .time {
439
+ color: #000;
440
+ font-size: var(--cutout-font-size);
441
+ font-weight: 600;
442
+ }
443
+ .phone-notch .status-icons {
444
+ display: flex;
445
+ gap: 4px;
446
+ align-items: center;
447
+ }
448
+ .phone-notch .status-icons span {
449
+ color: #000;
450
+ font-size: var(--cutout-font-size);
451
+ }
452
+ .phone-header {
453
+ background: transparent;
454
+ padding: 10px 15px;
455
+ display: flex;
456
+ align-items: center;
457
+ justify-content: flex-end;
458
+ cursor: move;
459
+ user-select: none;
460
+ border-bottom: none;
461
+ pointer-events: all;
462
+ }
463
+
464
+ .phone-screen {
465
+ background: white;
466
+ padding: 15px;
467
+ padding-top: calc(var(--cutout-height) + 10px);
468
+ padding-bottom: 60px;
469
+ height: var(--phone-screen-height);
470
+ overflow-y: scroll;
471
+ display: flex;
472
+ flex-direction: column;
473
+ scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
474
+ scrollbar-width: thin;
475
+ }
476
+
477
+ .phone-screen::-webkit-scrollbar {
478
+ width: 8px;
479
+ }
480
+
481
+ .phone-screen::-webkit-scrollbar-track {
482
+ background: transparent;
483
+ }
484
+
485
+ .phone-screen::-webkit-scrollbar-thumb {
486
+ background: rgba(0, 0, 0, 0.2);
487
+ border-radius: 4px;
488
+ }
489
+
490
+ .phone-screen::-webkit-scrollbar-thumb:hover {
491
+ background: rgba(0, 0, 0, 0.3);
492
+ }
493
+
494
+ @keyframes messageAppear {
495
+ 0% {
496
+ opacity: 0;
497
+ transform: scale(0.8);
498
+ }
499
+ 70% {
500
+ opacity: 1;
501
+ transform: scale(1.05);
502
+ }
503
+ 100% {
504
+ opacity: 1;
505
+ transform: scale(1);
506
+ }
507
+ }
508
+
509
+ .message {
510
+ padding: 10px 14px;
511
+ margin-bottom: 8px;
512
+ border-radius: 18px;
513
+ max-width: 70%;
514
+ font-size: 13px;
515
+ line-height: 1.2;
516
+ }
517
+ .message.animated {
518
+ animation: messageAppear var(--animation-time) ease-out forwards;
519
+ opacity: 0;
520
+ }
521
+ .message.incoming {
522
+ background: #e5e5ea;
523
+ color: #000;
524
+ margin-right: auto;
525
+ border-bottom-left-radius: 4px;
526
+ }
527
+ .message.outgoing {
528
+ background: #007aff;
529
+ color: white;
530
+ margin-left: auto;
531
+ text-align: left;
532
+ border-bottom-right-radius: 4px;
533
+ }
534
+ .attachment-wrapper {
535
+ max-width: 70%;
536
+ margin-bottom: 8px;
537
+ display: flex;
538
+ flex-direction: column;
539
+ gap: 4px;
540
+ }
541
+ .attachment-wrapper.incoming {
542
+ margin-right: auto;
543
+ align-items: flex-start;
544
+ }
545
+ .attachment-wrapper.outgoing {
546
+ margin-left: auto;
547
+ align-items: flex-end;
548
+ }
549
+ .attachment-wrapper.animated {
550
+ animation: messageAppear var(--animation-time) ease-out forwards;
551
+ opacity: 0;
552
+ }
553
+ .attachment {
554
+ border-radius: 12px;
555
+ overflow: hidden;
556
+ max-width: 100%;
557
+ }
558
+ .attachment img {
559
+ max-width: 100%;
560
+ display: block;
561
+ border-radius: 12px;
562
+ }
563
+ .attachment video {
564
+ max-width: 100%;
565
+ display: block;
566
+ border-radius: 12px;
567
+ }
568
+ .attachment-audio {
569
+ display: flex;
570
+ align-items: center;
571
+ gap: 8px;
572
+ padding: 6px;
573
+ background: white;
574
+ border: 1px solid #e5e5ea;
575
+ border-radius: 12px;
576
+ min-width: 160px;
577
+ }
578
+ .attachment-wrapper.outgoing .attachment-audio {
579
+ background: white;
580
+ border: none;
581
+ }
582
+ .attachment-audio audio {
583
+ flex: 1;
584
+ max-height: 30px;
585
+ }
586
+ .attachment-location {
587
+ border-radius: 12px;
588
+ overflow: hidden;
589
+ }
590
+ .event-info {
591
+ text-align: center;
592
+ font-size: 11px;
593
+ color: #8e8e93;
594
+ margin: 4px 0;
595
+ padding: 0 10px;
596
+ line-height: 1.3;
597
+ }
598
+ .event-info.animated {
599
+ animation: messageAppear var(--animation-time) ease-out forwards;
600
+ opacity: 0;
601
+ }
602
+ .message-input {
603
+ background: linear-gradient(
604
+ to top,
605
+ rgba(0, 0, 0, 0.1) 0%,
606
+ rgba(0, 0, 0, 0.05) 70%,
607
+ transparent 100%
608
+ );
609
+ padding: 8px 12px;
610
+ border-top: none;
611
+ display: flex;
612
+ align-items: center;
613
+ gap: 8px;
614
+ position: absolute;
615
+ bottom: 0px;
616
+ left: 0px;
617
+ right: 0px;
618
+ z-index: 10;
619
+ }
620
+ .message-input input {
621
+ flex: 1;
622
+ border: 1px solid #c6c6c8;
623
+ border-radius: 20px;
624
+ padding: 8px 15px;
625
+ font-size: 15px;
626
+ margin-bottom: 5px;
627
+ background: white;
628
+ border: none;
629
+ outline: none;
630
+ }
631
+ .message-input input::placeholder {
632
+ color: #8e8e93;
633
+ }
634
+ .attachment-button {
635
+ width: 30px;
636
+ height: 30px;
637
+ border-radius: 50%;
638
+ background: #fff;
639
+ border: none;
640
+ display: flex;
641
+ align-items: center;
642
+ justify-content: center;
643
+ cursor: pointer;
644
+ flex-shrink: 0;
645
+ margin-bottom: 5px;
646
+ transition: all var(--animation-time) ease;
647
+ color: #000;
648
+ }
649
+ .attachment-button:hover {
650
+ background: #f8f8f8ff;
651
+ transform: scale(1.05);
652
+ }
653
+ .attachment-button:active {
654
+ transform: scale(0.95);
655
+ }
656
+ .attachment-menu {
657
+ position: absolute;
658
+ bottom: 55px;
659
+ left: 12px;
660
+ background: white;
661
+ border-radius: 12px;
662
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
663
+ padding: 8px;
664
+ display: flex;
665
+ flex-direction: column;
666
+ gap: 4px;
667
+ opacity: 0;
668
+ pointer-events: none;
669
+ transform: translateY(10px);
670
+ transition: opacity var(--animation-time) ease, transform 0.2s ease;
671
+ z-index: 20;
672
+ }
673
+ .attachment-menu.open {
674
+ opacity: 1;
675
+ pointer-events: all;
676
+ transform: translateY(0);
677
+ }
678
+ .attachment-menu-item {
679
+ display: flex;
680
+ align-items: center;
681
+ gap: 8px;
682
+ padding: 8px 12px;
683
+ border-radius: 8px;
684
+ cursor: pointer;
685
+ transition: background var(--animation-time) ease;
686
+ white-space: nowrap;
687
+ font-size: 14px;
688
+ color: #1f2937;
689
+ }
690
+ .attachment-menu-item:hover {
691
+ background: #f3f4f6;
692
+ }
693
+ .attachment-menu-item temba-icon {
694
+ color: #007aff;
695
+ }
696
+ .quick-replies {
697
+ display: flex;
698
+ flex-wrap: wrap;
699
+ justify-content: center;
700
+ gap: 8px;
701
+ margin-top: 4px;
702
+ margin-bottom: 8px;
703
+ }
704
+ .quick-reply-btn {
705
+ background: white;
706
+ color: #007aff;
707
+ border: 1px solid #007aff;
708
+ border-radius: 18px;
709
+ padding: 4px 8px;
710
+ font-size: 11px;
711
+ cursor: pointer;
712
+ transition: all var(--animation-time) ease;
713
+ white-space: nowrap;
714
+ }
715
+ .quick-reply-btn:hover {
716
+ background: #007aff;
717
+ color: white;
718
+ cursor: pointer;
719
+ }
720
+ .quick-reply-btn:active {
721
+ transform: scale(0.95);
722
+ }
723
+ .quick-reply-btn.animated {
724
+ animation: messageAppear var(--animation-time) ease-out forwards;
725
+ opacity: 0;
726
+ }
727
+ `;
728
+ }
729
+ // method to reset attachment indices for testing
730
+ resetAttachmentIndices() {
731
+ this.imageIndex = 2;
732
+ this.videoIndex = 0;
733
+ this.audioIndex = 0;
734
+ this.locationIndex = 0;
735
+ }
736
+ get sizeConfig() {
737
+ return SIMULATOR_SIZES[this.size] || SIMULATOR_SIZES.medium;
738
+ }
739
+ get windowWidth() {
740
+ const config = this.sizeConfig;
741
+ return (config.contextWidth +
742
+ config.phoneWidth +
743
+ config.optionPaneWidth +
744
+ config.optionPaneGap +
745
+ config.contextOffset);
746
+ }
747
+ get leftBoundaryMargin() {
748
+ const config = this.sizeConfig;
749
+ return config.contextWidth + config.contextOffset;
750
+ }
751
+ get contextClosedLeft() {
752
+ const config = this.sizeConfig;
753
+ return config.contextWidth + config.contextOffset - config.phoneWidth;
754
+ }
755
+ updated(changes) {
756
+ super.updated(changes);
757
+ if (changes.has('flow') && this.flow) {
758
+ this.endpoint = `/flow/simulate/${this.flow}/`;
759
+ }
760
+ // handle attachment menu click outside listener
761
+ if (changes.has('attachmentMenuOpen')) {
762
+ if (this.attachmentMenuOpen) {
763
+ // create bound handler if it doesn't exist
764
+ if (!this.boundClickOutsideHandler) {
765
+ this.boundClickOutsideHandler =
766
+ this.handleClickOutsideAttachmentMenu.bind(this);
767
+ }
768
+ // add listener when menu opens
769
+ setTimeout(() => {
770
+ document.addEventListener('click', this.boundClickOutsideHandler);
771
+ }, 0);
772
+ }
773
+ else {
774
+ // remove listener when menu closes
775
+ if (this.boundClickOutsideHandler) {
776
+ document.removeEventListener('click', this.boundClickOutsideHandler);
777
+ }
778
+ }
779
+ }
780
+ // update floating window boundaries when size changes
781
+ if (changes.has('size')) {
782
+ requestAnimationFrame(() => {
783
+ var _a, _b;
784
+ const phoneWindow = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.getElementById('phone-window');
785
+ if (phoneWindow) {
786
+ // use the stored previous width since phoneWindow.width has already been updated
787
+ const oldWidth = this.previousWindowWidth || phoneWindow.width;
788
+ const oldRight = phoneWindow.left + oldWidth;
789
+ const config = this.sizeConfig;
790
+ const newWidth = this.windowWidth;
791
+ // store current width for next size change
792
+ this.previousWindowWidth = newWidth;
793
+ // update dimensions and boundaries
794
+ phoneWindow.width = newWidth;
795
+ phoneWindow.leftBoundaryMargin = this.leftBoundaryMargin;
796
+ phoneWindow.topBoundaryMargin = config.windowPadding;
797
+ phoneWindow.bottomBoundaryMargin = config.windowPadding;
798
+ // keep right edge in same position by adjusting left
799
+ let newLeft = oldRight - newWidth;
800
+ // apply same boundary logic as FloatingWindow.handleMouseMove
801
+ const padding = 20;
802
+ const minLeft = padding - this.leftBoundaryMargin;
803
+ const maxLeft = window.innerWidth -
804
+ newWidth -
805
+ padding +
806
+ phoneWindow.rightBoundaryMargin;
807
+ // clamp to boundaries
808
+ newLeft = Math.max(minLeft, Math.min(newLeft, maxLeft));
809
+ phoneWindow.left = newLeft;
810
+ // adjust vertical position if needed
811
+ const windowElement = (_b = phoneWindow.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.window');
812
+ const currentHeight = (windowElement === null || windowElement === void 0 ? void 0 : windowElement.offsetHeight) || config.phoneTotalHeight;
813
+ const maxTop = Math.max(padding - config.windowPadding, window.innerHeight - currentHeight - padding + config.windowPadding);
814
+ phoneWindow.top = Math.max(padding - config.windowPadding, Math.min(phoneWindow.top, maxTop));
815
+ }
816
+ });
817
+ }
818
+ else {
819
+ // store initial width when first rendered
820
+ if (!this.previousWindowWidth) {
821
+ this.previousWindowWidth = this.windowWidth;
822
+ }
823
+ }
824
+ }
825
+ disconnectedCallback() {
826
+ super.disconnectedCallback();
827
+ // clean up event listener when component is removed
828
+ if (this.boundClickOutsideHandler) {
829
+ document.removeEventListener('click', this.boundClickOutsideHandler);
830
+ }
831
+ }
832
+ handleShow() {
833
+ const phoneWindow = this.shadowRoot.getElementById('phone-window');
834
+ phoneWindow.show();
835
+ this.isVisible = true;
836
+ getStore().getState().setSimulatorActive(true);
837
+ // start the simulation if we haven't already
838
+ if (this.events.length === 0) {
839
+ this.startFlow();
840
+ }
841
+ }
842
+ async startFlow() {
843
+ const now = new Date().toISOString();
844
+ // set created_on to simulation start time
845
+ this.contact = { ...this.contact, created_on: now };
846
+ const body = {
847
+ contact: this.contact,
848
+ trigger: {
849
+ type: 'manual',
850
+ triggered_on: now,
851
+ flow: { uuid: this.flow, name: 'New Chat' },
852
+ params: {}
853
+ }
854
+ };
855
+ try {
856
+ const response = await postJSON(this.endpoint, body);
857
+ this.updateRunContext(response.json);
858
+ }
859
+ catch (error) {
860
+ console.error('Failed to start simulation:', error);
861
+ this.events = [
862
+ ...this.events,
863
+ {
864
+ type: 'error',
865
+ created_on: now,
866
+ text: 'Failed to start simulation'
867
+ }
868
+ ];
869
+ }
870
+ }
871
+ updateRunContext(runContext, msgInEvt) {
872
+ var _a;
873
+ if (msgInEvt) {
874
+ this.events = [...this.events, msgInEvt];
875
+ }
876
+ if (runContext.session) {
877
+ this.session = runContext.session;
878
+ // update our contact with the latest from the session
879
+ if (runContext.contact) {
880
+ this.contact = runContext.contact;
881
+ }
882
+ }
883
+ // store the context from the response
884
+ if (runContext.context) {
885
+ this.context = runContext.context;
886
+ }
887
+ if (runContext.events && runContext.events.length > 0) {
888
+ this.events = [...this.events, ...runContext.events];
889
+ // extract quick replies from the most recent sprint
890
+ this.currentQuickReplies = [];
891
+ for (const event of runContext.events) {
892
+ if (event.type === 'msg_created' && ((_a = event.msg) === null || _a === void 0 ? void 0 : _a.quick_replies)) {
893
+ this.currentQuickReplies = event.msg.quick_replies;
894
+ }
895
+ }
896
+ }
897
+ this.sprinting = false;
898
+ this.requestUpdate();
899
+ this.scrollToBottom();
900
+ this.updateActivity();
901
+ }
902
+ updateActivity() {
903
+ if (!this.session) {
904
+ return;
905
+ }
906
+ const pathCounts = {};
907
+ const nodeCounts = {};
908
+ // iterate through all runs to get path segment counts
909
+ for (const run of this.session.runs) {
910
+ if (run.path) {
911
+ for (let i = 0; i < run.path.length - 1; i++) {
912
+ const step = run.path[i];
913
+ const nextStep = run.path[i + 1];
914
+ if (step.exit_uuid && nextStep.node_uuid) {
915
+ const key = step.exit_uuid + ':' + nextStep.node_uuid;
916
+ pathCounts[key] = (pathCounts[key] || 0) + 1;
917
+ }
918
+ }
919
+ }
920
+ // set node counts on the last step of any active/waiting runs
921
+ if (run.status === 'active' || run.status === 'waiting') {
922
+ if (run.path && run.path.length > 0) {
923
+ const finalStep = run.path[run.path.length - 1];
924
+ if (finalStep && finalStep.node_uuid) {
925
+ nodeCounts[finalStep.node_uuid] =
926
+ (nodeCounts[finalStep.node_uuid] || 0) + 1;
927
+ }
928
+ }
929
+ }
930
+ }
931
+ // Update simulator activity in the store
932
+ getStore().getState().updateSimulatorActivity({
933
+ segments: pathCounts,
934
+ nodes: nodeCounts
935
+ });
936
+ // Fire follow event if following is enabled
937
+ if (this.following) {
938
+ this.fireFollowEvent();
939
+ }
940
+ }
941
+ fireFollowEvent() {
942
+ var _a;
943
+ if (!this.session || !this.session.runs || this.session.runs.length === 0) {
944
+ return;
945
+ }
946
+ // Find the first active or waiting run
947
+ let activeRun = this.session.runs.find((run) => run.status === 'active' || run.status === 'waiting');
948
+ // If no active/waiting run and simulation has ended, use the first completed run
949
+ if (!activeRun) {
950
+ activeRun = this.session.runs.find((run) => run.status === 'completed');
951
+ }
952
+ if (activeRun && activeRun.path && activeRun.path.length > 0) {
953
+ const finalStep = activeRun.path[activeRun.path.length - 1];
954
+ if (finalStep && finalStep.node_uuid) {
955
+ this.fireCustomEvent(CustomEventType.FollowSimulation, {
956
+ flowUuid: ((_a = activeRun.flow) === null || _a === void 0 ? void 0 : _a.uuid) || this.flow,
957
+ nodeUuid: finalStep.node_uuid
958
+ });
959
+ }
960
+ }
961
+ }
962
+ scrollToBottom() {
963
+ // wait for render, then scroll to bottom
964
+ setTimeout(() => {
965
+ var _a, _b;
966
+ const screen = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.phone-screen');
967
+ if (screen) {
968
+ screen.scrollTop = screen.scrollHeight;
969
+ }
970
+ // update previous count after animation completes
971
+ this.previousEventCount = this.events.length;
972
+ // return focus to input
973
+ const input = (_b = this.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.message-input input');
974
+ if (input) {
975
+ input.focus();
976
+ }
977
+ }, 50);
978
+ }
979
+ handleClose() {
980
+ const phoneWindow = this.shadowRoot.getElementById('phone-window');
981
+ // phoneWindow.hide();
982
+ phoneWindow.handleClose();
983
+ this.isVisible = false;
984
+ getStore().getState().setSimulatorActive(false);
985
+ }
986
+ handleReset() {
987
+ // reset simulation state
988
+ this.events = [];
989
+ this.session = null;
990
+ this.context = null;
991
+ this.inputValue = '';
992
+ this.sprinting = false;
993
+ this.previousEventCount = 0;
994
+ this.currentQuickReplies = [];
995
+ // Clear simulator activity data
996
+ getStore().getState().updateSimulatorActivity({
997
+ segments: {},
998
+ nodes: {}
999
+ });
1000
+ // reset contact to initial state
1001
+ this.contact = {
1002
+ uuid: 'fb3787ab-2eda-48a0-a2bc-e2ddadec1286',
1003
+ urns: ['tel:+12065551212'],
1004
+ fields: {},
1005
+ groups: [],
1006
+ language: 'eng',
1007
+ status: 'active',
1008
+ created_on: new Date().toISOString()
1009
+ };
1010
+ // restart the flow
1011
+ this.startFlow();
1012
+ }
1013
+ handleToggleFollow() {
1014
+ this.following = !this.following;
1015
+ }
1016
+ handleCycleSize() {
1017
+ const sizes = [
1018
+ 'small',
1019
+ 'medium',
1020
+ 'large'
1021
+ ];
1022
+ const currentIndex = sizes.indexOf(this.size);
1023
+ const nextIndex = (currentIndex + 1) % sizes.length;
1024
+ this.size = sizes[nextIndex];
1025
+ }
1026
+ handleToggleContextExplorer() {
1027
+ this.contextExplorerOpen = !this.contextExplorerOpen;
1028
+ // if opening the context explorer, ensure it's not off-screen
1029
+ if (this.contextExplorerOpen) {
1030
+ requestAnimationFrame(() => {
1031
+ var _a;
1032
+ const phoneWindow = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.getElementById('phone-window');
1033
+ if (phoneWindow) {
1034
+ const padding = 20;
1035
+ const contextExplorerLeft = this.sizeConfig.contextOffset;
1036
+ const minWindowLeft = padding - contextExplorerLeft;
1037
+ if (phoneWindow.left < minWindowLeft) {
1038
+ phoneWindow.left = minWindowLeft;
1039
+ }
1040
+ }
1041
+ });
1042
+ }
1043
+ }
1044
+ togglePath(path) {
1045
+ if (this.expandedPaths.has(path)) {
1046
+ this.expandedPaths.delete(path);
1047
+ }
1048
+ else {
1049
+ this.expandedPaths.add(path);
1050
+ }
1051
+ this.requestUpdate();
1052
+ }
1053
+ isExpandable(value) {
1054
+ if (value === null || typeof value !== 'object') {
1055
+ return false;
1056
+ }
1057
+ if (Array.isArray(value)) {
1058
+ return value.length > 0;
1059
+ }
1060
+ // check if object has keys other than __default__
1061
+ const keys = Object.keys(value).filter((key) => key !== '__default__');
1062
+ return keys.length > 0;
1063
+ }
1064
+ renderContextValue(value) {
1065
+ if (value === null || value === undefined)
1066
+ return '';
1067
+ if (typeof value === 'boolean')
1068
+ return html `<span class="context-value">${value}</span>`;
1069
+ if (typeof value === 'number')
1070
+ return html `<span class="context-value">${value}</span>`;
1071
+ if (typeof value === 'string')
1072
+ return html `<span class="context-value">${value}</span>`;
1073
+ if (Array.isArray(value))
1074
+ return html `<span class="context-value">[${value.length}]</span>`;
1075
+ return '';
1076
+ }
1077
+ buildExpression(path) {
1078
+ return `@${path}`;
1079
+ }
1080
+ async handleCopyExpression(path, event) {
1081
+ event.stopPropagation();
1082
+ const expression = this.buildExpression(path);
1083
+ try {
1084
+ await navigator.clipboard.writeText(expression);
1085
+ this.copiedExpression = expression;
1086
+ // clear the toast after 2 seconds
1087
+ setTimeout(() => {
1088
+ this.copiedExpression = '';
1089
+ }, 2000);
1090
+ }
1091
+ catch (err) {
1092
+ console.error('Failed to copy expression:', err);
1093
+ }
1094
+ }
1095
+ handleToggleShowAllKeys() {
1096
+ this.showAllKeys = !this.showAllKeys;
1097
+ this.toastMessage = this.showAllKeys
1098
+ ? 'Showing all keys'
1099
+ : 'Filtering out keys without values';
1100
+ // clear the toast after 2 seconds
1101
+ setTimeout(() => {
1102
+ this.toastMessage = '';
1103
+ }, 2000);
1104
+ }
1105
+ renderContextTree(obj, path = '') {
1106
+ if (!obj || typeof obj !== 'object') {
1107
+ return html ``;
1108
+ }
1109
+ let entries = Array.isArray(obj)
1110
+ ? obj.map((v, i) => [String(i), v])
1111
+ : Object.entries(obj).filter(([key]) => key !== '__default__');
1112
+ // filter out keys without values if showAllKeys is false
1113
+ if (!this.showAllKeys) {
1114
+ entries = entries.filter(([, value]) => {
1115
+ // keep if expandable (has children)
1116
+ if (this.isExpandable(value))
1117
+ return true;
1118
+ // keep if it has a displayable value (not null/undefined)
1119
+ if (value === null || value === undefined)
1120
+ return false;
1121
+ // keep primitives with values
1122
+ return (typeof value === 'boolean' ||
1123
+ typeof value === 'number' ||
1124
+ typeof value === 'string' ||
1125
+ Array.isArray(value));
1126
+ });
1127
+ }
1128
+ return html `${entries.map(([key, value]) => {
1129
+ const currentPath = path ? `${path}.${key}` : key;
1130
+ const isExpanded = this.expandedPaths.has(currentPath);
1131
+ const expandable = this.isExpandable(value);
1132
+ // check if this object has a __default__ value
1133
+ let displayValue = value;
1134
+ if (expandable &&
1135
+ !Array.isArray(value) &&
1136
+ value !== null &&
1137
+ typeof value === 'object' &&
1138
+ '__default__' in value) {
1139
+ displayValue = value.__default__;
1140
+ }
1141
+ return html `
1142
+ <div>
1143
+ <div
1144
+ class="context-item ${expandable ? 'context-item-expandable' : ''}"
1145
+ @click=${() => expandable && this.togglePath(currentPath)}
1146
+ >
1147
+ ${expandable
1148
+ ? html `<span
1149
+ class="context-expand-icon ${isExpanded ? 'expanded' : ''}"
1150
+ >›</span
1151
+ >`
1152
+ : html `<span class="context-expand-icon"></span>`}
1153
+ <span class="context-key ${expandable ? 'has-value' : ''}"
1154
+ >${key}
1155
+ <temba-icon
1156
+ class="context-copy-icon"
1157
+ name="copy"
1158
+ size="0.9"
1159
+ @click=${(e) => this.handleCopyExpression(currentPath, e)}
1160
+ ></temba-icon>
1161
+ </span>
1162
+ ${!isExpanded ? this.renderContextValue(displayValue) : html ``}
1163
+ </div>
1164
+ ${isExpanded
1165
+ ? html `<div class="context-children">
1166
+ ${this.renderContextTree(value, currentPath)}
1167
+ </div>`
1168
+ : html ``}
1169
+ </div>
1170
+ `;
1171
+ })}`;
1172
+ }
1173
+ async resume(text, attachment) {
1174
+ if ((!text && !attachment) || !this.session) {
1175
+ return;
1176
+ }
1177
+ this.sprinting = true;
1178
+ this.inputValue = '';
1179
+ this.currentQuickReplies = [];
1180
+ this.attachmentMenuOpen = false;
1181
+ const now = new Date().toISOString();
1182
+ const msgInEvt = {
1183
+ uuid: crypto.randomUUID(),
1184
+ type: 'msg_received',
1185
+ created_on: now,
1186
+ msg: {
1187
+ uuid: crypto.randomUUID(),
1188
+ text: text || '',
1189
+ urn: this.contact.urns[0],
1190
+ attachments: attachment ? [attachment] : []
1191
+ }
1192
+ };
1193
+ // show user's message immediately
1194
+ this.events = [...this.events, msgInEvt];
1195
+ this.requestUpdate();
1196
+ this.scrollToBottom();
1197
+ const body = {
1198
+ session: this.session,
1199
+ contact: this.contact,
1200
+ resume: {
1201
+ type: 'msg',
1202
+ event: msgInEvt,
1203
+ resumed_on: now
1204
+ }
1205
+ };
1206
+ try {
1207
+ const response = await postJSON(this.endpoint, body);
1208
+ // add a small delay before showing the reply to simulate typing
1209
+ await new Promise((resolve) => setTimeout(resolve, 400));
1210
+ // pass null for msgInEvt since we already added it
1211
+ this.updateRunContext(response.json, null);
1212
+ }
1213
+ catch (error) {
1214
+ console.error('Failed to resume simulation:', error);
1215
+ this.events = [
1216
+ ...this.events,
1217
+ {
1218
+ type: 'error',
1219
+ created_on: now,
1220
+ text: 'Failed to send message'
1221
+ }
1222
+ ];
1223
+ this.sprinting = false;
1224
+ }
1225
+ }
1226
+ handleKeyUp(evt) {
1227
+ if (evt.key === 'Enter') {
1228
+ const input = evt.target;
1229
+ const text = input.value.trim();
1230
+ if (text) {
1231
+ this.resume(text);
1232
+ }
1233
+ }
1234
+ }
1235
+ handleInput(evt) {
1236
+ const input = evt.target;
1237
+ this.inputValue = input.value;
1238
+ }
1239
+ handleQuickReply(quickReply) {
1240
+ if (!this.sprinting) {
1241
+ this.resume(quickReply);
1242
+ }
1243
+ }
1244
+ handleToggleAttachmentMenu() {
1245
+ this.attachmentMenuOpen = !this.attachmentMenuOpen;
1246
+ }
1247
+ handleClickOutsideAttachmentMenu(event) {
1248
+ var _a, _b;
1249
+ if (!this.attachmentMenuOpen) {
1250
+ return;
1251
+ }
1252
+ const menu = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.attachment-menu');
1253
+ const button = (_b = this.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.attachment-button');
1254
+ if (!menu || !button) {
1255
+ return;
1256
+ }
1257
+ // check if click is outside both menu and button
1258
+ const clickedInsideMenu = menu.contains(event.target);
1259
+ const clickedInsideButton = button.contains(event.target);
1260
+ if (!clickedInsideMenu && !clickedInsideButton) {
1261
+ this.attachmentMenuOpen = false;
1262
+ }
1263
+ }
1264
+ handleSendAttachment(attachmentType) {
1265
+ let attachment = '';
1266
+ switch (attachmentType) {
1267
+ case 'image':
1268
+ attachment = `image/jpeg:${TEST_IMAGES[this.imageIndex]}`;
1269
+ this.imageIndex = (this.imageIndex + 1) % TEST_IMAGES.length;
1270
+ break;
1271
+ case 'video':
1272
+ attachment = `video/mp4:${TEST_VIDEOS[this.videoIndex]}`;
1273
+ this.videoIndex = (this.videoIndex + 1) % TEST_VIDEOS.length;
1274
+ break;
1275
+ case 'audio':
1276
+ attachment = `audio/mp3:${TEST_AUDIO[this.audioIndex]}`;
1277
+ this.audioIndex = (this.audioIndex + 1) % TEST_AUDIO.length;
1278
+ break;
1279
+ case 'location':
1280
+ attachment = TEST_LOCATIONS[this.locationIndex];
1281
+ this.locationIndex = (this.locationIndex + 1) % TEST_LOCATIONS.length;
1282
+ break;
1283
+ }
1284
+ if (attachment) {
1285
+ this.resume('', attachment);
1286
+ }
1287
+ }
1288
+ getEventDescription(event) {
1289
+ switch (event.type) {
1290
+ case 'contact_groups_changed': {
1291
+ const groups = event.groups_added || [];
1292
+ const removedGroups = event.groups_removed || [];
1293
+ if (groups.length > 0) {
1294
+ const groupNames = groups.map((g) => `"${g.name}"`).join(', ');
1295
+ return `Added to ${groupNames}`;
1296
+ }
1297
+ if (removedGroups.length > 0) {
1298
+ const groupNames = removedGroups
1299
+ .map((g) => `"${g.name}"`)
1300
+ .join(', ');
1301
+ return `Removed from ${groupNames}`;
1302
+ }
1303
+ break;
1304
+ }
1305
+ case 'contact_field_changed': {
1306
+ const field = event.field;
1307
+ const value = event.value;
1308
+ const valueText = value ? value.text || value : '';
1309
+ if (field) {
1310
+ if (valueText) {
1311
+ return `Set contact "${field.name}" to "${valueText}"`;
1312
+ }
1313
+ else {
1314
+ return `Cleared contact "${field.name}"`;
1315
+ }
1316
+ }
1317
+ break;
1318
+ }
1319
+ case 'contact_language_changed':
1320
+ return `Set preferred language to "${event.language}"`;
1321
+ case 'contact_name_changed':
1322
+ return `Set contact name to "${event.name}"`;
1323
+ case 'contact_status_changed':
1324
+ return `Set status to "${event.status}"`;
1325
+ case 'contact_urns_changed':
1326
+ return `Added a URN for the contact`;
1327
+ case 'input_labels_added': {
1328
+ const labels = event.labels || [];
1329
+ if (labels.length > 0) {
1330
+ const labelNames = labels.map((l) => `"${l.name}"`).join(', ');
1331
+ return `Message labeled with ${labelNames}`;
1332
+ }
1333
+ break;
1334
+ }
1335
+ case 'run_result_changed':
1336
+ return `Set result "${event.name}" to "${event.value}"`;
1337
+ case 'run_started':
1338
+ case 'flow_entered': {
1339
+ const flow = event.flow;
1340
+ if (flow) {
1341
+ return `Entered flow "${flow.name}"`;
1342
+ }
1343
+ break;
1344
+ }
1345
+ case 'run_ended': {
1346
+ const flow = event.flow;
1347
+ if (flow) {
1348
+ return `Exited flow "${flow.name}"`;
1349
+ }
1350
+ break;
1351
+ }
1352
+ case 'email_created':
1353
+ case 'email_sent': {
1354
+ const recipients = event.to || event.addresses || [];
1355
+ const subject = event.subject;
1356
+ const recipientList = recipients
1357
+ .map((r) => `"${r}"`)
1358
+ .join(', ');
1359
+ return `Sent email to ${recipientList} with subject "${subject}"`;
1360
+ }
1361
+ case 'broadcast_created': {
1362
+ const translations = event.translations;
1363
+ const baseLanguage = event.base_language;
1364
+ if (translations && translations[baseLanguage]) {
1365
+ return `Sent broadcast: "${translations[baseLanguage].text}"`;
1366
+ }
1367
+ return `Sent broadcast`;
1368
+ }
1369
+ case 'session_triggered': {
1370
+ const flow = event.flow;
1371
+ if (flow) {
1372
+ return `Started somebody else in "${flow.name}"`;
1373
+ }
1374
+ break;
1375
+ }
1376
+ case 'ticket_opened': {
1377
+ const ticket = event.ticket;
1378
+ if (ticket && ticket.topic) {
1379
+ return `Ticket opened with topic "${ticket.topic.name}"`;
1380
+ }
1381
+ return `Ticket opened`;
1382
+ }
1383
+ case 'resthook_called':
1384
+ return `Triggered flow event "${event.resthook}"`;
1385
+ case 'webhook_called':
1386
+ return `Called ${event.url}`;
1387
+ case 'service_called': {
1388
+ const service = event.service;
1389
+ if (service === 'classifier') {
1390
+ return `Called classifier`;
1391
+ }
1392
+ return `Called ${service}`;
1393
+ }
1394
+ case 'airtime_transferred': {
1395
+ const amount = event.actual_amount;
1396
+ const currency = event.currency;
1397
+ const recipient = event.recipient;
1398
+ if (amount && currency && recipient) {
1399
+ return `Transferred ${amount} ${currency} to ${recipient}`;
1400
+ }
1401
+ break;
1402
+ }
1403
+ case 'info':
1404
+ return event.text;
1405
+ case 'warning':
1406
+ return `⚠️ ${event.text}`;
1407
+ }
1408
+ return null;
1409
+ }
1410
+ renderAlertMessage(type, text, animatedClass, animationDelay) {
1411
+ const config = {
1412
+ error: {
1413
+ icon: '❗',
1414
+ bgColor: '#fee2e2',
1415
+ textColor: '#991b1b',
1416
+ defaultText: 'An error occurred'
1417
+ },
1418
+ warning: {
1419
+ icon: '⚠️',
1420
+ bgColor: '#fef3c7',
1421
+ textColor: 'rgba(125, 87, 18, 0.8)',
1422
+ defaultText: 'A warning occurred'
1423
+ },
1424
+ failure: {
1425
+ icon: '💥',
1426
+ bgColor: '#fee2e2',
1427
+ textColor: '#991b1b',
1428
+ defaultText: 'A failure occurred'
1429
+ }
1430
+ }[type];
1431
+ return html `
1432
+ <div
1433
+ class="event-info ${animatedClass}"
1434
+ style="display:flex; align-items:center; background: ${config.bgColor}; color: ${config.textColor}; padding: 6px; margin: 4px 12px; border-radius: 8px; animation-delay: ${animationDelay}"
1435
+ >
1436
+ <div style="padding:4px;margin-right:6px;font-size:15px">
1437
+ ${config.icon}
1438
+ </div>
1439
+ <div style="padding-right:2px;text-align:left">
1440
+ ${text || config.defaultText}
1441
+ </div>
1442
+ </div>
1443
+ `;
1444
+ }
1445
+ renderAttachment(attachment) {
1446
+ // parse attachment format: "type/subtype:url" or "geo:lat,long"
1447
+ const parts = attachment.split(':');
1448
+ const type = parts[0];
1449
+ const content = parts.slice(1).join(':'); // rejoin in case url has colons
1450
+ if (type === 'geo') {
1451
+ // use temba-thumbnail for location to get map image
1452
+ return html `
1453
+ <div class="attachment-location">
1454
+ <temba-thumbnail attachment="${attachment}"></temba-thumbnail>
1455
+ </div>
1456
+ `;
1457
+ }
1458
+ else if (type.startsWith('image/')) {
1459
+ // custom image rendering
1460
+ return html `
1461
+ <div class="attachment">
1462
+ <img src="${content}" alt="Image attachment" />
1463
+ </div>
1464
+ `;
1465
+ }
1466
+ else if (type.startsWith('video/')) {
1467
+ // custom video rendering
1468
+ return html `
1469
+ <div class="attachment">
1470
+ <video controls>
1471
+ <source src="${content}" type="${type}" />
1472
+ </video>
1473
+ </div>
1474
+ `;
1475
+ }
1476
+ else if (type.startsWith('audio/')) {
1477
+ // custom audio rendering
1478
+ return html `
1479
+ <div class="attachment">
1480
+ <div class="attachment-audio">
1481
+ <audio controls>
1482
+ <source src="${content}" type="${type}" />
1483
+ </audio>
1484
+ </div>
1485
+ </div>
1486
+ `;
1487
+ }
1488
+ // fallback for unknown types
1489
+ return html `
1490
+ <div class="attachment">
1491
+ <span>Attachment</span>
1492
+ </div>
1493
+ `;
1494
+ }
1495
+ renderMessages() {
1496
+ if (this.events.length === 0) {
1497
+ return html `
1498
+ <div class="message incoming">👋 Welcome! Starting simulation...</div>
1499
+ `;
1500
+ }
1501
+ const eventTemplates = this.events.map((event, index) => {
1502
+ // only animate messages that are new (beyond previous count)
1503
+ const isNew = index >= this.previousEventCount;
1504
+ const animatedClass = isNew ? 'animated' : '';
1505
+ // stagger animations for new messages
1506
+ const animationDelay = isNew
1507
+ ? `${(index - this.previousEventCount) * 0.2}s`
1508
+ : '0s';
1509
+ if (event.type === 'msg_received' && event.msg) {
1510
+ const hasAttachments = event.msg.attachments && event.msg.attachments.length > 0;
1511
+ const hasText = event.msg.text && event.msg.text.trim().length > 0;
1512
+ return html `
1513
+ ${hasAttachments
1514
+ ? html `
1515
+ <div
1516
+ class="attachment-wrapper outgoing ${animatedClass}"
1517
+ style="animation-delay: ${animationDelay}"
1518
+ >
1519
+ ${event.msg.attachments.map((att) => this.renderAttachment(att))}
1520
+ </div>
1521
+ `
1522
+ : html ``}
1523
+ ${hasText
1524
+ ? html `
1525
+ <div
1526
+ class="message outgoing ${animatedClass}"
1527
+ style="animation-delay: ${animationDelay}"
1528
+ >
1529
+ ${event.msg.text}
1530
+ </div>
1531
+ `
1532
+ : html ``}
1533
+ `;
1534
+ }
1535
+ else if (event.type === 'msg_created' && event.msg) {
1536
+ const hasAttachments = event.msg.attachments && event.msg.attachments.length > 0;
1537
+ const hasText = event.msg.text && event.msg.text.trim().length > 0;
1538
+ return html `
1539
+ ${hasAttachments
1540
+ ? html `
1541
+ <div
1542
+ class="attachment-wrapper incoming ${animatedClass}"
1543
+ style="animation-delay: ${animationDelay}"
1544
+ >
1545
+ ${event.msg.attachments.map((att) => this.renderAttachment(att))}
1546
+ </div>
1547
+ `
1548
+ : html ``}
1549
+ ${hasText
1550
+ ? html `
1551
+ <div
1552
+ class="message incoming ${animatedClass}"
1553
+ style="animation-delay: ${animationDelay}"
1554
+ >
1555
+ ${event.msg.text}
1556
+ </div>
1557
+ `
1558
+ : html ``}
1559
+ `;
1560
+ }
1561
+ else if (event.type === 'failure') {
1562
+ return this.renderAlertMessage('failure', event.text, animatedClass, animationDelay);
1563
+ }
1564
+ else if (event.type === 'warning') {
1565
+ return this.renderAlertMessage('warning', event.text, animatedClass, animationDelay);
1566
+ }
1567
+ else if (event.type === 'error') {
1568
+ return this.renderAlertMessage('error', event.text, animatedClass, animationDelay);
1569
+ }
1570
+ else {
1571
+ // check if this is an event we should display
1572
+ const description = this.getEventDescription(event);
1573
+ if (description) {
1574
+ return html `
1575
+ <div
1576
+ class="event-info ${animatedClass}"
1577
+ style="animation-delay: ${animationDelay}"
1578
+ >
1579
+ ${description}
1580
+ </div>
1581
+ `;
1582
+ }
1583
+ }
1584
+ return html ``;
1585
+ });
1586
+ // render quick replies at the end if we have any from the most recent sprint
1587
+ const hasQuickReplies = this.currentQuickReplies.length > 0;
1588
+ const quickRepliesAnimationDelay = this.events.length >= this.previousEventCount
1589
+ ? `${(this.events.length - this.previousEventCount) * 0.2}s`
1590
+ : '0s';
1591
+ return html `
1592
+ ${eventTemplates}
1593
+ ${hasQuickReplies
1594
+ ? html `
1595
+ <div
1596
+ class="quick-replies animated"
1597
+ style="animation-delay: ${quickRepliesAnimationDelay}"
1598
+ >
1599
+ ${this.currentQuickReplies.map((qr) => html `
1600
+ <button
1601
+ class="quick-reply-btn animated"
1602
+ style="animation-delay: ${quickRepliesAnimationDelay}"
1603
+ @click=${() => this.handleQuickReply(qr.text)}
1604
+ >
1605
+ ${qr.text}
1606
+ </button>
1607
+ `)}
1608
+ </div>
1609
+ `
1610
+ : html ``}
1611
+ `;
1612
+ }
1613
+ render() {
1614
+ const config = this.sizeConfig;
1615
+ // set CSS custom properties dynamically based on size
1616
+ const styleVars = `
1617
+ --phone-width: ${config.phoneWidth}px;
1618
+ --phone-total-height: ${config.phoneTotalHeight}px;
1619
+ --context-width: ${config.contextWidth}px;
1620
+ --context-offset: ${config.contextOffset}px;
1621
+ --option-pane-width: ${config.optionPaneWidth}px;
1622
+ --option-pane-gap: ${config.optionPaneGap}px;
1623
+ --window-padding: ${config.windowPadding}px;
1624
+ --phone-screen-height: ${config.phoneScreenHeight}px;
1625
+ --context-height: ${config.contextHeight}px;
1626
+ --context-closed-left: ${this.contextClosedLeft}px;
1627
+ --cutout-height: ${config.cutoutHeight}px;
1628
+ --cutout-padding: ${config.cutoutPadding}px;
1629
+ --cutout-font-size: ${config.cutoutFontSize}px;
1630
+ --cutout-island-width: ${config.cutoutIslandWidth}px;
1631
+ --cutout-island-height: ${config.cutoutIslandHeight}px;
1632
+ --cutout-island-top: ${config.cutoutIslandTop}px;
1633
+ `;
1634
+ return html `
1635
+ <temba-floating-window
1636
+ id="phone-window"
1637
+ width="${this.windowWidth}"
1638
+ leftBoundaryMargin="${this.leftBoundaryMargin}"
1639
+ bottomBoundaryMargin="${config.windowPadding}"
1640
+ topBoundaryMargin="${config.windowPadding}"
1641
+ height="${config.phoneTotalHeight}"
1642
+ top="0"
1643
+ chromeless
1644
+ >
1645
+ <div class="phone-simulator" style="${styleVars}">
1646
+ <div
1647
+ class="context-explorer ${this.contextExplorerOpen ? 'open' : ''}"
1648
+ >
1649
+ <div class="context-explorer-scroll">
1650
+ ${this.context
1651
+ ? this.renderContextTree(this.context)
1652
+ : html `<div
1653
+ style="color: #9ca3af; padding: 8px; text-align: center;"
1654
+ >
1655
+ No context available
1656
+ </div>`}
1657
+ </div>
1658
+ <div class="context-gutter">
1659
+ <div
1660
+ class="context-gutter-btn ${this.showAllKeys ? '' : 'active'}"
1661
+ @click=${this.handleToggleShowAllKeys}
1662
+ title="${this.showAllKeys
1663
+ ? 'Show keys with values only'
1664
+ : 'Show all keys'}"
1665
+ >
1666
+ <temba-icon
1667
+ name="${this.showAllKeys ? 'filter' : 'filter'}"
1668
+ size="1"
1669
+ ></temba-icon>
1670
+ </div>
1671
+ <div class="context-gutter-spacer"></div>
1672
+ <div
1673
+ class="context-gutter-btn"
1674
+ @click=${this.handleToggleContextExplorer}
1675
+ title="Close"
1676
+ >
1677
+ <temba-icon name="x" size="1"></temba-icon>
1678
+ </div>
1679
+ </div>
1680
+ ${this.copiedExpression
1681
+ ? html `<div class="context-toast">
1682
+ Copied
1683
+ <span class="expression">${this.copiedExpression}</span>
1684
+ to the clipboard
1685
+ </div>`
1686
+ : this.toastMessage
1687
+ ? html `<div class="context-toast">${this.toastMessage}</div>`
1688
+ : html ``}
1689
+ </div>
1690
+
1691
+ <div
1692
+ class="phone-frame"
1693
+ style="pointer-events: ${this.isVisible ? 'all' : 'none'}"
1694
+ >
1695
+ <div class="phone-top drag-handle">
1696
+ <div class="phone-notch">
1697
+ <div class="dynamic-island"></div>
1698
+ </div>
1699
+ </div>
1700
+ <div class="phone-screen">${this.renderMessages()}</div>
1701
+ <div class="message-input">
1702
+ <button
1703
+ class="attachment-button"
1704
+ @click=${this.handleToggleAttachmentMenu}
1705
+ ?disabled=${this.sprinting}
1706
+ >
1707
+ <temba-icon name="plus" size="1.5"></temba-icon>
1708
+ </button>
1709
+ <input
1710
+ type="text"
1711
+ placeholder="Enter Message"
1712
+ .value=${this.inputValue}
1713
+ @input=${this.handleInput}
1714
+ @keyup=${this.handleKeyUp}
1715
+ ?disabled=${this.sprinting}
1716
+ />
1717
+ <div
1718
+ class="attachment-menu ${this.attachmentMenuOpen ? 'open' : ''}"
1719
+ >
1720
+ <div
1721
+ class="attachment-menu-item"
1722
+ @click=${() => this.handleSendAttachment('image')}
1723
+ >
1724
+ <temba-icon name="attachment_image" size="1.2"></temba-icon>
1725
+ <span>Image</span>
1726
+ </div>
1727
+ <div
1728
+ class="attachment-menu-item"
1729
+ @click=${() => this.handleSendAttachment('video')}
1730
+ >
1731
+ <temba-icon name="attachment_video" size="1.2"></temba-icon>
1732
+ <span>Video</span>
1733
+ </div>
1734
+ <div
1735
+ class="attachment-menu-item"
1736
+ @click=${() => this.handleSendAttachment('audio')}
1737
+ >
1738
+ <temba-icon name="attachment_audio" size="1.2"></temba-icon>
1739
+ <span>Audio</span>
1740
+ </div>
1741
+ <div
1742
+ class="attachment-menu-item"
1743
+ @click=${() => this.handleSendAttachment('location')}
1744
+ >
1745
+ <temba-icon
1746
+ name="attachment_location"
1747
+ size="1.2"
1748
+ ></temba-icon>
1749
+ <span>Location</span>
1750
+ </div>
1751
+ </div>
1752
+ </div>
1753
+ </div>
1754
+ <div class="option-pane">
1755
+ <button class="option-btn" @click=${this.handleClose} title="Close">
1756
+ <temba-icon name="x" size="1.5"></temba-icon>
1757
+ </button>
1758
+ <button
1759
+ class="option-btn ${this.following ? 'active' : ''}"
1760
+ @click=${this.handleToggleFollow}
1761
+ title="${this.following ? 'Following' : 'Follow'}"
1762
+ >
1763
+ <temba-icon name="follow" size="1.5"></temba-icon>
1764
+ </button>
1765
+
1766
+ <button
1767
+ class="option-btn ${this.contextExplorerOpen ? 'active' : ''}"
1768
+ @click=${this.handleToggleContextExplorer}
1769
+ title="Context Explorer"
1770
+ >
1771
+ <temba-icon name="expressions" size="1.5"></temba-icon>
1772
+ </button>
1773
+
1774
+ <button
1775
+ class="option-btn"
1776
+ @click=${this.handleCycleSize}
1777
+ title="Size: ${this.size}"
1778
+ >
1779
+ ${this.size === 'small'
1780
+ ? 'S'
1781
+ : this.size === 'medium'
1782
+ ? 'M'
1783
+ : 'L'}
1784
+ </button>
1785
+
1786
+ <button class="option-btn" @click=${this.handleReset} title="Reset">
1787
+ <temba-icon name="delete" size="1.5"></temba-icon>
1788
+ </button>
1789
+ </div>
1790
+ </div>
1791
+ </temba-floating-window>
1792
+
1793
+ <temba-floating-tab
1794
+ id="phone-tab"
1795
+ icon="simulator"
1796
+ label="Phone Simulator"
1797
+ color="#10b981"
1798
+ .hidden=${this.isVisible}
1799
+ @temba-button-clicked=${this.handleShow}
1800
+ ></temba-floating-tab>
1801
+ `;
1802
+ }
1803
+ }
1804
+ __decorate([
1805
+ property({ type: String })
1806
+ ], Simulator.prototype, "flow", void 0);
1807
+ __decorate([
1808
+ property({ type: String })
1809
+ ], Simulator.prototype, "endpoint", void 0);
1810
+ __decorate([
1811
+ property({ type: Number })
1812
+ ], Simulator.prototype, "animationTime", void 0);
1813
+ __decorate([
1814
+ fromCookie('simulator-size', 'small')
1815
+ ], Simulator.prototype, "size", void 0);
1816
+ __decorate([
1817
+ property({ type: Array })
1818
+ ], Simulator.prototype, "events", void 0);
1819
+ __decorate([
1820
+ property({ type: Object })
1821
+ ], Simulator.prototype, "session", void 0);
1822
+ __decorate([
1823
+ property({ type: Object })
1824
+ ], Simulator.prototype, "context", void 0);
1825
+ __decorate([
1826
+ property({ type: Object })
1827
+ ], Simulator.prototype, "contact", void 0);
1828
+ __decorate([
1829
+ property({ type: Boolean })
1830
+ ], Simulator.prototype, "sprinting", void 0);
1831
+ __decorate([
1832
+ property({ type: String })
1833
+ ], Simulator.prototype, "inputValue", void 0);
1834
+ __decorate([
1835
+ fromCookie('simulator-follow', true)
1836
+ ], Simulator.prototype, "following", void 0);
1837
+ __decorate([
1838
+ fromCookie('simulator-context-open', false)
1839
+ ], Simulator.prototype, "contextExplorerOpen", void 0);
1840
+ __decorate([
1841
+ property({ type: Object })
1842
+ ], Simulator.prototype, "expandedPaths", void 0);
1843
+ __decorate([
1844
+ property({ type: String })
1845
+ ], Simulator.prototype, "copiedExpression", void 0);
1846
+ __decorate([
1847
+ property({ type: String })
1848
+ ], Simulator.prototype, "toastMessage", void 0);
1849
+ __decorate([
1850
+ property({ type: Boolean })
1851
+ ], Simulator.prototype, "showAllKeys", void 0);
1852
+ __decorate([
1853
+ property({ type: Array })
1854
+ ], Simulator.prototype, "currentQuickReplies", void 0);
1855
+ __decorate([
1856
+ property({ type: Boolean })
1857
+ ], Simulator.prototype, "isVisible", void 0);
1858
+ __decorate([
1859
+ property({ type: Boolean })
1860
+ ], Simulator.prototype, "attachmentMenuOpen", void 0);
1861
+ //# sourceMappingURL=Simulator.js.map