Cardiosense-AG commited on
Commit
8f98a41
Β·
verified Β·
1 Parent(s): e914999

Update pages/02_Workflow_UI.py

Browse files
Files changed (1) hide show
  1. 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 (unchanged)
6
- # 2) Specialist Review (SOAP + Guideline Rationale + 3 specialist controls)
7
- # 3) Documentation & Billing (Completed cases only: EHR note expander, CPT, 837)
8
  #
9
- # Notes:
10
- # - No Referral Rationale. Single explainability layer: Guideline Rationale (bullets).
11
- # - Guideline Rationale persists into case JSON to survive page switches.
12
- # - SOAP draft generation triggers st.rerun() to populate text areas immediately.
 
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
- shutil.rmtree(store._cases_dir(), ignore_errors=True) # type: ignore
250
- st.success("βœ… All demo cases cleared. The default draft cases will be recreated on reload.")
251
- st.session_state["_seeded"] = False
252
  st.rerun()
253
  except Exception as e:
254
- st.error(f"Failed to clear cases: {e}")
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 (not any(soap.get(k, "").strip() for k in ("subjective", "objective", "assessment", "plan")))
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
- # Set flag so guideline rationale doesn't run on immediate rerun
386
- st.session_state["_suppress_guidelines"] = True
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: skip guideline generation until SOAP fields exist
420
- if not any((soap.get(k, "") or "").strip() for k in ("subjective", "objective", "assessment", "plan")):
421
- st.info("Generate SOAP draft first to enable guideline rationale.")
422
- st.stop()
423
-
424
-
425
-
426
- if not stale and (normalize_text(assess_new + plan_new)):
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
- store.update_case(selected, {
452
- "explainability": {
453
- "baseline": {"assessment_hash": text_hash(assess_new), "plan_hash": text_hash(plan_new)},
454
- "guideline_rationale": points,
455
- }
456
- })
457
- st.rerun()
458
- else:
459
- st.caption("Guideline rationale hidden after edits. Click **Re-run Guidelines** to refresh.")
 
 
 
 
 
 
 
 
460
 
461
  st.divider()
462
 
463
- # ---- Specialist Review Metadata (3 controls) ----
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 (do not persist attested until finalize)
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
- # Compose note with current SOAP + persisted/stored bullets (fallback to latest generation)
484
- g = generate_guideline_rationale(assessment_text=assess_new, plan_text=plan_new)
485
- points = (exp.get("guideline_rationale") or []) or (g.get("rationale", []) or [])
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": locals().get("subj_new", soap_now.get("subjective","")),
494
- "objective": locals().get("obj_new", soap_now.get("objective","")),
495
- "assessment": locals().get("assess_new", soap_now.get("assessment","")),
496
- "plan": locals().get("plan_new", soap_now.get("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 attestation now and mark completed
506
- store.update_case(selected, {"billing": {"minutes": minutes, "spoke": bool(spoke), "attested": True}})
507
- store.set_status(selected, "completed")
 
 
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) # render formatted markdown, not raw text
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
- # --- ICD-10 diagnosis suggestions (new) ---
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("Choose CPT (eligible)", options=elig_codes, index=elig_codes.index(chosen_default) if (chosen_default in elig_codes) else 0, key=f"cpt_{selected}")
 
 
 
 
 
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, # NEW: persist ICDs in case JSON
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, # NEW: include ICD list in claim
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
+