1/** 2 * builder.js v3 â Website list, card grid, settings, AI studio 3 * Launch checklist extracted to launch-checklist.js (2026-03-29) 4 * Site plan UI extracted to site-plan-ui.js (2026-03-29) 5 */ 6 7let builderCurrentTab = 'website'; 8let builderCurrentItem = null; 9let builderItems = []; 10let builderPages = []; 11 12function esc(s) { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } 13 14// ââ List View ââ 15 16async function loadBuilderList() { 17 try { 18 builderItems = await api('/builder/funnels-websites?type=' + builderCurrentTab); 19 renderBuilderGrid(); 20 } catch(e) { console.error('Builder load error:', e); } 21} 22 23function renderBuilderGrid() { 24 const grid = document.getElementById('builderGrid'); 25 const empty = document.getElementById('builderEmpty'); 26 if (!builderItems.length) { grid.innerHTML = ''; empty.style.display = ''; return; } 27 empty.style.display = 'none'; 28 29 // Fetch launch progress for all websites in parallel 30 var launchCache = {}; 31 var fetches = builderItems.map(function(item) { 32 return api('/launch-process/' + item.id).then(function(data) { 33 launchCache[item.id] = data; 34 }).catch(function() { launchCache[item.id] = null; }); 35 }); 36 37 Promise.all(fetches).then(function() { 38 grid.innerHTML = builderItems.map(function(item) { 39 return _renderWebsiteCard(item, launchCache[item.id]); 40 }).join(''); 41 }); 42} 43 44function _renderWebsiteCard(item, launch) { 45 var date = ((window.App && window.App.Dates) ? window.App.Dates.short(item.created_at) : new Date(item.created_at).toDateString()); 46 var statusBadge = (item.status === 'published' || item.status === 'live') 47 ? '<span style="color:#059669;font-size:11px;">â Live</span>' 48 : '<span style="color:#eab308;font-size:11px;">â Draft</span>'; 49 50 // Thumbnail 51 var thumbSrc = item.home_thumbnail || ''; 52 if (!thumbSrc && item.slug) { 53 var tplSlug = item.slug.replace(/-\d{13,}$/, ''); 54 if (tplSlug) thumbSrc = '/templates/thumbs/' + tplSlug + '.jpg'; 55 } 56 var thumbHtml = thumbSrc 57 ? '<div style="height:140px;overflow:hidden;border-radius:var(--rk-radius-lg) var(--rk-radius-lg) 0 0;background:var(--rk-silicon);">' + 58 '<img src="' + esc(thumbSrc) + '" style="width:100%;height:100%;object-fit:cover;object-position:top;" alt="" onerror="this.parentElement.style.display=\'none\'">' + 59 '</div>' 60 : '<div style="height:100px;overflow:hidden;border-radius:var(--rk-radius-lg) var(--rk-radius-lg) 0 0;background:var(--rk-silicon);display:flex;align-items:center;justify-content:center;color:var(--rk-slate-light);font-size:28px;"><i class="fas fa-globe"></i></div>'; 61 62 // Launch progress 63 var launchHtml = ''; 64 if (launch) { 65 var stepOrder = launch.stepOrder || ['discover', 'site_plan', 'build', 'personalise', 'pre_publish', 'seo', 'go_live']; 66 var steps = launch.steps || {}; 67 var completed = 0; 68 var total = stepOrder.length; // 7 main steps 69 var currentLabel = ''; 70 var stepLabels = { 71 discover: 'About You', site_plan: 'Site Plan', build: 'Write Copy', 72 personalise: 'Personalise', placeholder_audit: 'Personalise', 73 pre_publish: 'Pre-Publish Check', seo: 'SEO', go_live: 'Go Live' 74 }; 75 stepOrder.forEach(function(key) { 76 if (steps[key] && (steps[key].status === 'complete' || steps[key].status === 'approved')) completed++; 77 else if (!currentLabel) { 78 currentLabel = stepLabels[key] || key; 79 } 80 }); 81 if (!currentLabel) currentLabel = completed >= total ? 'Complete' : 'Get Started'; 82 83 var pct = Math.round((completed / total) * 100); 84 var progressColor = completed === 0 ? 'var(--rk-blue)' : completed >= stepOrder.length ? 'var(--rk-success)' : 'var(--rk-blue)'; 85 var btnLabel = completed === 0 ? 'ð Start Launch Checklist' : completed >= stepOrder.length ? 'â Launch Checklist Complete' : 'ð ' + currentLabel + ' · ' + completed + '/' + total; 86 87 launchHtml = '<button onclick="event.stopPropagation();builderOpenItem(' + item.id + ', \'ai-studio\')" ' + 88 'style="width:100%;padding:10px 14px;background:linear-gradient(135deg,var(--rk-navy),#312e81);color:#fff;border:none;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;font-family:Inter,sans-serif;text-align:left;transition:opacity 0.15s;position:relative;overflow:hidden;" ' + 89 'onmouseover="this.style.opacity=\'0.9\'" onmouseout="this.style.opacity=\'1\'">' + 90 '<div style="display:flex;justify-content:space-between;align-items:center;">' + 91 '<span>' + btnLabel + '</span>' + 92 '<span style="font-size:11px;opacity:0.7;">' + pct + '%</span>' + 93 '</div>' + 94 '<div style="margin-top:8px;height:4px;background:rgba(255,255,255,0.15);border-radius:2px;overflow:hidden;">' + 95 '<div style="height:100%;width:' + pct + '%;background:' + progressColor + ';border-radius:2px;transition:width 0.3s;"></div>' + 96 '</div>' + 97 '</button>'; 98 } else { 99 // No launch process yet 100 launchHtml = '<button onclick="event.stopPropagation();builderOpenItem(' + item.id + ', \'ai-studio\')" ' + 101 'style="width:100%;padding:10px 14px;background:linear-gradient(135deg,var(--rk-navy),#312e81);color:#fff;border:none;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;font-family:Inter,sans-serif;transition:opacity 0.15s;" ' + 102 'onmouseover="this.style.opacity=\'0.9\'" onmouseout="this.style.opacity=\'1\'">' + 103 'ð Start Launch Checklist' + 104 '</button>'; 105 } 106 107 // Single primary action. Not published yet â "Launch Studio" (into the 108 // Launch Studio for this site). Published â "Pages" (edit pages directly); 109 // the Launch Studio is one-and-done, so it's not offered again post-launch. 110 var published = (item.status === 'published' || item.status === 'live'); 111 var _btnStyle = 'style="width:100%;padding:11px 14px;background:linear-gradient(135deg,var(--rk-navy),#312e81);color:#fff;border:none;border-radius:10px;font-size:13px;font-weight:600;cursor:pointer;font-family:Inter,sans-serif;transition:opacity 0.15s;" onmouseover="this.style.opacity=\'0.9\'" onmouseout="this.style.opacity=\'1\'"'; 112 var actionsHtml = published 113 ? '<button onclick="event.stopPropagation();builderOpenItem(' + item.id + ')" ' + _btnStyle + '><i class="fas fa-file-alt" style="margin-right:6px;"></i>Pages</button>' 114 : '<button onclick="event.stopPropagation();(window.enterLaunchStudio ? window.enterLaunchStudio(' + item.id + ')
114: builderOpenAIStudio(' + item.id + '))" ' + _btnStyle + '><i class="fas fa-rocket" style="margin-right:6px;"></i>Launch Studio</button>'; 115 116 return '<div class="builder-card">' + 117 thumbHtml + 118 '<div style="padding:var(--rk-space-4);">' + 119 '<div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:12px;">' + 120 '<div>' + 121 '<div style="font-weight:500;font-size:14px;color:var(--rk-navy);">' + esc(item.name) + '</div>' + 122 '<div style="font-size:11px;color:var(--rk-slate-light);margin-top:3px;">' + 123 (item.page_count || 0) + ' pages · ' + statusBadge + ' · ' + date + 124 '</div>' + 125 '</div>' + 126 '<span onclick="event.stopPropagation();builderCardMenu(event,' + item.id + ')" style="cursor:pointer;font-size:18px;color:#999;padding:2px 6px;border-radius:4px;line-height:1;" onmouseover="this.style.background=\'#f0f0f0\'" onmouseout="this.style.background=\'transparent\'">⋮</span>' + 127 '</div>' + 128 // Single primary action (Launch Studio / Pages) 129 actionsHtml + 130 '</div></div>'; 131} 132 133// ââ Card Menu (three dots) ââ 134 135function builderCardMenu(e, id) { 136 e.stopPropagation(); 137 var item = builderItems.find(function(i) { return i.id === id; }); 138 if (!item) return; 139 showContextMenu(e, [ 140 { label: 'Rename', action: function() { 141 openInputModal('Rename', 'New name:', function(newName) { 142 api('/builder/funnels-websites/' + id, { method: 'PUT', body: { name: newName } }) 143 .then(function() { showToast('Renamed'); loadBuilderList(); }); 144 }, item.name); 145 }}, 146 { label: 'Settings', action: function() { 147 builderOpenItem(id, 'settings'); 148 }}, 149 { label: 'Clone', action: function() { 150 api('/builder/funnels-websites/' + id + '/clone', { method: 'POST' }) 151 .then(function() { showToast('Website cloned'); loadBuilderList(); }) 152 .catch(function(err) { showToast('Error: ' + err.message, 'error'); }); 153 }}, 154 { label: 'Archive', action: function() { 155 showDeleteConfirm('Archive "' + item.name + '"?', 'You can restore it later from the archive.', function() { 156 api('/builder/funnels-websites/' + id, { method: 'PUT', body: { status: 'archived' } }) 157 .then(function() { showToast('Archived'); loadBuilderList(); }); 158 }); 159 }} 160 ]); 161} 162 163// ââ Open Website ââ 164// 165// â ONE PAGE LIST (website round 8, L-b; Jev: go ahead). A WEBSITE's pages, 166// with their pictures and a refresh button, are on its own screen (Websites, 167// then Manage: launch-studio.js manageHtml). This used to open a second page 168// grid here, reachable only from the dashboard on a one-site account or 169// ?open_website=, so a coach with two sites never saw the pictures. Every way 170// in now lands on the site's own screen (window.openWebsiteScreen, the 171// Websites list's own rule). `tab` opens this view's other tabs (Sett
171ings, 172// the launch checklist), which are not the page list and stay. A funnel keeps 173// its pages here. 174 175async function builderOpenItem(id, tab) { 176 builderCurrentItem = builderItems.find(function(i) { return i.id === id; }); 177 if (!builderCurrentItem) { 178 // May have been called before list loaded (e.g. from dashboard) 179 try { 180 var items = await api('/builder/funnels-websites?type=website'); 181 builderItems = items || []; 182 builderCurrentItem = builderItems.find(function(i) { return i.id === id; }); 183 } catch(e) {} 184 if (!builderCurrentItem) return; 185 } 186 if ((!tab || tab === 'pages') && builderOpensSiteScreen(builderCurrentItem)) return; 187 document.getElementById('builderListView').style.display = 'none'; 188 document.getElementById('builderDetailView').style.display = ''; 189 var _lc = document.getElementById('builderListCrumbs'); if (_lc) _lc.style.display = 'none'; 190 document.getElementById('builderDetailTabs').style.display = 'flex'; 191 document.getElementById('builderBreadcrumbs').style.display = ''; 192 document.getElementById('builderBreadcrumbCurrent').textContent = builderCurrentItem.name; 193 document.getElementById('builderTitle').textContent = builderCurrentItem.name; 194 document.getElementById('builderSubtitle').style.display = 'none'; 195 document.getElementById('builderNewBtn').onclick = builderCreatePage; 196 document.getElementById('builderNewBtnText').textContent = 'Add New Page'; 197 builderSwitchDetailTab(tab || 'pages'); 198 if (!tab || tab === 'pages') await builderLoadPages(); 199} 200 201/** A website's pages are on its own screen; true when it was opened there. */ 202function builderOpensSiteScreen(item) { 203 if (!item || item.type === 'funnel' || typeof window.openWebsiteScreen !== 'function') return false; 204 window.openWebsiteScreen(item.id, item.status === 'published' || item.status === 'live'); 205 return true; 206} 207 208function builderGoBack() { 209 builderCurrentItem = null; 210 document.getElementById('builderListView').style.display = ''; 211 document.getElementById('builderDetailView').style.display = 'none'; 212 var _lc = document.getElementById('builderListCrumbs'); if (_lc) _lc.style.display = ''; 213 document.getElementById('builderDetailTabs').style.display = 'none'; 214 document.getElementById('builderBreadcrumbs').style.display = 'none'; 215 document.getElementById('builderTitle').textContent = 'Manage'; 216 document.getElementById('builderSubtitle').textContent = 'Build and manage your websites'; 217 document.getElementById('builderSubtitle').style.display = ''; 218 document.getElementById('builderNewBtn').onclick = function () { if (typeof window.showLaunchGallery === 'function') window.showLaunchGallery(); else builderCreateNew(); }; 219 document.getElementById('builderNewBtnText').textContent = 'New website'; 220 loadBuilderList(); 221} 222 223function builderCreateNew() { 224 openInputModal('New Website', 'Enter name:', function(name) { 225 api('/builder/funnels-websites', { method: 'POST', body: { type: 'website', name: name } }) 226 .then(function() { showToast('Website created'); loadBuilderList(); }) 227 .catch(function(e) { showToast('Error: ' + e.message, 'error'); }); 228 }); 229} 230 231// ââ AI Studio (opens from website card) ââ 232 233function builderOpenAIStudio(websiteId) { 234 builderOpenItem(websiteId, 'ai-studio'); 235} 236 237// ââ Detail Tabs ââ 238 239function builderSwitchTab(tab) { 240 builderCurrentTab = tab; 241 document.querySelectorAll('#builderTabs .builder-tab').forEach(function(t) { t.classList.toggle('active', t.dataset.tab === tab); }); 242 loadBuilderList(); 243} 244 245function builderSwitchDetailTab(tab) { 246 if (tab === 'preview') { builderPreviewSite(); return; } 247 // The Pages tab of a WEBSITE is its own screen now (L-b): one page list. 248 if (tab === 'pages' && builderOpensSiteScreen(builderCurrentItem)) return; 249 document.querySelectorAll('[data-dtab]').forEach(function(b) { b.classList.toggle('active', b.dataset.dtab === tab); }); 250 document.getElementById('builderPagesPanel').style.display = tab === 'pages' ? '' : 'none'; 251 document.getElementById('builderSettingsPanel').style.display = tab === 'settings' ? '' : 'none'; 252 var aiPanel = document.getElementById('builderAIStudioPanel'); 253 if (aiPanel) aiPanel.style.display = tab === 'ai-studio' ? '' : 'none'; 254 if (tab === 'settings' && builderCurrentItem) { 255 document.getElementById('builderSettName').value = builderCurrentItem.name || ''; 256 document.getElementById('builderSettSlug').value = builderCurrentItem.slug || ''; 257 document.getElementById('builderSettDesc').value = builderCurrentItem.description || ''; 258 document.getElementById('builderSettStatus').value = builderCurrentItem.status || 'draft'; 259 // Populate domain dropdown 260 var domSel = document.getElementById('builderSettDomain'); 261 domSel.innerHTML = '<option value="">No custom domain</option>'; 262 if (builderCurrentItem.domain) { 263 domSel.innerHTML += '<option value="' + esc(builderCurrentItem.domain) + '" selected>' + esc(builderCurrentItem.domain) + '</option>'; 264 } 265 // Extended settings 266 var settings = builderCurrentItem.settings || {}; 267 var favEl = document.getElementById('builderSettFavicon'); 268 if (favEl) favEl.value = settings.favicon_url || ''; 269 var headEl = document.getElementById('builderSettHeadCode'); 270 if (headEl) headEl.value = settings.head_tracking || ''; 271 var bodyEl = document.getElementById('builderSettBodyCode'); 272 if (bodyEl) bodyEl.value = settings.body_tracking || ''; 273 var imgOpt = document.getElementById('builderSettImgOpt'); 274 if (imgOpt) { imgOpt.checked = settings.image_optimization !== false; updateToggleLabel(imgOpt); } 275 var jsOpt = document.getElementById('builderSettJsOpt'); 276 if (jsOpt) { jsOpt.checked = settings.js_optimization !== false; updateToggleLabel(jsOpt); } 277 var gdpr = document.getElementById('builderSettGdprFonts'); 278 if (gdpr) { gdpr.checked = !!settings.gdpr_fonts; updateToggleLabel(gdpr); } 279 } 280 if (tab === 'ai-studio') { loadLaunchChecklist(); } 281} 282 283function builderPreviewSite() { 284 if (!builderCurrentItem) return; 285 var slug = builderCurrentItem.slug || builderCurrentItem.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'); 286 var base = window.__PAGES_BASE_URL || '';
287 window.open(base ? base.replace('*', slug) : '/p/' + slug, '_blank'); 288} 289 290function updateToggleLabel(checkbox) { 291 var label = checkbox.closest('.ws-toggle-row'); 292 if (label) { 293 var span = label.querySelector('.ws-toggle-label'); 294 if (span) span.textContent = checkbox.checked ? 'Enabled' : 'Disabled'; 295 } 296} 297// Wire toggle labels 298document.addEventListener('change', function(e) { 299 if (e.target.id && e.target.id.startsWith('builderSett') && e.target.type === 'checkbox') { 300 updateToggleLabel(e.target); 301 } 302}); 303 304async function confirmDeleteWebsite() { 305 if (!builderCurrentItem) return; 306 var name = builderCurrentItem.name || 'this website'; 307 if (!(await App.dialog.confirm({ title: 'Delete ' + name + '?', body: 'The website and all its pages will be deleted. This cannot be undone.', confirmLabel: 'Delete website', cancelLabel: 'Keep it', tone: 'danger' }))) return; 308 api('/builder/funnels-websites/' + builderCurrentItem.id, { method: 'DELETE' }).then(function() { 309 showToast('Website deleted', 'success'); 310 builderCurrentItem = null; 311 loadBuilderItems(); 312 }).catch(function(e) { showToast('Error: ' + e.message, 'error'); }); 313} 314 315async function builderSaveSettings() { 316 if (!builderCurrentItem) return; 317 try { 318 var data = { 319 name: document.getElementById('builderSettName').value, 320 slug: document.getElementById('builderSettSlug').value, 321 // Domain Hosting (Phase A): domain binding is managed via the funnel Settings 322 // DOMAIN picker + Domain Settings routing panel, not this legacy field â 323 // builder-routes ignores `domain` now, so we no longer send it. 324 description: document.getElementById('builderSettDesc').value, 325 status: document.getElementById('builderSettStatus').value, 326 settings: { 327 favicon_url: document.getElementById('builderSettFavicon').value || '', 328 head_tracking: document.getElementById('builderSettHeadCode').value || '', 329 body_tracking: document.getElementById('builderSettBodyCode').value || '', 330 image_optimization: document.getElementById('builderSettImgOpt').checked, 331 js_optimization: document.getElementById('builderSettJsOpt').checked, 332 gdpr_fonts: document.getElementById('builderSettGdprFonts').checked, 333 } 334 }; 335 var updated = await api('/builder/funnels-websites/' + builderCurrentItem.id, { method: 'PUT', body: JSON.stringify(data) }); 336 builderCurrentItem = Object.assign(builderCurrentItem, updated); 337 document.getElementById('builderTitle').textContent = updated.name; 338 showToast('Settings saved'); 339 } catch(e) { showToast('Error saving: ' + e.message, 'error'); } 340} 341 342// AI Studio page picker + run 343function populateAIStudioPagePicker() { 344 var picker = document.getElementById('aiStudioPagePicker'); 345 if (!picker || !builderPages.length) return; 346 picker.innerHTML = builderPages.map(function(pg) { 347 return '<label style="display:flex;align-items:center;gap:6px;font-size:13px;padding:4px 0;cursor:pointer;"><input type="checkbox" class="ai-studio-page-cb" value="' + pg.id + '" checked> ' + esc(pg.name) + '</label>'; 348 }).join(''); 349} 350 351document.addEventListener('change', function(e) { 352 if (e.target.name === 'aiStudioScope') { 353 var picker = document.getElementById('aiStudioPagePicker'); 354 if (picker) picker.style.display = e.target.value === 'select' ? '' : 'none'; 355 } 356}); 357 358function runAIStudio() { 359 var prompt = document.getElementById('aiStudioPrompt').value.trim(); 360 if (!prompt) { showToast('Enter an instruction first', 'error'); return; } 361 var scope = document.querySelector('input[name="aiStudioScope"]:checked').value; 362 var pageIds = []; 363 if (scope === 'all') { 364 pageIds = builderPages.map(function(p) { return p.id; }); 365 } else { 366 document.querySelectorAll('.ai-studio-page-cb:checked').forEach(function(cb) { pageIds.push(parseInt(cb.value)); }); 367 } 368 if (!pageIds.length) { showToast('Select at least one page', 'error'); return; } 369 var progress = document.getElementById('aiStudioProgress'); 370 progress.style.display = ''; 371 progress.innerHTML = '<div style="text-align:center;padding:20px;"><div style="display:inline-block;width:24px;height:24px;border:3px solid #e0e0e0;border-top-color:#6366f1;border-radius:50%;animation:spin 0.8s linear infinite;"></div><div style="margin-top:10px;font-size:13px;color:#666;">Processing ' + pageIds.length + ' page' + (pageIds.length > 1 ? 's' : '') + '...</div></div>'; 372 api('/builder/ai-studio/run', { method: 'POST', body: { websiteId: builderCurrentItem.id, prompt: prompt, pageIds: pageIds } }) 373 .then(function(result) { 374 if (window.App && window.App.TrialLock && window.App.TrialLock.offer(result, 375 function () { runAIStudio(); }, 376 'Maya rewrites your pages on your planâs allowance.')) { progress.style.display = 'none'; return; } 377 if (result.runId) { 378 progress.innerHTML = '<div style="text-align:center;padding:20px;"><div style="display:inline-block;width:24px;height:24px;border:3px solid #e0e0e0;border-top-color:#6366f1;border-radius:50%;animation:spin 0.8s linear infinite;"></div><div style="margin-top:10px;font-size:13px;color:#666;">Processing ' + result.pagesQueued + ' pages (' + result.task + ')...</div></div>'; 379 pollStudioRun(result.runId); 380 } else { 381 progress.innerHTML = '<div style="text-align:center;padding:20px;color:#059669;"><i class="fas fa-check-circle" style="font-size:24px;"></i><div style="margin-top:8px;font-size:14px;font-weight:500;">Done!</div></div>'; 382 setTimeout(function() { progress.style.display = 'none'; }, 5000); 383 builderLoadPages(); 384 } 385 }) 386 .catch(function(e) { 387 progress.innerHTML = '<div style="text-align:center;padding:20px;color:#dc2626;"><i class="fas fa-exclamation-circle" style="font-size:24px;"></i><div style="margin-top:8px;font-size:14px;">' + e.message + '</div></div>'; 388 }); 389} 390 391function pollStudioRun(runId) { 392 var progress = document.getElementById('aiStudioProgress'); 393 var interval = setInterval(function() { 394 api('/builder/ai-studio/status/' + runId).then(function(run) { 395 var p = run.progress || {}; 396 if (run.status === 'completed') { 397 clearInterval(interval); 398 var r = run.results || {}; 399 var details = ''; 400 if (r.pagesUpdated) details += r.pagesUpdated + ' pages updated. '; 401 if (r.sectionsEdited) details += r.sectionsEdited + ' sections edited. '; 402 if (r.errors && r.errors.length) details += r.errors.length + ' errors. '; 403 progress.innerHTML = '<div style="text-align:center;padding:20px;">' + 404 '<div style="color:#059669;"><i class="fas fa-check-circle" style="font-size:24px;"></i></div>' + 405 '<div style="margin-top:8px;font-size:14px;font-weight:500;">
405' + (details || 'Done!') + '</div>' + 406 '<div style="margin-top:12px;display:flex;gap:8px;justify-content:center;">' + 407 '<button onclick="undoStudioRun(' + runId + ')" style="padding:6px 16px;border:1px solid #d1d5db;border-radius:6px;background:#fff;cursor:pointer;font-size:13px;">↩ Undo All</button>' + 408 '<button onclick="document.getElementById(\'aiStudioProgress\').style.display=\'none\'" style="padding:6px 16px;border:none;border-radius:6px;background:#6366f1;color:#fff;cursor:pointer;font-size:13px;">✓ Keep Changes</button>' + 409 '</div></div>'; 410 builderLoadPages(); 411 } else if (run.status === 'failed') { 412 clearInterval(interval); 413 var errMsg = (run.results && run.results.error) ? run.results.error : 'Unknown error'; 414 progress.innerHTML = '<div style="text-align:center;padding:20px;color:#dc2626;"><i class="fas fa-exclamation-circle" style="font-size:24px;"></i><div style="margin-top:8px;font-size:14px;">Failed: ' + errMsg + '</div></div>'; 415 } else { 416 var currentPage = p.currentPage || '...'; 417 var processed = p.pagesProcessed || 0; 418 var total = p.pagesTotal || '?'; 419 progress.innerHTML = '<div style="text-align:center;padding:20px;">' + 420 '<div style="display:inline-block;width:24px;height:24px;border:3px solid #e0e0e0;border-top-color:#6366f1;border-radius:50%;animation:spin 0.8s linear infinite;"></div>' + 421 '<div style="margin-top:10px;font-size:13px;color:#666;">Processing: ' + currentPage + ' (' + processed + '/' + total + ')</div></div>'; 422 } 423 }).catch(function(e) { 424 console.log('[ai-studio] Poll error:', e.message); 425 }); 426 }, 2000); 427} 428 429function undoStudioRun(runId) { 430 var progress = document.getElementById('aiStudioProgress'); 431 progress.innerHTML = '<div style="text-align:center;padding:20px;"><div style="display:inline-block;width:24px;height:24px;border:3px solid #e0e0e0;border-top-color:#6366f1;border-radius:50%;animation:spin 0.8s linear infinite;"></div><div style="margin-top:10px;font-size:13px;color:#666;">Reverting changes...</div></div>'; 432 api('/builder/ai-studio/undo/' + runId, { method: 'POST' }) 433 .then(function(result) { 434 var reverted = result.pagesReverted || 0; 435 progress.innerHTML = '<div style="text-align:center;padding:20px;color:#059669;"><i class="fas fa-undo" style="font-size:24px;"></i><div style="margin-top:8px;font-size:14px;font-weight:500;">Reverted ' + reverted + ' pages</div></div>'; 436 setTimeout(function() { progress.style.display = 'none'; }, 3000); 437 builderLoadPages(); 438 }) 439 .catch(function(e) { 440 progress.innerHTML = '<div style="text-align:center;padding:20px;color:#dc2626;">Undo failed: ' + e.message + '</div>'; 441 }); 442} 443 444 445 446 447// W3 â sub-nav entries that drill into a single website when there 448// is only one, or stay on the Websites list otherwise. Bound to the 449// Website module's submenu via `clickAction` in ui-core.js. 450window.openWebsiteLaunchChecklist = async function () { 451 if (typeof openModule === 'function') openModule('funnels'); 452 try { 453 var ws = await api('/builder/funnels-websites?type=website'); 454 if (Array.isArray(ws) && ws.length === 1) { 455 await builderOpenItem(ws[0].id, 'ai-studio'); 456 } 457 } catch (e) { /* stay on the list */ } 458}; 459 460window.openWebsiteSettings = async function () { 461 if (typeof openModule === 'function') openModule('funnels'); 462 try { 463 var ws = await api('/builder/funnels-websites?type=website'); 464 if (Array.isArray(ws) && ws.length === 1) { 465 await builderOpenItem(ws[0].id, 'settings'); 466 } 467 } catch (e) { /* stay on the list */ } 468}; 469 470// Factory user title override 471if (typeof isFactoryUser === 'function' && isFactoryUser()) { 472 var _t = document.getElementById('builderMainTitle'); if (_t) _t.textContent = 'My Websites'; 473 var _s = document.getElementById('builderMainSubtitle'); if (_s) _s.textContent = 'Your AI-built websites'; 474} 475// Hide Funnels tab for non-admin 476(function() { var ft = document.getElementById('funnelsTab'); if (ft && !(currentUser && currentUser.is_platform_admin)) { ft.style.opacity = '0.5'; ft.style.pointerEvents = 'none'; } })(); 477
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.