Samarth-Health-AI commited on
Commit
b6c0b60
Β·
verified Β·
1 Parent(s): f3b208b

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +147 -71
src/streamlit_app.py CHANGED
@@ -5,20 +5,22 @@ import streamlit.components.v1 as components
5
  from audiorecorder import audiorecorder # NEW
6
  from audio_recorder_streamlit import audio_recorder
7
 
8
- import shutil
9
  from pydub import AudioSegment
10
 
11
  AudioSegment.ffmpeg = shutil.which("ffmpeg") or "ffmpeg"
12
  AudioSegment.ffprobe = shutil.which("ffprobe") or "ffprobe"
13
 
14
-
15
  # --- 0. PAGE CONFIG ---
16
- st.set_page_config(page_title="Clinical Portal", layout="wide", initial_sidebar_state="expanded")
 
 
 
 
17
  API_KEY = os.environ.get("OPENROUTER_API_KEY") or st.secrets.get("OPENROUTER_API_KEY")
18
 
19
-
20
- # --- 1. JAVASCRIPT ERROR SUPPRESSOR (kept; harmless now) ---
21
- components.html("""
22
  <script>
23
  const hideAudioErrors = () => {
24
  const selectors = [
@@ -45,11 +47,13 @@ components.html("""
45
  observer.observe(document.body, { childList: true, subtree: true });
46
  window.addEventListener('message', hideAudioErrors);
47
  </script>
48
- """, height=0)
 
 
49
 
50
-
51
- # --- 2. PHYSICIAN-OPTIMIZED CSS (unchanged; audio_input selectors now irrelevant) ---
52
- st.markdown("""
53
  <style>
54
  /* Global App Theme */
55
  .stApp {
@@ -57,7 +61,8 @@ st.markdown("""
57
  font-family: 'Inter', 'Segoe UI', sans-serif;
58
  color: #2c3e50;
59
  }
60
- /* AGGRESSIVE ERROR HIDING - Multi-layered approach */
 
61
  [data-testid="stAudioInput"] [data-testid="stNotification"],
62
  [data-testid="stAudioInput"] [role="alert"],
63
  [data-testid="stAudioInput"] div[class*="error"],
@@ -87,7 +92,8 @@ st.markdown("""
87
  padding: 25px 20px;
88
  box-shadow: 4px 0 12px rgba(0,0,0,0.08);
89
  }
90
-
 
91
  [data-testid="stSidebar"] * {
92
  color: #ffffff !important;
93
  }
@@ -266,12 +272,33 @@ st.markdown("""
266
  border-radius: 6px;
267
  font-weight: 600;
268
  }
269
- </style>
270
- """, unsafe_allow_html=True)
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
  # --- 3. HEADER BANNER ---
274
- st.markdown("""
 
275
  <div class="banner-container">
276
  <h1>πŸ₯ Clinical Intelligence Portal</h1>
277
  <p>AI-Powered SOAP Note Generation & Quality Assurance System</p>
@@ -279,7 +306,9 @@ st.markdown("""
279
  ⚠️ This system uses a free-tier AI model; performance may vary depending on API availability.
280
  </p>
281
  </div>
282
- """, unsafe_allow_html=True)
 
 
283
 
284
  # --- 4. PHYSICIAN-FRIENDLY SIDEBAR ---
285
  with st.sidebar:
@@ -287,7 +316,7 @@ with st.sidebar:
287
  '<div style="margin-bottom: 18px;">'
288
  '<h2 style="color:#ffffff; font-size:1.6rem; text-align:center;">Workflow</h2>'
289
  '</div>',
290
- unsafe_allow_html=True
291
  )
292
 
293
  # Access Control (card)
@@ -299,7 +328,7 @@ with st.sidebar:
299
  API_KEY = st.text_input("Enter API Key", type="password", help="OpenRouter or similar API key")
300
  st.markdown('</div>', unsafe_allow_html=True)
301
 
302
- # Voice Consultation (card) - keeps preview + checkbox in SAME card
303
  st.markdown('<div class="sb-card">', unsafe_allow_html=True)
304
  st.markdown('<div class="sb-title">🎀 Voice Consultation</div>', unsafe_allow_html=True)
305
 
@@ -311,7 +340,7 @@ with st.sidebar:
311
  icon_size="3x",
312
  energy_threshold=(-1.0, 1.0),
313
  pause_threshold=300.0,
314
- key="voice_recorder_main"
315
  )
316
 
317
  audio_data = None
@@ -334,7 +363,12 @@ with st.sidebar:
334
 
335
  if "voice_transcript" in st.session_state:
336
  st.text_area("πŸ“‹ Preview", st.session_state.voice_transcript, height=100, disabled=True)
337
- st.download_button("πŸ’Ύ Download", st.session_state.voice_transcript, "transcript.txt", use_container_width=True)
 
 
 
 
 
338
  st.checkbox("βœ“ Use Voice for Analysis", value=True, key="use_voice")
339
 
340
  st.markdown('</div>', unsafe_allow_html=True)
@@ -343,20 +377,35 @@ with st.sidebar:
343
  st.markdown('<div class="sb-card">', unsafe_allow_html=True)
344
  st.markdown('<div class="sb-title">πŸ“‚ Clinical Documents</div>', unsafe_allow_html=True)
345
 
346
- files = st.file_uploader("Upload Records", accept_multiple_files=True, type=["pdf", "docx", "txt"])
 
 
 
 
347
 
348
  sample_path = "src/sample_transcript.docx"
349
  if os.path.exists(sample_path):
350
  with open(sample_path, "rb") as f:
351
- st.download_button("πŸ“₯ Sample Template", f, "Sample_Template.docx", use_container_width=True)
 
 
 
 
 
352
  st.checkbox("πŸ“„ Use Sample", value=False, key="use_sample")
353
 
354
  st.markdown('</div>', unsafe_allow_html=True)
355
 
356
- # Execute Analysis (final green-glow card)
357
  st.markdown('<div class="sb-card sb-execute">', unsafe_allow_html=True)
358
- st.markdown('<div class="sb-title">πŸš€ Execute Analysis <span class="sb-arrow">β†’</span></div>', unsafe_allow_html=True)
359
- st.markdown('<div class="sb-subtitle">Ready? Hit Execute Below!</div>', unsafe_allow_html=True)
 
 
 
 
 
 
360
 
361
  if st.button(
362
  "🩺 Generate SOAP Note",
@@ -365,7 +414,6 @@ with st.sidebar:
365
  help="Combines voice + docs for full analysis",
366
  key="btn_generate_soap",
367
  ) and API_KEY:
368
- # Your existing generate logic (unchanged)
369
  status_label = st.empty()
370
  progress_bar = st.progress(0)
371
 
@@ -379,8 +427,14 @@ with st.sidebar:
379
 
380
  if st.session_state.get("use_voice") and "voice_transcript" in st.session_state:
381
  from langchain_core.documents import Document
 
382
  docs = process_docs_from_list(paths)
383
- docs.append(Document(page_content=st.session_state.voice_transcript, metadata={"source": "Voice Transcript"}))
 
 
 
 
 
384
  else:
385
  docs = process_docs_from_list(paths)
386
 
@@ -397,7 +451,9 @@ with st.sidebar:
397
  status_label.info("βœ… Auditing...")
398
  progress_bar.progress(90)
399
  st.session_state.eval = evaluate_clinical_output(
400
- st.session_state.raw_text, str(st.session_state.soap), API_KEY
 
 
401
  )
402
 
403
  progress_bar.progress(100)
@@ -407,7 +463,6 @@ with st.sidebar:
407
 
408
  st.markdown('</div>', unsafe_allow_html=True)
409
 
410
-
411
  # --- 5. MAIN WORKSPACE ---
412
  if "soap" in st.session_state:
413
  col_l, col_r = st.columns([1.75, 1.25])
@@ -418,42 +473,49 @@ if "soap" in st.session_state:
418
 
419
  for sec, data in st.session_state.soap.items():
420
  st.markdown(f'<div class="soap-header">{sec}</div>', unsafe_allow_html=True)
421
- st.markdown(f'<div class="soap-content">{data["content"]}</div>', unsafe_allow_html=True)
422
 
423
- # FIXED: Indented to stay within the 'for' loop and 'col_l' container
424
  with st.expander(f"πŸ” View Supporting Evidence for {sec}"):
425
  if data.get("evidence"):
426
  for i, snippet in enumerate(data["evidence"]):
427
- # Redact PHI from evidence chunks locally
428
  redacted_snippet = redact_phi(snippet)
429
-
430
- # Truncate long chunks (show first 300 chars)
431
  max_display_length = 300
432
  display_snippet = redacted_snippet[:max_display_length]
433
  if len(redacted_snippet) > max_display_length:
434
  display_snippet += "... [truncated]"
435
 
436
- st.markdown(f"""
437
- <div style="background: #f8f9fa; padding: 12px; border-radius: 6px; border-left: 3px solid #667eea; margin-bottom: 10px;">
438
- <p style="color: #1e3c72; font-weight: 600; font-size: 0.9rem; margin: 0 0 8px 0;">
439
- πŸ“„ Source Chunk {i+1}
440
- </p>
441
- <p style="color: #555; font-size: 0.95rem; line-height: 1.6; margin: 0;">
442
- {display_snippet}
443
- </p>
444
- </div>
445
- """, unsafe_allow_html=True)
 
 
 
 
446
  else:
447
  st.info("No direct transcript evidence found for this section.")
448
 
449
  st.divider()
450
 
451
- # Export Section Header
452
- st.markdown('<div style="text-align: center; margin: 20px 0 15px 0;"><h4 style="color: #1e3c72; font-weight: 700;">πŸ“₯ Export Clinical Note</h4></div>', unsafe_allow_html=True)
 
 
 
 
 
453
 
454
  col1, col2 = st.columns(2)
455
  with col1:
456
- st.markdown("""
 
457
  <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
458
  padding: 15px; border-radius: 10px; text-align: center;
459
  box-shadow: 0 4px 12px rgba(102,126,234,0.3); margin-bottom: 10px;">
@@ -464,17 +526,20 @@ if "soap" in st.session_state:
464
  Interoperable JSON for EHR systems
465
  </p>
466
  </div>
467
- """, unsafe_allow_html=True)
 
 
468
  st.download_button(
469
- "⬇️ Download FHIR",
470
- export_as_fhir(st.session_state.soap),
471
- "clinical_note.json",
472
  use_container_width=True,
473
- type="primary"
474
  )
475
 
476
  with col2:
477
- st.markdown("""
 
478
  <div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
479
  padding: 15px; border-radius: 10px; text-align: center;
480
  box-shadow: 0 4px 12px rgba(17,153,142,0.3); margin-bottom: 10px;">
@@ -485,13 +550,15 @@ if "soap" in st.session_state:
485
  Human-readable documentation
486
  </p>
487
  </div>
488
- """, unsafe_allow_html=True)
 
 
489
  st.download_button(
490
- "⬇️ Download Text",
491
- str(st.session_state.soap),
492
- "soap_note.txt",
493
  use_container_width=True,
494
- type="secondary"
495
  )
496
 
497
  with col_r:
@@ -500,24 +567,33 @@ if "soap" in st.session_state:
500
 
501
  for metric, details in st.session_state.eval.items():
502
  if isinstance(details, dict):
503
- score = details.get('score', 0)
504
  color = "metric-good" if score >= 4 else "metric-bad"
505
 
506
- st.markdown(f'<div class="metric-card {color}">{metric}: {score}/5 ⭐</div>', unsafe_allow_html=True)
507
- st.markdown(f'<div class="metric-reason">πŸ’‘ {details.get("reason", "Analysis verified.")}</div>', unsafe_allow_html=True)
 
 
 
 
 
 
508
 
509
  if metric != list(st.session_state.eval.keys())[-1]:
510
  st.divider()
511
 
512
- st.markdown('</div>', unsafe_allow_html=True)
513
 
514
  else:
515
- st.markdown("""
516
- <div class="empty-state">
517
- <h3>πŸ₯ Ready to Assist</h3>
518
- <p>Upload clinical documents or record a consultation to begin automated SOAP note generation.</p>
519
- <p style="font-size: 0.95rem; color: #95a5a6; margin-top: 15px;">
520
- πŸ‘ˆ Use the sidebar to get started
521
- </p>
522
- </div>
523
- """, unsafe_allow_html=True)
 
 
 
 
5
  from audiorecorder import audiorecorder # NEW
6
  from audio_recorder_streamlit import audio_recorder
7
 
 
8
  from pydub import AudioSegment
9
 
10
  AudioSegment.ffmpeg = shutil.which("ffmpeg") or "ffmpeg"
11
  AudioSegment.ffprobe = shutil.which("ffprobe") or "ffprobe"
12
 
 
13
  # --- 0. PAGE CONFIG ---
14
+ st.set_page_config(
15
+ page_title="Clinical Portal",
16
+ layout="wide",
17
+ initial_sidebar_state="expanded"
18
+ )
19
  API_KEY = os.environ.get("OPENROUTER_API_KEY") or st.secrets.get("OPENROUTER_API_KEY")
20
 
21
+ # --- 1. JAVASCRIPT ERROR SUPPRESSOR ---
22
+ components.html(
23
+ """
24
  <script>
25
  const hideAudioErrors = () => {
26
  const selectors = [
 
47
  observer.observe(document.body, { childList: true, subtree: true });
48
  window.addEventListener('message', hideAudioErrors);
49
  </script>
50
+ """,
51
+ height=0,
52
+ )
53
 
54
+ # --- 2. PHYSICIAN-OPTIMIZED CSS ---
55
+ st.markdown(
56
+ """
57
  <style>
58
  /* Global App Theme */
59
  .stApp {
 
61
  font-family: 'Inter', 'Segoe UI', sans-serif;
62
  color: #2c3e50;
63
  }
64
+
65
+ /* AGGRESSIVE ERROR HIDING */
66
  [data-testid="stAudioInput"] [data-testid="stNotification"],
67
  [data-testid="stAudioInput"] [role="alert"],
68
  [data-testid="stAudioInput"] div[class*="error"],
 
92
  padding: 25px 20px;
93
  box-shadow: 4px 0 12px rgba(0,0,0,0.08);
94
  }
95
+
96
+ /* Make sidebar text white by default (for dark gradient) */
97
  [data-testid="stSidebar"] * {
98
  color: #ffffff !important;
99
  }
 
272
  border-radius: 6px;
273
  font-weight: 600;
274
  }
 
 
275
 
276
+ /* === NEW: Fix text visibility in file uploader, inputs, checkboxes === */
277
+ /* File uploader text inside sidebar (label + dropzone) */
278
+ [data-testid="stSidebar"] [data-testid="stFileUploader"] * {
279
+ color: #1f2933 !important; /* dark text on white uploader background */
280
+ }
281
+
282
+ /* Text inputs and password fields */
283
+ [data-testid="stSidebar"] input,
284
+ [data-testid="stSidebar"] textarea {
285
+ color: #1f2933 !important;
286
+ }
287
+
288
+ /* Checkbox labels (e.g., Use Sample, Use Voice) */
289
+ [data-testid="stSidebar"] .stCheckbox label {
290
+ color: #1f2933 !important;
291
+ }
292
+ /* === END NEW === */
293
+
294
+ </style>
295
+ """,
296
+ unsafe_allow_html=True,
297
+ )
298
 
299
  # --- 3. HEADER BANNER ---
300
+ st.markdown(
301
+ """
302
  <div class="banner-container">
303
  <h1>πŸ₯ Clinical Intelligence Portal</h1>
304
  <p>AI-Powered SOAP Note Generation & Quality Assurance System</p>
 
306
  ⚠️ This system uses a free-tier AI model; performance may vary depending on API availability.
307
  </p>
308
  </div>
309
+ """,
310
+ unsafe_allow_html=True,
311
+ )
312
 
313
  # --- 4. PHYSICIAN-FRIENDLY SIDEBAR ---
314
  with st.sidebar:
 
316
  '<div style="margin-bottom: 18px;">'
317
  '<h2 style="color:#ffffff; font-size:1.6rem; text-align:center;">Workflow</h2>'
318
  '</div>',
319
+ unsafe_allow_html=True,
320
  )
321
 
322
  # Access Control (card)
 
328
  API_KEY = st.text_input("Enter API Key", type="password", help="OpenRouter or similar API key")
329
  st.markdown('</div>', unsafe_allow_html=True)
330
 
331
+ # Voice Consultation (card)
332
  st.markdown('<div class="sb-card">', unsafe_allow_html=True)
333
  st.markdown('<div class="sb-title">🎀 Voice Consultation</div>', unsafe_allow_html=True)
334
 
 
340
  icon_size="3x",
341
  energy_threshold=(-1.0, 1.0),
342
  pause_threshold=300.0,
343
+ key="voice_recorder_main",
344
  )
345
 
346
  audio_data = None
 
363
 
364
  if "voice_transcript" in st.session_state:
365
  st.text_area("πŸ“‹ Preview", st.session_state.voice_transcript, height=100, disabled=True)
366
+ st.download_button(
367
+ "πŸ’Ύ Download",
368
+ st.session_state.voice_transcript,
369
+ "transcript.txt",
370
+ use_container_width=True,
371
+ )
372
  st.checkbox("βœ“ Use Voice for Analysis", value=True, key="use_voice")
373
 
374
  st.markdown('</div>', unsafe_allow_html=True)
 
377
  st.markdown('<div class="sb-card">', unsafe_allow_html=True)
378
  st.markdown('<div class="sb-title">πŸ“‚ Clinical Documents</div>', unsafe_allow_html=True)
379
 
380
+ files = st.file_uploader(
381
+ "Upload Records",
382
+ accept_multiple_files=True,
383
+ type=["pdf", "docx", "txt"],
384
+ )
385
 
386
  sample_path = "src/sample_transcript.docx"
387
  if os.path.exists(sample_path):
388
  with open(sample_path, "rb") as f:
389
+ st.download_button(
390
+ "πŸ“₯ Sample Template",
391
+ f,
392
+ "Sample_Template.docx",
393
+ use_container_width=True,
394
+ )
395
  st.checkbox("πŸ“„ Use Sample", value=False, key="use_sample")
396
 
397
  st.markdown('</div>', unsafe_allow_html=True)
398
 
399
+ # Execute Analysis (final card)
400
  st.markdown('<div class="sb-card sb-execute">', unsafe_allow_html=True)
401
+ st.markdown(
402
+ '<div class="sb-title">πŸš€ Execute Analysis <span class="sb-arrow">β†’</span></div>',
403
+ unsafe_allow_html=True,
404
+ )
405
+ st.markdown(
406
+ '<div class="sb-subtitle">Ready? Hit Execute Below!</div>',
407
+ unsafe_allow_html=True,
408
+ )
409
 
410
  if st.button(
411
  "🩺 Generate SOAP Note",
 
414
  help="Combines voice + docs for full analysis",
415
  key="btn_generate_soap",
416
  ) and API_KEY:
 
417
  status_label = st.empty()
418
  progress_bar = st.progress(0)
419
 
 
427
 
428
  if st.session_state.get("use_voice") and "voice_transcript" in st.session_state:
429
  from langchain_core.documents import Document
430
+
431
  docs = process_docs_from_list(paths)
432
+ docs.append(
433
+ Document(
434
+ page_content=st.session_state.voice_transcript,
435
+ metadata={"source": "Voice Transcript"},
436
+ )
437
+ )
438
  else:
439
  docs = process_docs_from_list(paths)
440
 
 
451
  status_label.info("βœ… Auditing...")
452
  progress_bar.progress(90)
453
  st.session_state.eval = evaluate_clinical_output(
454
+ st.session_state.raw_text,
455
+ str(st.session_state.soap),
456
+ API_KEY,
457
  )
458
 
459
  progress_bar.progress(100)
 
463
 
464
  st.markdown('</div>', unsafe_allow_html=True)
465
 
 
466
  # --- 5. MAIN WORKSPACE ---
467
  if "soap" in st.session_state:
468
  col_l, col_r = st.columns([1.75, 1.25])
 
473
 
474
  for sec, data in st.session_state.soap.items():
475
  st.markdown(f'<div class="soap-header">{sec}</div>', unsafe_allow_html=True)
476
+ st.markdown(f'<div class="soap-content">{data['content']}</div>', unsafe_allow_html=True)
477
 
 
478
  with st.expander(f"πŸ” View Supporting Evidence for {sec}"):
479
  if data.get("evidence"):
480
  for i, snippet in enumerate(data["evidence"]):
 
481
  redacted_snippet = redact_phi(snippet)
482
+
 
483
  max_display_length = 300
484
  display_snippet = redacted_snippet[:max_display_length]
485
  if len(redacted_snippet) > max_display_length:
486
  display_snippet += "... [truncated]"
487
 
488
+ st.markdown(
489
+ f"""
490
+ <div style="background: #f8f9fa; padding: 12px; border-radius: 6px;
491
+ border-left: 3px solid #667eea; margin-bottom: 10px;">
492
+ <p style="color: #1e3c72; font-weight: 600; font-size: 0.9rem; margin: 0 0 8px 0;">
493
+ πŸ“„ Source Chunk {i+1}
494
+ </p>
495
+ <p style="color: #555; font-size: 0.95rem; line-height: 1.6; margin: 0;">
496
+ {display_snippet}
497
+ </p>
498
+ </div>
499
+ """,
500
+ unsafe_allow_html=True,
501
+ )
502
  else:
503
  st.info("No direct transcript evidence found for this section.")
504
 
505
  st.divider()
506
 
507
+ # Export Section Header
508
+ st.markdown(
509
+ '<div style="text-align: center; margin: 20px 0 15px 0;">'
510
+ '<h4 style="color: #1e3c72; font-weight: 700;">πŸ“₯ Export Clinical Note</h4>'
511
+ "</div>",
512
+ unsafe_allow_html=True,
513
+ )
514
 
515
  col1, col2 = st.columns(2)
516
  with col1:
517
+ st.markdown(
518
+ """
519
  <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
520
  padding: 15px; border-radius: 10px; text-align: center;
521
  box-shadow: 0 4px 12px rgba(102,126,234,0.3); margin-bottom: 10px;">
 
526
  Interoperable JSON for EHR systems
527
  </p>
528
  </div>
529
+ """,
530
+ unsafe_allow_html=True,
531
+ )
532
  st.download_button(
533
+ "⬇️ Download FHIR",
534
+ export_as_fhir(st.session_state.soap),
535
+ "clinical_note.json",
536
  use_container_width=True,
537
+ type="primary",
538
  )
539
 
540
  with col2:
541
+ st.markdown(
542
+ """
543
  <div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
544
  padding: 15px; border-radius: 10px; text-align: center;
545
  box-shadow: 0 4px 12px rgba(17,153,142,0.3); margin-bottom: 10px;">
 
550
  Human-readable documentation
551
  </p>
552
  </div>
553
+ """,
554
+ unsafe_allow_html=True,
555
+ )
556
  st.download_button(
557
+ "⬇️ Download Text",
558
+ str(st.session_state.soap),
559
+ "soap_note.txt",
560
  use_container_width=True,
561
+ type="secondary",
562
  )
563
 
564
  with col_r:
 
567
 
568
  for metric, details in st.session_state.eval.items():
569
  if isinstance(details, dict):
570
+ score = details.get("score", 0)
571
  color = "metric-good" if score >= 4 else "metric-bad"
572
 
573
+ st.markdown(
574
+ f'<div class="metric-card {color}">{metric}: {score}/5 ⭐</div>',
575
+ unsafe_allow_html=True,
576
+ )
577
+ st.markdown(
578
+ f'<div class="metric-reason">πŸ’‘ {details.get("reason", "Analysis verified.")}</div>',
579
+ unsafe_allow_html=True,
580
+ )
581
 
582
  if metric != list(st.session_state.eval.keys())[-1]:
583
  st.divider()
584
 
585
+ st.markdown("</div>", unsafe_allow_html=True)
586
 
587
  else:
588
+ st.markdown(
589
+ """
590
+ <div class="empty-state">
591
+ <h3>πŸ₯ Ready to Assist</h3>
592
+ <p>Upload clinical documents or record a consultation to begin automated SOAP note generation.</p>
593
+ <p style="font-size: 0.95rem; color: #95a5a6; margin-top: 15px;">
594
+ πŸ‘ˆ Use the sidebar to get started
595
+ </p>
596
+ </div>
597
+ """,
598
+ unsafe_allow_html=True,
599
+ )