frabbani commited on
Commit
564a1f3
·
1 Parent(s): 51f57f8

context added .

Browse files
Files changed (1) hide show
  1. static/index.html +597 -333
static/index.html CHANGED
@@ -1,6 +1,7 @@
1
  <!DOCTYPE html>
2
  <!-- VERSION: chart-colors-fix-v2-2026-02-03 -->
3
  <html lang="en">
 
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -13,21 +14,28 @@
13
  /* Color System */
14
  --primary: #5e72e4;
15
  --primary-dark: #465acb;
16
- --secondary: #252f49; /* Darker secondary for inputs/chips */
17
- --text-main: #e2e6ea; /* Off-white text */
 
 
18
  --text-muted: #8898aa;
19
  --success: #2dce89;
20
  --warning: #fb6340;
21
  --danger: #f5365c;
22
  --info: #11cdef;
23
- --derm: #9b59b6; /* Purple for skin analysis */
24
-
 
25
  /* Surfaces */
26
- --bg-body: #0f1219; /* Very dark background */
27
- --bg-card: #171c29; /* Dark card background */
 
 
28
  --bg-chat-user: #5e72e4;
29
- --bg-chat-bot: #252f49; /* Darker bubble */
30
- --border-color: #2b3553; /* Dark borders */
 
 
31
 
32
  /* Spacing & Radius */
33
  --radius-lg: 20px;
@@ -37,8 +45,12 @@
37
  --shadow-hover: 0 7px 14px rgba(0, 0, 0, 0.25), 0 3px 6px rgba(0, 0, 0, 0.2);
38
  }
39
 
40
- * { box-sizing: border-box; margin: 0; padding: 0; }
41
-
 
 
 
 
42
  body {
43
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
44
  background: var(--bg-body);
@@ -75,9 +87,18 @@
75
  z-index: 10;
76
  }
77
 
78
- .header-branding h1 { font-size: 18px; font-weight: 700; color: var(--text-main); }
79
- .header-branding p { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
80
-
 
 
 
 
 
 
 
 
 
81
  .connection-status {
82
  display: flex;
83
  align-items: center;
@@ -89,8 +110,18 @@
89
  border-radius: 30px;
90
  color: var(--text-main);
91
  }
92
- .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--danger); transition: background 0.3s; }
93
- .status-dot.connected { background: var(--success); }
 
 
 
 
 
 
 
 
 
 
94
 
95
  /* Patient Context Bar (Collapsible) */
96
  .context-bar {
@@ -99,6 +130,7 @@
99
  border-bottom: 1px solid var(--border-color);
100
  font-size: 13px;
101
  }
 
102
  .context-toggle {
103
  width: 100%;
104
  display: flex;
@@ -110,6 +142,7 @@
110
  font-weight: 600;
111
  align-items: center;
112
  }
 
113
  .context-details {
114
  display: none;
115
  margin-top: 15px;
@@ -118,17 +151,44 @@
118
  grid-template-columns: repeat(3, 1fr);
119
  gap: 15px;
120
  }
121
- .context-details.open { display: grid; }
122
- .detail-group h4 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-muted); margin-bottom: 8px; }
123
- .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; background: var(--secondary); font-size: 11px; margin-right: 4px; margin-bottom: 4px; color: var(--text-main); border: 1px solid rgba(255,255,255,0.05); }
124
- .tag.alert { background: rgba(245, 54, 92, 0.2); color: #ffadad; border-color: rgba(245, 54, 92, 0.3); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  /* Chat Area */
127
  .chat-viewport {
128
  flex: 1;
129
  overflow-y: auto;
130
  padding: 20px 25px;
131
- background: #131722; /* Slightly different dark for chat area */
 
132
  }
133
 
134
  .message-row {
@@ -136,8 +196,11 @@
136
  margin-bottom: 15px;
137
  width: 100%;
138
  }
139
- .message-row.user { justify-content: flex-end; }
140
-
 
 
 
141
  .bubble {
142
  max-width: 75%;
143
  padding: 12px 18px;
@@ -146,23 +209,28 @@
146
  line-height: 1.5;
147
  position: relative;
148
  }
149
-
150
  /* Standard Messages */
151
  .message-row.user .bubble {
152
  background: var(--primary);
153
  color: white;
154
  border-bottom-right-radius: 4px;
155
- box-shadow: 0 4px 6px rgba(0,0,0,0.2);
156
  }
 
157
  .message-row.assistant .bubble {
158
  background: var(--bg-chat-bot);
159
  color: var(--text-main);
160
  border-bottom-left-radius: 4px;
161
- border: 1px solid rgba(255,255,255,0.05);
162
  }
163
 
164
  /* Widgets (Charts/Audio - Not bubbles) */
165
- .message-row.widget-row { display: block; margin: 20px 0; }
 
 
 
 
166
  .widget-card {
167
  background: var(--bg-card);
168
  border: 1px solid var(--border-color);
@@ -172,9 +240,10 @@
172
  max-width: 100%;
173
  margin: 0 auto;
174
  }
 
175
  .widget-header {
176
  padding: 12px 20px;
177
- background: rgba(255,255,255,0.03);
178
  border-bottom: 1px solid var(--border-color);
179
  font-size: 13px;
180
  font-weight: 600;
@@ -183,17 +252,20 @@
183
  align-items: center;
184
  gap: 8px;
185
  }
186
- .widget-body { padding: 20px; }
 
 
 
187
 
188
  /* Agent Status - All hidden except via feedback card */
189
  .status-line {
190
  display: none !important;
191
  }
192
-
193
  /* Hide any stray status messages that might come through */
194
- .chat-viewport > div[style*="color: #11cdef"],
195
- .chat-viewport > div[style*="color: var(--info)"],
196
- .chat-viewport > span[style*="color: #11cdef"] {
197
  display: none !important;
198
  }
199
 
@@ -216,6 +288,7 @@
216
  border: 1px solid transparent;
217
  transition: border 0.2s;
218
  }
 
219
  .input-group:focus-within {
220
  background: var(--bg-card);
221
  border-color: var(--primary);
@@ -231,7 +304,10 @@
231
  outline: none;
232
  color: var(--text-main);
233
  }
234
- .input-group input::placeholder { color: var(--text-muted); }
 
 
 
235
 
236
  /* Action Buttons */
237
  .btn-icon {
@@ -247,18 +323,38 @@
247
  font-size: 18px;
248
  flex-shrink: 0;
249
  }
