Spaces:
Sleeping
Sleeping
| from html import escape | |
| import streamlit as st | |
| ARTICLE_CARD_STYLES = """ | |
| <style> | |
| .nl-article-card { | |
| background: #ffffff; | |
| border: 1px solid #d8dee9; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| margin: 0.65rem 0 0.9rem 0; | |
| } | |
| .nl-article-header { | |
| display: flex; | |
| align-items: flex-start; | |
| justify-content: space-between; | |
| gap: 1rem; | |
| } | |
| .nl-article-source { | |
| color: #64748b; | |
| font-size: 0.78rem; | |
| font-weight: 700; | |
| letter-spacing: 0.04em; | |
| margin-bottom: 0.25rem; | |
| text-transform: uppercase; | |
| } | |
| .nl-article-card h4 { | |
| color: #15202b; | |
| font-size: 1rem; | |
| line-height: 1.35; | |
| margin: 0; | |
| } | |
| .nl-article-card p { | |
| color: #475569; | |
| line-height: 1.55; | |
| margin: 0.65rem 0 0.8rem 0; | |
| } | |
| .nl-label { | |
| border: 1px solid; | |
| border-radius: 999px; | |
| font-size: 0.75rem; | |
| font-weight: 800; | |
| padding: 0.25rem 0.55rem; | |
| white-space: nowrap; | |
| } | |
| .nl-confidence-row { | |
| color: #64748b; | |
| display: flex; | |
| justify-content: space-between; | |
| font-size: 0.82rem; | |
| margin-bottom: 0.3rem; | |
| } | |
| .nl-confidence-row strong { | |
| color: #15202b; | |
| } | |
| .nl-confidence-track { | |
| background: #eef2f7; | |
| border-radius: 999px; | |
| height: 0.45rem; | |
| overflow: hidden; | |
| width: 100%; | |
| } | |
| .nl-confidence-track div { | |
| height: 100%; | |
| } | |
| .nl-read-link { | |
| color: #2457c5; | |
| display: inline-block; | |
| font-weight: 800; | |
| margin-top: 0.75rem; | |
| text-decoration: none; | |
| } | |
| .nl-read-link:hover { | |
| color: #1f4dac; | |
| text-decoration: underline; | |
| } | |
| @media (max-width: 760px) { | |
| .nl-article-header { | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| } | |
| </style> | |
| """ | |
| def inject_article_card_styles() -> None: | |
| st.markdown(ARTICLE_CARD_STYLES, unsafe_allow_html=True) | |
| def _safe_text(value: object, fallback: str = "") -> str: | |
| if value is None: | |
| return fallback | |
| text = str(value).strip() | |
| return text or fallback | |
| def _label_style(label: str) -> tuple[str, str]: | |
| if label.lower() == "biased": | |
| return "#c24138", "#fff4f2" | |
| return "#247857", "#effaf5" | |
| def smart_truncate(text, limit=80): | |
| if len(text) <= limit: | |
| return text | |
| return text[:limit].rsplit(" ", 1)[0] + "..." | |
| def render_article_card(article: dict, debug: bool = False) -> None: | |
| label = _safe_text(article.get("text_label"), "Unknown") | |
| confidence = float(article.get("confidence", 0) or 0) | |
| source = _safe_text(article.get("source"), "Unknown source") | |
| source_bias = _safe_text(article.get("source_bias"), "Unknown bias") | |
| source_bias_provenance = _safe_text(article.get("source_bias_provenance")) | |
| source_meta = f"{source} / {source_bias}" | |
| if source_bias_provenance and source_bias_provenance != "manual_demo": | |
| source_meta = f"{source_meta} / {source_bias_provenance}" | |
| url = _safe_text(article.get("url"), "#") | |
| description = _safe_text(article.get("description")) | |
| fallback_text = _safe_text(article.get("text"))[:280] | |
| excerpt = description or fallback_text or "No article excerpt was returned by the API." | |
| title = _safe_text(article.get("title")) or smart_truncate(excerpt, 80) | |
| accent, soft = _label_style(label) | |
| confidence_pct = max(0, min(confidence, 1)) * 100 | |
| st.markdown( | |
| f""" | |
| <div class="nl-article-card"> | |
| <div class="nl-article-header"> | |
| <div> | |
| <div class="nl-article-source">{escape(source_meta)}</div> | |
| <h4>{escape(title)}</h4> | |
| </div> | |
| <span class="nl-label" style="color:{accent}; background:{soft}; border-color:{accent};"> | |
| {escape(label)} | |
| </span> | |
| </div> | |
| <p>{escape(excerpt)}</p> | |
| <div class="nl-confidence-row"> | |
| <span>Confidence</span> | |
| <strong>{confidence:.2f}</strong> | |
| </div> | |
| <div class="nl-confidence-track"> | |
| <div style="width:{confidence_pct:.0f}%; background:{accent};"></div> | |
| </div> | |
| <a class="nl-read-link" href="{escape(url)}" target="_blank" rel="noopener noreferrer"> | |
| Read article | |
| </a> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| if debug: | |
| with st.expander("Model internals", expanded=False): | |
| if "similarity_score" in article: | |
| st.caption(f"Similarity score: {article['similarity_score']:.4f}") | |
| if "probabilities" in article: | |
| st.json(article["probabilities"]) | |