| |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>DiffusionGemma · Live Builder</title> |
| <style> |
| :root { |
| --bg: #0b0e14; |
| --panel: #121722; |
| --panel-2: #0f141d; |
| --border: #222a38; |
| --text: #e6edf3; |
| --muted: #8b97a7; |
| --accent: #7c5cff; |
| --accent-2: #18c29c; |
| --amber: #f5c451; |
| --green: #2ea043; |
| } |
| * { box-sizing: border-box; } |
| html, body { margin: 0; height: 100%; } |
| body { |
| background: radial-gradient(1200px 600px at 70% -10%, #1a2030 0%, var(--bg) 55%); |
| color: var(--text); |
| font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; |
| display: flex; flex-direction: column; height: 100vh; overflow: hidden; |
| } |
| header { |
| padding: 14px 22px; border-bottom: 1px solid var(--border); |
| display: flex; align-items: center; gap: 14px; flex: 0 0 auto; |
| } |
| header .logo { font-size: 22px; } |
| header h1 { font-size: 16px; margin: 0; font-weight: 650; letter-spacing: .2px; } |
| header p { margin: 0; color: var(--muted); font-size: 12.5px; } |
| .pill { |
| margin-left: auto; font-size: 12px; color: var(--muted); |
| border: 1px solid var(--border); border-radius: 999px; padding: 5px 12px; |
| display: flex; align-items: center; gap: 8px; white-space: nowrap; |
| } |
| .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); } |
| .dot.live { background: var(--amber); box-shadow: 0 0 8px var(--amber); } |
| .dot.done { background: var(--green); box-shadow: 0 0 8px var(--green); } |
| .dot.err { background: #f85149; box-shadow: 0 0 8px #f85149; } |
| |
| main { flex: 1 1 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; padding: 14px 18px; min-height: 0; } |
| .panel { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; display: flex; flex-direction: column; min-height: 0; overflow: hidden; } |
| .panel .cap { padding: 9px 14px; border-bottom: 1px solid var(--border); font-size: 12.5px; font-weight: 600; color: var(--muted); display: flex; align-items: center; gap: 8px; } |
| .panel .cap .sub { font-weight: 400; color: #5d6b7a; } |
| |
| |
| #code { flex: 1 1 auto; overflow: auto; margin: 0; padding: 12px 0; font-family: "SF Mono", ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace; font-size: 12px; line-height: 1.55; background: var(--panel-2); } |
| #code .ln { padding: 0 14px; white-space: pre-wrap; word-break: break-word; min-height: 1.55em; border-left: 3px solid transparent; } |
| #code .ln.live { animation: flash .6s ease-out; background: rgba(245,196,81,.05); } |
| #code .ln.diff { background: rgba(46,160,67,.13); border-left-color: var(--green); } |
| @keyframes flash { |
| 0% { background: rgba(245,196,81,.42); } |
| 100% { background: rgba(245,196,81,.05); } |
| } |
| #code::-webkit-scrollbar, .scroll::-webkit-scrollbar { width: 9px; height: 9px; } |
| #code::-webkit-scrollbar-thumb, .scroll::-webkit-scrollbar-thumb { background: #2a3344; border-radius: 8px; } |
| |
| |
| #preview { flex: 1 1 auto; border: 0; width: 100%; background: #fff; } |
| |
| |
| footer { flex: 0 0 auto; border-top: 1px solid var(--border); padding: 12px 18px; background: var(--panel-2); } |
| .row { display: flex; gap: 12px; align-items: flex-start; } |
| textarea#prompt { |
| flex: 1 1 auto; resize: none; height: 72px; background: var(--panel); color: var(--text); |
| border: 1px solid var(--border); border-radius: 10px; padding: 11px 13px; font-size: 14px; font-family: inherit; |
| } |
| textarea#prompt:focus { outline: none; border-color: var(--accent); } |
| .btns { display: flex; flex-direction: column; gap: 8px; width: 150px; } |
| button { font-family: inherit; font-size: 13.5px; font-weight: 600; border-radius: 10px; padding: 9px 12px; cursor: pointer; border: 1px solid var(--border); } |
| button.primary { background: linear-gradient(180deg, #8a6bff, #6b48f0); color: #fff; border: 0; } |
| button.primary:disabled { opacity: .5; cursor: not-allowed; } |
| button.ghost { background: transparent; color: var(--muted); } |
| button.ghost:hover:not(:disabled) { color: var(--text); border-color: #36405230; } |
| button.ghost:disabled { opacity: .4; cursor: not-allowed; } |
| |
| .meta { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; margin-top: 11px; color: var(--muted); font-size: 12px; } |
| .meta label { display: flex; align-items: center; gap: 7px; white-space: nowrap; } |
| .meta input[type="range"] { accent-color: var(--accent); width: 120px; } |
| .meta input[type="checkbox"] { accent-color: var(--accent); width: 15px; height: 15px; } |
| .meta .val { color: var(--text); font-variant-numeric: tabular-nums; min-width: 30px; } |
| .chips { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 11px; } |
| .chip { font-size: 12px; color: var(--muted); border: 1px solid var(--border); border-radius: 999px; padding: 5px 11px; cursor: pointer; background: transparent; } |
| .chip:hover { color: var(--text); border-color: var(--accent); } |
| .history { margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap; max-height: 46px; overflow: auto; } |
| .turn { font-size: 11.5px; color: var(--muted); border: 1px solid var(--border); border-radius: 8px; padding: 3px 9px; } |
| .turn b { color: var(--accent-2); } |
| |
| |
| .mode-switch { |
| display: inline-flex; border: 1px solid var(--border); border-radius: 999px; |
| padding: 3px; background: var(--panel-2); margin-left: 8px; |
| } |
| .mode-switch button { |
| font-size: 12px; font-weight: 600; padding: 6px 14px; border-radius: 999px; |
| border: 0; background: transparent; color: var(--muted); cursor: pointer; |
| } |
| .mode-switch button.active { |
| background: linear-gradient(180deg, #8a6bff, #6b48f0); color: #fff; |
| } |
| |
| |
| #pyView { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; } |
| #pyCode { |
| flex: 1 1 auto; overflow: auto; margin: 0; padding: 12px 14px; |
| font-family: "SF Mono", ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace; |
| font-size: 12.5px; line-height: 1.55; background: var(--panel-2); color: var(--text); |
| white-space: pre; tab-size: 4; |
| } |
| #pyCode .tok-kw { color: #ff7b72; } |
| #pyCode .tok-str { color: #a5d6ff; } |
| #pyCode .tok-com { color: #6e7681; font-style: italic; } |
| #pyCode .tok-num { color: #79c0ff; } |
| #pyCode .tok-fn { color: #d2a8ff; } |
| #pyCode .tok-bi { color: #ffa657; } |
| #pyConsole { |
| flex: 0 0 180px; overflow: auto; margin: 0; padding: 10px 14px; |
| font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace; |
| font-size: 12px; line-height: 1.5; background: #07090d; color: #c9d1d9; |
| border-top: 1px solid var(--border); white-space: pre-wrap; |
| } |
| #pyConsole .err { color: #f85149; } |
| #pyConsole .out { color: #8b97a7; } |
| .py-bar { padding: 8px 14px; border-bottom: 1px solid var(--border); display: flex; gap: 8px; align-items: center; } |
| .py-bar button { font-size: 12px; padding: 6px 12px; } |
| .py-bar .py-status { color: var(--muted); font-size: 12px; margin-left: auto; } |
| .py-bar .py-opt { color: var(--muted); font-size: 12px; display: flex; align-items: center; gap: 5px; cursor: pointer; } |
| .py-bar .py-opt input { accent-color: var(--accent); width: 14px; height: 14px; } |
| </style> |
| </head> |
| <body> |
| <header> |
| <span class="logo">🌫️→🌐</span> |
| <div> |
| <h1>DiffusionGemma · Live Builder</h1> |
| <p id="subtitle">Describe a website and a block-diffusion LLM writes the HTML by denoising — every token updates at once. Watch the raw canvas take shape (left) while it renders into a live page (right), then send follow-up prompts to tweak it.</p> |
| </div> |
| <div class="mode-switch" id="modeSwitch"> |
| <button data-mode="webpage" class="active">🌐 Webpage</button> |
| <button data-mode="python">🐍 Python</button> |
| </div> |
| <span class="pill"><span class="dot" id="statusDot"></span><span id="statusText">idle</span></span> |
| </header> |
|
|
| <main> |
| <section class="panel"> |
| <div class="cap">🧠 Model's view — diffusion canvas <span class="sub" id="capInfo"></span></div> |
| <div id="code" class="scroll"></div> |
| </section> |
| <section class="panel" id="rightPanel"> |
| |
| <div id="webMode" style="display:flex;flex-direction:column;flex:1;min-height:0;"> |
| <div class="cap">🌐 Live website</div> |
| <iframe id="preview" sandbox="allow-scripts"></iframe> |
| </div> |
| |
| <div id="pyMode" style="display:none;flex-direction:column;flex:1;min-height:0;"> |
| <div class="cap">🐍 Python code |
| <span class="sub" id="pyCapInfo"></span> |
| </div> |
| <div id="pyView"> |
| <pre id="pyCode"></pre> |
| <div class="py-bar"> |
| <button class="primary" id="runBtn">▶ Run</button> |
| <button class="ghost" id="clearConsoleBtn">Clear</button> |
| <label class="py-opt"><input type="checkbox" id="autoFeedErr" checked> auto-fix on error</label> |
| <label class="py-opt"><input type="checkbox" id="autoRetryRun" checked> auto-retry run</label> |
| <span class="py-status" id="pyStatus"></span> |
| </div> |
| <pre id="pyConsole"></pre> |
| </div> |
| </div> |
| </section> |
| </main> |
|
|
| <footer> |
| <div class="row"> |
| <textarea id="prompt" placeholder="Describe a website or Python script…"></textarea> |
| <div class="btns"> |
| <button class="primary" id="buildBtn">Build / Tweak</button> |
| <button class="ghost" id="resetBtn">Reset</button> |
| </div> |
| </div> |
| <div class="chips" id="chips"></div> |
| <div class="meta"> |
| <label>tokens <input type="range" id="maxTokens" min="2048" max="8192" step="256" value="8192"><span class="val" id="maxTokensV">8192</span></label> |
| <label>iterations/block <input type="range" id="maxIters" min="8" max="120" step="8" value="120"><span class="val" id="maxItersV">120</span></label> |
| <label>anim delay <input type="range" id="delay" min="0" max="0.3" step="0.02" value="0"><span class="val" id="delayV">0.0s</span></label> |
| <label><input type="checkbox" id="fullDenoise"> run all denoising steps (no early stop)</label> |
| <label><input type="checkbox" id="warmStart" checked> tweak in place (diffuse from current artifact, not noise)</label> |
| <span class="history" id="history"></span> |
| </div> |
| </footer> |
|
|
| <script type="module"> |
| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
| |
| const $ = (id) => document.getElementById(id); |
| const codeEl = $("code"), preview = $("preview"); |
| const statusDot = $("statusDot"), statusText = $("statusText"), capInfo = $("capInfo"); |
| const buildBtn = $("buildBtn"), resetBtn = $("resetBtn"), promptEl = $("prompt"); |
| const webMode = $("webMode"), pyMode = $("pyMode"), modeSwitch = $("modeSwitch"); |
| const pyCodeEl = $("pyCode"), pyConsole = $("pyConsole"), runBtn = $("runBtn"); |
| const clearConsoleBtn = $("clearConsoleBtn"), pyStatus = $("pyStatus"), pyCapInfo = $("pyCapInfo"); |
| const subtitle = $("subtitle"); |
| |
| let client = null; |
| let busy = false; |
| let messages = []; |
| let prevFrameLines = []; |
| let lastFinalLines = []; |
| let previewScrollY = 0; |
| let currentMode = "webpage"; |
| let pyodideReady = null; |
| let currentPySource = ""; |
| let autoFixRetries = 0; |
| const MAX_AUTO_RETRIES = 3; |
| |
| const SUBTITLES = { |
| webpage: "Describe a website and a block-diffusion LLM writes the HTML by denoising — every token updates at once. Watch the raw canvas take shape (left) while it renders into a live page (right), then send follow-up prompts to tweak it.", |
| python: "Describe a Python program and a block-diffusion LLM writes the code by denoising — every token updates at once. Watch the raw canvas take shape (left) while it appears as syntax-highlighted code (right). Press Run to execute it in your browser via Pyodide.", |
| }; |
| |
| const EXAMPLES = { |
| webpage: [ |
| "A bold landing page for a startup called 'Nimbus' that sells AI weather forecasting, with a hero section, three feature cards, and a call-to-action button.", |
| "A cozy personal blog homepage with a warm color palette, a header, an about section, and a list of three recent posts.", |
| "A neon synthwave portfolio page for a music producer with an animated gradient background.", |
| "A clean pricing page with three tiers (Free, Pro, Team), a feature comparison, and a FAQ.", |
| "A restaurant landing page with a hero image area, a menu grid, opening hours, and a reservation button.", |
| "A sleek product page for wireless headphones with specs, an image placeholder, and an add-to-cart bar.", |
| "A dark-mode dashboard mockup with a sidebar, four stat cards, and a placeholder chart.", |
| "A playful 'coming soon' page with a countdown vibe, an email signup box, and floating shapes.", |
| ], |
| python: [ |
| "A CLI todo-list manager using only the standard library (argparse + json file storage).", |
| "A function that generates a random maze and prints it as ASCII art.", |
| "A simple HTTP server using http.server that returns the current time as JSON.", |
| "A Conway's Game of Life simulation printed to the terminal with ANSI escapes.", |
| "A recursive fractal tree drawn with turtle graphics.", |
| "A text-based Tic-Tac-Toe game for two human players in the terminal.", |
| "A script that walks a directory tree and prints the largest N files by size.", |
| "A sieve of Eratosthenes that prints all primes below a given N.", |
| ], |
| }; |
| |
| window.addEventListener("message", (e) => { |
| if (e.data && typeof e.data.__gdiffScrollY === "number") previewScrollY = e.data.__gdiffScrollY; |
| }); |
| |
| |
| function esc(s) { return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); } |
| |
| function setStatus(kind, text) { |
| statusDot.className = "dot" + (kind ? " " + kind : ""); |
| statusText.textContent = text; |
| } |
| |
| function unchangedSet(a, b) { |
| const n = a.length, m = b.length; |
| const dp = Array.from({ length: n + 1 }, () => new Int32Array(m + 1)); |
| for (let i = n - 1; i >= 0; i--) |
| for (let j = m - 1; j >= 0; j--) |
| dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]); |
| const keep = new Set(); |
| let i = 0, j = 0; |
| while (i < n && j < m) { |
| if (a[i] === b[j]) { keep.add(j); i++; j++; } |
| else if (dp[i + 1][j] >= dp[i][j + 1]) i++; else j++; |
| } |
| return keep; |
| } |
| |
| function renderCode(source, { liveAgainst = null, diffAgainst = null } = {}) { |
| const lines = source.split("\n"); |
| const liveKeep = liveAgainst ? unchangedSet(liveAgainst, lines) : null; |
| const diffKeep = diffAgainst ? unchangedSet(diffAgainst, lines) : null; |
| const html = lines.map((ln, idx) => { |
| let cls = "ln"; |
| if (diffKeep && !diffKeep.has(idx)) cls += " diff"; |
| else if (liveKeep && !liveKeep.has(idx)) cls += " live"; |
| return `<div class="${cls}">${esc(ln) || " "}</div>`; |
| }).join(""); |
| codeEl.innerHTML = html; |
| return lines; |
| } |
| |
| function normalizeHtml(src) { |
| const lower = src.toLowerCase(); |
| const dt = lower.indexOf("<!doctype"); |
| if (dt !== -1) return src.slice(dt); |
| const h = lower.indexOf("<html"); |
| if (h !== -1) return "<!DOCTYPE html>\n" + src.slice(h); |
| const hd = lower.indexOf("<head"); |
| if (hd !== -1) return '<!DOCTYPE html>\n<html lang="en">\n' + src.slice(hd); |
| const bd = lower.indexOf("<body"); |
| if (bd !== -1) return '<!DOCTYPE html>\n<html lang="en">\n<head><meta charset="UTF-8"></head>\n' + src.slice(bd); |
| return src; |
| } |
| |
| function renderPreview(source) { |
| if (!source || !source.trim()) { preview.srcdoc = ""; previewScrollY = 0; return; } |
| source = normalizeHtml(source); |
| const y = Math.round(previewScrollY); |
| const inject = |
| "<script>(function(){var y=" + y + ";" + |
| "function r(){window.scrollTo(0,y);}" + |
| "r();requestAnimationFrame(r);window.addEventListener('load',r);" + |
| "window.addEventListener('scroll',function(){" + |
| "parent.postMessage({__gdiffScrollY:window.scrollY||document.documentElement.scrollTop||0},'*');" + |
| "},{passive:true});})();<\/script>"; |
| const idx = source.toLowerCase().lastIndexOf("</body>"); |
| preview.srcdoc = idx !== -1 ? source.slice(0, idx) + inject + source.slice(idx) : source + inject; |
| } |
| |
| |
| const PY_KW = new Set(["def","class","return","if","elif","else","for","while","in","not","and","or","is","import","from","as","with","try","except","finally","raise","yield","lambda","pass","break","continue","global","nonlocal","assert","del","async","await","True","False","None"]); |
| const PY_BI = new Set(["print","len","range","int","str","float","list","dict","set","tuple","open","input","enumerate","zip","map","filter","sorted","sum","min","max","abs","type","isinstance","super","self","__init__","__main__"]); |
| |
| function highlightPython(src) { |
| const lines = src.split("\n"); |
| return lines.map((line) => { |
| let out = ""; |
| let i = 0; |
| while (i < line.length) { |
| const c = line[i]; |
| if (c === "#") { out += `<span class="tok-com">${esc(line.slice(i))}</span>`; break; } |
| if (c === '"' || c === "'") { |
| const quote = c; |
| let j = i + 1; |
| if (line.slice(i, i + 3) === quote.repeat(3)) { |
| j = i + 3; |
| while (j < line.length && line.slice(j, j + 3) !== quote.repeat(3)) j++; |
| j = Math.min(line.length, j + 3); |
| } else { |
| while (j < line.length && line[j] !== quote) { if (line[j] === "\\" ) j++; j++; } |
| j = Math.min(line.length, j + 1); |
| } |
| out += `<span class="tok-str">${esc(line.slice(i, j))}</span>`; |
| i = j; continue; |
| } |
| if (/[0-9]/.test(c) && (i === 0 || !/[A-Za-z_]/.test(line[i - 1]))) { |
| let j = i; |
| while (j < line.length && /[0-9._eExXbBoOa-fA-F]/.test(line[j])) j++; |
| out += `<span class="tok-num">${esc(line.slice(i, j))}</span>`; |
| i = j; continue; |
| } |
| if (/[A-Za-z_]/.test(c)) { |
| let j = i; |
| while (j < line.length && /[A-Za-z0-9_]/.test(line[j])) j++; |
| const word = line.slice(i, j); |
| let k = j; while (k < line.length && line[k] === " ") k++; |
| if (PY_KW.has(word)) out += `<span class="tok-kw">${esc(word)}</span>`; |
| else if (PY_BI.has(word)) out += `<span class="tok-bi">${esc(word)}</span>`; |
| else if (line[k] === "(") out += `<span class="tok-fn">${esc(word)}</span>`; |
| else out += esc(word); |
| i = j; continue; |
| } |
| out += esc(c); |
| i++; |
| } |
| return out; |
| }).join("\n"); |
| } |
| |
| function renderPython(source) { |
| currentPySource = source || ""; |
| pyCodeEl.innerHTML = highlightPython(currentPySource) || " "; |
| } |
| |
| async function ensurePyodide() { |
| if (pyodideReady) return pyodideReady; |
| pyStatus.textContent = "loading Pyodide…"; |
| if (!window.loadPyodide) { |
| await new Promise((resolve, reject) => { |
| const s = document.createElement("script"); |
| s.src = "https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js"; |
| s.onload = resolve; s.onerror = reject; |
| document.head.appendChild(s); |
| }); |
| } |
| pyodideReady = window.loadPyodide({ |
| indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.2/full/", |
| }); |
| const py = await pyodideReady; |
| pyStatus.textContent = "Pyodide ready"; |
| return py; |
| } |
| |
| async function runPython() { |
| if (!currentPySource.trim()) return; |
| pyConsole.innerHTML = ""; |
| const log = (cls, s) => { |
| const span = document.createElement("span"); |
| span.className = cls; span.textContent = s; |
| pyConsole.appendChild(span); |
| pyConsole.appendChild(document.createTextNode("\n")); |
| }; |
| try { |
| pyStatus.textContent = "running…"; |
| const py = await ensurePyodide(); |
| py.setStdout({ batched: (s) => log("out", s) }); |
| py.setStderr({ batched: (s) => log("err", s) }); |
| await py.runPythonAsync(currentPySource); |
| pyStatus.textContent = "done"; |
| autoFixRetries = 0; |
| } catch (e) { |
| const errMsg = String(e && e.message ? e.message : e); |
| log("err", errMsg); |
| pyStatus.textContent = "error"; |
| |
| |
| if ($("autoFeedErr").checked && autoFixRetries < MAX_AUTO_RETRIES && currentMode === "python") { |
| autoFixRetries++; |
| pyStatus.textContent = `auto-fixing (${autoFixRetries}/${MAX_AUTO_RETRIES})...`; |
| |
| const errorPrompt = `The previous Python code failed with the following error:\n\n\`\`\`\n${errMsg}\n\`\`\`\n\nPlease fix the error and return the full updated script.`; |
| promptEl.value = errorPrompt; |
| |
| const success = await run(); |
| |
| |
| if (success && $("autoRetryRun").checked && currentPySource.trim()) { |
| await runPython(); |
| } |
| } else if (autoFixRetries >= MAX_AUTO_RETRIES) { |
| log("err", "Max auto-fix retries reached. Please debug manually."); |
| pyStatus.textContent = "max retries reached"; |
| } |
| } |
| } |
| |
| |
| function setMode(mode) { |
| currentMode = mode; |
| [...modeSwitch.children].forEach((b) => b.classList.toggle("active", b.dataset.mode === mode)); |
| const isPy = mode === "python"; |
| webMode.style.display = isPy ? "none" : "flex"; |
| pyMode.style.display = isPy ? "flex" : "none"; |
| subtitle.textContent = SUBTITLES[mode]; |
| renderChips(); |
| } |
| |
| function renderChips() { |
| const list = EXAMPLES[currentMode] || []; |
| $("chips").innerHTML = list.map((e, i) => |
| `<button class="chip" data-i="${i}">${esc(e.slice(0, 42))}…</button>`).join(""); |
| } |
| |
| |
| async function ensureClient() { |
| if (!client) { setStatus("", "connecting…"); client = await Client.connect(window.location.origin); } |
| return client; |
| } |
| |
| async function run() { |
| if (busy) return false; |
| const prompt = promptEl.value.trim(); |
| if (!prompt) { promptEl.focus(); return false; } |
| |
| busy = true; buildBtn.disabled = true; |
| const isTweak = messages.length > 0; |
| setStatus("live", isTweak ? "tweaking…" : "diffusing…"); |
| prevFrameLines = []; |
| if (!isTweak) { previewScrollY = 0; pyConsole.innerHTML = ""; } |
| const isPy = currentMode === "python"; |
| |
| let success = false; |
| try { |
| const c = await ensureClient(); |
| const payload = { |
| prompt, |
| history_json: JSON.stringify(messages), |
| max_new_tokens: parseInt($("maxTokens").value, 10), |
| max_iters: parseInt($("maxIters").value, 10), |
| full_denoise: $("fullDenoise").checked, |
| anim_delay: parseFloat($("delay").value), |
| warm_start: $("warmStart").checked, |
| mode: currentMode, |
| }; |
| |
| let finalSource = ""; |
| const sub = c.submit("/generate", payload); |
| for await (const ev of sub) { |
| if (ev.type === "data") { |
| const frame = JSON.parse(ev.data[0]); |
| if (frame.kind === "error") { setStatus("err", "error"); renderCode("/* " + frame.message + " */"); break; } |
| if (frame.kind === "done") { |
| finalSource = frame.source; |
| prevFrameLines = renderCode(finalSource, { diffAgainst: isTweak ? lastFinalLines : null }); |
| if (isPy) renderPython(finalSource); else renderPreview(finalSource); |
| setStatus("done", "done"); |
| continue; |
| } |
| const src = frame.source || ""; |
| prevFrameLines = renderCode(src, { liveAgainst: prevFrameLines }); |
| if (isPy) { |
| renderPython(src); |
| pyCapInfo.textContent = `block ${frame.block} · step ${frame.step}/${frame.max_iters}`; |
| } else { |
| renderPreview(src); |
| capInfo.textContent = `block ${frame.block} · step ${frame.step}/${frame.max_iters} · ${frame.canvas} tokens update simultaneously`; |
| } |
| setStatus("live", `${frame.kind === "draft" ? "diffusing" : "committed"} · block ${frame.block} · step ${frame.step}`); |
| } else if (ev.type === "status" && ev.stage === "error") { |
| setStatus("err", "error"); break; |
| } |
| } |
| |
| if (finalSource) { |
| messages.push({ role: "user", content: prompt }); |
| messages.push({ role: "assistant", content: finalSource }); |
| lastFinalLines = finalSource.split("\n"); |
| promptEl.value = ""; |
| renderHistory(); |
| success = true; |
| } |
| } catch (e) { |
| setStatus("err", "error"); |
| renderCode("/* connection error: " + (e && e.message ? e.message : e) + " */"); |
| } finally { |
| busy = false; buildBtn.disabled = false; |
| } |
| return success; |
| } |
| |
| function renderHistory() { |
| const turns = messages.filter((m) => m.role === "user"); |
| $("history").innerHTML = turns.map((m, i) => |
| `<span class="turn"><b>${i === 0 ? "build" : "tweak " + i}</b> · ${esc(m.content.slice(0, 40))}${m.content.length > 40 ? "…" : ""}</span>` |
| ).join(""); |
| } |
| |
| function reset() { |
| messages = []; prevFrameLines = []; lastFinalLines = []; |
| previewScrollY = 0; pyConsole.innerHTML = ""; currentPySource = ""; |
| autoFixRetries = 0; |
| codeEl.innerHTML = ""; renderPreview(""); renderPython(""); |
| $("history").innerHTML = ""; |
| capInfo.textContent = ""; pyCapInfo.textContent = ""; |
| setStatus("", "idle"); promptEl.value = ""; |
| } |
| |
| |
| buildBtn.addEventListener("click", run); |
| resetBtn.addEventListener("click", reset); |
| runBtn.addEventListener("click", runPython); |
| clearConsoleBtn.addEventListener("click", () => { pyConsole.innerHTML = ""; }); |
| modeSwitch.addEventListener("click", (e) => { |
| const m = e.target.getAttribute("data-mode"); |
| if (m) setMode(m); |
| }); |
| promptEl.addEventListener("keydown", (e) => { |
| if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); run(); } |
| }); |
| for (const [id, fmt] of [["maxTokens", (v) => v], ["maxIters", (v) => v], ["delay", (v) => (+v).toFixed(1) + "s"]]) { |
| const el = $(id), out = $(id + "V"); |
| el.addEventListener("input", () => (out.textContent = fmt(el.value))); |
| } |
| $("chips").addEventListener("click", (e) => { |
| const i = e.target.getAttribute("data-i"); |
| if (i !== null) { promptEl.value = EXAMPLES[currentMode][+i]; promptEl.focus(); } |
| }); |
| |
| setMode("webpage"); |
| renderPreview(""); |
| renderPython(""); |
| setStatus("", "idle"); |
| </script> |
| </body> |
| </html> |