Update src/streamlit_app.py
Browse files- 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
API_KEY = os.environ.get("OPENROUTER_API_KEY") or st.secrets.get("OPENROUTER_API_KEY")
|
| 18 |
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 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 |
-
""",
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
""",
|
|
|
|
|
|
|
| 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)
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
st.checkbox("π Use Sample", value=False, key="use_sample")
|
| 353 |
|
| 354 |
st.markdown('</div>', unsafe_allow_html=True)
|
| 355 |
|
| 356 |
-
# Execute Analysis (final
|
| 357 |
st.markdown('<div class="sb-card sb-execute">', unsafe_allow_html=True)
|
| 358 |
-
st.markdown(
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
|
|
|
|
|
|
| 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[
|
| 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(
|
| 437 |
-
|
| 438 |
-
<
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
else:
|
| 447 |
st.info("No direct transcript evidence found for this section.")
|
| 448 |
|
| 449 |
st.divider()
|
| 450 |
|
| 451 |
-
|
| 452 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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(
|
| 504 |
color = "metric-good" if score >= 4 else "metric-bad"
|
| 505 |
|
| 506 |
-
st.markdown(
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
|
| 509 |
if metric != list(st.session_state.eval.keys())[-1]:
|
| 510 |
st.divider()
|
| 511 |
|
| 512 |
-
st.markdown(
|
| 513 |
|
| 514 |
else:
|
| 515 |
-
st.markdown(
|
| 516 |
-
|
| 517 |
-
<
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
)
|