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