1/* Uno Studio prompt + suggestion pills (home page). */ 2(function () { 3 'use strict'; 4 5 var STORAGE_KEY = 'uno_studio_prompt'; 6 var MAX_LENGTH = (typeof unoStudio !== 'undefined' && unoStudio.promptMaxLength) ? unoStudio.promptMaxLength : 2000; 7 8 var form = document.getElementById('unoStudioForm'); 9 var input = document.getElementById('unoStudioInput'); 10 var overlay = document.getElementById('unoStudioOverlay'); 11 var backdrop = document.getElementById('unoStudioBackdrop'); 12 var closeBtn = document.getElementById('unoStudioClose'); 13 14 function buildCard(sample) { 15 var sampleId = sample.id || ''; 16 var sampleName = sample.displayName || ''; 17 if (!sampleId || !sampleName) return null; 18 19 var btn = document.createElement('button'); 20 btn.type = 'button'; 21 btn.className = 'uno-studio-prompt__card'; 22 btn.dataset.project = sampleId; 23 24 if (sample.iconUrl) { 25 var parsedIcon; 26 try { parsedIcon = new URL(sample.iconUrl); } catch (e) { parsedIcon = null; } 27 if (parsedIcon && parsedIcon.protocol === 'https:') { 28 var img = document.createElement('img'); 29 img.className = 'uno-studio-prompt__card-img'; 30 img.src = sample.iconUrl; 31 img.alt = sampleName; 32 img.loading = 'lazy'; 33 btn.appendChild(img); 34 } 35 } 36 37 var textDiv = document.createElement('div'); 38 textDiv.className = 'uno-studio-prompt__card-text'; 39 40 var strong = document.createElement('strong'); 41 strong.textContent = sampleName; 42 textDiv.appendChild(strong); 43 44 var span = document.createElement('span'); 45 span.textContent = sample.description || ''; 46 textDiv.appendChild(span); 47 48 btn.appendChild(textDiv); 49 50 btn.addEventListener('click', function () { 51 var project = btn.dataset.project; 52 if (!project) return; 53 54 // GA4: prebuilt-app selection. Label carries the dynamic project id so each 55 // featured app is measured separately even though the set comes from the API. 56 if (typeof gtag !== 'undefined') { 57 gtag('event', 'cta_click', { 58 event_category: 'homepage', 59 event_label: 'sample_' + project 60 }); 61 } 62 63 redirectToStudioProject(project); 64 }); 65 66 return btn; 67 } 68 69 function initPills() { 70 var container = document.querySelector('.uno-studio-prompt__suggestions[hidden]'); 71 if (!container) return; 72 if (typeof unoStudio === 'undefined' || !unoStudio.contentServiceUrl) return; 73 74 var serviceUrl; 75 try { serviceUrl = new URL(unoStudio.contentServiceUrl); } catch (e) { return; } 76 var validOrigin = (serviceUrl.protocol === 'https:' && serviceUrl.hostname.endsWith('.platform.uno')) || 77 serviceUrl.hostname === 'localhost'; 78 if (!validOrigin) return; 79 80 var cardList = container.querySelector('.uno-studio-prompt__cards'); 81 if (!cardList) return; 82 83 var count = parseInt(container.dataset.count, 10) || 3; 84 var baseUrl = unoStudio.contentServiceUrl.replace(/\/+$/, ''); 85 var fetchUrl = baseUrl + '/api/v1/gallery/catalog/featured?n=' + count; 86 var controller = new AbortController(); 87 var timer = setTimeout(function () { controller.abort(); }, 5000); 88 89 fetch(fetchUrl, { signal: controller.signal }) 90 .then(function (res) { 91 clearTimeout(timer); 92 if (!res.ok) return Promise.reject(new Error('HTTP ' + res.status)); 93 return res.json(); 94 }) 95 .then(function (data) { 96 var samples = (data && Array.isArray(data.samples)) ? data.samples : []; 97 if (samples.length > count) samples = samples.slice(0, count); 98 var addedCount = 0; 99 samples.forEach(function (sample) { 100 var card = buildCard(sample); 101 if (card) { cardList.appendChild(card); addedCount++; } 102 }); 103 if (addedCount > 0) container.removeAttribute('hidden'); 104 }) 105 .catch(function (err) { 106 clearTimeout(timer); 107 var isNetwork = err instanceof TypeError || (err && err.name === 'AbortError'); 108 console.error('[uno-studio] Pills fetch failed (' + (isNetwork ? 'network/timeout' : err.message) + ')'); 109 }); 110 } 111 112 if (document.readyState === 'loading') { 113 document.addEventListener('DOMContentLoaded', initPills); 114 } else { 115 initPills(); 116 } 117 118 if (!form || !input) return; 119 120 function setPromptCookie(prompt) { 121 var safe = prompt.substring(0, MAX_LENGTH); 122 document.cookie = STORAGE_KEY + '=' + encodeURIComponent(safe) + '; path=/; max-age=1800; SameSite=Lax; Secure'; 123 } 124 125 function setAutoCookie() { 126 document.cookie = STORAGE_KEY + '_auto=1; path=/; max-age=1800; SameSite=Lax; Secure'; 127 } 128 129 function clearPromptCookies() { 130 document.cookie = STORAGE_KEY + '=; path=/; max-age=0'; 131 document.cookie = STORAGE_KEY + '_auto=; path=/; max-age=0'; 132 } 133 134 function redirectToStudioProject(project) { 135 var studioBase; 136 try { studioBase = new URL(unoStudio.studioUrl); } catch (e) { return; } 137 if (studioBase.protocol !== 'https:') return; 138 studioBase.searchParams.set('project', project); 139 window.location.href = studioBase.toString(); 140 } 141 142 function buildStudioUrl(prompt, autoSubmit) { 143 var studioBase; 144 try { studioBase = new URL(unoStudio.studioUrl); } catch (e) { return ''; } 145 if (studioBase.protocol !== 'https:') return ''; 146 var url = unoStudio.studioUrl.replace(/\/+$/, '') + '?prompt=' + encodeURIComponent(prompt); 147 if (autoSubmit) url += '&autoSubmit=true'; 148 return url; 149 } 150 151 function redirectToStudio(prompt, autoSubmit) { 152 var url = buildStudioUrl(prompt, autoSubmit); 153 if (url) window.location.href = url; 154 } 155 156 function capturePromptBeacon(prompt) { 157 if (!unoStudio.ajaxUrl || !unoStudio.captureNonce) return; 158 try { 159 var data = new FormData(); 160 data.append('action', 'uno_studio_capture_prompt'); 161 data.append('nonce', unoStudio.captureNonce); 162 data.append('prompt', prompt.substring(0, MAX_LENGTH)); 163 if (navigator.sendBeacon) { 164 navigator.sendBeacon(unoStudio.ajaxUrl, data); 165 } else { 166 fetch(unoStudio.ajaxUrl, { method: 'POST', body: data, keepalive: true, credentials: 'same-origin' }); 167 } 168 } catch (e) {} 169 } 170 171 function savePromptAndRedirect(prompt, autoSubmit) {
172 if (unoStudio.isLoggedIn) { 173 capturePromptBeacon(prompt); 174 redirectToStudio(prompt, autoSubmit); 175 return; 176 } 177 setPromptCookie(prompt); 178 if (autoSubmit) setAutoCookie(); 179 openOverlay(); 180 } 181 182 function openOverlay() { 183 if (!overlay) return; 184 overlay.classList.add('is-open'); 185 document.body.style.overflow = 'hidden'; 186 overlay.querySelector('input[name="username"], input[name="email"]')?.focus(); 187 } 188 189 function closeOverlay() { 190 if (!overlay) return; 191 overlay.classList.remove('is-open'); 192 document.body.style.overflow = ''; 193 clearPromptCookies(); 194 } 195 196 if (closeBtn) { 197 closeBtn.addEventListener('click', closeOverlay); 198 } 199 200 if (backdrop) { 201 backdrop.addEventListener('click', closeOverlay); 202 } 203 204 document.addEventListener('keydown', function (e) { 205 if (e.key === 'Escape' && overlay && overlay.classList.contains('is-open')) { 206 closeOverlay(); 207 } 208 }); 209 210 if (overlay) { 211 var tabs = overlay.querySelectorAll('.ctt-auth-tab[data-panel]'); 212 var panels = overlay.querySelectorAll('.ctt-auth-panel[data-panel]'); 213
214 tabs.forEach(function (tab) { 215 tab.addEventListener('click', function () { 216 var target = tab.dataset.panel; 217 tabs.forEach(function (t) { 218 t.classList.toggle('is-active', t.dataset.panel === target); 219 }); 220 panels.forEach(function (p) { 221 p.classList.toggle('is-active', p.dataset.panel === target); 222 }); 223 }); 224 }); 225 } 226 227 // Match the Studio app prompt: Enter submits, Shift+Enter inserts a newline. 228 // Without this, Enter in the textarea only adds a line break and the user 229 // has to reach for the arrow button. Ignore Enter while an IME composition 230 // is in progress so CJK input does not submit half-typed text. 231 input.addEventListener('keydown', function (e) { 232 if (e.key !== 'Enter' || e.shiftKey) return; 233 if (e.isComposing || e.keyCode === 229) return; 234 e.preventDefault(); 235 if (typeof form.requestSubmit === 'function') { 236 form.requestSubmit(); 237 } else { 238 form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); 239 } 240 }); 241 242 form.addEventListener('submit', function (e) { 243 e.preventDefault(); 244 var prompt = input.value.trim(); 245 if (!prompt) return; 246 247 // GA4: homepage prompt submission. Uses the same cta_click/label scheme as 248 // the ga-event system. Lives here (not on the arrow) to also cover Enter. 249 if (typeof gtag !== 'undefined') { 250 gtag('event', 'cta_click', { 251 event_category: 'homepage', 252 event_label: 'prompt_submit' 253 }); 254 } 255 256 savePromptAndRedirect(prompt, false); 257 }); 258 259})();
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.