Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>beacon — acecalisto3</title> | |
| <script type="module" src="https://ajax.googleapis.com/ajax/libs/model-viewer/3.4.0/model-viewer.min.js"></script> | |
| <style> | |
| :root { | |
| --bg: #0a0a0f; | |
| --surface: #12121a; | |
| --border: #1e1e2e; | |
| --accent: #7df; | |
| --accent2: #afc; | |
| --text: #dde; | |
| --muted: #556; | |
| --error: #f77; | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { background: var(--bg); color: var(--text); font-family: 'SF Mono', 'Fira Code', monospace; min-height: 100vh; } | |
| header { | |
| padding: 1.5rem 2rem; | |
| border-bottom: 1px solid var(--border); | |
| display: flex; align-items: center; gap: 1rem; | |
| } | |
| header h1 { font-size: 1.1rem; color: var(--accent); letter-spacing: .1em; } | |
| header span { color: var(--muted); font-size: .85rem; } | |
| .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent2); box-shadow: 0 0 8px var(--accent2); animation: pulse 2s infinite; } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} } | |
| main { display: grid; grid-template-columns: 340px 1fr; min-height: calc(100vh - 65px); } | |
| /* ── Left: space cards ── */ | |
| .sidebar { border-right: 1px solid var(--border); padding: 1.5rem 1rem; overflow-y: auto; } | |
| .sidebar h2 { font-size: .7rem; color: var(--muted); letter-spacing: .15em; text-transform: uppercase; margin-bottom: 1rem; padding: 0 .5rem; } | |
| .card { | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| padding: .85rem 1rem; | |
| margin-bottom: .6rem; | |
| cursor: pointer; | |
| transition: border-color .15s, background .15s; | |
| } | |
| .card:hover { border-color: var(--accent); background: #12121a; } | |
| .card.active { border-color: var(--accent); background: #0d1a1f; } | |
| .card-cap { font-size: .65rem; color: var(--accent); letter-spacing: .12em; text-transform: uppercase; margin-bottom: .3rem; } | |
| .card-name { font-size: .9rem; color: var(--text); margin-bottom: .25rem; } | |
| .card-desc { font-size: .75rem; color: var(--muted); line-height: 1.4; } | |
| /* ── Right: active panel ── */ | |
| .panel { display: flex; flex-direction: column; padding: 2rem; gap: 1.5rem; overflow-y: auto; } | |
| .empty-state { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--muted); gap: .5rem; } | |
| .empty-state .icon { font-size: 2.5rem; opacity: .3; } | |
| .panel-header h2 { font-size: 1rem; color: var(--text); margin-bottom: .25rem; } | |
| .panel-header p { font-size: .8rem; color: var(--muted); } | |
| .badge { display: inline-block; font-size: .6rem; background: #0d1a1f; color: var(--accent); border: 1px solid var(--accent); border-radius: 4px; padding: .1rem .4rem; letter-spacing: .1em; text-transform: uppercase; margin-right: .4rem; } | |
| .prompt-area label { display: block; font-size: .7rem; color: var(--muted); letter-spacing: .1em; text-transform: uppercase; margin-bottom: .5rem; } | |
| textarea { | |
| width: 100%; background: var(--surface); border: 1px solid var(--border); | |
| border-radius: 6px; color: var(--text); font-family: inherit; font-size: .9rem; | |
| padding: .75rem 1rem; resize: vertical; min-height: 80px; line-height: 1.5; | |
| transition: border-color .15s; | |
| } | |
| textarea:focus { outline: none; border-color: var(--accent); } | |
| textarea::placeholder { color: var(--muted); } | |
| /* curl preview */ | |
| .preview-box { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 1rem; } | |
| .preview-box label { display: block; font-size: .65rem; color: var(--muted); letter-spacing: .12em; text-transform: uppercase; margin-bottom: .6rem; } | |
| .preview-box pre { font-size: .78rem; color: var(--accent2); line-height: 1.6; overflow-x: auto; white-space: pre-wrap; word-break: break-all; } | |
| .preview-box .resolving { color: var(--muted); font-style: italic; font-size: .8rem; } | |
| .actions { display: flex; gap: .75rem; align-items: center; } | |
| button { | |
| background: transparent; border: 1px solid var(--accent); color: var(--accent); | |
| border-radius: 6px; padding: .55rem 1.2rem; font-family: inherit; font-size: .85rem; | |
| cursor: pointer; letter-spacing: .05em; transition: background .15s, color .15s; | |
| } | |
| button:hover { background: var(--accent); color: var(--bg); } | |
| button:disabled { opacity: .4; cursor: not-allowed; } | |
| button.primary { background: var(--accent); color: var(--bg); } | |
| button.primary:hover { background: #9ef; } | |
| .status { font-size: .8rem; color: var(--muted); } | |
| /* output */ | |
| .output-box { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; } | |
| .output-header { padding: .75rem 1rem; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; } | |
| .output-header span { font-size: .7rem; color: var(--muted); letter-spacing: .1em; text-transform: uppercase; } | |
| .output-header .dl-links { display: flex; gap: .5rem; } | |
| .output-header a { font-size: .75rem; color: var(--accent); text-decoration: none; border: 1px solid var(--border); border-radius: 4px; padding: .2rem .6rem; } | |
| .output-header a:hover { border-color: var(--accent); } | |
| .output-body { padding: 1rem; } | |
| model-viewer { width: 100%; height: 400px; background: #080810; border-radius: 0 0 8px 8px; --poster-color: transparent; } | |
| .output-body img { max-width: 100%; border-radius: 4px; } | |
| .output-body audio { width: 100%; } | |
| .output-body pre { font-size: .8rem; line-height: 1.6; color: var(--accent2); white-space: pre-wrap; max-height: 400px; overflow-y: auto; } | |
| .output-body video { max-width: 100%; border-radius: 4px; } | |
| .error-msg { color: var(--error); font-size: .85rem; padding: .5rem 0; } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="dot"></div> | |
| <h1>📡 beacon</h1> | |
| <span id="owner-tag">acecalisto3 · loading spaces...</span> | |
| </header> | |
| <main> | |
| <aside class="sidebar"> | |
| <h2>available spaces</h2> | |
| <div id="cards"></div> | |
| </aside> | |
| <div class="panel" id="panel"> | |
| <div class="empty-state"> | |
| <div class="icon">⬡</div> | |
| <div>select a space to begin</div> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| const BASE = window.location.origin | |
| let activeSpace = null | |
| let previewTimer = null | |
| let currentOutputUrls = [] | |
| // ── Load space cards ────────────────────────────────────────────────────────── | |
| async function loadSpaces() { | |
| const res = await fetch(`${BASE}/space/list`) | |
| const spaces = await res.json() | |
| document.getElementById('owner-tag').textContent = `acecalisto3 · ${spaces.length} spaces` | |
| const cards = document.getElementById('cards') | |
| spaces.forEach(s => { | |
| const el = document.createElement('div') | |
| el.className = 'card' | |
| el.dataset.id = s.id | |
| el.innerHTML = ` | |
| <div class="card-cap">${s.capability}</div> | |
| <div class="card-name">${s.name}</div> | |
| <div class="card-desc">${s.description}</div> | |
| ` | |
| el.addEventListener('click', () => activateSpace(s, el)) | |
| cards.appendChild(el) | |
| }) | |
| } | |
| // ── Activate a space card ───────────────────────────────────────────────────── | |
| function activateSpace(space, el) { | |
| document.querySelectorAll('.card').forEach(c => c.classList.remove('active')) | |
| el.classList.add('active') | |
| activeSpace = space | |
| currentOutputUrls = [] | |
| renderPanel(space) | |
| } | |
| function renderPanel(space) { | |
| const inputHints = space.inputs.map(i => | |
| `${i.name}${i.required ? ' *' : ''}: ${i.description || i.type}` | |
| ).join(' · ') | |
| document.getElementById('panel').innerHTML = ` | |
| <div class="panel-header"> | |
| <h2> | |
| <span class="badge">${space.capability}</span> | |
| ${space.id} | |
| </h2> | |
| <p>${space.description}</p> | |
| ${inputHints ? `<p style="margin-top:.4rem;font-size:.75rem;color:var(--muted)">inputs: ${inputHints}</p>` : ''} | |
| </div> | |
| <div class="prompt-area"> | |
| <label>what do you want?</label> | |
| <textarea id="prompt-input" placeholder="describe your request in plain language — paste URLs directly in here too" rows="3"></textarea> | |
| </div> | |
| <div class="preview-box" id="preview-box"> | |
| <label>structured request preview</label> | |
| <div class="resolving">start typing to see the resolved call...</div> | |
| </div> | |
| <div class="actions"> | |
| <button class="primary" id="execute-btn" disabled>Execute →</button> | |
| <span class="status" id="status-msg"></span> | |
| </div> | |
| <div id="output-area"></div> | |
| ` | |
| document.getElementById('prompt-input').addEventListener('input', onPromptInput) | |
| document.getElementById('execute-btn').addEventListener('click', onExecute) | |
| } | |
| // ── Live preview (debounced) ────────────────────────────────────────────────── | |
| function onPromptInput(e) { | |
| const prompt = e.target.value.trim() | |
| const previewBox = document.getElementById('preview-box') | |
| const execBtn = document.getElementById('execute-btn') | |
| clearTimeout(previewTimer) | |
| execBtn.disabled = true | |
| if (!prompt) { | |
| previewBox.querySelector('div').className = 'resolving' | |
| previewBox.querySelector('div').textContent = 'start typing to see the resolved call...' | |
| return | |
| } | |
| previewBox.querySelector('div').className = 'resolving' | |
| previewBox.querySelector('div').textContent = 'resolving...' | |
| previewTimer = setTimeout(async () => { | |
| try { | |
| const res = await fetch(`${BASE}/space/preview`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ space: activeSpace.id, prompt }) | |
| }) | |
| const data = await res.json() | |
| if (data.error) throw new Error(data.error) | |
| const preview = document.getElementById('preview-box') | |
| preview.innerHTML = ` | |
| <label>structured request preview</label> | |
| <pre>${escHtml(data.curl)}</pre> | |
| <pre style="margin-top:.5rem;color:var(--muted);font-size:.72rem">${escHtml(JSON.stringify(data.structured, null, 2))}</pre> | |
| ` | |
| execBtn.disabled = false | |
| } catch (err) { | |
| const preview = document.getElementById('preview-box') | |
| preview.innerHTML = `<label>structured request preview</label><div class="resolving">${escHtml(err.message)}</div>` | |
| } | |
| }, 600) | |
| } | |
| // ── Execute ─────────────────────────────────────────────────────────────────── | |
| async function onExecute() { | |
| const prompt = document.getElementById('prompt-input').value.trim() | |
| const btn = document.getElementById('execute-btn') | |
| const status = document.getElementById('status-msg') | |
| btn.disabled = true | |
| status.textContent = 'calling space...' | |
| try { | |
| const res = await fetch(`${BASE}/space/ask`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ space: activeSpace.id, prompt }) | |
| }) | |
| const data = await res.json() | |
| if (data.error) throw new Error(data.error) | |
| status.textContent = 'done' | |
| currentOutputUrls = data.output_urls ?? [] | |
| renderOutput(data) | |
| } catch (err) { | |
| status.textContent = '' | |
| document.getElementById('output-area').innerHTML = | |
| `<div class="error-msg">⚠ ${escHtml(err.message)}</div>` | |
| } | |
| btn.disabled = false | |
| } | |
| // ── Render output ───────────────────────────────────────────────────────────── | |
| function renderOutput(data) { | |
| const urls = data.output_urls ?? [] | |
| const area = document.getElementById('output-area') | |
| const dlLinks = urls.map(u => { | |
| const filename = u.split('/').pop().split('?')[0] || 'output' | |
| return `<a href="${escHtml(u)}" download="${escHtml(filename)}" target="_blank">↓ ${escHtml(filename)}</a>` | |
| }).join('') | |
| let body = '' | |
| for (const url of urls) { | |
| const ext = url.split('.').pop().toLowerCase().split('?')[0] | |
| if (['glb', 'gltf', 'obj'].includes(ext)) { | |
| body += `<model-viewer src="${escHtml(url)}" ar auto-rotate camera-controls shadow-intensity="1"></model-viewer>` | |
| } else if (['png','jpg','jpeg','webp','gif'].includes(ext)) { | |
| body += `<img src="${escHtml(url)}" alt="output">` | |
| } else if (['mp4','webm'].includes(ext)) { | |
| body += `<video src="${escHtml(url)}" controls autoplay muted></video>` | |
| } else if (['mp3','wav','ogg','flac'].includes(ext)) { | |
| body += `<audio src="${escHtml(url)}" controls></audio>` | |
| } else { | |
| body += `<pre><a href="${escHtml(url)}" target="_blank">${escHtml(url)}</a></pre>` | |
| } | |
| } | |
| // If no file outputs, show raw data | |
| if (!body) { | |
| const raw = JSON.stringify(data.outputs ?? data, null, 2) | |
| body = `<pre>${escHtml(raw)}</pre>` | |
| } | |
| area.innerHTML = ` | |
| <div class="output-box"> | |
| <div class="output-header"> | |
| <span>output</span> | |
| <div class="dl-links">${dlLinks}</div> | |
| </div> | |
| <div class="output-body">${body}</div> | |
| </div> | |
| ` | |
| } | |
| function escHtml(s) { | |
| return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"') | |
| } | |
| loadSpaces() | |
| </script> | |
| </body> | |
| </html> | |