mekosotto commited on
Commit
b6f1745
·
1 Parent(s): ae883d4

feat(frontend): interactive BBB tab — SMILES input + decision card + SHAP bars

Browse files
Files changed (1) hide show
  1. src/frontend/app.py +89 -13
src/frontend/app.py CHANGED
@@ -297,22 +297,42 @@ def _render_sidebar(api_ok: bool, api_status: str) -> None:
297
  def _render_bbb_tab() -> None:
298
  _render_section(
299
  "MOLECULE — BBBP",
300
- "Blood-Brain-Barrier permeability",
301
- "Reads SMILES strings, validates with RDKit, and emits 2,048-bit "
302
- "Morgan circular fingerprints (ECFP4-equivalent) ready for any "
303
- "scikit-learn classifier.",
 
 
304
  )
305
- bbb_in = st.text_input("Input CSV path", "data/raw/bbbp.csv", key="bbb_in")
306
- bbb_out = st.text_input("Output Parquet path", "data/processed/bbbp_features.parquet", key="bbb_out")
307
- if st.button("Run BBB pipeline", type="primary", key="bbb_run"):
308
- with st.spinner("Computing fingerprints…"):
 
 
 
 
 
 
 
 
 
309
  try:
310
- _render_result(_post("/pipeline/bbb", {
311
- "input_path": bbb_in, "output_path": bbb_out,
312
- }))
313
- st.toast("BBB pipeline complete", icon="✅")
314
  except httpx.HTTPStatusError as e:
315
- st.error(f"Pipeline failed (HTTP {e.response.status_code}): {e.response.text}")
 
 
 
 
 
 
 
 
 
 
316
  except httpx.RequestError as e:
317
  st.error(f"Cannot reach FastAPI at {_API_URL}: {e!r}")
318
 
@@ -366,6 +386,62 @@ def _render_mri_tab() -> None:
366
  st.error(f"Cannot reach FastAPI at {_API_URL}: {e!r}")
367
 
368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  def main() -> None:
370
  """Streamlit entrypoint. Idempotent — Streamlit re-runs on every interaction."""
371
  st.set_page_config(
 
297
  def _render_bbb_tab() -> None:
298
  _render_section(
299
  "MOLECULE — BBBP",
300
+ "Blood-Brain-Barrier permeability decision",
301
+ "Enter a SMILES string. The system computes a 2,048-bit Morgan "
302
+ "fingerprint, runs it through a trained Random Forest classifier, "
303
+ "and returns the predicted permeability label, the model's "
304
+ "self-rated confidence, and the top SHAP feature attributions "
305
+ "explaining the decision.",
306
  )
307
+
308
+ smiles = st.text_input(
309
+ "SMILES string",
310
+ value="CCO",
311
+ key="bbb_smiles",
312
+ help="Examples: CCO (ethanol, BBB+), CC(=O)Nc1ccc(O)cc1 (paracetamol)",
313
+ )
314
+ top_k = st.slider(
315
+ "SHAP features to display", min_value=3, max_value=10, value=5, key="bbb_topk",
316
+ )
317
+
318
+ if st.button("Predict BBB permeability", type="primary", key="bbb_predict"):
319
+ with st.spinner("Computing fingerprint, predicting, and explaining…"):
320
  try:
321
+ result = _post("/predict/bbb", {"smiles": smiles, "top_k": top_k})
322
+ _render_prediction_card(result)
323
+ st.toast("Prediction complete", icon="✅")
 
324
  except httpx.HTTPStatusError as e:
325
+ if e.response.status_code == 503:
326
+ st.error(
327
+ "Model artifact not loaded yet. Run "
328
+ "`python -m src.models.bbb_model` to train it, "
329
+ "then retry."
330
+ )
331
+ else:
332
+ st.error(
333
+ f"Prediction failed (HTTP {e.response.status_code}): "
334
+ f"{e.response.text}"
335
+ )
336
  except httpx.RequestError as e:
337
  st.error(f"Cannot reach FastAPI at {_API_URL}: {e!r}")
338
 
 
386
  st.error(f"Cannot reach FastAPI at {_API_URL}: {e!r}")
387
 
388
 
389
+ def _render_prediction_card(result: dict) -> None:
390
+ """Render a B2B-styled decision card: label badge + confidence + SHAP bars."""
391
+ label_text = _html.escape(str(result["label_text"]))
392
+ badge_color = "#166534" if result["label"] == 1 else "#991B1B"
393
+ badge_bg = "#DCFCE7" if result["label"] == 1 else "#FEE2E2"
394
+ confidence_pct = result["confidence"] * 100
395
+
396
+ st.markdown(
397
+ f"""
398
+ <div style='background:#FFFFFF;border:1px solid #E2E8F0;border-radius:10px;
399
+ padding:1.5rem;margin:1rem 0;box-shadow:0 1px 2px rgba(15,23,42,0.04);'>
400
+ <p style='font-size:0.72rem;font-weight:700;color:#64748B;
401
+ letter-spacing:0.08em;text-transform:uppercase;margin:0;'>Prediction</p>
402
+ <div style='display:flex;align-items:center;gap:0.75rem;margin-top:0.4rem;'>
403
+ <span style='background:{badge_bg};color:{badge_color};
404
+ padding:0.4rem 0.9rem;border-radius:999px;
405
+ font-size:1rem;font-weight:700;letter-spacing:0.01em;'>
406
+ {label_text.upper()}
407
+ </span>
408
+ <span style='color:#475569;font-size:0.95rem;'>
409
+ Model confidence: <strong style='color:#0F172A;'>{confidence_pct:.1f}%</strong>
410
+ </span>
411
+ </div>
412
+ </div>
413
+ """,
414
+ unsafe_allow_html=True,
415
+ )
416
+
417
+ # Confidence bar
418
+ st.markdown(
419
+ "<p style='font-size:0.72rem;font-weight:700;color:#64748B;"
420
+ "letter-spacing:0.08em;text-transform:uppercase;margin:1rem 0 0.4rem 0;'>"
421
+ "Confidence</p>",
422
+ unsafe_allow_html=True,
423
+ )
424
+ st.progress(float(result["confidence"]))
425
+
426
+ # SHAP attributions chart
427
+ n_features = len(result["top_features"])
428
+ st.markdown(
429
+ f"<p style='font-size:0.72rem;font-weight:700;color:#64748B;"
430
+ f"letter-spacing:0.08em;text-transform:uppercase;margin:1.5rem 0 0.4rem 0;'>"
431
+ f"Top {n_features} SHAP attributions</p>",
432
+ unsafe_allow_html=True,
433
+ )
434
+ import pandas as pd
435
+ shap_df = pd.DataFrame(result["top_features"]).set_index("feature")
436
+ st.bar_chart(shap_df, height=240, color="#0369A1")
437
+
438
+ st.caption(
439
+ "Positive SHAP values pushed the model toward the predicted class; "
440
+ "negative values pushed it away. Feature names are 2,048-bit Morgan "
441
+ "fingerprint indices (`fp_<bit>`)."
442
+ )
443
+
444
+
445
  def main() -> None:
446
  """Streamlit entrypoint. Idempotent — Streamlit re-runs on every interaction."""
447
  st.set_page_config(