Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| """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' · <span style="color:{sev_col}">' | |
| f'SEVERITY {node.severity.upper()}</span>' | |
| f' · +{node.time_offset_hours}h' | |
| ) | |
| if confidence: | |
| meta += f' · 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' · 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, | |
| ) | |