// Create: describe a WAI app, watch it build itself from WAI widgets as a model writes it, // try its room, and take its program (app.oui) away: `wai create --from app.oui` makes // it a starter project on the person's machine (catalogue `app.export`). // // The page calls no model of its own and holds no key on any server. A program comes from // the visitor's own model (an endpoint and a key they type, called from this page alone), // from their coding agent (`wai mcp --toolset app`, through its bridge on 127.0.0.1), or from a // recorded stream; the page says which. Each chunk is parsed by wai-app's WebAssembly build // (joule-ui's line language), and the snapshot, every widget of it accepted by the WAI // registry, is rendered at once. import { openSession, guide } from "./session.js"; import { Renderer } from "./renderer.js"; import { components, context } from "./components.js"; import { ownModel, bridge, replay } from "./model.js"; const root = document.getElementById("create"); const $ = (id) => document.getElementById(id); const session = await openSession(); // The room service each kind of room is tried on (`data-service-live`, `-watch`, `-show`), // else `data-service`; every one on this page's own origin. const services = { live: root.dataset.serviceLive, watch: root.dataset.serviceWatch, show: root.dataset.serviceShow }; const ctx = context({ session, service: root.dataset.service, services, frame: root.dataset.frame }); const renderer = new Renderer($("app"), components, ctx); const test = (window.createTest = { session, ctx, renderer, snapshots: [], last: null, source: null, done: 0, errors: [] }); const SAYS = { none: "No model is called until you choose one. This page has no model of its own and holds no key.", own: (u) => `Model: your own, at ${u}, called from this page with the key you typed. The key stays in this tab's memory and goes to that address alone; it is never stored.`, agent: (u) => `Model: your coding agent, which streams the app to this page through the bridge on your machine (${u}).`, replay: (n) => `Model: none. This replays a recorded stream (${n}).`, }; const says = (t) => { $("model-says").textContent = t; }; function sourceNow() { const r = document.querySelector('input[name="source"]:checked'); return r ? r.value : "replay"; } function showSource() { const s = sourceNow(); for (const id of ["own", "agent", "replay"]) $(id).hidden = id !== s; says(SAYS.none); } for (const r of document.querySelectorAll('input[name="source"]')) r.addEventListener("change", showSource); showSource(); // ---------------------------------------------------------------------------- the stream let running = 0; async function build(chunks, label) { const run = ++running; session.reset(); test.snapshots = []; test.source = label; test.errors = []; $("export").disabled = true; $("download").hidden = true; $("export-status").textContent = ""; $("export-command").textContent = ""; root.dataset.state = "streaming"; try { for await (const c of chunks) { if (run !== running) return; show(session.push(c)); } if (run !== running) return; show(session.finish()); root.dataset.state = "done"; test.done++; } catch (e) { root.dataset.state = "failed"; test.errors.push(String(e.message || e)); says(`The stream stopped: ${e.message || e}`); } } function show(s) { renderer.render(s); test.last = s; test.snapshots.push({ widgets: s.nodes.length, keys: s.nodes.map((n) => n.key), errors: s.errors.map((e) => `${e.line}:${e.code}`), chars: session.program().length, finished: s.finished }); drawProgram(s); drawProblems(s); const ok = s.finished && s.room && !s.errors.some((e) => !e.warning); $("export").disabled = !ok; } function drawProgram(s) { const lines = session.program().split("\n"); const bad = new Map(); for (const e of s.errors) if (e.line) bad.set(e.line, `${e.code}: ${e.message}`); const pending = s.statements.filter((x) => !x.complete).map((x) => x.line); const ol = $("program"); ol.replaceChildren(...lines.map((l, i) => { const li = document.createElement("li"); li.textContent = l || " "; li.value = i + 1; if (bad.has(i + 1)) { li.className = "bad"; li.title = bad.get(i + 1); } else if (pending.includes(i + 1)) li.className = "pending"; return li; })); ol.scrollTop = ol.scrollHeight; } function drawProblems(s) { const ul = $("problems"); const shown = s.errors.length ? s.errors : []; ul.replaceChildren(...shown.map((e) => { const li = document.createElement("li"); li.dataset.code = e.code; li.className = e.warning ? "warn" : "bad"; li.textContent = `${e.line ? `line ${e.line}` : "the app"}: ${e.message} (${e.code})`; return li; })); if (!shown.length) { const li = document.createElement("li"); li.className = "ok"; li.textContent = s.finished ? "None: every line was accepted." : s.tree ? "None so far." : "Waiting for the first line."; ul.append(li); } } // ---------------------------------------------------------------------------- the sources let disconnect = null; $("describe").addEventListener("submit", async (e) => { e.preventDefault(); const prompt = $("prompt").value.trim(); const s = sourceNow(); if (s === "own") { const endpoint = $("own-endpoint").value.trim(); const model = $("own-model").value.trim(); let u; try { u = new URL(endpoint); } catch { return says("Give your model's address: the full URL of its chat/completions route."); } says(SAYS.own(u.host)); // The key is read here, at the call, and handed to the call alone. await build(ownModel({ endpoint, model, key: $("own-key").value, system: guide(), prompt }), "own"); } else if (s === "agent") { connectAgent(); } else { const name = $("recording").value; says(SAYS.replay(name)); const rec = await (await fetch(`${root.dataset.recorded}wai-app-${name}.json`, { cache: "no-store" })).json(); $("prompt").value = rec.prompt; await build(replay(rec, { speed: Number(root.dataset.speed || 1) }), "replay"); } }); function connectAgent() { if (disconnect) disconnect(); const url = $("agent-bridge").value.trim(); const code = $("agent-code").value.trim(); try { says(SAYS.agent(new URL(url).host)); disconnect = bridge({ url, code, onProgram: (iter) => build(iter, "agent"), onState: (st) => { $("agent-state").textContent = st === "connected" ? "Connected: ask your agent to build the app." : st === "retrying" ? "Not connected: is `wai mcp --toolset app` running, and the code right?" : "Closed."; }, }); } catch (e) { says(String(e.message || e)); } } // ---------------------------------------------------------------------------- the export // The program in its canonical form, as a file: `wai create --from app.oui` makes the // project of it, on the person's machine, with the one writer every WAI project has. $("export").addEventListener("click", () => { const status = $("export-status"); try { const text = session.canonical(); const title = (test.last && test.last.tree && test.last.tree.props && test.last.tree.props.title && test.last.tree.props.title.value) || "wai-app"; const dir = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "wai-app"; const a = $("download"); if (a.href) URL.revokeObjectURL(a.href); a.href = URL.createObjectURL(new Blob([text], { type: "text/plain" })); a.download = "app.oui"; a.textContent = `app.oui (${text.split("\n").filter(Boolean).length} lines)`; a.hidden = false; const command = `wai create ${dir} --from app.oui`; $("export-command").textContent = command; test.exported = { file: "app.oui", text, command }; status.textContent = "Then, where you saved it: run the command below, and `wai dev` in the project it makes. The project is Apache-2.0, and no key is in it."; a.click(); } catch (e) { status.textContent = `Not exported: ${e.message || e}`; } });