250
-
251
- .btn-record { background: #373f55; color: var(--text-main); }
252
- .btn-record:hover { background: #444c66; }
253
- .btn-record.recording {
254
- background: var(--danger);
255
- color: white;
 
 
 
 
 
 
 
256
  animation: pulse 1.5s infinite;
257
  }
258
-
259
- .btn-send { background: var(--primary); color: white; }
260
- .btn-send:hover { background: var(--primary-dark); transform: translateY(-1px); }
261
- .btn-send:disabled { background: #2b3553; color: #5a6585; cursor: not-allowed; transform: none; }
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
  /* Quick Actions - Chips Row with Report Button */
264
  .chips-row {
@@ -267,15 +363,17 @@
267
  gap: 8px;
268
  margin-bottom: 10px;
269
  }
270
-
271
  .chips-scroll {
272
  display: flex;
273
  gap: 8px;
274
  overflow-x: auto;
275
  flex: 1;
276
  padding-bottom: 2px;
277
- scrollbar-width: none; /* Hide scrollbar */
 
278
  }
 
279
  .chip {
280
  white-space: nowrap;
281
  padding: 6px 12px;
@@ -287,7 +385,12 @@
287
  cursor: pointer;
288
  transition: all 0.2s;
289
  }
290
- .chip:hover { border-color: var(--primary); color: var(--primary); background: rgba(94, 114, 228, 0.1); }
 
 
 
 
 
291
 
292
  /* Report Toggle Button - Inline & Compact */
293
  .btn-toggle-report {
@@ -305,47 +408,53 @@
305
  white-space: nowrap;
306
  flex-shrink: 0;
307
  }
308
-
309
  .btn-toggle-report:hover {
310
  border-color: var(--primary);
311
  color: var(--primary);
312
  background: rgba(94, 114, 228, 0.1);
313
  }
314
-
315
  .btn-toggle-report.has-report {
316
  border-color: var(--success);
317
  color: var(--success);
318
  background: rgba(45, 206, 137, 0.1);
319
  }
320
-
321
  .btn-toggle-report .icon {
322
  font-size: 12px;
323
  }
324
 
325
  /* Recording Overlay Logic */
326
  .recording-overlay {
327
- flex: 1;
328
- display: none;
329
  align-items: center;
330
  gap: 10px;
331
  padding-right: 10px;
332
  }
333
-
334
- .wave-viz {
335
- height: 20px;
336
- flex: 1;
337
- background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20" preserveAspectRatio="none"><path d="M0 10 Q 25 20 50 10 T 100 10" stroke="%23f5365c" fill="none" stroke-width="2"/></svg>');
338
  }
339
 
340
  /* Toggle States */
341
- .input-group.recording-active input { display: none; }
342
- .input-group.recording-active .recording-overlay { display: flex; }
 
 
 
 
 
343
 
344
  /* Audio Result Styling (The "Widget") */
345
  .audio-widget {
346
  display: grid;
347
  gap: 15px;
348
  }
 
349
  .score-container {
350
  text-align: center;
351
  padding: 15px;
@@ -353,9 +462,21 @@
353
  border: 1px solid var(--border-color);
354
  border-radius: 12px;
355
  }
356
- .score-val { font-size: 36px; font-weight: 800; display: block; line-height: 1; }
357
- .score-label { font-size: 12px; text-transform: uppercase; color: var(--text-muted); margin-top: 5px; }
358
-
 
 
 
 
 
 
 
 
 
 
 
 
359
  .risk-badge {
360
  padding: 8px 12px;
361
  border-radius: 6px;
@@ -364,21 +485,38 @@
364
  align-items: center;
365
  gap: 8px;
366
  }
367
- .risk-badge.low { background: rgba(45, 206, 137, 0.1); color: #2dce89; border: 1px solid rgba(45, 206, 137, 0.2); }
368
- .risk-badge.high { background: rgba(245, 54, 92, 0.1); color: #f5365c; border: 1px solid rgba(245, 54, 92, 0.2); }
 
 
 
 
 
 
 
 
 
 
 
369
  /* Default badge if no class */
370
- .risk-badge { background: var(--secondary); color: var(--text-main); border: 1px solid var(--border-color); }
 
 
 
 
371
 
372
  /* User Image Message */
373
  .message-row.user .bubble.with-image {
374
  padding: 8px;
375
  max-width: 300px;
376
  }
 
377
  .message-row.user .bubble.with-image img {
378
  max-width: 100%;
379
  border-radius: 12px;
380
  display: block;
381
  }
 
382
  .message-row.user .bubble.with-image .image-caption {
383
  padding: 8px 10px 4px;
384
  font-size: 13px;
@@ -389,15 +527,18 @@
389
  display: grid;
390
  gap: 15px;
391
  }
 
392
  .skin-widget .image-preview {
393
  text-align: center;
394
  }
 
395
  .skin-widget .image-preview img {
396
  max-width: 200px;
397
  max-height: 200px;
398
  border-radius: 12px;
399
  border: 2px solid var(--derm);
400
  }
 
401
  .skin-widget .quality-score {
402
  text-align: center;
403
  padding: 15px;
@@ -405,37 +546,53 @@
405
  border: 1px solid var(--border-color);
406
  border-radius: 12px;
407
  }
 
408
  .skin-widget .quality-val {
409
  font-size: 32px;
410
  font-weight: 800;
411
  display: block;
412
  line-height: 1;
413
  }
 
414
  .skin-widget .quality-label {
415
  font-size: 12px;
416
  text-transform: uppercase;
417
  color: var(--text-muted);
418
  margin-top: 5px;
419
  }
 
420
  .skin-widget .analysis-notes {
421
  background: var(--secondary);
422
  padding: 12px;
423
  border-radius: 8px;
424
  font-size: 13px;
425
  }
 
426
  .skin-widget .analysis-notes li {
427
  margin: 4px 0;
428
  list-style: none;
429
  }
 
430
  .skin-widget .analysis-notes li::before {
431
  content: "• ";
432
  color: var(--derm);
433
  }
434
 
435
  /* Image Upload Button */
436
- .btn-image { background: #373f55; color: var(--text-main); }
437
- .btn-image:hover { background: rgba(155, 89, 182, 0.3); color: var(--derm); }
438
- .btn-image:disabled { opacity: 0.5; cursor: not-allowed; }
 
 
 
 
 
 
 
 
 
 
 
439
 
440
  /* Hidden file input */
441
  #skinImageInput {
@@ -450,6 +607,7 @@
450
  padding-left: 0;
451
  flex-shrink: 0;
452
  }
 
453
  .image-upload-preview img {
454
  width: 36px;
455
  height: 36px;
@@ -457,9 +615,12 @@
457
  border-radius: 8px;
458
  border: 2px solid var(--derm);
459
  }
 
460
  .image-upload-preview .preview-text {
461
- display: none; /* Hide the "Skin image ready" text */
 
462
  }
 
463
  .image-upload-preview .btn-cancel-image {
464
  background: none;
465
  border: none;
@@ -469,32 +630,77 @@
469
  padding: 0 4px;
470
  line-height: 1;
471
  }
 
472
  .image-upload-preview .btn-cancel-image:hover {
473
  color: var(--danger);
474
  }
 
475
  /* Show preview alongside input - don't hide input */
476
- .input-group.image-pending .image-upload-preview { display: flex; }
477
- .input-group.image-pending input {
478
- display: block; /* Keep input visible */
 
 
 
 
479
  }
 
480
  .input-group.image-pending input::placeholder {
481
  color: var(--derm);
482
  }
483
 
484
  /* Skin chip styling */
485
- .chip.skin-chip:hover { border-color: var(--derm); color: var(--derm); background: rgba(155, 89, 182, 0.1); }
 
 
 
 
486
 
487
- @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(245, 54, 92, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(245, 54, 92, 0); } 100% { box-shadow: 0 0 0 0 rgba(245, 54, 92, 0); } }
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
  /* Markdown/Format helpers */
490
- .markdown-body ul, .markdown-body ol { padding-left: 20px; margin: 8px 0; }
491
- .markdown-body li { margin-bottom: 4px; }
492
- .markdown-body p { margin-bottom: 10px; }
493
- .markdown-body p:last-child { margin-bottom: 0; }
494
- .markdown-body strong { font-weight: 700; color: #fff; } /* Strong text white */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
 
496
  /* Tool Result Card */
497
- .tool-result-row { margin: 8px 0; }
 
 
 
498
  .tool-result-card {
499
  background: var(--bg-card);
500
  border: 1px solid var(--border-color);
@@ -502,8 +708,9 @@
502
  overflow: hidden;
503
  max-width: 100%;
504
  }
 
505
  .tool-result-header {
506
- background: rgba(255,255,255,0.05);
507
  padding: 8px 12px;
508
  font-weight: 600;
509
  font-size: 13px;
@@ -511,21 +718,28 @@
511
  text-transform: capitalize;
512
  border-bottom: 1px solid var(--border-color);
513
  }
 
514
  .tool-result-body {
515
  padding: 12px;
516
  font-size: 14px;
517
  color: var(--text-main);
518
  }
 
519
  .tool-result-list {
520
  list-style: none;
521
  padding: 0;
522
  margin: 0;
523
  }
 
524
  .tool-result-list li {
525
  padding: 6px 0;
526
  border-bottom: 1px solid var(--border-color);
527
  }
528
- .tool-result-list li:last-child { border-bottom: none; }
 
 
 
 
529
  .tool-result-list .badge {
530
  background: var(--secondary);
531
  padding: 2px 8px;
@@ -541,17 +755,28 @@
541
  animation: blink 0.7s infinite;
542
  margin-left: 2px;
543
  }
 
544
  @keyframes blink {
545
- 0%, 50% { opacity: 1; }
546
- 51%, 100% { opacity: 0; }
 
 
 
 
 
 
 
 
547
  }
548
 
549
  /* ==========================================
550
  UNIFIED AGENT FEEDBACK - Subtle & Merged
551
  ========================================== */
552
-
553
- .agent-feedback-row { margin: 10px 0; }
554
-
 
 
555
  .agent-feedback-card {
556
  background: rgba(255, 255, 255, 0.02);
557
  border: 1px solid rgba(255, 255, 255, 0.06);
@@ -559,7 +784,7 @@
559
  overflow: hidden;
560
  font-size: 12px;
561
  }
562
-
563
  .agent-feedback-header {
564
  display: flex;
565
  align-items: center;
@@ -569,11 +794,11 @@
569
  cursor: pointer;
570
  user-select: none;
571
  }
572
-
573
  .agent-feedback-header:hover {
574
  background: rgba(255, 255, 255, 0.04);
575
  }
576
-
577
  .agent-feedback-title {
578
  display: flex;
579
  align-items: center;
@@ -581,41 +806,41 @@
581
  color: var(--text-muted);
582
  font-weight: 500;
583
  }
584
-
585
  .agent-feedback-title .icon {
586
  font-size: 12px;
587
  opacity: 0.7;
588
  }
589
-
590
  .agent-feedback-toggle {
591
  color: var(--text-muted);
592
  font-size: 10px;
593
  transition: transform 0.2s;
594
  }
595
-
596
  .agent-feedback-card.expanded .agent-feedback-toggle {
597
  transform: rotate(180deg);
598
  }
599
-
600
  .agent-feedback-content {
601
  display: none;
602
  padding: 0;
603
  border-top: 1px solid rgba(255, 255, 255, 0.04);
604
  }
605
-
606
  .agent-feedback-card.expanded .agent-feedback-content {
607
  display: block;
608
  }
609
-
610
  .feedback-section {
611
  padding: 10px 12px;
612
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
613
  }
614
-
615
  .feedback-section:last-child {
616
  border-bottom: none;
617
  }
618
-
619
  .feedback-section-label {
620
  font-size: 10px;
621
  text-transform: uppercase;
@@ -624,50 +849,50 @@
624
  opacity: 0.7;
625
  margin-bottom: 6px;
626
  }
627
-
628
  .feedback-section-content {
629
  color: var(--text-main);
630
  opacity: 0.85;
631
  line-height: 1.5;
632
  }
633
-
634
  .feedback-section-content .sample-data {
635
  margin-top: 4px;
636
  font-size: 11px;
637
  opacity: 0.6;
638
  }
639
-
640
  .feedback-plan-item {
641
  display: flex;
642
  align-items: flex-start;
643
  gap: 8px;
644
  margin-bottom: 6px;
645
  }
646
-
647
  .feedback-plan-item:last-child {
648
  margin-bottom: 0;
649
  }
650
-
651
  .feedback-plan-bullet {
652
  color: var(--text-muted);
653
  opacity: 0.5;
654
  margin-top: 2px;
655
  }
656
-
657
  .feedback-plan-tool {
658
  font-weight: 500;
659
  }
660
-
661
  .feedback-plan-reason {
662
  font-size: 11px;
663
  opacity: 0.6;
664
  margin-top: 2px;
665
  }
666
-
667
  .feedback-facts {
668
  white-space: pre-wrap;
669
  }
670
-
671
  .feedback-citation {
672
  background: rgba(255, 255, 255, 0.06);
673
  padding: 1px 4px;
@@ -675,7 +900,7 @@
675
  font-family: monospace;
676
  font-size: 10px;
677
  }
678
-
679
  /* Iteration badge */
680
  .iteration-badge {
681
  background: var(--primary);
@@ -686,7 +911,7 @@
686
  margin-left: 8px;
687
  font-weight: 500;
688
  }
689
-
690
  /* Reflection section styles */
691
  .reflection-status {
692
  display: flex;
@@ -696,21 +921,21 @@
696
  border-radius: 6px;
697
  margin-bottom: 8px;
698
  }
699
-
700
  .reflection-status.reflection-complete {
701
  background: rgba(76, 175, 80, 0.15);
702
  border: 1px solid rgba(76, 175, 80, 0.3);
703
  }
704
-
705
  .reflection-status.reflection-gaps {
706
  background: rgba(255, 152, 0, 0.15);
707
  border: 1px solid rgba(255, 152, 0, 0.3);
708
  }
709
-
710
  .reflection-icon {
711
  font-size: 14px;
712
  }
713
-
714
  .confidence-badge {
715
  margin-left: auto;
716
  font-size: 10px;
@@ -718,25 +943,25 @@
718
  padding: 2px 6px;
719
  border-radius: 8px;
720
  }
721
-
722
  .reflection-gaps-list {
723
  font-size: 12px;
724
  color: var(--warning);
725
  margin-bottom: 6px;
726
  }
727
-
728
  .reflection-reasoning {
729
  font-size: 11px;
730
  opacity: 0.7;
731
  font-style: italic;
732
  }
733
-
734
  /* Workflow summary */
735
  .workflow-summary .feedback-section-content {
736
  display: flex;
737
  gap: 16px;
738
  }
739
-
740
  .workflow-stat {
741
  font-size: 12px;
742
  display: flex;
@@ -747,14 +972,14 @@
747
  /* ==========================================
748
  PRE-VISIT REPORT PANEL & MODAL
749
  ========================================== */
750
-
751
  /* Main content wrapper for side-by-side layout */
752
  .main-content-wrapper {
753
  display: flex;
754
  flex: 1;
755
  overflow: hidden;
756
  }
757
-
758
  .chat-container {
759
  flex: 1;
760
  display: flex;
@@ -772,11 +997,11 @@
772
  display: flex;
773
  flex-direction: column;
774
  }
775
-
776
  .report-panel.open {
777
  width: 320px;
778
  }
779
-
780
  .report-panel-header {
781
  padding: 15px;
782
  border-bottom: 1px solid var(--border-color);
@@ -784,13 +1009,13 @@
784
  justify-content: space-between;
785
  align-items: center;
786
  }
787
-
788
  .report-panel-header h3 {
789
  font-size: 14px;
790
  color: var(--text-main);
791
  margin: 0;
792
  }
793
-
794
  .report-panel-close {
795
  background: none;
796
  border: none;
@@ -799,13 +1024,13 @@
799
  font-size: 18px;
800
  padding: 4px;
801
  }
802
-
803
  .report-panel-body {
804
  flex: 1;
805
  overflow-y: auto;
806
  padding: 15px;
807
  }
808
-
809
  .report-preview {
810
  background: linear-gradient(135deg, rgba(94, 114, 228, 0.1) 0%, rgba(94, 114, 228, 0.05) 100%);
811
  border: 1px solid rgba(94, 114, 228, 0.3);
@@ -814,45 +1039,45 @@
814
  cursor: pointer;
815
  transition: all 0.2s;
816
  }
817
-
818
  .report-preview:hover {
819
  border-color: var(--primary);
820
  transform: translateY(-2px);
821
  }
822
-
823
  .report-preview-placeholder {
824
  text-align: center;
825
  color: var(--text-muted);
826
  padding: 30px 15px;
827
  }
828
-
829
  .report-preview-placeholder .icon {
830
  font-size: 32px;
831
  margin-bottom: 10px;
832
  }
833
-
834
  .report-preview h4 {
835
  font-size: 13px;
836
  color: var(--primary);
837
  margin: 0 0 8px 0;
838
  }
839
-
840
  .report-preview-concerns {
841
  font-size: 12px;
842
  color: var(--text-main);
843
  margin-bottom: 8px;
844
  }
845
-
846
  .report-preview-meta {
847
  font-size: 11px;
848
  color: var(--text-muted);
849
  }
850
-
851
  .report-panel-actions {
852
  padding: 15px;
853
  border-top: 1px solid var(--border-color);
854
  }
855
-
856
  .btn-generate-report {
857
  width: 100%;
858
  padding: 12px;
@@ -865,21 +1090,21 @@
865
  cursor: pointer;
866
  transition: background 0.2s;
867
  }
868
-
869
  .btn-generate-report:hover {
870
  background: var(--primary-dark);
871
  }
872
-
873
  .btn-generate-report:disabled {
874
  background: var(--secondary);
875
  cursor: not-allowed;
876
  }
877
-
878
  .btn-generate-report.loading {
879
  position: relative;
880
  color: transparent;
881
  }
882
-
883
  .btn-generate-report.loading::after {
884
  content: '';
885
  position: absolute;
@@ -893,9 +1118,11 @@
893
  border-radius: 50%;
894
  animation: spin 0.8s linear infinite;
895
  }
896
-
897
  @keyframes spin {
898
- to { transform: rotate(360deg); }
 
 
899
  }
900
 
901
  /* Loading indicator for image upload - pulsing dot instead of spinning */
@@ -909,9 +1136,19 @@
909
  margin-left: 6px;
910
  animation: pulse-dot 1s ease-in-out infinite;
911
  }
 
912
  @keyframes pulse-dot {
913
- 0%, 100% { opacity: 0.3; transform: scale(0.8); }
914
- 50% { opacity: 1; transform: scale(1.2); }
 
 
 
 
 
 
 
 
 
915
  }
916
 
917
  /* Thinking indicator - shows while agent is working */
@@ -923,10 +1160,12 @@
923
  color: var(--text-secondary);
924
  font-size: 0.85rem;
925
  }
 
926
  .thinking-indicator .thinking-dots {
927
  display: flex;
928
  gap: 4px;
929
  }
 
930
  .thinking-indicator .thinking-dots span {
931
  width: 6px;
932
  height: 6px;
@@ -934,12 +1173,30 @@
934
  background: var(--primary);
935
  animation: thinking-bounce 1.4s ease-in-out infinite;
936
  }
937
- .thinking-indicator .thinking-dots span:nth-child(2) { animation-delay: 0.2s; }
938
- .thinking-indicator .thinking-dots span:nth-child(3) { animation-delay: 0.4s; }
 
 
 
 
 
 
 
939
  @keyframes thinking-bounce {
940
- 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }
941
- 40% { opacity: 1; transform: translateY(-4px); }
 
 
 
 
 
 
 
 
 
 
942
  }
 
943
  .thinking-indicator .thinking-text {
944
  opacity: 0.8;
945
  transition: opacity 0.3s;
@@ -961,12 +1218,12 @@
961
  visibility: hidden;
962
  transition: all 0.3s;
963
  }
964
-
965
  .report-modal-overlay.open {
966
  opacity: 1;
967
  visibility: visible;
968
  }
969
-
970
  .report-modal {
971
  background: var(--bg-card);
972
  border-radius: var(--radius-lg);
@@ -979,11 +1236,11 @@
979
  transform: scale(0.9);
980
  transition: transform 0.3s;
981
  }
982
-
983
  .report-modal-overlay.open .report-modal {
984
  transform: scale(1);
985
  }
986
-
987
  .report-modal-header {
988
  padding: 20px;
989
  border-bottom: 1px solid var(--border-color);
@@ -991,18 +1248,18 @@
991
  justify-content: space-between;
992
  align-items: center;
993
  }
994
-
995
  .report-modal-header h2 {
996
  font-size: 18px;
997
  margin: 0;
998
  color: var(--text-main);
999
  }
1000
-
1001
  .report-modal-actions {
1002
  display: flex;
1003
  gap: 10px;
1004
  }
1005
-
1006
  .btn-modal-action {
1007
  padding: 8px 16px;
1008
  border-radius: var(--radius-sm);
@@ -1012,17 +1269,17 @@
1012
  border: none;
1013
  transition: all 0.2s;
1014
  }
1015
-
1016
  .btn-download {
1017
  background: var(--success);
1018
  color: white;
1019
  }
1020
-
1021
  .btn-close-modal {
1022
  background: var(--secondary);
1023
  color: var(--text-main);
1024
  }
1025
-
1026
  .report-modal-body {
1027
  flex: 1;
1028
  overflow-y: auto;
@@ -1034,87 +1291,87 @@
1034
  font-size: 14px;
1035
  color: var(--text-main);
1036
  }
1037
-
1038
  .previsit-report .report-header {
1039
  margin-bottom: 20px;
1040
  padding-bottom: 15px;
1041
  border-bottom: 2px solid var(--primary);
1042
  }
1043
-
1044
  .previsit-report .report-header h2 {
1045
  font-size: 20px;
1046
  color: var(--primary);
1047
  margin: 0 0 8px 0;
1048
  }
1049
-
1050
  .previsit-report .report-meta {
1051
  display: flex;
1052
  justify-content: space-between;
1053
  font-size: 13px;
1054
  }
1055
-
1056
  .previsit-report .patient-info {
1057
  color: var(--text-main);
1058
  }
1059
-
1060
  .previsit-report .report-date {
1061
  color: var(--text-muted);
1062
  }
1063
-
1064
  .previsit-report .report-section {
1065
  margin-bottom: 16px;
1066
  }
1067
-
1068
  .previsit-report .report-section h3 {
1069
  font-size: 14px;
1070
  color: var(--primary);
1071
  margin: 0 0 8px 0;
1072
  }
1073
-
1074
  .previsit-report .report-section p {
1075
  margin: 0;
1076
  line-height: 1.5;
1077
  }
1078
-
1079
  .previsit-report .report-section ul {
1080
  margin: 0;
1081
  padding-left: 20px;
1082
  }
1083
-
1084
  .previsit-report .report-section li {
1085
  margin: 4px 0;
1086
  }
1087
-
1088
  .previsit-report .report-columns {
1089
  display: flex;
1090
  gap: 20px;
1091
  }
1092
-
1093
  .previsit-report .report-section.half {
1094
  flex: 1;
1095
  }
1096
-
1097
  .previsit-report .vitals-grid {
1098
  display: grid;
1099
  grid-template-columns: repeat(2, 1fr);
1100
  gap: 8px;
1101
  }
1102
-
1103
  .previsit-report .vital-item {
1104
  background: var(--secondary);
1105
  padding: 8px 12px;
1106
  border-radius: var(--radius-sm);
1107
  }
1108
-
1109
  .previsit-report .vital-name {
1110
  color: var(--text-muted);
1111
  }
1112
-
1113
  .previsit-report .vital-value {
1114
  color: var(--text-main);
1115
  font-weight: 600;
1116
  }
1117
-
1118
  .previsit-report blockquote {
1119
  background: var(--secondary);
1120
  border-left: 3px solid var(--primary);
@@ -1123,13 +1380,13 @@
1123
  font-style: italic;
1124
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
1125
  }
1126
-
1127
  .previsit-report .attachments-list {
1128
  display: flex;
1129
  flex-direction: column;
1130
  gap: 8px;
1131
  }
1132
-
1133
  .previsit-report .attachment-item {
1134
  background: var(--secondary);
1135
  padding: 10px 12px;
@@ -1138,21 +1395,21 @@
1138
  align-items: center;
1139
  gap: 10px;
1140
  }
1141
-
1142
  .previsit-report .attachment-icon {
1143
  font-size: 20px;
1144
  }
1145
-
1146
  .previsit-report .attachment-title {
1147
  font-weight: 600;
1148
  }
1149
-
1150
  .previsit-report .attachment-summary {
1151
  font-size: 12px;
1152
  color: var(--text-muted);
1153
  margin-top: 4px;
1154
  }
1155
-
1156
  .previsit-report .report-footer {
1157
  margin-top: 20px;
1158
  padding-top: 15px;
@@ -1161,47 +1418,48 @@
1161
  color: var(--text-muted);
1162
  text-align: center;
1163
  }
1164
-
1165
  /* NEW: Compact list styling for immunizations/procedures/encounters */
1166
  .previsit-report .compact-list {
1167
  font-size: 12px;
1168
  margin: 0;
1169
  padding-left: 16px;
1170
  }
1171
-
1172
  .previsit-report .compact-list li {
1173
  margin: 3px 0;
1174
  line-height: 1.4;
1175
  }
1176
-
1177
  .previsit-report .date-badge {
1178
  color: var(--text-muted);
1179
  font-size: 11px;
1180
  }
1181
-
1182
  /* Allergy styling with severity indicators */
1183
  .previsit-report .allergies-list {
1184
  margin: 0;
1185
  padding-left: 16px;
1186
  }
1187
-
1188
  .previsit-report .allergies-list li {
1189
  margin: 4px 0;
1190
  }
1191
-
1192
  .previsit-report .allergies-list .allergy-high {
1193
  color: #ff6b6b;
1194
  }
1195
-
1196
  .previsit-report .allergies-list .allergy-high strong {
1197
  color: #ff4757;
1198
  }
1199
-
1200
  .previsit-report .allergies-list .allergy-low {
1201
  color: var(--text-main);
1202
  }
1203
  </style>
1204
  </head>
 
1205
  <body>
1206
 
1207
  <div class="app-shell">
@@ -1242,13 +1500,14 @@
1242
  <div class="chat-viewport" id="chatMessages">
1243
  <div class="message-row assistant">
1244
  <div class="bubble">
1245
- Hello! I'm ready to help. You can ask me to visualize your health data, use the microphone to analyze respiratory sounds, or upload a skin image for analysis.
 
1246
  </div>
1247
  </div>
1248
  </div>
1249
 
1250
  <div class="input-area">
1251
-
1252
  <div class="chips-row">
1253
  <div class="chips-scroll">
1254
  <div class="chip" data-q="Show me my blood pressure chart">📈 BP Chart</div>
@@ -1257,7 +1516,8 @@
1257
  <div class="chip" data-q="Analyze my cough risk">🎤 Cough Check</div>
1258
  <div class="chip skin-chip" data-action="upload-skin">🔬 Skin Analysis</div>
1259
  </div>
1260
- <button class="btn-toggle-report" id="btnToggleReport" onclick="toggleReportPanel()" title="Pre-Visit Report">
 
1261
  <span class="icon">📋</span>
1262
  <span>Report</span>
1263
  </button>
@@ -1268,7 +1528,7 @@
1268
 
1269
  <div class="input-group" id="mainInputGroup">
1270
  <input type="text" id="chatInput" placeholder="Ask a health question..." disabled>
1271
-
1272
  <div class="recording-overlay" id="recordingOverlay">
1273
  <span style="color: var(--danger); font-weight: 600; font-size: 14px;">Recording...</span>
1274
  <div class="wave-viz"></div>
@@ -1280,7 +1540,8 @@
1280
  <button class="btn-cancel-image" onclick="cancelImageUpload()">✕</button>
1281
  </div>
1282
 
1283
- <button class="btn-icon btn-image" id="imageBtn" title="Upload Skin Image" onclick="triggerImageUpload()">
 
1284
  📷
1285
  </button>
1286
  <button class="btn-icon btn-record" id="recordBtn" title="Record Audio" disabled>
@@ -1290,7 +1551,7 @@
1290
 
1291
  </button>
1292
  </div>
1293
-
1294
  <div style="text-align: center; margin-top: 10px; font-size: 11px; color: var(--text-muted);">
1295
  ⚠️ AI responses can be inaccurate. Consult a doctor.
1296
  </div>
@@ -1306,7 +1567,8 @@
1306
  <div class="report-panel-body">
1307
  <div class="report-preview-placeholder" id="reportPlaceholder">
1308
  <div class="icon">📝</div>
1309
- <p>Chat with me about your health concerns, then generate a summary to share with your doctor.</p>
 
1310
  </div>
1311
  <div class="report-preview" id="reportPreview" style="display: none;" onclick="openReportModal()">
1312
  <h4>Pre-Visit Summary Ready</h4>
@@ -1345,22 +1607,22 @@
1345
  // ==========================================
1346
  let patientId = null;
1347
  let chartCounter = 0;
1348
-
1349
  // Report tracking state
1350
  let conversationHistory = [];
1351
  let collectedToolResults = [];
1352
  let collectedAttachments = []; // Now includes {type, title, summary, imageData} for charts AND skin images
1353
  let currentReport = null;
1354
-
1355
  // Agent feedback state - for merging into single card
1356
  let currentFeedbackCard = null;
1357
  let feedbackData = { discovery: null, plan: null, facts: [], reflection: null, workflow: null, iteration: 1 };
1358
-
1359
  // Skin image upload state
1360
  let pendingImageData = null;
1361
  let pendingImageFile = null;
1362
  const skinImageInput = document.getElementById('skinImageInput');
1363
-
1364
  // DOM Elements
1365
  const chatMessages = document.getElementById('chatMessages');
1366
  const chatInput = document.getElementById('chatInput');
@@ -1401,7 +1663,7 @@
1401
  });
1402
 
1403
  // Collapsible Context
1404
- document.getElementById('summaryToggle').addEventListener('click', function() {
1405
  document.getElementById('summaryContent').classList.toggle('open');
1406
  const arrow = this.querySelector('span:last-child');
1407
  arrow.textContent = arrow.textContent === '▼' ? '▲' : '▼';
@@ -1414,7 +1676,7 @@
1414
  // ==========================================
1415
  // SKIN IMAGE UPLOAD HANDLING
1416
  // ==========================================
1417
-
1418
  function triggerImageUpload() {
1419
  skinImageInput.click();
1420
  }
@@ -1439,13 +1701,13 @@
1439
 
1440
  // Read and preview the image
1441
  const reader = new FileReader();
1442
- reader.onload = function(e) {
1443
  pendingImageData = e.target.result;
1444
-
1445
  // Show preview
1446
  document.getElementById('previewThumbnail').src = pendingImageData;
1447
  mainInputGroup.classList.add('image-pending');
1448
-
1449
  // Update placeholder to guide user
1450
  chatInput.placeholder = 'Add a message about this image (optional)...';
1451
  chatInput.focus();
@@ -1477,7 +1739,7 @@
1477
 
1478
  // Store data locally
1479
  const imageToSend = pendingImageData;
1480
-
1481
  // Clear UI state
1482
  cancelImageUpload();
1483
  chatInput.value = ''; // Clear the text input
@@ -1485,11 +1747,11 @@
1485
 
1486
  // 3. Add Loading Feedback (Fixes lack of feedback)
1487
  chatSend.disabled = true;
1488
-
1489
  // Reset feedback state for new image message
1490
  currentFeedbackCard = null;
1491
  feedbackData = { discovery: null, plan: null, facts: [], reflection: null, workflow: null, iteration: 1 };
1492
-
1493
  // Create/Show the feedback card immediately with a loading state
1494
  const feedbackCard = getOrCreateFeedbackCard();
1495
  const titleSpan = feedbackCard.querySelector('.agent-feedback-title span:last-child');
@@ -1501,7 +1763,7 @@
1501
 
1502
  // Send to agent
1503
  await sendAgentMessage(caption, imageToSend);
1504
-
1505
  chatSend.disabled = false;
1506
  chatInput.focus();
1507
  }
@@ -1517,7 +1779,7 @@
1517
  `;
1518
  chatMessages.appendChild(div);
1519
  forceScrollToBottom();
1520
-
1521
  // Track for conversation history
1522
  trackConversation('user', `[Uploaded skin image] ${caption}`);
1523
  }
@@ -1528,14 +1790,14 @@
1528
  const qualityScore = quality.score || 0;
1529
  const notes = quality.notes || ['No analysis notes available'];
1530
  const modelName = result.model || 'Derm Foundation';
1531
-
1532
  // Formulate the analysis summary
1533
  const analysisSummary = `Image Quality: ${qualityScore}/100. ${notes.join(' ')}`;
1534
-
1535
  // Fix: Check if we just added this image (to avoid duplicates in report)
1536
  // We look for the last attachment of type 'skin'
1537
  const lastAttachment = collectedAttachments.length > 0 ? collectedAttachments[collectedAttachments.length - 1] : null;
1538
-
1539
  if (lastAttachment && lastAttachment.type === 'skin' && lastAttachment.imageData === imageData) {
1540
  // Update the existing attachment with the analysis results
1541
  lastAttachment.summary += ` | Analysis: ${analysisSummary}`;
@@ -1544,11 +1806,11 @@
1544
  // Fallback: Track as new if for some reason it wasn't caught
1545
  trackAttachment('skin', `Skin Analysis (${modelName})`, analysisSummary, imageData);
1546
  }
1547
-
1548
  const scoreColor = qualityScore < 50 ? 'var(--danger)' : qualityScore < 75 ? 'var(--warning)' : 'var(--success)';
1549
-
1550
  const notesHtml = notes.map(n => `<li>${escapeHtml(n)}</li>`).join('');
1551
-
1552
  const html = `
1553
  <div class="message-row widget-row">
1554
  <div class="widget-card">
@@ -1577,10 +1839,10 @@
1577
  </div>
1578
  </div>
1579
  `;
1580
-
1581
  chatMessages.insertAdjacentHTML('beforeend', html);
1582
  forceScrollToBottom();
1583
-
1584
  // Track for conversation
1585
  trackConversation('assistant', `[Skin Analysis Result] Quality: ${qualityScore}/100. ${notes.join(' ')}`);
1586
  }
@@ -1610,11 +1872,11 @@
1610
  const res = await fetch('/api/patients');
1611
  const data = await res.json();
1612
  if (data.patients.length === 0) return;
1613
-
1614
  const patient = data.patients[0];
1615
  patientId = patient.id;
1616
  document.getElementById('patientSummaryName').textContent = `Patient: ${patient.display_name} (${patient.age}y)`;
1617
-
1618
  chatInput.disabled = false;
1619
  chatSend.disabled = false;
1620
  loadHealthDetails();
@@ -1632,9 +1894,9 @@
1632
  ]);
1633
 
1634
  // Render Tags
1635
- const renderTags = (list, key, alert=false) => {
1636
  if (!list || list.length === 0) return '<span class="tag">None</span>';
1637
- return list.map(item => `<span class="tag ${alert?'alert':''}">${item[key] || item.display || item.substance}</span>`).join('');
1638
  };
1639
 
1640
  document.getElementById('conditionsList').innerHTML = renderTags(conditions.conditions, 'display');
@@ -1649,11 +1911,11 @@
1649
  // ==========================================
1650
  // UI RENDERING
1651
  // ==========================================
1652
-
1653
  // Track if user has scrolled away - don't auto-scroll if they have
1654
  let userHasScrolledAway = false;
1655
  let scrollTimeout = null;
1656
-
1657
  // ANY upward scroll immediately stops auto-scroll
1658
  chatMessages.addEventListener('wheel', (e) => {
1659
  if (e.deltaY < 0) {
@@ -1667,13 +1929,13 @@
1667
  }
1668
  }
1669
  }, { passive: true });
1670
-
1671
  // Touch support
1672
  let touchStartY = 0;
1673
  chatMessages.addEventListener('touchstart', (e) => {
1674
  touchStartY = e.touches[0].clientY;
1675
  }, { passive: true });
1676
-
1677
  chatMessages.addEventListener('touchmove', (e) => {
1678
  const touchY = e.touches[0].clientY;
1679
  if (touchY > touchStartY) {
@@ -1681,20 +1943,20 @@
1681
  userHasScrolledAway = true;
1682
  }
1683
  }, { passive: true });
1684
-
1685
  // Debounced scroll - doesn't fight user as aggressively
1686
  function scrollToBottom(force = false) {
1687
  if (userHasScrolledAway && !force) return;
1688
-
1689
  // Cancel any pending scroll
1690
  if (scrollTimeout) cancelAnimationFrame(scrollTimeout);
1691
-
1692
  // Use requestAnimationFrame for smoother, less aggressive scrolling
1693
  scrollTimeout = requestAnimationFrame(() => {
1694
  chatMessages.scrollTop = chatMessages.scrollHeight;
1695
  });
1696
  }
1697
-
1698
  // Force scroll for new user messages and important events
1699
  function forceScrollToBottom() {
1700
  userHasScrolledAway = false;
@@ -1712,13 +1974,13 @@
1712
  // Reset feedback state for new message
1713
  currentFeedbackCard = null;
1714
  feedbackData = { discovery: null, plan: null, facts: [], reflection: null, workflow: null, iteration: 1 };
1715
-
1716
  const div = document.createElement('div');
1717
  div.className = 'message-row user';
1718
  div.innerHTML = `<div class="bubble">${escapeHtml(content)}</div>`;
1719
  chatMessages.appendChild(div);
1720
  forceScrollToBottom(); // Always scroll for user's own message
1721
-
1722
  // Track for report
1723
  trackConversation('user', content);
1724
  }
@@ -1727,17 +1989,17 @@
1727
  function addAssistantMessage(content) {
1728
  const div = document.createElement('div');
1729
  div.className = 'message-row assistant';
1730
-
1731
  // Remove "ANSWER:" prefix (case insensitive) and whitespace
1732
  const cleanContent = content.replace(/^ANSWER:\s*/i, '');
1733
-
1734
  // Convert Markdown to HTML using marked
1735
  const htmlContent = marked.parse(cleanContent);
1736
-
1737
- div.innerHTML = `<div class="bubble markdown-body">${htmlContent}</div>`;
1738
  chatMessages.appendChild(div);
1739
  scrollToBottom();
1740
-
1741
  // Track for report
1742
  trackConversation('assistant', cleanContent);
1743
  }
@@ -1748,7 +2010,7 @@
1748
 
1749
  // Thinking indicator - shows while agent is processing
1750
  let thinkingElement = null;
1751
-
1752
  function showThinking(text = 'Thinking') {
1753
  removeThinking(); // Remove any existing
1754
  const div = document.createElement('div');
@@ -1762,7 +2024,7 @@
1762
  thinkingElement = div;
1763
  scrollToBottom();
1764
  }
1765
-
1766
  function updateThinking(text) {
1767
  if (thinkingElement) {
1768
  const textEl = thinkingElement.querySelector('.thinking-text');
@@ -1771,7 +2033,7 @@
1771
  showThinking(text);
1772
  }
1773
  }
1774
-
1775
  function removeThinking() {
1776
  if (thinkingElement) {
1777
  thinkingElement.remove();
@@ -1794,7 +2056,7 @@
1794
  streamingText += token;
1795
  // Remove "ANSWER:" prefix dynamically during stream
1796
  const cleanText = streamingText.replace(/^ANSWER:\s*/i, '');
1797
-
1798
  // Using marked.parse here so markdown renders during stream
1799
  streamingBubble.innerHTML = marked.parse(cleanText);
1800
  scrollToBottom(); // Smart scroll - won't interrupt if user scrolled up
@@ -1804,13 +2066,13 @@
1804
  function endStreamingAnswer() {
1805
  if (streamingBubble) {
1806
  streamingBubble.classList.remove('streaming');
1807
-
1808
  // Track the completed streamed answer for report
1809
  if (streamingText.trim()) {
1810
  const cleanContent = streamingText.replace(/^ANSWER:\s*/i, '');
1811
  trackConversation('assistant', cleanContent);
1812
  }
1813
-
1814
  streamingBubble = null;
1815
  streamingText = "";
1816
  }
@@ -1819,7 +2081,7 @@
1819
  // Only show errors as assistant messages
1820
  function addSystemLog(text, type = 'status') {
1821
  if (type !== 'error') return; // Skip all non-error logs
1822
-
1823
  // Show errors as simple assistant messages
1824
  const div = document.createElement('div');
1825
  div.className = 'message-row assistant';
@@ -1856,7 +2118,7 @@
1856
  } catch (e) {
1857
  formattedContent = `<pre>${escapeHtml(result)}</pre>`;
1858
  }
1859
-
1860
  const div = document.createElement('div');
1861
  div.className = 'message-row tool-result-row';
1862
  div.innerHTML = `
@@ -1872,7 +2134,7 @@
1872
  // ==========================================
1873
  // UNIFIED AGENT FEEDBACK - Merged & Subtle
1874
  // ==========================================
1875
-
1876
  function getOrCreateFeedbackCard() {
1877
  if (!currentFeedbackCard) {
1878
  const div = document.createElement('div');
@@ -1895,23 +2157,23 @@
1895
  }
1896
  return currentFeedbackCard;
1897
  }
1898
-
1899
  function toggleFeedbackCard(header) {
1900
  const card = header.closest('.agent-feedback-card');
1901
  card.classList.toggle('expanded');
1902
  }
1903
-
1904
  function updateFeedbackCard() {
1905
  const card = getOrCreateFeedbackCard();
1906
  const content = card.querySelector('.agent-feedback-content');
1907
  const titleSpan = card.querySelector('.agent-feedback-title span:last-child');
1908
-
1909
  // Remove loading indicator when we have actual content
1910
  titleSpan.classList.remove('loading-indicator');
1911
-
1912
  let html = '';
1913
  let stepCount = 0;
1914
-
1915
  // Discovery section
1916
  if (feedbackData.discovery) {
1917
  stepCount++;
@@ -1925,7 +2187,7 @@
1925
  </div>
1926
  `;
1927
  }
1928
-
1929
  // Plan section (with iteration badge)
1930
  if (feedbackData.plan && feedbackData.plan.length > 0) {
1931
  stepCount++;
@@ -1941,11 +2203,11 @@
1941
  </div>
1942
  `;
1943
  }).join('');
1944
-
1945
- const iterationBadge = feedbackData.iteration > 1
1946
- ? `<span class="iteration-badge">Iteration ${feedbackData.iteration}</span>`
1947
  : '';
1948
-
1949
  html += `
1950
  <div class="feedback-section">
1951
  <div class="feedback-section-label">Query Plan ${iterationBadge}</div>
@@ -1953,7 +2215,7 @@
1953
  </div>
1954
  `;
1955
  }
1956
-
1957
  // Facts sections
1958
  if (feedbackData.facts.length > 0) {
1959
  feedbackData.facts.forEach(f => {
@@ -1967,7 +2229,7 @@
1967
  `;
1968
  });
1969
  }
1970
-
1971
  // Reflection section
1972
  if (feedbackData.reflection) {
1973
  stepCount++;
@@ -1975,7 +2237,7 @@
1975
  const statusIcon = r.has_enough_info ? '✓' : '⚠';
1976
  const statusClass = r.has_enough_info ? 'reflection-complete' : 'reflection-gaps';
1977
  const confidenceBar = Math.round((r.confidence || 0.8) * 100);
1978
-
1979
  let reflectionContent = `
1980
  <div class="reflection-status ${statusClass}">
1981
  <span class="reflection-icon">${statusIcon}</span>
@@ -1983,7 +2245,7 @@
1983
  <span class="confidence-badge">${confidenceBar}% confident</span>
1984
  </div>
1985
  `;
1986
-
1987
  if (r.gaps && r.gaps.length > 0) {
1988
  reflectionContent += `
1989
  <div class="reflection-gaps-list">
@@ -1991,11 +2253,11 @@
1991
  </div>
1992
  `;
1993
  }
1994
-
1995
  if (r.reasoning) {
1996
  reflectionContent += `<div class="reflection-reasoning">${escapeHtml(r.reasoning)}</div>`;
1997
  }
1998
-
1999
  html += `
2000
  <div class="feedback-section">
2001
  <div class="feedback-section-label">Reflection</div>
@@ -2003,7 +2265,7 @@
2003
  </div>
2004
  `;
2005
  }
2006
-
2007
  // Workflow summary (at the end)
2008
  if (feedbackData.workflow) {
2009
  const w = feedbackData.workflow;
@@ -2017,15 +2279,15 @@
2017
  </div>
2018
  `;
2019
  }
2020
-
2021
  content.innerHTML = html;
2022
-
2023
  // Update title with step count
2024
  if (stepCount > 0) {
2025
  titleSpan.textContent = `${stepCount} step${stepCount > 1 ? 's' : ''} completed`;
2026
  }
2027
  }
2028
-
2029
  function addDiscoveryCard(summary, manifest) {
2030
  let samplesHtml = '';
2031
  if (manifest && manifest.sample_values) {
@@ -2039,7 +2301,7 @@
2039
  }
2040
  samplesHtml = parts.join(' • ');
2041
  }
2042
-
2043
  feedbackData.discovery = { summary, samples: samplesHtml };
2044
  updateFeedbackCard();
2045
  }
@@ -2053,11 +2315,11 @@
2053
  function addFactsCard(toolName, facts, rawPreview) {
2054
  feedbackData.facts.push({ tool: toolName, facts });
2055
  updateFeedbackCard();
2056
-
2057
  // Track for report
2058
  trackToolResult(toolName, facts);
2059
  }
2060
-
2061
  function addReflectionCard(event) {
2062
  feedbackData.reflection = {
2063
  has_enough_info: event.has_enough_info,
@@ -2067,7 +2329,7 @@
2067
  };
2068
  updateFeedbackCard();
2069
  }
2070
-
2071
  function addWorkflowSummary(event) {
2072
  feedbackData.workflow = {
2073
  iterations: event.iterations || 1,
@@ -2085,27 +2347,27 @@
2085
  console.warn('Skipping empty chart data:', chartData?.title);
2086
  return;
2087
  }
2088
-
2089
  // NEW: Check if any dataset actually has data points
2090
  const hasDataPoints = chartData.datasets.some(ds => ds.data && ds.data.length > 0);
2091
  if (!hasDataPoints) {
2092
  console.warn('Skipping chart with empty datasets:', chartData?.title);
2093
  return;
2094
  }
2095
-
2096
  chartCounter++;
2097
  const canvasId = `canvas-${chartCounter}`;
2098
-
2099
  // Track as attachment for report (image will be added after render)
2100
  let summary = chartData.title;
2101
  if (chartData.summary && Array.isArray(chartData.summary)) {
2102
  summary += ': ' + chartData.summary.slice(0, 2).join('; ');
2103
  }
2104
-
2105
  // Create attachment object and store direct reference (not index)
2106
  const attachmentObj = { type: 'chart', title: chartData.title, summary, imageData: null };
2107
  collectedAttachments.push(attachmentObj);
2108
-
2109
  const div = document.createElement('div');
2110
  div.className = 'message-row widget-row';
2111
  div.innerHTML = `
@@ -2134,14 +2396,14 @@
2134
  }
2135
 
2136
  function createChartInstance(ctx, chartData, attachmentObj) {
2137
- const primaryColor = '#5e72e4';
2138
-
2139
  // Set global defaults for Dark Mode charts
2140
  Chart.defaults.color = '#8898aa';
2141
  Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
2142
 
2143
  const isBar = chartData.chart_type && chartData.chart_type.includes('bar');
2144
-
2145
  // FIX: If bar chart, use high opacity (CC = ~80%), otherwise low opacity (20 = ~12%)
2146
  const opacityHex = isBar ? 'CC' : '20';
2147
 
@@ -2165,13 +2427,13 @@
2165
  label: ds.label,
2166
  data: ds.data.map(d => typeof d === 'object' ? (d.value || d.y) : d),
2167
  borderColor: color,
2168
- backgroundColor: color + opacityHex,
2169
  tension: 0.3,
2170
  fill: !isBar // Don't fill bar charts
2171
  };
2172
  });
2173
  console.log(`[CHART DEBUG] Total datasets: ${datasets.length}, isBar: ${isBar}`);
2174
-
2175
  const labels = chartData.labels || chartData.datasets[0].data.map(d => d.date);
2176
 
2177
  new Chart(ctx, {
@@ -2182,26 +2444,26 @@
2182
  maintainAspectRatio: false,
2183
  // FIXED: Use animation onComplete callback to capture chart image
2184
  animation: {
2185
- onComplete: function() {
2186
  // This runs only when the chart is fully drawn
2187
  if (!attachmentObj) return;
2188
  try {
2189
  const canvas = ctx.canvas;
2190
-
2191
  // Create a temporary canvas to bake in a dark background
2192
  // This ensures the chart is visible in PDF export
2193
  const tempCanvas = document.createElement('canvas');
2194
  tempCanvas.width = canvas.width;
2195
  tempCanvas.height = canvas.height;
2196
  const tempCtx = tempCanvas.getContext('2d');
2197
-
2198
  // Fill dark background (matches our UI)
2199
  tempCtx.fillStyle = '#171c29';
2200
  tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
2201
-
2202
  // Draw the chart on top
2203
  tempCtx.drawImage(canvas, 0, 0);
2204
-
2205
  // Save this opaque image
2206
  attachmentObj.imageData = tempCanvas.toDataURL('image/png');
2207
  } catch (e) {
@@ -2231,14 +2493,14 @@
2231
  async function sendMessage() {
2232
  // Capture text immediately
2233
  const message = chatInput.value.trim();
2234
-
2235
  // Check if there's a pending skin image
2236
  if (pendingImageData) {
2237
  // Pass the message text to the image handler
2238
  await sendSkinImageForAnalysis(message);
2239
  return;
2240
  }
2241
-
2242
  if (!message || !patientId) return;
2243
 
2244
  addUserMessage(message);
@@ -2254,16 +2516,17 @@
2254
 
2255
  async function sendAgentMessage(message, skinImageData = null) {
2256
  try {
2257
- const requestBody = {
2258
- patient_id: patientId,
2259
- message
 
2260
  };
2261
-
2262
  // Include skin image data if provided
2263
  if (skinImageData) {
2264
  requestBody.skin_image_data = skinImageData;
2265
  }
2266
-
2267
  const response = await fetch('/api/agent/chat', {
2268
  method: 'POST',
2269
  headers: { 'Content-Type': 'application/json' },
@@ -2279,7 +2542,7 @@
2279
 
2280
  const chunk = decoder.decode(value);
2281
  const lines = chunk.split('\n');
2282
-
2283
  for (const line of lines) {
2284
  const trimmedLine = line.trim();
2285
  if (!trimmedLine || !trimmedLine.startsWith('data: ')) continue;
@@ -2290,7 +2553,7 @@
2290
  try {
2291
  const event = JSON.parse(dataStr);
2292
  switch (event.type) {
2293
- case 'status':
2294
  // Show thinking indicator with status text
2295
  updateThinking(event.message || 'Thinking');
2296
  break;
@@ -2308,7 +2571,7 @@
2308
  addPlanCard(event.tools, event.iteration || 1);
2309
  }
2310
  break;
2311
- case 'tool_call':
2312
  updateThinking(`Retrieving ${event.tool || 'data'}...`);
2313
  break;
2314
  case 'tool_result':
@@ -2332,7 +2595,7 @@
2332
  // Error recovery: Log tool errors but continue
2333
  console.warn(`Tool error: ${event.tool} - ${event.error}`);
2334
  break;
2335
- case 'chart_data':
2336
  renderChartWidget(event.data); break;
2337
  case 'skin_analysis':
2338
  // Handle skin analysis results from agent
@@ -2340,7 +2603,7 @@
2340
  renderSkinAnalysisWidget(event.data, event.image_data || skinImageData);
2341
  }
2342
  break;
2343
- case 'answer':
2344
  removeThinking();
2345
  addAssistantMessage(event.content); break;
2346
  case 'answer_start':
@@ -2357,8 +2620,8 @@
2357
  removeThinking();
2358
  addSystemLog(event.message, 'error'); break;
2359
  }
2360
- } catch (e) {
2361
- console.log('Skipping non-JSON chunk:', dataStr);
2362
  }
2363
  }
2364
  }
@@ -2374,7 +2637,7 @@
2374
  let mediaRecorder = null;
2375
  let audioChunks = [];
2376
  let isRecording = false;
2377
-
2378
  // 1. Check Capabilities
2379
  fetch('/api/audio/status').then(r => r.json()).then(data => {
2380
  if (data.available) {
@@ -2390,10 +2653,10 @@
2390
  mediaRecorder.stop();
2391
  isRecording = false;
2392
  recordBtn.classList.remove('recording');
2393
-
2394
  // Toggle UI back to Input
2395
  mainInputGroup.classList.remove('recording-active');
2396
-
2397
  // Don't show status log anymore
2398
  } else {
2399
  // START RECORDING
@@ -2401,7 +2664,7 @@
2401
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
2402
  mediaRecorder = new MediaRecorder(stream);
2403
  audioChunks = [];
2404
-
2405
  mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
2406
  mediaRecorder.onstop = () => {
2407
  const blob = new Blob(audioChunks, { type: 'audio/webm' });
@@ -2411,7 +2674,7 @@
2411
  mediaRecorder.start();
2412
  isRecording = true;
2413
  recordBtn.classList.add('recording');
2414
-
2415
  // Toggle UI to Overlay
2416
  mainInputGroup.classList.add('recording-active');
2417
 
@@ -2440,16 +2703,16 @@
2440
  const data = result.health_assessment || result.respiratory_indicators || {};
2441
  const score = data.respiratory_health_score || data.overall_score || 0;
2442
  const modelName = result.model || 'Unknown';
2443
-
2444
  // Track as attachment for report
2445
  const summary = `Respiratory Health Score: ${Math.round(score)}/100. ` +
2446
  `Cough: ${data.cough_level || 'None'}. ` +
2447
  `Breathing: ${data.breathing_quality || 'Normal'}.` +
2448
  (result.covid_risk ? ` COVID Risk: ${result.covid_risk.risk_level || 'Unknown'}.` : '');
2449
  trackAttachment('audio', `Respiratory Analysis (${modelName})`, summary);
2450
-
2451
  const scoreColor = score < 60 ? 'var(--danger)' : score < 80 ? 'var(--warning)' : 'var(--success)';
2452
-
2453
  const html = `
2454
  <div class="message-row widget-row">
2455
  <div class="widget-card">
@@ -2485,7 +2748,7 @@
2485
  </div>
2486
  </div>
2487
  `;
2488
-
2489
  chatMessages.insertAdjacentHTML('beforeend', html);
2490
  forceScrollToBottom(); // Force scroll to show audio results
2491
  }
@@ -2493,21 +2756,21 @@
2493
  // ==========================================
2494
  // PRE-VISIT REPORT FUNCTIONS
2495
  // ==========================================
2496
-
2497
  function toggleReportPanel() {
2498
  const panel = document.getElementById('reportPanel');
2499
  panel.classList.toggle('open');
2500
  }
2501
-
2502
  function openReportModal() {
2503
  if (!currentReport) return;
2504
  const overlay = document.getElementById('reportModalOverlay');
2505
  const body = document.getElementById('reportModalBody');
2506
-
2507
  // Build chart images HTML
2508
  let chartImagesHtml = '';
2509
  let skinImagesHtml = '';
2510
-
2511
  collectedAttachments.forEach(att => {
2512
  if (att.type === 'chart' && att.imageData) {
2513
  chartImagesHtml += `
@@ -2527,7 +2790,7 @@
2527
  `;
2528
  }
2529
  });
2530
-
2531
  // Combine report HTML with chart and skin images
2532
  let additionalSections = '';
2533
  if (chartImagesHtml) {
@@ -2546,24 +2809,24 @@
2546
  </div>
2547
  `;
2548
  }
2549
-
2550
  body.innerHTML = currentReport.html + additionalSections;
2551
-
2552
  overlay.classList.add('open');
2553
  }
2554
-
2555
  function closeReportModal(event) {
2556
  if (event && event.target !== event.currentTarget) return;
2557
  document.getElementById('reportModalOverlay').classList.remove('open');
2558
  }
2559
-
2560
  async function generateReport() {
2561
  if (!patientId) return;
2562
-
2563
  const btn = document.getElementById('btnGenerateReport');
2564
  btn.classList.add('loading');
2565
  btn.disabled = true;
2566
-
2567
  try {
2568
  const response = await fetch('/api/report/generate', {
2569
  method: 'POST',
@@ -2575,16 +2838,16 @@
2575
  attachments: collectedAttachments
2576
  })
2577
  });
2578
-
2579
  const data = await response.json();
2580
-
2581
  if (data.success) {
2582
  currentReport = data;
2583
  updateReportPreview(data.report);
2584
-
2585
  // Update toggle button
2586
  document.getElementById('btnToggleReport').classList.add('has-report');
2587
-
2588
  // Open panel if not already open
2589
  const panel = document.getElementById('reportPanel');
2590
  if (!panel.classList.contains('open')) {
@@ -2601,28 +2864,28 @@
2601
  btn.disabled = false;
2602
  }
2603
  }
2604
-
2605
  function updateReportPreview(report) {
2606
  document.getElementById('reportPlaceholder').style.display = 'none';
2607
  const preview = document.getElementById('reportPreview');
2608
  preview.style.display = 'block';
2609
-
2610
  const concerns = report.chief_concerns || [];
2611
- document.getElementById('previewConcerns').textContent =
2612
  concerns.length > 0 ? concerns.slice(0, 2).join(', ') : 'No specific concerns noted';
2613
-
2614
  const attachCount = (report.attachments || []).length;
2615
- document.getElementById('previewMeta').textContent =
2616
  `${report.generated_at} • ${attachCount} attachment${attachCount !== 1 ? 's' : ''}`;
2617
  }
2618
-
2619
  function downloadReport() {
2620
  if (!currentReport) return;
2621
-
2622
  // Build chart and skin images HTML from collected attachments
2623
  let chartImagesHtml = '';
2624
  let skinImagesHtml = '';
2625
-
2626
  collectedAttachments.forEach(att => {
2627
  if (att.type === 'chart' && att.imageData) {
2628
  chartImagesHtml += `
@@ -2642,7 +2905,7 @@
2642
  `;
2643
  }
2644
  });
2645
-
2646
  // Create container for PDF generation with explicit dark text on white background
2647
  const pdfContainer = document.createElement('div');
2648
  pdfContainer.innerHTML = `
@@ -2694,7 +2957,7 @@
2694
  </div>
2695
  </div>
2696
  `;
2697
-
2698
  // PDF options
2699
  const opt = {
2700
  margin: [10, 10, 10, 10],
@@ -2704,25 +2967,26 @@
2704
  jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
2705
  pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
2706
  };
2707
-
2708
  // Generate and download PDF
2709
  html2pdf().set(opt).from(pdfContainer).save();
2710
  }
2711
-
2712
  // Track conversation for report
2713
  function trackConversation(role, content) {
2714
  conversationHistory.push({ role, content });
2715
  }
2716
-
2717
  // Track tool results for report
2718
  function trackToolResult(tool, facts) {
2719
  collectedToolResults.push({ tool, facts });
2720
  }
2721
-
2722
  // Track attachments for report (including chart images)
2723
  function trackAttachment(type, title, summary, imageData = null) {
2724
  collectedAttachments.push({ type, title, summary, imageData });
2725
  }
2726
  </script>
2727
  </body>
 
2728
  </html>
 
1
  <!DOCTYPE html>
2
  <!-- VERSION: chart-colors-fix-v2-2026-02-03 -->
3
  <html lang="en">
4
+
5
  <head>
6
  <meta charset="UTF-8">
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 
14
  /* Color System */
15
  --primary: #5e72e4;
16
  --primary-dark: #465acb;
17
+ --secondary: #252f49;
18
+ /* Darker secondary for inputs/chips */
19
+ --text-main: #e2e6ea;
20
+ /* Off-white text */
21
  --text-muted: #8898aa;
22
  --success: #2dce89;
23
  --warning: #fb6340;
24
  --danger: #f5365c;
25
  --info: #11cdef;
26
+ --derm: #9b59b6;
27
+ /* Purple for skin analysis */
28
+
29
  /* Surfaces */
30
+ --bg-body: #0f1219;
31
+ /* Very dark background */
32
+ --bg-card: #171c29;
33
+ /* Dark card background */
34
  --bg-chat-user: #5e72e4;
35
+ --bg-chat-bot: #252f49;
36
+ /* Darker bubble */
37
+ --border-color: #2b3553;
38
+ /* Dark borders */
39
 
40
  /* Spacing & Radius */
41
  --radius-lg: 20px;
 
45
  --shadow-hover: 0 7px 14px rgba(0, 0, 0, 0.25), 0 3px 6px rgba(0, 0, 0, 0.2);
46
  }
