lfm2-transaction-encoder / encoder /src /demo /copilot_render_fraud_pattern.py
cdotsanghvi's picture
initial transaction co-pilot deployment
b3112c7
Raw
History Blame Contribute Delete
15 kB
"""HTML rendering for the Fraud Pattern Co-Pilot demo.
The unique panel for Fraud is a two-distribution scoreboard: 5 stage
bars + 4 type bars side-by-side, with stage and type recommendation
pills above. Visual vocabulary matches dispute and collections.
"""
from __future__ import annotations
import html
from typing import Iterable
import numpy as np
from encoder.src.data.synthetic_fraud_pattern import (
NUM_STAGES,
NUM_TYPES,
STAGE_NAMES,
TYPE_NAMES,
)
from encoder.src.demo.copilot_inference_fraud_pattern import (
STAGE_COLORS,
TYPE_COLORS,
FraudPatternCastMember,
FraudPatternResult,
)
_PAGE_BG = "#fafafa"
_INK = "#171717"
_INK_DIM = "#525252"
_BORDER = "rgba(0,0,0,0.08)"
_CARD_BG = "#ffffff"
def render_header() -> str:
return f"""
<div style="text-align: center; margin-bottom: 22px;">
<div style="
font-size: 11px;
color: {_INK_DIM};
font-family: 'JetBrains Mono', ui-monospace, monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 6px;
">
Liquid AI &middot; LFM2.5-350M backbone &middot; encoder + LoRA
</div>
<h1 style="
margin: 0;
font-size: 28px;
font-weight: 700;
color: {_INK};
letter-spacing: -0.02em;
">
Fraud Pattern Co-Pilot
</h1>
</div>
"""
def render_cast_strip(
cast: list[FraudPatternCastMember],
selected_idx: int,
) -> str:
cards: list[str] = []
for i, m in enumerate(cast):
is_sel = i == selected_idx
accent = STAGE_COLORS[m.stage_label]
border = accent if is_sel else _BORDER
bg = "#ffffff" if not is_sel else _hex_with_alpha(accent, 0.06)
cards.append(f"""
<div style="
flex: 1 1 0;
min-width: 0;
padding: 12px 14px;
background: {bg};
border: 1.5px solid {border};
border-radius: 10px;
">
<div style="
font-size: 13px;
font-weight: 600;
color: {_INK};
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
">{html.escape(m.display_name)}</div>
<div style="
font-size: 11px;
color: {_INK_DIM};
font-family: 'JetBrains Mono', ui-monospace, monospace;
text-transform: uppercase;
letter-spacing: 0.04em;
">
<span style="color: {accent};">●</span>
{html.escape(m.stage_label_name)} / {html.escape(m.type_label_name)}
</div>
</div>
""")
return f"""
<div style="display: flex; gap: 10px; margin-bottom: 18px;">
{''.join(cards)}
</div>
"""
def render_context(member: FraudPatternCastMember) -> str:
accent = STAGE_COLORS[member.stage_label]
return f"""
<div style="
background: {_CARD_BG};
border: 1px solid {_BORDER};
border-left: 4px solid {accent};
border-radius: 12px;
padding: 22px 26px;
margin-bottom: 18px;
">
<div style="
font-size: 11px;
color: {_INK_DIM};
font-family: 'JetBrains Mono', ui-monospace, monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 10px;
">
Fraud queue context &middot; pattern:
<span style="color: {_INK}; font-weight: 600;">{html.escape(member.pattern)}</span>
</div>
<div style="
font-size: 18px;
color: {_INK};
line-height: 1.5;
font-style: italic;
">
&ldquo;{html.escape(member.context_text)}&rdquo;
</div>
<div style="
font-size: 13px;
color: {_INK_DIM};
margin-top: 12px;
line-height: 1.5;
">
{html.escape(member.description)}
</div>
</div>
"""
def render_timeline(
flagged_idx: int,
top_k_positions: Iterable[int] | None = None,
attribution_probs: Iterable[float] | None = None,
num_positions: int = 64,
) -> str:
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 == flagged_idx:
dots.append(_dot_flagged(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"""
<div style="
background: {_CARD_BG};
border: 1px solid {_BORDER};
border-radius: 12px;
padding: 20px 22px;
margin-bottom: 18px;
">
<div style="
font-size: 11px;
color: {_INK_DIM};
font-family: 'JetBrains Mono', ui-monospace, monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 14px;
display: flex;
justify-content: space-between;
">
<span>64-transaction history &middot; oldest → newest</span>
<span>
<span style="color: #ef4444;">★</span> flagged &nbsp;
<span style="color: #f59e0b;">●</span> contributed
</span>
</div>
<div style="
display: flex;
justify-content: space-between;
align-items: center;
gap: 2px;
">
{''.join(dots)}
</div>
</div>
"""
def _dot_flagged(i: int) -> str:
return f"""
<div title="position {i} &middot; flagged" style="
width: 14px; height: 14px;
color: #ef4444;
font-size: 18px;
line-height: 14px;
text-align: center;
font-weight: 700;
">★</div>
"""
def _dot_attributed(i: int, alpha: float) -> str:
glow = max(0.4, min(1.0, alpha))
return f"""
<div title="position {i} &middot; contributed ({alpha:.2f})" style="
width: 12px; height: 12px;
border-radius: 50%;
background: rgba(245, 158, 11, {glow});
box-shadow: 0 0 8px rgba(245, 158, 11, {glow * 0.7});
"></div>
"""
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"""
<div title="position {i}" style="
width: 8px; height: 8px;
border-radius: 50%;
background: {bg};
"></div>
"""
def render_two_dist_scoreboard(result: FraudPatternResult | None) -> str:
"""Stage 5-bar + type 4-bar side-by-side scoreboard."""
if result is None:
return _scoreboard_placeholder()
stage_rows = _bars_for(
probs=result.stage_probs,
names=STAGE_NAMES,
colors=STAGE_COLORS,
winning_idx=result.predicted_stage,
)
type_rows = _bars_for(
probs=result.type_probs,
names=TYPE_NAMES,
colors=TYPE_COLORS,
winning_idx=result.predicted_type,
)
stage_color = STAGE_COLORS[result.predicted_stage]
type_color = TYPE_COLORS[result.predicted_type]
return f"""
<div style="
background: {_CARD_BG};
border: 1px solid {_BORDER};
border-radius: 12px;
padding: 22px 26px;
height: 100%;
box-sizing: border-box;
">
<div style="
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 16px;
">
<div style="
font-size: 11px;
color: {_INK_DIM};
font-family: 'JetBrains Mono', ui-monospace, monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
">
Pattern verdict
</div>
<div style="display: flex; gap: 8px;">
<div style="
display: inline-block;
padding: 4px 12px;
background: {_hex_with_alpha(stage_color, 0.12)};
color: {stage_color};
font-size: 12px;
font-weight: 700;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.04em;
">
stage: {html.escape(STAGE_NAMES[result.predicted_stage])}
&middot; {result.stage_probs[result.predicted_stage]:.2f}
</div>
<div style="
display: inline-block;
padding: 4px 12px;
background: {_hex_with_alpha(type_color, 0.12)};
color: {type_color};
font-size: 12px;
font-weight: 700;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.04em;
">
type: {html.escape(TYPE_NAMES[result.predicted_type])}
&middot; {result.type_probs[result.predicted_type]:.2f}
</div>
</div>
</div>
<div style="display: flex; gap: 20px;">
<div style="flex: 1;">
<div style="
font-size: 10px;
color: {_INK_DIM};
font-family: 'JetBrains Mono', ui-monospace, monospace;
text-transform: uppercase;
margin-bottom: 8px;
letter-spacing: 0.04em;
">Stage distribution</div>
{''.join(stage_rows)}
</div>
<div style="flex: 1;">
<div style="
font-size: 10px;
color: {_INK_DIM};
font-family: 'JetBrains Mono', ui-monospace, monospace;
text-transform: uppercase;
margin-bottom: 8px;
letter-spacing: 0.04em;
">Type distribution</div>
{''.join(type_rows)}
</div>
</div>
</div>
"""
def _bars_for(
probs: np.ndarray,
names: list[str],
colors: dict[int, str],
winning_idx: int,
) -> list[str]:
rows: list[str] = []
for i, (name, p) in enumerate(zip(names, probs, strict=True)):
accent = colors[i]
is_win = i == winning_idx
bar_w = max(2, int(float(p) * 100))
rows.append(f"""
<div style="
display: flex;
align-items: center;
gap: 8px;
padding: 5px 0;
border-bottom: 1px solid {_BORDER};
">
<div style="
width: 95px;
font-size: 11px;
font-weight: {600 if is_win else 500};
color: {_INK if is_win else _INK_DIM};
font-family: 'JetBrains Mono', ui-monospace, monospace;
">
{'★ ' if is_win else '&nbsp;&nbsp; '}{html.escape(name)}
</div>
<div style="
flex: 1;
height: 10px;
background: rgba(0,0,0,0.05);
border-radius: 5px;
position: relative;
overflow: hidden;
">
<div style="
position: absolute;
left: 0; top: 0; bottom: 0;
width: {bar_w}%;
background: {accent};
border-radius: 5px;
"></div>
</div>
<div style="
width: 36px;
text-align: right;
font-size: 11px;
font-weight: 600;
color: {accent};
font-family: 'JetBrains Mono', ui-monospace, monospace;
">
{float(p):.2f}
</div>
</div>
""")
return rows
def _scoreboard_placeholder() -> str:
return f"""
<div style="
background: {_CARD_BG};
border: 1px solid {_BORDER};
border-radius: 12px;
padding: 22px 26px;
height: 100%;
box-sizing: border-box;
color: {_INK_DIM};
">
<div style="
font-size: 11px;
font-family: 'JetBrains Mono', ui-monospace, monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 16px;
">
Pattern verdict
</div>
<div style="font-size: 14px; color: {_INK_DIM};">
Click <b>Analyze</b> to see pattern stage + type distributions.
</div>
</div>
"""
def render_reasoning(text: str | None) -> str:
if text is None:
body = f"""
<div style="font-size: 14px; color: {_INK_DIM}; font-style: italic;">
Reasoning will appear here once the model has read the customer's history.
</div>
"""
else:
body = f"""
<div style="
font-size: 14px;
color: {_INK};
line-height: 1.6;
white-space: pre-wrap;
font-family: ui-monospace, 'JetBrains Mono', monospace;
">{html.escape(text)}<span style="
display: inline-block;
width: 7px;
height: 14px;
background: {_INK};
margin-left: 2px;
vertical-align: text-bottom;
animation: copilot-blink 1s infinite;
"></span></div>
<style>
@keyframes copilot-blink {{
0%, 49% {{ opacity: 1; }}
50%, 100% {{ opacity: 0; }}
}}
</style>
"""
return f"""
<div style="
background: {_CARD_BG};
border: 1px solid {_BORDER};
border-radius: 12px;
padding: 22px 26px;
height: 100%;
box-sizing: border-box;
">
<div style="
font-size: 11px;
color: {_INK_DIM};
font-family: 'JetBrains Mono', ui-monospace, monospace;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 16px;
">
Reasoning
</div>
{body}
</div>
"""
def _hex_with_alpha(hex_color: str, alpha: float) -> str:
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})"