PageSourceSearch

https://skums.ac.ir/assets/js/bluetailapp.js

js skums.ac.ir collected 2026-10-02 10:51:29 UTC 94,437 bytes, 2,148 lines download raw bytes

1/**
2 * BlueTailApp Chat Loader
3 * =======================
4 * این فایل برای محیط production طراحی شده است
5 * تمام منابع chat را از پوشه /assets/js/bluetailapp/ و /assets/css/bluetailapp/ لود می‌کند
6 * 
7 * تفاوت با نسخه development:
8 * - مسیرها به /assets/... تغییر یافته‌اند
9 * - از CHAT_BOX_CONFIG به جای chatInterfaceConfig استفاده می‌کند
10 * - Speech Bubble از CHAT_BOX_CONFIG می‌خواند
11 * 
12 * نحوه استفاده:
13 * <script src="/assets/js/bluetailapp.js?siteid=1&show_chat_bot=1&userid=0"></script>
14 */
15
16(function() {
17    'use strict';
18
19    console.log('🚀 BlueTailApp Chat Loader initialized');
20
21    /**
22     * Detect a real page refresh (reload) vs a fresh navigation.
23     * Uses the modern PerformanceNavigationTiming API with a fallback to
24     * the legacy performance.navigation.type === 1 for older browsers.
25     */
26    function isPageRefresh() {
27        try {
28            const navEntries =
29                (performance.getEntriesByType && performance.getEntriesByType('navigation')) || [];
30            if (navEntries.length > 0 && navEntries[0].type) {
31                return navEntries[0].type === 'reload';
32            }
33            if (performance.navigation && typeof performance.navigation.type === 'number') {
34                return performance.navigation.type === 1; // TYPE_RELOAD
35            }
36        } catch (e) {
37            // ignore – treat as non-refresh
38        }
39        return false;
40    }
41
42    /**
43     * On page refresh, wipe chat/session keys from localStorage so the user
44     * starts fresh (same key set that closeAllBackendSessions() clears on
45     * "بستن چت"). Personal preferences (theme, soundEnabled, debug-mode) are
46     * intentionally preserved. The nested FAQ system does not persist any
47     * client-side state, so there's nothing FAQ-related to keep here.
48     */
49    function clearChatStateOnRefresh() {
50        if (!isPageRefresh()) return;
51
52        const sessionKeys = [
53            'online_chat_id',
54            'online_chat_started',
55            'chatbot_session_id',
56            'niafam-chat-used',
57            'niafam-chat-messages',
58            'niafam-user-data',
59            'niafam-guest-session-start',
60            'userName',
61            'userEmail',
62            'messageRatings',
63            // auth-session key — falls back to default; same default used by
64            // userInfoForm.authConfig?.storage?.key in script.js cleanup path
65            'niafamchat_user_session',
66            // FAQ tree cache — re-fetch on refresh so the user always picks
67            // up any content updates the backend team has published.
68            'niafam_faq_tree_cache'
69        ];
70
71        let removed = 0;
72        sessionKeys.forEach(key => {
73            try {
74                if (localStorage.getItem(key) !== null) {
75                    localStorage.removeItem(key);
76                    removed++;
77                }
78            } catch (e) {
79                // ignore individual key failures
80            }
81        });
82
83        console.log(`🔄 Page refreshed — cleared ${removed} chat/session localStorage keys (theme, sound, debug-mode preserved)`);
84    }
85
86    clearChatStateOnRefresh();
87
88    function getLoaderScriptSrc() {
89        try {
90            if (document.currentScript && document.currentScript.src) {
91                return document.currentScript.src;
92            }
93
94            const scripts = document.getElementsByTagName('script');
95            for (let i = scripts.length - 1; i >= 0; i--) {
96                const src = scripts[i] && scripts[i].src;
97                if (src && src.includes('/assets/js/bluetailapp.js')) {
98                    return src;
99                }
100            }
101        } catch (e) {
102            return null;
103        }
104
105        return null;
106    }
107
108    function getLoaderParams() {
109        const src = getLoaderScriptSrc();
110        if (!src) return new URLSearchParams();
111
112        try {
113            const u = new URL(src, window.location.origin);
114            return u.searchParams || new URLSearchParams();
115        } catch (e) {
116            const idx = src.indexOf('?');
117            return new URLSearchParams(idx >= 0 ? src.substring(idx + 1) : '');
118        }
119    }
120
121    const loaderParams = getLoaderParams();
122    if (!window.NIAFAM_CHAT_LOADER_PARAMS) {
123        const paramsObj = {};
124        try {
125            loaderParams.forEach((value, key) => {
126                paramsObj[key] = value;
127            });
128        } catch (e) {
129            // Ignore and fallback to empty
130        }
131        window.NIAFAM_CHAT_LOADER_PARAMS = paramsObj;
132    }
133
134    if (!window.NIAFAM_CHAT_CSRF_TOKEN) {
135        const csrf = loaderParams.get('csrf');
136        if (csrf) {
137            window.NIAFAM_CHAT_CSRF_TOKEN = csrf;
138        } else {
139            try {
140                const meta = document.querySelector('meta[name="csrf-token"]');
141                const metaToken = meta ? meta.getAttribute('content') : null;
142                if (metaToken) {
143                    window.NIAFAM_CHAT_CSRF_TOKEN = metaToken;
144                }
145            } catch (e) {
146                // ignore
147            }
148        }
149    }
150
151    // State management
152    let chatLoaded = false;
153    let chatLoading = false;
154    let loadStartTime = 0;
155    let userClickedToggle = false; // آیا کاربر روی chatToggle کلیک کرده؟
156
157    /**
158     * Create ConfigLoader Mock
159     * ایجاد یک configLoader ساده برای سازگاری با فایل‌های JS
160     */
161    function createConfigLoader() {
162        if (window.configLoader) {
163            console.log('ℹ️ configLoader already exists');
164            return;
165        }
166
167        // Get config immediately if available
168        const initialConfig = window.chatInterfaceConfig && window.chatInterfaceConfig[0] 
169            ? window.chatInterfaceConfig[0].configuration 
170            : null;
171        const initialTenantId = window.chatInterfaceConfig && window.chatInterfaceConfig[0]
172            ? window.chatInterfaceConfig[0].tenantId
173            : null;
174
175        window.configLoader = {
176            config: initialConfig,
177            isLoaded: initialConfig ? true : false,
178            tenantId: initialTenantId,
179            
180            async waitForConfig() {
181                // Config already loaded in convertConfigFormat
182                if (window.chatInterfaceConfig && window.chatInterfaceConfig[0]) {
183                    this.config = window.chatInterfaceConfig[0].configuration;
184                    this.tenantId = window.chatInterfaceConfig[0].tenantId;
185                    this.isLoaded = true;
186                    
187                    // Apply configuration to UI
188                    this.applyConfiguration();
189                    
190                    return this.config;
191                }
192                return null;
193            },
194            
195            getConfig() {
196                if (window.chatInterfaceConfig && window.chatInterfaceConfig[0]) {
197                    return window.chatInterfaceConfig[0].configuration;
198                }
199                return null;
200            },
201            
202            isConfigLoaded() {
203                return window.chatInterfaceConfig && window.chatInterfaceConfig[0] ? true : false;
204            },
205            
206            /**
207             * Apply configuration to the interface
208             */
209            applyConfiguration() {
210                console.log('🎨 Applying CHAT_BOX_CONFIG to interface...');
211                
212                if (!this.config) {
213                    console.warn('⚠️ No configuration to apply');
214                    return;
215                }
216
217                try {
218                    // Apply branding
219                    this.applyBranding();
220                    
221                    // Apply visual appearance
222                    this.applyVisualAppearance();
223                    
224                    // Apply content
225                    this.applyContent();
226                    
227                    console.log('✅ Configuration applied successfully');
228                    
229                } catch (error) {
230                    console.error('❌ Error applying configuration:', error);
231                }
232            },
233            
234            /**
235             * Apply branding settings
236             */
237            applyBranding() {
238                const branding = this.config.companyBranding;
239                if (!branding) {
240                    console.log('⏭️ No branding configuration found');
241                    return;
242                }
243                
244                console.log('🏷️ Applying branding:', branding);
245                
246                // Apply bot name to chat header
247                if (branding.botName) {
248                    const chatTitleH3 = document.querySelector('.chat-title h3');
249                    if (chatTitleH3) {
250                        chatTitleH3.textContent = branding.botName;
251                        console.log('✅ Bot name applied:', branding.botName);
252                    }
253                }
254                
255                // Note: Page title is NOT changed to avoid interfer
255ing with host page
256                // Branding is applied only to chat UI elements
257            },
258            
259            /**
260             * Apply visual appearance
261             */
262            applyVisualAppearance() {
263                const appearance = this.config.visualAppearance;
264                if (!appearance) {
265                    console.log('⏭️ No visual appearance configuration found');
266                    return;
267                }
268                
269                console.log('🎨 Applying visual appearance:', appearance);
270                
271                const root = document.documentElement;
272                
273                // Helper: Convert hex to RGB
274                const hexToRgb = (hex) => {
275                    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
276                    return result ? {
277                        r: parseInt(result[1], 16),
278                        g: parseInt(result[2], 16),
279                        b: parseInt(result[3], 16)
280                    } : null;
281                };
282                
283                // Helper: Convert RGB to HSL
284                const rgbToHsl = (r, g, b) => {
285                    r /= 255; g /= 255; b /= 255;
286                    const max = Math.max(r, g, b), min = Math.min(r, g, b);
287                    let h, s, l = (max + min) / 2;
288                    
289                    if (max === min) {
290                        h = s = 0;
291                    } else {
292                        const d = max - min;
293                        s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
294                        switch (max) {
295                            case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
296                            case g: h = ((b - r) / d + 2) / 6; break;
297                            case b: h = ((r - g) / d + 4) / 6; break;
298                        }
299                    }
300                    return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
301                };
302                
303                // Helper: Get luminance for contrast calculation
304                const getLuminance = (r, g, b) => {
305                    const [rs, gs, bs] = [r, g, b].map(c => {
306                        c /= 255;
307                        return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
308                    });
309                    return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
310                };
311                
312                // Helper: Determine if text should be white or black
313                const getContrastTextColor = (hex) => {
314                    const rgb = hexToRgb(hex);
315                    if (!rgb) return '#ffffff';
316                    const luminance = getLuminance(rgb.r, rgb.g, rgb.b);
317                    return luminance > 0.4 ? '#1e293b' : '#ffffff';
318                };
319                
320                // Apply colors with extensive logging
321                if (appearance.primaryColor) {
322                    console.log('🎨 Applying primary color:', appearance.primaryColor);
323                    const primaryHex = appearance.primaryColor;
324                    const rgb = hexToRgb(primaryHex);
325                    const hsl = rgb ? rgbToHsl(rgb.r, rgb.g, rgb.b) : null;
326                    
327                    // Set base primary color
328                    root.style.setProperty('--niafam-chat-primary', primaryHex);
329                    root.style.setProperty('--sapphire-500', primaryHex);
330                    root.style.setProperty('--sapphire-600', primaryHex);
331                    
332                    // Generate derived colors using HSL
333                    if (hsl) {
334                        // Lighter variant (+15% lightness)
335                        const lightL = Math.min(95, hsl.l + 15);
336                        root.style.setProperty('--niafam-chat-primary-light', `hsl(${hsl.h}, ${hsl.s}%, ${lightL}%)`);
337                        root.style.setProperty('--sapphire-400', `hsl(${hsl.h}, ${hsl.s}%, ${lightL}%)`);
338                        
339                        // Darker variant (-15% lightness)
340                        const darkL = Math.max(10, hsl.l - 15);
341                        root.style.setProperty('--niafam-chat-primary-dark', `hsl(${hsl.h}, ${hsl.s}%, ${darkL}%)`);
342                        root.style.setProperty('--sapphire-700', `hsl(${hsl.h}, ${hsl.s}%, ${darkL}%)`);
343                        
344                        // Very light variant for backgrounds (+40% lightness)
345                        const veryLightL = Math.min(97, hsl.l + 40);
346                        root.style.setProperty('--niafam-chat-surface-accent', `hsl(${hsl.h}, ${Math.max(10, hsl.s - 30)}%, ${veryLightL}%)`);
347                        root.style.setProperty('--sapphire-50', `hsl(${hsl.h}, ${Math.max(10, hsl.s - 30)}%, ${veryLightL}%)`);
348                        
349                        // Transparent variant for overlays
350                        root.style.setProperty('--niafam-chat-primary-transparent', `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, 0.9)`);
351                        
352                        // Text color based on contrast
353                        const textOnPrimary = getContrastTextColor(primaryHex);
354                        root.style.setProperty('--niafam-chat-text-on-primary', textOnPrimary);
355                        
356                        console.log('🎨 Generated colors - Light:', lightL + '%', 'Dark:', darkL + '%', 'Text:', textOnPrimary);
357                    }
358                    
359                    // Check if it was actually applied
360                    const appliedColor = getComputedStyle(root).getPropertyValue('--niafam-chat-primary');
361                    console.log('✅ Primary color check - Set:', primaryHex, 'Got:', appliedColor.trim());
362                    
363                    // Apply to common elements
364                    const additionalProperties = [
365                        '--button-bg-color',
366                        '--link-color', 
367                        '--accent-primary',
368                        '--theme-primary'
369                    ];
370                    
371                    additionalProperties.forEach(prop => {
372                        root.style.setProperty(prop, primaryHex);
373                    });
374                }
375                
376                if (appearance.accentColor) {
377                    console.log('🎨 Applying accent color:', appearance.accentColor);
378                    const accentHex = appearance.accentColor;
379                    const rgb = hexToRgb(accentHex);
380                    const hsl = rgb ? rgbToHsl(rgb.r, rgb.g, rgb.b) : null;
381                    
382                    root.style.setProperty('--accent-color', accentHex);
383                    root.style.setProperty('--flame-500', accentHex);
384                    root.style.setProperty('--niafam-chat-secondary', accentHex);
385                    
386                    // Generate derived accent colors
387                    if (hsl) {
388                        const lightL = Math.min(95, hsl.l + 15);
389                        root.style.setProperty('--accent-color-light', `hsl(${hsl.h}, ${hsl.s}%, ${lightL}%)`);
390                        root.style.setProperty('--flame-400', `hsl(${hsl.h}, ${hsl.s}%, ${lightL}%)`);
391                        
392                        const darkL = Math.max(10, hsl.l - 15);
393                        root.style.setProperty('--accent-color-dark', `hsl(${hsl.h}, ${hsl.s}%, ${darkL}%)`);
394                        root.style.setProperty('--flame-600', `hsl(${hsl.h}, ${hsl.s}%, ${darkL}%)`);
395                        
396                        // Text color based on contrast
397                        const textOnAccent = getContrastTextColor(accentHex);
398                        root.style.setProperty('--niafam-chat-text-on-accent', textOnAccent);
399                    }
400                    
401                    const appliedAccent = getComputedStyle(root).getPropertyValue('--accent-color');
402                    console.log('✅ Accent color check - Set:', accentHex, 'Got:', appliedAccent.trim());
403                    
404                    const accentProperties = [
405                        '--highlight-color',
406                        '--accent-secondary'
407                    ];
408                    
409                    accentProperties.forEach(prop => {
410                        root.style.setProperty(prop, accentHex);
411                    });
412                }
413                
414                // Apply border radius
415                if (appearance.borderRadius) {
416                    const radiusMap = {
417                        'sharp': '0px',
418                        'rounded': '8px',
419                        'pill': '20px'
420                    };
421                    const radius = radiusMap[appearance.borderRadius] || '8px';
422                    root.style.setProperty('--border-radius', radius);
423                    console.log('📐 Border radius applied:', radius);
424                }
425                
426                // Apply animation level as an independent body-class axis.
427                // enhanced = full modern base motion; normal/minimal progressively
428                // trim decorative + entrance motion (see animation-levels.css).
429                const animLevelClasses = ['anim-level-minimal', 'anim-level-normal', 'anim-level-enhanced'];
430                document.body.classList.remove(...animLevelClasses);
431                const animLevel = appearance.animations || 'normal';
432                document.body.classList.add('anim-level-' + animLevel);
433                console.log('⚡ Animation level applied:', animLevel);
434
435                // Apply chat style
436                // Modern is the base look (existing widget styles); classic/minimal are
437                // opt-in overrides applied as a body class. No class => modern preserved.
438                const chatStyleClasses = ['chat-style-classic', 'chat-style-minimal'];
439                document.body.classList.remove(...chatStyleClasses);
440                if (appearance.chatStyle && appearance.chatStyle !== 'modern') {
441                    document.body.classList.add('chat-style-' + appearance.chatStyle);
442                    console.log('💬 Chat style applied:', appearance.chatStyle);
443                }
444            },
445
446            /**
447             * Apply content settings
448             */
449            applyContent() {
450                const content = this.config.chatExperience;
451                if (!content) {
452                    console.log('⏭️ No content configuration found');
453                    return;
454                }
455                
456                console.log('📝 Content configuration found:', content);
457                
458                // Store quick replies globally
459                if (content.quickReplies) {
460                    window.CONFIG_QUICK_REPLIES = content.quickReplies;
461                    console.log('💬 Quick replies stored:', content.quickReplies);
462                }
463                
464                // Store bot personality
465                if (content.botPersonality) {
466                    window.CONFIG_BOT_PERSONALITY = content.botPersonality;
467                    console.log('🤖 Bot personality stored:', content.botPersonality);
468                }
469            }
470        };
471        
472        console.log('✅ configLoader created with full functionality');
473    }
474
475    /**
476     * تبدیل CHAT_BOX_CONFIG به فرمت chatInterfaceConfig
477     * برای سازگاری با chat-loader.js
478     */
479    function convertConfigFormat() {
480        if (!window.CHAT_BOX_CONFIG) {
481            console.warn('⚠️ CHAT_BOX_CONFIG not found');
482            return;
483        }
484
485        console.log('🔄 Converting CHAT_BOX_CONFIG to chatInterfaceConfig format...');
486        console.log('📋 Raw CHAT_BOX_CONFIG:', window.CHAT_BOX_CONFIG);
487
488        const config = window.CHAT_BOX_CONFIG;
489        
490        // Log specific sections
491        console.log('🎨 Visual config:', config.visual);
492        console.log('💬 Chat config:', config.chat);
493        console.log('🏢 General config:', config.general);
494
495        // ایجاد chatInterfaceConfig با فرمت مورد نیاز
496        window.chatInterfaceConfig = [{
497            tenantId: window.TENANT_ID || 'default-tenant',
498            configVersion: '1.0.0',
499            lastUpdated: new Date().toISOString(),
500            isActive: true,
501            configuration: {
502                // Company Branding - خواندن از companyBranding یا general
503                companyBranding: {
504                    companyName: config.companyBranding?.companyName || config.general?.companyName || 'شرکت نیافام',
505                    botName: config.companyBranding?.botName || config.general?.botName || 'دستیار هوشمند',
506                    welcomeMessage: config.companyBranding?.welcomeMessage || config.general?.welcomeMessage || 'سلام! چطور می‌تونم کمکتون کنم؟',
507                    logoUrl: config.companyBranding?.logoUrl || config.general?.companyLogo || '',
508                    supportPhone: config.general?.supportPhone || '',
509                    supportEmail: config.general?.supportEmail || ''
510                },
511
512                // Visual Appearance
513                visualAppearance: {
514                    primaryColor: (() => {
515                        const color = config.chat?.visualAppearance?.primaryColor || '#1976d2';
516                        console.log('🎨 Mapping primaryColor:', color, 'from:', config.chat?.visualAppearance?.primaryColor);
517                        return color;
518                    })(),
519                    accentColor: (() => {
520                        const color = config.chat?.visualAppearance?.accentColor || '#ff5722';
521                        console.log('🎨 Mapping accentColor:', color, 'from:', config.chat?.visualAppearance?.accentColor);
522                        return color;
523                    })(),
524                    chatStyle: config.chat?.visualAppearance?.chatStyle || 'modern',
525                    borderRadius: (() => {
526                        const radius = config.chat?.visualAppearance?.borderRadius || 'rounded';
527                        console.log('📐 Mapping borderRadius:', radius, 'from:', config.chat?.visualAppearance?.borderRadius);
528                        return radius;
529                    })(),
530                    animations: config.chat?.visualAppearance?.animations || 'normal'
531                },
532
533                // Chat Experience
534                chatExperience: {
535                    botPersonality: config.chat?.experience?.botPersonality || 'friendly',
536                    enableFileUpload: config.chat?.fileUpload?.enabled !== false,
537                    showTypingIndicator: config.chat?.experience?.showTypingIndicator !== false,
538                    autoGreeting: true
539                },
540
541                // Voice Recording
542                voiceRecording: config.voiceRecording || {
543                    enabled: true,
544                    maxDuration: 120,
545                    audioFormat: 'webm'
546                },
547
548                // Support Information
549                supportInformation: {
550                    supportPhone: config.general?.supportPhone || '',
551                    supportEmail: config.general?.supportEmail || '',
552                    workingHours: config.chat?.experience?.workingHours || '',
553                    responseTime: 'کمتر از ۲ دقیقه'
554                },
555
556                // Features Toggle
557                featuresToggle: {
558                    enableWelcomeScreen: config.welcomeScreen?.header ? true : false,
559                    enableFAQ: config.welcomeScreen?.options?.faq?.enabled !== false,
560                    enableDarkMode: true,
561                    enableNotifications: true,
562                    collectUserInfo: true,
563                    enableSoundEffects: config.chat?.soundEffects?.enabled !== false
564                },
565
566                // Welcome Screen
567                welcomeScreen: {
568                    header: config.welcomeScreen?.header || {
569                        title: 'نیافام چت',
570                        subtitle: 'دستیار هوشمند شما',
571                        icon: 'bot'
572                    },
573                    options: {
574                        // گفتگوی آنلاین (Live Chat)
575                        liveChat: {
576                            enabled: config.serverSettings?.onlineChatEnabled || false,
577                            title: config.welcomeScreen?.options?.liveChat?.title || 'گفتگوی آنلاین',
578                            description: config.welcomeScreen?.options?.liveChat?.description || 'صحبت با اپراتور انسانی',
579                            icon: config.welcomeScreen?.options?.liveChat?.icon || 'message-circle',
580                            chatMode: 'live'
581                        },
582                        // گپ هوشمند (AI Assistant)
583                        aiAssistant: {
584                            enabled: config.serverSettings?.aiAssistantEnabled || false,
585                            title: config.welcomeScreen?.options?.aiAssistant?.title || 'گپ هوشمند',
586                            description: config.welcomeScreen?.options?.aiAssistant?.description || 'پاسخ سریع با هوش مصنوعی',
587                            icon: config.welcomeScreen?.options?.aiAssistant?.icon || 'bot',
588                            chatMode: 'ai'
589                        },
590                        // دانشنامه
591                        faq: {
592                            enabled: config.welcomeScreen?.options?.faq?.enabled !== false,
593                            title: config.welcomeScreen?.options?.faq?.title || 'دانشنامه',
594                            description: config.welcomeScreen?.options?.faq?.description || 'مشاهده پاسخ سریع به سوالات رایج',
595                            icon: config.welcomeScreen?.options?.faq?.icon || 'help-circle'
596                        }
597                    },
598                    footer: {
599                        links: config.welcomeScreen?.footer?.links || []
600                    },
601                    chatGreeting: config.welcomeScreen?.chatGreeting || {
602                        withName: 'سلام {name} عزیز! ',
603                        withoutName: 'سلام! چطور می‌تونم کمکتون کنم؟'
604                    }
605                },
606
607                // Sound Effects
608                soundEffects: {
609                    enabled: config.chat?.soundEffects?.enabled !== false,
610                    volume: {
611                        notification: (config.chat?.soundEffects?.notificationVolume || 60) / 100,
612                        sendReceive: (config.chat?.soundEffects?.sendReceiveVolume || 50) / 100
613                    },
614                    files: {
615                        notification: '/assets/soundEffects/new-notification.mp3',
616                        sendReceive: '/assets/soundEffects/send-receive-notification.mp3'
617                    },
618                    playOn: {
619                        chatOpen: config.chat?.soundEffects?.soundOnChatOpen !== false,
620                        messageSend: config.chat?.soundEffects?.soundOnMessageSend !== false,
621                        messageReceive: config.chat?.soundEffects?.soundOnMessageReceive !== false,
622                        backgroundNotification: config.chat?.soundEffects?.soundOnBackgroun
622d !== false
623                    }
624                },
625
626                // Speech Bubble
627                speechBubble: config.visual?.speechBubble || {
628                    enabled: true,
629                    animation: 'float',
630                    duration: 10000,
631                    rotationInterval: 3500,
632                    messages: [
633                        'سلام! چطور می‌تونم کمکتون کنم؟',
634                        'سوالی دارید؟ بپرسید!',
635                        'من اینجام برای کمک!'
636                    ],
637                    showOnPageLoad: true,
638                    showOnNewMessage: true
639                },
640
641                // Client Authentication
642                clientAuthentication: config.clientAuthentication || {
643                    modes: {
644                        guestEnabled: true,
645                        authenticationEnabled: true,
646                        forceAuthentication: false
647                    },
648                    guest: {
649                        enabled: true,
650                        requireName: false,
651                        allowedFeatures: {
652                            sendMessage: true,
653                            uploadFile: true,
654                            voiceRecord: true
655                        }
656                    }
657                }
658            }
659        }];
660
661        // Store globally for other components (compatibility with config-loader.js)
662        window.LOADED_CONFIG = window.chatInterfaceConfig[0].configuration;
663        window.TENANT_ID = window.chatInterfaceConfig[0].tenantId;
664
665        console.log('✅ Config converted successfully');
666        console.log('📊 chatInterfaceConfig:', window.chatInterfaceConfig);
667        console.log('🔍 Final visualAppearance:', window.chatInterfaceConfig[0].configuration.visualAppearance);
668        console.log('🔍 Final companyBranding:', window.chatInterfaceConfig[0].configuration.companyBranding);
669        console.log('🔍 Final welcomeScreen:', window.chatInterfaceConfig[0].configuration.welcomeScreen);
670        console.log('🌍 LOADED_CONFIG set:', window.LOADED_CONFIG);
671        console.log('🏢 TENANT_ID set:', window.TENANT_ID);
672    }
673
674    /**
675     * Helper: Load CSS file dynamically
676     */
677    function loadStylesheet(url) {
678        return new Promise((resolve, reject) => {
679            const link = document.createElement('link');
680            link.rel = 'stylesheet';
681            link.href = url;
682            link.onload = () => {
683                console.log(`✅ CSS loaded: ${url}`);
684                resolve();
685            };
686            link.onerror = () => {
687                console.error(`❌ CSS failed: ${url}`);
688                reject(new Error(`Failed to load CSS: ${url}`));
689            };
690            document.head.appendChild(link);
691        });
692    }
693
694    /**
695     * Helper: Load multiple CSS files
696     */
697    function loadStylesheets(urls) {
698        console.log(`📦 Loading ${urls.length} CSS files...`);
699        return Promise.all(urls.map(url => loadStylesheet(url)));
700    }
701
702    /**
703     * Helper: Load JavaScript file dynamically
704     */
705    function loadScript(url) {
706        return new Promise((resolve, reject) => {
707            const script = document.createElement('script');
708            script.src = url;
709            script.onload = () => {
710                console.log(`✅ JS loaded: ${url}`);
711                resolve();
712            };
713            script.onerror = () => {
714                console.error(`❌ JS failed: ${url}`);
715                reject(new Error(`Failed to load script: ${url}`));
716            };
717            document.body.appendChild(script);
718        });
719    }
720
721    /**
722     * Helper: Load multiple JavaScript files sequentially
723     */
724    function loadScripts(urls) {
725        console.log(`📦 Loading ${urls.length} JS files...`);
726        return urls.reduce((promise, url) => {
727            return promise.then(() => loadScript(url));
728        }, Promise.resolve());
729    }
730
731    /**
732     * Show loading indicator
733     */
734    function showLoadingIndicator() {
735        const chatToggle = document.getElementById('chatToggle');
736        if (!chatToggle) return;
737        
738        chatToggle.classList.add('loading');
739        chatToggle.disabled = true;
740        
741        const icon = chatToggle.querySelector('svg');
742        if (icon) {
743            icon.style.display = 'none';
744        }
745        
746        const spinner = document.createElement('div');
747        spinner.className = 'chat-toggle-spinner';
748        spinner.innerHTML = `
749            <svg class="spinner" viewBox="0 0 50 50">
750                <circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"></circle>
751            </svg>
752        `;
753        chatToggle.appendChild(spinner);
754        
755        console.log('⏳ Loading chat resources...');
756    }
757
758    /**
759     * Hide loading indicator
760     */
761    function hideLoadingIndicator() {
762        const chatToggle = document.getElementById('chatToggle');
763        if (!chatToggle) return;
764        
765        chatToggle.classList.remove('loading');
766        chatToggle.disabled = false;
767        
768        const icon = chatToggle.querySelector('svg');
769        if (icon) {
770            icon.style.display = 'block';
771        }
772        
773        const spinner = chatToggle.querySelector('.chat-toggle-spinner');
774        if (spinner) {
775            spinner.remove();
776        }
777        
778        console.log('✅ Loading complete');
779    }
780
781    /**
782     * Show error message
783     */
784    function showErrorMessage(error) {
785        hideLoadingIndicator();
786        
787        const errorDiv = document.createElement('div');
788        errorDiv.style.cssText = `
789            position: fixed;
790            top: 50%;
791            left: 50%;
792            transform: translate(-50%, -50%);
793            background: white;
794            padding: 24px;
795            border-radius: 12px;
796            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
797            z-index: 99999;
798            direction: rtl;
799            text-align: center;
800            max-width: 400px;
801        `;
802        
803        errorDiv.innerHTML = `
804            <p style="color: #dc2626; font-weight: bold; margin-bottom: 12px;">❌ خطا در بارگذاری چت</p>
805            <p style="font-size: 0.9rem; color: #666; margin-bottom: 16px;">
806                ${error.message || 'لطفاً دوباره تلاش کنید'}
807            </p>
808            <button type="button" onclick="this.closest('div').remove()" 
809                    style="padding: 8px 16px; background: #2563eb; color: white; border: none; border-radius: 6px; cursor: pointer;">
810                بستن
811            </button>
812        `;
813        
814        document.body.appendChild(errorDiv);
815        
816        setTimeout(() => {
817            if (errorDiv.parentNode) {
818                errorDiv.remove();
819            }
820        }, 5000);
821    }
822
823    /**
824     * Main: Load all chat resources
825     */
826    async function loadChatResources() {
827        if (chatLoaded) {
828            console.log('✅ Chat already loaded');
829            openChatInterface();
830            return true;
831        }
832
833        if (chatLoading) {
834            console.log('⏳ Chat is already loading...');
835            return false;
836        }
837
838        chatLoading = true;
839        loadStartTime = performance.now();
840        
841        console.log('🚀 Starting BlueTailApp chat resources load...');
842        showLoadingIndicator();
843
844        try {
845            // Step 0: Convert config format
846            console.log('📝 Step 0/6: Converting config format...');
847            convertConfigFormat();
848            
849            // Create configLoader for compatibility
850            createConfigLoader();
851            
852            // Apply configuration immediately
853            if (window.configLoader && window.configLoader.applyConfiguration) {
854                console.log('🎨 Applying configuration to UI...');
855                window.configLoader.applyConfiguration();
856            }
857
858            // Step 1: Verify HTML elements exist
859            console.log('📝 Step 1/6: Verifying HTML elements...');
860            const chatContainer = document.getElementById('chatContainer');
861            const chatToggle = document.getElementById('chatToggle');
862            const chatSpeechBubble = document.getElementById('chatSpeechBubble');
863            
864            if (!chatContainer || !chatToggle || !chatSpeechBubble) {
865                throw new Error('Required HTML elements not found in page');
866            }
867            console.log('✅ HTML elements verified');
868
869            // Step 2: Load HTML template into chatContainer
870            console.log('📝 Step 2/6: Loading chat template...');
871            const templateHTML = `
872        <div class="search-container" id="searchContainer" style="display: none">
873            <input type="text" id="searchInput" class="search-input" placeholder="جستجو در پیام‌ها..."
874                autocomplete="off" />
875        </div>
876
877        <!-- Saved Messages Panel -->
878        <div class="saved-messages-panel" id="savedMessagesPanel" style="display: none">
879            <div class="saved-messages-header">
880                <div class="chat-header-info">
881                    <div class="avatar"><i data-lucide="bookmark"></i></div>
882                    <h3>پیام‌های ذخیره شده</h3>
883                </div>
884                <button type="button" id="closeSavedPanel" class="close-saved-btn" title="بستن">
885                    <i data-lucide="x"></i>
886                </button>
887            </div>
888
889            <div class="saved-messages-search">
890                <div class="panel-search-wrapper">
891                    <i data-lucide="search" class="panel-search-icon"></i>
892                    <input type="text" id="savedMessagesSearchInput" class="saved-search-input"
893                        placeholder="جستجو در پیام‌های ذخیره شده..." autocomplete="off" />
894                </div>
895            </div>
896
897            <div class="saved-messages-info">
898                <p><i data-lucide="info"></i> پیام‌ها تا ۳۰ روز ذخیره می‌شوند</p>
899            </div>
900
901            <div class="saved-messages-content" id="savedMessagesContent">
902                <div class="saved-messages-list" id="savedMessagesList">
903                    <!-- Saved messages will be dynamically populated here -->
904                </div>
905
906                <div class="saved-messages-empty" id="savedMessagesEmpty">
907                    <div class="empty-state">
908                        <i data-lucide="bookmark"></i>
909                        <h4>هیچ پیامی ذخیره نشده</h4>
910                        <p>پیام‌های مهم بات را با کلیک روی دکمه "ذخیره" نگه دارید</p>
911                    </div>
912                </div>
913            </div>
914
915            <div class="saved-messages-actions">
916                <button type="button" id="exportSavedMessages" class="saved-action-btn secondary">
917                    <i data-lucide="copy"></i>
918                    <span>خروجی</span>
919                </button>
920                <button type="button" id="clearAllSavedMessages" class="saved-action-btn danger">
921                    <i data-lucide="x"></i>
922                    <span>پاک کردن همه</span>
923                </button>
924            </div>
925        </div>
926
927        <!-- Search Results Panel -->
928        <div class="search-results-panel" id="searchResultsPanel" style="display: none">
929            <div class="search-results-header">
930                <div class="chat-header-info">
931                    <div class="avatar"><i data-lucide="search"></i></div>
932                    <h3>جستجو در پیام‌ها</h3>
933                </div>
934                <button type="button" id="closeSearchPanel" class="close-search-btn" title="بستن">
935                    <i data-lucide="x"></i>
936                </button>
937            </div>
938
939            <div class="search-panel-input">
940                <div class="panel-search-wrapper">
941                    <i data-lucide="search" class="panel-search-icon"></i>
942                    <input type="text" id="searchPanelInput" class="search-panel-field"
943                        placeholder="عبارت مورد نظر را وارد کنید..." autocomplete="off" />
944                    <button type="button" id="clearSearchBtn" class="clear-search-btn" title="پاک کردن">
945                        <i data-lucide="x"></i>
946                    </button>
947                </div>
948            </div>
949
950            <div class="search-results-info">
951                <p id="searchResultsCount"></p>
952            </div>
953
954            <div class="search-results-content" id="searchResultsContent">
955                <div class="search-results-list" id="searchResultsList">
956                    <!-- Search results will be dynamically populated here -->
957                </div>
958
959                <div class="search-results-empty" id="searchResultsEmpty">
960                    <div class="empty-state">
961                        <i data-lucide="search"></i>
962                        <h4>نتیجه‌ای یافت نشد</h4>
963                        <p>عبارت دیگری را جستجو کنید</p>
964                    </div>
965                </div>
966            </div>
967        </div>
968
969        <!-- Chat Header -->
970        <div class="chat-header">
971            <div class="chat-header-info">
972                <div class="avatar">
973                    <i data-lucide="bot"></i>
974                </div>
975                <div class="chat-title">
976                    <h3>دستیار نیافام</h3>
977                    <span class="status online">آنلاین</span>
978                </div>
979            </div>
980            <div class="chat-controls">
981                <!-- Secondary controls live in the ⋯ overflow menu; only ⋯ and close stay visible.
982                     Button ids are unchanged so all existing listeners keep working. -->
983                <div class="header-menu">
984                    <button type="button" id="headerMenuToggle" class="control-btn" title="گزینه‌ها"
985                        aria-haspopup="true" aria-expanded="false">
986                        <i data-lucide="more-horizontal"></i>
987                    </button>
988                    <div class="header-menu-panel" id="headerMenuPanel" role="menu">
989                        <button type="button" id="soundToggle" class="header-menu-item" title="صدا" role="menuitem">
990                            <i data-lucide="headphones"></i>
991                            <span class="menu-item-label">صدا</span>
992                        </button>
993                        <button type="button" id="searchToggle" class="header-menu-item" title="جستجو" role="menuitem">
994                            <i data-lucide="search"></i>
995                            <span class="menu-item-label">جستجو در گفتگو</span>
996                            <span class="search-count" id="searchCount">0</span>
997                        </button>
998                        <button type="button" id="savedMessagesToggle" class="header-menu-item" title="پیام‌های ذخیره شده" role="menuitem">
999                            <i data-lucide="bookmark"></i>
1000                            <span class="menu-item-label">پیام‌های ذخیره‌شده</span>
1001                            <span class="saved-count" id="savedCount">0</span>
1002                        </button>
1003                        <button type="button" id="minimizeChat" class="header-menu-item" title="کوچک کردن" role="menuitem">
1004                            <i data-lucide="minus"></i>
1005                            <span class="menu-item-label">کوچک کردن</span>
1006                        </button>
1007                    </div>
1008                </div>
1009                <button type="button" id="closeChat" class="control-btn close-chat-btn" title="بستن چت">
1010                    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1011                        <line x1="18" y1="6" x2="6" y2="18"></line>
1012                        <line x1="6" y1="6" x2="18" y2="18"></line>
1013                    </svg>
1014                </button>
1015            </div>
1016        </div>
1017
1018        <!-- Chat Messages -->
1019        <div id="chatMessages" class="chat-messages">
1020            <!-- Messages will be dynamically generated by JavaScript -->
1021            <!-- Typing Indicator will be added dynamically after messages -->
1022        </div>
1023
1024        <!-- Suggested Replies Container -->
1025        <div id="suggestedReplies" class="suggested-replies">
1026            <!-- Dynamically generated suggested replies will appear here -->
1027        </div>
1028
1029        <!-- Chat Input -->
1030        <div class="chat-input">
1031            <!-- Smart Input Container — one row, actions grouped by intent:
1032                 [attach · voice]  …field…  [emoji · send].
1033                 Ids are unchanged so all existing listeners keep working. -->
1034            <div class="smart-input-container">
1035                <!-- Lead cluster: bring content in (attach / voice), plus clear -->
1036                <div class="contextual-actions" id="contextualActions">
1037                    <div class="secondary-actions">
1038                        <button type="button" id="attachmentBtn" class="contextual-action-btn" title="ضمیمه فایل"
1039                            aria-label="ضمیمه کردن فایل">
1040                            <i data-lucide="paperclip"></i>
1041                        </button>
1042                        <!-- Hidden file input -->
1043                        <input type="file" id="fileInput" style="display: none;" multiple accept="" />
1044                        <button type="button" id="voiceBtn" class="contextual-action-btn" title="ضبط صوتی" aria-label="ضبط صوتی">
1045                            <i data-lucide="mic"></i>
1046                        </button>
1047                        <button type="button" id="clearBtn" class="contextual-action-btn clear-action" title="پاک کردن"
1048                            aria-label="پاک کردن متن" style="display: none;">
1049                            <i data-lucide="x"></i>
1050                        </button>
1051                    </div>
1052                </div>
1053
1054                <!-- Typing Area -->
1055                <div class="typing-area">
1056                    <textarea id="messageInput" placeholder="پیام خود را بنویسید..."
1057                        autocomplete="off" rows="1" maxlength="2000" role="textbox" aria-label="نوشتن پیام"
1058                        aria-describedby="charCounter"></textarea>
1059                </div>
1060
1061                <!-- Send cluster: emoji sits next to send (Telegram-style) -->
1062                <div class="primary-action">
1063                    <button type="button" id="emojiBtn" class="contextual-action-btn" title="ایموجی" aria-label="انتخاب ایموجی">
1064                        <i data-lucide="smile"></i>
1065                    </button>
1066                    <button type="button" id="sendButton" class="send-button" disabled aria-label="ارسال پیام">
1067                        <i data-lucide="send"></i>
1068                    </button>
1069                </div>
1070
1071                <!-- Character Counter — floats above the row, visible only when it has a state -->
1072                <div class="char-counter-container">
1073                    <div class="char-counter" id="charCounter" aria-live="polite" aria-atomic="true">0/2000</div>
1074                </div>
1075            </div>
1076
1077
1078            <!-- Voice Recording Overlay -->
1079            <div class="voice-recording-overlay" id="voiceRecordingOverlay" style="display: none">
1080                <div class="recording-content">
1081                    <!-- Compact Recording Area -->
1082                    <div class="recording-compact">
1083                        <div class="recording-info">
1084                            <i data-lucide="mic" class="recording-mic-icon"></i>
1085                            <span class="recording-time">0:00</span>
1086                        </div>
1087
1088                        <canvas id="audioVisualizer" width="180" height="50"></canvas>
1089
1090                        <div class="recording-actions">
1091                            <button type="button" class="record-btn cancel-btn" id="cancelRecording" title="لغو ضبط">
1092                                <i data-lucide="x"></i>
1093                            </button>
1094                            <button type="button" class="record-btn send-btn" id="stopRecording" title="ارسال پیام صوتی">
1095                                <i data-lucide="send"></i>
1096                            </button>
1097                        </div>
1098                    </div>
1099                </div>
1100            </div>
1101        </div>
1102            `;
1103            chatContainer.innerHTML = templateHTML;
1104            console.log('✅ Chat template loaded');
1105            
1106            // Apply branding after template is loaded (botName, etc.)
1107            if (window.configLoader && window.configLoader.applyBranding) {
1108                console.log('🏷️ Applying branding after template load...');
1109                window.configLoader.applyBranding();
1110            }
1111
1112            // Step 3: Load CSS files
1113            console.log('📝 Step 3/6: Loading CSS files...');
1114            await loadStylesheets([
1115                '/assets/css/bluetailapp/styles.css',
1116                '/assets/css/bluetailapp/welcome-styles.css',
1117                '/assets/css/bluetailapp/user-info-styles.css',
1118                '/assets/css/bluetailapp/faq-styles.css',
1119                '/assets/css/bluetailapp/file-upload.css',
1120                '/assets/css/bluetailapp/user-config-overrides.css',
1121                '/assets/css/bluetailapp/animation-levels.css'
1122            ]);
1123
1124            // Step 4: Verify configuration data
1125            console.log('📝 Step 4/6: Verifying configuration data...');
1126            
1127            if (!window.chatInterfaceConfig) {
1128                console.error('❌ chatInterfaceConfig not found!');
1129                throw new Error('chatInterfaceConfig is required');
1130            }
1131            
1132            console.log('✅ Configuration data available');
1133            console.log('   - chatInterfaceConfig: ✓ Converted from CHAT_BOX_CONFIG');
1134
1135            // Note: FAQ data is no longer pre-injected on window.faqData. The
1136            // nested FAQ system fetches its own tree from the server on demand
1137            // (see docs/api/NESTED-FAQ-API.md).
1138
1139            // Step 5: Load icons (only if not already loaded)
1140            console.log('📝 Step 5/6: Loading icons...');
1141            if (!window.CHAT_ICONS) {
1142                await loadScript('/assets/js/bluetailapp/icons.js');
1143            } else {
1144                console.log('ℹ️ Icons already loaded, skipping');
1145            }
1146
1147            // Step 6: Load chat scripts (in order, only if not already loaded)
1148            console.log('📝 Step 6/6: Loading chat scripts...');
1149            const scriptsToLoad = [];
1150            
1151            if (!window.WelcomeScreen) scriptsToLoad.push('/assets/js/bluetailapp/welcome.js');
1152            if (!window.FAQManager) scriptsToLoad.push('/assets/js/bluetailapp/faq.js');
1153            if (!window.SimpleChatBot) scriptsToLoad.push('/assets/js/bluetailapp/ai-chatbot.js');
1154            if (!window.SimpleOnlineChat) scriptsToLoad.push('/assets/js/bluetailapp/simple-online-chat.js');
1155            if (!window.UserInfoForm) scriptsToLoad.push('/assets/js/bluetailapp/user-info.js');
1156            if (!window.WelcomeContainer) scriptsToLoad.push('/assets/js/bluetailapp/welcome-container.js');
1157            if (!window.FileUploadManager) scriptsToLoad.push('/assets/js/bluetailapp/file-upload.js');
1158            if (!window.NiafamChatBot) scriptsToLoad.push('/assets/js/bluetailapp/script.js');
1159            
1160            if (scriptsToLoad.length > 0) {
1161                await loadScripts(scriptsToLoad);
1162            } else {
1163                console.log('ℹ️ All scripts already loaded, skipping');
1164            }
1165
1166            // Step 7: Initialize chat
1167            console.log('📝 Step 7/6: Initializing chat...');
1168            
1169            if (typeof NiafamChatBot !== 'undefined') {
1170                window.niafamChat = new NiafamChatBot();
1171                window.niafamChat.loadTheme();
1172                window.niafamChat.loadSoundPreference();
1173                console.log('✅ NiafamChatBot initialized');
1174            } else {
1175                console.warn('⚠️ NiafamChatBot class not found');
1176            }
1177
1178            const loadTime = performance.now() - loadStartTime;
1179            console.log(`✅ Chat loaded successfully in ${loadTime.toFixed(0)}ms`);
1180            
1181            chatLoaded = true;
1182            chatLoading = false;
1183            hideLoadingIndicator();
1184            
1185            // باز کردن چت فقط اگر کاربر روی chatToggle کلیک کرده باشد
1186            if (userClickedToggle) {
1187                openChatInterface();
1188                userClickedToggle = false; // reset flag
1189            }
1190            
1191            return true;
1192
1193        } catch (error) {
1194            console.error('❌ Error loading chat resources:', error);
1195            chatLoading = false;
1196            showErrorMessage(error);
1197            return false;
1198        }
1199    }
1200
1201    /**
1202     * Open chat interface
1203     */
1204    function openChatInterface() {
1205        // کاربر چت را باز کرد → پیام حباب به مقصد رسیده؛ فلگ ماندگار ست می‌شود تا
1206        // حباب دیگر در بازدیدهای بعدی نمایش داده نشود. (این کلید در پاک‌سازیِ
1207        // هنگام refresh حذف نمی‌شود، پس بین بازدیدها می‌مانَد.)
1208        try {
1209            localStorage.setItem('niafamchat_chat_opened', '1');
1210        } catch (e) {
1211            // ignore
1212        }
1213        // اگر حباب همین حالا در حال نمایش است، مخفی‌اش کن (display:none بر انیمیشن غالب است)
1214        const openBubble = document.getElementById('chatSpeechBubble');
1215        if (openBubble) {
1216            openBubble.classList.remove('show');
1217            openBubble.style.display = 'none';
1218        }
1219
1220        const chatContainer = document.getElementById('chatContainer');
1221        if (chatContainer) {
1222            chatContainer.style.display = '';
1223            
1224            setTimeout(() => {
1225                chatContainer.classList.add('active');
1226                console.log('✅ Chat interface opened');
1227                
1228                if (window.niafamChat) {
1229                    window.niafamChat.isOpen = true;
1230                }
1231            }, 50);
1232        }
1233        
1234        const chatToggle = document.getElementById('chatToggle');
1235        if (chatToggle) {
1236            chatToggle.classList.add('active');
1237        }
1238        
1239        setTimeout(() => {
1240            if (window.welcomeScreen && typeof window.welcomeScreen.show === 'function') {
1241                window.welcomeScreen.show();
1242                console.log('✅ Welcome screen displayed');
1243            }
1244            
1245            if (window.lucide && window.lucide.createIcons) {
1246                window.lucide.createIcons();
1247                console.log('✅ Icons initialized in chat');
1248            }
1249        }, 150);
1250    }
1251
1252    /**
1253     * Initialize Speech Bubble
1254     * خواندن تنظیمات از CHAT_BOX_CONFIG
1255     * با قابلیت محدود کردن تعداد نمایش (پیش‌فرض: 2 بار)
1256     */
1257    function initSpeechBubble() {
1258        // ========== Storage Keys ==========
1259        // localStorage: شمارش بین بازدیدها (بین‌سشنی) — تا پاک‌شدن دستی می‌مانَد.
1260        // sessionStorage: پرچم «در همین بازدید نمایش داده شد» — با بستن تب پاک می‌شود
1261        //   و باعث می‌شود در یک بازدید (حتی با چند بار لود/جابه‌جایی صفحه) حباب فقط
1262        //   یک‌بار نمایش داده و فقط یک‌بار شمرده شود.
1263        const STORAGE_KEY = 'niafamchat_speech_bubble';
1264        const SESSION_KEY = 'niafamchat_speech_bubble_session';
1265
1266        // ========== تشخیص موبایل ==========
1267        // در موبایل حباب باید کم‌مزاحمت‌تر باشد: بدون چرخش، سقف کمتر و مدت کوتاه‌تر.
1268        const isMobile = () => {
1269            try {
1270                return window.matchMedia('(pointer: coarse)').matches || window.innerWidth < 768;
1271            } catch (e) {
1272                return window.innerWidth < 768;
1273            }
1274        };
1275        const MOBILE = isMobile();
1276
1277        const MAX_VISITS = MOBILE ? 1 : 3; // موبایل: ۱ بازدید، دسکتاپ: ۳ (گام بعد: از config)
1278        const COOLDOWN_DAYS = 7; // پس از رسیدن به سقف، این تعداد روز بعد شمارنده ریست می‌شود (گام بعد: از config)
1279        const MOBILE_DURATION = 5000; // مدت نمایش در موبایل (کوتاه‌تر از دسکتاپ)
1280
1281        /**
1282         * آیا در همین بازدید (tab session) قبلاً نمایش داده‌ایم؟
1283         */
1284        const shownThisVisit = () => {
1285            try {
1286                return sessionStorage.getItem(SESSION_KEY) === '1';
1287            } catch (e) {
1288                return false;
1289            }
1290        };
1291
1292        /**
1293         * خواندن دادهٔ شمارش بازدید از localStorage
1294         */
1295        const getVisitData = () => {
1296            try {
1297                const data = localStorage.getItem(STORAGE_KEY);
1298                if (data) return JSON.parse(data);
1299            } catch (e) {
1300                console.warn('⚠️ Error reading speech bubble data from localStorage:', e);
1301            }
1302            return { visitCount: 0, lastShown: null, version: '2.0' };
1303        };
1304
1305        const getVisitCount = () => getVisitData().visitCount || 0;
1306
1307        /**
1308         * آیا از آخرین نمایش بیشتر از COOLDOWN_DAYS گذشته است؟
1309         * (برای ریست چرخهٔ نمایش تا کاربران برنگشته دوباره فرصت دیدن داشته باشند)
1310         */
1311        const isCooldownElapsed = (lastShownIso) => {
1312            if (!lastShownIso) return false;
1313            const last = new Date(lastShownIso).getTime();
1314            if (isNaN(last)) return false;
1315            const days = (Date.now() - last) / (1000 * 60 * 60 * 24);
1316            return days >= COOLDOWN_DAYS;
1317        };
1318
1319        /**
1320         * ریست شمارندهٔ بازدیدها (lastShown حفظ می‌شود و در registerShow بعدی به‌روز می‌شود)
1321         */
1322        const resetVisitCount = () => {
1323            try {
1324                const data = getVisitData();
1325                data.visitCount = 0;
1326                localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1327            } catch (e) {
1328                console.warn('⚠️ Error resetting speech bubble count:', e);
1329            }
1330        };
1331
1332        /**
1333         * ثبت نمایش: پرچم این بازدید را ست کن و فقط اگر بازدید جدیدی است،
1334         * شمارندهٔ بازدیدها را یک واحد زیاد کن.
1335         */
1336        const registerShow = () => {
1337            try {
1338                const isNewVisit = !shownThisVisit();
1339                sessionStorage.setItem(SESSION_KEY, '1');
1340                if (isNewVisit) {
1341                    const data = getVisitData();
1342                    data.visitCount = (data.visitCount || 0) + 1;
1343                    data.lastShown = new Date().toISOString();
1344                    data.version = '2.0';
1345                    localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
1346                    console.log(`📊 Speech bubble shown — visit ${data.visitCount}/${MAX_VISITS}`);
1347                }
1348            } catch (e) {
1349                console.warn('⚠️ Error saving speech bubble data to localStorage:', e);
1350            }
1351        };
1352
1353        /**
1354         * بررسی اینکه آیا باید speech bubble نمایش داده شود
1355         */
1356        const shouldShowBubble = () => {
1357            // کاربر قبلاً چت را باز کرده؟ (پیام رسیده) → دیگر حباب لازم نیست
1358            try {
1359                if (localStorage.getItem('niafamchat_chat_opened') === '1') {
1360                    console.log('ℹ️ User already opened chat before — speech bubble suppressed');
1361                    return false;
1362                }
1363            } catch (e) {
1364                // ignore
1365            }
1366            // در همین بازدید قبلاً نمایش داده شده؟
1367            if (shownThisVisit()) {
1368                console.log('ℹ️ Speech bubble already shown this visit, skipping');
1369                return false;
1370            }
1371            // به سقف تعداد بازدیدها رسیده؟
1372            const data = getVisitData();
1373            const visitCount = data.visitCount || 0;
1374            if (visitCount >= MAX_VISITS) {
1375                // اگر cooldown سپری شده، چرخه را از نو شروع کن
1376                if (isCooldownElapsed(data.lastShown)) {
1377                    console.log(`♻️ Speech bubble cooldown (${COOLDOWN_DAYS}d) elapsed — resetting visit count`);
1378                    resetVisitCount();
1379                    return true;
1380                }
1381                console.log(`ℹ️ Speech bubble reached max visits (${visitCount}/${MAX_VISITS}), skipping`);
1382                return false;
1383            }
1384            return true;
1385        };
1386
1387        const getSpeechBubbleConfig = () => {
1388            if (!window.CHAT_BOX_CONFIG || !window.CHAT_BOX_CONFIG.visual) {
1389                console.warn('⚠️ CHAT_BOX_CONFIG.visual not found, using defaults');
1390                return {
1391                    enabled: true,
1392                    animation: 'float',
1393                    messages: [
1394                        'سلام! چطور می‌تونم کمکتون Ú©Ù
1394†Ù…ØŸ',
1395                        'سوالی دارید؟ بپرسید!',
1396                        'من اینجام برای کمک!'
1397                    ],
1398                    duration: 10000,
1399                    rotationInterval: 3500
1400                };
1401            }
1402
1403            const config = window.CHAT_BOX_CONFIG.visual.speechBubble;
1404            return {
1405                enabled: config?.enabled !== false,
1406                animation: config?.animation || 'float',
1407                messages: config?.messages || [
1408                    'سلام! چطور می‌تونم کمکتون کنم؟',
1409                    'سوالی دارید؟ بپرسید!',
1410                    'من اینجام برای کمک!'
1411                ],
1412                duration: config?.duration || 10000,
1413                rotationInterval: config?.rotation || config?.rotationInterval || 3500
1414            };
1415        };
1416
1417        let currentIndex = 0;
1418        let rotationIntervalId = null;
1419
1420        function showSpeechBubble() {
1421            const config = getSpeechBubbleConfig();
1422
1423            // بررسی فعال بودن در config
1424            if (!config.enabled) {
1425                console.log('ℹ️ Speech bubble is disabled in config');
1426                return;
1427            }
1428
1429            // بررسی تعداد دفعات نمایش قبلی
1430            if (!shouldShowBubble()) {
1431                return;
1432            }
1433
1434            const bubble = document.getElementById('chatSpeechBubble');
1435            const textElement = bubble?.querySelector('.speech-bubble-text');
1436
1437            if (!bubble || !textElement) {
1438                console.warn('⚠️ Speech bubble elements not found');
1439                return;
1440            }
1441
1442            const messages = config.messages;
1443
1444            // اعمال کلاس انیمیشن
1445            const animClasses = ['anim-float', 'anim-bounce', 'anim-pulse', 'anim-swing',
1446                'anim-wobble', 'anim-fade', 'anim-slide', 'anim-none'];
1447            animClasses.forEach(cls => bubble.classList.remove(cls));
1448
1449            if (config.animation && config.animation !== 'none') {
1450                bubble.classList.add('anim-' + config.animation);
1451                console.log('🎬 Speech bubble animation applied:', config.animation);
1452            }
1453
1454            // Show first message
1455            textElement.textContent = messages[currentIndex];
1456            bubble.classList.add('show');
1457
1458            // ثبت نمایش (پرچم بازدید + شمارش بین‌بازدیدی)
1459            registerShow();
1460
1461            // چرخش پیام‌ها فقط در دسکتاپ؛ در موبایل یک پیام ثابت (کم‌مزاحمت، بدون حرکت)
1462            if (!MOBILE) {
1463                rotationIntervalId = setInterval(() => {
1464                    bubble.classList.remove('show');
1465                    setTimeout(() => {
1466                        currentIndex = (currentIndex + 1) % messages.length;
1467                        textElement.textContent = messages[currentIndex];
1468                        bubble.classList.add('show');
1469                    }, 400);
1470                }, config.rotationInterval);
1471            }
1472
1473            // مخفی‌شدن پس از مدت مشخص و پاکسازی (موبایل کوتاه‌تر)
1474            const hideAfter = MOBILE ? MOBILE_DURATION : config.duration;
1475            setTimeout(() => {
1476                bubble.classList.remove('show');
1477                // پاکسازی interval برای جلوگیری از memory leak
1478                if (rotationIntervalId) {
1479                    clearInterval(rotationIntervalId);
1480                    rotationIntervalId = null;
1481                }
1482            }, hideAfter);
1483        }
1484
1485        // آیا یک input/textarea/select/contenteditable فوکوس دارد؟
1486        // (کیبورد مجازی موبایل احتمالاً باز است → نمایش حباب مزاحم است)
1487        const isInputFocused = () => {
1488            const el = document.activeElement;
1489            if (!el) return false;
1490            const tag = el.tagName;
1491            return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || el.isContentEditable === true;
1492        };
1493
1494        // ========== زمان‌بندی نمایش ==========
1495        if (!MOBILE) {
1496            // دسکتاپ: کمی بعد از لود صفحه نمایش بده
1497            setTimeout(showSpeechBubble, 800);
1498        } else {
1499            // موبایل: فقط وقتی کاربر بی‌کار است نمایش بده — اسکرول متوقف شده و
1500            // کیبورد بسته باشد — تا وسط اسکرول یا هنگام تایپ پاپ نشود.
1501            const SCROLL_IDLE_MS = 1200;
1502            let idleTimer = null;
1503            let shownAttempted = false;
1504
1505            const cleanup = () => {
1506                window.removeEventListener('scroll', onScroll);
1507                if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; }
1508            };
1509
1510            const tryShow = () => {
1511                if (shownAttempted) return;
1512                // کیبورد باز/در حال تایپ؟ کمی بعد دوباره امتحان کن
1513                if (isInputFocused()) {
1514                    idleTimer = setTimeout(tryShow, SCROLL_IDLE_MS);
1515                    return;
1516                }
1517                shownAttempted = true;
1518                cleanup();
1519                showSpeechBubble();
1520            };
1521
1522            const onScroll = () => {
1523                if (idleTimer) clearTimeout(idleTimer);
1524                idleTimer = setTimeout(tryShow, SCROLL_IDLE_MS);
1525            };
1526
1527            window.addEventListener('scroll', onScroll, { passive: true });
1528            // اگر کاربر اصلاً اسکرول نکرد، پس از یک تأخیر اولیه امتحان کن
1529            idleTimer = setTimeout(tryShow, 1500);
1530        }
1531    }
1532
1533    /**
1534     * Inject Chat Styles
1535     * اضافه کردن CSS های دکمه chat و speech bubble
1536     */
1537    function injectChatStyles() {
1538        // Check if styles already injected
1539        if (document.getElementById('bluetailapp-chat-styles')) {
1540            return;
1541        }
1542
1543        const style = document.createElement('style');
1544        style.id = 'bluetailapp-chat-styles';
1545        style.textContent = `
1546            /* Chat Toggle Button */
1547            .chat-toggle {
1548                position: fixed;
1549                bottom: calc(20px + env(safe-area-inset-bottom, 0px));
1550                inset-inline-start: 20px;
1551                width: 60px;
1552                height: 60px;
1553                border-radius: 50%;
1554                background: #1976d2;
1555                border: none;
1556                cursor: pointer;
1557                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1558                display: flex;
1559                align-items: center;
1560                justify-content: center;
1561                transition: all 0.3s ease;
1562                z-index: 9998;
1563            }
1564
1565            .chat-toggle:hover {
1566                transform: scale(1.1);
1567                box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
1568            }
1569
1570            .chat-toggle svg {
1571                color: inherit; /* از رنگ parent (chatToggle) ارث‌بری می‌کند */
1572                width: 28px;
1573                height: 28px;
1574            }
1575
1576            .chat-badge {
1577                position: absolute;
1578                top: -4px;
1579                right: -4px;
1580                background: #ef4444;
1581                color: white;
1582                border-radius: 50%;
1583                width: 24px;
1584                height: 24px;
1585                display: flex;
1586                align-items: center;
1587                justify-content: center;
1588                font-size: 12px;
1589                font-weight: 600;
1590                box-shadow: 0 2px 8px rgba(239, 68, 68, 0.4);
1591            }
1592
1593            /* Loading Spinner */
1594            .chat-toggle.loading {
1595                pointer-events: none;
1596            }
1597
1598            .chat-toggle-spinner {
1599                position: absolute;
1600                inset: 0;
1601                display: flex;
1602                align-items: center;
1603                justify-content: center;
1604            }
1605
1606            .chat-toggle-spinner .spinner {
1607                width: 30px;
1608                height: 30px;
1609                animation: rotate 1s linear infinite;
1610            }
1611
1612            .chat-toggle-spinner .path {
1613                stroke: white;
1614                stroke-linecap: round;
1615                animation: dash 1.5s ease-in-out infinite;
1616            }
1617
1618            @keyframes rotate {
1619                100% { transform: rotate(360deg); }
1620            }
1621
1622            @keyframes dash {
1623                0% {
1624                    stroke-dasharray: 1, 150;
1625                    stroke-dashoffset: 0;
1626                }
1627                50% {
1628                    stroke-dasharray: 90, 150;
1629                    stroke-dashoffset: -35;
1630                }
1631                100% {
1632                    stroke-dasharray: 90, 150;
1633                    stroke-dashoffset: -124;
1634                }
1635            }
1636
1637            /* Speech Bubble */
1638            .chat-speech-bubble {
1639                position: fixed;
1640                bottom: calc(100px + env(safe-area-inset-bottom, 0px));
1641                inset-inline-start: 24px;
1642                background: #1976d2;
1643                color: white;
1644                padding: 12px 16px;
1645                border-radius: 12px;
1646                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1647                max-width: max-content;
1648                opacity: 0;
1649                transform: translateY(10px);
1650                transition: all 0.3s ease;
1651                pointer-events: none;
1652                z-index: 9997;
1653                direction: rtl;
1654            }
1655
1656            .chat-speech-bubble.show {
1657                opacity: 1;
1658                transform: translateY(0);
1659            }
1660
1661            .speech-bubble-arrow {
1662                position: absolute;
1663                bottom: -8px;
1664                right: 24px;
1665                width: 0;
1666                height: 0;
1667                border-left: 8px solid transparent;
1668                border-right: 8px solid transparent;
1669                border-top: 8px solid #1976d2;
1670            }
1671
1672            /* Speech Bubble Animations */
1673            @keyframes float {
1674                0%, 100% { transform: translateY(0); }
1675                50% { transform: translateY(-10px); }
1676            }
1677
1678            @keyframes bounce {
1679                0%, 100% { transform: translateY(0); }
1680                25% { transform: translateY(-8px); }
1681                50% { transform: translateY(0); }
1682                75% { transform: translateY(-4px); }
1683            }
1684
1685            @keyframes pulseScale {
1686                0%, 100% { transform: scale(1); }
1687                50% { transform: scale(1.05); }
1688            }
1689
1690            @keyframes swing {
1691                0%, 100% { transform: rotate(0deg); }
1692                25% { transform: rotate(3deg); }
1693                75% { transform: rotate(-3deg); }
1694            }
1695
1696            @keyframes wobble {
1697                0%, 100% { transform: translateX(0); }
1698                15% { transform: translateX(-5px) rotate(-2deg); }
1699                30% { transform: translateX(4px) rotate(1deg); }
1700                45% { transform: translateX(-3px) rotate(-1deg); }
1701                60% { transform: translateX(2px) rotate(0.5deg); }
1702                75% { transform: translateX(-1px) rotate(-0.5deg); }
1703            }
1704
1705            @keyframes fadePulse {
1706                0%, 100% { opacity: 1; }
1707                50% { opacity: 0.7; }
1708            }
1709
1710            @keyframes slideBounce {
1711                0%, 100% { transform: translateX(0); }
1712                25% { transform: translateX(5px); }
1713                75% { transform: translateX(-5px); }
1714            }
1715
1716            .chat-speech-bubble.anim-float {
1717                animation: float 3s ease-in-out infinite;
1718            }
1719
1720            .chat-speech-bubble.anim-bounce {
1721                animation: bounce 2s ease-in-out infinite;
1722            }
1723
1724            .chat-speech-bubble.anim-pulse {
1725                animation: pulseScale 2s ease-in-out infinite;
1726            }
1727
1728            .chat-speech-bubble.anim-swing {
1729                animation: swing 2.5s ease-in-out infinite;
1730                transform-origin: bottom left;
1731            }
1732
1733            .chat-speech-bubble.anim-wobble {
1734                animation: wobble 2s ease-in-out infinite;
1735            }
1736
1737            .chat-speech-bubble.anim-fade {
1738                animation: fadePulse 2s ease-in-out infinite;
1739            }
1740
1741            .chat-speech-bubble.anim-slide {
1742                animation: slideBounce 3s ease-in-out infinite;
1743            }
1744
1745            .chat-speech-bubble.anim-none {
1746                animation: none;
1747            }
1748
1749            /* Chat Container */
1750            .chat-container {
1751                display: none;
1752            }
1753
1754            /* Responsive */
1755            @media (max-width: 768px) {
1756                .chat-toggle {
1757                    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
1758                    inset-inline-start: 16px;
1759                    width: 56px;
1760                    height: 56px;
1761                }
1762
1763                .chat-speech-bubble {
1764                    bottom: calc(88px + env(safe-area-inset-bottom, 0px));
1765                    inset-inline-start: 16px;
1766                    max-width: 200px;
1767                }
1768            }
1769
1770            /* احترام به تنظیم کاهش حرکت کاربر: انیمیشن مداوم حباب حذف می‌شود */
1771            @media (prefers-reduced-motion: reduce) {
1772                .chat-speech-bubble {
1773                    animation: none !important;
1774                    transition: opacity 0.2s ease;
1775                }
1776            }
1777        `;
1778
1779        document.head.appendChild(style);
1780        console.log('✅ Chat styles injected');
1781    }
1782    
1783    /**
1784     * دریافت SVG آیکن برای chatToggle بر اساس نوع آیکن
1785     * @param {string} iconType - نوع آیکن: bot, message-circle, headphones, smile, zap
1786     * @returns {string} SVG string
1787     */
1788    function getChatToggleIcon(iconType) {
1789        const icons = {
1790            'bot': `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1791                <path d="M12 8V4H8"/>
1792                <rect width="16" height="12" x="4" y="8" rx="2"/>
1793                <path d="M2 14h2"/>
1794                <path d="M20 14h2"/>
1795                <path d="M15 13v2"/>
1796                <path d="M9 13v2"/>
1797            </svg>`,
1798            'message-circle': `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1799                <path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>
1800            </svg>`,
1801            'headphones': `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1802                <path d="M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3"/>
1803            </svg>`,
1804            'smile': `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1805                <circle cx="12" cy="12" r="10"/>
1806                <path d="M8 14s1.5 2 4 2 4-2 4-2"/>
1807                <line x1="9" x2="9.01" y1="9" y2="9"/>
1808                <line x1="15" x2="15.01" y1="9" y2="9"/>
1809            </svg>`,
1810            'zap': `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1811                <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
1812            </svg>`
1813        };
1814        
1815        const icon = icons[iconType] || icons['message-circle'];
1816        console.log('🎨 Chat toggle icon set to:', iconType);
1817        return icon;
1818    }
1819
1820    // ========================================
1821    // 🎨 توابع کمکی رنگ (Global Scope)
1822    // ========================================
1823    
1824    /**
1825     * تبدیل Hex به RGB
1826     */
1827    function hexToRgbGlobal(hex) {
1828        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1829        return result ? {
1830            r: parseInt(result[1], 16),
1831            g: parseInt(result[2], 16),
1832            b: parseInt(result[3], 16)
1833        } : null;
1834    }
1835    
1836    /**
1837     * تبدیل RGB به HSL
1838     */
1839    function rgbToHslGlobal(r, g, b) {
1840        r /= 255; g /= 255; b /= 255;
1841        const max = Math.max(r, g, b), min = Math.min(r, g, b);
1842        let h, s, l = (max + min) / 2;
1843        
1844        if (max === min) {
1845            h = s = 0;
1846        } else {
1847            const d = max - min;
1848            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1849            switch (max) {
1850                case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
1851                case g: h = ((b - r) / d + 2) / 6; break;
1852                case b: h = ((r - g) / d + 4) / 6; break;
1853            }
1854        }
1855        return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
1856    }
1857    
1858    /**
1859     * محاسبه Luminance برای تشخیص کنتراست
1860     */
1861    function getLuminanceGlobal(r, g, b) {
1862        const [rs, gs, bs] = [r, g, b].map(c => {
1863            c /= 255;
1864            return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
1865        });
1866        return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
1867    }
1868    
1869    /**
1870     * تنظیم روشنایی رنگ
1871     */
1872    function adjustColor(hex, percent) {
1873        const rgb = hexToRgbGlobal(hex);
1874        if (!rgb) return hex;
1875        
1876        const adjust = (value) => Math.max(0, Math.min(255, value + (percent * 2.55)));
1877        const r = Math.round(adjust(rgb.r));
1878        const g = Math.round(adjust(rgb.g));
1879        const b = Math.round(adjust(rgb.b));
1880        
1881        return `rgb(${r}, ${g}, ${b})`;
1882    }
1883
1884    /**
1885     * 🎨 اعمال رنگ‌های اولیه از CHAT_BOX_CONFIG
1886     * این تابع رنگ‌ها را قبل از لود شدن استایل‌های اصلی اعمال می‌کند
1887     */
1888    function applyInitialColors(chatToggle, speechBubble) {
1889        // بررسی مسیرهای ممکن برای تنظیمات رنگ
1890        // 1. chat.visualAppearance (ساختار اصلی سرور)
1891        // 2. visualAppearance (fallback)
1892        // 3. visual (fallback قدیمی)
1893        const visual = window.CHAT_BOX_CONFIG?.chat?.visualAppearance 
1894                    || window.CHAT_BOX_CONFIG?.visualAppearance 
1895                    || window.CHAT_BOX_CONFIG?.visual;
1896        
1897        console.log('🔍 Looking for visual config:', {
1898            'chat.visualAppearance': window.CHAT_BOX_CONFIG?.chat?.visualAppearance,
1899            'visualAppearance': window.CHAT_BOX_CONFIG?.visualAppearance,
1900            'visual': window.CHAT_BOX_CONFIG?.visual,
1901            'selected': visual
1902        });
1903        
1904        if (!visual || !visual.primaryColor) {
1905            console.log('ℹ️ No visual config found, using default colors');
1906            return;
1907        }
1908
1909        const primaryColor = visual.primaryColor;
1910        const accentColor = visual.accentColor || '#f97316';
1911
1912        console.log('🎨 Applying initial colors:', { primaryColor, accentColor });
1913
1914        // محاسبه رنگ‌های کنتراست
1915        const primaryRgb = hexToRgbGlobal(primaryColor);
1916        const accentRgb = hexToRgbGlobal(accentColor);
1917        
1918        // رنگ آیکن chatToggle بر اساس کنتراست با پس‌زمینه
1919        let iconColor = '#ffffff';
1920        if (primaryRgb) {
1921            const primaryLuminance = getLuminanceGlobal(primaryRgb.r, primaryRgb.g, primaryRgb.b);
1922            iconColor = primaryLuminance > 0.4 ? '#1e293b' : '#ffffff';
1923        }
1924        
1925        // رنگ متن بر اساس کنتراست با accentColor
1926        let accentTextColor = '#ffffff';
1927        if (accentRgb) {
1928            const accentLuminance = getLuminanceGlobal(accentRgb.r, accentRgb.g, accentRgb.b);
1929            accentTextColor = accentLuminance > 0.4 ? '#1e293b' : '#ffffff';
1930        }
1931
1932        // اعمال رنگ پس‌زمینه به chatToggle
1933        chatToggle.style.background = `linear-gradient(135deg, ${primaryColor}
1933 0%, ${adjustColor(primaryColor, -20)} 100%)`;
1934        chatToggle.style.color = iconColor; // رنگ آیکن
1935
1936        // اعمال رنگ به badge
1937        const badge = chatToggle.querySelector('.chat-badge');
1938        if (badge) {
1939            badge.style.background = accentColor;
1940            badge.style.color = accentTextColor;
1941        }
1942
1943        // 🎨 اعمال رنگ به Speech Bubble (از accentColor)
1944        if (speechBubble) {
1945            speechBubble.style.background = accentColor;
1946            speechBubble.style.color = accentTextColor;
1947            
1948            // رنگ arrow هم باید تغییر کند
1949            const arrow = speechBubble.querySelector('.speech-bubble-arrow');
1950            if (arrow) {
1951                arrow.style.borderTopColor = accentColor;
1952            }
1953        }
1954
1955        // تنظیم CSS variables برای استفاده در سایر المان‌ها
1956        document.documentElement.style.setProperty('--niafam-chat-primary', primaryColor);
1957        document.documentElement.style.setProperty('--accent-color', accentColor);
1958        document.documentElement.style.setProperty('--niafam-chat-text-on-primary', iconColor);
1959        document.documentElement.style.setProperty('--niafam-chat-text-on-accent', accentTextColor);
1960        
1961        // تولید رنگ‌های مشتق از primary
1962        if (primaryRgb) {
1963            const hsl = rgbToHslGlobal(primaryRgb.r, primaryRgb.g, primaryRgb.b);
1964            
1965            // Primary variants
1966            document.documentElement.style.setProperty('--niafam-chat-primary-light', 
1967                `hsl(${hsl.h}, ${hsl.s}%, ${Math.min(hsl.l + 15, 95)}%)`);
1968            document.documentElement.style.setProperty('--niafam-chat-primary-dark', 
1969                `hsl(${hsl.h}, ${hsl.s}%, ${Math.max(hsl.l - 15, 10)}%)`);
1970            document.documentElement.style.setProperty('--niafam-chat-primary-transparent', 
1971                `rgba(${primaryRgb.r}, ${primaryRgb.g}, ${primaryRgb.b}, 0.9)`);
1972            document.documentElement.style.setProperty('--niafam-chat-surface-accent', 
1973                `hsl(${hsl.h}, ${hsl.s}%, ${Math.min(hsl.l + 40, 97)}%)`);
1974        }
1975
1976        console.log('✅ Initial colors applied:', { primaryColor, accentColor, iconColor, accentTextColor });
1977    }
1978
1979    /**
1980     * Create Chat UI Elements
1981     * ایجاد دکمه chat، speech bubble و chat container
1982     */
1983    function createChatUI() {
1984        console.log('🎨 Creating chat UI elements...');
1985
1986        // Check if elements already exist
1987        if (document.getElementById('chatToggle')) {
1988            console.log('ℹ️ Chat UI elements already exist');
1989            return;
1990        }
1991
1992        // Inject styles first
1993        injectChatStyles();
1994
1995        // Create Chat Toggle Button
1996        const chatToggle = document.createElement('button');
1997        chatToggle.id = 'chatToggle';
1998        chatToggle.className = 'chat-toggle';
1999        chatToggle.setAttribute('aria-label', 'باز کردن چت');
2000        
2001        // خواندن آیکن از config
2002        const welcomeIcon = window.CHAT_BOX_CONFIG?.welcomeScreen?.header?.icon || 'message-circle';
2003        const iconSvg = getChatToggleIcon(welcomeIcon);
2004        
2005        chatToggle.innerHTML = `
2006            ${iconSvg}
2007            <span class="chat-badge" id="chatBadge">1</span>
2008        `;
2009
2010        // Create Speech Bubble
2011        const speechBubble = document.createElement('div');
2012        speechBubble.id = 'chatSpeechBubble';
2013        speechBubble.className = 'chat-speech-bubble';
2014        speechBubble.innerHTML = `
2015            <div class="speech-bubble-content">
2016                <span class="speech-bubble-text"></span>
2017            </div>
2018            <div class="speech-bubble-arrow"></div>
2019        `;
2020        
2021        // 🎨 اعمال رنگ‌ها از CHAT_BOX_CONFIG همان ابتدا (بعد از ساخت speechBubble)
2022        applyInitialColors(chatToggle, speechBubble);
2023
2024        // Create Chat Container
2025        const chatContainer = document.createElement('div');
2026        chatContainer.id = 'chatContainer';
2027        chatContainer.className = 'chat-container';
2028
2029        // Append to body
2030        document.body.appendChild(chatToggle);
2031        document.body.appendChild(speechBubble);
2032        document.body.appendChild(chatContainer);
2033
2034        console.log('✅ Chat UI elements created');
2035    }
2036
2037    /**
2038     * Check if chat should be displayed
2039     * بررسی اینکه آیا chat باید نمایش داده شود
2040     * 
2041     * منطق جدید:
2042     * - اگر هر دو لایسنس (chatLicense و chatbot_license) غیرفعال باشند → chatToggle نمایش داده نمی‌شود
2043     * - اگر حداقل یکی از گزینه‌ها (onlineChatEnabled یا aiAssistantEnabled) فعال باشد → chatToggle نمایش داده می‌شود
2044     */
2045    function shouldDisplayChat() {
2046        // Check URL parameters
2047        const urlParams = new URLSearchParams(window.location.search);
2048        const showchaticon = urlParams.get('showchaticon');
2049        
2050        // If showchaticon is explicitly set to 0, don't show chat
2051        if (showchaticon === '0') {
2052            console.log('ℹ️ Chat is disabled (showchaticon=0)');
2053            return false;
2054        }
2055        
2056        // Check CHAT_BOX_CONFIG if available
2057        if (window.CHAT_BOX_CONFIG) {
2058            const serverSettings = window.CHAT_BOX_CONFIG.serverSettings;
2059            
2060            // بررسی فعال بودن حداقل یکی از گزینه‌ها
2061            if (serverSettings) {
2062                const onlineChatEnabled = serverSettings.onlineChatEnabled === true;
2063                const aiAssistantEnabled = serverSettings.aiAssistantEnabled === true;
2064                
2065                // اگر هر دو غیرفعال باشند، chatToggle نمایش داده نمی‌شود
2066                if (!onlineChatEnabled && !aiAssistantEnabled) {
2067                    console.log('ℹ️ Chat is disabled - both onlineChatEnabled and aiAssistantEnabled are false');
2068                    return false;
2069                }
2070                
2071                console.log(`✅ Chat enabled - onlineChat: ${onlineChatEnabled}, aiAssistant: ${aiAssistantEnabled}`);
2072            }
2073            
2074            // بررسی chat.enabled (برای backward compatibility)
2075            if (window.CHAT_BOX_CONFIG.chat && window.CHAT_BOX_CONFIG.chat.enabled === false) {
2076                console.log('ℹ️ Chat is disabled in CHAT_BOX_CONFIG.chat.enabled');
2077                return false;
2078            }
2079        }
2080        
2081        return true;
2082    }
2083
2084    /**
2085     * Initialize loader
2086     */
2087    function init() {
2088        console.log('🎯 BlueTailApp Chat Loader ready');
2089
2090        // Check if chat should be displayed
2091        if (!shouldDisplayChat()) {
2092            console.log('⏭️ Chat initialization skipped');
2093            return;
2094        }
2095
2096        // Create chat UI elements
2097        createChatUI();
2098
2099        const chatToggle = document.getElementById('chatToggle');
2100        
2101        if (!chatToggle) {
2102            console.error('❌ Chat toggle button not found!');
2103            return;
2104        }
2105
2106        // Main click event
2107        chatToggle.addEventListener('click', function(e) {
2108            if (!chatLoaded) {
2109                e.preventDefault();
2110                e.stopPropagation();
2111                userClickedToggle = true; // کاربر روی دکمه کلیک کرد
2112                loadChatResources();
2113            }
2114        }, true);
2115
2116        // Initialize speech bubble
2117        initSpeechBubble();
2118
2119        // ❌ حذف prefetch خودکار - چت فقط با کلیک کاربر لود می‌شود
2120        // Optional: Prefetch after idle time
2121        // let idleTimer;
2122        // const resetIdleTimer = () => {
2123        //     clearTimeout(idleTimer);
2124        //     idleTimer = setTimeout(() => {
2125        //         if (!chatLoaded && !chatLoading) {
2126        //             console.log('💤 User idle, prefetching...');
2127        //             loadChatResources();
2128        //         }
2129        //     }, 10000);
2130        // };
2131
2132        // ['mousemove', 'keydown', 'scroll', 'touchstart'].forEach(event => {
2133        //     document.addEventListener(event, resetIdleTimer, { passive: true });
2134        // });
2135
2136        // resetIdleTimer(); // حذف شد
2137
2138        console.log('✅ BlueTailApp Chat Loader initialized successfully');
2139    }
2140
2141    // Initialize when DOM is ready
2142    if (document.readyState === 'loading') {
2143        document.addEventListener('DOMContentLoaded', init);
2144    } else {
2145        init();
2146    }
2147
2148})();

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.