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.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/README.md +408 -102
- data/examples/media_prompts_examples.rb +28 -0
- data/examples/multi_tenant_whatsapp_controller.rb +4 -8
- data/examples/whatsapp_controller.rb +1 -2
- data/examples/whatsapp_media_examples.rb +406 -0
- data/examples/whatsapp_message_job.rb +111 -0
- data/lib/flow_chat/base_processor.rb +7 -3
- data/lib/flow_chat/config.rb +36 -0
- data/lib/flow_chat/simulator/controller.rb +78 -0
- data/lib/flow_chat/simulator/views/simulator.html.erb +1707 -0
- data/lib/flow_chat/ussd/processor.rb +0 -1
- data/lib/flow_chat/ussd/prompt.rb +39 -5
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +7 -1
- data/lib/flow_chat/whatsapp/client.rb +439 -0
- data/lib/flow_chat/whatsapp/configuration.rb +41 -3
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +114 -114
- data/lib/flow_chat/whatsapp/processor.rb +0 -10
- data/lib/flow_chat/whatsapp/prompt.rb +118 -73
- data/lib/flow_chat/whatsapp/send_job_support.rb +79 -0
- metadata +8 -3
- data/lib/flow_chat/ussd/simulator/controller.rb +0 -51
- data/lib/flow_chat/ussd/simulator/views/simulator.html.erb +0 -239
@@ -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>
|