"""HTML rendering for the Co-Pilot demo. Each function takes typed data (a CastMember, a CopilotResult, etc.) and returns a self-contained HTML snippet. Gradio injects the snippets into `gr.HTML` components. Visual discipline: - Single typeface (Inter for body, JetBrains Mono for numbers). - Three colors only: page background near-white, near-black ink, one accent color per class (green/yellow/red). No gradient palettes, no shadows, no neon. Liquid demo aesthetic. - Inline styles only (no external CSS file). The renderer is one self-contained module so a future maintainer can edit visuals without touching the inference path. The renderer NEVER reaches into model internals. It consumes the serializable result objects produced by `copilot_inference.py`. """ from __future__ import annotations import html from typing import Iterable from encoder.src.demo.copilot_inference import ( LABEL_COLORS, LABEL_NAMES, CastMember, CopilotResult, ) # ---- shared constants ---- _PAGE_BG = "#fafafa" _INK = "#171717" _INK_DIM = "#525252" _BORDER = "rgba(0,0,0,0.08)" _CARD_BG = "#ffffff" _TONE_COLORS: dict[str, str] = { "angry": "#ef4444", "confused": "#a855f7", "casual": "#3b82f6", "formal": "#0ea5e9", "terse": "#737373", } # ---- cast strip ---- def render_cast_strip(cast: list[CastMember], selected_idx: int) -> str: """Six clickable archetype cards across the top. The card buttons are styled as anchors but the *click* is wired by Gradio via separate `gr.Button` components — this function only renders the visual chrome that mirrors the selected state. Selection is driven by the `selected_idx` argument. """ cards: list[str] = [] for i, m in enumerate(cast): is_sel = i == selected_idx accent = LABEL_COLORS[m.expected_label] border = accent if is_sel else _BORDER bg = "#ffffff" if not is_sel else _hex_with_alpha(accent, 0.06) cards.append(f"""
{html.escape(m.display_name)}
expected: {html.escape(_short_label(m.expected_label_name))}
""") return f"""
{''.join(cards)}
""" # ---- complaint quote ---- def render_complaint(member: CastMember) -> str: """Large pull-quote of what the customer wrote.""" tone_color = _TONE_COLORS.get(member.tone, _INK_DIM) return f"""
Complaint received · tone: {html.escape(member.tone)}
“{html.escape(member.complaint_text)}”
""" # ---- timeline ---- def render_timeline( disputed_idx: int, top_k_positions: Iterable[int] | None = None, attribution_probs: Iterable[float] | None = None, num_positions: int = 64, ) -> str: """64 dots; the disputed one is a red star, top-k are accent glows. If `top_k_positions` is None (pre-inference), all dots are neutral except the disputed star. If `attribution_probs` is provided, dot opacity scales with per-position attribution probability so the cluster around the highlighted positions reads as a glow gradient. """ # Explicit None check: numpy arrays raise ValueError under `or []`. top_k_set: set[int] = set() if top_k_positions is not None: top_k_set = {int(i) for i in top_k_positions} probs = list(attribution_probs) if attribution_probs is not None else None dots: list[str] = [] for i in range(num_positions): if i == disputed_idx: dots.append(_dot_disputed(i)) elif i in top_k_set: alpha = float(probs[i]) if probs is not None else 1.0 dots.append(_dot_attributed(i, alpha)) else: faint = float(probs[i]) * 0.4 if probs is not None else 0.0 dots.append(_dot_neutral(i, faint)) return f"""
64-transaction history · oldest → newest disputed   contributed
{''.join(dots)}
""" def _dot_disputed(i: int) -> str: return f"""
""" def _dot_attributed(i: int, alpha: float) -> str: glow = max(0.4, min(1.0, alpha)) return f"""
""" def _dot_neutral(i: int, faint: float = 0.0) -> str: bg = f"rgba(245, 158, 11, {faint:.2f})" if faint > 0.0 else "rgba(0,0,0,0.18)" return f"""
""" # ---- score panel ---- def render_score(result: CopilotResult | None) -> str: """Big number, color band, class label. When `result` is None (idle / pre-click), shows a placeholder that occupies the same vertical space so the layout doesn't jump when the score arrives. """ if result is None: return _score_placeholder() pct = int(round(result.score * 100)) color = result.color label = result.predicted_label return f"""
Model verdict
0.{pct:02d}
{html.escape(label)}
""" def _score_placeholder() -> str: return f"""
Model verdict
0.—
Awaiting analysis · click Analyze
""" # ---- reasoning panel (streams in) ---- def render_reasoning(text: str | None) -> str: """Model's LM-decoded reasoning, with a blinking caret while streaming.""" if text is None: body = f"""
Reasoning will appear here once the model has read the customer's history.
""" else: body = f"""
{html.escape(text)}
""" return f"""
Reasoning
{body}
""" # ---- header ---- def render_header() -> str: return f"""
Liquid AI · LFM2.5-350M backbone · encoder + LoRA

Dispute Co-Pilot

""" # ---- helpers ---- def _short_label(name: str) -> str: return { "unlikely_friendly_fraud": "likely legit", "ambiguous": "ambiguous", "likely_friendly_fraud": "likely fraud", }.get(name, name) def _hex_with_alpha(hex_color: str, alpha: float) -> str: """Convert #RRGGBB + alpha float into an rgba() string.""" h = hex_color.lstrip("#") if len(h) != 6: return hex_color r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) return f"rgba({r}, {g}, {b}, {alpha:.3f})"