47
 
48
+ * {
49
+ box-sizing: border-box;
50
+ margin: 0;
51
+ padding: 0;
52
+ }
53
+
54
  body {
55
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
56
  background: var(--bg-body);
 
87
  z-index: 10;
88
  }
89
 
90
+ .header-branding h1 {
91
+ font-size: 18px;
92
+ font-weight: 700;
93
+ color: var(--text-main);
94
+ }
95
+
96
+ .header-branding p {
97
+ font-size: 12px;
98
+ color: var(--text-muted);
99
+ margin-top: 2px;
100
+ }
101
+
102
  .connection-status {
103
  display: flex;
104
  align-items: center;
 
110
  border-radius: 30px;
111
  color: var(--text-main);
112
  }
113
+
114
+ .status-dot {
115
+ width: 8px;
116
+ height: 8px;
117
+ border-radius: 50%;
118
+ background: var(--danger);
119
+ transition: background 0.3s;
120
+ }
121
+
122
+ .status-dot.connected {
123
+ background: var(--success);
124
+ }
125
 
126
  /* Patient Context Bar (Collapsible) */
127
  .context-bar {
 
130
  border-bottom: 1px solid var(--border-color);
131
  font-size: 13px;
132
  }
133
+
134
  .context-toggle {
135
  width: 100%;
136
  display: flex;
 
142
  font-weight: 600;
143
  align-items: center;
144
  }
