| import streamlit as st |
| import os, shutil, re |
| from rag_pipeline import * |
| import streamlit.components.v1 as components |
| from audiorecorder import audiorecorder |
| 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" |
|
|
| |
| st.set_page_config( |
| page_title="Clinical Intelligence Portal", |
| layout="wide", |
| initial_sidebar_state="expanded" |
| ) |
| |
| |
| |
| 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 |
|
|
| |
| components.html( |
| """ |
| <script> |
| const hideAudioErrors = () => { |
| const selectors = [ |
| '[data-testid="stNotification"]', |
| '[role="alert"]', |
| '[data-baseweb="notification"]', |
| '.stNotification', |
| 'div[class*="Notification"]', |
| 'div[class*="Toast"]' |
| ]; |
| selectors.forEach(selector => { |
| document.querySelectorAll(selector).forEach(el => { |
| const text = el.textContent || el.innerText || ''; |
| if (text.includes('error has occurred') || text.includes('please try again') || text.includes('An error')) { |
| el.style.display = 'none'; |
| setTimeout(() => el.remove(), 50); |
| } |
| }); |
| }); |
| }; |
| hideAudioErrors(); |
| setInterval(hideAudioErrors, 50); |
| const observer = new MutationObserver(hideAudioErrors); |
| observer.observe(document.body, { childList: true, subtree: true }); |
| window.addEventListener('message', hideAudioErrors); |
| </script> |
| """, |
| height=0, |
| ) |
|
|
| |
| st.markdown( |
| """ |
| <style> |
| :root { |
| --bg: #f4f6f9; |
| --ink: #1f2933; |
| --ink-soft: #5b6b7b; |
| --line: #e3e8ef; |
| --card: #ffffff; |
| --surface: #f7f9fc; |
| --brand: #1f6feb; |
| --brand-deep: #13447a; |
| --teal: #0f9d8f; |
| --amber: #c47f12; |
| --dx: #185fa5; |
| --dx-bg: #e6f1fb; |
| --px: #0f6e56; |
| --px-bg: #e1f5ee; |
| --shadow: 0 1px 3px rgba(16,24,40,.08), 0 1px 2px rgba(16,24,40,.05); |
| } |
| @media (prefers-color-scheme: dark) { |
| :root { |
| --bg: #0e141b; |
| --ink: #e6edf3; |
| --ink-soft: #9aa7b5; |
| --line: #283341; |
| --card: #161d26; |
| --surface: #1d2630; |
| --dx-bg: rgba(31,111,235,.18); |
| --px-bg: rgba(15,157,143,.18); |
| --shadow: 0 1px 3px rgba(0,0,0,.5), 0 1px 2px rgba(0,0,0,.4); |
| } |
| } |
| |
| .stApp { background: var(--bg); color: var(--ink); font-family: 'Inter','Segoe UI',sans-serif; } |
| |
| /* keep the audio widget's transient errors hidden */ |
| [data-testid="stAudioInput"] [data-testid="stNotification"], |
| [data-testid="stAudioInput"] [role="alert"], |
| [data-testid="stAudioInput"] div[class*="error"], |
| [data-testid="stAudioInput"] div[class*="Error"], |
| [data-testid="stAudioInput"] div[data-baseweb="notification"], |
| [data-testid="stAudioInput"] .stNotification, |
| div[data-baseweb="toast"], |
| div[class*="stToast"] { |
| display: none !important; visibility: hidden !important; height: 0 !important; |
| overflow: hidden !important; position: absolute !important; left: -9999px !important; |
| } |
| |
| /* Sidebar: dark clinical gradient (works in both themes) */ |
| [data-testid="stSidebar"] { |
| background: linear-gradient(180deg, #13447a 0%, #1c5aa8 100%); |
| min-width: 360px; max-width: 360px; padding: 22px 18px; |
| } |
| [data-testid="stSidebar"] * { color: #ffffff; } |
| .sb-card { |
| background: rgba(255,255,255,0.10); border: 1px solid rgba(255,255,255,0.16); |
| border-radius: 10px; padding: 14px; margin-bottom: 14px; |
| } |
| .sb-title { font-size: .82rem; font-weight: 700; letter-spacing:.4px; text-transform: uppercase; |
| display:flex; align-items:center; gap:8px; margin-bottom: 10px; color:#fff; } |
| .sb-num { width:20px; height:20px; border-radius:6px; background:rgba(255,255,255,.22); |
| display:inline-grid; place-items:center; font-size:.72rem; font-weight:800; } |
| .sb-note { font-size:.74rem; opacity:.85; } |
| |
| /* file uploader / inputs stay readable (white box) */ |
| [data-testid="stSidebar"] [data-testid="stFileUploaderDropzone"] { |
| background:#fff !important; border-radius:10px !important; border:1px solid #cbd5e0 !important; |
| } |
| [data-testid="stSidebar"] [data-testid="stFileUploaderDropzone"] * { color:#111827 !important; } |
| /* "Browse files" button: force a high-contrast style in EVERY theme |
| (otherwise it renders dark-on-dark and disappears in dark mode) */ |
| [data-testid="stSidebar"] [data-testid="stFileUploaderDropzone"] button { |
| background:#1c5aa8 !important; color:#ffffff !important; |
| border:1px solid #1c5aa8 !important; font-weight:600 !important; |
| opacity:1 !important; |
| } |
| [data-testid="stSidebar"] [data-testid="stFileUploaderDropzone"] button:hover { |
| background:#13447a !important; border-color:#13447a !important; |
| } |
| [data-testid="stSidebar"] input, [data-testid="stSidebar"] textarea { color:#1f2933 !important; } |
| [data-testid="stSidebar"] .stCheckbox label { color:#ffffff !important; } |
| |
| /* Banner */ |
| .banner-container { |
| background: linear-gradient(120deg, #13447a 0%, #1f6feb 100%); |
| border-radius: 14px; padding: 26px 30px; margin-bottom: 22px; box-shadow: var(--shadow); |
| } |
| .banner-container h1 { color:#fff; font-size:1.6rem; font-weight:700; margin:0; } |
| .banner-container p { color:#e8f0fe; font-size:.92rem; margin:4px 0 0; } |
| .banner-warn { color:#ffd86b !important; font-size:.8rem !important; margin-top:8px !important; } |
| |
| /* Cards */ |
| .clinical-card { |
| background: var(--card); border:1px solid var(--line); border-radius:12px; |
| padding:18px 20px; box-shadow:var(--shadow); margin-bottom:16px; |
| } |
| .section-title { color:var(--ink); font-weight:700; font-size:1.05rem; margin:0 0 4px; } |
| |
| /* Vitals strip */ |
| .vitals { display:flex; flex-wrap:wrap; gap:0; align-items:center; |
| background:var(--card); border:1px solid var(--line); border-radius:12px; |
| padding:10px 6px; margin-bottom:16px; box-shadow:var(--shadow); } |
| .vital { display:flex; flex-direction:column; padding:2px 16px; border-left:1px solid var(--line); } |
| .vital:first-child { border-left:none; } |
| .vital .lbl { font-size:.62rem; letter-spacing:.5px; text-transform:uppercase; color:var(--ink-soft); } |
| .vital .val { font-size:.95rem; font-weight:700; color:var(--ink); } |
| |
| /* SOAP cards */ |
| .soap-card { background:var(--card); border:1px solid var(--line); border-radius:12px; |
| box-shadow:var(--shadow); overflow:hidden; margin-bottom:14px; } |
| .soap-head { display:flex; align-items:center; gap:10px; padding:11px 16px; border-bottom:1px solid var(--line); } |
| .soap-tag { width:26px; height:26px; border-radius:7px; display:grid; place-items:center; |
| color:#fff; font-weight:800; font-size:.85rem; flex:none; } |
| .soap-ttl { font-weight:700; font-size:.8rem; letter-spacing:.6px; text-transform:uppercase; color:var(--ink); } |
| .soap-ttl small { display:block; font-weight:500; text-transform:none; letter-spacing:0; color:var(--ink-soft); font-size:.72rem; } |
| .soap-list { margin:0; padding:12px 16px 14px 30px; } |
| .soap-list li { margin-bottom:7px; font-size:.9rem; color:var(--ink); line-height:1.5; } |
| .soap-empty { color:var(--ink-soft); font-style:italic; padding:12px 16px; font-size:.86rem; margin:0; } |
| |
| /* Coding / superbill */ |
| .grouphdr { display:flex; align-items:center; gap:8px; font-size:.74rem; font-weight:700; |
| letter-spacing:.6px; text-transform:uppercase; color:var(--ink-soft); margin:6px 0 6px; } |
| .grouphdr .dot { width:9px; height:9px; border-radius:50%; } |
| .code-row { display:flex; align-items:flex-start; gap:12px; padding:10px 0; border-top:1px solid var(--line); } |
| .code-finding { flex:1; font-size:.9rem; font-weight:600; color:var(--ink); } |
| .code-finding small { display:block; font-weight:400; color:var(--ink-soft); font-size:.76rem; margin-top:2px; } |
| .code-pill { font-family:Consolas,monospace; font-weight:700; font-size:.82rem; padding:4px 10px; border-radius:7px; white-space:nowrap; } |
| .code-pill.dx { background:var(--dx-bg); color:var(--dx); } |
| .code-pill.px { background:var(--px-bg); color:var(--px); } |
| .code-alt { font-size:.74rem; color:var(--ink-soft); margin-top:3px; } |
| .code-none { font-size:.74rem; color:var(--amber); font-weight:600; } |
| .srcbadge { font-size:.66rem; font-weight:700; padding:3px 9px; border-radius:999px; |
| background:var(--surface); color:var(--ink-soft); border:1px solid var(--line); } |
| |
| /* Quality metrics */ |
| .metric-card { padding:14px; border-radius:10px; font-weight:600; text-align:center; |
| margin-bottom:6px; color:white; font-size:1rem; } |
| .metric-good { background: var(--teal); } |
| .metric-bad { background: #c0392b; } |
| .metric-reason { color:var(--ink-soft); font-size:.84rem; line-height:1.5; margin-bottom:14px; |
| padding:10px; background:var(--surface); border-radius:6px; border:1px solid var(--line); } |
| |
| /* Disclaimer */ |
| .disc { font-size:.76rem; color:#7a5b00; background:#fff8ec; border:1px solid #f1e2c4; |
| border-radius:8px; padding:10px 14px; margin-top:6px; } |
| |
| .empty-state { text-align:center; padding:60px 40px; background:var(--card); |
| border:2px dashed var(--line); border-radius:14px; margin-top:20px; box-shadow:var(--shadow); } |
| .empty-state h3 { color:var(--ink); font-size:1.6rem; margin-bottom:10px; } |
| .empty-state p { color:var(--ink-soft); font-size:1rem; } |
| |
| .step-grid { display:flex; flex-wrap:wrap; gap:12px; justify-content:center; margin-top:18px; } |
| .step { background:var(--surface); border:1px solid var(--line); border-radius:10px; |
| padding:16px 14px; width:150px; text-align:center; } |
| .step .circle { width:36px; height:36px; border-radius:50%; background:var(--brand); color:#fff; |
| font-weight:800; display:grid; place-items:center; margin:0 auto 8px; } |
| .step h4 { margin:0 0 4px; font-size:.9rem; color:var(--ink); } |
| .step p { margin:0; font-size:.76rem; color:var(--ink-soft); line-height:1.4; } |
| </style> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| |
| def bullets_to_html(content: str) -> str: |
| if not content: |
| return '<p class="soap-empty">Not documented in transcript.</p>' |
| 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 '<p class="soap-empty">Not documented in transcript.</p>' |
| if len(items) == 1 and "not documented" in items[0].lower(): |
| return f'<p class="soap-empty">{items[0]}</p>' |
| return '<ul class="soap-list">' + "".join(f"<li>{i}</li>" for i in items) + "</ul>" |
|
|
|
|
| |
| st.markdown( |
| """ |
| <div class="banner-container"> |
| <h1>π₯ Clinical Intelligence Portal</h1> |
| <p>AI-assisted SOAP note generation, coding suggestions & quality assurance</p> |
| <p class="banner-warn">β οΈ Cost-efficient AI prototype backed by a strict Human-in-the-Loop (HITL) clinical validation framework.</p> |
| </div> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| |
| with st.sidebar: |
| st.markdown( |
| '<h2 style="color:#fff; font-size:1.4rem; text-align:center; margin-bottom:14px;">Workflow</h2>', |
| unsafe_allow_html=True, |
| ) |
|
|
| |
| st.markdown('<div class="sb-card">', unsafe_allow_html=True) |
| st.markdown('<div class="sb-title"><span class="sb-num">1</span>π Access</div>', 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('</div>', unsafe_allow_html=True) |
|
|
| |
| st.markdown('<div class="sb-card">', unsafe_allow_html=True) |
| st.markdown('<div class="sb-title"><span class="sb-num">2</span>π€ Record voice <span style="margin-left:auto;font-weight:400;text-transform:none;opacity:.7">optional</span></div>', 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('</div>', unsafe_allow_html=True) |
|
|
| |
| st.markdown('<div class="sb-card">', unsafe_allow_html=True) |
| st.markdown('<div class="sb-title"><span class="sb-num">3</span>π Add records</div>', unsafe_allow_html=True) |
| st.markdown('<div class="sb-note" style="margin-bottom:8px;">Meeting transcripts / clinical documents / any supporting artifact (PDF, DOCX, TXT)</div>', 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('</div>', unsafe_allow_html=True) |
|
|
| |
| st.markdown('<div class="sb-card">', unsafe_allow_html=True) |
| st.markdown('<div class="sb-title"><span class="sb-num">4</span>π Generate</div>', 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) |
| |
| |
| |
| 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('</div>', unsafe_allow_html=True) |
|
|
| |
| 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: |
| |
| vitals = st.session_state.get("vitals") or {} |
| if vitals: |
| st.markdown('<div class="section-title" style="margin-bottom:8px;">π©Ί Key Vitals</div>', |
| unsafe_allow_html=True) |
| chips = "".join( |
| f'<div class="vital"><span class="lbl">{k}</span><span class="val">{v}</span></div>' |
| for k, v in vitals.items() |
| ) |
| st.markdown(f'<div class="vitals">{chips}</div>', unsafe_allow_html=True) |
|
|
| col_l, col_r = st.columns([1.7, 1.3]) |
|
|
| with col_l: |
| st.markdown('<div class="section-title">π©Ί Clinical SOAP Note</div>', 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'<div class="soap-card">' |
| f'<div class="soap-head"><div class="soap-tag" style="background:{color}">{letter}</div>' |
| f'<div class="soap-ttl">{sec.title()}<small>{sub}</small></div></div>' |
| f'{body}</div>', |
| 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'<div style="background:var(--surface); padding:10px; border-radius:6px; ' |
| f'border-left:3px solid var(--brand); margin-bottom:8px; font-size:.82rem; color:var(--ink-soft);">' |
| f'<b style="color:var(--brand);">Source {i+1}</b><br>{redacted}{"..." if len(snippet) > 300 else ""}</div>', |
| unsafe_allow_html=True, |
| ) |
| else: |
| st.info("No direct transcript evidence found.") |
|
|
| |
| codes = st.session_state.get("codes") or {"diagnoses": [], "procedures": []} |
| st.markdown('<div class="clinical-card">', unsafe_allow_html=True) |
| st.markdown( |
| '<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px;">' |
| '<div class="section-title" style="margin:0;">π§Ύ Coding Suggestions β Superbill</div>' |
| '<div><span class="srcbadge">ICD-10-CM Β· NLM</span> <span class="srcbadge">HCPCS Β· CMS</span></div></div>', |
| 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'<div class="grouphdr"><span class="dot" style="background:{dot}"></span>{label}</div>' |
| if not entries: |
| html += '<div class="code-alt" style="padding:6px 0;">No items extracted.</div>' |
| 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'<div class="code-alt">Alternatives: {alts}</div>' if alts else "" |
| code_html = (f'<div style="text-align:right;"><span class="code-pill {cls}">{top["code"]}</span>' |
| f'<div class="code-alt">{top["name"]}</div>{alt_html}</div>') |
| else: |
| code_html = '<div style="text-align:right;"><span class="code-none">no match β code manually</span></div>' |
| html += (f'<div class="code-row"><div class="code-finding">{e.get("finding","")}</div>{code_html}</div>') |
| 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'<div class="disc">β οΈ 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.</div>', |
| unsafe_allow_html=True, |
| ) |
| st.download_button( |
| "β¬οΈ Export superbill (CSV)", |
| build_superbill_csv(codes), |
| "superbill.csv", |
| use_container_width=True, |
| ) |
| st.markdown('</div>', unsafe_allow_html=True) |
|
|
| |
| st.markdown('<div class="section-title">π₯ Export Clinical Note</div>', 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('<div class="section-title">π Quality Audit Report</div>', 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'<div class="metric-card {color}">{metric}: {score}/5 β</div>', unsafe_allow_html=True) |
| st.markdown(f'<div class="metric-reason">π‘ {details.get("reason", "Analysis verified.")}</div>', |
| unsafe_allow_html=True) |
|
|
| else: |
| st.markdown( |
| """ |
| <div class="empty-state"> |
| <h3>π₯ Ready to assist</h3> |
| <p>Follow the numbered steps in the sidebar to generate a SOAP note.</p> |
| <div class="step-grid"> |
| <div class="step"><div class="circle">1</div><h4>π Sign in</h4><p>API key auto detected (this is a prototype)</p></div> |
| <div class="step"><div class="circle">2</div><h4>π€ Record / upload</h4><p>Mic or drag a file</p></div> |
| <div class="step"><div class="circle">3</div><h4>π Confirm source</h4><p>File, voice, or sample</p></div> |
| <div class="step"><div class="circle">4</div><h4>π©Ί Generate</h4><p>Note appears here</p></div> |
| </div> |
| </div> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|