PageSourceSearch

https://app.replicants.ai/js/builder.js?v=20261001w8c5

js replicants.ai collected 2026-10-05 12:31:46 UTC 26,531 bytes, 477 lines download raw bytes

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\'">&#8942;</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;">&#8617; 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;">&#10003; 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.