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