ready-to-submit / presenters.py
marinarosa's picture
Fix space dropdown swap target, add next-step arrow cue
f79d783
Raw
History Blame Contribute Delete
8.03 kB
"""HTML fragment rendering — pure f-strings + html.escape, vivamais-style.
Every fragment the frontend swaps in is built here, so the routes in app.py
stay thin and the look lives in one place next to styles.css.
"""
from __future__ import annotations
import html
import re
from checks import Evaluation, PASS, WARN, FAIL
from guide import TAG_GLYPHS, TAG_SHORT_NAMES
from icons import ICON_PATHS
_esc = html.escape
STATUS_META = {
PASS: ("check", "ok", "all good"),
WARN: ("triangle-alert", "warn", "worth a look"),
FAIL: ("x", "fix", "needs fixing"),
}
VERDICT_META = {
"READY TO SUBMIT": ("ok", "star",
"Everything checks out — pitch your tent and submit it!"),
"ALMOST READY": ("warn", "compass",
"So close! No rule failures, just a few things worth a look below."),
"NOT READY": ("fix", "tent",
"Not there yet — a few trail markers to hit first. You've got this."),
}
def icon(name: str, cls: str = "", size: int = 18, sw: float = 1.8) -> str:
return (
f'<svg class="ic {cls}" viewBox="0 0 24 24" width="{size}" height="{size}" '
f'fill="none" stroke="currentColor" stroke-width="{sw}" stroke-linecap="round" '
f'stroke-linejoin="round" aria-hidden="true">{ICON_PATHS[name]}</svg>'
)
def stamp(glyph: str, tone: str = "rose", size: int = 44, spin: bool = False) -> str:
"""Mini replica of the field guide's StampBadge: disc + dashed ring + glyph."""
return (
f'<span class="stamp stamp--{tone} {"stamp--spin" if spin else ""}" '
f'style="--stamp-size:{size}px">'
f'<svg viewBox="0 0 132 132" width="{size}" height="{size}" aria-hidden="true">'
f'<circle cx="66" cy="66" r="63" class="stamp__disc"/>'
f'<circle cx="66" cy="66" r="63" fill="none" class="stamp__ring" '
f'stroke-width="2.5" stroke-dasharray="2.5 6" stroke-linecap="round"/>'
f'<circle cx="66" cy="66" r="50" fill="none" class="stamp__inner" stroke-width="1.5"/>'
f'</svg><span class="stamp__face">{icon(glyph, size=int(size * 0.4), sw=1.7)}</span></span>'
)
def _inline_md(text: str) -> str:
"""Escape, then bring back `code` spans and clickable links."""
out = _esc(text)
out = re.sub(r"`([^`]+)`", r"<code>\1</code>", out)
out = re.sub(r"“([^”]*)”", r"<em>“\1”</em>", out)
out = re.sub(
r"(https?://[^\s<]+)",
r'<a href="\1" target="_blank" rel="noopener">\1</a>',
out,
)
return out
def render_space_options(names: list[str]) -> str:
if not names:
return '<option value="">(could not load the org — type a space name)</option>'
opts = "".join(f'<option value="{_esc(n)}">{_esc(n)}</option>' for n in names)
return f'<option value="" disabled selected>pick a space…</option>{opts}'
def render_error(message: str) -> str:
return (
f'<section class="card card--fix" id="step-checks">'
f'<div class="card__head">{stamp("x", "rose", 40)}'
f'<h2>Hmm, that didn&rsquo;t work</h2></div>'
f'<p class="card__lede">{_inline_md(message)}</p></section>'
)
def render_results(ev: Evaluation, model_id: str, model_label: str) -> str:
"""Block 1 (rule check) + Block 2 shell (LLM review, filled by bridge.js)."""
next_cue = (
'<button id="next-step-cue" class="next-cue" type="button" '
'aria-label="Scroll to the friendly review">'
f'{icon("arrow-down", size=22, sw=2.2)}</button>'
)
return (_render_checks_block(ev)
+ render_review_shell(ev.space_id, model_id, model_label)
+ next_cue)
def _render_checks_block(ev: Evaluation) -> str:
tone, glyph, blurb = VERDICT_META[ev.verdict]
rows = "".join(_check_row(c.rule, c.status, c.evidence) for c in ev.checks)
chips = _tag_chips(ev)
opps = _opportunities(ev)
return (
f'<section class="card" id="step-checks">'
f'<span class="eyebrow">step 1 · rule check</span>'
f'<div class="card__head card__head--{tone}">{stamp(glyph, _tone_color(tone), 52)}'
f'<div><h2>{_esc(ev.verdict.title())}</h2>'
f'<p class="card__lede">{_esc(blurb)}</p>'
f'<p class="card__sub mono">{_esc(ev.space_id)}</p></div></div>'
f'<ul class="checks">{rows}</ul>'
f"{chips}{opps}"
f"</section>"
)
def _tone_color(tone: str) -> str:
return {"ok": "mint", "warn": "butter", "fix": "rose"}[tone]
def _check_row(rule: str, status: str, evidence: str) -> str:
glyph, tone, label = STATUS_META[status]
return (
f'<li class="check check--{tone}">'
f'<span class="check__mark">{icon(glyph, size=15, sw=2.4)}</span>'
f'<div class="check__body"><div class="check__top">'
f'<strong>{_inline_md(rule)}</strong>'
f'<span class="check__label">{label}</span></div>'
f'<p class="check__evidence">{_inline_md(evidence)}</p></div></li>'
)
def _tag_chips(ev: Evaluation) -> str:
managed = ev.facts.get("managed_tags") or {}
tags = managed.get("track", []) + managed.get("sponsor", []) + managed.get("achievement", [])
known = [t for t in tags if t in TAG_GLYPHS]
if not known:
return (
'<div class="chips"><span class="eyebrow">tags on this space</span>'
'<p class="chips__none">No tracks or badges tagged yet — the judges&rsquo; '
"tooling reads these from the README frontmatter.</p></div>"
)
chips = "".join(
f'<span class="chip">{stamp(TAG_GLYPHS[t], "rose" if t.startswith("track") else "lilac" if t.startswith("sponsor") else "mint", 34)}'
f'<span class="chip__text"><strong>{_esc(TAG_SHORT_NAMES[t])}</strong>'
f'<code>{_esc(t)}</code></span></span>'
for t in known
)
return f'<div class="chips"><span class="eyebrow">tags on this space</span><div class="chips__row">{chips}</div></div>'
def _opportunities(ev: Evaluation) -> str:
opps = ev.facts.get("opportunities") or []
if not opps:
return ""
items = "".join(
f'<li>{icon("sparkles", size=15, sw=1.9)}<span>{_inline_md(o)}</span></li>'
for o in opps
)
return (
f'<div class="opps"><span class="eyebrow">within reach</span>'
f'<p class="opps__lede">Badges &amp; prizes this Space might claim but hasn&rsquo;t yet:</p>'
f'<ul class="opps__list">{items}</ul></div>'
)
def render_review_shell(space_id: str, model_id: str, model_label: str) -> str:
return (
f'<section class="card card--review" id="step-review" data-state="waiting" '
f'data-space="{_esc(space_id)}" data-model="{_esc(model_id)}">'
f'<span class="eyebrow">step 2 · friendly review</span>'
f'<div class="card__head">{stamp("notebook-pen", "lilac", 52, spin=True)}'
f'<div><h2 id="review-title">{_esc(model_label)} is reading your Space&hellip;</h2>'
f'<p class="card__lede">Grounded in the rule check above — this takes a little '
f"longer the first time a model wakes up.</p></div></div>"
f'<div id="review-body" class="review__body review__body--waiting">'
f'<div class="shimmer"></div><div class="shimmer"></div><div class="shimmer shimmer--short"></div></div>'
f'<p class="disclaimer">{icon("triangle-alert", size=14, sw=2)} This is an automated '
f"+ AI evaluation — lovingly assembled, occasionally wrong. Double-check everything "
f'against the <a href="https://build-small-hackathon-field-guide.hf.space/" '
f'target="_blank" rel="noopener">official field guide</a> before you submit, '
f"regardless of what it says.</p></section>"
)
def render_review_chunk(markdown_html: str, done: bool) -> str:
"""The streamed inner body of Block 2 (already-rendered markdown HTML)."""
state = "done" if done else "streaming"
return (
f'<div id="review-body" class="review__body" data-state="{state}">'
f"{markdown_html}</div>"
)