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, '&') 580 .replace(/</g, '<') 581 .replace(/>/g, '>') 582 .replace(/"/g, '"') 583 .replace(/'/g, '''); 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.