| """Shared UI component helpers used across dashboard tabs.""" |
| from __future__ import annotations |
| import streamlit as st |
| from dashboard.i18n import t |
| from dashboard.theme import ( |
| GREEN, RED, AMBER, GRAY, BORDER, TEXT_FAINT, |
| BG, BG_MUTED, TEXT, TEXT_MUTED, |
| BULL_BG, BULL_BORDER, BEAR_BG, BEAR_BORDER, |
| PURPLE, AI_BORDER, AI_COLOR, AI_BADGE_BG, |
| IMPACT_HIGH_BG, IMPACT_MED_BG, IMPACT_LOW_TEXT, |
| FS_BODY, FS_META, FS_EYEBROW, FS_SECTION_LABEL, |
| ) |
|
|
| |
|
|
| _IMPACT_DOT_COLORS = { |
| "HIGH": (IMPACT_HIGH_BG, 3), |
| "MEDIUM": (IMPACT_MED_BG, 2), |
| "LOW": (IMPACT_LOW_TEXT, 1), |
| } |
|
|
| def impact_badge(level: str) -> str: |
| spec = _IMPACT_DOT_COLORS.get(level) |
| if not spec: |
| return "" |
| color, filled = spec |
| dots = "".join( |
| f'<span style="color:{color if i < filled else "#d1d5db"};font-size:0.55rem;line-height:1;">β</span>' |
| for i in range(3) |
| ) |
| return ( |
| f'<span title="Impact: {level}" style="display:inline-flex;align-items:center;gap:3px;' |
| f'background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;padding:1px 7px;">' |
| f'<span style="font-size:0.6rem;font-weight:600;color:#64748b;letter-spacing:0.05em;">impact</span>' |
| f'{dots}</span>' |
| ) |
|
|
|
|
| |
|
|
| def ai_badge(label: str = "AI Synthesis") -> str: |
| return ( |
| f'<span style="background:{AI_BADGE_BG};color:{AI_COLOR};' |
| f'border:1px solid {AI_BORDER};border-radius:4px;' |
| f'padding:1px 7px;font-size:0.62rem;font-weight:600;">β¦ {label}</span>' |
| ) |
|
|
|
|
| |
|
|
| def interpretation_card( |
| header_label: str, |
| content_html: str, |
| badge_label: str = "AI Synthesis", |
| ) -> str: |
| return ( |
| f'<div class="primer-card" style="background:{BG};border:1px solid {AI_BORDER};' |
| f'border-left:4px solid {PURPLE};border-radius:0 12px 12px 0;' |
| f'padding:20px 24px;margin-bottom:4px;">' |
| f'<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">' |
| f'<span style="font-size:{FS_EYEBROW};font-weight:700;letter-spacing:0.1em;' |
| f'text-transform:uppercase;color:{TEXT_MUTED};">{header_label}</span>' |
| f'{ai_badge(badge_label)}' |
| f'</div>' |
| f'{content_html}' |
| f'</div>' |
| ) |
|
|
|
|
| |
|
|
| _SOURCE_STYLES: dict[str, tuple[str, str, str]] = { |
| "transcript": ("#f0fdf4", "#86efac", "#16a34a"), |
| "10-q": ("#eff6ff", "#93c5fd", "#2563eb"), |
| "10-k": ("#eff6ff", "#93c5fd", "#2563eb"), |
| "filing": ("#eff6ff", "#93c5fd", "#2563eb"), |
| "news": ("#fff7ed", "#fdba74", "#ea580c"), |
| } |
|
|
| def source_badge(s: str) -> str: |
| if not s: |
| return "" |
| key = s.lower().strip() |
| for pattern, (bg, border, color) in _SOURCE_STYLES.items(): |
| if pattern in key: |
| return ( |
| f'<span style="background:{bg};color:{color};border:1px solid {border};' |
| f'border-radius:4px;padding:1px 8px;font-size:0.7rem;font-weight:500;">{s}</span>' |
| ) |
| return ( |
| f'<span style="background:#f3f4f6;color:#6b7280;border:1px solid #d1d5db;' |
| f'border-radius:4px;padding:1px 8px;font-size:0.7rem;font-weight:500;">{s}</span>' |
| ) |
|
|
|
|
| |
|
|
| def eyebrow_label(text: str, color: str = TEXT_MUTED) -> str: |
| """Uppercase micro-label used as a section/card eyebrow.""" |
| return ( |
| f'<span style="font-size:{FS_EYEBROW};font-weight:700;text-transform:uppercase;' |
| f'letter-spacing:0.1em;color:{color};">{text}</span>' |
| ) |
|
|
|
|
| |
|
|
| _IMPORTANCE_MAP: dict[str, tuple[str, str, str, str, str]] = { |
| |
| "HIGH": ("β", "#0f172a", "imp_critical", "#0f172a", "600"), |
| "MEDIUM": ("β", "#64748b", "imp_important", "#64748b", "500"), |
| "LOW": ("β", "#94a3b8", "imp_context", "#94a3b8", "500"), |
| } |
|
|
| def importance_marker(impact: str) -> str: |
| """Monochrome dot + word marker for impact level. No green/red β never competes with bull/bear.""" |
| spec = _IMPORTANCE_MAP.get((impact or "").upper()) |
| if not spec: |
| return "" |
| dot_char, dot_color, label_key, text_color, fw = spec |
| text_label = t(label_key) |
| return ( |
| f'<span style="font-size:{FS_META};font-weight:{fw};color:{text_color};' |
| f'display:inline-flex;align-items:center;gap:3px;">' |
| f'<span style="color:{dot_color};">{dot_char}</span>' |
| f'{text_label}' |
| f'</span>' |
| ) |
|
|
|
|
| |
|
|
| _RELIABILITY_LABEL_KEYS: dict[str, str] = { |
| "HIGH": "rel_high", |
| "MEDIUM": "rel_med", |
| "LOW": "rel_low", |
| } |
|
|
| def meta_row(reliability: str, source: str, impact: str) -> str: |
| """Single subtle attribution line replacing the 3-badge stack. |
| |
| Example output: β Critical Β· 10-Q Β· High confidence |
| Returns "" if all args are empty/None. |
| """ |
| if not reliability and not source and not impact: |
| return "" |
|
|
| parts: list[str] = [] |
|
|
| impact_str = (impact or "").upper() |
| marker_html = importance_marker(impact_str) |
| if marker_html: |
| parts.append(marker_html) |
|
|
| if source: |
| parts.append( |
| f'<span style="font-size:{FS_META};color:{TEXT_MUTED};">{source}</span>' |
| ) |
|
|
| if reliability: |
| rel_key = _RELIABILITY_LABEL_KEYS.get(reliability.upper()) |
| rel_label = t(rel_key) if rel_key else reliability.title() |
| parts.append( |
| f'<span style="font-size:{FS_META};color:{TEXT_MUTED};">{rel_label}</span>' |
| ) |
|
|
| if not parts: |
| return "" |
|
|
| sep = f'<span style="font-size:{FS_META};color:{TEXT_MUTED};">Β·</span>' |
| inner = sep.join(parts) |
| return ( |
| f'<div style="font-size:{FS_META};color:{TEXT_MUTED};margin-top:8px;' |
| f'display:flex;align-items:center;gap:4px;flex-wrap:wrap;">' |
| f'{inner}' |
| f'</div>' |
| ) |
|
|
|
|
| |
|
|
| _IMPACT_ORDER: dict[str, int] = {"HIGH": 0, "MEDIUM": 1, "LOW": 2} |
|
|
| def sort_by_impact(items: list[dict]) -> list[dict]: |
| """Stable sort: HIGH β MEDIUM β LOW β None/unknown. Returns a new list.""" |
| def _key(item: dict) -> int: |
| return _IMPACT_ORDER.get((item.get("impact") or "").upper(), 3) |
| return sorted(items, key=_key) |
|
|
|
|
| |
|
|
| def evidence_quote(snippet: str, bg: str = "#fafaf9", border_color: str = "#e5e7eb") -> str: |
| """Renders a collapsible evidence snippet.""" |
| if not snippet: |
| return "" |
| return ( |
| f'<details style="margin:4px 0;">' |
| f'<summary style="cursor:pointer;font-size:0.68rem;color:#9ca3af;font-weight:500;' |
| f'list-style:none;display:inline-flex;align-items:center;gap:4px;user-select:none;">' |
| f'<span style="font-size:0.6rem;">βΆ</span> {t("view_quote")}' |
| f'</summary>' |
| f'<div style="margin-top:4px;padding:4px 10px;background:{bg};' |
| f'border-left:3px solid {border_color};border-radius:0 4px 4px 0;">' |
| f'<span style="font-size:0.95rem;line-height:1;color:#d1d5db;' |
| f'font-family:Georgia,serif;margin-right:3px;vertical-align:-2px;">"</span>' |
| f'<span style="font-size:0.75rem;color:#6b7280;font-style:italic;line-height:1.35;">{snippet}</span>' |
| f'</div>' |
| f'</details>' |
| ) |
|
|
|
|
| |
|
|
| _DIMENSION_LABELS: dict[str, str] = { |
| "consensus_beat_mix": "Beat Mix", |
| "guidance_dynamics": "Guidance", |
| "narrative_vs_numbers": "Narrative", |
| "segment_mix": "Segment Mix", |
| "capital_allocation": "Capital", |
| } |
|
|
| _ASSESSMENT_STYLES: dict[str, tuple[str, str, str]] = { |
| "positive": (GREEN, BULL_BG, BULL_BORDER), |
| "neutral": (GRAY, BG_MUTED, BORDER), |
| "concerning": (RED, BEAR_BG, BEAR_BORDER), |
| } |
|
|
|
|
| |
|
|
| _DELTA_KIND_META: dict[str, tuple[str, str, str]] = { |
| |
| "risk_added": ("NEW RISK", "#ef4444", "#fef2f2"), |
| "risk_removed": ("REMOVED RISK", "#6b7280", "#f3f4f6"), |
| "risk_reworded": ("REWORDED RISK", "#f59e0b", "#fffbeb"), |
| "guidance_language_shift": ("GUIDANCE SHIFT", "#4f46e5", "#eef2ff"), |
| "term_frequency": ("FREQUENCY SHIFT", "#0ea5e9", "#f0f9ff"), |
| "kpi_dropped": ("DROPPED KPI", "#6b7280", "#f3f4f6"), |
| "tone_trend": ("TONE TREND", "#8b5cf6", "#f5f3ff"), |
| "topic_arc": ("TOPIC ARC", "#0ea5e9", "#f0f9ff"), |
| "recurring_evasion": ("RECURRING EVASION", "#dc2626", "#fef2f2"), |
| "topic_fade": ("TOPIC FADE", "#6b7280", "#f3f4f6"), |
| } |
|
|
| _SIG_COLORS: dict[str, str] = {"HIGH": "#ef4444", "MEDIUM": "#f59e0b", "LOW": "#9ca3af"} |
|
|
|
|
| def significance_badge(sig: str) -> str: |
| color = _SIG_COLORS.get(sig, "#9ca3af") |
| return ( |
| f'<span style="background:{color}1a;color:{color};border:1px solid {color}44;' |
| f'border-radius:4px;padding:1px 7px;font-size:0.65rem;font-weight:700;' |
| f'text-transform:uppercase;letter-spacing:0.06em;">{sig}</span>' |
| ) |
|
|
|
|
| def _redline_block(before: str, after: str) -> str: |
| """Render beforeβafter text with redline-style color coding.""" |
| parts: list[str] = [] |
| if before: |
| parts.append( |
| f'<div style="padding:8px 10px;background:#fef2f2;border-left:3px solid #fca5a5;' |
| f'border-radius:0 4px 4px 0;margin-bottom:4px;">' |
| f'<span style="font-size:0.6rem;font-weight:700;text-transform:uppercase;' |
| f'color:#ef4444;letter-spacing:0.07em;">before</span>' |
| f'<div style="font-size:0.78rem;font-style:italic;color:#7f1d1d;line-height:1.45;' |
| f'margin-top:3px;">“{before}”</div>' |
| f'</div>' |
| ) |
| if after: |
| parts.append( |
| f'<div style="padding:8px 10px;background:#ecfdf5;border-left:3px solid #6ee7b7;' |
| f'border-radius:0 4px 4px 0;">' |
| f'<span style="font-size:0.6rem;font-weight:700;text-transform:uppercase;' |
| f'color:#10b981;letter-spacing:0.07em;">after</span>' |
| f'<div style="font-size:0.78rem;font-style:italic;color:#064e3b;line-height:1.45;' |
| f'margin-top:3px;">“{after}”</div>' |
| f'</div>' |
| ) |
| return "".join(parts) |
|
|
|
|
| def section_header(label: str, accent: str = TEXT_MUTED, icon: str = "") -> str: |
| """Section header: accent tick + semibold dark label + hairline rule.""" |
| prefix = f"{icon} " if icon else "" |
| return ( |
| f'<div style="display:flex;align-items:center;gap:10px;margin:36px 0 14px;">' |
| f'<span style="width:4px;height:15px;border-radius:2px;background:{accent};flex-shrink:0;"></span>' |
| f'<span style="font-size:{FS_SECTION_LABEL};font-weight:700;text-transform:uppercase;' |
| f'letter-spacing:0.08em;color:{TEXT};white-space:nowrap;">{prefix}{label}</span>' |
| f'<div style="flex:1;height:1px;background:{BORDER};"></div>' |
| f'</div>' |
| ) |
|
|
|
|
| def section_divider(label: str, icon: str = "") -> str: |
| """Backward-compatible delegate β section_header.""" |
| return section_header(label, icon=icon) |
|
|
|
|
| def numbered_list( |
| items: list[str], |
| max_items: int | None = None, |
| more_label: str = "", |
| ) -> str: |
| """Render a numbered list with circular badges. |
| |
| Args: |
| items: List of text items to render. |
| max_items: If set, only the first N items are shown. |
| more_label: If max_items is set and there are more items, show this as a footer line. |
| |
| Returns: |
| HTML string (to be rendered with unsafe_allow_html=True). |
| """ |
| visible = items[:max_items] if max_items else items |
| rows_html = "" |
| for i, item in enumerate(visible, 1): |
| rows_html += ( |
| f'<div style="display:flex;align-items:flex-start;gap:12px;' |
| f'padding:12px 0;border-bottom:1px solid {BORDER};">' |
| f'<span style="display:inline-flex;align-items:center;justify-content:center;' |
| f'width:22px;height:22px;border-radius:50%;flex-shrink:0;margin-top:1px;' |
| f'background:{GRAY};color:#fff;font-size:0.7rem;font-weight:700;">{i}</span>' |
| f'<span style="font-size:{FS_BODY};line-height:1.55;color:{TEXT};">{item}</span>' |
| f'</div>' |
| ) |
| footer_html = "" |
| if more_label and max_items and len(items) > max_items: |
| footer_html = ( |
| f'<div style="padding:10px 0 4px;font-size:0.75rem;color:{TEXT_MUTED};">' |
| f'{more_label}</div>' |
| ) |
| return ( |
| f'<div style="border:1px solid {BORDER};border-radius:12px;' |
| f'padding:4px 16px;background:{BG};max-width:680px;">' |
| f'{rows_html}{footer_html}' |
| f'</div>' |
| ) |
|
|
|
|
| |
|
|
| _SIGNAL_TYPE_COLORS: dict[str, tuple[str, str]] = { |
| |
| "language_drift": ("#4f46e5", "#eef2ff"), |
| "qa_evasion": ("#dc2626", "#fef2f2"), |
| "omission": ("#d97706", "#fffbeb"), |
| "emphasis_shift": ("#6b7280", "#f3f4f6"), |
| "accounting_quality": ("#0ea5e9", "#f0f9ff"), |
| } |
|
|
|
|
| |
| |
| |
|
|
| _KIND_LABEL_KEYS: dict[str, str] = { |
| "tell": "kind_tell", "tension": "kind_tension", "delta": "kind_delta", |
| "change": "kind_change", "risk": "kind_risk", "quality": "kind_quality", |
| "point": "kind_point", "commentary": "kind_commentary", |
| "note": "kind_note", "news": "kind_news", |
| } |
|
|
| |
| _STANCE_ACCENTS: dict[str, str] = {"bull": GREEN, "bear": RED, "mixed": AMBER} |
|
|
|
|
| def _pill(label: str, fg: str, bg: str) -> str: |
| return ( |
| f'<span style="background:{bg};color:{fg};border:1px solid {fg}33;' |
| f'border-radius:4px;padding:1px 7px;font-size:0.65rem;font-weight:600;' |
| f'text-transform:uppercase;letter-spacing:0.06em;">{label}</span>' |
| ) |
|
|
|
|
| def _signal_category_chip(sig) -> str: |
| """Taxonomy chip: tell signal-types and delta kinds keep their forensic |
| colors; risk categories and quality dimensions stay neutral gray.""" |
| cat = sig.category |
| if not cat: |
| return "" |
| if sig.kind == "tell" and cat in _SIGNAL_TYPE_COLORS: |
| fg, bg = _SIGNAL_TYPE_COLORS[cat] |
| return _pill(t(f"sig_{cat}"), fg, bg) |
| if sig.kind == "delta" and cat in _DELTA_KIND_META: |
| _, fg, bg = _DELTA_KIND_META[cat] |
| return _pill(t(f"dk_{cat}"), fg, bg) |
| if sig.kind == "risk": |
| return _pill(cat, GRAY, "#f3f4f6") |
| if sig.kind == "quality": |
| return _pill(_DIMENSION_LABELS.get(cat, cat.replace("_", " ").title()), GRAY, "#f3f4f6") |
| return "" |
|
|
|
|
| def _tension_body(sig) -> str: |
| extra = sig.extra or {} |
| sides = [] |
| for key_label, fg, reading_key, ev_key in ( |
| ("tension_surface", "#059669", "bullish_reading", "bullish_evidence"), |
| ("tension_deeper", "#dc2626", "bearish_reading", "bearish_evidence"), |
| ): |
| ev = extra.get(ev_key) or {} |
| ev = ev if isinstance(ev, dict) else {} |
| sides.append( |
| f'<div style="flex:1;padding:12px 14px;background:{BG_MUTED};border:1px solid {BORDER};' |
| f'border-radius:8px;min-width:0;">' |
| f'<div style="margin-bottom:6px;">{eyebrow_label(t(key_label), fg)}</div>' |
| f'<div style="font-size:0.82rem;line-height:1.5;color:{TEXT};margin-bottom:8px;">' |
| f'{extra.get(reading_key, "")}</div>' |
| f'{evidence_quote(ev.get("evidence_snippet", ""), bg=BG, border_color=BORDER)}' |
| f'{meta_row(ev.get("reliability", ""), ev.get("source", ""), ev.get("impact", "") or "")}' |
| f'</div>' |
| ) |
| return f'<div style="display:flex;gap:10px;">{"".join(sides)}</div>' |
|
|
|
|
| def _delta_body(sig) -> str: |
| metric_html = ( |
| f'<div style="font-size:0.72rem;font-weight:600;color:#374151;' |
| f'background:#f8fafc;border:1px solid #e2e8f0;border-radius:4px;' |
| f'padding:4px 8px;margin-bottom:8px;">' |
| f'<span style="color:{TEXT_MUTED};font-size:0.65rem;font-weight:500;">' |
| f'{t("computed_label")} Β· </span>{sig.body}' |
| f'</div>' |
| ) if sig.body else "" |
| return metric_html + _redline_block(sig.before_text, sig.after_text) |
|
|
|
|
| def _tell_body(sig) -> str: |
| parts = [] |
| if sig.body: |
| parts.append( |
| f'<div style="font-size:{FS_BODY};font-style:italic;color:{TEXT};' |
| f'line-height:1.55;margin-bottom:10px;">{sig.body}</div>' |
| ) |
| implication = (sig.extra or {}).get("implication", "") |
| if implication: |
| parts.append( |
| f'<div style="border-top:1px solid {BORDER};padding-top:10px;margin-bottom:6px;">' |
| f'{eyebrow_label(t("btl_implication"), AMBER)}' |
| f'<div style="font-size:{FS_BODY};color:{TEXT};line-height:1.55;margin-top:4px;">' |
| f'{implication}</div>' |
| f'</div>' |
| ) |
| return "".join(parts) |
|
|
|
|
| def _news_body(sig) -> str: |
| extra = sig.extra or {} |
| url = extra.get("url", "") |
| date = extra.get("date", "") |
| date_html = ( |
| f'<div style="font-size:{FS_META};color:{TEXT_FAINT};margin-bottom:4px;">{date}</div>' |
| if date else "" |
| ) |
| title_html = ( |
| f'<a href="{url}" target="_blank" style="font-size:{FS_BODY};font-weight:600;' |
| f'color:{TEXT};text-decoration:none;border-bottom:1px solid {BORDER};">{sig.headline}</a>' |
| if url else |
| f'<div style="font-size:{FS_BODY};font-weight:600;color:{TEXT};">{sig.headline}</div>' |
| ) |
| body_html = ( |
| f'<div style="font-size:{FS_META};color:{TEXT_MUTED};line-height:1.5;margin-top:6px;">' |
| f'{sig.body}</div>' |
| if sig.body else "" |
| ) |
| return f"{date_html}{title_html}{body_html}" |
|
|
|
|
| def signal_card(sig) -> None: |
| """Render a normalized Signal (see dashboard/signal_feed.py) as one card.""" |
| from dashboard.signal_feed import EXPERIMENTAL_AI_KINDS, HEURISTIC_KINDS |
|
|
| accent = _STANCE_ACCENTS.get(sig.stance or "", BORDER) |
|
|
| |
| left_bits = [ |
| _pill(t(_KIND_LABEL_KEYS.get(sig.kind, "kind_note")), GRAY, "#f8fafc"), |
| _signal_category_chip(sig), |
| ] |
| if sig.is_new: |
| left_bits.append(_pill(t("new_badge"), RED, BEAR_BG)) |
| if sig.significance == "HIGH": |
| |
| left_bits.append(_pill("Material", TEXT, BG_MUTED)) |
|
|
| right_bits = [] |
| if sig.period_range: |
| right_bits.append( |
| f'<span style="font-size:0.65rem;color:{TEXT_MUTED};">{sig.period_range}</span>' |
| ) |
| if sig.kind in EXPERIMENTAL_AI_KINDS: |
| right_bits.append(ai_badge("AI Β· experimental")) |
| elif sig.kind in HEURISTIC_KINDS: |
| right_bits.append(_pill("Heuristic Β· validate", GRAY, "#f8fafc")) |
|
|
| header = ( |
| f'<div style="display:flex;align-items:center;justify-content:space-between;' |
| f'gap:8px;margin-bottom:10px;flex-wrap:wrap;">' |
| f'<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">' |
| f'{"".join(b for b in left_bits if b)}</div>' |
| f'<div style="display:flex;align-items:center;gap:6px;">' |
| f'{"".join(right_bits)}</div>' |
| f'</div>' |
| ) |
|
|
| |
| headline_html = "" |
| if sig.headline and sig.kind != "news": |
| headline_html = ( |
| f'<div style="font-size:{FS_BODY};font-weight:600;color:{TEXT};' |
| f'line-height:1.5;margin-bottom:6px;">{sig.headline}</div>' |
| ) |
|
|
| |
| if sig.kind == "quality": |
| assessment = (sig.extra or {}).get("assessment", "neutral") |
| color, bg, _border = _ASSESSMENT_STYLES.get(assessment, _ASSESSMENT_STYLES["neutral"]) |
| icon = {"positive": "β²", "neutral": "β", "concerning": "βΌ"}.get(assessment, "β") |
| label = t(f"quality_{assessment}") if assessment in ("positive", "neutral", "concerning") else assessment.title() |
| headline_html = ( |
| f'<div style="margin-bottom:8px;">' |
| f'<span style="display:inline-flex;align-items:center;gap:4px;' |
| f'background:{color}1a;color:{color};border:1px solid {color}44;' |
| f'border-radius:4px;padding:2px 8px;font-size:0.65rem;font-weight:700;">' |
| f'{icon} {label}</span>' |
| f'</div>' |
| ) + headline_html |
|
|
| |
| if sig.kind == "tension": |
| body_html = _tension_body(sig) |
| elif sig.kind == "delta": |
| body_html = _delta_body(sig) |
| elif sig.kind == "tell": |
| body_html = _tell_body(sig) |
| elif sig.kind == "news": |
| body_html = _news_body(sig) |
| elif sig.body: |
| body_html = ( |
| f'<div style="font-size:{FS_BODY};line-height:1.55;color:{TEXT};' |
| f'margin-bottom:8px;">{sig.body}</div>' |
| ) |
| else: |
| body_html = "" |
|
|
| |
| footer_html = "" |
| if sig.kind != "tension": |
| footer_html = ( |
| evidence_quote(sig.evidence_snippet, bg=BG_MUTED, border_color=BORDER) |
| + meta_row(sig.reliability, sig.source, sig.impact or "") |
| ) |
|
|
| |
| |
| |
| also_in = (sig.extra or {}).get("also_in") or [] |
| if also_in: |
| labels = " Β· ".join(t(_KIND_LABEL_KEYS.get(k, "kind_note")).upper() for k in also_in) |
| footer_html += ( |
| f'<div style="font-size:0.65rem;color:{TEXT_MUTED};margin-top:6px;">' |
| f'{t("also_classified_as")} {labels}</div>' |
| ) |
|
|
| st.markdown( |
| f'<div class="primer-card" style="background:{BG};border:1px solid {BORDER};' |
| f'border-left:3px solid {accent};border-radius:0 10px 10px 0;' |
| f'padding:14px 18px;margin-bottom:10px;max-width:760px;">' |
| f'{header}{headline_html}{body_html}{footer_html}' |
| f'</div>', |
| unsafe_allow_html=True, |
| ) |
|
|