1(function initNsT3asActionRowHtml() { 2 if (window.NS_T3AS_COPY_RESPONSE_BTN_HTML) { 3 return; 4 } 5 window.NS_T3AS_COPY_RESPONSE_BTN_HTML = 6 '<button type="button" class="ns-t3as-rating-btn ns-t3as-copy-response-btn" aria-label="Copy response" data-tooltip="Copy response"><svg height="20" width="20" aria-hidden="true" focusable="false" viewBox="0 0 14 16" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M1.77734 2.96289C2.10462 2.96289 2.37012 3.22838 2.37012 3.55566C2.37003 3.86235 2.13657 4.11509 1.83789 4.14551L1.7168 4.15137C1.41824 4.18181 1.18479 4.43369 1.18457 4.74023V13.0361C1.18467 14.0181 1.98137 14.8145 2.96289 14.8145H8.88867C9.19535 14.8145 9.44797 14.5818 9.47852 14.2832L9.48438 14.1611C9.51492 13.8625 9.76754 13.6299 10.0742 13.6299C10.4012 13.63 10.6658 13.8947 10.666 14.2217C10.666 15.2035 9.87051 16 8.88867 16H2.96289C1.3263 16 0.000103459 14.6722 0 13.0361V4.74023C0.000234322 3.75868 0.795773 2.96304 1.77734 2.96289ZM12.4443 2.37012C12.4442 1.71576 11.9141 1.18568 11.2598 1.18555H5.33398L5.21289 1.19141C4.65505 1.24796 4.21101 1.69123 4.1543 2.24902L4.14844 2.37012V11.2588C4.14844 11.5656 4.38192 11.8182 4.68066 11.8486L4.74121 11.8516H11.2598C11.9141 11.8514 12.4442 11.3213 12.4443 10.667V2.37012ZM13.6299 10.667C13.6297 11.9759 12.5687 13.037 11.2598 13.0371H4.74121C3.75937 13.0371 2.96289 12.2406 2.96289 11.2588V2.37012C2.96303 1.06111 4.02495 2.48186e-07 5.33398 0H11.2598C12.5687 0.000137107 13.6297 1.0612 13.6299 2.37012V10.667Z"></path></svg></button>'; 7 window.NS_T3AS_FEEDBACK_RATING_BTNS_HTML = 8 '<button type="button" class="ns-t3as-rating-btn" data-rating="positive" aria-label="Submit positive feedback" data-tooltip="Submit positive feedback"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"></path><path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"></path></svg></button>' + 9 '<button type="button" class="ns-t3as-rating-btn" data-rating="negative" aria-label="Submit negative feedback" data-tooltip="Submit negative feedback"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"></path><path d="M17 2h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"></path></svg></button>'; 10})(); 11 12document.addEventListener('DOMContentLoaded', () => { 13 const aiResultWrapper = document.querySelector('.nst3as-ai-search-result-injector'); 14 const searchLoader = aiResultWrapper?.querySelector('.search-loader'); 15 const searchResults = aiResultWrapper?.querySelector('.ns-t3as-search-results'); 16 if (!aiResultWrapper) return; 17 18 const baseUrl = window.location.origin; 19 let currentSearchHistoryId = 0; 20 let rootSearchHistoryId = 0; 21 let searchSessionId = ''; 22 let activeListenBtn = null; 23 let activeSpeechResultsEl = null; 24 window.nsT3asLastInjectorQuery = window.nsT3asLastInjectorQuery || ''; 25 26 const buildInjectorChatPostUrl = () => { 27 const u = new URL(window.location.href); 28 const params = new URLSearchParams(u.search); 29 params.set('type', '1597569646'); 30 params.set('tx_nst3as_aisearch[action]', 'searchChat'); 31 params.set('tx_nst3as_aisearch[controller]', 'T3As'); 32 u.search = params.toString(); 33 return u.toString(); 34 }; 35 36 const tryMountInjectorChat = () => { 37 if (typeof window.nsT3asInitChatMode !== 'function') return; 38 if (!searchResults || searchResults.querySelector('.ns-t3as-search-no-result')) return; 39 const chatRoot = aiResultWrapper.querySelector('.ns-t3as-chat-mode'); 40 if (!chatRoot || chatRoot.dataset.mounted === '1') return; 41 const csrf = chatRoot.dataset.csrf || ''; 42 const q = (window.nsT3asLastInjectorQuery || '').trim(); 43 if (!csrf || !q) return; 44 if (rootSearchHistoryId > 0) { 45 chatRoot.dataset.sessionRootSearchHistoryId = String(rootSearchHistoryId); 46 } 47 if (searchSessionId) { 48 chatRoot.dataset.sessionId = searchSessionId; 49 } 50 window.nsT3asInitChatMode(chatRoot, { 51 chatUrl: buildInjectorChatPostUrl(), 52 csrf, 53 initialQuery: q, 54 rootSearchHistoryId, 55 parentSearchHistoryId: rootSearchHistoryId, 56 sessionId: searchSessionId, 57 // ratingApiUrl: `${baseUrl}?type=1753285380&mode=api`, 58 ratingApiUrl: getSearchFeedbackPostUrl(), 59 getInitialResultHtml: () => searchResults.innerHTML, 60 getReferencesText: () => { 61 const el = searchResults.querySelector('.ns-t3as-source-links'); 62 return el ? el.innerText.trim() : ''; 63 }, 64 mainSearchResultsEl: searchResults, 65 }); 66 chatRoot.dataset.mounted = '1'; 67 }; 68 const parseSearchHistoryEventPayload = (rawEventData) => { 69 const empty = { searchHistoryId: 0, sessionId: '', parentSearchHistoryId: 0 }; 70 if (!rawEventData) return empty; 71 const asNumber = parseInt(rawEventData, 10); 72 if (!Number.isNaN(asNumber) && asNumber > 0) { 73 return { searchHistoryId: asNumber, sessionId: '', parentSearchHistoryId: asNumber }; 74 } 75 try { 76 const parsed = JSON.parse(rawEventData); 77 const id = parseInt(parsed?.search_history_id || parsed?.id || '0', 10); 78 const parentId = parseInt(parsed?.parent_search_history_id || '0', 10); 79 return { 80 searchHistoryId: Number.isNaN(id) ? 0 : id, 81 sessionId: parsed?.session_id ? String(parsed.session_id) : '', 82 parentSearchHistoryId: Number.isNaN(parentId) || parentId <= 0 ? (Number.isNaN(id) ? 0 : id) : parentId, 83 }; 84 } catch (e) { 85 return empty; 86 } 87 }; 88 89 const parseSearchHistoryEventId = (rawEventData) => parseSearchHistoryEventPayload(rawEventData).searchHistoryId; 90 91 const applySearchSessionMeta = (payload) => { 92 if (!payload || payload.searchHistoryId <= 0) return; 93 rootSearchHistoryId = payload.parentSearchHistoryId || payload.searchHistoryId; 94 searchSessionId = payload.sessionId || searchSessionId || ''; 95 if (rootSearchHistoryId > 0) { 96 document.querySelectorAll('.ns-t3as-chat-mode').forEach((el) => { 97 el.dataset.sessionRootSearchHistoryId = String(rootSearchHistoryId); 98 if (searchSessionId) { 99 el.dataset.sessionId = searchSessionId; 100 } 101 }); 102 } 103 }; 104 105 const applyStreamSearchHistoryMeta = (rawPayload, currentId = 0) => { 106 const meta = parseSearchHistoryEventPayload(rawPayload); 107 if (meta.searchHistoryId <= 0) { 108 return currentId > 0 ? currentId : 0; 109 } 110 applySearchSessionMeta(meta); 111 if (searchResults) { 112 searchResults.dataset.searchHistoryId = String(meta.searchHistoryId); 113 } 114 return meta.searchHistoryId; 115 }; 116 117 const resolveStreamSearchHistoryId = (...fallbacks) => { 118 const domId = resolveSearchHistoryIdFromDom(searchResults, 0); 119 if (domId > 0) { 120 return domId; 121 } 122 for (const candidate of fallbacks) { 123 const id = parseInt(String(candidate || '0'), 10); 124 if (!Number.isNaN(id) && id > 0) { 125 return id; 126 } 127 } 128 if (rootSearchHistoryId > 0) { 129 return rootSearchHistoryId; 130 } 131 return 0; 132 }; 133 134 const resolveSearchHistoryIdFromDom = (container, fallback = 0) => { 135 const meta = container?.querySelector?.('.ns-t3as-search-history-meta'); 136 if (meta) { 137 const fromMeta = parseInt(meta.getAttribute('data-search-history-id') || '0', 10); 138 if (!Number.isNaN(fromMeta) && fromMeta > 0) { 139 return fromMeta; 140 } 141 } 142 const fromDataset = parseInt(container?.dataset?.searchHistoryId || '0', 10); 143 if (!Number.isNaN(fromDataset) && fromDataset > 0) { 144 return fromDataset; 145 } 146 const fb = parseInt(String(fallback || '0'), 10); 147 return Number.isNaN(fb) ? 0 : fb; 148 }; 149 150 const getSearchFeedbackPostUrl = () => { 151 if (window.nsT3asSearchFeedbackUrl) { 152 return window.nsT3asSearchFeedbackUrl; 153 } 154 return `${baseUrl}?type=1597569645`; 155 } 156 157 const resolveLoaderType = (contextEl) => { 158 const results = contextEl?.closest?.('.ns-t3as-search-results') || contextEl; 159 const wrap = 160 contextEl?.closest?.('.ns-t3as-search-results-wrap') || 161 contextEl?.closest?.('.nst3as-ai-search-result-injector') || 162 aiResultWrapper; 163 const loaderEl = wrap?.querySelector?.('.ns-t3as-loader'); 164 const fromDataset = results?.dataset?.loaderType || wrap?.dataset?.loaderType; 165 if (fromDataset === 'skeleton' || fromDataset === 'typing') { 166 return fromDataset; 167 } 168 if (loaderEl?.classList.contains('ns-t3as-loader--skeleton')) { 169 return 'skeleton'; 170 } 171 return 'typing'; 172 }; 173 174 const resolveLoaderMessage = (contextEl) => { 175 const results = contextEl?.closest?.('.ns-t3as-search-results') || contextEl; 176 const wrap = 177 contextEl?.closest?.('.ns-t3as-search-results-wrap') || 178 contextEl?.closest?.('.nst3as-ai-search-result-injector') || 179 aiResultWrapper; 180 return results?.dataset?.loaderMessage || wrap?.dataset?.loaderMessage || ''; 181 }; 182 183 const createStreamingWaitElement = (loaderType = 'typing', message = '') => { 184 if (!document.getElementById('ns-t3as-spinner-style')) { 185 const style = document.createElement('style'); 186 style.id = 'ns-t3as-spinner-style'; 187 style.textContent = ` 188 .ns-t3as-stream-wait.ns-t3as-loader--spinner{flex-direction:column;justify-content:center;align-items:center;gap:24px;min-height:120px;padding:20px 0} 189 .ns-t3as-spinner-message{font-family:inherit;font-size:20px;font-weight:700;text-align:center} 190 .ns-t3as-spinner-icon{display:inline-flex} 191 .ns-t3as-spinner-icon svg{animation:nsT3asSpin .8s linear infinite} 192 @keyframes nsT3asSpin{to{transform:rotate(360deg)}} 193 `; 194 document.head.appendChild(style); 195 } 196 197 const waitEl = document.createElement('div'); 198 waitEl.className = 'ns-t3as-stream-wait ns-t3as-loader ns-t3as-loader--spinner is-active'; 199 waitEl.setAttribute('role', 'status'); 200 waitEl.setAttribute('aria-live', 'polite'); 201 waitEl.setAttribute('aria-busy', 'true'); 202 203 if (message) { 204 const messageEl = document.createElement('span'); 205 messageEl.className = 'ns-t3as-spinner-message';
206 messageEl.textContent = message; 207 waitEl.appendChild(messageEl); 208 } 209 210 const spinner = document.createElement('span'); 211 spinner.className = 'ns-t3as-spinner-icon'; 212 spinner.setAttribute('aria-hidden', 'true'); 213 spinner.innerHTML = '<svg viewBox="0 0 24 24" width="100" height="100" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="10" fill="none" stroke="#E20000" stroke-width="3" stroke-linecap="round" stroke-dasharray="47 15"/></svg>'; 214 waitEl.appendChild(spinner); 215 return waitEl; 216 }; 217 218 const removeStreamingWaitState = (resultsEl) => { 219 if (!resultsEl) { 220 return; 221 } 222 resultsEl.querySelectorAll('.ns-t3as-stream-wait').forEach((el) => el.remove()); 223 }; 224 225 const ensureStreamingWaitAtEnd = (resultsEl) => { 226 if (!resultsEl) { 227 return null; 228 } 229 let wait = resultsEl.querySelector('.ns-t3as-stream-wait'); 230 if (!wait) { 231 wait = createStreamingWaitElement(resolveLoaderType(resultsEl), resolveLoaderMessage(resultsEl)); 232 } else { 233 wait.remove(); 234 } 235 resultsEl.appendChild(wait); 236 return wait; 237 }; 238 239 const placeStreamingWaitState = (resultsEl) => { 240 if (!resultsEl) { 241 return null; 242 } 243 removeStreamingWaitState(resultsEl); 244 return ensureStreamingWaitAtEnd(resultsEl); 245 }; 246 247 const hideAllSearchLoaders = (root) => { 248 if (!root) { 249 return; 250 } 251 root.querySelectorAll('.ns-t3as-loader').forEach((loaderEl) => deactivateSearchLoader(loaderEl)); 252 }; 253 254 const injectorStreamWidget = { 255 ensureStreamingWaitAtEnd, 256 removeStreamingWaitState, 257 hideAllSearchLoaders, 258 syncListenButton: (contextEl) => syncListenButton(contextEl), 259 }; 260 261 const setInjectorChatModeAwaiting = (on) => { 262 const chatRoot = aiResultWrapper?.querySelector('.ns-t3as-chat-mode'); 263 if (!chatRoot || typeof window.nsT3asSetChatModeAwaiting !== 'function') { 264 return; 265 } 266 window.nsT3asSetChatModeAwaiting(chatRoot, on); 267 }; 268 269 const createSearchStreamRenderer = (resultsEl, wrapperEl, onPresentationComplete) => { 270 if (typeof window !== 'undefined' && window.NsT3asSearchStreamCharacterByCharacter?.createRenderer) { 271 return window.NsT3asSearchStreamCharacterByCharacter.createRenderer({ 272 results: resultsEl, 273 container: wrapperEl, 274 widget: injectorStreamWidget, 275 onFinalize: onPresentationComplete, 276 }); 277 } 278 279 let streamHtml = ''; 280 let contentEl = null; 281 let scrollFrame = null; 282 283 const resultsHaveVisibleContent = () => { 284 if (streamHtml.trim().length > 0) { 285 return true; 286 } 287 const content = resultsEl?.querySelector('.ns-t3as-search-content'); 288 if (content?.textContent?.trim()) { 289 return true; 290 } 291 if (!resultsEl) { 292 return false; 293 } 294 const probe = resultsEl.cloneNode(true); 295 probe.querySelectorAll('.ns-t3as-stream-wait, .ns-t3as-rating-row, .ns-t3as-loader').forEach((el) => el.remove()); 296 return (probe.textContent || '').trim().length > 0; 297 }; 298 299 const ensureContentEl = () => { 300 if (contentEl && contentEl.isConnected) { 301 return contentEl; 302 } 303 contentEl = resultsEl.querySelector('.ns-t3as-search-content'); 304 if (!contentEl) { 305 contentEl = document.createElement('div'); 306 contentEl.className = 'ns-t3as-search-content ns-t3as-is-streaming'; 307 const wait = resultsEl.querySelector('.ns-t3as-stream-wait'); 308 if (wait) { 309 resultsEl.insertBefore(contentEl, wait); 310 } else { 311 resultsEl.appendChild(contentEl); 312 } 313 } 314 return contentEl; 315 }; 316 317 const flushScroll = () => { 318 scrollFrame = null; 319 if (resultsEl) { 320 resultsEl.scrollTop = resultsEl.scrollHeight; 321 } 322 }; 323 324 const closeStreamingUi = () => { 325 removeStreamingWaitState(resultsEl); 326 hideAllSearchLoaders(wrapperEl); 327 resultsEl.querySelector('.ns-t3as-is-streaming')?.classList.remove('ns-t3as-is-streaming'); 328 }; 329 330 return { 331 appendChunk(chunk) { 332 if (!chunk) return; 333 const el = ensureContentEl(); 334 streamHtml += chunk; 335 el.innerHTML = streamHtml; 336 ensureStreamingWaitAtEnd(resultsEl); 337 if (!scrollFrame) { 338 scrollFrame = requestAnimationFrame(flushScroll); 339 } 340 }, 341 finish() { 342 if (scrollFrame) { 343 cancelAnimationFrame(scrollFrame); 344 scrollFrame = null; 345 } 346 const el = ensureContentEl(); 347 if (streamHtml) { 348 el.innerHTML = streamHtml; 349 } 350 closeStreamingUi(); 351 wrapperEl?.querySelector('.ns-t3as-pre-questions')?.classList.remove('ns-t3as-click-disable'); 352 flushScroll(); 353 if (typeof onPresentationComplete === 'function') { 354 onPresentationComplete(); 355 } 356 }, 357 hasContent() { 358 return resultsHaveVisibleContent(); 359 } 360 }; 361 }; 362 363 const copyEscapeHtmlText = (str) => 364 String(str) 365 .replace(/&/g, '&') 366 .replace(/</g, '<') 367 .replace(/>/g, '>'); 368 369 const copyEscapeHtmlAttr = (str) => 370 String(str) 371 .replace(/&/g, '&') 372 .replace(/"/g, '"') 373 .replace(/</g, '<'); 374 375 const extractSourceReferencesForCopy = (root) => { 376 const ul = root.querySelector('.ns-t3as-source-links'); 377 if (!ul) return []; 378 const out = []; 379 ul.querySelectorAll('li.ns-t3as-source-card').forEach((li) => { 380 const idx = parseInt(li.getAttribute('data-source-index') || '0', 10); 381 const a = 382 li.querySelector('a.ns-t3as-search-results-link[href]') || 383 li.querySelector('a[href]'); 384 const href = a?.getAttribute('href')?.trim(); 385 if (!href) return;
386 const n = Number.isFinite(idx) && idx > 0 ? idx : out.length + 1; 387 out.push({ idx: n, href }); 388 }); 389 out.sort((a, b) => a.idx - b.idx); 390 return out; 391 }; 392 393 const buildCopyHtmlDocument = (bodyInnerHtml) => { 394 const styles = 395 'body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;font-size:14px;line-height:1.5;color:#111827;margin:0;padding:12px}' + 396 'p{margin:0 0 .75em}ul,ol{margin:0 0 .75em 1.25em;padding:0}li{margin:0 0 .35em}' + 397 'h1,h2,h3,h4{margin:1em 0 .5em;font-weight:600;line-height:1.25}' + 398 'h1{font-size:1.25em}h2{font-size:1.15em}h3{font-size:1.05em}' + 399 'a{color:#2563eb;text-decoration:underline}' + 400 'table{border-collapse:collapse;width:100%}td,th{border:1px solid #e5e7eb;padding:6px 8px;text-align:left}' + 401 'blockquote{margin:0 0 .75em;padding-left:1em;border-left:3px solid #e5e7eb}' + 402 'strong,b{font-weight:600}' + 403 'hr.ns-t3as-copy-sep{border:none;border-top:1px solid #e5e7eb;margin:1.25em 0}' + 404 '.ns-t3as-copy-refs p{margin:0.35em 0;font-size:13px;line-height:1.45}' + 405 '.ns-t3as-copy-disclaimer{margin-top:0;padding-top:0;color:#6b7280;font-size:13px;line-height:1.45}'; 406 return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>${styles}</style></head><body>${bodyInnerHtml}</body></html>`; 407 }; 408 409 const getSearchResultsCopyPayload = (resultsEl) => { 410 if (!resultsEl) { 411 return { html: '', text: '' }; 412 } 413 const copyRoot = 414 resultsEl.closest('.ns-t3as-search-results-wrap') || 415 resultsEl.closest('.nst3as-ai-search-result-injector') || 416 resultsEl.parentElement; 417 const clone = resultsEl.cloneNode(true); 418 clone.querySelectorAll('.ns-t3as-rating-row').forEach((el) => el.remove()); 419 clone.querySelectorAll('script, style').forEach((el) => el.remove()); 420 421 const refs = extractSourceReferencesForCopy(clone); 422 clone.querySelectorAll('.ns-t3as-source-links').forEach((el) => el.remove()); 423 424 const fragment = clone.innerHTML.trim(); 425 let bodyText = (clone.innerText || '') 426 .replace(/\r\n/g, '\n') 427 .replace(/\n{3,}/g, '\n\n') 428 .trim(); 429 if (!bodyText && fragment) { 430 const plainHost = document.createElement('div'); 431 plainHost.innerHTML = fragment; 432 bodyText = (plainHost.innerText || '') 433 .replace(/\r\n/g, '\n') 434 .replace(/\n{3,}/g, '\n\n') 435 .trim(); 436 } 437 438 const refsPlain = refs.map((r) => `[${r.idx}] ${r.href}`).join('\n'); 439 const disclaimerPlain = (copyRoot?.querySelector('.ai-can-make-mistakes')?.innerText || '') 440 .replace(/\s+/g, ' ') 441 .trim(); 442 443 const textParts = []; 444 if (bodyText) textParts.push(bodyText); 445 if (refsPlain) textParts.push('', refsPlain); 446 if (disclaimerPlain) textParts.push('', disclaimerPlain); 447 const text = textParts.join('\n').trim(); 448 449 const refsHtml = 450 refs.length > 0 451 ? `<div class="ns-t3as-copy-refs">${refs 452 .map( 453 (r) => 454 `<p><strong>[${r.idx}]</strong> <a href="${copyEscapeHtmlAttr(r.href)}">${copyEscapeHtmlText(r.href)}</a></p>` 455 ) 456 .join('')}</div>` 457 : ''; 458 459 const disclaimerHtml = disclaimerPlain 460 ? `<p class="ns-t3as-copy-disclaimer">${copyEscapeHtmlText(disclaimerPlain)}</p>` 461 : ''; 462 463 let bodyInner = fragment; 464 if (refsHtml) { 465 bodyInner += (fragment ? '<hr class="ns-t3as-copy-sep" />' : '') + refsHtml; 466 } 467 if (disclaimerHtml) { 468 bodyInner += (fragment || refsHtml ? '<hr class="ns-t3as-copy-sep" />' : '') + disclaimerHtml; 469 } 470 471 if (!text && !bodyInner) { 472 return { html: '', text: '' }; 473 } 474 475 return { 476 html: bodyInner ? buildCopyHtmlDocument(bodyInner) : '', 477 text, 478 listenText: bodyText, 479 }; 480 }; 481 482 const getOverviewListenText = (resultsEl) => { 483 const content = resultsEl?.querySelector?.('.ns-t3as-search-content'); 484 if (content) { 485 const text = (content.innerText || '').replace(/\s+/g, ' ').trim(); 486 if (text) { 487 return text; 488 } 489 } 490 return getSearchResultsCopyPayload(resultsEl).listenText || ''; 491 }; 492 493 const createOverviewUtterance = (text) => 494 window.NsT3asOverviewSpeechHighlight?.createOverviewUtterance(text) || 495 new SpeechSynthesisUtterance(text); 496 497 const startOverviewSpeech = (utterance) => { 498 if (window.NsT3asOverviewSpeechHighlight?.startOverviewSpeech) { 499 window.NsT3asOverviewSpeechHighlight.startOverviewSpeech(utterance); 500 return; 501 } 502 window.speechSynthesis?.speak(utterance); 503 }; 504 505 const syncListenButton = (contextEl) => { 506 if (!isVoiceoverEnabled()) { 507 return; 508 } 509 const root = 510 contextEl?.closest?.('.nst3as-ai-search-result-injector') || 511 contextEl?.closest?.('.ns-t3as-search-results-wrap') || 512 contextEl; 513 const btn = root?.querySelector?.('.ns-t3as-listen-btn'); 514 const results = root?.querySelector?.('.ns-t3as-search-results'); 515 if (!btn) return; 516 if (!window.speechSynthesis) { 517 btn.hidden = true; 518 return; 519 } 520 btn.hidden = false;
521 const listenText = getOverviewListenText(results); 522 const isStreaming = window.NsT3asOverviewSpeechHighlight?.isResultsStreaming?.(results) || false; 523 const canListen = Boolean(listenText) && !isStreaming; 524 btn.disabled = !canListen; 525 btn.setAttribute('aria-disabled', canListen ? 'false' : 'true'); 526 const hl = window.NsT3asOverviewSpeechHighlight; 527 if (hl?.isPlayingForButton?.(btn)) { 528 setListenButtonSpeaking(btn); 529 } else if (hl?.isPausedForButton?.(btn)) { 530 setListenButtonPaused(btn); 531 } else { 532 hl?.resetListenButtonEl?.(btn); 533 } 534 hl?.updateListenControlUi?.(btn); 535 }; 536 537 const writeFormattedClipboard = async (plainText, html) => { 538 const clipboard = navigator.clipboard; 539 if ( 540 clipboard && 541 typeof clipboard.write === 'function' && 542 typeof ClipboardItem !== 'undefined' && 543 html 544 ) { 545 try { 546 await clipboard.write([ 547 new ClipboardItem({ 548 'text/html': new Blob([html], { type: 'text/html' }), 549 'text/plain': new Blob([plainText], { type: 'text/plain' }), 550 }), 551 ]); 552 return; 553 } catch (e) { 554 /* fall through */ 555 } 556 } 557 if (clipboard && typeof clipboard.writeText === 'function') { 558 await clipboard.writeText(plainText); 559 return; 560 } 561 const ta = document.createElement('textarea'); 562 ta.value = plainText; 563 ta.setAttribute('readonly', ''); 564 ta.style.position = 'fixed'; 565 ta.style.left = '-9999px'; 566 document.body.appendChild(ta); 567 ta.select(); 568 document.execCommand('copy'); 569 document.body.removeChild(ta); 570 }; 571 572 const exposeClipboardDebugPayload = (text, html) => { 573 if (typeof window === 'undefined') return; 574 window.__NS_T3AS_CLIPBOARD_DEBUG__ = { 575 textPlain: text, 576 textHtml: html, 577 at: Date.now(), 578 }; 579 const debug = 580 window.sessionStorage?.getItem('ns_t3as_clipboard_debug') === '1' || 581 window.localStorage?.getItem('ns_t3as_clipboard_debug') === '1' || 582 /(?:^|[?&])ns_t3as_clipboard_debug=1(?:&|$)/.test(window.location?.search || ''); 583 if (debug) { 584 console.log('[ns-t3as] clipboard text/plain:\n', text); 585 console.log('[ns-t3as] clipboard text/html:\n', html); 586 } else { 587 console.debug( 588 '[ns-t3as] Copied; inspect payloads on window.__NS_T3AS_CLIPBOARD_DEBUG__ ' + 589 '({ textPlain, textHtml }). Enable logs: sessionStorage.setItem("ns_t3as_clipboard_debug","1")' 590 ); 591 } 592 }; 593 594 const copySearchResponse = async (copyBtn) => { 595 const results = copyBtn.closest('.ns-t3as-search-results'); 596 const { html, text } = getSearchResultsCopyPayload(results); 597 if (!text && !html) return; 598 try { 599 exposeClipboardDebugPayload(text, html); 600 await writeFormattedClipboard(text, html); 601 const prevLabel = copyBtn.getAttribute('aria-label') || 'Copy response'; 602 copyBtn.classList.add('is-copied'); 603 copyBtn.setAttribute('aria-label', 'Copied'); 604 window.setTimeout(() => { 605 copyBtn.classList.remove('is-copied'); 606 copyBtn.setAttribute('aria-label', prevLabel); 607 }, 2000); 608 } catch (err) { 609 console.error('Copy response failed:', err); 610 } 611 }; 612 613 const isSearchFeedbackEnabled = () => { 614 if (searchResults?.dataset && 'enableSearchFeedback' in searchResults.dataset) { 615 return searchResults.dataset.enableSearchFeedback !== '0'; 616 } 617 const globalFlag = window.nsT3asEnableSearchFeedback; 618 return globalFlag === undefined || globalFlag === 1 || globalFlag === '1'; 619 }; 620 621 const isVoiceoverEnabled = () => { 622 if (searchResults?.dataset && 'enableVoiceover' in searchResults.dataset) { 623 return searchResults.dataset.enableVoiceover !== '0'; 624 } 625 const globalFlag = window.nsT3asEnableVoiceover; 626 return globalFlag === undefined || globalFlag === 1 || globalFlag === '1'; 627 }; 628 629 const appendRatingControls = () => { 630 if (!searchResults) { 631 return; 632 } 633 const feedbackEnabled = isSearchFeedbackEnabled(); 634 currentSearchHistoryId = resolveStreamSearchHistoryId(currentSearchHistoryId); 635 636 let row = searchResults.querySelector('.ns-t3as-rating-row'); 637 if (!row) { 638 row = document.createElement('div'); 639 row.className = 'ns-t3as-rating-row'; 640 searchResults.appendChild(row); 641 } 642 643 if (!row.querySelector('.ns-t3as-copy-response-btn')) { 644 row.insertAdjacentHTML('afterbegin', window.NS_T3AS_COPY_RESPONSE_BTN_HTML); 645 } 646 647 row.querySelectorAll('.ns-t3as-rating-btn[data-rating]').forEach((btn) => btn.remove()); 648 649 if (feedbackEnabled && currentSearchHistoryId > 0) { 650 row.insertAdjacentHTML('beforeend', window.NS_T3AS_FEEDBACK_RATING_BTNS_HTML); 651 const previousHistoryId = row.dataset.searchHistoryId || ''; 652 row.dataset.searchHistoryId = String(currentSearchHistoryId); 653 searchResults.dataset.searchHistoryId = String(currentSearchHistoryId); 654 if (previousHistoryId !== String(currentSearchHistoryId)) { 655 delete row.dataset.feedbackBound; 656 delete row.dataset.feedbackSubmitted; 657 row.classList.remove('is-submitted'); 658 } 659 row.querySelectorAll('.ns-t3as-rating-btn[data-rating]').forEach((btn) => { 660 btn.dataset.searchHistoryId = String(currentSearchHistoryId); 661 }); 662 bindInjectorRatingRow(row); 663 } else { 664 delete row.dataset.feedbackBound; 665 delete row.dataset.feedbackSubmitted; 666 row.classList.remove('is-submitted'); 667 if (currentSearchHistoryId > 0) { 668 row.dataset.searchHistoryId = String(currentSearchHistoryId); 669 searchResults.dataset.searchHistoryId = String(currentSearchHistoryId); 670 } 671 } 672 }; 673 674 const bindInjectorRatingRow = (row) => { 675 if (!row) { 676 return; 677 } 678 if (typeof window.FeedbackRatingUi === 'undefined') { 679 window.setTimeout(() => bindInjectorRatingRow(row), 50); 680 return; 681 } 682 const labelsContainer = document.getElementById('searchResults') || row; 683 window.FeedbackRatingUi.bindRow(row, {
684 labelsContainer, 685 getMessageId: (ratingRow, btn) => { 686 if (window.FeedbackRatingUi?.resolveSearchHistoryIdForRow) { 687 return window.FeedbackRatingUi.resolveSearchHistoryIdForRow(ratingRow, btn); 688 } 689 return resolveSearchHistoryIdFromDom(searchResults, parseInt(ratingRow.dataset.searchHistoryId || '0', 10)); 690 }, 691 onSubmit: async (searchHistoryId, rating, feedbackReason, feedbackComment) => { 692 const historyId = searchHistoryId > 0 693 ? searchHistoryId 694 : resolveSearchHistoryIdFromDom(searchResults, parseInt(row.dataset.searchHistoryId || String(currentSearchHistoryId) || '0', 10)); 695 if (historyId <= 0) { 696 console.error('AI search feedback: missing search_history_id.'); 697 return false; 698 } 699 const primaryUrl = getSearchFeedbackPostUrl(); 700 const fallbackUrl = `${baseUrl}?type=1597569645`; 701 const injectorFallback = `${baseUrl}?type=1753285380&mode=api`; 702 return submitSearchRating( 703 historyId, 704 rating, 705 feedbackReason, 706 feedbackComment, 707 primaryUrl, 708 fallbackUrl !== primaryUrl ? fallbackUrl : injectorFallback 709 ); 710 }, 711 }); 712 }; 713 714 const stopOverviewSpeech = () => { 715 const hl = window.NsT3asOverviewSpeechHighlight; 716 if (hl?.stopOverviewSpeech) { 717 hl.stopOverviewSpeech(); 718 } else if (window.speechSynthesis) { 719 window.speechSynthesis.cancel(); 720 } 721 activeSpeechResultsEl = null; 722 activeListenBtn = null; 723 }; 724 725 const setListenButtonSpeaking = (btn) => { 726 const pauseLabel = btn.dataset.labelPause || 'Pause'; 727 btn.classList.remove('is-paused'); 728 btn.classList.add('is-speaking'); 729 btn.setAttribute('aria-label', pauseLabel); 730 btn.setAttribute('aria-pressed', 'true'); 731 btn.dataset.tooltip = pauseLabel; 732 window.NsT3asOverviewSpeechHighlight?.updateListenControlUi?.(btn); 733 }; 734 735 const setListenButtonPaused = (btn) => { 736 const resumeLabel = btn.dataset.labelResume || 'Resume'; 737 btn.classList.remove('is-speaking'); 738 btn.classList.add('is-paused'); 739 btn.setAttribute('aria-label', resumeLabel); 740 btn.setAttribute('aria-pressed', 'false'); 741 btn.dataset.tooltip = resumeLabel; 742 window.NsT3asOverviewSpeechHighlight?.updateListenControlUi?.(btn); 743 }; 744 745 const toggleOverviewListen = (listenBtn) => { 746 if (!window.speechSynthesis || !searchResults || listenBtn.disabled || !isVoiceoverEnabled()) return; 747 const hl = window.NsT3asOverviewSpeechHighlight; 748 749 if (hl?.isPausedForButton?.(listenBtn)) { 750 if (hl.resumeOverviewSpeech()) { 751 activeListenBtn = listenBtn; 752 setListenButtonSpeaking(listenBtn); 753 return; 754 } 755 stopOverviewSpeech(); 756 } 757 if (hl?.isPlayingForButton?.(listenBtn)) { 758 if (hl.pauseOverviewSpeech()) { 759 activeListenBtn = listenBtn; 760 setListenButtonPaused(listenBtn); 761 } 762 return; 763 } 764 765 const text = getOverviewListenText(searchResults); 766 if (!text || hl?.isResultsStreaming?.(searchResults)) return; 767 stopOverviewSpeech(); 768 activeSpeechResultsEl = searchResults; 769 const finishSpeech = () => { 770 if (activeListenBtn !== listenBtn) return; 771 activeSpeechResultsEl = null; 772 activeListenBtn = null; 773 window.NsT3asOverviewSpeechHighlight?.resetListenButtonEl?.(listenBtn); 774 }; 775 if (hl?.beginOverviewSpeechSession) { 776 hl.beginOverviewSpeechSession(listenBtn, searchResults, text, finishSpeech); 777 } 778 activeListenBtn = listenBtn; 779 setListenButtonSpeaking(listenBtn); 780 }; 781 782 const formatRatingForPost = (rating) => { 783 const normalizedRating = parseInt(String(rating), 10); 784 if (normalizedRating === -1) { 785 return 'negative'; 786 } 787 if (normalizedRating === 1) { 788 return 'positive'; 789 } 790 return ''; 791 }; 792 793 const postSearchRatingToUrl = async (url, searchHistoryId, ratingToken, feedbackReason = '', feedbackComment = '') => { 794 try { 795 const body = new URLSearchParams(); 796 body.set('submit_rating', '1'); 797 body.set('search_history_id', String(searchHistoryId)); 798 body.set('rating', ratingToken); 799 if (feedbackReason) { 800 body.set('feedback_reason', feedbackReason); 801 } 802 if (feedbackComment) { 803 body.set('feedback_comment', feedbackComment); 804 } 805 const response = await fetch(url, { 806 method: 'POST', 807 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, 808 credentials: 'same-origin', 809 body: body.toString() 810 }); 811 const raw = await response.text(); 812 if (!response.ok) { 813 console.error('Search rating HTTP error:', response.status, url, raw); 814 return { 815 success: false, 816 message: 'Server error (' + response.status + '). Check browser console for details.', 817 }; 818 } 819 let payload;
820 try { 821 payload = JSON.parse(raw); 822 } catch (e) { 823 console.error('Search rating: expected JSON from', url, raw.slice(0, 300)); 824 return { 825 success: false, 826 message: 'Invalid server response (not JSON). Flush TYPO3 caches and retry.', 827 }; 828 } 829 if (payload?.success !== true) { 830 console.error('Search rating rejected:', url, payload?.message || payload); 831 return { 832 success: false, 833 message: payload?.message || 'Feedback was rejected by the server.', 834 }; 835 } 836 return { success: true }; 837 } catch (err) { 838 console.error('Search rating failed:', url, err); 839 return { success: false, message: err?.message || 'Network error while saving feedback.' }; 840 } 841 }; 842 843 const submitSearchRating = async ( 844 searchHistoryId, 845 rating, 846 feedbackReason = '', 847 feedbackComment = '', 848 primaryUrl = '', 849 fallbackUrl = '' 850 ) => { 851 const ratingToken = formatRatingForPost(rating); 852 if (!searchHistoryId || ratingToken === '') { 853 return { success: false, message: 'Missing search history id or rating.' }; 854 } 855 const urls = [primaryUrl || getSearchFeedbackPostUrl(), fallbackUrl] 856 .filter((url, index, all) => url && all.indexOf(url) === index); 857 for (const url of urls) { 858 const result = await postSearchRatingToUrl(url, searchHistoryId, ratingToken, feedbackReason, feedbackComment); 859 if (result && result.success === true) { 860 return result; 861 } 862 } 863 return { success: false, message: 'Could not save feedback. Please try again.' }; 864 }; 865 866 aiResultWrapper.addEventListener('click', (event) => { 867 const listenBtn = event.target.closest('.ns-t3as-listen-btn'); 868 if (listenBtn) { 869 event.preventDefault(); 870 toggleOverviewListen(listenBtn); 871 return; 872 } 873 const copyBtn = event.target.closest('.ns-t3as-copy-response-btn'); 874 if (copyBtn) { 875 event.preventDefault(); 876 copySearchResponse(copyBtn); 877 return; 878 } 879 }); 880 881 fetch(`${baseUrl}?type=1753285380&mode=config`) 882 .then(res => { 883 if (!res.ok) { 884 throw new Error(`Failed to fetch config: ${res.status}`); 885 } 886 return res.json(); 887 }) 888 .then(config => { 889 const searchClass = config.searchClass?.trim(); 890 891 let searchTerm = ''; 892 let aiType = ''; 893 if (searchClass) { 894 const byId = document.getElementById(searchClass); 895 if (byId) { 896 searchTerm = byId.value; 897 } else { 898 const byClass = document.getElementsByClassName(searchClass); 899 if (byClass.length > 0 && byClass[0]) { 900 searchTerm = byClass[0].value; 901 } 902 } 903 } 904 905 if (searchTerm != '') { 906 startAIStreaming(searchTerm, config.aiType, config.refLinks); 907 } 908 909 function startAIStreaming(searchTerm, aiType, refLinks) { 910 stopOverviewSpeech(); 911 window.nsT3asLastInjectorQuery = searchTerm; 912 setInjectorChatModeAwaiting(true); 913 aiResultWrapper.style.display = 'block'; 914 clearSearchResultsKeepingLoader(searchResults, searchLoader); 915 searchResults.style.display = 'block'; 916 syncListenButton(aiResultWrapper); 917 if (!searchTerm || searchTerm.trim() === '') return; 918 919 if (searchTerm && searchTerm.trim() !== '') { 920 aiResultWrapper?.querySelector('.ai-overview')?.classList.remove('d-none'); 921 aiResultWrapper?.querySelector('.ai-can-make-mistakes')?.classList.add('d-none'); 922 const url = `${baseUrl}?type=1753285380&mode=api&message=${encodeURIComponent(searchTerm)}&aiType=${aiType}&refLinks=${refLinks}`; 923 const usesSseStream = aiType === 'openai' || aiType === 'claude' || aiType === 'mistral' 924 || aiType === 'gemini' || aiType === 'ollama'; 925 if (usesSseStream) { 926 hideAllSearchLoaders(aiResultWrapper); 927 placeStreamingWaitState(searchResults); 928 } else if (searchLoader) { 929 searchResults.prepend(searchLoader); 930 showLoader(); 931 } 932 933 if (aiType == 'gemini' || aiType == 'customllm') { 934 fetch(url) 935 .then(res => res.json()) 936 .then(data => { 937 const meta = { 938 searchHistoryId: parseInt(data?.search_history_id || '0', 10), 939 sessionId: data?.session_id ? String(data.session_id) : '', 940 parentSearchHistoryId: parseInt( 941 data?.parent_search_history_id || data?.search_history_id || '0', 942 10 943 ), 944 }; 945 applySearchSessionMeta(meta); 946 currentSearchHistoryId = meta.searchHistoryId; 947 if (data?.html) { 948 const resultWrapper = document.createElement("div"); 949 resultWrapper.id = "customSearchResults"; 950 resultWrapper.innerHTML = data.html; 951
952 resultWrapper.childNodes.forEach(node => { 953 searchResults.appendChild(node.cloneNode(true)); 954 }); 955 } 956 appendRatingControls(); 957 tryMountInjectorChat(); 958 setInjectorChatModeAwaiting(false); 959 hideLoader(); 960 aiResultWrapper.querySelector('.ai-overview')?.classList.remove('d-none'); 961 aiResultWrapper.querySelector('.ai-can-make-mistakes')?.classList.remove('d-none'); 962 syncListenButton(aiResultWrapper); 963 }) 964 } 965 if(aiType == 'openai' || aiType == 'claude' || aiType == 'mistral' || aiType == 'gemini' || aiType == 'ollama'){ 966 const eventSource = new EventSource(url); 967 eventSource.addEventListener('stream_start', () => { 968 aiResultWrapper.querySelector('.ai-overview')?.classList.remove('d-none'); 969 }); 970 eventSource.addEventListener('search_history', (event) => { 971 currentSearchHistoryId = applyStreamSearchHistoryMeta(event?.data || '', currentSearchHistoryId); 972 }); 973 let streamFinished = false; 974 let streamPresentationCompleted = false; 975 const completeStreamPresentation = () => { 976 if (streamPresentationCompleted) { 977 return; 978 } 979 streamPresentationCompleted = true; 980 currentSearchHistoryId = resolveStreamSearchHistoryId(currentSearchHistoryId); 981 appendRatingControls(); 982 tryMountInjectorChat(); 983 setInjectorChatModeAwaiting(false); 984 hideLoader(); 985 aiResultWrapper.querySelector('.ai-overview')?.classList.remove('d-none'); 986 aiResultWrapper.querySelector('.ai-can-make-mistakes')?.classList.remove('d-none'); 987 syncListenButton(aiResultWrapper); 988 }; 989 const streamRenderer = createSearchStreamRenderer(searchResults, aiResultWrapper, completeStreamPresentation); 990 eventSource.addEventListener('append', (event) => { 991 if (!event?.data) { 992 return; 993 } 994 searchResults.insertAdjacentHTML('beforeend', event.data); 995 if (searchResults.querySelector('.ns-t3as-source-links')) { 996 aiResultWrapper?.classList.add('ns-t3as-search-results-wrap--has-sources'); 997 } 998 currentSearchHistoryId = resolveStreamSearchHistoryId(currentSearchHistoryId); 999 }); 1000 eventSource.onmessage = (e) => { 1001 if (e.data === "[DONE]") { 1002 streamFinished = true; 1003 eventSource.close(); 1004 streamRenderer.finish(); 1005 currentSearchHistoryId = resolveStreamSearchHistoryId(currentSearchHistoryId); 1006 if (!streamRenderer.hasContent() && !searchResults.querySelector('.ns-t3as-search-no-result')) { 1007 const fallback = searchResults.dataset.emptyStreamText 1008 || searchResults.dataset.noresultText 1009 || ''; 1010 if (fallback) { 1011 searchResults.innerHTML = `<div class="ns-t3as-search-content"><p>${fallback}</p></div>`; 1012 } 1013 } 1014 return; 1015 } 1016 1017 try { 1018 const chunk = e.data.split("/**")[0]; 1019 if (!chunk) return; 1020 const historyPrefix = window.NS_T3AS_SEARCH_HISTORY_SSE_PREFIX || '__NS_T3AS_SEARCH_HISTORY__'; 1021 if (chunk.startsWith(historyPrefix)) { 1022 currentSearchHistoryId = applyStreamSearchHistoryMeta( 1023 chunk.slice(historyPrefix.length), 1024 currentSearchHistoryId 1025 ); 1026 return; 1027 } 1028 1029 hideLoader(); 1030 // aiResultWrapper.querySelector('.ai-overview')?.classList.remove('d-none'); 1031 // aiResultWrapper.querySelector('.ai-can-make-mistakes')?.classList.remove('d-none'); 1032 1033 // const resultWrapper = document.createElement("div"); 1034 // resultWrapper.id = "customSearchResults"; 1035 // resultWrapper.innerHTML = chunk; 1036
1037 // resultWrapper.childNodes.forEach(node => { 1038 // searchResults.appendChild(node.cloneNode(true)); 1039 // }); 1040 1041 streamRenderer.appendChunk(chunk); 1042 syncListenButton(aiResultWrapper); 1043 aiResultWrapper.querySelector('.ai-overview')?.classList.remove('d-none'); 1044 } catch (err) { 1045 console.error("Failed to parse SSE data:", err); 1046 } 1047 }; 1048 1049 eventSource.onerror = (e) => { 1050 eventSource.close(); 1051 if (streamFinished) { 1052 return; 1053 } 1054 if (streamRenderer.hasContent() && currentSearchHistoryId > 0 && searchResults.innerHTML.trim()) { 1055 appendRatingControls(); 1056 syncListenButton(aiResultWrapper); 1057 } 1058 streamRenderer.finish(); 1059 hideLoader(); 1060 if (!streamRenderer.hasContent()) { 1061 aiResultWrapper.style.display = "none"; 1062 aiResultWrapper.innerHTML = searchResults.dataset.noresultText; 1063 } 1064 }; 1065 } 1066 1067 } 1068 } 1069 } 1070 ); 1071 1072 const ensureSearchTypingDots = (loader) => { 1073 if (!loader) { 1074 return; 1075 } 1076 let dots = loader.querySelector('.ns-t3as-typing-dots'); 1077 if (!dots) { 1078 dots = document.createElement('span'); 1079 dots.className = 'ns-t3as-typing-dots'; 1080 dots.setAttribute('aria-hidden', 'true'); 1081 for (let i = 0; i < 3; i += 1) { 1082 dots.appendChild(document.createElement('span')); 1083 } 1084 loader.appendChild(dots); 1085 return; 1086 } 1087 if (dots.querySelectorAll('span').length < 3) { 1088 dots.innerHTML = '<span></span><span></span><span></span>'; 1089 } 1090 }; 1091 1092 const clearSearchResultsKeepingLoader = (results, loader) => { 1093 if (!results) { 1094 return; 1095 } 1096 Array.from(results.children).forEach((child) => { 1097 if (child !== loader) { 1098 child.remove(); 1099 } 1100 }); 1101 }; 1102 1103 const activateSearchLoader = (loader) => { 1104 if (!loader) { 1105 return; 1106 } 1107 const isSkeleton = loader.classList.contains('ns-t3as-loader--skeleton'); 1108 loader.classList.add('is-active'); 1109 loader.style.display = 'flex'; 1110 const skeleton = loader.querySelector('.ns-t3as-skeleton-loader'); 1111 const dots = loader.querySelector('.ns-t3as-typing-dots'); 1112 if (isSkeleton) { 1113 if (skeleton) { 1114 skeleton.style.display = 'flex'; 1115 } 1116 if (dots) { 1117 dots.style.display = 'none'; 1118 } 1119 return; 1120 } 1121 ensureSearchTypingDots(loader); 1122 if (skeleton) { 1123 skeleton.style.display = 'none'; 1124 } 1125 if (dots) { 1126 dots.style.display = 'inline-flex'; 1127 } 1128 }; 1129 1130 const activateSearchTypingLoader = (loader) => { 1131 activateSearchLoader(loader); 1132 }; 1133 1134 const deactivateSearchLoader = (loader) => { 1135 if (!loader) { 1136 return; 1137 } 1138 loader.classList.remove('is-active'); 1139 loader.style.display = 'none'; 1140 }; 1141 1142 const showLoader = () => { 1143 activateSearchLoader(searchLoader); 1144 }; 1145 1146 const hideLoader = () => { 1147 deactivateSearchLoader(searchLoader); 1148 }; 1149});
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.