@nyaruka/temba-components 0.136.1 → 0.138.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/CHANGELOG.md +19 -0
- package/demo/components/webchat/example.html +2 -2
- package/dist/temba-components.js +692 -622
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +123 -44
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +2 -2
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/events/eventRenderers.js +442 -0
- package/out-tsc/src/events/eventRenderers.js.map +1 -0
- package/out-tsc/src/flow/CanvasNode.js +45 -24
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +308 -18
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +0 -1
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/Plumber.js +110 -64
- package/out-tsc/src/flow/Plumber.js.map +1 -1
- package/out-tsc/src/list/ShortcutList.js +1 -1
- package/out-tsc/src/list/ShortcutList.js.map +1 -1
- package/out-tsc/src/live/ContactChat.js +12 -321
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/simulator/Simulator.js +439 -575
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +12 -2
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-node.test.js +2 -1
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor-revisions.test.js +106 -0
- package/out-tsc/test/temba-flow-editor-revisions.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor.test.js +14 -10
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js +7 -1
- package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
- package/out-tsc/test/temba-flow-plumber.test.js +6 -0
- package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +51 -32
- package/out-tsc/test/temba-simulator.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/contacts/chat-failure.png +0 -0
- package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
- package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
- package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
- package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- 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/display/Chat.ts +123 -44
- package/src/display/FloatingTab.ts +2 -2
- package/src/events/eventRenderers.ts +527 -0
- package/src/flow/CanvasNode.ts +54 -29
- package/src/flow/Editor.ts +360 -19
- package/src/flow/NodeEditor.ts +0 -1
- package/src/flow/Plumber.ts +123 -69
- package/src/list/ShortcutList.ts +1 -1
- package/src/live/ContactChat.ts +17 -376
- package/src/simulator/Simulator.ts +498 -617
- package/src/store/AppState.ts +13 -2
- package/test/temba-flow-editor-node.test.ts +2 -1
- package/test/temba-flow-editor-revisions.test.ts +134 -0
- package/test/temba-flow-editor.test.ts +16 -10
- package/test/temba-flow-plumber-connections.test.ts +7 -1
- package/test/temba-flow-plumber.test.ts +6 -0
- package/test/temba-simulator.test.ts +64 -34
|
@@ -3,9 +3,11 @@ import { html } from 'lit-html';
|
|
|
3
3
|
import { RapidElement } from '../RapidElement';
|
|
4
4
|
import { css } from 'lit';
|
|
5
5
|
import { property } from 'lit/decorators.js';
|
|
6
|
-
import { postJSON, fromCookie } from '../utils';
|
|
6
|
+
import { postJSON, fromCookie, generateUUIDv7 } from '../utils';
|
|
7
7
|
import { getStore } from '../store/Store';
|
|
8
8
|
import { CustomEventType } from '../interfaces';
|
|
9
|
+
import { MessageType } from '../display/Chat';
|
|
10
|
+
import { Events, renderEvent } from '../events/eventRenderers';
|
|
9
11
|
// test attachment URLs
|
|
10
12
|
const TEST_IMAGES = [
|
|
11
13
|
'https://s3.amazonaws.com/floweditor-assets.temba.io/simulator/sim_image_a.jpg',
|
|
@@ -28,8 +30,7 @@ const TEST_LOCATIONS = [
|
|
|
28
30
|
const SIMULATOR_SIZES = {
|
|
29
31
|
small: {
|
|
30
32
|
phoneWidth: 270,
|
|
31
|
-
|
|
32
|
-
phoneTotalHeight: 576,
|
|
33
|
+
phoneTotalHeight: 530,
|
|
33
34
|
phoneScreenHeight: 376,
|
|
34
35
|
contextWidth: 336,
|
|
35
36
|
contextHeight: 416,
|
|
@@ -46,8 +47,7 @@ const SIMULATOR_SIZES = {
|
|
|
46
47
|
},
|
|
47
48
|
medium: {
|
|
48
49
|
phoneWidth: 300,
|
|
49
|
-
|
|
50
|
-
phoneTotalHeight: 720,
|
|
50
|
+
phoneTotalHeight: 600,
|
|
51
51
|
phoneScreenHeight: 470,
|
|
52
52
|
contextWidth: 420,
|
|
53
53
|
contextHeight: 520,
|
|
@@ -64,8 +64,7 @@ const SIMULATOR_SIZES = {
|
|
|
64
64
|
},
|
|
65
65
|
large: {
|
|
66
66
|
phoneWidth: 360,
|
|
67
|
-
|
|
68
|
-
phoneTotalHeight: 864,
|
|
67
|
+
phoneTotalHeight: 700,
|
|
69
68
|
phoneScreenHeight: 564,
|
|
70
69
|
contextWidth: 504,
|
|
71
70
|
contextHeight: 624,
|
|
@@ -89,6 +88,7 @@ export class Simulator extends RapidElement {
|
|
|
89
88
|
this.animationTime = 200;
|
|
90
89
|
this.events = [];
|
|
91
90
|
this.previousEventCount = 0;
|
|
91
|
+
this.chat = null;
|
|
92
92
|
this.session = null;
|
|
93
93
|
this.context = null;
|
|
94
94
|
this.contact = {
|
|
@@ -154,7 +154,6 @@ export class Simulator extends RapidElement {
|
|
|
154
154
|
backdrop-filter: blur(10px);
|
|
155
155
|
border-radius: 16px;
|
|
156
156
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
157
|
-
pointer-events: all;
|
|
158
157
|
}
|
|
159
158
|
.option-btn {
|
|
160
159
|
background: rgba(255, 255, 255, 0.1);
|
|
@@ -186,6 +185,7 @@ export class Simulator extends RapidElement {
|
|
|
186
185
|
|
|
187
186
|
.phone-frame {
|
|
188
187
|
width: var(--phone-width);
|
|
188
|
+
height: var(--phone-total-height);
|
|
189
189
|
border-radius: 40px;
|
|
190
190
|
border: 6px solid #1f2937;
|
|
191
191
|
box-shadow: 0 0px 30px rgba(0, 0, 0, 0.4);
|
|
@@ -290,12 +290,49 @@ export class Simulator extends RapidElement {
|
|
|
290
290
|
background: rgba(255, 255, 255, 0.5);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
/* Custom scrollbar for chat area to allow content to flow behind input */
|
|
294
|
+
.custom-scrollbar-container {
|
|
295
|
+
position: absolute;
|
|
296
|
+
top: 40px;
|
|
297
|
+
bottom: var(--bottom-input-height, 60px);
|
|
298
|
+
right: 4px;
|
|
299
|
+
width: 10px;
|
|
300
|
+
z-index: 20;
|
|
301
|
+
overflow-y: auto;
|
|
302
|
+
overflow-x: hidden;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.custom-scrollbar-container::-webkit-scrollbar {
|
|
306
|
+
width: 6px;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.custom-scrollbar-container::-webkit-scrollbar-track {
|
|
310
|
+
background: transparent;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.custom-scrollbar-container::-webkit-scrollbar-thumb {
|
|
314
|
+
background: rgba(0, 0, 0, 0.2);
|
|
315
|
+
border-radius: 3px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.custom-scrollbar-container::-webkit-scrollbar-thumb:hover {
|
|
319
|
+
background: rgba(0, 0, 0, 0.4);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.custom-scrollbar-content {
|
|
323
|
+
width: 100%;
|
|
324
|
+
}
|
|
325
|
+
|
|
293
326
|
.context-explorer.open {
|
|
294
327
|
left: var(--context-offset);
|
|
295
328
|
opacity: 1;
|
|
296
329
|
pointer-events: auto;
|
|
297
330
|
}
|
|
298
331
|
|
|
332
|
+
.context-explorer.hidden {
|
|
333
|
+
pointer-events: none !important;
|
|
334
|
+
}
|
|
335
|
+
|
|
299
336
|
.context-item {
|
|
300
337
|
display: flex;
|
|
301
338
|
align-items: flex-start;
|
|
@@ -462,170 +499,95 @@ export class Simulator extends RapidElement {
|
|
|
462
499
|
}
|
|
463
500
|
|
|
464
501
|
.phone-screen {
|
|
502
|
+
position: absolute;
|
|
503
|
+
top: 0;
|
|
504
|
+
left: 0;
|
|
505
|
+
right: 0;
|
|
506
|
+
bottom: 0;
|
|
465
507
|
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
508
|
display: flex;
|
|
472
509
|
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
510
|
}
|
|
480
511
|
|
|
481
|
-
|
|
482
|
-
|
|
512
|
+
temba-chat {
|
|
513
|
+
flex: 1;
|
|
514
|
+
display: flex;
|
|
515
|
+
flex-direction: column;
|
|
516
|
+
min-height: 0;
|
|
517
|
+
--color-chat-in: #e5e5ea;
|
|
518
|
+
--color-chat-out: #007aff;
|
|
519
|
+
--chat-top-padding: calc(var(--cutout-height));
|
|
520
|
+
--chat-bottom-padding: calc(var(--bottom-input-height, 80px) - 10px);
|
|
483
521
|
}
|
|
484
522
|
|
|
485
|
-
.
|
|
486
|
-
|
|
487
|
-
|
|
523
|
+
.bottom-input-container {
|
|
524
|
+
position: absolute;
|
|
525
|
+
bottom: 0px;
|
|
526
|
+
left: 0px;
|
|
527
|
+
right: 0px;
|
|
528
|
+
z-index: 10;
|
|
488
529
|
}
|
|
489
530
|
|
|
490
|
-
.
|
|
491
|
-
|
|
531
|
+
.bottom-input-container::before {
|
|
532
|
+
content: '';
|
|
533
|
+
position: absolute;
|
|
534
|
+
top: 0;
|
|
535
|
+
left: 0;
|
|
536
|
+
right: 0;
|
|
537
|
+
bottom: 0;
|
|
538
|
+
background: rgba(255, 255, 255, 0.45);
|
|
539
|
+
backdrop-filter: blur(10px);
|
|
540
|
+
-webkit-mask-image: linear-gradient(to bottom, transparent, black 20px);
|
|
541
|
+
mask-image: linear-gradient(to bottom, transparent, black 20px);
|
|
542
|
+
z-index: -1;
|
|
492
543
|
}
|
|
493
544
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
opacity: 1;
|
|
501
|
-
transform: scale(1.05);
|
|
502
|
-
}
|
|
503
|
-
100% {
|
|
504
|
-
opacity: 1;
|
|
505
|
-
transform: scale(1);
|
|
506
|
-
}
|
|
545
|
+
.quick-replies-container {
|
|
546
|
+
display: flex;
|
|
547
|
+
flex-wrap: wrap;
|
|
548
|
+
justify-content: center;
|
|
549
|
+
gap: 6px;
|
|
550
|
+
z-index: 9;
|
|
507
551
|
}
|
|
508
552
|
|
|
509
|
-
.
|
|
510
|
-
padding:
|
|
511
|
-
margin-bottom: 8px;
|
|
553
|
+
.quick-reply-btn {
|
|
554
|
+
padding: 4px 8px;
|
|
512
555
|
border-radius: 18px;
|
|
513
|
-
|
|
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;
|
|
556
|
+
border: 1px solid var(--color-primary, #007aff);
|
|
573
557
|
background: white;
|
|
574
|
-
|
|
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;
|
|
558
|
+
color: var(--color-primary, #007aff);
|
|
592
559
|
font-size: 11px;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
line-height: 1.3;
|
|
560
|
+
cursor: pointer;
|
|
561
|
+
transition: all 0.2s ease;
|
|
562
|
+
flex-shrink: 0;
|
|
597
563
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
564
|
+
|
|
565
|
+
.quick-reply-btn:hover:not(:disabled) {
|
|
566
|
+
background: var(--color-primary, #007aff);
|
|
567
|
+
color: white;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.quick-reply-btn:disabled {
|
|
571
|
+
opacity: 0.5;
|
|
572
|
+
cursor: not-allowed;
|
|
601
573
|
}
|
|
574
|
+
|
|
602
575
|
.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
576
|
padding: 8px 12px;
|
|
610
577
|
border-top: none;
|
|
611
578
|
display: flex;
|
|
612
579
|
align-items: center;
|
|
613
580
|
gap: 8px;
|
|
614
|
-
position: absolute;
|
|
615
|
-
bottom: 0px;
|
|
616
|
-
left: 0px;
|
|
617
|
-
right: 0px;
|
|
618
581
|
z-index: 10;
|
|
619
582
|
}
|
|
620
583
|
.message-input input {
|
|
621
584
|
flex: 1;
|
|
622
|
-
border: 1px solid #
|
|
585
|
+
border: 1px solid #c6c6c857;
|
|
623
586
|
border-radius: 20px;
|
|
624
587
|
padding: 8px 15px;
|
|
625
588
|
font-size: 15px;
|
|
626
589
|
margin-bottom: 5px;
|
|
627
|
-
|
|
628
|
-
border: none;
|
|
590
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
629
591
|
outline: none;
|
|
630
592
|
}
|
|
631
593
|
.message-input input::placeholder {
|
|
@@ -636,7 +598,7 @@ export class Simulator extends RapidElement {
|
|
|
636
598
|
height: 30px;
|
|
637
599
|
border-radius: 50%;
|
|
638
600
|
background: #fff;
|
|
639
|
-
border:
|
|
601
|
+
border: 1px solid #c6c6c857;
|
|
640
602
|
display: flex;
|
|
641
603
|
align-items: center;
|
|
642
604
|
justify-content: center;
|
|
@@ -644,6 +606,7 @@ export class Simulator extends RapidElement {
|
|
|
644
606
|
flex-shrink: 0;
|
|
645
607
|
margin-bottom: 5px;
|
|
646
608
|
transition: all var(--animation-time) ease;
|
|
609
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
647
610
|
color: #000;
|
|
648
611
|
}
|
|
649
612
|
.attachment-button:hover {
|
|
@@ -693,37 +656,6 @@ export class Simulator extends RapidElement {
|
|
|
693
656
|
.attachment-menu-item temba-icon {
|
|
694
657
|
color: #007aff;
|
|
695
658
|
}
|
|
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
659
|
`;
|
|
728
660
|
}
|
|
729
661
|
// method to reset attachment indices for testing
|
|
@@ -752,8 +684,95 @@ export class Simulator extends RapidElement {
|
|
|
752
684
|
const config = this.sizeConfig;
|
|
753
685
|
return config.contextWidth + config.contextOffset - config.phoneWidth;
|
|
754
686
|
}
|
|
687
|
+
connectedCallback() {
|
|
688
|
+
super.connectedCallback();
|
|
689
|
+
}
|
|
690
|
+
firstUpdated(changes) {
|
|
691
|
+
super.firstUpdated(changes);
|
|
692
|
+
this.chat = this.shadowRoot.querySelector('temba-chat');
|
|
693
|
+
// if we have events that were collected before chat was ready, add them now
|
|
694
|
+
if (this.chat && this.events.length > 0) {
|
|
695
|
+
this.chat.addMessages(this.events, null, true);
|
|
696
|
+
}
|
|
697
|
+
this.setupCustomScrollbar();
|
|
698
|
+
}
|
|
699
|
+
setupCustomScrollbar() {
|
|
700
|
+
var _a, _b, _c;
|
|
701
|
+
const chat = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('temba-chat');
|
|
702
|
+
const scrollContainer = (_b = this.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.custom-scrollbar-container');
|
|
703
|
+
const scrollContent = (_c = this.shadowRoot) === null || _c === void 0 ? void 0 : _c.querySelector('.custom-scrollbar-content');
|
|
704
|
+
if (!chat || !scrollContainer || !scrollContent)
|
|
705
|
+
return;
|
|
706
|
+
chat.updateComplete.then(() => {
|
|
707
|
+
var _a;
|
|
708
|
+
const chatScroll = (_a = chat.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.scroll');
|
|
709
|
+
if (!chatScroll)
|
|
710
|
+
return;
|
|
711
|
+
let ignoreScroll = false;
|
|
712
|
+
// Sync from chat to custom scrollbar
|
|
713
|
+
chatScroll.addEventListener('scroll', () => {
|
|
714
|
+
if (!ignoreScroll) {
|
|
715
|
+
ignoreScroll = true;
|
|
716
|
+
// Chat: 0 (bottom) ... -Max (top) (Negative scrolling)
|
|
717
|
+
// Custom: Max (bottom) ... 0 (top) (Positive scrolling)
|
|
718
|
+
const maxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
|
|
719
|
+
// Math.abs to handle negative scrollTop
|
|
720
|
+
const distanceFromBottom = Math.abs(chatScroll.scrollTop);
|
|
721
|
+
const newCustomScrollTop = maxScroll - distanceFromBottom;
|
|
722
|
+
scrollContainer.scrollTop = newCustomScrollTop;
|
|
723
|
+
requestAnimationFrame(() => (ignoreScroll = false));
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
// Sync from custom scrollbar to chat
|
|
727
|
+
scrollContainer.addEventListener('scroll', () => {
|
|
728
|
+
if (!ignoreScroll) {
|
|
729
|
+
ignoreScroll = true;
|
|
730
|
+
const maxScroll = scrollContainer.scrollHeight - scrollContainer.clientHeight;
|
|
731
|
+
const distanceFromBottom = maxScroll - scrollContainer.scrollTop;
|
|
732
|
+
// chat scrollTop should be -distanceFromBottom
|
|
733
|
+
chatScroll.scrollTop = -distanceFromBottom;
|
|
734
|
+
requestAnimationFrame(() => (ignoreScroll = false));
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
// Sync height
|
|
738
|
+
const syncHeight = () => {
|
|
739
|
+
const chatMaxScroll = chatScroll.scrollHeight - chatScroll.clientHeight;
|
|
740
|
+
const customClientHeight = scrollContainer.clientHeight;
|
|
741
|
+
// ensure minimum height
|
|
742
|
+
if (chatMaxScroll <= 0) {
|
|
743
|
+
scrollContent.style.height = '100%';
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const newHeight = chatMaxScroll + customClientHeight;
|
|
747
|
+
scrollContent.style.height = `${newHeight}px`;
|
|
748
|
+
// If we were effectively at the bottom, stay at the bottom
|
|
749
|
+
// This is a heuristic, assuming if we're close enough we're "at bottom"
|
|
750
|
+
// But the Chat component handles scrollToBottom on new messages, which fires scroll event,
|
|
751
|
+
// which updates us. So we might not need to force it here unless resize happens without message.
|
|
752
|
+
if (Math.abs(chatScroll.scrollTop) < 5) {
|
|
753
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
// Observe changes
|
|
757
|
+
const observer = new MutationObserver(syncHeight);
|
|
758
|
+
observer.observe(chatScroll, {
|
|
759
|
+
childList: true,
|
|
760
|
+
subtree: true,
|
|
761
|
+
attributes: true
|
|
762
|
+
});
|
|
763
|
+
const resizeObserver = new ResizeObserver(syncHeight);
|
|
764
|
+
resizeObserver.observe(chatScroll);
|
|
765
|
+
// Initial sync
|
|
766
|
+
syncHeight();
|
|
767
|
+
});
|
|
768
|
+
}
|
|
755
769
|
updated(changes) {
|
|
756
770
|
super.updated(changes);
|
|
771
|
+
if (changes.has('currentQuickReplies') ||
|
|
772
|
+
changes.has('keyboardVisible') ||
|
|
773
|
+
changes.has('attachmentMenuOpen')) {
|
|
774
|
+
this.updateBottomInputHeight();
|
|
775
|
+
}
|
|
757
776
|
if (changes.has('flow') && this.flow) {
|
|
758
777
|
this.endpoint = `/flow/simulate/${this.flow}/`;
|
|
759
778
|
}
|
|
@@ -834,8 +853,12 @@ export class Simulator extends RapidElement {
|
|
|
834
853
|
phoneWindow.show();
|
|
835
854
|
this.isVisible = true;
|
|
836
855
|
getStore().getState().setSimulatorActive(true);
|
|
856
|
+
// ensure chat component is available
|
|
857
|
+
if (!this.chat) {
|
|
858
|
+
this.chat = this.shadowRoot.querySelector('temba-chat');
|
|
859
|
+
}
|
|
837
860
|
// start the simulation if we haven't already
|
|
838
|
-
if (this.
|
|
861
|
+
if (!this.session) {
|
|
839
862
|
this.startFlow();
|
|
840
863
|
}
|
|
841
864
|
}
|
|
@@ -858,20 +881,37 @@ export class Simulator extends RapidElement {
|
|
|
858
881
|
}
|
|
859
882
|
catch (error) {
|
|
860
883
|
console.error('Failed to start simulation:', error);
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
884
|
+
const errorEvent = {
|
|
885
|
+
uuid: generateUUIDv7(),
|
|
886
|
+
type: 'error',
|
|
887
|
+
created_on: new Date(now),
|
|
888
|
+
_rendered: {
|
|
889
|
+
html: html `<p>Failed to start simulation</p>`,
|
|
890
|
+
type: MessageType.Error
|
|
867
891
|
}
|
|
868
|
-
|
|
892
|
+
};
|
|
893
|
+
if (this.chat) {
|
|
894
|
+
this.chat.addMessages([errorEvent], null, true);
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
this.events = [...this.events, errorEvent];
|
|
898
|
+
}
|
|
869
899
|
}
|
|
870
900
|
}
|
|
871
901
|
updateRunContext(runContext, msgInEvt) {
|
|
872
902
|
var _a;
|
|
903
|
+
const newEvents = [];
|
|
904
|
+
// add the user's message if provided
|
|
873
905
|
if (msgInEvt) {
|
|
874
|
-
|
|
906
|
+
// ensure it has a UUID
|
|
907
|
+
if (!msgInEvt.uuid) {
|
|
908
|
+
msgInEvt.uuid = generateUUIDv7();
|
|
909
|
+
}
|
|
910
|
+
// ensure created_on is a Date object
|
|
911
|
+
if (typeof msgInEvt.created_on === 'string') {
|
|
912
|
+
msgInEvt.created_on = new Date(msgInEvt.created_on);
|
|
913
|
+
}
|
|
914
|
+
newEvents.push(msgInEvt);
|
|
875
915
|
}
|
|
876
916
|
if (runContext.session) {
|
|
877
917
|
this.session = runContext.session;
|
|
@@ -884,18 +924,60 @@ export class Simulator extends RapidElement {
|
|
|
884
924
|
if (runContext.context) {
|
|
885
925
|
this.context = runContext.context;
|
|
886
926
|
}
|
|
927
|
+
// extract quick replies from the most recent sprint
|
|
928
|
+
this.currentQuickReplies = [];
|
|
887
929
|
if (runContext.events && runContext.events.length > 0) {
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
930
|
+
for (const rawEvent of runContext.events) {
|
|
931
|
+
// skip msg_received events from the server since we already added the user's message
|
|
932
|
+
if (rawEvent.type === 'msg_received') {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
// skip msg_created events without a proper msg property
|
|
936
|
+
if (rawEvent.type === 'msg_created' && !rawEvent.msg) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
// convert to ContactEvent
|
|
940
|
+
const event = {
|
|
941
|
+
...rawEvent,
|
|
942
|
+
uuid: rawEvent.uuid || generateUUIDv7(),
|
|
943
|
+
created_on: typeof rawEvent.created_on === 'string'
|
|
944
|
+
? new Date(rawEvent.created_on)
|
|
945
|
+
: rawEvent.created_on
|
|
946
|
+
};
|
|
947
|
+
// pre-render non-message events
|
|
948
|
+
this.prerenderEvent(event);
|
|
949
|
+
// extract quick replies from msg_created events
|
|
892
950
|
if (event.type === 'msg_created' && ((_a = event.msg) === null || _a === void 0 ? void 0 : _a.quick_replies)) {
|
|
893
951
|
this.currentQuickReplies = event.msg.quick_replies;
|
|
894
952
|
}
|
|
953
|
+
const isMessage = event.type === 'msg_created';
|
|
954
|
+
const msg = event.msg;
|
|
955
|
+
// Check if the event should be displayed.
|
|
956
|
+
// 1. If it's a message, it must have text or attachments
|
|
957
|
+
if (isMessage) {
|
|
958
|
+
const hasText = msg.text && msg.text.trim().length > 0;
|
|
959
|
+
const hasAttachments = msg.attachments && msg.attachments.length > 0;
|
|
960
|
+
if (!hasText && !hasAttachments) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// 2. If it's not a message, it must have been rendered by prerenderEvent
|
|
965
|
+
else if (!event._rendered) {
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
newEvents.push(event);
|
|
895
969
|
}
|
|
896
970
|
}
|
|
971
|
+
// add all new events to chat component if it exists
|
|
972
|
+
if (this.chat) {
|
|
973
|
+
this.chat.addMessages(newEvents, null, true);
|
|
974
|
+
}
|
|
975
|
+
else {
|
|
976
|
+
// fallback: store events and add them once chat is ready
|
|
977
|
+
this.events = [...this.events, ...newEvents];
|
|
978
|
+
}
|
|
897
979
|
this.sprinting = false;
|
|
898
|
-
this.requestUpdate();
|
|
980
|
+
this.requestUpdate(); // trigger re-render for quick replies
|
|
899
981
|
this.scrollToBottom();
|
|
900
982
|
this.updateActivity();
|
|
901
983
|
}
|
|
@@ -960,6 +1042,18 @@ export class Simulator extends RapidElement {
|
|
|
960
1042
|
}
|
|
961
1043
|
}
|
|
962
1044
|
scrollToBottom() {
|
|
1045
|
+
if (this.chat) {
|
|
1046
|
+
// chat component handles scrolling, but we still need to focus input
|
|
1047
|
+
this.chat.scrollToBottom();
|
|
1048
|
+
setTimeout(() => {
|
|
1049
|
+
var _a;
|
|
1050
|
+
const input = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.message-input input');
|
|
1051
|
+
if (input) {
|
|
1052
|
+
input.focus();
|
|
1053
|
+
}
|
|
1054
|
+
}, 50);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
963
1057
|
// wait for render, then scroll to bottom
|
|
964
1058
|
setTimeout(() => {
|
|
965
1059
|
var _a, _b;
|
|
@@ -976,9 +1070,39 @@ export class Simulator extends RapidElement {
|
|
|
976
1070
|
}
|
|
977
1071
|
}, 50);
|
|
978
1072
|
}
|
|
1073
|
+
prerenderEvent(event) {
|
|
1074
|
+
// skip if already rendered or is a message event
|
|
1075
|
+
if (event._rendered ||
|
|
1076
|
+
event.type === Events.MSG_CREATED ||
|
|
1077
|
+
event.type === Events.MSG_RECEIVED) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
// handle simulator-specific events (errors, warnings, failures)
|
|
1081
|
+
if (event.type === 'error' || event.type === 'failure') {
|
|
1082
|
+
event._rendered = {
|
|
1083
|
+
html: renderEvent(event, true),
|
|
1084
|
+
type: MessageType.Error
|
|
1085
|
+
};
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
if (event.type === 'warning') {
|
|
1089
|
+
event._rendered = {
|
|
1090
|
+
html: renderEvent(event, true),
|
|
1091
|
+
type: MessageType.Note
|
|
1092
|
+
};
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
// try to render as a standard event
|
|
1096
|
+
const rendered = renderEvent(event, true);
|
|
1097
|
+
if (rendered) {
|
|
1098
|
+
event._rendered = {
|
|
1099
|
+
html: rendered,
|
|
1100
|
+
type: MessageType.Inline
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
979
1104
|
handleClose() {
|
|
980
1105
|
const phoneWindow = this.shadowRoot.getElementById('phone-window');
|
|
981
|
-
// phoneWindow.hide();
|
|
982
1106
|
phoneWindow.handleClose();
|
|
983
1107
|
this.isVisible = false;
|
|
984
1108
|
getStore().getState().setSimulatorActive(false);
|
|
@@ -992,6 +1116,10 @@ export class Simulator extends RapidElement {
|
|
|
992
1116
|
this.sprinting = false;
|
|
993
1117
|
this.previousEventCount = 0;
|
|
994
1118
|
this.currentQuickReplies = [];
|
|
1119
|
+
// reset chat component
|
|
1120
|
+
if (this.chat) {
|
|
1121
|
+
this.chat.reset();
|
|
1122
|
+
}
|
|
995
1123
|
// Clear simulator activity data
|
|
996
1124
|
getStore().getState().updateSimulatorActivity({
|
|
997
1125
|
segments: {},
|
|
@@ -1179,19 +1307,34 @@ export class Simulator extends RapidElement {
|
|
|
1179
1307
|
this.currentQuickReplies = [];
|
|
1180
1308
|
this.attachmentMenuOpen = false;
|
|
1181
1309
|
const now = new Date().toISOString();
|
|
1182
|
-
|
|
1183
|
-
|
|
1310
|
+
// create the event for the API (with ISO string date)
|
|
1311
|
+
const msgInEvtForAPI = {
|
|
1312
|
+
uuid: generateUUIDv7(),
|
|
1184
1313
|
type: 'msg_received',
|
|
1185
1314
|
created_on: now,
|
|
1186
1315
|
msg: {
|
|
1187
|
-
uuid:
|
|
1316
|
+
uuid: generateUUIDv7(),
|
|
1188
1317
|
text: text || '',
|
|
1189
1318
|
urn: this.contact.urns[0],
|
|
1190
|
-
|
|
1319
|
+
direction: 'in',
|
|
1320
|
+
type: 'text',
|
|
1321
|
+
attachments: attachment ? [attachment] : [],
|
|
1322
|
+
quick_replies: [],
|
|
1323
|
+
channel: { uuid: generateUUIDv7(), name: 'Simulator' }
|
|
1191
1324
|
}
|
|
1192
1325
|
};
|
|
1193
|
-
//
|
|
1194
|
-
|
|
1326
|
+
// create the ContactEvent for display (with Date object)
|
|
1327
|
+
const msgInEvt = {
|
|
1328
|
+
...msgInEvtForAPI,
|
|
1329
|
+
created_on: new Date(now)
|
|
1330
|
+
};
|
|
1331
|
+
// show user's message immediately via chat component
|
|
1332
|
+
if (this.chat) {
|
|
1333
|
+
this.chat.addMessages([msgInEvt], null, true);
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
this.events = [...this.events, msgInEvt];
|
|
1337
|
+
}
|
|
1195
1338
|
this.requestUpdate();
|
|
1196
1339
|
this.scrollToBottom();
|
|
1197
1340
|
const body = {
|
|
@@ -1199,7 +1342,7 @@ export class Simulator extends RapidElement {
|
|
|
1199
1342
|
contact: this.contact,
|
|
1200
1343
|
resume: {
|
|
1201
1344
|
type: 'msg',
|
|
1202
|
-
event:
|
|
1345
|
+
event: msgInEvtForAPI,
|
|
1203
1346
|
resumed_on: now
|
|
1204
1347
|
}
|
|
1205
1348
|
};
|
|
@@ -1212,14 +1355,21 @@ export class Simulator extends RapidElement {
|
|
|
1212
1355
|
}
|
|
1213
1356
|
catch (error) {
|
|
1214
1357
|
console.error('Failed to resume simulation:', error);
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1358
|
+
const errorEvent = {
|
|
1359
|
+
uuid: generateUUIDv7(),
|
|
1360
|
+
type: 'error',
|
|
1361
|
+
created_on: new Date(now),
|
|
1362
|
+
_rendered: {
|
|
1363
|
+
html: html `<p>Failed to send message</p>`,
|
|
1364
|
+
type: MessageType.Error
|
|
1221
1365
|
}
|
|
1222
|
-
|
|
1366
|
+
};
|
|
1367
|
+
if (this.chat) {
|
|
1368
|
+
this.chat.addMessages([errorEvent], null, true);
|
|
1369
|
+
}
|
|
1370
|
+
else {
|
|
1371
|
+
this.events = [...this.events, errorEvent];
|
|
1372
|
+
}
|
|
1223
1373
|
this.sprinting = false;
|
|
1224
1374
|
}
|
|
1225
1375
|
}
|
|
@@ -1236,9 +1386,9 @@ export class Simulator extends RapidElement {
|
|
|
1236
1386
|
const input = evt.target;
|
|
1237
1387
|
this.inputValue = input.value;
|
|
1238
1388
|
}
|
|
1239
|
-
|
|
1240
|
-
if (!this.sprinting) {
|
|
1241
|
-
this.resume(
|
|
1389
|
+
handleQuickReplyClick(text) {
|
|
1390
|
+
if (!this.sprinting && text) {
|
|
1391
|
+
this.resume(text);
|
|
1242
1392
|
}
|
|
1243
1393
|
}
|
|
1244
1394
|
handleToggleAttachmentMenu() {
|
|
@@ -1285,330 +1435,15 @@ export class Simulator extends RapidElement {
|
|
|
1285
1435
|
this.resume('', attachment);
|
|
1286
1436
|
}
|
|
1287
1437
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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'
|
|
1438
|
+
updateBottomInputHeight() {
|
|
1439
|
+
requestAnimationFrame(() => {
|
|
1440
|
+
var _a;
|
|
1441
|
+
const bottomContainer = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.bottom-input-container');
|
|
1442
|
+
if (bottomContainer) {
|
|
1443
|
+
const height = bottomContainer.offsetHeight;
|
|
1444
|
+
this.style.setProperty('--bottom-input-height', `${height}px`);
|
|
1429
1445
|
}
|
|
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
1446
|
});
|
|
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
1447
|
}
|
|
1613
1448
|
render() {
|
|
1614
1449
|
const config = this.sizeConfig;
|
|
@@ -1630,9 +1465,11 @@ export class Simulator extends RapidElement {
|
|
|
1630
1465
|
--cutout-island-width: ${config.cutoutIslandWidth}px;
|
|
1631
1466
|
--cutout-island-height: ${config.cutoutIslandHeight}px;
|
|
1632
1467
|
--cutout-island-top: ${config.cutoutIslandTop}px;
|
|
1468
|
+
--animation-time: ${this.animationTime}ms;
|
|
1633
1469
|
`;
|
|
1634
1470
|
return html `
|
|
1635
1471
|
<temba-floating-window
|
|
1472
|
+
style="--transition-duration: ${this.animationTime}ms"
|
|
1636
1473
|
id="phone-window"
|
|
1637
1474
|
width="${this.windowWidth}"
|
|
1638
1475
|
leftBoundaryMargin="${this.leftBoundaryMargin}"
|
|
@@ -1644,7 +1481,9 @@ export class Simulator extends RapidElement {
|
|
|
1644
1481
|
>
|
|
1645
1482
|
<div class="phone-simulator" style="${styleVars}">
|
|
1646
1483
|
<div
|
|
1647
|
-
class="context-explorer ${this.contextExplorerOpen
|
|
1484
|
+
class="context-explorer ${this.contextExplorerOpen
|
|
1485
|
+
? 'open'
|
|
1486
|
+
: ''} ${this.isVisible ? 'visible' : 'hidden'}"
|
|
1648
1487
|
>
|
|
1649
1488
|
<div class="context-explorer-scroll">
|
|
1650
1489
|
${this.context
|
|
@@ -1697,61 +1536,86 @@ export class Simulator extends RapidElement {
|
|
|
1697
1536
|
<div class="dynamic-island"></div>
|
|
1698
1537
|
</div>
|
|
1699
1538
|
</div>
|
|
1700
|
-
<
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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')}
|
|
1539
|
+
<temba-chat class="phone-screen" .showTimestamps=${false}>
|
|
1540
|
+
</temba-chat>
|
|
1541
|
+
<div class="custom-scrollbar-container">
|
|
1542
|
+
<div class="custom-scrollbar-content"></div>
|
|
1543
|
+
</div>
|
|
1544
|
+
|
|
1545
|
+
<div class="bottom-input-container">
|
|
1546
|
+
${this.currentQuickReplies.length > 0
|
|
1547
|
+
? html `<div class="quick-replies-container">
|
|
1548
|
+
${this.currentQuickReplies.map((qr) => html `
|
|
1549
|
+
<button
|
|
1550
|
+
class="quick-reply-btn"
|
|
1551
|
+
@click=${() => this.handleQuickReplyClick(qr.text)}
|
|
1552
|
+
?disabled=${this.sprinting}
|
|
1553
|
+
>
|
|
1554
|
+
${qr.text}
|
|
1555
|
+
</button>
|
|
1556
|
+
`)}
|
|
1557
|
+
</div>`
|
|
1558
|
+
: null}
|
|
1559
|
+
<div class="message-input">
|
|
1560
|
+
<button
|
|
1561
|
+
class="attachment-button"
|
|
1562
|
+
@click=${this.handleToggleAttachmentMenu}
|
|
1563
|
+
?disabled=${this.sprinting}
|
|
1737
1564
|
>
|
|
1738
|
-
<temba-icon name="
|
|
1739
|
-
|
|
1740
|
-
|
|
1565
|
+
<temba-icon name="plus" size="1.5"></temba-icon>
|
|
1566
|
+
</button>
|
|
1567
|
+
<input
|
|
1568
|
+
type="text"
|
|
1569
|
+
placeholder="Enter Message"
|
|
1570
|
+
.value=${this.inputValue}
|
|
1571
|
+
@input=${this.handleInput}
|
|
1572
|
+
@keyup=${this.handleKeyUp}
|
|
1573
|
+
?disabled=${this.sprinting}
|
|
1574
|
+
/>
|
|
1741
1575
|
<div
|
|
1742
|
-
class="attachment-menu
|
|
1743
|
-
|
|
1576
|
+
class="attachment-menu ${this.attachmentMenuOpen
|
|
1577
|
+
? 'open'
|
|
1578
|
+
: ''}"
|
|
1744
1579
|
>
|
|
1745
|
-
<
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1580
|
+
<div
|
|
1581
|
+
class="attachment-menu-item"
|
|
1582
|
+
@click=${() => this.handleSendAttachment('image')}
|
|
1583
|
+
>
|
|
1584
|
+
<temba-icon name="attachment_image" size="1.2"></temba-icon>
|
|
1585
|
+
<span>Image</span>
|
|
1586
|
+
</div>
|
|
1587
|
+
<div
|
|
1588
|
+
class="attachment-menu-item"
|
|
1589
|
+
@click=${() => this.handleSendAttachment('video')}
|
|
1590
|
+
>
|
|
1591
|
+
<temba-icon name="attachment_video" size="1.2"></temba-icon>
|
|
1592
|
+
<span>Video</span>
|
|
1593
|
+
</div>
|
|
1594
|
+
<div
|
|
1595
|
+
class="attachment-menu-item"
|
|
1596
|
+
@click=${() => this.handleSendAttachment('audio')}
|
|
1597
|
+
>
|
|
1598
|
+
<temba-icon name="attachment_audio" size="1.2"></temba-icon>
|
|
1599
|
+
<span>Audio</span>
|
|
1600
|
+
</div>
|
|
1601
|
+
<div
|
|
1602
|
+
class="attachment-menu-item"
|
|
1603
|
+
@click=${() => this.handleSendAttachment('location')}
|
|
1604
|
+
>
|
|
1605
|
+
<temba-icon
|
|
1606
|
+
name="attachment_location"
|
|
1607
|
+
size="1.2"
|
|
1608
|
+
></temba-icon>
|
|
1609
|
+
<span>Location</span>
|
|
1610
|
+
</div>
|
|
1750
1611
|
</div>
|
|
1751
1612
|
</div>
|
|
1752
1613
|
</div>
|
|
1753
1614
|
</div>
|
|
1754
|
-
<div
|
|
1615
|
+
<div
|
|
1616
|
+
class="option-pane"
|
|
1617
|
+
style="pointer-events:${this.isVisible ? 'all' : 'none'}"
|
|
1618
|
+
>
|
|
1755
1619
|
<button class="option-btn" @click=${this.handleClose} title="Close">
|
|
1756
1620
|
<temba-icon name="x" size="1.5"></temba-icon>
|
|
1757
1621
|
</button>
|