PageSourceSearch

https://www.museorisorgimentotorino.it/wp-content/plugins/sg-ai-chatbot-client/assets/js/load-widget.js

js museorisorgimentotorino.it collected 2026-09-24 21:53:22 UTC 29,736 bytes, 720 lines download raw bytes

1const chatShell = document.querySelector('#sg-chat-shell');
2const chatBotContainer = document.querySelector('#sg-ai-chatbot-container');
3const chatBotForm = document.querySelector('#sg-ai-chatbot-container #sg-chat-widget');
4const signinSignupWrap = document.querySelector('#sg-auth-wrap');
5const loginForm = document.querySelector('#sg-ai-chatbot-container #sg-signin-form');
6const signupForm = document.querySelector('#sg-ai-chatbot-container #sg-signup-form');
7const lostPasswordForm = document.querySelector('#sg-ai-chatbot-container #sg-lostpassword-form');
8const resetPasswordForm = document.querySelector('#sg-ai-chatbot-container #sg-resetpassword-form');
9const chatToggle = document.querySelector('#sg-chat-toggle');
10const clearBtn = document.querySelector('#sg-clear');
11const sendBtn = document.querySelector('#sg-send');
12const key = chatBotContainer.dataset.client;
13const baseUrl = chatBotContainer.dataset.serverUrl;
14
15
16const endpoints = {
17    login: baseUrl + '/wp-json/sg-chat/v1/user/login',
18    register: baseUrl + '/wp-json/sg-chat/v1/user',
19    embed: baseUrl + '/wp-json/sg-chat/v1/embed',
20    chat: baseUrl + '/wp-json/sg-chat/v1/chat',
21    refreshToken: baseUrl + '/wp-json/sg-chat/v1/token/refresh',
22    contextClear: baseUrl + '/wp-json/sg-chat/v1/context/clear',
23    lostPassword: baseUrl + '/wp-json/sg-chat/v1/user/send-password-reset',
24    resetPassword: baseUrl + '/wp-json/sg-chat/v1/user/password-reset'
25};
26
27document.addEventListener('DOMContentLoaded', function () {
28
29    const sessionId = sessionStorage.getItem('sg_session_id');
30    if (chatToggle) {
31        const currentUrl = new URL(window.location.href);
32        if (currentUrl.searchParams.has('sg-shell-auto-open') && currentUrl.searchParams.get('sg-shell-auto-open') === 'yes') {
33            chatToggle.click();
34        }
35    }
36    if (resetPasswordForm && chatToggle) {
37        loadEmbed('', sessionId);
38        resetPasswordForm.addEventListener('submit', handleResetPassword);
39    }
40    if (chatBotContainer && chatBotForm && loginForm && lostPasswordForm && signupForm && chatToggle) {
41        if (key) {
42            const accessToken = sessionStorage.getItem('sg_access_token');
43
44            chatBotForm.addEventListener('submit', handleChat);
45            $('#sg-input').on('keydown', (e) => {
46                if (e.key === 'Enter' && !e.shiftKey) {
47                    e.preventDefault();
48                    chatBotForm.querySelector('[name="chat"]').click();
49                }
50            });
51            loginForm.addEventListener('submit', handleLogin);
52            signupForm.addEventListener('submit', handleRegister);
53            if (lostPasswordForm) {
54                lostPasswordForm.addEventListener('submit', handleLostPassword);
55            }
56            if (clearBtn) {
57                clearBtn.addEventListener('click', handleClearConversation);
58            }
59            if (accessToken) {
60                loadEmbed(accessToken, sessionId);
61            } else {
62                loadEmbed('', sessionId);
63            }
64
65            document.querySelectorAll('a.toggle-signin-signup-form').forEach(function (a) {
66                a.addEventListener('click', toggleSigninSignupForm);
67            });
68
69            document.querySelectorAll('a.toggle-signin-lostpassword-form').forEach(function (a) {
70                a.addEventListener('click', toggleSigninLostpasswordForm);
71            });
72
73            refreshSession();
74            refreshToken();
75            setInterval(function () {
76                refreshToken();
77                refreshSession();
78            }, 10000);
79        }
80    }
81});
82
83function toggleSigninSignupForm(e) {
84
85
86    e.preventDefault();
87    const currentForm = e.target.closest('form');
88    const wrap = currentForm.parentElement;
89    let otherFormSelector, currentHeaderId, otherHeaderId;
90    if (currentForm.id === 'sg-signin-form') {
91
92        otherFormSelector = '#sg-signup-form';
93        currentHeaderId = '#sg-signin-header';
94        otherHeaderId = '#sg-signup-header';
95    } else {
96
97        otherFormSelector = '#sg-signin-form';
98        currentHeaderId = '#sg-signup-header';
99        otherHeaderId = '#sg-signin-header';
100    }
101    const otherForm = wrap.querySelector(otherFormSelector);
102
103    currentForm.style.display = 'none';
104    otherForm.style.display = 'flex';
105    const currentHeader = wrap.querySelector(currentHeaderId);
106    const otherHeader = wrap.querySelector(otherHeaderId);
107    if (currentHeader) currentHeader.style.display = 'none';
108    if (otherHeader) otherHeader.style.display = '';
109
110}
111
112function toggleSigninLostpasswordForm(e) {
113
114    e.preventDefault();
115    const allForms = ['#sg-signin-form', '#sg-signup-form', '#sg-lostpassword-form'];
116    const currentForm = e.target.closest('form');
117    const wrap = currentForm.parentElement;
118    const targetSelector = currentForm.id === 'sg-lostpassword-form' ? '#sg-signin-form' : '#sg-lostpassword-form';
119    allForms.forEach(function (sel) {
120        const f = wrap.querySelector(sel);
121        if (f) f.style.display = 'none';
122    });
123    const targetForm = wrap.querySelector(targetSelector);
124    if (targetForm) targetForm.style.display = 'flex';
125
126}
127
128function refreshSession() {
129    const sessionExpiresAt = sessionStorage.getItem('sg_session_expires');
130    const sessionId = sessionStorage.getItem('sg_session_id');
131
132    if (sessionId && sessionExpiresAt && Math.floor(Date.now() / 1000) < Number(sessionExpiresAt)) {
133        hideTurnstile('chat');
134    } else if (sessionExpiresAt && Math.floor(Date.now() / 1000) > Number(sessionExpiresAt)) {
135        showTurnstile('chat');
136        resetTurnstile('chat');
137        sessionStorage.removeItem('sg_session_expires');
138        sessionStorage.removeItem('sg_session_id');
139    }
140    //alert('ss')
141
142    showTurnstile('lostpassword');
143}
144
145function refreshToken() {
146    const accessExpires = sessionStorage.getItem('sg_access_expires_at');
147    const refreshExpires = sessionStorage.getItem('sg_refresh_expires_at');
148
149    const refreshToken = sessionStorage.getItem('sg_refresh_token');
150
151    const currentTimeStamp = Math.floor(Date.now() / 1000);
152
153    const accessExpiresIn = accessExpires - currentTimeStamp;
154    const refreshExpiresIn = refreshExpires - currentTimeStamp;
155
156    if (accessExpiresIn < 60 && refreshExpiresIn > 0) {
157        fetch(endpoints.refreshToken + '?token=' + refreshToken)
158            .then(async function (res) {
159                if (res.ok) {
160                    const jsonData = await res.json();
161                    return jsonData;
162                }
163
164                throw new Error('Errore durante il refresh del token.');
165
166            })
167            .then(function (data) {
168                const currentTimeStamp = Math.floor(Date.now() / 1000);
169                if (typeof data.access_token === 'string' && data.access_token.length) {
170                    sessionStorage.setItem('sg_access_token', data.access_token);
171                }
172                if (typeof data.refresh_token === 'string' && data.refresh_token.length) {
173                    sessionStorage.setItem('sg_refresh_token', data.refresh_token);
174                }
175                if (typeof data.access_expires_in === 'number' && data.access_expires_in > 0) {
176                    sessionStorage.setItem('sg_access_expires_at', currentTimeStamp + data.access_expires_in);
177                }
178                if (typeof data.refresh_expires_in === 'number' && data.refresh_expires_in > 0) {
179                    sessionStorage.setItem('sg_refresh_expires_at', currentTimeStamp + data.refresh_expires_in);
180                }
181            }).catch(resetOauthProcess);
182    }
183
184}
185
186function resetOauthProcess() {
187    sessionStorage.removeItem('sg_access_token');
188    sessionStorage.removeItem('sg_refresh_token');
189    sessionStorage.removeItem('sg_access_expires_at');
190    sessionStorage.removeItem('sg_refresh_expires_at');
191    if (signinSignupWrap)
192        signinSignupWrap.style.display = 'flex';
193    if (chatBotForm)
194        chatBotForm.style.display = 'none';
195}
196
197document.addEventListener('click', function (e) {
198    if (e.target.closest('.sg-message.sg-assistant .sample-message')) {
199
200        e.preventDefault();
201        if (document.querySelector('#sg-send').getAttribute('disabled') === 'disabled') {
202            return;
203        }
204
205        document.querySelector('#sg-input').value = e.target.innerHTML;
206        document.querySelector('#sg-input').innerHTML = e.target.innerHTML;
207        document.querySelector('#sg-send').click();
208    }
209});
210
211function loadEmbed(accessToken, sessionId) {
212    const embedUrl = new URL(endpoints.embed);
213    embedUrl.searchParams.set('client', encodeURIComponent(key));
214    if (accessToken) {
215        embedUrl.searchParams.set('access_token', accessToken);
216    }
217    if (sessionId) {
218        embedUrl.searchParams.set('session_id', sessionId);
219    }
220
221
222    fetch(embedUrl.toString())
223        .then(async function (res) {
224            const jsonResponse = await res.json();
225            if (!res.ok && res.status !== 401) {
226
227                throw new Error(jsonResponse.message || 'Errore nel caricamento del widget.');
228            }
229            return { status: res.status, data: jsonResponse };
230        })
231        .then(function (res) {
232            if (res.status === 401) {
233                if (signinSignupWrap)
234                    signinSignupWrap.style.display = 'flex';
235                if (chatBotForm)
236                    chatBotForm.style.display = 'none';
237            } else {
238                if (signinSignupWrap)
239                    signinSignupWrap.style.display = 'none';
240                if (chatBotForm)
241                    chatBotForm.style.display = 'flex';
242            }
243
244            if (typeof res.data.settings.bg_color === 'string') {
245                document.body.style.setProperty('--sg-main-color', res.data.settings.bg_color);
246            }
247            if (typeof res.data.settings.text_color === 'string') {
248                document.body.style.setProperty('--sg-text-color', res.data.settings.text_color);
249            }
250            if (typeof res.data.settings.link_assistant_color === 'string') {
251                document.body.style.setProperty('--sg-assistant-link-color', res.data.settings.link_assistant_color);
252            }
253            if (typeof res.data.settings.chat_title === 'string') {
254                const chatTitle = chatShell.querySelector('#sg-chat-title');
255                if (chatTitle) {
256                    chatTitle.innerHTML = res.data.settings.chat_title;
257                }
258            }
259
260            if (typeof res.data.settings.custom_css === 'string' && res.data.settings.custom_css) {
261                let customCssStyleTag = document.querySelector('#sg-custom-css');
262                if (!customCssStyleTag) {
263                    customCssStyleTag = document.createElement('style');
264                    customCssStyleTag.id = 'sg-custom-css';
265                    document.head.appendChild(customCssStyleTag);
266                }
267                customCssStyleTag.innerText = res.data.settings.custom_css;
268            }
269
270            if (typeof res.data.settings.welcome_message === 'string' && chatBotContainer) {
271                const welcomeMessage = chatBotContainer.querySelector('#sg-messages .sg-message-content');
272                if (welcomeMessage) {
273                    welcomeMessage.innerText = res.data.settings.welcome_message;
274                }
275            }
276
277            if (typeof res.data.settings.sample_messages === 'string' && chatBotContainer) {
278                const welcomeMessage = chatBotContainer.querySelector('#sg-messages .sg-message-content');
279                if (welcomeMessage) {
280                    const sampleMessages = JSON.parse(res.data.settings.sample_messages);
281                    const br = document.createElement('br');
282                    const ul = document.createElement('ul');
283                    welcomeMessage.insertAdjacentElement('beforeend', br);
284                    welcomeMessage.insertAdjacentElement('beforeend', ul);
285                    sampleMessages.forEach(function (sampleMessage) {
286
287                        const li = document.createElement('li');
288                        const sampleMessageTag = document.createElement('a');
289                        sampleMessageTag.classList.add('sample-message');
290                        sampleMessageTag.innerHTML = sampleMessage;
291                        sampleMessageTag.href = 'javascript:void(0);';
292                        li.insertAdjacentElement('beforeend', sampleMessageTag);
293                        ul.insertAdjacentElement('beforeend', li);
294                    });
295                }
296            }
297
298            if (typeof res.data.context === 'object' && chatBotContainer) {
299                for (const message of res.data.context) {
300
301                    addMessage(enrichAssistantMessage(message.content), message.role, false, false);
302                }
303            }
304            if (typeof res.data.settings.input_placeholder === 'string' && chatBotForm) {
305                const textArea = chatBotForm.querySelector('textarea[name="sg-input"]');
306                if (textArea) {
307                    textArea.setAttribute('placeholder', res.data.settings.input_placeholder);
308                }
309            }
310            if (chatBotForm) {
311                if (typeof res.data.settings.error_connection === 'string' && res.data.settings.error_connection.length) {
312                    chatBotForm.setAttribute('data-error-connection-msg', res.data.settings.error_connection);
313                }
314                if (typeof res.data.settings.error_rate_limit === 'string' && res.data.settings.error_rate_limit.length) {
315                    chatBotForm.setAttribute('data-error-rate-limit-msg', res.data.settings.error_rate_limit);
316                }
317                if (typeof res.data.settings.error_generic === 'string' && res.data.settings.error_generic.length) {
318                    chatBotForm.setAttribute('data-error-generic-msg', res.data.settings.error_generic);
319                }
320            }
321            if (typeof res.data.settings.button_icon_url === 'string' && res.data.settings.button_icon_url.length) {
322                chatToggle.querySelector('#sg-chat-toggle img').setAttribute('src', res.data.settings.button_icon_url);
323            }
324            if (typeof res.data.settings.send_button_text === 'string' && res.data.settings.send_button_text.length) {
325                chatToggle.querySelector('#sg-chat-toggle img').setAttribute('src', res.data.settings.button_icon_url);
326            }
327
328            if (typeof res.data.settings.full_screen_button === 'string' && res.data.settings.full_screen_button.length && res.data.settings.full_screen_button == '0') {
329                document.querySelector('#sg-chat-expand').style.display = 'none';
330            }
331            if (typeof res.data.settings.new_window_button === 'string' && res.data.settings.new_window_button.length && res.data.settings.new_window_button == '0') {
332                document.querySelector('#sg-open-new-window').style.display = 'none';
333            }
334
335
336            if (typeof res.data.settings.cloudflare_turnstile_site === 'string' && res.data.settings.cloudflare_turnstile_site.length) {
337
338                renderTurnstile(res.data.settings.cloudflare_turnstile_site, 'login');
339
340                renderTurnstile(res.data.settings.cloudflare_turnstile_site, 'register');
341
342                renderTurnstile(res.data.settings.cloudflare_turnstile_site, 'lostpassword');
343
344                if (typeof res.data.settings.require_logged_in === 'string' && res.data.settings.require_logged_in === '0') {
345
346                    renderTurnstile(res.data.settings.cloudflare_turnstile_site, 'chat');
347                    chatRequiresTurnstile = true;
348                    // rimozione turnstile registrazione e login
349                    hideTurnstile('login');
350                    hideTurnstile('register');
351                } else {
352                    // rimozione turnstile chat
353                    hideTurnstile('chat');
354                }
355
356            }
357
358            const signinHeading = document.querySelector('#sg-signin-header');
359            if (signinHeading && typeof res.data.settings.signin_message === 'string' && res.data.settings.signin_message.length) {
360                signinHeading.innerHTML = res.data.settings.signin_message;
361            }
362            const signupHeading = document.querySelector('#sg-signup-header');
363            if (signupHeading && typeof res.data.settings.signup_message === 'string' && res.data.settings.signup_message.length) {
364                signupHeading.innerHTML = res.data.settings.signup_message;
365            }
366            chatToggle.style.display = 'block';
367            chatShell.classList.add('sg-ready');
368
369
370        })
371        .catch(resetOauthProcess);
372}
373
374// Mappa URL corsi/provincia per arricchimento automatico delle risposte
375const courseUrlMap = {
376    'cav_udine': 'https://aisfvg.it/prodotto/avvicinamento-al-vino-udine-settembre-2026/',
377    'cav_trieste': 'https://aisfvg.it/prodotto/avvicinamento-al-vino-trieste-settembre-2026/',
378    'cav_cormons': 'https://aisfvg.it/prodotto/avvicinamento-al-vino-cormons-ottobre-2026/',
379    'cav_pordenone': 'https://aisfvg.it/prodotto/avvicinamento-al-vino-pordenone-febbraio-2027/',
380    'birra_pordenone': 'https://aisfvg.it/prodotto/avvicinamento-alla-birra-trieste-novembre-2026/',
381};
382
383function detectCourseAndProvince(text) {
384    const lower = text.toLowerCase();
385    let course = null;
386    let province = null;
387
388    if (lower.includes('cav') || lower.includes('avvicinamento al vino')) course = 'cav';
389    else if (lower.includes('primo livello')) course = 'primo';
390    else if (lower.includes('secondo livello')) course = 'secondo';
391    else if (lower.includes('terzo livello')) course = 'terzo';
392    else if (lower.includes('birra')) course = 'birra';
393
394    const provinces = ['udine', 'trieste', 'gorizia', 'pordenone', 'cormons'];
395    for (const p of provinces) {
396        if (lower.includes(p)) {
397            province = p;
398            break;
399        }
400    }
401
402    return { course, province };
403}
404
405function enrichAssistantMessage(text) {
406    const { course, province } = detectCourseAndProvince(text);
407    if (!course || !province) return text;
408
409    const key = `${course}_${province}`;
410    const url = courseUrlMap[key];
411    if (!url) return text;
412
413    const ctaReplacement = `Per maggiori dettagli, [visita la pagina del corso](${url}).`;
414
415    // Trova l'inizio di qualsiasi CTA che inviti a contattare e rimuovi tutto da li in poi
416    const contactCtaStartRegex = /Per iscriverti o per (ulteriori|maggiori) informazioni,?/gi;
417    const contactMatch = contactCtaStartRegex.exec(text);
418    if (contactMatch) {
419        text = text.slice(0, contactMatch.index).trim();
420    }
421
422    // Rimuovi domande di chiusura alla fine
423    text = text.replace(/\n\n(Se hai altre domande[^?\n]*\?|Hai bisogno di [^?\n]*\?|Posso aiutarti[^?\n]*\?|Hai altre domande[^.\n]*\.)\s*$/gi, '');
424
425    // Aggiungi CTA pulita se manca
426    if (!text.includes(url)) {
427        text += `\n\n` + ctaReplacement;
428    }
429
430    return text;
431}
432
433function handleChatResponse(response) {
434    if (response.success) {
435        const message = enrichAssistantMessage(response.data.message);
436        if(document.body.classList.contains('sg-chatbot-admin')) {
437            console.log('====== DEBUG SG AI CHATBOT ======', response.data.debug);
438        }
439        addMessage(message, 'assistant', true);
440    } else {
441        addMessage('❌ ' + response.error, 'error');
442    }
443}
444
445function addMessage(text, role, allowHtml = false, animation = true) {
446    const $messages = document.querySelector('#sg-messages');
447    if (!$messages) return;
448
449    const timestamp = new Date().toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
450    const formattedText = allowHtml ? formatMessageWithHtml(text) : formatMessage(text);
451
452    const msgEl = document.createElement('div');
453    msgEl.classList.add('sg-message', 'sg-' + role);
454
455    const contentEl = document.createElement('div');
456    contentEl.classList.add('sg-message-content');
457    contentEl.innerHTML = '';
458
459    let interval, current = 0;
460    const charsPerTick = 3;
461
462    if (animation) {
463        animate({
464            timing: x => -0.5 * (Math.cos(Math.PI * x) - 1),
465            duration: role === 'assistant' ? text.length * 12 : 1,
466            draw: function (progress) {
467                if (progress < 0) {
468                    progress = 0;
469                }
470                const partialLength = Math.floor(text.length * progress);
471                const partialText = text.slice(0, partialLength);
472
473                contentEl.innerHTML = allowHtml ? formatMessageWithHtml(partialText) : formatMessage(partialText);
474            },
475            callback: function () {
476                contentEl.innerHTML = allowHtml ? formatMessageWithHtml(text) : formatMessage(text);
477                if (role === 'assistant') {
478                    sendBtn.removeAttribute('disabled');
479                }
480            }
481
482        })
483    } else {
484        contentEl.innerHTML = allowHtml ? formatMessageWithHtml(text) : formatMessage(text);
485    }
486
487
488    const timeEl = document.createElement('div');
489    timeEl.classList.add('sg-message-time');
490    timeEl.textContent = timestamp;
491
492    msgEl.appendChild(contentEl);
493    msgEl.appendChild(timeEl);
494
495    if (role === 'assistant') {
496        const copyBtn = document.createElement('button');
497        copyBtn.classList.add('sg-copy');
498        copyBtn.title = 'Copia';
499        copyBtn.textContent = '📋';
500        msgEl.appendChild(copyBtn);
501    }
502
503    $messages.appendChild(msgEl);
504
505    if (role === 'assistant' && (text.includes('\\[') || text.includes('\\('))) {
506        renderMath(msgEl);
507    }
508
509    scrollToBottom();
510
511    if (role === 'assistant') {
512        msgEl.querySelector('.sg-copy').addEventListener('click', function () {
513            navigator.clipboard.writeText(text);
514            copyBtn.textContent = '✅';
515            setTimeout(() => { copyBtn.textContent = '📋'; }, 2000);
516        });
517    }
518
519    function animate({ timing, draw, duration, callback = function () { } }) {
520        let start = performance.now();
521
522        requestAnimationFrame(function animate(time) {
523            let timeFraction = (time - start) / duration;
524            if (timeFraction > 1) timeFraction = 1;
525
526            let progress = timing(timeFraction);
527
528            draw(progress);
529
530            if (timeFraction < 1) {
531                requestAnimationFrame(animate);
532            } else {
533                callback();
534            }
535        });
536    }
537}
538
539function renderMath(element) {
540    if (typeof renderMathInElement !== 'undefined') {
541        renderMathInElement(element, {
542            delimiters: [
543                { left: '\\[', right: '\\]', display: true },
544                { left: '\\(', right: '\\)', display: false }
545            ],
546            throwOnError: false
547        });
548    }
549}
550
551function showTyping() {
552    const $messages = document.querySelector('#sg-messages');
553    if (!$messages || document.getElementById('sg-typing')) return;
554
555    const typingEl = document.createElement('div');
556    typingEl.id = 'sg-typing';
557    typingEl.classList.add('sg-message', 'sg-assistant');
558    typingEl.innerHTML = `
559        <div class="sg-message-content sg-typing-indicator">
560            <span></span><span></span><span></span>
561        </div>
562    `;
563    $messages.appendChild(typingEl);
564    scrollToBottom();
565}
566
567function hideTyping() {
568    const el = document.getElementById('sg-typing');
569    if (el) el.remove();
570}
571
572function scrollToBottom() {
573    const $messages = document.querySelector('#sg-messages');
574    if ($messages) $messages.scrollTop = $messages.scrollHeight;
575}
576
577function formatMessage(text) {
578    const escapeHtml = (unsafe) => unsafe
579        .replace(/&/g, '&amp;')
580        .replace(/</g, '&lt;')
581        .replace(/>/g, '&gt;')
582        .replace(/"/g, '&quot;')
583        .replace(/'/g, '&#039;');
584
585    const latexBlocks = [];
586    text = text.replace(/\\\[([\s\S]*?)\\\]/g, (match, formula) => {
587        latexBlocks.push(formula);
588        return `__LATEX_BLOCK_${latexBlocks.length - 1}__`;
589    });
590
591    const latexInline = [];
592    text = text.replace(/\\\((.*?)\\\)/g, (match, formula) => {
593        latexInline.push(formula);
594        return `__LATEX_INLINE_${latexInline.length - 1}__`;
595    });
596
597    text = escapeHtml(text);
598    text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
599    text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
600    text = text.replace(/\n/g, '<br>');
601    text = text.replace(/(https?:\/\/[^\s\)<>,]+)/g, (match) => {
602        const url = match.replace(/[.,;:!?]+$/, '');
603        return `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`;
604    });
605
606    text = text.replace(/__LATEX_BLOCK_(\d+)__/g, (match, index) =>
607        `<div class="math-block">\\[${latexBlocks[index]}\\]</div>`);
608    text = text.replace(/__LATEX_INLINE_(\d+)__/g, (match, index) =>
609        `<span class="math-inline">\\(${latexInline[index]}\\)</span>`);
610
611    return text;
612}
613
614function formatMessageWithHtml(text) {
615    const htmlTags = [];
616    text = text.replace(/(<\/?[a-zA-Z][^>]*>)/g, (match) => {
617        htmlTags.push(match);
618        return `__HTML_TAG_${htmlTags.length - 1}__`;
619    });
620
621    text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
622
623    // Converti link Markdown [testo](url) in tag <a>
624    text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
625
626    // Formattazione inline
627    text = text.replace(/\*\*([^*]+?)\*\*/g, '<strong>$1</strong>');
628    text = text.replace(/(?<!\*)\*([^*\n]+?)\*(?!\*)/g, '<em>$1</em>');
629    text = text.replace(/`([^`]+?)`/g, '<code>$1</code>');
630
631    // Converti heading Markdown inline in tag <hN>, anche se preceduti da tag HTML preservati
632    text = text.replace(/(^|\n|\s*__HTML_TAG_\d+__\s*)(#{1,6})\s+([^\n<]+?)(?=\n|\s*__HTML_TAG_\d+__\s*|$)/gm, (match, sep, hashes, content) => {
633        const level = hashes.length;
634        return `${sep || ''}<h${level}>${content.trim()}</h${level}>`;
635    });
636
637    // Sostituisci i placeholder dei tag block-level piu comuni con marker semantici
638    // per poterli usare come separatori durante il parsing
639    const pOpenMarker = '___P_OPEN___';
640    const pCloseMarker = '___P_CLOSE___';
641    const brMarker = '___BR___';
642    text = text.replace(/__HTML_TAG_(\d+)__/g, (match, index) => {
643        const tag = htmlTags[index];
644        if (tag === '<p>') return pOpenMarker;
645        if (tag === '</p>') return pCloseMarker;
646        if (tag === '<br>' || tag === '<br/>' || tag === '<br />') return brMarker;
647        return match;
648    });
649
650    // Separa blocchi per doppie newline o marker di apertura/chiusura paragrafo
651    const blocks = text.split(new RegExp(`\n\s*\n|${pOpenMarker}|${pCloseMarker}`));
652    let html = '';
653
654    for (let block of blocks) {
655        if (!block.trim()) continue;
656
657        // Separa un heading iniziale (anche se preceduto da br/placeholder) dal resto del blocco
658        const headingMatch = block.match(new RegExp(`^(.*?)((?:\s*(?:__HTML_TAG_\d+__|${brMarker})\s*)*<h[1-6]>.*?<\/h[1-6]>(?:\s*(?:__HTML_TAG_\d+__|${brMarker})\s*)*)(.*)$`, 's'));
659        if (headingMatch) {
660            const before = headingMatch[1].trim();
661            const heading = headingMatch[2].trim();
662            const after = headingMatch[3].trim();
663
664            // Se c'e' testo prima dell'heading, wrappalo in un paragrafo
665            if (before && !new RegExp(`^(?:__HTML_TAG_\d+__|${brMarker}\s*)+$`).test(before)) {
666                html += processSimpleBlock(before);
667            }
668            html += heading;
669            if (!after) continue;
670            block = after;
671        }
672
673        html += processSimpleBlock(block);
674    }
675
676    // Ripristina marker semantici in tag HTML originali
677    html = html.replace(new RegExp(pOpenMarker, 'g'), '<p>');
678    html = html.replace(new RegExp(pCloseMarker, 'g'), '</p>');
679    html = html.replace(new RegExp(brMarker, 'g'), '<br>');
680
681    // Ripristina altri tag HTML preservati
682    html = html.replace(/__HTML_TAG_(\d+)__/g, (match, index) => htmlTags[index]);
683
684    // Pulizia
685    html = html.replace(/(<br>){3,}/g, '<br><br>');
686    html = html.replace(/<p>\s*<\/p>/g, '');
687    html = html.replace(/<p>(\s*<h[1-6]>.*?<\/h[1-6]>\s*)<\/p>/g, '$1');
688    html = html.replace(/<\/h([1-6])>\s*<br>/g, '</h$1>');
689    html = html.replace(/<p>\s*(<ul class="sg-list">[\s\S]*?<\/ul>)\s*<\/p>/g, '$1');
690    html = html.replace(/<p>\s*<br>/g, '<p>');
691
692    return html;
693
694    function processSimpleBlock(block) {
695        const markerRegex = new RegExp(`^(\s*(?:__HTML_TAG_\d+__|${brMarker})\s*)+`);
696        const lines = block.split('\n')
697            .flatMap(line => line.split(new RegExp(brMarker)))
698            .map(line => line.replace(markerRegex, ''))
699            .filter(line => line.trim());
700        if (!lines.length) return '';
701
702        const firstLine = lines[0].trim();
703        const isBullet = /^[-\u2022*]\s/.test(firstLine);
704        const isIndentedList = lines.length > 1 && lines.every(line => /^\s+/.test(line.replace(markerRegex, '')));
705
706        if (isBullet || isIndentedList) {
707            let listHtml = `<ul class="sg-list">`;
708            for (let line of lines) {
709                const content = line.replace(markerRegex, '').trim()
710                    .replace(/^(\d+\.\s*|[-\u2022*]\s*)/, '');
711                listHtml += `<li>${content}</li>`;
712            }
713            listHtml += `</ul>`;
714            return listHtml;
715        } else {
716            const content = block.split('\n').flatMap(line => line.split(new RegExp(brMarker))).map(line => line.trim()).join('<br>');
717            return `<span class="block">${content}</span>`;
718        }
719    }
720}

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.