mekosotto Claude Opus 4.7 (1M context) commited on
Commit
d5a285c
·
1 Parent(s): 3f348a3

feat(frontend): inline AI Assistant in EEG + MRI tabs

Browse files

- EEG tab gains an expander after pipeline results: 3 preset questions
+ custom input + Ask button → POST /explain/eeg.
- MRI tab gains a parallel expander inside _render_combat_diagnostics:
feeds site_gap_pre/post + reduction_factor + n_subjects (derived
from distinct subject_id count) into POST /explain/mri.
- Both expanders show source/model audit caption like the BBB
AI Assistant tab. Uses last_eeg_run session state.
- No new tests — UI wiring covered by import-smoke tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. src/frontend/app.py +83 -2
src/frontend/app.py CHANGED
@@ -409,15 +409,57 @@ def _render_eeg_tab() -> None:
409
  if st.button("Run EEG pipeline", type="primary", key="eeg_run"):
410
  with st.spinner("Filtering and running ICA…"):
411
  try:
412
- _render_result(_post("/pipeline/eeg", {
413
  "input_path": eeg_in, "output_path": eeg_out,
414
- }))
 
 
415
  st.toast("EEG pipeline complete", icon="✅")
416
  except httpx.HTTPStatusError as e:
417
  st.error(f"Pipeline failed (HTTP {e.response.status_code}): {e.response.text}")
418
  except httpx.RequestError as e:
419
  st.error(f"Cannot reach FastAPI at {_API_URL}: {e!r}")
420
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
  def _render_mri_tab() -> None:
423
  _render_section(
@@ -638,6 +680,45 @@ def _render_combat_diagnostics(result: dict) -> None:
638
  f"site-gap reduction quantifies."
639
  )
640
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
 
642
  def _render_ai_assistant_tab() -> None:
643
  """Day-7 T3C: chat-style explainer for the most recent BBB prediction."""
 
409
  if st.button("Run EEG pipeline", type="primary", key="eeg_run"):
410
  with st.spinner("Filtering and running ICA…"):
411
  try:
412
+ result = _post("/pipeline/eeg", {
413
  "input_path": eeg_in, "output_path": eeg_out,
414
+ })
415
+ st.session_state["last_eeg_run"] = result
416
+ _render_result(result)
417
  st.toast("EEG pipeline complete", icon="✅")
418
  except httpx.HTTPStatusError as e:
419
  st.error(f"Pipeline failed (HTTP {e.response.status_code}): {e.response.text}")
420
  except httpx.RequestError as e:
421
  st.error(f"Cannot reach FastAPI at {_API_URL}: {e!r}")
422
 
423
+ # Day-8 T1C: AI Assistant inline for EEG
424
+ last_eeg = st.session_state.get("last_eeg_run")
425
+ if last_eeg is not None:
426
+ with st.expander("Ask the AI Assistant about this EEG run", expanded=False):
427
+ eeg_q_presets = [
428
+ "Why were certain ICA components dropped?",
429
+ "What does the bandpass filter do?",
430
+ "Is this run consistent with previous runs?",
431
+ ]
432
+ eeg_preset = st.selectbox(
433
+ "Preset question", options=eeg_q_presets, key="eeg_ai_preset",
434
+ )
435
+ eeg_custom = st.text_input(
436
+ "Or type your own question (optional)",
437
+ value="", key="eeg_ai_custom",
438
+ )
439
+ eeg_question = eeg_custom.strip() or eeg_preset
440
+ if st.button("Ask AI Assistant", key="eeg_ai_ask"):
441
+ with st.spinner("Composing rationale…"):
442
+ try:
443
+ eeg_resp = _post(
444
+ "/explain/eeg",
445
+ {
446
+ "rows": int(last_eeg.get("rows", 0)),
447
+ "columns": int(last_eeg.get("columns", 0)),
448
+ "duration_sec": float(last_eeg.get("duration_sec", 0.0)),
449
+ "mlflow_run_id": last_eeg.get("mlflow_run_id"),
450
+ "user_question": eeg_question,
451
+ },
452
+ )
453
+ st.markdown(f"**A:** {eeg_resp['rationale']}")
454
+ st.caption(
455
+ f"Source: `{eeg_resp.get('source', '?')}` · "
456
+ f"Model: `{eeg_resp.get('model') or '—'}`"
457
+ )
458
+ except httpx.HTTPStatusError as e:
459
+ st.error(f"Assistant failed (HTTP {e.response.status_code}): {e.response.text}")
460
+ except httpx.RequestError as e:
461
+ st.error(f"Cannot reach FastAPI: {e!r}")
462
+
463
 
464
  def _render_mri_tab() -> None:
465
  _render_section(
 
680
  f"site-gap reduction quantifies."
681
  )
682
 
683
+ # Day-8 T1C: AI Assistant inline for MRI
684
+ n_subjects = len({r["subject_id"] for r in result.get("rows", [])})
685
+ with st.expander("Ask the AI Assistant about this ComBat run", expanded=False):
686
+ mri_q_presets = [
687
+ "Why does ComBat matter for multi-site MRI?",
688
+ "How significant is this reduction factor?",
689
+ "What would I lose without harmonization?",
690
+ ]
691
+ mri_preset = st.selectbox(
692
+ "Preset question", options=mri_q_presets, key="mri_ai_preset",
693
+ )
694
+ mri_custom = st.text_input(
695
+ "Or type your own question (optional)",
696
+ value="", key="mri_ai_custom",
697
+ )
698
+ mri_question = mri_custom.strip() or mri_preset
699
+ if st.button("Ask AI Assistant", key="mri_ai_ask"):
700
+ with st.spinner("Composing rationale…"):
701
+ try:
702
+ mri_resp = _post(
703
+ "/explain/mri",
704
+ {
705
+ "site_gap_pre": float(result["site_gap_pre"]),
706
+ "site_gap_post": float(result["site_gap_post"]),
707
+ "reduction_factor": float(result["reduction_factor"]),
708
+ "n_subjects": n_subjects,
709
+ "user_question": mri_question,
710
+ },
711
+ )
712
+ st.markdown(f"**A:** {mri_resp['rationale']}")
713
+ st.caption(
714
+ f"Source: `{mri_resp.get('source', '?')}` · "
715
+ f"Model: `{mri_resp.get('model') or '—'}`"
716
+ )
717
+ except httpx.HTTPStatusError as e:
718
+ st.error(f"Assistant failed (HTTP {e.response.status_code}): {e.response.text}")
719
+ except httpx.RequestError as e:
720
+ st.error(f"Cannot reach FastAPI: {e!r}")
721
+
722
 
723
  def _render_ai_assistant_tab() -> None:
724
  """Day-7 T3C: chat-style explainer for the most recent BBB prediction."""