Martechsol commited on
Commit
6da6999
·
1 Parent(s): b8928f4

Deploy RAG Chatbot to Hugging Face Spaces

Browse files
Files changed (2) hide show
  1. app/admin/router.py +50 -0
  2. app/admin/templates/admin.html +664 -1215
app/admin/router.py CHANGED
@@ -230,6 +230,56 @@ async def list_sessions(username: str = Depends(verify_admin)):
230
  return JSONResponse(content={"sessions": sessions, "total": len(sessions)})
231
 
232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  @admin_router.get("/api/admin/sessions/{session_id}", include_in_schema=False)
234
  async def get_session_detail(
235
  session_id: str, username: str = Depends(verify_admin)
 
230
  return JSONResponse(content={"sessions": sessions, "total": len(sessions)})
231
 
232
 
233
+ @admin_router.get("/api/admin/analytics", include_in_schema=False)
234
+ async def admin_analytics(username: str = Depends(verify_admin)):
235
+ """Returns aggregated analytics for the dashboard."""
236
+ from datetime import datetime, timedelta, timezone
237
+
238
+ files = await asyncio.to_thread(_list_json_files)
239
+ total_sessions = len(files)
240
+ total_messages = 0
241
+
242
+ now_utc = datetime.now(timezone.utc)
243
+ today_start = now_utc.replace(hour=0, minute=0, second=0, microsecond=0)
244
+ today_sessions = 0
245
+
246
+ days_data = {}
247
+ for i in range(6, -1, -1):
248
+ d = (today_start - timedelta(days=i)).strftime("%Y-%m-%d")
249
+ days_data[d] = {"sessions": 0, "messages": 0}
250
+
251
+ for fpath in files:
252
+ try:
253
+ data = await asyncio.to_thread(_read_json, fpath)
254
+ msg_count = data.get("message_count", 0)
255
+ total_messages += msg_count
256
+
257
+ created_str = data.get("created_at", "")
258
+ if created_str:
259
+ try:
260
+ created_dt = datetime.fromisoformat(created_str.replace('Z', '+00:00'))
261
+ if created_dt >= today_start:
262
+ today_sessions += 1
263
+
264
+ date_str = created_dt.strftime("%Y-%m-%d")
265
+ if date_str in days_data:
266
+ days_data[date_str]["sessions"] += 1
267
+ days_data[date_str]["messages"] += msg_count
268
+ except ValueError:
269
+ pass
270
+ except Exception as e:
271
+ logger.warning("Analytics: Failed to read %s: %s", fpath, e)
272
+
273
+ trends = [{"date": k, "sessions": v["sessions"], "messages": v["messages"]} for k, v in days_data.items()]
274
+
275
+ return JSONResponse(content={
276
+ "total_sessions": total_sessions,
277
+ "total_messages": total_messages,
278
+ "today_sessions": today_sessions,
279
+ "trends": trends
280
+ })
281
+
282
+
283
  @admin_router.get("/api/admin/sessions/{session_id}", include_in_schema=False)
284
  async def get_session_detail(
285
  session_id: str, username: str = Depends(verify_admin)
app/admin/templates/admin.html CHANGED
@@ -1,1459 +1,908 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
-
4
  <head>
5
  <meta charset="UTF-8" />
6
  <meta name="viewport" content="width=device-width,initial-scale=1.0" />
7
- <title>Martechsol — Admin Panel</title>
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
10
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
 
11
  <style>
12
- *,
13
- *::before,
14
- *::after {
15
- box-sizing: border-box;
16
- margin: 0;
17
- padding: 0
18
- }
19
-
20
  :root {
21
- --bg: #0a0d14;
22
- --surface: #111827;
23
- --surface2: #1a2235;
24
- --surface3: #1f2a3d;
25
- --border: #1e2d45;
26
- --accent: #3b82f6;
27
- --accent2: #6366f1;
28
  --green: #10b981;
29
  --red: #ef4444;
30
  --yellow: #f59e0b;
31
- --text: #f1f5f9;
32
- --text2: #94a3b8;
33
- --text3: #64748b;
34
  --radius: 12px;
35
- --shadow: 0 4px 24px rgba(0, 0, 0, .4);
 
 
36
  }
37
 
38
- html,
39
- body {
40
  height: 100%;
41
  font-family: 'Inter', sans-serif;
42
  background: var(--bg);
43
  color: var(--text);
44
- overflow: hidden
45
  }
46
 
 
 
 
 
 
 
 
 
47
  /* ── Login Screen ── */
48
  #login-screen {
49
- position: fixed;
50
- inset: 0;
51
- display: flex;
52
- align-items: center;
53
- justify-content: center;
54
- background: radial-gradient(ellipse at 50% 0%, #1a2a4a 0%, var(--bg) 70%);
55
  z-index: 9999;
56
  }
57
-
58
  .login-card {
59
- width: 380px;
60
- background: var(--surface);
 
61
  border: 1px solid var(--border);
62
- border-radius: 20px;
63
- padding: 44px 40px;
64
- box-shadow: var(--shadow);
65
  animation: fadeUp .5s ease;
66
  }
67
-
68
- .login-logo {
69
- text-align: center;
70
- margin-bottom: 28px
71
- }
72
-
73
- .login-logo svg {
74
- width: 52px;
75
- height: 52px
76
- }
77
-
78
  .login-logo h1 {
79
- font-size: 22px;
80
- font-weight: 700;
81
- margin-top: 12px;
82
  background: linear-gradient(135deg, var(--accent), var(--accent2));
83
- -webkit-background-clip: text;
84
- -webkit-text-fill-color: transparent
85
  }
86
-
87
- .login-logo p {
88
- font-size: 13px;
89
- color: var(--text3);
90
- margin-top: 4px
91
- }
92
-
93
- .form-group {
94
- margin-bottom: 16px
95
- }
96
-
97
  .form-group label {
98
- display: block;
99
- font-size: 12px;
100
- font-weight: 600;
101
- color: var(--text2);
102
- text-transform: uppercase;
103
- letter-spacing: .06em;
104
- margin-bottom: 6px
105
  }
106
-
107
  .form-group input {
108
- width: 100%;
109
- padding: 11px 14px;
110
- background: var(--surface2);
111
- border: 1px solid var(--border);
112
- border-radius: 8px;
113
- color: var(--text);
114
- font-size: 14px;
115
- font-family: inherit;
116
- outline: none;
117
- transition: border-color .2s;
118
- }
119
-
120
- .form-group input:focus {
121
- border-color: var(--accent)
122
- }
123
-
124
- .btn-login {
125
- width: 100%;
126
- padding: 12px;
127
- border: none;
128
- border-radius: 8px;
129
- cursor: pointer;
130
- font-size: 14px;
131
- font-weight: 600;
132
- font-family: inherit;
133
- background: linear-gradient(135deg, var(--accent), var(--accent2));
134
- color: #fff;
135
- transition: opacity .2s, transform .15s;
136
- margin-top: 4px;
137
- }
138
-
139
- .btn-login:hover {
140
- opacity: .9;
141
- transform: translateY(-1px)
142
- }
143
-
144
  .login-error {
145
- display: none;
146
- background: rgba(239, 68, 68, .12);
147
- border: 1px solid rgba(239, 68, 68, .3);
148
- border-radius: 8px;
149
- padding: 10px 14px;
150
- font-size: 13px;
151
- color: #fca5a5;
152
- margin-top: 14px;
153
  }
154
 
155
- /* ── App Shell ── */
156
  #app {
157
- display: none;
158
- height: 100vh;
159
- display: grid;
160
- grid-template-columns: 280px 1fr;
161
- grid-template-rows: 60px 1fr
162
- }
163
-
164
- .topbar {
165
- grid-column: 1/-1;
166
- display: flex;
167
- align-items: center;
168
- justify-content: space-between;
169
- padding: 0 24px;
170
- background: var(--surface);
171
- border-bottom: 1px solid var(--border);
172
- }
173
-
174
- .topbar-brand {
175
- display: flex;
176
- align-items: center;
177
- gap: 10px;
178
- font-weight: 700;
179
- font-size: 16px
180
- }
181
-
182
- .topbar-brand .dot {
183
- width: 8px;
184
- height: 8px;
185
- background: var(--green);
186
- border-radius: 50%;
187
- box-shadow: 0 0 8px var(--green);
188
- animation: pulse 2s infinite
189
- }
190
-
191
- .topbar-right {
192
- display: flex;
193
- align-items: center;
194
- gap: 14px
195
- }
196
-
197
- .badge {
198
- font-size: 11px;
199
- padding: 3px 10px;
200
- border-radius: 20px;
201
- background: rgba(59, 130, 246, .15);
202
- color: var(--accent);
203
- border: 1px solid rgba(59, 130, 246, .25)
204
- }
205
-
206
- .btn-logout {
207
- padding: 7px 16px;
208
- border: 1px solid var(--border);
209
- border-radius: 8px;
210
- background: transparent;
211
- color: var(--text2);
212
- font-size: 13px;
213
- font-family: inherit;
214
- cursor: pointer;
215
- transition: all .2s;
216
- }
217
-
218
- .btn-logout:hover {
219
- border-color: var(--red);
220
- color: var(--red)
221
  }
222
 
223
  /* ── Sidebar ── */
224
  .sidebar {
225
- background: var(--surface);
226
- border-right: 1px solid var(--border);
227
- overflow: hidden;
228
- display: flex;
229
- flex-direction: column;
230
  }
231
-
232
- .sidebar-head {
233
- padding: 16px 20px;
234
  border-bottom: 1px solid var(--border);
235
  }
236
-
237
- .sidebar-head h2 {
238
- font-size: 12px;
239
- font-weight: 600;
240
- text-transform: uppercase;
241
- letter-spacing: .08em;
242
- color: var(--text3)
243
- }
244
-
245
- .sidebar-stats {
246
- display: flex;
247
- gap: 16px;
248
- margin-top: 10px
249
- }
250
-
251
- .stat {
252
- text-align: center;
253
- flex: 1;
254
- background: var(--surface2);
255
- border-radius: 8px;
256
- padding: 8px 4px
257
- }
258
-
259
- .stat-val {
260
- font-size: 20px;
261
- font-weight: 700;
262
- color: var(--accent)
263
- }
264
-
265
- .stat-lbl {
266
- font-size: 10px;
267
- color: var(--text3);
268
- margin-top: 1px
269
- }
270
-
271
- .sidebar-actions {
272
- padding: 12px 16px;
273
- display: flex;
274
- gap: 8px;
275
- border-bottom: 1px solid var(--border)
276
- }
277
-
278
- .btn-export {
279
- flex: 1;
280
- padding: 8px;
281
- border: 1px solid var(--border);
282
- border-radius: 8px;
283
- background: var(--surface2);
284
- color: var(--text2);
285
- font-size: 12px;
286
- font-weight: 600;
287
- cursor: pointer;
288
- display: flex;
289
- align-items: center;
290
- justify-content: center;
291
- gap: 6px;
292
- transition: all .2s;
293
- }
294
-
295
- .btn-export:hover {
296
- border-color: var(--accent);
297
- color: var(--accent);
298
- background: rgba(59, 130, 246, 0.05)
299
- }
300
-
301
- .export-dropdown {
302
- position: relative;
303
- display: inline-block;
304
- flex: 1
305
- }
306
-
307
- .export-menu {
308
- position: absolute;
309
- top: 100%;
310
- left: 0;
311
- right: 0;
312
- background: var(--surface3);
313
- border: 1px solid var(--border);
314
- border-radius: 8px;
315
- margin-top: 4px;
316
- z-index: 100;
317
- box-shadow: var(--shadow);
318
- overflow: hidden;
319
- /* Smooth show/hide with max-height + opacity */
320
- max-height: 0;
321
- opacity: 0;
322
- visibility: hidden;
323
- transition: max-height .35s ease, opacity .25s ease, visibility 0s .35s;
324
- pointer-events: none;
325
  }
326
-
327
- .export-menu.open {
328
- max-height: 200px;
329
- opacity: 1;
330
- visibility: visible;
331
- transition: max-height .35s ease, opacity .25s ease, visibility 0s 0s;
332
- pointer-events: auto;
333
- }
334
-
335
- .export-menu button {
336
- width: 100%;
337
- padding: 10px;
338
- border: none;
339
- background: none;
340
- color: var(--text2);
341
- font-size: 12px;
342
- text-align: left;
343
- cursor: pointer;
344
- transition: all .2s;
345
- }
346
-
347
- .export-menu button:hover {
348
- background: var(--surface2);
349
- color: var(--text)
350
- }
351
-
352
- .search-wrap {
353
- padding: 12px 16px;
354
- border-bottom: 1px solid var(--border)
355
- }
356
-
357
- .search-wrap input {
358
- width: 100%;
359
- padding: 8px 12px;
360
- background: var(--surface2);
361
- border: 1px solid var(--border);
362
- border-radius: 8px;
363
- color: var(--text);
364
- font-size: 13px;
365
- font-family: inherit;
366
- outline: none;
367
  }
368
-
369
- .search-wrap input:focus {
370
- border-color: var(--accent)
 
 
371
  }
372
-
373
- .session-list {
374
- flex: 1;
375
- overflow-y: auto;
376
- padding: 8px
377
  }
 
 
378
 
379
- .session-list::-webkit-scrollbar {
380
- width: 4px
381
  }
382
 
383
- .session-list::-webkit-scrollbar-track {
384
- background: transparent
 
385
  }
386
-
387
- .session-list::-webkit-scrollbar-thumb {
388
- background: var(--border);
389
- border-radius: 4px
390
  }
391
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  .session-item {
393
- padding: 12px 14px;
394
- border-radius: 10px;
395
- cursor: pointer;
396
- margin-bottom: 4px;
397
- border: 1px solid transparent;
398
- transition: all .2s;
399
- }
400
-
401
- .session-item:hover {
402
- background: var(--surface2);
403
- border-color: var(--border)
404
  }
405
-
406
  .session-item.active {
407
- background: rgba(59, 130, 246, .1);
408
- border-color: rgba(59, 130, 246, .35)
409
- }
410
-
411
- .session-item-id {
412
- font-size: 12px;
413
- font-weight: 600;
414
- color: var(--text);
415
- white-space: nowrap;
416
- overflow: hidden;
417
- text-overflow: ellipsis
418
- }
419
-
420
- .session-item-meta {
421
- display: flex;
422
- justify-content: space-between;
423
- margin-top: 4px
424
- }
425
-
426
- .session-item-time {
427
- font-size: 11px;
428
- color: var(--text3)
429
- }
430
-
431
- .session-item-count {
432
- font-size: 11px;
433
- background: var(--surface3);
434
- padding: 1px 7px;
435
- border-radius: 20px;
436
- color: var(--text2)
437
- }
438
-
439
- .no-sessions {
440
- text-align: center;
441
- padding: 40px 20px;
442
- color: var(--text3);
443
- font-size: 13px
444
- }
445
-
446
- /* ── Main Content ── */
447
- .main {
448
- overflow: hidden;
449
- display: flex;
450
- flex-direction: column;
451
- background: var(--bg)
452
- }
453
-
454
- .main-placeholder {
455
- flex: 1;
456
- display: flex;
457
- flex-direction: column;
458
- align-items: center;
459
- justify-content: center;
460
- color: var(--text3);
461
- gap: 14px;
462
- }
463
-
464
- .main-placeholder svg {
465
- width: 56px;
466
- height: 56px;
467
- opacity: .3
468
- }
469
-
470
- .main-placeholder p {
471
- font-size: 14px
472
- }
473
-
474
- .chat-view {
475
- flex: 1;
476
- display: flex;
477
- flex-direction: column;
478
- overflow: hidden
479
  }
 
 
 
 
 
 
 
480
 
 
481
  .chat-header {
482
- padding: 16px 24px;
483
- background: var(--surface);
484
- border-bottom: 1px solid var(--border);
485
- display: flex;
486
- align-items: center;
487
- justify-content: space-between;
488
- }
489
-
490
- .chat-header-info h3 {
491
- font-size: 14px;
492
- font-weight: 600
493
- }
494
-
495
- .chat-header-info p {
496
- font-size: 12px;
497
- color: var(--text3);
498
- margin-top: 2px
499
- }
500
-
501
- .chat-header-actions {
502
- display: flex;
503
- gap: 8px
504
- }
505
-
506
- .btn-del {
507
- padding: 6px 14px;
508
- border: 1px solid rgba(239, 68, 68, .3);
509
- border-radius: 8px;
510
- background: rgba(239, 68, 68, .08);
511
- color: #fca5a5;
512
- font-size: 12px;
513
- font-family: inherit;
514
- cursor: pointer;
515
- transition: all .2s;
516
- }
517
-
518
- .btn-del:hover {
519
- background: rgba(239, 68, 68, .2)
520
- }
521
-
522
- .messages-area {
523
- flex: 1;
524
- overflow-y: auto;
525
- padding: 24px;
526
- display: flex;
527
- flex-direction: column;
528
- gap: 16px
529
- }
530
-
531
- .messages-area::-webkit-scrollbar {
532
- width: 4px
533
- }
534
-
535
- .messages-area::-webkit-scrollbar-thumb {
536
- background: var(--border);
537
- border-radius: 4px
538
- }
539
-
540
- .msg-block {
541
- display: flex;
542
- flex-direction: column;
543
- gap: 8px
544
- }
545
-
546
- .msg-label {
547
- font-size: 10px;
548
- font-weight: 700;
549
- text-transform: uppercase;
550
- letter-spacing: .08em;
551
- display: flex;
552
- align-items: center;
553
- gap: 6px
554
- }
555
-
556
- .msg-label.q {
557
- color: var(--accent)
558
- }
559
-
560
- .msg-label.a {
561
- color: var(--green)
562
- }
563
-
564
  .msg-bubble {
565
- padding: 13px 16px;
566
- border-radius: 10px;
567
- font-size: 14px;
568
- line-height: 1.65;
569
- white-space: pre-wrap;
570
- word-break: break-word;
571
- max-width: 100%;
572
  }
573
-
574
- .msg-bubble.q {
575
- background: rgba(59, 130, 246, .1);
576
- border: 1px solid rgba(59, 130, 246, .2);
577
- color: var(--text)
578
  }
579
-
580
- .msg-bubble.a {
581
- background: rgba(16, 185, 129, .08);
582
- border: 1px solid rgba(16, 185, 129, .2);
583
- color: var(--text)
584
  }
585
 
586
- .msg-ts {
587
- font-size: 11px;
588
  color: var(--text3);
589
- margin-top: 2px
590
- }
591
-
592
- .msg-num {
593
- font-size: 11px;
594
- background: var(--surface2);
595
- padding: 1px 8px;
596
- border-radius: 20px;
597
- color: var(--text3)
598
- }
599
-
600
- .spinner {
601
- width: 32px;
602
- height: 32px;
603
- border: 3px solid var(--border);
604
- border-top-color: var(--accent);
605
- border-radius: 50%;
606
- animation: spin .7s linear infinite;
607
- margin: auto;
608
  }
 
609
 
 
610
  .toast {
611
- position: fixed;
612
- bottom: 24px;
613
- right: 24px;
614
- padding: 12px 20px;
615
- background: var(--surface2);
616
- border: 1px solid var(--border);
617
- border-radius: 10px;
618
- font-size: 13px;
619
- color: var(--text);
620
- box-shadow: var(--shadow);
621
- transform: translateY(20px);
622
- opacity: 0;
623
- transition: all .3s;
624
- pointer-events: none;
625
- z-index: 9998;
626
- }
627
-
628
- .toast.show {
629
- transform: translateY(0);
630
- opacity: 1
631
- }
632
-
633
- .toast.success {
634
- border-color: rgba(16, 185, 129, .4);
635
- color: #6ee7b7
636
- }
637
-
638
- .toast.error {
639
- border-color: rgba(239, 68, 68, .4);
640
- color: #fca5a5
641
- }
642
-
643
- /* ── Export Modal ── */
644
- .export-modal-overlay {
645
- position: fixed;
646
- inset: 0;
647
- background: rgba(0,0,0,.6);
648
- backdrop-filter: blur(4px);
649
- z-index: 10000;
650
- display: flex;
651
- align-items: center;
652
- justify-content: center;
653
- opacity: 0;
654
- visibility: hidden;
655
- transition: opacity .25s ease, visibility 0s .25s;
656
- }
657
- .export-modal-overlay.open {
658
- opacity: 1;
659
- visibility: visible;
660
- transition: opacity .25s ease, visibility 0s 0s;
661
- }
662
- .export-modal {
663
- background: var(--surface);
664
- border: 1px solid var(--border);
665
- border-radius: 16px;
666
- padding: 32px 28px;
667
- width: 380px;
668
- box-shadow: 0 20px 60px rgba(0,0,0,.6);
669
- transform: translateY(16px) scale(.97);
670
- transition: transform .25s ease;
671
- }
672
- .export-modal-overlay.open .export-modal {
673
- transform: translateY(0) scale(1);
674
- }
675
- .export-modal-header {
676
- display: flex;
677
- align-items: center;
678
- justify-content: space-between;
679
- margin-bottom: 22px;
680
- }
681
- .export-modal-header h3 {
682
- font-size: 15px;
683
- font-weight: 700;
684
- background: linear-gradient(135deg, var(--accent), var(--accent2));
685
- -webkit-background-clip: text;
686
- -webkit-text-fill-color: transparent;
687
- }
688
- .export-modal-close {
689
- background: none;
690
- border: none;
691
- color: var(--text3);
692
- cursor: pointer;
693
- font-size: 20px;
694
- line-height: 1;
695
- transition: color .2s;
696
- padding: 2px 6px;
697
- border-radius: 6px;
698
- }
699
- .export-modal-close:hover { color: var(--text); background: var(--surface2); }
700
- .export-modal-section {
701
- margin-bottom: 20px;
702
- }
703
- .export-modal-label {
704
- font-size: 11px;
705
- font-weight: 600;
706
- text-transform: uppercase;
707
- letter-spacing: .07em;
708
- color: var(--text3);
709
- margin-bottom: 10px;
710
- }
711
- .export-radio-group {
712
- display: flex;
713
- flex-direction: column;
714
- gap: 8px;
715
- }
716
- .export-radio-option {
717
- display: flex;
718
- align-items: flex-start;
719
- gap: 10px;
720
- padding: 11px 14px;
721
- border-radius: 10px;
722
- border: 1px solid var(--border);
723
- background: var(--surface2);
724
- cursor: pointer;
725
- transition: all .2s;
726
- }
727
- .export-radio-option:hover {
728
- border-color: rgba(59,130,246,.4);
729
- background: rgba(59,130,246,.05);
730
- }
731
- .export-radio-option input[type=radio] {
732
- margin-top: 1px;
733
- accent-color: var(--accent);
734
- width: 15px;
735
- height: 15px;
736
- flex-shrink: 0;
737
- cursor: pointer;
738
- }
739
- .export-radio-option.selected {
740
- border-color: rgba(59,130,246,.5);
741
- background: rgba(59,130,246,.08);
742
- }
743
- .export-radio-text strong {
744
- font-size: 13px;
745
- font-weight: 600;
746
- color: var(--text);
747
- display: block;
748
- }
749
- .export-radio-text span {
750
- font-size: 11px;
751
- color: var(--text3);
752
- margin-top: 2px;
753
- display: block;
754
- }
755
- .export-format-group {
756
- display: flex;
757
- gap: 8px;
758
- }
759
- .export-format-btn {
760
- flex: 1;
761
- padding: 9px 12px;
762
- border-radius: 9px;
763
- border: 1px solid var(--border);
764
- background: var(--surface2);
765
- color: var(--text2);
766
- font-size: 12px;
767
- font-weight: 600;
768
- font-family: inherit;
769
- cursor: pointer;
770
- transition: all .2s;
771
- text-align: center;
772
- }
773
- .export-format-btn:hover { border-color: rgba(59,130,246,.4); color: var(--text); }
774
- .export-format-btn.active {
775
- border-color: var(--accent);
776
- background: rgba(59,130,246,.12);
777
- color: var(--accent);
778
- }
779
- .btn-export-modal-submit {
780
- width: 100%;
781
- padding: 12px;
782
- border: none;
783
- border-radius: 10px;
784
- background: linear-gradient(135deg, var(--accent), var(--accent2));
785
- color: #fff;
786
- font-size: 14px;
787
- font-weight: 600;
788
- font-family: inherit;
789
- cursor: pointer;
790
- transition: opacity .2s, transform .15s;
791
- display: flex;
792
- align-items: center;
793
- justify-content: center;
794
- gap: 8px;
795
- margin-top: 4px;
796
- }
797
- .btn-export-modal-submit:hover { opacity: .88; transform: translateY(-1px); }
798
- .btn-export-modal-submit:disabled { opacity: .5; cursor: not-allowed; transform: none; }
799
 
800
  /* ── Animations ── */
801
  @keyframes fadeUp {
802
- from {
803
- opacity: 0;
804
- transform: translateY(18px)
805
- }
806
-
807
- to {
808
- opacity: 1;
809
- transform: translateY(0)
810
- }
811
- }
812
-
813
- @keyframes pulse {
814
-
815
- 0%,
816
- 100% {
817
- opacity: 1
818
- }
819
-
820
- 50% {
821
- opacity: .4
822
- }
823
  }
824
-
825
- @keyframes spin {
826
- to {
827
- transform: rotate(360deg)
828
- }
829
  }
830
  </style>
831
  </head>
832
-
833
  <body>
834
 
835
  <!-- LOGIN SCREEN -->
836
  <div id="login-screen">
837
  <div class="login-card">
838
  <div class="login-logo">
839
- <svg viewBox="0 0 52 52" fill="none">
840
- <rect width="52" height="52" rx="14" fill="url(#lg)" />
841
- <path d="M16 26h20M26 16v20" stroke="#fff" stroke-width="2.5" stroke-linecap="round" />
842
  <defs>
843
- <linearGradient id="lg" x1="0" y1="0" x2="52" y2="52">
844
- <stop stop-color="#3b82f6" />
845
- <stop offset="1" stop-color="#6366f1" />
846
  </linearGradient>
847
  </defs>
848
  </svg>
849
- <h1>Martechsol Admin</h1>
850
- <p>Secure Chat Analytics Dashboard</p>
851
  </div>
852
- <!-- Stage 1: Username & Password -->
853
  <div id="login-stage-1">
854
  <div class="form-group">
855
  <label>Username</label>
856
- <input type="text" id="inp-user" placeholder="Enter username" autocomplete="username" />
857
  </div>
858
  <div class="form-group">
859
  <label>Password</label>
860
- <input type="password" id="inp-pass" placeholder="Enter password" autocomplete="current-password" />
861
  </div>
862
- <button class="btn-login" id="btn-login">Sign In</button>
863
  </div>
864
 
865
- <!-- Stage 2: OTP (Hidden by default) -->
866
- <div id="login-stage-2" style="display:none">
867
  <div class="form-group">
868
- <label>One-Time Password (OTP)</label>
869
- <input type="text" id="inp-otp" placeholder="6-digit code" maxlength="6"
870
- style="text-align:center;letter-spacing:0.5em;font-size:18px;font-weight:700" />
871
- <p id="otp-hint" style="font-size:11px;color:var(--text3);margin-top:8px;text-align:center">Code sent to your
872
- email</p>
873
  </div>
874
- <button class="btn-login" id="btn-verify">Verify & Enter</button>
875
- <button class="btn-logout" id="btn-back" style="width:100%;margin-top:10px;border:none">Back to Login</button>
876
  </div>
877
 
878
- <div class="login-error" id="login-error">Invalid credentials. Please try again.</div>
879
  </div>
880
  </div>
881
 
882
- <!-- APP SHELL (hidden until login) -->
883
- <div id="app" style="display:none">
884
- <!-- Topbar -->
885
- <header class="topbar">
886
- <div class="topbar-brand">
887
- <div class="dot"></div>
888
- Martechsol Admin Panel
 
 
 
889
  </div>
890
- <div class="topbar-right">
891
- <span class="badge" id="total-badge">Loading…</span>
892
- <button class="btn-logout" id="btn-logout">Sign Out</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
893
  </div>
894
- </header>
895
 
896
- <!-- Sidebar -->
897
- <aside class="sidebar">
898
- <div class="sidebar-head">
899
- <h2>Sessions</h2>
900
- <div class="sidebar-stats">
901
- <div class="stat">
902
- <div class="stat-val" id="stat-total">—</div>
903
- <div class="stat-lbl">Total</div>
 
 
 
 
 
 
 
 
 
 
 
 
904
  </div>
905
- <div class="stat">
906
- <div class="stat-val" id="stat-msgs"></div>
907
- <div class="stat-lbl">Messages</div>
908
  </div>
909
- <div class="stat">
910
- <div class="stat-val" id="stat-today"></div>
911
- <div class="stat-lbl">Today</div>
912
  </div>
913
  </div>
 
 
 
914
  </div>
915
- <div class="sidebar-actions">
916
- <button class="btn-export" id="btn-open-export-modal" style="flex:1">
917
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
918
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" />
919
- </svg>
920
- Export Chat
921
- </button>
922
- </div>
923
- <div class="search-wrap">
924
- <input type="text" id="search-inp" placeholder="Search sessions…" />
925
- </div>
926
- <div class="session-list" id="session-list">
927
- <div class="no-sessions">Loading sessions…</div>
928
- </div>
929
- </aside>
930
 
931
- <!-- Main -->
932
- <main class="main" id="main-area">
933
- <div class="main-placeholder" id="placeholder">
934
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
935
- <path
936
- d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
937
- stroke-linecap="round" stroke-linejoin="round" />
938
- </svg>
939
- <p>Select a session from the sidebar to view the transcript</p>
940
- </div>
941
- <div class="chat-view" id="chat-view" style="display:none">
942
- <div class="chat-header">
943
- <div class="chat-header-info">
944
- <h3 id="view-session-id">Session ID</h3>
945
- <p id="view-session-user" style="font-weight:600;color:var(--accent);margin-top:2px"></p>
946
- <p id="view-session-meta">Loading…</p>
947
  </div>
948
- <div class="chat-header-actions">
949
- <div class="export-dropdown">
950
- <button class="btn-export" style="padding: 6px 12px;">
951
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
952
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" />
953
- </svg>
954
- Export Session
955
- </button>
956
- <div class="export-menu" style="right:0;left:auto;min-width:120px">
957
- <button id="btn-export-session-excel">Excel (.xlsx)</button>
958
- <button id="btn-export-session-json">JSON (.json)</button>
 
 
 
 
959
  </div>
 
 
 
 
 
 
 
960
  </div>
961
- <button class="btn-del" id="btn-delete-session">Delete Session</button>
962
  </div>
963
  </div>
964
- <div class="messages-area" id="messages-area"></div>
965
  </div>
966
  </main>
967
  </div>
968
 
969
- <div class="toast" id="toast"></div>
970
-
971
  <!-- Export Modal -->
972
- <div class="export-modal-overlay" id="export-modal-overlay">
973
- <div class="export-modal" role="dialog" aria-modal="true" aria-labelledby="export-modal-title">
974
- <div class="export-modal-header">
975
- <h3 id="export-modal-title">Export Chat History</h3>
976
- <button class="export-modal-close" id="export-modal-close" aria-label="Close">&times;</button>
977
  </div>
978
-
979
- <div class="export-modal-section">
980
- <div class="export-modal-label">Export Scope</div>
981
- <div class="export-radio-group">
982
- <label class="export-radio-option selected" id="radio-label-one">
983
- <input type="radio" name="export-scope" value="one" id="export-scope-one" checked />
984
- <div class="export-radio-text">
985
- <strong>All sessions in one file</strong>
986
- <span>Combines every session into a single export file</span>
987
- </div>
988
- </label>
989
- <label class="export-radio-option" id="radio-label-sep">
990
- <input type="radio" name="export-scope" value="separate" id="export-scope-separate" />
991
- <div class="export-radio-text">
992
- <strong>Each session as a separate file</strong>
993
- <span>Downloads a ZIP archive containing one file per session</span>
994
- </div>
995
- </label>
996
  </div>
997
- </div>
998
-
999
- <div class="export-modal-section">
1000
- <div class="export-modal-label">File Format</div>
1001
- <div class="export-format-group">
1002
- <button class="export-format-btn active" id="fmt-excel" data-fmt="excel">
1003
- 📊 Excel (.xlsx)
1004
- </button>
1005
- <button class="export-format-btn" id="fmt-json" data-fmt="json">
1006
- 🗂️ JSON (.json)
1007
- </button>
1008
  </div>
 
 
 
 
 
 
1009
  </div>
1010
-
1011
- <button class="btn-export-modal-submit" id="btn-export-modal-submit">
1012
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
1013
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" />
1014
- </svg>
1015
- Export Now
1016
- </button>
1017
  </div>
1018
  </div>
1019
 
1020
- <script>
1021
- (function () {
1022
 
 
 
1023
  let _creds = null;
1024
  let _allSessions = [];
1025
  let _activeId = null;
1026
-
 
1027
  const STORAGE_KEY = 'martech_admin_creds';
1028
 
1029
- // ── Helpers ──────────────────────────────────────────────
1030
- function $(id) { return document.getElementById(id) }
1031
- function authHeader() { return 'Basic ' + btoa(_creds.user + ':' + _creds.pass) }
1032
 
1033
- function showToast(msg, type = '') {
1034
  const t = $('toast');
1035
- t.textContent = msg; t.className = 'toast ' + (type || '');
1036
- t.classList.add('show');
1037
  setTimeout(() => t.classList.remove('show'), 3000);
1038
  }
1039
 
1040
  function fmtDate(iso) {
1041
  if (!iso) return '—';
1042
  const d = new Date(iso);
1043
- return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
1044
  }
1045
-
1046
  function shortId(id) {
1047
  if (!id) return '—';
1048
- return id.length > 16 ? id.slice(0, 8) + '…' + id.slice(-6) : id;
1049
- }
1050
-
1051
- function todayCount(sessions) {
1052
- const today = new Date().toDateString();
1053
- return sessions.filter(s => {
1054
- try { return new Date(s.created_at).toDateString() === today } catch { return false }
1055
- }).length;
1056
  }
1057
 
1058
- // ── Login ─────────────────────────────────────────────────
1059
  async function doLogin() {
1060
- const u = $('inp-user').value.trim();
1061
- const p = $('inp-pass').value;
1062
- if (!u || !p) return;
1063
-
1064
- $('btn-login').textContent = 'Authenticating…';
1065
- $('btn-login').disabled = true;
1066
- $('login-error').style.display = 'none';
1067
-
1068
  try {
1069
  const r = await fetch('/api/admin/request-otp', {
1070
- method: 'POST',
1071
- headers: { 'Content-Type': 'application/json' },
1072
- body: JSON.stringify({ username: u, password: p })
1073
  });
1074
-
1075
- if (r.ok) {
1076
- const data = await r.json();
1077
- // Switch to OTP stage
1078
- $('login-stage-1').style.display = 'none';
1079
- $('login-stage-2').style.display = 'block';
1080
- if (data.email_status === 'logged_to_console_only') {
1081
- $('otp-hint').textContent = 'SMTP not configured. Check server logs.';
1082
- $('otp-hint').style.color = 'var(--yellow)';
1083
- } else {
1084
- $('otp-hint').textContent = 'Code sent to your registered email.';
1085
- $('otp-hint').style.color = 'var(--text3)';
1086
- }
1087
  $('inp-otp').focus();
1088
  } else {
1089
  const err = await r.json();
1090
  $('login-error').textContent = err.detail || 'Login failed.';
1091
  $('login-error').style.display = 'block';
1092
  }
1093
- } catch (e) {
1094
- $('login-error').textContent = 'Network error: ' + e.message;
1095
- $('login-error').style.display = 'block';
1096
  } finally {
1097
- $('btn-login').textContent = 'Sign In';
1098
- $('btn-login').disabled = false;
1099
  }
1100
  }
1101
 
1102
  async function doVerifyOtp() {
1103
- const u = $('inp-user').value.trim();
1104
- const p = $('inp-pass').value;
1105
- const otp = $('inp-otp').value.trim();
1106
- if (!otp) return;
1107
-
1108
- $('btn-verify').textContent = 'Verifying…';
1109
- $('btn-verify').disabled = true;
1110
- $('login-error').style.display = 'none';
1111
-
1112
  try {
1113
  const r = await fetch('/api/admin/verify-otp', {
1114
- method: 'POST',
1115
- headers: { 'Content-Type': 'application/json' },
1116
- body: JSON.stringify({ username: u, password: p, code: otp })
1117
  });
1118
-
1119
- if (r.ok) {
1120
- _creds = { user: u, pass: p, otp_verified: true };
1121
  localStorage.setItem(STORAGE_KEY, JSON.stringify(_creds));
1122
- $('login-screen').style.display = 'none';
1123
  $('app').style.display = 'grid';
1124
- loadSessions();
1125
- showToast('Login successful', 'success');
1126
  } else {
1127
  const err = await r.json();
1128
- $('login-error').textContent = err.detail || 'Invalid OTP.';
1129
- $('login-error').style.display = 'block';
1130
- $('inp-otp').value = '';
1131
  }
1132
- } catch (e) {
1133
- $('login-error').textContent = 'Network error: ' + e.message;
1134
- $('login-error').style.display = 'block';
1135
  } finally {
1136
- $('btn-verify').textContent = 'Verify & Enter';
1137
- $('btn-verify').disabled = false;
1138
  }
1139
  }
1140
 
1141
- $('btn-login').addEventListener('click', doLogin);
1142
- $('btn-verify').addEventListener('click', doVerifyOtp);
1143
- $('btn-back').addEventListener('click', () => {
1144
- $('login-stage-2').style.display = 'none';
1145
- $('login-stage-1').style.display = 'block';
1146
- $('login-error').style.display = 'none';
1147
- $('inp-otp').value = '';
1148
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1149
 
1150
- [$('inp-user'), $('inp-pass')].forEach(el => el.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin() }));
1151
- $('inp-otp').addEventListener('keydown', e => { if (e.key === 'Enter') doVerifyOtp() });
 
 
 
 
 
1152
 
1153
- $('btn-logout').addEventListener('click', () => {
1154
- _creds = null; _allSessions = []; _activeId = null;
1155
- localStorage.removeItem(STORAGE_KEY);
1156
- $('app').style.display = 'none';
1157
- $('login-screen').style.display = 'flex';
1158
- $('login-stage-2').style.display = 'none';
1159
- $('login-stage-1').style.display = 'block';
1160
- $('inp-pass').value = '';
1161
- $('inp-otp').value = '';
1162
- $('login-error').style.display = 'none';
1163
  });
1164
 
1165
- // ── Auto-Login ────────────────────────────────────────────
1166
- (function init() {
1167
- const stored = localStorage.getItem(STORAGE_KEY);
1168
- if (stored) {
1169
- try {
1170
- const parsed = JSON.parse(stored);
1171
- if (parsed.user && parsed.pass && parsed.otp_verified) {
1172
- _creds = parsed;
1173
- $('login-screen').style.display = 'none';
1174
- $('app').style.display = 'grid';
1175
- loadSessions();
1176
- } else {
1177
- // Force re-login if OTP was not verified
1178
- localStorage.removeItem(STORAGE_KEY);
1179
- }
1180
- } catch (e) {
1181
- localStorage.removeItem(STORAGE_KEY);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1182
  }
1183
- }
1184
- })();
1185
 
1186
- // ── Sessions list ─────────────────────────────────────────
1187
  async function loadSessions() {
1188
- $('session-list').innerHTML = '<div class="no-sessions"><div class="spinner"></div></div>';
1189
  try {
1190
- const r = await fetch('/api/admin/sessions', { headers: { Authorization: authHeader() } });
1191
- if (!r.ok) throw new Error('HTTP ' + r.status);
1192
  const data = await r.json();
1193
  _allSessions = data.sessions || [];
1194
- renderSidebar(_allSessions);
1195
- updateStats(_allSessions);
1196
- } catch (e) {
1197
- $('session-list').innerHTML = '<div class="no-sessions">Failed to load sessions.</div>';
1198
- showToast('Could not fetch sessions: ' + e.message, 'error');
1199
  }
1200
  }
1201
 
1202
- function updateStats(sessions) {
1203
- const totalMsgs = sessions.reduce((a, s) => a + (s.message_count || 0), 0);
1204
- $('stat-total').textContent = sessions.length;
1205
- $('stat-msgs').textContent = totalMsgs;
1206
- $('stat-today').textContent = todayCount(sessions);
1207
- $('total-badge').textContent = sessions.length + ' Sessions';
1208
- }
1209
-
1210
- function renderSidebar(sessions) {
1211
- if (!sessions.length) {
1212
- $('session-list').innerHTML = '<div class="no-sessions">No sessions yet.</div>';
1213
  return;
1214
  }
1215
- $('session-list').innerHTML = sessions.map(s => {
 
1216
  const isDefault = !s.user_name || s.user_name === 'default name' || s.user_name === 'default_name';
1217
- const displayName = isDefault
1218
- ? (s.session_id || '').replace(/_/g, ' ').trim() || shortId(s.session_id)
1219
- : s.user_name;
1220
  return `
1221
- <div class="session-item${s.session_id === _activeId ? ' active' : ''}"
1222
- data-id="${s.session_id}">
1223
- <div class="session-item-id" title="${displayName}">${displayName}</div>
1224
- <div class="session-item-meta">
1225
- <span class="session-item-time">${fmtDate(s.last_active)}</span>
1226
- <span class="session-item-count">${s.message_count || 0} msg${s.message_count !== 1 ? 's' : ''}</span>
1227
- </div>
1228
- </div>
1229
- `;
 
 
1230
  }).join('');
 
1231
  document.querySelectorAll('.session-item').forEach(el => {
1232
- el.addEventListener('click', () => openSession(el.dataset.id));
1233
  });
1234
  }
1235
 
1236
- $('search-inp').addEventListener('input', e => {
1237
- const q = e.target.value.toLowerCase();
1238
- const filtered = _allSessions.filter(s =>
1239
- s.session_id.toLowerCase().includes(q) ||
1240
- (s.user_name || '').toLowerCase().includes(q)
1241
- );
1242
- renderSidebar(filtered);
1243
- });
1244
 
1245
- // ── Session detail ────────────────────────────────────────
1246
  async function openSession(id) {
1247
  _activeId = id;
1248
- document.querySelectorAll('.session-item').forEach(el => {
1249
- el.classList.toggle('active', el.dataset.id === id);
1250
- });
1251
- $('placeholder').style.display = 'none';
1252
- $('chat-view').style.display = 'flex';
1253
- $('view-session-id').textContent = shortId(id);
1254
- $('view-session-user').textContent = '';
1255
- $('view-session-meta').textContent = 'Loading…';
1256
- $('messages-area').innerHTML = '<div class="spinner" style="margin-top:60px"></div>';
1257
-
1258
  try {
1259
- const r = await fetch('/api/admin/sessions/' + encodeURIComponent(id),
1260
- { headers: { Authorization: authHeader() } });
1261
- if (!r.ok) throw new Error('HTTP ' + r.status);
1262
  const data = await r.json();
1263
- renderSession(data);
1264
- } catch (e) {
1265
- $('messages-area').innerHTML = '<div style="color:var(--red);padding:20px">Failed to load session: ' + e.message + '</div>';
1266
- }
1267
- }
1268
-
1269
- function renderSession(data) {
1270
- $('view-session-user').textContent = data.user_name || 'Guest User';
1271
- $('view-session-meta').textContent =
1272
- 'Created: ' + fmtDate(data.created_at) + ' | Last active: ' + fmtDate(data.last_active) +
1273
- ' | ' + (data.message_count || 0) + ' messages';
1274
-
1275
- const msgs = data.messages || [];
1276
- if (!msgs.length) {
1277
- $('messages-area').innerHTML = '<div style="color:var(--text3);text-align:center;margin-top:40px">No messages in this session.</div>';
1278
- return;
 
 
 
 
 
 
 
 
 
 
1279
  }
1280
- $('messages-area').innerHTML = msgs.map(m => `
1281
- <div class="msg-block">
1282
- <div style="display:flex;align-items:center;gap:8px">
1283
- <span class="msg-label q">
1284
- <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>
1285
- User Question
1286
- </span>
1287
- <span class="msg-num">#${m.id || '—'}</span>
1288
- <span class="msg-ts">${fmtDate(m.timestamp)}</span>
1289
- </div>
1290
- <div class="msg-bubble q">${escHtml(m.question)}</div>
1291
- <div class="msg-label a" style="margin-top:6px">
1292
- <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
1293
- Assistant Answer
1294
- </div>
1295
- <div class="msg-bubble a">${escHtml(m.answer)}</div>
1296
- </div>
1297
- `).join('');
1298
  }
1299
 
1300
- function escHtml(str) {
1301
- return String(str || '')
1302
- .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
1303
- .replace(/"/g, '&quot;');
1304
  }
1305
 
1306
- // ── Delete session ────────────────────────────────────────
1307
- $('btn-delete-session').addEventListener('click', async () => {
1308
- if (!_activeId) return;
1309
- if (!confirm('Delete session ' + shortId(_activeId) + '? This cannot be undone.')) return;
1310
  try {
1311
- const r = await fetch('/api/admin/sessions/' + encodeURIComponent(_activeId),
1312
- { method: 'DELETE', headers: { Authorization: authHeader() } });
1313
- if (!r.ok) throw new Error('HTTP ' + r.status);
1314
- showToast('Session deleted.', 'success');
1315
- _allSessions = _allSessions.filter(s => s.session_id !== _activeId);
1316
  _activeId = null;
1317
- $('chat-view').style.display = 'none';
1318
- $('placeholder').style.display = 'flex';
1319
- renderSidebar(_allSessions);
1320
- updateStats(_allSessions);
1321
- } catch (e) { showToast('Delete failed: ' + e.message, 'error') }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1322
  });
1323
 
1324
- // ── Export Session (header) dropdown hover logic ──────────
1325
- document.querySelectorAll('.export-dropdown').forEach(dropdown => {
1326
- const menu = dropdown.querySelector('.export-menu');
1327
- let hideTimer = null;
1328
- function showMenu() { clearTimeout(hideTimer); menu.classList.add('open'); }
1329
- function scheduleHide() {
1330
- clearTimeout(hideTimer);
1331
- hideTimer = setTimeout(() => menu.classList.remove('open'), 300);
1332
  }
1333
- dropdown.addEventListener('mouseenter', showMenu);
1334
- dropdown.addEventListener('mouseleave', scheduleHide);
1335
- menu.addEventListener('mouseenter', showMenu);
1336
- menu.addEventListener('mouseleave', scheduleHide);
1337
- menu.querySelectorAll('button').forEach(btn => {
1338
- btn.addEventListener('click', () => { clearTimeout(hideTimer); menu.classList.remove('open'); });
1339
- });
1340
  });
1341
 
1342
- // ── Exports shared download helper ──────────────────────
1343
- async function downloadExport(url, filename) {
1344
- const r = await fetch(url, { headers: { Authorization: authHeader() } });
1345
- if (!r.ok) throw new Error('HTTP ' + r.status);
1346
  const blob = await r.blob();
1347
  const a = document.createElement('a');
1348
  a.href = URL.createObjectURL(blob);
1349
- a.download = filename;
1350
- a.click();
1351
- }
1352
-
1353
- // ── Session header export (unchanged behaviour) ────────────
1354
- $('btn-export-session-excel').addEventListener('click', () => {
1355
- if (!_activeId) return;
1356
- downloadExport(`/api/admin/export/session/${encodeURIComponent(_activeId)}?format=excel`,
1357
- `session_${_activeId}.xlsx`).then(() => showToast('Export successful', 'success'))
1358
- .catch(e => showToast('Export failed: ' + e.message, 'error'));
1359
- });
1360
- $('btn-export-session-json').addEventListener('click', () => {
1361
- if (!_activeId) return;
1362
- downloadExport(`/api/admin/export/session/${encodeURIComponent(_activeId)}?format=json`,
1363
- `session_${_activeId}.json`).then(() => showToast('Export successful', 'success'))
1364
- .catch(e => showToast('Export failed: ' + e.message, 'error'));
1365
- });
1366
-
1367
- // ── Sidebar Export Modal ───────────────────────────────────
1368
- let _exportFmt = 'excel'; // active format selection
1369
-
1370
- function openExportModal() {
1371
- $('export-modal-overlay').classList.add('open');
1372
  }
1373
- function closeExportModal() {
1374
- $('export-modal-overlay').classList.remove('open');
1375
- }
1376
-
1377
- $('btn-open-export-modal').addEventListener('click', openExportModal);
1378
- $('export-modal-close').addEventListener('click', closeExportModal);
1379
- $('export-modal-overlay').addEventListener('click', e => {
1380
- if (e.target === $('export-modal-overlay')) closeExportModal();
1381
- });
1382
- document.addEventListener('keydown', e => {
1383
- if (e.key === 'Escape') closeExportModal();
1384
- });
1385
-
1386
- // Radio option highlight
1387
- document.querySelectorAll('input[name="export-scope"]').forEach(radio => {
1388
- radio.addEventListener('change', () => {
1389
- $('radio-label-one').classList.toggle('selected', $('export-scope-one').checked);
1390
- $('radio-label-sep').classList.toggle('selected', $('export-scope-separate').checked);
1391
- });
1392
- });
1393
 
1394
- // Format toggle buttons
1395
- document.querySelectorAll('.export-format-btn').forEach(btn => {
1396
- btn.addEventListener('click', () => {
1397
- document.querySelectorAll('.export-format-btn').forEach(b => b.classList.remove('active'));
1398
- btn.classList.add('active');
1399
- _exportFmt = btn.dataset.fmt;
1400
- });
1401
- });
1402
 
1403
- // Submit export
1404
- $('btn-export-modal-submit').addEventListener('click', async () => {
1405
  const scope = document.querySelector('input[name="export-scope"]:checked').value;
1406
  const fmt = _exportFmt;
1407
- const btn = $('btn-export-modal-submit');
1408
- btn.disabled = true;
1409
- btn.innerHTML = '<span style="display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite"></span> Exporting…';
1410
-
1411
  try {
1412
- if (scope === 'one') {
1413
- // Single file — existing endpoint
1414
  const ext = fmt === 'excel' ? 'xlsx' : 'json';
1415
- await downloadExport(`/api/admin/export/all?format=${fmt}`, `all_chats_export.${ext}`);
1416
- showToast('Export successful', 'success');
1417
- closeExportModal();
1418
  } else {
1419
- // Separate files ZIP
1420
- if (!_allSessions.length) { showToast('No sessions to export', 'error'); return; }
1421
  const zip = new JSZip();
1422
  const ext = fmt === 'excel' ? 'xlsx' : 'json';
1423
- let done = 0;
1424
- for (const s of _allSessions) {
1425
  try {
1426
- const r = await fetch(
1427
- `/api/admin/export/session/${encodeURIComponent(s.session_id)}?format=${fmt}`,
1428
- { headers: { Authorization: authHeader() } }
1429
- );
1430
- if (r.ok) {
1431
  const blob = await r.blob();
1432
- const safeName = (s.user_name || s.session_id).replace(/[^a-z0-9_\-]/gi, '_');
1433
- zip.file(`${safeName}_${s.session_id.slice(-6)}.${ext}`, blob);
1434
- done++;
1435
  }
1436
- } catch {/* skip failed sessions */}
1437
  }
1438
- if (!done) throw new Error('No sessions could be exported');
1439
- const zipBlob = await zip.generateAsync({ type: 'blob' });
1440
  const a = document.createElement('a');
1441
  a.href = URL.createObjectURL(zipBlob);
1442
- a.download = `chat_export_${new Date().toISOString().slice(0,10)}.zip`;
1443
  a.click();
1444
- showToast(`Exported ${done} session${done !== 1 ? 's' : ''} as ZIP`, 'success');
1445
- closeExportModal();
1446
  }
1447
- } catch (e) {
1448
- showToast('Export failed: ' + e.message, 'error');
 
1449
  } finally {
1450
- btn.disabled = false;
1451
- btn.innerHTML = '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" /></svg> Export Now';
1452
  }
1453
- });
1454
 
1455
  })();
1456
  </script>
1457
  </body>
1458
-
1459
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width,initial-scale=1.0" />
6
+ <title>Martechsol — Admin Dashboard</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com" />
8
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
9
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
11
  <style>
12
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
13
+
 
 
 
 
 
 
14
  :root {
15
+ --bg: #f8fafc;
16
+ --surface: #ffffff;
17
+ --surface-hover: #f1f5f9;
18
+ --border: #e2e8f0;
19
+ --accent: #8b5cf6;
20
+ --accent2: #a855f7;
21
+ --accent-light: #ede9fe;
22
  --green: #10b981;
23
  --red: #ef4444;
24
  --yellow: #f59e0b;
25
+ --text: #0f172a;
26
+ --text2: #475569;
27
+ --text3: #94a3b8;
28
  --radius: 12px;
29
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
30
+ --shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
31
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
32
  }
33
 
34
+ html, body {
 
35
  height: 100%;
36
  font-family: 'Inter', sans-serif;
37
  background: var(--bg);
38
  color: var(--text);
39
+ overflow: hidden;
40
  }
41
 
42
+ /* ── Utilities ── */
43
+ .flex { display: flex; }
44
+ .items-center { align-items: center; }
45
+ .justify-between { justify-content: space-between; }
46
+ .gap-2 { gap: 8px; }
47
+ .gap-4 { gap: 16px; }
48
+ .hidden { display: none !important; }
49
+
50
  /* ── Login Screen ── */
51
  #login-screen {
52
+ position: fixed; inset: 0;
53
+ display: flex; align-items: center; justify-content: center;
54
+ background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
 
 
 
55
  z-index: 9999;
56
  }
 
57
  .login-card {
58
+ width: 380px; background: var(--surface);
59
+ border-radius: 24px; padding: 48px 40px;
60
+ box-shadow: var(--shadow-lg);
61
  border: 1px solid var(--border);
 
 
 
62
  animation: fadeUp .5s ease;
63
  }
64
+ .login-logo { text-align: center; margin-bottom: 32px; }
65
+ .login-logo svg { width: 56px; height: 56px; margin: 0 auto; display: block; }
 
 
 
 
 
 
 
 
 
66
  .login-logo h1 {
67
+ font-size: 24px; font-weight: 700; margin-top: 16px;
 
 
68
  background: linear-gradient(135deg, var(--accent), var(--accent2));
69
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
 
70
  }
71
+ .login-logo p { font-size: 14px; color: var(--text2); margin-top: 4px; }
72
+ .form-group { margin-bottom: 20px; }
 
 
 
 
 
 
 
 
 
73
  .form-group label {
74
+ display: block; font-size: 12px; font-weight: 600; color: var(--text2);
75
+ text-transform: uppercase; letter-spacing: .06em; margin-bottom: 8px;
 
 
 
 
 
76
  }
 
77
  .form-group input {
78
+ width: 100%; padding: 12px 16px; background: #f8fafc;
79
+ border: 1px solid var(--border); border-radius: 12px;
80
+ color: var(--text); font-size: 15px; outline: none; transition: all .2s;
81
+ }
82
+ .form-group input:focus { border-color: var(--accent); background: #fff; box-shadow: 0 0 0 3px var(--accent-light); }
83
+
84
+ .btn-primary {
85
+ width: 100%; padding: 14px; border: none; border-radius: 12px; cursor: pointer;
86
+ font-size: 15px; font-weight: 600; background: linear-gradient(135deg, var(--accent), var(--accent2));
87
+ color: #fff; transition: all .2s; box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
88
+ }
89
+ .btn-primary:hover { opacity: .9; transform: translateY(-2px); box-shadow: 0 6px 16px rgba(139, 92, 246, 0.4); }
90
+ .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
91
+
92
+ .btn-secondary {
93
+ width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 12px; cursor: pointer;
94
+ font-size: 14px; font-weight: 600; background: var(--surface); color: var(--text2); transition: all .2s;
95
+ }
96
+ .btn-secondary:hover { background: var(--surface-hover); color: var(--text); }
97
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  .login-error {
99
+ display: none; background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px;
100
+ padding: 12px; font-size: 14px; color: #b91c1c; margin-top: 16px; text-align: center;
 
 
 
 
 
 
101
  }
102
 
103
+ /* ── App Layout ── */
104
  #app {
105
+ display: none; height: 100vh; width: 100vw;
106
+ grid-template-columns: 260px 1fr;
107
+ grid-template-rows: 1fr;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  }
109
 
110
  /* ── Sidebar ── */
111
  .sidebar {
112
+ background: var(--surface); border-right: 1px solid var(--border);
113
+ display: flex; flex-direction: column; z-index: 10;
 
 
 
114
  }
115
+ .sidebar-header {
116
+ padding: 24px; display: flex; align-items: center; gap: 12px;
 
117
  border-bottom: 1px solid var(--border);
118
  }
119
+ .sidebar-header svg { width: 32px; height: 32px; }
120
+ .sidebar-header h2 { font-size: 18px; font-weight: 700; color: var(--text); }
121
+
122
+ .nav-menu { padding: 20px 16px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
123
+ .nav-item {
124
+ display: flex; align-items: center; gap: 12px; padding: 12px 16px;
125
+ border-radius: 10px; color: var(--text2); font-weight: 500; font-size: 15px;
126
+ cursor: pointer; transition: all .2s; border: 1px solid transparent;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  }
128
+ .nav-item svg { width: 20px; height: 20px; opacity: 0.7; }
129
+ .nav-item:hover { background: var(--surface-hover); color: var(--text); }
130
+ .nav-item.active {
131
+ background: var(--accent-light); color: var(--accent);
132
+ border-color: rgba(139, 92, 246, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  }
134
+ .nav-item.active svg { opacity: 1; }
135
+
136
+ .nav-section-title {
137
+ font-size: 11px; font-weight: 700; text-transform: uppercase;
138
+ letter-spacing: .08em; color: var(--text3); margin: 20px 0 8px 16px;
139
  }
140
+
141
+ .quick-link {
142
+ display: flex; align-items: center; justify-content: space-between;
143
+ padding: 10px 16px; border-radius: 8px; color: var(--text2);
144
+ font-size: 13px; text-decoration: none; transition: background .2s; margin-bottom: 4px;
145
  }
146
+ .quick-link:hover { background: var(--surface-hover); color: var(--accent); }
147
+ .quick-link svg { width: 14px; height: 14px; }
148
 
149
+ .sidebar-footer {
150
+ padding: 20px; border-top: 1px solid var(--border);
151
  }
152
 
153
+ /* ── Main Area ── */
154
+ .main-area {
155
+ display: flex; flex-direction: column; background: var(--bg); overflow: hidden;
156
  }
157
+ .topbar {
158
+ height: 70px; padding: 0 32px; background: var(--surface);
159
+ border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between;
160
+ flex-shrink: 0;
161
  }
162
+ .topbar-title { font-size: 20px; font-weight: 600; color: var(--text); }
163
+
164
+ .view-container { flex: 1; overflow-y: auto; padding: 32px; display: none; }
165
+ .view-container.active { display: block; animation: fadeUp .4s ease; }
166
+
167
+ /* ── Dashboard View ── */
168
+ .stats-grid {
169
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 24px; margin-bottom: 32px;
170
+ }
171
+ .stat-card {
172
+ background: var(--surface); border-radius: 16px; padding: 24px;
173
+ border: 1px solid var(--border); box-shadow: var(--shadow-sm);
174
+ display: flex; flex-direction: column; gap: 8px;
175
+ }
176
+ .stat-card-title { font-size: 14px; font-weight: 500; color: var(--text2); }
177
+ .stat-card-value { font-size: 36px; font-weight: 700; color: var(--text); display: flex; align-items: baseline; gap: 8px; }
178
+ .stat-card-value.accent { color: var(--accent); }
179
+
180
+ .chart-container {
181
+ background: var(--surface); border-radius: 16px; padding: 24px;
182
+ border: 1px solid var(--border); box-shadow: var(--shadow-sm);
183
+ height: 400px; width: 100%;
184
+ }
185
+
186
+ /* ── Sessions View ── */
187
+ #view-sessions { padding: 0; display: flex; height: 100%; }
188
+ .sessions-sidebar {
189
+ width: 320px; background: var(--surface); border-right: 1px solid var(--border);
190
+ display: flex; flex-direction: column; flex-shrink: 0;
191
+ }
192
+ .sessions-header {
193
+ padding: 20px; border-bottom: 1px solid var(--border);
194
+ display: flex; flex-direction: column; gap: 12px;
195
+ }
196
+ .search-input {
197
+ width: 100%; padding: 10px 14px; border: 1px solid var(--border);
198
+ border-radius: 10px; background: var(--bg); font-size: 14px; outline: none;
199
+ }
200
+ .search-input:focus { border-color: var(--accent); }
201
+ .session-actions { display: flex; gap: 8px; }
202
+
203
+ .session-list { flex: 1; overflow-y: auto; padding: 12px; }
204
  .session-item {
205
+ padding: 14px; border-radius: 12px; cursor: pointer; margin-bottom: 8px;
206
+ border: 1px solid transparent; transition: all .2s;
 
 
 
 
 
 
 
 
 
207
  }
208
+ .session-item:hover { background: var(--bg); border-color: var(--border); }
209
  .session-item.active {
210
+ background: var(--accent-light); border-color: rgba(139, 92, 246, 0.3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  }
212
+ .session-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
213
+ .session-item-name { font-weight: 600; font-size: 14px; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
214
+ .session-item.active .session-item-name { color: var(--accent); }
215
+ .session-item-time { font-size: 11px; color: var(--text3); }
216
+ .session-item-meta { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: var(--text2); }
217
+ .msg-badge { background: var(--bg); padding: 2px 8px; border-radius: 20px; font-size: 11px; font-weight: 600; border: 1px solid var(--border); }
218
+ .session-item.active .msg-badge { background: #fff; color: var(--accent); border-color: rgba(139, 92, 246, 0.3); }
219
 
220
+ .chat-view { flex: 1; display: flex; flex-direction: column; background: var(--bg); }
221
  .chat-header {
222
+ padding: 20px 32px; background: var(--surface); border-bottom: 1px solid var(--border);
223
+ display: flex; align-items: center; justify-content: space-between;
224
+ }
225
+ .chat-header-info h3 { font-size: 18px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
226
+ .chat-header-info p { font-size: 13px; color: var(--text2); }
227
+ .chat-header-actions { display: flex; gap: 12px; }
228
+
229
+ .chat-messages {
230
+ flex: 1; overflow-y: auto; padding: 32px; display: flex; flex-direction: column; gap: 24px;
231
+ }
232
+ .msg-wrapper { display: flex; flex-direction: column; max-width: 80%; }
233
+ .msg-wrapper.user { align-self: flex-end; }
234
+ .msg-wrapper.bot { align-self: flex-start; }
235
+
236
+ .msg-meta { font-size: 11px; color: var(--text3); margin-bottom: 6px; display: flex; gap: 8px; align-items: center; }
237
+ .msg-wrapper.user .msg-meta { justify-content: flex-end; }
238
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  .msg-bubble {
240
+ padding: 16px 20px; border-radius: 18px; font-size: 15px; line-height: 1.6;
241
+ box-shadow: var(--shadow-sm); white-space: pre-wrap; word-wrap: break-word;
 
 
 
 
 
242
  }
243
+ .msg-wrapper.user .msg-bubble {
244
+ background: var(--surface); border: 1px solid var(--border); color: var(--text);
245
+ border-bottom-right-radius: 4px;
 
 
246
  }
247
+ .msg-wrapper.bot .msg-bubble {
248
+ background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #fff;
249
+ border-bottom-left-radius: 4px; border: none;
 
 
250
  }
251
 
252
+ .placeholder-view {
253
+ flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
254
  color: var(--text3);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  }
256
+ .placeholder-view svg { width: 64px; height: 64px; opacity: 0.5; margin-bottom: 16px; }
257
 
258
+ /* ── Toasts & Modals ── */
259
  .toast {
260
+ position: fixed; bottom: 32px; right: 32px; padding: 16px 24px;
261
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
262
+ font-size: 14px; font-weight: 500; color: var(--text); box-shadow: var(--shadow-lg);
263
+ transform: translateY(20px); opacity: 0; transition: all .3s cubic-bezier(0.4, 0, 0.2, 1);
264
+ pointer-events: none; z-index: 9998; display: flex; align-items: center; gap: 12px;
265
+ }
266
+ .toast.show { transform: translateY(0); opacity: 1; }
267
+ .toast.success { border-left: 4px solid var(--green); }
268
+ .toast.error { border-left: 4px solid var(--red); }
269
+
270
+ .modal-overlay {
271
+ position: fixed; inset: 0; background: rgba(15, 23, 42, 0.4); backdrop-filter: blur(4px);
272
+ z-index: 10000; display: flex; align-items: center; justify-content: center;
273
+ opacity: 0; visibility: hidden; transition: all .2s;
274
+ }
275
+ .modal-overlay.open { opacity: 1; visibility: visible; }
276
+ .modal {
277
+ background: var(--surface); border-radius: 20px; padding: 32px; width: 420px;
278
+ box-shadow: var(--shadow-lg); transform: scale(0.95); transition: all .2s;
279
+ }
280
+ .modal-overlay.open .modal { transform: scale(1); }
281
+ .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
282
+ .modal-header h3 { font-size: 18px; font-weight: 600; color: var(--text); }
283
+ .modal-close { background: none; border: none; cursor: pointer; color: var(--text3); padding: 4px; }
284
+ .modal-close:hover { color: var(--text); }
285
+
286
+ .export-option {
287
+ display: flex; align-items: center; gap: 12px; padding: 16px; border: 1px solid var(--border);
288
+ border-radius: 12px; margin-bottom: 12px; cursor: pointer; transition: all .2s;
289
+ }
290
+ .export-option:hover { background: var(--bg); }
291
+ .export-option.selected { border-color: var(--accent); background: var(--accent-light); }
292
+ .export-option input { width: 18px; height: 18px; accent-color: var(--accent); }
293
+ .export-option-text strong { display: block; font-size: 14px; font-weight: 600; color: var(--text); }
294
+ .export-option-text span { display: block; font-size: 12px; color: var(--text2); margin-top: 4px; }
295
+
296
+ .format-btn {
297
+ flex: 1; padding: 12px; border: 1px solid var(--border); border-radius: 10px;
298
+ background: var(--bg); font-weight: 600; font-size: 13px; color: var(--text2); cursor: pointer;
299
+ }
300
+ .format-btn.active { background: var(--surface); border-color: var(--accent); color: var(--accent); box-shadow: var(--shadow-sm); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  /* ── Animations ── */
303
  @keyframes fadeUp {
304
+ from { opacity: 0; transform: translateY(10px); }
305
+ to { opacity: 1; transform: translateY(0); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  }
307
+ @keyframes spin { to { transform: rotate(360deg); } }
308
+ .spinner {
309
+ width: 24px; height: 24px; border: 2px solid var(--border); border-top-color: var(--accent);
310
+ border-radius: 50%; animation: spin .8s linear infinite; margin: auto;
 
311
  }
312
  </style>
313
  </head>
 
314
  <body>
315
 
316
  <!-- LOGIN SCREEN -->
317
  <div id="login-screen">
318
  <div class="login-card">
319
  <div class="login-logo">
320
+ <svg viewBox="0 0 56 56" fill="none">
321
+ <rect width="56" height="56" rx="16" fill="url(#lg)"/>
322
+ <path d="M18 28h20M28 18v20" stroke="#fff" stroke-width="3" stroke-linecap="round"/>
323
  <defs>
324
+ <linearGradient id="lg" x1="0" y1="0" x2="56" y2="56">
325
+ <stop stop-color="#8b5cf6"/><stop offset="1" stop-color="#a855f7"/>
 
326
  </linearGradient>
327
  </defs>
328
  </svg>
329
+ <h1>Welcome Back</h1>
330
+ <p>Sign in to manage your AI agents</p>
331
  </div>
332
+
333
  <div id="login-stage-1">
334
  <div class="form-group">
335
  <label>Username</label>
336
+ <input type="text" id="inp-user" placeholder="admin" autocomplete="username" />
337
  </div>
338
  <div class="form-group">
339
  <label>Password</label>
340
+ <input type="password" id="inp-pass" placeholder="••••••••" autocomplete="current-password" />
341
  </div>
342
+ <button class="btn-primary" id="btn-login">Sign In</button>
343
  </div>
344
 
345
+ <div id="login-stage-2" class="hidden">
 
346
  <div class="form-group">
347
+ <label>Security Code (OTP)</label>
348
+ <input type="text" id="inp-otp" placeholder="123456" maxlength="6" style="text-align:center;letter-spacing:8px;font-size:20px;font-weight:700" />
349
+ <p id="otp-hint" style="font-size:12px;color:var(--text2);margin-top:12px;text-align:center">Code sent to your email.</p>
 
 
350
  </div>
351
+ <button class="btn-primary" id="btn-verify">Verify Code</button>
352
+ <button class="btn-secondary" id="btn-back" style="margin-top:12px">Back</button>
353
  </div>
354
 
355
+ <div class="login-error" id="login-error"></div>
356
  </div>
357
  </div>
358
 
359
+ <!-- APP LAYOUT -->
360
+ <div id="app">
361
+ <!-- Sidebar -->
362
+ <aside class="sidebar">
363
+ <div class="sidebar-header">
364
+ <svg viewBox="0 0 32 32" fill="none">
365
+ <rect width="32" height="32" rx="8" fill="url(#lg)"/>
366
+ <path d="M10 16h12M16 10v12" stroke="#fff" stroke-width="2.5" stroke-linecap="round"/>
367
+ </svg>
368
+ <h2>Admin Panel</h2>
369
  </div>
370
+
371
+ <nav class="nav-menu">
372
+ <div class="nav-item active" data-view="dashboard" id="nav-dashboard">
373
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
374
+ Dashboard
375
+ </div>
376
+ <div class="nav-item" data-view="sessions" id="nav-sessions">
377
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
378
+ Live Sessions
379
+ </div>
380
+
381
+ <div class="nav-section-title">Quick Links</div>
382
+ <a href="/ui" target="_blank" class="quick-link">
383
+ Main Chat UI (Gradio)
384
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
385
+ </a>
386
+ <a href="/widget" target="_blank" class="quick-link">
387
+ Chat Widget Demo
388
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>
389
+ </a>
390
+ <a href="/static/chat-loader.js" target="_blank" class="quick-link">
391
+ chat-loader.js
392
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
393
+ </a>
394
+ <a href="/static/chat-loader2.js" target="_blank" class="quick-link">
395
+ chat-loader2.js
396
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg>
397
+ </a>
398
+ </nav>
399
+
400
+ <div class="sidebar-footer">
401
+ <button class="btn-secondary" id="btn-logout" style="width:100%; border-color:transparent">Sign Out</button>
402
  </div>
403
+ </aside>
404
 
405
+ <!-- Main Content -->
406
+ <main class="main-area">
407
+ <header class="topbar">
408
+ <div class="topbar-title" id="topbar-title">Dashboard Overview</div>
409
+ <div class="flex items-center gap-4">
410
+ <label style="display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text2);cursor:pointer">
411
+ <input type="checkbox" id="toggle-refresh" checked style="accent-color:var(--accent)" /> Auto-refresh
412
+ </label>
413
+ <button class="btn-secondary" id="btn-open-export" style="padding: 8px 16px; font-size: 13px;">
414
+ Export Data
415
+ </button>
416
+ </div>
417
+ </header>
418
+
419
+ <!-- View: Dashboard -->
420
+ <div class="view-container active" id="view-dashboard">
421
+ <div class="stats-grid">
422
+ <div class="stat-card">
423
+ <div class="stat-card-title">Total Sessions</div>
424
+ <div class="stat-card-value accent" id="dash-total-sessions">-</div>
425
  </div>
426
+ <div class="stat-card">
427
+ <div class="stat-card-title">Today's Sessions</div>
428
+ <div class="stat-card-value" id="dash-today-sessions">-</div>
429
  </div>
430
+ <div class="stat-card">
431
+ <div class="stat-card-title">Total Messages</div>
432
+ <div class="stat-card-value" id="dash-total-messages">-</div>
433
  </div>
434
  </div>
435
+ <div class="chart-container">
436
+ <canvas id="messagesChart"></canvas>
437
+ </div>
438
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
 
440
+ <!-- View: Sessions -->
441
+ <div class="view-container" id="view-sessions" style="display:none; padding:0;">
442
+ <div class="sessions-sidebar">
443
+ <div class="sessions-header">
444
+ <input type="text" class="search-input" id="search-inp" placeholder="Search sessions..." />
445
+ <div class="session-actions">
446
+ <button class="btn-secondary" id="btn-refresh-sessions" style="padding: 6px; font-size: 12px; flex:1">↻ Refresh</button>
447
+ </div>
 
 
 
 
 
 
 
 
448
  </div>
449
+ <div class="session-list" id="session-list">
450
+ <!-- Populated by JS -->
451
+ <div style="padding: 40px; text-align:center; color: var(--text3)"><div class="spinner"></div></div>
452
+ </div>
453
+ </div>
454
+ <div class="chat-view">
455
+ <div id="chat-placeholder" class="placeholder-view">
456
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
457
+ <p>Select a session to view the conversation</p>
458
+ </div>
459
+ <div id="chat-active" class="hidden" style="display:flex; flex-direction:column; height:100%;">
460
+ <div class="chat-header">
461
+ <div class="chat-header-info">
462
+ <h3 id="chat-user-name">Guest User</h3>
463
+ <p id="chat-meta">Session ID: -</p>
464
  </div>
465
+ <div class="chat-header-actions">
466
+ <button class="btn-secondary" id="btn-export-single" style="padding: 6px 12px; font-size:12px; border-color:var(--border)">Export</button>
467
+ <button class="btn-secondary" id="btn-delete-session" style="padding: 6px 12px; font-size:12px; color:var(--red); border-color:rgba(239,68,68,0.2)">Delete</button>
468
+ </div>
469
+ </div>
470
+ <div class="chat-messages" id="messages-area">
471
+ <!-- Populated by JS -->
472
  </div>
 
473
  </div>
474
  </div>
 
475
  </div>
476
  </main>
477
  </div>
478
 
 
 
479
  <!-- Export Modal -->
480
+ <div class="modal-overlay" id="export-modal">
481
+ <div class="modal">
482
+ <div class="modal-header">
483
+ <h3>Export Chat History</h3>
484
+ <button class="modal-close" id="export-modal-close"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg></button>
485
  </div>
486
+
487
+ <label class="export-option selected" id="opt-all">
488
+ <input type="radio" name="export-scope" value="all" checked />
489
+ <div class="export-option-text">
490
+ <strong>All Sessions Combined</strong>
491
+ <span>Export every chat session into a single file.</span>
 
 
 
 
 
 
 
 
 
 
 
 
492
  </div>
493
+ </label>
494
+ <label class="export-option" id="opt-zip">
495
+ <input type="radio" name="export-scope" value="zip" />
496
+ <div class="export-option-text">
497
+ <strong>Separate Files (ZIP)</strong>
498
+ <span>Download an archive containing individual session files.</span>
 
 
 
 
 
499
  </div>
500
+ </label>
501
+
502
+ <div style="margin: 24px 0 12px; font-size: 12px; font-weight: 600; color: var(--text3); text-transform: uppercase;">Format</div>
503
+ <div class="flex gap-2" style="margin-bottom: 24px;">
504
+ <button class="format-btn active" data-fmt="excel">Excel (.xlsx)</button>
505
+ <button class="format-btn" data-fmt="json">JSON (.json)</button>
506
  </div>
507
+
508
+ <button class="btn-primary" id="btn-submit-export">Generate Export</button>
 
 
 
 
 
509
  </div>
510
  </div>
511
 
512
+ <div class="toast" id="toast">Message</div>
 
513
 
514
+ <script>
515
+ (function(){
516
  let _creds = null;
517
  let _allSessions = [];
518
  let _activeId = null;
519
+ let _refreshInterval = null;
520
+ let _chartInstance = null;
521
  const STORAGE_KEY = 'martech_admin_creds';
522
 
523
+ const $ = id => document.getElementById(id);
524
+ const authHeader = () => 'Basic ' + btoa(_creds.user + ':' + _creds.pass);
 
525
 
526
+ function showToast(msg, type = 'success') {
527
  const t = $('toast');
528
+ t.innerHTML = (type === 'success' ? ' ' : '⚠ ') + msg;
529
+ t.className = 'toast ' + type + ' show';
530
  setTimeout(() => t.classList.remove('show'), 3000);
531
  }
532
 
533
  function fmtDate(iso) {
534
  if (!iso) return '—';
535
  const d = new Date(iso);
536
+ return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
537
  }
538
+
539
  function shortId(id) {
540
  if (!id) return '—';
541
+ return id.length > 12 ? id.slice(0, 6) + '…' + id.slice(-4) : id;
 
 
 
 
 
 
 
542
  }
543
 
544
+ // ── Login Flow ���
545
  async function doLogin() {
546
+ const u = $('inp-user').value.trim(), p = $('inp-pass').value;
547
+ if(!u || !p) return;
548
+ $('btn-login').textContent = 'Authenticating...'; $('btn-login').disabled = true; $('login-error').style.display = 'none';
 
 
 
 
 
549
  try {
550
  const r = await fetch('/api/admin/request-otp', {
551
+ method: 'POST', headers: {'Content-Type': 'application/json'},
552
+ body: JSON.stringify({username: u, password: p})
 
553
  });
554
+ if(r.ok) {
555
+ $('login-stage-1').classList.add('hidden');
556
+ $('login-stage-2').classList.remove('hidden');
 
 
 
 
 
 
 
 
 
 
557
  $('inp-otp').focus();
558
  } else {
559
  const err = await r.json();
560
  $('login-error').textContent = err.detail || 'Login failed.';
561
  $('login-error').style.display = 'block';
562
  }
563
+ } catch(e) {
564
+ $('login-error').textContent = 'Network error.'; $('login-error').style.display = 'block';
 
565
  } finally {
566
+ $('btn-login').textContent = 'Sign In'; $('btn-login').disabled = false;
 
567
  }
568
  }
569
 
570
  async function doVerifyOtp() {
571
+ const u = $('inp-user').value.trim(), p = $('inp-pass').value, otp = $('inp-otp').value.trim();
572
+ if(!otp) return;
573
+ $('btn-verify').textContent = 'Verifying...'; $('btn-verify').disabled = true; $('login-error').style.display = 'none';
 
 
 
 
 
 
574
  try {
575
  const r = await fetch('/api/admin/verify-otp', {
576
+ method: 'POST', headers: {'Content-Type': 'application/json'},
577
+ body: JSON.stringify({username: u, password: p, code: otp})
 
578
  });
579
+ if(r.ok) {
580
+ _creds = {user: u, pass: p, otp_verified: true};
 
581
  localStorage.setItem(STORAGE_KEY, JSON.stringify(_creds));
582
+ $('login-screen').classList.add('hidden');
583
  $('app').style.display = 'grid';
584
+ initApp();
585
+ showToast('Welcome back, Admin!');
586
  } else {
587
  const err = await r.json();
588
+ $('login-error').textContent = err.detail || 'Invalid OTP.'; $('login-error').style.display = 'block';
 
 
589
  }
590
+ } catch(e) {
591
+ $('login-error').textContent = 'Network error.'; $('login-error').style.display = 'block';
 
592
  } finally {
593
+ $('btn-verify').textContent = 'Verify Code'; $('btn-verify').disabled = false;
 
594
  }
595
  }
596
 
597
+ $('btn-login').onclick = doLogin;
598
+ $('btn-verify').onclick = doVerifyOtp;
599
+ $('btn-back').onclick = () => {
600
+ $('login-stage-2').classList.add('hidden'); $('login-stage-1').classList.remove('hidden'); $('login-error').style.display = 'none'; $('inp-otp').value='';
601
+ };
602
+ $('inp-pass').onkeydown = e => { if(e.key==='Enter') doLogin() };
603
+ $('inp-otp').onkeydown = e => { if(e.key==='Enter') doVerifyOtp() };
604
+
605
+ $('btn-logout').onclick = () => {
606
+ _creds = null; localStorage.removeItem(STORAGE_KEY);
607
+ window.location.reload();
608
+ };
609
+
610
+ // Auto Login check
611
+ (function checkAuth(){
612
+ try {
613
+ const stored = JSON.parse(localStorage.getItem(STORAGE_KEY));
614
+ if(stored && stored.user && stored.otp_verified) {
615
+ _creds = stored;
616
+ $('login-screen').classList.add('hidden');
617
+ $('app').style.display = 'grid';
618
+ initApp();
619
+ } else { localStorage.removeItem(STORAGE_KEY); }
620
+ } catch(e) {}
621
+ })();
622
 
623
+ // ── App Init & Routing ──
624
+ function initApp() {
625
+ switchView('dashboard');
626
+ loadAnalytics();
627
+ loadSessions();
628
+ setupAutoRefresh();
629
+ }
630
 
631
+ document.querySelectorAll('.nav-item').forEach(el => {
632
+ el.onclick = () => {
633
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
634
+ el.classList.add('active');
635
+ switchView(el.dataset.view);
636
+ }
 
 
 
 
637
  });
638
 
639
+ function switchView(view) {
640
+ document.querySelectorAll('.view-container').forEach(v => {
641
+ v.style.display = 'none';
642
+ v.classList.remove('active');
643
+ });
644
+ const v = $('view-' + view);
645
+ v.style.display = view === 'sessions' ? 'flex' : 'block';
646
+ setTimeout(() => v.classList.add('active'), 10);
647
+ $('topbar-title').textContent = view === 'dashboard' ? 'Dashboard Overview' : 'Live Chat Sessions';
648
+
649
+ if(view === 'dashboard') loadAnalytics();
650
+ if(view === 'sessions') loadSessions();
651
+ }
652
+
653
+ // ── Dashboard Analytics ──
654
+ async function loadAnalytics() {
655
+ if(!_creds) return;
656
+ try {
657
+ const r = await fetch('/api/admin/analytics', { headers: {Authorization: authHeader()} });
658
+ if(!r.ok) return;
659
+ const data = await r.json();
660
+ $('dash-total-sessions').textContent = data.total_sessions;
661
+ $('dash-today-sessions').textContent = data.today_sessions;
662
+ $('dash-total-messages').textContent = data.total_messages;
663
+
664
+ renderChart(data.trends);
665
+ } catch(e) { console.error('Analytics error:', e); }
666
+ }
667
+
668
+ function renderChart(trends) {
669
+ const ctx = $('messagesChart').getContext('2d');
670
+ const labels = trends.map(t => {
671
+ const d = new Date(t.date); return d.toLocaleDateString(undefined, {month:'short', day:'numeric'});
672
+ });
673
+ const msgData = trends.map(t => t.messages);
674
+
675
+ if(_chartInstance) _chartInstance.destroy();
676
+
677
+ _chartInstance = new Chart(ctx, {
678
+ type: 'line',
679
+ data: {
680
+ labels: labels,
681
+ datasets: [{
682
+ label: 'Messages',
683
+ data: msgData,
684
+ borderColor: '#8b5cf6',
685
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
686
+ borderWidth: 3,
687
+ tension: 0.4,
688
+ fill: true,
689
+ pointBackgroundColor: '#fff',
690
+ pointBorderColor: '#8b5cf6',
691
+ pointBorderWidth: 2,
692
+ pointRadius: 4,
693
+ pointHoverRadius: 6
694
+ }]
695
+ },
696
+ options: {
697
+ responsive: true,
698
+ maintainAspectRatio: false,
699
+ plugins: { legend: { display: false } },
700
+ scales: {
701
+ y: { beginAtZero: true, grid: { borderDash: [4, 4], color: '#e2e8f0' }, border:{display:false} },
702
+ x: { grid: { display: false }, border:{display:false} }
703
+ },
704
+ interaction: { intersect: false, mode: 'index' }
705
  }
706
+ });
707
+ }
708
 
709
+ // ── Sessions Management ──
710
  async function loadSessions() {
 
711
  try {
712
+ const r = await fetch('/api/admin/sessions', { headers: {Authorization: authHeader()} });
713
+ if(!r.ok) throw new Error('Failed to fetch');
714
  const data = await r.json();
715
  _allSessions = data.sessions || [];
716
+ renderSessionList(_allSessions);
717
+ } catch(e) {
718
+ $('session-list').innerHTML = `<div style="text-align:center;color:var(--red);padding:20px">Error loading sessions</div>`;
 
 
719
  }
720
  }
721
 
722
+ function renderSessionList(sessions) {
723
+ const q = $('search-inp').value.toLowerCase();
724
+ const filtered = sessions.filter(s => s.session_id.toLowerCase().includes(q) || (s.user_name||'').toLowerCase().includes(q));
725
+
726
+ if(!filtered.length) {
727
+ $('session-list').innerHTML = `<div style="text-align:center;color:var(--text3);padding:40px;font-size:13px">No sessions found</div>`;
 
 
 
 
 
728
  return;
729
  }
730
+
731
+ $('session-list').innerHTML = filtered.map(s => {
732
  const isDefault = !s.user_name || s.user_name === 'default name' || s.user_name === 'default_name';
733
+ const name = isDefault ? shortId(s.session_id) : s.user_name;
 
 
734
  return `
735
+ <div class="session-item ${s.session_id === _activeId ? 'active' : ''}" data-id="${s.session_id}">
736
+ <div class="session-item-header">
737
+ <span class="session-item-name" title="${name}">${name}</span>
738
+ <span class="session-item-time">${fmtDate(s.last_active).split(' ')[1] || ''}</span>
739
+ </div>
740
+ <div class="session-item-meta">
741
+ <span>${fmtDate(s.last_active).split(' ')[0] || ''}</span>
742
+ <span class="msg-badge">${s.message_count} msg</span>
743
+ </div>
744
+ </div>
745
+ `;
746
  }).join('');
747
+
748
  document.querySelectorAll('.session-item').forEach(el => {
749
+ el.onclick = () => openSession(el.dataset.id);
750
  });
751
  }
752
 
753
+ $('search-inp').oninput = () => renderSessionList(_allSessions);
754
+ $('btn-refresh-sessions').onclick = loadSessions;
 
 
 
 
 
 
755
 
 
756
  async function openSession(id) {
757
  _activeId = id;
758
+ renderSessionList(_allSessions);
759
+ $('chat-placeholder').style.display = 'none';
760
+ $('chat-active').style.display = 'flex';
761
+ $('messages-area').innerHTML = '<div class="spinner"></div>';
762
+
 
 
 
 
 
763
  try {
764
+ const r = await fetch('/api/admin/sessions/' + encodeURIComponent(id), { headers: {Authorization: authHeader()} });
765
+ if(!r.ok) throw new Error('Fetch failed');
 
766
  const data = await r.json();
767
+ $('chat-user-name').textContent = data.user_name || 'Guest User';
768
+ $('chat-meta').textContent = 'ID: ' + id + ' • ' + (data.message_count||0) + ' messages';
769
+
770
+ const msgs = data.messages || [];
771
+ if(!msgs.length) {
772
+ $('messages-area').innerHTML = '<div style="text-align:center;color:var(--text3);margin-top:40px">No conversation history.</div>';
773
+ return;
774
+ }
775
+
776
+ let html = '';
777
+ msgs.forEach(m => {
778
+ html += `
779
+ <div class="msg-wrapper user">
780
+ <div class="msg-meta">User • ${fmtDate(m.timestamp).split(' ')[1] || ''}</div>
781
+ <div class="msg-bubble">${esc(m.question)}</div>
782
+ </div>
783
+ <div class="msg-wrapper bot">
784
+ <div class="msg-meta">AI Agent • ${fmtDate(m.timestamp).split(' ')[1] || ''}</div>
785
+ <div class="msg-bubble">${esc(m.answer)}</div>
786
+ </div>
787
+ `;
788
+ });
789
+ $('messages-area').innerHTML = html;
790
+ $('messages-area').scrollTop = $('messages-area').scrollHeight;
791
+ } catch(e) {
792
+ $('messages-area').innerHTML = `<div style="color:var(--red);text-align:center;padding:20px">Failed to load chat</div>`;
793
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
794
  }
795
 
796
+ function esc(str) {
797
+ return String(str||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
 
 
798
  }
799
 
800
+ $('btn-delete-session').onclick = async () => {
801
+ if(!_activeId || !confirm('Permanently delete this session?')) return;
 
 
802
  try {
803
+ await fetch('/api/admin/sessions/' + encodeURIComponent(_activeId), { method: 'DELETE', headers: {Authorization: authHeader()} });
804
+ showToast('Session deleted');
 
 
 
805
  _activeId = null;
806
+ $('chat-placeholder').style.display = 'flex';
807
+ $('chat-active').style.display = 'none';
808
+ loadSessions(); loadAnalytics();
809
+ } catch(e) { showToast('Delete failed', 'error'); }
810
+ };
811
+
812
+ // Auto Refresh
813
+ function setupAutoRefresh() {
814
+ clearInterval(_refreshInterval);
815
+ if($('toggle-refresh').checked) {
816
+ _refreshInterval = setInterval(() => {
817
+ if(document.querySelector('.view-container.active').id === 'view-dashboard') loadAnalytics();
818
+ if(document.querySelector('.view-container.active').id === 'view-sessions') loadSessions();
819
+ if(_activeId) {
820
+ // optional: re-fetch active session silently to see new messages
821
+ // but we skip to avoid scroll jumps, user can click refresh
822
+ }
823
+ }, 15000); // 15 seconds
824
+ }
825
+ }
826
+ $('toggle-refresh').onchange = setupAutoRefresh;
827
+
828
+ // ── Export functionality ──
829
+ $('btn-open-export').onclick = () => $('export-modal').classList.add('open');
830
+ $('export-modal-close').onclick = () => $('export-modal').classList.remove('open');
831
+
832
+ document.querySelectorAll('input[name="export-scope"]').forEach(rad => {
833
+ rad.onchange = () => {
834
+ $('opt-all').classList.toggle('selected', $('opt-all').querySelector('input').checked);
835
+ $('opt-zip').classList.toggle('selected', $('opt-zip').querySelector('input').checked);
836
+ };
837
  });
838
 
839
+ let _exportFmt = 'excel';
840
+ document.querySelectorAll('.format-btn').forEach(btn => {
841
+ btn.onclick = () => {
842
+ document.querySelectorAll('.format-btn').forEach(b => b.classList.remove('active'));
843
+ btn.classList.add('active');
844
+ _exportFmt = btn.dataset.fmt;
 
 
845
  }
 
 
 
 
 
 
 
846
  });
847
 
848
+ async function downloadFile(url, filename) {
849
+ const r = await fetch(url, { headers: {Authorization: authHeader()} });
850
+ if(!r.ok) throw new Error('Fetch failed');
 
851
  const blob = await r.blob();
852
  const a = document.createElement('a');
853
  a.href = URL.createObjectURL(blob);
854
+ a.download = filename; a.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
855
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
856
 
857
+ $('btn-export-single').onclick = async () => {
858
+ if(!_activeId) return;
859
+ try {
860
+ await downloadFile(`/api/admin/export/session/${encodeURIComponent(_activeId)}?format=excel`, `session_${_activeId}.xlsx`);
861
+ showToast('Export downloaded');
862
+ } catch(e) { showToast('Export failed', 'error'); }
863
+ };
 
864
 
865
+ $('btn-submit-export').onclick = async () => {
 
866
  const scope = document.querySelector('input[name="export-scope"]:checked').value;
867
  const fmt = _exportFmt;
868
+ const btn = $('btn-submit-export');
869
+ btn.disabled = true; btn.textContent = 'Generating...';
870
+
 
871
  try {
872
+ if(scope === 'all') {
 
873
  const ext = fmt === 'excel' ? 'xlsx' : 'json';
874
+ await downloadFile(`/api/admin/export/all?format=${fmt}`, `all_chats_export.${ext}`);
875
+ showToast('Export downloaded');
 
876
  } else {
877
+ if(!_allSessions.length) throw new Error('No sessions');
 
878
  const zip = new JSZip();
879
  const ext = fmt === 'excel' ? 'xlsx' : 'json';
880
+ for(const s of _allSessions) {
 
881
  try {
882
+ const r = await fetch(`/api/admin/export/session/${encodeURIComponent(s.session_id)}?format=${fmt}`, {headers:{Authorization:authHeader()}});
883
+ if(r.ok) {
 
 
 
884
  const blob = await r.blob();
885
+ const name = (s.user_name || s.session_id).replace(/[^a-z0-9_\-]/gi, '_');
886
+ zip.file(`${name}_${s.session_id.slice(-6)}.${ext}`, blob);
 
887
  }
888
+ } catch(e) {}
889
  }
890
+ const zipBlob = await zip.generateAsync({type: 'blob'});
 
891
  const a = document.createElement('a');
892
  a.href = URL.createObjectURL(zipBlob);
893
+ a.download = `chats_export_${new Date().toISOString().slice(0,10)}.zip`;
894
  a.click();
895
+ showToast('ZIP Export downloaded');
 
896
  }
897
+ $('export-modal').classList.remove('open');
898
+ } catch(e) {
899
+ showToast(e.message, 'error');
900
  } finally {
901
+ btn.disabled = false; btn.textContent = 'Generate Export';
 
902
  }
903
+ };
904
 
905
  })();
906
  </script>
907
  </body>
 
908
  </html>