| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>self-plan</title> |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&family=Inter:wght@300;400;500&display=swap'); |
| * { margin: 0; padding: 0; box-sizing: border-box; } |
| body { |
| font-family: "Inter", sans-serif; |
| font-size: 13px; font-weight: 300; line-height: 1.6; |
| color: #1a1a1a; background: #fafafa; |
| padding: 32px 40px; max-width: 960px; margin: 0 auto; |
| } |
| h1 { |
| font-family: "JetBrains Mono", monospace; |
| font-size: 15px; font-weight: 400; letter-spacing: 2px; |
| margin-bottom: 2px; |
| } |
| .meta { |
| font-family: "JetBrains Mono", monospace; |
| color: #999; font-size: 10px; letter-spacing: 0.5px; |
| margin-bottom: 24px; |
| } |
| .input-row { |
| display: flex; gap: 8px; margin-bottom: 24px; |
| } |
| .input-row input { |
| flex: 1; font-family: "Inter", sans-serif; |
| font-size: 13px; font-weight: 300; |
| padding: 8px 12px; border: 1px solid #ddd; |
| background: #fff; outline: none; |
| } |
| .input-row input:focus { border-color: #888; } |
| .btn { |
| font-family: "JetBrains Mono", monospace; |
| font-size: 11px; font-weight: 400; |
| padding: 8px 20px; border: 1px solid #1a1a1a; |
| background: #1a1a1a; color: #fff; cursor: pointer; |
| letter-spacing: 0.5px; |
| } |
| .btn:hover { background: #333; } |
| .btn:disabled { background: #ccc; border-color: #ccc; cursor: default; } |
| .btn-outline { |
| background: #fff; color: #1a1a1a; border: 1px solid #ccc; |
| } |
| .btn-outline:hover { border-color: #888; } |
| |
| .section { margin-bottom: 20px; } |
| .section-label { |
| font-family: "JetBrains Mono", monospace; |
| font-size: 9px; font-weight: 400; |
| text-transform: uppercase; letter-spacing: 2px; |
| color: #999; margin-bottom: 6px; |
| } |
| .code-block { |
| font-family: "JetBrains Mono", monospace; |
| font-size: 11px; font-weight: 300; line-height: 1.7; |
| background: #f4f4f4; border: 1px solid #e0e0e0; |
| padding: 12px 16px; white-space: pre-wrap; |
| overflow-x: auto; color: #333; |
| } |
| .code-block .kw { color: #7a3e9d; } |
| .code-block .fn { color: #1a6; } |
| .code-block .str { color: #9a6700; } |
| .code-block .num { color: #0969a5; } |
| .code-block .cm { color: #999; font-style: italic; } |
| .code-block .dec { color: #7a3e9d; } |
| |
| .log-block { |
| font-family: "JetBrains Mono", monospace; |
| font-size: 11px; font-weight: 300; line-height: 1.9; |
| } |
| .log-line { color: #555; } |
| .log-line .ts { color: #bbb; } |
| .log-line .agent-name { color: #2a7; font-weight: 400; } |
| .log-line .count { color: #888; } |
| .log-line .check { color: #2a7; } |
| .log-line.error { color: #c33; } |
| .log-line.done { color: #1a1a1a; font-weight: 400; } |
| |
| .result-block { |
| font-size: 13px; font-weight: 300; line-height: 1.75; |
| color: #1a1a1a; |
| } |
| .result-block h1, .result-block h2, .result-block h3 { |
| font-family: "Inter", sans-serif; |
| font-weight: 500; margin: 16px 0 8px; |
| } |
| .result-block h1 { font-size: 18px; } |
| .result-block h2 { font-size: 15px; } |
| .result-block h3 { font-size: 13px; } |
| .result-block p { margin-bottom: 10px; } |
| .result-block ul, .result-block ol { margin: 8px 0 8px 20px; } |
| .result-block li { margin-bottom: 4px; } |
| .result-block code { |
| font-family: "JetBrains Mono", monospace; |
| font-size: 11px; background: #f0f0f0; padding: 1px 4px; |
| } |
| .result-block hr { border: none; border-top: 1px solid #eee; margin: 16px 0; } |
| .result-block a { color: #0969a5; } |
| .result-block strong { font-weight: 500; } |
| |
| .confirm-row { |
| display: flex; gap: 8px; margin-top: 10px; margin-bottom: 20px; |
| } |
| |
| .hidden { display: none; } |
| .divider { border-top: 1px solid #eee; margin: 20px 0; } |
| .spinner { display: inline-block; animation: spin 1s linear infinite; } |
| @keyframes spin { to { transform: rotate(360deg); } } |
| </style> |
| </head> |
| <body> |
|
|
| <h1>SELF-PLAN</h1> |
| <div class="meta">multi-agent orchestration — plans that write themselves</div> |
|
|
| <div class="input-row"> |
| <input id="task" type="text" placeholder="describe a task..." autofocus> |
| <button class="btn" id="run" onclick="generate()">RUN</button> |
| </div> |
|
|
| <div class="section hidden" id="s-plan"> |
| <div class="section-label">plan</div> |
| <div class="code-block" id="plan-code"></div> |
| <div class="confirm-row" id="confirm-row"> |
| <button class="btn" id="btn-execute" onclick="execute()">EXECUTE</button> |
| <button class="btn btn-outline" onclick="reset()">CANCEL</button> |
| </div> |
| </div> |
|
|
| <div class="section hidden" id="s-log"> |
| <div class="section-label">log</div> |
| <div class="log-block" id="log-feed"></div> |
| </div> |
|
|
| <div class="divider hidden" id="s-divider"></div> |
|
|
| <div class="section hidden" id="s-result"> |
| <div class="section-label">result</div> |
| <div class="result-block" id="result-text"></div> |
| </div> |
|
|
| <script> |
| const $ = s => document.getElementById(s); |
| const show = s => $(s).classList.remove("hidden"); |
| const hide = s => $(s).classList.add("hidden"); |
| |
| let currentCode = ""; |
| let currentTask = ""; |
| |
| function addLog(msg, cls = "") { |
| const div = document.createElement("div"); |
| div.className = "log-line" + (cls ? " " + cls : ""); |
| div.innerHTML = msg; |
| $("log-feed").appendChild(div); |
| div.scrollIntoView({behavior: "smooth", block: "end"}); |
| } |
| |
| function fmtLog(msg) { |
| return msg |
| .replace(/▶ \[(\d+)x\]\s*(\w+)/g, '▶ <span class="count">[$1x]</span> <span class="agent-name">$2</span>') |
| .replace(/▶\s*(\w+)/g, '▶ <span class="agent-name">$1</span>') |
| .replace(/✓ \[(\d+)x\]\s*(\w+)/g, '<span class="check">✓</span> <span class="count">[$1x]</span> <span class="agent-name">$2</span>') |
| .replace(/✓\s*(\w+)/g, '<span class="check">✓</span> <span class="agent-name">$1</span>') |
| .replace(/\[([0-9.]+s)\]/g, '<span class="ts">[$1]</span>'); |
| } |
| |
| function reset() { |
| hide("s-plan"); hide("s-log"); hide("s-divider"); hide("s-result"); |
| $("run").disabled = false; |
| $("task").disabled = false; |
| currentCode = ""; |
| } |
| |
| async function generate() { |
| currentTask = $("task").value.trim(); |
| if (!currentTask) return; |
| |
| $("run").disabled = true; |
| $("task").disabled = true; |
| $("log-feed").innerHTML = ""; |
| $("result-text").innerHTML = ""; |
| hide("s-plan"); hide("s-log"); hide("s-divider"); hide("s-result"); |
| |
| show("s-log"); |
| addLog('<span class="spinner">◑</span> generating plan...'); |
| |
| try { |
| const resp = await fetch("/generate?task=" + encodeURIComponent(currentTask)); |
| const data = await resp.json(); |
| currentCode = data.code; |
| |
| $("log-feed").innerHTML = ""; |
| addLog("plan generated"); |
| |
| $("plan-code").innerHTML = hlPython(currentCode); |
| show("s-plan"); |
| } catch(e) { |
| addLog("error: " + e.message, "error"); |
| $("run").disabled = false; |
| $("task").disabled = false; |
| } |
| } |
| |
| function execute() { |
| $("btn-execute").disabled = true; |
| $("log-feed").innerHTML = ""; |
| show("s-log"); |
| |
| const params = new URLSearchParams({task: currentTask, code: currentCode}); |
| const es = new EventSource("/execute?" + params.toString()); |
| |
| es.addEventListener("log", e => { |
| const {msg} = JSON.parse(e.data); |
| const cls = msg.startsWith("done") ? "done" : ""; |
| addLog(fmtLog(msg), cls); |
| }); |
| |
| es.addEventListener("result", e => { |
| const {text} = JSON.parse(e.data); |
| $("result-text").innerHTML = marked.parse(text); |
| show("s-divider"); |
| show("s-result"); |
| es.close(); |
| $("run").disabled = false; |
| $("task").disabled = false; |
| $("btn-execute").disabled = false; |
| }); |
| |
| es.addEventListener("error", e => { |
| if (e.data) { |
| const {msg} = JSON.parse(e.data); |
| addLog(msg, "error"); |
| } |
| es.close(); |
| $("run").disabled = false; |
| $("task").disabled = false; |
| $("btn-execute").disabled = false; |
| }); |
| |
| es.onerror = () => { |
| es.close(); |
| $("run").disabled = false; |
| $("task").disabled = false; |
| $("btn-execute").disabled = false; |
| }; |
| } |
| |
| $("task").addEventListener("keydown", e => { |
| if (e.key === "Enter") generate(); |
| }); |
| |
| function hlPython(code) { |
| const KW = new Set(["def","class","return","if","elif","else","for","while", |
| "in","import","from","as","not","and","or","is","with","try","except", |
| "finally","raise","yield","break","continue","pass","True","False", |
| "None","lambda","global","async","await"]); |
| const tokens = []; |
| let i = 0; |
| while (i < code.length) { |
| if (code[i] === "#") { |
| let j = code.indexOf("\n", i); if (j === -1) j = code.length; |
| tokens.push({type:"cm", text:code.slice(i, j)}); i = j; continue; |
| } |
| if (code.slice(i,i+3) === '"""' || code.slice(i,i+3) === "'''") { |
| const q = code.slice(i,i+3); let j = code.indexOf(q, i+3); |
| j = j === -1 ? code.length : j+3; |
| tokens.push({type:"str", text:code.slice(i,j)}); i = j; continue; |
| } |
| if (code[i]==='"' || code[i]==="'" || (code[i]==='f' && i+1<code.length && (code[i+1]==='"'||code[i+1]==="'"))) { |
| let s = i; if (code[i]==='f') i++; |
| const q = code[i]; i++; |
| while (i < code.length && code[i] !== q) { if (code[i]==="\\") i++; i++; } |
| i++; tokens.push({type:"str", text:code.slice(s,i)}); continue; |
| } |
| if (/\d/.test(code[i]) && (i===0 || !/\w/.test(code[i-1]))) { |
| let j = i; while (j < code.length && /[\d.]/.test(code[j])) j++; |
| tokens.push({type:"num", text:code.slice(i,j)}); i = j; continue; |
| } |
| if (/[a-zA-Z_]/.test(code[i])) { |
| let j = i; while (j < code.length && /\w/.test(code[j])) j++; |
| const word = code.slice(i,j); |
| let k = j; while (k < code.length && code[k]===" ") k++; |
| if (KW.has(word)) tokens.push({type:"kw", text:word}); |
| else if (code[k]==="(") tokens.push({type:"fn", text:word}); |
| else tokens.push({type:"", text:word}); |
| i = j; continue; |
| } |
| if (code[i]==="@" && (i===0 || code[i-1]==="\n")) { |
| let j = i+1; while (j < code.length && /\w/.test(code[j])) j++; |
| tokens.push({type:"dec", text:code.slice(i,j)}); i = j; continue; |
| } |
| tokens.push({type:"", text:code[i]}); i++; |
| } |
| return tokens.map(t => { |
| const esc = t.text.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); |
| return t.type ? `<span class="${t.type}">${esc}</span>` : esc; |
| }).join(""); |
| } |
| </script> |
| </body> |
| </html> |
|
|