@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/demo/components/webchat/example.html +2 -2
  3. package/dist/temba-components.js +692 -622
  4. package/dist/temba-components.js.map +1 -1
  5. package/out-tsc/src/display/Chat.js +123 -44
  6. package/out-tsc/src/display/Chat.js.map +1 -1
  7. package/out-tsc/src/display/FloatingTab.js +2 -2
  8. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  9. package/out-tsc/src/events/eventRenderers.js +442 -0
  10. package/out-tsc/src/events/eventRenderers.js.map +1 -0
  11. package/out-tsc/src/flow/CanvasNode.js +45 -24
  12. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  13. package/out-tsc/src/flow/Editor.js +308 -18
  14. package/out-tsc/src/flow/Editor.js.map +1 -1
  15. package/out-tsc/src/flow/NodeEditor.js +0 -1
  16. package/out-tsc/src/flow/NodeEditor.js.map +1 -1
  17. package/out-tsc/src/flow/Plumber.js +110 -64
  18. package/out-tsc/src/flow/Plumber.js.map +1 -1
  19. package/out-tsc/src/list/ShortcutList.js +1 -1
  20. package/out-tsc/src/list/ShortcutList.js.map +1 -1
  21. package/out-tsc/src/live/ContactChat.js +12 -321
  22. package/out-tsc/src/live/ContactChat.js.map +1 -1
  23. package/out-tsc/src/simulator/Simulator.js +439 -575
  24. package/out-tsc/src/simulator/Simulator.js.map +1 -1
  25. package/out-tsc/src/store/AppState.js +12 -2
  26. package/out-tsc/src/store/AppState.js.map +1 -1
  27. package/out-tsc/test/temba-flow-editor-node.test.js +2 -1
  28. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  29. package/out-tsc/test/temba-flow-editor-revisions.test.js +106 -0
  30. package/out-tsc/test/temba-flow-editor-revisions.test.js.map +1 -0
  31. package/out-tsc/test/temba-flow-editor.test.js +14 -10
  32. package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
  33. package/out-tsc/test/temba-flow-plumber-connections.test.js +7 -1
  34. package/out-tsc/test/temba-flow-plumber-connections.test.js.map +1 -1
  35. package/out-tsc/test/temba-flow-plumber.test.js +6 -0
  36. package/out-tsc/test/temba-flow-plumber.test.js.map +1 -1
  37. package/out-tsc/test/temba-simulator.test.js +51 -32
  38. package/out-tsc/test/temba-simulator.test.js.map +1 -1
  39. package/package.json +1 -1
  40. package/screenshots/truth/contacts/chat-failure.png +0 -0
  41. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  42. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  43. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  44. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  45. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  46. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  47. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  48. package/screenshots/truth/simulator/after-message-sent.png +0 -0
  49. package/screenshots/truth/simulator/after-reset.png +0 -0
  50. package/screenshots/truth/simulator/attachment-menu.png +0 -0
  51. package/screenshots/truth/simulator/context-expanded.png +0 -0
  52. package/screenshots/truth/simulator/context-explorer-open.png +0 -0
  53. package/screenshots/truth/simulator/event-info.png +0 -0
  54. package/screenshots/truth/simulator/image-attachment.png +0 -0
  55. package/screenshots/truth/simulator/open-initial.png +0 -0
  56. package/screenshots/truth/simulator/quick-replies.png +0 -0
  57. package/src/display/Chat.ts +123 -44
  58. package/src/display/FloatingTab.ts +2 -2
  59. package/src/events/eventRenderers.ts +527 -0
  60. package/src/flow/CanvasNode.ts +54 -29
  61. package/src/flow/Editor.ts +360 -19
  62. package/src/flow/NodeEditor.ts +0 -1
  63. package/src/flow/Plumber.ts +123 -69
  64. package/src/list/ShortcutList.ts +1 -1
  65. package/src/live/ContactChat.ts +17 -376
  66. package/src/simulator/Simulator.ts +498 -617
  67. package/src/store/AppState.ts +13 -2
  68. package/test/temba-flow-editor-node.test.ts +2 -1
  69. package/test/temba-flow-editor-revisions.test.ts +134 -0
  70. package/test/temba-flow-editor.test.ts +16 -10
  71. package/test/temba-flow-plumber-connections.test.ts +7 -1
  72. package/test/temba-flow-plumber.test.ts +6 -0
  73. 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
