An AI participant's caption shows the marker AI system and the model's pin beside its origin label; a principal whose grant carries ai is named an AI participant, marked so in the timeline's lifecycle, and its events track followed in any room (a captioner publishes no media), as live-rooms §4.4 asks of a sink. e2e/room.e2e.mjs's scenario captioner runs wai-room-agent with its scripted test recognizer against a room service with [ai] captioners and checks what a page shows. Co-Authored-By: Claude Opus 5.5 <[email protected]>
177 lines
10 KiB
HTML
177 lines
10 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); }
|
||
#caption .ai { font-size: 12px; padding: 1px 6px; border-radius: 3px; background: var(--machine); color: var(--panel); font-weight: 600; }
|
||
#caption .model { font-size: 11px; color: var(--muted); font-family: ui-monospace, monospace; }
|
||
#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>
|
||
<!-- The room's own source (WAI co-watch §15.7): each frame at the instant its object states,
|
||
with what was late or refused. -->
|
||
<section id="source" hidden style="grid-column: 1 / -1">
|
||
<h2>The room's source</h2>
|
||
<canvas id="source-canvas" width="640" height="360" aria-label="The room's own source, each frame at its stated instant"></canvas>
|
||
<p id="source-state" role="status" aria-live="polite"></p>
|
||
</section>
|
||
<!-- A watch room (WAI co-watch): the source the room watches, its shared controls and how
|
||
closely the room keeps together. -->
|
||
<section id="cowatch" hidden style="grid-column: 1 / -1">
|
||
<h2>Watching together</h2>
|
||
<video id="cowatch-media" playsinline style="width: 100%; max-height: 360px; background: #101218; border-radius: 4px"></video>
|
||
<div class="controls" style="margin: 8px 0 0">
|
||
<button id="cw-play" type="button">Play</button>
|
||
<button id="cw-pause" type="button">Pause</button>
|
||
<button id="cw-back" type="button">Back 10 s</button>
|
||
<button id="cw-fwd" type="button">Ahead 10 s</button>
|
||
<label for="cw-rate">Speed</label>
|
||
<select id="cw-rate"><option value="750000">0.75×</option><option value="1000000" selected>1×</option><option value="1250000">1.25×</option><option value="1500000">1.5×</option></select>
|
||
<span id="cw-sync" role="status" aria-live="polite"></span>
|
||
</div>
|
||
</section>
|
||
<!-- A remote desktop room (WAI remote-desktop): the host's screen, what it captures and who
|
||
holds the floor. -->
|
||
<section id="remote" hidden style="grid-column: 1 / -1" aria-label="Remote desktop">
|
||
<h2>Remote desktop</h2>
|
||
<canvas id="remote-canvas" width="1280" height="720" aria-label="The host's screen"></canvas>
|
||
<div class="controls" style="margin: 8px 0 0">
|
||
<button id="remote-request" type="button" hidden>Ask for control</button>
|
||
<button id="remote-release" type="button" hidden>Give control back</button>
|
||
<span id="remote-state" role="status" aria-live="polite"></span>
|
||
</div>
|
||
</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>
|
||
</section>
|
||
<!-- 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>
|