@nyaruka/temba-components 0.136.0 → 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 +19 -0
- package/demo/components/webchat/example.html +2 -2
- package/dist/temba-components.js +537 -578
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/Chat.js +123 -44
- package/out-tsc/src/display/Chat.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +2 -6
- package/out-tsc/src/display/FloatingTab.js.map +1 -1
- package/out-tsc/src/events/eventRenderers.js +442 -0
- package/out-tsc/src/events/eventRenderers.js.map +1 -0
- package/out-tsc/src/flow/CanvasNode.js +18 -1
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +10 -7
- 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/layout/FloatingWindow.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 +432 -541
- package/out-tsc/src/simulator/Simulator.js.map +1 -1
- package/out-tsc/src/store/AppState.js +33 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/test/temba-appstate-node-sorting.test.js +430 -0
- package/out-tsc/test/temba-appstate-node-sorting.test.js.map +1 -0
- package/out-tsc/test/temba-floating-tab.test.js +0 -9
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -1
- package/out-tsc/test/temba-flow-editor.test.js +261 -0
- package/out-tsc/test/temba-flow-editor.test.js.map +1 -1
- package/out-tsc/test/temba-simulator.test.js +51 -32
- package/out-tsc/test/temba-simulator.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
- package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
- package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
- package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
- package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
- package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
- package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
- package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
- package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
- package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
- package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
- package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
- package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
- package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
- 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/floating-tab/gray.png +0 -0
- package/screenshots/truth/floating-tab/green.png +0 -0
- package/screenshots/truth/floating-tab/purple.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
- package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
- package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
- package/screenshots/truth/simulator/after-message-sent.png +0 -0
- package/screenshots/truth/simulator/after-reset.png +0 -0
- package/screenshots/truth/simulator/attachment-menu.png +0 -0
- package/screenshots/truth/simulator/context-expanded.png +0 -0
- package/screenshots/truth/simulator/context-explorer-open.png +0 -0
- package/screenshots/truth/simulator/event-info.png +0 -0
- package/screenshots/truth/simulator/image-attachment.png +0 -0
- package/screenshots/truth/simulator/open-initial.png +0 -0
- package/screenshots/truth/simulator/quick-replies.png +0 -0
- package/src/display/Chat.ts +123 -44
- package/src/display/FloatingTab.ts +2 -7
- package/src/events/eventRenderers.ts +527 -0
- package/src/flow/CanvasNode.ts +18 -1
- package/src/flow/Editor.ts +11 -7
- package/src/flow/NodeEditor.ts +0 -1
- package/src/layout/FloatingWindow.ts +1 -1
- package/src/list/ShortcutList.ts +1 -1
- package/src/live/ContactChat.ts +17 -376
- package/src/simulator/Simulator.ts +492 -564
- package/src/store/AppState.ts +56 -0
- package/test/temba-appstate-node-sorting.test.ts +506 -0
- package/test/temba-floating-tab.test.ts +0 -11
- package/test/temba-flow-editor.test.ts +297 -0
- package/test/temba-simulator.test.ts +64 -34
|
@@ -3,9 +3,11 @@ import { html } from 'lit-html';
|
|
|
3
3
|
import { RapidElement } from '../RapidElement';
|
|
4
4
|
import { css } from 'lit';
|
|
5
5
|
import { property } from 'lit/decorators.js';
|
|
6
|
-
import { postJSON, fromCookie } from '../utils';
|
|
6
|
+
import { postJSON, fromCookie, generateUUIDv7 } from '../utils';
|
|
7
7
|
import { getStore } from '../store/Store';
|
|
8
8
|
import { CustomEventType } from '../interfaces';
|
|
9
|
+
import { MessageType } from '../display/Chat';
|
|
10
|
+
import { Events, renderEvent } from '../events/eventRenderers';
|
|
9
11
|
// test attachment URLs
|
|
10
12
|
const TEST_IMAGES = [
|
|
11
13
|
'https://s3.amazonaws.com/floweditor-assets.temba.io/simulator/sim_image_a.jpg',
|
|
@@ -28,8 +30,7 @@ const TEST_LOCATIONS = [
|
|
|
28
30
|
const SIMULATOR_SIZES = {
|
|
29
31
|
small: {
|
|
30
32
|
phoneWidth: 270,
|
|
31
|
-
|
|
32
|
-
phoneTotalHeight: 576,
|
|
33
|
+
phoneTotalHeight: 530,
|
|
33
34
|
phoneScreenHeight: 376,
|
|
34
35
|
contextWidth: 336,
|
|
35
36
|
contextHeight: 416,
|
|
@@ -46,8 +47,7 @@ const SIMULATOR_SIZES = {
|
|
|
46
47
|
},
|
|
47
48
|
medium: {
|
|
48
49
|
phoneWidth: 300,
|
|
49
|
-
|
|
50
|
-
phoneTotalHeight: 720,
|
|
50
|
+
phoneTotalHeight: 600,
|
|
51
51
|
phoneScreenHeight: 470,
|
|
52
52
|
contextWidth: 420,
|
|
53
53
|
contextHeight: 520,
|
|
@@ -64,8 +64,7 @@ const SIMULATOR_SIZES = {
|
|
|
64
64
|
},
|
|
65
65
|
large: {
|
|
66
66
|
phoneWidth: 360,
|
|
67
|
-
|
|
68
|
-
phoneTotalHeight: 864,
|
|
67
|
+
phoneTotalHeight: 700,
|
|
69
68
|
phoneScreenHeight: 564,
|
|
70
69
|
contextWidth: 504,
|
|
71
70
|
contextHeight: 624,
|
|
@@ -89,6 +88,7 @@ export class Simulator extends RapidElement {
|
|
|
89
88
|
this.animationTime = 200;
|
|
90
89
|
this.events = [];
|
|
91
90
|
this.previousEventCount = 0;
|
|
91
|
+
this.chat = null;
|
|
92
92
|
this.session = null;
|
|
93
93
|
this.context = null;
|
|
94
94
|
this.contact = {
|
|
@@ -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,13 +1067,43 @@ 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
|
-
phoneWindow.hide();
|
|
1103
|
+
// phoneWindow.hide();
|
|
1104
|
+
phoneWindow.handleClose();
|
|
982
1105
|
this.isVisible = false;
|
|
983
1106
|
getStore().getState().setSimulatorActive(false);
|
|
984
|
-
const phoneTab = this.shadowRoot.getElementById('phone-tab');
|
|
985
|
-
phoneTab.hidden = false;
|
|
986
1107
|
}
|
|
987
1108
|
handleReset() {
|
|
988
1109
|
// reset simulation state
|
|
@@ -993,6 +1114,10 @@ export class Simulator extends RapidElement {
|
|
|
993
1114
|
this.sprinting = false;
|
|
994
1115
|
this.previousEventCount = 0;
|
|
995
1116
|
this.currentQuickReplies = [];
|
|
1117
|
+
// reset chat component
|
|
1118
|
+
if (this.chat) {
|
|
1119
|
+
this.chat.reset();
|
|
1120
|
+
}
|
|
996
1121
|
// Clear simulator activity data
|
|
997
1122
|
getStore().getState().updateSimulatorActivity({
|
|
998
1123
|
segments: {},
|
|
@@ -1180,19 +1305,34 @@ export class Simulator extends RapidElement {
|
|
|
1180
1305
|
this.currentQuickReplies = [];
|
|
1181
1306
|
this.attachmentMenuOpen = false;
|
|
1182
1307
|
const now = new Date().toISOString();
|
|
1183
|
-
|
|
1184
|
-
|
|
1308
|
+
// create the event for the API (with ISO string date)
|
|
1309
|
+
const msgInEvtForAPI = {
|
|
1310
|
+
uuid: generateUUIDv7(),
|
|
1185
1311
|
type: 'msg_received',
|
|
1186
1312
|
created_on: now,
|
|
1187
1313
|
msg: {
|
|
1188
|
-
uuid:
|
|
1314
|
+
uuid: generateUUIDv7(),
|
|
1189
1315
|
text: text || '',
|
|
1190
1316
|
urn: this.contact.urns[0],
|
|
1191
|
-
|
|
1317
|
+
direction: 'in',
|
|
1318
|
+
type: 'text',
|
|
1319
|
+
attachments: attachment ? [attachment] : [],
|
|
1320
|
+
quick_replies: [],
|
|
1321
|
+
channel: { uuid: generateUUIDv7(), name: 'Simulator' }
|
|
1192
1322
|
}
|
|
1193
1323
|
};
|
|
1194
|
-
//
|
|
1195
|
-
|
|
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
|
+
}
|
|
1196
1336
|
this.requestUpdate();
|
|
1197
1337
|
this.scrollToBottom();
|
|
1198
1338
|
const body = {
|
|
@@ -1200,7 +1340,7 @@ export class Simulator extends RapidElement {
|
|
|
1200
1340
|
contact: this.contact,
|
|
1201
1341
|
resume: {
|
|
1202
1342
|
type: 'msg',
|
|
1203
|
-
event:
|
|
1343
|
+
event: msgInEvtForAPI,
|
|
1204
1344
|
resumed_on: now
|
|
1205
1345
|
}
|
|
1206
1346
|
};
|
|
@@ -1213,14 +1353,21 @@ export class Simulator extends RapidElement {
|
|
|
1213
1353
|
}
|
|
1214
1354
|
catch (error) {
|
|
1215
1355
|
console.error('Failed to resume simulation:', error);
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
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
|
|
1222
1363
|
}
|
|
1223
|
-
|
|
1364
|
+
};
|
|
1365
|
+
if (this.chat) {
|
|
1366
|
+
this.chat.addMessages([errorEvent], null, true);
|
|
1367
|
+
}
|
|
1368
|
+
else {
|
|
1369
|
+
this.events = [...this.events, errorEvent];
|
|
1370
|
+
}
|
|
1224
1371
|
this.sprinting = false;
|
|
1225
1372
|
}
|
|
1226
1373
|
}
|
|
@@ -1237,9 +1384,9 @@ export class Simulator extends RapidElement {
|
|
|
1237
1384
|
const input = evt.target;
|
|
1238
1385
|
this.inputValue = input.value;
|
|
1239
1386
|
}
|
|
1240
|
-
|
|
1241
|
-
if (!this.sprinting) {
|
|
1242
|
-
this.resume(
|
|
1387
|
+
handleQuickReplyClick(text) {
|
|
1388
|
+
if (!this.sprinting && text) {
|
|
1389
|
+
this.resume(text);
|
|
1243
1390
|
}
|
|
1244
1391
|
}
|
|
1245
1392
|
handleToggleAttachmentMenu() {
|
|
@@ -1286,296 +1433,15 @@ export class Simulator extends RapidElement {
|
|
|
1286
1433
|
this.resume('', attachment);
|
|
1287
1434
|
}
|
|
1288
1435
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
return `Added to ${groupNames}`;
|
|
1297
|
-
}
|
|
1298
|
-
if (removedGroups.length > 0) {
|
|
1299
|
-
const groupNames = removedGroups
|
|
1300
|
-
.map((g) => `"${g.name}"`)
|
|
1301
|
-
.join(', ');
|
|
1302
|
-
return `Removed from ${groupNames}`;
|
|
1303
|
-
}
|
|
1304
|
-
break;
|
|
1305
|
-
}
|
|
1306
|
-
case 'contact_field_changed': {
|
|
1307
|
-
const field = event.field;
|
|
1308
|
-
const value = event.value;
|
|
1309
|
-
const valueText = value ? value.text || value : '';
|
|
1310
|
-
if (field) {
|
|
1311
|
-
if (valueText) {
|
|
1312
|
-
return `Set contact "${field.name}" to "${valueText}"`;
|
|
1313
|
-
}
|
|
1314
|
-
else {
|
|
1315
|
-
return `Cleared contact "${field.name}"`;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
break;
|
|
1319
|
-
}
|
|
1320
|
-
case 'contact_language_changed':
|
|
1321
|
-
return `Set preferred language to "${event.language}"`;
|
|
1322
|
-
case 'contact_name_changed':
|
|
1323
|
-
return `Set contact name to "${event.name}"`;
|
|
1324
|
-
case 'contact_status_changed':
|
|
1325
|
-
return `Set status to "${event.status}"`;
|
|
1326
|
-
case 'contact_urns_changed':
|
|
1327
|
-
return `Added a URN for the contact`;
|
|
1328
|
-
case 'input_labels_added': {
|
|
1329
|
-
const labels = event.labels || [];
|
|
1330
|
-
if (labels.length > 0) {
|
|
1331
|
-
const labelNames = labels.map((l) => `"${l.name}"`).join(', ');
|
|
1332
|
-
return `Message labeled with ${labelNames}`;
|
|
1333
|
-
}
|
|
1334
|
-
break;
|
|
1335
|
-
}
|
|
1336
|
-
case 'run_result_changed':
|
|
1337
|
-
return `Set result "${event.name}" to "${event.value}"`;
|
|
1338
|
-
case 'run_started':
|
|
1339
|
-
case 'flow_entered': {
|
|
1340
|
-
const flow = event.flow;
|
|
1341
|
-
if (flow) {
|
|
1342
|
-
return `Entered flow "${flow.name}"`;
|
|
1343
|
-
}
|
|
1344
|
-
break;
|
|
1345
|
-
}
|
|
1346
|
-
case 'run_ended': {
|
|
1347
|
-
const flow = event.flow;
|
|
1348
|
-
if (flow) {
|
|
1349
|
-
return `Exited flow "${flow.name}"`;
|
|
1350
|
-
}
|
|
1351
|
-
break;
|
|
1352
|
-
}
|
|
1353
|
-
case 'email_created':
|
|
1354
|
-
case 'email_sent': {
|
|
1355
|
-
const recipients = event.to || event.addresses || [];
|
|
1356
|
-
const subject = event.subject;
|
|
1357
|
-
const recipientList = recipients
|
|
1358
|
-
.map((r) => `"${r}"`)
|
|
1359
|
-
.join(', ');
|
|
1360
|
-
return `Sent email to ${recipientList} with subject "${subject}"`;
|
|
1361
|
-
}
|
|
1362
|
-
case 'broadcast_created': {
|
|
1363
|
-
const translations = event.translations;
|
|
1364
|
-
const baseLanguage = event.base_language;
|
|
1365
|
-
if (translations && translations[baseLanguage]) {
|
|
1366
|
-
return `Sent broadcast: "${translations[baseLanguage].text}"`;
|
|
1367
|
-
}
|
|
1368
|
-
return `Sent broadcast`;
|
|
1369
|
-
}
|
|
1370
|
-
case 'session_triggered': {
|
|
1371
|
-
const flow = event.flow;
|
|
1372
|
-
if (flow) {
|
|
1373
|
-
return `Started somebody else in "${flow.name}"`;
|
|
1374
|
-
}
|
|
1375
|
-
break;
|
|
1376
|
-
}
|
|
1377
|
-
case 'ticket_opened': {
|
|
1378
|
-
const ticket = event.ticket;
|
|
1379
|
-
if (ticket && ticket.topic) {
|
|
1380
|
-
return `Ticket opened with topic "${ticket.topic.name}"`;
|
|
1381
|
-
}
|
|
1382
|
-
return `Ticket opened`;
|
|
1383
|
-
}
|
|
1384
|
-
case 'resthook_called':
|
|
1385
|
-
return `Triggered flow event "${event.resthook}"`;
|
|
1386
|
-
case 'webhook_called':
|
|
1387
|
-
return `Called ${event.url}`;
|
|
1388
|
-
case 'service_called': {
|
|
1389
|
-
const service = event.service;
|
|
1390
|
-
if (service === 'classifier') {
|
|
1391
|
-
return `Called classifier`;
|
|
1392
|
-
}
|
|
1393
|
-
return `Called ${service}`;
|
|
1394
|
-
}
|
|
1395
|
-
case 'airtime_transferred': {
|
|
1396
|
-
const amount = event.actual_amount;
|
|
1397
|
-
const currency = event.currency;
|
|
1398
|
-
const recipient = event.recipient;
|
|
1399
|
-
if (amount && currency && recipient) {
|
|
1400
|
-
return `Transferred ${amount} ${currency} to ${recipient}`;
|
|
1401
|
-
}
|
|
1402
|
-
break;
|
|
1403
|
-
}
|
|
1404
|
-
case 'info':
|
|
1405
|
-
return event.text;
|
|
1406
|
-
case 'warning':
|
|
1407
|
-
return `⚠️ ${event.text}`;
|
|
1408
|
-
}
|
|
1409
|
-
return null;
|
|
1410
|
-
}
|
|
1411
|
-
renderAttachment(attachment) {
|
|
1412
|
-
// parse attachment format: "type/subtype:url" or "geo:lat,long"
|
|
1413
|
-
const parts = attachment.split(':');
|
|
1414
|
-
const type = parts[0];
|
|
1415
|
-
const content = parts.slice(1).join(':'); // rejoin in case url has colons
|
|
1416
|
-
if (type === 'geo') {
|
|
1417
|
-
// use temba-thumbnail for location to get map image
|
|
1418
|
-
return html `
|
|
1419
|
-
<div class="attachment-location">
|
|
1420
|
-
<temba-thumbnail attachment="${attachment}"></temba-thumbnail>
|
|
1421
|
-
</div>
|
|
1422
|
-
`;
|
|
1423
|
-
}
|
|
1424
|
-
else if (type.startsWith('image/')) {
|
|
1425
|
-
// custom image rendering
|
|
1426
|
-
return html `
|
|
1427
|
-
<div class="attachment">
|
|
1428
|
-
<img src="${content}" alt="Image attachment" />
|
|
1429
|
-
</div>
|
|
1430
|
-
`;
|
|
1431
|
-
}
|
|
1432
|
-
else if (type.startsWith('video/')) {
|
|
1433
|
-
// custom video rendering
|
|
1434
|
-
return html `
|
|
1435
|
-
<div class="attachment">
|
|
1436
|
-
<video controls>
|
|
1437
|
-
<source src="${content}" type="${type}" />
|
|
1438
|
-
</video>
|
|
1439
|
-
</div>
|
|
1440
|
-
`;
|
|
1441
|
-
}
|
|
1442
|
-
else if (type.startsWith('audio/')) {
|
|
1443
|
-
// custom audio rendering
|
|
1444
|
-
return html `
|
|
1445
|
-
<div class="attachment">
|
|
1446
|
-
<div class="attachment-audio">
|
|
1447
|
-
<audio controls>
|
|
1448
|
-
<source src="${content}" type="${type}" />
|
|
1449
|
-
</audio>
|
|
1450
|
-
</div>
|
|
1451
|
-
</div>
|
|
1452
|
-
`;
|
|
1453
|
-
}
|
|
1454
|
-
// fallback for unknown types
|
|
1455
|
-
return html `
|
|
1456
|
-
<div class="attachment">
|
|
1457
|
-
<span>Attachment</span>
|
|
1458
|
-
</div>
|
|
1459
|
-
`;
|
|
1460
|
-
}
|
|
1461
|
-
renderMessages() {
|
|
1462
|
-
if (this.events.length === 0) {
|
|
1463
|
-
return html `
|
|
1464
|
-
<div class="message incoming">👋 Welcome! Starting simulation...</div>
|
|
1465
|
-
`;
|
|
1466
|
-
}
|
|
1467
|
-
const eventTemplates = this.events.map((event, index) => {
|
|
1468
|
-
// only animate messages that are new (beyond previous count)
|
|
1469
|
-
const isNew = index >= this.previousEventCount;
|
|
1470
|
-
const animatedClass = isNew ? 'animated' : '';
|
|
1471
|
-
// stagger animations for new messages
|
|
1472
|
-
const animationDelay = isNew
|
|
1473
|
-
? `${(index - this.previousEventCount) * 0.2}s`
|
|
1474
|
-
: '0s';
|
|
1475
|
-
if (event.type === 'msg_received' && event.msg) {
|
|
1476
|
-
const hasAttachments = event.msg.attachments && event.msg.attachments.length > 0;
|
|
1477
|
-
const hasText = event.msg.text && event.msg.text.trim().length > 0;
|
|
1478
|
-
return html `
|
|
1479
|
-
${hasAttachments
|
|
1480
|
-
? html `
|
|
1481
|
-
<div
|
|
1482
|
-
class="attachment-wrapper outgoing ${animatedClass}"
|
|
1483
|
-
style="animation-delay: ${animationDelay}"
|
|
1484
|
-
>
|
|
1485
|
-
${event.msg.attachments.map((att) => this.renderAttachment(att))}
|
|
1486
|
-
</div>
|
|
1487
|
-
`
|
|
1488
|
-
: html ``}
|
|
1489
|
-
${hasText
|
|
1490
|
-
? html `
|
|
1491
|
-
<div
|
|
1492
|
-
class="message outgoing ${animatedClass}"
|
|
1493
|
-
style="animation-delay: ${animationDelay}"
|
|
1494
|
-
>
|
|
1495
|
-
${event.msg.text}
|
|
1496
|
-
</div>
|
|
1497
|
-
`
|
|
1498
|
-
: html ``}
|
|
1499
|
-
`;
|
|
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`);
|
|
1500
1443
|
}
|
|
1501
|
-
else if (event.type === 'msg_created' && event.msg) {
|
|
1502
|
-
const hasAttachments = event.msg.attachments && event.msg.attachments.length > 0;
|
|
1503
|
-
const hasText = event.msg.text && event.msg.text.trim().length > 0;
|
|
1504
|
-
return html `
|
|
1505
|
-
${hasAttachments
|
|
1506
|
-
? html `
|
|
1507
|
-
<div
|
|
1508
|
-
class="attachment-wrapper incoming ${animatedClass}"
|
|
1509
|
-
style="animation-delay: ${animationDelay}"
|
|
1510
|
-
>
|
|
1511
|
-
${event.msg.attachments.map((att) => this.renderAttachment(att))}
|
|
1512
|
-
</div>
|
|
1513
|
-
`
|
|
1514
|
-
: html ``}
|
|
1515
|
-
${hasText
|
|
1516
|
-
? html `
|
|
1517
|
-
<div
|
|
1518
|
-
class="message incoming ${animatedClass}"
|
|
1519
|
-
style="animation-delay: ${animationDelay}"
|
|
1520
|
-
>
|
|
1521
|
-
${event.msg.text}
|
|
1522
|
-
</div>
|
|
1523
|
-
`
|
|
1524
|
-
: html ``}
|
|
1525
|
-
`;
|
|
1526
|
-
}
|
|
1527
|
-
else if (event.type === 'error') {
|
|
1528
|
-
return html `
|
|
1529
|
-
<div
|
|
1530
|
-
class="message incoming ${animatedClass}"
|
|
1531
|
-
style="background: #ff4444; color: white; animation-delay: ${animationDelay}"
|
|
1532
|
-
>
|
|
1533
|
-
⚠️ ${event.text || 'An error occurred'}
|
|
1534
|
-
</div>
|
|
1535
|
-
`;
|
|
1536
|
-
}
|
|
1537
|
-
else {
|
|
1538
|
-
// check if this is an event we should display
|
|
1539
|
-
const description = this.getEventDescription(event);
|
|
1540
|
-
if (description) {
|
|
1541
|
-
return html `
|
|
1542
|
-
<div
|
|
1543
|
-
class="event-info ${animatedClass}"
|
|
1544
|
-
style="animation-delay: ${animationDelay}"
|
|
1545
|
-
>
|
|
1546
|
-
${description}
|
|
1547
|
-
</div>
|
|
1548
|
-
`;
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
return html ``;
|
|
1552
1444
|
});
|
|
1553
|
-
// render quick replies at the end if we have any from the most recent sprint
|
|
1554
|
-
const hasQuickReplies = this.currentQuickReplies.length > 0;
|
|
1555
|
-
const quickRepliesAnimationDelay = this.events.length >= this.previousEventCount
|
|
1556
|
-
? `${(this.events.length - this.previousEventCount) * 0.2}s`
|
|
1557
|
-
: '0s';
|
|
1558
|
-
return html `
|
|
1559
|
-
${eventTemplates}
|
|
1560
|
-
${hasQuickReplies
|
|
1561
|
-
? html `
|
|
1562
|
-
<div
|
|
1563
|
-
class="quick-replies animated"
|
|
1564
|
-
style="animation-delay: ${quickRepliesAnimationDelay}"
|
|
1565
|
-
>
|
|
1566
|
-
${this.currentQuickReplies.map((qr) => html `
|
|
1567
|
-
<button
|
|
1568
|
-
class="quick-reply-btn animated"
|
|
1569
|
-
style="animation-delay: ${quickRepliesAnimationDelay}"
|
|
1570
|
-
@click=${() => this.handleQuickReply(qr.text)}
|
|
1571
|
-
>
|
|
1572
|
-
${qr.text}
|
|
1573
|
-
</button>
|
|
1574
|
-
`)}
|
|
1575
|
-
</div>
|
|
1576
|
-
`
|
|
1577
|
-
: html ``}
|
|
1578
|
-
`;
|
|
1579
1445
|
}
|
|
1580
1446
|
render() {
|
|
1581
1447
|
const config = this.sizeConfig;
|
|
@@ -1597,16 +1463,18 @@ export class Simulator extends RapidElement {
|
|
|
1597
1463
|
--cutout-island-width: ${config.cutoutIslandWidth}px;
|
|
1598
1464
|
--cutout-island-height: ${config.cutoutIslandHeight}px;
|
|
1599
1465
|
--cutout-island-top: ${config.cutoutIslandTop}px;
|
|
1466
|
+
--animation-time: ${this.animationTime}ms;
|
|
1600
1467
|
`;
|
|
1601
1468
|
return html `
|
|
1602
1469
|
<temba-floating-window
|
|
1470
|
+
style="--transition-duration: ${this.animationTime}ms"
|
|
1603
1471
|
id="phone-window"
|
|
1604
1472
|
width="${this.windowWidth}"
|
|
1605
1473
|
leftBoundaryMargin="${this.leftBoundaryMargin}"
|
|
1606
1474
|
bottomBoundaryMargin="${config.windowPadding}"
|
|
1607
1475
|
topBoundaryMargin="${config.windowPadding}"
|
|
1608
1476
|
height="${config.phoneTotalHeight}"
|
|
1609
|
-
top="
|
|
1477
|
+
top="0"
|
|
1610
1478
|
chromeless
|
|
1611
1479
|
>
|
|
1612
1480
|
<div class="phone-simulator" style="${styleVars}">
|
|
@@ -1664,56 +1532,78 @@ export class Simulator extends RapidElement {
|
|
|
1664
1532
|
<div class="dynamic-island"></div>
|
|
1665
1533
|
</div>
|
|
1666
1534
|
</div>
|
|
1667
|
-
<
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
<span>Image</span>
|
|
1693
|
-
</div>
|
|
1694
|
-
<div
|
|
1695
|
-
class="attachment-menu-item"
|
|
1696
|
-
@click=${() => this.handleSendAttachment('video')}
|
|
1697
|
-
>
|
|
1698
|
-
<temba-icon name="attachment_video" size="1.2"></temba-icon>
|
|
1699
|
-
<span>Video</span>
|
|
1700
|
-
</div>
|
|
1701
|
-
<div
|
|
1702
|
-
class="attachment-menu-item"
|
|
1703
|
-
@click=${() => this.handleSendAttachment('audio')}
|
|
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}
|
|
1704
1560
|
>
|
|
1705
|
-
<temba-icon name="
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|
+
/>
|
|
1708
1571
|
<div
|
|
1709
|
-
class="attachment-menu
|
|
1710
|
-
|
|
1572
|
+
class="attachment-menu ${this.attachmentMenuOpen
|
|
1573
|
+
? 'open'
|
|
1574
|
+
: ''}"
|
|
1711
1575
|
>
|
|
1712
|
-
<
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
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>
|
|
1717
1607
|
</div>
|
|
1718
1608
|
</div>
|
|
1719
1609
|
</div>
|
|
@@ -1762,6 +1652,7 @@ export class Simulator extends RapidElement {
|
|
|
1762
1652
|
icon="simulator"
|
|
1763
1653
|
label="Phone Simulator"
|
|
1764
1654
|
color="#10b981"
|
|
1655
|
+
.hidden=${this.isVisible}
|
|
1765
1656
|
@temba-button-clicked=${this.handleShow}
|
|
1766
1657
|
></temba-floating-tab>
|
|
1767
1658
|
`;
|