"""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, )