@nyaruka/temba-components 0.134.6 → 0.136.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +16 -8
- package/CHANGELOG.md +84 -0
- package/demo/components/webchat/example.html +4 -2
- package/dist/static/svg/index.svg +1 -1
- package/dist/temba-components.js +1346 -334
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/Icons.js +2 -1
- package/out-tsc/src/Icons.js.map +1 -1
- package/out-tsc/src/display/Chat.js +7 -2
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/Thumbnail.js +65 -8
- package/out-tsc/src/display/Thumbnail.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +11 -0
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +224 -2
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +320 -1
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/interfaces.js +1 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/layout/FloatingWindow.js +30 -8
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +1827 -0
- package/out-tsc/src/simulator/Simulator.js.map +1 -0
- package/out-tsc/src/store/AppState.js +33 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/src/utils.js +55 -6
- package/out-tsc/src/utils.js.map +1 -1
- package/out-tsc/temba-modules.js +2 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +1 -1
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +3 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +3 -1
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +642 -0
- package/out-tsc/test/temba-simulator.test.js.map +1 -0
- package/out-tsc/test/temba-thumbnail.test.js +120 -0
- package/out-tsc/test/temba-thumbnail.test.js.map +1 -0
- package/out-tsc/test/temba-utils-index.test.js +12 -6
- package/out-tsc/test/temba-utils-index.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +1 -1
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/simulator/after-message-sent.png +0 -0
- package/screenshots/truth/simulator/after-reset.png +0 -0
- package/screenshots/truth/simulator/attachment-menu.png +0 -0
- package/screenshots/truth/simulator/context-expanded.png +0 -0
- package/screenshots/truth/simulator/context-explorer-open.png +0 -0
- package/screenshots/truth/simulator/event-info.png +0 -0
- package/screenshots/truth/simulator/image-attachment.png +0 -0
- package/screenshots/truth/simulator/open-initial.png +0 -0
- package/screenshots/truth/simulator/quick-replies.png +0 -0
- package/src/Icons.ts +2 -1
- package/src/display/Chat.ts +10 -1
- package/src/display/Thumbnail.ts +67 -8
- package/src/flow/CanvasNode.ts +12 -0
- package/src/flow/Editor.ts +240 -1
- package/src/flow/Plumber.ts +371 -2
- package/src/interfaces.ts +2 -1
- package/src/layout/FloatingWindow.ts +36 -11
- package/src/simulator/Simulator.ts +2008 -0
- package/src/store/AppState.ts +53 -0
- package/src/utils.ts +59 -6
- package/static/svg/index.svg +1 -1
- package/static/svg/work/traced/route.svg +1 -0
- package/static/svg/work/used/route.svg +3 -0
- package/temba-modules.ts +2 -0
- package/test/temba-flow-editor.test.ts +1 -1
- package/test/temba-flow-plumber-connections.test.ts +4 -1
- package/test/temba-flow-plumber.test.ts +4 -1
- package/test/temba-simulator.test.ts +866 -0
- package/test/temba-thumbnail.test.ts +150 -0
- package/test/temba-utils-index.test.ts +14 -6
- package/test/utils.test.ts +1 -1
|
@@ -0,0 +1,1827 @@
|
|
|
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
|
+
this.isVisible = false;
|
|
983
|
+
getStore().getState().setSimulatorActive(false);
|
|
984
|
+
const phoneTab = this.shadowRoot.getElementById('phone-tab');
|
|
985
|
+
phoneTab.hidden = false;
|
|
986
|
+
}
|
|
987
|
+
handleReset() {
|
|
988
|
+
// reset simulation state
|
|
989
|
+
this.events = [];
|
|
990
|
+
this.session = null;
|
|
991
|
+
this.context = null;
|
|
992
|
+
this.inputValue = '';
|
|
993
|
+
this.sprinting = false;
|
|
994
|
+
this.previousEventCount = 0;
|
|
995
|
+
this.currentQuickReplies = [];
|
|
996
|
+
// Clear simulator activity data
|
|
997
|
+
getStore().getState().updateSimulatorActivity({
|
|
998
|
+
segments: {},
|
|
999
|
+
nodes: {}
|
|
1000
|
+
});
|
|
1001
|
+
// reset contact to initial state
|
|
1002
|
+
this.contact = {
|
|
1003
|
+
uuid: 'fb3787ab-2eda-48a0-a2bc-e2ddadec1286',
|
|
1004
|
+
urns: ['tel:+12065551212'],
|
|
1005
|
+
fields: {},
|
|
1006
|
+
groups: [],
|
|
1007
|
+
language: 'eng',
|
|
1008
|
+
status: 'active',
|
|
1009
|
+
created_on: new Date().toISOString()
|
|
1010
|
+
};
|
|
1011
|
+
// restart the flow
|
|
1012
|
+
this.startFlow();
|
|
1013
|
+
}
|
|
1014
|
+
handleToggleFollow() {
|
|
1015
|
+
this.following = !this.following;
|
|
1016
|
+
}
|
|
1017
|
+
handleCycleSize() {
|
|
1018
|
+
const sizes = [
|
|
1019
|
+
'small',
|
|
1020
|
+
'medium',
|
|
1021
|
+
'large'
|
|
1022
|
+
];
|
|
1023
|
+
const currentIndex = sizes.indexOf(this.size);
|
|
1024
|
+
const nextIndex = (currentIndex + 1) % sizes.length;
|
|
1025
|
+
this.size = sizes[nextIndex];
|
|
1026
|
+
}
|
|
1027
|
+
handleToggleContextExplorer() {
|
|
1028
|
+
this.contextExplorerOpen = !this.contextExplorerOpen;
|
|
1029
|
+
// if opening the context explorer, ensure it's not off-screen
|
|
1030
|
+
if (this.contextExplorerOpen) {
|
|
1031
|
+
requestAnimationFrame(() => {
|
|
1032
|
+
var _a;
|
|
1033
|
+
const phoneWindow = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.getElementById('phone-window');
|
|
1034
|
+
if (phoneWindow) {
|
|
1035
|
+
const padding = 20;
|
|
1036
|
+
const contextExplorerLeft = this.sizeConfig.contextOffset;
|
|
1037
|
+
const minWindowLeft = padding - contextExplorerLeft;
|
|
1038
|
+
if (phoneWindow.left < minWindowLeft) {
|
|
1039
|
+
phoneWindow.left = minWindowLeft;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
togglePath(path) {
|
|
1046
|
+
if (this.expandedPaths.has(path)) {
|
|
1047
|
+
this.expandedPaths.delete(path);
|
|
1048
|
+
}
|
|
1049
|
+
else {
|
|
1050
|
+
this.expandedPaths.add(path);
|
|
1051
|
+
}
|
|
1052
|
+
this.requestUpdate();
|
|
1053
|
+
}
|
|
1054
|
+
isExpandable(value) {
|
|
1055
|
+
if (value === null || typeof value !== 'object') {
|
|
1056
|
+
return false;
|
|
1057
|
+
}
|
|
1058
|
+
if (Array.isArray(value)) {
|
|
1059
|
+
return value.length > 0;
|
|
1060
|
+
}
|
|
1061
|
+
// check if object has keys other than __default__
|
|
1062
|
+
const keys = Object.keys(value).filter((key) => key !== '__default__');
|
|
1063
|
+
return keys.length > 0;
|
|
1064
|
+
}
|
|
1065
|
+
renderContextValue(value) {
|
|
1066
|
+
if (value === null || value === undefined)
|
|
1067
|
+
return '';
|
|
1068
|
+
if (typeof value === 'boolean')
|
|
1069
|
+
return html `<span class="context-value">${value}</span>`;
|
|
1070
|
+
if (typeof value === 'number')
|
|
1071
|
+
return html `<span class="context-value">${value}</span>`;
|
|
1072
|
+
if (typeof value === 'string')
|
|
1073
|
+
return html `<span class="context-value">${value}</span>`;
|
|
1074
|
+
if (Array.isArray(value))
|
|
1075
|
+
return html `<span class="context-value">[${value.length}]</span>`;
|
|
1076
|
+
return '';
|
|
1077
|
+
}
|
|
1078
|
+
buildExpression(path) {
|
|
1079
|
+
return `@${path}`;
|
|
1080
|
+
}
|
|
1081
|
+
async handleCopyExpression(path, event) {
|
|
1082
|
+
event.stopPropagation();
|
|
1083
|
+
const expression = this.buildExpression(path);
|
|
1084
|
+
try {
|
|
1085
|
+
await navigator.clipboard.writeText(expression);
|
|
1086
|
+
this.copiedExpression = expression;
|
|
1087
|
+
// clear the toast after 2 seconds
|
|
1088
|
+
setTimeout(() => {
|
|
1089
|
+
this.copiedExpression = '';
|
|
1090
|
+
}, 2000);
|
|
1091
|
+
}
|
|
1092
|
+
catch (err) {
|
|
1093
|
+
console.error('Failed to copy expression:', err);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
handleToggleShowAllKeys() {
|
|
1097
|
+
this.showAllKeys = !this.showAllKeys;
|
|
1098
|
+
this.toastMessage = this.showAllKeys
|
|
1099
|
+
? 'Showing all keys'
|
|
1100
|
+
: 'Filtering out keys without values';
|
|
1101
|
+
// clear the toast after 2 seconds
|
|
1102
|
+
setTimeout(() => {
|
|
1103
|
+
this.toastMessage = '';
|
|
1104
|
+
}, 2000);
|
|
1105
|
+
}
|
|
1106
|
+
renderContextTree(obj, path = '') {
|
|
1107
|
+
if (!obj || typeof obj !== 'object') {
|
|
1108
|
+
return html ``;
|
|
1109
|
+
}
|
|
1110
|
+
let entries = Array.isArray(obj)
|
|
1111
|
+
? obj.map((v, i) => [String(i), v])
|
|
1112
|
+
: Object.entries(obj).filter(([key]) => key !== '__default__');
|
|
1113
|
+
// filter out keys without values if showAllKeys is false
|
|
1114
|
+
if (!this.showAllKeys) {
|
|
1115
|
+
entries = entries.filter(([, value]) => {
|
|
1116
|
+
// keep if expandable (has children)
|
|
1117
|
+
if (this.isExpandable(value))
|
|
1118
|
+
return true;
|
|
1119
|
+
// keep if it has a displayable value (not null/undefined)
|
|
1120
|
+
if (value === null || value === undefined)
|
|
1121
|
+
return false;
|
|
1122
|
+
// keep primitives with values
|
|
1123
|
+
return (typeof value === 'boolean' ||
|
|
1124
|
+
typeof value === 'number' ||
|
|
1125
|
+
typeof value === 'string' ||
|
|
1126
|
+
Array.isArray(value));
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
return html `${entries.map(([key, value]) => {
|
|
1130
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
1131
|
+
const isExpanded = this.expandedPaths.has(currentPath);
|
|
1132
|
+
const expandable = this.isExpandable(value);
|
|
1133
|
+
// check if this object has a __default__ value
|
|
1134
|
+
let displayValue = value;
|
|
1135
|
+
if (expandable &&
|
|
1136
|
+
!Array.isArray(value) &&
|
|
1137
|
+
value !== null &&
|
|
1138
|
+
typeof value === 'object' &&
|
|
1139
|
+
'__default__' in value) {
|
|
1140
|
+
displayValue = value.__default__;
|
|
1141
|
+
}
|
|
1142
|
+
return html `
|
|
1143
|
+
<div>
|
|
1144
|
+
<div
|
|
1145
|
+
class="context-item ${expandable ? 'context-item-expandable' : ''}"
|
|
1146
|
+
@click=${() => expandable && this.togglePath(currentPath)}
|
|
1147
|
+
>
|
|
1148
|
+
${expandable
|
|
1149
|
+
? html `<span
|
|
1150
|
+
class="context-expand-icon ${isExpanded ? 'expanded' : ''}"
|
|
1151
|
+
>›</span
|
|
1152
|
+
>`
|
|
1153
|
+
: html `<span class="context-expand-icon"></span>`}
|
|
1154
|
+
<span class="context-key ${expandable ? 'has-value' : ''}"
|
|
1155
|
+
>${key}
|
|
1156
|
+
<temba-icon
|
|
1157
|
+
class="context-copy-icon"
|
|
1158
|
+
name="copy"
|
|
1159
|
+
size="0.9"
|
|
1160
|
+
@click=${(e) => this.handleCopyExpression(currentPath, e)}
|
|
1161
|
+
></temba-icon>
|
|
1162
|
+
</span>
|
|
1163
|
+
${!isExpanded ? this.renderContextValue(displayValue) : html ``}
|
|
1164
|
+
</div>
|
|
1165
|
+
${isExpanded
|
|
1166
|
+
? html `<div class="context-children">
|
|
1167
|
+
${this.renderContextTree(value, currentPath)}
|
|
1168
|
+
</div>`
|
|
1169
|
+
: html ``}
|
|
1170
|
+
</div>
|
|
1171
|
+
`;
|
|
1172
|
+
})}`;
|
|
1173
|
+
}
|
|
1174
|
+
async resume(text, attachment) {
|
|
1175
|
+
if ((!text && !attachment) || !this.session) {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
this.sprinting = true;
|
|
1179
|
+
this.inputValue = '';
|
|
1180
|
+
this.currentQuickReplies = [];
|
|
1181
|
+
this.attachmentMenuOpen = false;
|
|
1182
|
+
const now = new Date().toISOString();
|
|
1183
|
+
const msgInEvt = {
|
|
1184
|
+
uuid: crypto.randomUUID(),
|
|
1185
|
+
type: 'msg_received',
|
|
1186
|
+
created_on: now,
|
|
1187
|
+
msg: {
|
|
1188
|
+
uuid: crypto.randomUUID(),
|
|
1189
|
+
text: text || '',
|
|
1190
|
+
urn: this.contact.urns[0],
|
|
1191
|
+
attachments: attachment ? [attachment] : []
|
|
1192
|
+
}
|
|
1193
|
+
};
|
|
1194
|
+
// show user's message immediately
|
|
1195
|
+
this.events = [...this.events, msgInEvt];
|
|
1196
|
+
this.requestUpdate();
|
|
1197
|
+
this.scrollToBottom();
|
|
1198
|
+
const body = {
|
|
1199
|
+
session: this.session,
|
|
1200
|
+
contact: this.contact,
|
|
1201
|
+
resume: {
|
|
1202
|
+
type: 'msg',
|
|
1203
|
+
event: msgInEvt,
|
|
1204
|
+
resumed_on: now
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
try {
|
|
1208
|
+
const response = await postJSON(this.endpoint, body);
|
|
1209
|
+
// add a small delay before showing the reply to simulate typing
|
|
1210
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
1211
|
+
// pass null for msgInEvt since we already added it
|
|
1212
|
+
this.updateRunContext(response.json, null);
|
|
1213
|
+
}
|
|
1214
|
+
catch (error) {
|
|
1215
|
+
console.error('Failed to resume simulation:', error);
|
|
1216
|
+
this.events = [
|
|
1217
|
+
...this.events,
|
|
1218
|
+
{
|
|
1219
|
+
type: 'error',
|
|
1220
|
+
created_on: now,
|
|
1221
|
+
text: 'Failed to send message'
|
|
1222
|
+
}
|
|
1223
|
+
];
|
|
1224
|
+
this.sprinting = false;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
handleKeyUp(evt) {
|
|
1228
|
+
if (evt.key === 'Enter') {
|
|
1229
|
+
const input = evt.target;
|
|
1230
|
+
const text = input.value.trim();
|
|
1231
|
+
if (text) {
|
|
1232
|
+
this.resume(text);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
handleInput(evt) {
|
|
1237
|
+
const input = evt.target;
|
|
1238
|
+
this.inputValue = input.value;
|
|
1239
|
+
}
|
|
1240
|
+
handleQuickReply(quickReply) {
|
|
1241
|
+
if (!this.sprinting) {
|
|
1242
|
+
this.resume(quickReply);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
handleToggleAttachmentMenu() {
|
|
1246
|
+
this.attachmentMenuOpen = !this.attachmentMenuOpen;
|
|
1247
|
+
}
|
|
1248
|
+
handleClickOutsideAttachmentMenu(event) {
|
|
1249
|
+
var _a, _b;
|
|
1250
|
+
if (!this.attachmentMenuOpen) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
const menu = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.attachment-menu');
|
|
1254
|
+
const button = (_b = this.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.attachment-button');
|
|
1255
|
+
if (!menu || !button) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
// check if click is outside both menu and button
|
|
1259
|
+
const clickedInsideMenu = menu.contains(event.target);
|
|
1260
|
+
const clickedInsideButton = button.contains(event.target);
|
|
1261
|
+
if (!clickedInsideMenu && !clickedInsideButton) {
|
|
1262
|
+
this.attachmentMenuOpen = false;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
handleSendAttachment(attachmentType) {
|
|
1266
|
+
let attachment = '';
|
|
1267
|
+
switch (attachmentType) {
|
|
1268
|
+
case 'image':
|
|
1269
|
+
attachment = `image/jpeg:${TEST_IMAGES[this.imageIndex]}`;
|
|
1270
|
+
this.imageIndex = (this.imageIndex + 1) % TEST_IMAGES.length;
|
|
1271
|
+
break;
|
|
1272
|
+
case 'video':
|
|
1273
|
+
attachment = `video/mp4:${TEST_VIDEOS[this.videoIndex]}`;
|
|
1274
|
+
this.videoIndex = (this.videoIndex + 1) % TEST_VIDEOS.length;
|
|
1275
|
+
break;
|
|
1276
|
+
case 'audio':
|
|
1277
|
+
attachment = `audio/mp3:${TEST_AUDIO[this.audioIndex]}`;
|
|
1278
|
+
this.audioIndex = (this.audioIndex + 1) % TEST_AUDIO.length;
|
|
1279
|
+
break;
|
|
1280
|
+
case 'location':
|
|
1281
|
+
attachment = TEST_LOCATIONS[this.locationIndex];
|
|
1282
|
+
this.locationIndex = (this.locationIndex + 1) % TEST_LOCATIONS.length;
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
if (attachment) {
|
|
1286
|
+
this.resume('', attachment);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
getEventDescription(event) {
|
|
1290
|
+
switch (event.type) {
|
|
1291
|
+
case 'contact_groups_changed': {
|
|
1292
|
+
const groups = event.groups_added || [];
|
|
1293
|
+
const removedGroups = event.groups_removed || [];
|
|
1294
|
+
if (groups.length > 0) {
|
|
1295
|
+
const groupNames = groups.map((g) => `"${g.name}"`).join(', ');
|
|
1296
|
+
return `Added to ${groupNames}`;
|
|
1297
|
+
}
|
|
1298
|
+
if (removedGroups.length > 0) {
|
|
1299
|
+
const groupNames = removedGroups
|
|
1300
|
+
.map((g) => `"${g.name}"`)
|
|
1301
|
+
.join(', ');
|
|
1302
|
+
return `Removed from ${groupNames}`;
|
|
1303
|
+
}
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
case 'contact_field_changed': {
|
|
1307
|
+
const field = event.field;
|
|
1308
|
+
const value = event.value;
|
|
1309
|
+
const valueText = value ? value.text || value : '';
|
|
1310
|
+
if (field) {
|
|
1311
|
+
if (valueText) {
|
|
1312
|
+
return `Set contact "${field.name}" to "${valueText}"`;
|
|
1313
|
+
}
|
|
1314
|
+
else {
|
|
1315
|
+
return `Cleared contact "${field.name}"`;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
case 'contact_language_changed':
|
|
1321
|
+
return `Set preferred language to "${event.language}"`;
|
|
1322
|
+
case 'contact_name_changed':
|
|
1323
|
+
return `Set contact name to "${event.name}"`;
|
|
1324
|
+
case 'contact_status_changed':
|
|
1325
|
+
return `Set status to "${event.status}"`;
|
|
1326
|
+
case 'contact_urns_changed':
|
|
1327
|
+
return `Added a URN for the contact`;
|
|
1328
|
+
case 'input_labels_added': {
|
|
1329
|
+
const labels = event.labels || [];
|
|
1330
|
+
if (labels.length > 0) {
|
|
1331
|
+
const labelNames = labels.map((l) => `"${l.name}"`).join(', ');
|
|
1332
|
+
return `Message labeled with ${labelNames}`;
|
|
1333
|
+
}
|
|
1334
|
+
break;
|
|
1335
|
+
}
|
|
1336
|
+
case 'run_result_changed':
|
|
1337
|
+
return `Set result "${event.name}" to "${event.value}"`;
|
|
1338
|
+
case 'run_started':
|
|
1339
|
+
case 'flow_entered': {
|
|
1340
|
+
const flow = event.flow;
|
|
1341
|
+
if (flow) {
|
|
1342
|
+
return `Entered flow "${flow.name}"`;
|
|
1343
|
+
}
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
case 'run_ended': {
|
|
1347
|
+
const flow = event.flow;
|
|
1348
|
+
if (flow) {
|
|
1349
|
+
return `Exited flow "${flow.name}"`;
|
|
1350
|
+
}
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
case 'email_created':
|
|
1354
|
+
case 'email_sent': {
|
|
1355
|
+
const recipients = event.to || event.addresses || [];
|
|
1356
|
+
const subject = event.subject;
|
|
1357
|
+
const recipientList = recipients
|
|
1358
|
+
.map((r) => `"${r}"`)
|
|
1359
|
+
.join(', ');
|
|
1360
|
+
return `Sent email to ${recipientList} with subject "${subject}"`;
|
|
1361
|
+
}
|
|
1362
|
+
case 'broadcast_created': {
|
|
1363
|
+
const translations = event.translations;
|
|
1364
|
+
const baseLanguage = event.base_language;
|
|
1365
|
+
if (translations && translations[baseLanguage]) {
|
|
1366
|
+
return `Sent broadcast: "${translations[baseLanguage].text}"`;
|
|
1367
|
+
}
|
|
1368
|
+
return `Sent broadcast`;
|
|
1369
|
+
}
|
|
1370
|
+
case 'session_triggered': {
|
|
1371
|
+
const flow = event.flow;
|
|
1372
|
+
if (flow) {
|
|
1373
|
+
return `Started somebody else in "${flow.name}"`;
|
|
1374
|
+
}
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
case 'ticket_opened': {
|
|
1378
|
+
const ticket = event.ticket;
|
|
1379
|
+
if (ticket && ticket.topic) {
|
|
1380
|
+
return `Ticket opened with topic "${ticket.topic.name}"`;
|
|
1381
|
+
}
|
|
1382
|
+
return `Ticket opened`;
|
|
1383
|
+
}
|
|
1384
|
+
case 'resthook_called':
|
|
1385
|
+
return `Triggered flow event "${event.resthook}"`;
|
|
1386
|
+
case 'webhook_called':
|
|
1387
|
+
return `Called ${event.url}`;
|
|
1388
|
+
case 'service_called': {
|
|
1389
|
+
const service = event.service;
|
|
1390
|
+
if (service === 'classifier') {
|
|
1391
|
+
return `Called classifier`;
|
|
1392
|
+
}
|
|
1393
|
+
return `Called ${service}`;
|
|
1394
|
+
}
|
|
1395
|
+
case 'airtime_transferred': {
|
|
1396
|
+
const amount = event.actual_amount;
|
|
1397
|
+
const currency = event.currency;
|
|
1398
|
+
const recipient = event.recipient;
|
|
1399
|
+
if (amount && currency && recipient) {
|
|
1400
|
+
return `Transferred ${amount} ${currency} to ${recipient}`;
|
|
1401
|
+
}
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
case 'info':
|
|
1405
|
+
return event.text;
|
|
1406
|
+
case 'warning':
|
|
1407
|
+
return `⚠️ ${event.text}`;
|
|
1408
|
+
}
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
renderAttachment(attachment) {
|
|
1412
|
+
// parse attachment format: "type/subtype:url" or "geo:lat,long"
|
|
1413
|
+
const parts = attachment.split(':');
|
|
1414
|
+
const type = parts[0];
|
|
1415
|
+
const content = parts.slice(1).join(':'); // rejoin in case url has colons
|
|
1416
|
+
if (type === 'geo') {
|
|
1417
|
+
// use temba-thumbnail for location to get map image
|
|
1418
|
+
return html `
|
|
1419
|
+
<div class="attachment-location">
|
|
1420
|
+
<temba-thumbnail attachment="${attachment}"></temba-thumbnail>
|
|
1421
|
+
</div>
|
|
1422
|
+
`;
|
|
1423
|
+
}
|
|
1424
|
+
else if (type.startsWith('image/')) {
|
|
1425
|
+
// custom image rendering
|
|
1426
|
+
return html `
|
|
1427
|
+
<div class="attachment">
|
|
1428
|
+
<img src="${content}" alt="Image attachment" />
|
|
1429
|
+
</div>
|
|
1430
|
+
`;
|
|
1431
|
+
}
|
|
1432
|
+
else if (type.startsWith('video/')) {
|
|
1433
|
+
// custom video rendering
|
|
1434
|
+
return html `
|
|
1435
|
+
<div class="attachment">
|
|
1436
|
+
<video controls>
|
|
1437
|
+
<source src="${content}" type="${type}" />
|
|
1438
|
+
</video>
|
|
1439
|
+
</div>
|
|
1440
|
+
`;
|
|
1441
|
+
}
|
|
1442
|
+
else if (type.startsWith('audio/')) {
|
|
1443
|
+
// custom audio rendering
|
|
1444
|
+
return html `
|
|
1445
|
+
<div class="attachment">
|
|
1446
|
+
<div class="attachment-audio">
|
|
1447
|
+
<audio controls>
|
|
1448
|
+
<source src="${content}" type="${type}" />
|
|
1449
|
+
</audio>
|
|
1450
|
+
</div>
|
|
1451
|
+
</div>
|
|
1452
|
+
`;
|
|
1453
|
+
}
|
|
1454
|
+
// fallback for unknown types
|
|
1455
|
+
return html `
|
|
1456
|
+
<div class="attachment">
|
|
1457
|
+
<span>Attachment</span>
|
|
1458
|
+
</div>
|
|
1459
|
+
`;
|
|
1460
|
+
}
|
|
1461
|
+
renderMessages() {
|
|
1462
|
+
if (this.events.length === 0) {
|
|
1463
|
+
return html `
|
|
1464
|
+
<div class="message incoming">👋 Welcome! Starting simulation...</div>
|
|
1465
|
+
`;
|
|
1466
|
+
}
|
|
1467
|
+
const eventTemplates = this.events.map((event, index) => {
|
|
1468
|
+
// only animate messages that are new (beyond previous count)
|
|
1469
|
+
const isNew = index >= this.previousEventCount;
|
|
1470
|
+
const animatedClass = isNew ? 'animated' : '';
|
|
1471
|
+
// stagger animations for new messages
|
|
1472
|
+
const animationDelay = isNew
|
|
1473
|
+
? `${(index - this.previousEventCount) * 0.2}s`
|
|
1474
|
+
: '0s';
|
|
1475
|
+
if (event.type === 'msg_received' && event.msg) {
|
|
1476
|
+
const hasAttachments = event.msg.attachments && event.msg.attachments.length > 0;
|
|
1477
|
+
const hasText = event.msg.text && event.msg.text.trim().length > 0;
|
|
1478
|
+
return html `
|
|
1479
|
+
${hasAttachments
|
|
1480
|
+
? html `
|
|
1481
|
+
<div
|
|
1482
|
+
class="attachment-wrapper outgoing ${animatedClass}"
|
|
1483
|
+
style="animation-delay: ${animationDelay}"
|
|
1484
|
+
>
|
|
1485
|
+
${event.msg.attachments.map((att) => this.renderAttachment(att))}
|
|
1486
|
+
</div>
|
|
1487
|
+
`
|
|
1488
|
+
: html ``}
|
|
1489
|
+
${hasText
|
|
1490
|
+
? html `
|
|
1491
|
+
<div
|
|
1492
|
+
class="message outgoing ${animatedClass}"
|
|
1493
|
+
style="animation-delay: ${animationDelay}"
|
|
1494
|
+
>
|
|
1495
|
+
${event.msg.text}
|
|
1496
|
+
</div>
|
|
1497
|
+
`
|
|
1498
|
+
: html ``}
|
|
1499
|
+
`;
|
|
1500
|
+
}
|
|
1501
|
+
else if (event.type === 'msg_created' && event.msg) {
|
|
1502
|
+
const hasAttachments = event.msg.attachments && event.msg.attachments.length > 0;
|
|
1503
|
+
const hasText = event.msg.text && event.msg.text.trim().length > 0;
|
|
1504
|
+
return html `
|
|
1505
|
+
${hasAttachments
|
|
1506
|
+
? html `
|
|
1507
|
+
<div
|
|
1508
|
+
class="attachment-wrapper incoming ${animatedClass}"
|
|
1509
|
+
style="animation-delay: ${animationDelay}"
|
|
1510
|
+
>
|
|
1511
|
+
${event.msg.attachments.map((att) => this.renderAttachment(att))}
|
|
1512
|
+
</div>
|
|
1513
|
+
`
|
|
1514
|
+
: html ``}
|
|
1515
|
+
${hasText
|
|
1516
|
+
? html `
|
|
1517
|
+
<div
|
|
1518
|
+
class="message incoming ${animatedClass}"
|
|
1519
|
+
style="animation-delay: ${animationDelay}"
|
|
1520
|
+
>
|
|
1521
|
+
${event.msg.text}
|
|
1522
|
+
</div>
|
|
1523
|
+
`
|
|
1524
|
+
: html ``}
|
|
1525
|
+
`;
|
|
1526
|
+
}
|
|
1527
|
+
else if (event.type === 'error') {
|
|
1528
|
+
return html `
|
|
1529
|
+
<div
|
|
1530
|
+
class="message incoming ${animatedClass}"
|
|
1531
|
+
style="background: #ff4444; color: white; animation-delay: ${animationDelay}"
|
|
1532
|
+
>
|
|
1533
|
+
⚠️ ${event.text || 'An error occurred'}
|
|
1534
|
+
</div>
|
|
1535
|
+
`;
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
// check if this is an event we should display
|
|
1539
|
+
const description = this.getEventDescription(event);
|
|
1540
|
+
if (description) {
|
|
1541
|
+
return html `
|
|
1542
|
+
<div
|
|
1543
|
+
class="event-info ${animatedClass}"
|
|
1544
|
+
style="animation-delay: ${animationDelay}"
|
|
1545
|
+
>
|
|
1546
|
+
${description}
|
|
1547
|
+
</div>
|
|
1548
|
+
`;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return html ``;
|
|
1552
|
+
});
|
|
1553
|
+
// render quick replies at the end if we have any from the most recent sprint
|
|
1554
|
+
const hasQuickReplies = this.currentQuickReplies.length > 0;
|
|
1555
|
+
const quickRepliesAnimationDelay = this.events.length >= this.previousEventCount
|
|
1556
|
+
? `${(this.events.length - this.previousEventCount) * 0.2}s`
|
|
1557
|
+
: '0s';
|
|
1558
|
+
return html `
|
|
1559
|
+
${eventTemplates}
|
|
1560
|
+
${hasQuickReplies
|
|
1561
|
+
? html `
|
|
1562
|
+
<div
|
|
1563
|
+
class="quick-replies animated"
|
|
1564
|
+
style="animation-delay: ${quickRepliesAnimationDelay}"
|
|
1565
|
+
>
|
|
1566
|
+
${this.currentQuickReplies.map((qr) => html `
|
|
1567
|
+
<button
|
|
1568
|
+
class="quick-reply-btn animated"
|
|
1569
|
+
style="animation-delay: ${quickRepliesAnimationDelay}"
|
|
1570
|
+
@click=${() => this.handleQuickReply(qr.text)}
|
|
1571
|
+
>
|
|
1572
|
+
${qr.text}
|
|
1573
|
+
</button>
|
|
1574
|
+
`)}
|
|
1575
|
+
</div>
|
|
1576
|
+
`
|
|
1577
|
+
: html ``}
|
|
1578
|
+
`;
|
|
1579
|
+
}
|
|
1580
|
+
render() {
|
|
1581
|
+
const config = this.sizeConfig;
|
|
1582
|
+
// set CSS custom properties dynamically based on size
|
|
1583
|
+
const styleVars = `
|
|
1584
|
+
--phone-width: ${config.phoneWidth}px;
|
|
1585
|
+
--phone-total-height: ${config.phoneTotalHeight}px;
|
|
1586
|
+
--context-width: ${config.contextWidth}px;
|
|
1587
|
+
--context-offset: ${config.contextOffset}px;
|
|
1588
|
+
--option-pane-width: ${config.optionPaneWidth}px;
|
|
1589
|
+
--option-pane-gap: ${config.optionPaneGap}px;
|
|
1590
|
+
--window-padding: ${config.windowPadding}px;
|
|
1591
|
+
--phone-screen-height: ${config.phoneScreenHeight}px;
|
|
1592
|
+
--context-height: ${config.contextHeight}px;
|
|
1593
|
+
--context-closed-left: ${this.contextClosedLeft}px;
|
|
1594
|
+
--cutout-height: ${config.cutoutHeight}px;
|
|
1595
|
+
--cutout-padding: ${config.cutoutPadding}px;
|
|
1596
|
+
--cutout-font-size: ${config.cutoutFontSize}px;
|
|
1597
|
+
--cutout-island-width: ${config.cutoutIslandWidth}px;
|
|
1598
|
+
--cutout-island-height: ${config.cutoutIslandHeight}px;
|
|
1599
|
+
--cutout-island-top: ${config.cutoutIslandTop}px;
|
|
1600
|
+
`;
|
|
1601
|
+
return html `
|
|
1602
|
+
<temba-floating-window
|
|
1603
|
+
id="phone-window"
|
|
1604
|
+
width="${this.windowWidth}"
|
|
1605
|
+
leftBoundaryMargin="${this.leftBoundaryMargin}"
|
|
1606
|
+
bottomBoundaryMargin="${config.windowPadding}"
|
|
1607
|
+
topBoundaryMargin="${config.windowPadding}"
|
|
1608
|
+
height="${config.phoneTotalHeight}"
|
|
1609
|
+
top="60"
|
|
1610
|
+
chromeless
|
|
1611
|
+
>
|
|
1612
|
+
<div class="phone-simulator" style="${styleVars}">
|
|
1613
|
+
<div
|
|
1614
|
+
class="context-explorer ${this.contextExplorerOpen ? 'open' : ''}"
|
|
1615
|
+
>
|
|
1616
|
+
<div class="context-explorer-scroll">
|
|
1617
|
+
${this.context
|
|
1618
|
+
? this.renderContextTree(this.context)
|
|
1619
|
+
: html `<div
|
|
1620
|
+
style="color: #9ca3af; padding: 8px; text-align: center;"
|
|
1621
|
+
>
|
|
1622
|
+
No context available
|
|
1623
|
+
</div>`}
|
|
1624
|
+
</div>
|
|
1625
|
+
<div class="context-gutter">
|
|
1626
|
+
<div
|
|
1627
|
+
class="context-gutter-btn ${this.showAllKeys ? '' : 'active'}"
|
|
1628
|
+
@click=${this.handleToggleShowAllKeys}
|
|
1629
|
+
title="${this.showAllKeys
|
|
1630
|
+
? 'Show keys with values only'
|
|
1631
|
+
: 'Show all keys'}"
|
|
1632
|
+
>
|
|
1633
|
+
<temba-icon
|
|
1634
|
+
name="${this.showAllKeys ? 'filter' : 'filter'}"
|
|
1635
|
+
size="1"
|
|
1636
|
+
></temba-icon>
|
|
1637
|
+
</div>
|
|
1638
|
+
<div class="context-gutter-spacer"></div>
|
|
1639
|
+
<div
|
|
1640
|
+
class="context-gutter-btn"
|
|
1641
|
+
@click=${this.handleToggleContextExplorer}
|
|
1642
|
+
title="Close"
|
|
1643
|
+
>
|
|
1644
|
+
<temba-icon name="x" size="1"></temba-icon>
|
|
1645
|
+
</div>
|
|
1646
|
+
</div>
|
|
1647
|
+
${this.copiedExpression
|
|
1648
|
+
? html `<div class="context-toast">
|
|
1649
|
+
Copied
|
|
1650
|
+
<span class="expression">${this.copiedExpression}</span>
|
|
1651
|
+
to the clipboard
|
|
1652
|
+
</div>`
|
|
1653
|
+
: this.toastMessage
|
|
1654
|
+
? html `<div class="context-toast">${this.toastMessage}</div>`
|
|
1655
|
+
: html ``}
|
|
1656
|
+
</div>
|
|
1657
|
+
|
|
1658
|
+
<div
|
|
1659
|
+
class="phone-frame"
|
|
1660
|
+
style="pointer-events: ${this.isVisible ? 'all' : 'none'}"
|
|
1661
|
+
>
|
|
1662
|
+
<div class="phone-top drag-handle">
|
|
1663
|
+
<div class="phone-notch">
|
|
1664
|
+
<div class="dynamic-island"></div>
|
|
1665
|
+
</div>
|
|
1666
|
+
</div>
|
|
1667
|
+
<div class="phone-screen">${this.renderMessages()}</div>
|
|
1668
|
+
<div class="message-input">
|
|
1669
|
+
<button
|
|
1670
|
+
class="attachment-button"
|
|
1671
|
+
@click=${this.handleToggleAttachmentMenu}
|
|
1672
|
+
?disabled=${this.sprinting}
|
|
1673
|
+
>
|
|
1674
|
+
<temba-icon name="plus" size="1.5"></temba-icon>
|
|
1675
|
+
</button>
|
|
1676
|
+
<input
|
|
1677
|
+
type="text"
|
|
1678
|
+
placeholder="Enter Message"
|
|
1679
|
+
.value=${this.inputValue}
|
|
1680
|
+
@input=${this.handleInput}
|
|
1681
|
+
@keyup=${this.handleKeyUp}
|
|
1682
|
+
?disabled=${this.sprinting}
|
|
1683
|
+
/>
|
|
1684
|
+
<div
|
|
1685
|
+
class="attachment-menu ${this.attachmentMenuOpen ? 'open' : ''}"
|
|
1686
|
+
>
|
|
1687
|
+
<div
|
|
1688
|
+
class="attachment-menu-item"
|
|
1689
|
+
@click=${() => this.handleSendAttachment('image')}
|
|
1690
|
+
>
|
|
1691
|
+
<temba-icon name="attachment_image" size="1.2"></temba-icon>
|
|
1692
|
+
<span>Image</span>
|
|
1693
|
+
</div>
|
|
1694
|
+
<div
|
|
1695
|
+
class="attachment-menu-item"
|
|
1696
|
+
@click=${() => this.handleSendAttachment('video')}
|
|
1697
|
+
>
|
|
1698
|
+
<temba-icon name="attachment_video" size="1.2"></temba-icon>
|
|
1699
|
+
<span>Video</span>
|
|
1700
|
+
</div>
|
|
1701
|
+
<div
|
|
1702
|
+
class="attachment-menu-item"
|
|
1703
|
+
@click=${() => this.handleSendAttachment('audio')}
|
|
1704
|
+
>
|
|
1705
|
+
<temba-icon name="attachment_audio" size="1.2"></temba-icon>
|
|
1706
|
+
<span>Audio</span>
|
|
1707
|
+
</div>
|
|
1708
|
+
<div
|
|
1709
|
+
class="attachment-menu-item"
|
|
1710
|
+
@click=${() => this.handleSendAttachment('location')}
|
|
1711
|
+
>
|
|
1712
|
+
<temba-icon
|
|
1713
|
+
name="attachment_location"
|
|
1714
|
+
size="1.2"
|
|
1715
|
+
></temba-icon>
|
|
1716
|
+
<span>Location</span>
|
|
1717
|
+
</div>
|
|
1718
|
+
</div>
|
|
1719
|
+
</div>
|
|
1720
|
+
</div>
|
|
1721
|
+
<div class="option-pane">
|
|
1722
|
+
<button class="option-btn" @click=${this.handleClose} title="Close">
|
|
1723
|
+
<temba-icon name="x" size="1.5"></temba-icon>
|
|
1724
|
+
</button>
|
|
1725
|
+
<button
|
|
1726
|
+
class="option-btn ${this.following ? 'active' : ''}"
|
|
1727
|
+
@click=${this.handleToggleFollow}
|
|
1728
|
+
title="${this.following ? 'Following' : 'Follow'}"
|
|
1729
|
+
>
|
|
1730
|
+
<temba-icon name="follow" size="1.5"></temba-icon>
|
|
1731
|
+
</button>
|
|
1732
|
+
|
|
1733
|
+
<button
|
|
1734
|
+
class="option-btn ${this.contextExplorerOpen ? 'active' : ''}"
|
|
1735
|
+
@click=${this.handleToggleContextExplorer}
|
|
1736
|
+
title="Context Explorer"
|
|
1737
|
+
>
|
|
1738
|
+
<temba-icon name="expressions" size="1.5"></temba-icon>
|
|
1739
|
+
</button>
|
|
1740
|
+
|
|
1741
|
+
<button
|
|
1742
|
+
class="option-btn"
|
|
1743
|
+
@click=${this.handleCycleSize}
|
|
1744
|
+
title="Size: ${this.size}"
|
|
1745
|
+
>
|
|
1746
|
+
${this.size === 'small'
|
|
1747
|
+
? 'S'
|
|
1748
|
+
: this.size === 'medium'
|
|
1749
|
+
? 'M'
|
|
1750
|
+
: 'L'}
|
|
1751
|
+
</button>
|
|
1752
|
+
|
|
1753
|
+
<button class="option-btn" @click=${this.handleReset} title="Reset">
|
|
1754
|
+
<temba-icon name="delete" size="1.5"></temba-icon>
|
|
1755
|
+
</button>
|
|
1756
|
+
</div>
|
|
1757
|
+
</div>
|
|
1758
|
+
</temba-floating-window>
|
|
1759
|
+
|
|
1760
|
+
<temba-floating-tab
|
|
1761
|
+
id="phone-tab"
|
|
1762
|
+
icon="simulator"
|
|
1763
|
+
label="Phone Simulator"
|
|
1764
|
+
color="#10b981"
|
|
1765
|
+
@temba-button-clicked=${this.handleShow}
|
|
1766
|
+
></temba-floating-tab>
|
|
1767
|
+
`;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
__decorate([
|
|
1771
|
+
property({ type: String })
|
|
1772
|
+
], Simulator.prototype, "flow", void 0);
|
|
1773
|
+
__decorate([
|
|
1774
|
+
property({ type: String })
|
|
1775
|
+
], Simulator.prototype, "endpoint", void 0);
|
|
1776
|
+
__decorate([
|
|
1777
|
+
property({ type: Number })
|
|
1778
|
+
], Simulator.prototype, "animationTime", void 0);
|
|
1779
|
+
__decorate([
|
|
1780
|
+
fromCookie('simulator-size', 'small')
|
|
1781
|
+
], Simulator.prototype, "size", void 0);
|
|
1782
|
+
__decorate([
|
|
1783
|
+
property({ type: Array })
|
|
1784
|
+
], Simulator.prototype, "events", void 0);
|
|
1785
|
+
__decorate([
|
|
1786
|
+
property({ type: Object })
|
|
1787
|
+
], Simulator.prototype, "session", void 0);
|
|
1788
|
+
__decorate([
|
|
1789
|
+
property({ type: Object })
|
|
1790
|
+
], Simulator.prototype, "context", void 0);
|
|
1791
|
+
__decorate([
|
|
1792
|
+
property({ type: Object })
|
|
1793
|
+
], Simulator.prototype, "contact", void 0);
|
|
1794
|
+
__decorate([
|
|
1795
|
+
property({ type: Boolean })
|
|
1796
|
+
], Simulator.prototype, "sprinting", void 0);
|
|
1797
|
+
__decorate([
|
|
1798
|
+
property({ type: String })
|
|
1799
|
+
], Simulator.prototype, "inputValue", void 0);
|
|
1800
|
+
__decorate([
|
|
1801
|
+
fromCookie('simulator-follow', true)
|
|
1802
|
+
], Simulator.prototype, "following", void 0);
|
|
1803
|
+
__decorate([
|
|
1804
|
+
fromCookie('simulator-context-open', false)
|
|
1805
|
+
], Simulator.prototype, "contextExplorerOpen", void 0);
|
|
1806
|
+
__decorate([
|
|
1807
|
+
property({ type: Object })
|
|
1808
|
+
], Simulator.prototype, "expandedPaths", void 0);
|
|
1809
|
+
__decorate([
|
|
1810
|
+
property({ type: String })
|
|
1811
|
+
], Simulator.prototype, "copiedExpression", void 0);
|
|
1812
|
+
__decorate([
|
|
1813
|
+
property({ type: String })
|
|
1814
|
+
], Simulator.prototype, "toastMessage", void 0);
|
|
1815
|
+
__decorate([
|
|
1816
|
+
property({ type: Boolean })
|
|
1817
|
+
], Simulator.prototype, "showAllKeys", void 0);
|
|
1818
|
+
__decorate([
|
|
1819
|
+
property({ type: Array })
|
|
1820
|
+
], Simulator.prototype, "currentQuickReplies", void 0);
|
|
1821
|
+
__decorate([
|
|
1822
|
+
property({ type: Boolean })
|
|
1823
|
+
], Simulator.prototype, "isVisible", void 0);
|
|
1824
|
+
__decorate([
|
|
1825
|
+
property({ type: Boolean })
|
|
1826
|
+
], Simulator.prototype, "attachmentMenuOpen", void 0);
|
|
1827
|
+
//# sourceMappingURL=Simulator.js.map
|