open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/room/index.html
Transaction Science 1739817397 Merge B1 #413 (the browser sink and contentBinding) into train/live-room
By meaning:
- Page: app.js keeps the age parameters and paragraph and adds debug=binding;
  imports, state and roomTest hooks of age, co and binding side by side.
  index.html carries the co section and the binding table, each with its CSS.
- room.e2e.mjs carries scenarioBinding whole beside scenarioAge and
  scenarioCo; default list `...,age,co` with a room, `binding` always;
  run.sh's node unit tests are every page module's (capsule, media,
  presentation, privacy-pass, binding).
- wai-live-web: the wai dependency takes co_performance and browser_sink;
  the crate doc lists both.
- joule-moq-web README: one node-test bullet for capsule, media and binding,
  then privacy-pass and ad-presentation; scenarios age, co and binding.
- REGISTRY prose names exploration and browser-sink together; the shared
  lists keep both sides; export regenerated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 03:45:27 -04:00

141 lines
8.2 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MoQ room client</title>
<link rel="icon" href="data:,">
<style>
:root { --bg: #f6f7f9; --fg: #15171a; --muted: #5d6470; --panel: #ffffff; --line: #d9dde3; --human: #1f6f43; --machine: #8a4b00; }
@media (prefers-color-scheme: dark) { :root { --bg: #111317; --fg: #e8eaee; --muted: #9aa2ae; --panel: #1a1d23; --line: #2c313a; --human: #6fd39b; --machine: #f0a957; } }
* { box-sizing: border-box; }
[hidden] { display: none !important; }
body { margin: 0; padding: 16px; background: var(--bg); color: var(--fg); font: 14px/1.45 system-ui, sans-serif; }
header { display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap; margin-bottom: 12px; }
h1 { font-size: 16px; margin: 0; }
#who { color: var(--muted); font-family: ui-monospace, monospace; }
main { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 12px; }
@media (max-width: 760px) { main { grid-template-columns: minmax(0, 1fr); } }
section { background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 10px; min-width: 0; }
canvas { width: 100%; height: auto; display: block; background: #101218; border-radius: 4px; }
#caption { min-height: 2.6em; margin-top: 8px; display: flex; gap: 8px; align-items: baseline; flex-wrap: wrap; }
#caption .origin { font-size: 12px; padding: 1px 6px; border-radius: 3px; border: 1px solid currentColor; }
#caption .origin-human { color: var(--human); }
#caption .origin-machine, #caption .origin-machine-reviewed { color: var(--machine); }
#timeline { list-style: none; margin: 0; padding: 0; max-height: 280px; overflow: auto; font-family: ui-monospace, monospace; font-size: 12px; }
#timeline li { padding: 2px 0; border-bottom: 1px solid var(--line); overflow-wrap: anywhere; }
#log { white-space: pre-wrap; font-family: ui-monospace, monospace; font-size: 11px; color: var(--muted); max-height: 220px; overflow: auto; margin: 0; overflow-wrap: anywhere; }
h2 { font-size: 13px; margin: 0 0 6px; color: var(--muted); font-weight: 600; }
.controls { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
#status { color: var(--muted); }
/* Co-performance: the floor, what's next, the audience and fidelity. */
.co-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }
.co-grid h3 { font-size: 12px; margin: 0 0 4px; color: var(--muted); font-weight: 600; }
.co-grid ul, .co-grid ol { margin: 0; padding-left: 18px; font-size: 13px; }
.co-grid li { padding: 1px 0; overflow-wrap: anywhere; }
#co-now { margin: 0 0 6px; font-weight: 600; }
#co-marks, #co-left { color: var(--muted); font-size: 12px; margin: 4px 0 0; }
#co-intents li[data-outcome="upcoming"] { color: var(--machine); }
#co-intents li[data-outcome="kept"] { color: var(--human); }
#co-lead { margin-top: 8px; }
#co-lead input[type="number"] { width: 4.5em; }
#co-input { margin-top: 6px; }
#co-options { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 6px; }
table.binding { border-collapse: collapse; font-family: ui-monospace, monospace; font-size: 12px; width: 100%; }
table.binding th, table.binding td { text-align: left; padding: 2px 8px 2px 0; border-bottom: 1px solid var(--line); white-space: nowrap; }
table.binding tr[data-state="verified"] td:nth-child(2) { color: var(--human); }
table.binding tr[data-state="refused"] td:nth-child(2), table.binding tr[data-state="unbound"] td:nth-child(2) { color: var(--machine); }
</style>
</head>
<body>
<header><h1>MoQ room client</h1><span id="who">loading</span></header>
<!-- An end-to-end encrypted room: its epoch and the members of its group. -->
<div id="e2ee" class="controls" hidden><strong>End-to-end encrypted</strong> <span id="e2ee-state"></span></div>
<!-- With a room service (?service=<url>): enter as a publisher or a viewer. -->
<div id="room-app" class="controls">
<button id="join" type="button">Join with camera and microphone</button>
<button id="watch" type="button">Watch</button>
<button id="leave" type="button" hidden>Leave</button>
<span id="status" role="status" aria-live="polite"></span>
<form id="caption-form" hidden><label for="caption-text">Caption</label> <input id="caption-text" maxlength="200" autocomplete="off"> <button type="submit">Send</button></form>
</div>
<main>
<section>
<h2>The room, composed</h2>
<canvas id="canvas" width="640" height="360"></canvas>
<div id="caption" aria-live="polite"></div>
<div id="dvr" class="controls" style="margin: 8px 0 0">
<label for="seek-s">Seconds back</label>
<input id="seek-s" type="number" min="1" max="900" value="10" style="width: 5em">
<button id="seek" type="button" disabled>Seek back</button>
<button id="live" type="button" hidden>Back to live</button>
<span id="dvr-status" role="status" aria-live="polite"></span>
</div>
<canvas id="dvr-canvas" width="640" height="360" hidden style="margin-top: 8px" aria-label="The participant's earlier video, from the relay's window"></canvas>
</section>
<section>
<h2>Events</h2>
<ul id="timeline"></ul>
</section>
<!-- A room with co-performance (WAI co-performance): see co.js. -->
<section id="co" hidden style="grid-column: 1 / -1" aria-label="Co-performance">
<h2>Co-performance</h2>
<div class="co-grid">
<div>
<h3>The floor</h3>
<p id="co-now" role="status" aria-live="polite"></p>
<ol id="co-turns"></ol>
<p id="co-marks"></p>
<div class="controls" style="margin: 6px 0 0"><button id="co-pass" type="button" hidden>Pass the floor</button></div>
</div>
<div>
<h3>What's next</h3>
<ul id="co-intents" aria-live="polite"></ul>
</div>
<div>
<h3>The audience</h3>
<p id="co-question"></p>
<form id="co-input" hidden>
<div id="co-options"></div>
<div id="co-prompt-row" hidden><label for="co-prompt">Your prompt</label> <input id="co-prompt" maxlength="200" autocomplete="off"> <button type="submit">Send</button></div>
<p id="co-left"></p>
</form>
<ul id="co-selections"></ul>
</div>
<div>
<h3>Fidelity</h3>
<ul id="co-fidelity"></ul>
</div>
</div>
<div id="co-lead" class="controls" hidden>
<label for="co-turn-s">Turn (s)</label> <input id="co-turn-s" type="number" min="1" max="60" value="4">
<label for="co-turns-n">Turns</label> <input id="co-turns-n" type="number" min="1" max="64" value="4">
<label for="co-mode">Out of turn</label> <select id="co-mode"><option value="mark">marked</option><option value="refuse">refused</option></select>
<button id="co-start" type="button">Start turns</button>
<label for="co-q">Ask</label> <input id="co-q" maxlength="200" placeholder="Which way next?">
<label for="co-options-n">Options</label> <input id="co-options-n" type="number" min="2" max="32" value="3">
<label for="co-window-s">Open for (s)</label> <input id="co-window-s" type="number" min="2" max="300" value="8">
<button id="co-vote" type="button">Open a vote</button>
<button id="co-prompts" type="button">Ask for prompts</button>
<button id="co-fid" type="button">State fidelity</button>
</div>
<!-- With ?debug=binding: the state of the frame each tile shows (WAI browser-sink §10). -->
<section id="binding-debug" style="grid-column: 1 / -1" hidden>
<h2>Frames on screen</h2>
<div style="overflow-x: auto"><table class="binding">
<thead><tr><th>Tile</th><th>State</th><th>Tier</th><th>Digest</th><th>Receipt</th><th>Chain</th><th>Provenance</th><th>Route</th><th>Power efficient</th><th>Energy</th></tr></thead>
<tbody></tbody>
</table></div>
</section>
<section style="grid-column: 1 / -1">
<h2>Log</h2>
<pre id="log"></pre>
</section>
</main>
<script type="module">
// The controls are for a room service alone; a fixture's page has no use for them.
if (!new URLSearchParams(location.search).get("service")) document.getElementById("room-app").hidden = true;
</script>
<script type="module" src="./app.js"></script>
</body>
</html>