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 '" # --- 3. HEADER BANNER --- st.markdown( """ """, 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, )