flow_chat 0.4.0 → 0.4.2

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.
@@ -0,0 +1,1982 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>FlowChat Simulator</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <style>
7
+ * {
8
+ box-sizing: border-box;
9
+ margin: 0;
10
+ padding: 0;
11
+ }
12
+
13
+ body {
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
15
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
16
+ height: 100vh;
17
+ overflow: hidden;
18
+ color: #333;
19
+ }
20
+
21
+ .container {
22
+ height: 100vh;
23
+ display: flex;
24
+ flex-direction: column;
25
+ background: white;
26
+ }
27
+
28
+ .header {
29
+ background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
30
+ color: white;
31
+ padding: 15px 25px;
32
+ position: relative;
33
+ flex-shrink: 0;
34
+ }
35
+
36
+ .header h1 {
37
+ font-size: 20px;
38
+ font-weight: 700;
39
+ margin-bottom: 2px;
40
+ }
41
+
42
+ .header p {
43
+ opacity: 0.8;
44
+ font-size: 12px;
45
+ font-weight: 300;
46
+ }
47
+
48
+ .main-content {
49
+ flex: 1;
50
+ display: flex;
51
+ background: #f8f9fa;
52
+ min-height: 0;
53
+ }
54
+
55
+ .control-panel {
56
+ width: 280px;
57
+ background: white;
58
+ border-right: 1px solid #e9ecef;
59
+ display: flex;
60
+ flex-direction: column;
61
+ flex-shrink: 0;
62
+ }
63
+
64
+ .config-section {
65
+ padding: 15px;
66
+ border-bottom: 1px solid #f0f0f0;
67
+ }
68
+
69
+ .config-section h3 {
70
+ color: #2c3e50;
71
+ font-size: 14px;
72
+ font-weight: 600;
73
+ margin-bottom: 10px;
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 8px;
77
+ }
78
+
79
+ .config-icon {
80
+ width: 20px;
81
+ height: 20px;
82
+ border-radius: 4px;
83
+ display: flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ font-size: 12px;
87
+ color: white;
88
+ font-weight: bold;
89
+ }
90
+
91
+ .form-group {
92
+ margin-bottom: 12px;
93
+ }
94
+
95
+ .form-group:last-child {
96
+ margin-bottom: 0;
97
+ }
98
+
99
+ .form-group label {
100
+ display: block;
101
+ font-size: 11px;
102
+ font-weight: 600;
103
+ color: #495057;
104
+ margin-bottom: 4px;
105
+ text-transform: uppercase;
106
+ letter-spacing: 0.5px;
107
+ }
108
+
109
+ .form-group select,
110
+ .form-group input {
111
+ width: 100%;
112
+ padding: 8px 10px;
113
+ border: 1px solid #e9ecef;
114
+ border-radius: 6px;
115
+ font-size: 13px;
116
+ transition: all 0.2s ease;
117
+ background: white;
118
+ }
119
+
120
+ .form-group select:focus,
121
+ .form-group input:focus {
122
+ outline: none;
123
+ border-color: #667eea;
124
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
125
+ }
126
+
127
+ .config-details {
128
+ background: #f8f9fa;
129
+ border-radius: 6px;
130
+ padding: 8px;
131
+ margin-top: 8px;
132
+ border: 1px solid #e9ecef;
133
+ font-size: 11px;
134
+ }
135
+
136
+ .config-detail-item {
137
+ display: flex;
138
+ justify-content: space-between;
139
+ align-items: center;
140
+ margin-bottom: 4px;
141
+ }
142
+
143
+ .config-detail-item:last-child {
144
+ margin-bottom: 0;
145
+ }
146
+
147
+ .config-detail-label {
148
+ color: #6c757d;
149
+ font-weight: 500;
150
+ }
151
+
152
+ .config-detail-value {
153
+ color: #495057;
154
+ font-weight: 600;
155
+ font-family: 'Monaco', 'Menlo', monospace;
156
+ font-size: 10px;
157
+ }
158
+
159
+ .actions-section {
160
+ padding: 15px;
161
+ background: #fafbfc;
162
+ border-top: 1px solid #e9ecef;
163
+ flex-shrink: 0;
164
+ }
165
+
166
+ .btn {
167
+ width: 100%;
168
+ padding: 10px 16px;
169
+ border: none;
170
+ border-radius: 6px;
171
+ cursor: pointer;
172
+ font-weight: 600;
173
+ font-size: 12px;
174
+ transition: all 0.2s ease;
175
+ text-transform: uppercase;
176
+ letter-spacing: 0.5px;
177
+ margin-bottom: 8px;
178
+ }
179
+
180
+ .btn:last-child {
181
+ margin-bottom: 0;
182
+ }
183
+
184
+ .btn-success {
185
+ background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
186
+ color: white;
187
+ box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3);
188
+ }
189
+
190
+ .btn-success:hover:not(:disabled) {
191
+ transform: translateY(-1px);
192
+ box-shadow: 0 4px 8px rgba(40, 167, 69, 0.4);
193
+ }
194
+
195
+ .btn-danger {
196
+ background: linear-gradient(135deg, #dc3545 0%, #fd7e14 100%);
197
+ color: white;
198
+ box-shadow: 0 2px 4px rgba(220, 53, 69, 0.3);
199
+ }
200
+
201
+ .btn-danger:hover:not(:disabled) {
202
+ transform: translateY(-1px);
203
+ box-shadow: 0 4px 8px rgba(220, 53, 69, 0.4);
204
+ }
205
+
206
+ .btn:disabled {
207
+ opacity: 0.6;
208
+ cursor: not-allowed;
209
+ transform: none !important;
210
+ box-shadow: none !important;
211
+ }
212
+
213
+ .simulator-area {
214
+ flex: 1;
215
+ display: flex;
216
+ min-height: 0;
217
+ }
218
+
219
+ .phone-container {
220
+ flex: 1;
221
+ display: flex;
222
+ flex-direction: column;
223
+ align-items: center;
224
+ justify-content: center;
225
+ padding: 20px;
226
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
227
+ gap: 20px;
228
+ }
229
+
230
+ .phone-simulator {
231
+ width: 320px;
232
+ height: 600px;
233
+ background: #000;
234
+ border-radius: 25px;
235
+ padding: 20px;
236
+ position: relative;
237
+ box-shadow: 0 15px 35px rgba(0,0,0,0.3);
238
+ flex-shrink: 0;
239
+ }
240
+
241
+ .phone-simulator::before {
242
+ content: '';
243
+ position: absolute;
244
+ top: 8px;
245
+ left: 50%;
246
+ transform: translateX(-50%);
247
+ width: 60px;
248
+ height: 4px;
249
+ background: #333;
250
+ border-radius: 2px;
251
+ }
252
+
253
+ .bottom-input-panel {
254
+ width: 320px;
255
+ background: white;
256
+ border-radius: 15px;
257
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
258
+ border: 1px solid #e9ecef;
259
+ flex-shrink: 0;
260
+ }
261
+
262
+ .input-section {
263
+ padding: 15px;
264
+ }
265
+
266
+ .controls-container {
267
+ width: 300px;
268
+ background: white;
269
+ border-left: 1px solid #e9ecef;
270
+ display: flex;
271
+ flex-direction: column;
272
+ flex-shrink: 0;
273
+ }
274
+
275
+ .request-log-section {
276
+ flex: 1;
277
+ padding: 15px;
278
+ border-bottom: 1px solid #e9ecef;
279
+ display: flex;
280
+ flex-direction: column;
281
+ min-height: 0;
282
+ }
283
+
284
+ .screen {
285
+ width: 100%;
286
+ height: 100%;
287
+ background: white;
288
+ border-radius: 18px;
289
+ overflow: hidden;
290
+ display: flex;
291
+ flex-direction: column;
292
+ position: relative;
293
+ min-height: 0;
294
+ }
295
+
296
+ .ussd-screen {
297
+ background: #000;
298
+ color: #00ff41;
299
+ font-family: 'Courier New', monospace;
300
+ font-size: 13px;
301
+ padding: 20px;
302
+ white-space: pre-wrap;
303
+ overflow-y: auto;
304
+ flex: 1;
305
+ line-height: 1.4;
306
+ }
307
+
308
+ .whatsapp-screen {
309
+ background: linear-gradient(180deg, #e5ddd5 0%, #d1c7b8 100%);
310
+ flex: 1;
311
+ display: flex;
312
+ flex-direction: column;
313
+ min-height: 0;
314
+ height: 100%;
315
+ }
316
+
317
+ .whatsapp-header {
318
+ background: #075e54;
319
+ color: white;
320
+ padding: 15px 20px;
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 12px;
324
+ flex-shrink: 0;
325
+ }
326
+
327
+ .contact-avatar {
328
+ width: 40px;
329
+ height: 40px;
330
+ border-radius: 50%;
331
+ background: linear-gradient(135deg, #128c7e, #25d366);
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: center;
335
+ font-weight: bold;
336
+ font-size: 14px;
337
+ color: white;
338
+ }
339
+
340
+ .contact-info h4 {
341
+ font-size: 16px;
342
+ font-weight: 500;
343
+ margin-bottom: 2px;
344
+ }
345
+
346
+ .contact-info p {
347
+ font-size: 12px;
348
+ opacity: 0.8;
349
+ }
350
+
351
+ .messages-area {
352
+ flex: 1;
353
+ padding: 20px;
354
+ overflow-y: auto;
355
+ min-height: 0;
356
+ max-height: 100%;
357
+ display: flex;
358
+ flex-direction: column;
359
+ }
360
+
361
+ .message {
362
+ margin-bottom: 15px;
363
+ max-width: 80%;
364
+ animation: messageSlide 0.3s ease;
365
+ flex-shrink: 0;
366
+ }
367
+
368
+ @keyframes messageSlide {
369
+ from {
370
+ opacity: 0;
371
+ transform: translateY(8px);
372
+ }
373
+ to {
374
+ opacity: 1;
375
+ transform: translateY(0);
376
+ }
377
+ }
378
+
379
+ .message.incoming {
380
+ align-self: flex-start;
381
+ }
382
+
383
+ .message.outgoing {
384
+ align-self: flex-end;
385
+ margin-left: auto;
386
+ }
387
+
388
+ .message-bubble {
389
+ padding: 12px 16px;
390
+ border-radius: 15px;
391
+ word-wrap: break-word;
392
+ word-break: break-word;
393
+ white-space: pre-wrap;
394
+ position: relative;
395
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
396
+ font-size: 14px;
397
+ line-height: 1.4;
398
+ max-height: 300px;
399
+ overflow-y: auto;
400
+ overflow-x: hidden;
401
+ }
402
+
403
+ .message.incoming .message-bubble {
404
+ background: white;
405
+ border-bottom-left-radius: 6px;
406
+ }
407
+
408
+ .message.outgoing .message-bubble {
409
+ background: #dcf8c6;
410
+ border-bottom-right-radius: 6px;
411
+ }
412
+
413
+ .interactive-buttons {
414
+ display: flex;
415
+ flex-direction: column;
416
+ gap: 8px;
417
+ margin-top: 10px;
418
+ }
419
+
420
+ .interactive-button {
421
+ background: #f0f0f0;
422
+ border: 1px solid #e0e0e0;
423
+ padding: 10px 14px;
424
+ border-radius: 10px;
425
+ cursor: pointer;
426
+ transition: all 0.2s;
427
+ text-align: left;
428
+ font-size: 13px;
429
+ font-weight: 500;
430
+ }
431
+
432
+ .interactive-button:hover {
433
+ background: #e8f5e8;
434
+ border-color: #075e54;
435
+ transform: translateY(-1px);
436
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
437
+ }
438
+
439
+ .request-log-header {
440
+ display: flex;
441
+ justify-content: space-between;
442
+ align-items: center;
443
+ margin-bottom: 10px;
444
+ }
445
+
446
+ .request-log-title {
447
+ font-size: 12px;
448
+ font-weight: 600;
449
+ color: #495057;
450
+ text-transform: uppercase;
451
+ letter-spacing: 0.5px;
452
+ }
453
+
454
+ .clear-log-btn {
455
+ padding: 4px 8px;
456
+ border: 1px solid #e9ecef;
457
+ border-radius: 4px;
458
+ background: white;
459
+ color: #6c757d;
460
+ font-size: 10px;
461
+ cursor: pointer;
462
+ transition: all 0.2s ease;
463
+ }
464
+
465
+ .clear-log-btn:hover {
466
+ background: #f8f9fa;
467
+ border-color: #dc3545;
468
+ color: #dc3545;
469
+ }
470
+
471
+ .request-log {
472
+ flex: 1;
473
+ background: #f8f9fa;
474
+ border: 1px solid #e9ecef;
475
+ border-radius: 6px;
476
+ overflow-y: auto;
477
+ font-family: 'Monaco', 'Menlo', monospace;
478
+ font-size: 10px;
479
+ line-height: 1.4;
480
+ min-height: 0;
481
+ }
482
+
483
+ .request-log-empty {
484
+ padding: 20px;
485
+ text-align: center;
486
+ color: #6c757d;
487
+ font-style: italic;
488
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
489
+ }
490
+
491
+ .log-entry {
492
+ padding: 8px 10px;
493
+ border-bottom: 1px solid #e9ecef;
494
+ animation: logEntrySlide 0.3s ease;
495
+ background: white;
496
+ border-radius: 4px;
497
+ margin-bottom: 4px;
498
+ }
499
+
500
+ .log-entry:last-child {
501
+ border-bottom: none;
502
+ }
503
+
504
+ .log-entry-header {
505
+ cursor: pointer;
506
+ display: flex;
507
+ justify-content: space-between;
508
+ align-items: flex-start;
509
+ padding: 4px 0;
510
+ user-select: none;
511
+ }
512
+
513
+ .log-entry-header:hover {
514
+ background: #f8f9fa;
515
+ border-radius: 3px;
516
+ margin: -2px;
517
+ padding: 6px 2px;
518
+ }
519
+
520
+ .log-entry-left {
521
+ flex: 1;
522
+ min-width: 0;
523
+ }
524
+
525
+ .log-entry-toggle {
526
+ margin-left: 8px;
527
+ font-size: 10px;
528
+ color: #6c757d;
529
+ transition: transform 0.2s ease;
530
+ flex-shrink: 0;
531
+ }
532
+
533
+ .log-entry.expanded .log-entry-toggle {
534
+ transform: rotate(90deg);
535
+ }
536
+
537
+ .log-entry-body {
538
+ display: none;
539
+ margin-top: 8px;
540
+ padding-top: 8px;
541
+ border-top: 1px solid #f0f0f0;
542
+ }
543
+
544
+ .log-entry.expanded .log-entry-body {
545
+ display: block;
546
+ }
547
+
548
+ .log-textarea {
549
+ width: 100%;
550
+ background: #f8f9fa;
551
+ border: 1px solid #e9ecef;
552
+ border-radius: 3px;
553
+ padding: 6px 8px;
554
+ font-family: 'Monaco', 'Menlo', monospace;
555
+ font-size: 9px;
556
+ line-height: 1.3;
557
+ resize: vertical;
558
+ min-height: 60px;
559
+ max-height: 200px;
560
+ margin-bottom: 6px;
561
+ color: #495057;
562
+ }
563
+
564
+ .log-textarea:focus {
565
+ outline: none;
566
+ border-color: #667eea;
567
+ box-shadow: 0 0 0 1px rgba(102, 126, 234, 0.1);
568
+ }
569
+
570
+ .log-section-title {
571
+ font-size: 9px;
572
+ font-weight: 600;
573
+ color: #6c757d;
574
+ margin-bottom: 3px;
575
+ text-transform: uppercase;
576
+ letter-spacing: 0.5px;
577
+ }
578
+
579
+ .input-group {
580
+ display: flex;
581
+ gap: 8px;
582
+ align-items: stretch;
583
+ margin-bottom: 10px;
584
+ }
585
+
586
+ .message-input {
587
+ flex: 1;
588
+ padding: 10px 12px;
589
+ border: 1px solid #e9ecef;
590
+ border-radius: 20px;
591
+ font-size: 12px;
592
+ outline: none;
593
+ transition: all 0.2s ease;
594
+ resize: none;
595
+ max-height: 80px;
596
+ }
597
+
598
+ .message-input:focus {
599
+ border-color: #667eea;
600
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
601
+ }
602
+
603
+ .send-btn {
604
+ padding: 10px 16px;
605
+ border: none;
606
+ border-radius: 20px;
607
+ cursor: pointer;
608
+ font-weight: 600;
609
+ font-size: 11px;
610
+ transition: all 0.2s ease;
611
+ text-transform: uppercase;
612
+ letter-spacing: 0.5px;
613
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
614
+ color: white;
615
+ box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
616
+ flex-shrink: 0;
617
+ }
618
+
619
+ .send-btn:hover:not(:disabled) {
620
+ transform: translateY(-1px);
621
+ box-shadow: 0 4px 8px rgba(102, 126, 234, 0.4);
622
+ }
623
+
624
+ .send-btn:disabled {
625
+ opacity: 0.6;
626
+ cursor: not-allowed;
627
+ transform: none !important;
628
+ box-shadow: none !important;
629
+ }
630
+
631
+ .status-section {
632
+ padding: 15px;
633
+ background: #f8f9fa;
634
+ border-top: 1px solid #e9ecef;
635
+ flex-shrink: 0;
636
+ }
637
+
638
+ .status-grid {
639
+ display: grid;
640
+ grid-template-columns: 1fr 1fr;
641
+ gap: 10px;
642
+ font-size: 10px;
643
+ }
644
+
645
+ .status-item {
646
+ display: flex;
647
+ align-items: center;
648
+ gap: 6px;
649
+ padding: 6px 8px;
650
+ background: white;
651
+ border-radius: 6px;
652
+ border: 1px solid #e9ecef;
653
+ }
654
+
655
+ .status-dot {
656
+ width: 6px;
657
+ height: 6px;
658
+ border-radius: 50%;
659
+ animation: pulse 2s ease-in-out infinite;
660
+ flex-shrink: 0;
661
+ }
662
+
663
+ .status-dot.connected {
664
+ background: #28a745;
665
+ }
666
+
667
+ .status-dot.disconnected {
668
+ background: #dc3545;
669
+ }
670
+
671
+ .status-dot.connecting {
672
+ background: #ffc107;
673
+ }
674
+
675
+ @keyframes pulse {
676
+ 0%, 100% { opacity: 1; }
677
+ 50% { opacity: 0.5; }
678
+ }
679
+
680
+ .char-count {
681
+ color: #6c757d;
682
+ text-align: right;
683
+ margin-top: 4px;
684
+ font-size: 10px;
685
+ }
686
+
687
+ .hidden {
688
+ display: none !important;
689
+ }
690
+
691
+ /* Responsive adjustments */
692
+ @media (max-width: 1200px) {
693
+ .control-panel {
694
+ width: 250px;
695
+ }
696
+
697
+ .controls-container {
698
+ width: 280px;
699
+ }
700
+
701
+ .phone-simulator, .bottom-input-panel {
702
+ width: 300px;
703
+ }
704
+
705
+ .phone-simulator {
706
+ height: 560px;
707
+ }
708
+ }
709
+
710
+ @media (max-width: 1024px) {
711
+ .main-content {
712
+ flex-direction: column;
713
+ }
714
+
715
+ .control-panel {
716
+ width: 100%;
717
+ height: 200px;
718
+ display: grid;
719
+ grid-template-columns: 1fr 1fr 200px;
720
+ border-right: none;
721
+ border-bottom: 1px solid #e9ecef;
722
+ }
723
+
724
+ .actions-section {
725
+ border-top: none;
726
+ border-left: 1px solid #e9ecef;
727
+ }
728
+
729
+ .simulator-area {
730
+ flex-direction: row;
731
+ }
732
+
733
+ .controls-container {
734
+ width: 280px;
735
+ }
736
+
737
+ .phone-simulator, .bottom-input-panel {
738
+ width: 280px;
739
+ }
740
+
741
+ .phone-simulator {
742
+ height: 520px;
743
+ }
744
+ }
745
+
746
+ @media (max-width: 768px) {
747
+ .control-panel {
748
+ grid-template-columns: 1fr;
749
+ height: auto;
750
+ }
751
+
752
+ .simulator-area {
753
+ flex-direction: column;
754
+ }
755
+
756
+ .phone-container {
757
+ order: 1;
758
+ padding: 10px;
759
+ }
760
+
761
+ .controls-container {
762
+ order: 2;
763
+ width: 100%;
764
+ height: 250px;
765
+ border-left: none;
766
+ border-top: 1px solid #e9ecef;
767
+ }
768
+
769
+ .phone-simulator, .bottom-input-panel {
770
+ width: 280px;
771
+ }
772
+
773
+ .phone-simulator {
774
+ height: 480px;
775
+ }
776
+
777
+ .bottom-input-panel {
778
+ width: 100%;
779
+ max-width: 280px;
780
+ }
781
+ }
782
+
783
+ @keyframes logEntrySlide {
784
+ from {
785
+ opacity: 0;
786
+ transform: translateX(10px);
787
+ }
788
+ to {
789
+ opacity: 1;
790
+ transform: translateX(0);
791
+ }
792
+ }
793
+
794
+ .log-timestamp {
795
+ color: #6c757d;
796
+ font-size: 9px;
797
+ margin-bottom: 2px;
798
+ }
799
+
800
+ .log-request {
801
+ margin-bottom: 4px;
802
+ }
803
+
804
+ .log-method {
805
+ display: inline-block;
806
+ padding: 1px 4px;
807
+ border-radius: 2px;
808
+ font-weight: bold;
809
+ font-size: 9px;
810
+ margin-right: 4px;
811
+ }
812
+
813
+ .log-method.post {
814
+ background: #28a745;
815
+ color: white;
816
+ }
817
+
818
+ .log-method.get {
819
+ background: #007bff;
820
+ color: white;
821
+ }
822
+
823
+ .log-url {
824
+ color: #495057;
825
+ word-break: break-all;
826
+ }
827
+
828
+ .log-response {
829
+ color: #6c757d;
830
+ font-size: 9px;
831
+ }
832
+
833
+ .log-status {
834
+ display: inline-block;
835
+ padding: 1px 4px;
836
+ border-radius: 2px;
837
+ font-weight: bold;
838
+ margin-right: 4px;
839
+ }
840
+
841
+ .log-status.success {
842
+ background: #d4edda;
843
+ color: #155724;
844
+ }
845
+
846
+ .log-status.error {
847
+ background: #f8d7da;
848
+ color: #721c24;
849
+ }
850
+ </style>
851
+ </head>
852
+ <body>
853
+ <div class="container">
854
+ <div class="header">
855
+ <h1>🚀 FlowChat Simulator</h1>
856
+ <p>Test your conversation flows</p>
857
+ </div>
858
+
859
+ <div class="main-content">
860
+ <div class="control-panel">
861
+ <!-- Configuration Selection -->
862
+ <div class="config-section">
863
+ <h3>
864
+ <div class="config-icon" style="background: #667eea;">⚙️</div>
865
+ Configuration
866
+ </h3>
867
+
868
+ <div class="form-group">
869
+ <label for="config-select">Environment</label>
870
+ <select id="config-select">
871
+ <% configurations.each do |key, config| %>
872
+ <option value="<%= key %>" <%= key == default_config_key ? 'selected' : '' %>
873
+ data-config='<%= config.to_json %>'>
874
+ <%= config[:icon] %> <%= config[:name] %>
875
+ </option>
876
+ <% end %>
877
+ </select>
878
+ </div>
879
+
880
+ <div id="config-details" class="config-details">
881
+ <!-- Dynamic config details will be populated here -->
882
+ </div>
883
+ </div>
884
+
885
+ <!-- User Settings -->
886
+ <div class="config-section">
887
+ <h3>
888
+ <div class="config-icon" style="background: #17a2b8;">👤</div>
889
+ User Settings
890
+ </h3>
891
+
892
+ <div class="form-group">
893
+ <label for="phone-number">Phone Number</label>
894
+ <input type="tel" id="phone-number" value="<%= default_phone_number %>" placeholder="+1234567890">
895
+ </div>
896
+
897
+ <div class="form-group" id="contact-name-group">
898
+ <label for="contact-name">Contact Name</label>
899
+ <input type="text" id="contact-name" value="<%= default_contact_name %>" placeholder="John Doe">
900
+ </div>
901
+ </div>
902
+
903
+ <!-- Quick Actions -->
904
+ <div class="actions-section">
905
+ <button id="start-btn" class="btn btn-success" disabled>
906
+ 🚀 Start Session
907
+ </button>
908
+ <button id="reset-btn" class="btn btn-danger" disabled>
909
+ 🔄 Reset
910
+ </button>
911
+ </div>
912
+ </div>
913
+
914
+ <div class="simulator-area">
915
+ <div class="phone-container">
916
+ <div class="phone-simulator">
917
+ <div class="screen">
918
+ <!-- USSD Screen -->
919
+ <div id="ussd-screen" class="ussd-screen hidden">
920
+ <!-- USSD content will be displayed here -->
921
+ </div>
922
+
923
+ <!-- WhatsApp Screen -->
924
+ <div id="whatsapp-screen" class="whatsapp-screen hidden">
925
+ <div class="whatsapp-header">
926
+ <div class="contact-avatar" id="contact-avatar">JD</div>
927
+ <div class="contact-info">
928
+ <h4 id="header-contact-name">Business</h4>
929
+ <p>Active now</p>
930
+ </div>
931
+ </div>
932
+ <div class="messages-area" id="messages-area">
933
+ <!-- Messages will be displayed here -->
934
+ </div>
935
+ </div>
936
+ </div>
937
+ </div>
938
+
939
+ <!-- Bottom Input Panel -->
940
+ <div class="bottom-input-panel">
941
+ <div class="input-section">
942
+ <div class="input-group">
943
+ <textarea id="message-input" class="message-input" placeholder="Type your message..." disabled rows="2"></textarea>
944
+ <button id="send-btn" class="send-btn" disabled>Send</button>
945
+ </div>
946
+ <div class="char-count" id="char-count"></div>
947
+ </div>
948
+ </div>
949
+ </div>
950
+
951
+ <div class="controls-container">
952
+ <div class="request-log-section">
953
+ <div class="request-log-header">
954
+ <span class="request-log-title">Request Log</span>
955
+ <button id="clear-log-btn" class="clear-log-btn">Clear</button>
956
+ </div>
957
+ <div id="request-log" class="request-log">
958
+ <div class="request-log-empty">No requests yet. Start a session to see HTTP traffic.</div>
959
+ </div>
960
+ </div>
961
+
962
+ <div class="status-section">
963
+ <div class="status-grid">
964
+ <div class="status-item">
965
+ <div class="status-dot disconnected" id="status-dot"></div>
966
+ <span id="status-text">Ready</span>
967
+ </div>
968
+
969
+ <div class="status-item">
970
+ <span id="config-indicator">Select Config</span>
971
+ </div>
972
+ </div>
973
+ </div>
974
+ </div>
975
+ </div>
976
+ </div>
977
+ </div>
978
+
979
+ <script>
980
+ // Application State
981
+ const state = {
982
+ currentConfig: null,
983
+ isRunning: false,
984
+ sessionId: null,
985
+ platform: null
986
+ }
987
+
988
+ // Configuration Data
989
+ const configurations = <%= configurations.to_json.html_safe %>;
990
+
991
+ // DOM Elements
992
+ const elements = {
993
+ configSelect: document.getElementById('config-select'),
994
+ phoneNumber: document.getElementById('phone-number'),
995
+ contactName: document.getElementById('contact-name'),
996
+ contactNameGroup: document.getElementById('contact-name-group'),
997
+ configDetails: document.getElementById('config-details'),
998
+
999
+ startBtn: document.getElementById('start-btn'),
1000
+ resetBtn: document.getElementById('reset-btn'),
1001
+ sendBtn: document.getElementById('send-btn'),
1002
+
1003
+ ussdScreen: document.getElementById('ussd-screen'),
1004
+ whatsappScreen: document.getElementById('whatsapp-screen'),
1005
+ messagesArea: document.getElementById('messages-area'),
1006
+ contactAvatar: document.getElementById('contact-avatar'),
1007
+ headerContactName: document.getElementById('header-contact-name'),
1008
+
1009
+ messageInput: document.getElementById('message-input'),
1010
+ charCount: document.getElementById('char-count'),
1011
+ statusText: document.getElementById('status-text'),
1012
+ statusDot: document.getElementById('status-dot'),
1013
+ configIndicator: document.getElementById('config-indicator'),
1014
+
1015
+ requestLog: document.getElementById('request-log'),
1016
+ clearLogBtn: document.getElementById('clear-log-btn')
1017
+ }
1018
+
1019
+ // Event Listeners
1020
+ elements.configSelect.addEventListener('change', handleConfigChange)
1021
+ elements.phoneNumber.addEventListener('input', updateUserSettings)
1022
+ elements.contactName.addEventListener('input', updateContactInfo)
1023
+
1024
+ elements.startBtn.addEventListener('click', startSession)
1025
+ elements.resetBtn.addEventListener('click', resetSession)
1026
+ elements.sendBtn.addEventListener('click', sendMessage)
1027
+ elements.clearLogBtn.addEventListener('click', clearRequestLog)
1028
+
1029
+ elements.messageInput.addEventListener('keypress', (e) => {
1030
+ if (e.key === 'Enter' && !e.shiftKey) {
1031
+ e.preventDefault()
1032
+ sendMessage()
1033
+ }
1034
+ })
1035
+
1036
+ elements.messageInput.addEventListener('input', updateCharCount)
1037
+
1038
+ // Configuration Management
1039
+ function handleConfigChange() {
1040
+ const selectedOption = elements.configSelect.selectedOptions[0]
1041
+ const configKey = selectedOption.value
1042
+ const configData = JSON.parse(selectedOption.dataset.config)
1043
+
1044
+ state.currentConfig = { key: configKey, ...configData }
1045
+ state.platform = configData.processor_type
1046
+
1047
+ updateConfigDetails()
1048
+ updateUI()
1049
+ resetSession()
1050
+ }
1051
+
1052
+ function updateConfigDetails() {
1053
+ if (!state.currentConfig) return
1054
+
1055
+ const config = state.currentConfig
1056
+ const details = `
1057
+ <div class="config-detail-item">
1058
+ <span class="config-detail-label">Endpoint:</span>
1059
+ <span class="config-detail-value">${config.endpoint}</span>
1060
+ </div>
1061
+ <div class="config-detail-item">
1062
+ <span class="config-detail-label">Provider:</span>
1063
+ <span class="config-detail-value">${config.provider}</span>
1064
+ </div>
1065
+ <div class="config-detail-item">
1066
+ <span class="config-detail-label">Type:</span>
1067
+ <span class="config-detail-value">${config.processor_type.toUpperCase()}</span>
1068
+ </div>
1069
+ `
1070
+
1071
+ elements.configDetails.innerHTML = details
1072
+ elements.configIndicator.textContent = `${config.icon} ${config.name}`
1073
+ }
1074
+
1075
+ function updateUserSettings() {
1076
+ if (state.currentConfig && state.currentConfig.processor_type === 'whatsapp') {
1077
+ updateContactInfo()
1078
+ }
1079
+ }
1080
+
1081
+ function updateContactInfo() {
1082
+ const name = elements.contactName.value || 'Business'
1083
+ elements.headerContactName.textContent = name
1084
+
1085
+ const initials = name.split(' ')
1086
+ .map(n => n[0])
1087
+ .join('')
1088
+ .substr(0, 2)
1089
+ .toUpperCase()
1090
+ elements.contactAvatar.textContent = initials
1091
+ }
1092
+
1093
+ function updateUI() {
1094
+ if (!state.currentConfig) return
1095
+
1096
+ const isWhatsApp = state.currentConfig.processor_type === 'whatsapp'
1097
+
1098
+ // Show/hide platform-specific elements
1099
+ elements.contactNameGroup.style.display = isWhatsApp ? 'block' : 'none'
1100
+ elements.ussdScreen.classList.toggle('hidden', isWhatsApp)
1101
+ elements.whatsappScreen.classList.toggle('hidden', !isWhatsApp)
1102
+
1103
+ // Update input placeholder
1104
+ elements.messageInput.placeholder = isWhatsApp ?
1105
+ 'Type your WhatsApp message...' :
1106
+ 'Enter USSD input...'
1107
+
1108
+ // Update button states
1109
+ const canStart = state.currentConfig && !state.isRunning
1110
+ elements.startBtn.disabled = !canStart
1111
+ elements.resetBtn.disabled = !state.isRunning
1112
+ elements.sendBtn.disabled = !state.isRunning
1113
+ elements.messageInput.disabled = !state.isRunning
1114
+ }
1115
+
1116
+ function updateCharCount() {
1117
+ const length = elements.messageInput.value.length
1118
+ const maxLength = state.platform === 'ussd' ? <%= pagesize %> : 4096
1119
+
1120
+ elements.charCount.textContent = `${length}/${maxLength} chars`
1121
+
1122
+ if (length > maxLength) {
1123
+ elements.charCount.style.color = '#dc3545'
1124
+ } else if (length > maxLength * 0.8) {
1125
+ elements.charCount.style.color = '#ffc107'
1126
+ } else {
1127
+ elements.charCount.style.color = '#6c757d'
1128
+ }
1129
+ }
1130
+
1131
+ function updateStatus(text, status = 'disconnected') {
1132
+ elements.statusText.textContent = text
1133
+ elements.statusDot.className = `status-dot ${status}`
1134
+ }
1135
+
1136
+ // Session Control
1137
+ async function startSession() {
1138
+ if (!state.currentConfig) return
1139
+
1140
+ try {
1141
+ elements.startBtn.disabled = true
1142
+ updateStatus('Starting...', 'connecting')
1143
+
1144
+ state.sessionId = generateSessionId()
1145
+ state.isRunning = true
1146
+
1147
+ if (state.currentConfig.processor_type === 'ussd') {
1148
+ await makeUSSDRequest()
1149
+ } else {
1150
+ await makeWhatsAppRequest()
1151
+ }
1152
+
1153
+ updateStatus('Connected', 'connected')
1154
+
1155
+ } catch (error) {
1156
+ handleError(error)
1157
+ } finally {
1158
+ updateUI()
1159
+ }
1160
+ }
1161
+
1162
+ async function sendMessage() {
1163
+ const message = elements.messageInput.value.trim()
1164
+ if (!message || !state.isRunning) return
1165
+
1166
+ try {
1167
+ updateStatus('Sending...', 'connecting')
1168
+
1169
+ // Add outgoing message to WhatsApp chat
1170
+ if (state.currentConfig.processor_type === 'whatsapp') {
1171
+ addMessage(message, true)
1172
+ }
1173
+
1174
+ elements.messageInput.value = ''
1175
+ updateCharCount()
1176
+
1177
+ if (state.currentConfig.processor_type === 'ussd') {
1178
+ await makeUSSDRequest(message)
1179
+ } else {
1180
+ await makeWhatsAppRequest(message)
1181
+ }
1182
+
1183
+ updateStatus('Connected', 'connected')
1184
+
1185
+ } catch (error) {
1186
+ handleError(error)
1187
+ }
1188
+ }
1189
+
1190
+ function resetSession() {
1191
+ state.isRunning = false
1192
+ state.sessionId = null
1193
+
1194
+ elements.messageInput.value = ''
1195
+ elements.ussdScreen.textContent = ''
1196
+ elements.messagesArea.innerHTML = ''
1197
+
1198
+ updateCharCount()
1199
+ updateStatus('Ready', 'disconnected')
1200
+ updateUI()
1201
+ }
1202
+
1203
+ // Request Logging
1204
+ function addRequestLog(method, url, requestData, responseData, status, error = null) {
1205
+ // Remove empty state if present
1206
+ const emptyState = elements.requestLog.querySelector('.request-log-empty')
1207
+ if (emptyState) {
1208
+ emptyState.remove()
1209
+ }
1210
+
1211
+ const timestamp = new Date().toLocaleTimeString()
1212
+ const logEntry = document.createElement('div')
1213
+ logEntry.className = 'log-entry'
1214
+
1215
+ const statusClass = error ? 'error' : 'success'
1216
+ const statusText = error ? `${status} Error` : `${status} OK`
1217
+
1218
+ // Create collapsible header
1219
+ const headerDiv = document.createElement('div')
1220
+ headerDiv.className = 'log-entry-header'
1221
+
1222
+ const leftDiv = document.createElement('div')
1223
+ leftDiv.className = 'log-entry-left'
1224
+
1225
+ leftDiv.innerHTML = `
1226
+ <div class="log-timestamp">${timestamp}</div>
1227
+ <div class="log-request">
1228
+ <span class="log-method ${method.toLowerCase()}">${method}</span>
1229
+ <span class="log-url">${url}</span>
1230
+ </div>
1231
+ <div class="log-response">
1232
+ <span class="log-status ${statusClass}">${statusText}</span>
1233
+ ${error ? error : 'Success'}
1234
+ </div>
1235
+ `
1236
+
1237
+ const toggleDiv = document.createElement('div')
1238
+ toggleDiv.className = 'log-entry-toggle'
1239
+ toggleDiv.textContent = '▶'
1240
+
1241
+ headerDiv.appendChild(leftDiv)
1242
+ headerDiv.appendChild(toggleDiv)
1243
+
1244
+ // Create collapsible body
1245
+ const bodyDiv = document.createElement('div')
1246
+ bodyDiv.className = 'log-entry-body'
1247
+
1248
+ // Add request data if present
1249
+ if (requestData) {
1250
+ const requestSection = document.createElement('div')
1251
+ requestSection.innerHTML = '<div class="log-section-title">Request Body</div>'
1252
+
1253
+ const requestTextarea = document.createElement('textarea')
1254
+ requestTextarea.className = 'log-textarea'
1255
+ requestTextarea.readOnly = true
1256
+ requestTextarea.value = JSON.stringify(requestData, null, 2)
1257
+ requestTextarea.rows = Math.min(10, requestTextarea.value.split('\n').length)
1258
+
1259
+ requestSection.appendChild(requestTextarea)
1260
+ bodyDiv.appendChild(requestSection)
1261
+ }
1262
+
1263
+ // Add response data if present
1264
+ if (responseData) {
1265
+ const responseSection = document.createElement('div')
1266
+ responseSection.innerHTML = '<div class="log-section-title">Response Body</div>'
1267
+
1268
+ const responseTextarea = document.createElement('textarea')
1269
+ responseTextarea.className = 'log-textarea'
1270
+ responseTextarea.readOnly = true
1271
+ responseTextarea.value = typeof responseData === 'string' ?
1272
+ responseData :
1273
+ JSON.stringify(responseData, null, 2)
1274
+ responseTextarea.rows = Math.min(10, responseTextarea.value.split('\n').length)
1275
+
1276
+ responseSection.appendChild(responseTextarea)
1277
+ bodyDiv.appendChild(responseSection)
1278
+ }
1279
+
1280
+ // Add click handler for expand/collapse
1281
+ headerDiv.addEventListener('click', () => {
1282
+ logEntry.classList.toggle('expanded')
1283
+ })
1284
+
1285
+ logEntry.appendChild(headerDiv)
1286
+ logEntry.appendChild(bodyDiv)
1287
+
1288
+ elements.requestLog.appendChild(logEntry)
1289
+ elements.requestLog.scrollTop = elements.requestLog.scrollHeight
1290
+
1291
+ // Keep only last 20 entries
1292
+ const entries = elements.requestLog.querySelectorAll('.log-entry')
1293
+ if (entries.length > 20) {
1294
+ entries[0].remove()
1295
+ }
1296
+ }
1297
+
1298
+ function clearRequestLog() {
1299
+ elements.requestLog.innerHTML = '<div class="request-log-empty">No requests yet. Start a session to see HTTP traffic.</div>'
1300
+ }
1301
+
1302
+ // API Communication
1303
+ async function makeUSSDRequest(userInput = null) {
1304
+ const config = state.currentConfig
1305
+ const phoneNumber = elements.phoneNumber.value
1306
+
1307
+ let requestData = {}
1308
+
1309
+ switch (config.provider) {
1310
+ case 'nalo':
1311
+ requestData = {
1312
+ USERID: state.sessionId,
1313
+ MSISDN: phoneNumber,
1314
+ USERDATA: userInput || '',
1315
+ MSGTYPE: userInput === null
1316
+ }
1317
+ break
1318
+ case 'nsano':
1319
+ requestData = {
1320
+ network: 'MTN',
1321
+ msisdn: phoneNumber,
1322
+ msg: userInput || '',
1323
+ UserSessionID: state.sessionId
1324
+ }
1325
+ break
1326
+ default:
1327
+ throw new Error(`Unsupported USSD provider: ${config.provider}`)
1328
+ }
1329
+
1330
+ try {
1331
+ const response = await fetch(config.endpoint, {
1332
+ method: 'POST',
1333
+ headers: { 'Content-Type': 'application/json' },
1334
+ body: JSON.stringify(requestData)
1335
+ })
1336
+
1337
+ const data = await response.json()
1338
+
1339
+ // Log the request
1340
+ addRequestLog('POST', config.endpoint, requestData, data, response.status)
1341
+
1342
+ if (!response.ok) {
1343
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
1344
+ }
1345
+
1346
+ switch (config.provider) {
1347
+ case 'nalo':
1348
+ displayUSSDResponse(data.MSG)
1349
+ state.isRunning = data.MSGTYPE
1350
+ break
1351
+ case 'nsano':
1352
+ displayUSSDResponse(data.USSDResp.title)
1353
+ state.isRunning = data.USSDResp.action === 'input'
1354
+ break
1355
+ }
1356
+
1357
+ if (!state.isRunning) {
1358
+ updateStatus('Session completed', 'disconnected')
1359
+
1360
+ // Disable input immediately when session ends
1361
+ elements.messageInput.disabled = true
1362
+ elements.sendBtn.disabled = true
1363
+
1364
+ // Clear session ID and update UI to enable reset button
1365
+ setTimeout(() => {
1366
+ state.sessionId = null
1367
+ updateStatus('Session ended', 'disconnected')
1368
+ updateUI()
1369
+ }, 1000) // Brief delay to let user see the completion status
1370
+ }
1371
+ } catch (error) {
1372
+ // Log the error
1373
+ addRequestLog('POST', config.endpoint, requestData, null, 0, error.message)
1374
+ throw error
1375
+ }
1376
+ }
1377
+
1378
+ async function makeWhatsAppRequest(userInput = null) {
1379
+ const config = state.currentConfig
1380
+ const phoneNumber = elements.phoneNumber.value
1381
+ const contactName = elements.contactName.value
1382
+
1383
+ // For initiation, send initial message
1384
+ let isInitialMessage = false
1385
+ if (userInput === null) {
1386
+ userInput = 'hi'
1387
+ isInitialMessage = true
1388
+ }
1389
+
1390
+ // Add the initial message to chat history so user can see what was sent
1391
+ if (isInitialMessage) {
1392
+ addMessage(userInput, true)
1393
+ }
1394
+
1395
+ // Create WhatsApp-compatible webhook payload with simulator mode enabled
1396
+ const webhookData = {
1397
+ simulator_mode: true,
1398
+ entry: [{
1399
+ id: "entry_123",
1400
+ time: Math.floor(Date.now() / 1000),
1401
+ changes: [{
1402
+ value: {
1403
+ messaging_product: "whatsapp",
1404
+ metadata: {
1405
+ display_phone_number: phoneNumber.replace('+', ''),
1406
+ phone_number_id: "phone_number_id_123"
1407
+ },
1408
+ messages: [{
1409
+ id: `wamid.${Date.now()}`,
1410
+ from: phoneNumber.replace('+', ''),
1411
+ timestamp: Math.floor(Date.now() / 1000).toString(),
1412
+ text: { body: userInput },
1413
+ type: 'text'
1414
+ }],
1415
+ contacts: [{
1416
+ profile: { name: contactName },
1417
+ wa_id: phoneNumber.replace('+', '')
1418
+ }]
1419
+ },
1420
+ field: "messages"
1421
+ }]
1422
+ }]
1423
+ }
1424
+
1425
+ try {
1426
+ const response = await fetch(config.endpoint, {
1427
+ method: 'POST',
1428
+ headers: { 'Content-Type': 'application/json' },
1429
+ body: JSON.stringify(webhookData)
1430
+ })
1431
+
1432
+ // Read the response body once and store it
1433
+ const responseText = await response.text()
1434
+ let responseData = null
1435
+
1436
+ // Try to parse as JSON first
1437
+ if (response.headers.get('content-type')?.includes('application/json')) {
1438
+ try {
1439
+ responseData = JSON.parse(responseText)
1440
+
1441
+ // Check if this is a simulator response
1442
+ if (responseData.mode === 'simulator') {
1443
+ // Display the simulated response
1444
+ displaySimulatorResponse(responseData)
1445
+ addRequestLog('POST', config.endpoint, webhookData, responseData, response.status)
1446
+ return
1447
+ }
1448
+ } catch (jsonError) {
1449
+ console.warn('Failed to parse JSON response:', jsonError)
1450
+ console.warn('Response text:', responseText)
1451
+ responseData = responseText
1452
+ }
1453
+ } else {
1454
+ responseData = responseText
1455
+ }
1456
+
1457
+ // Log the request with actual response
1458
+ addRequestLog('POST', config.endpoint, webhookData, responseData, response.status)
1459
+
1460
+ if (!response.ok) {
1461
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
1462
+ }
1463
+
1464
+ // For non-simulator mode, show informational message
1465
+ setTimeout(() => {
1466
+ addInfoMessage(
1467
+ `✅ Webhook delivered successfully (${response.status})\n\n` +
1468
+ `📱 In a real WhatsApp integration:\n` +
1469
+ `• Your endpoint processes this webhook\n` +
1470
+ `• Response messages are sent via WhatsApp Cloud API\n` +
1471
+ `• Messages appear in the actual WhatsApp chat\n\n` +
1472
+ `💡 This simulator shows the webhook delivery only.\n` +
1473
+ `To enable full simulator mode, configure your endpoint\n` +
1474
+ `to handle simulator_mode parameter and return JSON responses.`
1475
+ )
1476
+ }, 500)
1477
+
1478
+ } catch (error) {
1479
+ // Log the error
1480
+ addRequestLog('POST', config.endpoint, webhookData, null, 0, error.message)
1481
+
1482
+ // Provide helpful error messages for common issues
1483
+ let errorMessage = error.message
1484
+
1485
+ if (error.message.includes('Failed to fetch')) {
1486
+ errorMessage = 'Cannot connect to endpoint. Please check:\n' +
1487
+ '• Endpoint URL is correct\n' +
1488
+ '• Server is running\n' +
1489
+ '• CORS is configured if cross-origin\n' +
1490
+ '• SSL certificate is valid (for HTTPS)'
1491
+ } else if (error.message.includes('404')) {
1492
+ errorMessage = 'Endpoint not found (404). Please verify:\n' +
1493
+ '• The webhook URL is correct\n' +
1494
+ '• The route is properly configured\n' +
1495
+ '• The controller/handler exists'
1496
+ } else if (error.message.includes('500')) {
1497
+ errorMessage = 'Server error (500). Check server logs for:\n' +
1498
+ '• Application errors\n' +
1499
+ '• Missing dependencies\n' +
1500
+ '• Configuration issues'
1501
+ }
1502
+
1503
+ // Add error info message to chat
1504
+ setTimeout(() => {
1505
+ addInfoMessage(
1506
+ `❌ Request Failed\n\n` +
1507
+ `Error: ${errorMessage}\n\n` +
1508
+ `💡 For simulator mode support, ensure your endpoint:\n` +
1509
+ `• Accepts POST requests with simulator_mode parameter\n` +
1510
+ `• Returns JSON with mode: "simulator" for simulator requests\n` +
1511
+ `• Handles webhook verification (if required by your setup)`
1512
+ )
1513
+ }, 500)
1514
+
1515
+ throw error
1516
+ }
1517
+ }
1518
+
1519
+ // Display Functions
1520
+ function displayUSSDResponse(content) {
1521
+ elements.ussdScreen.textContent = content
1522
+ updateCharCount()
1523
+ }
1524
+
1525
+ function displaySimulatorResponse(simulatorData) {
1526
+ const messagePayload = simulatorData.would_send
1527
+ const messageInfo = simulatorData.message_info
1528
+
1529
+ // Extract message content based on type
1530
+ let messageText = ''
1531
+ let interactive = null
1532
+ let mediaContent = null
1533
+
1534
+ switch (messagePayload.type) {
1535
+ case 'text':
1536
+ messageText = messagePayload.text.body
1537
+ break
1538
+ case 'interactive':
1539
+ if (messagePayload.interactive.type === 'button') {
1540
+ messageText = messagePayload.interactive.body.text
1541
+ interactive = {
1542
+ buttons: messagePayload.interactive.action.buttons.map(btn => ({
1543
+ id: btn.reply.id,
1544
+ title: btn.reply.title
1545
+ }))
1546
+ }
1547
+ } else if (messagePayload.interactive.type === 'list') {
1548
+ messageText = messagePayload.interactive.body.text
1549
+ // For lists, we'll show a simplified view
1550
+ const sections = messagePayload.interactive.action.sections
1551
+ if (sections && sections.length > 0) {
1552
+ interactive = {
1553
+ buttons: sections.flatMap(section =>
1554
+ section.rows.map(row => ({
1555
+ id: row.id,
1556
+ title: row.title,
1557
+ description: row.description
1558
+ }))
1559
+ )
1560
+ }
1561
+ }
1562
+ }
1563
+ break
1564
+ case 'template':
1565
+ messageText = `Template: ${messagePayload.template.name}`
1566
+ if (messagePayload.template.components) {
1567
+ const bodyComponent = messagePayload.template.components.find(c => c.type === 'BODY')
1568
+ if (bodyComponent && bodyComponent.parameters) {
1569
+ messageText += `\n${bodyComponent.parameters.map(p => p.text).join(' ')}`
1570
+ }
1571
+ }
1572
+ break
1573
+ case 'image':
1574
+ messageText = messagePayload.image.caption || ''
1575
+ mediaContent = {
1576
+ type: 'image',
1577
+ url: messagePayload.image.link || `https://via.placeholder.com/300x200/25d366/white?text=Image+ID:+${messagePayload.image.id || 'media_123'}`,
1578
+ caption: messagePayload.image.caption
1579
+ }
1580
+ break
1581
+ case 'document':
1582
+ messageText = messagePayload.document.caption || ''
1583
+ mediaContent = {
1584
+ type: 'document',
1585
+ url: messagePayload.document.link,
1586
+ filename: messagePayload.document.filename || 'document.pdf',
1587
+ caption: messagePayload.document.caption
1588
+ }
1589
+ break
1590
+ case 'audio':
1591
+ messageText = ''
1592
+ mediaContent = {
1593
+ type: 'audio',
1594
+ url: messagePayload.audio.link || `data:audio/wav;base64,UklGRnoGAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQoGAACBhYqFbF1fdJivrJBhNjVgodDbq2EcBj+a2/LDciUFLIHO8tiJNwgZaLvt559NEAxQp+PwtmMcBjiR1/LMeSwFJHfH8N2QQAoUXrTp66hVFApGn+DyvmkcBzuU2e+saDUGOWOJ6+2SV0gYKT2L5zJAGyJH4f2GHz+7zJMxtx9R5Dsl`
1595
+ }
1596
+ break
1597
+ case 'video':
1598
+ messageText = messagePayload.video.caption || ''
1599
+ mediaContent = {
1600
+ type: 'video',
1601
+ url: messagePayload.video.link || `https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4`,
1602
+ caption: messagePayload.video.caption
1603
+ }
1604
+ break
1605
+ case 'location':
1606
+ const loc = messagePayload.location
1607
+ messageText = loc.name || 'Shared location'
1608
+ if (loc.address) messageText += `\n${loc.address}`
1609
+ mediaContent = {
1610
+ type: 'location',
1611
+ latitude: loc.latitude || 0,
1612
+ longitude: loc.longitude || 0,
1613
+ name: loc.name,
1614
+ address: loc.address
1615
+ }
1616
+ break
1617
+ default:
1618
+ messageText = `Unsupported message type: ${messagePayload.type}`
1619
+ console.warn('Unsupported message type in simulator:', messagePayload.type, messagePayload)
1620
+ }
1621
+
1622
+ // Add the simulated message to the chat
1623
+ addMessage(messageText, false, messagePayload.type, interactive, mediaContent)
1624
+ }
1625
+
1626
+ function addMessage(content, isOutgoing = false, type = 'text', interactive = null, mediaContent = null) {
1627
+ const messageDiv = document.createElement('div')
1628
+ messageDiv.className = `message ${isOutgoing ? 'outgoing' : 'incoming'}`
1629
+
1630
+ const bubbleDiv = document.createElement('div')
1631
+ bubbleDiv.className = 'message-bubble'
1632
+
1633
+ // Handle media content first
1634
+ if (mediaContent) {
1635
+ const mediaContainer = document.createElement('div')
1636
+ mediaContainer.className = 'media-container'
1637
+ mediaContainer.style.marginBottom = content ? '8px' : '0'
1638
+
1639
+ switch (mediaContent.type) {
1640
+ case 'image':
1641
+ const img = document.createElement('img')
1642
+ img.src = mediaContent.url
1643
+ img.style.maxWidth = '100%'
1644
+ img.style.height = 'auto'
1645
+ img.style.borderRadius = '8px'
1646
+ img.style.display = 'block'
1647
+ img.alt = mediaContent.caption || 'Image'
1648
+ img.onerror = function() {
1649
+ this.src = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjBmMGYwIi8+PHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxNCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPvCfk7cgSW1hZ2U8L3RleHQ+PC9zdmc+'
1650
+ this.style.border = '1px solid #e0e0e0'
1651
+ }
1652
+ mediaContainer.appendChild(img)
1653
+ break
1654
+
1655
+ case 'video':
1656
+ const video = document.createElement('video')
1657
+ video.src = mediaContent.url
1658
+ video.controls = true
1659
+ video.style.maxWidth = '100%'
1660
+ video.style.height = 'auto'
1661
+ video.style.borderRadius = '8px'
1662
+ video.style.display = 'block'
1663
+ video.preload = 'metadata'
1664
+ video.onerror = function() {
1665
+ const placeholder = document.createElement('div')
1666
+ placeholder.style.width = '300px'
1667
+ placeholder.style.height = '200px'
1668
+ placeholder.style.backgroundColor = '#f0f0f0'
1669
+ placeholder.style.border = '1px solid #e0e0e0'
1670
+ placeholder.style.borderRadius = '8px'
1671
+ placeholder.style.display = 'flex'
1672
+ placeholder.style.alignItems = 'center'
1673
+ placeholder.style.justifyContent = 'center'
1674
+ placeholder.style.color = '#999'
1675
+ placeholder.style.fontSize = '14px'
1676
+ placeholder.textContent = '🎥 Video'
1677
+ mediaContainer.replaceChild(placeholder, video)
1678
+ }
1679
+ mediaContainer.appendChild(video)
1680
+ break
1681
+
1682
+ case 'audio':
1683
+ const audio = document.createElement('audio')
1684
+ audio.src = mediaContent.url
1685
+ audio.controls = true
1686
+ audio.style.width = '100%'
1687
+ audio.style.maxWidth = '300px'
1688
+ audio.preload = 'metadata'
1689
+ audio.onerror = function() {
1690
+ const placeholder = document.createElement('div')
1691
+ placeholder.style.padding = '12px 16px'
1692
+ placeholder.style.backgroundColor = '#f0f0f0'
1693
+ placeholder.style.border = '1px solid #e0e0e0'
1694
+ placeholder.style.borderRadius = '8px'
1695
+ placeholder.style.display = 'flex'
1696
+ placeholder.style.alignItems = 'center'
1697
+ placeholder.style.gap = '8px'
1698
+ placeholder.style.color = '#666'
1699
+ placeholder.style.fontSize = '14px'
1700
+ placeholder.innerHTML = '🎵 <span>Audio message</span>'
1701
+ mediaContainer.replaceChild(placeholder, audio)
1702
+ }
1703
+ mediaContainer.appendChild(audio)
1704
+ break
1705
+
1706
+ case 'document':
1707
+ const docContainer = document.createElement('div')
1708
+ docContainer.style.padding = '12px 16px'
1709
+ docContainer.style.backgroundColor = '#f8f9fa'
1710
+ docContainer.style.border = '1px solid #e0e0e0'
1711
+ docContainer.style.borderRadius = '8px'
1712
+ docContainer.style.display = 'flex'
1713
+ docContainer.style.alignItems = 'center'
1714
+ docContainer.style.gap = '12px'
1715
+ docContainer.style.cursor = 'pointer'
1716
+ docContainer.style.transition = 'background-color 0.2s'
1717
+
1718
+ const docIcon = document.createElement('div')
1719
+ docIcon.style.fontSize = '24px'
1720
+ docIcon.textContent = getDocumentIcon(mediaContent.filename)
1721
+
1722
+ const docInfo = document.createElement('div')
1723
+ docInfo.style.flex = '1'
1724
+ docInfo.style.minWidth = '0'
1725
+
1726
+ const docName = document.createElement('div')
1727
+ docName.style.fontWeight = '600'
1728
+ docName.style.fontSize = '14px'
1729
+ docName.style.color = '#333'
1730
+ docName.style.overflow = 'hidden'
1731
+ docName.style.textOverflow = 'ellipsis'
1732
+ docName.style.whiteSpace = 'nowrap'
1733
+ docName.textContent = mediaContent.filename
1734
+
1735
+ const docType = document.createElement('div')
1736
+ docType.style.fontSize = '12px'
1737
+ docType.style.color = '#666'
1738
+ docType.textContent = 'Document'
1739
+
1740
+ docInfo.appendChild(docName)
1741
+ docInfo.appendChild(docType)
1742
+ docContainer.appendChild(docIcon)
1743
+ docContainer.appendChild(docInfo)
1744
+
1745
+ // Add click handler if URL is available
1746
+ if (mediaContent.url) {
1747
+ const downloadIcon = document.createElement('div')
1748
+ downloadIcon.style.fontSize = '18px'
1749
+ downloadIcon.style.color = '#666'
1750
+ downloadIcon.textContent = '⬇️'
1751
+ docContainer.appendChild(downloadIcon)
1752
+
1753
+ docContainer.onclick = () => {
1754
+ if (mediaContent.url.startsWith('http')) {
1755
+ window.open(mediaContent.url, '_blank')
1756
+ }
1757
+ }
1758
+
1759
+ docContainer.onmouseover = () => {
1760
+ docContainer.style.backgroundColor = '#e8f5e8'
1761
+ }
1762
+ docContainer.onmouseout = () => {
1763
+ docContainer.style.backgroundColor = '#f8f9fa'
1764
+ }
1765
+ }
1766
+
1767
+ mediaContainer.appendChild(docContainer)
1768
+ break
1769
+
1770
+ case 'location':
1771
+ const locationContainer = document.createElement('div')
1772
+ locationContainer.style.border = '1px solid #e0e0e0'
1773
+ locationContainer.style.borderRadius = '8px'
1774
+ locationContainer.style.overflow = 'hidden'
1775
+ locationContainer.style.maxWidth = '300px'
1776
+
1777
+ // Create a simple map-like visualization
1778
+ const mapDiv = document.createElement('div')
1779
+ mapDiv.style.height = '150px'
1780
+ mapDiv.style.background = 'linear-gradient(135deg, #a8e6cf 0%, #dcedc1 100%)'
1781
+ mapDiv.style.position = 'relative'
1782
+ mapDiv.style.display = 'flex'
1783
+ mapDiv.style.alignItems = 'center'
1784
+ mapDiv.style.justifyContent = 'center'
1785
+ mapDiv.style.fontSize = '32px'
1786
+ mapDiv.textContent = '📍'
1787
+
1788
+ // Add coordinate text
1789
+ const coordDiv = document.createElement('div')
1790
+ coordDiv.style.position = 'absolute'
1791
+ coordDiv.style.bottom = '8px'
1792
+ coordDiv.style.right = '8px'
1793
+ coordDiv.style.background = 'rgba(255,255,255,0.9)'
1794
+ coordDiv.style.padding = '4px 8px'
1795
+ coordDiv.style.borderRadius = '4px'
1796
+ coordDiv.style.fontSize = '10px'
1797
+ coordDiv.style.color = '#666'
1798
+ coordDiv.style.fontFamily = 'monospace'
1799
+ coordDiv.textContent = `${mediaContent.latitude.toFixed(4)}, ${mediaContent.longitude.toFixed(4)}`
1800
+ mapDiv.appendChild(coordDiv)
1801
+
1802
+ const locationInfo = document.createElement('div')
1803
+ locationInfo.style.padding = '12px'
1804
+ locationInfo.style.backgroundColor = 'white'
1805
+
1806
+ if (mediaContent.name) {
1807
+ const nameDiv = document.createElement('div')
1808
+ nameDiv.style.fontWeight = '600'
1809
+ nameDiv.style.fontSize = '14px'
1810
+ nameDiv.style.color = '#333'
1811
+ nameDiv.style.marginBottom = '4px'
1812
+ nameDiv.textContent = mediaContent.name
1813
+ locationInfo.appendChild(nameDiv)
1814
+ }
1815
+
1816
+ if (mediaContent.address) {
1817
+ const addressDiv = document.createElement('div')
1818
+ addressDiv.style.fontSize = '12px'
1819
+ addressDiv.style.color = '#666'
1820
+ addressDiv.textContent = mediaContent.address
1821
+ locationInfo.appendChild(addressDiv)
1822
+ }
1823
+
1824
+ // Add link to open in maps
1825
+ const mapsLink = document.createElement('div')
1826
+ mapsLink.style.fontSize = '12px'
1827
+ mapsLink.style.color = '#25d366'
1828
+ mapsLink.style.cursor = 'pointer'
1829
+ mapsLink.style.marginTop = '8px'
1830
+ mapsLink.textContent = '🗺️ View in Maps'
1831
+ mapsLink.onclick = () => {
1832
+ const url = `https://www.google.com/maps?q=${mediaContent.latitude},${mediaContent.longitude}`
1833
+ window.open(url, '_blank')
1834
+ }
1835
+ locationInfo.appendChild(mapsLink)
1836
+
1837
+ locationContainer.appendChild(mapDiv)
1838
+ locationContainer.appendChild(locationInfo)
1839
+ mediaContainer.appendChild(locationContainer)
1840
+ break
1841
+ }
1842
+
1843
+ bubbleDiv.appendChild(mediaContainer)
1844
+ }
1845
+
1846
+ // Add text content if present
1847
+ if (content) {
1848
+ const textDiv = document.createElement('div')
1849
+ textDiv.textContent = content
1850
+ bubbleDiv.appendChild(textDiv)
1851
+ }
1852
+
1853
+ messageDiv.appendChild(bubbleDiv)
1854
+
1855
+ // Add interactive elements for incoming messages
1856
+ if (!isOutgoing && interactive && interactive.buttons) {
1857
+ const buttonsDiv = document.createElement('div')
1858
+ buttonsDiv.className = 'interactive-buttons'
1859
+
1860
+ interactive.buttons.forEach(button => {
1861
+ const btn = document.createElement('div')
1862
+ btn.className = 'interactive-button'
1863
+
1864
+ // Create button content with title and optional description
1865
+ const titleSpan = document.createElement('div')
1866
+ titleSpan.style.fontWeight = '600'
1867
+ titleSpan.textContent = button.title
1868
+ btn.appendChild(titleSpan)
1869
+
1870
+ if (button.description) {
1871
+ const descSpan = document.createElement('div')
1872
+ descSpan.style.fontSize = '11px'
1873
+ descSpan.style.color = '#666'
1874
+ descSpan.style.marginTop = '2px'
1875
+ descSpan.textContent = button.description
1876
+ btn.appendChild(descSpan)
1877
+ }
1878
+
1879
+ btn.onclick = () => selectOption(button.id)
1880
+ buttonsDiv.appendChild(btn)
1881
+ })
1882
+
1883
+ bubbleDiv.appendChild(buttonsDiv)
1884
+ }
1885
+
1886
+ messageDiv.appendChild(bubbleDiv)
1887
+
1888
+ elements.messagesArea.appendChild(messageDiv)
1889
+ elements.messagesArea.scrollTop = elements.messagesArea.scrollHeight
1890
+ }
1891
+
1892
+ function getDocumentIcon(filename) {
1893
+ if (!filename) return '📄'
1894
+
1895
+ const ext = filename.split('.').pop().toLowerCase()
1896
+
1897
+ const iconMap = {
1898
+ 'pdf': '📕',
1899
+ 'doc': '📘',
1900
+ 'docx': '📘',
1901
+ 'xls': '📗',
1902
+ 'xlsx': '📗',
1903
+ 'ppt': '📙',
1904
+ 'pptx': '📙',
1905
+ 'txt': '📝',
1906
+ 'zip': '🗜️',
1907
+ 'rar': '🗜️',
1908
+ '7z': '🗜️',
1909
+ 'mp3': '🎵',
1910
+ 'wav': '🎵',
1911
+ 'mp4': '🎥',
1912
+ 'avi': '🎥',
1913
+ 'jpg': '🖼️',
1914
+ 'jpeg': '🖼️',
1915
+ 'png': '🖼️',
1916
+ 'gif': '🖼️'
1917
+ }
1918
+
1919
+ return iconMap[ext] || '📄'
1920
+ }
1921
+
1922
+ function addInfoMessage(content) {
1923
+ const messageDiv = document.createElement('div')
1924
+ messageDiv.className = 'message incoming'
1925
+
1926
+ const bubbleDiv = document.createElement('div')
1927
+ bubbleDiv.className = 'message-bubble'
1928
+ bubbleDiv.style.background = '#e3f2fd'
1929
+ bubbleDiv.style.borderColor = '#2196f3'
1930
+ bubbleDiv.style.whiteSpace = 'pre-line'
1931
+ bubbleDiv.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
1932
+ bubbleDiv.textContent = content
1933
+
1934
+ messageDiv.appendChild(bubbleDiv)
1935
+ elements.messagesArea.appendChild(messageDiv)
1936
+ elements.messagesArea.scrollTop = elements.messagesArea.scrollHeight
1937
+ }
1938
+
1939
+ function selectOption(optionId) {
1940
+ // Show visual feedback that the option was selected
1941
+ const buttons = document.querySelectorAll('.interactive-button')
1942
+ buttons.forEach(btn => {
1943
+ if (btn.onclick && btn.onclick.toString().includes(optionId)) {
1944
+ btn.style.background = '#dcf8c6'
1945
+ btn.style.borderColor = '#075e54'
1946
+ setTimeout(() => {
1947
+ btn.style.background = '#f0f0f0'
1948
+ btn.style.borderColor = '#e0e0e0'
1949
+ }, 200)
1950
+ }
1951
+ })
1952
+
1953
+ elements.messageInput.value = optionId
1954
+ sendMessage()
1955
+ }
1956
+
1957
+ // Utility Functions
1958
+ function generateSessionId() {
1959
+ return btoa(Math.random().toString()).substr(10, 10)
1960
+ }
1961
+
1962
+ function handleError(error) {
1963
+ console.error('Simulator error:', error)
1964
+ updateStatus(`Error: ${error.message}`, 'disconnected')
1965
+ alert(`Error: ${error.message}`)
1966
+ state.isRunning = false
1967
+ updateUI()
1968
+ }
1969
+
1970
+ // Initialize
1971
+ function init() {
1972
+ // Set default configuration
1973
+ handleConfigChange()
1974
+ updateUI()
1975
+ updateStatus('Ready', 'disconnected')
1976
+ }
1977
+
1978
+ // Start the application
1979
+ init()
1980
+ </script>
1981
+ </body>
1982
+ </html>