cascade_risk / app /components /reference.py
Lucasoppem's picture
Sync from GitHub main
bbae929 verified
Raw
History Blame Contribute Delete
6.05 kB
"""Reference events panel + prediction details."""
from __future__ import annotations
import html
import streamlit as st
from app.styles import TOKENS, domain_color, severity_color
from src.models.schemas import PredictionResult
def _render_one_reference(entry: dict) -> str:
"""Render one reference row as HTML.
``entry`` follows PredictionResult.reference_info shape:
``{event_id, similarity[, country, date, num_cascade_nodes, top_domains]}``.
Optional fields are omitted from the rendered secondary line when missing
(fallback mode uses only event_id + similarity).
"""
event_id = entry.get("event_id", "?")
similarity = entry.get("similarity") or 0
pct = int(round(similarity * 100))
# Secondary metadata line: country · date · num_cascade_nodes
meta_bits: list[str] = []
country = entry.get("country")
date_ = entry.get("date")
if country and date_:
meta_bits.append(f"{html.escape(str(country))} · {html.escape(str(date_))}")
elif country:
meta_bits.append(html.escape(str(country)))
elif date_:
meta_bits.append(html.escape(str(date_)))
n_nodes = entry.get("num_cascade_nodes")
if isinstance(n_nodes, int):
meta_bits.append(f"{n_nodes} cascade nodes")
meta_line = (
f'<div class="node-meta" style="margin:2px 0 6px 0">{ " · ".join(meta_bits) }</div>'
if meta_bits else ""
)
# top_domains pills (reuse the DAG legend style)
top_domains = entry.get("top_domains") or []
pills = ""
if top_domains:
pill_html = "".join(
f'<span class="domain-pill" style="font-size:10px;padding:2px 8px">'
f'<span class="swatch" style="background:{domain_color(d)}"></span>'
f'{html.escape(str(d))}</span>'
for d in top_domains
)
pills = (
f'<div style="display:flex;flex-wrap:wrap;gap:6px;'
f'margin:0 0 6px 0">{pill_html}</div>'
)
return (
f'<div class="ref-row" style="flex-direction:column;'
f'align-items:stretch;gap:4px">'
f' <div style="display:flex;align-items:center;gap:14px">'
f' <span class="ref-id">{html.escape(event_id)}</span>'
f' <span class="ref-meter">'
f' <span class="ref-meter-fill" style="width:{pct}%"></span>'
f' </span>'
f' <span class="ref-pct">{pct}%</span>'
f' </div>'
f' {meta_line}'
f' {pills}'
f'</div>'
)
def render_reference_events(result: PredictionResult) -> None:
"""Historical analogues with similarity meters + enriched provenance.
Consumes ``result.reference_info`` when populated (post-v2 schema). Falls
back to event_id + similarity pairs when the chain index was unavailable
at predict time, so older callers/artifacts still render correctly.
"""
if not result.reference_event_ids and not result.reference_info:
st.markdown(
'<div class="node-meta">No historical analogues retrieved.</div>',
unsafe_allow_html=True,
)
return
# Prefer enriched info when present; otherwise synthesize minimal entries
# from the parallel lists so rendering stays uniform.
if result.reference_info:
entries = result.reference_info
else:
entries = [
{"event_id": eid,
"similarity": result.similarity_scores[i]
if i < len(result.similarity_scores) else 0}
for i, eid in enumerate(result.reference_event_ids)
]
rows = "".join(_render_one_reference(e) for e in entries)
st.markdown(
f'<div class="panel" style="padding:6px 16px 4px 16px">{rows}</div>',
unsafe_allow_html=True,
)
st.markdown(
'<div class="node-meta" style="margin-top:8px">'
'RAG · MiniLM-L6 · cosine similarity'
'</div>',
unsafe_allow_html=True,
)
def render_prediction_details(result: PredictionResult) -> None:
"""Per-node detail cards."""
chain = result.predicted_chain
if not chain.cascade_events:
return
for node in chain.cascade_events:
confidence = result.confidence_scores.get(node.id, 0)
sev_col = severity_color(node.severity)
dcol = domain_color(node.domain)
# Plain-text label (Streamlit doesn't render HTML here)
label = (
f"{node.id} · {node.description[:72]}"
f"{'…' if len(node.description) > 72 else ''}"
f" [{node.severity.upper()}]"
)
with st.expander(label, expanded=False):
meta = (
f'<div class="node-meta">'
f'<span style="color:{dcol}">● {node.domain}</span>'
f' &nbsp;·&nbsp; <span style="color:{sev_col}">'
f'SEVERITY {node.severity.upper()}</span>'
f' &nbsp;·&nbsp; +{node.time_offset_hours}h'
)
if confidence:
meta += f' &nbsp;·&nbsp; CONF {confidence:.0%}'
if node.parent_ids:
joint_label = (
f' <span style="color:{TOKENS["amber"]}">⋈ JOINT</span>'
if len(node.parent_ids) >= 2 else ""
)
meta += (
f' &nbsp;·&nbsp; CAUSED BY {", ".join(node.parent_ids)}'
f'{joint_label}'
)
meta += "</div>"
st.markdown(meta, unsafe_allow_html=True)
st.markdown(
f'<div style="font-family:Figtree;font-size:14.5px;'
f'color:{TOKENS["text"]};line-height:1.6;margin-top:10px">'
f'<span style="color:{TOKENS["text_muted"]};font-family:JetBrains Mono;'
f'font-size:10px;letter-spacing:0.18em;text-transform:uppercase;'
f'display:block;margin-bottom:4px">Mechanism</span>'
f'{node.mechanism}'
f'</div>',
unsafe_allow_html=True,
)