@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
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { html, TemplateResult } from 'lit-html';
|
|
2
2
|
import { RapidElement } from '../RapidElement';
|
|
3
3
|
import { FloatingWindow } from '../layout/FloatingWindow';
|
|
4
|
-
import { FloatingTab } from '../display/FloatingTab';
|
|
5
4
|
import { css, PropertyValueMap } from 'lit';
|
|
6
5
|
import { property } from 'lit/decorators.js';
|
|
7
|
-
import { postJSON, fromCookie } from '../utils';
|
|
6
|
+
import { postJSON, fromCookie, generateUUIDv7 } from '../utils';
|
|
8
7
|
import { getStore } from '../store/Store';
|
|
9
8
|
import { CustomEventType } from '../interfaces';
|
|
9
|
+
import { Chat, ContactEvent, MessageType } from '../display/Chat';
|
|
10
|
+
import { Events, renderEvent } from '../events/eventRenderers';
|
|
10
11
|
|
|
11
12
|
// test attachment URLs
|
|
12
13
|
const TEST_IMAGES = [
|
|
@@ -74,7 +75,6 @@ interface RunContext {
|
|
|
74
75
|
|
|
75
76
|
interface SimulatorSize {
|
|
76
77
|
phoneWidth: number;
|
|
77
|
-
phoneHeight: number;
|
|
78
78
|
phoneTotalHeight: number;
|
|
79
79
|
phoneScreenHeight: number;
|
|
80
80
|
contextWidth: number;
|
|
@@ -94,8 +94,7 @@ interface SimulatorSize {
|
|
|
94
94
|
const SIMULATOR_SIZES: Record<string, SimulatorSize> = {
|
|
95
95
|
small: {
|
|
96
96
|
phoneWidth: 270,
|
|
97
|
-
|
|
98
|
-
phoneTotalHeight: 576,
|
|
97
|
+
phoneTotalHeight: 530,
|
|
99
98
|
phoneScreenHeight: 376,
|
|
100
99
|
contextWidth: 336,
|
|
101
100
|
contextHeight: 416,
|
|
@@ -112,8 +111,7 @@ const SIMULATOR_SIZES: Record<string, SimulatorSize> = {
|
|
|
112
111
|
},
|
|
113
112
|
medium: {
|
|
114
113
|
phoneWidth: 300,
|
|
115
|
-
|
|
116
|
-
phoneTotalHeight: 720,
|
|
114
|
+
phoneTotalHeight: 600,
|
|
117
115
|
phoneScreenHeight: 470,
|
|
118
116
|
contextWidth: 420,
|
|
119
117
|
contextHeight: 520,
|
|
@@ -130,8 +128,7 @@ const SIMULATOR_SIZES: Record<string, SimulatorSize> = {
|
|
|
130
128
|
},
|
|
131
129
|
large: {
|
|
132
130
|
phoneWidth: 360,
|
|
133
|
-
|
|
134
|
-
phoneTotalHeight: 864,
|
|
131
|
+
phoneTotalHeight: 700,
|
|
135
132
|
phoneScreenHeight: 564,
|
|
136
133
|
contextWidth: 504,
|
|
137
134
|
contextHeight: 624,
|
|
@@ -218,6 +215,7 @@ export class Simulator extends RapidElement {
|
|
|
218
215
|
|
|
219
216
|
.phone-frame {
|
|
220
217
|
width: var(--phone-width);
|
|
218
|
+
height: var(--phone-total-height);
|
|
221
219
|
border-radius: 40px;
|
|
222
220
|
border: 6px solid #1f2937;
|
|
223
221
|
box-shadow: 0 0px 30px rgba(0, 0, 0, 0.4);
|
|
@@ -322,6 +320,39 @@ export class Simulator extends RapidElement {
|
|
|
322
320
|
background: rgba(255, 255, 255, 0.5);
|
|
323
321
|
}
|
|
324
322
|
|
|
323
|
+
/* Custom scrollbar for chat area to allow content to flow behind input */
|
|
324
|
+
.custom-scrollbar-container {
|
|
325
|
+
position: absolute;
|
|
326
|
+
top: 40px;
|
|
327
|
+
bottom: var(--bottom-input-height, 60px);
|
|
328
|
+
right: 4px;
|
|
329
|
+
width: 10px;
|
|
330
|
+
z-index: 20;
|
|
331
|
+
overflow-y: auto;
|
|
332
|
+
overflow-x: hidden;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.custom-scrollbar-container::-webkit-scrollbar {
|
|
336
|
+
width: 6px;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.custom-scrollbar-container::-webkit-scrollbar-track {
|
|
340
|
+
background: transparent;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.custom-scrollbar-container::-webkit-scrollbar-thumb {
|
|
344
|
+
background: rgba(0, 0, 0, 0.2);
|
|
345
|
+
border-radius: 3px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.custom-scrollbar-container::-webkit-scrollbar-thumb:hover {
|
|
349
|
+
background: rgba(0, 0, 0, 0.4);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.custom-scrollbar-content {
|
|
353
|
+
width: 100%;
|
|
354
|
+
}
|
|
355
|
+
|
|
325
356
|
.context-explorer.open {
|
|
326
357
|
left: var(--context-offset);
|
|
327
358
|
opacity: 1;
|
|
@@ -494,170 +525,95 @@ export class Simulator extends RapidElement {
|
|
|
494
525
|
}
|
|
495
526
|
|
|
496
527
|
.phone-screen {
|
|
528
|
+
position: absolute;
|
|
529
|
+
top: 0;
|
|
530
|
+
left: 0;
|
|
531
|
+
right: 0;
|
|
532
|
+
bottom: 0;
|
|
497
533
|
background: white;
|
|
498
|
-
padding: 15px;
|
|
499
|
-
padding-top: calc(var(--cutout-height) + 10px);
|
|
500
|
-
padding-bottom: 60px;
|
|
501
|
-
height: var(--phone-screen-height);
|
|
502
|
-
overflow-y: scroll;
|
|
503
534
|
display: flex;
|
|
504
535
|
flex-direction: column;
|
|
505
|
-
scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
|
|
506
|
-
scrollbar-width: thin;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
.phone-screen::-webkit-scrollbar {
|
|
510
|
-
width: 8px;
|
|
511
536
|
}
|
|
512
537
|
|
|
513
|
-
|
|
514
|
-
|
|
538
|
+
temba-chat {
|
|
539
|
+
flex: 1;
|
|
540
|
+
display: flex;
|
|
541
|
+
flex-direction: column;
|
|
542
|
+
min-height: 0;
|
|
543
|
+
--color-chat-in: #e5e5ea;
|
|
544
|
+
--color-chat-out: #007aff;
|
|
545
|
+
--chat-top-padding: calc(var(--cutout-height));
|
|
546
|
+
--chat-bottom-padding: calc(var(--bottom-input-height, 80px) - 10px);
|
|
515
547
|
}
|
|
516
548
|
|
|
517
|
-
.
|
|
518
|
-
|
|
519
|
-
|
|
549
|
+
.bottom-input-container {
|
|
550
|
+
position: absolute;
|
|
551
|
+
bottom: 0px;
|
|
552
|
+
left: 0px;
|
|
553
|
+
right: 0px;
|
|
554
|
+
z-index: 10;
|
|
520
555
|
}
|
|
521
556
|
|
|
522
|
-
.
|
|
523
|
-
|
|
557
|
+
.bottom-input-container::before {
|
|
558
|
+
content: '';
|
|
559
|
+
position: absolute;
|
|
560
|
+
top: 0;
|
|
561
|
+
left: 0;
|
|
562
|
+
right: 0;
|
|
563
|
+
bottom: 0;
|
|
564
|
+
background: rgba(255, 255, 255, 0.45);
|
|
565
|
+
backdrop-filter: blur(10px);
|
|
566
|
+
-webkit-mask-image: linear-gradient(to bottom, transparent, black 20px);
|
|
567
|
+
mask-image: linear-gradient(to bottom, transparent, black 20px);
|
|
568
|
+
z-index: -1;
|
|
524
569
|
}
|
|
525
570
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
opacity: 1;
|
|
533
|
-
transform: scale(1.05);
|
|
534
|
-
}
|
|
535
|
-
100% {
|
|
536
|
-
opacity: 1;
|
|
537
|
-
transform: scale(1);
|
|
538
|
-
}
|
|
571
|
+
.quick-replies-container {
|
|
572
|
+
display: flex;
|
|
573
|
+
flex-wrap: wrap;
|
|
574
|
+
justify-content: center;
|
|
575
|
+
gap: 6px;
|
|
576
|
+
z-index: 9;
|
|
539
577
|
}
|
|
540
578
|
|
|
541
|
-
.
|
|
542
|
-
padding:
|
|
543
|
-
margin-bottom: 8px;
|
|
579
|
+
.quick-reply-btn {
|
|
580
|
+
padding: 4px 8px;
|
|
544
581
|
border-radius: 18px;
|
|
545
|
-
|
|
546
|
-
font-size: 13px;
|
|
547
|
-
line-height: 1.2;
|
|
548
|
-
}
|
|
549
|
-
.message.animated {
|
|
550
|
-
animation: messageAppear var(--animation-time) ease-out forwards;
|
|
551
|
-
opacity: 0;
|
|
552
|
-
}
|
|
553
|
-
.message.incoming {
|
|
554
|
-
background: #e5e5ea;
|
|
555
|
-
color: #000;
|
|
556
|
-
margin-right: auto;
|
|
557
|
-
border-bottom-left-radius: 4px;
|
|
558
|
-
}
|
|
559
|
-
.message.outgoing {
|
|
560
|
-
background: #007aff;
|
|
561
|
-
color: white;
|
|
562
|
-
margin-left: auto;
|
|
563
|
-
text-align: left;
|
|
564
|
-
border-bottom-right-radius: 4px;
|
|
565
|
-
}
|
|
566
|
-
.attachment-wrapper {
|
|
567
|
-
max-width: 70%;
|
|
568
|
-
margin-bottom: 8px;
|
|
569
|
-
display: flex;
|
|
570
|
-
flex-direction: column;
|
|
571
|
-
gap: 4px;
|
|
572
|
-
}
|
|
573
|
-
.attachment-wrapper.incoming {
|
|
574
|
-
margin-right: auto;
|
|
575
|
-
align-items: flex-start;
|
|
576
|
-
}
|
|
577
|
-
.attachment-wrapper.outgoing {
|
|
578
|
-
margin-left: auto;
|
|
579
|
-
align-items: flex-end;
|
|
580
|
-
}
|
|
581
|
-
.attachment-wrapper.animated {
|
|
582
|
-
animation: messageAppear var(--animation-time) ease-out forwards;
|
|
583
|
-
opacity: 0;
|
|
584
|
-
}
|
|
585
|
-
.attachment {
|
|
586
|
-
border-radius: 12px;
|
|
587
|
-
overflow: hidden;
|
|
588
|
-
max-width: 100%;
|
|
589
|
-
}
|
|
590
|
-
.attachment img {
|
|
591
|
-
max-width: 100%;
|
|
592
|
-
display: block;
|
|
593
|
-
border-radius: 12px;
|
|
594
|
-
}
|
|
595
|
-
.attachment video {
|
|
596
|
-
max-width: 100%;
|
|
597
|
-
display: block;
|
|
598
|
-
border-radius: 12px;
|
|
599
|
-
}
|
|
600
|
-
.attachment-audio {
|
|
601
|
-
display: flex;
|
|
602
|
-
align-items: center;
|
|
603
|
-
gap: 8px;
|
|
604
|
-
padding: 6px;
|
|
605
|
-
background: white;
|
|
606
|
-
border: 1px solid #e5e5ea;
|
|
607
|
-
border-radius: 12px;
|
|
608
|
-
min-width: 160px;
|
|
609
|
-
}
|
|
610
|
-
.attachment-wrapper.outgoing .attachment-audio {
|
|
582
|
+
border: 1px solid var(--color-primary, #007aff);
|
|
611
583
|
background: white;
|
|
612
|
-
|
|
613
|
-
}
|
|
614
|
-
.attachment-audio audio {
|
|
615
|
-
flex: 1;
|
|
616
|
-
max-height: 30px;
|
|
617
|
-
}
|
|
618
|
-
.attachment-location {
|
|
619
|
-
border-radius: 12px;
|
|
620
|
-
overflow: hidden;
|
|
621
|
-
}
|
|
622
|
-
.event-info {
|
|
623
|
-
text-align: center;
|
|
584
|
+
color: var(--color-primary, #007aff);
|
|
624
585
|
font-size: 11px;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
line-height: 1.3;
|
|
586
|
+
cursor: pointer;
|
|
587
|
+
transition: all 0.2s ease;
|
|
588
|
+
flex-shrink: 0;
|
|
629
589
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
590
|
+
|
|
591
|
+
.quick-reply-btn:hover:not(:disabled) {
|
|
592
|
+
background: var(--color-primary, #007aff);
|
|
593
|
+
color: white;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.quick-reply-btn:disabled {
|
|
597
|
+
opacity: 0.5;
|
|
598
|
+
cursor: not-allowed;
|
|
633
599
|
}
|
|
600
|
+
|
|
634
601
|
.message-input {
|
|
635
|
-
background: linear-gradient(
|
|
636
|
-
to top,
|
|
637
|
-
rgba(0, 0, 0, 0.1) 0%,
|
|
638
|
-
rgba(0, 0, 0, 0.05) 70%,
|
|
639
|
-
transparent 100%
|
|
640
|
-
);
|
|
641
602
|
padding: 8px 12px;
|
|
642
603
|
border-top: none;
|
|
643
604
|
display: flex;
|
|
644
605
|
align-items: center;
|
|
645
606
|
gap: 8px;
|
|
646
|
-
position: absolute;
|
|
647
|
-
bottom: 0px;
|
|
648
|
-
left: 0px;
|
|
649
|
-
right: 0px;
|
|
650
607
|
z-index: 10;
|
|
651
608
|
}
|
|
652
609
|
.message-input input {
|
|
653
610
|
flex: 1;
|
|
654
|
-
border: 1px solid #
|
|
611
|
+
border: 1px solid #c6c6c857;
|
|
655
612
|
border-radius: 20px;
|
|
656
613
|
padding: 8px 15px;
|
|
657
614
|
font-size: 15px;
|
|
658
615
|
margin-bottom: 5px;
|
|
659
|
-
|
|
660
|
-
border: none;
|
|
616
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
661
617
|
outline: none;
|
|
662
618
|
}
|
|
663
619
|
.message-input input::placeholder {
|
|
@@ -668,7 +624,7 @@ export class Simulator extends RapidElement {
|
|
|
668
624
|
height: 30px;
|
|
669
625
|
border-radius: 50%;
|
|
670
626
|
background: #fff;
|
|
671
|
-
border:
|
|
627
|
+
border: 1px solid #c6c6c857;
|
|
672
628
|
display: flex;
|
|
673
629
|
align-items: center;
|
|
674
630
|
justify-content: center;
|
|
@@ -676,6 +632,7 @@ export class Simulator extends RapidElement {
|
|
|
676
632
|
flex-shrink: 0;
|
|
677
633
|
margin-bottom: 5px;
|
|
678
634
|
transition: all var(--animation-time) ease;
|
|
635
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
679
636
|
color: #000;
|
|
680
637
|
}
|
|
681
638
|
.attachment-button:hover {
|
|
@@ -725,37 +682,6 @@ export class Simulator extends RapidElement {
|
|
|
725
682
|
.attachment-menu-item temba-icon {
|
|
726
683
|
color: #007aff;
|
|
727
684
|
}
|
|
728
|
-
.quick-replies {
|
|
729
|
-
display: flex;
|
|
730
|
-
flex-wrap: wrap;
|
|
731
|
-
justify-content: center;
|
|
732
|
-
gap: 8px;
|
|
733
|
-
margin-top: 4px;
|
|
734
|
-
margin-bottom: 8px;
|
|
735
|
-
}
|
|
736
|
-
.quick-reply-btn {
|
|
737
|
-
background: white;
|
|
738
|
-
color: #007aff;
|
|
739
|
-
border: 1px solid #007aff;
|
|
740
|
-
border-radius: 18px;
|
|
741
|
-
padding: 4px 8px;
|
|
742
|
-
font-size: 11px;
|
|
743
|
-
cursor: pointer;
|
|
744
|
-
transition: all var(--animation-time) ease;
|
|
745
|
-
white-space: nowrap;
|
|
746
|
-
}
|
|
747
|
-
.quick-reply-btn:hover {
|
|
748
|
-
background: #007aff;
|
|
749
|
-
color: white;
|
|
750
|
-
cursor: pointer;
|
|
751
|
-
}
|
|
752
|
-
.quick-reply-btn:active {
|
|
753
|
-
transform: scale(0.95);
|
|
754
|
-
}
|
|
755
|
-
.quick-reply-btn.animated {
|
|
756
|
-
animation: messageAppear var(--animation-time) ease-out forwards;
|
|
757
|
-
opacity: 0;
|
|
758
|
-
}
|
|
759
685
|
`;
|
|
760
686
|
}
|
|
761
687
|
|
|
@@ -772,9 +698,10 @@ export class Simulator extends RapidElement {
|
|
|
772
698
|
size: 'small' | 'medium' | 'large';
|
|
773
699
|
|
|
774
700
|
@property({ type: Array })
|
|
775
|
-
private events:
|
|
701
|
+
private events: ContactEvent[] = [];
|
|
776
702
|
|
|
777
703
|
private previousEventCount = 0;
|
|
704
|
+
private chat: Chat = null;
|
|
778
705
|
|
|
779
706
|
@property({ type: Object })
|
|
780
707
|
private session: Session | null = null;
|
|
@@ -869,10 +796,130 @@ export class Simulator extends RapidElement {
|
|
|
869
796
|
return config.contextWidth + config.contextOffset - config.phoneWidth;
|
|
870
797
|
}
|
|
871
798
|
|
|
799
|
+
public connectedCallback() {
|
|
800
|
+
super.connectedCallback();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
protected firstUpdated(
|
|
804
|
+
changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
805
|
+
): void {
|
|
806
|
+
super.firstUpdated(changes);
|
|
807
|
+
this.chat = this.shadowRoot.querySelector('temba-chat');
|
|
808
|
+
|
|
809
|
+
// if we have events that were collected before chat was ready, add them now
|
|
810
|
+
if (this.chat && this.events.length > 0) {
|
|
811
|
+
this.chat.addMessages(this.events, null, true);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
this.setupCustomScrollbar();
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private setupCustomScrollbar() {
|
|
818
|
+
const chat = this.shadowRoot?.querySelector('temba-chat') as Chat;
|
|
819
|
+
const scrollContainer = this.shadowRoot?.querySelector(
|
|
820
|
+
'.custom-scrollbar-container'
|
|
821
|
+
) as HTMLElement;
|
|
822
|
+
const scrollContent = this.shadowRoot?.querySelector(
|
|
823
|
+
'.custom-scrollbar-content'
|
|
824
|
+
) as HTMLElement;
|
|
825
|
+
|
|
826
|
+
if (!chat || !scrollContainer || !scrollContent) return;
|
|
827
|
+
|
|
828
|
+
chat.updateComplete.then(() => {
|
|
829
|
+
const chatScroll = chat.shadowRoot?.querySelector(
|
|
830
|
+
'.scroll'
|
|
831
|
+
) as HTMLElement;
|
|
832
|
+
if (!chatScroll) return;
|
|
833
|
+
|
|
834
|
+
let ignoreScroll = false;
|
|
835
|
+
|
|
836
|
+
// Sync from chat to custom scrollbar
|
|
837
|
+
chatScroll.addEventListener('scroll', () => {
|
|
838
|
+
if (!ignoreScroll) {
|
|
839
|
+
ignoreScroll = true;
|
|
840
|
+
// Chat: 0 (bottom) ... -Max (top) (Negative scrolling)
|
|
841
|
+
// Custom: Max (bottom) ... 0 (top) (Positive scrolling)
|
|
842
|
+
|
|
843
|
+
const maxScroll =
|
|
844
|
+
scrollContainer.scrollHeight - scrollContainer.clientHeight;
|
|
845
|
+
// Math.abs to handle negative scrollTop
|
|
846
|
+
const distanceFromBottom = Math.abs(chatScroll.scrollTop);
|
|
847
|
+
const newCustomScrollTop = maxScroll - distanceFromBottom;
|
|
848
|
+
|
|
849
|
+
scrollContainer.scrollTop = newCustomScrollTop;
|
|
850
|
+
|
|
851
|
+
requestAnimationFrame(() => (ignoreScroll = false));
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
// Sync from custom scrollbar to chat
|
|
856
|
+
scrollContainer.addEventListener('scroll', () => {
|
|
857
|
+
if (!ignoreScroll) {
|
|
858
|
+
ignoreScroll = true;
|
|
859
|
+
|
|
860
|
+
const maxScroll =
|
|
861
|
+
scrollContainer.scrollHeight - scrollContainer.clientHeight;
|
|
862
|
+
const distanceFromBottom = maxScroll - scrollContainer.scrollTop;
|
|
863
|
+
|
|
864
|
+
// chat scrollTop should be -distanceFromBottom
|
|
865
|
+
chatScroll.scrollTop = -distanceFromBottom;
|
|
866
|
+
|
|
867
|
+
requestAnimationFrame(() => (ignoreScroll = false));
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// Sync height
|
|
872
|
+
const syncHeight = () => {
|
|
873
|
+
const chatMaxScroll = chatScroll.scrollHeight - chatScroll.clientHeight;
|
|
874
|
+
const customClientHeight = scrollContainer.clientHeight;
|
|
875
|
+
|
|
876
|
+
// ensure minimum height
|
|
877
|
+
if (chatMaxScroll <= 0) {
|
|
878
|
+
scrollContent.style.height = '100%';
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const newHeight = chatMaxScroll + customClientHeight;
|
|
883
|
+
scrollContent.style.height = `${newHeight}px`;
|
|
884
|
+
|
|
885
|
+
// If we were effectively at the bottom, stay at the bottom
|
|
886
|
+
// This is a heuristic, assuming if we're close enough we're "at bottom"
|
|
887
|
+
// But the Chat component handles scrollToBottom on new messages, which fires scroll event,
|
|
888
|
+
// which updates us. So we might not need to force it here unless resize happens without message.
|
|
889
|
+
if (Math.abs(chatScroll.scrollTop) < 5) {
|
|
890
|
+
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
// Observe changes
|
|
895
|
+
const observer = new MutationObserver(syncHeight);
|
|
896
|
+
observer.observe(chatScroll, {
|
|
897
|
+
childList: true,
|
|
898
|
+
subtree: true,
|
|
899
|
+
attributes: true
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
const resizeObserver = new ResizeObserver(syncHeight);
|
|
903
|
+
resizeObserver.observe(chatScroll);
|
|
904
|
+
|
|
905
|
+
// Initial sync
|
|
906
|
+
syncHeight();
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
872
910
|
protected updated(
|
|
873
911
|
changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
874
912
|
): void {
|
|
875
913
|
super.updated(changes);
|
|
914
|
+
|
|
915
|
+
if (
|
|
916
|
+
changes.has('currentQuickReplies') ||
|
|
917
|
+
changes.has('keyboardVisible') ||
|
|
918
|
+
changes.has('attachmentMenuOpen')
|
|
919
|
+
) {
|
|
920
|
+
this.updateBottomInputHeight();
|
|
921
|
+
}
|
|
922
|
+
|
|
876
923
|
if (changes.has('flow') && this.flow) {
|
|
877
924
|
this.endpoint = `/flow/simulate/${this.flow}/`;
|
|
878
925
|
}
|
|
@@ -978,8 +1025,13 @@ export class Simulator extends RapidElement {
|
|
|
978
1025
|
this.isVisible = true;
|
|
979
1026
|
getStore().getState().setSimulatorActive(true);
|
|
980
1027
|
|
|
1028
|
+
// ensure chat component is available
|
|
1029
|
+
if (!this.chat) {
|
|
1030
|
+
this.chat = this.shadowRoot.querySelector('temba-chat');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
981
1033
|
// start the simulation if we haven't already
|
|
982
|
-
if (this.
|
|
1034
|
+
if (!this.session) {
|
|
983
1035
|
this.startFlow();
|
|
984
1036
|
}
|
|
985
1037
|
}
|
|
@@ -1005,20 +1057,37 @@ export class Simulator extends RapidElement {
|
|
|
1005
1057
|
this.updateRunContext(response.json as RunContext);
|
|
1006
1058
|
} catch (error) {
|
|
1007
1059
|
console.error('Failed to start simulation:', error);
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1060
|
+
const errorEvent = {
|
|
1061
|
+
uuid: generateUUIDv7(),
|
|
1062
|
+
type: 'error',
|
|
1063
|
+
created_on: new Date(now),
|
|
1064
|
+
_rendered: {
|
|
1065
|
+
html: html`<p>Failed to start simulation</p>`,
|
|
1066
|
+
type: MessageType.Error
|
|
1067
|
+
}
|
|
1068
|
+
} as ContactEvent;
|
|
1069
|
+
if (this.chat) {
|
|
1070
|
+
this.chat.addMessages([errorEvent], null, true);
|
|
1071
|
+
} else {
|
|
1072
|
+
this.events = [...this.events, errorEvent];
|
|
1073
|
+
}
|
|
1016
1074
|
}
|
|
1017
1075
|
}
|
|
1018
1076
|
|
|
1019
|
-
private updateRunContext(runContext: RunContext, msgInEvt?:
|
|
1077
|
+
private updateRunContext(runContext: RunContext, msgInEvt?: ContactEvent) {
|
|
1078
|
+
const newEvents: ContactEvent[] = [];
|
|
1079
|
+
|
|
1080
|
+
// add the user's message if provided
|
|
1020
1081
|
if (msgInEvt) {
|
|
1021
|
-
|
|
1082
|
+
// ensure it has a UUID
|
|
1083
|
+
if (!msgInEvt.uuid) {
|
|
1084
|
+
msgInEvt.uuid = generateUUIDv7();
|
|
1085
|
+
}
|
|
1086
|
+
// ensure created_on is a Date object
|
|
1087
|
+
if (typeof msgInEvt.created_on === 'string') {
|
|
1088
|
+
msgInEvt.created_on = new Date(msgInEvt.created_on);
|
|
1089
|
+
}
|
|
1090
|
+
newEvents.push(msgInEvt);
|
|
1022
1091
|
}
|
|
1023
1092
|
|
|
1024
1093
|
if (runContext.session) {
|
|
@@ -1035,20 +1104,70 @@ export class Simulator extends RapidElement {
|
|
|
1035
1104
|
this.context = runContext.context;
|
|
1036
1105
|
}
|
|
1037
1106
|
|
|
1107
|
+
// extract quick replies from the most recent sprint
|
|
1108
|
+
this.currentQuickReplies = [];
|
|
1109
|
+
|
|
1038
1110
|
if (runContext.events && runContext.events.length > 0) {
|
|
1039
|
-
|
|
1111
|
+
for (const rawEvent of runContext.events) {
|
|
1112
|
+
// skip msg_received events from the server since we already added the user's message
|
|
1113
|
+
if (rawEvent.type === 'msg_received') {
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// skip msg_created events without a proper msg property
|
|
1118
|
+
if (rawEvent.type === 'msg_created' && !(rawEvent as any).msg) {
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1040
1121
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1122
|
+
// convert to ContactEvent
|
|
1123
|
+
const event: ContactEvent = {
|
|
1124
|
+
...rawEvent,
|
|
1125
|
+
uuid: rawEvent.uuid || generateUUIDv7(),
|
|
1126
|
+
created_on:
|
|
1127
|
+
typeof rawEvent.created_on === 'string'
|
|
1128
|
+
? new Date(rawEvent.created_on)
|
|
1129
|
+
: rawEvent.created_on
|
|
1130
|
+
} as ContactEvent;
|
|
1131
|
+
|
|
1132
|
+
// pre-render non-message events
|
|
1133
|
+
this.prerenderEvent(event);
|
|
1134
|
+
|
|
1135
|
+
// extract quick replies from msg_created events
|
|
1136
|
+
if (event.type === 'msg_created' && (event as any).msg?.quick_replies) {
|
|
1137
|
+
this.currentQuickReplies = (event as any).msg.quick_replies;
|
|
1046
1138
|
}
|
|
1139
|
+
|
|
1140
|
+
const isMessage = event.type === 'msg_created';
|
|
1141
|
+
const msg = (event as any).msg;
|
|
1142
|
+
|
|
1143
|
+
// Check if the event should be displayed.
|
|
1144
|
+
// 1. If it's a message, it must have text or attachments
|
|
1145
|
+
if (isMessage) {
|
|
1146
|
+
const hasText = msg.text && msg.text.trim().length > 0;
|
|
1147
|
+
const hasAttachments = msg.attachments && msg.attachments.length > 0;
|
|
1148
|
+
if (!hasText && !hasAttachments) {
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
// 2. If it's not a message, it must have been rendered by prerenderEvent
|
|
1153
|
+
else if (!event._rendered) {
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
newEvents.push(event);
|
|
1047
1158
|
}
|
|
1048
1159
|
}
|
|
1049
1160
|
|
|
1161
|
+
// add all new events to chat component if it exists
|
|
1162
|
+
if (this.chat) {
|
|
1163
|
+
this.chat.addMessages(newEvents, null, true);
|
|
1164
|
+
} else {
|
|
1165
|
+
// fallback: store events and add them once chat is ready
|
|
1166
|
+
this.events = [...this.events, ...newEvents];
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1050
1169
|
this.sprinting = false;
|
|
1051
|
-
this.requestUpdate();
|
|
1170
|
+
this.requestUpdate(); // trigger re-render for quick replies
|
|
1052
1171
|
this.scrollToBottom();
|
|
1053
1172
|
this.updateActivity();
|
|
1054
1173
|
}
|
|
@@ -1127,6 +1246,19 @@ export class Simulator extends RapidElement {
|
|
|
1127
1246
|
}
|
|
1128
1247
|
|
|
1129
1248
|
private scrollToBottom() {
|
|
1249
|
+
if (this.chat) {
|
|
1250
|
+
// chat component handles scrolling, but we still need to focus input
|
|
1251
|
+
this.chat.scrollToBottom();
|
|
1252
|
+
setTimeout(() => {
|
|
1253
|
+
const input = this.shadowRoot?.querySelector(
|
|
1254
|
+
'.message-input input'
|
|
1255
|
+
) as HTMLInputElement;
|
|
1256
|
+
if (input) {
|
|
1257
|
+
input.focus();
|
|
1258
|
+
}
|
|
1259
|
+
}, 50);
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1130
1262
|
// wait for render, then scroll to bottom
|
|
1131
1263
|
setTimeout(() => {
|
|
1132
1264
|
const screen = this.shadowRoot?.querySelector('.phone-screen');
|
|
@@ -1146,16 +1278,52 @@ export class Simulator extends RapidElement {
|
|
|
1146
1278
|
}, 50);
|
|
1147
1279
|
}
|
|
1148
1280
|
|
|
1281
|
+
private prerenderEvent(event: ContactEvent) {
|
|
1282
|
+
// skip if already rendered or is a message event
|
|
1283
|
+
if (
|
|
1284
|
+
event._rendered ||
|
|
1285
|
+
event.type === Events.MSG_CREATED ||
|
|
1286
|
+
event.type === Events.MSG_RECEIVED
|
|
1287
|
+
) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// handle simulator-specific events (errors, warnings, failures)
|
|
1292
|
+
if (event.type === 'error' || event.type === 'failure') {
|
|
1293
|
+
event._rendered = {
|
|
1294
|
+
html: renderEvent(event, true),
|
|
1295
|
+
type: MessageType.Error
|
|
1296
|
+
};
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if (event.type === 'warning') {
|
|
1301
|
+
event._rendered = {
|
|
1302
|
+
html: renderEvent(event, true),
|
|
1303
|
+
type: MessageType.Note
|
|
1304
|
+
};
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// try to render as a standard event
|
|
1309
|
+
const rendered = renderEvent(event, true);
|
|
1310
|
+
if (rendered) {
|
|
1311
|
+
event._rendered = {
|
|
1312
|
+
html: rendered,
|
|
1313
|
+
type: MessageType.Inline
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1149
1318
|
private handleClose() {
|
|
1150
1319
|
const phoneWindow = this.shadowRoot.getElementById(
|
|
1151
1320
|
'phone-window'
|
|
1152
1321
|
) as FloatingWindow;
|
|
1153
|
-
phoneWindow.hide();
|
|
1322
|
+
// phoneWindow.hide();
|
|
1323
|
+
|
|
1324
|
+
phoneWindow.handleClose();
|
|
1154
1325
|
this.isVisible = false;
|
|
1155
1326
|
getStore().getState().setSimulatorActive(false);
|
|
1156
|
-
|
|
1157
|
-
const phoneTab = this.shadowRoot.getElementById('phone-tab') as FloatingTab;
|
|
1158
|
-
phoneTab.hidden = false;
|
|
1159
1327
|
}
|
|
1160
1328
|
|
|
1161
1329
|
private handleReset() {
|
|
@@ -1168,6 +1336,11 @@ export class Simulator extends RapidElement {
|
|
|
1168
1336
|
this.previousEventCount = 0;
|
|
1169
1337
|
this.currentQuickReplies = [];
|
|
1170
1338
|
|
|
1339
|
+
// reset chat component
|
|
1340
|
+
if (this.chat) {
|
|
1341
|
+
this.chat.reset();
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1171
1344
|
// Clear simulator activity data
|
|
1172
1345
|
getStore().getState().updateSimulatorActivity({
|
|
1173
1346
|
segments: {},
|
|
@@ -1387,20 +1560,36 @@ export class Simulator extends RapidElement {
|
|
|
1387
1560
|
this.attachmentMenuOpen = false;
|
|
1388
1561
|
|
|
1389
1562
|
const now = new Date().toISOString();
|
|
1390
|
-
|
|
1391
|
-
|
|
1563
|
+
|
|
1564
|
+
// create the event for the API (with ISO string date)
|
|
1565
|
+
const msgInEvtForAPI = {
|
|
1566
|
+
uuid: generateUUIDv7(),
|
|
1392
1567
|
type: 'msg_received',
|
|
1393
1568
|
created_on: now,
|
|
1394
1569
|
msg: {
|
|
1395
|
-
uuid:
|
|
1570
|
+
uuid: generateUUIDv7(),
|
|
1396
1571
|
text: text || '',
|
|
1397
1572
|
urn: this.contact.urns[0],
|
|
1398
|
-
|
|
1573
|
+
direction: 'in',
|
|
1574
|
+
type: 'text',
|
|
1575
|
+
attachments: attachment ? [attachment] : [],
|
|
1576
|
+
quick_replies: [],
|
|
1577
|
+
channel: { uuid: generateUUIDv7(), name: 'Simulator' }
|
|
1399
1578
|
}
|
|
1400
1579
|
};
|
|
1401
1580
|
|
|
1402
|
-
//
|
|
1403
|
-
|
|
1581
|
+
// create the ContactEvent for display (with Date object)
|
|
1582
|
+
const msgInEvt = {
|
|
1583
|
+
...msgInEvtForAPI,
|
|
1584
|
+
created_on: new Date(now)
|
|
1585
|
+
} as ContactEvent;
|
|
1586
|
+
|
|
1587
|
+
// show user's message immediately via chat component
|
|
1588
|
+
if (this.chat) {
|
|
1589
|
+
this.chat.addMessages([msgInEvt], null, true);
|
|
1590
|
+
} else {
|
|
1591
|
+
this.events = [...this.events, msgInEvt];
|
|
1592
|
+
}
|
|
1404
1593
|
this.requestUpdate();
|
|
1405
1594
|
this.scrollToBottom();
|
|
1406
1595
|
|
|
@@ -1409,7 +1598,7 @@ export class Simulator extends RapidElement {
|
|
|
1409
1598
|
contact: this.contact,
|
|
1410
1599
|
resume: {
|
|
1411
1600
|
type: 'msg',
|
|
1412
|
-
event:
|
|
1601
|
+
event: msgInEvtForAPI,
|
|
1413
1602
|
resumed_on: now
|
|
1414
1603
|
}
|
|
1415
1604
|
};
|
|
@@ -1424,14 +1613,20 @@ export class Simulator extends RapidElement {
|
|
|
1424
1613
|
this.updateRunContext(response.json as RunContext, null);
|
|
1425
1614
|
} catch (error) {
|
|
1426
1615
|
console.error('Failed to resume simulation:', error);
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1616
|
+
const errorEvent = {
|
|
1617
|
+
uuid: generateUUIDv7(),
|
|
1618
|
+
type: 'error',
|
|
1619
|
+
created_on: new Date(now),
|
|
1620
|
+
_rendered: {
|
|
1621
|
+
html: html`<p>Failed to send message</p>`,
|
|
1622
|
+
type: MessageType.Error
|
|
1623
|
+
}
|
|
1624
|
+
} as ContactEvent;
|
|
1625
|
+
if (this.chat) {
|
|
1626
|
+
this.chat.addMessages([errorEvent], null, true);
|
|
1627
|
+
} else {
|
|
1628
|
+
this.events = [...this.events, errorEvent];
|
|
1629
|
+
}
|
|
1435
1630
|
this.sprinting = false;
|
|
1436
1631
|
}
|
|
1437
1632
|
}
|
|
@@ -1451,9 +1646,9 @@ export class Simulator extends RapidElement {
|
|
|
1451
1646
|
this.inputValue = input.value;
|
|
1452
1647
|
}
|
|
1453
1648
|
|
|
1454
|
-
private
|
|
1455
|
-
if (!this.sprinting) {
|
|
1456
|
-
this.resume(
|
|
1649
|
+
private handleQuickReplyClick(text: string) {
|
|
1650
|
+
if (!this.sprinting && text) {
|
|
1651
|
+
this.resume(text);
|
|
1457
1652
|
}
|
|
1458
1653
|
}
|
|
1459
1654
|
|
|
@@ -1508,310 +1703,16 @@ export class Simulator extends RapidElement {
|
|
|
1508
1703
|
}
|
|
1509
1704
|
}
|
|
1510
1705
|
|
|
1511
|
-
private
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
}
|
|
1520
|
-
if (removedGroups.length > 0) {
|
|
1521
|
-
const groupNames = removedGroups
|
|
1522
|
-
.map((g: any) => `"${g.name}"`)
|
|
1523
|
-
.join(', ');
|
|
1524
|
-
return `Removed from ${groupNames}`;
|
|
1525
|
-
}
|
|
1526
|
-
break;
|
|
1527
|
-
}
|
|
1528
|
-
case 'contact_field_changed': {
|
|
1529
|
-
const field = (event as any).field;
|
|
1530
|
-
const value = (event as any).value;
|
|
1531
|
-
const valueText = value ? value.text || value : '';
|
|
1532
|
-
if (field) {
|
|
1533
|
-
if (valueText) {
|
|
1534
|
-
return `Set contact "${field.name}" to "${valueText}"`;
|
|
1535
|
-
} else {
|
|
1536
|
-
return `Cleared contact "${field.name}"`;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
break;
|
|
1540
|
-
}
|
|
1541
|
-
case 'contact_language_changed':
|
|
1542
|
-
return `Set preferred language to "${(event as any).language}"`;
|
|
1543
|
-
case 'contact_name_changed':
|
|
1544
|
-
return `Set contact name to "${(event as any).name}"`;
|
|
1545
|
-
case 'contact_status_changed':
|
|
1546
|
-
return `Set status to "${(event as any).status}"`;
|
|
1547
|
-
case 'contact_urns_changed':
|
|
1548
|
-
return `Added a URN for the contact`;
|
|
1549
|
-
case 'input_labels_added': {
|
|
1550
|
-
const labels = (event as any).labels || [];
|
|
1551
|
-
if (labels.length > 0) {
|
|
1552
|
-
const labelNames = labels.map((l: any) => `"${l.name}"`).join(', ');
|
|
1553
|
-
return `Message labeled with ${labelNames}`;
|
|
1554
|
-
}
|
|
1555
|
-
break;
|
|
1556
|
-
}
|
|
1557
|
-
case 'run_result_changed':
|
|
1558
|
-
return `Set result "${(event as any).name}" to "${
|
|
1559
|
-
(event as any).value
|
|
1560
|
-
}"`;
|
|
1561
|
-
case 'run_started':
|
|
1562
|
-
case 'flow_entered': {
|
|
1563
|
-
const flow = (event as any).flow;
|
|
1564
|
-
if (flow) {
|
|
1565
|
-
return `Entered flow "${flow.name}"`;
|
|
1566
|
-
}
|
|
1567
|
-
break;
|
|
1568
|
-
}
|
|
1569
|
-
case 'run_ended': {
|
|
1570
|
-
const flow = (event as any).flow;
|
|
1571
|
-
if (flow) {
|
|
1572
|
-
return `Exited flow "${flow.name}"`;
|
|
1573
|
-
}
|
|
1574
|
-
break;
|
|
1575
|
-
}
|
|
1576
|
-
case 'email_created':
|
|
1577
|
-
case 'email_sent': {
|
|
1578
|
-
const recipients = (event as any).to || (event as any).addresses || [];
|
|
1579
|
-
const subject = (event as any).subject;
|
|
1580
|
-
const recipientList = recipients
|
|
1581
|
-
.map((r: string) => `"${r}"`)
|
|
1582
|
-
.join(', ');
|
|
1583
|
-
return `Sent email to ${recipientList} with subject "${subject}"`;
|
|
1584
|
-
}
|
|
1585
|
-
case 'broadcast_created': {
|
|
1586
|
-
const translations = (event as any).translations;
|
|
1587
|
-
const baseLanguage = (event as any).base_language;
|
|
1588
|
-
if (translations && translations[baseLanguage]) {
|
|
1589
|
-
return `Sent broadcast: "${translations[baseLanguage].text}"`;
|
|
1590
|
-
}
|
|
1591
|
-
return `Sent broadcast`;
|
|
1706
|
+
private updateBottomInputHeight() {
|
|
1707
|
+
requestAnimationFrame(() => {
|
|
1708
|
+
const bottomContainer = this.shadowRoot?.querySelector(
|
|
1709
|
+
'.bottom-input-container'
|
|
1710
|
+
) as HTMLElement;
|
|
1711
|
+
if (bottomContainer) {
|
|
1712
|
+
const height = bottomContainer.offsetHeight;
|
|
1713
|
+
this.style.setProperty('--bottom-input-height', `${height}px`);
|
|
1592
1714
|
}
|
|
1593
|
-
case 'session_triggered': {
|
|
1594
|
-
const flow = (event as any).flow;
|
|
1595
|
-
if (flow) {
|
|
1596
|
-
return `Started somebody else in "${flow.name}"`;
|
|
1597
|
-
}
|
|
1598
|
-
break;
|
|
1599
|
-
}
|
|
1600
|
-
case 'ticket_opened': {
|
|
1601
|
-
const ticket = (event as any).ticket;
|
|
1602
|
-
if (ticket && ticket.topic) {
|
|
1603
|
-
return `Ticket opened with topic "${ticket.topic.name}"`;
|
|
1604
|
-
}
|
|
1605
|
-
return `Ticket opened`;
|
|
1606
|
-
}
|
|
1607
|
-
case 'resthook_called':
|
|
1608
|
-
return `Triggered flow event "${(event as any).resthook}"`;
|
|
1609
|
-
case 'webhook_called':
|
|
1610
|
-
return `Called ${(event as any).url}`;
|
|
1611
|
-
case 'service_called': {
|
|
1612
|
-
const service = (event as any).service;
|
|
1613
|
-
if (service === 'classifier') {
|
|
1614
|
-
return `Called classifier`;
|
|
1615
|
-
}
|
|
1616
|
-
return `Called ${service}`;
|
|
1617
|
-
}
|
|
1618
|
-
case 'airtime_transferred': {
|
|
1619
|
-
const amount = (event as any).actual_amount;
|
|
1620
|
-
const currency = (event as any).currency;
|
|
1621
|
-
const recipient = (event as any).recipient;
|
|
1622
|
-
if (amount && currency && recipient) {
|
|
1623
|
-
return `Transferred ${amount} ${currency} to ${recipient}`;
|
|
1624
|
-
}
|
|
1625
|
-
break;
|
|
1626
|
-
}
|
|
1627
|
-
case 'info':
|
|
1628
|
-
return (event as any).text;
|
|
1629
|
-
case 'warning':
|
|
1630
|
-
return `⚠️ ${(event as any).text}`;
|
|
1631
|
-
}
|
|
1632
|
-
return null;
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
private renderAttachment(attachment: string): TemplateResult {
|
|
1636
|
-
// parse attachment format: "type/subtype:url" or "geo:lat,long"
|
|
1637
|
-
const parts = attachment.split(':');
|
|
1638
|
-
const type = parts[0];
|
|
1639
|
-
const content = parts.slice(1).join(':'); // rejoin in case url has colons
|
|
1640
|
-
|
|
1641
|
-
if (type === 'geo') {
|
|
1642
|
-
// use temba-thumbnail for location to get map image
|
|
1643
|
-
return html`
|
|
1644
|
-
<div class="attachment-location">
|
|
1645
|
-
<temba-thumbnail attachment="${attachment}"></temba-thumbnail>
|
|
1646
|
-
</div>
|
|
1647
|
-
`;
|
|
1648
|
-
} else if (type.startsWith('image/')) {
|
|
1649
|
-
// custom image rendering
|
|
1650
|
-
return html`
|
|
1651
|
-
<div class="attachment">
|
|
1652
|
-
<img src="${content}" alt="Image attachment" />
|
|
1653
|
-
</div>
|
|
1654
|
-
`;
|
|
1655
|
-
} else if (type.startsWith('video/')) {
|
|
1656
|
-
// custom video rendering
|
|
1657
|
-
return html`
|
|
1658
|
-
<div class="attachment">
|
|
1659
|
-
<video controls>
|
|
1660
|
-
<source src="${content}" type="${type}" />
|
|
1661
|
-
</video>
|
|
1662
|
-
</div>
|
|
1663
|
-
`;
|
|
1664
|
-
} else if (type.startsWith('audio/')) {
|
|
1665
|
-
// custom audio rendering
|
|
1666
|
-
return html`
|
|
1667
|
-
<div class="attachment">
|
|
1668
|
-
<div class="attachment-audio">
|
|
1669
|
-
<audio controls>
|
|
1670
|
-
<source src="${content}" type="${type}" />
|
|
1671
|
-
</audio>
|
|
1672
|
-
</div>
|
|
1673
|
-
</div>
|
|
1674
|
-
`;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
// fallback for unknown types
|
|
1678
|
-
return html`
|
|
1679
|
-
<div class="attachment">
|
|
1680
|
-
<span>Attachment</span>
|
|
1681
|
-
</div>
|
|
1682
|
-
`;
|
|
1683
|
-
}
|
|
1684
|
-
|
|
1685
|
-
private renderMessages(): TemplateResult {
|
|
1686
|
-
if (this.events.length === 0) {
|
|
1687
|
-
return html`
|
|
1688
|
-
<div class="message incoming">👋 Welcome! Starting simulation...</div>
|
|
1689
|
-
`;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
const eventTemplates = this.events.map((event, index) => {
|
|
1693
|
-
// only animate messages that are new (beyond previous count)
|
|
1694
|
-
const isNew = index >= this.previousEventCount;
|
|
1695
|
-
const animatedClass = isNew ? 'animated' : '';
|
|
1696
|
-
// stagger animations for new messages
|
|
1697
|
-
const animationDelay = isNew
|
|
1698
|
-
? `${(index - this.previousEventCount) * 0.2}s`
|
|
1699
|
-
: '0s';
|
|
1700
|
-
|
|
1701
|
-
if (event.type === 'msg_received' && event.msg) {
|
|
1702
|
-
const hasAttachments =
|
|
1703
|
-
event.msg.attachments && event.msg.attachments.length > 0;
|
|
1704
|
-
const hasText = event.msg.text && event.msg.text.trim().length > 0;
|
|
1705
|
-
|
|
1706
|
-
return html`
|
|
1707
|
-
${hasAttachments
|
|
1708
|
-
? html`
|
|
1709
|
-
<div
|
|
1710
|
-
class="attachment-wrapper outgoing ${animatedClass}"
|
|
1711
|
-
style="animation-delay: ${animationDelay}"
|
|
1712
|
-
>
|
|
1713
|
-
${event.msg.attachments.map((att: string) =>
|
|
1714
|
-
this.renderAttachment(att)
|
|
1715
|
-
)}
|
|
1716
|
-
</div>
|
|
1717
|
-
`
|
|
1718
|
-
: html``}
|
|
1719
|
-
${hasText
|
|
1720
|
-
? html`
|
|
1721
|
-
<div
|
|
1722
|
-
class="message outgoing ${animatedClass}"
|
|
1723
|
-
style="animation-delay: ${animationDelay}"
|
|
1724
|
-
>
|
|
1725
|
-
${event.msg.text}
|
|
1726
|
-
</div>
|
|
1727
|
-
`
|
|
1728
|
-
: html``}
|
|
1729
|
-
`;
|
|
1730
|
-
} else if (event.type === 'msg_created' && event.msg) {
|
|
1731
|
-
const hasAttachments =
|
|
1732
|
-
event.msg.attachments && event.msg.attachments.length > 0;
|
|
1733
|
-
const hasText = event.msg.text && event.msg.text.trim().length > 0;
|
|
1734
|
-
|
|
1735
|
-
return html`
|
|
1736
|
-
${hasAttachments
|
|
1737
|
-
? html`
|
|
1738
|
-
<div
|
|
1739
|
-
class="attachment-wrapper incoming ${animatedClass}"
|
|
1740
|
-
style="animation-delay: ${animationDelay}"
|
|
1741
|
-
>
|
|
1742
|
-
${event.msg.attachments.map((att: string) =>
|
|
1743
|
-
this.renderAttachment(att)
|
|
1744
|
-
)}
|
|
1745
|
-
</div>
|
|
1746
|
-
`
|
|
1747
|
-
: html``}
|
|
1748
|
-
${hasText
|
|
1749
|
-
? html`
|
|
1750
|
-
<div
|
|
1751
|
-
class="message incoming ${animatedClass}"
|
|
1752
|
-
style="animation-delay: ${animationDelay}"
|
|
1753
|
-
>
|
|
1754
|
-
${event.msg.text}
|
|
1755
|
-
</div>
|
|
1756
|
-
`
|
|
1757
|
-
: html``}
|
|
1758
|
-
`;
|
|
1759
|
-
} else if (event.type === 'error') {
|
|
1760
|
-
return html`
|
|
1761
|
-
<div
|
|
1762
|
-
class="message incoming ${animatedClass}"
|
|
1763
|
-
style="background: #ff4444; color: white; animation-delay: ${animationDelay}"
|
|
1764
|
-
>
|
|
1765
|
-
⚠️ ${(event as any).text || 'An error occurred'}
|
|
1766
|
-
</div>
|
|
1767
|
-
`;
|
|
1768
|
-
} else {
|
|
1769
|
-
// check if this is an event we should display
|
|
1770
|
-
const description = this.getEventDescription(event);
|
|
1771
|
-
if (description) {
|
|
1772
|
-
return html`
|
|
1773
|
-
<div
|
|
1774
|
-
class="event-info ${animatedClass}"
|
|
1775
|
-
style="animation-delay: ${animationDelay}"
|
|
1776
|
-
>
|
|
1777
|
-
${description}
|
|
1778
|
-
</div>
|
|
1779
|
-
`;
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
return html``;
|
|
1783
1715
|
});
|
|
1784
|
-
|
|
1785
|
-
// render quick replies at the end if we have any from the most recent sprint
|
|
1786
|
-
const hasQuickReplies = this.currentQuickReplies.length > 0;
|
|
1787
|
-
const quickRepliesAnimationDelay =
|
|
1788
|
-
this.events.length >= this.previousEventCount
|
|
1789
|
-
? `${(this.events.length - this.previousEventCount) * 0.2}s`
|
|
1790
|
-
: '0s';
|
|
1791
|
-
|
|
1792
|
-
return html`
|
|
1793
|
-
${eventTemplates}
|
|
1794
|
-
${hasQuickReplies
|
|
1795
|
-
? html`
|
|
1796
|
-
<div
|
|
1797
|
-
class="quick-replies animated"
|
|
1798
|
-
style="animation-delay: ${quickRepliesAnimationDelay}"
|
|
1799
|
-
>
|
|
1800
|
-
${this.currentQuickReplies.map(
|
|
1801
|
-
(qr: any) => html`
|
|
1802
|
-
<button
|
|
1803
|
-
class="quick-reply-btn animated"
|
|
1804
|
-
style="animation-delay: ${quickRepliesAnimationDelay}"
|
|
1805
|
-
@click=${() => this.handleQuickReply(qr.text)}
|
|
1806
|
-
>
|
|
1807
|
-
${qr.text}
|
|
1808
|
-
</button>
|
|
1809
|
-
`
|
|
1810
|
-
)}
|
|
1811
|
-
</div>
|
|
1812
|
-
`
|
|
1813
|
-
: html``}
|
|
1814
|
-
`;
|
|
1815
1716
|
}
|
|
1816
1717
|
|
|
1817
1718
|
protected render(): TemplateResult {
|
|
@@ -1835,17 +1736,19 @@ export class Simulator extends RapidElement {
|
|
|
1835
1736
|
--cutout-island-width: ${config.cutoutIslandWidth}px;
|
|
1836
1737
|
--cutout-island-height: ${config.cutoutIslandHeight}px;
|
|
1837
1738
|
--cutout-island-top: ${config.cutoutIslandTop}px;
|
|
1739
|
+
--animation-time: ${this.animationTime}ms;
|
|
1838
1740
|
`;
|
|
1839
1741
|
|
|
1840
1742
|
return html`
|
|
1841
1743
|
<temba-floating-window
|
|
1744
|
+
style="--transition-duration: ${this.animationTime}ms"
|
|
1842
1745
|
id="phone-window"
|
|
1843
1746
|
width="${this.windowWidth}"
|
|
1844
1747
|
leftBoundaryMargin="${this.leftBoundaryMargin}"
|
|
1845
1748
|
bottomBoundaryMargin="${config.windowPadding}"
|
|
1846
1749
|
topBoundaryMargin="${config.windowPadding}"
|
|
1847
1750
|
height="${config.phoneTotalHeight}"
|
|
1848
|
-
top="
|
|
1751
|
+
top="0"
|
|
1849
1752
|
chromeless
|
|
1850
1753
|
>
|
|
1851
1754
|
<div class="phone-simulator" style="${styleVars}">
|
|
@@ -1903,56 +1806,80 @@ export class Simulator extends RapidElement {
|
|
|
1903
1806
|
<div class="dynamic-island"></div>
|
|
1904
1807
|
</div>
|
|
1905
1808
|
</div>
|
|
1906
|
-
<
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
<div
|
|
1934
|
-
class="attachment-menu-item"
|
|
1935
|
-
@click=${() => this.handleSendAttachment('video')}
|
|
1936
|
-
>
|
|
1937
|
-
<temba-icon name="attachment_video" size="1.2"></temba-icon>
|
|
1938
|
-
<span>Video</span>
|
|
1939
|
-
</div>
|
|
1940
|
-
<div
|
|
1941
|
-
class="attachment-menu-item"
|
|
1942
|
-
@click=${() => this.handleSendAttachment('audio')}
|
|
1809
|
+
<temba-chat class="phone-screen" .showTimestamps=${false}>
|
|
1810
|
+
</temba-chat>
|
|
1811
|
+
<div class="custom-scrollbar-container">
|
|
1812
|
+
<div class="custom-scrollbar-content"></div>
|
|
1813
|
+
</div>
|
|
1814
|
+
|
|
1815
|
+
<div class="bottom-input-container">
|
|
1816
|
+
${this.currentQuickReplies.length > 0
|
|
1817
|
+
? html`<div class="quick-replies-container">
|
|
1818
|
+
${this.currentQuickReplies.map(
|
|
1819
|
+
(qr) => html`
|
|
1820
|
+
<button
|
|
1821
|
+
class="quick-reply-btn"
|
|
1822
|
+
@click=${() => this.handleQuickReplyClick(qr.text)}
|
|
1823
|
+
?disabled=${this.sprinting}
|
|
1824
|
+
>
|
|
1825
|
+
${qr.text}
|
|
1826
|
+
</button>
|
|
1827
|
+
`
|
|
1828
|
+
)}
|
|
1829
|
+
</div>`
|
|
1830
|
+
: null}
|
|
1831
|
+
<div class="message-input">
|
|
1832
|
+
<button
|
|
1833
|
+
class="attachment-button"
|
|
1834
|
+
@click=${this.handleToggleAttachmentMenu}
|
|
1835
|
+
?disabled=${this.sprinting}
|
|
1943
1836
|
>
|
|
1944
|
-
<temba-icon name="
|
|
1945
|
-
|
|
1946
|
-
|
|
1837
|
+
<temba-icon name="plus" size="1.5"></temba-icon>
|
|
1838
|
+
</button>
|
|
1839
|
+
<input
|
|
1840
|
+
type="text"
|
|
1841
|
+
placeholder="Enter Message"
|
|
1842
|
+
.value=${this.inputValue}
|
|
1843
|
+
@input=${this.handleInput}
|
|
1844
|
+
@keyup=${this.handleKeyUp}
|
|
1845
|
+
?disabled=${this.sprinting}
|
|
1846
|
+
/>
|
|
1947
1847
|
<div
|
|
1948
|
-
class="attachment-menu
|
|
1949
|
-
|
|
1848
|
+
class="attachment-menu ${this.attachmentMenuOpen
|
|
1849
|
+
? 'open'
|
|
1850
|
+
: ''}"
|
|
1950
1851
|
>
|
|
1951
|
-
<
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1852
|
+
<div
|
|
1853
|
+
class="attachment-menu-item"
|
|
1854
|
+
@click=${() => this.handleSendAttachment('image')}
|
|
1855
|
+
>
|
|
1856
|
+
<temba-icon name="attachment_image" size="1.2"></temba-icon>
|
|
1857
|
+
<span>Image</span>
|
|
1858
|
+
</div>
|
|
1859
|
+
<div
|
|
1860
|
+
class="attachment-menu-item"
|
|
1861
|
+
@click=${() => this.handleSendAttachment('video')}
|
|
1862
|
+
>
|
|
1863
|
+
<temba-icon name="attachment_video" size="1.2"></temba-icon>
|
|
1864
|
+
<span>Video</span>
|
|
1865
|
+
</div>
|
|
1866
|
+
<div
|
|
1867
|
+
class="attachment-menu-item"
|
|
1868
|
+
@click=${() => this.handleSendAttachment('audio')}
|
|
1869
|
+
>
|
|
1870
|
+
<temba-icon name="attachment_audio" size="1.2"></temba-icon>
|
|
1871
|
+
<span>Audio</span>
|
|
1872
|
+
</div>
|
|
1873
|
+
<div
|
|
1874
|
+
class="attachment-menu-item"
|
|
1875
|
+
@click=${() => this.handleSendAttachment('location')}
|
|
1876
|
+
>
|
|
1877
|
+
<temba-icon
|
|
1878
|
+
name="attachment_location"
|
|
1879
|
+
size="1.2"
|
|
1880
|
+
></temba-icon>
|
|
1881
|
+
<span>Location</span>
|
|
1882
|
+
</div>
|
|
1956
1883
|
</div>
|
|
1957
1884
|
</div>
|
|
1958
1885
|
</div>
|
|
@@ -2001,6 +1928,7 @@ export class Simulator extends RapidElement {
|
|
|
2001
1928
|
icon="simulator"
|
|
2002
1929
|
label="Phone Simulator"
|
|
2003
1930
|
color="#10b981"
|
|
1931
|
+
.hidden=${this.isVisible}
|
|
2004
1932
|
@temba-button-clicked=${this.handleShow}
|
|
2005
1933
|
></temba-floating-tab>
|
|
2006
1934
|
`;
|