import streamlit as st
import os, shutil, re
from rag_pipeline import *
import streamlit.components.v1 as components
from audiorecorder import audiorecorder # NEW
from audio_recorder_streamlit import audio_recorder
from pydub import AudioSegment
AudioSegment.ffmpeg = shutil.which("ffmpeg") or "ffmpeg"
AudioSegment.ffprobe = shutil.which("ffprobe") or "ffprobe"
# --- 0. PAGE CONFIG ---
st.set_page_config(
page_title="Clinical Intelligence Portal",
layout="wide",
initial_sidebar_state="expanded"
)
# Accept either a Groq key (preferred free provider) or an OpenRouter key.
# Having any one of these set enables the app; the LLM client (get_openrouter_llm)
# picks Groq automatically when GROQ_API_KEY is present.
API_KEY = os.environ.get("GROQ_API_KEY") or os.environ.get("OPENROUTER_API_KEY")
if not API_KEY:
try:
API_KEY = st.secrets["OPENROUTER_API_KEY"]
except Exception:
API_KEY = None # no secret configured; user can enter the key in the sidebar
# --- 1. JAVASCRIPT ERROR SUPPRESSOR (audio widget noise) ---
components.html(
"""
""",
height=0,
)
# --- 2. THEME-ADAPTIVE CSS (follows Chrome light/dark automatically) ---
st.markdown(
"""
""",
unsafe_allow_html=True,
)
# --- Helper: render SOAP content as a clean bullet list (fixes alignment) ---
def bullets_to_html(content: str) -> str:
if not content:
return '
Not documented in transcript.
'
lines = [l.strip() for l in content.splitlines() if l.strip()]
items = []
for l in lines:
l = re.sub(r'^[\-\*โขโข]\s*', '', l)
l = re.sub(r'^\d+[\.\)]\s*', '', l)
if l:
items.append(l)
if not items:
return 'Not documented in transcript.
'
if len(items) == 1 and "not documented" in items[0].lower():
return f'{items[0]}
'
return '' + "".join(f"- {i}
" for i in items) + "
"
# --- 3. HEADER BANNER ---
st.markdown(
"""
๐ฅ Clinical Intelligence Portal
AI-assisted SOAP note generation, coding suggestions & quality assurance
โ ๏ธ Cost-efficient AI prototype backed by a strict Human-in-the-Loop (HITL) clinical validation framework.
""",
unsafe_allow_html=True,
)
# --- 4. SIDEBAR WORKFLOW ---
with st.sidebar:
st.markdown(
'Workflow
',
unsafe_allow_html=True,
)
# 1 โ Access
st.markdown('', unsafe_allow_html=True)
st.markdown('
1๐ Access
', unsafe_allow_html=True)
if API_KEY:
st.success("โ API key auto detected (prototype)")
else:
API_KEY = st.text_input("Enter API Key", type="password", help="OpenRouter or similar API key")
st.markdown('
', unsafe_allow_html=True)
# 2 โ Voice
st.markdown('', unsafe_allow_html=True)
st.markdown('
2๐ค Record voice optional
', unsafe_allow_html=True)
audio_bytes = audio_recorder(
text="",
recording_color="#e8453c",
neutral_color="#6aa36f",
icon_name="microphone",
icon_size="3x",
energy_threshold=(-1.0, 1.0),
pause_threshold=300.0,
key="voice_recorder_main",
)
audio_data = None
if audio_bytes:
audio_data = audio_bytes
st.audio(audio_data, format="audio/wav")
if API_KEY:
status_placeholder = st.empty()
if st.button("๐ Transcribe Voice", use_container_width=True, key="btn_transcribe_voice"):
if audio_data is None:
status_placeholder.warning("โ ๏ธ Record audio first.")
else:
with st.spinner("๐ Transcribing..."):
try:
st.session_state.voice_transcript = transcribe_and_tag(audio_data, API_KEY)
status_placeholder.success("โ Done!")
except Exception as e:
status_placeholder.error(f"โ {str(e)}")
if "voice_transcript" in st.session_state:
st.text_area("๐ Preview", st.session_state.voice_transcript, height=100, disabled=True)
st.download_button("๐พ Download", st.session_state.voice_transcript, "transcript.txt", use_container_width=True)
st.checkbox("โ Use Voice for Analysis", value=True, key="use_voice")
st.markdown('
', unsafe_allow_html=True)
# 3 โ Documents
st.markdown('', unsafe_allow_html=True)
st.markdown('
3๐ Add records
', unsafe_allow_html=True)
st.markdown('
Meeting transcripts / clinical documents / any supporting artifact (PDF, DOCX, TXT)
', unsafe_allow_html=True)
files = st.file_uploader(
"Upload records",
accept_multiple_files=True,
type=["pdf", "docx", "txt"],
label_visibility="collapsed",
)
sample_path = "src/sample_transcript.pdf"
if os.path.exists(sample_path):
with open(sample_path, "rb") as f:
st.download_button("๐ฅ Download Sample Transcript", f, "sample_template.pdf", use_container_width=True)
st.checkbox("๐ Use built-in sample transcript", value=False, key="use_sample")
st.markdown('
', unsafe_allow_html=True)
# 4 โ Generate
st.markdown('', unsafe_allow_html=True)
st.markdown('
4๐ Generate
', unsafe_allow_html=True)
if st.button("๐ฉบ Generate SOAP Note", use_container_width=True, type="primary",
help="Combines voice + documents for full analysis", key="btn_generate_soap") and API_KEY:
status_label = st.empty()
progress_bar = st.progress(0)
paths = [sample_path] if st.session_state.get("use_sample") else []
if files:
for f in files:
fp = f"/tmp/{f.name}"
with open(fp, "wb") as b:
b.write(f.getbuffer())
paths.append(fp)
if st.session_state.get("use_voice") and "voice_transcript" in st.session_state:
from langchain_core.documents import Document
docs = process_docs_from_list(paths)
docs.append(Document(page_content=st.session_state.voice_transcript, metadata={"source": "Voice Transcript"}))
else:
docs = process_docs_from_list(paths)
if docs:
status_label.info("๐ Indexing...")
progress_bar.progress(30)
vdb = build_semantic_index(docs)
status_label.info("โ๏ธ Generating SOAP...")
progress_bar.progress(55)
st.session_state.raw_text = "\n".join([d.page_content for d in docs])
st.session_state.soap = generate_rag_soap(vdb, API_KEY, docs)
# Capture vitals from BOTH the raw transcript and the generated
# SOAP note, so every available vital (BP, HR, Temp, SpO2, RR) is
# picked up even if it only appears in the structured note.
st.session_state.vitals = extract_vitals(
f"{st.session_state.raw_text}\n{st.session_state.soap}"
)
status_label.info("๐งพ Suggesting codes...")
progress_bar.progress(75)
st.session_state.codes = generate_codes(st.session_state.soap, API_KEY)
status_label.info("โ
Auditing...")
progress_bar.progress(92)
st.session_state.eval = evaluate_clinical_output(
st.session_state.raw_text, str(st.session_state.soap), API_KEY)
progress_bar.progress(100)
st.success("โ Complete!")
else:
st.warning("Upload docs or record voice first.")
st.markdown('
', unsafe_allow_html=True)
# --- 5. MAIN WORKSPACE ---
SOAP_META = {
"SUBJECTIVE": ("S", "#1f6feb", "What the patient reports"),
"OBJECTIVE": ("O", "#0f9d8f", "Exam & measured findings"),
"ASSESSMENT": ("A", "#c47f12", "Clinical impression"),
"PLAN": ("P", "#13447a", "Next steps & orders"),
}
if "soap" in st.session_state:
# Key Vitals strip (best-effort). Shown whenever at least one vital is found.
vitals = st.session_state.get("vitals") or {}
if vitals:
st.markdown('๐ฉบ Key Vitals
',
unsafe_allow_html=True)
chips = "".join(
f'{k}{v}
'
for k, v in vitals.items()
)
st.markdown(f'{chips}
', unsafe_allow_html=True)
col_l, col_r = st.columns([1.7, 1.3])
with col_l:
st.markdown('๐ฉบ Clinical SOAP Note
', unsafe_allow_html=True)
items = list(st.session_state.soap.items())
rows = [st.columns(2), st.columns(2)]
cells = [rows[0][0], rows[0][1], rows[1][0], rows[1][1]]
for cell, (sec, data) in zip(cells, items):
letter, color, sub = SOAP_META.get(sec, (sec[:1], "#1f6feb", ""))
body = bullets_to_html(data.get("content", ""))
with cell:
st.markdown(
f''
f'
{letter}
'
f'
{sec.title()}{sub}
'
f'{body}
',
unsafe_allow_html=True,
)
with st.expander(f"๐ Evidence โ {sec.title()}"):
if data.get("evidence"):
for i, snippet in enumerate(data["evidence"]):
redacted = redact_phi(snippet)[:300]
st.markdown(
f''
f'Source {i+1}
{redacted}{"..." if len(snippet) > 300 else ""}
',
unsafe_allow_html=True,
)
else:
st.info("No direct transcript evidence found.")
# --- Coding / Superbill panel ---
codes = st.session_state.get("codes") or {"diagnoses": [], "procedures": []}
st.markdown('', unsafe_allow_html=True)
st.markdown(
'
'
'
๐งพ Coding Suggestions โ Superbill
'
'
ICD-10-CM ยท NLM HCPCS ยท CMS
',
unsafe_allow_html=True,
)
def render_code_group(entries, kind):
cls = "dx" if kind == "dx" else "px"
dot = "#185fa5" if kind == "dx" else "#0f6e56"
label = "Diagnoses (from Assessment)" if kind == "dx" else "Procedures & services (from Plan)"
html = f'
{label}
'
if not entries:
html += '
No items extracted.
'
for e in entries:
cands = e.get("candidates") or []
if cands:
top = cands[0]
alts = ", ".join(f"{c['code']}" for c in cands[1:]) if len(cands) > 1 else ""
alt_html = f'
Alternatives: {alts}
' if alts else ""
code_html = (f'
{top["code"]}'
f'
{top["name"]}
{alt_html}
')
else:
code_html = '
no match โ code manually
'
html += (f'
{e.get("finding","")}
{code_html}
')
return html
st.markdown(render_code_group(codes.get("diagnoses", []), "dx"), unsafe_allow_html=True)
st.markdown(render_code_group(codes.get("procedures", []), "px"), unsafe_allow_html=True)
st.markdown(
f'
โ ๏ธ AI-suggested codes for clinician review only. Final code selection '
f'and billing responsibility rest with the provider. CPT excluded (AMA license required); '
f'procedures use free HCPCS Level II.
',
unsafe_allow_html=True,
)
st.download_button(
"โฌ๏ธ Export superbill (CSV)",
build_superbill_csv(codes),
"superbill.csv",
use_container_width=True,
)
st.markdown('
', unsafe_allow_html=True)
# --- Exports ---
st.markdown('๐ฅ Export Clinical Note
', unsafe_allow_html=True)
c1, c2 = st.columns(2)
with c1:
st.download_button("โฌ๏ธ Download FHIR (JSON)", export_as_fhir(st.session_state.soap),
"clinical_note.json", use_container_width=True, type="primary")
with c2:
st.download_button("โฌ๏ธ Download Text", str(st.session_state.soap),
"soap_note.txt", use_container_width=True)
with col_r:
st.markdown('๐ Quality Audit Report
', unsafe_allow_html=True)
for metric, details in st.session_state.get("eval", {}).items():
if isinstance(details, dict):
score = details.get("score", 0)
color = "metric-good" if score >= 4 else "metric-bad"
st.markdown(f'{metric}: {score}/5 โญ
', unsafe_allow_html=True)
st.markdown(f'๐ก {details.get("reason", "Analysis verified.")}
',
unsafe_allow_html=True)
else:
st.markdown(
"""
๐ฅ Ready to assist
Follow the numbered steps in the sidebar to generate a SOAP note.
1
๐ Sign in
API key auto detected (this is a prototype)
2
๐ค Record / upload
Mic or drag a file
3
๐ Confirm source
File, voice, or sample
4
๐ฉบ Generate
Note appears here
""",
unsafe_allow_html=True,
)