| """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’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’ ' |
| "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 & prizes this Space might claim but hasn’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…</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>" |
| ) |
|
|