145
+
146
  .context-details {
147
  display: none;
148
  margin-top: 15px;
 
151
  grid-template-columns: repeat(3, 1fr);
152
  gap: 15px;
153
  }
154
+
155
+ .context-details.open {
156
+ display: grid;
157
+ }
158
+
159
+ .detail-group h4 {
160
+ font-size: 11px;
161
+ text-transform: uppercase;
162
+ letter-spacing: 0.5px;
163
+ color: var(--text-muted);
164
+ margin-bottom: 8px;
165
+ }
166
+
167
+ .tag {
168
+ display: inline-block;
169
+ padding: 2px 8px;
170
+ border-radius: 4px;
171
+ background: var(--secondary);
172
+ font-size: 11px;
173
+ margin-right: 4px;
174
+ margin-bottom: 4px;
175
+ color: var(--text-main);
176
+ border: 1px solid rgba(255, 255, 255, 0.05);
177
+ }
178
+
179
+ .tag.alert {
180
+ background: rgba(245, 54, 92, 0.2);
181
+ color: #ffadad;
182
+ border-color: rgba(245, 54, 92, 0.3);
183
+ }
184
 
185
  /* Chat Area */
186
  .chat-viewport {
187
  flex: 1;
188
  overflow-y: auto;
189
  padding: 20px 25px;
190
+ background: #131722;
191
+ /* Slightly different dark for chat area */
192
  }
