"""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 badge ─────────────────────────────────────────────────────────────
_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'●'
for i in range(3)
)
return (
f''
f'impact'
f'{dots}'
)
# ── AI interpretation badge ───────────────────────────────────────────────────
def ai_badge(label: str = "AI Synthesis") -> str:
return (
f'✦ {label}'
)
# ── AI interpretation card wrapper ────────────────────────────────────────────
def interpretation_card(
header_label: str,
content_html: str,
badge_label: str = "AI Synthesis",
) -> str:
return (
f'
'
f'
'
f'{header_label}'
f'{ai_badge(badge_label)}'
f'
'
f'{content_html}'
f'
'
)
# ── Source type badge ─────────────────────────────────────────────────────────
_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'{s}'
)
return (
f'{s}'
)
# ── Eyebrow label ─────────────────────────────────────────────────────────────
def eyebrow_label(text: str, color: str = TEXT_MUTED) -> str:
"""Uppercase micro-label used as a section/card eyebrow."""
return (
f'{text}'
)
# ── Importance marker ─────────────────────────────────────────────────────────
_IMPORTANCE_MAP: dict[str, tuple[str, str, str, str, str]] = {
# impact → (dot_char, dot_color, label_i18n_key, text_color, font_weight)
"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''
f'{dot_char}'
f'{text_label}'
f''
)
# ── Meta row ─────────────────────────────────────────────────────────────────
_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'{source}'
)
if reliability:
rel_key = _RELIABILITY_LABEL_KEYS.get(reliability.upper())
rel_label = t(rel_key) if rel_key else reliability.title()
parts.append(
f'{rel_label}'
)
if not parts:
return ""
sep = f'·'
inner = sep.join(parts)
return (
f''
f'{inner}'
f'
'
)
# ── Sort by impact ────────────────────────────────────────────────────────────
_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)
# ── Evidence blockquote ───────────────────────────────────────────────────────
def evidence_quote(snippet: str, bg: str = "#fafaf9", border_color: str = "#e5e7eb") -> str:
"""Renders a collapsible evidence snippet."""
if not snippet:
return ""
return (
f''
f''
f'▶ {t("view_quote")}'
f'
'
f''
f'"'
f'{snippet}'
f'
'
f' '
)
# ── Earnings quality card helpers ─────────────────────────────────────────────
_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),
}
# ── Analyst Edge components ───────────────────────────────────────────────────
_DELTA_KIND_META: dict[str, tuple[str, str, str]] = {
# kind → (label, fg_color, bg_color)
"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'{sig}'
)
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''
f'
before'
f'
“{before}”
'
f'
'
)
if after:
parts.append(
f''
f'
after'
f'
“{after}”
'
f'
'
)
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''
f'
'
f'
{prefix}{label}'
f'
'
f'
'
)
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''
f'{i}'
f'{item}'
f'
'
)
footer_html = ""
if more_label and max_items and len(items) > max_items:
footer_html = (
f''
f'{more_label}
'
)
return (
f''
f'{rows_html}{footer_html}'
f'
'
)
# ── Between-the-lines signal-type colors (shared with verdict's Tell card) ───
_SIGNAL_TYPE_COLORS: dict[str, tuple[str, str]] = {
# signal_type → (fg_color, bg_color); labels resolve via t(f"sig_{type}")
"language_drift": ("#4f46e5", "#eef2ff"), # indigo
"qa_evasion": ("#dc2626", "#fef2f2"), # red
"omission": ("#d97706", "#fffbeb"), # amber
"emphasis_shift": ("#6b7280", "#f3f4f6"), # gray
"accounting_quality": ("#0ea5e9", "#f0f9ff"), # sky
}
# ── Unified signal card (Decision Stack) ─────────────────────────────────────
# One renderer for every Signal kind; the body adapts per kind. Replaces
# fact_card / tension_card / delta_card / subtext_card / earnings_quality_card.
_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 is the only colored dimension of the card frame: bull/bear/mixed.
_STANCE_ACCENTS: dict[str, str] = {"bull": GREEN, "bear": RED, "mixed": AMBER}
def _pill(label: str, fg: str, bg: str) -> str:
return (
f'{label}'
)
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''
f'
{eyebrow_label(t(key_label), fg)}
'
f'
'
f'{extra.get(reading_key, "")}
'
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'
'
)
return f'{"".join(sides)}
'
def _delta_body(sig) -> str:
metric_html = (
f''
f''
f'{t("computed_label")} · {sig.body}'
f'
'
) 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'{sig.body}
'
)
implication = (sig.extra or {}).get("implication", "")
if implication:
parts.append(
f''
f'{eyebrow_label(t("btl_implication"), AMBER)}'
f'
'
f'{implication}
'
f'
'
)
return "".join(parts)
def _news_body(sig) -> str:
extra = sig.extra or {}
url = extra.get("url", "")
date = extra.get("date", "")
date_html = (
f'{date}
'
if date else ""
)
title_html = (
f'{sig.headline}'
if url else
f'{sig.headline}
'
)
body_html = (
f''
f'{sig.body}
'
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)
# ── Header row: taxonomy chips left, period + AI badge right ───────────
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":
# Preserve item-level ranking without exposing an uncalibrated HIGH / MED / LOW scale.
left_bits.append(_pill("Material", TEXT, BG_MUTED))
right_bits = []
if sig.period_range:
right_bits.append(
f'{sig.period_range}'
)
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''
f'
'
f'{"".join(b for b in left_bits if b)}
'
f'
'
f'{"".join(right_bits)}
'
f'
'
)
# ── Headline ────────────────────────────────────────────────────────────
headline_html = ""
if sig.headline and sig.kind != "news":
headline_html = (
f'{sig.headline}
'
)
# ── Quality assessment chip rides next to the headline ─────────────────
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''
f''
f'{icon} {label}'
f'
'
) + headline_html
# ── Body by kind ────────────────────────────────────────────────────────
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'{sig.body}
'
)
else:
body_html = ""
# ── Footer: evidence + attribution (tension carries these per side) ────
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 "")
)
# ── Dedup cross-reference — this canonical card absorbed near-duplicate
# facts from other sections; note where else the same fact was classified
# instead of showing it as 3-4 separate cards.
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''
f'{t("also_classified_as")} {labels}
'
)
st.markdown(
f''
f'{header}{headline_html}{body_html}{footer_html}'
f'
',
unsafe_allow_html=True,
)