PageSourceSearch

https://zielen.pl/wp-content/plugins/bb-asystent-ai/assets/js/frontend.js?ver=3.10.19

js zielen.pl collected 2026-10-04 01:29:06 UTC 344,792 bytes, 8,506 lines download raw bytes

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, "&lt;")
242                    .replace(/>/g, "&gt;")
243                    .replace(/"/g, "&quot;")
244                    .replace(/'/g, "&#39;");
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, "&amp;")
378                        .replace(/</g, "&lt;")
379                        .replace(/>/g, "&gt;")
380                        .replace(/"/g, "&quot;")
381                        .replace(/'/g, "&#39;");
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(/&nbsp;/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*(["']|&quot;)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*&quot;tel:[^&]+&quot;/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}![obrazek](${url})`,
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, "&amp;")
1187                .replace(/</g, "&lt;")
1188                .replace(/>/g, "&gt;")
1189                .replace(/"/g, "&quot;")
1190                .replace(/'/g, "&#39;");
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 "&lt;br&gt;".
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 "&lt;br&gt;" from older versions.
4486                // Normalize it back to text so we don't render literal "&lt;br&gt;" 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("&lt;") ||
4814                            href.includes("&gt;") ||
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("&lt;") ||
4839                            href.includes("&gt;") ||
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;amp;nbsp; -> &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.