newslens / src /ui /components /article_card.py
Jitender20's picture
Add NewsLens Streamlit app
208266a
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"])