"""The Computer use tab: real runs of blink driving apps from screenshots, then how one step works.
Everything here is HTML built from space/cua/gallery.json (see build_gallery.py) and space/cua/pipeline.json
(build_pipeline.py: one recorded step re-rendered from its raw screen and checked against the run), plus one
script that plays it: the stage (a strip of runs, or one run in the player), the scenario gallery, how a step
goes from the raw screen to blink's decision, and the tiles beyond clicks. Nothing here calls a model; Screen
click, which does, sits between that explainer and the tiles (app.py). Every word the tab shows is in COPY.
"""
from __future__ import annotations
import functools
import html
import json
import math
import os
import urllib.parse
import screens
HERE = os.path.dirname(os.path.abspath(__file__))
GALLERY = os.path.join(HERE, "cua", "gallery.json")
SCHEMA = "blink-cua-gallery/1"
PIPELINE = os.path.join(HERE, "cua", "pipeline.json")
PIPELINE_SCHEMA = "blink-cua-pipeline/1"
TAB = "computer-use"
PARAMS = ("scenario", "model", "run", "step")
# Every string the tab shows, in one place, so the words can change without touching the code.
# Placeholders in braces are filled in by the page; keep them.
COPY = {
"title": "Watch blink use ten apps",
"pitch": "Each app's own page supplies the boxes; no detector model finds them. blink sees them numbered "
"on a screenshot and answers three questions.",
"how_link": "How a step works",
"mock": "Test data, not real blink runs.",
"stat_apps": "original apps",
"stat_runs": "of {n} runs completed",
"stat_step": "median per step",
"stat_gate": "risky clicks paused on",
"runs": "{ok}/{n} runs",
"open": "Watch",
"gallery": "Pick an app",
"phone": "phone",
"canvas": "canvas app",
"turns": "turn-based",
# the player
"close": "Back to apps",
"video": "Video",
"steps": "Steps",
"play": "Play steps",
"pause": "Pause",
"prev": "Previous step",
"next": "Next step",
"step_of": "Step {n}/{total}",
"finished": "Task done",
"failed": "Task not done",
"steps_taken": "{n} steps",
"steps_taken_one": "1 step",
"asked": "{n} times asked first",
"per_step": "{ms} ms per step",
"interval": "95% interval {lo} to {hi}%",
"matched": "{pct}% picked the right box",
"show_failed": "Watch a miss",
"show_best": "Watch a finish",
"show_other": "Watch another",
"seed": "seed {seed}",
"expected": "Right box: {n}",
"click": "Click {n}",
"lane": "Lane {n}",
"ask": "Ask before clicking {n}",
"type": "Type {value} in {n}",
"select": "Pick {value} in {n}",
"done": "Looks done",
"none": "Nothing to click",
"done_q": "Done?",
"risky_q": "Risky?",
"ms": "{ms} ms",
"tokens": "{n} image tokens",
"models": "Same tasks, other models",
"task": "Task",
"no_video": "No video here. The steps still work.",
"no_steps": "Steps unavailable. The video is still here.",
# what blink was sent at a step, under its answers
"in_title": "What blink got",
"in_shot": "Screenshot",
"in_dom": "{n} numbered boxes, listed by the page's DOM",
"in_canvas": "{n} numbered boxes, from the app's own hit regions",
"in_prev": "Earlier actions",
"in_none": "None yet",
"in_more": "+{n} earlier",
"in_asked": "Questions",
"in_second": "Then, for box {n}",
"in_boxes": "boxes 1\u2013{n}",
"in_yesno": "yes or no",
"in_options": "{n} options",
"in_picked": "picked",
"in_tokens": "{n} input tokens over every pass, 0 generated",
"in_names": "Names beside the numbers come from the page, for you to read. blink is not sent them: it gets "
"the numbered screenshot, and a name only inside its own earlier actions.",
"in_json": "Request JSON",
"in_json_second": "Second request, after the box",
"in_image": "data:image/png;base64,\u2026 (the numbered screenshot)",
# how it reads a screenshot
"how_eyebrow": "From screenshot to click",
"how_line": "One pass per question. A probability for every answer, no text generated.",
"how_nodes": (
("Marked screenshot", "Boxes show what can be clicked."),
("Vision encoder", "{base}, unchanged by blink training."),
("Image tokens", "One per {side} \u00d7 {side} px patch."),
("Three questions", "Next box, done yet, risky click."),
("Probabilities", "One for every offered answer."),
("Next step", "Click, pause, stop, or hand over."),
),
"how_facts": (
"{width} \u00d7 {height} px",
"{blocks} blocks \u00b7 hidden {hidden}",
"{tokens}",
"3 questions",
"one forward pass each",
"{outcomes}",
),
"how_outcomes": "click \u00b7 pause \u00b7 done \u00b7 hand over",
"how_questions": ("Which box?", "Done?", "Risky?"),
# how one step works: a recorded step from its raw screen to blink's decision (pipeline.json)
"pipe_title": "How one step works",
"pipe_line": "Step {step} of a recorded {title} run, from the raw screen to blink's decision. Everything "
"before blink is ordinary browser automation, so blink is the only model in the loop.",
"pipe_before": "Before blink: no model",
"pipe_inside": "Inside blink: one forward pass per question",
"pipe_nodes": (
("Raw screen", "Playwright opens the app and screenshots it."),
("What can be clicked", ("One query to the page's DOM lists its links, buttons, fields, menus and other "
"controls, with positions. Hidden, disabled and covered ones drop out.")),
("Numbered boxes", ("Each gets a numbered box, drawn into the pixels. This picture is all blink sees of "
"the page.")),
("The request", ("The picture, the task, blink's earlier actions and three questions. No page code, "
"element list or answer key.")),
("Image tokens", ("Each model's base vision encoder, unchanged by blink training, makes one token per "
"{side} \u00d7 {side} px patch.")),
("Three questions", ("Each is one forward pass. The options get letters, and blink scores the letters "
"instead of writing.")),
("Probabilities", "One for every offered answer."),
("Decision", ("Done at {done_at} stops; risky at {risky_at} asks first; otherwise the top box is "
"clicked. A field gets one more question for its value.")),
),
"pipe_facts": (
"{width} \u00d7 {height} px \u00b7 {shot_ms}",
"{kept} elements \u00b7 {dom_ms}",
"boxes 1\u2013{kept} \u00b7 drawn in {draw_ms}",
"{prev} \u00b7 3 questions",
"{tokens} image tokens",
"{input_tokens} input tokens in 3 passes \u00b7 0 generated",
"{agree}",
"{act} \u00b7 {lead} {ms}",
),
"pipe_prev": ("no earlier actions", "1 earlier action", "{n} earlier actions"),
"pipe_agree": "all {n} models pick box {pick}",
"pipe_split": "{lead} picks box {pick}",
"pipe_raw_alt": "The app as Playwright captured it, before any marks",
"pipe_marked_alt": "The same screen with numbered boxes, as blink got it",
"pipe_src_title": "Where the boxes come from",
"pipe_src": (
(("{dom} app with HTML controls", "{dom} apps with HTML controls"),
"The page's DOM, read in the browser by one query."),
(("{canvas} app drawn on a canvas", "{canvas} apps drawn on a canvas"),
"Each app reports where it drew its targets: {names}."),
(("A screenshot of your own", "A screenshot of your own"),
"You draw the boxes, or a numbered grid stands in. Try it below."),
),
"pipe_none": "No detector, OCR or second vision model in any of them.",
"pipe_time": "Times before blink are medians from a local re-run of every app's first screen ({apps} apps, "
"{reps} passes each); the recorded runs do not time those stages. Drawing the boxes is quick: the "
"whole marking step, mostly saving the picture as a PNG, takes about {mark}. blink's time is the "
"recorded call for all three questions.",
"pipe_open": "Step through this run",
"pipe_request": "The exact request for this step",
"pipe_request_note": "One request carries all three questions. The screenshot field holds the numbered "
"picture as a PNG; nothing else is sent.",
"pipe_list": "What the page listed",
"pipe_list_note": "The harness's DOM query found these {kept} elements, in reading order. Their roles and "
"names are shown here for you; blink is not sent them.",
"pipe_cols": ("Box", "Role", "Name", "Pixels (x1, y1, x2, y2)"),
"pipe_check": "Re-rendered from the app, seed and task of the recorded run: the page listed the same "
"{kept} boxes, and marking this raw screen reproduces the screenshot every model was sent, "
"pixel for pixel.",
# try it
"try_title": "Try your screenshot",
# beyond clicks
"more_eyebrow": "Beyond clicks",
"more_line": "Voice, audio, screen checks, and document pages.",
"sound": "Play with sound",
}
def esc(s) -> str:
return html.escape(str(s), quote=True)
# --- the gallery -----------------------------------------------------------------------
def path() -> str:
"""BLINK_GALLERY points the page at another build (a mock, a local check); the Space ships cua/gallery.json."""
return os.environ.get("BLINK_GALLERY") or GALLERY
@functools.lru_cache(maxsize=4)
def _load(where: str, stamp: float):
with open(where, encoding="utf-8") as fh:
g = json.load(fh)
if g.get("schema") != SCHEMA or not g.get("scenarios"):
return None
return g
def load() -> dict | None:
"""The gallery, or None when there is none to show (the tab then leads with the explainer and Screen click)."""
where = path()
try:
stamp = os.path.getmtime(where)
except OSError:
return None
try:
return _load(where, stamp)
except (OSError, ValueError):
return None
def available() -> bool:
return load() is not None
def media(g: dict, rel: str | None) -> str:
return g["media_base"] + urllib.parse.quote(rel) if rel else ""
def scenario(g: dict, sid: str) -> dict | None:
return next((s for s in g["scenarios"] if s["id"] == sid), None)
def runs_line(st: dict) -> str:
return COPY["runs"].format(ok=st["successes"], n=st["episodes"])
# --- deep links ---------------------------------------------------------------------
def parse_link(raw, g: dict | None = None) -> dict:
"""?tab=computer-use&scenario=shop&model=blink-4b&run=failed&step=3 -> what the player opens.
Unknown scenarios, models and steps fall back rather than fail: a stale link still opens the tab."""
g = g if g is not None else load()
out = {"scenario": None, "model": None, "run": "showcase", "step": None}
if not isinstance(raw, str) or g is None:
return out
params = urllib.parse.parse_qs(raw.partition("#")[0].lstrip("?"))
asked = (params.get("scenario") or [""])[0].strip().lower()
sc = scenario(g, asked)
if sc is None:
return out
out["scenario"] = sc["id"]
models = [m for m in g["models"] if m in sc["models"]]
want = (params.get("model") or [""])[0].strip()
lead = g.get("lead_model") or g["vision_model"]
out["model"] = want if want in models else (lead if lead in models else models[0])
item = sc["models"][out["model"]]
if (params.get("run") or [""])[0].strip().lower() in ("failed", "failure") and item.get("failure"):
out["run"] = "failure"
ep = g["episodes"][item["showcase" if out["run"] == "showcase" else "failure"]]
try:
step = int((params.get("step") or [""])[0])
except ValueError:
step = None
if step is not None and ep["steps"]:
out["step"] = min(max(step, 1), len(ep["steps"]))
return out
# --- the stage: a strip of runs --------------------------------------------------------
def _video(g: dict, ep: dict, *, auto: bool, cls: str = "") -> str:
poster = media(g, ep.get("poster"))
src = media(g, ep.get("preview") or ep.get("video"))
if not src:
return f'' if poster else ""
return (f'')
def hero_order(g: dict) -> list[str]:
"""The strip's runs: a phone between two screens where there is one, then the rest, in registry order."""
ids = [e for e in g.get("hero", []) if e in g["episodes"]]
phones = [e for e in ids if g["episodes"][e]["frame"][1] > g["episodes"][e]["frame"][0]]
wide = [e for e in ids if e not in phones]
if phones and len(wide) >= 2:
return [wide[0], phones[0], wide[1], *[e for e in ids if e not in (wide[0], phones[0], wide[1])]]
return ids
def _link(sid: str) -> str:
return "?" + urllib.parse.urlencode({"tab": TAB, "scenario": sid})
def hero(g: dict) -> str:
reels = []
for i, eid in enumerate(hero_order(g)):
ep = g["episodes"][eid]
sc = scenario(g, ep["scenario"])
st = sc["models"][ep["model"]]["stats"]
w, h = ep["frame"]
reels.append(
f' w else ""}" href="{esc(_link(sc["id"]))}" data-sid="{esc(sc["id"])}"'
f' style="--ar:{w} / {h};--i:{i}">{_video(g, ep, auto=True)}'
f'{esc(sc["title"])}{esc(runs_line(st))}'
)
ids = hero_order(g)[:3]
wide = all(g["episodes"][e]["frame"][0] > g["episodes"][e]["frame"][1] for e in ids)
return (f'
{"".join(reels)}
'
if reels else "")
def _pct(v: float | None) -> str:
return "\u2013" if v is None else f"{round(v * 100):d}%"
def stat_row(g: dict) -> str:
lead = g.get("lead_model") or g["vision_model"]
total = g["totals"].get(lead) or next(iter(g["totals"].values()), None)
if not total:
return ""
who = lead.rsplit("/", 1)[-1] if lead in g["totals"] else ""
items = [(str(total["scenarios"]), COPY["stat_apps"]),
(_pct(total["rate"]), COPY["stat_runs"].format(n=total["episodes"]) + (f" \u00b7 {who}" if who else ""))]
if total.get("p50_ms"):
items.append((f"{total['p50_ms'] / 1000:.1f} s", COPY["stat_step"]))
if total.get("gate_recall") is not None:
items.append((_pct(total["gate_recall"]), COPY["stat_gate"]))
return ('
'
+ "".join(f'{esc(v)}{esc(k)}' for i, (v, k) in enumerate(items))
+ "
")
def _dots(outcomes: list[dict]) -> str:
return (''
+ "".join(f'' for o in outcomes) + "")
def cards(g: dict) -> str:
out = []
for i, sc in enumerate(g["scenarios"]):
lead = g.get("lead_model") or g["vision_model"]
model = lead if lead in sc["models"] else next(iter(sc["models"]))
item = sc["models"][model]
ep = g["episodes"].get(item["showcase"] or "")
if ep is None:
continue
w, h = ep["frame"]
# the game waits for each decision (the harness lockstep clock), so its card says so
tag = (COPY["phone"] if sc["device"] == "phone" else COPY["turns"] if sc["kind"] == "game"
else COPY["canvas"] if sc["kind"] != "dom" else "")
out.append(
f''
f' w else ""}">{_video(g, ep, auto=False)}'
+ (f'{esc(tag)}' if tag else "")
+ f'{esc(sc["title"])}'
f'{_dots(item["outcomes"])}{esc(runs_line(item["stats"]))}'
f'{esc(model)}'
)
return (f'
{esc(COPY["gallery"])}
'
f'')
# --- how it reads a screenshot -------------------------------------------------------------
def explainer(facts: dict, thumb: str = "", answers: dict | None = None, outcome: str = "") -> str:
"""Six nodes, told in order when they come into view: the numbered screenshot, the vision encoder,
the image tokens laid over the screenshot as the patches they are, the typed questions, their
probabilities and the action. `facts` are ui.vision_facts(); `answers` a saved run's, when there is one."""
tokens = facts.get("tokens") or 0
side = facts.get("side") or 32
width, height = facts.get("width") or 1440, facts.get("height") or 864
cols, rows = max(1, math.ceil(width / side)), max(1, math.ceil(height / side))
values = dict(facts, tokens=f"{tokens:,} image tokens" if tokens else "image tokens",
side=side, width=width, height=height, outcome=outcome or COPY["click"].format(n="N"),
outcomes=COPY["how_outcomes"])
probs = []
if answers:
element = answers.get("element", {})
top = sorted(element.get("probabilities", {}).items(), key=lambda kv: -kv[1])[:3]
probs = [(f"box {k}", v, k == element.get("choice")) for k, v in top]
probs += [(COPY["done_q"], answers.get("done", {}).get("noul"), False),
(COPY["risky_q"], answers.get("risky", {}).get("noul"), False)]
win_class = ' class="win"'
bars = "".join(
f'
{esc(k)}'
f'{esc(_pct(p))}
' for i, (k, p, win) in enumerate(probs))
art = [
f'' if thumb else '',
'' + "".join(f'' for k in range(int(facts.get("blocks") or 27)))
+ "",
(f''
+ (f'' if thumb else "") + ''
+ f'{tokens:,}'),
'' + "".join(f'{esc(q)}'
for k, q in enumerate(COPY["how_questions"])) + "",
f'
{bars}
',
f'{esc(values["outcome"])}',
]
nodes = []
for i, ((label, caption), fact, figure) in enumerate(zip(COPY["how_nodes"], COPY["how_facts"], art, strict=True)):
nodes.append(
f'
{"".join(nodes)}')
# --- how one step works: a recorded step from its raw screen to blink's decision ----------------------
def pipeline_path() -> str:
"""BLINK_PIPELINE points the page at another record; the Space ships cua/pipeline.json."""
return os.environ.get("BLINK_PIPELINE") or PIPELINE
@functools.lru_cache(maxsize=4)
def _load_pipeline(where: str, stamp: float):
with open(where, encoding="utf-8") as fh:
p = json.load(fh)
return p if p.get("schema") == PIPELINE_SCHEMA else None
def load_pipeline() -> dict | None:
"""The re-rendered step build_pipeline.py checked against its run, or None."""
where = pipeline_path()
try:
return _load_pipeline(where, os.path.getmtime(where))
except (OSError, ValueError):
return None
def pipeline_fits(g: dict | None, p: dict | None) -> bool:
"""Whether the record describes a scenario this gallery shows, with answers to draw."""
if not g or not p:
return False
try:
return (scenario(g, p["scenario"]) is not None and bool(p["answers"]) and bool(p["candidates"])
and all(a.get("p") and len(a["p"]) == len(p["candidates"]) for a in p["answers"].values()))
except (KeyError, TypeError):
return False
def _ms(v) -> str:
v = float(v)
return f"{v / 1000:.1f} s" if v >= 1000 else f"{max(1, round(v)):d} ms"
def _box_style(b, frame) -> str:
w, h = frame
return (f"--x:{b[0] / w * 100:.3f}%;--y:{b[1] / h * 100:.3f}%;"
f"--w:{(b[2] - b[0]) / w * 100:.3f}%;--h:{(b[3] - b[1]) / h * 100:.3f}%")
def pipeline(g: dict, p: dict, try_here: bool = True) -> str:
"""How one step works: the recorded step in pipeline.json, from its raw screen to blink's decision.
The first row is the browser's work (no model): the raw screen, the DOM's list of what can be clicked,
the numbered boxes and the request. The second is blink's: image tokens, one forward pass per question,
the probabilities and the decision. Then where boxes come from in every app, the exact request, and the
page's list. Every number comes from pipeline.json (checked against the run when it was built) or the
gallery; the nodes are told in order when they come into view, like explainer()'s. `try_here` says Screen
click follows on the page, so a screenshot of one's own gets its line."""
sc = scenario(g, p["scenario"])
models = [m for m in g["models"] if m in p["answers"]] or list(p["answers"])
lead = g.get("lead_model") if g.get("lead_model") in p["answers"] else models[0]
a = p["answers"][lead]
frame = p["frame"]
width, height = frame
kept = len(p["candidates"])
tokens = int(a.get("visual_tokens") or 0)
side = math.isqrt(int(a["image_pixels"]) // tokens) if tokens and a.get("image_pixels") else 32
t = p["timing"]
state = p["request"]["state"]
questions = p["request"]["questions"]
prev = len(state.get("previous_actions") or [])
pick = a["pick"]
picks = {p["answers"][m]["pick"] for m in models}
act = (COPY["done"] if a.get("act") == "done" else COPY["click"].format(n=pick) if pick else COPY["none"])
values = {
"step": p["step"], "title": sc["title"], "width": width, "height": height, "side": side,
"shot_ms": _ms(t["screenshot_ms"]["p50"]), "dom_ms": _ms(t["dom_ms"]["p50"]),
"draw_ms": _ms(t["draw_ms"]["p50"]), "kept": kept,
"prev": COPY["pipe_prev"][min(prev, 2)].format(n=prev),
"tokens": f"{tokens:,}", "input_tokens": f"{int(a.get('input_tokens') or 0):,}",
"agree": (COPY["pipe_agree"].format(n=len(models), pick=pick) if len(picks) == 1 and len(models) > 1
else COPY["pipe_split"].format(lead=lead, pick=pick)),
"act": act, "lead": lead, "ms": _ms(a["ms"]),
"done_at": _pct(screens.DONE_AT), "risky_at": _pct(screens.RISKY_AT),
}
raw, marked = media(p, p["raw"]), media(p, p["marked"])
# 1-4: the browser's work, drawn from the real screen
outlines = "".join(f''
for k, c in enumerate(p["candidates"]))
lines = [("task", f"\u201c{state.get('task', '')}\u201d"),
("previous_actions", json.dumps(state.get("previous_actions") or [], ensure_ascii=False)),
("screenshot_note", f"\u201c{state.get('screenshot_note', '')}\u201d")]
asked = " \u00b7 ".join(COPY["how_questions"])
request_art = (''
+ "".join(f"{esc(k)} {esc(v)}" for k, v in lines)
+ 'screenshot[image 1]'
+ f'questions {esc(asked)}')
before = [
(f''
f''),
(f''
f'{outlines}{kept}'),
(f''
f''),
request_art,
]
# 5-8: blink's work, as explainer() draws it
cols, rows = max(1, math.ceil(width / side)), max(1, math.ceil(height / side))
ranked = sorted(range(kept), key=lambda i: -a["p"][i])[:3]
probs = [(f"box {i + 1}", a["p"][i], i + 1 == pick) for i in ranked]
probs += [(COPY["done_q"], a.get("done"), False), (COPY["risky_q"], a.get("risky"), False)]
win_class = ' class="win"'
bars = "".join(
f'
{esc(k)}'
f'{esc(_pct(v))}
' for i, (k, v, win) in enumerate(probs))
inside = [
(f''
f'{tokens:,}'),
'' + "".join(f'{esc(q)}'
for k, q in enumerate(COPY["how_questions"])) + "",
f'
{bars}
',
f'{esc(act)}',
]
def flow(arts, first: int, cls: str) -> str:
items = []
for i, art in enumerate(arts, first):
label, caption = COPY["pipe_nodes"][i]
items.append(
f'
')
return f'{"".join(items)}'
# where the boxes come from, in every app the gallery shows and in Screen click
kinds = [s.get("kind") or "dom" for s in g["scenarios"]]
drawn = [s["title"] for s in g["scenarios"] if (s.get("kind") or "dom") != "dom"]
src_values = {"dom": kinds.count("dom"), "canvas": len(drawn), "names": ", ".join(drawn)}
counts = (src_values["dom"], len(drawn), 2 if try_here else 0)
sources = "".join(
f'
'
for (heads, text), n in zip(COPY["pipe_src"], counts) if n)
# the exact request, and the page's own list of what it found
body = {"model": "blink", "state": {**state, "screenshot": COPY["in_image"]}, "questions": questions}
request_json = json.dumps(body, indent=2, ensure_ascii=False)
table = "".join(f'
{c["n"]}
{esc(c["role"])}
{esc(c["name"])}
'
f'
{esc(", ".join(str(v) for v in c["box"]))}
' for c in p["candidates"])
head_cells = "".join(f"
{esc(h)}
" for h in COPY["pipe_cols"])
item = sc["models"].get(lead) or {}
opener = (f''
if item.get("showcase") == p["episodes"].get(lead) else "")
timing = COPY["pipe_time"].format(apps=t["apps"], reps=t["reps"], mark=_ms(t["mark_ms"]["p50"]))
return (
f'
'
f'
{esc(COPY["pipe_title"])}
{opener}
'
f'
{esc(COPY["pipe_line"].format(**values))}
'
f'
{esc(COPY["pipe_before"])}
{flow(before, 0, "blk-cxp-big")}'
f'
{esc(COPY["pipe_inside"])}
{flow(inside, 4, "blk-cxp-model")}'
f'
{esc(COPY["pipe_src_title"])}
'
f'
{sources}
{esc(COPY["pipe_none"])}
'
f'
{esc(timing)}
'
f'
'
f'{esc(COPY["pipe_request"])}'
f'
{esc(COPY["pipe_request_note"])}
{esc(request_json)}
'
f'{esc(COPY["pipe_list"])}'
f'
{esc(COPY["pipe_list_note"].format(kept=kept))}
'
f'
{head_cells}
'
f'{table}
'
f'
{esc(COPY["pipe_check"].format(kept=kept))}
'
)
# --- beyond clicks -------------------------------------------------------------------
def tiles(g: dict | None) -> str:
items = (g or {}).get("more") or []
if not items:
return ""
out = []
for i, t in enumerate(items):
still = media(g, t.get("poster") or t.get("chart") or (t.get("images") or [None])[0])
metric = t.get("metric") or {}
out.append(
f'"
)
data = esc(json.dumps({"more": items, "media_base": g["media_base"], "copy": {"sound": COPY["sound"]}},
separators=(",", ":"), ensure_ascii=False))
return (f''
f'
{esc(COPY["more_eyebrow"])}
{esc(COPY["more_line"])}
'
f'
{"".join(out)}
')
# --- assembly -----------------------------------------------------------------------
def client_data(g: dict) -> dict:
"""What the page carries: every run's summary and step rail. Each run's steps are fetched from beside its
media when the player first needs them (build_gallery.py stages them), so the page stays light."""
keep = ("media_base", "mock", "models", "vision_model", "lead_model", "scenarios")
out = {k: g[k] for k in keep if k in g}
out["episodes"] = {eid: {k: v for k, v in ep.items() if k != "steps"} for eid, ep in g["episodes"].items()}
out["rule_cut"] = {"done_at": screens.DONE_AT, "risky_at": screens.RISKY_AT}
return out
def pad_for(width: int) -> int:
"""How far a page mark reaches past its box: screens.style's stroke and halo."""
stroke = max(2, round(width / 360))
return stroke + max(1, stroke // 2)
def top(g: dict | None, explain: str = "") -> str:
"""The tab above Screen click: the stage, the gallery, then how one step works."""
how = (f' '
if g is not None and 'id="cua-how"' in explain else "")
head = (f'
{esc(COPY["title"])}
'
f'
{esc(COPY["pitch"])}{how}
')
if g is None:
return f'
{head}{explain}
'
data = client_data(g)
for ep in data["episodes"].values():
ep.setdefault("pad", pad_for(ep["frame"][0]))
banner = f'
`
+ got(ep, s);
};
// what blink was sent at this step, under what it answered: the numbered screenshot, the task above, its own
// earlier actions and the questions, word for word from the run (build_gallery.step_view). Older galleries
// carry no questions, and then nothing is said rather than something guessed.
const optionsOf = (q) => {
const said = Object.values((q && q.criteria) || {});
if (q && q.type === 'noul') return C.in_yesno;
if (said.length && said.every((d, j) => d === `box ${j + 1}`)) return fmt(C.in_boxes, { n: said.length });
return fmt(C.in_options, { n: said.length });
};
const got = (ep, s) => {
const qs = s.questions || {};
const keys = Object.keys(qs);
if (!keys.length) return '';
const kind = (byId[ep.scenario] || {}).kind;
const prev = Array.isArray(s.previous_actions) ? s.previous_actions : [];
const recent = prev.slice(-3);
const main = keys.filter((k) => k !== 'value');
const ask = (k) => `