Wizard-Vibe-Core / static /wizard.js
dryymatt's picture
πŸ§™β€β™‚οΈ First Commit: static/wizard.js
de6cc14 verified
/**
* ╔══════════════════════════════════════════════════════╗
* β•‘ 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 = '<span class="placeholder">Streaming...</span>';
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');