193
 
194
  .message-row {
 
196
  margin-bottom: 15px;
197
  width: 100%;
198
  }
199
+
200
+ .message-row.user {
201
+ justify-content: flex-end;
202
+ }
203
+
204
  .bubble {
205
  max-width: 75%;
206
  padding: 12px 18px;
 
209
  line-height: 1.5;
210
  position: relative;
211
  }
212
+
213
  /* Standard Messages */
214
  .message-row.user .bubble {
215
  background: var(--primary);
216
  color: white;
217
  border-bottom-right-radius: 4px;
218
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
219
  }
220
+
221
  .message-row.assistant .bubble {
222
  background: var(--bg-chat-bot);
223
  color: var(--text-main);
224
  border-bottom-left-radius: 4px;
225
+ border: 1px solid rgba(255, 255, 255, 0.05);
226
  }
227
 
228
  /* Widgets (Charts/Audio - Not bubbles) */
229
+ .message-row.widget-row {
230
+ display: block;
231
+ margin: 20px 0;
232
+ }
233
+
234
  .widget-card {
235
  background: var(--bg-card);
236
  border: 1px solid var(--border-color);
 
240
  max-width: 100%;
241
  margin: 0 auto;
242
  }
243
+
244
  .widget-header {
245
  padding: 12px 20px;
246
+ background: rgba(255, 255, 255, 0.03);
247
  border-bottom: 1px solid var(--border-color);
248
  font-size: 13px;
249
  font-weight: 600;
 
252
  align-items: center;
253
  gap: 8px;
254
  }
255
+
256
+ .widget-body {
257
+ padding: 20px;
258
+ }
259
 
260
  /* Agent Status - All hidden except via feedback card */
261
  .status-line {
262
  display: none !important;
263
  }
264
+
265
  /* Hide any stray status messages that might come through */
266
+ .chat-viewport>div[style*="color: #11cdef"],
267
+ .chat-viewport>div[style*="color: var(--info)"],
268
+ .chat-viewport>span[style*="color: #11cdef"] {
269
  display: none !important;
270
  }
271
 
 
288
  border: 1px solid transparent;
289
  transition: border 0.2s;
290
  }
291
+
292
  .input-group:focus-within {
293
  background: var(--bg-card);
294
  border-color: var(--primary);
 
304
  outline: none;
305
  color: var(--text-main);
306
  }
307
+
308
+ .input-group input::placeholder {
309
+ color: var(--text-muted);
310
+ }
311
 
312
  /* Action Buttons */
313
  .btn-icon {
 
323
  font-size: 18px;
324
  flex-shrink: 0;
325
  }
326
+
327
+ .btn-record {
328
+ background: #373f55;
329
+ color: var(--text-main);
330
+ }
331
+
332
+ .btn-record:hover {
333
+ background: #444c66;
334
+ }
335
+
336
+ .btn-record.recording {
337
+ background: var(--danger);
338
+ color: white;
339
  animation: pulse 1.5s infinite;
340
  }
341
+
342
+ .btn-send {
343
+ background: var(--primary);
344
+ color: white;
345
+ }
346
+
347
+ .btn-send:hover {
348
+ background: var(--primary-dark);
349
+ transform: translateY(-1px);
350
+ }
351
+
352
+ .btn-send:disabled {
353
+ background: #2b3553;
354
+ color: #5a6585;
355
+ cursor: not-allowed;
356
+ transform: none;
357
+ }
358
 
359
  /* Quick Actions - Chips Row with Report Button */
360
  .chips-row {
 
363
  gap: 8px;
364
  margin-bottom: 10px;
365
  }
366
+
367
  .chips-scroll {
368
  display: flex;
369
  gap: 8px;
370
  overflow-x: auto;
371
  flex: 1;
372
  padding-bottom: 2px;
373
+ scrollbar-width: none;
374
+ /* Hide scrollbar */
375
  }
376
+
377
  .chip {
378
  white-space: nowrap;
379
  padding: 6px 12px;
 
385
  cursor: pointer;
386
  transition: all 0.2s;
387
  }
388
+
389
+ .chip:hover {
390
+ border-color: var(--primary);
391
+ color: var(--primary);
392
+ background: rgba(94, 114, 228, 0.1);
393
+ }
394
 
395
  /* Report Toggle Button - Inline & Compact */
396
  .btn-toggle-report {
 
408
  white-space: nowrap;
409
  flex-shrink: 0;
410
  }
411
+
412
  .btn-toggle-report:hover {
413
  border-color: var(--primary);
414
  color: var(--primary);
415
  background: rgba(94, 114, 228, 0.1);
416
  }
417
+
418
  .btn-toggle-report.has-report {
419
  border-color: var(--success);
420
  color: var(--success);
421
  background: rgba(45, 206, 137, 0.1);
422
  }
423
+
424
  .btn-toggle-report .icon {
425
  font-size: 12px;
426
  }
427
 
428
  /* Recording Overlay Logic */
429
  .recording-overlay {
430
+ flex: 1;
431
+ display: none;
432
  align-items: center;
433
  gap: 10px;
434
  padding-right: 10px;
435
  }
436
+
437
+ .wave-viz {
438
+ height: 20px;
439
+ flex: 1;
440
+ background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 20" preserveAspectRatio="none"><path d="M0 10 Q 25 20 50 10 T 100 10" stroke="%23f5365c" fill="none" stroke-width="2"/></svg>');
441
  }
442
 
443
  /* Toggle States */
444
+ .input-group.recording-active input {
445
+ display: none;
446
+ }
447
+
448
+ .input-group.recording-active .recording-overlay {
449
+ display: flex;
450
+ }
451
 
452
  /* Audio Result Styling (The "Widget") */
453
  .audio-widget {
454
  display: grid;
455
  gap: 15px;
456
  }
457
+
458
  .score-container {
459
  text-align: center;
460
  padding: 15px;
 
462
  border: 1px solid var(--border-color);
463
  border-radius: 12px;
464
  }
465
+
466
+ .score-val {
467
+ font-size: 36px;
468
+ font-weight: 800;
469
+ display: block;
470
+ line-height: 1;
471
+ }
472
+
473
+ .score-label {
474
+ font-size: 12px;
475
+ text-transform: uppercase;
476
+ color: var(--text-muted);
477
+ margin-top: 5px;
478
+ }
479
+
480
  .risk-badge {
481
  padding: 8px 12px;
482
  border-radius: 6px;
 
485
  align-items: center;
486
  gap: 8px;
487
  }
488
+
489
+ .risk-badge.low {
490
+ background: rgba(45, 206, 137, 0.1);
491
+ color: #2dce89;
492
+ border: 1px solid rgba(45, 206, 137, 0.2);
493
+ }
494
+
495
+ .risk-badge.high {
496
+ background: rgba(245, 54, 92, 0.1);
497
+ color: #f5365c;
498
+ border: 1px solid rgba(245, 54, 92, 0.2);
499
+ }
500
+
501
  /* Default badge if no class */
502
+ .risk-badge {
503
+ background: var(--secondary);
504
+ color: var(--text-main);
505
+ border: 1px solid var(--border-color);
506
+ }
507
 
508
  /* User Image Message */
509
  .message-row.user .bubble.with-image {
510
  padding: 8px;
511
  max-width: 300px;
512
  }
513
+
514
  .message-row.user .bubble.with-image img {
515
  max-width: 100%;
516
  border-radius: 12px;
517
  display: block;
518
  }
519
+
520
  .message-row.user .bubble.with-image .image-caption {
521
  padding: 8px 10px 4px;
522
  font-size: 13px;
 
527
  display: grid;
528
  gap: 15px;
529
  }
530
+
531
  .skin-widget .image-preview {
532
  text-align: center;
533
  }
534
+
535
  .skin-widget .image-preview img {
536
  max-width: 200px;
537
  max-height: 200px;
538
  border-radius: 12px;
539
  border: 2px solid var(--derm);
540
  }
541
+
542
  .skin-widget .quality-score {
543
  text-align: center;
544
  padding: 15px;
 
546
  border: 1px solid var(--border-color);
547
  border-radius: 12px;
548
  }
549
+
550
  .skin-widget .quality-val {
551
  font-size: 32px;
552
  font-weight: 800;
553
  display: block;
554
  line-height: 1;
555
  }
556
+
557
  .skin-widget .quality-label {
558
  font-size: 12px;
559
  text-transform: uppercase;
560
  color: var(--text-muted);
561
  margin-top: 5px;
562
  }
563
+
564
  .skin-widget .analysis-notes {
565
  background: var(--secondary);
566
  padding: 12px;
567
  border-radius: 8px;
568
  font-size: 13px;
569
  }
570
+
571
  .skin-widget .analysis-notes li {
572
  margin: 4px 0;
573
  list-style: none;
574
  }
575
+
576
  .skin-widget .analysis-notes li::before {
577
  content: "• ";
578
  color: var(--derm);
579
  }
580
 
581
  /* Image Upload Button */
582
+ .btn-image {
583
+ background: #373f55;
584
+ color: var(--text-main);
585
+ }
586
+
587
+ .btn-image:hover {
588
+ background: rgba(155, 89, 182, 0.3);
589
+ color: var(--derm);
590
+ }
591
+
592
+ .btn-image:disabled {
593
+ opacity: 0.5;
594
+ cursor: not-allowed;
595
+ }
596
 
597
  /* Hidden file input */
598
  #skinImageInput {
 
607
  padding-left: 0;
608
  flex-shrink: 0;
609
  }
610
+
611
  .image-upload-preview img {
612
  width: 36px;
613
  height: 36px;
 
615
  border-radius: 8px;
616
  border: 2px solid var(--derm);
617
  }
618
+
619
  .image-upload-preview .preview-text {
620
+ display: none;
621
+ /* Hide the "Skin image ready" text */
622
  }
623
+
624
  .image-upload-preview .btn-cancel-image {
625
  background: none;
626
  border: none;
 
630
  padding: 0 4px;
631
  line-height: 1;
632
  }
633
+
634
  .image-upload-preview .btn-cancel-image:hover {
635
  color: var(--danger);
636
  }
637
+
638
  /* Show preview alongside input - don't hide input */
639
+ .input-group.image-pending .image-upload-preview {
640
+ display: flex;
641
+ }
642
+
643
+ .input-group.image-pending input {
644
+ display: block;
645
+ /* Keep input visible */
646
  }
647
+
648
  .input-group.image-pending input::placeholder {
649
  color: var(--derm);
650
  }
651
 
652
  /* Skin chip styling */
653
+ .chip.skin-chip:hover {
654
+ border-color: var(--derm);
655
+ color: var(--derm);
656
+ background: rgba(155, 89, 182, 0.1);
657
+ }
658
 
659
+ @keyframes pulse {
660
+ 0% {
661
+ box-shadow: 0 0 0 0 rgba(245, 54, 92, 0.4);
662
+ }
663
+
664
+ 70% {
665
+ box-shadow: 0 0 0 10px rgba(245, 54, 92, 0);
666
+ }
667
+
668
+ 100% {
669
+ box-shadow: 0 0 0 0 rgba(245, 54, 92, 0);
670
+ }
671
+ }
672
 
673
  /* Markdown/Format helpers */
674
+ .markdown-body ul,
675
+ .markdown-body ol {
676
+ padding-left: 20px;
677
+ margin: 8px 0;
678
+ }
679
+
680
+ .markdown-body li {
681
+ margin-bottom: 4px;
682
+ }
683
+
684
+ .markdown-body p {
685
+ margin-bottom: 10px;
686
+ }
687
+
688
+ .markdown-body p:last-child {
689
+ margin-bottom: 0;
690
+ }
691
+
692
+ .markdown-body strong {
693
+ font-weight: 700;
694
+ color: #fff;
695
+ }
696
+
697
+ /* Strong text white */
698
 
