app.builder
○ no hash
Components
loading...
0 blocks
DRAG COMPONENTS HERE
or ask Claude to design it
CODIE SOURCE
LIVE PREVIEW
Claude Designer
Tell me what your app should do — I'll write the CODIE and it'll appear on the canvas.

Try: "Build a wallet-gated agent dashboard with a feed and mint button"
Dashboard with feed
Mint page + wallet gate
Agent profile card
Prize desk + SYNTH
Search + data table
Add a skeleton loader
${data.html}`; iframe.srcdoc = wrapped; } catch (e) { console.warn('preview failed:', e); } } function updateHashDisplay(hash) { const el = document.getElementById('codie-hash-display'); el.textContent = '⬡ ' + hash.slice(0, 16) + '…'; el.title = 'BLAKE3: ' + hash + '\n(click to copy)'; } document.getElementById('codie-hash-display').addEventListener('click', () => { if (!currentHash) return; navigator.clipboard.writeText(currentHash).then(() => toast('Hash copied!')); }); // ── Panel toggles ────────────────────────────────────────────────────── document.getElementById('toggle-codie-btn').addEventListener('click', () => { const el = document.getElementById('codie-editor-wrap'); const btn = document.getElementById('toggle-codie-btn'); const open = el.classList.toggle('open'); btn.classList.toggle('active', open); if (open) updateCodeEditor(); }); document.getElementById('toggle-preview-btn').addEventListener('click', () => { const el = document.getElementById('preview-panel'); const btn = document.getElementById('toggle-preview-btn'); const open = el.classList.toggle('open'); btn.classList.toggle('active', open); if (open) schedulePreview(); }); // Canvas mode tabs document.querySelectorAll('.canvas-tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('.canvas-tab').forEach(t => t.classList.remove('active')); tab.classList.add('active'); const mode = tab.dataset.mode; const codie = document.getElementById('codie-editor-wrap'); const preview = document.getElementById('preview-panel'); const codieBtn = document.getElementById('toggle-codie-btn'); const previewBtn = document.getElementById('toggle-preview-btn'); if (mode === 'source') { codie.classList.add('open'); codieBtn.classList.add('active'); preview.classList.remove('open'); previewBtn.classList.remove('active'); updateCodeEditor(); } else if (mode === 'preview') { codie.classList.remove('open'); codieBtn.classList.remove('active'); preview.classList.add('open'); previewBtn.classList.add('active'); schedulePreview(); } else { codie.classList.remove('open'); codieBtn.classList.remove('active'); preview.classList.remove('open'); previewBtn.classList.remove('active'); } }); }); // Device preview buttons document.querySelectorAll('.device-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.device-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const iframe = document.getElementById('preview-iframe'); const d = btn.dataset.device; iframe.className = ''; if (d === 'desktop') iframe.classList.add('desktop'); if (d === 'tablet') iframe.classList.add('tablet'); }); }); // Clear canvas document.getElementById('clear-canvas-btn').addEventListener('click', () => { if (blocks.length === 0) return; if (!confirm('Clear canvas?')) return; blocks = []; renderCanvas(); currentHash = null; document.getElementById('codie-hash-display').textContent = '⬡ no hash'; }); // ── Claude chat ──────────────────────────────────────────────────────── async function sendChatMessage() { const input = document.getElementById('chat-input'); const msg = input.value.trim(); if (!msg) return; input.value = ''; appendChatMsg('user', msg); chatHistory.push({ role: 'user', content: msg }); setThinking(true); try { const res = await fetch('/builder/claude', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: msg, current_codie: blocksToCodie(blocks) || null, history: chatHistory.slice(-10), // last 5 turns wallet: null, }), }); const data = await res.json(); setThinking(false); totalTokens += data.tokens_used || 0; document.getElementById('chat-token-display').textContent = totalTokens > 0 ? totalTokens + ' tokens' : ''; // Store assistant reply in history chatHistory.push({ role: 'assistant', content: data.message }); // Render the message, highlighting CODIE blocks appendChatAssistant(data); } catch (e) { setThinking(false); appendChatMsg('assistant', '⚠ Connection error. Check the API server.'); setStatusDot('error'); setTimeout(() => setStatusDot('ready'), 3000); } } function appendChatMsg(role, text) { const history = document.getElementById('chat-history'); const div = document.createElement('div'); div.className = 'chat-msg ' + role; div.innerHTML = `
${role === 'user' ? 'You' : '⚡'}
${escHtml(text).replace(/\n/g,'
')}
`; history.appendChild(div); history.scrollTop = history.scrollHeight; } function appendChatAssistant(data) { const history = document.getElementById('chat-history'); const div = document.createElement('div'); div.className = 'chat-msg assistant'; // Strip the ```codie block from displayed text, show it separately let displayText = data.message .replace(/```codie[\s\S]*?```/g, '') .trim(); let inner = `
${displayText ? escHtml(displayText).replace(/\n/g,'
') : ''} `; if (data.codie) { inner += `
${escHtml(data.codie)}
`; } inner += '
'; div.innerHTML = inner; history.appendChild(div); history.scrollTop = history.scrollHeight; // If Claude produced CODIE with a hash, show it if (data.hash) { currentHash = data.hash; updateHashDisplay(data.hash); } } function applyClaudeCodie(codie) { blocks = codieToBlocks(codie); renderCanvas(); // Open preview if it's closed const previewPanel = document.getElementById('preview-panel'); if (!previewPanel.classList.contains('open')) { document.getElementById('toggle-preview-btn').click(); } toast('✓ Canvas updated from Claude'); } window.applyClaudeCodie = applyClaudeCodie; function sendSuggestion(chip) { document.getElementById('chat-input').value = chip.textContent; sendChatMessage(); } window.sendSuggestion = sendSuggestion; document.getElementById('chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChatMessage(); } }); document.getElementById('chat-send-btn').addEventListener('click', sendChatMessage); function setThinking(on) { const btn = document.getElementById('chat-send-btn'); btn.disabled = on; setStatusDot(on ? 'thinking' : 'ready'); if (on) { const history = document.getElementById('chat-history'); const div = document.createElement('div'); div.id = 'thinking-indicator'; div.className = 'chat-msg assistant'; div.innerHTML = `
designing
`; history.appendChild(div); history.scrollTop = history.scrollHeight; } else { const el = document.getElementById('thinking-indicator'); if (el) el.remove(); } } function setStatusDot(state) { const dot = document.getElementById('claude-status-dot'); dot.className = 'claude-status-dot'; if (state === 'thinking') dot.classList.add('thinking'); if (state === 'error') dot.classList.add('error'); } // ── Save / Deploy ────────────────────────────────────────────────────── document.getElementById('save-btn').addEventListener('click', async () => { const name = document.getElementById('app-name-input').value.trim() || 'Untitled App'; const codie = blocksToCodie(blocks).trim(); if (!codie) { toast('Canvas is empty', 'err'); return; } setSaveStatus('saving…'); try { // Ensure we have a fresh hash const preRes = await fetch('/builder/preview', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ codie }), }); const preData = await preRes.json(); // Check for existing app_id in URL params const params = new URLSearchParams(location.search); const appId = params.get('app_id'); const agentId = params.get('agent_id') || '00000000-0000-0000-0000-000000000001'; const wallet = params.get('wallet') || ''; const body = { owner_wallet: wallet, agent_id: agentId, name, xml_composition: codie, compiled_html: preData.html || '', codie_hash: preData.hash, }; let saveRes; if (appId) { saveRes = await fetch('/apps/' + appId, { method: 'PUT', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ name, xml_composition: codie, compiled_html: preData.html, codie_hash: preData.hash }), }); } else { saveRes = await fetch('/apps', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body), }); } if (saveRes.ok) { const saved = await saveRes.json(); if (!appId) { const url = new URL(location.href); url.searchParams.set('app_id', saved.id); history.replaceState({}, '', url); } setSaveStatus('saved ✓'); toast('App saved!'); setTimeout(() => setSaveStatus(''), 3000); } else { setSaveStatus(''); toast('Save failed', 'err'); } } catch (e) { setSaveStatus(''); toast('Save error: ' + e.message, 'err'); } }); document.getElementById('deploy-btn').addEventListener('click', async () => { // Deploy = save + open the rendered app at /render/:hash if (!currentHash) { toast('Build first (preview)', 'err'); return; } window.open('/render/' + currentHash, '_blank'); }); function setSaveStatus(msg) { document.getElementById('save-status').textContent = msg; } // ── Toast ────────────────────────────────────────────────────────────── function toast(msg, type = 'ok') { const el = document.createElement('div'); el.className = 'toast ' + type; el.textContent = msg; document.getElementById('toast-container').appendChild(el); setTimeout(() => el.remove(), 3000); } // ── Palette loader ───────────────────────────────────────────────────── async function loadPalette() { try { const res = await fetch('/builder/components'); const groups = await res.json(); const scroll = document.getElementById('palette-scroll'); scroll.innerHTML = ''; groups.forEach(group => { const g = document.createElement('div'); g.className = 'pal-group'; g.innerHTML = `
${group.group.toUpperCase()}
`; const items = g.querySelector('.pal-items'); group.items.forEach(item => { const tile = document.createElement('div'); tile.className = 'pal-tile'; tile.draggable = true; tile.dataset.codie = item.codie; tile.dataset.label = item.label; tile.innerHTML = `
${item.icon}
${item.label}
${item.desc}
${item.id} `; initPaletteDrag(tile, item); // Click to add directly tile.addEventListener('click', () => { blocks.push(makeBlock(item)); renderCanvas(); toast(item.icon + ' ' + item.label + ' added'); }); items.appendChild(tile); }); scroll.appendChild(g); }); // Palette search filter document.getElementById('palette-search').addEventListener('input', function() { const q = this.value.toLowerCase(); document.querySelectorAll('.pal-tile').forEach(tile => { const match = tile.dataset.label.toLowerCase().includes(q) || tile.dataset.codie.toLowerCase().includes(q); tile.style.display = match ? '' : 'none'; }); }); } catch (e) { document.getElementById('palette-scroll').innerHTML = '
Failed to load components
'; } } // ── Load existing app from URL ───────────────────────────────────────── async function loadExistingApp() { const params = new URLSearchParams(location.search); const appId = params.get('app_id'); if (!appId) return; try { const res = await fetch('/apps/' + appId); if (!res.ok) return; const app = await res.json(); document.getElementById('app-name-input').value = app.name; if (app.xml_composition) { blocks = codieToBlocks(app.xml_composition); renderCanvas(); } if (app.codie_hash) { currentHash = app.codie_hash; updateHashDisplay(app.codie_hash); } toast('App loaded: ' + app.name); } catch (e) { console.warn('Failed to load app:', e); } } // ── Boot ─────────────────────────────────────────────────────────────── (async () => { await loadPalette(); await loadExistingApp(); renderCanvas(); })();