1/** 2 * Brainbox Asystent AI - Frontend JavaScript (Vanilla JS) 3 */ 4 5// Zapobiegaj redeklaracji - sprawdź czy już istnieje 6if (typeof window.bbai_CHAT_DEBUG_ENABLED === 'undefined') { 7 window.bbai_CHAT_DEBUG_ENABLED = false; 8 9 try { 10 const debugOverride = 11 typeof window !== "undefined" && 12 window.localStorage && 13 window.localStorage.getItem("bb_asystent_ai_debug") === "1"; 14 15 if ( 16 debugOverride && 17 typeof BBAsystentAI !== "undefined" && 18 !!BBAsystentAI.debug 19 ) { 20 window.bbai_CHAT_DEBUG_ENABLED = true; 21 } 22 } catch (error) { 23 // Ignore storage access issues silently 24 } 25} 26 27// Użyj zmiennej globalnej dla kompatybilnoÅci wstecznej 28// Zapobiegaj redeklaracji ORIGINAL_CONSOLE 29if (typeof window.ORIGINAL_CONSOLE === 'undefined') { 30 window.ORIGINAL_CONSOLE = 31 typeof window !== "undefined" && window.console 32 ? { 33 log: window.console.log.bind(window.console), 34 warn: window.console.warn.bind(window.console), 35 error: window.console.error.bind(window.console), 36 } 37 : { log: () => {}, warn: () => {}, error: () => {} }; 38} 39// Użyj zmiennych globalnych bezpoÅrednio, aby uniknÄ Ä redeklaracji 40// Nie deklaruj lokalnych zmiennych const - używaj bezpoÅrednio window.* 41 42(function () { 43 "use strict"; 44 45 // Zachowaj dane z wp_localize_script zanim nadpiszemy global przez instancjÄ klasy 46 const bootstrapData = { 47 ...(typeof window !== "undefined" && window.BBAsystentAI 48 ? window.BBAsystentAI 49 : {}), 50 }; 51 52 // Fallbacki gdyby inline dane nie zostaÅy wstrzykniÄte (np. blokada przez consent) 53 const fallbackAjaxUrl = 54 (typeof window !== "undefined" && window.ajaxurl) || 55 (typeof window !== "undefined" && 56 window.wp && 57 window.wp.ajax && 58 window.wp.ajax.settings && 59 window.wp.ajax.settings.url) || 60 ""; 61 if (!bootstrapData.ajaxUrl && fallbackAjaxUrl) { 62 bootstrapData.ajaxUrl = fallbackAjaxUrl; 63 } 64 65 if (!bootstrapData.ajaxUrl && typeof window !== "undefined") { 66 const origin = window.location.origin || ""; 67 bootstrapData.ajaxUrl = `${origin}/wp-admin/admin-ajax.php`; 68 } 69 70 const fallbackRestUrl = 71 typeof window !== "undefined" && 72 window.wpApiSettings && 73 window.wpApiSettings.root 74 ? `${window.wpApiSettings.root}bb-asystent-ai/v1/` 75 : ""; 76 if (!bootstrapData.restUrl && fallbackRestUrl) { 77 bootstrapData.restUrl = fallbackRestUrl; 78 } 79 80 if (!bootstrapData.restUrl && typeof window !== "undefined") { 81 const origin = window.location.origin || ""; 82 bootstrapData.restUrl = `${origin}/wp-json/bb-asystent-ai/v1/`; 83 } 84 85 if (!bootstrapData.ajaxUrl) { 86 console.error( 87 "BB Asystent AI: brak danych bootstrap (ajaxUrl) â użyjemy jeszcze window.ajaxurl lub dodaj inline wp_localize_script.", 88 ); 89 } 90 91 // Debug funkcje: domyÅlnie wyciszone, można wÅÄ czyÄ przez localStorage bb_asystent_ai_debug=1 92 // (wymaga też WP_DEBUG po stronie backendu, bo tylko wtedy bootstrapData.debug jest true). 93 const debugLog = (...args) => { 94 if ( 95 window.bbai_CHAT_DEBUG_ENABLED && 96 window.ORIGINAL_CONSOLE && 97 window.ORIGINAL_CONSOLE.log 98 ) { 99 window.ORIGINAL_CONSOLE.log(...args); 100 } 101 }; 102 const debugWarn = (...args) => { 103 if ( 104 window.bbai_CHAT_DEBUG_ENABLED && 105 window.ORIGINAL_CONSOLE && 106 window.ORIGINAL_CONSOLE.warn 107 ) { 108 window.ORIGINAL_CONSOLE.warn(...args); 109 } 110 }; 111 const debugError = (...args) => { 112 // Tylko bÅÄdy sÄ logowane - użyj bezpoÅrednio window.ORIGINAL_CONSOLE 113 if (window.ORIGINAL_CONSOLE && window.ORIGINAL_CONSOLE.error) { 114 window.ORIGINAL_CONSOLE.error(...args); 115 } 116 }; 117 118 const getCurrentOrigin = () => { 119 if (typeof window === "undefined" || !window.location) { 120 return ""; 121 } 122 return window.location.origin || ""; 123 }; 124 125 const isHttpHref = (href) => /^https?:\/\//i.test(href) || /^www\./i.test(href); 126 127 const normalizeComparableHref = (href) => { 128 const value = (href || "").toString().trim(); 129 if (/^www\./i.test(value)) { 130 return `https://${value}`; 131 } 132 return value; 133 }; 134 135 const isSameSiteHref = (href) => { 136 const value = normalizeComparableHref(href); 137 if (!value || !isHttpHref(value)) { 138 return true; 139 } 140 141 try { 142 const url = new URL(value, getCurrentOrigin()); 143 return url.origin === getCurrentOrigin(); 144 } catch (error) { 145 return false;
146 } 147 }; 148 149 const applyLinkTargetPolicy = (link, href) => { 150 if (!link) { 151 return; 152 } 153 154 if (isHttpHref(href) && !isSameSiteHref(href)) { 155 link.setAttribute("target", "_blank"); 156 link.setAttribute("rel", "noopener noreferrer nofollow ugc"); 157 return; 158 } 159 160 link.removeAttribute("target"); 161 link.removeAttribute("rel"); 162 }; 163 164 /** 165 * Debounce - opóźnia wykonanie funkcji do momentu gdy nie ma nowych wywoÅaÅ przez okreÅlony czas 166 * @param {Function} func - funkcja do debounce'owania 167 * @param {number} wait - czas oczekiwania w ms 168 * @returns {Function} - debounced funkcja 169 */ 170 const debounce = (func, wait) => { 171 let timeout; 172 return function executedFunction(...args) { 173 const later = () => { 174 clearTimeout(timeout); 175 func.apply(this, args); 176 }; 177 clearTimeout(timeout); 178 timeout = setTimeout(later, wait); 179 }; 180 }; 181 182 /** 183 * Throttle - ogranicza czÄstotliwoÅÄ wywoÅaÅ funkcji do max raz na okreÅlony czas 184 * @param {Function} func - funkcja do throttle'owania 185 * @param {number} limit - minimalny czas miÄdzy wywoÅaniami w ms 186 * @returns {Function} - throttled funkcja 187 */ 188 const throttle = (func, limit) => { 189 let inThrottle = false; 190 let lastArgs = null; 191 return function executedFunction(...args) { 192 if (!inThrottle) { 193 func.apply(this, args); 194 inThrottle = true; 195 setTimeout(() => { 196 inThrottle = false; 197 if (lastArgs) { 198 func.apply(this, lastArgs); 199 lastArgs = null; 200 } 201 }, limit); 202 } else { 203 lastArgs = args; 204 } 205 }; 206 }; 207 208 class MarkdownFormatter { 209 constructor(markedInstance, escapeFallback) { 210 const candidate = 211 markedInstance || 212 (typeof window !== "undefined" ? window.marked : null); 213 this.marked = candidate || null; 214 this.escapeFallback = 215 typeof escapeFallback === "function" 216 ? escapeFallback 217 : (value) => (value ?? "").toString(); 218 this.isConfigured = false; 219 220 if (this.marked) { 221 this.configure(); 222 } else { 223 debugWarn( 224 "BB Asystent AI: marked.js not available, using plain text renderer", 225 ); 226 this.isConfigured = true; 227 } 228 } 229 230 configure() { 231 if (!this.marked) { 232 return; 233 } 234 235 const escapeAttr = (value) => { 236 if (value === null || value === undefined) { 237 return ""; 238 } 239 return String(value) 240 .replace(/&/g, "&") 241 .replace(/</g, "<") 242 .replace(/>/g, ">") 243 .replace(/"/g, """) 244 .replace(/'/g, "'"); 245 }; 246 247 const unwrapMarkdownHref = (href) => { 248 let value = (href || "").toString().trim(); 249 // Some models output link destinations as nested Markdown: [url](url) 250 // Keep unwrapping a few levels to land on the real destination. 251 for (let i = 0; i < 3; i++) { 252 const match = value.match(/^\[([^\]]+)\]\(([^)]+)\)$/); 253 if (!match) { 254 break; 255 } 256 value = (match[2] || "").trim(); 257 } 258 259 // Remove surrounding angle brackets: <https://example.com> 260 const angle = value.match(/^<([^>]+)>$/); 261 if (angle) { 262 value = (angle[1] || "").trim(); 263 } 264 265 return value; 266 }; 267 268 const normalizeHref = (href) => { 269 let value = unwrapMarkdownHref(href); 270 if (!value) { 271 return ""; 272 } 273 if (/^www\./i.test(value)) { 274 value = `https://${value}`; 275 } 276 return value; 277 }; 278 279 // Custom renderer for readable HTML output 280 const renderer = new this.marked.Renderer(); 281 282 renderer.link = function (token) { 283 const href = normalizeHref(token.href || ""); 284 const title = token.title; 285 const text = this.parser.parseInline(token.tokens); 286 const isPhone = href.startsWith("tel:"); 287 const isEmail = href.startsWith("mailto:"); 288 const isAnchor = href.startsWith("#"); 289 const isExternal = !isPhone && !isEmail && !isAnchor && isHttpHref(href) && !isSameSiteHref(href); 290 const icon = isPhone ? "" : isEmail ? "" : ""; 291 const classes = isPhone 292 ? "fac-link phone-link" 293 : isEmail 294 ? "fac-link email-link" 295 : "fac-link external-link"; 296 const titleAttr = title ? ` title="${escapeAttr(title)}"` : ""; 297 const targetAttr = isExternal ? ` target="_blank" rel="noopener noreferrer nofollow ugc"` : ""; 298 const safeHref = escapeAttr(href); 299 return `<a href="${safeHref}" class="${classes}"${titleAttr}${targetAttr}>${icon}${text}</a>`; 300 }; 301 302 renderer.paragraph = function (token) { 303 const text = this.parser.parseInline(token.tokens); 304 return `<p class="markdown-paragraph">${text}</p>\n`; 305 }; 306 307 renderer.heading = function (token) { 308 const text = this.parser.parseInline(token.tokens); 309 const level = token.depth; 310 return `<h${level} class="markdown-heading markdown-h${level}">${text}</h${level}>\n`; 311 }; 312 313 renderer.strong = function (token) { 314 const text = this.parser.parseInline(token.tokens); 315 return `<strong class="markdown-bold">${text}</strong>`; 316 }; 317 318 renderer.em = function (token) { 319 const text = this.parser.parseInline(token.tokens); 320 return `<em class="markdown-italic">${text}</em>`; 321 }; 322 323 renderer.blockquote = function (token) { 324 const quote = this.parser.parse(token.tokens); 325 return `<blockquote class="markdown-quote">${quote}</blockquote>\n`; 326 }; 327 328 renderer.code = function (token) { 329 const code = token.text; 330 const language = token.lang; 331 const lang = language ? ` data-language="${language}"` : ""; 332 return `<pre class="markdown-code-block"${lang}><code>${code}</code></pre>\n`; 333 }; 334 335 renderer.codespan = function (token) { 336 const code = token.text; 337 return `<code class="markdown-inline-code">${code}</code>`; 338 }; 339 340 renderer.hr = function () { 341 return '<hr class="markdown-separator" />\n'; 342 }; 343 344 renderer.table = function (token) { 345 let header = ""; 346 let body = ""; 347 348 for (const cell of token.header) { 349 header += this.tablecell(cell); 350 } 351 header = `<tr>${header}</tr>`; 352 353 for (const row of token.rows) { 354 let rowContent = ""; 355 for (const cell of row) { 356 rowContent += this.tablecell(cell); 357 } 358 body += `<tr>${rowContent}</tr>`; 359 } 360 361 return `<table class="markdown-table"> 362 <thead>${header}</thead> 363 <tbody>${body}
363</tbody> 364 </table>\n`; 365 }; 366 367 // Custom renderer for images - responsywny grid layout dla wielu obrazów 368 renderer.image = function (token) { 369 const href = token.href || ""; 370 const title = token.title || ""; 371 const text = token.text || ""; 372 373 // Escape HTML attributes 374 const escapeHtml = (str) => { 375 if (!str) return ""; 376 return String(str) 377 .replace(/&/g, "&") 378 .replace(/</g, "<") 379 .replace(/>/g, ">") 380 .replace(/"/g, """) 381 .replace(/'/g, "'"); 382 }; 383 384 const titleAttr = title ? ` title="${escapeHtml(title)}"` : ""; 385 const altAttr = text ? ` alt="${escapeHtml(text)}"` : ""; 386 const srcAttr = escapeHtml(href); 387 388 return `<img src="${srcAttr}"${altAttr}${titleAttr} class="markdown-image" style="max-width: 100%; height: auto; border-radius: 8px; margin: 0.5rem 0;" />`; 389 }; 390 391 this.marked.use({ 392 renderer: renderer, 393 breaks: false, 394 gfm: true, 395 headerIds: false, 396 mangle: false, 397 pedantic: false, 398 smartLists: true, 399 smartypants: true, 400 }); 401 402 this.isConfigured = true; 403 debugLog("FUNERAL DEBUG: MarkdownFormatter configured marked.js"); 404 } 405 406 render(raw) { 407 let text = raw; 408 if (typeof text !== "string") { 409 if (text === null || text === undefined) { 410 return ""; 411 } 412 text = String(text); 413 } 414 415 if (!this.marked) { 416 return this.escapeFallback(text).replace(/\n/g, "<br>"); 417 } 418 419 if (!this.isConfigured) { 420 this.configure(); 421 } 422 423 try { 424 let normalized = this.normalizeStructure(text); 425 426 const hasHtmlTags = 427 /<(p|br|a|div|span|strong|em|ul|ol|li|h[1-6])[>\s]/i.test( 428 text, 429 ); 430 431 432 if (hasHtmlTags) { 433 return text; 434 } 435 436 normalized = this.normalizeContactShortcodes(normalized); 437 normalized = this.normalizeMarkdownLinks(normalized); 438 normalized = this.applyMarkdownAutolinks(normalized); 439 440 const html = this.marked.parse(normalized); 441 442 return this.decorateLists(html); 443 } catch (error) { 444 debugError("Markdown render failed:", error); 445 return this.escapeFallback(text).replace(/\n/g, "<br>"); 446 } 447 } 448 449 normalizeMarkdownLinks(text) { 450 if (typeof text !== "string" || text === "") { 451 return text; 452 } 453 let output = text; 454 455 // Fix nested destinations like: [Kremacje]([https://a.pl](https://a.pl)) 456 // => [Kremacje](https://a.pl) 457 output = output.replace( 458 /\[([^\]]+)\]\(\s*\[([^\]]+)\]\(\s*(https?:\/\/[^)\s]+)\s*\)\s*\)/gi, 459 (match, label, _innerLabel, href) => `[${label}](${href})`, 460 ); 461 462 // Fix angle-bracket destinations: [text](<https://example.com>) 463 output = output.replace( 464 /\[([^\]]+)\]\(\s*<\s*(https?:\/\/[^>\s]+)\s*>\s*\)/gi, 465 (match, label, href) => `[${label}](${href})`, 466 ); 467 468 return output; 469 } 470 471 normalizeContactShortcodes(text) { 472 if (typeof text !== "string" || text === "") { 473 return text; 474 } 475 476 const formatPhoneDisplay = (tel) => { 477 const digits = (tel || "").replace(/\D/g, ""); 478 if (digits.length === 11 && digits.startsWith("48")) { 479 return `+48 ${digits.slice(2, 5)} ${digits.slice(5, 8)} ${digits.slice(8)}`; 480 } 481 if (digits.length === 9) { 482 return `+48 ${digits.slice(0, 3)} ${digits.slice(3, 6)} ${digits.slice(6)}`; 483 } 484 return tel; 485 }; 486 487 const extractPhone = (value) => { 488 const plain = String(value || "") 489 .replace(/<[^>]+>/g, " ") 490 .replace(/[âð]/g, " ") 491 .replace(/ /gi, " "); 492 const matches = plain.match(/(?:\+?\s*48[\s-]?)?\d(?:[\s-]?\d){8,10}/g) || []; 493 for (const candidate of matches) { 494 const digits = candidate.replace(/\D/g, ""); 495 if (digits.length === 9) { 496 return `+48${digits}`; 497 } 498 if (digits.length === 11 && digits.startsWith("48")) { 499 return `+${digits}`; 500 } 501 if (digits.length >= 10 && candidate.trim().startsWith("+")) { 502 return `+${digits}`; 503 } 504 } 505 506 return ""; 507 }; 508 509 const extractEmail = (value) => { 510 const match = String(value || "").match(/\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/); 511 return match ? match[0] : ""; 512 }; 513 514 const extractUrl = (value) => { 515 const match = String(value || "").match(/\b(?:https?:\/\/|www\.)[^\s<>)]+/i); 516 if (!match) { 517 return ""; 518 } 519 return /^www\./i.test(match[0]) ? `https://${match[0]}` : match[0]; 520 }; 521 522 return text.replace(/\{\{\s*(tel|phone|mail|email|url|link)\s*:\s*([\s\S]*?)\s*\}\}/gi, (match, type, value) => {
523 const key = String(type || "").toLowerCase(); 524 525 if (key === "tel" || key === "phone") { 526 const tel = extractPhone(value); 527 return tel ? `[${formatPhoneDisplay(tel)}](tel:${tel})` : String(value || "").trim(); 528 } 529 530 if (key === "mail" || key === "email") { 531 const email = extractEmail(value); 532 return email ? `[${email}](mailto:${email})` : String(value || "").trim(); 533 } 534 535 const url = extractUrl(value); 536 return url ? `[${url}](${url})` : String(value || "").trim(); 537 }); 538 } 539 540 normalizeStructure(text) { 541 let normalized = text.replace(/\r\n/g, "\n"); 542 543 normalized = normalized.replace(/:(?=\s*\d+\.\s)/g, ":\n\n"); 544 545 normalized = normalized.replace( 546 /(\d+\.\s[^\n]*?)(?=\s*\d+\.\s|$)/g, 547 (item) => `${item.trimEnd()}\n`, 548 ); 549 550 normalized = normalized.replace(/([^\n])###/g, "$1\n\n###"); 551 normalized = normalized.replace(/\n{3,}/g, "\n\n"); 552 553 normalized = normalized.replace( 554 /([.!?])(\s*)(?=[A-ZÅÅŻŹÄÄÄÃ])/g, 555 (match, punct) => `${punct}\n`, 556 ); 557 558 return normalized; 559 } 560 561 applyMarkdownAutolinks(text) { 562 if (typeof text !== "string" || text === "") { 563 return text; 564 } 565 566 let output = text; 567 const hasExistingPhoneLinks = 568 /href\s*=\s*(["']|")tel:/i.test(output) || 569 /\]\(tel:/i.test(output) || 570 /\bphone-link\b/i.test(output) || 571 /\bfac-link\b/i.test(output); 572 const placeholders = []; 573 const protect = (pattern) => { 574 output = output.replace(pattern, (match) => { 575 const token = `@@BB_AI_TOKEN_${placeholders.length}@@`; 576 placeholders.push(match); 577 return token; 578 }); 579 }; 580 581 // Protect code blocks, inline code and existing Markdown/HTML. 582 protect(/```[\s\S]*?```/g); 583 protect(/`[^`\n]+`/g); 584 protect(/!\[[^\]]*]\([^)]+\)/g); 585 protect(/\[[^\]]+]\([^)]+\)/g); 586 protect(/<[^>\n]+>/g); 587 protect(/href\s*=\s*(["'])tel:[^"']+\1/gi); 588 protect(/href\s*=\s*"tel:[^&]+"/gi); 589 590 // Convert bare image URLs on their own line into Markdown images. 591 output = output.replace( 592 /(^|\n)(\s*)(https?:\/\/[^\s<]+?\.(?:png|jpe?g|gif|webp|svg))(?=\s*(\n|$))/gi, 593 (match, lineStart, indent, url) => 594 `${lineStart}${indent}`, 595 ); 596 protect(/!\[[^\]]*]\([^)]+\)/g); 597 598 // Convert bare URLs into Markdown links so they are clickable. 599 output = output.replace( 600 /(?:(https?:\/\/)|(?:www\.))([^\s<]+)/gi, 601 (match, scheme) => { 602 let display = match; 603 let trailing = ""; 604 while (/[),.!?]$/.test(display)) { 605 trailing = display.slice(-1) + trailing; 606 display = display.slice(0, -1); 607 } 608 const href = scheme ? display : `https://${display}`; 609 return `[${display}](${href})${trailing}`; 610 }, 611 ); 612 protect(/\[[^\]]+]\([^)]+\)/g); 613 614 // Convert emails into Markdown links. 615 output = output.replace( 616 /\b([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\b/g, 617 (match) => `[${match}](mailto:${match})`, 618 ); 619 protect(/\[[^\]]+]\([^)]+\)/g); 620 621 // Convert phone numbers into Markdown tel links. 622 const phonePatterns = [ 623 /\+48\s?\d{3}\s?\d{3}\s?\d{3}/g, 624 /\+48\s?\d{2}\s?\d{3}\s?\d{2}\s?\d{2}/g, 625 /\+48\d{9,10}/g, 626 /\b48\s?\d{9,10}\b/g, 627 /\b\d{3}\s?\d{3}\s?\d{3}\b/g, 628 /\b\d{9}\b/g, 629 ]; 630 631 if (!hasExistingPhoneLinks) { 632 phonePatterns.forEach((pattern) => { 633 const bracketed = new RegExp( 634 `\\[(${pattern.source})\\]`, 635 pattern.flags, 636 ); 637 output = output.replace(bracketed, (match, number) => { 638 const digits = number.replace(/[^\d+]/g, ""); 639 const tel = digits.startsWith("+") 640 ? digits 641 : digits.startsWith("48") 642 ? "+" + digits 643 : "+48" + digits; 644 return `[${number}](tel:${tel})`; 645 }); 646 }); 647 648 phonePatterns.forEach((pattern) => { 649 output = output.replace( 650 pattern, 651 (match, offset) => { 652 const beforeMatch = output.substring(0, offset); 653 const lastLt = beforeMatch.lastIndexOf("<"); 654 const lastGt = beforeMatch.lastIndexOf(">"); 655 if (lastLt > lastGt) { 656 return match; 657 } 658 const digits = match.replace(/[^\d+]/g, ""); 659 const tel = digits.startsWith("+") 660 ? digits 661 : digits.startsWith("48") 662 ? "+" + digits 663 : "+48" + digits; 664 return `[${match}](tel:${tel})`; 665 }, 666 ); 667 }); 668 } 669 670 // ZamieÅ "Zobacz na mapie" (i warianty) nastÄpujÄ ce po linii z adresem 671 // zawierajÄ cym polski kod pocztowy (XX-XXX) na link do Google Maps. 672 output = output.replace( 673 /([-*â¢]\s*)([^\n]*\d{2}-\d{3}\s+\S[^\n]*)\n(\s*[-*â¢]\s*)(zobacz na mapie|pokaż na mapie|sprawdź na mapie|mapa)(?
673=\s*\n|$)/gi, 674 (match, prefix1, address, prefix2, linkText) => { 675 const cleanAddress = address.replace(/[*_`[\]]/g, "").trim(); 676 const encoded = encodeURIComponent(cleanAddress + ", Polska"); 677 const url = `https://maps.google.com/maps?q=${encoded}`; 678 return `${prefix1}${address}\n${prefix2}[${linkText}](${url})`; 679 }, 680 ); 681 682 placeholders.forEach((value, index) => { 683 output = output.replace(`@@BB_AI_TOKEN_${index}@@`, value); 684 }); 685 686 return output; 687 } 688 689 decorateLists(html) { 690 try { 691 if (typeof html !== "string" || html === "") { 692 return html; 693 } 694 695 if (typeof document === "undefined") { 696 return html; 697 } 698 699 const container = document.createElement("div"); 700 container.innerHTML = html; 701 702 703 container.querySelectorAll("ol").forEach((node) => { 704 node.classList.add("markdown-list"); 705 }); 706 container.querySelectorAll("ul").forEach((node) => { 707 node.classList.add("markdown-list"); 708 }); 709 container.querySelectorAll("li").forEach((node) => { 710 node.classList.add("markdown-list-item"); 711 }); 712 713 // Grupuj obrazy w responsywny grid layout 714 this.groupImagesInGrid(container); 715 716 717 return container.innerHTML; 718 } catch (error) { 719 // W przypadku bÅÄdu, zwrÃ³Ä oryginalny HTML 720 if (typeof console !== "undefined" && console.error) { 721 console.error("BB Asystent AI: Error in decorateLists:", error); 722 } 723 return html; 724 } 725 } 726 727 groupImagesInGrid(container) { 728 // NAJPROSTSZE ROZWIÄZANIE: WyÅÄ cz grid layout, zostaw obrazy jak sÄ (jeden pod drugim) 729 // Obrazy sÄ już poprawnie renderowane przez marked.js, wiÄc nie trzeba ich grupowaÄ 730 // JeÅli jest wiÄcej obrazów, bÄdÄ wyÅwietlane jedno pod drugim (jak w markdown) 731 return; 732 733 /* WYÅÄCZONE - kod poniżej nie jest używany 734 try { 735 if (!container || typeof container.querySelectorAll !== "function") { 736 return; // Kontener nie jest poprawnym elementem DOM 737 } 738 739 // Znajdź wszystkie obrazy w kontenerze 740 const images = Array.from(container.querySelectorAll("img.markdown-image")); 741 742 if (images.length <= 1) { 743 return; // Nie grupuj, jeÅli jest tylko jeden obraz lub brak 744 } 745 746 // UPROSZCZONA LOGIKA: Zgrupuj wszystkie obrazy w kontenerze razem 747 // JeÅli sÄ w tym samym kontenerze (np. w tej samej wiadomoÅci), zgrupuj je wszystkie 748 const imageGroups = [images]; // Zawsze tworzymy jednÄ grupÄ ze wszystkich obrazów 749 750 751 // Utwórz grid dla każdej grupy 752 imageGroups.forEach((group, groupIndex) => { 753 754 // Utwórz kontener grid 755 const gridContainer = document.createElement("div"); 756 gridContainer.className = "markdown-images-grid"; 757 gridContainer.style.cssText = 758 "display: grid; " + 759 "grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); " + 760 "gap: 1rem; " + 761 "margin: 1rem 0; " + 762 "width: 100%;"; 763 764 // Znajdź wspólny kontener dla wszystkich obrazów (np. <ol>, <ul>, gÅówny kontener) 765 const firstImg = group[0]; 766 let commonContainer = container; 767 768 // Znajdź najwyższy wspólny kontener dla wszystkich obrazów 769 const findCommonAncestor = (elements) => { 770 try { 771 if (elements.length === 0) return null; 772 if (elements.length === 1) return elements[0]?.parentElement || null; 773 774 let ancestor = elements[0]?.parentElement; 775 while (ancestor && ancestor !== container) { 776 if (elements.every(el => el && ancestor.contains(el))) { 777 return ancestor; 778 } 779 ancestor = ancestor.parentElement; 780 } 781 return container; 782 } catch (e) { 783 // W przypadku bÅÄdu, zwrÃ³Ä gÅówny kontener 784 return container; 785 } 786 }; 787 788 const allParents = group.map(img => img.parentElement).filter(p => p); 789 commonContainer = findCommonAncestor(group) || container; 790 791 // Zbierz wszystkie obrazy do grid container 792 group.forEach((img) => { 793 const imgClone = img.cloneNode(true); 794 imgClone.style.cssText = 795 "max-width: 100%; " + 796 "height: auto; " + 797 "border-radius: 8px; " + 798 "object-fit: cover; " +
799 "cursor: pointer;"; 800 801 // Dodaj click handler do lightbox (jeÅli istnieje) 802 imgClone.addEventListener("click", function() { 803 if (window.bbAiLightbox && typeof window.bbAiLightbox.open === "function") { 804 window.bbAiLightbox.open(imgClone.src, imgClone.alt || ""); 805 } 806 }); 807 808 gridContainer.appendChild(imgClone); 809 }); 810 811 // WAÅ»NE: Zapisz referencjÄ PRZED usuniÄciem obrazów 812 const firstImgParent = firstImg.parentElement; 813 const firstImgGrandparent = firstImgParent?.parentElement; 814 const insertPosition = firstImgParent || firstImg; 815 816 // UsuÅ wszystkie oryginalne obrazy 817 const removedParents = new Set(); 818 group.forEach((img) => { 819 const parent = img.parentElement; 820 img.remove(); 821 // Zaznacz paragrafy do usuniÄcia (ale nie usuwaj jeszcze) 822 if (parent && parent.tagName === "P" && parent.children.length === 0) { 823 removedParents.add(parent); 824 } 825 }); 826 827 // UsuÅ puste paragrafy 828 removedParents.forEach(parent => { 829 if (parent && parent.parentElement) { 830 parent.remove(); 831 } 832 }); 833 834 // Wstaw grid w odpowiednim miejscu 835 // JeÅli pierwszy obraz byÅ w paragrafie, który zostaÅ usuniÄty, znajdź nastÄpny element 836 let insertTarget = null; 837 if (firstImgParent && firstImgParent.parentElement && !removedParents.has(firstImgParent)) { 838 // Paragraf nadal istnieje 839 insertTarget = firstImgParent; 840 } else if (firstImgGrandparent && firstImgGrandparent !== container) { 841 // Paragraf zostaÅ usuniÄty, znajdź nastÄpny element w dziadku 842 const children = Array.from(firstImgGrandparent.children); 843 const firstImgIndex = children.indexOf(firstImgParent); 844 if (firstImgIndex >= 0 && firstImgIndex < children.length - 1) { 845 insertTarget = children[firstImgIndex + 1]; 846 } else { 847 // Wstaw na koÅcu dziadka 848 insertTarget = null; // appendChild 849 } 850 } else { 851 // Fallback: wstaw na koÅcu kontenera 852 insertTarget = null; 853 } 854 855 // Wstaw grid PRZED usuniÄciem obrazów, aby mieÄ pewnoÅÄ, że referencja istnieje 856 let gridInserted = false; 857 if (firstImgParent && firstImgParent.parentElement) { 858 // Wstaw grid przed pierwszym obrazem (w tym samym paragrafie lub przed paragrafem) 859 firstImgParent.parentElement.insertBefore(gridContainer, firstImgParent); 860 gridInserted = true; 861 } else if (firstImgGrandparent && firstImgGrandparent !== container) { 862 // Wstaw do dziadka 863 firstImgGrandparent.insertBefore(gridContainer, firstImgParent || null); 864 gridInserted = true; 865 } else { 866 // Fallback: wstaw na koÅcu kontenera 867 container.appendChild(gridContainer); 868 gridInserted = true; 869 } 870 871 }); 872 } catch (error) { 873 // W przypadku bÅÄdu, loguj ale nie blokuj caÅego skryptu 874 if (typeof console !== "undefined" && console.error) { 875 console.error("BB Asystent AI: Error in groupImagesInGrid:", error); 876 } 877 } 878 */ 879 } 880 } 881 882 class ClassicChatMode { 883 constructor({ ajaxUrl, nonce }) { 884 this.ajaxUrl = ajaxUrl; 885 this.nonce = nonce; 886 } 887 888 buildUrl(action) { 889 const base = this.ajaxUrl || ""; 890 if (!action || !base) { 891 return base; 892 } 893 const separator = base.includes("?") ? "&" : "?"; 894 return `${base}${separator}action=${encodeURIComponent(action)}`; 895 } 896 897 isSafari() { 898 const ua = navigator.userAgent || ""; 899 return /Safari/.test(ua) && !/Chrome/.test(ua) && !/Chromium/.test(ua); 900 } 901 902 async send(message, sessionId, retryCount = 0) { 903 const maxRetries = this.isSafari() ? 3 : 2; 904 const retryDelay = this.isSafari() ? 1000 : 500; 905 906 const action = "bbai_ai_send_message"; 907 const url = this.buildUrl(action); 908 const formData = new FormData(); 909 formData.append("action", action); 910 formData.append("message", message); 911 formData.append("session_id", sessionId); 912 formData.append("nonce", this.nonce); 913 914 try { 915 const response = await fetch(url, { 916 method: "POST", 917 body: formData, 918 cache: "no-store", 919 credentials: "same-origin", 920 headers: { 921 "X-Requested-With": "XMLHttpRequest", 922 }, 923 }); 924 925 if (!response.ok) { 926 // Retry dla bÅÄdów 5xx i niektórych 4xx w Safari 927 if ( 928 retryCount < maxRetries && 929 (response.status >= 500 || 930 (this.isSafari() && response.status === 0)) 931 ) { 932 await new Promise((resolve) => 933 setTimeout(resolve, retryDelay * (retryCount + 1)), 934 ); 935 return this.send(message, sessionId, retryCount + 1); 936 } 937 throw new Error(`HTTP error! status: ${response.status}`); 938 } 939 940 const contentType = response.headers.get("content-type"); 941 if (!contentType || !contentType.includes("application/json")) { 942 // Safari czasami zwraca zÅy content-type 943 const text = await response.text(); 944 try { 945 return JSON.parse(text); 946 } catch (e) { 947 if (retryCount < maxRetries) { 948 await new Promise((resolve) => 949 setTimeout(resolve, retryDelay * (retryCount + 1)), 950 ); 951 return this.send(message, sessionId, retryCount + 1); 952 } 953 throw new Error("Invalid JSON response"); 954 } 955 } 956 957 return response.json(); 958 } catch (error) { 959 // Retry dla bÅÄdów sieciowych w Safari 960 if ( 961 retryCount < maxRetries && 962 (error.name === "TypeError" || 963 error.message.includes("Failed to fetch") || 964 error.message.includes("NetworkError")) 965 ) { 966 await new Promise((resolve) => 967 setTimeout(resolve, retryDelay * (retryCount + 1)), 968 ); 969 return this.send(message, sessionId, retryCount + 1); 970 } 971 throw error; 972 } 973 } 974 } 975 976 class StreamingChatMode { 977 constructor(app, { restUrl, sessionId }) { 978 this.app = app; 979 this.restUrl = restUrl || ""; 980 this.sessionId = sessionId || ""; 981 this.pendingProducts = null; 982 this.pendingToolUi = []; 983 this.streamBuffer = ""; 984 985 // Bufor akumulujÄ cy tekst do renderowania 986 this._textBufferParts = []; 987 988 // RAF-based render scheduling (znacznie pÅynniejsze niż setTimeout 100ms, 989 // a kontrola backpressure przez _renderScheduled gwarantuje brak overshootu). 990 this._renderScheduled = false;
991 this._lastFrameId = null; 992 this._pendingArgs = null; 993 994 // Cache stabilnego prefiksu HTML â nie re-parsujemy markdown caÅego tekstu, 995 // tylko dirty tail (od ostatniej granicy akapitu / nie-otwartego bloku kodu). 996 // Klucz: kontentNode (WeakMap żeby nie wyciekaÄ DOM-u po usuniÄciu wiadomoÅci). 997 this._stableCache = new WeakMap(); 998 999 // Flaga ostatniego renderowania â telemetria. 1000 this._lastRenderTime = 0; 1001 } 1002 1003 // ============== Helpery do obsÅugi SSE meta ============== 1004 1005 _dispatchMetaSidecar(payload) { 1006 const meta = payload && payload.meta; 1007 if (!meta) return; 1008 const app = this.app; 1009 if (meta.tool_call && typeof app.handleToolCallEvent === "function") { 1010 app.handleToolCallEvent(meta.tool_call); 1011 } 1012 if (meta.ui) { 1013 if (this._shouldDeferToolUi(meta.ui)) { 1014 this._queueToolUi(meta); 1015 } else { 1016 this.handleUiMeta(meta); 1017 } 1018 } 1019 if (meta.ui_facets) { 1020 app.renderToolFacets(meta.ui_facets, meta); 1021 } 1022 if (meta.ui_used_filters) { 1023 app.renderFacetSummary(meta.ui_used_filters); 1024 } 1025 if (meta.nekrolog_context) { 1026 app.nekrologContext = meta.nekrolog_context; 1027 } 1028 if (meta.ui_products) { 1029 this.pendingProducts = { 1030 products: meta.ui_products || [], 1031 nekrolog: meta.nekrolog_context || null, 1032 }; 1033 } 1034 } 1035 1036 _shouldDeferToolUi(ui) { 1037 return !!( 1038 ui && 1039 typeof ui === "object" && 1040 ui.type === "obituary_results" 1041 ); 1042 } 1043 1044 _queueToolUi(meta) { 1045 if (!meta || !meta.ui) { 1046 return; 1047 } 1048 const type = meta.ui.type || ""; 1049 this.pendingToolUi = this.pendingToolUi.filter((item) => item.type !== type); 1050 this.pendingToolUi.push({ type, meta }); 1051 } 1052 1053 _normalizeMessageText(value) { 1054 return String(value || "") 1055 .replace(/<[^>]+>/g, " ") 1056 .replace(/\s+/g, " ") 1057 .trim() 1058 .toLowerCase(); 1059 } 1060 1061 _hasAssistantMessageWithText(text) { 1062 const normalized = this._normalizeMessageText(text); 1063 if (!normalized) { 1064 return false; 1065 } 1066 return Array.from( 1067 document.querySelectorAll( 1068 ".bb-asystent-ai-message.assistant .bb-asystent-ai-message-content", 1069 ), 1070 ).some((node) => this._normalizeMessageText(node.textContent) === normalized); 1071 } 1072 1073 _flushDeferredToolUi() { 1074 const queue = this.pendingToolUi.splice(0); 1075 queue.forEach((item) => this.handleUiMeta(item.meta)); 1076 1077 if (this.pendingProducts) { 1078 this.app.renderProductSuggestions( 1079 this.pendingProducts.products, 1080 this.pendingProducts.nekrolog, 1081 ); 1082 this.pendingProducts = null; 1083 } 1084 } 1085 1086 _createAssistantNode(app) { 1087 const node = app.addMessage( 1088 "assistant", 1089 "", 1090 app.getCurrentTime(), 1091 true, 1092 null, 1093 { type: "ai_response" }, 1094 ); 1095 let content = null; 1096 if (node) { 1097 node.classList.add("streaming"); 1098 content = node.querySelector(".bb-asystent-ai-message-content"); 1099 } 1100 return { node, content }; 1101 } 1102 1103 _isSheetsSource(meta) { 1104 return !!(meta && (meta.source === "google_sheets" || meta.source === "llms_gsheet")); 1105 } 1106 1107 _isInstantPayload(payload, assistantNode) { 1108 if (assistantNode && assistantNode.dataset.type === "instant") { 1109 return true; 1110 } 1111 const meta = payload && payload.meta; 1112 if (!meta) return false;
1113 return ( 1114 meta.source === "google_sheets" || 1115 meta.source === "llms_gsheet" || 1116 meta.source === "tool" 1117 ); 1118 } 1119 1120 _accumulate(currentText, payload, isInstant) { 1121 if (payload.final) { 1122 return payload.final; 1123 } 1124 if (payload.delta) { 1125 return currentText + payload.delta; 1126 } 1127 return currentText; 1128 } 1129 1130 /** 1131 * Dzieli tekst na bezpieczny "stable prefix" (już zamkniÄte akapity i bloki) 1132 * i "dirty tail" (ostatni, ciÄ gle uzupeÅniany fragment). 1133 * Granica: ostatnie "\n\n" PO ostatnim zamkniÄtym bloku ```...```. 1134 * JeÅli blok kodu jest otwarty (nieparzysta liczba "```" w tekÅcie) â 1135 * caÅy tekst od poczÄ tku tego bloku idzie jako dirty (renderowany jako plain <pre>). 1136 * 1137 * @returns {{stable: string, dirty: string, openCodeBlock: boolean}} 1138 */ 1139 _splitStableAndDirty(text) { 1140 if (typeof text !== "string" || text.length === 0) { 1141 return { stable: "", dirty: "", openCodeBlock: false }; 1142 } 1143 1144 // Wykryj otwarty blok kodu (nieparzysta liczba "```") 1145 const fenceCount = (text.match(/```/g) || []).length; 1146 const openCodeBlock = fenceCount % 2 === 1; 1147 1148 if (openCodeBlock) { 1149 // Blok kodu otwarty â stable to wszystko PRZED ostatnim ``` 1150 const lastFence = text.lastIndexOf("```"); 1151 if (lastFence <= 0) { 1152 return { stable: "", dirty: text, openCodeBlock: true }; 1153 } 1154 // Zostawiamy stable do ostatniego "\n\n" przed otwartym fencem 1155 const before = text.slice(0, lastFence); 1156 const lastBreak = before.lastIndexOf("\n\n"); 1157 if (lastBreak < 0) { 1158 return { stable: "", dirty: text, openCodeBlock: true }; 1159 } 1160 return { 1161 stable: text.slice(0, lastBreak + 2), 1162 dirty: text.slice(lastBreak + 2), 1163 openCodeBlock: true, 1164 }; 1165 } 1166 1167 // Brak otwartego bloku â dziel po ostatnim "\n\n" 1168 const lastBreak = text.lastIndexOf("\n\n"); 1169 if (lastBreak < 0) { 1170 // CaÅa treÅÄ jest jeszcze "dirty" (jeden akapit w trakcie) 1171 return { stable: "", dirty: text, openCodeBlock: false }; 1172 } 1173 return { 1174 stable: text.slice(0, lastBreak + 2), 1175 dirty: text.slice(lastBreak + 2), 1176 openCodeBlock: false, 1177 }; 1178 } 1179 1180 /** 1181 * Bezpieczne wstawianie zwykÅego tekstu jako HTML â z escape'em <,>,&,'',"" 1182 */ 1183 _escapeHtml(value) { 1184 if (value === null || value === undefined) return ""; 1185 return String(value) 1186 .replace(/&/g, "&") 1187 .replace(/</g, "<") 1188 .replace(/>/g, ">") 1189 .replace(/"/g, """) 1190 .replace(/'/g, "'"); 1191 } 1192 1193 /** 1194 * Renderuje "dirty tail" tanio: jeÅli mamy otwarty blok kodu, 1195 * używamy <pre> z escape'em (bez parsowania markdown na każdy chunk), 1196 * w przeciwnym razie parsuje markdown TYLKO tego krótkiego fragmentu. 1197 */ 1198 _renderDirty(text, openCodeBlock) { 1199 if (!text) return ""; 1200 if (openCodeBlock) { 1201 // WyciÄ gnij jÄzyk z fenca jeÅli jest, wyrenderuj jako pre 1202 const fenceMatch = text.match(/```([\w+-]*)\n?([\s\S]*)$/); 1203 if (fenceMatch) { 1204 const lang = fenceMatch[1] || ""; 1205 const code = fenceMatch[2] || ""; 1206 const langAttr = lang 1207 ? ` data-language="${this._escapeHtml(lang)}"` 1208 : ""; 1209 return `<pre class="markdown-code-block"${langAttr}><code>${this._escapeHtml(code)}</code></pre>`; 1210 } 1211 return `<pre class="markdown-code-block"><code>${this._escapeHtml(text)}</code></pre>`; 1212 } 1213 // Krótki fragment â peÅny markdown, ale O(dirty.length), nie O(caÅoÅÄ) 1214 return this.app.renderMarkdown(text); 1215 } 1216 1217 /** 1218 * Wykonuje faktyczne renderowanie do DOM â incremental. 1219 * @private 1220 */ 1221 _performRender(contentNode, textToRender, isInstant, payload, assistantNode) { 1222 if (!contentNode) return; 1223 1224 // Instant (Google Sheets/LLMS): usuÅ blockquote i prefiks ">" 1225 if (isInstant) { 1226 textToRender = textToRender 1227 .replace(/^\s*<blockquote[^>]*>/i, "") 1228 .replace(/<\/blockquote>\s*$/i, ""); 1229 textToRender = textToRender 1230 .split("\n") 1231 .map((l) => l.replace(/^\s*>\s?/, "")) 1232 .join("\n"); 1233 } 1234 1235 const isStreaming = 1236 assistantNode && assistantNode.classList.contains("streaming"); 1237 1238 let html; 1239 1240 if (!isStreaming || isInstant) { 1241 // Tryb finalny â renderujemy caÅoÅÄ raz i koniec, bez bawienia siÄ w cac
1241he. 1242 html = this.app.renderMarkdown(textToRender); 1243 // WyczyÅÄ cache dla tego wÄzÅa â wiadomoÅÄ jest finalna. 1244 this._stableCache.delete(contentNode); 1245 } else { 1246 // STREAMING â split + cache stabilnego HTML 1247 const { stable, dirty, openCodeBlock } = 1248 this._splitStableAndDirty(textToRender); 1249 1250 let cached = this._stableCache.get(contentNode); 1251 if (!cached || cached.text !== stable) { 1252 // Stable siÄ zmieniÅ â re-renderuj prefix raz i zacache'uj. 1253 cached = { 1254 text: stable, 1255 html: stable ? this.app.renderMarkdown(stable) : "", 1256 }; 1257 this._stableCache.set(contentNode, cached); 1258 } 1259 1260 let dirtyHtml = this._renderDirty(dirty, openCodeBlock); 1261 1262 // Kursor strumienia na koÅcu dirty (jedna mutacja DOM zamiast regexa 1263 // szukajÄ cego ostatniego zamykajÄ cego taga w peÅnym HTML). 1264 if (dirtyHtml) { 1265 dirtyHtml = dirtyHtml.replace( 1266 /(<\/[^>]+>)\s*$/, 1267 '<span class="bb-stream-cursor"></span>$1', 1268 ); 1269 if (!dirtyHtml.includes("bb-stream-cursor")) { 1270 dirtyHtml += '<span class="bb-stream-cursor"></span>'; 1271 } 1272 } else { 1273 dirtyHtml = '<span class="bb-stream-cursor"></span>'; 1274 } 1275 1276 html = cached.html + dirtyHtml; 1277 } 1278 1279 // Jedna mutacja DOM â innerHTML jest najszybszy dla peÅnego replace. 1280 contentNode.innerHTML = html; 1281 1282 if (assistantNode && this.app.adjustAssistantMessageScroll) { 1283 this.app.adjustAssistantMessageScroll(assistantNode); 1284 } 1285 1286 this._lastRenderTime = 1287 typeof performance !== "undefined" && performance.now 1288 ? performance.now() 1289 : Date.now(); 1290 } 1291 1292 /** 1293 * Renderuje tekst podczas streamingu z throttlingiem RAF. 1294 * Wszystkie wywoÅania w tej samej ramce zlewajÄ siÄ do jednego renderu 1295 * (ostatnie argumenty wygrywajÄ ). 1296 */ 1297 renderStreamingContent(contentNode, text, isInstant, payload, assistantNode) { 1298 this._pendingArgs = [contentNode, text, isInstant, payload, assistantNode]; 1299 1300 if (this._renderScheduled) { 1301 return; 1302 } 1303 this._renderScheduled = true; 1304 1305 const flush = () => { 1306 this._renderScheduled = false; 1307 this._lastFrameId = null; 1308 const args = this._pendingArgs; 1309 this._pendingArgs = null; 1310 if (args) { 1311 this._performRender.apply(this, args); 1312 } 1313 }; 1314 1315 if (typeof window !== "undefined" && window.requestAnimationFrame) { 1316 this._lastFrameId = window.requestAnimationFrame(flush); 1317 } else { 1318 this._lastFrameId = setTimeout(flush, 16); 1319 } 1320 } 1321 1322 /** 1323 * Renderuje finalny tekst natychmiast (anuluje pending RAF). 1324 */ 1325 renderFinalContent(contentNode, text, isInstant, payload, assistantNode) { 1326 // Anuluj pending render â chcemy mieÄ ostateczny widok bez "migniÄcia". 1327 if (this._lastFrameId !== null) { 1328 if (typeof window !== "undefined" && window.cancelAnimationFrame) { 1329 window.cancelAnimationFrame(this._lastFrameId); 1330 } else { 1331 clearTimeout(this._lastFrameId); 1332 } 1333 this._lastFrameId = null; 1334 } 1335 this._renderScheduled = false; 1336 this._pendingArgs = null; 1337 1338 this._performRender( 1339 contentNode, 1340 text, 1341 isInstant, 1342 payload, 1343 assistantNode, 1344 ); 1345 } 1346 1347 setSessionId(sessionId) { 1348 this.sessionId = sessionId; 1349 } 1350 1351 setRestUrl(restUrl) { 1352 this.restUrl = restUrl; 1353 } 1354 1355 isEnabled() { 1356 const settings = this.app.settings || {}; 1357 return ( 1358 settings.responseMode === "streaming" && 1359 settings.forceClassicMode !== true && 1360 typeof this.restUrl === "string" && 1361 this.restUrl.length > 0 1362 ); 1363 } 1364 1365 parseEventStreamFromBuffer() { 1366 const events = []; 1367 const normalized = this.streamBuffer.replace(/\r\n/g, "\n"); 1368 const parts = normalized.split("\n\n"); 1369 this.streamBuffer = parts.pop() || ""; 1370 1371 for (const part of parts) { 1372 if (!part) { 1373 continue; 1374 } 1375 const lines = part.split("\n"); 1376 const currentEvent = { event: "message", data: "" }; 1377 for (const line of lines) { 1378 if (line.startsWith("event:")) { 1379 currentEvent.event = line.slice(6).trim(); 1380 } else if (line.startsWith("data:")) { 1381 const data = line.slice(5).trim(); 1382 currentEvent.data = currentEvent.data 1383 ? `${currentEvent.data}\n${data}` 1384 : data; 1385 } else { 1386 // Ignore comments (:) and unknown fields 1387 } 1388 } 1389 events.push(currentEvent); 1390 } 1391 1392 return events; 1393 } 1394 1395 isSafari() { 1396 const ua = navigator.userAgent || ""; 1397 return /Safari/.test(ua) && !/Chrome/.test(ua) && !/Chromium/.test(ua); 1398 } 1399 1400 /** 1401 * Recovery z `/history` â po nieudanym streamie pobiera ostatniÄ 1402 * wiadomoÅÄ asystenta z bazy (zapisanÄ przez backend mimo zerwanego streamu)
1403 * i renderuje jÄ w UI. Kluczowe dla Safari, gdzie fetch potrafi 1404 * urwaÄ poÅÄ czenie a backend i tak skoÅczy generowanie. 1405 * 1406 * @param {string} userMessage TreÅÄ pytania użytkownika (do dopasowania). 1407 * @param {Element|null} existingAssistantNode IstniejÄ cy stub wiadomoÅci lub null. 1408 * @returns {Promise<boolean>} true jeÅli udaÅo siÄ odzyskaÄ i wyrenderowaÄ. 1409 */ 1410 async _attemptRecoveryFromHistory(userMessage, existingAssistantNode = null) { 1411 if (!this.restUrl || !this.sessionId) { 1412 return false; 1413 } 1414 1415 try { 1416 // Backend potrzebuje chwili na zapis assistant message. 1417 // Safari bywa wolniejsze przy wznawianiu fetch po bÅÄdzie sieci. 1418 const waitMs = this.isSafari() ? 2200 : 1300; 1419 await new Promise((resolve) => setTimeout(resolve, waitMs)); 1420 1421 const url = `${this.restUrl}history/${encodeURIComponent(this.sessionId)}`; 1422 const response = await fetch(url, { 1423 method: "GET", 1424 headers: { 1425 "X-Requested-With": "XMLHttpRequest", 1426 }, 1427 credentials: "same-origin", 1428 cache: "no-store", 1429 }); 1430 1431 if (!response.ok) { 1432 debugWarn( 1433 "BB Asystent AI: recovery fetch non-OK", 1434 response.status, 1435 ); 1436 return false; 1437 } 1438 1439 const data = await response.json(); 1440 if (!data || !data.success || !Array.isArray(data.messages)) { 1441 return false; 1442 } 1443 1444 // Recovery musi pominÄ Ä starsze odpowiedzi asystenta (np. powitanie 1445 // zapisane przy otwieraniu czatu). Bierzemy tylko wiadomoÅÄ, która 1446 // pojawiÅa siÄ PO ostatnim user message â inaczej na pierwszej 1447 // wiadomoÅci w nowej sesji "odzyskalibyÅmy" welcome zamiast 1448 // poprawnie wpaÅÄ do fallbacku AJAX. 1449 let lastUserIndex = -1; 1450 for (let i = data.messages.length - 1; i >= 0; i--) { 1451 const m = data.messages[i] || {}; 1452 const role = (m.type || m.role || "").toLowerCase(); 1453 if (role === "user") { 1454 lastUserIndex = i; 1455 break; 1456 } 1457 } 1458 1459 if (lastUserIndex === -1) { 1460 return false; 1461 } 1462 1463 let recovered = ""; 1464 for (let i = data.messages.length - 1; i > lastUserIndex; i--) { 1465 const m = data.messages[i] || {}; 1466 const role = (m.type || m.role || "").toLowerCase(); 1467 if (role === "assistant" || role === "ai") { 1468 recovered = (m.content || m.message || "").toString(); 1469 break; 1470 } 1471 } 1472 1473 if (!recovered.trim()) { 1474 return false; 1475 } 1476 1477 const app = this.app; 1478 let node = existingAssistantNode; 1479 let contentNode = node 1480 ? node.querySelector(".bb-asystent-ai-message-content") 1481 : null; 1482 1483 if (!node || !contentNode) { 1484 node = app.addMessage( 1485 "assistant", 1486 "", 1487 app.getCurrentTime(), 1488 true, 1489 null, 1490 { type: "ai_response" }, 1491 ); 1492 contentNode = node 1493 ? node.querySelector(".bb-asystent-ai-message-content") 1494 : null; 1495 } 1496 1497 if (node) { 1498 node.classList.remove("streaming"); 1499 node.dataset.final = "true"; 1500 node.dataset.recovered = "true"; 1501 } 1502 1503 if (contentNode) { 1504 this.renderFinalContent( 1505 contentNode, 1506 recovered, 1507 false, 1508 {}, 1509 node, 1510 ); 1511 } 1512 1513 if (typeof app.hideTypingIndicator === "function") { 1514 app.hideTypingIndicator(); 1515 } 1516 if (typeof app.persistMessagesCache === "function") { 1517 app.persistMessagesCache(); 1518 } 1519 if (node && typeof app.scrollToMessageTop === "function") { 1520 app.scrollToMessageTop(node); 1521 } 1522 1523 debugLog("BB Asystent AI: recovered message from /history"); 1524 return true; 1525 } catch (e) { 1526 debugWarn("BB Asystent AI: recovery failed", e); 1527 return false;
1528 } 1529 } 1530 1531 async stream(message, retryCount = 0) { 1532 if (!this.isEnabled()) { 1533 return false; 1534 } 1535 1536 this.streamBuffer = ""; 1537 1538 if (!this.restUrl) { 1539 debugWarn( 1540 "FUNERAL DEBUG: REST URL not available, skipping streaming", 1541 ); 1542 return false; 1543 } 1544 1545 const maxRetries = this.isSafari() ? 2 : 1; 1546 const retryDelay = this.isSafari() ? 1500 : 500; 1547 1548 const app = this.app; 1549 const controller = new AbortController(); 1550 1551 try { 1552 const response = await fetch(`${this.restUrl}stream`, { 1553 method: "POST", 1554 headers: { 1555 "Content-Type": "application/json", 1556 "X-Requested-With": "XMLHttpRequest", 1557 }, 1558 body: JSON.stringify({ 1559 message, 1560 session_id: this.sessionId, 1561 }), 1562 signal: controller.signal, 1563 cache: "no-store", 1564 credentials: "same-origin", 1565 }); 1566 1567 if (!response.ok || !response.body) { 1568 // Retry dla bÅÄdów w Safari 1569 if ( 1570 retryCount < maxRetries && 1571 (response.status >= 500 || 1572 (this.isSafari() && response.status === 0)) 1573 ) { 1574 debugWarn( 1575 `FUNERAL DEBUG: Streaming failed, retrying (${retryCount + 1}/${maxRetries})`, 1576 ); 1577 await new Promise((resolve) => 1578 setTimeout(resolve, retryDelay * (retryCount + 1)), 1579 ); 1580 return this.stream(message, retryCount + 1); 1581 } 1582 debugWarn( 1583 "FUNERAL DEBUG: Streaming endpoint returned non-OK status", 1584 response.status, 1585 ); 1586 // Po wszystkich retry â sprawdź czy backend zdÄ Å¼yÅ zapisaÄ odpowiedź. 1587 const recovered = 1588 await this._attemptRecoveryFromHistory(message); 1589 if (recovered) { 1590 if (this.app.persistMessagesCache) { 1591 this.app.persistMessagesCache(); 1592 } 1593 return true; 1594 } 1595 return false; 1596 } 1597 1598 const reader = response.body.getReader(); 1599 const decoder = new TextDecoder(); 1600 1601 let assistantNode = null; 1602 let contentNode = null; 1603 let accumulatedText = ""; 1604 let streamCompleted = false; 1605 let shouldBreakWhile = false; 1606 1607 while (true) { 1608 const { value, done } = await reader.read(); 1609 if (done || shouldBreakWhile) { 1610 break; 1611 } 1612 1613 const chunk = decoder.decode(value, { stream: true }); 1614 this.streamBuffer += chunk; 1615 const events = this.parseEventStreamFromBuffer(); 1616 1617 for (const event of events) { 1618 if (event.event === "message" && event.data) { 1619 try { 1620 const payload = JSON.parse(event.data); 1621 1622 // Routing meta-sidecarów (tool_call, ui, facets, products, nekrolog). 1623 this._dispatchMetaSidecar(payload); 1624 1625 if (payload.delta || payload.final) { 1626 if ( 1627 payload.delta && 1628 payload.meta && 1629 payload.meta.source === "tool" && 1630 this._hasAssistantMessageWithText(payload.delta) 1631 ) { 1632 continue; 1633 } 1634 if (!assistantNode) { 1635 const created = this._createAssistantNode(app); 1636 assistantNode = created.node; 1637 contentNode = created.content; 1638 } 1639 1640 if (accumulatedText === "") { 1641 app.hideTypingIndicator(); 1642 } 1643 1644 if (payload.meta && payload.meta.persona) { 1645 app.updateAssistantPersona(payload.meta.persona); 1646 } 1647 if (this._isSheetsSource(payload.meta) && assistantNode) { 1648 assistantNode.dataset.type = "instant"; 1649 } 1650 1651 const isInstant = this._isInstantPayload(payload, assistantNode); 1652 accumulatedText = this._accumulate(accumulatedText, payload, isInstant); 1653 1654 if (contentNode) { 1655 const renderer = payload.final 1656 ? this.renderFinalContent 1657 : this.renderStreamingContent; 1658 renderer.call( 1659 this, 1660 contentNode, 1661 accumulatedText, 1662 isInstant, 1663 payload, 1664 assistantNode, 1665 ); 1666 }
1667 // WiadomoÅÄ asystenta jest zakotwiczona na górze (scrollToMessageTop 1668 // przy tworzeniu node), wiÄc nie scrollujemy w dóŠpodczas streamu â 1669 // user czyta od poczÄ tku odpowiedzi. 1670 } 1671 1672 if (payload.error) { 1673 debugError( 1674 "FUNERAL DEBUG: Stream error payload", 1675 payload.error, 1676 ); 1677 controller.abort(); 1678 if ( 1679 assistantNode && 1680 assistantNode.parentNode 1681 ) { 1682 assistantNode.parentNode.removeChild( 1683 assistantNode, 1684 ); 1685 } 1686 return false; 1687 } 1688 } catch (streamError) { 1689 debugError( 1690 "FUNERAL DEBUG: Failed to parse stream payload", 1691 streamError, 1692 event, 1693 ); 1694 } 1695 } else if (event.event === "done") { 1696 debugLog( 1697 "FUNERAL DEBUG: Stream done event received, completing...", 1698 ); 1699 if (assistantNode) { 1700 const contentText = assistantNode 1701 .querySelector(".bb-asystent-ai-message-content") 1702 ?.textContent.trim() || ""; 1703 if (contentText === "" && assistantNode.parentNode) { 1704 assistantNode.parentNode.removeChild(assistantNode); 1705 } 1706 assistantNode.classList.remove("streaming"); 1707 assistantNode.dataset.final = "true"; 1708 } 1709 this._flushDeferredToolUi(); 1710 streamCompleted = true; 1711 shouldBreakWhile = true; 1712 break; 1713 } 1714 } 1715 } 1716 1717 if (streamCompleted && accumulatedText.trim() !== "") { 1718 app.persistMessagesCache(); 1719 app.trackEvent("message_sent", { 1720 message_length: message.length, 1721 streamed: true, 1722 }); 1723 app.unreadCount++; 1724 app.updateBadgeWithCount(); 1725 app.armReadAcknowledgementIfNeeded(); 1726 try { 1727 controller.abort(); 1728 } catch (e) {} 1729 return true; 1730 } 1731 1732 if (streamCompleted && accumulatedText.trim() === "") { 1733 try { 1734 controller.abort(); 1735 } catch (e) {} 1736 // Stream zamkniÄty bez treÅci â spróbuj odzyskaÄ z /history. 1737 const recovered = 1738 await this._attemptRecoveryFromHistory(message, assistantNode); 1739 if (recovered) { 1740 if (app.persistMessagesCache) app.persistMessagesCache(); 1741 return true; 1742 } 1743 return false; 1744 } 1745 1746 try { 1747 controller.abort(); 1748 } catch (e) {} 1749 1750 // Stream urwany w trakcie (brak event:done) â zapisany w bazie? 1751 if (accumulatedText.trim() === "") { 1752 const recovered = 1753 await this._attemptRecoveryFromHistory(message, assistantNode); 1754 if (recovered) { 1755 if (app.persistMessagesCache) app.persistMessagesCache(); 1756 return true; 1757 } 1758 } 1759 return false;
1760 } catch (error) { 1761 if (error.name !== "AbortError") { 1762 // Retry dla bÅÄdów sieciowych w Safari 1763 if ( 1764 retryCount < maxRetries && 1765 (error.name === "TypeError" || 1766 error.message.includes("Failed to fetch") || 1767 error.message.includes("NetworkError")) 1768 ) { 1769 debugWarn( 1770 `FUNERAL DEBUG: Streaming network error, retrying (${retryCount + 1}/${maxRetries})`, 1771 error, 1772 ); 1773 await new Promise((resolve) => 1774 setTimeout(resolve, retryDelay * (retryCount + 1)), 1775 ); 1776 return this.stream(message, retryCount + 1); 1777 } 1778 debugWarn( 1779 "FUNERAL DEBUG: Streaming request failed, falling back to AJAX", 1780 error, 1781 ); 1782 try { 1783 controller.abort(); 1784 } catch (e) {} 1785 // Po wszystkich retry â ostatnia szansa: odzyskaj odpowiedź 1786 // z bazy (Safari: fetch padÅ, ale backend skoÅczyÅ generowanie). 1787 const recovered = await this._attemptRecoveryFromHistory(message); 1788 if (recovered) { 1789 if (this.app.persistMessagesCache) { 1790 this.app.persistMessagesCache(); 1791 } 1792 return true; 1793 } 1794 return false; 1795 } 1796 debugLog("FUNERAL DEBUG: Stream aborted normally"); 1797 // Aborted (np. przez kolejne wysÅanie wiadomoÅci) â nie odzyskujemy. 1798 return false; 1799 } 1800 } 1801 1802 handleUiMeta(meta) { 1803 const app = this.app; 1804 if (!meta || !meta.ui) { 1805 return; 1806 } 1807 if ( 1808 typeof meta.ui === "object" && 1809 meta.ui.type === "obituary_results" 1810 ) { 1811 app.renderObituaryResults(meta.ui); 1812 return; 1813 } 1814 1815 const typingRoot = document.getElementById( 1816 "bb-asystent-ai-typing", 1817 ); 1818 1819 if (meta.ui === "search_start") { 1820 app.hideTypingIndicator(); 1821 this.ensureStatusStyles(); 1822 if (typingRoot) { 1823 let status = typingRoot.querySelector(".typing-status"); 1824 if (status) { 1825 this.ensureTypingStatusStyles(); 1826 status.innerHTML = 1827 '<span class="fac-globe-spinner">ð</span>Szukam wiarygodnych informacjiâ¦'; 1828 status.style.display = "block"; 1829 } 1830 } 1831 } else if (meta.ui === "search_end") { 1832 if (typingRoot) { 1833 const status = typingRoot.querySelector(".typing-status"); 1834 if (status) { 1835 status.innerHTML = "FinalizujÄ odpowied
1835źâ¦"; 1836 status.style.display = "block"; 1837 } 1838 } 1839 } else if (meta.ui === "fetch_start" && meta.url) { 1840 const host = this.safeUrlHost(meta.url); 1841 if (typingRoot) { 1842 const status = typingRoot.querySelector(".typing-status"); 1843 if (status) { 1844 this.ensureTypingStatusStyles(); 1845 status.innerHTML = `<span class="fac-globe-spinner">ð</span>Czytam: ${host}`; 1846 status.style.display = "block"; 1847 } 1848 } 1849 } else if (meta.ui === "fetch_end" && meta.url) { 1850 const host = this.safeUrlHost(meta.url); 1851 if (typingRoot) { 1852 const status = typingRoot.querySelector(".typing-status"); 1853 if (status) { 1854 const ok = !!meta.ok; 1855 status.innerHTML = ok 1856 ? `<span class="fac-globe-spinner">ð</span>Przeczytano: ${host}` 1857 : `â ï¸ Nie udaÅo siÄ pobraÄ: ${host}`; 1858 status.style.display = "block"; 1859 } 1860 } 1861 } 1862 } 1863 1864 ensureStatusStyles() { 1865 if (document.getElementById("bb-asystent-ai-status-styles")) { 1866 return; 1867 } 1868 1869 const style = document.createElement("style"); 1870 style.id = "bb-asystent-ai-status-styles"; 1871 style.textContent = ` 1872.bb-asystent-ai-message.status .bb-asystent-ai-message-content { opacity: 0.95; }
1873.bb-asystent-ai-message.status { animation: fac-status-fade-in 240ms ease-out; } 1874@keyframes fac-status-fade-in { from { transform: translateY(2px); opacity: 0 } to { transform: translateY(0); opacity: 1 } } 1875.search-status .bb-asystent-ai-message-content::after { 1876 content: ""; 1877 display: inline-block; 1878 width: 6px; height: 6px; border-radius: 50%; 1879 background: currentColor; margin-left: 8px; opacity: .6; 1880 box-shadow: 10px 0 0 currentColor, 20px 0 0 currentColor; 1881 animation: fac-ellipsis 1s infinite ease-in-out; 1882 vertical-align: middle; 1883} 1884@keyframes fac-ellipsis { 1885 0% { box-shadow: 0 0 0 currentColor, 10px 0 0 currentColor, 20px 0 0 currentColor } 1886 33% { box-shadow: 0 0 0 currentColor, 0 0 0 currentColor, 20px 0 0 currentColor } 1887 66% { box-shadow: 0 0 0 currentColor, 10px 0 0 currentColor, 0 0 0 currentColor } 1888 100% { box-shadow: 0 0 0 currentColor, 10px 0 0 currentColor, 20px 0 0 currentColor } 1889} 1890.fetch-status .bb-asystent-ai-message-content::before { 1891 content: ""; 1892 display: inline-block; 1893 width: 12px; height: 12px; margin-right: 8px; 1894 border-radius: 50%; border: 2px solid currentColor; border-right-color: transparent; 1895 opacity: .6; animation: fac-spin 1s linear infinite; vertical-align: -2px; 1896} 1897@keyframes fac-spin { to { transform: rotate(360deg) } } 1898`; 1899 document.head.appendChild(style); 1900 } 1901 1902 ensureTypingStatusStyles() { 1903 if ( 1904 document.getElementById("bb-asystent-ai-typing-status-styles") 1905 ) { 1906 return; 1907 } 1908 const style = document.createElement("style"); 1909 style.id = "bb-asystent-ai-typing-status-styles"; 1910 style.textContent = ` 1911#bb-asystent-ai-typing .typing-status { display: block !important; } 1912.fac-globe-spinner { display: inline-block; margin-right: 6px; animation: fac-rotate 1.1s linear infinite; } 1913@keyframes fac-rotate { to { transform: rotate(360deg) } }`; 1914 document.head.appendChild(style); 1915 } 1916 1917 safeUrlHost(url) { 1918 let host = url; 1919 try { 1920 host = new URL(url).host || url; 1921 } catch (e) { 1922 host = url; 1923 } 1924 return host; 1925 } 1926 } 1927 1928 // Utility: Auto-link emails, URLs and Polish phone numbers 1929 // Może byÄ wywoÅywana na surowym tekÅcie (przed markdown) lub HTML (po markdown) 1930 // W przypadku HTML, pomija rzeczy które sÄ już w tagach <a> 1931 1932 /** 1933 * Wspólny silnik linkify: skanuje `text` regex'em, dla każdego dopasowania 1934 * sprawdza czy NIE jest wewnÄ trz <a>, <tag>, ani markdown-linku [text](url), 1935 * po czym dla bezpiecznych dopasowaÅ woÅa `buildReplacement(match, m)` żeby 1936 * uzyskaÄ HTML zastÄpujÄ cy. Zwraca przerobiony string. 1937 */ 1938 function _linkifyByPattern(text, pattern, buildReplacement, opts = {}) { 1939 const checkMarkdown = opts.checkMarkdown !== false; 1940 let m; 1941 const items = []; 1942 while ((m = pattern.exec(text)) !== null) { 1943 const match = m[0]; 1944 const offset = m.index; 1945 if (_isInsideHtmlOrAnchor(text, offset)) continue; 1946 if (match.includes('<a ') || match.includes('href=')) continue; 1947 if (checkMarkdown && _isInsideMarkdownLink(text, offset, match.length)) continue; 1948 const replacement = buildReplacement(match, m); 1949 if (replacement === null || replacement === undefined) continue; 1950 items.push({ match, replacement, offset }); 1951 } 1952 // apply in reverse order to preserve offsets 1953 let out = text; 1954 for (let i = items.length - 1; i >= 0; i--) { 1955 const it = items[i]; 1956 out = out.substring(0, it.offset) + it.replacement + out.substring(it.offset + it.match.length); 1957 } 1958 return out; 1959 } 1960 1961 function _isInsideHtmlOrAnchor(text, offset) { 1962 const before = text.substring(0, offset); 1963 const lastLt = before.lastIndexOf("<"); 1964 const lastGt = before.lastIndexOf(">"); 1965 if (lastLt > lastGt) return true; // inside a tag 1966 const lastOpenA = before.lastIndexOf('<a '); 1967 const lastCloseA = before.lastIndexOf('</a>'); 1968 return lastOpenA > lastCloseA; 1969 } 1970 1971 function _isInsideMarkdownLink(text, offset, matchLen) { 1972 const before = text.substring(Math.max(0, offset - 200), offset); 1973 const after = text.substring(offset + matchLen, offset + matchLen + 200); 1974 return /\[[^\]]*$/.test(before) && (/^[^\]]*\]/.test(after) || /^[^\]]*\(/.test(after)); 1975 } 1976 1977 function _externalTargetAttr(href) { 1978 return isSameSiteHref(href) 1979 ? "" 1980 : ' target="_blank" rel="noopener noreferrer nofollow ugc"'; 1981 } 1982 1983 function linkifyContacts(text) { 1984 if (typeof text !== "string") return ""; 1985 let out = text; 1986 1987 // E-mails 1988 out = _linkifyByPattern( 1989 out, 1990 /\b([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\b/g, 1991 (match) => `<a class="fac-link email-link" href="mailto:${match}">${match}</a>`, 1992 { checkMarkdown: false }, 1993 ); 1994 1995 // Google Maps 1996 out = _linkifyByPattern( 1997 out, 1998 /(https?:\/\/)?(www\.)?(maps\.google\.com|google\.com\/maps|goo\.gl\/maps)[^\s<")]*/gi, 1999 (match) => { 2000 const href = match.startsWith('http') ? match : 'https://' + match; 2001 return `<a class="fac-link maps-link" href="${href}"${_externalTargetAttr(href)}>${match}</a>`; 2002 }, 2003 { checkMarkdown: false }, 2004 ); 2005 2006 // Generic URLs (http/https or www.) 2007 out = _linkifyByPattern( 2008 out, 2009 /(?:(https?:\/\/)|(www\.))([^\s<"'>]+)/g, 2010 (match, m) => { 2011 const href = m[1] ? match : "https://" + match; 2012 return `<a class="fac-link external-link" href="${href}"${_externalTargetAttr(href)}>${match}</a>`; 2013 }, 2014 { checkMarkdown: true }, 2015 ); 2016 2017 // Addresses auto-linking (Google Maps search) disabled. 2018 // It tended to capture too much surrounding text and produced confusing anchors. 2019 // Keep only explicit URLs/emails/phones, and rely on Markdown links from knowledge/prompt. 2020 2021 // Phone numbers (PL common formats) - improved patterns 2022 // KolejnoÅÄ ważna: bardziej specyficzne wzorce najpierw 2023 const phonePatterns = [ 2024 /\+48\s?\d{2}\s?\d{3}\s?\d{2}\s?\d{2}/g, // +48 58 341 08 36 (bardziej specyficzny) 2025 /\+48\s?\d{3}\s?\d{3}\s?\d{3}/g, // +48 606 201 260 2026 /\+48\d{9,10}/g, // +48606201260 2027 /\b48\s?\d{9,10}\b/g, // 48 606201260 2028 // Wzorce dla formatów z spacjami (bardziej specyficzne najpierw) 2029 /\b\d{2}\s?\d{3}\s?\d{2}\s?\d{2}\b/g, // 58 341 20 71 (format 2-3-2-2) 2030 /\b\d{3}\s?\d{3}\s?\d{3}\b/g, // 606 201 260, 605 050 017 (format 3-3-3) 2031 /\b\d{9}\b/g, // 606201260 (bez spacji) 2032 ]; 2033 2034 phonePatterns.forEach((re) => { 2035 out = out.replace(re, (match, offset) => { 2036 // Skip if already inside a link tag (HTML) 2037 const beforeMatch = out.substring(0, offset); 2038 const lastLt = beforeMatch.lastIndexOf("<"); 2039 const lastGt = beforeMatch.lastIndexOf(">"); 2040 if (lastLt > lastGt) { 2041 return match; 2042 } 2043 const openTags = (beforeMatch.match(/<a\s[^>]*>/gi) || []).length; 2044 const closeTags = (beforeMatch.match(/<\/a>/gi) || []).length; 2045 if (openTags > closeTags) { 2046 return match; // Already inside a link, don't process 2047 } 2048 2049 // Skip if inside markdown link syntax [text](url) or [text][ref] 2050 const beforeContext = beforeMatch.substring(Math.max(0, beforeMatch.length - 100)); 2051 const afterContext = out.substring(offset + match.length, offset + match.length + 100); 2052 // Sprawdź czy przed match jest "[" a po match jest "]" lub "(" 2053 if (beforeContext.match(/\[[^\]]*$/) && (afterContext.match(/^[^\]]*\]/) || afterContext.match(/^[^\]]*\(/))) { 2054 return match; // Inside markdown link, don't process 2055 } 2056 2057 const digits = match.replace(/[^\d+]/g, ""); 2058 const tel = digits.startsWith("+") 2059 ? digits 2060 : digits.startsWith("48") 2061 ? "+" + digits 2062 : "+48" + digits; 2063 return `<a class="fac-link phone-link" href="tel:${tel}">${match}</a>`; 2064 }); 2065 }); 2066 2067 return out; 2068 } 2069 2070 class BBAsystentAI { 2071 constructor() { 2072 this.isOpen = false;
2073 this.isLoading = false; 2074 this.isInitialized = false; 2075 this.sessionId = bootstrapData.sessionId; 2076 this.ajaxUrl = bootstrapData.ajaxUrl || ""; 2077 this.restUrl = bootstrapData.restUrl || ""; 2078 this.settings = bootstrapData.settings || {}; 2079 // Cookie consent sprawdzany jest przez inline skrypt, nie tutaj 2080 this.cookieConsentIntegrationEnabled = false; 2081 this.strings = bootstrapData.strings || {}; 2082 this.tooltipShown = false; 2083 this.lastActivity = Date.now(); 2084 this.autoCloseTimeout = null; 2085 this.unreadCount = 0; 2086 this.chatHistoryLoaded = false; // Flaga zapobiegajÄ ca wielokrotnemu Åadowaniu 2087 this.persistedMessages = []; 2088 this.assistantPersonaTimer = null; 2089 this.animatedQuestions = []; 2090 this.currentQuestionIndex = 0; 2091 this.questionInterval = null; 2092 this.animatedQuestionsContainer = null; 2093 this.baseAnimatedQuestions = []; 2094 this.customAnimatedQuestion = null; 2095 this.customAnimatedQuestionKey = "bb_asystent_ai_custom_question"; 2096 this.customAnimatedQuestionToggleKey = 2097 "bb_asystent_ai_custom_question_enabled"; 2098 this.facetSelections = {}; 2099 this.facetMeta = null; 2100 this.nekrologContext = null; 2101 this.readAcknowledgementArmed = false; 2102 this.readAcknowledgementListeners = []; 2103 this.markReadInFlight = false; 2104 this.lastMarkReadTime = null; 2105 this.markReadPending = false; 2106 this.handleMobileLayoutRefresh = () => { 2107 this.applyMobileBubbleLayout(); 2108 this.updateChatWindowOffset(); 2109 this.updateChatWindowSize(); // Aktualizuj wymiary przy resize 2110 }; 2111 this.isFullscreen = false; 2112 this.menuEventsBound = false; 2113 this.welcomeMessageShown = false; 2114 this.lastWelcomeSession = null; 2115 this.goodbyeMessageShown = false; 2116 this.lastGoodbyeSession = null; 2117 this._topPinnedAssistantMessages = new WeakSet(); 2118 2119 // Debounced wersja scrollToBottom dla streamingu - zapobiega nadmiernemu scrollowaniu 2120 this._debouncedScrollToBottom = debounce(() => { 2121 this.scrollToBottom(); 2122 }, 50); 2123 2124 // Cache dla referencji DOM - unikamy wielokrotnego querySelector 2125 this._cachedDOM = { 2126 messagesContainer: null, 2127 chatWindow: null, 2128 lastCacheTime: 0 2129 }; 2130 2131 this.markdownFormatter = new MarkdownFormatter( 2132 typeof window !== "undefined" ? window.marked || null : null, 2133 this.escapeHtml.bind(this), 2134 ); 2135 this.classicMode = new ClassicChatMode({ 2136 ajaxUrl: this.ajaxUrl, 2137 nonce: bootstrapData.nonce, 2138 }); 2139 this.streamingMode = new StreamingChatMode(this, { 2140 restUrl: this.restUrl, 2141 sessionId: this.sessionId, 2142 }); 2143 2144 debugLog( 2145 "FUNERAL DEBUG: Constructor - initial sessionId:", 2146 this.sessionId, 2147 ); 2148 2149 this.restoreSessionId(); 2150 if (this.streamingMode) { 2151 this.streamingMode.setSessionId(this.sessionId); 2152 } 2153 this.restorePersistedMessages(); 2154 2155 // Cookie consent sprawdzany jest przez inline skrypt - nie trzeba tutaj 2156 // Inicjalizuj czat normalnie 2157 if (document.readyState === 'loading') { 2158 document.addEventListener('DOMContentLoaded', () => { 2159 debugLog("FUNERAL DEBUG: DOMContentLoaded - initializing chat"); 2160 this.init(); 2161 }); 2162 } else { 2163 debugLog("FUNERAL DEBUG: DOM already loaded - initializing chat"); 2164 this.init(); 2165 } 2166 } 2167 2168 getAjaxUrl(action = "") { 2169 const base = this.ajaxUrl || bootstrapData.ajaxUrl || ""; 2170 if (!action || !base) { 2171 return base; 2172 } 2173 const hasActionParam = 2174 typeof URL === "function" && 2175 (() => { 2176 try { 2177 const url = new URL(base, window.location.origin); 2178 return url.searchParams.has("action"); 2179 } catch (e) { 2180 return false;
2181 } 2182 })(); 2183 2184 if (hasActionParam) { 2185 return base; 2186 } 2187 2188 const separator = base.includes("?") ? "&" : "?"; 2189 return `${base}${separator}action=${encodeURIComponent(action)}`; 2190 } 2191 2192 restoreSessionId() { 2193 try { 2194 const cookieSession = this.getCookie("bb_asystent_ai_session"); 2195 const storedSession = localStorage.getItem( 2196 "bb_asystent_ai_session_id", 2197 ); 2198 2199 if (!this.sessionId && cookieSession) { 2200 this.sessionId = cookieSession; 2201 debugLog( 2202 "FUNERAL DEBUG: SessionId restored from cookie:", 2203 this.sessionId, 2204 ); 2205 if (this.streamingMode) { 2206 this.streamingMode.setSessionId(this.sessionId); 2207 } 2208 } 2209 2210 if (!this.sessionId && storedSession) { 2211 this.sessionId = storedSession; 2212 debugLog( 2213 "FUNERAL DEBUG: SessionId restored from localStorage backup:", 2214 this.sessionId, 2215 ); 2216 if (this.streamingMode) { 2217 this.streamingMode.setSessionId(this.sessionId); 2218 } 2219 } 2220 2221 if (this.sessionId) { 2222 localStorage.setItem( 2223 "bb_asystent_ai_session_id", 2224 this.sessionId, 2225 ); 2226 if (this.streamingMode) { 2227 this.streamingMode.setSessionId(this.sessionId); 2228 } 2229 } 2230 } catch (error) { 2231 debugWarn( 2232 "FUNERAL DEBUG: Failed to restore session ID:", 2233 error, 2234 ); 2235 } 2236 } 2237 2238 getCookie(name) { 2239 const match = document.cookie.match( 2240 new RegExp( 2241 "(?:^|; )" + 2242 name.replace(/([.$?*|{}()\[\]\\\/\+^])/g, "\\$1") + 2243 "=([^;]*)", 2244 ), 2245 ); 2246 return match ? decodeURIComponent(match[1]) : null; 2247 } 2248 2249 restorePersistedMessages() { 2250 try { 2251 const raw = localStorage.getItem("bb_asystent_ai_messages"); 2252 if (!raw) { 2253 return; 2254 } 2255 2256 const parsed = JSON.parse(raw); 2257 if (!parsed || !Array.isArray(parsed.messages)) { 2258 return; 2259 } 2260 2261 // Sprawdź czy sesja siÄ zmieniÅa (wygasÅa lub nowa) 2262 if (parsed.sessionId !== this.sessionId) { 2263 debugLog( 2264 "FUNERAL DEBUG: Session changed - old:", 2265 parsed.sessionId, 2266 "new:", 2267 this.sessionId, 2268 ); 2269 // WyczyÅÄ stare wiadomoÅci - to nowa sesja 2270 localStorage.removeItem("bb_asystent_ai_messages"); 2271 this.persistedMessages = []; 2272 return; 2273 } 2274 2275 // Sprawdź czy sÄ poprawne wiadomoÅci (czy nie ma [object Object]) 2276 const hasInvalidMessages = parsed.messages.some((msg) => { 2277 const isInvalid = 2278 typeof msg.content === "object" || 2279 (typeof msg.content === "string" && 2280 msg.content.includes("[object Object]")); 2281 if (isInvalid) { 2282 debugWarn( 2283 "FUNERAL DEBUG: Found invalid message in localStorage:", 2284 msg, 2285 ); 2286 } 2287 return isInvalid; 2288 }); 2289 2290 if (hasInvalidMessages) { 2291 debugWarn( 2292 "FUNERAL DEBUG: Invalid messages found in localStorage - clearing cache", 2293 ); 2294 localStorage.removeItem("bb_asystent_ai_messages"); 2295 this.persistedMessages = []; 2296 return; 2297 } 2298 2299 this.persistedMessages = parsed.messages; 2300 debugLog( 2301 "FUNERAL DEBUG: Loaded", 2302 parsed.messages.length,
2303 "messages from localStorage cache", 2304 ); 2305 } catch (error) { 2306 debugWarn( 2307 "FUNERAL DEBUG: Failed to restore persisted messages:", 2308 error, 2309 ); 2310 } 2311 } 2312 2313 persistMessagesCache() { 2314 try { 2315 if (!this.sessionId) { 2316 return; 2317 } 2318 2319 const messagesContainer = document.getElementById( 2320 "bb-asystent-ai-messages", 2321 ); 2322 if (!messagesContainer) { 2323 return; 2324 } 2325 2326 const decodeHtml = (value) => { 2327 try { 2328 const textarea = document.createElement("textarea"); 2329 textarea.innerHTML = value || ""; 2330 return textarea.value || ""; 2331 } catch (e) { 2332 return value || ""; 2333 } 2334 }; 2335 2336 const htmlToPlainText = (html) => { 2337 let text = (html || "").toString(); 2338 // Preserve line breaks across reloads. This also fixes old caches where "<br>" 2339 // was accidentally escaped and stored as "<br>". 2340 text = decodeHtml(text); 2341 text = text.replace(/<br\s*\/?>/gi, "\n"); 2342 text = text.replace(/<\/p>\s*<p[^>]*>/gi, "\n\n"); 2343 text = text.replace(/<\/?p[^>]*>/gi, ""); 2344 text = text.replace(/<[^>]+>/g, ""); 2345 text = text.replace(/\r\n/g, "\n"); 2346 text = text.replace(/\n{3,}/g, "\n\n"); 2347 return text.trim(); 2348 }; 2349 2350 const items = Array.from( 2351 messagesContainer.querySelectorAll( 2352 ".bb-asystent-ai-message", 2353 ), 2354 ).map((node) => { 2355 // Tool-call entries serializujemy z osobnym typem i payloadem 2356 if (node.classList.contains("tool-call")) { 2357 let parsed = null; 2358 try { 2359 parsed = node.dataset.toolPayload 2360 ? JSON.parse(node.dataset.toolPayload) 2361 : null; 2362 } catch (e) { 2363 parsed = null; 2364 } 2365 return { 2366 type: "tool_call", 2367 content: parsed?.summary || "", 2368 tool_call: parsed, 2369 timestamp: "", 2370 messageId: null, 2371 }; 2372 } 2373 2374 const isAssistant = node.classList.contains("assistant"); 2375 const contentNode = node.querySelector( 2376 ".bb-asystent-ai-message-content", 2377 ); 2378 const rawHtml = contentNode?.innerHTML || node.innerHTML; 2379 2380 return { 2381 type: isAssistant ? "assistant" : "user", 2382 // For user messages, store plain text, not innerHTML, otherwise <br> accumulates 2383 // and gets double-escaped after refresh. 2384 content: isAssistant ? rawHtml : htmlToPlainText(rawHtml), 2385 timestamp: 2386 node.querySelector(".bb-asystent-ai-message-time") 2387 ?.textContent || "", 2388 messageId: node.dataset.messageId || null, 2389 }; 2390 }); 2391 2392 localStorage.setItem( 2393 "bb_asystent_ai_messages", 2394 JSON.stringify({ 2395 sessionId: this.sessionId, 2396 messages: items, 2397 }), 2398 ); 2399 } catch (error) { 2400 debugWarn( 2401 "FUNERAL DEBUG: Failed to persist messages cache:", 2402 error, 2403 ); 2404 } 2405 } 2406 2407 init() { 2408 debugLog("FUNERAL DEBUG: init() called"); 2409 2410 this.bindEvents(); 2411 this.bindAiCopyGuard(); 2412 this.setupKeyboardShortcuts(); 2413 this.loadChatHistory(); 2414 // NIE ÅADUJ FAQ TUTAJ - sÄ Åadowane warunkowo w loadChatHistory() 2415 // this.loadQuickQuestions(); // Dodana obsÅuga predefiniowanych pytaÅ 2416 this.initTooltip(); 2417 this.setupReminders(); 2418 this.setupAutoClose(); 2419 this.setupBubbleAnimations(); 2420 // Delay dla animowanych pytaŠżeby DOM byÅ gotowy 2421 setTimeout(() => { 2422 this.initAnimatedQuestions(); 2423 }, 1000); 2424 this.applyDynamicStyles(); 2425 this.updateChatWindowSize(); 2426 this.setupMobileLayout(); 2427 this.updateChatWindowOffset(); 2428 2429 debugLog("FUNERAL DEBUG: init() completed"); 2430 2431 if (!this.settings.tooltipEnabled) { 2432 const tooltip = document.getElementById( 2433 "bb-asystent-ai-tooltip", 2434 ); 2435 if (tooltip) { 2436 tooltip.remove(); 2437 } 2438 } 2439 2440 this.isInitialized = true; 2441 } 2442 2443 initTooltip() { 2444 // Sprawdź czy tooltip jest wÅÄ czony 2445 if (!this.settings.tooltipEnabled) { 2446 return; 2447 } 2448 2449 // Pastylka z animowanymi pytaniami nie blokuje tooltipa â pozycjÄ ogarnia CSS 2450 const bubble = document.getElementById("bb-asystent-ai-bubble"); 2451 if (bubble && bubble.classList.contains("has-animated-text")) { 2452 debugLog("FUNERAL DEBUG: Tooltip enabled for animated pill"); 2453 // Nie przerywamy â pozwalamy na normalne wyÅwietlenie tooltipa 2454 } 2455
2456 const delay = (this.settings.tooltipDelay || 3) * 1000; 2457 const duration = (this.settings.tooltipDuration || 8) * 1000; 2458 2459 // Pokaż tooltip po okreÅlonym opóźnieniu 2460 setTimeout(() => { 2461 if (!this.isOpen && !this.tooltipShown) { 2462 this.showTooltip(); 2463 } 2464 }, delay); 2465 2466 // Ukryj tooltip po okreÅlonym czasie (jeÅli duration > 0) 2467 if (duration > 0) { 2468 setTimeout(() => { 2469 this.hideTooltip(); 2470 }, delay + duration); 2471 } 2472 2473 // Setup reminders 2474 this.setupReminders(); 2475 } 2476 2477 setupReminders() { 2478 if ( 2479 !this.settings.reminderEnabled || 2480 !this.settings.tooltipEnabled 2481 ) { 2482 return; 2483 } 2484 2485 const reminderDelay = (this.settings.reminderDelay || 2) * 60000; // Convert to milliseconds 2486 2487 setInterval(() => { 2488 // Sprawdź czy użytkownik nie jest aktywny w czacie 2489 if ( 2490 !this.isOpen && 2491 !this.tooltipShown && 2492 this.hasUnreadMessages() 2493 ) { 2494 this.showReminder(); 2495 } 2496 }, reminderDelay); 2497 } 2498 2499 hasUnreadMessages() { 2500 // Prosta logika - w przyszÅoÅci można rozszerzyÄ o prawdziwe sprawdzanie nieprzeczytanych wiadomoÅci 2501 return ( 2502 document.getElementById("bb-asystent-ai-badge").style 2503 .display !== "none" 2504 ); 2505 } 2506 2507 showReminder() { 2508 if (!this.settings.tooltipEnabled) { 2509 return; 2510 } 2511 // Pokaż tooltip jako przypomnienie 2512 this.showTooltip(); 2513 2514 // Dodaj efekt pulsowania do bubble 2515 const bubble = document.getElementById("bb-asystent-ai-bubble"); 2516 if (bubble) { 2517 bubble.classList.add("has-notification"); 2518 setTimeout(() => { 2519 bubble.classList.remove("has-notification"); 2520 }, 3000); 2521 } 2522 } 2523 2524 showTooltip() { 2525 if (!this.settings.tooltipEnabled) { 2526 return; 2527 } 2528 const tooltip = document.getElementById("bb-asystent-ai-tooltip"); 2529 if (tooltip && !this.tooltipShown) { 2530 // Aktualizuj tekst tooltipa 2531 const tooltipTextElement = 2532 tooltip.querySelector(".tooltip-text"); 2533 if (tooltipTextElement && this.strings.tooltipText) { 2534 tooltipTextElement.textContent = this.strings.tooltipText; 2535 } 2536 2537 tooltip.classList.add("bb-ai-show"); 2538 this.tooltipShown = true; 2539 } 2540 } 2541 2542 hideTooltip() { 2543 const tooltip = document.getElementById("bb-asystent-ai-tooltip"); 2544 if (tooltip) { 2545 // Zatrzymaj wszystkie animacje CSS 2546 tooltip.style.animation = "none"; 2547 tooltip.style.transition = "none"; 2548 2549 // UsuÅ klasy i natychmiast ukryj 2550 tooltip.classList.remove("bb-ai-show"); 2551 tooltip.classList.add("hide"); 2552 2553 // Natychmiast ukryj tooltip 2554 tooltip.style.opacity = "0"; 2555 tooltip.style.visibility = "hidden"; 2556 tooltip.style.transform = "translateY(50%) translateX(20px)"; 2557 2558 this.tooltipShown = true; 2559 2560 // PrzywrÃ³Ä transition po krótkiej chwili dla przyszÅych animacji 2561 setTimeout(() => { 2562 if (tooltip) { 2563 tooltip.style.transition = ""; 2564 } 2565 }, 100); 2566 } 2567 } 2568 2569 hideBadge() { 2570 const badge = document.getElementById("bb-asystent-ai-badge"); 2571 if (badge) { 2572 badge.classList.remove("bb-ai-show", "animate-in"); 2573 badge.style.display = "none"; 2574 badge.style.opacity = "0"; 2575 } 2576 } 2577 2578 showBadge() { 2579 const badge = document.getElementById("bb-asystent-ai-badge"); 2580 if (badge && this.settings.notificationBadgeEnabled) { 2581 badge.style.backgroundColor = 2582 this.settings.notificationBadgeColor || "#e74c3c"; 2583 badge.textContent = 2584 this.settings.notificationBadgeNumber || "1"; 2585 badge.style.display = "flex"; 2586 badge.classList.add("bb-ai-show"); 2587 2588 // Dodaj animacjÄ wejÅcia z opóźnieniem 2589 setTimeout(() => { 2590 badge.classList.add("animate-in"); 2591 }, 100); 2592 } 2593 } 2594 2595 updateBadgeVisibility() { 2596 if (this.settings.notificationBadgeEnabled) { 2597 this.showBadge(); 2598 } else { 2599 this.hideBadge(); 2600 } 2601 } 2602 2603 async getUnreadCount() { 2604 try { 2605 // Nie sprawdzaj nieprzeczytanych jeÅli chat jest otwarty 2606 if (this.isOpen) {
2607 debugLog("Chat is open, skipping unread count check"); 2608 if (this.unreadCount > 0) { 2609 this.updateBadgeWithCount(); 2610 this.armReadAcknowledgementIfNeeded(); 2611 } 2612 return this.unreadCount; 2613 } 2614 2615 const action = "bbai_ai_get_unread_count"; 2616 const url = this.getAjaxUrl(action); 2617 const formData = new FormData(); 2618 formData.append("action", action); 2619 formData.append("session_id", this.sessionId); 2620 formData.append("nonce", BBAsystentAI.nonce); 2621 2622 const response = await fetch(url, { 2623 method: "POST", 2624 body: formData, 2625 }); 2626 2627 if (!response.ok) { 2628 if (response.status === 403) { 2629 debugWarn( 2630 "Unread count request unauthorized (403), using 0", 2631 ); 2632 this.unreadCount = 0; 2633 this.hideBadge(); 2634 return 0; 2635 } 2636 throw new Error(`HTTP error! status: ${response.status}`); 2637 } 2638 2639 const result = await response.json(); 2640 2641 if (result.success) { 2642 this.unreadCount = result.data.unread_count; 2643 debugLog("Unread count:", this.unreadCount); 2644 2645 this.updateBadgeWithCount(); 2646 if (this.unreadCount > 0) { 2647 this.armReadAcknowledgementIfNeeded(); 2648 } 2649 return this.unreadCount; 2650 } else { 2651 debugLog("Error getting unread count:", result.data); 2652 // W przypadku bÅÄdu, ukryj badge 2653 this.hideBadge(); 2654 } 2655 } catch (error) { 2656 debugError("Error getting unread count:", error); 2657 // W przypadku bÅÄdu, ukryj badge 2658 this.hideBadge(); 2659 } 2660 return 0; 2661 } 2662 2663 armReadAcknowledgementIfNeeded() { 2664 if ( 2665 !this.isOpen || 2666 this.unreadCount <= 0 || 2667 this.readAcknowledgementArmed 2668 ) { 2669 return; 2670 } 2671 2672 const chatWindow = document.getElementById( 2673 "bb-asystent-ai-window", 2674 ); 2675 if (!chatWindow) { 2676 return; 2677 } 2678 2679 const messages = document.getElementById( 2680 "bb-asystent-ai-messages", 2681 ); 2682 const input = document.getElementById("bb-asystent-ai-input"); 2683 const footer = document.getElementById("bb-asystent-ai-form"); 2684 2685 const acknowledge = () => { 2686 this.markMessagesAsRead(); 2687 }; 2688 2689 const listeners = []; 2690 const attach = (target, event, options) => { 2691 if (!target) { 2692 return; 2693 } 2694 const handler = () => { 2695 acknowledge(); 2696 }; 2697 if (options) { 2698 target.addEventListener(event, handler, options); 2699 } else { 2700 target.addEventListener(event, handler); 2701 } 2702 listeners.push({ target, event, handler, options }); 2703 }; 2704 2705 const passive = { passive: true }; 2706 attach(chatWindow, "mousemove", passive); 2707 attach(chatWindow, "click"); 2708 attach(chatWindow, "touchstart", passive); 2709 attach(chatWindow, "keydown"); 2710 attach(chatWindow, "wheel", passive); 2711 attach(messages, "scroll", passive); 2712 attach(input, "focus"); 2713 attach(input, "input"); 2714 attach(footer, "submit"); 2715 2716 if (listeners.length === 0) { 2717 return; 2718 } 2719 2720 this.readAcknowledgementListeners = listeners; 2721 this.readAcknowledgementArmed = true; 2722 } 2723 2724 removeReadAcknowledgement() { 2725 if (!this.readAcknowledgementArmed) { 2726 return; 2727 }
2728 this.readAcknowledgementListeners.forEach( 2729 ({ target, event, handler }) => { 2730 if (target) { 2731 target.removeEventListener(event, handler); 2732 } 2733 }, 2734 ); 2735 this.readAcknowledgementListeners = []; 2736 this.readAcknowledgementArmed = false; 2737 } 2738 2739 async markMessagesAsRead() { 2740 if (this.unreadCount <= 0 || this.markReadInFlight) { 2741 return; 2742 } 2743 2744 // Throttle: nie wywoÅuj czÄÅciej niż raz na 2 sekundy 2745 const now = Date.now(); 2746 if (this.lastMarkReadTime && (now - this.lastMarkReadTime) < 2000) { 2747 // Zaplanuj wywoÅanie po zakoÅczeniu throttlingu 2748 if (!this.markReadPending) { 2749 this.markReadPending = true; 2750 setTimeout(() => { 2751 this.markReadPending = false; 2752 if (this.unreadCount > 0 && !this.markReadInFlight) { 2753 this.markMessagesAsRead(); 2754 } 2755 }, 2000 - (now - this.lastMarkReadTime)); 2756 } 2757 return; 2758 } 2759 2760 this.removeReadAcknowledgement(); 2761 this.markReadInFlight = true; 2762 this.lastMarkReadTime = now; 2763 2764 try { 2765 const action = "bbai_ai_mark_messages_read"; 2766 const url = this.getAjaxUrl(action); 2767 const formData = new FormData(); 2768 formData.append("action", action); 2769 formData.append("session_id", this.sessionId); 2770 formData.append("nonce", BBAsystentAI.nonce); 2771 2772 const response = await fetch(url, { 2773 method: "POST", 2774 body: formData, 2775 credentials: "same-origin", // WysyÅaj cookies z Å¼Ä daniem 2776 }); 2777 2778 // Sprawdź status odpowiedzi przed parsowaniem JSON 2779 if (!response.ok) { 2780 2781 throw new Error(`HTTP error! status: ${response.status}`); 2782 } 2783 2784 const result = await response.json(); 2785 2786 if (result.success) { 2787 this.unreadCount = 0; 2788 this.updateBadgeWithCount(); 2789 debugLog("Messages marked as read"); 2790 } else { 2791 debugWarn("Failed to mark messages as read:", result.data); 2792 this.armReadAcknowledgementIfNeeded(); 2793 } 2794 } catch (error) { 2795 // BÅÄ d 403 dla endpointu pomocniczego nie powinien "haÅasowaÄ" jak bÅÄ d krytyczny. 2796 if (error.message && error.message.includes("403")) { 2797 debugWarn( 2798 "Mark messages read unauthorized (403), skipping.", 2799 ); 2800 this.markReadInFlight = false; 2801 return; 2802 } 2803 debugError("Error marking messages as read:", error); 2804 this.armReadAcknowledgementIfNeeded(); 2805 } finally { 2806 this.markReadInFlight = false; 2807 } 2808 } 2809 2810 updateBadgeWithCount() { 2811 const badge = document.getElementById("bb-asystent-ai-badge"); 2812 if (!badge || !this.settings.notificationBadgeEnabled) { 2813 return; 2814 } 2815 2816 if (this.unreadCount > 0) { 2817 badge.style.backgroundColor = 2818 this.settings.notificationBadgeColor || "#e74c3c"; 2819 badge.textContent = 2820 this.unreadCount > 99 ? "99+" : this.unreadCount.toString(); 2821 badge.style.display = "flex"; 2822 badge.classList.add("bb-ai-show"); 2823 2824 // Dodaj animacjÄ tylko jeÅli badge nie byÅ już widoczny 2825 if (!badge.classList.contains("animate-in")) { 2826 setTimeout(() => { 2827 badge.classList.add("animate-in"); 2828 }, 100); 2829 } 2830 } else { 2831 badge.classList.remove("bb-ai-show", "animate-in"); 2832 badge.style.display = "none"; 2833 badge.style.opacity = "0"; 2834 this.removeReadAcknowledgement(); 2835 } 2836 } 2837 2838 setupAutoClose() { 2839 if (!this.settings.autoCloseEnabled || !this.isOpen) { 2840 return; 2841 } 2842 2843 // WyczyÅÄ poprzedni timeout 2844 if (this.autoCloseTimeout) { 2845 clearTimeout(this.autoCloseTimeout); 2846 } 2847
2848 const delay = (this.settings.autoCloseDelay || 5) * 60000; // Convert to milliseconds 2849 2850 this.autoCloseTimeout = setTimeout(() => { 2851 if (this.isOpen && Date.now() - this.lastActivity >= delay) { 2852 this.sendGoodbyeMessage(); 2853 } 2854 }, delay); 2855 } 2856 2857 resetAutoCloseTimer() { 2858 this.lastActivity = Date.now(); 2859 this.setupAutoClose(); 2860 } 2861 2862 async sendGoodbyeMessage() { 2863 if (this.addGoodbyeMessageOnce(this.strings.goodbyeMessage)) { 2864 // ZakoÅcz sesjÄ w bazie danych 2865 try { 2866 const action = "bbai_ai_end_session"; 2867 const url = this.getAjaxUrl(action); 2868 const formData = new FormData(); 2869 formData.append("action", action); 2870 formData.append("session_id", this.sessionId); 2871 formData.append("nonce", BBAsystentAI.nonce); 2872 2873 await fetch(url, { 2874 method: "POST", 2875 body: formData, 2876 }); 2877 2878 this.trackEvent("chat_auto_closed"); 2879 } catch (error) { 2880 debugError("Error ending session:", error); 2881 } 2882 2883 // Po 3 sekundach zamknij chat i rozpocznij nowÄ sesjÄ 2884 setTimeout(() => { 2885 this.closeChat(); 2886 // Po zamkniÄciu, przygotuj nowÄ sesjÄ na przyszÅoÅÄ 2887 this.prepareNewSessionForNextOpen(); 2888 }, 3000); 2889 } else { 2890 // JeÅli nie ma wiadomoÅci pożegnalnej, zamknij od razu 2891 this.closeChat(); 2892 this.prepareNewSessionForNextOpen(); 2893 } 2894 } 2895 2896 bindEvents() { 2897 const bubble = document.getElementById("bb-asystent-ai-bubble"); 2898 const minimizeBtn = document.getElementById( 2899 "bb-asystent-ai-minimize", 2900 ); 2901 const form = document.getElementById("bb-asystent-ai-form"); 2902 const input = document.getElementById("bb-asystent-ai-input"); 2903 const tooltip = document.getElementById("bb-asystent-ai-tooltip"); 2904 const fullscreenToggle = document.getElementById( 2905 "bb-asystent-ai-fullscreen-toggle", 2906 ); 2907 const menuTrigger = document.getElementById( 2908 "bb-asystent-ai-menu-trigger", 2909 ); 2910 this.setupMenuDelegates(); 2911 2912 if (menuTrigger) { 2913 menuTrigger.addEventListener("click", (event) => { 2914 event.preventDefault(); 2915 event.stopPropagation(); 2916 this.toggleMenu(); 2917 }); 2918 } 2919 2920 if (bubble) { 2921 bubble.addEventListener("click", (event) => { 2922 // Zapobiegaj przypadkowemu otwieraniu linków (np. gdy widget jest w <a>) 2923 if (event) { 2924 event.preventDefault(); 2925 event.stopPropagation(); 2926 } 2927 this.toggleChat(); 2928 }); 2929 } 2930 2931 if (minimizeBtn) { 2932 minimizeBtn.addEventListener("click", () => this.closeChat()); 2933 } 2934 2935 if (form) { 2936 form.addEventListener("submit", (e) => { 2937 e.preventDefault(); 2938 this.sendMessage(); 2939 }); 2940 } 2941 2942 if (input) { 2943 input.addEventListener("keypress", (e) => { 2944 if (e.key === "Enter" && !e.shiftKey) { 2945 e.preventDefault(); 2946 this.sendMessage(); 2947 } 2948 }); 2949 2950 input.addEventListener("input", () => { 2951 this.autoResizeInput(); 2952 this.resetAutoCloseTimer(); 2953 }); 2954 2955 input.addEventListener("focus", () => { 2956 this.resetAutoCloseTimer(); 2957 }); 2958 } 2959 2960 if (tooltip) { 2961 tooltip.addEventListener("click", (event) => { 2962 // Tak samo jak dla bubble â nie pozwalaj dziedziczÄ cym linkom wykonaÄ nawigacji 2963 if (event) { 2964 event.preventDefault(); 2965 event.stopPropagation(); 2966 } 2967 this.hideTooltip(); 2968 this.openChat(); 2969 }); 2970 } 2971 2972 if (fullscreenToggle) { 2973 fullscreenToggle.addEventListener("click", (event) => { 2974 event.stopPropagation(); 2975 event.preventDefault(); 2976 this.toggleFullscreen(); 2977 }); 2978 } 2979 2980 document.addEventListener("keydown", (event) => { 2981 if (event.key === "Escape" && this.isFullscreen) { 2982 this.exitFullscreen(); 2983 } 2984 if (event.key === "Escape") { 2985 this.closeMenu(); 2986 } 2987 }); 2988 2989 // Activity tracking 2990 document.addEventListener("mousemove", () => { 2991 this.lastActivity = Date.now(); 2992 }); 2993 2994 document.addEventListener("keypress", () => { 2995 this.lastActivity = Date.now(); 2996 }); 2997 } 2998 2999 /** 3000 * ZwrÃ³Ä element docelowy zdarzenia klikniÄcia w bezpieczny sposób. 3001 */ 3002 getEventTargetElement(event) { 3003 if (!event) { 3004 return null; 3005 } 3006 const target = event.target || event.srcElement || null; 3007 return target instanceof Element ? target : null; 3008 } 3009 3010 /** 3011 * Znajdź najbliższy element pasujÄ cy do selektora, zaczynajÄ c od podanego wÄzÅa. 3012 */ 3013 findClosestMatch(element, selector) { 3014 if (!element || !(element instanceof Element)) { 3015 return null; 3016 } 3017 if (typeof element.closest === "function") { 3018 return element.closest(selector); 3019 } 3020 // Fallback dla bardzo starych przeglÄ darek â przejÅcie w górÄ drzewa DOM 3021 let node = element; 3022 while (node) { 3023 if (node.matches && node.matches(selector)) { 3024 return node; 3025 } 3026 node = node.parentElement; 3027 } 3028 return null; 3029 } 3030 3031 setupMenuDelegates() { 3032 if (this.menuEventsBound) { 3033 return; 3034 } 3035 3036 this.menuEventsBound = true; 3037 3038 this._menuDocumentHandler = (event) => { 3039 const menu = document.getElementById("bb-asystent-ai-menu"); 3040 const trigger = document.getElementById( 3041 "bb-asystent-ai-menu-trigger", 3042 ); 3043 const target = this.getEventTargetElement(event); 3044 const wrapper = 3045 trigger?.closest(".bb-asystent-ai-menu-wrapper") || null; 3046 3047 if (!menu || !trigger || !target || !wrapper) { 3048 return; 3049 } 3050 3051 const isOpen = 3052 menu.classList.contains("bb-ai-show") || 3053 trigger.getAttribute("aria-expanded") === "true"; 3054 if (!isOpen) { 3055 return; 3056 } 3057 3058 // Klik w menu lub jego przyciski â obsÅuż akcje i nie zamykaj przez document handler. 3059 if (wrapper.contains(target)) { 3060 if (this.findClosestMatch(target, "#bb-asystent-ai-view-history")) { 3061 event.preventDefault(); 3062 this.showChatHistory(); 3063 this.closeMenu(); 3064 return; 3065 } 3066 if (this.findClosestMatch(target, "#bb-asystent-ai-new-chat")) { 3067 event.preventDefault(); 3068 this.startNewChat(); 3069 this.closeMenu(); 3070 return; 3071 } 3072 if (this.findClosestMatch(target, "#bb-asystent-ai-end-chat")) { 3073 event.preventDefault(); 3074 this.endCurrentChat(); 3075 this.closeMenu(); 3076 return; 3077 } 3078 return; // klik w obrÄbie wrappera (np. w przycisk) â nic wiÄcej. 3079 } 3080 3081 // Klik poza menu â zamknij. 3082 this.closeMenu(); 3083 }; 3084 3085 // NasÅuch w fazie bubble, żeby nie kolidowaÄ z innymi capture handlerami. 3086 document.addEventListener("click", this._menuDocumentHandler, false); 3087 3088 window.addEventListener("blur", () => { 3089 this.closeMenu(); 3090 }); 3091 } 3092 3093 setupKeyboardShortcuts() { 3094 // Ctrl/Cmd + K to open chat 3095 document.addEventListener("keydown", (e) => { 3096 if ((e.ctrlKey || e.metaKey) && e.key === "k") { 3097 e.preventDefault(); 3098 this.openChat(); 3099 } 3100 }); 3101 } 3102 3103 toggleChat() { 3104 // Cookie consent sprawdzany jest przez inline skrypt - jeÅli tutaj doszliÅmy, to zgoda jest OK 3105 if (this.isOpen) { 3106 this.closeChat(); 3107 } else { 3108 this.openChat(); 3109 // Zatrzymaj animacjÄ pytaÅ gdy chat siÄ otwiera 3110 this.stopQuestionAnimation(); 3111 // Dodaj klasÄ chat-open do pastylki 3112 this.updatePillState(true); 3113 } 3114 } 3115 3116 openChat() { 3117 const window = document.getElementById("bb-asystent-ai-window"); 3118 const bubble = document.getElementById("bb-asystent-ai-bubble"); 3119 const widget = document.getElementById("bb-asystent-ai-widget"); 3120 3121 if (!window || !bubble) return; 3122 3123 // Ukryj tooltip natychmiast gdy czat siÄ otwiera 3124 this.hideTooltip(); 3125 3126 // Ukryj awatar i online indicator gdy chat siÄ otwiera 3127 this.hideBubbleAvatar(); 3128 3129 // Dodatkowe ukrycie tooltipa przez usuniÄcie z DOM po krótkim czasie 3130 setTimeout(() => { 3131 const tooltip = document.getElementById( 3132 "bb-asystent-ai-tooltip", 3133 ); 3134 if (tooltip) { 3135 tooltip.style.display = "none"; 3136 } 3137 }, 50); 3138 3139 if (widget) { 3140 widget.style.zIndex = "2147483647"; 3141 } 3142 3143 window.style.display = "flex"; 3144 setTimeout(() => { 3145 window.classList.add("bb-ai-show"); 3146 bubble.classList.add("active"); 3147 // Ensure CSS hides bubble avatar when chat is active 3148 (function ensureBubbleActiveStyle() { 3149 const styleId = "fac-bubble-active-style"; 3150 if (!document.getElementById(styleId)) { 3151 const s = document.createElement("style"); 3152 s.id = styleId; 3153 s.textContent = 3154 ".bb-asystent-ai-bubble.active .bubble-avatar{display:none!important;}"; 3155 document.head.appendChild(s); 3156 } 3157 })(); 3158 3159 // Auto-scroll to newest message after chat becomes visible 3160 this.scrollToBottom(); 3161 3162 // Nie Åaduj historii tutaj - jest już zaÅadowana podczas inicjalizacji 3163 // this.loadChatHistory(); // USUNIÄTE - powodowaÅo duplikacjÄ 3164 }, 50); 3165 3166 this.isOpen = true; 3167 this.applyMobileBubbleLayout(); 3168 this.updateChatWindowOffset(); 3169 this.updateChatWindowSize(); // Aktualizuj wymiary przy otwieraniu 3170 this.armReadAcknowledgementIfNeeded(); 3171 3172 // Auto-close timer 3173 this.resetAutoCloseTimer(); 3174 3175 // Fokus na input 3176 setTimeout(() => { 3177 const input = document.getElementById("bb-asystent-ai-input"); 3178 if (input) { 3179 const supportsMatchMedia = 3180 typeof window.matchMedia === "function"; 3181 const isMobile = 3182 supportsMatchMedia && 3183 window.matchMedia("(max-width: 768px)").matches; 3184 if (!isMobile) { 3185 input.focus(); 3186 } 3187 } 3188 }, 100); 3189 } 3190 3191 closeChat() { 3192 const window = document.getElementById("bb-asystent-ai-window"); 3193 const bubble = document.getElementById("bb-asystent-ai-bubble"); 3194 const widget = document.getElementById("bb-asystent-ai-widget"); 3195 3196 if (!window || !bubble) return; 3197 3198 this.removeReadAcknowledgement(); 3199 3200 window.classList.remove("bb-ai-show"); 3201 bubble.classList.remove("active"); 3202 3203 // UsuÅ klasÄ chat-open z pastylki 3204 this.updatePillState(false); 3205 3206 // Pokaż awatar i online indicator gdy chat siÄ zamyka 3207 this.showBubbleAvatar(); 3208 3209 setTimeout(() => { 3210 window.style.display = "none"; 3211 3212 // PrzywrÃ³Ä tooltip do normalnego stanu 3213 const tooltip = document.getElementById( 3214 "bb-asystent-ai-tooltip", 3215 ); 3216 if (tooltip && this.settings.tooltipEnabled) { 3217 tooltip.style.display = ""; 3218 tooltip.style.animation = ""; 3219 tooltip.style.transition = ""; 3220 } 3221 }, 300); 3222 3223 this.isOpen = false;
3224 if (widget) { 3225 widget.style.zIndex = ""; 3226 } 3227 if (this.isFullscreen) { 3228 this.exitFullscreen(); 3229 } 3230 this.applyMobileBubbleLayout(); 3231 this.updateChatWindowOffset(); 3232 3233 // WyczyÅÄ auto-close timer 3234 if (this.autoCloseTimeout) { 3235 clearTimeout(this.autoCloseTimeout); 3236 this.autoCloseTimeout = null; 3237 } 3238 3239 // Sprawdź nieprzeczytane wiadomoÅci tylko po krótkim opóźnieniu 3240 // (żeby uniknÄ Ä faÅszywych powiadomieÅ gdy chat siÄ wÅaÅnie zamyka) 3241 setTimeout(() => { 3242 if (!this.isOpen) { 3243 // Upewnij siÄ Å¼e chat nadal jest zamkniÄty 3244 this.getUnreadCount(); 3245 } 3246 }, 1000); 3247 3248 // Resetuj tooltip status (ale nie natychmiast pokazuj) 3249 this.tooltipShown = false; 3250 3251 // Track chat close event (ale nie koÅczymy sesji!) 3252 this.trackEvent("chat_minimized"); 3253 } 3254 3255 toggleFullscreen() { 3256 if (typeof window === "undefined") { 3257 return; 3258 } 3259 if (window.innerWidth <= 768) { 3260 return; 3261 } 3262 3263 if (!this.isOpen) { 3264 this.openChat(); 3265 } 3266 3267 if (this.isFullscreen) { 3268 this.exitFullscreen(); 3269 } else { 3270 this.enterFullscreen(); 3271 } 3272 } 3273 3274 enterFullscreen() { 3275 const widget = document.getElementById("bb-asystent-ai-widget"); 3276 const chatWindow = document.getElementById( 3277 "bb-asystent-ai-window", 3278 ); 3279 3280 if (!widget || !chatWindow) { 3281 return; 3282 } 3283 3284 this.isFullscreen = true; 3285 widget.classList.add("fac-desktop-fullscreen"); 3286 chatWindow.classList.add("fac-desktop-fullscreen"); 3287 chatWindow.style.removeProperty("width"); 3288 chatWindow.style.removeProperty("height"); 3289 if (typeof document !== "undefined" && document.body) { 3290 document.body.classList.add("fac-chat-fullscreen-open"); 3291 } 3292 this.updateFullscreenButton(true); 3293 this.closeMenu(); 3294 this.updateChatWindowOffset(); 3295 } 3296 3297 exitFullscreen() { 3298 if (!this.isFullscreen) { 3299 return; 3300 } 3301 3302 const widget = document.getElementById("bb-asystent-ai-widget"); 3303 const chatWindow = document.getElementById( 3304 "bb-asystent-ai-window", 3305 ); 3306 3307 this.isFullscreen = false; 3308 3309 if (widget) { 3310 widget.classList.remove("fac-desktop-fullscreen"); 3311 } 3312 3313 if (chatWindow) { 3314 chatWindow.classList.remove("fac-desktop-fullscreen"); 3315 } 3316 3317 if (typeof document !== "undefined" && document.body) { 3318 document.body.classList.remove("fac-chat-fullscreen-open"); 3319 } 3320 this.updateFullscreenButton(false); 3321 this.updateChatWindowOffset(); 3322 this.updateChatWindowSize(); 3323 } 3324 3325 updateFullscreenButton(isActive) { 3326 const toggle = document.getElementById( 3327 "bb-asystent-ai-fullscreen-toggle", 3328 ); 3329 if (!toggle) { 3330 return; 3331 } 3332 3333 toggle.classList.toggle("is-active", !!isActive); 3334 toggle.setAttribute("aria-pressed", isActive ? "true" : "false"); 3335 3336 const enterLabel = 3337 toggle.getAttribute("data-enter-label") || 3338 toggle.getAttribute("title") || 3339 ""; 3340 const exitLabel = toggle.getAttribute("data-exit-label") || ""; 3341 const label = isActive && exitLabel ? exitLabel : enterLabel; 3342 3343 if (label) { 3344 toggle.setAttribute("aria-label", label); 3345 toggle.setAttribute("title", label); 3346 } 3347 } 3348 3349 // Dodaj nowe funkcje do ukrywania/pokazywania awatara 3350 hideBubbleAvatar() { 3351 const bubbleAvatar = document.querySelector(".bubble-avatar"); 3352 const avatarImage = document.querySelector( 3353 ".bubble-avatar .avatar-image", 3354 ); 3355 const onlineIndicator = document.querySelector( 3356 ".bubble-avatar .online-indicator", 3357 ); 3358 3359 if (bubbleAvatar) { 3360 bubbleAvatar.style.display = "none"; 3361 } 3362 if (avatarImage) { 3363 avatarImage.style.display = "none"; 3364 } 3365 if (onlineIndicator) { 3366 onlineIndicator.style.display = "none"; 3367 } 3368 } 3369 3370 showBubbleAvatar() { 3371 const bubbleAvatar = document.querySelector(".bubble-avatar"); 3372 const avatarImage = document.querySelector( 3373 ".bubble-avatar .avatar-image", 3374 ); 3375 const onlineIndicator = document.querySelector( 3376 ".bubble-avatar .online-indicator", 3377 ); 3378 3379 if (bubbleAvatar) { 3380 bubbleAvatar.style.display = "flex"; 3381 } 3382 if (avatarImage) { 3383 avatarImage.style.display = "block"; 3384 } 3385 if (onlineIndicator) { 3386 onlineIndicator.style.display = "block"; 3387 } 3388 } 3389 3390 async sendMessage() { 3391 const input = document.getElementById("bb-asystent-ai-input"); 3392 const inputContainer = 3393 document.getElementById("bb-asystent-ai-input-container") || 3394 document.querySelector(".bb-asystent-ai-input-container"); 3395 3396 if (!input) return; 3397 3398 const message = input.value.trim(); 3399 3400 if (!message || this.isLoading) { 3401 return; 3402 } 3403 3404 // Validate message length 3405 if (message.length > 500) { 3406 this.showError( 3407 "WiadomoÅÄ jest zbyt dÅuga (maksymalnie 500 znaków)", 3408 ); 3409 return; 3410 } 3411 3412 debugLog( 3413 "FUNERAL DEBUG: Sending message:", 3414 message.substring(0, 50), 3415 "for session:", 3416 this.sessionId, 3417 ); 3418 3419 // Reset auto-close timer 3420 this.resetAutoCloseTimer(); 3421 3422 this.isLoading = true; 3423 3424 // Blokuj input i dodaj klasÄ waiting 3425 input.disabled = true; 3426 if (inputContainer) { 3427 inputContainer.classList.add("waiting"); 3428 } 3429 3430 this.updateSendButton(true); 3431 3432 // Add user message to chat UI 3433 this.addMessage("user", message); 3434 this.persistMessagesCache(); 3435 input.value = ""; 3436 this.autoResizeInput(); 3437 3438 // Ukryj predefiniowane pytania po pierwszej wiadomoÅci użytkownika 3439 this.hideQuickQuestions(); 3440 // WyczyÅÄ stare propozycje produktów przy nowym pytaniu 3441 this.renderProductSuggestions([]); 3442 3443 // Show typing indicator 3444 this.showTypingIndicator(); 3445 this.startAssistantPersonaLoop(); 3446 3447 try { 3448 const forceClassic = 3449 this.settings && 3450 this.settings.forceClassicMode === true; 3451 let streamSuccess = false;
3452 3453 if (this.streamingMode && !forceClassic) { 3454 streamSuccess = await this.streamingMode.stream(message); 3455 } 3456 3457 if (!streamSuccess) { 3458 const response = await this.classicMode.send( 3459 message, 3460 this.sessionId, 3461 ); 3462 3463 if (response.success) { 3464 const d = response.data; 3465 if (d && Array.isArray(d.sources)) { 3466 delete d.sources; 3467 } 3468 this.handleAssistantResponse(d, message.length); 3469 } else { 3470 this.showError( 3471 response.data || this.strings.errorMessage, 3472 ); 3473 } 3474 } 3475 } catch (error) { 3476 debugError("Chat API Error:", error); 3477 // Lepsze komunikaty bÅÄdów dla Safari 3478 let errorMessage = this.strings.errorMessage; 3479 if (this.isSafari && this.isSafari()) { 3480 if ( 3481 error.message && 3482 (error.message.includes("Failed to fetch") || 3483 error.message.includes("NetworkError")) 3484 ) { 3485 errorMessage = 3486 "BÅÄ d poÅÄ czenia. Sprawdź poÅÄ czenie internetowe i spróbuj ponownie."; 3487 } else if (error.message && error.message.includes("HTTP error")) { 3488 errorMessage = 3489 "BÅÄ d serwera. Spróbuj ponownie za chwilÄ."; 3490 } 3491 } 3492 this.showError(errorMessage); 3493 } finally { 3494 this.hideTypingIndicator(); 3495 this.isLoading = false; 3496 3497 // Odblokuj input i usuÅ klasÄ waiting 3498 input.disabled = false; 3499 if (inputContainer) { 3500 inputContainer.classList.remove("waiting"); 3501 } 3502 3503 this.updateSendButton(false); 3504 const supportsMatchMedia = 3505 typeof window.matchMedia === "function"; 3506 const isMobile = 3507 supportsMatchMedia && 3508 window.matchMedia("(max-width: 768px)").matches; 3509 if (!isMobile) { 3510 input.focus(); 3511 } 3512 3513 // OdÅwież cache po zakoÅczeniu operacji 3514 this.persistMessagesCache(); 3515 } 3516 } 3517 3518 setInputMessage(value) { 3519 const input = document.getElementById("bb-asystent-ai-input"); 3520 if (!input) return; 3521 input.value = value; 3522 this.autoResizeInput(); 3523 this.updateSendButton(false); 3524 } 3525 3526 ensureFacetStyles() { 3527 if (document.getElementById("bb-asystent-ai-facets-style")) { 3528 return; 3529 } 3530 const style = document.createElement("style"); 3531 style.id = "bb-asystent-ai-facets-style"; 3532 style.textContent = ` 3533.bb-ai-facets-wrapper { margin: 12px 16px 4px; padding: 12px; border-radius: 10px; background: #f6f7fb; border: 1px solid #e3e6f0; } 3534.bb-ai-facets-wrapper h4 { margin: 0 0 8px; font-size: 13px; font-weight: 600; color: #1f2937; display: flex; align-items: center; gap: 6px; } 3535.bb-ai-facet { margin-bottom: 8px; } 3536.bb-ai-facet-label { font-size: 12px; font-weight: 600; color: #4b5563; margin-bottom: 6px; display: block; } 3537.bb-ai-facet-options { display: flex; flex-wrap: wrap; gap: 6px; } 3538.bb-ai-chip { border: 1px solid #d4d7e0; background: #fff; color: #1f2937; border-radius: 9999px; padding: 6px 10px; font-size: 12px; cursor: pointer; transition: all 120ms ease-in-out; } 3539.bb-ai-chip:hover { border-color: #c3c7d5; box-shadow: 0 1px 3px rgba(0,0,0,0.06); } 3540.bb-ai-chip.active { background: #1f7bf2; color: #fff; border-color: #1f7bf2; box-shadow: 0 8px 20px rgba(31,123,242,0.2); } 3541.bb-ai-facets-actions { display: flex; gap: 8px; align-items: center; margin-top: 6px; }
3542.bb-ai-facets-actions button { border: none; border-radius: 8px; padding: 8px 12px; font-size: 12px; cursor: pointer; transition: all 120ms ease-in-out; } 3543.bb-ai-apply { background: #111827; color: #fff; } 3544.bb-ai-apply:hover { background: #0b1222; } 3545.bb-ai-clear { background: #eceef3; color: #1f2937; } 3546.bb-ai-clear:hover { background: #e4e7ef; } 3547.bb-ai-facet-summary { margin-top: 6px; font-size: 12px; color: #4b5563; display: flex; gap: 6px; flex-wrap: wrap; } 3548.bb-ai-facet-summary .bb-ai-chip { background: #f0f3f9; border-color: #d4d7e0; color: #1f2937; cursor: default; } 3549.bb-ai-products-wrapper { background: #f8f9fb; border: 1px solid #e5e7ed; border-radius: 16px; padding: 12px; margin: 10px 8px 14px; box-shadow: 0 10px 30px rgba(17,24,39,0.06); width: calc(100% - 16px); max-width: calc(100% - 16px); box-sizing: border-box; animation: bb-ai-tool-panel-in 220ms ease-out both; } 3550@keyframes bb-ai-tool-panel-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } 3551.bb-ai-tool-panel-header { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; margin-bottom: 10px; } 3552.bb-ai-tool-panel-header strong { display: block; color: #111827; font-size: 14px; line-height: 1.25; } 3553.bb-ai-tool-panel-header p { margin: 3px 0 0; color: #4b5563; font-size: 12px; line-height: 1.4; } 3554.bb-ai-tool-eyebrow { display: block; color: #64748b; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .02em; margin-bottom: 2px; } 3555.bb-ai-tool-close { border: 1px solid #d4d7e0; background: #fff; color: #1f2937; border-radius: 8px; padding: 7px 9px; font-size: 12px; cursor: pointer; flex: 0 0 auto; } 3556.bb-ai-tool-close:hover { background: #f1f5f9; } 3557.bb-ai-products { --bb-ai-accent: var(--accent-color, #1f7bf2); width: 100%; display: flex; flex-direction: column; gap: 10px; max-height: min(52vh, 420px); overflow-y: auto; padding: 4px; } 3558.bb-ai-products::-webkit-scrollbar { width: 6px; } 3559.bb-ai-products::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.18); border-radius: 999px; } 3560.bb-ai-product-card { background: #fff; border: 1px solid #e6e8ef; border-radius: 16px; padding: 10px; box-shadow: 0 6px 16px rgba(17,24,39,0.06); display: flex; flex-direction: row; gap: 10px; min-width: 0; align-items: center; } 3561.bb-ai-product-card img { width: 82px; height: 82px; object-fit: cover; border-radius: 12px; background: #f6f7fb; flex-shrink: 0; } 3562.bb-ai-product-body { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; } 3563.bb-ai-product-actions { display: grid; grid-template-columns: 1fr 0.95fr; gap: 8px; align-items: center; margin-top: 6px; } 3564.bb-ai-product-actions > :only-child { grid-column: 1 / -1; } 3565.bb-ai-product-title { font-size: 13px; font-weight: 600; color: #111827; line-height: 1.3; min-height: 36px; } 3566.bb-ai-product-price { font-size: 12px; color: var(--bb-ai-accent); font-weight: 700; } 3567.bb-ai-product-actions button { width: 100%; border: none; border-radius: 10px; padding: 9px 10px; font-size: 12px; cursor: pointer; transition: all 120ms ease-in-out; min-height: 38px; } 3568.bb-ai-product-choose { background: var(--chat-send-button-bg, var(--bb-ai-accent)) !important; color: var(--bubble-text-color, #fff) !important; box-shadow: 0 6px 14px rgba(31,123,242,0.18); } 3569.bb-ai-product-choose:hover { background: #0f5fc2; } 3570.bb-ai-product-link { background: #eef0f6 !important; color: #1f2937 !important; text-decoration: none; display: inline-flex; justify-content: center; align-items: center; padding: 9px 10px; border-radius: 10px; min-width: 68px; min-height: 38px; text-align: center; font-size: 12px; box-sizing: border-box; } 3571.bb-ai-product-link:hover { background: #e4e7ef; } 3572.bb-ai-obituaries { display: flex; flex-direction: column; gap: 10px; max-height: min(54vh, 460px); overflow-y: auto; padding: 3px; } 3573.bb-ai-obituary-card { background: #fff; border: 1px solid #e6e8ef; border-radius: 16px; padding: 10px; display: grid; grid-template-columns: 74px minmax(0, 1fr); gap: 10px; align-items: center; box-shadow: 0 6px 16px rgba(17,24,39,0.06); }
3574.bb-ai-obituary-photo { width: 74px; height: 92px; object-fit: cover; border-radius: 12px; background: linear-gradient(135deg, #eef0f6, #fff); border: 1px solid #e6e8ef; } 3575.bb-ai-obituary-photo-fallback { width: 74px; height: 92px; border-radius: 12px; background: linear-gradient(135deg, #eef0f6, #fff); border: 1px solid #e6e8ef; display: flex; align-items: center; justify-content: center; color: #64748b; font-size: 24px; } 3576.bb-ai-obituary-body { min-width: 0; display: flex; flex-direction: column; gap: 6px; } 3577.bb-ai-obituary-name { font-size: 13px; font-weight: 700; color: #111827; } 3578.bb-ai-obituary-meta { font-size: 12px; color: #4b5563; line-height: 1.35; } 3579.bb-ai-obituary-empty { color: #4b5563; font-size: 12px; padding: 8px; } 3580.bb-ai-order-card { margin: 8px 12px 14px 58px; padding: 12px; border-radius: 12px; border: 1px solid #e3e6f0; background: #fff; box-shadow: 0 6px 16px rgba(17,24,39,0.08); max-width: calc(100% - 70px); box-sizing: border-box; animation: bb-ai-tool-panel-in 220ms ease-out both; } 3581.bb-ai-order-card h4 { margin: 0 0 10px; font-size: 14px; font-weight: 700; color: #111827; } 3582.bb-ai-order-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); gap: 10px; } 3583.bb-ai-order-field { display: flex; flex-direction: column; gap: 4px; } 3584.bb-ai-order-field label { font-size: 12px; font-weight: 600; color: #4b5563; } 3585.bb-ai-order-field input, .bb-ai-order-field select, .bb-ai-order-field textarea { border: 1px solid #d4d7e0; border-radius: 8px; padding: 8px 10px; font-size: 13px; font-family: inherit; } 3586.bb-ai-order-field textarea { resize: vertical; min-height: 88px; } 3587.bb-ai-order-field-wide { grid-column: 1 / -1; } 3588.bb-ai-order-summary { border: 1px solid #e5e7eb; background: #f8fafc; border-radius: 8px; padding: 9px 10px; color: #111827; font-size: 13px; } 3589.bb-ai-order-actions { margin-top: 10px; display: flex; gap: 8px; } 3590.bb-ai-order-actions button { flex: 1; border: none; border-radius: 8px; padding: 10px 12px; font-size: 13px; cursor: pointer; transition: all 120ms ease-in-out; } 3591.bb-ai-order-submit { background: var(--chat-send-button-bg, var(--send-button-color, var(--accent-color, #3498db))) !important; color: var(--bubble-text-color, var(--send-button-text-color, #fff)) !important; box-shadow: 0 8px 18px color-mix(in srgb, var(--chat-send-button-bg, var(--send-button-color, var(--accent-color, #3498db))) 28%, transparent); } 3592.bb-ai-order-submit:hover { filter: brightness(0.94); transform: translateY(-1px); } 3593.bb-ai-order-cancel { background: #eceef3; color: #1f2937; } 3594.bb-ai-order-cancel:hover { background: #e4e7ef; } 3595.bb-ai-order-submit:disabled { opacity: 0.65; cursor: wait; } 3596@media (max-width: 520px) { 3597 .bb-ai-products-wrapper, 3598 .bb-ai-order-card { margin-left: 8px; margin-right: 8px; max-width: calc(100% - 16px); } 3599 .bb-ai-obituary-card { grid-template-columns: 58px minmax(0, 1fr); } 3600 .bb-ai-obituary-photo, .bb-ai-obituary-photo-fallback { width: 58px; height: 76px; } 3601 .bb-ai-order-actions { flex-direction: column; } 3602} 3603.bb-ai-lightbox { position: fixed; inset: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 99999; padding: 24px; } 3604.bb-ai-lightbox img { max-width: 90vw; max-height: 90vh; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.35); } 3605.bb-ai-lightbox-close { position: absolute; top: 16px; right: 16px; background: rgba(0,0,0,0.6); color: #fff; border: none; border-radius: 9999px; width: 36px; height: 36px; cursor: pointer; font-size: 18px; line-height: 1; } 3606`; 3607 document.head.appendChild(style); 3608 } 3609 3610 renderToolFacets(facets, meta = {}) { 3611 // Facety wyÅÄ czone w UI â caÅoÅÄ opieramy na wiadomoÅciach tekstowych. 3612 return; 3613 } 3614 3615 renderFacetSummary(summary) { 3616 // Facety wyÅÄ czone â nic nie renderujemy. 3617 return; 3618 } 3619 3620 normalizePriceDisplay(prod) { 3621 if (!prod) return null; 3622 let raw = 3623 prod.price_display !== undefined && prod.price_display !== null 3624 ? prod.price_display 3625 : prod.price; 3626 if (raw === undefined || raw === null || raw === "") { 3627 return null; 3628 } 3629 if (typeof raw === "number") {
3630 raw = raw.toLocaleString("pl-PL", { 3631 minimumFractionDigits: 0, 3632 maximumFractionDigits: 2, 3633 }); 3634 } else { 3635 raw = String(raw).trim(); 3636 } 3637 if (!/[â¬$£¥â½â¿]|zÅ|pln/i.test(raw)) { 3638 raw = `${raw} zÅ`; 3639 } 3640 return raw; 3641 } 3642 3643 renderProductSuggestions(products, nekrologContext = null) { 3644 const productsWrapper = this.ensureProductsWrapper(); 3645 if (!productsWrapper) return; 3646 let container = document.getElementById("bb-asystent-ai-products"); 3647 if (nekrologContext && nekrologContext.funeral_already_occurred) { 3648 this.renderToolPanelHeader( 3649 productsWrapper, 3650 "Informacja", 3651 "Zamówienie kwiatów", 3652 "Ten pogrzeb już siÄ odbyÅ, wiÄc nie pokazujÄ propozycji zamówienia kwiatów na tÄ ceremoniÄ.", 3653 ); 3654 if (container) container.remove(); 3655 this.scrollToBottom(); 3656 return; 3657 } 3658 if (!products || !Array.isArray(products) || products.length === 0) { 3659 if (container) { 3660 container.remove(); 3661 } 3662 // jeÅli nie ma facetów ani produktów â usuÅ caÅÄ kartÄ 3663 const facets = document.getElementById("bb-asystent-ai-facets"); 3664 if (!facets && productsWrapper.parentNode) { 3665 productsWrapper.remove(); 3666 } 3667 return; 3668 } 3669 3670 this.ensureFacetStyles(); 3671 if (nekrologContext) { 3672 this.nekrologContext = nekrologContext; 3673 } 3674 this.renderToolPanelHeader( 3675 productsWrapper, 3676 "Proponowane narzÄdzie", 3677 "Kwiaty i produkty", 3678 "PokaÅ¼Ä propozycje z oferty. Możesz wybraÄ produkt albo zamknÄ Ä tÄ sekcjÄ.", 3679 ); 3680 if (!container) { 3681 container = document.createElement("div"); 3682 container.id = "bb-asystent-ai-products"; 3683 container.className = "bb-ai-products"; 3684 productsWrapper.appendChild(container); 3685 } else { 3686 container.innerHTML = ""; 3687 productsWrapper.appendChild(container); 3688 } 3689 3690 products.forEach((prod) => { 3691 const card = document.createElement("div"); 3692 card.className = "bb-ai-product-card"; 3693 3694 const body = document.createElement("div"); 3695 body.className = "bb-ai-product-body"; 3696 3697 if (prod.image) { 3698 const img = document.createElement("img"); 3699 img.src = prod.image; 3700 img.alt = prod.name || "Produkt"; 3701 card.appendChild(img); 3702 } 3703 3704 const title = document.createElement("div"); 3705 title.className = "bb-ai-product-title"; 3706 title.textContent = prod.name || "Produkt"; 3707 body.appendChild(title); 3708 3709 const priceDisplay = this.normalizePriceDisplay(prod); 3710 if (priceDisplay) { 3711 const price = document.createElement("div"); 3712 price.className = "bb-ai-product-price"; 3713 price.textContent = priceDisplay; 3714 body.appendChild(price); 3715 } 3716 3717 const actions = document.createElement("div"); 3718 actions.className = "bb-ai-product-actions"; 3719 3720 const choose = document.createElement("button"); 3721 choose.type = "button"; 3722 choose.className = "bb-ai-product-choose"; 3723 choose.textContent = "Zamów ten produkt"; 3724 choose.addEventListener("click", () => { 3725 // WyÅlij wybór jako wiadomoÅÄ do chatu â backend wygeneruje link. 3726 const msg = `Wybieram produkt: ${prod.name || "produkt"}`; 3727 this.setInputMessage(msg); 3728 this.sendMessage(); 3729 this.scrollToBottom(); 3730 }); 3731 actions.appendChild(choose); 3732 3733 if (prod.url) { 3734 const link = document.createElement("a"); 3735 link.href = prod.url;
3736 applyLinkTargetPolicy(link, prod.url); 3737 link.className = "bb-ai-product-link"; 3738 link.textContent = "Zobacz"; 3739 actions.appendChild(link); 3740 } 3741 3742 body.appendChild(actions); 3743 card.appendChild(body); 3744 container.appendChild(card); 3745 }); 3746 3747 // Scroll viewport do nagÅówka wiadomoÅci, żeby siatka nie chowaÅa headera/close 3748 this.scrollToBottom(); 3749 } 3750 3751 renderToolPanelHeader(wrapper, eyebrow, title, description) { 3752 let header = wrapper.querySelector(".bb-ai-tool-panel-header"); 3753 if (!header) { 3754 header = document.createElement("div"); 3755 header.className = "bb-ai-tool-panel-header"; 3756 wrapper.prepend(header); 3757 } 3758 header.innerHTML = ""; 3759 3760 const text = document.createElement("div"); 3761 const small = document.createElement("span"); 3762 small.className = "bb-ai-tool-eyebrow"; 3763 small.textContent = eyebrow; 3764 const strong = document.createElement("strong"); 3765 strong.textContent = title; 3766 const desc = document.createElement("p"); 3767 desc.textContent = description; 3768 text.appendChild(small); 3769 text.appendChild(strong); 3770 text.appendChild(desc); 3771 3772 const close = document.createElement("button"); 3773 close.type = "button"; 3774 close.className = "bb-ai-tool-close"; 3775 close.setAttribute("aria-label", "Zamknij narzÄdzie"); 3776 close.textContent = "Nie teraz"; 3777 close.addEventListener("click", () => { 3778 wrapper.remove(); 3779 }); 3780 3781 header.appendChild(text); 3782 header.appendChild(close); 3783 } 3784 3785 renderObituaryResults(uiMeta) { 3786 const messages = document.getElementById("bb-asystent-ai-messages"); 3787 if (!messages) return; 3788 this.ensureFacetStyles(); 3789 const existing = document.getElementById("bb-ai-obituaries-wrapper"); 3790 if (existing) existing.remove(); 3791 3792 const wrapper = document.createElement("div"); 3793 wrapper.id = "bb-ai-obituaries-wrapper"; 3794 wrapper.className = "bb-ai-products-wrapper bb-ai-obituaries-wrapper"; 3795 this.renderToolPanelHeader( 3796 wrapper, 3797 "Proponowane narzÄdzie", 3798 "Nekrologi", 3799 "To sÄ wyniki z bazy nekrologów. Możesz otworzyÄ wpis lub zamknÄ Ä tÄ sekcjÄ.", 3800 ); 3801 3802 const list = document.createElement("div"); 3803 list.className = "bb-ai-obituaries"; 3804 const items = Array.isArray(uiMeta.items) ? uiMeta.items : []; 3805 items.forEach((item) => { 3806 const card = document.createElement("div"); 3807 card.className = "bb-ai-obituary-card"; 3808 3809 const photo = item.photo && (item.photo.medium || item.photo.full) 3810 ? item.photo.medium || item.photo.full 3811 : ""; 3812 if (photo) { 3813 const img = document.createElement("img"); 3814 img.className = "bb-ai-obituary-photo"; 3815 img.src = photo; 3816 img.alt = item.name || "Nekrolog"; 3817 card.appendChild(img); 3818 } else { 3819 const fallback = document.createElement("div"); 3820 fallback.className = "bb-ai-obituary-photo-fallback"; 3821 fallback.setAttribute("aria-hidden", "true"); 3822 fallback.textContent = "â"; 3823 card.appendChild(fallback); 3824 } 3825 3826 const body = document.createElement("div"); 3827 body.className = "bb-ai-obituary-body"; 3828 3829 const name = document.createElement("div"); 3830 name.className = "bb-ai-obituary-name"; 3831 name.textContent = item.name || "Nekrolog"; 3832 body.appendChild(name); 3833 3834 const meta = document.createElement("div"); 3835 meta.className = "bb-ai-obituary-meta"; 3836 const parts = []; 3837 const funeralDate = item.funeral_date_human || item.funeral_date || ""; 3838 if (funeralDate) { 3839 parts.push( 3840 `Pogrzeb: ${funeralDate}`, 3841 ); 3842 } 3843 if (item.cemetery) { 3844 parts.push(item.cemetery); 3845 } 3846 meta.textContent = parts.join(" ⢠"); 3847 body.appendChild(meta); 3848 3849 const actions = document.createElement("div"); 3850 actions.className = "bb-ai-product-actions"; 3851 if (item.url) { 3852 const link = document.createElement("a"); 3853 link.href = item.url;
3854 applyLinkTargetPolicy(link, item.url); 3855 link.className = "bb-ai-product-link"; 3856 link.textContent = "Otwórz"; 3857 actions.appendChild(link); 3858 } 3859 const use = document.createElement("button"); 3860 use.type = "button"; 3861 use.className = "bb-ai-product-choose"; 3862 use.textContent = "Użyj w rozmowie"; 3863 use.addEventListener("click", () => { 3864 const msg = item.id 3865 ? `Użyj nekrologu ID ${item.id} w dalszej rozmowie.` 3866 : `Użyj nekrologu ${item.name || ""} w dalszej rozmowie.`; 3867 this.setInputMessage(msg); 3868 this.sendMessage(); 3869 wrapper.remove(); 3870 }); 3871 actions.appendChild(use); 3872 body.appendChild(actions); 3873 card.appendChild(body); 3874 3875 list.appendChild(card); 3876 }); 3877 3878 if (items.length === 0) { 3879 const empty = document.createElement("div"); 3880 empty.className = "bb-ai-obituary-empty"; 3881 empty.textContent = "Nie znaleziono wyników do pokazania."; 3882 list.appendChild(empty); 3883 } 3884 3885 wrapper.appendChild(list); 3886 messages.appendChild(wrapper); 3887 this.scrollToBottom(); 3888 } 3889 3890 ensureProductsWrapper() { 3891 const messages = document.getElementById("bb-asystent-ai-messages"); 3892 if (!messages) return null; 3893 const facets = document.getElementById("bb-asystent-ai-facets"); 3894 if (facets) { 3895 facets.remove(); 3896 } 3897 let wrapper = document.getElementById("bb-ai-products-wrapper"); 3898 if (!wrapper) { 3899 wrapper = document.createElement("div"); 3900 wrapper.id = "bb-ai-products-wrapper"; 3901 wrapper.className = "bb-ai-products-wrapper"; 3902 } 3903 const lastAssistant = Array.from( 3904 messages.querySelectorAll(".bb-asystent-ai-message.assistant"), 3905 ).pop(); 3906 const lastMsg = Array.from( 3907 messages.querySelectorAll(".bb-asystent-ai-message"), 3908 ).pop(); 3909 3910 const target = lastAssistant || lastMsg; 3911 if (target) { 3912 const next = target.nextSibling; 3913 if (next !== wrapper) { 3914 if (wrapper.parentNode !== messages) { 3915 messages.appendChild(wrapper); 3916 } 3917 messages.insertBefore(wrapper, next); 3918 } 3919 this.scrollToBottom(); 3920 } else if (!messages.contains(wrapper)) { 3921 messages.appendChild(wrapper); 3922 } 3923 return wrapper; 3924 } 3925 3926 toggleFacetSelection(button) { 3927 const facetId = button.dataset.facetId; 3928 const value = button.dataset.value || ""; 3929 const label = button.dataset.label || value; 3930 const min = 3931 button.dataset.min !== undefined 3932 ? Number(button.dataset.min) 3933 : null; 3934 const max = 3935 button.dataset.max !== undefined && 3936 button.dataset.max !== "undefined" 3937 ? Number(button.dataset.max) 3938 : null; 3939 3940 const parent = button.closest(".bb-ai-facet"); 3941 if (parent) { 3942 parent 3943 .querySelectorAll(".bb-ai-chip") 3944 .forEach((node) => node.classList.remove("active")); 3945 } 3946 3947 button.classList.add("active"); 3948 this.facetSelections[facetId] = { value, label, min, max }; 3949 } 3950 3951 buildFacetPrompt() { 3952 const selections = this.facetSelections || {}; 3953 const parts = []; 3954 3955 if (selections.category) { 3956 parts.push(`kategoria ${selections.category.label}`); 3957 } 3958 if (selections.price_range) { 3959 const pr = selections.price_range; 3960 const label = pr.label 3961 ? pr.label 3962 : pr.min !== null && pr.max !== null 3963 ? `${pr.min}â${pr.max} zÅ` 3964 : pr.min !== null 3965 ? `powyżej ${pr.min} zÅ` 3966 : pr.max !== null 3967 ? `do ${pr.max} zÅ` 3968 : ""; 3969 if (label) { 3970 parts.push(`budżet ${label}`); 3971 } 3972 } 3973 3974 if (parts.length === 0) { 3975 return ""; 3976 } 3977 3978 return `Pokaż produkty (${parts.join( 3979 ", ", 3980 )}
3980) i zaproponuj link do zamówienia na pogrzeb.`; 3981 } 3982 3983 applyFacetSelection() { 3984 const prompt = this.buildFacetPrompt(); 3985 if (!prompt) { 3986 return; 3987 } 3988 this.setInputMessage(prompt); 3989 this.sendMessage(); 3990 } 3991 3992 async streamAssistantResponse(message) { 3993 if (!this.streamingMode) { 3994 return false; 3995 } 3996 return this.streamingMode.stream(message); 3997 } 3998 3999 ensureAssistantPlaceholder() { 4000 // NIE usuwaj typing indicator tutaj - zostanie usuniÄty przy pierwszym delta 4001 // const typing = document.getElementById('bb-asystent-ai-typing'); 4002 // if (typing) { 4003 // typing.remove(); 4004 // } 4005 4006 const lastAssistant = Array.from( 4007 document.querySelectorAll(".bb-asystent-ai-message.assistant"), 4008 ).pop(); 4009 if (!lastAssistant || lastAssistant.dataset.final === "true") { 4010 const node = this.addMessage( 4011 "assistant", 4012 "", 4013 this.getCurrentTime(), 4014 true, 4015 null, 4016 null, 4017 ); 4018 if (node) { 4019 node.classList.add("streaming"); 4020 } 4021 } 4022 } 4023 4024 renderMarkdown(text) { 4025 const { visible, mark } = this.splitAiMark( 4026 typeof text === "string" ? text : text == null ? "" : String(text), 4027 ); 4028 const raw = 4029 this.markdownFormatter && typeof this.markdownFormatter.render === "function" 4030 ? this.markdownFormatter.render(visible) 4031 : (() => { 4032 const value = visible; 4033 return this.escapeHtml(value).replace(/\n/g, "<br>"); 4034 })(); 4035 // Ensure URLs/emails/phones become clickable and fix malformed hrefs 4036 // (e.g. when a model returns Markdown syntax inside the href). 4037 const withLinks = linkifyContacts(raw); 4038 return this.cleanRenderedHtml(withLinks) + this.wrapAiMarkHtml(mark); 4039 } 4040 4041 splitAiMark(text) { 4042 if (typeof text !== "string" || text === "") { 4043 return { visible: text || "", mark: "" }; 4044 } 4045 const match = text.match(/[\uFE00-\uFE0F\u{E0100}-\u{E01EF}]+$/u); 4046 if (!match) { 4047 return { visible: text, mark: "" }; 4048 } 4049 return { 4050 visible: text.slice(0, text.length - match[0].length), 4051 mark: match[0], 4052 }; 4053 } 4054 4055 wrapAiMarkHtml(mark) { 4056 if (!mark) { 4057 return ""; 4058 } 4059 return `<span class="bb-ai-mark" aria-hidden="true">${mark}</span>`; 4060 } 4061 4062 getAssistantClipboardText(contentEl, selectedText) { 4063 const suffix = 4064 (this.strings && this.strings.aiCopySuffix) || 4065 "[Wygenerowano przez Asystent Pogrzebowy AI]"; 4066 const markEl = contentEl.querySelector(".bb-ai-mark"); 4067 const mark = markEl ? markEl.textContent || "" : ""; 4068 let text = 4069 typeof selectedText === "string" && selectedText !== "" 4070 ? selectedText 4071 : ""; 4072 if (text === "") { 4073 const clone = contentEl.cloneNode(true); 4074 clone.querySelectorAll(".bb-ai-mark").forEach((node) => node.remove()); 4075 text = (clone.innerText || clone.textContent || "").replace( 4076 /\s+$/u, 4077 "", 4078 ); 4079 } 4080 const split = this.splitAiMark(text); 4081 text = split.visible.replace(/\s+$/u, ""); 4082 const vs = mark || split.mark; 4083 if (!text.includes(suffix)) { 4084 text = text + "\n\n" + suffix; 4085 } 4086 return text + vs; 4087 } 4088 4089 bindAiCopyGuard() { 4090 const root = document.getElementById("bb-asystent-ai-messages"); 4091 if (!root || root.dataset.aiCopyBound === "1") { 4092 return; 4093 } 4094 root.dataset.aiCopyBound = "1"; 4095 const handler = (event) => this.handleAssistantCopy(event); 4096 root.addEventListener("copy", handler); 4097 root.addEventListener("cut", handler); 4098 } 4099 4100 handleAssistantCopy(event) { 4101 const target = event.target; 4102 if (!target || typeof target.closest !== "function") { 4103 return; 4104 } 4105 const bubble = target.closest(".bb-asystent-ai-message.assistant"); 4106 if (!bubble || bubble.classList.contains("tool-call")) { 4107 return; 4108 } 4109 const content = bubble.querySelector(".bb-asystent-ai-message-content"); 4110 if (!content || !event.clipboardData) { 4111 return; 4112 } 4113 4114 let selected = ""; 4115 const selection = window.getSelection(); 4116 if (selection && selection.rangeCount > 0) { 4117 const range = selection.getRangeAt(0); 4118 if ( 4119 content.contains(range.commonAncestorContainer) || 4120 range.intersectsNode(content) 4121 ) { 4122 selected = selection.toString(); 4123 } 4124 } 4125 4126 event.clipboardData.setData( 4127 "text/plain", 4128 this.getAssistantClipboardText(content, selected), 4129 ); 4130 event.preventDefault(); 4131 } 4132 4133 handleAssistantResponse(data, messageLength) { 4134 debugLog( 4135 "FUNERAL DEBUG: Handling assistant response from fallback", 4136 ); 4137 4138 // Ukryj typing indicator przed dodaniem wiadomoÅci 4139 this.hideTypingIndicator(); 4140 4141 const messageId = data.message_id || null; 4142 const responseType = data.type || "assistant"; 4143 4144 if (data.tool_call && typeof this.addToolCallMessage === "function") { 4145 this.addToolCallMessage(data.tool_call, { 4146 persist: false, 4147 scroll: false, 4148 }); 4149 } 4150 4151 let contentToRender = data.message; 4152 let meta = null; 4153 if (responseType === "instant") { 4154 meta = { 4155 instant: true, 4156 source: data.source || "google_sheets", 4157 matchType: data.match_type || "partial", 4158 score: data.score, 4159 }; 4160 } else { 4161 // Classic (AJAX) path: pre-render Markdown once and mark to skip in addMessage 4162 contentToRender = this.renderMarkdown(data.message); 4163 meta = { 4164 type: "ai_response", 4165 skipMarkdown: true, 4166 }; 4167 } 4168 4169 const assistantElement = this.addMessage( 4170 "assistant", 4171 contentToRender, 4172 data.timestamp, 4173 true, 4174 messageId, 4175 meta, 4176 ); 4177 4178 if (assistantElement && responseType === "instant") { 4179 assistantElement.classList.remove("streaming"); 4180 assistantElement.dataset.type = "instant"; 4181 } else if (assistantElement) { 4182 assistantElement.dataset.type = "ai_response"; 4183 } 4184 4185 this.persistMessagesCache(); 4186 this.trackEvent("message_sent", { message_length: messageLength }); 4187 4188 this.unreadCount++; 4189 this.updateBadgeWithCount(); 4190 this.armReadAcknowledgementIfNeeded(); 4191 } 4192 4193 async callAPI(message) { 4194 if (!this.classicMode) { 4195 throw new Error("Classic chat mode is not configured"); 4196 } 4197 return this.classicMode.send(message, this.sessionId); 4198 } 4199 4200 // ============== Tool Call Card (kompaktowy kafelek w czacie) ============== 4201 4202 ensureToolCallStyles() { 4203 if (document.getElementById("bb-asystent-ai-tool-call-styles")) { 4204 return; 4205 } 4206 const style = document.createElement("style"); 4207 style.id = "bb-asystent-ai-tool-call-styles"; 4208 style.textContent = ` 4209.bb-asystent-ai-message.tool-call { 4210 background: rgba(248, 250, 252, 0.92); 4211 border: 1px solid rgba(226, 232, 240, 0.9); 4212 border-radius: 8px; 4213 padding: 7px 10px; 4214 margin: 12px 12px 8px 58px; 4215 max-width: calc(100% - 70px); 4216 box-sizing: border-box; 4217 font-size: 12.5px; 4218 color: #475569; 4219 display: flex; 4220 align-items: center; 4221 gap: 8px; 4222 animation: bb-tool-fade-in 200ms ease-out both; 4223} 4224@keyframes bb-tool-fade-in { 4225 from { opacity: 0; transform: translateY(6px); } 4226 to { opacity: 1; transform: translateY(0); } 4227} 4228.bb-asystent-ai-message.tool-call .bb-tc-icon { 4229 flex: 0 0 auto; 4230 width: 14px; 4231 height: 14px; 4232 color: #64748b; 4233} 4234.bb-asystent-ai-message.tool-call .bb-tc-icon svg { 4235 width: 100%; 4236 height: 100%; 4237} 4238.bb-asystent-ai-message.tool-call .bb-tc-summary { 4239 flex: 1 1 auto; 4240 line-height: 1.3; 4241 word-break: break-word; 4242} 4243.bb-asystent-ai-message.tool-call .bb-tc-spinner { 4244 display: inline-block; 4245 width: 10px; 4246 height: 10px; 4247 border: 2px solid #cbd5e1; 4248 border-top-color: #6b7280; 4249 border-radius: 50%; 4250 margin-left: 6px; 4251 vertical-align: -1px; 4252 animation: bb-tc-spin 0.9s linear infinite; 4253} 4254@keyframes bb-tc-spin { to { transform: rotate(360deg); } } 4255.bb-asystent-ai-message.tool-call.tc-status-running { color: #475569; } 4256.bb-asystent-ai-message.tool-call.tc-status-ok { color: #166534; background: #f7fbf8; border-color: #dbe8df; } 4257.bb-asystent-ai-message.tool-call.tc-status-error { color: #991b1b; background: #fff7f7; border-color: #f2d5d5; } 4258@media (max-width: 520px) { 4259 .bb-asystent-ai-message.tool-call { 4260 margin-left: 8px; 4261 margin-right: 8px; 4262 max-width: calc(100% - 16px); 4263 } 4264} 4265.bb-asystent-ai-message.tool-call details { margin-top: 4px; font-size: 12px; opacity: 0.85; }
4266.bb-asystent-ai-message.tool-call details summary { cursor: pointer; user-select: none; } 4267.bb-asystent-ai-message.tool-call details code { background: #fff; padding: 1px 4px; border-radius: 3px; } 4268`; 4269 document.head.appendChild(style); 4270 } 4271 4272 /** DomyÅlny SVG ikonki dla kafelka tool_call. */ 4273 _defaultToolCallIcon() { 4274 return '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a4 4 0 0 0-3.87 5.05L3 12.18V18a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3v-5.82l-5.13-5.13A4 4 0 0 0 12 2zm0 2a2 2 0 1 1 0 4 2 2 0 0 1 0-4z"/></svg>'; 4275 } 4276 4277 /** Buduje payload do zapisania w `data-tool-payload`. */ 4278 _toolCallPayloadFor(toolCall, status) { 4279 return { 4280 id: toolCall.id, 4281 name: toolCall.name || "", 4282 display_name: toolCall.display_name || "", 4283 summary: toolCall.summary || "", 4284 status, 4285 error_code: toolCall.error_code || "", 4286 phase: toolCall.phase || "finished", 4287 args: toolCall.args || null, 4288 }; 4289 } 4290 4291 /** Renderuje sekcjÄ `<details>` z argumentami toola (jeÅli sÄ ). */ 4292 _toolCallArgsHtml(args) { 4293 if (!args || typeof args !== "object" || Array.isArray(args)) { 4294 return ""; 4295 } 4296 const entries = Object.entries(args).filter( 4297 ([, v]) => v !== "" && v !== null && v !== undefined, 4298 ); 4299 if (entries.length === 0) { 4300 return ""; 4301 } 4302 const pretty = entries 4303 .map(([k, v]) => { 4304 const value = typeof v === "string" ? v : JSON.stringify(v); 4305 return `<code>${this.escapeHtml(String(k))}</code>: ${this.escapeHtml(value)}`; 4306 }) 4307 .join(", "); 4308 return `<details><summary>argumenty</summary>${pretty}</details>`; 4309 } 4310 4311 /** Schedule persistMessagesCache w trybie idle. */ 4312 _schedulePersist() { 4313 if (typeof requestIdleCallback === "function") { 4314 requestIdleCallback(() => this.persistMessagesCache()); 4315 } else { 4316 setTimeout(() => this.persistMessagesCache(), 100); 4317 } 4318 } 4319 4320 /** Tworzy lub aktualizuje kafelek tool_call w czacie. Aktualizacja po `id`. */ 4321 addToolCallMessage(toolCall, options = {}) { 4322 const messages = document.getElementById("bb-asystent-ai-messages"); 4323 if (!messages || !toolCall || !toolCall.id) { 4324 return null; 4325 } 4326 this.ensureToolCallStyles(); 4327 4328 const existing = messages.querySelector( 4329 `.bb-asystent-ai-message.tool-call[data-tool-id="${toolCall.id}"]`, 4330 ); 4331 const node = existing || document.createElement("div"); 4332 const status = String(toolCall.status || "running"); 4333 const showSpinner = status === "running" || toolCall.phase === "started"; 4334 4335 node.className = `bb-asystent-ai-message tool-call tc-status-${status}`; 4336 node.dataset.toolId = toolCall.id; 4337 node.dataset.toolPayload = JSON.stringify(this._toolCallPayloadFor(toolCall, status)); 4338 4339 const summaryText = toolCall.summary || toolCall.display_name || "NarzÄdzie"; 4340 const iconSvg = toolCall.icon_svg || this._defaultToolCallIcon(); 4341 const argsHtml = this._toolCallArgsHtml(toolCall.args); 4342 4343 node.innerHTML = ` 4344 <span class="bb-tc-icon" aria-hidden="true">${iconSvg}</span> 4345 <div class="bb-tc-summary"> 4346 ${this.escapeHtml(summaryText)}${showSpinner ? '<span class="bb-tc-spinner"></span>' : ""} 4347 ${argsHtml} 4348 </div> 4349 `; 4350 4351 if (!existing) { 4352 messages.appendChild(node); 4353 } 4354 if (options.scroll !== false) { 4355 (this._debouncedScrollToBottom || this.scrollToBottom).call(this); 4356 } 4357 if (options.persist !== false) { 4358 this._schedulePersist(); 4359 } 4360 return node; 4361 } 4362 4363 /** 4364 * ObsÅuguje zdarzenia tool_call ze strumienia (started/finished). 4365 */ 4366 handleToolCallEvent(toolCall) { 4367 if (!toolCall || !toolCall.id) { 4368 return; 4369 } 4370 // Dodajemy/aktualizujemy kafelek. UI dodatkowy (np. formularz) 4371 // przychodzi osobno przez meta.ui i jest renderowany jak dotÄ d. 4372 this.addToolCallMessage(toolCall); 4373 } 4374 4375 addMessage( 4376 type, 4377 content, 4378 timestamp = null, 4379 shouldAdd = true, 4380 messageId = null, 4381 meta = null, 4382 ) { 4383 const messages = document.getElementById( 4384 "bb-asystent-ai-messages", 4385 ); 4386 if (!messages && shouldAdd) return null; 4387 4388 const now = Date.now(); 4389 const incomingText = this.normalizeSystemText(content); 4390 if (type === "assistant" && incomingText !== "") { 4391 const lastAssistant = Array.from( 4392 messages ? messages.querySelectorAll(".bb-asystent-ai-message.assistant") : [], 4393 ).pop(); 4394 if (lastAssistant) { 4395 const lastContent = lastAssistant.querySelector( 4396 ".bb-asystent-ai-message-content", 4397 ); 4398 const lastText = this.normalizeSystemText( 4399 lastContent?.textContent || "", 4400 ); 4401 const lastRenderedAt = Number( 4402 lastAssistant.dataset.renderedAt || "0", 4403 ); 4404 const isToolLike = 4405 meta && 4406 (meta.type === "tool_followup" || 4407 meta.type === "ai_response" || 4408 meta.type === "system" || 4409 meta.source === "tool"); 4410 if ( 4411 isToolLike && 4412 lastText !== "" && 4413 lastText === incomingText && 4414 lastRenderedAt > 0 && 4415 now - lastRenderedAt < 4000 4416 ) { 4417 return lastAssistant; 4418 } 4419 } 4420 } 4421 4422 // Sprawdź czy wiadomoÅÄ wyglÄ da na zaszyfrowanÄ i spróbuj jÄ naprawiÄ 4423 content = this.fixEncryptedMessage(content); 4424 4425 const time = timestamp || this.getCurrentTime(); 4426 const messageDiv = document.createElement("div"); 4427 messageDiv.className = `bb-asystent-ai-message ${type}`; 4428 4429 if (messageId) { 4430 messageDiv.dataset.messageId = messageId; 4431 } 4432 messageDiv.dataset.renderedAt = String(now); 4433 if (meta && meta.type) { 4434 messageDiv.dataset.type = meta.type; 4435 } 4436 if (type === "assistant") { 4437 messageDiv.dataset.ai = "true"; 4438 messageDiv.dataset.aiOrigin = "bb-asystent-ai"; 4439 if (messageId) { 4440 messageDiv.dataset.aiMessageId = String(messageId); 4441 } 4442 } 4443 4444 const avatarHtml = this.generateAvatarHtml(type); 4445 const userName = 4446 type === "user" ? "Ty" : this.strings.botName || "Asystent";
4447 const aiChip = 4448 type === "assistant" 4449 ? '<span class="bb-asystent-ai-ai-badge" title="TreÅÄ wygenerowana przez AI">AI</span>' 4450 : ""; 4451 4452 let processedContent; 4453 if (type === "assistant") { 4454 if (meta && meta.skipMarkdown === true) { 4455 // Content already pre-rendered as HTML (e.g., classic AJAX path) 4456 processedContent = content; 4457 } else if ( 4458 meta && 4459 (meta.type === "quick_response" || meta.sanitize) 4460 ) { 4461 // Dla szybkich odpowiedzi renderuj jako zwykÅy tekst (bez Markdown) 4462 processedContent = this.escapeHtml(content).replace( 4463 /\n/g, 4464 "<br>", 4465 ); 4466 } else { 4467 // Parsuj markdown dla wiadomoÅci asystenta 4468 // Dla odpowiedzi instant (Google Sheets) usuÅ blockquote, aby wyglÄ daÅy jak zwykÅe wiadomoÅci 4469 if (meta && meta.instant) { 4470 content = content 4471 .replace(/^\s*<blockquote[^>]*>/i, "") 4472 .replace(/<\/blockquote>\s*$/i, ""); 4473 content = content 4474 .split("\n") 4475 .map((l) => l.replace(/^\s*>\s?/, "")) 4476 .join("\n"); 4477 } 4478 // NAJPIERW renderuj markdown (żeby linki markdown [text](url) byÅy poprawnie przetworzone) 4479 processedContent = this.renderMarkdown(content); 4480 } 4481 } else { 4482 // Dla wiadomoÅci użytkownika: najpierw dekoduj wielokrotnie zakodowane encje HTML, 4483 // a potem zakoduj znaki specjalne dla bezpieczeÅstwa 4484 let decodedContent = this.decodeHtmlEntities(content); 4485 // Users' cached messages might contain "<br>" or "<br>" from older versions. 4486 // Normalize it back to text so we don't render literal "<br>" on refresh. 4487 decodedContent = decodedContent 4488 .replace(/<br\s*\/?>/gi, "\n") 4489 .replace(/<\/p>\s*<p[^>]*>/gi, "\n\n") 4490 .replace(/<\/?p[^>]*>/gi, "") 4491 .replace(/<[^>]+>/g, "") 4492 .replace(/\r\n/g, "\n") 4493 .replace(/\n{3,}/g, "\n\n") 4494 .trim(); 4495 processedContent = this.escapeHtml(decodedContent).replace( 4496 /\n/g, 4497 "<br>", 4498 ); 4499 } 4500 4501 // Final pass: linkify anything that remained as plain text and normalize <a> attributes/classes. 4502 processedContent = this.cleanRenderedHtml(linkifyContacts(processedContent)); 4503 4504 messageDiv.innerHTML = ` 4505 ${avatarHtml} 4506 <div class="bb-asystent-ai-message-wrapper"> 4507 <div class="bb-asystent-ai-message-header"> 4508 <div class="bb-asystent-ai-message-name">${userName}${aiChip}</div> 4509 <div class="bb-asystent-ai-message-time">${time}</div> 4510 </div> 4511 <div class="bb-asystent-ai-message-content"> 4512 ${processedContent} 4513 </div> 4514 </div> 4515 `; 4516 4517 if (type === "assistant" && meta && typeof meta === "object") { 4518 if (meta.instant) { 4519 // Render as a normal assistant message (no special styling class) 4520 } 4521 if (meta.source) { 4522 messageDiv.dataset.source = meta.source; 4523 } 4524 if (meta.matchType) { 4525 messageDiv.dataset.matchType = meta.matchType; 4526 } 4527 if (meta.score !== undefined) { 4528 messageDiv.dataset.score = meta.score; 4529 } 4530 shouldAdd = shouldAdd === false ? false : true; 4531 } 4532 4533 if (shouldAdd && messages) { 4534 messages.appendChild(messageDiv); 4535 debugLog("FUNERAL DEBUG: Message added to DOM, type:", type); 4536 4537 // Dodaj Åledzenie klikniÄÄ dla wiadomoÅci asystenta 4538 if (type === "assistant") { 4539 debugLog( 4540 "FUNERAL DEBUG: Setting up click tracking for assistant message", 4541 ); 4542 this.setupClickTracking(messageDiv); 4543 } else { 4544 debugLog( 4545 "FUNERAL DEBUG: Skipping click tracking for user message", 4546 ); 4547 } 4548 4549 if (type === "assistant") { 4550 this.adjustAssistantMessageScroll(messageDiv); 4551 } else { 4552 this.scrollToBottom(); 4553 } 4554 if (typeof requestIdleCallback === "function") { 4555 requestIdleCallback(() => this.persistMessagesCache()); 4556 } else { 4557 setTimeout(() => this.persistMessagesCache(), 150); 4558 } 4559 } else { 4560 debugLog( 4561 "FUNERAL DEBUG: Message NOT added to DOM (shouldAdd:", 4562 shouldAdd, 4563 ", messages element exists:", 4564 !!messages, 4565 ")", 4566 ); 4567 } 4568 4569 return messageDiv; 4570 } 4571 4572 fixEncryptedMessage(content) { 4573 // Sprawdź czy wiadomoÅÄ wyglÄ da na zaszyfrowanÄ (base64 z dÅugimi ciÄ gami) 4574 if (typeof content !== "string") { 4575 return content; 4576 } 4577 4578 // JeÅli wiadomoÅÄ jest bardzo dÅuga i skÅada siÄ gÅównie z znaków base64 4579 if (content.length > 50 && /^[A-Za-z0-9+/=]+$/.test(content)) { 4580 debugWarn( 4581 "Detected potentially encrypted message:", 4582 content.substring(0, 50) + "...", 4583 ); 4584 4585 // Spróbuj zdekodowaÄ base64 - jeÅli siÄ nie uda, zwrÃ³Ä oryginalnÄ wiadomoÅÄ 4586 try { 4587 const decoded = atob(content); 4588 // Sprawdź czy zdekodowana wiadomoÅÄ zawiera sensowny tekst 4589 if ( 4590 decoded.length > 0 && 4591 decoded.length < content.length && 4592 /[a-zA-ZÄ ÄÄÅÅóÅźżÄÄÄÅÅÃÅŹŻ\s]/.test(decoded) 4593 ) { 4594 debugLog("Successfully decoded encrypted message"); 4595 return decoded; 4596 } 4597 } catch (e) { 4598 debugWarn("Failed to decode base64 message:", e); 4599 } 4600 } 4601 4602 // Sprawdź czy to nie jest ciÄ g znaków, który wyglÄ da na hash/ID zamiast wiadomoÅci 4603 if ( 4604 content.length > 30 && 4605 /^[A-Za-z0-9]{30,}$/.test(content) && 4606 !/\s/.test(content) 4607 ) { 4608 debugWarn( 4609 "Detected hash-like content instead of message:", 4610 content.substring(0, 30) + "...", 4611 ); 4612 // ZwrÃ³Ä komunikat zastÄpczy dla niezdekodowanych wiadomoÅci 4613 return "WiadomoÅÄ nie może zostaÄ wyÅwietlona (bÅÄ d dekodowania)"; 4614 } 4615 4616 return content; 4617 } 4618 4619 generateAvatarHtml(type) { 4620 if (type === "user") { 4621 return ` 4622 <div class="bb-asystent-ai-message-avatar"> 4623 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 4624 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path> 4625 <circle cx="12" cy="7" r="4"></circle> 4626 </svg> 4627 </div> 4628 `; 4629 } 4630 4631 // Awatar asystenta na podstawie ustawieÅ 4632 const avatarType = this.settings.botAvatarType || "icon"; 4633 let avatarContent = ""; 4634 4635 if (avatarType === "image" && this.settings.botAvatarUrl) { 4636 avatarContent = `<img src="${this.settings.botAvatarUrl}" alt="${this.strings.botName}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;" />`; 4637 } else if (avatarType === "emoji") { 4638 const iconClassMap = { 4639 user: "dashicons-admin-users", 4640 robot: "dashicons-admin-generic", 4641 heart: "dashicons-heart", 4642 star: "dashicons-star-filled", 4643 flower: "dashicons-palmtree", 4644 angel: "dashicons-universal-access", 4645 dove: "dashicons-editor-removeformatting", 4646 candle: "dashicons-lightbulb", 4647 }; 4648 const iconClass = 4649 iconClassMap[this.settings.botAvatarIcon] || 4650 iconClassMap["user"]; 4651 avatarContent = `<span class="dashicons ${iconClass}" style="font-size: 16px;"></span>`;
4652 } else { 4653 // Default SVG icon (robot) 4654 avatarContent = ` 4655 <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 4656 <rect x="6" y="8" width="12" height="8" rx="2"></rect> 4657 <circle cx="10" cy="12" r="1"></circle> 4658 <circle cx="14" cy="12" r="1"></circle> 4659 <path d="M12 8V5"></path> 4660 <path d="M8 16v2h8v-2"></path> 4661 </svg> 4662 `; 4663 } 4664 4665 return ` 4666 <div class="bb-asystent-ai-message-avatar" title="${this.strings.botName}"> 4667 ${avatarContent} 4668 </div> 4669 `; 4670 } 4671 4672 showTypingIndicator() { 4673 // UsuÅ poprzedni wskaźnik jeÅli istnieje 4674 this.hideTypingIndicator(); 4675 4676 const indicator = document.createElement("div"); 4677 indicator.id = "bb-asystent-ai-typing"; 4678 indicator.className = "bb-asystent-ai-typing"; 4679 4680 const avatarHtml = this.generateAvatarHtml("assistant"); 4681 4682 indicator.innerHTML = ` 4683 ${avatarHtml} 4684 <div class="typing-indicator-wrapper"> 4685 <div class="typing-indicator"> 4686 <span></span> 4687 <span></span> 4688 <span></span> 4689 </div> 4690 <div class="typing-status" style="display: none;"></div> 4691 </div> 4692 `; 4693 4694 const messages = document.getElementById( 4695 "bb-asystent-ai-messages", 4696 ); 4697 if (messages) { 4698 messages.appendChild(indicator); 4699 this.scrollToBottom(); 4700 this.updateSendButton(true); 4701 } 4702 } 4703 4704 cleanRenderedHtml(html) { 4705 if (!html) return ""; 4706 try { 4707 const parser = new DOMParser(); 4708 const doc = parser.parseFromString( 4709 `<div id="bbai-wrap">${html}</div>`, 4710 "text/html", 4711 ); 4712 const wrapper = doc.getElementById("bbai-wrap"); 4713 if (!wrapper) return html; 4714 4715 const unwrapMarkdownHref = (href) => { 4716 let value = (href || "").toString().trim(); 4717 for (let i = 0; i < 3; i++) { 4718 const match = value.match(/^\[([^\]]+)\]\(([^)]+)\)$/); 4719 if (!match) { 4720 break; 4721 } 4722 value = (match[2] || "").trim(); 4723 } 4724 const angle = value.match(/^<([^>]+)>$/); 4725 if (angle) { 4726 value = (angle[1] || "").trim(); 4727 } 4728 if (/^www\./i.test(value)) { 4729 value = `https://${value}`; 4730 } 4731 return value; 4732 }; 4733 4734 const normalizeTelHref = (href, text) => { 4735 const raw = href.replace(/^tel:/i, ""); 4736 const fromHref = raw.replace(/[^\d+]/g, ""); 4737 const fromText = (text || "").replace(/[^\d+]/g, ""); 4738 let digits = fromHref || fromText; 4739 if (!digits) { 4740 return ""; 4741 } 4742 if (!digits.startsWith("+")) { 4743 digits = digits.startsWith("48") 4744 ? "+" + digits 4745 : "+48" + digits; 4746 } 4747 return `tel:${digits}`; 4748 }; 4749 4750 const formatTelText = (href, text) => { 4751 const digits = (href || text || "").replace(/\D/g, ""); 4752 if (digits.length === 11 && digits.startsWith("48")) { 4753 return `+48 ${digits.slice(2, 5)} ${digits.slice(5, 8)} ${digits.slice(8)}`; 4754 } 4755 if (digits.length === 9) { 4756 return `+48 ${digits.slice(0, 3)} ${digits.slice(3, 6)} ${digits.slice(6)}`; 4757 } 4758 return (text || "").replace(/[âð]/g, "").replace(/\s+/g, " ").trim(); 4759 }; 4760 4761 const normalizeMailtoHref = (href, text) => { 4762 const raw = href.replace(/^mailto:/i, ""); 4763 const candidate = (raw || text || "").trim(); 4764 if (!candidate) { 4765 return ""; 4766 } 4767 return `mailto:${candidate}`; 4768 }; 4769 4770 wrapper.querySelectorAll("a").forEach((link) => { 4771 const nestedLinks = link.querySelectorAll("a"); 4772 nestedLinks.forEach((nested) => { 4773 nested.replaceWith( 4774 doc.createTextNode(nested.textContent || ""), 4775 ); 4776 }); 4777 4778 const rawHref = link.getAttribute("href") || ""; 4779 const href = unwrapMarkdownHref(rawHref); 4780 if (href && href !== rawHref) { 4781 link.setAttribute("href", href); 4782 } 4783 4784 // Normalize/standardize link classes and safe attributes. 4785 link.classList.add("fac-link"); 4786 const isHttp = 4787 /^https?:\/\//i.test(href) || /^www\./i.test(href); 4788 const isMail = /^mailto:/i.test(href); 4789 const isTel = /^tel:/i.test(href); 4790 const isAnchor = /^#/i.test(href); 4791 const isMaps = 4792 /(?:^https?:\/\/)?(?:www\.)?(?:maps\.google\.com|google\.com\/maps|goo\.gl\/maps)/i.test( 4793 href, 4794 ); 4795 if (isMaps) { 4796 link.classList.add("maps-link"); 4797 } else if (isTel) { 4798 link.classList.add("phone-link"); 4799 } else if (isMail) { 4800 link.classList.add("email-link"); 4801 } else if (isHttp) { 4802 link.classList.add("external-link"); 4803 } 4804 4805 if (isHttp && !isAnchor && !isMail && !isTel) {
4806 applyLinkTargetPolicy(link, href); 4807 } 4808 4809 if (/^tel:/i.test(href)) { 4810 const needsFix = 4811 href.includes("<") || 4812 href.includes(">") || 4813 href.includes("<") || 4814 href.includes(">") || 4815 href.includes("class=") || 4816 href.includes("[") || 4817 href.includes("]") || 4818 href.includes("(") || 4819 href.includes(")") || 4820 /[âð]/.test(href) || 4821 /tel:|\[|\]|\(|\)|[âð]/i.test(link.textContent || ""); 4822 if (needsFix) { 4823 const normalized = normalizeTelHref( 4824 href, 4825 link.textContent, 4826 ); 4827 if (normalized) { 4828 link.setAttribute("href", normalized); 4829 link.textContent = formatTelText(normalized, link.textContent); 4830 } 4831 } 4832 } 4833 4834 if (/^mailto:/i.test(href)) { 4835 const needsFix = 4836 href.includes("<") || 4837 href.includes(">") || 4838 href.includes("<") || 4839 href.includes(">") || 4840 href.includes("class="); 4841 if (needsFix) { 4842 const normalized = normalizeMailtoHref( 4843 href, 4844 link.textContent, 4845 ); 4846 if (normalized) { 4847 link.setAttribute("href", normalized); 4848 } 4849 } 4850 } 4851 }); 4852 4853 // Add consistent "Mapa" links for address-like lines, without wrapping the address itself. 4854 // This avoids giant anchors and accidental capture of surrounding sentences. 4855 // 4856 // Rules: 4857 // - require a PL postal code and a street keyword (ul./al./plac/przy/etc) 4858 // - append a separate "(Mapa)" link based on a minimal query "postal city" 4859 const hasStreetKeyword = (t) => 4860 /\b(ul\.?|ulica|al\.?|aleja|pl\.?|plac|przy|os\.?|rynek)\b/i.test( 4861 t || "", 4862 ); 4863 const postalCityRe = /\b(\d{2}-\d{3})\s+([\p{L}][\p{L}-]{1,40})\b/iu; 4864 wrapper.querySelectorAll("li, p").forEach((node) => { 4865 if (!node || node.querySelector("a.maps-link")) { 4866 return; 4867 } 4868 const text = (node.textContent || "").replace(/\s+/g, " ").trim(); 4869 if (!text || !/\b\d{2}-\d{3}\b/.test(text) || !hasStreetKeyword(text)) { 4870 return; 4871 } 4872 const m = text.match(postalCityRe); 4873 if (!m) { 4874 return; 4875 } 4876 const postal = (m[1] || "").trim(); 4877 const city = (m[2] || "").trim(); 4878 if (!postal || !city) { 4879 return; 4880 } 4881 4882 const href = `https://maps.google.com/maps?q=${encodeURIComponent( 4883 `${postal} ${city}, Polska`, 4884 )}`; 4885 4886 const mapLink = doc.createElement("a"); 4887 mapLink.className = "fac-link maps-link"; 4888 mapLink.setAttribute("href", href); 4889 mapLink.setAttribute("target", "_blank"); 4890 mapLink.setAttribute("rel", "noopener noreferrer nofollow ugc"); 4891 mapLink.textContent = "Mapa"; 4892 4893 node.appendChild(doc.createTextNode(" (")); 4894 node.appendChild(mapLink); 4895 node.appendChild(doc.createTextNode(")")); 4896 }); 4897 4898 wrapper.querySelectorAll("li").forEach((li) => { 4899 const text = (li.textContent || "").trim(); 4900 // Zachowaj <li> jeÅli zawiera obrazki, nawet jeÅli textContent jest pusty 4901 const hasImages = li.querySelector("img"); 4902 if (text === "" && !hasImages) { 4903 li.remove(); 4904 } 4905 }); 4906 4907 wrapper.querySelectorAll("ul,ol").forEach((list) => { 4908 if (!list.querySelector("li")) { 4909 list.replaceWith(...Array.from(list.childNodes)); 4910 return; 4911 } 4912 4913 // Sprawdź czy lista zawiera tylko obrazki (bez tekstu) 4914 // JeÅli tak, usuÅ numeracjÄ/kropki i wyÅwietl obrazki jeden pod drugim 4915 const listItems = list.querySelectorAll("li"); 4916 let hasOnlyImages = true;
4917 listItems.forEach((li) => { 4918 const text = (li.textContent || "").trim(); 4919 const hasImg = li.querySelector("img"); 4920 if (text !== "" || !hasImg) { 4921 hasOnlyImages = false; 4922 } 4923 }); 4924 4925 if (hasOnlyImages && listItems.length > 0) { 4926 // ZamieÅ listÄ na div z obrazkami (bez numeracji) 4927 const imagesContainer = doc.createElement("div"); 4928 imagesContainer.className = "bb-images-gallery"; 4929 listItems.forEach((li) => { 4930 const imgs = li.querySelectorAll("img"); 4931 imgs.forEach((img) => { 4932 imagesContainer.appendChild(img.cloneNode(true)); 4933 }); 4934 }); 4935 list.replaceWith(imagesContainer); 4936 } 4937 }); 4938 4939 return wrapper.innerHTML; 4940 } catch (e) { 4941 return html.replace(/<li>\s*<\/li>/g, ""); 4942 } 4943 } 4944 4945 4946 hideTypingIndicator() { 4947 const indicator = document.getElementById("bb-asystent-ai-typing"); 4948 if (indicator) { 4949 indicator.remove(); 4950 } 4951 this.updateSendButton(false); 4952 this.stopAssistantPersonaLoop(); 4953 } 4954 4955 updateSendButton(loading) { 4956 const button = document.querySelector( 4957 '#bb-asystent-ai-form button[type="submit"]', 4958 ); 4959 if (!button) return; 4960 4961 if (loading) { 4962 button.disabled = true; 4963 button.classList.add("loading"); 4964 button.innerHTML = ` 4965 <style> 4966 @keyframes fac-dot{0%,80%,100%{opacity:.35;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}} 4967 </style> 4968 <div style="display:flex;gap:4px;align-items:center;justify-content:center;width:18px;height:18px;"> 4969 <span style="display:block;width:4px;height:4px;border-radius:50%;background:currentColor;animation:fac-dot 1s infinite ease-in-out;animation-delay:0s;"></span> 4970 <span style="display:block;width:4px;height:4px;border-radius:50%;background:currentColor;animation:fac-dot 1s infinite ease-in-out;animation-delay:.15s;"></span> 4971 <span style="display:block;width:4px;height:4px;border-radius:50%;background:currentColor;animation:fac-dot 1s infinite ease-in-out;animation-delay:.3s;"></span> 4972 </div> 4973 `; 4974 } else { 4975 button.disabled = false; 4976 button.classList.remove("loading"); 4977 button.innerHTML = ` 4978 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 4979 <line x1="22" y1="2" x2="11" y2="13"></line> 4980 <polygon points="22,2 15,22 11,13 2,9 22,2"></polygon> 4981 </svg> 4982 `; 4983 } 4984 } 4985 4986 showError(message) { 4987 // Remove existing error 4988 const existingError = document.getElementById( 4989 "bb-asystent-ai-error", 4990 ); 4991 if (existingError) { 4992 existingError.remove(); 4993 } 4994 4995 // Create new error 4996 const errorDiv = document.createElement("div"); 4997 errorDiv.id = "bb-asystent-ai-error"; 4998 errorDiv.className = "bb-asystent-ai-error"; 4999 errorDiv.textContent = message; 5000 5001 const messages = document.getElementById( 5002 "bb-asystent-ai-messages", 5003 ); 5004 if (messages) { 5005 messages.appendChild(errorDiv); 5006 this.scrollToBottom(); 5007 5008 // Auto remove after 5 seconds 5009 setTimeout(() => { 5010 if (errorDiv.parentNode) { 5011 errorDiv.remove(); 5012 } 5013 }, 5000); 5014 } 5015 } 5016
5017 scrollToBottom(retry = 0) { 5018 const messages = document.getElementById( 5019 "bb-asystent-ai-messages", 5020 ); 5021 if (!messages) return; 5022 5023 const chatWindow = document.getElementById( 5024 "bb-asystent-ai-window", 5025 ); 5026 const isHidden = 5027 !chatWindow || 5028 window.getComputedStyle(chatWindow).display === "none" || 5029 !chatWindow.classList.contains("bb-ai-show"); 5030 5031 const doScroll = () => { 5032 try { 5033 messages.scrollTop = messages.scrollHeight; 5034 } catch (e) {} 5035 }; 5036 5037 if (isHidden) { 5038 // Defer auto-scroll until the chat becomes visible 5039 if ( 5040 typeof MutationObserver !== "undefined" && 5041 chatWindow && 5042 !this._scrollOnOpenObserver 5043 ) { 5044 this._scrollOnOpenObserver = new MutationObserver(() => { 5045 const visible = 5046 window.getComputedStyle(chatWindow).display !== 5047 "none" && chatWindow.classList.contains("bb-ai-show"); 5048 if (visible) { 5049 this._scrollOnOpenObserver.disconnect(); 5050 this._scrollOnOpenObserver = null; 5051 setTimeout(doScroll, 50); 5052 } 5053 }); 5054 this._scrollOnOpenObserver.observe(chatWindow, { 5055 attributes: true, 5056 attributeFilter: ["style", "class"], 5057 }); 5058 } 5059 5060 // Fallback: retry a few times if observer is unavailable 5061 if (retry < 10) { 5062 setTimeout(() => this.scrollToBottom(retry + 1), 150); 5063 } 5064 return; 5065 } 5066 5067 // If visible, scroll on the next frame for consistent layout 5068 if (typeof requestAnimationFrame === "function") { 5069 requestAnimationFrame(doScroll); 5070 } else { 5071 doScroll(); 5072 } 5073 } 5074 5075 scrollToMessageTop(el, retry = 0) { 5076 const messages = document.getElementById("bb-asystent-ai-messages"); 5077 if (!messages || !el) return; 5078 5079 const chatWindow = document.getElementById("bb-asystent-ai-window"); 5080 const isHidden = 5081 !chatWindow || 5082 window.getComputedStyle(chatWindow).display === "none" || 5083 !chatWindow.classList.contains("bb-ai-show"); 5084 5085 const doScroll = () => { 5086 try { 5087 const offset = Math.max(0, el.offsetTop - 8); 5088 messages.scrollTop = offset; 5089 } catch (e) {} 5090 }; 5091 5092 if (isHidden) { 5093 if (retry < 10) { 5094 setTimeout(() => this.scrollToMessageTop(el, retry + 1), 150); 5095 } 5096 return; 5097 } 5098 5099 if (typeof requestAnimationFrame === "function") { 5100 requestAnimationFrame(doScroll); 5101 } else { 5102 doScroll(); 5103 } 5104 } 5105 5106 adjustAssistantMessageScroll(el, retry = 0) { 5107 const messages = document.getElementById("bb-asystent-ai-messages"); 5108 if (!messages || !el) return; 5109 5110 const chatWindow = document.getElementById("bb-asystent-ai-window"); 5111 const isHidden = 5112 !chatWindow || 5113 window.getComputedStyle(chatWindow).display === "none" || 5114 !chatWindow.classList.contains("bb-ai-show"); 5115 5116 if (isHidden) { 5117 if (retry < 10) { 5118 setTimeout( 5119 () => this.adjustAssistantMessageScroll(el, retry + 1), 5120 150, 5121 ); 5122 } 5123 return; 5124 } 5125 5126 const doScroll = () => { 5127 try { 5128 const messageHeight = el.getBoundingClientRect().height; 5129 const availableHeight = Math.max(0, messages.clientHeight - 16); 5130 const fitsInViewport = messageHeight <= availableHeight; 5131 5132 if (fitsInViewport) { 5133 this._topPinnedAssistantMessages.delete(el); 5134 messages.scrollTop = messages.scrollHeight; 5135 return; 5136 } 5137 5138 if (this._topPinnedAssistantMessages.has(el)) { 5139 return; 5140 } 5141 5142 messages.scrollTop = Math.max(0, el.offsetTop - 8); 5143 this._topPinnedAssistantMessages.add(el); 5144 } catch (e) {} 5145 }; 5146 5147 if (typeof requestAnimationFrame === "function") { 5148 requestAnimationFrame(doScroll); 5149 } else { 5150 doScroll(); 5151 } 5152 } 5153 5154 resetMessageFlagsForSession() { 5155 this.welcomeMessageShown = false;
5156 this.goodbyeMessageShown = false; 5157 } 5158 5159 getEventTargetElement(event) { 5160 if (!event || !event.target) { 5161 return null; 5162 } 5163 5164 if ( 5165 typeof Element !== "undefined" && 5166 event.target instanceof Element 5167 ) { 5168 return event.target; 5169 } 5170 5171 return event.target.parentElement || null; 5172 } 5173 5174 findClosestMatch(element, selector) { 5175 if (!element) { 5176 return null; 5177 } 5178 5179 if (typeof element.closest === "function") { 5180 try { 5181 return element.closest(selector); 5182 } catch (error) { 5183 // Ignore selector errors 5184 } 5185 } 5186 5187 let node = element; 5188 while (node && node.nodeType === 1) { 5189 const matches = 5190 node.matches || 5191 node.msMatchesSelector || 5192 node.webkitMatchesSelector || 5193 node.mozMatchesSelector; 5194 if (matches && matches.call(node, selector)) { 5195 return node; 5196 } 5197 node = node.parentElement; 5198 } 5199 5200 return null; 5201 } 5202 5203 normalizeSystemText(value) { 5204 if (value === null || value === undefined) { 5205 return ""; 5206 } 5207 5208 let text = typeof value === "string" ? value : String(value); 5209 text = text.replace(/[\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu, ""); 5210 text = text.replace(/<[^>]*>/g, " "); 5211 return text.replace(/\s+/g, " ").trim().toLowerCase(); 5212 } 5213 5214 markSystemMessage(type) { 5215 const sessionKey = this.sessionId || "default"; 5216 5217 if (type === "welcome") { 5218 this.lastWelcomeSession = sessionKey; 5219 this.welcomeMessageShown = true; 5220 } else if (type === "goodbye") { 5221 this.lastGoodbyeSession = sessionKey; 5222 this.goodbyeMessageShown = true; 5223 } 5224 } 5225 5226 getSystemMessageTemplate(type) { 5227 if (type === "welcome") { 5228 return ( 5229 this.strings.welcomeMessage || 5230 this.strings.greetingMessage || 5231 "Witaj! Jestem asystentem AI. W czym mogÄ Ci pomóc?" 5232 ); 5233 } 5234 if (type === "goodbye") { 5235 return this.strings.goodbyeMessage || ""; 5236 } 5237 return ""; 5238 } 5239 5240 ensureSingleSystemMessage(type) { 5241 const messagesContainer = document.getElementById( 5242 "bb-asystent-ai-messages", 5243 ); 5244 if (!messagesContainer) { 5245 return; 5246 } 5247 5248 const normalizedTarget = this.normalizeSystemText(
5249 this.getSystemMessageTemplate(type), 5250 ); 5251 if (!normalizedTarget) { 5252 return; 5253 } 5254 5255 let found = false; 5256 messagesContainer 5257 .querySelectorAll(".bb-asystent-ai-message.assistant") 5258 .forEach((msg) => { 5259 const contentEl = msg.querySelector( 5260 ".bb-asystent-ai-message-content", 5261 ); 5262 if (!contentEl) { 5263 return; 5264 } 5265 const normalizedCurrent = this.normalizeSystemText( 5266 contentEl.textContent || "", 5267 ); 5268 if (normalizedCurrent === normalizedTarget) { 5269 if (found) { 5270 msg.remove(); 5271 } else { 5272 found = true; 5273 msg.dataset.systemMessage = type; 5274 } 5275 } 5276 }); 5277 5278 if (found) { 5279 this.markSystemMessage(type); 5280 } 5281 } 5282 5283 addWelcomeMessageOnce(messageOverride = null) { 5284 const sessionKey = this.sessionId || "default"; 5285 if ( 5286 this.welcomeMessageShown && 5287 this.lastWelcomeSession === sessionKey 5288 ) { 5289 return false; 5290 } 5291 5292 let welcomeMessage = messageOverride; 5293 if ( 5294 typeof welcomeMessage === "object" && 5295 welcomeMessage !== null 5296 ) { 5297 try { 5298 welcomeMessage = JSON.stringify(welcomeMessage); 5299 } catch (error) { 5300 welcomeMessage = ""; 5301 } 5302 } 5303 5304 if ( 5305 typeof welcomeMessage !== "string" || 5306 welcomeMessage.trim() === "" 5307 ) { 5308 welcomeMessage = 5309 this.strings.welcomeMessage || 5310 this.strings.greetingMessage || 5311 "Witaj! Jestem asystentem AI. W czym mogÄ Ci pomóc?"; 5312 } 5313 5314 const node = this.addMessage( 5315 "assistant", 5316 welcomeMessage, 5317 null, 5318 true, 5319 null, 5320 { type: "system_welcome" }, 5321 ); 5322 if (node) { 5323 node.dataset.systemMessage = "welcome"; 5324 } 5325 this.ensureSingleSystemMessage("welcome"); 5326 return true; 5327 } 5328 5329 addGoodbyeMessageOnce(messageOverride = null) { 5330 let goodbyeMessage = 5331 typeof messageOverride === "string" 5332 ? messageOverride 5333 : this.strings.goodbyeMessage || ""; 5334 5335 if ( 5336 typeof goodbyeMessage === "object" && 5337 goodbyeMessage !== null 5338 ) { 5339 try { 5340 goodbyeMessage = JSON.stringify(goodbyeMessage); 5341 } catch (error) { 5342 goodbyeMessage = ""; 5343 } 5344 } 5345 5346 if (typeof goodbyeMessage !== "string") { 5347 goodbyeMessage = ""; 5348 } 5349 5350 goodbyeMessage = goodbyeMessage.trim(); 5351 if (goodbyeMessage === "") { 5352 return false; 5353 } 5354 5355 const sessionKey = this.sessionId || "default"; 5356 if ( 5357 this.goodbyeMessageShown && 5358 this.lastGoodbyeSession === sessionKey 5359 ) { 5360 return false; 5361 } 5362 5363 const node = this.addMessage( 5364 "assistant", 5365 goodbyeMessage, 5366 null, 5367 true, 5368 null, 5369 { type: "system_goodbye" }, 5370 ); 5371 if (node) { 5372 node.dataset.systemMessage = "goodbye"; 5373 } 5374 this.ensureSingleSystemMessage("goodbye"); 5375 return true; 5376 } 5377 5378 autoResizeInput() { 5379 const input = document.getElementById("bb-asystent-ai-input"); 5380 if (!input) return; 5381 5382 input.style.height = "auto"; 5383 input.style.height = Math.min(input.scrollHeight, 120) + "px"; 5384 } 5385 5386 getCurrentTime() { 5387 const now = new Date(); 5388 return now.toLocaleTimeString("pl-PL", { 5389 hour: "2-digit", 5390 minute: "2-digit", 5391 }); 5392 } 5393 5394 /** 5395 * Dekoduje wielokrotnie zakodowane encje HTML 5396 * Naprawia problemy z wielokrotnym kodowaniem (np. &amp;amp;nbsp; -> ) 5397 */ 5398 decodeHtmlEntities(text) { 5399 if (typeof text !== "string") { 5400 return text; 5401 } 5402 5403 // Dekoduj wielokrotnie zakodowane encje HTML
5404 // Używamy textarea, który automatycznie dekoduje encje HTML 5405 let decoded = text; 5406 let previousDecoded = ""; 5407 5408 // Dekoduj wielokrotnie, aż nie bÄdzie wiÄcej zmian 5409 while (decoded !== previousDecoded) { 5410 previousDecoded = decoded; 5411 const textarea = document.createElement("textarea"); 5412 textarea.innerHTML = decoded; 5413 decoded = textarea.value; 5414 } 5415 5416 return decoded; 5417 } 5418 5419 escapeHtml(text) { 5420 const div = document.createElement("div"); 5421 div.textContent = text == null ? "" : String(text); 5422 return div.innerHTML; 5423 } 5424 5425 _safeParseJson(value) { 5426 if (typeof value !== "string" || value === "") { 5427 return null; 5428 } 5429 try { 5430 return JSON.parse(value); 5431 } catch (e) { 5432 return null; 5433 } 5434 } 5435 5436 loadChatHistory() { 5437 if (this.chatHistoryLoaded) { 5438 debugLog( 5439 "FUNERAL DEBUG: Chat history already loaded, skipping...", 5440 ); 5441 return; 5442 } 5443 5444 this.chatHistoryLoaded = true; 5445 debugLog("FUNERAL DEBUG: Loading chat history..."); 5446 5447 const messagesContainer = document.getElementById( 5448 "bb-asystent-ai-messages", 5449 ); 5450 5451 const renderHistory = (messages) => { 5452 if (!messagesContainer) { 5453 return; 5454 } 5455 5456 const greetingMessages = messagesContainer.querySelectorAll( 5457 ".bb-asystent-ai-message", 5458 ); 5459 const greetingText = 5460 this.strings.welcomeMessage || 5461 this.strings.greetingMessage || 5462 "Witaj!"; 5463 greetingMessages.forEach((msg) => { 5464 if (msg.textContent.includes(greetingText)) { 5465 msg.remove(); 5466 } 5467 }); 5468 5469 const normalizedWelcome = this.normalizeSystemText( 5470 this.strings.welcomeMessage || 5471 this.strings.greetingMessage || 5472 "", 5473 ); 5474 const normalizedGoodbye = this.normalizeSystemText( 5475 this.strings.goodbyeMessage || "", 5476 ); 5477 5478 messages.forEach((msg) => { 5479 // Wpisy tool_call majÄ osobny rendering (kompaktowy kafelek). 5480 if ((msg.type || msg.message_type) === "tool_call") { 5481 const tc = msg.tool_call || 5482 (typeof msg.content === "string" && msg.content.startsWith("{") 5483 ? this._safeParseJson(msg.content)?.tool_call 5484 : null); 5485 if (tc) { 5486 this.addToolCallMessage(tc, { persist: false, scroll: false }); 5487 } 5488 return; 5489 } 5490 5491 const messageId = msg.messageId || msg.id || null; 5492 // Upewnij siÄ, że content jest stringiem 5493 let content = msg.content; 5494 if (typeof content === "object") { 5495 debugWarn( 5496 "FUNERAL DEBUG: Message content is object, converting to string:", 5497 content, 5498 ); 5499 content = JSON.stringify(content); 5500 } 5501 const normalizedContent = this.normalizeSystemText(content); 5502 if ( 5503 normalizedWelcome && 5504 normalizedContent === normalizedWelcome && 5505 msg.type === "assistant" 5506 ) { 5507 this.markSystemMessage("welcome"); 5508 } 5509 if ( 5510 normalizedGoodbye && 5511 normalizedContent === normalizedGoodbye && 5512 msg.type === "assistant" 5513 ) { 5514 this.markSystemMessage("goodbye"); 5515 } 5516 this.addMessage( 5517 msg.type, 5518 content, 5519 msg.timestamp, 5520 true, 5521 messageId, 5522 ); 5523 }); 5524 5525 this.ensureSingleSystemMessage("welcome"); 5526 this.ensureSingleSystemMessage("goodbye"); 5527 5528 this.scrollToBottom(); 5529 if (typeof requestIdleCallback === "function") { 5530 requestIdleCallback(() => this.persistMessagesCache()); 5531 } else { 5532 setTimeout(() => this.persistMessagesCache(), 150); 5533 } 5534 }; 5535 5536 if ( 5537 Array.isArray(this.persistedMessages) && 5538 this.persistedMessages.length > 0 5539 ) { 5540 debugLog( 5541 "FUNERAL DEBUG: Rendering persisted messages from cache", 5542 ); 5543 // JeÅli widok startowy już ma wiadomoÅci (np. welcome), nie dubluj 5544 const existing = messagesContainer.querySelectorAll( 5545 ".bb-asystent-ai-message", 5546 ); 5547 if (!existing || existing.length === 0) { 5548 renderHistory(this.persistedMessages); 5549 } 5550 this.hideQuickQuestions(); 5551 return; 5552 } 5553 5554 const formData = new FormData(); 5555 const action = "bbai_ai_load_chat_history"; 5556 const url = this.getAjaxUrl(action); 5557 formData.append("action", action); 5558 formData.append("session_id", this.sessionId); 5559 formData.append("nonce", BBAsystentAI.nonce); 5560 5561 fetch(url, { 5562 method: "POST", 5563 body: formData, 5564 }) 5565 .then((response) => response.json()) 5566 .then((data) => { 5567 debugLog("FUNERAL DEBUG: Chat history response:", data); 5568 5569 if ( 5570 data.success && 5571 Array.isArray(data.data.messages) && 5572 data.data.messages.length > 0 5573 ) { 5574 debugLog( 5575 "FUNERAL DEBUG: Found", 5576 data.data.messages.length, 5577 "messages in history", 5578 ); 5579 renderHistory(data.data.messages); 5580 this.hideQuickQuestions(); 5581 } else { 5582 const welcomeMessage = 5583 this.strings.welcomeMessage || 5584 "Witaj! Jak mogÄ Ci pomóc?"; 5585 if (this.addWelcomeMessageOnce(welcomeMessage)) { 5586 this.showQuickQuestions(); 5587 this.scrollToBottom(); 5588 this.persistMessagesCache(); 5589 } 5590 } 5591 }) 5592 .catch((error) => { 5593 debugError( 5594 "FUNERAL DEBUG: Error loading chat history:", 5595 error, 5596 ); 5597 const welcomeMessage = 5598 this.strings.welcomeMessage || 5599 "Witaj! Jak mogÄ Ci pomóc?"; 5600 if (this.addWelcomeMessageOnce(welcomeMessage)) { 5601 this.showQuickQuestions(); 5602 this.scrollToBottom(); 5603 } 5604 }); 5605 } 5606 5607 /** 5608 * Konfiguruje Åledzenie klikniÄÄ dla wiadomoÅci 5609 */ 5610 setupClickTracking(messageElement) { 5611 debugLog( 5612 "FUNERAL DEBUG: setupClickTracking called for element:", 5613 messageElement, 5614 ); 5615 5616 // Znajdź wszystkie linki w wiadomoÅci 5617 const links = messageElement.querySelectorAll("a"); 5618 debugLog("FUNERAL DEBUG: Found", links.length, "links in message"); 5619 5620 links.forEach((link, index) => { 5621 debugLog( 5622 `FUNERAL DEBUG: Setting up tracking for link ${index + 1}:`, 5623 link.href, 5624 link.textContent, 5625 ); 5626 5627 link.addEventListener("click", (event) => { 5628 debugLog("FUNERAL DEBUG: Link clicked!", link.href); 5629 5630 // Nie blokuj domyÅlnego dziaÅania - pozwól na otwarcie linku 5631 // ale Åledź klikniÄcie 5632 const href = link.getAttribute("href"); 5633 const text = link.textContent || href; 5634 5635 // Wykryj typ klikniÄcia 5636 const clickType = this.detectClickType(href); 5637 debugLog("FUNERAL DEBUG: Detected click type:", clickType); 5638 5639 // WyÅlij dane Åledzenia 5640 this.trackClick(clickType, href, text, messageElement); 5641 }); 5642 }); 5643 5644 // USUNIÄTO: detectAndWrapPhoneNumbers - telefony sÄ już parsowane w PHP przez MarkdownService 5645 } 5646 5647 /** 5648 * Wykrywa typ klikniÄcia na podstawie href 5649 */ 5650 detectClickType(href) { 5651 debugLog("FUNERAL DEBUG: detectClickType called with href:", href); 5652 debugLog( 5653 "FUNERAL DEBUG: window.location.hostname:", 5654 window.location.hostname, 5655 ); 5656 debugLog( 5657 "FUNERAL DEBUG: window.location.origin:", 5658 window.location.origin, 5659 ); 5660 5661 if (!href) return "link"; 5662 5663 if (href.startsWith("tel:")) { 5664 debugLog("FUNERAL DEBUG: Detected as phone"); 5665 return "phone"; 5666 } else if (href.startsWith("mailto:")) { 5667 debugLog("FUNERAL DEBUG: Detected as email"); 5668 return "email"; 5669 } else if (this.isInternalLink(href)) { 5670 debugLog("FUNERAL DEBUG: Detected as internal_link"); 5671 return "internal_link"; 5672 } else { 5673 debugLog("FUNERAL DEBUG: Detected as external link"); 5674 return "link"; 5675 } 5676 } 5677 5678 /** 5679 * Sprawdza czy link jest wewnÄtrzny (do tej samej domeny) 5680 */ 5681 isInternalLink(href) { 5682 debugLog("FUNERAL DEBUG: isInternalLink called with href:", href); 5683 try { 5684 const url = new URL(href, window.location.origin); 5685 debugLog("FUNERAL DEBUG: Parsed URL origin:", url.origin); 5686 debugLog( 5687 "FUNERAL DEBUG: Current origin:", 5688 window.location.origin, 5689 ); 5690 5691 // Porównaj origin (protocol + hostname + port) zamiast tylko hostname 5692 const isInternal = url.origin === window.location.origin; 5693 debugLog("FUNERAL DEBUG: Is internal (by origin)?", isInternal); 5694 5695 // Fallback - jeÅli origin siÄ nie zgadza, sprawdź hostname (dla przypadków różnych protokoÅów) 5696 if (!isInternal) { 5697 const hostnameMatch = 5698 url.hostname === window.location.hostname; 5699 debugLog( 5700 "FUNERAL DEBUG: Is internal (by hostname)?", 5701 hostnameMatch, 5702 ); 5703 return hostnameMatch; 5704 } 5705 5706 return isInternal; 5707 } catch (e) { 5708 debugLog( 5709 "FUNERAL DEBUG: URL parsing failed, checking if relative:", 5710 e.message, 5711 ); 5712 // JeÅli URL jest relatywny, to jest wewnÄtrzny 5713 const isRelative = 5714 !href.startsWith("http://") && !href.startsWith("https://"); 5715 debugLog("FUNERAL DEBUG: Is relative (internal)?", isRelative); 5716 return isRelative; 5717 } 5718 } 5719 5720 /** 5721 * WysyÅa dane o klikniÄciu do serwera 5722 */ 5723 trackClick(clickType, clickValue, clickText, messageElement) { 5724 debugLog("FUNERAL DEBUG: Tracking click:", { 5725 clickType, 5726 clickValue, 5727 clickText, 5728 }); 5729 5730 // Znajdź ID wiadomoÅci (możemy je dodaÄ jako data attribute) 5731 const messageId = messageElement.dataset.messageId || 0; 5732 debugLog("FUNERAL DEBUG: Message ID for tracking:", messageId); 5733 debugLog("FUNERAL DEBUG: Session ID for tracking:", this.sessionId); 5734 debugLog("FUNERAL DEBUG: Nonce for tracking:", BBAsystentAI.nonce); 5735 5736 const action = "bbai_ai_track_click"; 5737 const url = this.getAjaxUrl(action); 5738 const formData = new FormData(); 5739 formData.append("action", action); 5740 formData.append("session_id", this.sessionId); 5741 formData.append("click_type", clickType); 5742 formData.append("click_value", clickValue); 5743 formData.append("click_text", clickText); 5744 formData.append("message_id", messageId); 5745 formData.append("nonce", BBAsystentAI.nonce); 5746 5747 debugLog( 5748 "FUNERAL DEBUG: FormData prepared, sending AJAX request to:", 5749 BBAsystentAI.ajaxUrl, 5750 ); 5751
5752 // WyÅlij dane w tle (nie czekaj na odpowiedź) 5753 fetch(url, { 5754 method: "POST", 5755 body: formData, 5756 }) 5757 .then((response) => { 5758 debugLog( 5759 "FUNERAL DEBUG: AJAX response received, status:", 5760 response.status, 5761 ); 5762 return response.json(); 5763 }) 5764 .then((data) => { 5765 debugLog("FUNERAL DEBUG: AJAX response data:", data); 5766 if (data.success) { 5767 debugLog("FUNERAL DEBUG: Click tracked successfully"); 5768 } else { 5769 debugWarn( 5770 "FUNERAL DEBUG: Failed to track click:", 5771 data.data, 5772 ); 5773 } 5774 }) 5775 .catch((error) => { 5776 debugError("FUNERAL DEBUG: Error tracking click:", error); 5777 }); 5778 } 5779 5780 trackEvent(eventName, data = {}) { 5781 // Google Analytics 4 tracking 5782 if (typeof gtag !== "undefined") { 5783 gtag("event", eventName, { 5784 event_category: "bb_asystent_ai", 5785 event_label: this.sessionId, 5786 ...data, 5787 }); 5788 } 5789 5790 // WordPress tracking hook 5791 if (typeof wp !== "undefined" && wp.hooks) { 5792 wp.hooks.doAction("bb_asystent_ai_event", eventName, data); 5793 } 5794 } 5795 5796 // Public API methods 5797 open() { 5798 this.openChat(); 5799 } 5800 5801 close() { 5802 this.closeChat(); 5803 } 5804 5805 sendCustomMessage(message) { 5806 const input = document.getElementById("bb-asystent-ai-input"); 5807 if (input) { 5808 input.value = message; 5809 this.sendMessage(); 5810 } 5811 } 5812 5813 isVisible() { 5814 return this.isOpen; 5815 } 5816 5817 // Menu methods 5818 toggleMenu() { 5819 const menu = document.getElementById("bb-asystent-ai-menu"); 5820 const trigger = document.getElementById("bb-asystent-ai-menu-trigger"); 5821 if (!menu || !trigger) { 5822 return; 5823 } 5824 5825 const isOpen = 5826 menu.classList.contains("bb-ai-show") || 5827 trigger.getAttribute("aria-expanded") === "true"; 5828 if (isOpen) { 5829 this.closeMenu(); 5830 } else { 5831 this.openMenu(); 5832 } 5833 } 5834 5835 openMenu() { 5836 const menu = document.getElementById("bb-asystent-ai-menu"); 5837 const trigger = document.getElementById("bb-asystent-ai-menu-trigger"); 5838 if (!menu || !trigger) { 5839 return; 5840 } 5841 const wrapper = trigger.closest(".bb-asystent-ai-menu-wrapper"); 5842 menu.classList.add("bb-ai-show"); 5843 menu.hidden = false; 5844 menu.removeAttribute("hidden"); 5845 menu.style.display = "block"; // awaryjnie wymuÅ widocznoÅÄ, gdy hidden nie zostanie usuniÄty 5846 // Ensure the menu stays within the viewport (open upwards if needed). 5847 try { 5848 // Reset to default "below trigger" positioning first. 5849 menu.style.top = "calc(100% + 6px)"; 5850 menu.style.bottom = "auto"; 5851 menu.style.right = "0"; 5852 menu.style.left = "auto"; 5853 menu.style.marginTop = "0"; 5854 menu.style.marginBottom = "0"; 5855 5856 // Measure after making it visible. 5857 const rect = menu.getBoundingClientRect(); 5858 const viewportH = 5859 typeof window !== "undefined" && window.innerHeight 5860 ? window.innerHeight 5861 : document.documentElement.clientHeight; 5862 const viewportW = 5863 typeof window !== "undefined" && window.innerWidth 5864 ? window.innerWidth 5865 : document.documentElement.clientWidth; 5866 5867 // If the menu would go below the viewport, open it above the trigger. 5868 if (rect.bottom > viewportH - 8) { 5869 menu.style.top = "auto"; 5870 menu.style.bottom = "calc(100% + 6px)"; 5871 } 5872 5873 // If it overflows horizontally, pin to the left edge of the wrapper. 5874 const rect2 = menu.getBoundingClientRect(); 5875 if (rect2.left < 8) { 5876 menu.style.left = "0"; 5877 menu.style.right = "auto"; 5878 } else if (rect2.right > viewportW - 8) { 5879 menu.style.right = "0"; 5880 menu.style.left = "auto"; 5881 } 5882 } catch (e) { 5883 // ignore positioning errors 5884 } 5885 trigger.setAttribute("aria-expanded", "true"); 5886 if (wrapper) { 5887 wrapper.setAttribute("data-menu-open", "1"); 5888 } 5889 } 5890 5891 closeMenu() { 5892 const menu = document.getElementById("bb-asystent-ai-menu"); 5893 const trigger = document.getElementById("bb-asystent-ai-menu-trigger"); 5894 if (!menu || !trigger) { 5895 return; 5896 } 5897 const wrapper = trigger.closest(".bb-asystent-ai-menu-wrapper"); 5898 menu.classList.remove("bb-ai-show"); 5899 menu.hidden = true; 5900 menu.setAttribute("hidden", "hidden"); 5901 menu.style.display = "none"; 5902 trigger.setAttribute("aria-expanded", "false"); 5903 if (wrapper) { 5904 wrapper.setAttribute("data-menu-open", "0"); 5905 } 5906 } 5907 5908 async showChatHistory() { 5909 try { 5910 const action = "bbai_ai_get_chat_sessions"; 5911 const url = this.getAjaxUrl(action); 5912 const formData = new FormData(); 5913 formData.append("action", action); 5914 formData.append("nonce", BBAsystentAI.nonce); 5915 5916 const response = await fetch(url, { 5917 method: "POST", 5918 body: formData, 5919 }); 5920 5921 const result = await response.json(); 5922 5923 if (result.success) { 5924 this.displayChatSessions(result.data.sessions); 5925 } else { 5926 this.showError("Nie udaÅo siÄ zaÅadowaÄ historii czatu."); 5927 } 5928 } catch (error) { 5929 debugError("Error loading chat history:", error); 5930 this.showError("BÅÄ d podczas Åadowania historii."); 5931 } 5932 } 5933 5934 displayChatSessions(sessions) { 5935 const messages = document.getElementById( 5936 "bb-asystent-ai-messages", 5937 ); 5938 if (!messages) return; 5939 5940 messages.innerHTML = ""; 5941 5942 // NagÅówek z opcjÄ powrotu do bieÅ¼Ä cej sesji 5943 const header = document.createElement("div"); 5944 header.className = "chat-history-header"; 5945 header.innerHTML = ` 5946 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding: 16px; background: linear-gradient(135deg, #f8f9fa, #e9ecef); border-radius: 12px; border: 1px solid #dee2e6;"> 5947 <div> 5948 <h4 style="margin: 0; color: #2c3e50; font-size: 16px; font-weight: 600;">Historia czatów</h4> 5949 <p style="margin: 4px 0 0 0; color: #6c757d; font-size: 12px;">Wybierz sesjÄ, aby zobaczyÄ poprzednie rozmowy</p> 5950 </div> 5951 <button onclick="window.BBAsystentAI.loadCurrentSession()" style=" 5952 background: linear-gradient(135deg, #007cba, #0085d1); 5953 color: white; 5954 border: none; 5955 padding: 8px 16px; 5956 border-radius: 8px; 5957 cursor: pointer; 5958 font-size: 12px; 5959 font-weight: 500; 5960 transition: all 0.2s ease; 5961 box-shadow: 0 2px 4px rgba(0, 124, 186, 0.2); 5962 " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'"> 5963 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px; vertical-align: -2px;"> 5964 <path d="M19 12H5M12 19l-7-7 7-7"/> 5965 </svg> 5966 Powrót do czatu 5967 </button> 5968 </div> 5969 `; 5970 messages.appendChild(header); 5971 5972 if (sessions.length === 0) { 5973 const noSessions = document.createElement("div"); 5974 noSessions.innerHTML = ` 5975 <div style="text-align: center; padding: 40px 20px; color: #6c757d;"> 5976 <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" style="margin-bottom: 16px; opacity: 0.5;"> 5977 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> 5978 </svg> 5979 <p style="margin: 0; font-size: 14px;">Brak historii czatów</p> 5980 <p style="margin: 8px 0 0 0; font-size: 12px; opacity: 0.8;">Rozpocznij rozmowÄ, aby utworzyÄ pierwszÄ sesjÄ</p> 5981 </div> 5982 `; 5983 messages.appendChild(noSessions); 5984 return; 5985 } 5986 5987 sessions.forEach((session) => { 5988 const sessionElement = document.createElement("div"); 5989 sessionElement.className = "chat-session-item"; 5990 sessionElement.style.cssText = `
5991 padding: 16px; 5992 margin-bottom: 12px; 5993 background: white; 5994 border: 1px solid #e9ecef; 5995 border-radius: 12px; 5996 cursor: pointer; 5997 transition: all 0.2s ease; 5998 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); 5999 `; 6000 6001 const date = new Date(session.created_at).toLocaleString( 6002 "pl-PL", 6003 { 6004 year: "numeric", 6005 month: "long", 6006 day: "numeric", 6007 hour: "2-digit", 6008 minute: "2-digit", 6009 }, 6010 ); 6011 6012 const messageCount = session.message_count || 0; 6013 const status = 6014 session.status === "active" ? "Aktywny" : "ZakoÅczony"; 6015 const statusColor = 6016 session.status === "active" ? "#28a745" : "#6c757d"; 6017 const statusIconClass = 6018 session.status === "active" 6019 ? "dashicons-yes-alt" 6020 : "dashicons-minus"; 6021 6022 sessionElement.innerHTML = ` 6023 <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 8px;"> 6024 <strong style="color: #2c3e50; font-size: 14px; font-weight: 600;"> 6025 <span class="dashicons ${statusIconClass}" style="vertical-align: text-bottom;"></span> 6026 Sesja z ${date} 6027 </strong> 6028 <span style="color: ${statusColor}; font-size: 11px; background: ${statusColor}20; padding: 2px 8px; border-radius: 12px; font-weight: 500;">${status}</span> 6029 </div> 6030 <div style="display: flex; justify-content: space-between; align-items: center; color: #6c757d; font-size: 12px;"> 6031 <span class="dashicons dashicons-format-chat"></span> 6032 <span>${messageCount} wiadomoÅci</span> 6033 <span class="dashicons dashicons-media-text" style="margin-left: auto;"></span> 6034 <span>${session.page_title || "Nieznana strona"}</span> 6035 </div> 6036 `; 6037 6038 sessionElement.addEventListener("click", () => { 6039 this.loadChatSession(session.session_id); 6040 }); 6041 6042 sessionElement.addEventListener("mouseenter", () => { 6043 sessionElement.style.backgroundColor = "#f8f9fa"; 6044 sessionElement.style.borderColor = "#007cba"; 6045 sessionElement.style.transform = "translateY(-2px)"; 6046 sessionElement.style.boxShadow = 6047 "0 4px 12px rgba(0, 124, 186, 0.15)"; 6048 }); 6049 6050 sessionElement.addEventListener("mouseleave", () => { 6051 sessionElement.style.backgroundColor = "white"; 6052 sessionElement.style.borderColor = "#e9ecef"; 6053 sessionElement.style.transform = "translateY(0)"; 6054 sessionElement.style.boxShadow = 6055 "0 2px 4px rgba(0, 0, 0, 0.04)"; 6056 }); 6057 6058 messages.appendChild(sessionElement); 6059 }); 6060 6061 this.scrollToBottom(); 6062 } 6063 6064 async loadChatSession(sessionId) { 6065 try { 6066 debugLog("Loading chat session:", sessionId); 6067 6068 const action = "bbai_ai_get_session_history"; 6069 const url = this.getAjaxUrl(action); 6070 const formData = new FormData(); 6071 formData.append("action", action); 6072 formData.append("session_id", sessionId); 6073 formData.append("nonce", BBAsystentAI.nonce); 6074 6075 const response = await fetch(url, { 6076 method: "POST", 6077 body: formData, 6078 }); 6079 6080 const result = await response.json(); 6081 6082 // Debug: pokaż surowe dane z serwera 6083 debugLog("Server response for session history:", result); 6084 debugLog("Response success:", result.success); 6085 debugLog("Response data:", result.data); 6086 6087 if (result.success && result.data && result.data.messages) { 6088 debugLog("Messages data:", result.data.messages); 6089 debugLog( 6090 "Number of messages:", 6091 result.data.messages.length, 6092 ); 6093 6094 // Debug: pokaż przykÅad pierwszej wiadomoÅci jeÅli istnieje 6095 if (result.data.messages.length > 0) { 6096 debugLog( 6097 "First message example:", 6098 result.data.messages[0], 6099 ); 6100 debugLog(
6101 "First message keys:", 6102 Object.keys(result.data.messages[0]), 6103 ); 6104 } 6105 6106 this.displaySessionMessages( 6107 result.data.messages, 6108 sessionId, 6109 ); 6110 } else { 6111 debugError("Failed to load session messages:", result); 6112 debugError( 6113 "Error message:", 6114 result.data || "Unknown error", 6115 ); 6116 this.showError("Nie udaÅo siÄ zaÅadowaÄ wiadomoÅci sesji."); 6117 } 6118 } catch (error) { 6119 debugError("Error loading session:", error); 6120 this.showError("BÅÄ d podczas Åadowania sesji."); 6121 } 6122 } 6123 6124 displaySessionMessages(messages, sessionId) { 6125 debugLog( 6126 "displaySessionMessages called with:", 6127 messages.length, 6128 "messages", 6129 ); 6130 debugLog("Sample message structure:", messages[0]); 6131 6132 const messagesContainer = document.getElementById( 6133 "bb-asystent-ai-messages", 6134 ); 6135 if (!messagesContainer) return; 6136 6137 // Clear current messages 6138 messagesContainer.innerHTML = ""; 6139 6140 // NIE resetuj flagi Åadowania historii - to powodowaÅo problemy z sesjami! 6141 // this.chatHistoryLoaded = false; // USUNIÄTE - powodowaÅo resetowanie sesji 6142 6143 // Add navigation header 6144 const navigationHeader = document.createElement("div"); 6145 navigationHeader.className = "session-navigation-header"; 6146 navigationHeader.innerHTML = ` 6147 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding: 12px 16px; background: linear-gradient(135deg, #e3f2fd, #f8f9fa); border-radius: 12px; border-left: 4px solid #007cba;"> 6148 <div style="display: flex; align-items: center; gap: 8px;"> 6149 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color: #007cba;"> 6150 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> 6151 <path d="M9 10h6M9 14h4"></path> 6152 </svg> 6153 <div> 6154 <h4 style="margin: 0; color: #2c3e50; font-size: 14px; font-weight: 600;">Historia sesji</h4> 6155 <p style="margin: 2px 0 0 0; color: #6c757d; font-size: 11px;">${messages.length} wiadomoÅci w sesji</p> 6156 </div> 6157 </div> 6158 <div style="display: flex; gap: 8px;"> 6159 <button onclick="window.BBAsystentAI.showChatHistory()" style=" 6160 background: #6c757d; 6161 color: white; 6162 border: none; 6163 padding: 6px 12px; 6164 border-radius: 6px; 6165 cursor: pointer; 6166 font-size: 11px; 6167 font-weight: 500; 6168 transition: all 0.2s ease; 6169 " onmouseover="this.style.backgroundColor='#5a6268'" onmouseout="this.style.backgroundColor='#6c757d'"> 6170 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px; vertical-align: -1px;"> 6171 <path d="M19 12H5M12 19l-7-7 7-7"/> 6172 </svg> 6173 Lista sesji 6174 </button> 6175 <button onclick="window.BBAsystentAI.loadCurrentSession()" style=" 6176 background: linear-gradient(135deg, #007cba, #0085d1); 6177 color: white; 6178 border: none; 6179 padding: 6px 12px; 6180 border-radius: 6px; 6181 cursor: pointer; 6182 font-size: 11px; 6183 font-weight: 500; 6184 transition: all 0.2s ease; 6185 box-shadow: 0 2px 4px rgba(0, 124, 186, 0.2); 6186 " onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'"> 6187 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px; vertical-align: -1px;"> 6188 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> 6189 <circle cx="9" cy="10" r="1"/> 6190 <circle cx="15" cy="10" r="1"/> 6191 <path d="M9 14h6"/> 6192 </svg> 6193 Aktualny czat 6194 </button> 6195 </div> 6196 </div> 6197 `; 6198 messagesContainer.appendChild(navigationHeader); 6199 6200 // Display messages with improved styling 6201 if (messages.length === 0) { 6202 debugLog("No messages to display"); 6203 const noMessages = document.createElement("div"); 6204 noMessages.innerHTML = ` 6205 <div style="text-align: center; padding: 40px 20px; color: #6c757d;"> 6206 <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" style="margin-bottom: 16px; opacity: 0.5;"> 6207 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> 6208 </svg> 6209 <p style="margin: 0; font-size: 14px;">Ta sesja nie zawiera wiadomoÅci</p> 6210 <p style="margin: 8px 0 0 0; font-size: 12px; opacity: 0.8;">Możliwe, że sesja zostaÅa przerwana przed rozpoczÄciem rozmowy</p> 6211 </div> 6212 `; 6213 messagesContainer.appendChild(noMessages); 6214 return; 6215 } 6216 6217 // Add messages with timestamp information
6218 messages.forEach((msg, index) => { 6219 debugLog(`Processing message ${index + 1}:`, msg); 6220 6221 // Sprawdź różne możliwe pola dla typu wiadomoÅci - wiÄcej opcji 6222 const messageType = msg.message_type || msg.type || "assistant"; 6223 6224 // Tool-call: kompaktowy kafelek (pomija standardowy renderer) 6225 if (messageType === "tool_call") { 6226 let tc = msg.tool_call; 6227 if (!tc && typeof msg.message === "string") { 6228 tc = this._safeParseJson(msg.message)?.tool_call || null; 6229 } 6230 if (!tc && typeof msg.content === "string") { 6231 tc = this._safeParseJson(msg.content)?.tool_call || null; 6232 } 6233 if (tc) { 6234 const node = this.addToolCallMessage(tc, { 6235 persist: false, 6236 scroll: false, 6237 }); 6238 if (node) { 6239 node.classList.add("historical-message"); 6240 } 6241 } 6242 return; 6243 } 6244 6245 // Sprawdź różne możliwe pola dla treÅci wiadomoÅci - dodaj wiÄcej opcji 6246 let messageContent = 6247 msg.message || 6248 msg.content || 6249 msg.text || 6250 "WiadomoÅÄ nie może zostaÄ wyÅwietlona"; 6251 6252 // Sprawdź czy wiadomoÅÄ nie jest pusta po odszyfrrowaniu 6253 if (!messageContent || messageContent.trim() === "") { 6254 messageContent = 6255 "Pusta wiadomoÅÄ (możliwy bÅÄ d dekodowania)"; 6256 } 6257 6258 // Sprawdź różne możliwe pola dla czasu 6259 const messageTime = 6260 msg.created_at || 6261 msg.timestamp || 6262 msg.time || 6263 new Date().toISOString(); 6264 6265 debugLog( 6266 `Message type: ${messageType}, content length: ${messageContent.length}, content preview: "${messageContent.substring(0, 50)}..."`, 6267 ); 6268 6269 // Pobierz ID wiadomoÅci z różnych możliwych pól 6270 const messageId = msg.id || msg.message_id || null; 6271 6272 const messageElement = this.addMessage( 6273 messageType, 6274 messageContent, 6275 messageTime, 6276 true, 6277 messageId, 6278 ); 6279 6280 // Add historical message styling 6281 if (messageElement) { 6282 messageElement.style.opacity = "0.95"; 6283 messageElement.classList.add("historical-message"); 6284 6285 // Add subtle animation 6286 setTimeout(() => { 6287 messageElement.style.opacity = "1"; 6288 messageElement.style.transform = "translateY(0)"; 6289 }, index * 100); 6290
6291 // WiadomoÅÄ jest już dodana do DOM przez addMessage, nie trzeba rÄcznie appendChild 6292 } 6293 }); 6294 6295 // Add session info footer 6296 const sessionInfo = document.createElement("div"); 6297 sessionInfo.className = "session-info-footer"; 6298 const firstMessage = messages[0] || {}; 6299 const sessionDate = 6300 firstMessage.created_at || 6301 firstMessage.timestamp || 6302 new Date().toISOString(); 6303 6304 sessionInfo.innerHTML = ` 6305 <div style="margin-top: 20px; padding: 12px 16px; background: #f8f9fa; border-radius: 8px; text-align: center; border: 1px solid #e9ecef;"> 6306 <p style="margin: 0; color: #6c757d; font-size: 11px; opacity: 0.8;"> 6307 ð To byÅa poprzednia sesja czatu z ${new Date(sessionDate).toLocaleString("pl-PL")} 6308 </p> 6309 <p style="margin: 4px 0 0 0; color: #6c757d; font-size: 10px; opacity: 0.6;"> 6310 ID sesji: ${sessionId.substring(0, 16)}... 6311 </p> 6312 </div> 6313 `; 6314 messagesContainer.appendChild(sessionInfo); 6315 6316 this.scrollToBottom(); 6317 } 6318 6319 loadCurrentSession() { 6320 // Sprawdź czy już jesteÅmy w bieÅ¼Ä cej sesji 6321 const messagesContainer = document.getElementById( 6322 "bb-asystent-ai-messages", 6323 ); 6324 if (!messagesContainer) return; 6325 6326 // Sprawdź czy kontener ma już wiadomoÅci dla bieÅ¼Ä cej sesji 6327 const hasMessages = messagesContainer.children.length > 0; 6328 const hasCurrentSessionHeader = messagesContainer.querySelector( 6329 ".current-session-header", 6330 ); 6331 6332 // JeÅli już mamy wiadomoÅci i nagÅówek bieÅ¼Ä cej sesji, nie rób nic 6333 if (hasMessages && hasCurrentSessionHeader) { 6334 debugLog("Already in current session view, skipping reload"); 6335 return; 6336 } 6337 6338 // Clear messages only if coming from history view 6339 if (messagesContainer.querySelector(".chat-history-header")) { 6340 // Smooth transition 6341 messagesContainer.style.transition = "opacity 0.2s ease-out"; 6342 messagesContainer.style.opacity = "0.7"; 6343 6344 setTimeout(() => { 6345 // WyczyÅÄ kontener tylko gdy wracamy z historii 6346 messagesContainer.innerHTML = ""; 6347 6348 // Add current session header 6349 const currentSessionHeader = document.createElement("div"); 6350 currentSessionHeader.className = "current-session-header"; 6351 currentSessionHeader.innerHTML = ` 6352 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding: 12px 16px; background: linear-gradient(135deg, #e8f5e8, #f0f8f0); border-radius: 12px; border-left: 4px solid #28a745;"> 6353 <div style="display: flex; align-items: center; gap: 8px;"> 6354 <div style="width: 8px; height: 8px; background: #28a745; border-radius: 50%; animation: pulse 2s infinite;"></div> 6355 <div> 6356 <h4 style="margin: 0; color: #2c3e50; font-size: 14px; font-weight: 600;">Aktualny czat</h4> 6357 <p style="margin: 2px 0 0 0; color: #6c757d; font-size: 11px;">Kontynuuj rozmowÄ lub rozpocznij nowy temat</p> 6358 </div> 6359 </div> 6360 <button onclick="window.BBAsystentAI.showChatHistory()" style=" 6361 background: #6c757d; 6362 color: white; 6363 border: none; 6364 padding: 6px 12px; 6365 border-radius: 6px; 6366 cursor: pointer; 6367 font-size: 11px; 6368 font-weight: 500; 6369 transition: all 0.2s ease; 6370 " onmouseover="this.style.backgroundColor='#5a6268'" onmouseout="this.style.backgroundColor='#6c757d'"> 6371 <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 4px; vertical-align: -1px;"> 6372 <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/> 6373 </svg> 6374 Historia 6375 </button> 6376 </div> 6377 `; 6378 messagesContainer.appendChild(currentSessionHeader); 6379 6380 // Reset flagÄ i wymuÅ zaÅadowanie historii bieÅ¼Ä cej sesji 6381 this.chatHistoryLoaded = false;
6382 6383 // Load current session history 6384 this.loadChatHistoryForce().then(() => { 6385 // Restore opacity after loading 6386 messagesContainer.style.opacity = "1"; 6387 // Oznacz historiÄ jako zaÅadowanÄ po pomyÅlnym zaÅadowaniu 6388 this.chatHistoryLoaded = true; 6389 }); 6390 }, 150); 6391 } 6392 } 6393 6394 isSafari() { 6395 const ua = navigator.userAgent || ""; 6396 return /Safari/.test(ua) && !/Chrome/.test(ua) && !/Chromium/.test(ua); 6397 } 6398 6399 async fetchWithRetry(url, options, maxRetries = 3) { 6400 const isSafari = this.isSafari(); 6401 const retryDelay = isSafari ? 1000 : 500; 6402 let lastError = null; 6403 6404 for (let attempt = 0; attempt <= maxRetries; attempt++) { 6405 try { 6406 const response = await fetch(url, { 6407 ...options, 6408 headers: { 6409 ...options.headers, 6410 "X-Requested-With": "XMLHttpRequest", 6411 }, 6412 cache: "no-store", 6413 credentials: "same-origin", 6414 }); 6415 6416 if (response.ok) { 6417 return response; 6418 } 6419 6420 // Retry dla bÅÄdów 5xx i niektórych 4xx w Safari 6421 if ( 6422 attempt < maxRetries && 6423 (response.status >= 500 || 6424 (isSafari && response.status === 0)) 6425 ) { 6426 await new Promise((resolve) => 6427 setTimeout(resolve, retryDelay * (attempt + 1)), 6428 ); 6429 continue; 6430 } 6431 6432 return response; 6433 } catch (error) { 6434 lastError = error; 6435 if ( 6436 attempt < maxRetries && 6437 (error.name === "TypeError" || 6438 error.message.includes("Failed to fetch") || 6439 error.message.includes("NetworkError")) 6440 ) { 6441 await new Promise((resolve) => 6442 setTimeout(resolve, retryDelay * (attempt + 1)), 6443 ); 6444 continue; 6445 } 6446 throw error; 6447 } 6448 } 6449 6450 throw lastError || new Error("Max retries exceeded"); 6451 } 6452 6453 async parseJsonResponse(response, fallback = null) { 6454 if (!response) { 6455 return fallback; 6456 } 6457 6458 try { 6459 const text = await response.text(); 6460 if (!text) { 6461 return fallback; 6462 } 6463 return JSON.parse(text); 6464 } catch (error) { 6465 debugWarn("BB Asystent AI: invalid JSON response", error); 6466 return fallback; 6467 } 6468 } 6469 6470 async startNewChat() { 6471 if ( 6472 confirm( 6473 "Czy na pewno chcesz rozpoczÄ Ä nowy czat? BieÅ¼Ä ca rozmowa zostanie zakoÅczona.", 6474 ) 6475 ) { 6476 try { 6477 debugLog( 6478 "FUNERAL DEBUG: Starting new chat, resetting chatHistoryLoaded", 6479 ); 6480 6481 // End current session and wait for completion 6482 const endAction = "bbai_ai_end_session"; 6483 const endUrl = this.getAjaxUrl(endAction); 6484 const endFormData = new FormData(); 6485 endFormData.append("action", endAction); 6486 endFormData.append("session_id", this.sessionId); 6487 endFormData.append("nonce", BBAsystentAI.nonce); 6488 6489 try { 6490 await this.fetchWithRetry(endUrl, { 6491 method: "POST", 6492 body: endFormData, 6493 }); 6494 } catch (error) { 6495 debugWarn( 6496 "FUNERAL DEBUG: Failed to end session, continuing anyway", 6497 error, 6498 ); 6499 } 6500 6501 // Request new session ID from server 6502 const createAction = "bbai_ai_create_new_session"; 6503 const createUrl = this.getAjaxUrl(createAction); 6504 const newSessionFormData = new FormData(); 6505 newSessionFormData.append( 6506 "action", 6507 createAction, 6508 ); 6509 newSessionFormData.append("nonce", BBAsystentAI.nonce); 6510 6511 let newSessionResult = null; 6512 try { 6513 const newSessionResponse = await this.fetchWithRetry( 6514 createUrl, 6515 { 6516 method: "POST", 6517 body: newSessionFormData, 6518 }, 6519 ); 6520 newSessionResult = await this.parseJsonResponse( 6521 newSessionResponse, 6522 { success: false }, 6523 ); 6524 } catch (error) { 6525 debugWarn( 6526 "FUNERAL DEBUG: Failed to create session from server, using fallback", 6527 error, 6528 ); 6529 newSessionResult = { success: false }; 6530 } 6531 6532 if (newSessionResult && newSessionResult.success) { 6533 // Use new session ID from server 6534 this.sessionId = newSessionResult.data.session_id; 6535 this.resetMessageFlagsForSession(); 6536 // Zapisz nowy session ID z fallback dla Safari 6537 try { 6538 localStorage.setItem( 6539 "bb_asystent_ai_session_id", 6540 this.sessionId, 6541 ); 6542 } catch (e) { 6543 debugWarn( 6544 "FUNERAL DEBUG: localStorage failed, using cookie only", 6545 e, 6546 ); 6547 } 6548 if (this.streamingMode) { 6549 this.streamingMode.setSessionId(this.sessionId); 6550 } 6551 // WyczyÅÄ stare wiadomoÅci z poprzedniej sesji 6552 try { 6553 localStorage.removeItem("bb_asystent_ai_messages"); 6554 } catch (e) { 6555 debugWarn( 6556 "FUNERAL DEBUG: Failed to clear messages from localStorage", 6557 e, 6558 ); 6559 } 6560 debugLog( 6561 "FUNERAL DEBUG: New session created:", 6562 this.sessionId, 6563 ); 6564 } else { 6565 // Fallback: generate locally if server fails 6566 debugWarn( 6567 "Server failed to create new session, using local generation", 6568 ); 6569 const newSessionId = 6570 "session_" + 6571 Date.now() + 6572 "_" + 6573 Math.random().toString(36).substr(2, 9); 6574 this.sessionId = newSessionId; 6575 this.resetMessageFlagsForSession(); 6576 if (this.streamingMode) { 6577 this.streamingMode.setSessionId(this.sessionId); 6578 } 6579 6580 // Update cookie manually for fallback 6581 const cookieOptions = `path=/; max-age=${30 * 24 * 60 * 60}; secure=${location.protocol === "https:"}; samesite=lax`; 6582 document.cookie = `bb_asystent_ai_session=${newSessionId}; ${cookieOptions}`; 6583 // Zapisz nowy session ID z fallback dla Safari 6584 try { 6585 localStorage.setItem( 6586 "bb_asystent_ai_session_id", 6587 this.sessionId, 6588 ); 6589 } catch (e) { 6590 debugWarn( 6591 "FUNERAL DEBUG: localStorage failed, using cookie only", 6592 e, 6593 ); 6594 } 6595 // WyczyÅÄ stare wiadomoÅci 6596 try { 6597 localStorage.removeItem("bb_asystent_ai_messages"); 6598 } catch (e) { 6599 debugWarn( 6600 "FUNERAL DEBUG: Failed to clear messages from localStorage", 6601 e, 6602 ); 6603 } 6604 } 6605 6606 // Clear messages container 6607 const messagesContainer = document.getElementById( 6608 "bb-asystent-ai-messages", 6609 ); 6610 if (messagesContainer) { 6611 messagesContainer.innerHTML = ""; 6612 debugLog("FUNERAL DEBUG: Messages container cleared"); 6613 } 6614 6615 // Reset tylko flagÄ Åadowania historii dla nowej sesji 6616 this.chatHistoryLoaded = false;
6617 debugLog( 6618 "FUNERAL DEBUG: chatHistoryLoaded reset to false for new session", 6619 ); 6620 6621 // Add welcome message and save it to database 6622 let welcomeMessage = 6623 this.strings.welcomeMessage || 6624 "Witaj! Jestem asystentem AI. W czym mogÄ Ci pomóc?"; 6625 6626 // Upewnij siÄ Å¼e jest stringiem 6627 if (typeof welcomeMessage === "object") { 6628 debugError( 6629 "FUNERAL DEBUG: welcomeMessage is object! Converting to default string", 6630 ); 6631 welcomeMessage = 6632 "Witaj! Jestem asystentem AI. W czym mogÄ Ci pomóc?"; 6633 } 6634 6635 debugLog( 6636 "FUNERAL DEBUG: startNewChat - welcomeMessage type:", 6637 typeof welcomeMessage, 6638 ); 6639 debugLog( 6640 "FUNERAL DEBUG: startNewChat - welcomeMessage value:", 6641 welcomeMessage, 6642 ); 6643 const inserted = this.addWelcomeMessageOnce(welcomeMessage); 6644 6645 if (inserted) { 6646 this.showQuickQuestions(); 6647 this.scrollToBottom(); 6648 6649 // Save welcome message to database for new session 6650 const action = "bbai_ai_save_welcome_message"; 6651 const url = this.getAjaxUrl(action); 6652 const welcomeFormData = new FormData(); 6653 welcomeFormData.append("action", action); 6654 welcomeFormData.append("message", welcomeMessage); 6655 welcomeFormData.append("session_id", this.sessionId); 6656 welcomeFormData.append("nonce", BBAsystentAI.nonce); 6657 6658 const welcomeResponse = await fetch(url, { 6659 method: "POST", 6660 body: welcomeFormData, 6661 }); 6662 6663 const welcomeResult = await this.parseJsonResponse( 6664 welcomeResponse, 6665 { success: false }, 6666 ); 6667 6668 if (!welcomeResult.success) { 6669 debugError( 6670 "Failed to save welcome message:", 6671 welcomeResult, 6672 ); 6673 } else { 6674 debugLog( 6675 "FUNERAL DEBUG: Welcome message saved for new session", 6676 ); 6677 } 6678 } 6679 6680 // Mark history as loaded since we just added welcome message 6681 this.chatHistoryLoaded = true; 6682 debugLog( 6683 "FUNERAL DEBUG: chatHistoryLoaded set to true after adding welcome", 6684 ); 6685 6686 // Reset unread count 6687 this.unreadCount = 0; 6688 this.updateBadgeWithCount(); 6689 6690 this.trackEvent("new_chat_started"); 6691 } catch (error) { 6692 debugError("Error starting new chat:", error); 6693 this.showError("BÅÄ d podczas rozpoczynania nowego czatu."); 6694 } 6695 } 6696 } 6697 6698 async endCurrentChat() { 6699 if (confirm("Czy na pewno chcesz zakoÅczyÄ bieÅ¼Ä cy czat?")) { 6700 try { 6701 // Oznacz sesjÄ jako zakoÅczonÄ w bazie danych 6702 const action = "bbai_ai_end_session"; 6703 const url = this.getAjaxUrl(action); 6704 const formData = new FormData(); 6705 formData.append("action", action); 6706 formData.append("session_id", this.sessionId); 6707 formData.append("nonce", BBAsystentAI.nonce); 6708 6709 const response = await fetch(url, { 6710 method: "POST", 6711 body: formData, 6712 }); 6713 6714 const result = await this.parseJsonResponse(response, { 6715 success: false, 6716 }); 6717 6718 if (result.success) {
6719 // WyÅlij wiadomoÅÄ pożegnalnÄ (która zostanie zapisana) 6720 const inserted = this.addGoodbyeMessageOnce( 6721 this.strings.goodbyeMessage, 6722 ); 6723 if (inserted) { 6724 // Zapisz wiadomoÅÄ pożegnalnÄ do bazy 6725 const goodbyeAction = 6726 "bbai_ai_save_welcome_message"; 6727 const goodbyeUrl = 6728 this.getAjaxUrl(goodbyeAction); 6729 const goodbyeFormData = new FormData(); 6730 goodbyeFormData.append( 6731 "action", 6732 goodbyeAction, 6733 ); 6734 goodbyeFormData.append( 6735 "message", 6736 this.strings.goodbyeMessage, 6737 ); 6738 goodbyeFormData.append( 6739 "session_id", 6740 this.sessionId, 6741 ); 6742 goodbyeFormData.append( 6743 "nonce", 6744 BBAsystentAI.nonce, 6745 ); 6746 6747 fetch(goodbyeUrl, { 6748 method: "POST", 6749 body: goodbyeFormData, 6750 }).catch((error) => { 6751 debugError( 6752 "Error saving goodbye message:", 6753 error, 6754 ); 6755 }); 6756 } 6757 6758 // Reset badge 6759 this.hideBadge(); 6760 6761 this.trackEvent("chat_ended_manually"); 6762 6763 const restartDelayMs = 1200; // chwilÄ pokaż pożegnanie 6764 6765 setTimeout(async () => { 6766 // Zamknij okno (ukryje pożegnanie) 6767 this.closeChat(); 6768 6769 // WyczyÅÄ widok i cache bieÅ¼Ä cej sesji 6770 const messagesContainer = document.getElementById( 6771 "bb-asystent-ai-messages", 6772 ); 6773 if (messagesContainer) { 6774 messagesContainer.innerHTML = ""; 6775 } 6776 this.chatHistory = []; 6777 this.chatHistoryLoaded = false; 6778 this.unreadCount = 0; 6779 this.persistMessagesCache(); 6780 6781 // Przygotuj nowÄ sesjÄ i uruchom Åwieży czat bez przeÅadowania strony 6782 try { 6783 await this.prepareNewSessionForNextOpen(); 6784 await this.startNewChat(); 6785 this.openChat(); // otwórz od razu nowÄ sesjÄ 6786 } catch (prepError) { 6787 debugError( 6788 "Error preparing new session after end chat:", 6789 prepError, 6790 ); 6791 } 6792 }, restartDelayMs); 6793 } else { 6794 this.showError("BÅÄ d podczas koÅczenia czatu."); 6795 } 6796 } catch (error) { 6797 debugError("Error ending chat:", error); 6798 this.showError("BÅÄ d podczas koÅczenia czatu."); 6799 } 6800 } 6801 } 6802 6803 async prepareNewSessionForNextOpen() { 6804 try { 6805 debugLog( 6806 "FUNERAL DEBUG: prepareNewSessionForNextOpen - current sessionId:", 6807 this.sessionId, 6808 ); 6809 6810 // Request new session ID from server dla nastÄpnego otwarcia 6811 const action = "bbai_ai_create_new_session"; 6812 const url = this.getAjaxUrl(action); 6813 const newSessionFormData = new FormData(); 6814 newSessionFormData.append( 6815 "action", 6816 action, 6817 ); 6818 newSessionFormData.append("nonce", BBAsystentAI.nonce); 6819 6820 const newSessionResponse = await fetch(url, { 6821 method: "POST", 6822 body: newSessionFormData, 6823 }); 6824 6825 const newSessionResult = await this.parseJsonResponse( 6826 newSessionResponse, 6827 { success: false }, 6828 ); 6829 6830 if (newSessionResult.success) { 6831 // Use new session ID from server 6832 debugLog( 6833 "FUNERAL DEBUG: New session ID from server in prepareNew:", 6834 newSessionResult.data.session_id, 6835 ); 6836 this.sessionId = newSessionResult.data.session_id; 6837 if (this.streamingMode) { 6838 this.streamingMode.setSessionId(this.sessionId); 6839 } 6840 // Aktualizuj backup w localStorage 6841 localStorage.setItem( 6842 "bb_asystent_ai_session_backup", 6843 this.sessionId, 6844 ); 6845 } else { 6846 // Fallback: generate locally if server fails 6847 debugWarn( 6848 "Server failed to create new session, using local generation", 6849 ); 6850 const newSessionId = 6851 "session_" + 6852 Date.now() + 6853 "_" + 6854 Math.random().toString(36).substr(2, 9); 6855 debugLog( 6856 "FUNERAL DEBUG: Generated local session ID in prepareNew:", 6857 newSessionId, 6858 ); 6859 this.sessionId = newSessionId; 6860 if (this.streamingMode) { 6861 this.streamingMode.setSessionId(this.sessionId); 6862 } 6863 6864 // Update cookie manually for fallback 6865 document.cookie = `bb_asystent_ai_session=${newSessionId}; path=/; max-age=${30 * 24 * 60 * 60}; secure=${location.protocol === "https:"}; samesite=lax`; 6866 // Aktualizuj backup w localStorage 6867 localStorage.setItem( 6868 "bb_asystent_ai_session_backup", 6869 this.sessionId, 6870 ); 6871 } 6872 6873 this.resetMessageFlagsForSession(); 6874 6875 // WAÅ»NE: NIE resetuj flagi chatHistoryLoaded tutaj! 6876 debugLog( 6877 "FUNERAL DEBUG: prepareNewSessionForNextOpen completed, new sessionId:", 6878 this.sessionId, 6879 ); 6880 } catch (error) { 6881 debugError("Error preparing new session:", error); 6882 } 6883 } 6884 6885 setupBubbleAnimations() { 6886 const bubble = document.getElementById("bb-asystent-ai-bubble"); 6887 if (!bubble) return; 6888 6889 // Zastosuj style dynamiczne 6890 this.applyDynamicStyles(); 6891 6892 // ObsÅuga animacji 6893 const animationStyle = this.settings.animationStyle || "bounce"; 6894 const animationDelay = (this.settings.animationDelay || 3) * 1000; 6895 6896 if (animationStyle !== "none" && animationDelay > 0) { 6897 setTimeout(() => { 6898 if (!this.isOpen) { 6899 this.startBubbleAnimation(bubble, animationStyle); 6900 } 6901 }, animationDelay); 6902 } 6903 6904 // ObsÅuga efektu hover 6905 if (!this.settings.hoverEffect) { 6906 bubble.style.transform = "none"; 6907 bubble.addEventListener("mouseenter", () => { 6908 bubble.style.transform = "none"; 6909 }); 6910 } 6911 } 6912 6913 applyDynamicStyles() { 6914 // Pobierz element bubble 6915 const bubble = document.getElementById("bb-asystent-ai-bubble"); 6916 if (!bubble) { 6917 debugWarn( 6918 "FUNERAL DEBUG: Bubble element not found, skipping style application", 6919 ); 6920 return; 6921 } 6922 6923 // Pozycja bubble 6924 const widget = document.getElementById("bb-asystent-ai-widget"); 6925 const position = this.settings.bubblePosition || "bottom-right"; 6926 const marginX = this.settings.marginX || 20; 6927 const marginY = this.settings.marginY || 20; 6928 6929 if (widget) { 6930 // UsuÅ wszystkie klasy pozycji 6931 widget.className = widget.className.replace( 6932 /position-\S+/g, 6933 "", 6934 ); 6935 widget.classList.add(`position-${position}`); 6936 6937 // Zastosuj marginesy 6938 const positionStyles = this.getPositionStyles( 6939 position, 6940 marginX, 6941 marginY, 6942 ); 6943 Object.assign(widget.style, positionStyles); 6944 } 6945 6946 // Rozmiar bubble 6947 const bubbleSize = this.settings.bubbleSize || "medium"; 6948 const sizeMap = { 6949 small: 50, 6950 medium: 60, 6951 large: 70, 6952 "extra-large": 80, 6953 }; 6954 const size = sizeMap[bubbleSize] || 60; 6955 if (bubble.classList.contains("has-animated-text")) { 6956 bubble.style.removeProperty("width"); 6957 bubble.style.removeProperty("height"); 6958 } else { 6959 bubble.style.width = `${size}px`; 6960 bubble.style.height = `${size}px`; 6961 } 6962 6963 // Kolory bubble 6964 const bubbleColor = this.settings.bubbleColor || "#2c3e50"; 6965 const secondaryColor = this.adjustBrightness(bubbleColor, 20); 6966 const bubbleRgba = this.hexToRgba(bubbleColor, 0.3); 6967 6968 // Ustaw kolory 6969 bubble.style.background = `linear-gradient(135deg, ${bubbleColor}, ${secondaryColor})`; 6970 bubble.style.setProperty( 6971 "--bubble-secondary-color", 6972 secondaryColor, 6973 ); 6974 6975 // Styl bubble (border-radius) 6976 const bubbleStyle = this.settings.bubbleStyle || "circle"; 6977 let borderRadius = "50%"; 6978 switch (bubbleStyle) { 6979 case "circle": 6980 borderRadius = "50%"; 6981 break; 6982 case "rounded": 6983 borderRadius = "15px"; 6984 break; 6985 case "square": 6986 borderRadius = "0"; 6987 break; 6988 case "pill": 6989 borderRadius = "30px"; 6990 break; 6991 } 6992 6993 // Ustaw CSS custom properties i style 6994 bubble.style.setProperty("--bubble-border-radius", borderRadius); 6995 bubble.style.setProperty("--bubble-color", bubbleColor); 6996 bubble.style.setProperty("--ripple-color", bubbleRgba); 6997 bubble.style.borderRadius = borderRadius; 6998 6999 const glowEnabled = 7000 this.settings.bubbleGlowEnabled === true || 7001 this.settings.bubbleGlowEnabled === 1 || 7002 this.settings.bubbleGlowEnabled === "1"; 7003 const glowPrimary = 7004 this.settings.bubbleGlowPrimary || 7005 this.settings.accentColor || 7006 bubbleColor; 7007 const glowSecondary = 7008 this.settings.bubbleGlowSecondary || 7009 this.adjustBrightness(bubbleColor, 35); 7010 7011 if (glowEnabled) { 7012 bubble.classList.add("has-glow-border"); 7013 bubble.style.setProperty("--fac-glow-primary", glowPrimary); 7014 bubble.style.setProperty("--fac-glow-secondary", glowSecondary); 7015 bubble.style.removeProperty("box-shadow"); 7016 } else { 7017 bubble.classList.remove("has-glow-border"); 7018 bubble.style.removeProperty("--fac-glow-primary"); 7019 bubble.style.removeProperty("--fac-glow-secondary"); 7020 const shadowStyle = this.settings.shadowStyle || "medium"; 7021 const shadowMap = { 7022 none: "none", 7023 light: "0 2px 10px rgba(0, 0, 0, 0.1)", 7024 medium: "0 4px 20px rgba(0, 0, 0, 0.15)", 7025 strong: "0 8px 30px rgba(0, 0, 0, 0.25)", 7026 }; 7027 bubble.style.boxShadow = 7028 shadowMap[shadowStyle] || shadowMap.medium; 7029 } 7030 7031 // Kolory ikon w bubble - używaj ustawieÅ z admin 7032 const bubbleIconColor = this.settings.bubbleIconColor || "#ffffff"; 7033 const icons = bubble.querySelectorAll( 7034 ".bubble-icon svg, .bubble-icon svg path", 7035 ); 7036 icons.forEach((icon) => { 7037 icon.style.fill = bubbleIconColor; 7038 }); 7039 7040 // Kolory UI 7041 this.applyUIColors(); 7042 7043 // Typografia 7044 this.applyTypography(); 7045 7046 // Aktualizuj okno czatu 7047 this.updateChatWindowSize(); 7048 } 7049 7050 applyUIColors() { 7051 // Pobierz schemat kolorów z ustawieÅ 7052 const colorScheme = this.settings.colorScheme || "professional"; 7053 7054 // Ustaw atrybut data-color-scheme na oknie czatu 7055 const chatWindow = document.getElementById( 7056 "bb-asystent-ai-window", 7057 ); 7058 if (chatWindow) { 7059 chatWindow.setAttribute("data-color-scheme", colorScheme); 7060 } 7061 7062 // Ustaw atrybut także na bubble dla spójnoÅci 7063 const bubble = document.getElementById("bb-asystent-ai-bubble"); 7064 if (bubble) { 7065 bubble.setAttribute("data-color-scheme", colorScheme); 7066 } 7067 7068 // JeÅli to schemat niestandardowy, zastosuj kolory z ustawieÅ 7069 if (colorScheme === "custom") { 7070 this.applyCustomColors(); 7071 } 7072 7073 // Zastosuj dodatkowe kolory niezależnie od schematu 7074 this.applyAdditionalColors(); 7075 } 7076 7077 applyAdditionalColors() { 7078 const root = document.documentElement; 7079 7080 // Kolory stopki 7081 if (this.settings.footerBgColor) { 7082 root.style.setProperty( 7083 "--chat-footer-bg", 7084 this.settings.footerBgColor, 7085 ); 7086 } 7087 if (this.settings.footerTextColor) { 7088 root.style.setProperty( 7089 "--chat-footer-text", 7090 this.settings.footerTextColor, 7091 ); 7092 } 7093 7094 // Kolory tooltip 7095 if (this.settings.tooltipBgColor) { 7096 root.style.setProperty( 7097 "--tooltip-bg", 7098 this.settings.tooltipBgColor, 7099 ); 7100 } 7101 if (this.settings.tooltipTextColor) { 7102 root.style.setProperty( 7103 "--tooltip-text", 7104 this.settings.tooltipTextColor, 7105 ); 7106 } 7107 7108 // Kolory ikon menu 7109 if (this.settings.menuIconColor) { 7110 root.style.setProperty( 7111 "--menu-icon-color", 7112 this.settings.menuIconColor, 7113 ); 7114 7115 // Zastosuj bezpoÅrednio do ikon menu 7116 const menuIcons = document.querySelectorAll( 7117 ".bb-asystent-ai-menu-trigger, .bb-asystent-ai-minimize", 7118 ); 7119 menuIcons.forEach((icon) => { 7120 icon.style.color = this.settings.menuIconColor; 7121 }); 7122 7123 // Zastosuj do SVG w ikonach 7124 const menuSVGs = document.querySelectorAll( 7125 ".bb-asystent-ai-menu-trigger svg, .bb-asystent-ai-minimize svg", 7126
7126 ); 7127 menuSVGs.forEach((svg) => { 7128 svg.style.fill = this.settings.menuIconColor; 7129 }); 7130 } 7131 // Kolory dropdownu menu 7132 if (this.settings.menuBgColor) { 7133 root.style.setProperty( 7134 "--menu-bg-color", 7135 this.settings.menuBgColor, 7136 ); 7137 } 7138 if (this.settings.menuTextColor) { 7139 root.style.setProperty( 7140 "--menu-text-color", 7141 this.settings.menuTextColor, 7142 ); 7143 } 7144 if (this.settings.menuHoverBgColor) { 7145 root.style.setProperty( 7146 "--menu-hover-bg-color", 7147 this.settings.menuHoverBgColor, 7148 ); 7149 } 7150 } 7151 7152 applyCustomColors() { 7153 const chatWindow = document.getElementById( 7154 "bb-asystent-ai-window", 7155 ); 7156 if (!chatWindow) return; 7157 7158 // Użyj bezpoÅrednio kolorów z ustawieÅ dla niestandardowego schematu 7159 const settings = this.settings; 7160 7161 // Ustaw zmienne CSS dla niestandardowego schematu 7162 const customVars = { 7163 "--user-bg-color": settings.userBgColor || "#3498db", 7164 "--user-text-color": settings.userTextColor || "#ffffff", 7165 "--user-name-color": settings.userNameColor || "#2980b9", 7166 "--user-time-color": settings.userTimeColor || "#7fb3d3", 7167 "--assistant-bg-color": settings.assistantBgColor || "#ecf0f1", 7168 "--assistant-text-color": 7169 settings.assistantTextColor || "#2c3e50", 7170 "--assistant-name-color": 7171 settings.assistantNameColor || "#34495e", 7172 "--assistant-time-color": 7173 settings.assistantTimeColor || "#95a5a6", 7174 "--header-bg-color": settings.headerBgColor || "#2c3e50", 7175 "--header-text-color": settings.headerTextColor || "#ffffff", 7176 "--input-bg-color": settings.inputBgColor || "#ffffff", 7177 "--send-button-color": settings.sendButtonColor || "#3498db", 7178 "--bubble-color": settings.bubbleColor || "#3498db", 7179 "--accent-color": settings.accentColor || "#3498db", 7180 "--menu-bg-color": 7181 settings.menuBgColor || settings.headerBgColor || "#ffffff", 7182 "--menu-text-color": 7183 settings.menuTextColor || 7184 settings.headerTextColor || 7185 "#333333", 7186 "--menu-hover-bg-color": 7187 settings.menuHoverBgColor || 7188 settings.headerBgColor || 7189 "rgba(0,0,0,0.05)", 7190 "--menu-border-color": 7191 settings.menuBgColor || 7192 settings.headerBgColor || 7193 "#e9ecef", 7194 "--menu-icon-color": 7195 settings.menuIconColor || 7196 settings.headerTextColor || 7197 "#ffffff", 7198 // FAQ kolory 7199 "--custom-faq-bg-color": settings.faqBgColor || "#f8f9fa", 7200 "--custom-faq-border-color": 7201 settings.faqBorderColor || "#dee2e6", 7202 "--custom-faq-header-color": 7203 settings.faqHeaderColor || "#2c3e50", 7204 "--custom-faq-description-color": 7205 settings.faqDescriptionColor || "#6c757d", 7206 "--custom-faq-button-bg-color": 7207 settings.faqButtonBgColor || "#ffffff", 7208 "--custom-faq-button-border-color": 7209 settings.faqButtonBorderColor || "#e9ecef", 7210 "--custom-faq-button-text-color": 7211 settings.faqButtonTextColor || "#495057", 7212 "--custom-faq-button-hover-bg-color": 7213 settings.faqButtonHoverBgColor || "#007cba", 7214 "--custom-faq-button-hover-text-color": 7215 settings.faqButtonHoverTextColor || "#ffffff", 7216 }; 7217 7218 // Zastosuj zmienne CSS
7219 Object.keys(customVars).forEach((key) => { 7220 chatWindow.style.setProperty(key, customVars[key]); 7221 }); 7222 } 7223 7224 applyTypography() { 7225 const fontFamily = this.settings.fontFamily || "system"; 7226 const fontSize = this.settings.fontSize || 14; 7227 7228 if (fontFamily !== "system") { 7229 const fontMap = { 7230 arial: "Arial, sans-serif", 7231 helvetica: "Helvetica, Arial, sans-serif", 7232 georgia: "Georgia, serif", 7233 times: '"Times New Roman", Times, serif', 7234 roboto: '"Roboto", sans-serif', 7235 "open-sans": '"Open Sans", sans-serif', 7236 lato: '"Lato", sans-serif', 7237 montserrat: '"Montserrat", sans-serif', 7238 }; 7239 7240 const fontValue = fontMap[fontFamily] || fontMap.arial; 7241 const widget = document.getElementById( 7242 "bb-asystent-ai-widget", 7243 ); 7244 if (widget) { 7245 widget.style.fontFamily = fontValue; 7246 } 7247 } 7248 7249 // Rozmiar czcionki 7250 const fontSizeValue = 7251 (typeof fontSize === "number" ? fontSize : 14) + "px"; 7252 const widget = document.getElementById("bb-asystent-ai-widget"); 7253 if (widget) { 7254 widget.style.fontSize = fontSizeValue; 7255 } 7256 } 7257 7258 hexToRgba(hex, alpha = 1.0) { 7259 const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec( 7260 hex, 7261 ); 7262 if (!result) return `rgba(0, 0, 0, ${alpha})`; 7263 7264 const r = parseInt(result[1], 16); 7265 const g = parseInt(result[2], 16); 7266 const b = parseInt(result[3], 16); 7267 7268 return `rgba(${r}, ${g}, ${b}, ${alpha})`; 7269 } 7270 7271 getPositionStyles(position, marginX, marginY) { 7272 const styles = {}; 7273 7274 switch (position) { 7275 case "bottom-right": 7276 styles.bottom = `${marginY}px`; 7277 styles.right = `${marginX}px`; 7278 styles.top = "auto"; 7279 styles.left = "auto"; 7280 break; 7281 case "bottom-left": 7282 styles.bottom = `${marginY}px`; 7283 styles.left = `${marginX}px`; 7284 styles.top = "auto"; 7285 styles.right = "auto"; 7286 break; 7287 case "bottom-center": 7288 styles.bottom = `${marginY}px`; 7289 styles.left = "50%"; 7290 styles.transform = "translateX(-50%)"; 7291 styles.top = "auto"; 7292 styles.right = "auto"; 7293 break; 7294 case "top-right": 7295 styles.top = `${marginY}px`; 7296 styles.right = `${marginX}px`; 7297 styles.bottom = "auto"; 7298 styles.left = "auto"; 7299 break; 7300 case "top-left": 7301 styles.top = `${marginY}px`; 7302 styles.left = `${marginX}px`; 7303 styles.bottom = "auto"; 7304 styles.right = "auto"; 7305 break; 7306 case "center-right": 7307 styles.top = "50%"; 7308 styles.right = `${marginX}px`; 7309 styles.transform = "translateY(-50%)"; 7310 styles.bottom = "auto"; 7311 styles.left = "auto"; 7312 break; 7313 case "center-left": 7314 styles.top = "50%"; 7315 styles.left = `${marginX}px`; 7316 styles.transform = "translateY(-50%)"; 7317 styles.bottom = "auto"; 7318 styles.right = "auto"; 7319 break; 7320 } 7321 7322 return styles; 7323 } 7324 7325 updateChatWindowSize() { 7326 const chatWindow = document.getElementById( 7327 "bb-asystent-ai-window", 7328 ); 7329 if (!chatWindow) return; 7330 if (this.isFullscreen) { 7331 chatWindow.style.removeProperty("width"); 7332 chatWindow.style.removeProperty("height"); 7333 chatWindow.style.removeProperty("maxWidth"); 7334 chatWindow.style.removeProperty("maxHeight"); 7335 return; 7336 } 7337 7338 const windowPreset = this.settings.windowSizePreset || "medium"; 7339 let width, height; 7340 7341 if (windowPreset === "custom") { 7342 width = this.settings.windowCustomWidth || 420; 7343 height = this.settings.windowCustomHeight || 580; 7344 } else { 7345 const presets = { 7346 small: { width: 340, height: 460 }, 7347 medium: { width: 420, height: 580 }, 7348 large: { width: 520, height: 680 }, 7349 }; 7350 const preset = presets[windowPreset] || presets.medium; 7351 width = preset.width; 7352 height = preset.height; 7353 } 7354 7355 // Responsywne ograniczenia - nie może wystawaÄ poza ekran 7356 const viewportWidth = window.innerWidth;
7357 const viewportHeight = window.innerHeight; 7358 const margin = 40; // Margines od krawÄdzi ekranu 7359 const tabletMargin = 20; // Mniejszy margines na tablecie 7360 7361 // Zastosuj tylko na desktop i tablecie (nie mobile) 7362 if (viewportWidth > 768) { 7363 // Oblicz maksymalne dostÄpne wymiary 7364 let maxWidth, maxHeight; 7365 7366 if (viewportWidth <= 1024) { 7367 // Tablet 7368 maxWidth = viewportWidth - tabletMargin; 7369 maxHeight = viewportHeight - tabletMargin; 7370 } else { 7371 // Desktop 7372 maxWidth = viewportWidth - margin; 7373 maxHeight = viewportHeight - margin; 7374 } 7375 7376 // Ogranicz wymiary do dostÄpnego miejsca 7377 const finalWidth = Math.min(width, maxWidth); 7378 const finalHeight = Math.min(height, maxHeight); 7379 7380 // Ustaw wymiary z ograniczeniami 7381 chatWindow.style.width = `${finalWidth}px`; 7382 chatWindow.style.height = `${finalHeight}px`; 7383 chatWindow.style.maxWidth = `${maxWidth}px`; 7384 chatWindow.style.maxHeight = `${maxHeight}px`; 7385 7386 // Minimalne wymiary dla tabletu 7387 if (viewportWidth <= 1024) { 7388 chatWindow.style.minWidth = "280px"; 7389 chatWindow.style.minHeight = "400px"; 7390 } else { 7391 chatWindow.style.removeProperty("minWidth"); 7392 chatWindow.style.removeProperty("minHeight"); 7393 } 7394 } else { 7395 // Mobile - peÅny ekran 7396 chatWindow.style.removeProperty("width"); 7397 chatWindow.style.removeProperty("height"); 7398 chatWindow.style.removeProperty("maxWidth"); 7399 chatWindow.style.removeProperty("maxHeight"); 7400 chatWindow.style.removeProperty("minWidth"); 7401 chatWindow.style.removeProperty("minHeight"); 7402 } 7403 } 7404 7405 updateChatWindowOffset() { 7406 if (typeof window === "undefined") { 7407 return; 7408 } 7409 7410 const widget = document.getElementById("bb-asystent-ai-widget"); 7411 const chatWindow = document.getElementById( 7412 "bb-asystent-ai-window", 7413 ); 7414 7415 if (!widget || !chatWindow) { 7416 return; 7417 } 7418 7419 const isBottomPosition = 7420 widget.classList.contains("position-bottom-right") || 7421 widget.classList.contains("position-bottom-left") || 7422 widget.classList.contains("position-bottom-center"); 7423 const supportsMatchMedia = typeof window.matchMedia === "function"; 7424 const isMobile = supportsMatchMedia 7425 ? window.matchMedia("(max-width: 768px)").matches 7426 : window.innerWidth <= 768; 7427 const shouldApplyOffset = 7428 isBottomPosition && 7429 !this.isFullscreen && 7430 !isMobile && 7431 this.isOpen; 7432 7433 if (shouldApplyOffset) { 7434 // Leave room for the floating bubble when the chat window is not fullscreen. 7435 widget.style.setProperty("--fac-window-offset", "80px"); 7436 chatWindow.style.setProperty("--fac-window-offset", "80px"); 7437 } else { 7438 widget.style.removeProperty("--fac-window-offset"); 7439 chatWindow.style.removeProperty("--fac-window-offset"); 7440 } 7441 } 7442 7443 setupMobileLayout() { 7444 if (typeof window === "undefined") { 7445 return; 7446 } 7447 7448 this.applyMobileBubbleLayout(); 7449 7450 window.addEventListener("resize", this.handleMobileLayoutRefresh); 7451 window.addEventListener( 7452 "orientationchange", 7453 this.handleMobileLayoutRefresh, 7454 ); 7455 } 7456 7457 applyMobileBubbleLayout() { 7458 if (typeof window === "undefined") { 7459 return; 7460 } 7461 7462 const widget = document.getElementById("bb-asystent-ai-widget"); 7463 const bubble = document.getElementById("bb-asystent-ai-bubble"); 7464 7465 if (!widget || !bubble) { 7466 return; 7467 } 7468 7469 const isMobile = 7470 typeof window.matchMedia === "function" && 7471 window.matchMedia("(max-width: 768px)").matches; 7472 7473 if (isMobile) { 7474 if (this.isFullscreen) { 7475 this.exitFullscreen(); 7476 } 7477 widget.classList.add("fac-mobile-bar"); 7478 } else { 7479 widget.classList.remove("fac-mobile-bar"); 7480 } 7481 } 7482 7483 startBubbleAnimation(bubble, animationStyle) { 7484 // UsuÅ wszystkie animacje 7485 bubble.classList.remove( 7486 "animate-bounce", 7487 "animate-pulse", 7488 "animate-shake", 7489 "animate-glow", 7490 "animate-rotate", 7491 ); 7492 7493 // Dodaj nowÄ animacjÄ 7494 switch (animationStyle) { 7495 case "bounce": 7496 bubble.classList.add("animate-bounce"); 7497 break; 7498 case "pulse": 7499 bubble.classList.add("animate-pulse"); 7500 break; 7501 case "shake": 7502 bubble.classList.add("animate-shake"); 7503 break; 7504 case "glow": 7505 bubble.classList.add("animate-glow"); 7506 break; 7507 case "rotate": 7508 // bubble.classList.add('animate-rotate'); // WyÅÄ czono animacjÄ obracania 7509 break; 7510 } 7511 7512 // UsuÅ animacjÄ po pewnym czasie 7513 setTimeout(() => { 7514 bubble.classList.remove( 7515 "animate-bounce", 7516 "animate-pulse", 7517 "animate-shake", 7518 "animate-glow", 7519 "animate-rotate", 7520 ); 7521 }, 3000); 7522 } 7523 7524 adjustBrightness(hex, percent) { 7525 const num = parseInt(hex.replace("#", ""), 16); 7526 const amt = Math.round(2.55 * percent); 7527 const R = (num >> 16) + amt; 7528 const G = ((num >> 8) & 0x00ff) + amt; 7529 const B = (num & 0x0000ff) + amt; 7530 7531 return ( 7532 "#" + 7533 ( 7534 0x1000000 + 7535 (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + 7536 (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + 7537 (B < 255 ? (B < 1 ? 0 : B) : 255) 7538 ) 7539 .toString(16) 7540 .slice(1) 7541 ); 7542 } 7543 7544 async loadChatHistoryForce() { 7545 try { 7546 debugLog( 7547 "Force loading chat history for session:", 7548 this.sessionId, 7549 ); 7550 7551 const historyAction = "bbai_ai_get_history"; 7552 const historyUrl = this.getAjaxUrl(historyAction); 7553 const formData = new FormData(); 7554 formData.append("action", historyAction); 7555 formData.append("session_id", this.sessionId); 7556 formData.append("nonce", BBAsystentAI.nonce); 7557 7558 const response = await fetch(historyUrl, { 7559 method: "POST", 7560 body: formData, 7561 }); 7562 7563 const result = await response.json(); 7564 7565 if ( 7566 result.success && 7567 result.data.messages && 7568 result.data.messages.length > 0 7569 ) { 7570 // ZaÅaduj istniejÄ ce wiadomoÅci 7571 debugLog( 7572 "Force loading existing messages:", 7573 result.data.messages.length, 7574 );
7575 result.data.messages.forEach((msg) => { 7576 if ((msg.type || msg.message_type) === "tool_call") { 7577 const tc = 7578 msg.tool_call || 7579 this._safeParseJson(msg.content)?.tool_call || 7580 this._safeParseJson(msg.message)?.tool_call; 7581 if (tc) { 7582 this.addToolCallMessage(tc, { 7583 persist: false, 7584 scroll: false, 7585 }); 7586 } 7587 return; 7588 } 7589 const messageId = msg.id || null; 7590 this.addMessage( 7591 msg.type, 7592 msg.content, 7593 msg.timestamp, 7594 true, 7595 messageId, 7596 ); 7597 }); 7598 this.scrollToBottom(); 7599 } else { 7600 // Dodaj wiadomoÅÄ powitalnÄ 7601 debugLog("No chat history found, adding welcome message"); 7602 const welcomeMessage = 7603 this.strings.welcomeMessage || 7604 "Witaj! Jestem asystentem AI. W czym mogÄ Ci pomóc?"; 7605 const inserted = this.addWelcomeMessageOnce(welcomeMessage); 7606 7607 if (inserted) { 7608 this.showQuickQuestions(); 7609 7610 // Zapisz w bazie danych 7611 const welcomeAction = "bbai_ai_save_welcome_message"; 7612 const welcomeUrl = this.getAjaxUrl(welcomeAction); 7613 const welcomeFormData = new FormData(); 7614 welcomeFormData.append( 7615 "action", 7616 welcomeAction, 7617 ); 7618 welcomeFormData.append("message", welcomeMessage); 7619 welcomeFormData.append("session_id", this.sessionId); 7620 welcomeFormData.append("nonce", BBAsystentAI.nonce); 7621 7622 await fetch(welcomeUrl, { 7623 method: "POST", 7624 body: welcomeFormData, 7625 }); 7626 } 7627 } 7628 } catch (error) { 7629 debugError("Error force loading chat history:", error); 7630 // W przypadku bÅÄdu, dodaj wiadomoÅÄ powitalnÄ 7631 const welcomeMessage = 7632 this.strings.welcomeMessage || 7633 "Witaj! Jestem asystentem AI. W czym mogÄ Ci pomóc?"; 7634 if (this.addWelcomeMessageOnce(welcomeMessage)) { 7635 this.showQuickQuestions(); 7636 this.scrollToBottom(); 7637 } 7638 } 7639 } 7640 /** 7641 * Åaduje i wyÅwietla predefiniowane pytania 7642 */ 7643 loadQuickQuestions() { 7644 debugLog("FUNERAL DEBUG: Loading quick questions"); 7645 7646 // Sprawdź czy sÄ dostÄpne predefiniowane pytania 7647 if ( 7648 !BBAsystentAI.quickResponses || 7649 BBAsystentAI.quickResponses.length === 0 7650 ) { 7651 debugLog( 7652 "FUNERAL DEBUG: No quick responses available - system disabled or no questions configured", 7653 ); 7654 this.hideQuickQuestions(); 7655 return; 7656 } 7657 7658 debugLog( 7659 "FUNERAL DEBUG: Quick responses found:", 7660 BBAsystentAI.quickResponses, 7661 ); 7662 7663 const quickQuestionsContainer = document.getElementById( 7664 "bb-asystent-ai-quick-questions", 7665 ); 7666 const quickQuestionsList = document.getElementById( 7667 "quick-questions-list", 7668 ); 7669 7670 if (!quickQuestionsContainer || !quickQuestionsList) { 7671 debugLog("FUNERAL DEBUG: Quick questions elements not found"); 7672 return; 7673 } 7674 7675 // WyczyÅÄ poprzedniÄ zawartoÅÄ 7676 quickQuestionsList.innerHTML = ""; 7677 7678 // Dodaj każde pytanie jako przycisk
7679 BBAsystentAI.quickResponses.forEach((question, index) => { 7680 const questionText = (question?.question || "") 7681 .toString() 7682 .trim(); 7683 if (!questionText) { 7684 return; 7685 } 7686 const answerText = (question?.answer ?? "").toString(); 7687 7688 const questionButton = document.createElement("button"); 7689 questionButton.type = "button"; 7690 questionButton.className = "quick-question-button"; 7691 7692 const indexSpan = document.createElement("span"); 7693 indexSpan.className = "quick-question-index"; 7694 indexSpan.textContent = `${index + 1}.`; 7695 7696 const textSpan = document.createElement("span"); 7697 textSpan.className = "quick-question-text"; 7698 textSpan.textContent = questionText; 7699 7700 questionButton.appendChild(indexSpan); 7701 questionButton.appendChild(textSpan); 7702 7703 questionButton.addEventListener("click", () => { 7704 this.handleQuickQuestionClick(questionText, answerText); 7705 }); 7706 7707 quickQuestionsList.appendChild(questionButton); 7708 }); 7709 7710 // Pokaż kontener z pytaniami TYLKO jeÅli udaÅo siÄ zaÅadowaÄ pytania 7711 if (quickQuestionsList.children.length > 0) { 7712 quickQuestionsContainer.style.display = "block"; 7713 debugLog("FUNERAL DEBUG: Quick questions loaded and shown"); 7714 } else { 7715 debugLog( 7716 "FUNERAL DEBUG: No questions loaded - keeping FAQ hidden", 7717 ); 7718 this.hideQuickQuestions(); 7719 } 7720 } 7721 7722 /** 7723 * Pokazuje predefiniowane pytania - tylko jeÅli sÄ dostÄpne i wÅÄ czone 7724 */ 7725 showQuickQuestions() { 7726 // Sprawdź czy system przygotowanych odpowiedzi jest wÅÄ czony i czy sÄ pytania 7727 if ( 7728 !BBAsystentAI.quickResponses || 7729 BBAsystentAI.quickResponses.length === 0 7730 ) { 7731 debugLog( 7732 "FUNERAL DEBUG: No quick responses available - hiding FAQ section", 7733 ); 7734 this.hideQuickQuestions(); 7735 return; 7736 } 7737 7738 const quickQuestionsContainer = document.getElementById( 7739 "bb-asystent-ai-quick-questions", 7740 ); 7741 if (quickQuestionsContainer) { 7742 // Sprawdź czy pytania sÄ już zaÅadowane, jeÅli nie - zaÅaduj je 7743 const quickQuestionsList = document.getElementById( 7744 "quick-questions-list", 7745 ); 7746 if ( 7747 quickQuestionsList && 7748 quickQuestionsList.children.length === 0 7749 ) { 7750 this.loadQuickQuestions(); 7751 } else { 7752 quickQuestionsContainer.style.display = "block"; 7753 debugLog("FUNERAL DEBUG: Quick questions shown"); 7754 } 7755 } 7756 } 7757 7758 /** 7759 * Ukrywa predefiniowane pytania 7760 */ 7761 hideQuickQuestions() { 7762 const quickQuestionsContainer = document.getElementById( 7763 "bb-asystent-ai-quick-questions", 7764 ); 7765 if (quickQuestionsContainer) { 7766 quickQuestionsContainer.style.display = "none"; 7767 debugLog("FUNERAL DEBUG: Quick questions hidden"); 7768 } 7769 } 7770 7771 /** 7772 * ObsÅuguje klikniÄcie w predefiniowane pytanie 7773 */ 7774 handleQuickQuestionClick(question, answer) { 7775 debugLog("FUNERAL DEBUG: Quick question clicked:", question); 7776 7777 // Ukryj predefiniowane pytania 7778 this.hideQuickQuestions(); 7779 7780 // Dodaj pytanie użytkownika do czatu 7781 this.addMessage("user", question); 7782
7783 // Natychmiast dodaj odpowiedź asystenta 7784 setTimeout(() => { 7785 this.addMessage("assistant", answer, null, true, null, { 7786 type: "quick_response", 7787 instant: true, 7788 sanitize: true, 7789 }); 7790 this.scrollToBottom(); 7791 }, 500); 7792 7793 // PrzewiÅ do doÅu 7794 this.scrollToBottom(); 7795 } 7796 7797 startAssistantPersonaLoop() { 7798 if (this.assistantPersonaTimer) { 7799 clearInterval(this.assistantPersonaTimer); 7800 } 7801 7802 const phrases = this.getAssistantPersonaPhrases(); 7803 let index = 0; 7804 7805 // Zawsze zaczynaj od pierwszej frazy (krok 1) 7806 this.updateAssistantPersona(phrases[0]); 7807 7808 this.assistantPersonaTimer = setInterval(() => { 7809 index++; 7810 // ZATRZYMAJ na ostatniej frazie - NIE zapÄtlaj! 7811 if (index >= phrases.length) { 7812 clearInterval(this.assistantPersonaTimer); 7813 this.assistantPersonaTimer = null; 7814 // Pozostaw ostatniÄ frazÄ widocznÄ do koÅca 7815 return; 7816 } 7817 this.updateAssistantPersona(phrases[index]); 7818 }, 3500); 7819 } 7820 7821 stopAssistantPersonaLoop() { 7822 if (this.assistantPersonaTimer) { 7823 clearInterval(this.assistantPersonaTimer); 7824 this.assistantPersonaTimer = null; 7825 } 7826 7827 this.updateAssistantPersona(""); 7828 } 7829 7830 updateAssistantPersona(text) { 7831 const indicator = document.getElementById("bb-asystent-ai-typing"); 7832 if (!indicator) { 7833 return; 7834 } 7835 7836 let wrapper = indicator.querySelector(".typing-indicator-wrapper"); 7837 if (!wrapper) { 7838 wrapper = document.createElement("div"); 7839 wrapper.className = "typing-indicator-wrapper"; 7840 7841 const dots = indicator.querySelector(".typing-indicator"); 7842 if (dots) { 7843 indicator.replaceChild(wrapper, dots); 7844 wrapper.appendChild(dots); 7845 } else { 7846 wrapper.innerHTML = 7847 '<div class="typing-indicator"><span></span><span></span><span></span></div>'; 7848 indicator.appendChild(wrapper); 7849 } 7850 } 7851 7852 let status = indicator.querySelector(".typing-status"); 7853 if (!status) { 7854 status = document.createElement("div"); 7855 status.className = "typing-status"; 7856 wrapper.appendChild(status); 7857 } 7858 7859 status.textContent = text; 7860 status.style.display = text ? "block" : "none"; 7861 } 7862 7863 getAssistantPersonaPhrases() { 7864 const localized = this.strings.typingPhrases; 7865 if (Array.isArray(localized) && localized.length > 0) { 7866 return localized; 7867 } 7868 7869 // Frazy w LOGICZNEJ kolejnoÅci - krok po kroku (NIE zapÄtlajÄ siÄ!) 7870 return [ 7871 "AnalizujÄ Twoje pytanieâ¦", 7872 "Przetwarzam informacjeâ¦",
7873 "PrzygotowujÄ odpowiedźâ¦", 7874 "GenerujÄ wiadomoÅÄâ¦", 7875 ]; 7876 } 7877 7878 /** 7879 * Inicjalizuje animowane pytania w pastylce 7880 */ 7881 initAnimatedQuestions() { 7882 debugLog("FUNERAL DEBUG: initAnimatedQuestions called"); 7883 const bubble = document.getElementById("bb-asystent-ai-bubble"); 7884 debugLog("FUNERAL DEBUG: bubble found:", !!bubble); 7885 debugLog( 7886 "FUNERAL DEBUG: has animated-text class:", 7887 bubble?.classList.contains("has-animated-text"), 7888 ); 7889 7890 if (!bubble || !bubble.classList.contains("has-animated-text")) { 7891 debugLog( 7892 "FUNERAL DEBUG: No animated text bubble found, skipping", 7893 ); 7894 return; 7895 } 7896 7897 // Pobierz pytania z quick responses lub użyj domyÅlnych 7898 this.animatedQuestions = this.getAnimatedQuestions(); 7899 debugLog( 7900 "FUNERAL DEBUG: animated questions:", 7901 this.animatedQuestions, 7902 ); 7903 7904 this.baseAnimatedQuestions = Array.isArray(this.animatedQuestions) 7905 ? [...this.animatedQuestions] 7906 : []; 7907 7908 const controlsInitialized = this.setupAnimatedQuestionControls(); 7909 7910 if (!controlsInitialized) { 7911 debugLog( 7912 "FUNERAL DEBUG: Controls not available, refreshing animation with defaults", 7913 ); 7914 this.refreshAnimatedQuestions(); 7915 } 7916 } 7917 7918 /** 7919 * Pobiera pytania do animacji 7920 */ 7921 getAnimatedQuestions() { 7922 debugLog("FUNERAL DEBUG: getAnimatedQuestions called"); 7923 const configured = Array.isArray(this.settings.animatedQuestions) 7924 ? this.settings.animatedQuestions 7925 .map((item) => 7926 typeof item === "string" ? item.trim() : "", 7927 ) 7928 .filter((item) => item.length > 0) 7929 : []; 7930 7931 if (configured.length > 0) { 7932 debugLog( 7933 "FUNERAL DEBUG: Using configured animated questions:", 7934 configured, 7935 ); 7936 return configured.slice(0, 10); 7937 } 7938 7939 // Spróbuj pobraÄ z quick responses 7940 if ( 7941 BBAsystentAI.quickResponses && 7942 BBAsystentAI.quickResponses.length > 0 7943 ) { 7944 let questions = BBAsystentAI.quickResponses 7945 .map((q) => (q?.question || "").toString().trim()) 7946 .filter((q) => q.length > 0); 7947 7948 if (this.settings.animatedQuestionsShuffle) { 7949 questions = this.shuffleArray(questions); 7950 } 7951 7952 questions = questions.slice(0, 10); 7953 7954 debugLog( 7955 "FUNERAL DEBUG: Using quick responses fallback:", 7956 questions, 7957 ); 7958 return questions; 7959 } 7960 7961 // DomyÅlne pytania 7962 const defaultQuestions = [ 7963 "W czym możemy pomóc, korzystajÄ c z informacji na tej stronie?", 7964 "Chcesz poznaÄ kolejne kroki organizacji pożegnania lub uroczystoÅci?", 7965 "Potrzebujesz wsparcia w formalnoÅciach urzÄdowych lub dokumentach pogrzebowych?", 7966 "Szukasz terminów, miejsc lub przebiegu planowanych ceremonii i nekrologów?", 7967 "InteresujÄ CiÄ możliwoÅci personalizacji uroczystoÅci albo pamiÄ tek?", 7968 "Czy mam pomóc znaleÅºÄ nekrolog lub szczegóÅy konkretnego pożegnania?", 7969 "Jak możemy wesprzeÄ rodzinÄ w przygotowaniu treÅci na stronÄ pamiÄci?", 7970 "Chcesz omówiÄ transport, kwiaty, oprawÄ muzycznÄ lub inne dodatki?", 7971 "Potrzebujesz szybkich odpowiedzi dotyczÄ cych naszej oferty, cen i terminów?", 7972 "Wolisz zaczÄ Ä od najczÄstszych pytaÅ klientów o nasze usÅugi?", 7973 ]; 7974 debugLog( 7975 "FUNERAL DEBUG: Using default questions:", 7976 defaultQuestions, 7977 ); 7978 return defaultQuestions; 7979 } 7980 7981 setupAnimatedQuestionControls() { 7982 debugLog("FUNERAL DEBUG: setupAnimatedQuestionControls called"); 7983 const input = document.getElementById( 7984 "bb-asystent-ai-animated-text", 7985 ); 7986 const toggle = document.getElementById( 7987 "bb-asystent-ai-animated-toggle", 7988 ); 7989 const counter = document.getElementById( 7990 "bb-asystent-ai-animated-counter", 7991 ); 7992 7993 if (!input || !toggle) { 7994 debugLog( 7995 "FUNERAL DEBUG: Animated question controls not present", 7996 ); 7997 return false;
7998 } 7999 8000 const maxLength = 8001 parseInt(input.getAttribute("maxlength"), 10) || 140; 8002 8003 const updateCounter = () => { 8004 if (!counter) { 8005 return; 8006 } 8007 const length = this.normalizeAnimatedQuestion( 8008 input.value, 8009 ).length; 8010 counter.textContent = `${length}/${maxLength}`; 8011 }; 8012 8013 let savedText = ""; 8014 let savedEnabled = true; 8015 8016 try { 8017 if (typeof window !== "undefined" && window.localStorage) { 8018 const stored = 8019 window.localStorage.getItem( 8020 this.customAnimatedQuestionKey, 8021 ) || ""; 8022 if (stored) { 8023 savedText = stored; 8024 } 8025 } 8026 } catch (error) { 8027 debugWarn( 8028 "FUNERAL DEBUG: Unable to read stored custom question", 8029 error, 8030 ); 8031 } 8032 8033 try { 8034 if (typeof window !== "undefined" && window.localStorage) { 8035 const storedToggle = window.localStorage.getItem( 8036 this.customAnimatedQuestionToggleKey, 8037 ); 8038 if (storedToggle !== null) { 8039 savedEnabled = storedToggle !== "false"; 8040 } 8041 } 8042 } catch (error) { 8043 debugWarn( 8044 "FUNERAL DEBUG: Unable to read stored custom question toggle", 8045 error, 8046 ); 8047 } 8048 8049 if (savedText) { 8050 input.value = savedText; 8051 } 8052 toggle.checked = savedEnabled; 8053 updateCounter(); 8054 8055 const handleChange = () => { 8056 updateCounter(); 8057 this.applyCustomAnimatedQuestion(input.value, toggle.checked); 8058 }; 8059 8060 input.addEventListener("input", handleChange); 8061 toggle.addEventListener("change", handleChange); 8062 8063 handleChange(); 8064 return true; 8065 } 8066 8067 applyCustomAnimatedQuestion(rawText, enabled) { 8068 const normalized = this.normalizeAnimatedQuestion(rawText); 8069 8070 try { 8071 if (typeof window !== "undefined" && window.localStorage) { 8072 window.localStorage.setItem( 8073 this.customAnimatedQuestionKey, 8074 normalized, 8075 ); 8076 window.localStorage.setItem( 8077 this.customAnimatedQuestionToggleKey, 8078 enabled ? "true" : "false", 8079 ); 8080 } 8081 } catch (error) { 8082 debugWarn( 8083 "FUNERAL DEBUG: Unable to persist custom animated question", 8084 error, 8085 ); 8086 } 8087 8088 if (!Array.isArray(this.baseAnimatedQuestions)) { 8089 this.baseAnimatedQuestions = []; 8090 } 8091 if (this.baseAnimatedQuestions.length === 0) { 8092 this.baseAnimatedQuestions = [...this.getAnimatedQuestions()]; 8093 } 8094 8095 if (enabled && normalized.length > 0) { 8096 this.customAnimatedQuestion = normalized; 8097 const filteredBase = this.baseAnimatedQuestions.filter( 8098 (question) => 8099 this.normalizeAnimatedQuestion( 8100 question, 8101 ).toLowerCase() !== normalized.toLowerCase(), 8102 ); 8103 this.animatedQuestions = [normalized, ...filteredBase]; 8104 } else { 8105 this.customAnimatedQuestion = null; 8106 this.animatedQuestions = [...this.baseAnimatedQuestions]; 8107 } 8108 8109 if ( 8110 !Array.isArray(this.animatedQuestions) || 8111 this.animatedQuestions.length === 0 8112 ) { 8113 this.animatedQuestions = this.getAnimatedQuestions(); 8114 this.baseAnimatedQuestions = [...this.animatedQuestions]; 8115 } 8116 8117 this.currentQuestionIndex = 0; 8118 this.refreshAnimatedQuestions(); 8119 } 8120 8121 normalizeAnimatedQuestion(text) { 8122 if (typeof text !== "string") { 8123 return ""; 8124 } 8125 return text.replace(/\s+/g, " ").trim(); 8126 } 8127 8128 refreshAnimatedQuestions() { 8129 debugLog("FUNERAL DEBUG: refreshAnimatedQuestions called"); 8130 this.stopQuestionAnimation(); 8131 this.currentQuestionIndex = 0; 8132 8133 const container = document.getElementById("animated-questions"); 8134 if (!container) { 8135 debugLog( 8136 "FUNERAL DEBUG: No animated questions container available", 8137 ); 8138 this.animatedQuestionsContainer = null; 8139 return; 8140 } 8141 8142 this.animatedQuestionsContainer = container; 8143 container.innerHTML = ""; 8144 8145 if ( 8146 !Array.isArray(this.animatedQuestions) || 8147 this.animatedQuestions.length === 0 8148 ) { 8149 debugLog( 8150 "FUNERAL DEBUG: Animated questions list empty after refresh", 8151 ); 8152 return; 8153 } 8154 8155 this.startQuestionAnimation(); 8156 } 8157 8158 shuffleArray(list) { 8159 const array = [...list]; 8160 for (let i = array.length - 1; i > 0; i -= 1) { 8161 const j = Math.floor(Math.random() * (i + 1)); 8162 [array[i], array[j]] = [array[j], array[i]]; 8163 } 8164 return array; 8165 } 8166 8167 /** 8168 * Rozpoczyna animacjÄ pytaÅ 8169 */ 8170 startQuestionAnimation() { 8171 debugLog("FUNERAL DEBUG: startQuestionAnimation called"); 8172 // Zawsze zatrzymaj poprzedni interwaÅ, żeby nie dublowaÄ animacji 8173 this.stopQuestionAnimation(); 8174 const container = document.getElementById("animated-questions"); 8175 debugLog("FUNERAL DEBUG: container found:", !!container); 8176 debugLog("FUNERAL DEBUG: container element:", container); 8177 8178 if (!container) { 8179 debugLog( 8180 "FUNERAL DEBUG: No animated-questions container found - searching for alternatives", 8181 ); 8182 // Spróbuj znaleÅºÄ kontener w inny sposób 8183 const bubble = document.getElementById( 8184 "bb-asystent-ai-bubble", 8185 ); 8186 if (bubble) { 8187 const altContainer = bubble.querySelector( 8188 ".animated-questions", 8189 ); 8190 debugLog( 8191 "FUNERAL DEBUG: Alternative container found:", 8192 !!altContainer, 8193 ); 8194 if (altContainer) { 8195 this.animatedQuestionsContainer = altContainer; 8196 } 8197 } 8198 return; 8199 } 8200 8201 this.animatedQuestionsContainer = container; 8202 8203 // WyczyÅÄ poprzednie pytania 8204 container.innerHTML = ""; 8205 8206 // Dodaj pierwsze pytanie 8207 debugLog("FUNERAL DEBUG: Showing first question"); 8208 this.showNextQuestion(); 8209 8210 // Ustaw interwaÅ dla kolejnych pytaÅ 8211 this.questionInterval = setInterval(() => { 8212 debugLog("FUNERAL DEBUG: Showing next question via interval"); 8213 this.showNextQuestion(); 8214 }, 5000); // Zmiana co 5 sekundy 8215 8216 debugLog("FUNERAL DEBUG: Question animation started with interval"); 8217 } 8218 8219 /** 8220 * Pokazuje nastÄpne pytanie. Zaprojektowane tak, że: 8221 * - jednoczeÅnie w DOM jest MAKS. 2 elementy (1 active + 1 exiting), 8222 * - re-entrancy guard zapobiega wyÅcigowi przy szybkich wywoÅaniach, 8223 * - pozostaÅe (osierocone) elementy sÄ twardo usuwane na wejÅciu. 8224 */ 8225 showNextQuestion() { 8226 const container = 8227 this.animatedQuestionsContainer || 8228 document.getElementById("animated-questions"); 8229 8230 if (!container || this.animatedQuestions.length === 0) { 8231 return; 8232 } 8233 8234 // Re-entrancy: jeÅli przejÅcie trwa, ignoruj kolejne wywoÅanie. 8235 if (this._questionTransitionPending) { 8236 return; 8237 } 8238 this._questionTransitionPending = true; 8239 8240 // SprzÄ tanie: usuÅ wszystkie elementy oprócz ostatniego active 8241 // (którego użyjemy jako âexiting"). Tym samym nawet jeÅli historycznie 8242 // zostaÅo coÅ zaÅmiecone, tu siÄ czyÅci. 8243 const items = container.querySelectorAll(".question-text"); 8244 const activeNode = container.querySelector(".question-text.active");
8245 items.forEach((node) => { 8246 if (node !== activeNode) { 8247 node.remove(); 8248 } 8249 }); 8250 8251 // Stary aktywny element przechodzi w exiting i jest usuwany po transition. 8252 if (activeNode) { 8253 activeNode.classList.remove("active"); 8254 activeNode.classList.add("exiting"); 8255 // Slot na wypadek gdyby transitionend nie odpaliÅ (np. visibility: hidden). 8256 const fallbackTimer = setTimeout(() => { 8257 if (activeNode.parentNode) activeNode.remove(); 8258 }, 700); 8259 activeNode.addEventListener( 8260 "transitionend", 8261 () => { 8262 clearTimeout(fallbackTimer); 8263 if (activeNode.parentNode) activeNode.remove(); 8264 }, 8265 { once: true }, 8266 ); 8267 } 8268 8269 // Pobierz tekst nowego pytania. 8270 let questionText = this.animatedQuestions[this.currentQuestionIndex]; 8271 if (typeof questionText !== "string") { 8272 questionText = ""; 8273 } 8274 questionText = questionText.trim(); 8275 if (questionText.length > 140) { 8276 questionText = `${questionText.slice(0, 138).trimEnd()}..`; 8277 } 8278 8279 const questionElement = document.createElement("div"); 8280 questionElement.className = "question-text"; 8281 questionElement.textContent = questionText; 8282 container.appendChild(questionElement); 8283 8284 // Aktywuj animacjÄ po jednej klatce â gwarantuje, że transition zadziaÅa. 8285 requestAnimationFrame(() => { 8286 requestAnimationFrame(() => { 8287 questionElement.classList.add("active"); 8288 this._questionTransitionPending = false; 8289 }); 8290 }); 8291 8292 this.currentQuestionIndex = 8293 (this.currentQuestionIndex + 1) % this.animatedQuestions.length; 8294 } 8295 8296 /** 8297 * Zatrzymuje animacjÄ pytaÅ 8298 */ 8299 stopQuestionAnimation() { 8300 if (this.questionInterval) { 8301 clearInterval(this.questionInterval); 8302 this.questionInterval = null; 8303 } 8304 } 8305 8306 /** 8307 * Aktualizuje stan pastylki (otwarty/zamkniÄty) 8308 */ 8309 updatePillState(isOpen) { 8310 const bubble = document.getElementById("bb-asystent-ai-bubble"); 8311 if (!bubble || !bubble.classList.contains("has-animated-text")) { 8312 return; 8313 } 8314 8315 if (isOpen) { 8316 bubble.classList.add("chat-open"); 8317 debugLog("FUNERAL DEBUG: Added chat-open class to pill"); 8318 } else { 8319 bubble.classList.remove("chat-open"); 8320 debugLog("FUNERAL DEBUG: Removed chat-open class from pill"); 8321 // Restart animacji pytaÅ gdy chat siÄ zamyka 8322 setTimeout(() => { 8323 this.startQuestionAnimation(); 8324 }, 300); // Delay żeby animacja przejÅcia siÄ skoÅczyÅa 8325 } 8326 } 8327 8328 // Cookie consent metody usuniÄte - sÄ teraz w inline skrypcie w ChatWidget.php 8329 } // Koniec klasy BBAsystentAI 8330 8331 // Ustaw statyczne wÅaÅciwoÅci klasy na dane z wp_localize_script 8332 Object.assign(BBAsystentAI, bootstrapData); 8333 BBAsystentAI.settings = bootstrapData.settings || {}; 8334 BBAsystentAI.strings = bootstrapData.strings || {}; 8335 BBAsystentAI.quickResponses = bootstrapData.quickResponses || []; 8336 BBAsystentAI.ajaxUrl = bootstrapData.ajaxUrl || ""; 8337 BBAsystentAI.restUrl = bootstrapData.restUrl || ""; 8338 BBAsystentAI.nonce = bootstrapData.nonce || ""; 8339 BBAsystentAI.sessionId = bootstrapData.sessionId || ""; 8340 8341 // Patch: render assistant instant/quick messages as normal markdown (via marked.js) 8342 // by stripping meta flags that trigger sanitize/plain rendering paths. 8343 if ( 8344 typeof BBAsystentAI !== "undefined" && 8345 !BBAsystentAI.__patchedQuickAsNormal 8346 ) { 8347 (function () { 8348 const originalAddMessage = BBAsystentAI.prototype.addMessage; 8349 BBAsystentAI.prototype.addMessage = function ( 8350 type, 8351 content, 8352 timestamp = null, 8353 shouldAdd = true, 8354 messageId = null, 8355 meta = null, 8356 ) { 8357 if ( 8358 type === "assistant" && 8359 meta && 8360 (meta.type === "quick_response" || 8361 meta.instant || 8362 meta.sanitize) 8363 ) { 8364 // Drop special flags so assistant content is rendered with Markdown like a normal message 8365 meta = null; 8366 } 8367 return originalAddMessage.call( 8368 this, 8369 type, 8370 content, 8371 timestamp, 8372 shouldAdd, 8373 messageId, 8374 meta, 8375 ); 8376 }; 8377 BBAsystentAI.__patchedQuickAsNormal = true; 8378 })(); 8379 } 8380 8381 // Patch: render assistant instant/quick messages as normal markdown (via marked.js) 8382 if ( 8383 typeof BBAsystentAI !== "undefined" && 8384 !BBAsystentAI.__patchedQuickAsNormal 8385 ) { 8386 (function () { 8387 const originalAddMessage = BBAsystentAI.prototype.addMessage; 8388 BBAsystentAI.prototype.addMessage = function ( 8389 type, 8390 content, 8391 timestamp = null, 8392 shouldAdd = true, 8393 messageId = null, 8394 meta = null, 8395 ) { 8396 if ( 8397 type === "assistant" && 8398 meta && 8399 (meta.type === "quick_response" || 8400 meta.instant || 8401 meta.sanitize) 8402 ) { 8403 // Drop special flags so assistant content is rendered with Markdown like a normal message 8404 meta = null; 8405 } 8406 return originalAddMessage.call( 8407 this, 8408 type, 8409 content, 8410 timestamp, 8411 shouldAdd, 8412 messageId, 8413 meta, 8414 ); 8415 }; 8416 BBAsystentAI.__patchedQuickAsNormal = true; 8417 })(); 8418 } 8419 // Initialize when DOM is ready 8420 if (document.readyState === "loading") { 8421 document.addEventListener("DOMContentLoaded", () => { 8422 window.BBAsystentAI = new BBAsystentAI(); 8423 }); 8424 } else { 8425 window.BBAsystentAI = new BBAsystentAI(); 8426 } 8427 8428 /** 8429 * Awaryjny fallback dla menu trzech kropek, gdyby gÅówna logika nie zadziaÅaÅa 8430 * (np. kolizja z innymi skryptami). Nie dubluje zdarzeÅ â odpala siÄ tylko raz. 8431 */ 8432 if (!window.__bbAsystentAIMenuFallbackBound) { 8433 window.__bbAsystentAIMenuFallbackBound = true; 8434 8435 const bindFallbackMenu = () => { 8436 const trigger = document.getElementById("bb-asystent-ai-menu-trigger"); 8437 const menu = document.getElementById("bb-asystent-ai-menu"); 8438 const wrapper = trigger?.closest(".bb-asystent-ai-menu-wrapper") || null; 8439 if (!trigger || !menu || !wrapper) { 8440 return; 8441 } 8442 8443 const open = () => { 8444 menu.classList.add("bb-ai-show"); 8445 menu.hidden = false;
8446 menu.removeAttribute("hidden"); 8447 menu.style.display = "block"; 8448 trigger.setAttribute("aria-expanded", "true"); 8449 wrapper.setAttribute("data-menu-open", "1"); 8450 }; 8451 8452 const close = () => { 8453 menu.classList.remove("bb-ai-show"); 8454 menu.hidden = true; 8455 menu.setAttribute("hidden", "hidden"); 8456 menu.style.display = "none"; 8457 trigger.setAttribute("aria-expanded", "false"); 8458 wrapper.setAttribute("data-menu-open", "0"); 8459 }; 8460 8461 const toggle = (event) => { 8462 event.preventDefault(); 8463 event.stopPropagation(); 8464 const isOpen = 8465 menu.classList.contains("bb-ai-show") || 8466 trigger.getAttribute("aria-expanded") === "true"; 8467 if (isOpen) { 8468 close(); 8469 } else { 8470 open(); 8471 } 8472 }; 8473 8474 trigger.addEventListener("click", toggle); 8475 8476 document.addEventListener( 8477 "click", 8478 (event) => { 8479 const target = event.target; 8480 if (!target || !(target instanceof Element)) { 8481 return; 8482 } 8483 const isOpen = 8484 menu.classList.contains("bb-ai-show") || 8485 trigger.getAttribute("aria-expanded") === "true"; 8486 if (!isOpen) { 8487 return; 8488 } 8489 if (wrapper.contains(target)) { 8490 return; 8491 } 8492 close(); 8493 }, 8494 false, 8495 ); 8496 8497 window.addEventListener("blur", close); 8498 }; 8499 8500 if (document.readyState === "loading") { 8501 document.addEventListener("DOMContentLoaded", bindFallbackMenu); 8502 } else { 8503 bindFallbackMenu(); 8504 } 8505 } 8506})();
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.