699
  /* Tool Result Card */
700
+ .tool-result-row {
701
+ margin: 8px 0;
702
+ }
703
+
704
  .tool-result-card {
705
  background: var(--bg-card);
706
  border: 1px solid var(--border-color);
 
708
  overflow: hidden;
709
  max-width: 100%;
710
  }
711
+
712
  .tool-result-header {
713
+ background: rgba(255, 255, 255, 0.05);
714
  padding: 8px 12px;
715
  font-weight: 600;
716
  font-size: 13px;
 
718
  text-transform: capitalize;
719
  border-bottom: 1px solid var(--border-color);
720
  }
721
+
722
  .tool-result-body {
723
  padding: 12px;
724
  font-size: 14px;
725
  color: var(--text-main);
726
  }
727
+
728
  .tool-result-list {
729
  list-style: none;
730
  padding: 0;
731
  margin: 0;
732
  }
733
+
734
  .tool-result-list li {
735
  padding: 6px 0;
736
  border-bottom: 1px solid var(--border-color);
737
  }
738
+
739
+ .tool-result-list li:last-child {
740
+ border-bottom: none;
741
+ }
742
+
743
  .tool-result-list .badge {
744
  background: var(--secondary);
745
  padding: 2px 8px;
 
755
  animation: blink 0.7s infinite;
756
  margin-left: 2px;
757
  }
758
+
759
  @keyframes blink {
760
+
761
+ 0%,
762
+ 50% {
763
+ opacity: 1;
764
+ }
765
+
766
+ 51%,
767
+ 100% {
768
+ opacity: 0;
769
+ }
770
  }
771
 
772
  /* ==========================================
773
  UNIFIED AGENT FEEDBACK - Subtle & Merged
774
  ========================================== */
775
+
776
+ .agent-feedback-row {
777
+ margin: 10px 0;
778
+ }
779
+
780
  .agent-feedback-card {
781
  background: rgba(255, 255, 255, 0.02);
782
  border: 1px solid rgba(255, 255, 255, 0.06);
 
784
  overflow: hidden;
785
  font-size: 12px;
786
  }
787
+
788
  .agent-feedback-header {
789
  display: flex;
790
  align-items: center;
 
794
  cursor: pointer;
795
  user-select: none;
796
  }
797
+
798
  .agent-feedback-header:hover {
799
  background: rgba(255, 255, 255, 0.04);
800
  }
801
+
802
  .agent-feedback-title {
803
  display: flex;
804
  align-items: center;
 
806
  color: var(--text-muted);
807
  font-weight: 500;
808
  }
809
+
810
  .agent-feedback-title .icon {
811
  font-size: 12px;
812
  opacity: 0.7;
813
  }
814
+
815
  .agent-feedback-toggle {
816
  color: var(--text-muted);
817
  font-size: 10px;
818
  transition: transform 0.2s;
819
  }
820
+
821
  .agent-feedback-card.expanded .agent-feedback-toggle {
822
  transform: rotate(180deg);
823
  }
824
+
825
  .agent-feedback-content {
826
  display: none;
827
  padding: 0;
828
  border-top: 1px solid rgba(255, 255, 255, 0.04);
829
  }
830
+
831
  .agent-feedback-card.expanded .agent-feedback-content {
832
  display: block;
833
  }
834
+
835
  .feedback-section {
836
  padding: 10px 12px;
837
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
838
  }
839
+
840
  .feedback-section:last-child {
841
  border-bottom: none;
842
  }
843
+
844
  .feedback-section-label {
845
  font-size: 10px;
846
  text-transform: uppercase;
 
849
  opacity: 0.7;
850
  margin-bottom: 6px;
851
  }
852
+
853
  .feedback-section-content {
854
  color: var(--text-main);
855
  opacity: 0.85;
856
  line-height: 1.5;
857
  }
858
+
859
  .feedback-section-content .sample-data {
860
  margin-top: 4px;
861
  font-size: 11px;
862
  opacity: 0.6;
863
  }
864
+
865
  .feedback-plan-item {
866
  display: flex;
867
  align-items: flex-start;
868
  gap: 8px;
869
  margin-bottom: 6px;
870
  }
871
+
872
  .feedback-plan-item:last-child {
873
  margin-bottom: 0;
874
  }
875
+
876
  .feedback-plan-bullet {
877
  color: var(--text-muted);
878
  opacity: 0.5;
879
  margin-top: 2px;
880
  }
881
+
882
  .feedback-plan-tool {
883
  font-weight: 500;
884
  }
885
+
886
  .feedback-plan-reason {
887
  font-size: 11px;
888
  opacity: 0.6;
889
  margin-top: 2px;
890
  }
891
+
892
  .feedback-facts {
893
  white-space: pre-wrap;
894
  }
895
+
896
  .feedback-citation {
897
  background: rgba(255, 255, 255, 0.06);
898
  padding: 1px 4px;
 
900
  font-family: monospace;
901
  font-size: 10px;
902
  }
903
+
904
  /* Iteration badge */
905
  .iteration-badge {
906
  background: var(--primary);
 
911
  margin-left: 8px;
912
  font-weight: 500;
913
  }
914
+
915
  /* Reflection section styles */
916
  .reflection-status {
917
  display: flex;
 
921
  border-radius: 6px;
922
  margin-bottom: 8px;
923
  }
924
+
925
  .reflection-status.reflection-complete {
926
  background: rgba(76, 175, 80, 0.15);
927
  border: 1px solid rgba(76, 175, 80, 0.3);
928
  }
929
+
930
  .reflection-status.reflection-gaps {
931
  background: rgba(255, 152, 0, 0.15);
932
  border: 1px solid rgba(255, 152, 0, 0.3);
933
  }
934
+
935
  .reflection-icon {
936
  font-size: 14px;
937
  }
938
+
939
  .confidence-badge {
940
  margin-left: auto;
941
  font-size: 10px;
 
943
  padding: 2px 6px;
944
  border-radius: 8px;
945
  }
946
+
947
  .reflection-gaps-list {
948
  font-size: 12px;
949
  color: var(--warning);
950
  margin-bottom: 6px;
951
  }
952
+
953
  .reflection-reasoning {
954
  font-size: 11px;
955
  opacity: 0.7;
956
  font-style: italic;
957
  }
958
+
959
  /* Workflow summary */
960
  .workflow-summary .feedback-section-content {
961
  display: flex;
962
  gap: 16px;
963
  }
964
+
965
  .workflow-stat {
966
  font-size: 12px;
967
  display: flex;
 
972
  /* ==========================================
973
  PRE-VISIT REPORT PANEL & MODAL
974
  ========================================== */
975
+
976
  /* Main content wrapper for side-by-side layout */
977
  .main-content-wrapper {
978
  display: flex;
979
  flex: 1;
980
  overflow: hidden;
981
  }
982
+
983
  .chat-container {
984
  flex: 1;
985
  display: flex;
 
997
  display: flex;
998
  flex-direction: column;
999
  }
1000
+
1001
  .report-panel.open {
1002
  width: 320px;
1003
  }
1004
+
1005
  .report-panel-header {
1006
  padding: 15px;
1007
  border-bottom: 1px solid var(--border-color);
 
1009
  justify-content: space-between;
1010
  align-items: center;
1011
  }
1012
+
1013
  .report-panel-header h3 {
1014
  font-size: 14px;
1015
  color: var(--text-main);
1016
  margin: 0;
1017
  }
1018
+
1019
  .report-panel-close {
1020
  background: none;
1021
  border: none;
 
1024
  font-size: 18px;
1025
  padding: 4px;
1026
  }
1027
+
1028
  .report-panel-body {
1029
  flex: 1;
1030
  overflow-y: auto;
1031
  padding: 15px;
1032
  }
1033
+
1034
  .report-preview {
1035
  background: linear-gradient(135deg, rgba(94, 114, 228, 0.1) 0%, rgba(94, 114, 228, 0.05) 100%);
1036
  border: 1px solid rgba(94, 114, 228, 0.3);
 
1039
  cursor: pointer;
1040
  transition: all 0.2s;
1041
  }
1042
+
1043
  .report-preview:hover {
1044
  border-color: var(--primary);
1045
  transform: translateY(-2px);
1046
  }
1047
+
1048
  .report-preview-placeholder {
1049
  text-align: center;
1050
  color: var(--text-muted);
1051
  padding: 30px 15px;
1052
  }
1053
+
1054
  .report-preview-placeholder .icon {
1055
  font-size: 32px;
1056
  margin-bottom: 10px;
1057
  }
1058
+
1059
  .report-preview h4 {
1060
  font-size: 13px;
1061
  color: var(--primary);
1062
  margin: 0 0 8px 0;
1063
  }
1064
+
1065
  .report-preview-concerns {
1066
  font-size: 12px;
1067
  color: var(--text-main);
1068
  margin-bottom: 8px;
1069
  }
1070
+
1071
  .report-preview-meta {
1072
  font-size: 11px;
1073
  color: var(--text-muted);
1074
  }
1075
+
1076
  .report-panel-actions {
1077
  padding: 15px;
1078
  border-top: 1px solid var(--border-color);
1079
  }
1080
+
1081
  .btn-generate-report {
1082
  width: 100%;
1083
  padding: 12px;
 
1090
  cursor: pointer;
1091
  transition: background 0.2s;
1092
  }
1093
+
1094
  .btn-generate-report:hover {
1095
  background: var(--primary-dark);
1096
  }
1097
+
1098
  .btn-generate-report:disabled {
1099
  background: var(--secondary);
1100
  cursor: not-allowed;
1101
  }
1102
+
1103
  .btn-generate-report.loading {
1104
  position: relative;
1105
  color: transparent;
1106
  }
1107
+
1108
  .btn-generate-report.loading::after {
1109
  content: '';
1110
  position: absolute;
 
1118
  border-radius: 50%;
1119
  animation: spin 0.8s linear infinite;
1120
  }
1121
+
1122
  @keyframes spin {
1123
+ to {
1124
+ transform: rotate(360deg);
1125
+ }
1126
  }
1127
 
1128
  /* Loading indicator for image upload - pulsing dot instead of spinning */
 
1136
  margin-left: 6px;
1137
  animation: pulse-dot 1s ease-in-out infinite;
1138
  }
1139
+
1140
  @keyframes pulse-dot {
1141
+
1142
+ 0%,
1143
+ 100% {
1144
+ opacity: 0.3;
1145
+ transform: scale(0.8);
1146
+ }
1147
+
1148
+ 50% {
1149
+ opacity: 1;
1150
+ transform: scale(1.2);
1151
+ }
1152
  }
1153
 
1154
  /* Thinking indicator - shows while agent is working */
 
1160
  color: var(--text-secondary);
1161
  font-size: 0.85rem;
1162
  }
1163
+
1164
  .thinking-indicator .thinking-dots {
1165
  display: flex;
1166
  gap: 4px;
1167
  }
1168
+
1169
  .thinking-indicator .thinking-dots span {
1170
  width: 6px;
1171
  height: 6px;
 
1173
  background: var(--primary);
1174
  animation: thinking-bounce 1.4s ease-in-out infinite;
1175
  }
1176
+
1177
+ .thinking-indicator .thinking-dots span:nth-child(2) {
1178
+ animation-delay: 0.2s;
1179
+ }
1180
+
1181
+ .thinking-indicator .thinking-dots span:nth-child(3) {
1182
+ animation-delay: 0.4s;
1183
+ }
1184
+
1185
  @keyframes thinking-bounce {
1186
+
1187
+ 0%,
1188
+ 80%,
1189
+ 100% {
1190
+ opacity: 0.3;
1191
+ transform: translateY(0);
1192
+ }
1193
+
1194
+ 40% {
1195
+ opacity: 1;
1196
+ transform: translateY(-4px);
1197
+ }
1198
  }
1199
+
1200
  .thinking-indicator .thinking-text {
1201
  opacity: 0.8;
1202
  transition: opacity 0.3s;
 
1218
  visibility: hidden;
1219
  transition: all 0.3s;
1220
  }
1221
+
1222
  .report-modal-overlay.open {
1223
  opacity: 1;
1224
  visibility: visible;
1225
  }
1226
+
1227
  .report-modal {
1228
  background: var(--bg-card);
1229
  border-radius: var(--radius-lg);
 
1236
  transform: scale(0.9);
1237
  transition: transform 0.3s;
1238
  }
1239
+
1240
  .report-modal-overlay.open .report-modal {
1241
  transform: scale(1);
1242
  }
1243
+
1244
  .report-modal-header {
1245
  padding: 20px;
1246
  border-bottom: 1px solid var(--border-color);
 
1248
  justify-content: space-between;
1249
  align-items: center;
1250
  }
1251
+
1252
  .report-modal-header h2 {
1253
  font-size: 18px;
1254
  margin: 0;
1255
  color: var(--text-main);
1256
  }
1257
+
1258
  .report-modal-actions {
1259
  display: flex;
1260
  gap: 10px;
1261
  }
1262
+
1263
  .btn-modal-action {
1264
  padding: 8px 16px;
1265
  border-radius: var(--radius-sm);
 
1269
  border: none;
1270
  transition: all 0.2s;
1271
  }
1272
+
1273
  .btn-download {
1274
  background: var(--success);
1275
  color: white;
1276
  }
1277
+
1278
  .btn-close-modal {
1279
  background: var(--secondary);
1280
  color: var(--text-main);
1281
  }
1282
+
1283
  .report-modal-body {
1284
  flex: 1;
1285
  overflow-y: auto;
 
1291
  font-size: 14px;
1292
  color: var(--text-main);
1293
  }
1294
+
1295
  .previsit-report .report-header {
1296
  margin-bottom: 20px;
1297
  padding-bottom: 15px;
1298
  border-bottom: 2px solid var(--primary);
1299
  }
1300
+
1301
  .previsit-report .report-header h2 {
1302
  font-size: 20px;
1303
  color: var(--primary);
1304
  margin: 0 0 8px 0;
1305
  }
1306
+
1307
  .previsit-report .report-meta {
1308
  display: flex;
1309
  justify-content: space-between;
1310
  font-size: 13px;
1311
  }
1312
+
1313
  .previsit-report .patient-info {
1314
  color: var(--text-main);
1315
  }
1316
+
1317
  .previsit-report .report-date {
1318
  color: var(--text-muted);
1319
  }
1320
+
1321
  .previsit-report .report-section {
1322
  margin-bottom: 16px;
1323
  }
1324
+
1325
  .previsit-report .report-section h3 {
1326
  font-size: 14px;
1327
  color: var(--primary);
1328
  margin: 0 0 8px 0;
1329
  }
1330
+
1331
  .previsit-report .report-section p {
1332
  margin: 0;
1333
  line-height: 1.5;
1334
  }
1335
+
1336
  .previsit-report .report-section ul {
1337
  margin: 0;
1338
  padding-left: 20px;
1339
  }
1340
+
1341
  .previsit-report .report-section li {
1342
  margin: 4px 0;
1343
  }
1344
+
1345
  .previsit-report .report-columns {
1346
  display: flex;
1347
  gap: 20px;
1348
  }
1349
+
1350
  .previsit-report .report-section.half {
1351
  flex: 1;
1352
  }
1353
+
1354
  .previsit-report .vitals-grid {
1355
  display: grid;
1356
  grid-template-columns: repeat(2, 1fr);
1357
  gap: 8px;
1358
  }
1359
+
1360
  .previsit-report .vital-item {
1361
  background: var(--secondary);
1362
  padding: 8px 12px;
1363
  border-radius: var(--radius-sm);
1364
  }
1365
+
1366
  .previsit-report .vital-name {
1367
  color: var(--text-muted);
1368
  }
1369
+
1370
  .previsit-report .vital-value {
1371
  color: var(--text-main);
1372
  font-weight: 600;
1373
  }
1374
+
1375
  .previsit-report blockquote {
1376
  background: var(--secondary);
1377
  border-left: 3px solid var(--primary);
 
1380
  font-style: italic;
1381
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
1382
  }
1383
+
1384
  .previsit-report .attachments-list {
1385
  display: flex;
1386
  flex-direction: column;
1387
  gap: 8px;
1388
  }
1389
+
1390
  .previsit-report .attachment-item {
1391
  background: var(--secondary);
1392
  padding: 10px 12px;
 
1395
  align-items: center;
1396
  gap: 10px;
1397
  }
1398
+
1399
  .previsit-report .attachment-icon {
1400
  font-size: 20px;
1401
  }
