flow_chat 0.4.0 → 0.4.1

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,1707 @@
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
+
1533
+ switch (messagePayload.type) {
1534
+ case 'text':
1535
+ messageText = messagePayload.text.body
1536
+ break
1537
+ case 'interactive':
1538
+ if (messagePayload.interactive.type === 'button') {
1539
+ messageText = messagePayload.interactive.body.text
1540
+ interactive = {
1541
+ buttons: messagePayload.interactive.action.buttons.map(btn => ({
1542
+ id: btn.reply.id,
1543
+ title: btn.reply.title
1544
+ }))
1545
+ }
1546
+ } else if (messagePayload.interactive.type === 'list') {
1547
+ messageText = messagePayload.interactive.body.text
1548
+ // For lists, we'll show a simplified view
1549
+ const sections = messagePayload.interactive.action.sections
1550
+ if (sections && sections.length > 0) {
1551
+ interactive = {
1552
+ buttons: sections.flatMap(section =>
1553
+ section.rows.map(row => ({
1554
+ id: row.id,
1555
+ title: row.title,
1556
+ description: row.description
1557
+ }))
1558
+ )
1559
+ }
1560
+ }
1561
+ }
1562
+ break
1563
+ case 'template':
1564
+ messageText = `Template: ${messagePayload.template.name}`
1565
+ if (messagePayload.template.components) {
1566
+ const bodyComponent = messagePayload.template.components.find(c => c.type === 'BODY')
1567
+ if (bodyComponent && bodyComponent.parameters) {
1568
+ messageText += `\n${bodyComponent.parameters.map(p => p.text).join(' ')}`
1569
+ }
1570
+ }
1571
+ break
1572
+ case 'image':
1573
+ messageText = messagePayload.image.caption || 'Image message'
1574
+ // In a real implementation, you might want to show the image
1575
+ messageText = `📷 ${messageText}`
1576
+ break
1577
+ case 'document':
1578
+ messageText = messagePayload.document.caption || messagePayload.document.filename || 'Document'
1579
+ messageText = `📄 ${messageText}`
1580
+ break
1581
+ case 'audio':
1582
+ messageText = '🎵 Audio message'
1583
+ break
1584
+ case 'video':
1585
+ messageText = messagePayload.video.caption || 'Video message'
1586
+ messageText = `🎥 ${messageText}`
1587
+ break
1588
+ case 'location':
1589
+ const loc = messagePayload.location
1590
+ messageText = `📍 Location: ${loc.name || 'Shared location'}`
1591
+ if (loc.address) messageText += `\n${loc.address}`
1592
+ break
1593
+ default:
1594
+ messageText = `Unsupported message type: ${messagePayload.type}`
1595
+ console.warn('Unsupported message type in simulator:', messagePayload.type, messagePayload)
1596
+ }
1597
+
1598
+ // Add the simulated message to the chat
1599
+ addMessage(messageText, false, messagePayload.type, interactive)
1600
+ }
1601
+
1602
+ function addMessage(content, isOutgoing = false, type = 'text', interactive = null) {
1603
+ const messageDiv = document.createElement('div')
1604
+ messageDiv.className = `message ${isOutgoing ? 'outgoing' : 'incoming'}`
1605
+
1606
+ const bubbleDiv = document.createElement('div')
1607
+ bubbleDiv.className = 'message-bubble'
1608
+ bubbleDiv.textContent = content
1609
+
1610
+ messageDiv.appendChild(bubbleDiv)
1611
+
1612
+ // Add interactive elements for incoming messages
1613
+ if (!isOutgoing && interactive && interactive.buttons) {
1614
+ const buttonsDiv = document.createElement('div')
1615
+ buttonsDiv.className = 'interactive-buttons'
1616
+
1617
+ interactive.buttons.forEach(button => {
1618
+ const btn = document.createElement('div')
1619
+ btn.className = 'interactive-button'
1620
+
1621
+ // Create button content with title and optional description
1622
+ const titleSpan = document.createElement('div')
1623
+ titleSpan.style.fontWeight = '600'
1624
+ titleSpan.textContent = button.title
1625
+ btn.appendChild(titleSpan)
1626
+
1627
+ if (button.description) {
1628
+ const descSpan = document.createElement('div')
1629
+ descSpan.style.fontSize = '11px'
1630
+ descSpan.style.color = '#666'
1631
+ descSpan.style.marginTop = '2px'
1632
+ descSpan.textContent = button.description
1633
+ btn.appendChild(descSpan)
1634
+ }
1635
+
1636
+ btn.onclick = () => selectOption(button.id)
1637
+ buttonsDiv.appendChild(btn)
1638
+ })
1639
+
1640
+ bubbleDiv.appendChild(buttonsDiv)
1641
+ }
1642
+
1643
+ elements.messagesArea.appendChild(messageDiv)
1644
+ elements.messagesArea.scrollTop = elements.messagesArea.scrollHeight
1645
+ }
1646
+
1647
+ function addInfoMessage(content) {
1648
+ const messageDiv = document.createElement('div')
1649
+ messageDiv.className = 'message incoming'
1650
+
1651
+ const bubbleDiv = document.createElement('div')
1652
+ bubbleDiv.className = 'message-bubble'
1653
+ bubbleDiv.style.background = '#e3f2fd'
1654
+ bubbleDiv.style.borderColor = '#2196f3'
1655
+ bubbleDiv.style.whiteSpace = 'pre-line'
1656
+ bubbleDiv.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
1657
+ bubbleDiv.textContent = content
1658
+
1659
+ messageDiv.appendChild(bubbleDiv)
1660
+ elements.messagesArea.appendChild(messageDiv)
1661
+ elements.messagesArea.scrollTop = elements.messagesArea.scrollHeight
1662
+ }
1663
+
1664
+ function selectOption(optionId) {
1665
+ // Show visual feedback that the option was selected
1666
+ const buttons = document.querySelectorAll('.interactive-button')
1667
+ buttons.forEach(btn => {
1668
+ if (btn.onclick && btn.onclick.toString().includes(optionId)) {
1669
+ btn.style.background = '#dcf8c6'
1670
+ btn.style.borderColor = '#075e54'
1671
+ setTimeout(() => {
1672
+ btn.style.background = '#f0f0f0'
1673
+ btn.style.borderColor = '#e0e0e0'
1674
+ }, 200)
1675
+ }
1676
+ })
1677
+
1678
+ elements.messageInput.value = optionId
1679
+ sendMessage()
1680
+ }
1681
+
1682
+ // Utility Functions
1683
+ function generateSessionId() {
1684
+ return btoa(Math.random().toString()).substr(10, 10)
1685
+ }
1686
+
1687
+ function handleError(error) {
1688
+ console.error('Simulator error:', error)
1689
+ updateStatus(`Error: ${error.message}`, 'disconnected')
1690
+ alert(`Error: ${error.message}`)
1691
+ state.isRunning = false
1692
+ updateUI()
1693
+ }
1694
+
1695
+ // Initialize
1696
+ function init() {
1697
+ // Set default configuration
1698
+ handleConfigChange()
1699
+ updateUI()
1700
+ updateStatus('Ready', 'disconnected')
1701
+ }
1702
+
1703
+ // Start the application
1704
+ init()
1705
+ </script>
1706
+ </body>
1707
+ </html>