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.