wai/wai-ui becomes wai/wai-app, the library `wai_app`, with no binary and no `cli` feature: `wai`, built with its feature `app`, is its command line and its MCP server. Its served files are named wai-app-* (the WebAssembly package wai_app), the recordings wai-app-*.json. - export.rs gives the files of a starter project for a program and the facts its project file holds (title, summary, `wai dev`'s mode for the app's room, a watch room's file, a show's audience); `wai create --from app.oui` writes them, the project file and the licence. The export no longer writes app.json, room.toml, relay.toml, package.json, check.mjs or serve.mjs: wai.json names the room, `wai dev` runs it and `wai app check` checks the project. Its README and AGENTS.md say so, and the README its licence. - runtime.rs reads a Create runtime by its listing (create/wai-app-runtime.json), checking each file's SHA-256. - bridge.rs keeps the bridge on 127.0.0.1 a coding agent streams a program into the page through, with a stream helper; the stdio server, its tools and the binary's test go (`wai mcp --toolset app` serves them). - The page's Export hands over app.oui, the program in its canonical form, and the command that makes it a project; the ZIP writer and the page's export in WebAssembly go. The page names `wai mcp --toolset app` for the coding agent. - joule-ui/docs/create.md §4 and §5: the export as `wai create --from app.oui`, and the agent's tools app_guide, app_check, app_stream and app_export. Co-Authored-By: Claude Opus 5.5 <[email protected]>
199 lines
12 KiB
HTML
199 lines
12 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>The room</title>
|
||
<!-- A frame of the page that holds a room widget, not a page of its own: not indexed. -->
|
||
<meta name="robots" content="noindex">
|
||
<link rel="icon" href="data:,">
|
||
<!-- The room client of joule-moq-web in a frame of the page that holds a WAI room widget
|
||
(wai-app's components.js): the same markup as the client's own page, every element it
|
||
looks up present, and shown in part. The widget drives the client through its interface
|
||
(roomTest) from the page around it, which seals every action; the frame shows the room's
|
||
video (view=stage), the file a watch room watches (view=watch), or nothing (view=none). -->
|
||
<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); }
|
||
|
||
body.embed { padding: 0; background: transparent; }
|
||
body.embed > header, body.embed #room-app, body.embed #dvr, body.embed #dvr-canvas, body.embed #remote,
|
||
body.embed #co, body.embed #binding-debug, body.embed main > section:has(#timeline), body.embed main > section:has(#log) { display: none !important; }
|
||
body.embed main { display: block; }
|
||
body.embed main > section { border: 0; padding: 0; background: transparent; }
|
||
body.embed #cowatch .controls > :not(#cw-sync) { display: none !important; }
|
||
body.embed[data-view="none"] main { display: none; }
|
||
body.embed[data-view="watch"] main > section:has(#canvas) { display: none; }
|
||
body.embed[data-view="stage"] #cowatch { display: none !important; }
|
||
</style>
|
||
</head>
|
||
<body class="embed">
|
||
|
||
<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>
|
||
<!-- 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>
|
||
<button id="sound" type="button" hidden style="margin: 6px 0">Turn the room's sound on</button>
|
||
<script>
|
||
document.body.dataset.view = new URLSearchParams(location.search).get("view") || "none";
|
||
// The room's audio plays in this frame, and a browser starts a frame's audio only after a
|
||
// click in the frame itself: every audio context the client makes is kept here, and while
|
||
// one waits for that click, this button asks for it.
|
||
const contexts = [];
|
||
if (window.AudioContext) {
|
||
const Real = window.AudioContext;
|
||
window.AudioContext = class extends Real {
|
||
constructor(...a) { super(...a); contexts.push(this); }
|
||
};
|
||
}
|
||
const sound = document.getElementById("sound");
|
||
sound.addEventListener("click", () => { for (const c of contexts) c.resume(); });
|
||
setInterval(() => { sound.hidden = !contexts.some((c) => c.state === "suspended"); }, 500);
|
||
</script>
|
||
<script type="module" src="../../../joule-code/joule-code-rs/crates/joule-moq-web/web/room/app.js"></script>
|
||
</body>
|
||
</html>
|