Update pages/02_Workflow_UI.py
Browse files- pages/02_Workflow_UI.py +99 -99
pages/02_Workflow_UI.py
CHANGED
|
@@ -2,14 +2,15 @@
|
|
| 2 |
# -----------------------------------------------------------------------------
|
| 3 |
# Phase-3+ Simplified β 3-tab workflow
|
| 4 |
# Tabs:
|
| 5 |
-
# 1) PCP Referral
|
| 6 |
-
# 2) Specialist Review (SOAP + Guideline Rationale +
|
| 7 |
-
# 3) Documentation & Billing (
|
| 8 |
#
|
| 9 |
-
#
|
| 10 |
-
#
|
| 11 |
-
#
|
| 12 |
-
#
|
|
|
|
| 13 |
# -----------------------------------------------------------------------------
|
| 14 |
|
| 15 |
from __future__ import annotations
|
|
@@ -19,6 +20,7 @@ import shutil
|
|
| 19 |
import html
|
| 20 |
from pathlib import Path
|
| 21 |
from typing import Any, Dict, List, Optional
|
|
|
|
| 22 |
|
| 23 |
import streamlit as st
|
| 24 |
|
|
@@ -30,7 +32,6 @@ from src.explainability import text_hash, normalize_text, is_stale
|
|
| 30 |
from src.guideline_annotator import generate_guideline_rationale
|
| 31 |
from src.ai_core import generate_soap_draft
|
| 32 |
from src.model_loader import active_model_status
|
| 33 |
-
from datetime import datetime
|
| 34 |
|
| 35 |
|
| 36 |
# --------------------------- Page Setup ---------------------------------
|
|
@@ -133,10 +134,9 @@ def _latest_export(case_id: str, pattern_suffix: str) -> Optional[Path]:
|
|
| 133 |
matches = sorted(exp.glob(f"EC-{case_id}_*{pattern_suffix}"), key=lambda p: p.stat().st_mtime)
|
| 134 |
return matches[-1] if matches else None
|
| 135 |
|
| 136 |
-
|
| 137 |
def _note_markdown(case: Dict[str, Any], summary: str, soap: Dict[str, str],
|
| 138 |
guideline_points: List[str], endnotes: List[Dict[str, Any]]) -> str:
|
| 139 |
-
"""Generate formatted EHR-style consultation report (matches sample format)."""
|
| 140 |
p = case.get("patient", {}) or {}
|
| 141 |
c = case.get("consult", {}) or {}
|
| 142 |
consult_id = case.get("case_id", "")
|
|
@@ -191,6 +191,23 @@ def _note_markdown(case: Dict[str, Any], summary: str, soap: Dict[str, str],
|
|
| 191 |
|
| 192 |
return header + referral_section + soap_section + guideline_section + endnote_section + attestation
|
| 193 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
|
| 196 |
# --------------------------- Global callouts --------------------------------
|
|
@@ -243,15 +260,15 @@ if st.button("β New Case", use_container_width=True):
|
|
| 243 |
st.success(f"Created new case {new_id}. Refreshing listβ¦")
|
| 244 |
st.rerun()
|
| 245 |
|
| 246 |
-
# Reset demo
|
| 247 |
if st.button("ποΈ Reset Demo (clear all cases)", use_container_width=True):
|
| 248 |
try:
|
| 249 |
-
|
| 250 |
-
st.success("β
All demo cases cleared
|
| 251 |
-
st.session_state["_seeded"] =
|
| 252 |
st.rerun()
|
| 253 |
except Exception as e:
|
| 254 |
-
st.error(f"Failed to
|
| 255 |
|
| 256 |
# Selection
|
| 257 |
default_case_id = st.session_state.get("_current_case_id") or items[0]["case_id"]
|
|
@@ -335,23 +352,13 @@ with tab_spec:
|
|
| 335 |
st.info("Submit the referral on the PCP tab to activate Specialist Review.")
|
| 336 |
st.stop()
|
| 337 |
|
| 338 |
-
# Prevent guideline rationale or re-render during immediate SOAP rerun
|
| 339 |
-
if st.session_state.pop("_suppress_guidelines", False):
|
| 340 |
-
st.stop()
|
| 341 |
-
|
| 342 |
-
|
| 343 |
read_only = status == "completed"
|
| 344 |
|
| 345 |
st.subheader("SOAP Draft")
|
|
|
|
| 346 |
|
| 347 |
-
# Cache SOAP in session to avoid stale disk reads
|
| 348 |
-
st.session_state[f"soap_{selected}"] = case.get("soap_draft", {})
|
| 349 |
-
soap = st.session_state[f"soap_{selected}"]
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
# Generate SOAP if needed
|
| 354 |
-
gen_needed = (status == "submitted") and (
|
| 355 |
if gen_needed and not read_only:
|
| 356 |
if st.button("Generate SOAP Draft (LLM)", key=f"gen_{selected}"):
|
| 357 |
try:
|
|
@@ -377,14 +384,12 @@ with tab_spec:
|
|
| 377 |
st.warning(f"LLM generation unavailable; seeded a minimal draft. ({type(e).__name__})")
|
| 378 |
result = {"error": str(e)}
|
| 379 |
|
| 380 |
-
# Persist and force rerun so text areas populate instantly
|
| 381 |
store.update_case(selected, {
|
| 382 |
"soap_draft": {"subjective": subj.strip(), "objective": obj.strip(), "assessment": assess.strip(), "plan": plan.strip()},
|
| 383 |
"explainability": explain_patch,
|
| 384 |
})
|
| 385 |
-
#
|
| 386 |
-
st.
|
| 387 |
-
st.rerun()
|
| 388 |
|
| 389 |
# Editable fields
|
| 390 |
c1, c2 = st.columns(2)
|
|
@@ -395,7 +400,7 @@ with tab_spec:
|
|
| 395 |
assess_new = st.text_area("Assessment", value=soap.get("assessment",""), height=140, disabled=read_only, key=f"assess_{selected}")
|
| 396 |
plan_new = st.text_area("Plan", value=soap.get("plan",""), height=180, disabled=read_only, key=f"plan_{selected}")
|
| 397 |
|
| 398 |
-
# Persist edits (whitespace-insensitive)
|
| 399 |
if (not read_only) and (status == "submitted"):
|
| 400 |
changed = any([
|
| 401 |
normalize_text(subj_new) != normalize_text(soap.get("subjective") or ""),
|
|
@@ -416,51 +421,44 @@ with tab_spec:
|
|
| 416 |
|
| 417 |
stale = is_stale(assess_new, assess_hash0) or is_stale(plan_new, plan_hash0)
|
| 418 |
|
| 419 |
-
# Guard:
|
| 420 |
-
if not
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
# Use stored bullets if available; else compute and persist once
|
| 428 |
-
stored_points = (exp.get("guideline_rationale") or [])
|
| 429 |
-
if stored_points:
|
| 430 |
-
_render_guideline_bullets(stored_points)
|
| 431 |
-
else:
|
| 432 |
-
g = generate_guideline_rationale(assessment_text=assess_new, plan_text=plan_new)
|
| 433 |
-
warn = (g.get("warning") or "").strip()
|
| 434 |
-
if warn:
|
| 435 |
-
st.info(warn)
|
| 436 |
-
try:
|
| 437 |
-
st.page_link("pages/01_RAG_Corpus_Prep.py", label="Open RAG Prep")
|
| 438 |
-
except Exception:
|
| 439 |
-
pass
|
| 440 |
-
points = g.get("rationale", []) or []
|
| 441 |
-
_render_guideline_bullets(points)
|
| 442 |
-
if points:
|
| 443 |
-
# Persist once so it survives navigation
|
| 444 |
-
store.update_case(selected, {"explainability": {"guideline_rationale": points}})
|
| 445 |
-
else:
|
| 446 |
-
# Hide until re-run; allow refresh which sets new baselines and persists bullets
|
| 447 |
-
if (not read_only) and (status == "submitted"):
|
| 448 |
-
if st.button("β» Re-run Guidelines", key=f"rerun_{selected}"):
|
| 449 |
g = generate_guideline_rationale(assessment_text=assess_new, plan_text=plan_new)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
points = g.get("rationale", []) or []
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
})
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
|
| 461 |
st.divider()
|
| 462 |
|
| 463 |
-
#
|
| 464 |
st.subheader("Specialist Review")
|
| 465 |
b = case.get("billing", {}) or {}
|
| 466 |
minutes_val = int(b.get("minutes", 5))
|
|
@@ -470,7 +468,7 @@ with tab_spec:
|
|
| 470 |
attested = bool(b.get("attested", False)) if read_only else False
|
| 471 |
attested = st.checkbox("I attest that the consult note is complete.", value=attested, disabled=read_only, key=f"att_{selected}")
|
| 472 |
|
| 473 |
-
# Persist time/spoke as they change (
|
| 474 |
if (not read_only) and (status == "submitted"):
|
| 475 |
if (minutes != minutes_val) or (spoke != bool(b.get("spoke", False))):
|
| 476 |
store.update_case(selected, {"billing": {"minutes": minutes, "spoke": bool(spoke)}})
|
|
@@ -480,20 +478,19 @@ with tab_spec:
|
|
| 480 |
finalize_btn = st.button("Finalize Consult", type="primary", disabled=not can_finalize, key=f"finalize_{selected}")
|
| 481 |
|
| 482 |
if finalize_btn:
|
| 483 |
-
#
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
endnotes = g.get("endnotes", []) or []
|
| 487 |
|
| 488 |
soap_now = case.get("soap_draft", {}) or {}
|
| 489 |
note_md = _note_markdown(
|
| 490 |
case,
|
| 491 |
build_referral_summary(case),
|
| 492 |
{
|
| 493 |
-
"subjective":
|
| 494 |
-
"objective":
|
| 495 |
-
"assessment":
|
| 496 |
-
"plan":
|
| 497 |
},
|
| 498 |
points, endnotes=endnotes
|
| 499 |
)
|
|
@@ -502,14 +499,15 @@ with tab_spec:
|
|
| 502 |
note_path = config.make_export_path(selected, "Sample Consultation Report.md")
|
| 503 |
Path(note_path).write_text(note_md, encoding="utf-8")
|
| 504 |
|
| 505 |
-
# Persist
|
| 506 |
-
store.update_case(selected, {
|
| 507 |
-
|
|
|
|
|
|
|
| 508 |
|
| 509 |
st.session_state["_show_exports"] = True
|
| 510 |
-
st.success("Finalized. Consult note exported; proceed to the **Documentation & Billing** tab to submit a claim.")
|
| 511 |
st.caption(f"Note: {note_path}")
|
| 512 |
-
# Optional: st.rerun() here would reset tab focus; we leave it so user can jump tabs manually.
|
| 513 |
|
| 514 |
|
| 515 |
# ===== DOCUMENTATION & BILLING TAB =====
|
|
@@ -525,7 +523,7 @@ with tab_bill:
|
|
| 525 |
if note_path and note_path.exists():
|
| 526 |
with st.expander("π View EHR Consultation Note", expanded=True):
|
| 527 |
md = note_path.read_text(encoding="utf-8")
|
| 528 |
-
st.markdown(md)
|
| 529 |
st.caption(f"File: {note_path.name}")
|
| 530 |
else:
|
| 531 |
st.warning("No consultation note found for this case. Finalize the consult to generate one.")
|
|
@@ -548,11 +546,9 @@ with tab_bill:
|
|
| 548 |
st.write(f"{icon} **{s.code}** β {s.descriptor} β ${s.rate:.2f}")
|
| 549 |
st.caption(s.why)
|
| 550 |
|
| 551 |
-
#
|
| 552 |
-
from src.billing import autosuggest_icd # top of file already imports billing; safe to import here too
|
| 553 |
-
|
| 554 |
soap_now = case.get("soap_draft", {}) or {}
|
| 555 |
-
icd_suggestions = autosuggest_icd(
|
| 556 |
assessment_text=soap_now.get("assessment", ""),
|
| 557 |
plan_text=soap_now.get("plan", "")
|
| 558 |
)
|
|
@@ -561,7 +557,6 @@ with tab_bill:
|
|
| 561 |
for s in icd_suggestions:
|
| 562 |
st.write(f"β
**{s.code}** β {s.description} (confidence {int(s.confidence*100)}%)")
|
| 563 |
st.caption(s.why)
|
| 564 |
-
# Allow user to choose 1β3 codes
|
| 565 |
icd_options = [f"{s.code} β {s.description}" for s in icd_suggestions]
|
| 566 |
picked_icd = st.multiselect(
|
| 567 |
"Select diagnosis code(s) to include on the claim",
|
|
@@ -572,12 +567,16 @@ with tab_bill:
|
|
| 572 |
picked_icd_codes = [o.split(" β ")[0] for o in picked_icd]
|
| 573 |
else:
|
| 574 |
st.info("No ICD-10 suggestions available for this case.")
|
| 575 |
-
picked_icd_codes = []
|
| 576 |
|
| 577 |
-
|
| 578 |
chosen_default = b.get("cpt_code") or (elig_codes[0] if elig_codes else None)
|
| 579 |
if elig_codes:
|
| 580 |
-
chosen = st.selectbox(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
else:
|
| 582 |
st.warning("No eligible CPT for the current minutes/spoke combination.", icon="β οΈ")
|
| 583 |
chosen = None
|
|
@@ -591,10 +590,10 @@ with tab_bill:
|
|
| 591 |
store.update_case(selected, {
|
| 592 |
"billing": {
|
| 593 |
"cpt_code": chosen,
|
| 594 |
-
"icd_codes": picked_icd_codes,
|
| 595 |
}
|
| 596 |
})
|
| 597 |
-
|
| 598 |
# Build and save 837 JSON (includes ICD codes)
|
| 599 |
pick = next((s for s in suggestions if s.code == chosen), None)
|
| 600 |
rate = float(getattr(pick, "rate", 0.0)) if pick else 0.0
|
|
@@ -605,11 +604,11 @@ with tab_bill:
|
|
| 605 |
minutes=minutes,
|
| 606 |
spoke=bool(spoke),
|
| 607 |
attested=True,
|
| 608 |
-
icd_codes=picked_icd_codes,
|
| 609 |
)
|
| 610 |
claim_path = config.make_export_path(selected, "Sample 837 Json.json")
|
| 611 |
Path(claim_path).write_text(json.dumps(claim, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 612 |
-
|
| 613 |
st.success("Claim submitted. 837 JSON generated (see preview below).")
|
| 614 |
st.caption(f"Claim: {claim_path.name}")
|
| 615 |
|
|
@@ -637,3 +636,4 @@ with tab_bill:
|
|
| 637 |
|
| 638 |
|
| 639 |
|
|
|
|
|
|
| 2 |
# -----------------------------------------------------------------------------
|
| 3 |
# Phase-3+ Simplified β 3-tab workflow
|
| 4 |
# Tabs:
|
| 5 |
+
# 1) PCP Referral
|
| 6 |
+
# 2) Specialist Review (SOAP + Guideline Rationale + time/spoke/attestation)
|
| 7 |
+
# 3) Documentation & Billing (completed cases only: EHR note, CPT + ICD, 837 JSON)
|
| 8 |
#
|
| 9 |
+
# Assumes:
|
| 10 |
+
# - No Referral Rationale
|
| 11 |
+
# - Single Guideline Rationale layer (bullets) via guideline_annotator
|
| 12 |
+
# - ai_core.generate_soap_draft returns 4-string SOAP keys
|
| 13 |
+
# - billing.autosuggest_cpt + billing.autosuggest_icd + billing.build_837_claim
|
| 14 |
# -----------------------------------------------------------------------------
|
| 15 |
|
| 16 |
from __future__ import annotations
|
|
|
|
| 20 |
import html
|
| 21 |
from pathlib import Path
|
| 22 |
from typing import Any, Dict, List, Optional
|
| 23 |
+
from datetime import datetime
|
| 24 |
|
| 25 |
import streamlit as st
|
| 26 |
|
|
|
|
| 32 |
from src.guideline_annotator import generate_guideline_rationale
|
| 33 |
from src.ai_core import generate_soap_draft
|
| 34 |
from src.model_loader import active_model_status
|
|
|
|
| 35 |
|
| 36 |
|
| 37 |
# --------------------------- Page Setup ---------------------------------
|
|
|
|
| 134 |
matches = sorted(exp.glob(f"EC-{case_id}_*{pattern_suffix}"), key=lambda p: p.stat().st_mtime)
|
| 135 |
return matches[-1] if matches else None
|
| 136 |
|
|
|
|
| 137 |
def _note_markdown(case: Dict[str, Any], summary: str, soap: Dict[str, str],
|
| 138 |
guideline_points: List[str], endnotes: List[Dict[str, Any]]) -> str:
|
| 139 |
+
"""Generate formatted EHR-style consultation report (matches sample-like format)."""
|
| 140 |
p = case.get("patient", {}) or {}
|
| 141 |
c = case.get("consult", {}) or {}
|
| 142 |
consult_id = case.get("case_id", "")
|
|
|
|
| 191 |
|
| 192 |
return header + referral_section + soap_section + guideline_section + endnote_section + attestation
|
| 193 |
|
| 194 |
+
def _soap_is_empty(soap: Dict[str, str]) -> bool:
|
| 195 |
+
return not any((soap or {}).get(k, "").strip() for k in ("subjective", "objective", "assessment", "plan"))
|
| 196 |
+
|
| 197 |
+
def _seed_once() -> None:
|
| 198 |
+
if not st.session_state.get("_seeded", False):
|
| 199 |
+
store.seed_cases(reset=True)
|
| 200 |
+
st.session_state["_seeded"] = True
|
| 201 |
+
|
| 202 |
+
def _case_options() -> List[Dict[str, Any]]:
|
| 203 |
+
items = store.list_cases()
|
| 204 |
+
if not items:
|
| 205 |
+
_seed_once()
|
| 206 |
+
items = store.list_cases()
|
| 207 |
+
return items
|
| 208 |
+
|
| 209 |
+
def _get_case(case_id: str) -> Dict[str, Any]:
|
| 210 |
+
return store.read_case(case_id) or {}
|
| 211 |
|
| 212 |
|
| 213 |
# --------------------------- Global callouts --------------------------------
|
|
|
|
| 260 |
st.success(f"Created new case {new_id}. Refreshing listβ¦")
|
| 261 |
st.rerun()
|
| 262 |
|
| 263 |
+
# Reset demo (use seed reset; don't rely on internal paths)
|
| 264 |
if st.button("ποΈ Reset Demo (clear all cases)", use_container_width=True):
|
| 265 |
try:
|
| 266 |
+
store.seed_cases(reset=True)
|
| 267 |
+
st.success("β
All demo cases cleared and re-seeded.")
|
| 268 |
+
st.session_state["_seeded"] = True
|
| 269 |
st.rerun()
|
| 270 |
except Exception as e:
|
| 271 |
+
st.error(f"Failed to reset cases: {e}")
|
| 272 |
|
| 273 |
# Selection
|
| 274 |
default_case_id = st.session_state.get("_current_case_id") or items[0]["case_id"]
|
|
|
|
| 352 |
st.info("Submit the referral on the PCP tab to activate Specialist Review.")
|
| 353 |
st.stop()
|
| 354 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
read_only = status == "completed"
|
| 356 |
|
| 357 |
st.subheader("SOAP Draft")
|
| 358 |
+
soap = case.get("soap_draft", {}) or {"subjective": "", "objective": "", "assessment": "", "plan": ""}
|
| 359 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
# Generate SOAP if needed
|
| 361 |
+
gen_needed = (status == "submitted") and _soap_is_empty(soap)
|
| 362 |
if gen_needed and not read_only:
|
| 363 |
if st.button("Generate SOAP Draft (LLM)", key=f"gen_{selected}"):
|
| 364 |
try:
|
|
|
|
| 384 |
st.warning(f"LLM generation unavailable; seeded a minimal draft. ({type(e).__name__})")
|
| 385 |
result = {"error": str(e)}
|
| 386 |
|
|
|
|
| 387 |
store.update_case(selected, {
|
| 388 |
"soap_draft": {"subjective": subj.strip(), "objective": obj.strip(), "assessment": assess.strip(), "plan": plan.strip()},
|
| 389 |
"explainability": explain_patch,
|
| 390 |
})
|
| 391 |
+
# allow simple tab toggle to show new SOAP; no extra rerun complexity here
|
| 392 |
+
st.success("SOAP draft generated. If fields appear empty, toggle tabs once to refresh the view.")
|
|
|
|
| 393 |
|
| 394 |
# Editable fields
|
| 395 |
c1, c2 = st.columns(2)
|
|
|
|
| 400 |
assess_new = st.text_area("Assessment", value=soap.get("assessment",""), height=140, disabled=read_only, key=f"assess_{selected}")
|
| 401 |
plan_new = st.text_area("Plan", value=soap.get("plan",""), height=180, disabled=read_only, key=f"plan_{selected}")
|
| 402 |
|
| 403 |
+
# Persist edits (whitespace-insensitive) while in submitted state
|
| 404 |
if (not read_only) and (status == "submitted"):
|
| 405 |
changed = any([
|
| 406 |
normalize_text(subj_new) != normalize_text(soap.get("subjective") or ""),
|
|
|
|
| 421 |
|
| 422 |
stale = is_stale(assess_new, assess_hash0) or is_stale(plan_new, plan_hash0)
|
| 423 |
|
| 424 |
+
# Guard: only show guideline rationale if we have SOAP content
|
| 425 |
+
if not _soap_is_empty(soap):
|
| 426 |
+
if not stale and (normalize_text(assess_new + plan_new)):
|
| 427 |
+
# Use stored bullets if available; else compute once and store
|
| 428 |
+
stored_points = (exp.get("guideline_rationale") or [])
|
| 429 |
+
if stored_points:
|
| 430 |
+
_render_guideline_bullets(stored_points)
|
| 431 |
+
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
g = generate_guideline_rationale(assessment_text=assess_new, plan_text=plan_new)
|
| 433 |
+
warn = (g.get("warning") or "").strip()
|
| 434 |
+
if warn:
|
| 435 |
+
st.info(warn)
|
| 436 |
+
try:
|
| 437 |
+
st.page_link("pages/01_RAG_Corpus_Prep.py", label="Open RAG Prep")
|
| 438 |
+
except Exception:
|
| 439 |
+
pass
|
| 440 |
points = g.get("rationale", []) or []
|
| 441 |
+
_render_guideline_bullets(points)
|
| 442 |
+
if points:
|
| 443 |
+
store.update_case(selected, {"explainability": {"guideline_rationale": points}})
|
| 444 |
+
else:
|
| 445 |
+
if (not read_only) and (status == "submitted"):
|
| 446 |
+
if st.button("β» Re-run Guidelines", key=f"rerun_{selected}"):
|
| 447 |
+
g = generate_guideline_rationale(assessment_text=assess_new, plan_text=plan_new)
|
| 448 |
+
points = g.get("rationale", []) or []
|
| 449 |
+
store.update_case(selected, {
|
| 450 |
+
"explainability": {
|
| 451 |
+
"baseline": {"assessment_hash": text_hash(assess_new), "plan_hash": text_hash(plan_new)},
|
| 452 |
+
"guideline_rationale": points,
|
| 453 |
+
}
|
| 454 |
+
})
|
| 455 |
+
st.rerun()
|
| 456 |
+
else:
|
| 457 |
+
st.caption("Guideline rationale hidden after edits. Click **Re-run Guidelines** to refresh.")
|
| 458 |
|
| 459 |
st.divider()
|
| 460 |
|
| 461 |
+
# Specialist Review metadata (3 controls)
|
| 462 |
st.subheader("Specialist Review")
|
| 463 |
b = case.get("billing", {}) or {}
|
| 464 |
minutes_val = int(b.get("minutes", 5))
|
|
|
|
| 468 |
attested = bool(b.get("attested", False)) if read_only else False
|
| 469 |
attested = st.checkbox("I attest that the consult note is complete.", value=attested, disabled=read_only, key=f"att_{selected}")
|
| 470 |
|
| 471 |
+
# Persist time/spoke as they change (not attested until finalize)
|
| 472 |
if (not read_only) and (status == "submitted"):
|
| 473 |
if (minutes != minutes_val) or (spoke != bool(b.get("spoke", False))):
|
| 474 |
store.update_case(selected, {"billing": {"minutes": minutes, "spoke": bool(spoke)}})
|
|
|
|
| 478 |
finalize_btn = st.button("Finalize Consult", type="primary", disabled=not can_finalize, key=f"finalize_{selected}")
|
| 479 |
|
| 480 |
if finalize_btn:
|
| 481 |
+
# Use persisted guideline bullets; do NOT regenerate at finalize
|
| 482 |
+
points = (case.get("explainability", {}) or {}).get("guideline_rationale") or []
|
| 483 |
+
endnotes: List[Dict[str, Any]] = []
|
|
|
|
| 484 |
|
| 485 |
soap_now = case.get("soap_draft", {}) or {}
|
| 486 |
note_md = _note_markdown(
|
| 487 |
case,
|
| 488 |
build_referral_summary(case),
|
| 489 |
{
|
| 490 |
+
"subjective": subj_new or soap_now.get("subjective",""),
|
| 491 |
+
"objective": obj_new or soap_now.get("objective",""),
|
| 492 |
+
"assessment": assess_new or soap_now.get("assessment",""),
|
| 493 |
+
"plan": plan_new or soap_now.get("plan",""),
|
| 494 |
},
|
| 495 |
points, endnotes=endnotes
|
| 496 |
)
|
|
|
|
| 499 |
note_path = config.make_export_path(selected, "Sample Consultation Report.md")
|
| 500 |
Path(note_path).write_text(note_md, encoding="utf-8")
|
| 501 |
|
| 502 |
+
# Persist billing info and mark completed
|
| 503 |
+
store.update_case(selected, {
|
| 504 |
+
"billing": {"minutes": minutes, "spoke": bool(spoke), "attested": True},
|
| 505 |
+
"status": "completed",
|
| 506 |
+
})
|
| 507 |
|
| 508 |
st.session_state["_show_exports"] = True
|
| 509 |
+
st.success("β
Finalized. Consult note exported; proceed to the **Documentation & Billing** tab to submit a claim.")
|
| 510 |
st.caption(f"Note: {note_path}")
|
|
|
|
| 511 |
|
| 512 |
|
| 513 |
# ===== DOCUMENTATION & BILLING TAB =====
|
|
|
|
| 523 |
if note_path and note_path.exists():
|
| 524 |
with st.expander("π View EHR Consultation Note", expanded=True):
|
| 525 |
md = note_path.read_text(encoding="utf-8")
|
| 526 |
+
st.markdown(md)
|
| 527 |
st.caption(f"File: {note_path.name}")
|
| 528 |
else:
|
| 529 |
st.warning("No consultation note found for this case. Finalize the consult to generate one.")
|
|
|
|
| 546 |
st.write(f"{icon} **{s.code}** β {s.descriptor} β ${s.rate:.2f}")
|
| 547 |
st.caption(s.why)
|
| 548 |
|
| 549 |
+
# ICD-10 suggestions block (from Assessment + Plan)
|
|
|
|
|
|
|
| 550 |
soap_now = case.get("soap_draft", {}) or {}
|
| 551 |
+
icd_suggestions = billing.autosuggest_icd(
|
| 552 |
assessment_text=soap_now.get("assessment", ""),
|
| 553 |
plan_text=soap_now.get("plan", "")
|
| 554 |
)
|
|
|
|
| 557 |
for s in icd_suggestions:
|
| 558 |
st.write(f"β
**{s.code}** β {s.description} (confidence {int(s.confidence*100)}%)")
|
| 559 |
st.caption(s.why)
|
|
|
|
| 560 |
icd_options = [f"{s.code} β {s.description}" for s in icd_suggestions]
|
| 561 |
picked_icd = st.multiselect(
|
| 562 |
"Select diagnosis code(s) to include on the claim",
|
|
|
|
| 567 |
picked_icd_codes = [o.split(" β ")[0] for o in picked_icd]
|
| 568 |
else:
|
| 569 |
st.info("No ICD-10 suggestions available for this case.")
|
| 570 |
+
picked_icd_codes: List[str] = []
|
| 571 |
|
|
|
|
| 572 |
chosen_default = b.get("cpt_code") or (elig_codes[0] if elig_codes else None)
|
| 573 |
if elig_codes:
|
| 574 |
+
chosen = st.selectbox(
|
| 575 |
+
"Choose CPT (eligible)",
|
| 576 |
+
options=elig_codes,
|
| 577 |
+
index=elig_codes.index(chosen_default) if (chosen_default in elig_codes) else 0,
|
| 578 |
+
key=f"cpt_{selected}"
|
| 579 |
+
)
|
| 580 |
else:
|
| 581 |
st.warning("No eligible CPT for the current minutes/spoke combination.", icon="β οΈ")
|
| 582 |
chosen = None
|
|
|
|
| 590 |
store.update_case(selected, {
|
| 591 |
"billing": {
|
| 592 |
"cpt_code": chosen,
|
| 593 |
+
"icd_codes": picked_icd_codes,
|
| 594 |
}
|
| 595 |
})
|
| 596 |
+
|
| 597 |
# Build and save 837 JSON (includes ICD codes)
|
| 598 |
pick = next((s for s in suggestions if s.code == chosen), None)
|
| 599 |
rate = float(getattr(pick, "rate", 0.0)) if pick else 0.0
|
|
|
|
| 604 |
minutes=minutes,
|
| 605 |
spoke=bool(spoke),
|
| 606 |
attested=True,
|
| 607 |
+
icd_codes=picked_icd_codes,
|
| 608 |
)
|
| 609 |
claim_path = config.make_export_path(selected, "Sample 837 Json.json")
|
| 610 |
Path(claim_path).write_text(json.dumps(claim, ensure_ascii=False, indent=2), encoding="utf-8")
|
| 611 |
+
|
| 612 |
st.success("Claim submitted. 837 JSON generated (see preview below).")
|
| 613 |
st.caption(f"Claim: {claim_path.name}")
|
| 614 |
|
|
|
|
| 636 |
|
| 637 |
|
| 638 |
|
| 639 |
+
|