- phoneHeight: 576,
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
- phoneHeight: 720,
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
- phoneHeight: 864,
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
- .phone-screen::-webkit-scrollbar-track {
482
- background: transparent;
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
- .phone-screen::-webkit-scrollbar-thumb {
486
- background: rgba(0, 0, 0, 0.2);
487
- border-radius: 4px;
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
- .phone-screen::-webkit-scrollbar-thumb:hover {
491
- background: rgba(0, 0, 0, 0.3);
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
- @keyframes messageAppear {
495
- 0% {
496
- opacity: 0;
497
- transform: scale(0.8);
498
- }
499
- 70% {
500
- opacity: 1;
501
- transform: scale(1.05);
502
- }
503
- 100% {
504
- opacity: 1;
505
- transform: scale(1);
506
- }
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
- .message {
510
- padding: 10px 14px;
511
- margin-bottom: 8px;
553
+ .quick-reply-btn {
554
+ padding: 4px 8px;
512
555
  border-radius: 18px;
513
- max-width: 70%;
514
- font-size: 13px;
515
- line-height: 1.2;
516
- }
517
- .message.animated {
518
- animation: messageAppear var(--animation-time) ease-out forwards;
519
- opacity: 0;
520
- }
521
- .message.incoming {
522
- background: #e5e5ea;
523
- color: #000;
524
- margin-right: auto;
525
- border-bottom-left-radius: 4px;
526
- }
527
- .message.outgoing {
528
- background: #007aff;
529
- color: white;
530
- margin-left: auto;
531
- text-align: left;
532
- border-bottom-right-radius: 4px;
533
- }
534
- .attachment-wrapper {
535
- max-width: 70%;
536
- margin-bottom: 8px;
537
- display: flex;
538
- flex-direction: column;
539
- gap: 4px;
540
- }
541
- .attachment-wrapper.incoming {
542
- margin-right: auto;
543
- align-items: flex-start;
544
- }
545
- .attachment-wrapper.outgoing {
546
- margin-left: auto;
547
- align-items: flex-end;
548
- }
549
- .attachment-wrapper.animated {
550
- animation: messageAppear var(--animation-time) ease-out forwards;
551
- opacity: 0;
552
- }
553
- .attachment {
554
- border-radius: 12px;
555
- overflow: hidden;
556
- max-width: 100%;
557
- }
558
- .attachment img {
559
- max-width: 100%;
560
- display: block;
561
- border-radius: 12px;
562
- }
563
- .attachment video {
564
- max-width: 100%;
565
- display: block;
566
- border-radius: 12px;
567
- }
568
- .attachment-audio {
569
- display: flex;
570
- align-items: center;
571
- gap: 8px;
572
- padding: 6px;
556
+ border: 1px solid var(--color-primary, #007aff);
573
557
  background: white;
574
- border: 1px solid #e5e5ea;
575
- border-radius: 12px;
576
- min-width: 160px;
577
- }
578
- .attachment-wrapper.outgoing .attachment-audio {
579
- background: white;
580
- border: none;
581
- }
582
- .attachment-audio audio {
583
- flex: 1;
584
- max-height: 30px;
585
- }
586
- .attachment-location {
587
- border-radius: 12px;
588
- overflow: hidden;
589
- }
590
- .event-info {
591
- text-align: center;
558
+ color: var(--color-primary, #007aff);
592
559
  font-size: 11px;
593
- color: #8e8e93;
594
- margin: 4px 0;
595
- padding: 0 10px;
596
- line-height: 1.3;
560
+ cursor: pointer;
561
+ transition: all 0.2s ease;
562
+ flex-shrink: 0;
597
563
  }
598
- .event-info.animated {
599
- animation: messageAppear var(--animation-time) ease-out forwards;
600
- opacity: 0;
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 #c6c6c8;
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
- background: white;
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: none;
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.events.length === 0) {
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
- this.events = [
862
- ...this.events,
863
- {
864
- type: 'error',
865
- created_on: now,
866
- text: 'Failed to start simulation'
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
- this.events = [...this.events, msgInEvt];
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
- this.events = [...this.events, ...runContext.events];
889
- // extract quick replies from the most recent sprint
890
- this.currentQuickReplies = [];
891
- for (const event of runContext.events) {
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
- const msgInEvt = {
1183
- uuid: crypto.randomUUID(),
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: crypto.randomUUID(),
1316
+ uuid: generateUUIDv7(),
1188
1317
  text: text || '',
1189
1318
  urn: this.contact.urns[0],
1190
- attachments: attachment ? [attachment] : []
1319
+ direction: 'in',
1320
+ type: 'text',
1321
+ attachments: attachment ? [attachment] : [],
1322
+ quick_replies: [],
1323
+ channel: { uuid: generateUUIDv7(), name: 'Simulator' }
1191
1324
  }
1192
1325
  };
1193
- // show user's message immediately
1194
- this.events = [...this.events, msgInEvt];
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: msgInEvt,
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
- this.events = [
1216
- ...this.events,
1217
- {
1218
- type: 'error',
1219
- created_on: now,
1220
- text: 'Failed to send message'
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
- handleQuickReply(quickReply) {
1240
- if (!this.sprinting) {
1241
- this.resume(quickReply);
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
- getEventDescription(event) {
1289
- switch (event.type) {
1290
- case 'contact_groups_changed': {
1291
- const groups = event.groups_added || [];
1292
- const removedGroups = event.groups_removed || [];
1293
- if (groups.length > 0) {
1294
- const groupNames = groups.map((g) => `"${g.name}"`).join(', ');
1295
- return `Added to ${groupNames}`;
1296
- }
1297
- if (removedGroups.length > 0) {
1298
- const groupNames = removedGroups
1299
- .map((g) => `"${g.name}"`)
1300
- .join(', ');
1301
- return `Removed from ${groupNames}`;
1302
- }
1303
- break;
1304
- }
1305
- case 'contact_field_changed': {
1306
- const field = event.field;
1307
- const value = event.value;
1308
- const valueText = value ? value.text || value : '';
1309
- if (field) {
1310
- if (valueText) {
1311
- return `Set contact "${field.name}" to "${valueText}"`;
1312
- }
1313
- else {
1314
- return `Cleared contact "${field.name}"`;
1315
- }
1316
- }
1317
- break;
1318
- }
1319
- case 'contact_language_changed':
1320
- return `Set preferred language to "${event.language}"`;
1321
- case 'contact_name_changed':
1322
- return `Set contact name to "${event.name}"`;
1323
- case 'contact_status_changed':
1324
- return `Set status to "${event.status}"`;
1325
- case 'contact_urns_changed':
1326
- return `Added a URN for the contact`;
1327
- case 'input_labels_added': {
1328
- const labels = event.labels || [];
1329
- if (labels.length > 0) {
1330
- const labelNames = labels.map((l) => `"${l.name}"`).join(', ');
1331
- return `Message labeled with ${labelNames}`;
1332
- }
1333
- break;
1334
- }
1335
- case 'run_result_changed':
1336
- return `Set result "${event.name}" to "${event.value}"`;
1337
- case 'run_started':
1338
- case 'flow_entered': {
1339
- const flow = event.flow;
1340
- if (flow) {
1341
- return `Entered flow "${flow.name}"`;
1342
- }
1343
- break;
1344
- }
1345
- case 'run_ended': {
1346
- const flow = event.flow;
1347
- if (flow) {
1348
- return `Exited flow "${flow.name}"`;
1349
- }
1350
- break;
1351
- }
1352
- case 'email_created':
1353
- case 'email_sent': {
1354
- const recipients = event.to || event.addresses || [];
1355
- const subject = event.subject;
1356
- const recipientList = recipients
1357
- .map((r) => `"${r}"`)
1358
- .join(', ');
1359
- return `Sent email to ${recipientList} with subject "${subject}"`;
1360
- }
1361
- case 'broadcast_created': {
1362
- const translations = event.translations;
1363
- const baseLanguage = event.base_language;
1364
- if (translations && translations[baseLanguage]) {
1365
- return `Sent broadcast: "${translations[baseLanguage].text}"`;
1366
- }
1367
- return `Sent broadcast`;
1368
- }
1369
- case 'session_triggered': {
1370
- const flow = event.flow;
1371
- if (flow) {
1372
- return `Started somebody else in "${flow.name}"`;
1373
- }
1374
- break;
1375
- }
1376
- case 'ticket_opened': {
1377
- const ticket = event.ticket;
1378
- if (ticket && ticket.topic) {
1379
- return `Ticket opened with topic "${ticket.topic.name}"`;
1380
- }
1381
- return `Ticket opened`;
1382
- }
1383
- case 'resthook_called':
1384
- return `Triggered flow event "${event.resthook}"`;
1385
- case 'webhook_called':
1386
- return `Called ${event.url}`;
1387
- case 'service_called': {
1388
- const service = event.service;
1389
- if (service === 'classifier') {
1390
- return `Called classifier`;
1391
- }
1392
- return `Called ${service}`;
1393
- }
1394
- case 'airtime_transferred': {
1395
- const amount = event.actual_amount;
1396
- const currency = event.currency;
1397
- const recipient = event.recipient;
1398
- if (amount && currency && recipient) {
1399
- return `Transferred ${amount} ${currency} to ${recipient}`;
1400
- }
1401
- break;
1402
- }
1403
- case 'info':
1404
- return event.text;
1405
- case 'warning':
1406
- return `⚠️ ${event.text}`;
1407
- }
1408
- return null;
1409
- }
1410
- renderAlertMessage(type, text, animatedClass, animationDelay) {
1411
- const config = {
1412
- error: {
1413
- icon: '❗',
1414
- bgColor: '#fee2e2',
1415
- textColor: '#991b1b',
1416
- defaultText: 'An error occurred'
1417
- },
1418
- warning: {
1419
- icon: '⚠️',
1420
- bgColor: '#fef3c7',
1421
- textColor: 'rgba(125, 87, 18, 0.8)',
1422
- defaultText: 'A warning occurred'
1423
- },
1424
- failure: {
1425
- icon: '💥',
1426
- bgColor: '#fee2e2',
1427
- textColor: '#991b1b',
1428
- defaultText: 'A failure occurred'
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 ? 'open' : ''}"
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
- <div class="phone-screen">${this.renderMessages()}</div>
1701
- <div class="message-input">
1702
- <button
1703
- class="attachment-button"
1704
- @click=${this.handleToggleAttachmentMenu}
1705
- ?disabled=${this.sprinting}
1706
- >
1707
- <temba-icon name="plus" size="1.5"></temba-icon>
1708
- </button>
1709
- <input
1710
- type="text"
1711
- placeholder="Enter Message"
1712
- .value=${this.inputValue}
1713
- @input=${this.handleInput}
1714
- @keyup=${this.handleKeyUp}
1715
- ?disabled=${this.sprinting}
1716
- />
1717
- <div
1718
- class="attachment-menu ${this.attachmentMenuOpen ? 'open' : ''}"
1719
- >
1720
- <div
1721
- class="attachment-menu-item"
1722
- @click=${() => this.handleSendAttachment('image')}
1723
- >
1724
- <temba-icon name="attachment_image" size="1.2"></temba-icon>
1725
- <span>Image</span>
1726
- </div>
1727
- <div
1728
- class="attachment-menu-item"
1729
- @click=${() => this.handleSendAttachment('video')}
1730
- >
1731
- <temba-icon name="attachment_video" size="1.2"></temba-icon>
1732
- <span>Video</span>
1733
- </div>
1734
- <div
1735
- class="attachment-menu-item"
1736
- @click=${() => this.handleSendAttachment('audio')}
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="attachment_audio" size="1.2"></temba-icon>
1739
- <span>Audio</span>
1740
- </div>
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-item"
1743
- @click=${() => this.handleSendAttachment('location')}
1576
+ class="attachment-menu ${this.attachmentMenuOpen
1577
+ ? 'open'
1578
+ : ''}"
1744
1579
  >
1745
- <temba-icon
1746
- name="attachment_location"
1747
- size="1.2"
1748
- ></temba-icon>
1749
- <span>Location</span>
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 class="option-pane">
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>