1402
+
1403
  .previsit-report .attachment-title {
1404
  font-weight: 600;
1405
  }
1406
+
1407
  .previsit-report .attachment-summary {
1408
  font-size: 12px;
1409
  color: var(--text-muted);
1410
  margin-top: 4px;
1411
  }
1412
+
1413
  .previsit-report .report-footer {
1414
  margin-top: 20px;
1415
  padding-top: 15px;
 
1418
  color: var(--text-muted);
1419
  text-align: center;
1420
  }
1421
+
1422
  /* NEW: Compact list styling for immunizations/procedures/encounters */
1423
  .previsit-report .compact-list {
1424
  font-size: 12px;
1425
  margin: 0;
1426
  padding-left: 16px;
1427
  }
1428
+
1429
  .previsit-report .compact-list li {
1430
  margin: 3px 0;
1431
  line-height: 1.4;
1432
  }
1433
+
1434
  .previsit-report .date-badge {
1435
  color: var(--text-muted);
1436
  font-size: 11px;
1437
  }
1438
+
1439
  /* Allergy styling with severity indicators */
1440
  .previsit-report .allergies-list {
1441
  margin: 0;
1442
  padding-left: 16px;
1443
  }
1444
+
1445
  .previsit-report .allergies-list li {
1446
  margin: 4px 0;
1447
  }
1448
+
1449
  .previsit-report .allergies-list .allergy-high {
1450
  color: #ff6b6b;
1451
  }
1452
+
1453
  .previsit-report .allergies-list .allergy-high strong {
1454
  color: #ff4757;
1455
  }
1456
+
1457
  .previsit-report .allergies-list .allergy-low {
1458
  color: var(--text-main);
1459
  }
1460
  </style>
1461
  </head>
1462
+
1463
  <body>
1464
 
1465
  <div class="app-shell">
 
1500
  <div class="chat-viewport" id="chatMessages">
1501
  <div class="message-row assistant">
1502
  <div class="bubble">
1503
+ Hello! I'm ready to help. You can ask me to visualize your health data, use the microphone
1504
+ to analyze respiratory sounds, or upload a skin image for analysis.
1505
  </div>
1506
  </div>
1507
  </div>
1508
 
1509
  <div class="input-area">
1510
+
1511
  <div class="chips-row">
1512
  <div class="chips-scroll">
1513
  <div class="chip" data-q="Show me my blood pressure chart">📈 BP Chart</div>
 
1516
  <div class="chip" data-q="Analyze my cough risk">🎤 Cough Check</div>
1517
  <div class="chip skin-chip" data-action="upload-skin">🔬 Skin Analysis</div>
1518
  </div>
1519
+ <button class="btn-toggle-report" id="btnToggleReport" onclick="toggleReportPanel()"
1520
+ title="Pre-Visit Report">
1521
  <span class="icon">📋</span>
1522
  <span>Report</span>
1523
  </button>
 
1528
 
1529
  <div class="input-group" id="mainInputGroup">
1530
  <input type="text" id="chatInput" placeholder="Ask a health question..." disabled>
1531
+
1532
  <div class="recording-overlay" id="recordingOverlay">
1533
  <span style="color: var(--danger); font-weight: 600; font-size: 14px;">Recording...</span>
1534
  <div class="wave-viz"></div>
 
1540
  <button class="btn-cancel-image" onclick="cancelImageUpload()">✕</button>
1541
  </div>
1542
 
1543
+ <button class="btn-icon btn-image" id="imageBtn" title="Upload Skin Image"
1544
+ onclick="triggerImageUpload()">
1545
  📷
1546
  </button>
1547
  <button class="btn-icon btn-record" id="recordBtn" title="Record Audio" disabled>
 
1551
 
1552
  </button>
1553
  </div>
1554
+
1555
  <div style="text-align: center; margin-top: 10px; font-size: 11px; color: var(--text-muted);">
1556
  ⚠️ AI responses can be inaccurate. Consult a doctor.
1557
  </div>
 
1567
  <div class="report-panel-body">
1568
  <div class="report-preview-placeholder" id="reportPlaceholder">
1569
  <div class="icon">📝</div>
1570
+ <p>Chat with me about your health concerns, then generate a summary to share with your doctor.
1571
+ </p>
1572
  </div>
1573
  <div class="report-preview" id="reportPreview" style="display: none;" onclick="openReportModal()">
1574
  <h4>Pre-Visit Summary Ready</h4>
 
1607
  // ==========================================
1608
  let patientId = null;
1609
  let chartCounter = 0;
1610
+
1611
  // Report tracking state
1612
  let conversationHistory = [];
1613
  let collectedToolResults = [];
1614
  let collectedAttachments = []; // Now includes {type, title, summary, imageData} for charts AND skin images
1615
  let currentReport = null;
1616
+
1617
  // Agent feedback state - for merging into single card
1618
  let currentFeedbackCard = null;
1619
  let feedbackData = { discovery: null, plan: null, facts: [], reflection: null, workflow: null, iteration: 1 };
1620
+
1621
  // Skin image upload state
1622
  let pendingImageData = null;
1623
  let pendingImageFile = null;
1624
  const skinImageInput = document.getElementById('skinImageInput');
1625
+
1626
  // DOM Elements
1627
  const chatMessages = document.getElementById('chatMessages');
1628
  const chatInput = document.getElementById('chatInput');
 
1663
  });
1664
 
1665
  // Collapsible Context
