PageSourceSearch

https://www.kitzbuehel.com/typo3conf/ext/ns_t3as/Resources/Public/JavaScript/ai-search-injector.js?1786540979

js kitzbuehel.com collected 2026-10-02 06:04:20 UTC 44,354 bytes, 1,149 lines download raw bytes

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, '&amp;')
366      .replace(/</g, '&lt;')
367      .replace(/>/g, '&gt;');
368
369  const copyEscapeHtmlAttr = (str) =>
370    String(str)
371      .replace(/&/g, '&amp;')
372      .replace(/"/g, '&quot;')
373      .replace(/</g, '&lt;');
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.