/** * ╔══════════════════════════════════════════════════════╗ * ║ WIZARD-VIBE STUDIO — Client Runtime ║ * ║ SSE Streaming · Sandbox iframe · Publish Gate ║ * ╚══════════════════════════════════════════════════════╝ */ const S = { sessionId: null, sandboxStatus: 'idle', fullCode: '', errorsFound: 0, errorsFixed: 0, publishData: null }; const D = { prompt: document.getElementById('prompt-input'), vibeBtn: document.getElementById('vibe-btn'), codeContent: document.getElementById('code-content'), previewFrame: document.getElementById('preview-frame'), previewOverlay: document.getElementById('preview-overlay'), statusDot: document.getElementById('status-dot'), statusText: document.getElementById('status-text'), statusPhase: document.getElementById('status-phase'), sandboxBadge: document.getElementById('sandbox-badge'), publishBtn: document.getElementById('publish-btn'), publishGate: document.getElementById('publish-gate'), gateIcon: document.querySelector('.gate-icon'), gateText: document.querySelector('.gate-text'), publishResult: document.getElementById('publish-result'), publishUrl: document.getElementById('publish-url'), githubLink: document.getElementById('github-link'), spacesLink: document.getElementById('spaces-link'), agentLink: document.getElementById('agent-link'), modelBadges: document.getElementById('model-badges'), wizardHat: document.getElementById('wizard-hat'), statusRing: document.getElementById('status-ring'), copyBtn: document.getElementById('copy-btn'), }; // ═══ SSE STREAMING — ASYNC ITERATOR CLIENT ═══ async function* streamFromServer(prompt) { const resp = await fetch('/api/stream', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({prompt}) }); if (!resp.ok) throw new Error(`Stream error: ${resp.status}`); const reader = resp.body.getReader(), dec = new TextDecoder(); let buf = ''; while (true) { const {done, value} = await reader.read(); if (done) break; buf += dec.decode(value, {stream:true}); const lines = buf.split('\n'); buf = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const d = line.slice(6); if (!d || d==='{}') continue; try { yield JSON.parse(d); } catch { yield {raw:d}; } } } } } // ═══ STATE MACHINE — WIZARD HAT MORPHING ═══ function setSandboxStatus(st) { S.sandboxStatus = st; D.wizardHat.classList.remove('stable','error','published','building'); D.statusRing.classList.remove('stable','error','published'); D.statusDot.classList.remove('building','streaming','stable','error','published'); D.publishGate.classList.remove('unlocked'); const map = { building: {hat:'building', dot:'building', badge:'🔨', btn:true, gate:[false,'🔒','Building...']}, streaming: {hat:'', dot:'streaming',badge:'📡', btn:true, gate:[false,'🔒','Streaming...']}, stable: {hat:'stable', dot:'stable', badge:'✅', btn:false,gate:[true,'🔓','Sandbox stable — Ready']}, error: {hat:'error', dot:'error', badge:'❌', btn:true, gate:[false,'🔒','Sandbox errors']}, published: {hat:'published',dot:'published',badge:'🌟',btn:false,gate:[true,'🌟','Published to A2A']}, }; const m = map[st] || map.building; if (m.hat) D.wizardHat.classList.add(m.hat); if (st==='stable'||st==='published') D.statusRing.classList.add(st); D.statusDot.classList.add(m.dot); D.sandboxBadge.textContent = m.badge; D.publishBtn.disabled = m.btn; if (m.gate) { if (m.gate[0]) D.publishGate.classList.add('unlocked'); D.gateIcon.textContent = m.gate[1]; D.gateText.textContent = m.gate[2]; } if (st==='published') D.publishBtn.classList.add('gold'); else D.publishBtn.classList.remove('gold'); D.statusText.textContent = st.charAt(0).toUpperCase()+st.slice(1); } // ═══ IFRAME HOT-RELOAD ═══ function updateIframe(code) { const blob = new Blob([code], {type:'text/html'}); const url = URL.createObjectURL(blob); if (D.previewFrame._blobUrl) URL.revokeObjectURL(D.previewFrame._blobUrl); D.previewFrame._blobUrl = url; D.previewFrame.src = url; D.previewOverlay.classList.add('hidden'); } function appendCode(chunk) { if (D.codeContent.querySelector('.placeholder')) D.codeContent.innerHTML = ''; const span = document.createElement('span'); span.className='code-chunk-new'; span.textContent=chunk; D.codeContent.appendChild(span); D.codeContent.parentElement.scrollTop = D.codeContent.parentElement.scrollHeight; } function updateModelBadges(plan) { D.modelBadges.innerHTML = ''; if (!plan?.models) return; plan.models.forEach(m => { const b = document.createElement('span'); b.className = `model-badge ${m.task_type}`; b.textContent = m.model_id.split('/').pop(); b.title = `${m.model_id} (${Math.round(m.confidence*100)}%)`; D.modelBadges.appendChild(b); }); } // ═══ MAIN GENERATION ═══ async function generate() { const prompt = D.prompt.value.trim(); if (!prompt) return; S.fullCode = ''; S.errorsFound = 0; S.errorsFixed = 0; S.publishData = null; D.codeContent.innerHTML = 'Streaming...'; D.previewOverlay.classList.remove('hidden'); D.publishResult.style.display = 'none'; D.publishBtn.classList.remove('gold'); D.publishBtn.disabled = true; D.modelBadges.innerHTML = ''; D.vibeBtn.disabled = true; D.vibeBtn.querySelector('.btn-text').textContent = 'Generating...'; setSandboxStatus('building'); D.statusPhase.textContent = 'Connecting...'; try { for await (const ev of streamFromServer(prompt)) { if (ev.phase) D.statusPhase.textContent = ev.phase; if (ev.plan) updateModelBadges(ev.plan); if (ev.chunk) { appendCode(ev.chunk); S.fullCode += ev.chunk; } if (ev.partial) updateIframe(ev.partial); if (ev.heal) { S.errorsFound = ev.errors_found; S.errorsFixed = ev.errors_fixed; } if (ev.sandbox) setSandboxStatus(ev.status); } } catch (err) { console.error('Stream error:', err); setSandboxStatus('error'); D.statusText.textContent = `Error: ${err.message}`; } finally { D.vibeBtn.disabled = false; D.vibeBtn.querySelector('.btn-text').textContent = 'Generate'; D.statusText.textContent = 'Done'; } } // ═══ PUBLISH ═══ async function publish() { const repoName = prompt('Repo name:', `wizard-vibe-${Date.now().toString(36)}`); if (!repoName) return; D.publishBtn.disabled = true; D.publishBtn.querySelector('.btn-text').textContent = 'Publishing...'; try { const resp = await fetch('/api/publish', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({repo_name:repoName, description:D.prompt.value.trim().slice(0,200)}), }); const r = await resp.json(); if (r.success) { S.publishData = r; setSandboxStatus('published'); D.publishResult.style.display = 'block'; D.publishUrl.textContent = r.spaces?.url || r.github?.repo_url || 'Published'; if (r.github?.repo_url) { D.githubLink.href = r.github.repo_url; D.githubLink.style.display = 'inline'; } if (r.spaces?.url) { D.spacesLink.href = r.spaces.url; D.spacesLink.style.display = 'inline'; } if (r.agent_url) { D.agentLink.href = r.agent_url; D.agentLink.style.display = 'inline'; } D.publishResult.scrollIntoView({behavior:'smooth',block:'nearest'}); } else { D.statusText.textContent = `Publish error: ${r.error}`; } } catch (err) { D.statusText.textContent = `Publish error: ${err.message}`; } finally { D.publishBtn.disabled = false; D.publishBtn.querySelector('.btn-text').textContent = 'Publish to A2A Network'; } } // ═══ EVENT BINDINGS ═══ D.vibeBtn.addEventListener('click', generate); D.publishBtn.addEventListener('click', publish); D.prompt.addEventListener('keydown', e => { if ((e.ctrlKey||e.metaKey) && e.key==='Enter') { e.preventDefault(); generate(); } }); D.copyBtn.addEventListener('click', async () => { if (!S.fullCode) return; try { await navigator.clipboard.writeText(S.fullCode); } catch {} }); console.log('🧙‍♂️ Wizard-Vibe Studio — Liquid Glass Client ready');