Meter AI
Online · Ready to build
Tell me what you're building — we'll build it together

Press Enter to send

Type anything. "I run a pizza shop." "Build me a CRM." "I need a stock screener."
Meter builds it in 60 seconds.

0%
Building your app...
Connect Your Data Sources
Hire an AI Agent
Quick Actions
You're live.
'; iframe.srcdoc = shell; await new Promise(resolve => { let done = false; const fin = () => { if (!done) { done = true; resolve(); } }; iframe.addEventListener('load', fin, { once: true }); setTimeout(fin, 1200); }); const iDoc = iframe.contentDocument; if (!iDoc) { iframe.srcdoc = fullHtml; return; } const root = iDoc.getElementById('mc-build-root'); if (!root) { iframe.srcdoc = fullHtml; return; } const stagger = fast ? 90 : 180; for (let i = 0; i < blocks.length; i++) { const node = blocks[i].cloneNode(true); node.classList.add('mc-block'); root.appendChild(node); try { node.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (e) {} await new Promise(r => setTimeout(r, stagger)); } } /* Dynamic narration — return build-step copy that reflects what the user asked for. Pat's "football coach" principle: human needs to feel like the system is thinking ABOUT THEIR specific request, not running the same 4-step script for everyone. */ function narrateBuild(prompt) { const p = (prompt || '').toLowerCase(); // Keyword-tuned step sets — more specific than generic "Building..." const POOLS = [ { match: /bookkeep|accounting|expense|ledger|invoice|receipt/, steps: ['Sketching your books layout...', 'Adding expense categories...', 'Wiring the receipt list...', 'Drafting a monthly summary...'] }, { match: /etsy|shopify|ecommerce|store|product|cart/, steps: ['Framing the storefront...', 'Wiring product cards + inventory...', 'Adding checkout flow...', 'Drafting order-history view...'] }, { match: /crm|lead|pipeline|contact|sales/, steps: ['Laying out the pipeline...', 'Adding contact records...', 'Wiring deal-stage drag-drop...', 'Drafting activity feed...'] }, { match: /restaurant|food|menu|table|reservation|kitchen/, steps: ['Sketching the floor view...', 'Adding the menu + modifiers...', 'Wiring table turns + wait times...', 'Drafting tonight\'s covers...'] }, { match: /booking|appointment|schedul|calendar|slot/, steps: ['Framing your calendar...', 'Adding availability slots...', 'Wiring booking confirmations...', 'Drafting upcoming appointments...'] }, { match: /dashboard|analytics|metric|kpi|chart|report/, steps: ['Laying out the dashboard grid...', 'Wiring live KPIs...', 'Drafting charts + sparklines...', 'Pulling in the data sources...'] }, { match: /trading|portfolio|stock|crypto|bloomberg|invest/, steps: ['Sketching the terminal...', 'Adding ticker + watchlist...', 'Wiring chart + news feed...', 'Drafting positions + P&L...'] }, { match: /patient|clinic|therapy|health|intake|ehr/, steps: ['Laying out the chart view...', 'Adding intake + notes...', 'Wiring appointments + billing...', 'Drafting patient summary...'] }, { match: /legal|law|contract|firm|attorney|case/, steps: ['Framing the matter list...', 'Adding document drafting...', 'Wiring billing + trust accounting...', 'Drafting client portal...'] }, { match: /field|dispatch|hvac|plumb|technician|crew/, steps: ['Sketching the dispatch board...', 'Adding crew + job cards...', 'Wiring route optimization...', 'Drafting the customer SMS flow...'] }, { match: /newsletter|blog|content|publish|subscrib/, steps: ['Framing the editor...', 'Adding subscriber list + segments...', 'Wiring Stripe subscriptions...', 'Drafting the send flow...'] }, { match: /property|real.?estate|rental|tenant|mls/, steps: ['Laying out listings grid...', 'Adding property details + photos...', 'Wiring comps + cap rate...', 'Drafting showing scheduler...'] }, ]; for (const pool of POOLS) { if (pool.match.test(p)) return pool.steps; } // Generic but specific-feeling fallback — drops the prompt's own words in const shortPrompt = (prompt || 'your app').trim().slice(0, 40); return [ `Reading your request: "${shortPrompt}"...`, 'Picking the right Tools + Services...', 'Sketching the layout...', 'Wiring the interactive pieces...', 'Almost there...' ]; } async function chatBuildOrExtend(text) { const iframe = document.getElementById('previewIframe'); const pb = document.getElementById('progressBar'); const overlay = document.getElementById('buildingOverlay'); const label = document.getElementById('buildingLabel'); const stepsEl = document.getElementById('buildingSteps'); const percentEl = document.getElementById('buildingPercent'); const placeholder = document.getElementById('previewPlaceholder'); const iframeWrap = document.getElementById('iframeWrap'); // Snapshot current app HTML for extend context let currentHtml = ''; try { if (iframe && iframe.srcdoc && iframe.srcdoc.length > 500) { currentHtml = iframe.srcdoc; } else if (iframe && iframe.contentDocument && iframe.contentDocument.documentElement) { currentHtml = iframe.contentDocument.documentElement.outerHTML; } } catch (e) { /* cross-origin fallback — treat as fresh build */ } const isExtend = !!(currentHtml && currentHtml.length > 800); addMeterMessage(isExtend ? `Adding that to your app now — watch the preview.` : `Building it — watch the preview.`, 150); // Visual building state (reusing existing DOM) state.building = true; if (placeholder) placeholder.style.display = 'none'; if (iframe) iframe.style.display = 'block'; if (iframeWrap) iframeWrap.classList.add('building-glow'); if (pb) pb.className = 'progress-bar running'; if (!isExtend && overlay) { overlay.className = 'building-overlay active'; if (label) label.textContent = 'Building your app...'; if (stepsEl) stepsEl.innerHTML = ''; if (percentEl) percentEl.textContent = '0%'; // Dynamic narration: steps that reflect what THEY asked for, not generic copy const steps = narrateBuild(text); steps.forEach((s, i) => { const el = document.createElement('div'); el.className = 'building-step'; el.innerHTML = `
${s}`; stepsEl.appendChild(el); setTimeout(() => { if (i > 0) stepsEl.children[i - 1].className = 'building-step visible done'; el.className = 'building-step visible active'; }, i * 500); }); } let pct = 0; const pctInterval = setInterval(() => { pct = Math.min(pct + 1, 95); if (percentEl) percentEl.textContent = pct + '%'; }, 250); const prompt = isExtend ? `Extend the following HTML app by adding: "${text}". Preserve EVERY existing section, style, color, module, and piece of demo data. Add the new functionality as a clearly visible new section. Return the FULL updated HTML — no markdown, no fences, starting with .\n\n\n${currentHtml}\n` : text; const t0 = Date.now(); try { try { window.mc && window.mc.track('build_started', { extend: isExtend, prompt_length: (text||'').length }); } catch(_){} // BF-02 2026-04-17: consume SSE so tokens paint into the iframe live. // No more 15-second silent wait — the user sees the build assemble. const res = await fetch('https://metercall-builder.fly.dev/api/build', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' }, body: JSON.stringify({ prompt }) }); if (!res.ok) { const errBody = await res.text().catch(() => ''); throw new Error('builder ' + res.status + (errBody ? ': ' + errBody.slice(0, 120) : '')); } if (!res.body) throw new Error('no response body'); const reader = res.body.getReader(); const decoder = new TextDecoder(); let sseBuf = ''; let accumulated = ''; let lastPaintAt = 0; let doneData = null; let streamError = null; let validationReasons = null; // Show the iframe and start progressive painting. if (iframe && placeholder) placeholder.style.display = 'none'; if (iframe) iframe.style.display = 'block'; while (true) { const { done, value } = await reader.read(); if (done) break; sseBuf += decoder.decode(value, { stream: true }); const events = sseBuf.split('\n\n'); sseBuf = events.pop(); for (const block of events) { const lines = block.split('\n'); let eventType = 'message'; let dataLine = null; for (const ln of lines) { if (ln.startsWith('event:')) eventType = ln.slice(6).trim(); else if (ln.startsWith('data:')) dataLine = ln.slice(5).trim(); } if (!dataLine) continue; let payload; try { payload = JSON.parse(dataLine); } catch(_) { continue; } if (eventType === 'chunk' && payload.text) { accumulated += payload.text; // Progressive paint — only once we have so we don't render plain text. const now = Date.now(); if (iframe && now - lastPaintAt > 120 && accumulated.includes(']/i); iframe.srcdoc = idx >= 0 ? accumulated.slice(idx) : accumulated; lastPaintAt = now; } } else if (eventType === 'provider') { // Optional: could surface "Building with Groq..." to the user. } else if (eventType === 'validation') { if (!payload.ok) { validationReasons = payload.reasons || []; // Wipe the bad partial render and wait for the retry stream. accumulated = ''; if (iframe) iframe.srcdoc = ''; } } else if (eventType === 'attempt') { // Starting a new attempt — clear the accumulator to catch the retry stream. if (payload.attempt > 1) { accumulated = ''; if (iframe) iframe.srcdoc = ''; } } else if (eventType === 'done') { doneData = payload; } else if (eventType === 'error') { streamError = payload; } } } if (streamError) { throw new Error(streamError.error + (streamError.detail ? ': ' + streamError.detail : '') + (validationReasons ? ' (' + validationReasons.join('; ') + ')' : '')); } if (!doneData) throw new Error('stream ended without done event'); clearInterval(pctInterval); if (percentEl) percentEl.textContent = '100%'; if (pb) pb.className = 'progress-bar done'; try { window.mc && window.mc.track('build_completed', { extend: isExtend, elapsed_ms: Date.now() - t0, provider: doneData.provider }); } catch(_){} // Final paint to guarantee the iframe has the full verified HTML, not a throttled partial. // Fetch the saved /built/{slug}/app.html so we're showing the exact persisted artifact. try { const verify = await fetch(('https://metercall-builder.fly.dev' + doneData.url), { cache: 'no-store' }); if (verify.ok) { const fullHtml = await verify.text(); if (iframe) iframe.srcdoc = fullHtml; } else if (accumulated) { // Fallback to whatever we streamed. const idx = accumulated.search(/]/i); if (iframe) iframe.srcdoc = idx >= 0 ? accumulated.slice(idx) : accumulated; } } catch(_) { if (accumulated && iframe) { const idx = accumulated.search(/]/i); iframe.srcdoc = idx >= 0 ? accumulated.slice(idx) : accumulated; } } const data = { html: accumulated, url: doneData.url, slug: doneData.slug }; if (data.url) state.currentModuleUrl = data.url; if (stepsEl && stepsEl.lastChild) stepsEl.lastChild.className = 'building-step visible done'; setTimeout(() => { if (overlay) overlay.className = 'building-overlay'; if (pb) pb.className = 'progress-bar'; if (iframeWrap) { iframeWrap.classList.remove('building-glow'); iframeWrap.classList.add('flash-green'); setTimeout(() => iframeWrap.classList.remove('flash-green'), 1100); } state.building = false; const badge = document.getElementById('livePreviewBadge'); if (badge) badge.style.display = 'block'; deductCredits(isExtend ? 8000 : 25000); addMeterMessage( isExtend ? `Done. "${text}" is in the preview — scroll to see the new section. Type what to add next.` : `Built. That's a real prototype running in the preview — not a template. Type what to add on top and I'll stack it in.`, 300 ); }, 350); } catch (err) { clearInterval(pctInterval); if (overlay) overlay.className = 'building-overlay'; if (pb) pb.className = 'progress-bar'; if (iframeWrap) iframeWrap.classList.remove('building-glow'); state.building = false; // Fast-fail detection: if we errored in <800ms it's almost certainly a // rate-limit, auth, or credit-balance failure — NOT a real build that died. // Surface the specific cause instead of the generic "try again" to let the // user know why. const elapsed = Date.now() - t0; const raw = (err && err.message) || 'network'; let friendly = raw; if (/429|rate limit/i.test(raw)) friendly = 'Too many builds this hour — wait ~5 min and retry. (Free tier: 60/hour.)'; else if (/credit balance|too low/i.test(raw)) friendly = 'Out of AI credits on our side — Pat is aware, topping up. Your prompt is saved.'; else if (/401|unauthorized/i.test(raw)) friendly = 'Auth hiccup — refresh the page and try again.'; else if (/500|502|503|504|upstream/i.test(raw)) friendly = 'Builder is momentarily down. Retrying in 30s usually works.'; else if (elapsed < 800) friendly = 'Builder rejected the request fast (network or endpoint issue). Not you — us.'; try { window.mc && window.mc.track('build_failed', { reason: String(raw).slice(0,120), elapsed_ms: elapsed }); } catch(_){} console.error('[chatBuildOrExtend]', raw, `(${elapsed}ms)`); addMeterMessage(friendly, 250); showToast('Builder: ' + friendly.slice(0, 60), 'error'); } } function respondAdded(featureName, icon) { addMeterMessage(`Added! ${icon} ${featureName} is now wired in. You can customize it in the preview, or keep adding features.`, 800); showToast(`${featureName} added`, 'success'); state.addedFeatures.add(featureName); } function respondConnected(serviceName) { addMeterMessage(`Connected! ${serviceName} is now syncing with your app. Your data will start flowing in 30 seconds.`, 800); showToast(`Connected to ${serviceName}`, 'success'); state.connectedServices.add(serviceName); } // ══════════════════════════════════════ // BUILD A MODULE // ══════════════════════════════════════ const MODULE_CONFIG = { restaurant: { label: 'Restaurant Suite', buildingMsg: "On it. Pulling in Toast, OpenTable, and Google Reviews. Your restaurant command center is coming up...", steps: ['Pulling in OpenTable...', 'Connecting Stripe payments...', 'Wiring up Toast POS...', 'Building reservation flow...'], src: 'https://metercall.com/modules/restaurant', doneMsg: "Boom — check this out. Full restaurant ops with table management, kitchen display, and reservations. What do you want to add?", qa: [ [ { id: 'online-order', label: '+ Online Ordering', msg: 'Add online ordering' }, { id: 'loyalty', label: '+ Loyalty Program', msg: 'Add loyalty program' }, { id: 'inventory-r', label: '+ Inventory', msg: 'Add inventory management' }, { id: 'staff-r', label: '+ Staff Scheduling', msg: 'Add staff scheduling' }, ] ] }, crm: { label: 'CRM', buildingMsg: "On it. Setting up your pipeline, pulling in contacts, and wiring up the deal tracker...", steps: ['Importing contacts...', 'Setting up pipeline stages...', 'Connecting email...', 'Building deal tracker...'], src: 'https://metercall.com/modules/crm', doneMsg: "Boom — your CRM is live. Contacts imported, pipeline staged, deals tracked. What do you want to bolt on?", qa: [ [ { id: 'email-seq', label: '+ Email Sequences', msg: 'Add email sequences' }, { id: 'lead-scoring', label: '+ Lead Scoring', msg: 'Add lead scoring' }, { id: 'tasks', label: '+ Task Manager', msg: 'Add task manager' }, { id: 'sms-crm', label: '+ SMS Follow-up', msg: 'Add SMS follow-up' }, ] ] }, dashboard: { label: 'Analytics Dashboard', buildingMsg: "On it. Connecting your data sources and spinning up the real-time charts...", steps: ['Connecting data sources...', 'Building KPI cards...', 'Wiring up charts...', 'Setting up real-time sync...'], src: 'https://metercall.com/modules/dashboard', doneMsg: "There it is. Everything's live and updating in real-time. Click around — it's all interactive. Want to add more widgets?", qa: [ [ { id: 'revenue-chart', label: '+ Revenue Chart', msg: 'Add revenue chart' }, { id: 'funnel', label: '+ Sales Funnel', msg: 'Add sales funnel' }, { id: 'cohort', label: '+ Cohort Analysis', msg: 'Add cohort analysis' }, { id: 'alerts', label: '+ Smart Alerts', msg: 'Add smart alerts' }, ] ] }, crypto: { label: 'Crypto Trader Dashboard', buildingMsg: "On it. Connecting exchange APIs and pulling in live prices. Your trading command center is almost ready...", steps: ['Connecting exchange APIs...', 'Pulling live prices...', 'Building portfolio tracker...', 'Setting up alerts...'], src: 'https://metercall.com/modules/crypto', doneMsg: "Boom. ₿ Portfolio, P&L, live prices — all wired up and ticking. What do you want to add?", qa: [ [ { id: 'price-alerts', label: '+ Price Alerts', msg: 'Add price alerts' }, { id: 'tax-report', label: '+ Tax Reports', msg: 'Add tax reporting' }, { id: 'dca', label: '+ DCA Bot', msg: 'Add DCA bot' }, { id: 'defi', label: '+ DeFi Tracker', msg: 'Add DeFi tracker' }, ] ] }, legal: { label: 'Law Firm Suite', buildingMsg: "On it. Setting up matter management, time tracking, and billing. Give me a second...", steps: ['Setting up matter management...', 'Building time tracker...', 'Connecting billing...', 'Loading document templates...'], src: 'https://metercall.com/modules/legal', doneMsg: "There it is. Matter management, time tracking, billing, and doc templates — all ready to go. What do you want to add?", qa: [ [ { id: 'intake', label: '+ Client Intake', msg: 'Add client intake form' }, { id: 'esign', label: '+ E-Signatures', msg: 'Add e-signatures' }, { id: 'time-entry', label: '+ Time Entry', msg: 'Add time entry' }, { id: 'conflict', label: '+ Conflict Check', msg: 'Add conflict check' }, ] ] }, property: { label: 'Property Management Suite', buildingMsg: "On it. Loading your properties, wiring up the tenant portal, and setting up rent collection...", steps: ['Loading property data...', 'Connecting tenant portal...', 'Wiring maintenance requests...', 'Setting up rent collection...'], src: 'https://metercall.com/modules/property', doneMsg: "Boom. Tenants, maintenance requests, and rent collection are live. Click around. What do you want to add?", qa: [ [ { id: 'lease', label: '+ Lease Manager', msg: 'Add lease manager' }, { id: 'vacancy', label: '+ Vacancy Tracker', msg: 'Add vacancy tracker' }, { id: 'maintenance', label: '+ Maintenance', msg: 'Add maintenance requests' }, { id: 'tenant-portal', label: '+ Tenant Portal', msg: 'Add tenant portal' }, ] ] }, }; function buildModule(type, originalMsg) { const config = MODULE_CONFIG[type]; if (!config) return; state.currentModule = type; state.building = true; // Switch to module tab activatePanelTab('module'); activateNavTab('module'); // Meter says it's building addMeterMessage(config.buildingMsg || `On it. Building your ${config.label} now...`, 400); // Start progress bar const pb = document.getElementById('progressBar'); pb.className = 'progress-bar running'; // Show building overlay const overlay = document.getElementById('buildingOverlay'); const label = document.getElementById('buildingLabel'); const stepsEl = document.getElementById('buildingSteps'); const percentEl = document.getElementById('buildingPercent'); overlay.className = 'building-overlay active'; label.textContent = `Building your ${config.label}...`; stepsEl.innerHTML = ''; percentEl.textContent = '0%'; // Add glow to iframe wrap while building const iframeWrap = document.getElementById('iframeWrap'); iframeWrap.classList.add('building-glow'); // Hide placeholder, hide iframe document.getElementById('previewPlaceholder').style.display = 'none'; const ghostPrompt = document.getElementById('ghostPromptText'); if (ghostPrompt) ghostPrompt.style.display = 'none'; const iframe = document.getElementById('previewIframe'); iframe.style.display = 'none'; const totalTime = config.steps.length * 350 + 400; // Percent counter — ticks from 0 to 99 over totalTime let pct = 0; const pctInterval = setInterval(() => { pct = Math.min(pct + 1, 99); percentEl.textContent = pct + '%'; if (pct >= 99) clearInterval(pctInterval); }, totalTime / 100); // Animate each step — overlap toasts for speed feel config.steps.forEach((stepText, i) => { const stepEl = document.createElement('div'); stepEl.className = 'building-step'; stepEl.innerHTML = `
${stepText}`; stepsEl.appendChild(stepEl); setTimeout(() => { // Mark prev as done if (i > 0) { const prev = stepsEl.children[i - 1]; prev.className = 'building-step visible done'; } stepEl.className = 'building-step visible active'; showToast(stepText, 'info'); }, i * 350 + 150); }); setTimeout(() => { clearInterval(pctInterval); percentEl.textContent = '100%'; // Mark last step done const lastStep = stepsEl.lastChild; if (lastStep) lastStep.className = 'building-step visible done'; // Finish progress bar pb.className = 'progress-bar done'; // Load the REAL module page var REAL_URLS = { restaurant: '/apps/restaurant-table-turns.html', crm: '/products/enterprise/crm-platform.html', crypto: '/products/enterprise/defi-platform.html', legal: '/apps/law-firm.html', dashboard: '/apps/analytics.html', property: '/apps/real-estate-agency.html', hvac: '/apps/hvac.html', booking: '/apps/medspa.html', ecommerce: '/apps/shopify-recovery.html', finance: '/products/enterprise/trading-platform.html', }; iframe.src = REAL_URLS[config.key || state.currentModule] || '/apps/analytics.html'; iframe.style.display = 'block'; setTimeout(() => { overlay.className = 'building-overlay'; pb.className = 'progress-bar'; state.building = false; // Remove glow, flash green for 1s iframeWrap.classList.remove('building-glow'); iframeWrap.classList.add('flash-green'); setTimeout(() => iframeWrap.classList.remove('flash-green'), 1100); // Show live preview badge const badge = document.getElementById('livePreviewBadge'); if (badge) badge.style.display = 'block'; // Deduct credits for building a module deductCredits(25000); // Meter done message addMeterMessage(config.doneMsg, 400); showToast(`${config.label} deployed to preview`, 'success'); // Update quick actions if (config.qa) { const extraQa = [...config.qa, DEFAULT_QA[2], DEFAULT_QA[3]]; renderQuickActions( extraQa.map((row, i) => ({ row: i, buttons: row })) ); } }, 600); }, totalTime); } function loadDemoPage(iframe, label) { const html = `

${escapeHtmlInline(label)}

● Live
Revenue (MTD)
$24,830
↑ 12% vs last month
Active Users
1,284
↑ 8% vs last month
Open Tickets
37
↑ 3 new today
Conversion Rate
4.2%
↑ 0.3% vs last week

Recent Activity

NameActionAmountStatus
Acme CorpInvoice #1041$3,200Paid
Beta LLCNew signupActive
Gamma IncInvoice #1040$1,800Pending
Delta CoSupport ticketOpen
Epsilon LtdRenewal$960Complete

Pipeline

${['Prospecting', 'Qualified', 'Proposal', 'Closed Won'].map((stage, i) => { const widths = [85, 62, 40, 28]; return `
${stage}${widths[i]}%
`; }).join('')}
`; iframe.srcdoc = html; } function escapeHtmlInline(s) { return s.replace(//g,'>'); } function loadDemo() { buildModule('dashboard', 'Build me a dashboard'); addUserMessage('Build me a dashboard'); } // ══════════════════════════════════════ // QUICK ACTIONS RENDERING // ══════════════════════════════════════ function renderQuickActions(rows) { const container = document.getElementById('qaRows'); container.innerHTML = ''; rows.forEach(rowDef => { const buttons = Array.isArray(rowDef) ? rowDef : rowDef.buttons; const row = document.createElement('div'); row.className = 'qa-row'; buttons.forEach(btn => { const el = document.createElement('button'); const isAdded = state.addedFeatures.has(btn.id) || state.connectedServices.has(btn.id); if (btn.deploy) { el.className = 'qa-btn deploy'; el.textContent = btn.label; el.onclick = openDeployModal; } else { el.className = `qa-btn ${isAdded ? 'added' : ''}`; el.textContent = isAdded ? `${btn.label.replace(/^\+\s*/, '')}` : btn.label; el.dataset.id = btn.id; el.dataset.msg = btn.msg; el.onclick = function() { handleQuickAction(this, btn); }; } row.appendChild(el); }); container.appendChild(row); }); } function handleQuickAction(el, btn) { if (el.classList.contains('added')) return; // Mark button as added el.classList.add('added'); const cleanLabel = btn.label.replace(/^\+\s*/, ''); el.textContent = `${cleanLabel}`; state.addedFeatures.add(btn.id); deductCredits(5000); // Send message to chat addUserMessage(btn.msg); processUserMessage(btn.msg); } // ══════════════════════════════════════ // PANEL TAB SWITCHING // ══════════════════════════════════════ function switchPanelTab(btn, tab) { // Update panel tab buttons document.querySelectorAll('.panel-tab').forEach(t => t.classList.remove('active')); btn.classList.add('active'); activatePanelTab(tab); } function activatePanelTab(tab) { state.currentPanelTab = tab; // Switch views document.querySelectorAll('.preview-view').forEach(v => v.classList.remove('active')); const view = document.getElementById(`view-${tab}`); if (view) view.classList.add('active'); // Sync panel tab buttons document.querySelectorAll('.panel-tab').forEach(t => { const isActive = t.getAttribute('onclick') && t.getAttribute('onclick').includes(`'${tab}'`); if (isActive) t.classList.add('active'); else t.classList.remove('active'); }); } function switchNavTab(btn, tab) { document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active')); btn.classList.add('active'); state.currentNavTab = tab; // Mirror to panel tab activatePanelTab(tab); } function activateNavTab(tab) { document.querySelectorAll('.nav-tab').forEach(t => { const isActive = t.getAttribute('onclick') && t.getAttribute('onclick').includes(`'${tab}'`); if (isActive) t.classList.add('active'); else t.classList.remove('active'); }); } // ══════════════════════════════════════ // CONNECTORS // ══════════════════════════════════════ function renderConnectors() { const grid = document.getElementById('connectorsGrid'); grid.innerHTML = ''; CONNECTORS.forEach(c => { const card = document.createElement('div'); const isConnected = state.connectedServices.has(c.id); card.className = `connector-card ${isConnected ? 'connected' : ''}`; card.innerHTML = `
${c.icon}
${c.name}
${isConnected ? 'Connected' : c.category}
`; card.onclick = () => connectService(card, c); grid.appendChild(card); }); } function connectService(card, connector) { if (state.connectedServices.has(connector.id)) return; state.connectedServices.add(connector.id); card.className = 'connector-card connected'; card.querySelector('.connector-status').textContent = 'Connected'; addUserMessage(`Connect ${connector.name}`); respondConnected(connector.name); showToast(`${connector.name} connected`, 'success'); } // ══════════════════════════════════════ // AGENTS // ══════════════════════════════════════ function renderAgents() { const container = document.getElementById('agentCards'); container.innerHTML = ''; AGENTS.forEach(agent => { const card = document.createElement('div'); const isHired = state.hiredAgents.has(agent.id); card.className = `agent-card ${isHired ? 'hired' : ''}`; card.innerHTML = `
${agent.icon}
${agent.name}
${agent.desc}
`; const hireBtn = card.querySelector('.agent-hire-btn'); hireBtn.onclick = () => hireAgent(card, agent, hireBtn); container.appendChild(card); }); } function hireAgent(card, agent, btn) { if (state.hiredAgents.has(agent.id)) return; state.hiredAgents.add(agent.id); card.className = 'agent-card hired'; btn.textContent = 'Hired'; addMeterMessage(`${agent.icon} ${agent.name} hired and active. They'll start working immediately — no setup required.`, 700); showToast(`${agent.name} is now working for you`, 'success'); } // ══════════════════════════════════════ // TOAST NOTIFICATIONS // ══════════════════════════════════════ function showToast(msg, type = 'info') { const container = document.getElementById('toastContainer'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = msg; container.appendChild(toast); requestAnimationFrame(() => { requestAnimationFrame(() => { toast.classList.add('show'); }); }); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 400); }, 3000); } // ══════════════════════════════════════ // DEPLOY MODAL // ══════════════════════════════════════ function openDeployModal() { const modal = document.getElementById('deployModal'); modal.classList.add('open'); // Pre-fill app name based on current module const nameInput = document.getElementById('deployName'); const subInput = document.getElementById('deploySubdomain'); const config = state.currentModule ? MODULE_CONFIG[state.currentModule] : null; if (config && !nameInput.value) { nameInput.value = config.label; subInput.value = config.label.toLowerCase().replace(/\s+/g, '-'); } } function closeDeployModal() { document.getElementById('deployModal').classList.remove('open'); } function closeDeployOnBackdrop(e) { if (e.target === document.getElementById('deployModal')) closeDeployModal(); } let _liveUrl = ''; function confirmDeploy() { const name = document.getElementById('deployName').value.trim() || 'My App'; const sub = document.getElementById('deploySubdomain').value.trim() || 'my-app'; closeDeployModal(); addMeterMessage(`Going live with ${escapeHtml(name)}...`, 200); setTimeout(() => { _liveUrl = `https://${escapeHtml(sub)}.metercall.app`; addMeterMessage(`${escapeHtml(name)} is live! Share it with anyone — changes sync in real-time.`, 1500); // Show the big Go Live success bar const bar = document.getElementById('goliveBar'); const urlLink = document.getElementById('goliveUrlLink'); urlLink.href = _liveUrl; urlLink.textContent = _liveUrl; bar.classList.add('show'); // Fire confetti fireConfetti(); }, 1800); } function fireConfetti() { const colors = ['#f97316','#ec4899','#8b5cf6','#10b981','#fbbf24','#3b82f6']; for (let i = 0; i < 60; i++) { setTimeout(() => { const dot = document.createElement('div'); dot.className = 'confetti-dot'; dot.style.cssText = ` left: ${Math.random() * 100}vw; top: -10px; background: ${colors[Math.floor(Math.random() * colors.length)]}; width: ${6 + Math.random() * 6}px; height: ${6 + Math.random() * 6}px; border-radius: ${Math.random() > 0.5 ? '50%' : '2px'}; animation-duration: ${1.5 + Math.random() * 2}s; animation-delay: 0s; `; document.body.appendChild(dot); setTimeout(() => dot.remove(), 3500); }, i * 30); } } function copyLiveUrl() { if (!_liveUrl) return; navigator.clipboard.writeText(_liveUrl).catch(() => {}); const btn = document.getElementById('goliveCopyBtn'); btn.textContent = 'Copied!'; btn.classList.add('copied'); setTimeout(() => { btn.textContent = 'Copy link'; btn.classList.remove('copied'); }, 2000); } function dismissGoLive() { document.getElementById('goliveBar').classList.remove('show'); } function openPreviewExternal() { showToast('Opening preview in new tab...', 'info'); setTimeout(() => { window.open('about:blank', '_blank'); }, 300); } // ══════════════════════════════════════ // INIT // ══════════════════════════════════════ // Render quick actions on load renderQuickActions(DEFAULT_QA);