1666
+ document.getElementById('summaryToggle').addEventListener('click', function () {
1667
  document.getElementById('summaryContent').classList.toggle('open');
1668
  const arrow = this.querySelector('span:last-child');
1669
  arrow.textContent = arrow.textContent === '▼' ? '▲' : '▼';
 
1676
  // ==========================================
1677
  // SKIN IMAGE UPLOAD HANDLING
1678
  // ==========================================
1679
+
1680
  function triggerImageUpload() {
1681
  skinImageInput.click();
1682
  }
 
1701
 
1702
  // Read and preview the image
1703
  const reader = new FileReader();
1704
+ reader.onload = function (e) {
1705
  pendingImageData = e.target.result;
1706
+
1707
  // Show preview
1708
  document.getElementById('previewThumbnail').src = pendingImageData;
1709
  mainInputGroup.classList.add('image-pending');
1710
+
1711
  // Update placeholder to guide user
1712
  chatInput.placeholder = 'Add a message about this image (optional)...';
1713
  chatInput.focus();
 
1739
 
1740
  // Store data locally
1741
  const imageToSend = pendingImageData;
1742
+
1743
  // Clear UI state
1744
  cancelImageUpload();
1745
  chatInput.value = ''; // Clear the text input
 
1747
 
1748
  // 3. Add Loading Feedback (Fixes lack of feedback)
1749
  chatSend.disabled = true;
1750
+
1751
  // Reset feedback state for new image message
1752
  currentFeedbackCard = null;
1753
  feedbackData = { discovery: null, plan: null, facts: [], reflection: null, workflow: null, iteration: 1 };
1754
+
1755
  // Create/Show the feedback card immediately with a loading state
1756
  const feedbackCard = getOrCreateFeedbackCard();
1757
  const titleSpan = feedbackCard.querySelector('.agent-feedback-title span:last-child');
 
1763
 
1764
  // Send to agent
1765
  await sendAgentMessage(caption, imageToSend);
1766
+
1767
  chatSend.disabled = false;
1768
  chatInput.focus();
1769
  }
 
1779
  `;
1780
  chatMessages.appendChild(div);
1781
  forceScrollToBottom();
1782
+
1783
  // Track for conversation history
1784
  trackConversation('user', `[Uploaded skin image] ${caption}`);
1785
  }
 
1790
  const qualityScore = quality.score || 0;
1791
  const notes = quality.notes || ['No analysis notes available'];
1792
  const modelName = result.model || 'Derm Foundation';
1793
+
1794
  // Formulate the analysis summary
1795
  const analysisSummary = `Image Quality: ${qualityScore}/100. ${notes.join(' ')}`;
1796
+
1797
  // Fix: Check if we just added this image (to avoid duplicates in report)
1798
  // We look for the last attachment of type 'skin'
1799
  const lastAttachment = collectedAttachments.length > 0 ? collectedAttachments[collectedAttachments.length - 1] : null;
1800
+
1801
  if (lastAttachment && lastAttachment.type === 'skin' && lastAttachment.imageData === imageData) {
1802
  // Update the existing attachment with the analysis results
1803
  lastAttachment.summary += ` | Analysis: ${analysisSummary}`;
 
1806
  // Fallback: Track as new if for some reason it wasn't caught
1807
  trackAttachment('skin', `Skin Analysis (${modelName})`, analysisSummary, imageData);
1808
  }
1809
+
1810
  const scoreColor = qualityScore < 50 ? 'var(--danger)' : qualityScore < 75 ? 'var(--warning)' : 'var(--success)';
1811
+
1812
  const notesHtml = notes.map(n => `<li>${escapeHtml(n)}</li>`).join('');
1813
+
1814
  const html = `
1815
  <div class="message-row widget-row">
1816
  <div class="widget-card">
 
1839
  </div>
1840
  </div>
1841
  `;
1842
+
1843
  chatMessages.insertAdjacentHTML('beforeend', html);
1844
  forceScrollToBottom();
1845
+
1846
  // Track for conversation
1847
  trackConversation('assistant', `[Skin Analysis Result] Quality: ${qualityScore}/100. ${notes.join(' ')}`);
1848
  }
 
1872
  const res = await fetch('/api/patients');
1873
  const data = await res.json();
1874
  if (data.patients.length === 0) return;
1875
+
1876
  const patient = data.patients[0];
1877
  patientId = patient.id;
1878
  document.getElementById('patientSummaryName').textContent = `Patient: ${patient.display_name} (${patient.age}y)`;
1879
+
1880
  chatInput.disabled = false;
1881
  chatSend.disabled = false;
1882
  loadHealthDetails();
 
1894
  ]);
1895
 
1896
  // Render Tags
1897
+ const renderTags = (list, key, alert = false) => {
1898
  if (!list || list.length === 0) return '<span class="tag">None</span>';
1899
+ return list.map(item => `<span class="tag ${alert ? 'alert' : ''}">${item[key] || item.display || item.substance}</span>`).join('');
1900
  };
1901
 
1902
  document.getElementById('conditionsList').innerHTML = renderTags(conditions.conditions, 'display');
 
1911
  // ==========================================
1912
  // UI RENDERING
1913
  // ==========================================
1914
+
1915
  // Track if user has scrolled away - don't auto-scroll if they have
1916
  let userHasScrolledAway = false;
1917
  let scrollTimeout = null;
1918
+
1919
  // ANY upward scroll immediately stops auto-scroll
1920
  chatMessages.addEventListener('wheel', (e) => {
1921
  if (e.deltaY < 0) {
 
1929
  }
1930
  }
1931
  }, { passive: true });
1932
+
1933
  // Touch support
1934
  let touchStartY = 0;
1935
  chatMessages.addEventListener('touchstart', (e) => {
1936
  touchStartY = e.touches[0].clientY;
1937
  }, { passive: true });
1938
+
1939
  chatMessages.addEventListener('touchmove', (e) => {
1940
  const touchY = e.touches[0].clientY;
1941
  if (touchY > touchStartY) {
 
1943
  userHasScrolledAway = true;
1944
  }
1945
  }, { passive: true });
1946
+
1947
  // Debounced scroll - doesn't fight user as aggressively
1948
  function scrollToBottom(force = false) {
1949
  if (userHasScrolledAway && !force) return;
1950
+
1951
  // Cancel any pending scroll
1952
  if (scrollTimeout) cancelAnimationFrame(scrollTimeout);
1953
+
1954
  // Use requestAnimationFrame for smoother, less aggressive scrolling
1955
  scrollTimeout = requestAnimationFrame(() => {
1956
  chatMessages.scrollTop = chatMessages.scrollHeight;
1957
  });
1958
  }
1959
+
1960
  // Force scroll for new user messages and important events
1961
  function forceScrollToBottom() {
1962
  userHasScrolledAway = false;
 
1974
  // Reset feedback state for new message
1975
  currentFeedbackCard = null;
1976
  feedbackData = { discovery: null, plan: null, facts: [], reflection: null, workflow: null, iteration: 1 };
1977
+
1978
  const div = document.createElement('div');
1979
  div.className = 'message-row user';
1980
  div.innerHTML = `<div class="bubble">${escapeHtml(content)}</div>`;
1981
  chatMessages.appendChild(div);
1982
  forceScrollToBottom(); // Always scroll for user's own message
1983
+
1984
  // Track for report
1985
  trackConversation('user', content);
1986
  }
 
1989
  function addAssistantMessage(content) {
1990
  const div = document.createElement('div');
1991
  div.className = 'message-row assistant';
1992
+
1993
  // Remove "ANSWER:" prefix (case insensitive) and whitespace
1994
  const cleanContent = content.replace(/^ANSWER:\s*/i, '');
1995
+
1996
  // Convert Markdown to HTML using marked
1997
  const htmlContent = marked.parse(cleanContent);
1998
+
1999
+ div.innerHTML = `<div class="bubble markdown-body">${htmlContent}</div>`;
2000
  chatMessages.appendChild(div);
2001
  scrollToBottom();
2002
+
2003
  // Track for report
2004
  trackConversation('assistant', cleanContent);
2005
  }
 
2010
 
2011
  // Thinking indicator - shows while agent is processing
2012
  let thinkingElement = null;
2013
+
2014
  function showThinking(text = 'Thinking') {
2015
  removeThinking(); // Remove any existing
2016
  const div = document.createElement('div');
 
2024
  thinkingElement = div;
2025
  scrollToBottom();
2026
  }
2027
+
2028
  function updateThinking(text) {
2029
  if (thinkingElement) {
2030
  const textEl = thinkingElement.querySelector('.thinking-text');
 
2033
  showThinking(text);
2034
  }
2035
  }
2036
+
2037
  function removeThinking() {
2038
  if (thinkingElement) {
2039
  thinkingElement.remove();
 
2056
  streamingText += token;
2057
  // Remove "ANSWER:" prefix dynamically during stream
2058
  const cleanText = streamingText.replace(/^ANSWER:\s*/i, '');
2059
+
2060
  // Using marked.parse here so markdown renders during stream
2061
  streamingBubble.innerHTML = marked.parse(cleanText);
2062
  scrollToBottom(); // Smart scroll - won't interrupt if user scrolled up
 
2066
  function endStreamingAnswer() {
2067
  if (streamingBubble) {
2068
  streamingBubble.classList.remove('streaming');
2069
+
2070
  // Track the completed streamed answer for report
2071
  if (streamingText.trim()) {
2072
  const cleanContent = streamingText.replace(/^ANSWER:\s*/i, '');
2073
  trackConversation('assistant', cleanContent);
2074
  }
2075
+
2076
  streamingBubble = null;
2077
  streamingText = "";
2078
  }
 
2081
  // Only show errors as assistant messages
2082
  function addSystemLog(text, type = 'status') {
2083
  if (type !== 'error') return; // Skip all non-error logs
2084
+
2085
  // Show errors as simple assistant messages
2086
  const div = document.createElement('div');
2087
  div.className = 'message-row assistant';
 
2118
  } catch (e) {
2119
  formattedContent = `<pre>${escapeHtml(result)}</pre>`;
2120
  }
2121
+
2122
  const div = document.createElement('div');
2123
  div.className = 'message-row tool-result-row';
2124
  div.innerHTML = `
 
2134
  // ==========================================
2135
  // UNIFIED AGENT FEEDBACK - Merged & Subtle
2136
  // ==========================================
2137
+
2138
  function getOrCreateFeedbackCard() {
2139
  if (!currentFeedbackCard) {
2140
  const div = document.createElement('div');
 
2157
  }
2158
  return currentFeedbackCard;
2159
  }
2160
+
2161
  function toggleFeedbackCard(header) {
2162
  const card = header.closest('.agent-feedback-card');
2163
  card.classList.toggle('expanded');
2164
  }
2165
+
2166
  function updateFeedbackCard() {
2167
  const card = getOrCreateFeedbackCard();
2168
  const content = card.querySelector('.agent-feedback-content');
2169
  const titleSpan = card.querySelector('.agent-feedback-title span:last-child');
2170
+
2171
  // Remove loading indicator when we have actual content
2172
  titleSpan.classList.remove('loading-indicator');
2173
+
2174
  let html = '';
2175
  let stepCount = 0;
2176
+
2177
  // Discovery section
2178
  if (feedbackData.discovery) {
2179
  stepCount++;
 
2187
  </div>
2188
  `;
2189
  }
2190
+
2191
  // Plan section (with iteration badge)
2192
  if (feedbackData.plan && feedbackData.plan.length > 0) {
2193
  stepCount++;
 
2203
  </div>
2204
  `;
2205
  }).join('');
2206
+
2207
+ const iterationBadge = feedbackData.iteration > 1
2208
+ ? `<span class="iteration-badge">Iteration ${feedbackData.iteration}</span>`
2209
  : '';
2210
+
2211
  html += `
2212
  <div class="feedback-section">
2213
  <div class="feedback-section-label">Query Plan ${iterationBadge}</div>
 
2215
  </div>
2216
  `;
2217
  }
2218
+
2219
  // Facts sections
2220
  if (feedbackData.facts.length > 0) {
2221
  feedbackData.facts.forEach(f => {
 
2229
  `;
2230
  });
2231
  }
2232
+
2233
  // Reflection section
2234
  if (feedbackData.reflection) {
2235
  stepCount++;
 
2237
  const statusIcon = r.has_enough_info ? '✓' : '⚠';
2238
  const statusClass = r.has_enough_info ? 'reflection-complete' : 'reflection-gaps';
2239
  const confidenceBar = Math.round((r.confidence || 0.8) * 100);
2240
+
2241
  let reflectionContent = `
2242
  <div class="reflection-status ${statusClass}">
2243
  <span class="reflection-icon">${statusIcon}</span>
 
2245
  <span class="confidence-badge">${confidenceBar}% confident</span>
2246
  </div>
2247
  `;
2248
+
2249
  if (r.gaps && r.gaps.length > 0) {
2250
  reflectionContent += `
2251
  <div class="reflection-gaps-list">
 
2253
  </div>
2254
  `;
2255
  }
2256
+
2257
  if (r.reasoning) {
2258
  reflectionContent += `<div class="reflection-reasoning">${escapeHtml(r.reasoning)}</div>`;
2259
  }
2260
+
2261
  html += `
2262
  <div class="feedback-section">
2263
  <div class="feedback-section-label">Reflection</div>
 
2265
  </div>
2266
  `;
2267
  }
2268
+
2269
  // Workflow summary (at the end)
2270
  if (feedbackData.workflow) {
2271
  const w = feedbackData.workflow;
 
2279
  </div>
2280
  `;
2281
  }
2282
+
2283
  content.innerHTML = html;
2284
+
2285
  // Update title with step count
2286
  if (stepCount > 0) {
2287
  titleSpan.textContent = `${stepCount} step${stepCount > 1 ? 's' : ''} completed`;
2288
  }
2289
  }
2290
+
2291
  function addDiscoveryCard(summary, manifest) {
2292
  let samplesHtml = '';
2293
  if (manifest && manifest.sample_values) {
 
2301
  }
2302
  samplesHtml = parts.join(' • ');
2303
  }
2304
+
2305
  feedbackData.discovery = { summary, samples: samplesHtml };
2306
  updateFeedbackCard();
2307
  }
 
2315
  function addFactsCard(toolName, facts, rawPreview) {
2316
  feedbackData.facts.push({ tool: toolName, facts });
2317
  updateFeedbackCard();
2318
+
2319
  // Track for report
2320
  trackToolResult(toolName, facts);
2321
  }
2322
+
2323
  function addReflectionCard(event) {
2324
  feedbackData.reflection = {
2325
  has_enough_info: event.has_enough_info,
 
2329
  };
2330
  updateFeedbackCard();
2331
  }
2332
+
2333
  function addWorkflowSummary(event) {
2334
  feedbackData.workflow = {
2335
  iterations: event.iterations || 1,
 
2347
  console.warn('Skipping empty chart data:', chartData?.title);
2348
  return;
2349
  }
2350
+
2351
  // NEW: Check if any dataset actually has data points
2352
  const hasDataPoints = chartData.datasets.some(ds => ds.data && ds.data.length > 0);
2353
  if (!hasDataPoints) {
2354
  console.warn('Skipping chart with empty datasets:', chartData?.title);
2355
  return;
2356
  }
2357
+
2358
  chartCounter++;
2359
  const canvasId = `canvas-${chartCounter}`;
2360
+
2361
  // Track as attachment for report (image will be added after render)
2362
  let summary = chartData.title;
2363
  if (chartData.summary && Array.isArray(chartData.summary)) {
2364
  summary += ': ' + chartData.summary.slice(0, 2).join('; ');
2365
  }
2366
+
2367
  // Create attachment object and store direct reference (not index)
2368
  const attachmentObj = { type: 'chart', title: chartData.title, summary, imageData: null };
2369
  collectedAttachments.push(attachmentObj);
2370
+
2371
  const div = document.createElement('div');
2372
  div.className = 'message-row widget-row';
2373
  div.innerHTML = `
 
2396
  }
2397
 
2398
  function createChartInstance(ctx, chartData, attachmentObj) {
2399
+ const primaryColor = '#5e72e4';
2400
+
2401
  // Set global defaults for Dark Mode charts
2402
  Chart.defaults.color = '#8898aa';
2403
  Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
2404
 
2405
  const isBar = chartData.chart_type && chartData.chart_type.includes('bar');
2406
+
2407
  // FIX: If bar chart, use high opacity (CC = ~80%), otherwise low opacity (20 = ~12%)
2408
  const opacityHex = isBar ? 'CC' : '20';
2409
 
 
2427
  label: ds.label,
2428
  data: ds.data.map(d => typeof d === 'object' ? (d.value || d.y) : d),
2429
  borderColor: color,
2430
+ backgroundColor: color + opacityHex,
2431
  tension: 0.3,
2432
  fill: !isBar // Don't fill bar charts
2433
  };
2434
  });
2435
  console.log(`[CHART DEBUG] Total datasets: ${datasets.length}, isBar: ${isBar}`);
2436
+
2437
  const labels = chartData.labels || chartData.datasets[0].data.map(d => d.date);
2438
 
2439
  new Chart(ctx, {
 
2444
  maintainAspectRatio: false,
2445
  // FIXED: Use animation onComplete callback to capture chart image
2446
  animation: {
2447
+ onComplete: function () {
2448
  // This runs only when the chart is fully drawn
2449
  if (!attachmentObj) return;
2450
  try {
2451
  const canvas = ctx.canvas;
2452
+
2453
  // Create a temporary canvas to bake in a dark background
2454
  // This ensures the chart is visible in PDF export
2455
  const tempCanvas = document.createElement('canvas');
2456
  tempCanvas.width = canvas.width;
2457
  tempCanvas.height = canvas.height;
2458
  const tempCtx = tempCanvas.getContext('2d');
2459
+
2460
  // Fill dark background (matches our UI)
2461
  tempCtx.fillStyle = '#171c29';
2462
  tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
2463
+
2464
  // Draw the chart on top
2465
  tempCtx.drawImage(canvas, 0, 0);
2466
+
2467
  // Save this opaque image
2468
  attachmentObj.imageData = tempCanvas.toDataURL('image/png');
2469
  } catch (e) {
 
2493
  async function sendMessage() {
2494
  // Capture text immediately
2495
  const message = chatInput.value.trim();
2496
+
2497
  // Check if there's a pending skin image
2498
  if (pendingImageData) {
2499
  // Pass the message text to the image handler
2500
  await sendSkinImageForAnalysis(message);
2501
  return;
2502
  }
2503
+
2504
  if (!message || !patientId) return;
2505
 
2506
  addUserMessage(message);
 
2516
 
2517
  async function sendAgentMessage(message, skinImageData = null) {
2518
  try {
2519
+ const requestBody = {
2520
+ patient_id: patientId,
2521
+ message,
2522
+ conversation_history: conversationHistory
2523
  };
2524
+
2525
  // Include skin image data if provided
2526
  if (skinImageData) {
2527
  requestBody.skin_image_data = skinImageData;
2528
  }
2529
+
2530
  const response = await fetch('/api/agent/chat', {
2531
  method: 'POST',
2532
  headers: { 'Content-Type': 'application/json' },
 
2542
 
2543
  const chunk = decoder.decode(value);
2544
  const lines = chunk.split('\n');
2545
+
2546
  for (const line of lines) {
2547
  const trimmedLine = line.trim();
2548
  if (!trimmedLine || !trimmedLine.startsWith('data: ')) continue;
 
2553
  try {
2554
  const event = JSON.parse(dataStr);
2555
  switch (event.type) {
2556
+ case 'status':
2557
  // Show thinking indicator with status text
2558
  updateThinking(event.message || 'Thinking');
2559
  break;
 
2571
  addPlanCard(event.tools, event.iteration || 1);
2572
  }
2573
  break;
2574
+ case 'tool_call':
2575
  updateThinking(`Retrieving ${event.tool || 'data'}...`);
2576
  break;
2577
  case 'tool_result':
 
2595
  // Error recovery: Log tool errors but continue
2596
  console.warn(`Tool error: ${event.tool} - ${event.error}`);
2597
  break;
2598
+ case 'chart_data':
2599
  renderChartWidget(event.data); break;
2600
  case 'skin_analysis':
2601
  // Handle skin analysis results from agent
 
2603
  renderSkinAnalysisWidget(event.data, event.image_data || skinImageData);
2604
  }
2605
  break;
2606
+ case 'answer':
2607
  removeThinking();
2608
  addAssistantMessage(event.content); break;
2609
  case 'answer_start':
 
2620
  removeThinking();
2621
  addSystemLog(event.message, 'error'); break;
2622
  }
2623
+ } catch (e) {
2624
+ console.log('Skipping non-JSON chunk:', dataStr);
2625
  }
2626
  }
2627
  }
 
2637
  let mediaRecorder = null;
2638
  let audioChunks = [];
2639
  let isRecording = false;
2640
+
2641
  // 1. Check Capabilities
2642
  fetch('/api/audio/status').then(r => r.json()).then(data => {
2643
  if (data.available) {
 
2653
  mediaRecorder.stop();
2654
  isRecording = false;
2655
  recordBtn.classList.remove('recording');
2656
+
2657
  // Toggle UI back to Input
2658
  mainInputGroup.classList.remove('recording-active');
2659
+
2660
  // Don't show status log anymore
2661
  } else {
2662
  // START RECORDING
 
2664
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
2665
  mediaRecorder = new MediaRecorder(stream);
2666
  audioChunks = [];
2667
+
2668
  mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
2669
  mediaRecorder.onstop = () => {
2670
  const blob = new Blob(audioChunks, { type: 'audio/webm' });
 
2674
  mediaRecorder.start();
2675
  isRecording = true;
2676
  recordBtn.classList.add('recording');
2677
+
2678
  // Toggle UI to Overlay
2679
  mainInputGroup.classList.add('recording-active');
2680
 
 
2703
  const data = result.health_assessment || result.respiratory_indicators || {};
2704
  const score = data.respiratory_health_score || data.overall_score || 0;
2705
  const modelName = result.model || 'Unknown';
2706
+
2707
  // Track as attachment for report
2708
  const summary = `Respiratory Health Score: ${Math.round(score)}/100. ` +
2709
  `Cough: ${data.cough_level || 'None'}. ` +
2710
  `Breathing: ${data.breathing_quality || 'Normal'}.` +
2711
  (result.covid_risk ? ` COVID Risk: ${result.covid_risk.risk_level || 'Unknown'}.` : '');
2712
  trackAttachment('audio', `Respiratory Analysis (${modelName})`, summary);
2713
+
2714
  const scoreColor = score < 60 ? 'var(--danger)' : score < 80 ? 'var(--warning)' : 'var(--success)';
2715
+
2716
  const html = `
2717
  <div class="message-row widget-row">
2718
  <div class="widget-card">
 
2748
  </div>
2749
  </div>
2750
  `;
2751
+
2752
  chatMessages.insertAdjacentHTML('beforeend', html);
2753
  forceScrollToBottom(); // Force scroll to show audio results
2754
  }
 
2756
  // ==========================================
2757
  // PRE-VISIT REPORT FUNCTIONS
2758
  // ==========================================
2759
+
2760
  function toggleReportPanel() {
2761
  const panel = document.getElementById('reportPanel');
2762
  panel.classList.toggle('open');
2763
  }
2764
+
2765
  function openReportModal() {
2766
  if (!currentReport) return;
2767
  const overlay = document.getElementById('reportModalOverlay');
2768
  const body = document.getElementById('reportModalBody');
2769
+
2770
  // Build chart images HTML
2771
  let chartImagesHtml = '';
2772
  let skinImagesHtml = '';
2773
+
2774
  collectedAttachments.forEach(att => {
2775
  if (att.type === 'chart' && att.imageData) {
2776
  chartImagesHtml += `
 
2790
  `;
2791
  }
2792
  });
2793
+
2794
  // Combine report HTML with chart and skin images
2795
  let additionalSections = '';
2796
  if (chartImagesHtml) {
 
2809
  </div>
2810
  `;
2811
  }
2812
+
2813
  body.innerHTML = currentReport.html + additionalSections;
2814
+
2815
  overlay.classList.add('open');
2816
  }
2817
+
2818
  function closeReportModal(event) {
2819
  if (event && event.target !== event.currentTarget) return;
2820
  document.getElementById('reportModalOverlay').classList.remove('open');
2821
  }
2822
+
2823
  async function generateReport() {
2824
  if (!patientId) return;
2825
+
2826
  const btn = document.getElementById('btnGenerateReport');
2827
  btn.classList.add('loading');
2828
  btn.disabled = true;
2829
+
2830
  try {
2831
  const response = await fetch('/api/report/generate', {
2832
  method: 'POST',
 
2838
  attachments: collectedAttachments
2839
  })
2840
  });
2841
+
2842
  const data = await response.json();
2843
+
2844
  if (data.success) {
2845
  currentReport = data;
2846
  updateReportPreview(data.report);
2847
+
2848
  // Update toggle button
2849
  document.getElementById('btnToggleReport').classList.add('has-report');
2850
+
2851
  // Open panel if not already open
2852
  const panel = document.getElementById('reportPanel');
2853
  if (!panel.classList.contains('open')) {
 
2864
  btn.disabled = false;
2865
  }
2866
  }
2867
+
2868
  function updateReportPreview(report) {
2869
  document.getElementById('reportPlaceholder').style.display = 'none';
2870
  const preview = document.getElementById('reportPreview');
2871
  preview.style.display = 'block';
2872
+
2873
  const concerns = report.chief_concerns || [];
2874
+ document.getElementById('previewConcerns').textContent =
2875
  concerns.length > 0 ? concerns.slice(0, 2).join(', ') : 'No specific concerns noted';
2876
+
2877
  const attachCount = (report.attachments || []).length;
2878
+ document.getElementById('previewMeta').textContent =
2879
  `${report.generated_at} • ${attachCount} attachment${attachCount !== 1 ? 's' : ''}`;
2880
  }
2881
+
2882
  function downloadReport() {
2883
  if (!currentReport) return;
2884
+
2885
  // Build chart and skin images HTML from collected attachments
2886
  let chartImagesHtml = '';
2887
  let skinImagesHtml = '';
2888
+
2889
  collectedAttachments.forEach(att => {
2890
  if (att.type === 'chart' && att.imageData) {
2891
  chartImagesHtml += `
 
2905
  `;
2906
  }
2907
  });
2908
+
2909
  // Create container for PDF generation with explicit dark text on white background
2910
  const pdfContainer = document.createElement('div');
2911
  pdfContainer.innerHTML = `
 
2957
  </div>
2958
  </div>
2959
  `;
2960
+
2961
  // PDF options
2962
  const opt = {
2963
  margin: [10, 10, 10, 10],
 
2967
  jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
2968
  pagebreak: { mode: ['avoid-all', 'css', 'legacy'] }
2969
  };
2970
+
2971
  // Generate and download PDF
2972
  html2pdf().set(opt).from(pdfContainer).save();
2973
  }
2974
+
2975
  // Track conversation for report
2976
  function trackConversation(role, content) {
2977
  conversationHistory.push({ role, content });
2978
  }
2979
+
2980
  // Track tool results for report
2981
  function trackToolResult(tool, facts) {
2982
  collectedToolResults.push({ tool, facts });
2983
  }
2984
+
2985
  // Track attachments for report (including chart images)
2986
  function trackAttachment(type, title, summary, imageData = null) {
2987
  collectedAttachments.push({ type, title, summary, imageData });
2988
  }
2989
  </script>
2990
  </body>
2991
+
2992
  </html>