1// Chat Widget v6 - Dynamic Base URL Detection 2// Version: 6 - Automatically detects base URL from widget script source 3(function() { 4 'use strict'; 5 6 // Dynamically detect base URL from where widget.js is loaded 7 const scriptTag = document.currentScript || document.querySelector('script[src*="widget.js"]'); 8 const LOVABLE_BASE_URL = scriptTag ? new URL(scriptTag.src).origin : window.location.origin; 9 10 const SUPABASE_URL = 'https://sleuzjmoofdaxzqtcljc.supabase.co'; 11 const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InNsZXV6am1vb2ZkYXh6cXRjbGpjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjQ5Nzg5NzUsImV4cCI6MjA4MDU1NDk3NX0.qpoc45oXbVr_gzpQ8XG_9_DRrrPLEe7fcqe9JUVsYiI'; 12 13 let widgetId = null; 14 let settings = null; 15 let isOpen = false; 16 let messages = []; 17 let conversationId = null; 18 let visitorId = null; 19 let visitorInfo = null; 20 let hasCompletedForm = false; 21 let teaserShown = false; 22 let teaserTimer = null; 23 let aiAgentInfo = null; 24 let heartbeatInterval = null; 25 let visitorLocation = null; 26 let conversationStatus = 'ai_handling'; 27 let realtimeChannel = null; 28 let typingChannel = null; 29 let isAgentTyping = false; 30 let typingTimeout = null; 31 let messageReactions = {}; // Store reactions by message_id 32 let messageIdMap = {}; // Map message index to actual message IDs 33 34 // Random AI agent name generator with animated avatars 35 const AI_FIRST_NAMES = ['Nova', 'Atlas', 'Zara', 'Orion', 'Luna', 'Echo', 'Kai', 'Aria', 'Neo', 'Sage', 'Phoenix', 'Ember', 'Astra', 'Cipher', 'Volt']; 36 const AI_TITLES = ['AI', 'Bot', 'Assistant', 'Guide', 'Helper', 'Agent']; 37 const AI_AVATARS = ['ð¤', 'ð¦', 'ð±', 'ð¦', 'ð¼', 'ð¦', 'ð¬', 'ð¦', 'ð', 'ð¦', 'ð¨', 'ð¦', 'ð¯', 'ð»', 'ð¦']; 38 const AI_COLORS = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6']; 39 40 function getAiAgentInfo() { 41 if (aiAgentInfo) return aiAgentInfo; 42 43 // Use widgetId + visitorId as seed for consistent avatar per user 44 const seed = (widgetId || '') + (visitorId || '') + Date.now().toString().slice(0, -6); 45 let hash = 0; 46 for (let i = 0; i < seed.length; i++) { 47 hash = ((hash << 5) - hash) + seed.charCodeAt(i); 48 hash = hash & hash; 49 } 50 hash = Math.abs(hash); 51 52 const firstName = AI_FIRST_NAMES[hash % AI_FIRST_NAMES.length]; 53 const title = AI_TITLES[(hash >> 4) % AI_TITLES.length]; 54 const avatar = AI_AVATARS[(hash >> 8) % AI_AVATARS.length]; 55 const color = AI_COLORS[(hash >> 12) % AI_COLORS.length]; 56 57 aiAgentInfo = { 58 name: `${firstName} ${title}`, 59 avatar, 60 color 61 }; 62 63 return aiAgentInfo; 64 } 65 66 // Visitor info storage key 67 function getVisitorInfoKey() { 68 return 'chatbot_visitor_info_' + widgetId; 69 } 70 71 function getVisitorId() { 72 let id = localStorage.getItem('chatbot_visitor_id'); 73 if (!id) { 74 id = 'visitor_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now(); 75 localStorage.setItem('chatbot_visitor_id', id); 76 } 77 return id; 78 } 79 80 function loadVisitorInfo() { 81 try { 82 const stored = localStorage.getItem(getVisitorInfoKey()); 83 if (stored) { 84 visitorInfo = JSON.parse(stored); 85 hasCompletedForm = true; 86 return visitorInfo; 87 } 88 } catch (e) { 89 console.error('[ChatBot] Error loading visitor info:', e); 90 } 91 return null; 92 } 93 94 function saveVisitorInfo(info) { 95 try { 96 visitorInfo = info; 97 hasCompletedForm = true; 98 localStorage.setItem(getVisitorInfoKey(), JSON.stringify(info)); 99 } catch (e) { 100 console.error('[ChatBot] Error saving visitor info:', e); 101 } 102 } 103 104 // Conversation ID storage functions 105 function getConversationIdKey() { 106 return 'chatbot_conversation_id_' + widgetId; 107 } 108 109 function loadConversationId() { 110 try { 111 return localStorage.getItem(getConversationIdKey()); 112 } catch (e) { 113 console.error('[ChatBot] Error loading conversation ID:', e); 114 return null; 115 } 116 } 117 118 function saveConversationId(id) { 119 try { 120 localStorage.setItem(getConversationIdKey(), id); 121 } catch (e) { 122 console.error('[ChatBot] Error saving conversation ID:', e); 123 } 124 } 125 126 127 async function fetchExistingMessages() { 128 if (!conversationId) return []; 129 130 try { 131 const url = new URL('/rest/v1/messages', SUPABASE_URL); 132 url.searchParams.set('conversation_id', 'eq.' + conversationId); 133 url.searchParams.set('select', '*'); 134 url.searchParams.set('order', 'created_at.asc'); 135 136 const response = await fetch(url.toString(), { 137 headers: { 138 'apikey': SUPABASE_ANON_KEY, 139 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY 140 } 141 }); 142 143 const data = await response.json(); 144 console.log('[ChatBot] Fetched existing messages:', data.length); 145 146 // Store message ID mapping 147 data.forEach((msg, index) => { 148 messageIdMap[index] = msg.id; 149 }); 150 151 // Fetch reactions for these messages 152 await fetchMessageReactions(data.map(m => m.id)); 153 154 return data.map(msg => ({ 155 id: msg.id, 156 role: msg.sender_type === 'visitor' ? 'visitor' : 'ai', 157 content: msg.content 158 })); 159 } catch (error) { 160 console.error('[ChatBot] Failed to fetch messages:', error); 161 return []; 162 } 163 } 164 165 async function markMessagesRead() { 166 if (!conversationId || !visitorId) return; 167 try { 168 await fetch(SUPABASE_URL + '/functions/v1/mark-messages-read', { 169 method: 'POST', 170 headers: { 171 'apikey': SUPABASE_ANON_KEY, 172 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY, 173 'Content-Type': 'application/json' 174 }, 175 body: JSON.stringify({ conversationId: conversationId, visitorId: visitorId }) 176 }); 177 } catch (error) { 178 console.error('[ChatBot] Failed to mark messages read:', error); 179 } 180 } 181 182 async function fetchMessageReactions(messageIds) { 183 if (!messageIds || messageIds.length === 0 || !conversationId || !visitorId) return; 184 185 try { 186 const response = await fetch(SUPABASE_URL + '/functions/v1/visitor-reactions', { 187 method: 'POST', 188 headers: { 189 'apikey': SUPABASE_ANON_KEY, 190 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY, 191 'Content-Type': 'application/json' 192 }, 193 body: JSON.stringify({ action: 'list', conversationId: conversationId, visitorId: visitorId }) 194 }); 195 196 const result = await response.json(); 197 const data = (result && result.reactions) || []; 198 199 messageReactions = {}; 200 data.forEach(r => { 201 if (!messageReactions[r.message_id]) { 202 messageReactions[r.message_id] = []; 203 } 204 messageReactions[r.message_id].push(r); 205 }); 206 207 console.log('[ChatBot] Fetched reactions:', Object.keys(messageReactions).length, 'messages have reactions'); 208 } catch (error) { 209 console.error('[ChatBot] Failed to fetch reactions:', error); 210 } 211 } 212 213 async function toggleReaction(messageId, emoji) { 214 if (!messageId || !visitorId || !conversationId) return; 215 216 const existing = (messageReactions[messageId] || []).find( 217 r => r.emoji === emoji && r.reactor_type === 'visitor' && r.reactor_id === visitorId 218 ); 219 220 try { 221 const response = await fetch(SUPABASE_URL + '/functions/v1/visitor-reactions', { 222 method: 'POST', 223 headers: { 224 'apikey': SUPABASE_ANON_KEY, 225 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY, 226 'Content-Type': 'application/json' 227 }, 228 body: JSON.stringify({ 229 action: 'toggle', 230 conversationId: conversationId, 231 visitorId: visitorId, 232 messageId: messageId, 233 emoji: emoji 234 }) 235 }); 236 237 const result = await response.json(); 238 239 if (result && result.removed) { 240 messageReactions[messageId] = (messageReactions[messageId] || []).filter(r => r.id !== result.removed); 241 } else if (result && result.reaction) { 242 if (!messageReactions[messageId]) { 243 messageReactions[messageId] = []; 244 } 245 messageReactions[messageId].push(result.reaction); 246 } else if (existing) { 247 messageReactions[messageId] = (messageReactions[messageId] || []).filter(r => r.id !== existing.id); 248 } 249 250 renderMessages(); 251 } catch (error) { 252 console.error('[ChatBot] Failed to toggle reaction:', error); 253 } 254 } 255 256 async function fetchSettings(id) { 257 try { 258 const url = new URL('/rest/v1/public_widget_config', SUPABASE_URL); 259 url.searchParams.set('id', 'eq.' + id); 260 url.searchParams.set('select', '*'); 261 262 console.log('[ChatBot] Fetching settings from:', url.toString()); 263 264 const response = await fetch(url.toString(), { 265 headers: { 266 'apikey': SUPABASE_ANON_KEY, 267 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY 268 } 269 }); 270 const data = await response.json(); 271 console.log('[ChatBot] Raw API response:', data); 272 return data[0] || null; 273 } catch (error) { 274 console.error('[ChatBot] Failed to fetch widget settings:', error); 275 return null; 276 } 277 } 278 279 // Track page history 280 let pageHistory = []; 281 282 function trackPageVisit() { 283 const currentPage = { 284 url: window.location.href, 285 title: document.title, 286 timestamp: new Date().toISOString() 287 }; 288 289 // Avoid duplicate consecutive entries 290 const lastPage = pageHistory[pageHistory.length - 1]; 291 if (!lastPage || lastPage.url !== currentPage.url) { 292 pageHistory.push(currentPage); 293 // Keep only last 20 pages 294 if (pageHistory.length > 20) { 295 pageHistory = pageHistory.slice(-20); 296 } 297 } 298 } 299 300 function getDeviceInfo() { 301 const ua = navigator.userAgent; 302 let browser = 'Unknown'; 303 let os = 'Unknown'; 304 let device = 'Desktop'; 305 306 // Detect browser 307 if (ua.includes('Firefox')) browser = 'Firefox'; 308 else if (ua.includes('Edg')) browser = 'Edge';
309 else if (ua.includes('Chrome')) browser = 'Chrome'; 310 else if (ua.includes('Safari')) browser = 'Safari'; 311 else if (ua.includes('Opera') || ua.includes('OPR')) browser = 'Opera'; 312 313 // Detect OS 314 if (ua.includes('Windows')) os = 'Windows'; 315 else if (ua.includes('Mac OS')) os = 'macOS'; 316 else if (ua.includes('Linux')) os = 'Linux'; 317 else if (ua.includes('Android')) os = 'Android'; 318 else if (ua.includes('iOS') || ua.includes('iPhone') || ua.includes('iPad')) os = 'iOS'; 319 320 // Detect device type 321 if (/Mobi|Android|iPhone|iPad|iPod/i.test(ua)) { 322 device = /iPad/i.test(ua) ? 'Tablet' : 'Mobile'; 323 } 324 325 return { browser, os, device, userAgent: ua.substring(0, 200) }; 326 } 327 328 async function sendMessage(message) { 329 // Track the current page before sending 330 trackPageVisit(); 331 332 try { 333 const url = new URL('/functions/v1/chat', SUPABASE_URL); 334 335 const response = await fetch(url.toString(), { 336 method: 'POST', 337 headers: { 338 'Content-Type': 'application/json', 339 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY 340 }, 341 body: JSON.stringify({ 342 message: message, 343 conversation_id: conversationId, 344 widget_id: widgetId, 345 visitor_id: visitorId, 346 page_url: window.location.href, 347 device_info: getDeviceInfo(), 348 page_history: pageHistory, 349 visitor_info: visitorInfo 350 }) 351 }); 352 const data = await response.json(); 353 if (data.conversation_id) { 354 conversationId = data.conversation_id; 355 saveConversationId(conversationId); 356 } 357 return { 358 response: data.response || 'Sorry, I encountered an error. Please try again.', 359 quick_replies: data.quick_replies || [] 360 }; 361 } catch (error) { 362 console.error('[ChatBot] Failed to send message:', error); 363 return { 364 response: 'Sorry, I encountered an error. Please try again.', 365 quick_replies: [] 366 }; 367 } 368 } 369 370 // Auto-discovery: collect page data and send to backend 371 async function discoverPageData() { 372 const discoveryKey = 'chatbot_discovered_' + widgetId + '_' + window.location.hostname; 373 const lastDiscovery = localStorage.getItem(discoveryKey); 374 const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000); 375 376 // Only discover once per day per domain 377 if (lastDiscovery && parseInt(lastDiscovery) > oneDayAgo) { 378 console.log('[ChatBot] Discovery already done recently, skipping'); 379 return; 380 } 381 382 try { 383 // Collect current page data 384 const currentPage = { 385 url: window.location.href, 386 title: document.title, 387 description: document.querySelector('meta[name="description"]')?.getAttribute('content') || '', 388 content: extractPageContent(), 389 sitemapUrl: findSitemapUrl() 390 }; 391 392 // Collect internal links 393 const links = Array.from(document.querySelectorAll('a[href]')) 394 .map(a => { 395 try { 396 const url = new URL(a.href, window.location.origin); 397 if (url.hostname === window.location.hostname) { 398 return { url: url.href, title: a.textContent?.trim() || '' }; 399 } 400 } catch (e) {} 401 return null; 402 }) 403 .filter(Boolean) 404 .filter((link, index, self) => 405 self.findIndex(l => l.url === link.url) === index 406 ) 407 .slice(0, 100); 408 409 console.log('[ChatBot] Discovered', links.length, 'internal links'); 410 411 // Send discovery data to backend 412 const response = await fetch(SUPABASE_URL + '/functions/v1/auto-discover', { 413 method: 'POST', 414 headers: { 415 'Content-Type': 'application/json', 416 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY 417 }, 418 body: JSON.stringify({ 419 widgetId: widgetId, 420 domain: window.location.hostname, 421 currentPage: currentPage, 422 pages: links 423 }) 424 }); 425 426 if (response.ok) { 427 localStorage.setItem(discoveryKey, Date.now().toString()); 428 console.log('[ChatBot] Discovery data sent successfully'); 429 } else { 430 console.error('[ChatBot] Discovery failed:', await response.text()); 431 } 432 } catch (error) { 433 console.error('[ChatBot] Discovery error:', error); 434 } 435 } 436 437 // Visitor presence tracking 438 async function trackVisitorPresence() { 439 if (!widgetId || !visitorId) return; 440 441 try { 442 const deviceInfo = getDeviceInfo(); 443 const payload = { 444 visitor_id: visitorId, 445 widget_id: widgetId, 446 current_page_url: window.location.href, 447 page_title: document.title, 448 device_info: deviceInfo, 449 location: visitorLocation || {}, 450 last_seen_at: new Date().toISOString() 451 }; 452 453 const url = new URL('/rest/v1/active_visitors', SUPABASE_URL); 454 455 await fetch(url.toString(), { 456 method: 'POST', 457 headers: { 458 'apikey': SUPABASE_ANON_KEY, 459 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY, 460 'Content-Type': 'application/json', 461 'Prefer': 'return=minimal' 462 }, 463 body: JSON.stringify(payload) 464 }); 465 466 467 console.log('[ChatBot] Visitor presence updated'); 468 } catch (error) { 469 console.error('[ChatBot] Failed to track visitor:', error); 470 } 471 } 472 473 async function fetchVisitorLocation() { 474 try { 475 // Use a free IP geolocation API 476 const response = await fetch('https://ipapi.co/json/'); 477 if (response.ok) { 478 const data = await response.json(); 479 visitorLocation = { 480 country: data.country_name, 481 city: data.city, 482 region: data.region 483 }; 484 } 485 } catch (error) { 486 console.log('[ChatBot] Could not fetch location:', error); 487 } 488 } 489 490 function startHeartbeat() { 491 // Initial presence update 492 trackVisitorPresence(); 493 494 // Update every 30 seconds 495 heartbeatInterval = setInterval(trackVisitorPresence, 30000); 496 497 // Track page changes via visibility 498 document.addEventListener('visibilitychange', () => { 499 if (document.visibilityState === 'visible') { 500 trackVisitorPresence(); 501 } 502 }); 503 504 // Track before unload (best effort) 505 window.addEventListener('beforeunload', () => { 506 // Use sendBeacon for reliability 507 const payload = JSON.stringify({ 508 visitor_id: visitorId, 509 widget_id: widgetId, 510 last_seen_at: new Date(Date.now() - 120000).toISOString() // Mark as 2 min ago = offline 511 }); 512 513 const url = new URL('/rest/v1/active_visitors', SUPABASE_URL); 514 url.searchParams.set('visitor_id', 'eq.' + visitorId); 515 url.searchParams.set('widget_id', 'eq.' + widgetId); 516 517 navigator.sendBeacon && navigator.sendBeacon( 518 url.toString() + '&apikey=' + SUPABASE_ANON_KEY, 519 new Blob([payload], { type: 'application/json' }) 520 ); 521 }); 522 } 523 524 // Subscribe to conversation status changes 525 function subscribeToConversation() { 526 if (!conversationId) return; 527 528 // Simple polling approach for status changes (every 10 seconds) 529 const pollInterval = setInterval(async () => { 530 if (!conversationId) { 531 clearInterval(pollInterval); 532 return; 533 } 534
535 try { 536 const url = new URL('/rest/v1/conversations', SUPABASE_URL); 537 url.searchParams.set('id', 'eq.' + conversationId); 538 url.searchParams.set('select', 'status,resolved_at'); 539 540 const response = await fetch(url.toString(), { 541 headers: { 542 'apikey': SUPABASE_ANON_KEY, 543 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY 544 } 545 }); 546 547 if (isOpen) markMessagesRead(); 548 549 const data = await response.json(); 550 if (data[0]) { 551 const newStatus = data[0].status; 552 if (newStatus !== conversationStatus) { 553 conversationStatus = newStatus; 554 if (newStatus === 'resolved') { 555 showFeedbackOverlay(); 556 } 557 } 558 } 559 } catch (error) { 560 console.error('[ChatBot] Failed to poll conversation status:', error); 561 } 562 }, 10000); 563 } 564 565 // Typing indicator functions 566 function subscribeToTyping() { 567 if (!conversationId || typingChannel) return; 568 569 console.log('[ChatBot] Subscribing to typing channel'); 570 571 // Use Supabase Realtime channel for presence 572 const channelName = 'typing:' + conversationId; 573 574 // Create a simple WebSocket-based approach using broadcast 575 typingChannel = { 576 lastBroadcast: 0 577 }; 578 579 // Poll for agent typing status (simple approach) 580 const typingPollInterval = setInterval(async () => { 581 if (!conversationId) { 582 clearInterval(typingPollInterval); 583 return; 584 } 585 586 try { 587 // Check if there's any recent agent activity (last 5 seconds) 588 const url = new URL('/rest/v1/messages', SUPABASE_URL); 589 url.searchParams.set('conversation_id', 'eq.' + conversationId); 590 url.searchParams.set('sender_type', 'eq.agent'); 591 url.searchParams.set('select', 'created_at'); 592 url.searchParams.set('order', 'created_at.desc'); 593 url.searchParams.set('limit', '1'); 594 595 const response = await fetch(url.toString(), { 596 headers: { 597 'apikey': SUPABASE_ANON_KEY, 598 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY 599 } 600 }); 601 602 const data = await response.json(); 603 if (data[0]) { 604 const lastMessageTime = new Date(data[0].created_at).getTime(); 605 const now = Date.now(); 606 // If last message was within 3 seconds, agent might have just sent 607 if (now - lastMessageTime < 3000 && isAgentTyping) { 608 hideAgentTypingIndicator(); 609 } 610 } 611 } catch (error) { 612 // Silently fail 613 } 614 }, 5000); 615 } 616 617 function broadcastVisitorTyping(isTyping) { 618 // Clear existing timeout 619 if (typingTimeout) { 620 clearTimeout(typingTimeout); 621 } 622 623 // Auto-clear typing after 3 seconds 624 if (isTyping) { 625 typingTimeout = setTimeout(() => { 626 // Typing stopped 627 }, 3000); 628 } 629 630 // Note: Full realtime presence would require Supabase JS client 631 // For now, this is a stub that could be expanded 632 console.log('[ChatBot] Visitor typing:', isTyping); 633 } 634 635 function showAgentTypingIndicator() { 636 if (isAgentTyping) return; 637 isAgentTyping = true; 638 639 const messagesContainer = document.querySelector('.chatbot-messages'); 640 if (!messagesContainer) return; 641 642 // Remove existing typing indicator 643 const existing = messagesContainer.querySelector('.chatbot-typing-indicator'); 644 if (existing) existing.remove(); 645 646 const agent = getAiAgentInfo(); 647 const typingHtml = document.createElement('div'); 648 typingHtml.className = 'chatbot-typing-indicator'; 649 typingHtml.innerHTML = ` 650 <div class="chatbot-message-row ai"> 651 <div class="chatbot-avatar-wrapper"> 652 <div class="chatbot-ai-avatar" style="background: ${agent.color}"> 653 ${agent.avatar} 654 </div> 655 </div> 656 <div class="chatbot-typing-bubble"> 657 <span class="chatbot-typing-dot"></span> 658 <span class="chatbot-typing-dot"></span> 659 <span class="chatbot-typing-dot"></span> 660 </div> 661 </div> 662 `; 663 664 messagesContainer.appendChild(typingHtml); 665 messagesContainer.scrollTop = messagesContainer.scrollHeight; 666 } 667 668 function hideAgentTypingIndicator() { 669 isAgentTyping = false;
670 const indicator = document.querySelector('.chatbot-typing-indicator'); 671 if (indicator) indicator.remove(); 672 } 673 674 // End chat and show feedback 675 async function endChat() { 676 if (!conversationId) return; 677 678 try { 679 const url = new URL('/rest/v1/conversations', SUPABASE_URL); 680 url.searchParams.set('id', 'eq.' + conversationId); 681 682 await fetch(url.toString(), { 683 method: 'PATCH', 684 headers: { 685 'apikey': SUPABASE_ANON_KEY, 686 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY, 687 'Content-Type': 'application/json', 688 'Prefer': 'return=minimal' 689 }, 690 body: JSON.stringify({ 691 status: 'resolved', 692 resolved_at: new Date().toISOString(), 693 resolved_by: 'visitor' 694 }) 695 }); 696 697 conversationStatus = 'resolved'; 698 showFeedbackOverlay(); 699 } catch (error) { 700 console.error('[ChatBot] Failed to end chat:', error); 701 } 702 } 703 704 // Submit feedback 705 async function submitFeedback(rating, comment) { 706 if (!conversationId) return; 707 708 try { 709 const url = new URL('/rest/v1/conversations', SUPABASE_URL); 710 url.searchParams.set('id', 'eq.' + conversationId); 711 712 await fetch(url.toString(), { 713 method: 'PATCH', 714 headers: { 715 'apikey': SUPABASE_ANON_KEY, 716 'Authorization': 'Bearer ' + SUPABASE_ANON_KEY, 717 'Content-Type': 'application/json', 718 'Prefer': 'return=minimal' 719 }, 720 body: JSON.stringify({ 721 feedback_rating: rating, 722 feedback_comment: comment || null 723 }) 724 }); 725 726 console.log('[ChatBot] Feedback submitted'); 727 } catch (error) { 728 console.error('[ChatBot] Failed to submit feedback:', error); 729 } 730 } 731 732 // Show feedback overlay 733 function showFeedbackOverlay() { 734 const chatView = document.querySelector('.chatbot-chat-view'); 735 if (!chatView) return; 736 737 // Remove existing overlay if any 738 const existingOverlay = chatView.querySelector('.chatbot-feedback-overlay'); 739 if (existingOverlay) existingOverlay.remove(); 740 741 const overlay = document.createElement('div'); 742 overlay.className = 'chatbot-feedback-overlay'; 743 overlay.innerHTML = ` 744 <div class="chatbot-feedback-icon"> 745 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> 746 <path d="M20 6L9 17l-5-5"/> 747 </svg> 748 </div> 749 <h3 class="chatbot-feedback-title">Chat Ended</h3> 750 <p class="chatbot-feedback-subtitle">How was your experience?</p> 751 <div class="chatbot-rating-stars"> 752 <button class="chatbot-star" data-rating="1">â </button> 753 <button class="chatbot-star" data-rating="2">â </button> 754 <button class="chatbot-star" data-rating="3">â </button> 755 <button class="chatbot-star" data-rating="4">â </button> 756 <button class="chatbot-star" data-rating="5">â </button> 757 </div> 758 <textarea class="chatbot-feedback-textarea" placeholder="Any additional comments? (optional)" rows="3"></textarea> 759 <div class="chatbot-feedback-actions"> 760 <button class="chatbot-feedback-submit" disabled>Submit Feedback</button> 761 <button class="chatbot-feedback-skip">Skip & Start New Chat</button> 762 </div> 763 `; 764 765 chatView.appendChild(overlay); 766 767 let selectedRating = 0; 768 const stars = overlay.querySelectorAll('.chatbot-star'); 769 const submitBtn = overlay.querySelector('.chatbot-feedback-submit'); 770 const skipBtn = overlay.querySelector('.chatbot-feedback-skip'); 771 const textarea = overlay.querySelector('.chatbot-feedback-textarea'); 772 773 // Star rating 774 stars.forEach(star => { 775 star.addEventListener('click', () => { 776 selectedRating = parseInt(star.dataset.rating); 777 stars.forEach((s, i) => { 778 s.classList.toggle('active', i < selectedRating); 779 }); 780 submitBtn.disabled = false; 781 }); 782 783 star.addEventListener('mouseenter', () => { 784 const rating = parseInt(star.dataset.rating); 785 stars.forEach((s, i) => { 786 s.style.color = i < rating ? '#fbbf24' : '#d1d5db'; 787 }); 788 }); 789 790 star.addEventListener('mouseleave', () => {
791 stars.forEach((s, i) => { 792 s.style.color = i < selectedRating ? '#fbbf24' : '#d1d5db'; 793 }); 794 }); 795 }); 796 797 // Submit feedback 798 submitBtn.addEventListener('click', async () => { 799 submitBtn.disabled = true; 800 submitBtn.textContent = 'Submitting...'; 801 802 await submitFeedback(selectedRating, textarea.value.trim()); 803 showFeedbackThanks(overlay); 804 }); 805 806 // Skip feedback 807 skipBtn.addEventListener('click', () => { 808 startNewChat(); 809 }); 810 } 811 812 function showFeedbackThanks(overlay) { 813 overlay.innerHTML = ` 814 <div class="chatbot-feedback-icon"> 815 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> 816 <path d="M20 6L9 17l-5-5"/> 817 </svg> 818 </div> 819 <div class="chatbot-feedback-thanks"> 820 <h3>Thank you!</h3> 821 <p>Your feedback helps us improve.</p> 822 <button class="chatbot-new-chat-btn">Start New Chat</button> 823 </div> 824 `; 825 826 overlay.querySelector('.chatbot-new-chat-btn').addEventListener('click', startNewChat); 827 } 828 829 function startNewChat() { 830 // Clear conversation data 831 localStorage.removeItem(getConversationIdKey()); 832 conversationId = null; 833 messages = []; 834 conversationStatus = 'ai_handling'; 835 aiAgentInfo = null; // Reset AI agent for new personality 836 837 // Remove feedback overlay 838 const overlay = document.querySelector('.chatbot-feedback-overlay'); 839 if (overlay) overlay.remove(); 840 841 // Reset chat view 842 const chatView = document.querySelector('.chatbot-chat-view'); 843 const messagesContainer = chatView.querySelector('.chatbot-messages'); 844 const inputArea = chatView.querySelector('.chatbot-input-area'); 845 846 if (messagesContainer) messagesContainer.remove(); 847 if (inputArea) inputArea.remove(); 848 849 // Show chat interface with welcome message 850 showChatInterface(chatView); 851 852 // Switch to home view 853 switchToHome(); 854 } 855 856 function extractPageContent() { 857 // Clone the body to manipulate 858 const clone = document.body.cloneNode(true); 859 860 // Remove script, style, nav, footer, header elements 861 const removeSelectors = ['script', 'style', 'nav', 'footer', 'header', 'aside', '.chatbot-widget-container']; 862 removeSelectors.forEach(selector => { 863 clone.querySelectorAll(selector).forEach(el => el.remove()); 864 }); 865 866 // Get text content 867 let text = clone.textContent || ''; 868 869 // Clean up whitespace 870 text = text.replace(/\s+/g, ' ').trim(); 871 872 // Limit to first 10000 characters 873 return text.substring(0, 10000); 874 } 875 876 function findSitemapUrl() { 877 // Check common sitemap locations 878 const possibleSitemaps = [ 879 '/sitemap.xml', 880 '/sitemap_index.xml', 881 '/sitemap-index.xml' 882 ]; 883 884 // Also check robots.txt link if present 885 const robotsLink = document.querySelector('link[rel="robots"]'); 886 if (robotsLink) { 887 return window.location.origin + '/sitemap.xml'; 888 } 889 890 return window.location.origin + '/sitemap.xml'; 891 } 892 893 function createStyles() { 894 const style = document.createElement('style'); 895 style.textContent = ` 896 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); 897 898 .chatbot-widget-container * { 899 box-sizing: border-box; 900 font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 901 } 902 .chatbot-bubble { 903 position: fixed; 904 bottom: 24px; 905 width: 64px; 906 height: 64px; 907 border-radius: 50%; 908 cursor: pointer; 909 display: flex; 910 align-items: center; 911 justify-content: center; 912 box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.05); 913 transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.25s ease; 914 z-index: 999998; 915 background: linear-gradient(135deg, #1a1a1a 0%, #333 100%); 916 } 917 .chatbot-bubble:hover { 918 transform: scale(1.08); 919 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.22), 0 0 0 1px rgba(0, 0, 0, 0.08); 920 } 921 .chatbot-bubble svg { 922 width: 28px; 923 height: 28px; 924 } 925 .chatbot-bubble.open svg.chat-icon { 926 display: none; 927 } 928 .chatbot-bubble.open svg.close-icon { 929 display: block; 930 } 931 .chatbot-bubble svg.close-icon { 932 display: none; 933 } 934 935 /* Teaser styles */ 936 .chatbot-teaser { 937 position: fixed; 938 bottom: 100px; 939 background: #fff; 940 border: none; 941 border-radius: 16px; 942 padding: 14px 18px; 943 max-width: 240px; 944 box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.04); 945 z-index: 999997; 946 animation: chatbot-teaser-in 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); 947 cursor: pointer; 948 } 949 .chatbot-teaser.left { 950 left: 24px; 951 } 952 .chatbot-teaser.right { 953 right: 24px; 954 } 955 .chatbot-teaser::after { 956 content: ''; 957 position: absolute; 958 bottom: -8px; 959 width: 0; 960 height: 0; 961 border-left: 8px solid transparent; 962 border-right: 8px solid transparent; 963 border-top: 8px solid #fff; 964 } 965 .chatbot-teaser.left::after { 966 left: 24px; 967 } 968 .chatbot-teaser.right::after { 969 right: 24px; 970 } 971 .chatbot-teaser-text { 972 font-size: 14px; 973 font-weight: 500; 974 color: #1a1a1a; 975 line-height: 1.5; 976 margin: 0; 977 padding-right: 20px; 978 } 979 .chatbot-teaser-close { 980 position: absolute; 981 top: 10px; 982 right: 10px;
983 width: 20px; 984 height: 20px; 985 border: none; 986 background: #f3f4f6; 987 border-radius: 50%; 988 cursor: pointer; 989 display: flex; 990 align-items: center; 991 justify-content: center; 992 font-size: 14px; 993 color: #6b7280; 994 transition: all 0.2s ease; 995 } 996 .chatbot-teaser-close:hover { 997 background: #e5e7eb; 998 color: #374151; 999 } 1000 @keyframes chatbot-teaser-in { 1001 from { 1002 opacity: 0; 1003 transform: translateY(12px) scale(0.92); 1004 } 1005 to { 1006 opacity: 1; 1007 transform: translateY(0) scale(1); 1008 } 1009 } 1010 @keyframes chatbot-teaser-out { 1011 from { 1012 opacity: 1; 1013 transform: translateY(0) scale(1); 1014 } 1015 to { 1016 opacity: 0; 1017 transform: translateY(12px) scale(0.92); 1018 } 1019 } 1020 1021 1022 .chatbot-window { 1023 position: fixed; 1024 bottom: 100px; 1025 width: 400px; 1026 height: 620px; 1027 border-radius: 24px; 1028 overflow: hidden; 1029 display: none; 1030 flex-direction: column; 1031 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.05); 1032 z-index: 999999; 1033 background: #fff; 1034 } 1035 .chatbot-window.open { 1036 display: flex; 1037 animation: chatbot-slide-up 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); 1038 } 1039 @keyframes chatbot-slide-up { 1040 from { 1041 opacity: 0; 1042 transform: translateY(24px) scale(0.96); 1043 } 1044 to { 1045 opacity: 1; 1046 transform: translateY(0) scale(1); 1047 } 1048 } 1049 1050 /* Home view styles */ 1051 .chatbot-home-view { 1052 flex: 1; 1053 display: flex; 1054 flex-direction: column; 1055 background: #fafafa; 1056 min-height: 0; 1057 overflow: hidden; 1058 } 1059 .chatbot-home-header { 1060 background: linear-gradient(180deg, #1a1a1a 0%, #2a2a2a 50%, #3d3d3d 100%); 1061 padding: 20px 20px 36px; 1062 color: #fff; 1063 position: relative; 1064 flex-shrink: 0; 1065 } 1066 .chatbot-home-header-top { 1067 display: flex; 1068 align-items: center; 1069 justify-content: space-between; 1070 margin-bottom: 20px; 1071 } 1072 .chatbot-home-logo { 1073 width: 44px; 1074 height: 44px; 1075 border-radius: 12px; 1076 background: #fff; 1077 display: flex; 1078 align-items: center; 1079 justify-content: center; 1080 overflow: hidden; 1081 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 1082 } 1083 .chatbot-home-logo img { 1084 width: 100%; 1085 height: 100%; 1086 object-fit: cover; 1087 } 1088 .chatbot-home-logo svg { 1089 width: 24px; 1090 height: 24px; 1091 } 1092 .chatbot-home-close { 1093 background: rgba(255, 255, 255, 0.1); 1094 border: none; 1095 cursor: pointer; 1096 padding: 8px; 1097 color: rgba(255,255,255,0.8); 1098 transition: all 0.2s ease; 1099 border-radius: 10px; 1100 } 1101 .chatbot-home-close:hover { 1102 background: rgba(255, 255, 255, 0.2); 1103 color: #fff; 1104 } 1105 .chatbot-home-close svg { 1106 width: 20px; 1107 height: 20px; 1108 } 1109 .chatbot-home-greeting { 1110 font-size: 26px; 1111 font-weight: 700; 1112 line-height: 1.25; 1113 margin: 0; 1114 letter-spacing: -0.02em; 1115 } 1116 .chatbot-home-greeting .wave { 1117 display: inline-block; 1118 animation: wave-animation 2.5s infinite; 1119 transform-origin: 70% 70%; 1120 margin-left: 4px; 1121 } 1122 @keyframes wave-animation { 1123 0% { transform: rotate(0deg); } 1124 10% { transform: rotate(14deg); } 1125 20% { transform: rotate(-8deg); } 1126 30% { transform: rotate(14deg); } 1127 40% { transform: rotate(-4deg); } 1128 50% { transform: rotate(10deg); } 1129 60% { transform: rotate(0deg); } 1130 100% { transform: rotate(0deg); } 1131 } 1132 1133 .chatbot-home-content { 1134 flex: 1; 1135 padding: 0 16px 16px; 1136 margin-top: -20px; 1137 overflow-y: auto; 1138 overflow-x: hidden; 1139 min-height: 0; 1140 -webkit-overflow-scrolling: touch; 1141 } 1142 1143 .chatbot-powered-by { 1144 text-align: center; 1145 padding: 14px 0 6px; 1146 font-size: 11px; 1147 color: #9ca3af; 1148 } 1149 .chatbot-powered-by a { 1150 color: #6b7280; 1151 text-decoration: none; 1152 font-weight: 600; 1153 transition: color 0.2s ease; 1154 } 1155 .chatbot-powered-by a:hover { 1156 color: #374151; 1157 } 1158 1159 .chatbot-action-card { 1160 background: #fff; 1161 border-radius: 16px; 1162 padding: 16px 18px; 1163 margin-bottom: 12px; 1164 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(0, 0, 0, 0.03); 1165 cursor: pointer; 1166 display: flex; 1167 align-items: center; 1168 justify-content: space-between; 1169 transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); 1170 } 1171 .chatbot-action-card:hover { 1172 transform: translateY(-2px); 1173 box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08), 0 0 0 1px rgba(0, 0, 0, 0.04); 1174 } 1175 .chatbot-action-card-content { 1176 flex: 1; 1177 } 1178 .chatbot-action-card-title { 1179 font-size: 15px; 1180 font-weight: 600; 1181 color: #1a1a1a; 1182 margin: 0 0 3px 0; 1183 letter-spacing: -0.01em; 1184 } 1185 .chatbot-action-card-subtitle { 1186 font-size: 13px; 1187 color: #6b7280; 1188 margin: 0; 1189 } 1190 .chatbot-action-card-icon { 1191 width: 40px; 1192 height: 40px; 1193 display: flex; 1194 align-items: center; 1195 justify-content: center; 1196 color: #1a1a1a; 1197 background: #f5f5f5; 1198 border-radius: 12px; 1199 transition: all 0.2s ease; 1200 } 1201 .chatbot-action-card:hover .chatbot-action-card-icon { 1202 background: #ebebeb; 1203 } 1204 .chatbot-action-card-icon svg {
1205 width: 20px; 1206 height: 20px; 1207 } 1208 1209 1210 .chatbot-status-card { 1211 background: #fff; 1212 border-radius: 14px; 1213 padding: 14px 16px; 1214 margin-bottom: 12px; 1215 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), 0 0 0 1px rgba(0, 0, 0, 0.03); 1216 display: flex; 1217 align-items: center; 1218 gap: 12px; 1219 } 1220 .chatbot-status-icon { 1221 width: 32px; 1222 height: 32px; 1223 border-radius: 50%; 1224 background: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%); 1225 display: flex; 1226 align-items: center; 1227 justify-content: center; 1228 flex-shrink: 0; 1229 } 1230 .chatbot-status-icon svg { 1231 width: 16px; 1232 height: 16px; 1233 color: #22c55e; 1234 } 1235 .chatbot-status-text { 1236 flex: 1; 1237 min-width: 0; 1238 } 1239 .chatbot-status-title { 1240 font-size: 13px; 1241 font-weight: 600; 1242 color: #1a1a1a; 1243 margin: 0 0 2px 0; 1244 } 1245 .chatbot-status-subtitle { 1246 font-size: 12px; 1247 color: #6b7280; 1248 margin: 0; 1249 } 1250 1251 /* Section labels */ 1252 .chatbot-section-label { 1253 font-size: 11px; 1254 font-weight: 700; 1255 color: #9ca3af; 1256 text-transform: uppercase; 1257 letter-spacing: 0.08em; 1258 margin: 16px 0 10px 4px; 1259 } 1260 1261 /* Quick Questions */ 1262 .chatbot-quick-questions { 1263 display: flex; 1264 flex-direction: column; 1265 gap: 8px; 1266 margin-bottom: 14px; 1267 } 1268 .chatbot-quick-question { 1269 display: flex; 1270 align-items: center; 1271 gap: 12px; 1272 background: #fff; 1273 border: 1px solid #e5e7eb; 1274 border-radius: 14px; 1275 padding: 14px 16px; 1276 cursor: pointer; 1277 text-align: left; 1278 font-size: 14px; 1279 font-weight: 500; 1280 color: #1a1a1a; 1281 transition: all 0.2s ease; 1282 } 1283 .chatbot-quick-question:hover { 1284 border-color: #1a1a1a; 1285 background: #fafafa; 1286 transform: translateX(4px); 1287 } 1288 .chatbot-qq-icon { 1289 font-size: 16px; 1290 flex-shrink: 0; 1291 width: 28px; 1292 height: 28px; 1293 display: flex; 1294 align-items: center; 1295 justify-content: center; 1296 background: #f5f5f5; 1297 border-radius: 8px; 1298 } 1299 1300 /* Useful Links */ 1301 .chatbot-links-section { 1302 display: flex; 1303 flex-wrap: wrap; 1304 gap: 8px; 1305 margin-bottom: 14px; 1306 } 1307 .chatbot-link-item { 1308 display: flex; 1309 align-items: center; 1310 gap: 6px; 1311 background: #f3f4f6; 1312 border-radius: 20px; 1313 padding: 8px 14px; 1314 font-size: 12px; 1315 font-weight: 500; 1316 color: #374151; 1317 text-decoration: none; 1318 transition: all 0.2s ease; 1319 } 1320 .chatbot-link-item:hover { 1321 background: #e5e7eb; 1322 color: #1a1a1a; 1323 transform: translateY(-1px); 1324 } 1325 .chatbot-link-item svg { 1326 width: 14px; 1327 height: 14px; 1328 } 1329 1330 /* Bottom navigation */ 1331 .chatbot-bottom-nav { 1332 display: flex; 1333 border-top: 1px solid #e5e7eb; 1334 background: #fff; 1335 padding: 4px 0; 1336 } 1337 .chatbot-nav-item { 1338 flex: 1; 1339 display: flex; 1340 flex-direction: column; 1341 align-items: center; 1342 padding: 12px 8px; 1343 background: none; 1344 border: none; 1345 cursor: pointer; 1346 color: #9ca3af; 1347 font-size: 12px; 1348 font-weight: 500; 1349 gap: 4px; 1350 transition: color 0.2s ease; 1351 position: relative; 1352 } 1353 .chatbot-nav-item:hover { 1354 color: #6b7280; 1355 } 1356 .chatbot-nav-item.active { 1357 color: #1a1a1a; 1358 } 1359 .chatbot-nav-item.active::after { 1360 content: ''; 1361 position: absolute; 1362 bottom: 4px; 1363 left: 50%; 1364 transform: translateX(-50%); 1365 width: 4px; 1366 height: 4px; 1367 background: #1a1a1a; 1368 border-radius: 50%; 1369 } 1370 .chatbot-nav-item svg { 1371 width: 24px; 1372 height: 24px; 1373 } 1374 .chatbot-nav-badge { 1375 position: absolute; 1376 top: 6px; 1377 right: calc(50% - 22px); 1378 background: #ef4444; 1379 color: #fff; 1380 font-size: 10px; 1381 font-weight: 700; 1382 padding: 2px 6px; 1383 border-radius: 10px; 1384 min-width: 18px; 1385 text-align: center; 1386 } 1387 1388 /* Chat view (messages) */ 1389 .chatbot-chat-view { 1390 flex: 1; 1391 display: none; 1392 flex-direction: column; 1393 background: #fff; 1394 min-height: 0; 1395 overflow: hidden; 1396 } 1397 .chatbot-chat-view.active { 1398 display: flex; 1399 } 1400 .chatbot-home-view.hidden { 1401 display: none; 1402 } 1403 1404 .chatbot-chat-header { 1405 padding: 18px 20px; 1406 display: flex; 1407 align-items: center; 1408 gap: 14px;
1409 border-bottom: 1px solid #f0f0f0; 1410 background: #fff; 1411 flex-shrink: 0; 1412 } 1413 .chatbot-chat-back { 1414 background: #f5f5f5; 1415 border: none; 1416 cursor: pointer; 1417 padding: 8px; 1418 color: #6b7280; 1419 display: flex; 1420 align-items: center; 1421 justify-content: center; 1422 border-radius: 10px; 1423 transition: all 0.2s ease; 1424 } 1425 .chatbot-chat-back:hover { 1426 background: #e5e5e5; 1427 color: #1a1a1a; 1428 } 1429 .chatbot-chat-back svg { 1430 width: 18px; 1431 height: 18px; 1432 } 1433 .chatbot-chat-header-title { 1434 font-size: 16px; 1435 font-weight: 600; 1436 color: #1a1a1a; 1437 margin: 0; 1438 flex: 1; 1439 letter-spacing: -0.01em; 1440 } 1441 1442 .chatbot-messages { 1443 flex: 1; 1444 overflow-y: auto; 1445 overflow-x: hidden; 1446 padding: 20px; 1447 display: flex; 1448 flex-direction: column; 1449 gap: 14px; 1450 background: linear-gradient(180deg, #fafafa 0%, #f5f5f5 100%); 1451 min-height: 0; 1452 -webkit-overflow-scrolling: touch; 1453 } 1454 .chatbot-message { 1455 max-width: 85%; 1456 padding: 14px 18px; 1457 border-radius: 20px; 1458 font-size: 14px; 1459 line-height: 1.55; 1460 word-wrap: break-word; 1461 } 1462 .chatbot-message a { 1463 color: inherit; 1464 text-decoration: underline; 1465 font-weight: 600; 1466 } 1467 .chatbot-message a:hover { 1468 opacity: 0.85; 1469 } 1470 .chatbot-message.visitor { 1471 align-self: flex-end; 1472 background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%); 1473 color: #fff; 1474 border-bottom-right-radius: 6px; 1475 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 1476 } 1477 .chatbot-message.ai, .chatbot-message.agent { 1478 align-self: flex-start; 1479 background: #fff; 1480 color: #1f2937; 1481 border: none; 1482 border-bottom-left-radius: 6px; 1483 padding: 14px 16px; 1484 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 1485 } 1486 .chatbot-message.ai a { 1487 color: #1a1a1a; 1488 } 1489 .chatbot-ai-header { 1490 display: flex; 1491 align-items: center; 1492 gap: 10px; 1493 margin-bottom: 8px; 1494 } 1495 .chatbot-ai-avatar { 1496 width: 30px; 1497 height: 30px; 1498 border-radius: 50%; 1499 display: flex; 1500 align-items: center; 1501 justify-content: center; 1502 font-size: 15px; 1503 animation: chatbot-avatar-wiggle 2s ease-in-out infinite; 1504 box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); 1505 } 1506 @keyframes chatbot-avatar-wiggle { 1507 0%, 100% { transform: rotate(-3deg) scale(1); } 1508 25% { transform: rotate(3deg) scale(1.05); } 1509 50% { transform: rotate(-3deg) scale(1); } 1510 75% { transform: rotate(3deg) scale(1.02); } 1511 } 1512 .chatbot-ai-name { 1513 font-size: 13px; 1514 font-weight: 600; 1515 color: #374151; 1516 } 1517 .chatbot-ai-content { 1518 font-size: 14px; 1519 line-height: 1.55; 1520 } 1521 .chatbot-ai-content .chatbot-md-p { margin: 0 0 8px 0; } 1522 .chatbot-ai-content .chatbot-md-p:last-child { margin-bottom: 0; } 1523 .chatbot-ai-content .chatbot-md-list { margin: 6px 0 8px 0; padding-left: 20px; } 1524 .chatbot-ai-content .chatbot-md-list li { margin: 2px 0; } 1525 .chatbot-ai-content .chatbot-md-h { font-size: 14px; font-weight: 700; margin: 10px 0 6px 0; line-height: 1.3; } 1526 .chatbot-ai-content strong { font-weight: 700; } 1527 .chatbot-ai-content em { font-style: italic; } 1528 .chatbot-ai-content .chatbot-code-inline { 1529 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; 1530 font-size: 12.5px; 1531 background: rgba(0,0,0,0.06); 1532 padding: 1px 5px; 1533 border-radius: 4px; 1534 } 1535 .chatbot-ai-content .chatbot-code-block { 1536 background: #0f172a; 1537 color: #f8fafc; 1538 padding: 10px 12px; 1539 border-radius: 8px; 1540 overflow-x: auto; 1541 margin: 8px 0; 1542 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; 1543 font-size: 12.5px; 1544 line-height: 1.5; 1545 } 1546 .chatbot-link-buttons { 1547 display: flex; 1548 flex-wrap: wrap; 1549 gap: 8px; 1550 margin-top: 12px; 1551 } 1552 .chatbot-link-btn { 1553 display: inline-flex; 1554 align-items: center; 1555 gap: 6px; 1556 padding: 10px 14px; 1557 background: #f5f5f5; 1558 border: none; 1559 border-radius: 10px; 1560 font-size: 13px; 1561 font-weight: 500; 1562 color: #374151; 1563 text-decoration: none; 1564 transition: all 0.2s ease; 1565 max-width: 100%; 1566 word-break: break-all; 1567 } 1568 .chatbot-link-btn:hover { 1569 background: #e5e5e5; 1570 color: #1a1a1a; 1571 transform: translateY(-1px); 1572 } 1573 .chatbot-link-btn svg { 1574 width: 14px; 1575 height: 14px; 1576 flex-shrink: 0; 1577 } 1578 .chatbot-typing { 1579 align-self: flex-start; 1580 background: #fff; 1581 padding: 14px 18px; 1582 border-radius: 20px; 1583 border-bottom-left-radius: 6px; 1584 display: flex; 1585 gap: 5px; 1586 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 1587 } 1588 .chatbot-typing span { 1589 width: 8px; 1590 height: 8px; 1591 border-radius: 50%; 1592 background: #9ca3af; 1593 animation: chatbot-bounce 1.4s infinite ease-in-out; 1594 } 1595 .chatbot-typing span:nth-child(1) { animation-delay: -0.32s; } 1596 .chatbot-typing span:nth-child(2) { animation-delay: -0.16s; } 1597 @keyframes chatbot-bounce { 1598 0%, 80%, 100% { transform: scale(0.8); } 1599 40% { transform: scale(1.2); } 1600 } 1601 1602 /* Typing indicator for agent */ 1603 .chatbot-typing-indicator { 1604 animation: chatbot-fade-in 0.25s ease-out; 1605 } 1606 @keyframes chatbot-fade-in { 1607 from { opacity: 0; transform: translateY(6px); } 1608 to { opacity: 1; transform: translateY(0); } 1609 } 1610 .chatbot-typing-bubble { 1611 background: #fff; 1612 padding: 12px 16px; 1613 border-radius: 20px; 1614 border-bottom-left-radius: 6px; 1615 display: flex; 1616 gap: 5px; 1617 align-items: center; 1618 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 1619 } 1620 .chatbot-typing-dot { 1621 width: 8px; 1622 height: 8px; 1623 border-radius: 50%; 1624 background: #9ca3af; 1625 animation: chatbot-bounce 1.4s infinite ease-in-out; 1626 } 1627 .chatbot-typing-dot:nth-child(1) { animation-delay: -0.32s; } 1628 .chatbot-typing-dot:nth-child(2) { animation-delay: -0.16s; } 1629 .chatbot-typing-dot:nth-child(3) { animation-delay: 0s; } 1630 .chatbot-input-area { 1631 padding: 16px 20px; 1632 background: #fff;
1633 border-top: 1px solid #f0f0f0; 1634 display: flex; 1635 gap: 10px; 1636 align-items: center; 1637 } 1638 .chatbot-input { 1639 flex: 1; 1640 padding: 14px 18px; 1641 border: 1px solid #e5e7eb; 1642 border-radius: 28px; 1643 font-size: 14px; 1644 outline: none; 1645 transition: all 0.2s ease; 1646 background: #fafafa; 1647 color: #1a1a1a; 1648 } 1649 .chatbot-input:focus { 1650 border-color: #1a1a1a; 1651 background: #fff; 1652 box-shadow: 0 0 0 3px rgba(26, 26, 26, 0.08); 1653 } 1654 .chatbot-input::placeholder { 1655 color: #9ca3af; 1656 } 1657 .chatbot-send { 1658 width: 48px; 1659 height: 48px; 1660 border: none; 1661 border-radius: 50%; 1662 cursor: pointer; 1663 display: flex; 1664 align-items: center; 1665 justify-content: center; 1666 transition: all 0.2s ease; 1667 background: linear-gradient(135deg, #1a1a1a 0%, #333 100%); 1668 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 1669 } 1670 .chatbot-send:hover { 1671 transform: scale(1.05); 1672 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 1673 } 1674 .chatbot-send:disabled { 1675 opacity: 0.5; 1676 cursor: not-allowed; 1677 transform: none; 1678 box-shadow: none; 1679 } 1680 .chatbot-send svg { 1681 width: 20px; 1682 height: 20px; 1683 fill: #fff; 1684 } 1685 1686 /* Pre-chat form styles */ 1687 .chatbot-prechat-form { 1688 flex: 1; 1689 padding: 24px 20px; 1690 display: flex; 1691 flex-direction: column; 1692 gap: 18px; 1693 background: #fafafa; 1694 overflow-y: auto; 1695 } 1696 .chatbot-prechat-title { 1697 font-size: 20px; 1698 font-weight: 700; 1699 color: #1a1a1a; 1700 margin: 0 0 4px 0; 1701 letter-spacing: -0.02em; 1702 } 1703 .chatbot-prechat-subtitle { 1704 font-size: 14px; 1705 color: #6b7280; 1706 margin: 0 0 8px 0; 1707 } 1708 .chatbot-form-group { 1709 display: flex; 1710 flex-direction: column; 1711 gap: 8px; 1712 } 1713 .chatbot-form-label { 1714 font-size: 13px; 1715 font-weight: 600; 1716 color: #374151; 1717 } 1718 .chatbot-form-label .optional { 1719 font-weight: 400; 1720 color: #9ca3af; 1721 margin-left: 4px; 1722 } 1723 .chatbot-form-input { 1724 padding: 14px 16px; 1725 border: 1px solid #e5e7eb; 1726 border-radius: 14px; 1727 font-size: 14px; 1728 outline: none; 1729 transition: all 0.2s ease; 1730 background: #fff; 1731 color: #1f2937; 1732 } 1733 .chatbot-form-input::placeholder { 1734 color: #9ca3af; 1735 } 1736 .chatbot-form-input:focus { 1737 border-color: #1a1a1a; 1738 box-shadow: 0 0 0 3px rgba(26, 26, 26, 0.08); 1739 } 1740 .chatbot-form-input.error { 1741 border-color: #ef4444; 1742 } 1743 .chatbot-form-error { 1744 font-size: 12px; 1745 color: #ef4444; 1746 margin-top: 2px; 1747 } 1748 .chatbot-form-actions { 1749 display: flex; 1750 flex-direction: column; 1751 gap: 12px; 1752 margin-top: 12px; 1753 } 1754 .chatbot-form-submit { 1755 padding: 14px 24px; 1756 border: none; 1757 border-radius: 14px; 1758 font-size: 15px; 1759 font-weight: 600; 1760 cursor: pointer; 1761 transition: all 0.2s ease; 1762 background: linear-gradient(135deg, #1a1a1a 0%, #333 100%); 1763 color: #fff; 1764 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); 1765 } 1766 .chatbot-form-submit:hover { 1767 transform: translateY(-1px); 1768 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); 1769 } 1770 .chatbot-form-skip { 1771 padding: 12px 24px; 1772 background: transparent; 1773 border: none; 1774 color: #6b7280; 1775 font-size: 14px; 1776 font-weight: 500; 1777 cursor: pointer; 1778 transition: color 0.2s ease; 1779 } 1780 .chatbot-form-skip:hover { 1781 color: #374151; 1782 } 1783 1784 /* Quick replies styles */ 1785 .chatbot-quick-replies { 1786 display: flex; 1787 flex-wrap: wrap; 1788 gap: 8px; 1789 padding: 10px 0; 1790 animation: chatbot-fade-in 0.3s ease-out; 1791 } 1792 @keyframes chatbot-fade-in { 1793 from { opacity: 0; transform: translateY(6px); } 1794 to { opacity: 1; transform: translateY(0); } 1795 } 1796 .chatbot-quick-reply { 1797 background: #fff; 1798 border: 1.5px solid #1a1a1a; 1799 color: #1a1a1a; 1800 border-radius: 20px; 1801 padding: 10px 16px; 1802 font-size: 13px; 1803 font-weight: 500; 1804 cursor: pointer; 1805 transition: all 0.2s ease; 1806 white-space: nowrap; 1807 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 1808 } 1809 .chatbot-quick-reply:hover { 1810 background: #1a1a1a; 1811 color: #fff; 1812 transform: translateY(-2px); 1813 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 1814 } 1815 .chatbot-quick-reply:active { 1816 transform: translateY(0); 1817 } 1818 1819 /* Message reactions */ 1820 .chatbot-message-wrapper { 1821 display: flex; 1822 flex-direction: column; 1823 gap: 4px; 1824 } 1825 .chatbot-message-wrapper.visitor { 1826 align-items: flex-end; 1827 } 1828 .chatbot-message-wrapper.ai { 1829 align-items: flex-start; 1830 } 1831 .chatbot-reactions { 1832 display: flex; 1833 flex-wrap: wrap; 1834 gap: 4px; 1835 } 1836 .chatbot-reaction-badge { 1837 display: flex; 1838 align-items: center; 1839 gap: 2px; 1840 padding: 2px 6px; 1841 font-size: 12px; 1842 border-radius: 12px; 1843 background: #fff; 1844 border: 1px solid #e5e7eb; 1845 cursor: pointer; 1846 transition: all 0.2s; 1847 } 1848 .chatbot-reaction-badge:hover { 1849 border-color: #d1d5db; 1850 background: #f9fafb; 1851 } 1852 .chatbot-reaction-badge.active { 1853 background: rgba(99, 102, 241, 0.1); 1854 border-color: rgba(99, 102, 241, 0.3); 1855 } 1856 .chatbot-reaction-badge .count { 1857 font-size: 10px; 1858 color: #6b7280; 1859 } 1860 .chatbot-reaction-picker { 1861 display: none; 1862 position: absolute; 1863 bottom: 100%; 1864 right: 0; 1865 background: #fff; 1866 border: 1px solid #e5e7eb; 1867 border-radius: 20px; 1868 padding: 4px 6px; 1869 box-shadow: 0 2px 10px rgba(0,0,0,0.1); 1870 gap: 2px; 1871 margin-bottom: 4px; 1872 z-index: 10; 1873 } 1874 .chatbot-reaction-picker.show { 1875 display: flex; 1876 } 1877 .chatbot-reaction-btn { 1878 background: none; 1879 border: none; 1880 font-size: 16px; 1881 padding: 4px; 1882 cursor: pointer; 1883 border-radius: 4px; 1884 transition: background 0.2s; 1885 } 1886 .chatbot-reaction-btn:hover { 1887 background: #f3f4f6; 1888 }
1889 .chatbot-add-reaction { 1890 background: none; 1891 border: none; 1892 cursor: pointer; 1893 padding: 4px; 1894 opacity: 0; 1895 transition: opacity 0.2s; 1896 color: #9ca3af; 1897 display: flex; 1898 align-items: center; 1899 justify-content: center; 1900 } 1901 .chatbot-message-wrapper:hover .chatbot-add-reaction { 1902 opacity: 1; 1903 } 1904 .chatbot-add-reaction:hover { 1905 color: #6b7280; 1906 } 1907 .chatbot-add-reaction svg { 1908 width: 16px; 1909 height: 16px; 1910 } 1911 .chatbot-message-footer { 1912 display: flex; 1913 align-items: center; 1914 gap: 4px; 1915 position: relative; 1916 } 1917 1918 /* Loading spinner */ 1919 .chatbot-loading { 1920 display: flex; 1921 flex-direction: column; 1922 align-items: center; 1923 justify-content: center; 1924 padding: 40px 20px; 1925 gap: 12px; 1926 color: #6b7280; 1927 font-size: 14px; 1928 } 1929 .chatbot-loading-spinner { 1930 width: 28px; 1931 height: 28px; 1932 border: 3px solid #e5e7eb; 1933 border-top-color: #1a1a1a; 1934 border-radius: 50%; 1935 animation: chatbot-spin 0.8s linear infinite; 1936 } 1937 @keyframes chatbot-spin { 1938 to { transform: rotate(360deg); } 1939 } 1940 1941 /* End Chat button */ 1942 .chatbot-end-chat-btn { 1943 background: none; 1944 border: none; 1945 cursor: pointer; 1946 padding: 6px; 1947 color: #6b7280; 1948 display: flex; 1949 align-items: center; 1950 justify-content: center; 1951 border-radius: 6px; 1952 transition: all 0.2s; 1953 } 1954 .chatbot-end-chat-btn:hover { 1955 background: #f3f4f6; 1956 color: #ef4444; 1957 } 1958 .chatbot-end-chat-btn svg { 1959 width: 20px; 1960 height: 20px; 1961 } 1962 1963 /* Feedback overlay */ 1964 .chatbot-feedback-overlay { 1965 position: absolute; 1966 inset: 0; 1967 background: rgba(255, 255, 255, 0.98); 1968 display: flex; 1969 flex-direction: column; 1970 align-items: center; 1971 justify-content: center; 1972 padding: 24px; 1973 z-index: 100; 1974 animation: chatbot-fade-in 0.3s ease-out; 1975 } 1976 .chatbot-feedback-icon { 1977 width: 64px; 1978 height: 64px; 1979 background: #dcfce7; 1980 border-radius: 50%; 1981 display: flex; 1982 align-items: center; 1983 justify-content: center; 1984 margin-bottom: 16px; 1985 } 1986 .chatbot-feedback-icon svg { 1987 width: 32px; 1988 height: 32px; 1989 color: #22c55e; 1990 } 1991 .chatbot-feedback-title { 1992 font-size: 18px; 1993 font-weight: 600; 1994 color: #1f2937; 1995 margin: 0 0 8px 0; 1996 text-align: center; 1997 } 1998 .chatbot-feedback-subtitle { 1999 font-size: 14px; 2000 color: #6b7280; 2001 margin: 0 0 24px 0; 2002 text-align: center; 2003 } 2004 .chatbot-rating-stars { 2005 display: flex; 2006 gap: 8px; 2007 margin-bottom: 16px; 2008 } 2009 .chatbot-star { 2010 background: none; 2011 border: none; 2012 cursor: pointer; 2013 padding: 4px; 2014 font-size: 28px; 2015 color: #d1d5db; 2016 transition: all 0.2s; 2017 } 2018 .chatbot-star:hover, 2019 .chatbot-star.active { 2020 color: #fbbf24; 2021 transform: scale(1.1); 2022 } 2023 .chatbot-feedback-textarea { 2024 width: 100%; 2025 padding: 12px; 2026 border: 1px solid #e5e7eb; 2027 border-radius: 10px; 2028 font-size: 14px; 2029 resize: none; 2030 margin-bottom: 16px; 2031 outline: none; 2032 transition: border-color 0.2s; 2033 } 2034 .chatbot-feedback-textarea:focus { 2035 border-color: #1a1a1a; 2036 } 2037 .chatbot-feedback-actions { 2038 display: flex; 2039 flex-direction: column; 2040 gap: 10px; 2041 width: 100%; 2042 } 2043 .chatbot-feedback-submit { 2044 padding: 12px 20px; 2045 border: none; 2046 border-radius: 10px; 2047 font-size: 14px; 2048 font-weight: 500; 2049 cursor: pointer; 2050 background: #1a1a1a; 2051 color: #fff; 2052 transition: opacity 0.2s; 2053 } 2054 .chatbot-feedback-submit:hover { 2055 opacity: 0.9; 2056 } 2057 .chatbot-feedback-submit:disabled { 2058 opacity: 0.5;
2059 cursor: not-allowed; 2060 } 2061 .chatbot-feedback-skip { 2062 padding: 10px 20px; 2063 background: transparent; 2064 border: none; 2065 color: #6b7280; 2066 font-size: 13px; 2067 cursor: pointer; 2068 transition: color 0.2s; 2069 } 2070 .chatbot-feedback-skip:hover { 2071 color: #374151; 2072 } 2073 .chatbot-feedback-thanks { 2074 text-align: center; 2075 } 2076 .chatbot-feedback-thanks h3 { 2077 font-size: 18px; 2078 font-weight: 600; 2079 color: #1f2937; 2080 margin: 0 0 8px 0; 2081 } 2082 .chatbot-feedback-thanks p { 2083 font-size: 14px; 2084 color: #6b7280; 2085 margin: 0 0 24px 0; 2086 } 2087 .chatbot-new-chat-btn { 2088 padding: 12px 24px; 2089 border: 1px solid #e5e7eb; 2090 border-radius: 10px; 2091 font-size: 14px; 2092 font-weight: 500; 2093 cursor: pointer; 2094 background: #fff; 2095 color: #1f2937; 2096 transition: all 0.2s; 2097 } 2098 .chatbot-new-chat-btn:hover { 2099 background: #f9fafb; 2100 border-color: #d1d5db; 2101 } 2102 2103 /* Chat resolved banner */ 2104 .chatbot-resolved-banner { 2105 background: #fef3c7; 2106 border-bottom: 1px solid #fcd34d; 2107 padding: 8px 16px; 2108 text-align: center; 2109 font-size: 13px; 2110 color: #92400e; 2111 } 2112 2113 2114 @media (max-width: 480px) { 2115 .chatbot-window { 2116 width: calc(100% - 24px); 2117 height: calc(100% - 120px); 2118 bottom: 92px; 2119 left: 12px !important; 2120 right: 12px !important; 2121 border-radius: 20px; 2122 } 2123 .chatbot-teaser { 2124 max-width: calc(100% - 100px); 2125 } 2126 .chatbot-quick-reply { 2127 font-size: 13px; 2128 padding: 8px 14px; 2129 } 2130 .chatbot-home-greeting { 2131 font-size: 22px; 2132 } 2133 .chatbot-bubble { 2134 width: 60px; 2135 height: 60px; 2136 bottom: 20px; 2137 } 2138 .chatbot-messages { 2139 padding: 16px; 2140 } 2141 .chatbot-message { 2142 padding: 12px 16px; 2143 } 2144 .chatbot-input-area { 2145 padding: 14px 16px; 2146 } 2147 .chatbot-input { 2148 padding: 12px 16px; 2149 } 2150 .chatbot-send { 2151 width: 44px; 2152 height: 44px; 2153 } 2154 } 2155 `; 2156 document.head.appendChild(style); 2157 } 2158 2159 2160 let currentView = 'home'; // 'home' or 'messages' 2161 2162 function toggleWindow() { 2163 isOpen = !isOpen; 2164 const chatWin = document.querySelector('.chatbot-window'); 2165 const bubble = document.querySelector('.chatbot-bubble'); 2166 if (isOpen) { 2167 chatWin.classList.add('open'); 2168 bubble.classList.add('open'); 2169 hideTeaser(); 2170 markMessagesRead(); 2171 } else { 2172 chatWin.classList.remove('open'); 2173 bubble.classList.remove('open'); 2174 } 2175 } 2176 2177 function switchToMessages() { 2178 currentView = 'messages'; 2179 const homeView = document.querySelector('.chatbot-home-view'); 2180 const chatView = document.querySelector('.chatbot-chat-view'); 2181 const homeNav = document.querySelector('.chatbot-nav-item[data-view="home"]'); 2182 const messagesNav = document.querySelector('.chatbot-nav-item[data-view="messages"]'); 2183 2184 if (homeView) homeView.classList.add('hidden'); 2185 if (chatView) chatView.classList.add('active'); 2186 if (homeNav) homeNav.classList.remove('active'); 2187 if (messagesNav) messagesNav.classList.add('active'); 2188 2189 // Focus on input 2190 const input = document.querySelector('.chatbot-input'); 2191 if (input) input.focus(); 2192 } 2193 2194 function switchToHome() { 2195 currentView = 'home'; 2196 const homeView = document.querySelector('.chatbot-home-view'); 2197 const chatView = document.querySelector('.chatbot-chat-view'); 2198 const homeNav = document.querySelector('.chatbot-nav-item[data-view="home"]'); 2199 const messagesNav = document.querySelector('.chatbot-nav-item[data-view="messages"]'); 2200 2201 if (homeView) homeView.classList.remove('hidden'); 2202 if (chatView) chatView.classList.remove('active'); 2203 if (homeNav) homeNav.classList.add('active'); 2204 if (messagesNav) messagesNav.classList.remove('active'); 2205 } 2206 2207 // Teaser functions 2208 function playTeaserSound() { 2209 try { 2210 // Create a subtle notification sound using Web Audio API 2211 const audioContext = new (window.AudioContext || window.webkitAudioContext)(); 2212 const oscillator = audioContext.createOscillator(); 2213 const gainNode = audioContext.createGain(); 2214 2215 oscillator.connect(gainNode); 2216 gainNode.connect(audioContext.destination); 2217 2218 oscillator.frequency.setValueAtTime(800, audioContext.currentTime); 2219 oscillator.frequency.setValueAtTime(1000, audioContext.currentTime + 0.1); 2220 2221 gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); 2222 gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3); 2223 2224 oscillator.start(audioContext.currentTime); 2225 oscillator.stop(audioContext.currentTime + 0.3); 2226 } catch (e) { 2227 console.log('[ChatBot] Sound playback not supported'); 2228 } 2229 } 2230 2231 function showTeaser() { 2232 if (teaserShown || isOpen) return; 2233 2234 const teaserDismissed = sessionStorage.getItem('chatbot_teaser_dismissed_' + widgetId); 2235 if (teaserDismissed) return; 2236 2237 const position = settings?.position || 'right'; 2238 const message = settings?.teaser_message || 'How can I help you?'; 2239 const soundEnabled = settings?.teaser_sound_enabled !== false; 2240 2241 const teaser = document.createElement('div'); 2242 teaser.className = 'chatbot-teaser ' + position; 2243 teaser.innerHTML = ` 2244 <p class="chatbot-teaser-text">${message}</p> 2245 <button class="chatbot-teaser-close">Ã</button> 2246 `; 2247 2248 document.querySelector('.chatbot-widget-container').appendChild(teaser); 2249 teaserShown = true; 2250 2251 // Play sound if enabled 2252 if (soundEnabled) { 2253 playTeaserSound(); 2254 } 2255 2256 // Click on teaser opens chat 2257 teaser.addEventListener('click', (e) => { 2258 if (!e.target.classList.contains('chatbot-teaser-close')) { 2259 hideTeaser(); 2260 if (!isOpen) toggleWindow(); 2261 } 2262 }); 2263 2264 // Close button 2265 teaser.querySelector('.chatbot-teaser-close').addEventListener('click', (e) => { 2266 e.stopPropagation(); 2267 hideTeaser(); 2268 sessionStorage.setItem('chatbot_teaser_dismissed_' + widgetId, 'true'); 2269 }); 2270 } 2271 2272 function hideTeaser() { 2273 const teaser = document.querySelector('.chatbot-teaser'); 2274 if (teaser) { 2275 teaser.style.animation = 'chatbot-teaser-out 0.3s ease-out forwards'; 2276 setTimeout(() => teaser.remove(), 300); 2277 } 2278 if (teaserTimer) { 2279 clearTimeout(teaserTimer); 2280 teaserTimer = null; 2281 } 2282 } 2283 2284 function startTeaserTimer() { 2285 const teaserEnabled = settings?.teaser_enabled !== false;
2286 const delay = (settings?.teaser_delay || 5) * 1000; 2287 2288 if (!teaserEnabled) return; 2289 2290 teaserTimer = setTimeout(() => { 2291 showTeaser(); 2292 }, delay); 2293 } 2294 2295 function extractLinks(text) { 2296 const urlRegex = /(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/g; 2297 const links = []; 2298 let match; 2299 while ((match = urlRegex.exec(text)) !== null) { 2300 links.push(match[1]); 2301 } 2302 return links; 2303 } 2304 2305 function removeLinksFromText(text) { 2306 const urlRegex = /(https?:\/\/[^\s<>"{}|\\^`\[\]]+)/g; 2307 // Remove URLs and clean up extra whitespace/punctuation 2308 return text.replace(urlRegex, '').replace(/\s+/g, ' ').replace(/\s+([.,!?])/g, '$1').trim(); 2309 } 2310 2311 function getLinkDomain(url) { 2312 try { 2313 const u = new URL(url); 2314 return u.hostname.replace('www.', ''); 2315 } catch { 2316 return url; 2317 } 2318 } 2319 2320 function isInternalLink(url) { 2321 try { 2322 const linkHost = new URL(url).hostname; 2323 const currentHost = window.location.hostname; 2324 return linkHost === currentHost || linkHost === 'www.' + currentHost || 'www.' + linkHost === currentHost; 2325 } catch { 2326 return false; 2327 } 2328 } 2329 2330 function renderLinkButtons(links) { 2331 if (!links || links.length === 0) return ''; 2332 2333 return '<div class="chatbot-link-buttons">' + links.map(url => { 2334 const isInternal = isInternalLink(url); 2335 const domain = getLinkDomain(url); 2336 const target = isInternal ? '_self' : '_blank'; 2337 const rel = isInternal ? '' : 'rel="noopener noreferrer"'; 2338 const icon = isInternal 2339 ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>' 2340 : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3"/></svg>'; 2341 return '<a href="' + url + '" target="' + target + '" ' + rel + ' class="chatbot-link-btn">' + icon + '<span>' + domain + '</span></a>'; 2342 }).join('') + '</div>'; 2343 } 2344 2345 function escapeHtml(text) { 2346 const div = document.createElement('div'); 2347 div.textContent = text; 2348 return div.innerHTML; 2349 } 2350 2351 // Lightweight, safe markdown formatter. Escapes HTML first, then applies 2352 // a small subset of markdown: headings, bold, italic, inline code, code blocks, 2353 // unordered/ordered lists, and paragraph/line breaks. 2354 function formatMarkdown(text) { 2355 if (!text) return ''; 2356 let src = String(text).replace(/\r\n/g, '\n'); 2357 const codeBlocks = []; 2358 src = src.replace(/```([\s\S]*?)```/g, function(_, code){ 2359 codeBlocks.push(code); 2360 return '\u0000CB' + (codeBlocks.length - 1) + '\u0000'; 2361 }); 2362 src = escapeHtml(src); 2363 src = src.replace(/`([^`\n]+)`/g, '<code class="chatbot-code-inline">$1</code>'); 2364 src = src.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>'); 2365 src = src.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>'); 2366 src = src.replace(/^### (.+)$/gm, '<h4 class="chatbot-md-h">$1</h4>'); 2367 src = src.replace(/^## (.+)$/gm, '<h3 class="chatbot-md-h">$1</h3>'); 2368 src = src.replace(/^# (.+)$/gm, '<h2 class="chatbot-md-h">$1</h2>'); 2369 const lines = src.split('\n'); 2370 const out = []; 2371 let i = 0; 2372 while (i < lines.length) { 2373 const ul = /^\s*[-*]\s+(.*)$/.exec(lines[i]); 2374 const ol = /^\s*\d+\.\s+(.*)$/.exec(lines[i]); 2375 if (ul || ol) { 2376 const tag = ul ? 'ul' : 'ol'; 2377 const re = ul ? /^\s*[-*]\s+(.*)$/ : /^\s*\d+\.\s+(.*)$/; 2378 const items = []; 2379 while (i < lines.length && re.test(lines[i])) { 2380 items.push('<li>' + lines[i].replace(re, '$1') + '</li>'); 2381 i++; 2382 } 2383 out.push('<' + tag + ' class="chatbot-md-list">' + items.join('') + '</' + tag + '>'); 2384 } else { 2385 out.push(lines[i]); 2386 i++; 2387 } 2388 } 2389 src = out.join('\n'); 2390 src = src.split(/\n{2,}/).map(function(block){ 2391 if (/^\s*<(ul|ol|h2|h3|h4)/.test(block)) return block; 2392 if (/^\u0000CB\d+\u0000$/.test(block.trim())) return block; 2393 return '<p class="chatbot-md-p">' + block.replace(/\n/g, '<br>') + '</p>'; 2394 }).join(''); 2395 src = src.replace(/\u0000CB(\d+)\u0000/g, function(_, n){ 2396 return '<pre class="chatbot-code-block"><code>' + escapeHtml(codeBlocks[+n]) + '</code></pre>'; 2397 }); 2398 return src; 2399 } 2400 2401 const REACTION_EMOJIS = ['\u{1F44D}', '\u2764\uFE0F', '\u{1F60A}', '\u{1F602}', '\u{1F62E}', '\u{1F622}']; 2402 2403 function renderReactions(messageId) { 2404 const reactions = messageReactions[messageId] || []; 2405 if (reactions.length === 0) return ''; 2406 2407 // Group by emoji
2408 const grouped = {}; 2409 reactions.forEach(r => { 2410 if (!grouped[r.emoji]) grouped[r.emoji] = { count: 0, hasMyReaction: false }; 2411 grouped[r.emoji].count++; 2412 if (r.reactor_type === 'visitor' && r.reactor_id === visitorId) { 2413 grouped[r.emoji].hasMyReaction = true; 2414 } 2415 }); 2416 2417 return '<div class="chatbot-reactions">' + Object.entries(grouped).map(([emoji, data]) => 2418 `<button class="chatbot-reaction-badge${data.hasMyReaction ? ' active' : ''}" data-emoji="${emoji}" data-message-id="${messageId}"> 2419 <span>${emoji}</span>${data.count > 1 ? `<span class="count">${data.count}</span>` : ''} 2420 </button>` 2421 ).join('') + '</div>'; 2422 } 2423 2424 function renderReactionPicker(messageId) { 2425 return ` 2426 <div class="chatbot-message-footer"> 2427 <button class="chatbot-add-reaction" data-message-id="${messageId}"> 2428 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 2429 <circle cx="12" cy="12" r="10"/> 2430 <path d="M8 14s1.5 2 4 2 4-2 4-2"/> 2431 <line x1="9" y1="9" x2="9.01" y2="9"/> 2432 <line x1="15" y1="9" x2="15.01" y2="9"/> 2433 </svg> 2434 </button> 2435 <div class="chatbot-reaction-picker" data-message-id="${messageId}"> 2436 ${REACTION_EMOJIS.map(e => `<button class="chatbot-reaction-btn" data-emoji="${e}" data-message-id="${messageId}">${e}</button>`).join('')} 2437 </div> 2438 </div> 2439 `; 2440 } 2441 2442 function renderMessages() { 2443 const container = document.querySelector('.chatbot-messages'); 2444 if (!container) return; 2445 2446 const agent = getAiAgentInfo(); 2447 2448 container.innerHTML = messages.map((msg, index) => { 2449 const safeContent = escapeHtml(msg.content); 2450 const messageId = msg.id || messageIdMap[index]; 2451 const reactionsHtml = messageId ? renderReactions(messageId) : ''; 2452 const pickerHtml = messageId ? renderReactionPicker(messageId) : ''; 2453 2454 if (msg.role === 'ai') { 2455 // Extract links and render as buttons 2456 const links = extractLinks(msg.content); 2457 const textWithoutLinks = formatMarkdown(removeLinksFromText(msg.content)); 2458 const linkButtons = renderLinkButtons(links); 2459 2460 return ` 2461 <div class="chatbot-message-wrapper ${msg.role}"> 2462 <div class="chatbot-message ${msg.role}"> 2463 <div class="chatbot-ai-header"> 2464 <div class="chatbot-ai-avatar" style="background: ${agent.color};">${agent.avatar}</div> 2465 <span class="chatbot-ai-name">${agent.name}</span> 2466 </div> 2467 <div class="chatbot-ai-content">${textWithoutLinks}${linkButtons}</div> 2468 ${pickerHtml} 2469 </div> 2470 ${reactionsHtml} 2471 </div> 2472 `; 2473 } 2474 return ` 2475 <div class="chatbot-message-wrapper ${msg.role}"> 2476 <div class="chatbot-message ${msg.role}">${safeContent}${pickerHtml}</div> 2477 ${reactionsHtml} 2478 </div> 2479 `; 2480 }).join(''); 2481 2482 // Add event listeners for reaction buttons 2483 container.querySelectorAll('.chatbot-add-reaction').forEach(btn => { 2484 btn.addEventListener('click', (e) => { 2485 e.stopPropagation(); 2486 const messageId = btn.dataset.messageId; 2487 const picker = container.querySelector(`.chatbot-reaction-picker[data-message-id="${messageId}"]`); 2488 // Hide all other pickers 2489 container.querySelectorAll('.chatbot-reaction-picker.show').forEach(p => p.classList.remove('show')); 2490 picker?.classList.toggle('show'); 2491 }); 2492 }); 2493 2494 container.querySelectorAll('.chatbot-reaction-btn').forEach(btn => { 2495 btn.addEventListener('click', (e) => { 2496 e.stopPropagation(); 2497 const messageId = btn.dataset.messageId; 2498 const emoji = btn.dataset.emoji; 2499 toggleReaction(messageId, emoji); 2500 container.querySelector(`.chatbot-reaction-picker[data-message-id="${messageId}"]`)?.classList.remove('show'); 2501 }); 2502 }); 2503 2504 container.querySelectorAll('.chatbot-reaction-badge').forEach(btn => { 2505 btn.addEventListener('click', (e) => { 2506 e.stopPropagation(); 2507 const messageId = btn.dataset.messageId; 2508 const emoji = btn.dataset.emoji; 2509 toggleReaction(messageId, emoji); 2510 }); 2511 }); 2512 2513 // Close picker when clicking outside 2514 document.addEventListener('click', () => { 2515 container.querySelectorAll('.chatbot-reaction-picker.show').forEach(p => p.classList.remove('show')); 2516 }); 2517 2518 container.scrollTop = container.scrollHeight; 2519 } 2520 2521 function renderQuickReplies(quickReplies) { 2522 // Remove existing quick replies 2523 const existing = document.querySelector('.chatbot-quick-replies'); 2524 if (existing) existing.remove(); 2525 2526 if (!quickReplies || quickReplies.length === 0) return; 2527 2528 const container = document.querySelector('.chatbot-messages'); 2529 if (!container) return; 2530 2531 const primaryColor = settings?.primary_color || '#6366f1'; 2532 2533 const wrapper = document.createElement('div'); 2534 wrapper.className = 'chatbot-quick-replies'; 2535 wrapper.innerHTML = quickReplies.map(reply => 2536 `<button class="chatbot-quick-reply" style="border-color: ${primaryColor}; color: ${primaryColor};">${escapeHtml(reply)}</button>` 2537 ).join(''); 2538 2539 container.appendChild(wrapper); 2540 container.scrollTop = container.scrollHeight; 2541 2542 // Add click handlers 2543 wrapper.querySelectorAll('.chatbot-quick-reply').forEach(btn => { 2544 btn.addEventListener('click', () => { 2545 const input = document.querySelector('.chatbot-input'); 2546 if (input) { 2547 input.value = btn.textContent; 2548 handleSend(input); 2549 } 2550 }); 2551 }); 2552 } 2553 2554 function showTyping() { 2555 const container = document.querySelector('.chatbot-messages'); 2556 if (!container) return; 2557 2558 const typing = document.createElement('div'); 2559 typing.className = 'chatbot-typing'; 2560 typing.innerHTML = '<span></span><span></span><span></span>'; 2561 container.appendChild(typing); 2562 container.scrollTop = container.scrollHeight; 2563 } 2564 2565 function hideTyping() { 2566 const typing = document.querySelector('.chatbot-typing'); 2567 if (typing) typing.remove(); 2568 } 2569 2570 async function handleSend(input) { 2571 const message = input.value.trim(); 2572 if (!message) return; 2573 2574 input.value = ''; 2575 messages.push({ role: 'visitor', content: message }); 2576 renderMessages(); 2577 2578 const primaryColor = settings?.primary_color || '#6366f1'; 2579 const textColor = settings?.text_color || '#ffffff'; 2580 const visitorMsg = document.querySelector('.chatbot-message.visitor:last-child'); 2581 if (visitorMsg) { 2582 visitorMsg.style.backgroundColor = primaryColor; 2583 visitorMsg.style.color = textColor; 2584 } 2585 2586 // Remove existing quick replies before sending 2587 const existingReplies = document.querySelector('.chatbot-quick-replies'); 2588 if (existingReplies) existingReplies.remove(); 2589 2590 showTyping(); 2591 const result = await sendMessage(message); 2592 hideTyping(); 2593 2594 messages.push({ role: 'ai', content: result.response }); 2595 renderMessages(); 2596 2597 // Show quick reply suggestions 2598 if (result.quick_replies && result.quick_replies.length > 0) { 2599 renderQuickReplies(result.quick_replies); 2600 } 2601 2602 } 2603 2604 async function showChatInterface(chatView) { 2605 const welcomeMessage = settings?.welcome_message || 'Hello! How can I help you today?'; 2606 2607 // Add messages container and input area to chat view 2608 const messagesDiv = document.createElement('div'); 2609 messagesDiv.className = 'chatbot-messages'; 2610 2611 const inputArea = document.createElement('div'); 2612 inputArea.className = 'chatbot-input-area'; 2613 inputArea.innerHTML = ` 2614 <input type="text" class="chatbot-input" placeholder="Type a message..." /> 2615 <button class="chatbot-send"> 2616 <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> 2617 <path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/> 2618 </svg> 2619 </button> 2620 `; 2621 2622 chatView.appendChild(messagesDiv); 2623 chatView.appendChild(inputArea); 2624 2625 // Load existing messages if we have a conversation, otherwise show wel
2625come 2626 if (conversationId) { 2627 // Show loading spinner while fetching 2628 messagesDiv.innerHTML = ` 2629 <div class="chatbot-loading"> 2630 <div class="chatbot-loading-spinner"></div> 2631 <span>Loading messages...</span> 2632 </div> 2633 `; 2634 2635 const existingMessages = await fetchExistingMessages(); 2636 if (existingMessages.length > 0) { 2637 messages = existingMessages; 2638 } else { 2639 messages = [{ role: 'ai', content: welcomeMessage }]; 2640 } 2641 2642 // Subscribe to conversation status changes 2643 subscribeToConversation(); 2644 } else { 2645 messages = [{ role: 'ai', content: welcomeMessage }]; 2646 } 2647 renderMessages(); 2648 2649 // Set up input listeners 2650 const input = inputArea.querySelector('.chatbot-input'); 2651 const sendBtn = inputArea.querySelector('.chatbot-send'); 2652 2653 sendBtn.addEventListener('click', () => handleSend(input)); 2654 input.addEventListener('keypress', (e) => { 2655 if (e.key === 'Enter') handleSend(input); 2656 }); 2657 2658 // Track visitor typing 2659 input.addEventListener('input', (e) => { 2660 const value = e.target.value; 2661 broadcastVisitorTyping(value.trim().length > 0); 2662 }); 2663 2664 // Subscribe to typing indicators 2665 subscribeToTyping(); 2666 } 2667 2668 function showPrechatForm(container) { 2669 const formHtml = ` 2670 <div class="chatbot-prechat-form"> 2671 <div> 2672 <h3 class="chatbot-prechat-title">Welcome! ð</h3> 2673 <p class="chatbot-prechat-subtitle">Please share some details so we can help you better.</p> 2674 </div> 2675 2676 <div class="chatbot-form-group"> 2677 <label class="chatbot-form-label">Name <span class="optional">(required)</span></label> 2678 <input type="text" class="chatbot-form-input" id="chatbot-name" placeholder="Your name" required /> 2679 </div> 2680 2681 <div class="chatbot-form-group"> 2682 <label class="chatbot-form-label">Email <span class="optional">(required)</span></label> 2683 <input type="email" class="chatbot-form-input" id="chatbot-email" placeholder="[email protected]" required /> 2684 </div> 2685 2686 <div class="chatbot-form-group"> 2687 <label class="chatbot-form-label">Phone <span class="optional">(optional)</span></label> 2688 <input type="tel" class="chatbot-form-input" id="chatbot-phone" placeholder="+1 (555) 000-0000" /> 2689 </div> 2690 2691 <div class="chatbot-form-group"> 2692 <label class="chatbot-form-label">Company <span class="optional">(optional)</span></label> 2693 <input type="text" class="chatbot-form-input" id="chatbot-company" placeholder="Company name" /> 2694 </div> 2695 2696 <div class="chatbot-form-group"> 2697 <label class="chatbot-form-label">Title/Role <span class="optional">(optional)</span></label> 2698 <input type="text" class="chatbot-form-input" id="chatbot-designation" placeholder="Your role" /> 2699 </div> 2700 2701 <div class="chatbot-form-actions"> 2702 <button class="chatbot-form-submit" id="chatbot-submit">Start Chat</button> 2703 </div> 2704 </div> 2705 `; 2706 2707 container.insertAdjacentHTML('beforeend', formHtml); 2708 2709 // Form submission handler 2710 const submitBtn = container.querySelector('#chatbot-submit'); 2711 const nameInput = container.querySelector('#chatbot-name'); 2712 const emailInput = container.querySelector('#chatbot-email'); 2713 const phoneInput = container.querySelector('#chatbot-phone'); 2714 const companyInput = container.querySelector('#chatbot-company'); 2715 const designationInput = container.querySelector('#chatbot-designation'); 2716 2717 submitBtn.addEventListener('click', async () => { 2718 // Validate required fields 2719 let hasError = false; 2720 2721 // Clear previous errors 2722 container.querySelectorAll('.chatbot-form-error').forEach(el => el.remove()); 2723 container.querySelectorAll('.chatbot-form-input.error').forEach(el => el.classList.remove('error')); 2724 2725 if (!nameInput.value.trim()) { 2726 nameInput.classList.add('error'); 2727 nameInput.insertAdjacentHTML('afterend', '<span class="chatbot-form-error">Name is required</span>'); 2728 hasError = true; 2729 } 2730 2731 if (!emailInput.value.trim()) { 2732 emailInput.classList.add('error'); 2733 emailInput.insertAdjacentHTML('afterend', '<span class="chatbot-form-error">Email is required</span>'); 2734 hasError = true; 2735 } else if (!isValidEmail(emailInput.value.trim())) { 2736 emailInput.classList.add('error'); 2737 emailInput.insertAdjacentHTML('afterend', '<span class="chatbot-form-error">Please enter a valid email</span>'); 2738 hasError = true; 2739 } 2740 2741 if (hasError) return; 2742 2743 // Save visitor info 2744 const info = { 2745 name: nameInput.value.trim(), 2746 email: emailInput.value.trim(), 2747 phone: phoneInput.value.trim() || null, 2748 company: companyInput.value.trim() || null, 2749 designation: designationInput.value.trim() || null 2750 }; 2751 saveVisitorInfo(info); 2752 2753 // Remove form and show chat interface 2754 const form = container.querySelector('.chatbot-prechat-form'); 2755 if (form) form.remove(); 2756 await showChatInterface(container); 2757 2758 // Update greeting with name 2759 const greeting = document.querySelector('.chatbot-home-greeting'); 2760 if (greeting) { 2761 greeting.innerHTML = `Hi ${info.name.split(' ')[0]} <span class="wave">ð</span><br/>How can we help?`; 2762 } 2763 }); 2764 2765 } 2766 2767 function isValidEmail(email) { 2768 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); 2769 } 2770 2771 function createWidget() { 2772 if (!document.body) { 2773 console.error('[ChatBot] document.body not available'); 2774 return; 2775 } 2776 2777 console.log('[ChatBot] Creating widget...'); 2778 2779 // Load any existing visitor info 2780 loadVisitorInfo(); 2781 2782 const container = document.createElement('div'); 2783 container.className = 'chatbot-widget-container'; 2784 2785 const position = settings?.position || 'right'; 2786 const name = settings?.name || 'Support'; 2787 const logoUrl = settings?.logo_url; 2788 const visitorName = visitorInfo?.name || 'there'; 2789 const firstName = visitorName.split(' ')[0]; 2790 2791 // Custom links with fallback defaults 2792 const customLinks = settings?.custom_links || [ 2793 { label: 'Homepage', url: '/', icon: 'home' }, 2794 { label: 'About Us', url: '/about', icon: 'info' }, 2795 { label: 'Contact', url: '/contact', icon: 'mail' } 2796 ]; 2797 2798 // Icon SVGs for links 2799 const linkIcons = { 2800 home: '<path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>', 2801 info: '<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>', 2802 mail: '<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>', 2803 phone: '<path d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042
280311.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>', 2804 help: '<path d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>', 2805 book: '<path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>', 2806 link: '<path d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>' 2807 }; 2808 2809 // Generate links HTML 2810 const linksHtml = customLinks.map(link => { 2811 const iconPath = linkIcons[link.icon] || linkIcons.link; 2812 const href = link.url.startsWith('http') ? link.url : window.location.origin + link.url; 2813 return ` 2814 <a href="${href}" target="_blank" class="chatbot-link-item"> 2815 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 2816 ${iconPath} 2817 </svg> 2818 <span>${link.label}</span> 2819 </a> 2820 `; 2821 }).join(''); 2822 2823 // Create bubble with chat and close icons 2824 const bubble = document.createElement('div'); 2825 bubble.className = 'chatbot-bubble'; 2826 bubble.style[position] = '20px'; 2827 bubble.innerHTML = ` 2828 <svg class="chat-icon" fill="#fff" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> 2829 <path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/> 2830 </svg> 2831 <svg class="close-icon" fill="#fff" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> 2832 <path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/> 2833 </svg> 2834 `; 2835 2836 // Create window 2837 const chatWindow = document.createElement('div'); 2838 chatWindow.className = 'chatbot-window'; 2839 chatWindow.style[position] = '20px'; 2840 2841 // Logo HTML 2842 const logoHtml = logoUrl 2843 ? `<img src="${logoUrl}" alt="${name}" />` 2844 : `<svg viewBox="0 0 24 24" fill="#1a1a1a"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>`; 2845 2846 // Build the home view 2847 chatWindow.innerHTML = ` 2848 <!-- Home View --> 2849 <div class="chatbot-home-view"> 2850 <div class="chatbot-home-header"> 2851 <div class="chatbot-home-header-top"> 2852 <div class="chatbot-home-logo">${logoHtml}</div> 2853 <button class="chatbot-home-close"> 2854 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 2855 <path d="M6 18L18 6M6 6l12 12"/> 2856 </svg> 2857 </button> 2858 </div> 2859 <h2 class="chatbot-home-greeting">Hi ${firstName} <span class="wave">ð</span><br/>How can we help?</h2> 2860 </div> 2861 2862 <div class="chatbot-home-content"> 2863 <div class="chatbot-action-card" id="chatbot-send-message"> 2864 <div class="chatbot-action-card-content"> 2865 <h4 class="chatbot-action-card-title">Send us a message</h4> 2866 <p class="chatbot-action-card-subtitle">We typically reply in a few minutes</p> 2867 </div> 2868 <div class="chatbot-action-card-icon"> 2869 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 2870 <path d="M5 12h14M12 5l7 7-7 7"/> 2871 </svg> 2872 </div> 2873 </div> 2874 2875 2876 <!-- Quick Questions Section --> 2877 <div class="chatbot-section-label">Popular questions</div> 2878 <div class="chatbot-quick-questions"> 2879 <button class="chatbot-quick-question" data-question="What services do you offer?"> 2880 <span class="chatbot-qq-icon">ð¼</span> 2881 <span>What services do you offer?</span> 2882 </button> 2883 <button class="chatbot-quick-question" data-question="How can I get started?"> 2884 <span class="chatbot-qq-icon">ð</span>
2885 <span>How can I get started?</span> 2886 </button> 2887 <button class="chatbot-quick-question" data-question="What are your pricing plans?"> 2888 <span class="chatbot-qq-icon">ð°</span> 2889 <span>What are your pricing plans?</span> 2890 </button> 2891 <button class="chatbot-quick-question" data-question="How do I contact support?"> 2892 <span class="chatbot-qq-icon">ð</span> 2893 <span>How do I contact support?</span> 2894 </button> 2895 </div> 2896 2897 <!-- Useful Links Section --> 2898 <div class="chatbot-section-label">Useful links</div> 2899 <div class="chatbot-links-section"> 2900 ${linksHtml} 2901 </div> 2902 2903 <div class="chatbot-status-card"> 2904 <div class="chatbot-status-icon"> 2905 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"> 2906 <path d="M20 6L9 17l-5-5"/> 2907 </svg> 2908 </div> 2909 <div class="chatbot-status-text"> 2910 <h5 class="chatbot-status-title">Status: All Systems Operational</h5> 2911 <p class="chatbot-status-subtitle">Updated ${new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</p> 2912 </div> 2913 </div> 2914 2915 <div class="chatbot-powered-by"> 2916 Created by <a href="https://aimmediahouse.com" target="_blank" rel="noopener">AIM Media House</a> 2917 </div> 2918 </div> 2919 2920 <div class="chatbot-bottom-nav"> 2921 <button class="chatbot-nav-item active" data-view="home"> 2922 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 2923 <path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/> 2924 </svg> 2925 Home 2926 </button> 2927 <button class="chatbot-nav-item" data-view="messages"> 2928 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 2929 <path d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/> 2930 </svg> 2931 Messages 2932 ${messages.length > 0 ? '<span class="chatbot-nav-badge">' + messages.length + '</span>' : ''} 2933 </button> 2934 </div> 2935 </div> 2936 2937 <!-- Chat/Messages View --> 2938 <div class="chatbot-chat-view"> 2939 <div class="chatbot-chat-header"> 2940 <button class="chatbot-chat-back"> 2941 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 2942 <path d="M15 19l-7-7 7-7"/> 2943 </svg> 2944 </button> 2945 <h3 class="chatbot-chat-header-title">${name}</h3> 2946 <button class="chatbot-end-chat-btn" title="End Chat"> 2947 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 2948 <path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4M16 17l5-5-5-5M21 12H9"/> 2949 </svg> 2950 </button> 2951 </div> 2952 </div> 2953 `; 2954 2955 // Get the chat view and set up chat interface 2956 const chatView = chatWindow.querySelector('.chatbot-chat-view'); 2957 2958 // Check if we need pre-chat form or direct chat 2959 if (hasCompletedForm && visitorInfo) { 2960 showChatInterface(chatView).catch(err => console.error('[ChatBot] Error loading chat:', err)); 2961 } else { 2962 // Show pre-chat form in chat view 2963 showPrechatForm(chatView); 2964 } 2965 2966 container.appendChild(bubble); 2967 container.appendChild(chatWindow); 2968 document.body.appendChild(container); 2969 2970 console.log('[ChatBot] Widget appended to body'); 2971 2972 // Event listeners 2973 bubble.addEventListener('click', toggleWindow); 2974 chatWindow.querySelector('.chatbot-home-close').addEventListener('click', toggleWindow); 2975 2976 // Navigation 2977 chatWindow.querySelector('#chatbot-send-message').addEventListener('click', switchToMessages); 2978 chatWindow.querySelector('.chatbot-nav-item[data-view="home"]').addEventListener('click', switchToHome); 2979 chatWindow.querySelector('.chatbot-nav-item[data-view="messages"]').addEventListener('click', switchToMessages); 2980 chatWindow.querySelector('.chatbot-chat-back').addEventListener('click', switchToHome); 2981 chatWindow.querySelector('.chatbot-end-chat-btn').addEventListener('click', endChat); 2982 2983 2984 // Quick Questions - click to send as message 2985 chatWindow.querySelectorAll('.chatbot-quick-question').forEach(btn => { 2986 btn.addEventListener('click', async () => { 2987 const question = btn.getAttribute('data-question'); 2988 if (question) { 2989 // Switch to messages view 2990 switchToMessages(); 2991 2992 // Wait a bit for the chat interface to be ready 2993 setTimeout(async () => { 2994 const input = document.querySelector('.chatbot-input'); 2995 if (input) { 2996 input.value = question; 2997 handleSend(input); 2998 } 2999 }, 100); 3000 } 3001 }); 3002 }); 3003 3004 // Trigger auto-discovery after widget loads 3005 setTimeout(() => { 3006 discoverPageData(); 3007 }, 2000); 3008 3009 // Start teaser timer 3010 startTeaserTimer(); 3011 } 3012 3013 function processCommand(action, id) {
3014 if (action === 'init') { 3015 console.log('[ChatBot] Init called with ID:', id); 3016 3017 function init() { 3018 console.log('[ChatBot] Initializing...'); 3019 widgetId = id; 3020 visitorId = getVisitorId(); 3021 conversationId = loadConversationId(); 3022 console.log('[ChatBot] Loaded conversation ID:', conversationId); 3023 3024 createStyles(); 3025 3026 3027 // Fetch location first (async) 3028 fetchVisitorLocation().then(() => { 3029 fetchSettings(id).then(data => { 3030 console.log('[ChatBot] Settings loaded:', data); 3031 if (!data) { 3032 console.error('[ChatBot] Failed to load widget settings - check widget ID'); 3033 return; 3034 } 3035 settings = data; 3036 createWidget(); 3037 3038 // Start visitor presence heartbeat 3039 startHeartbeat(); 3040 }).catch(error => { 3041 console.error('[ChatBot] Error fetching settings:', error); 3042 }); 3043 }); 3044 } 3045 3046 // Wait for DOM to be ready 3047 if (document.readyState === 'loading') { 3048 console.log('[ChatBot] Waiting for DOM...'); 3049 document.addEventListener('DOMContentLoaded', init); 3050 } else { 3051 init(); 3052 } 3053 } 3054 } 3055 3056 // Process any queued commands from the embed snippet 3057 const queue = window.chatbot && window.chatbot.q; 3058 if (queue && Array.isArray(queue)) { 3059 console.log('[ChatBot] Processing queued commands:', queue.length); 3060 queue.forEach(function(args) { 3061 processCommand.apply(null, args); 3062 }); 3063 } 3064 3065 // Set up chatbot function for future calls 3066 window.chatbot = function(action, id) { 3067 processCommand(action, id); 3068 }; 3069 3070 console.log('[ChatBot] Script loaded'); 3071})();
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.