promptstat / ui /components /card.py
xxixx1028's picture
Deploy PromptStat — UI shell + MiniCPM4.1-8B + 4-LoRA hybrid (Modal)
dc9f530 verified
Raw
History Blame Contribute Delete
4.63 kB
"""Render a `CardData` into the flip-card HTML (front + back) and the per-axis accordion bodies.
The radar canvas carries its data declaratively in `data-scores` / `data-labels`; the global
`__omcDrawRadar()` (in theme.HEAD) reads those after the result screen is shown. No inline
<script> (which Gradio would not execute).
"""
from __future__ import annotations
from html import escape
from ..data import CardData, Axis
from ..theme import TEAL
_AVATAR_SVG = (
"<svg width='30' height='30' viewBox='0 0 24 24' fill='none' stroke='#9AA0A8' "
"stroke-width='1.6'><circle cx='12' cy='8' r='4'/>"
"<path d='M4 20c0-4 4-6 8-6s8 2 8 6'/></svg>"
)
# honesty tag per scoring provenance: (css_class, label). real scoring drops the amber warning.
_NOTE = {
"placeholder": ("omc-placeholder", "⚗ demo scores — heuristic placeholder, not the final ML model"),
"real-base": ("omc-real", "✓ scored on MiniCPM-8B"),
"real-lora": ("omc-real", "✓ scored on MiniCPM-8B + LoRA hybrid"),
}
def _note_html(provenance: str, small: bool = False) -> str:
cls, label = _NOTE.get(provenance, _NOTE["placeholder"])
if small:
cls += "-sm"
return f"<div class='{cls}'>{label}</div>"
def _scope_note(card: CardData) -> str:
"""Honest scope disclaimer for single-conversation (paste) analysis."""
if not card.single_conversation:
return ""
return (
"<div class='omc-scope'><b>Sample analysis · single conversation</b>"
"<span>Score reflects this one conversation only — not your overall AI usage. "
"For a full skill profile, upload your ChatGPT/Claude export.</span></div>"
)
def _stars_html(ovr: float) -> str:
value = ovr / 2.0 # 0-10 -> 0-5 stars
cells = []
for i in range(5):
fill = max(0.0, min(1.0, value - i)) * 100
cells.append(
f"<span class='omc-star'>★<span class='fill' style='width:{fill:.0f}%'>★</span></span>"
)
return "<div class='omc-stars'>" + "".join(cells) + "</div>"
def _front(card: CardData) -> str:
letter, color = card.tier
scores = ",".join(str(s) for s in card.radar_scores())
labels = "|".join(a.name for a in card.axes)
return f"""
<div class="flip-card-face flip-card-front">
<div class="omc-avatar">{_AVATAR_SVG}</div>
<div class="omc-name">{escape(card.name)}</div>
<div class="omc-tier-wrap">
<span class="omc-tier" style="background:{color}22;color:{color};border:1px solid {color}55;">TIER {letter}</span>
</div>
<div class="omc-ovr">{card.overall:.1f}<span class="slash"> /10</span></div>
{_stars_html(card.overall)}
{_note_html(card.provenance)}
{_scope_note(card)}
<div class="omc-radar-wrap">
<canvas id="omc-radar" data-scores="{scores}" data-labels="{escape(labels)}"></canvas>
</div>
<div class="flip-hint">tap to flip ↻</div>
</div>"""
def _bar(axis: Axis) -> str:
pct = axis.score * 10
return f"""
<div class="omc-bar-row">
<div class="omc-bar-head"><span>{escape(axis.name)}</span><span class="v">{axis.score:.0f}/10</span></div>
<div class="omc-bar"><div class="fill" style="width:{pct:.0f}%"></div></div>
</div>"""
def _back(card: CardData) -> str:
bars = "".join(_bar(a) for a in card.axes)
crit = " · ".join(f"<b>{v}</b> {escape(k)}" for k, v in card.critical.as_pairs())
low = [a.name for a in card.axes if a.confidence == "low"]
conf = ("Lower-confidence axes: " + ", ".join(low) + ".") if low else "All axes high confidence."
return f"""
<div class="flip-card-face flip-card-back">
<div class="omc-back-title">Axis breakdown</div>
{bars}
<div class="omc-crit"><b>Critical breakdown</b><br>{crit}</div>
<div class="omc-conf">{escape(conf)}</div>
{_note_html(card.provenance)}
<div class="omc-improve">{escape(card.improvement)}</div>
<div class="flip-hint">tap to flip ↻</div>
</div>"""
def render_card(card: CardData) -> str:
return (
'<div class="flip-card"><div class="flip-card-inner">'
+ _front(card) + _back(card)
+ "</div></div>"
)
def render_accordion_body(axis: Axis, provenance: str = "placeholder") -> str:
quotes = "".join(f"<span class='q'>{escape(q)}</span>" for q in axis.evidence)
return (
f"<div class='omc-evidence'>"
f"{_note_html(provenance, small=True)}"
f"<span class='score'>{axis.score:.0f}/10</span> · confidence: {escape(axis.confidence)}"
f"{quotes}<div class='tip'>💡 {escape(axis.tip)}</div></div>"
)