feat(frontend): interactive BBB tab — SMILES input + decision card + SHAP bars
Browse files- 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 |
-
"
|
| 302 |
-
"
|
| 303 |
-
"
|
|
|
|
|
|
|
| 304 |
)
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
try:
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
st.toast("BBB pipeline complete", icon="✅")
|
| 314 |
except httpx.HTTPStatusError as e:
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|