PageSourceSearch

https://furie.ai/furie.js?propertyId=29

js furie.ai collected 2026-10-05 15:13:30 UTC 105,642 bytes, 3,034 lines download raw bytes

1
2
3(function () {
4
5        // --- SETUP: CONSTANTS AND STATE VARIABLES ---
6
7        const currentScript = document.currentScript || (function () {
8 
9                const scripts = document.getElementsByTagName('script');
10
11                return scripts[scripts.length - 1];
12
13        })();
14
15
16
17        const urlParams = new URLSearchParams(currentScript.src.split('?')[1] || '');
18
19        const propertyId = urlParams.get("propertyId") || "";
20
21        const apiEndpoint = "https://www.furie.ai/furie.php";
22        
23        const globalImgUrl = 'https://www.furie.ai/img/'; 
24        
25        
26        // 1. DEFINE THE DEFAULT PROMO PHRASES
27    const DEFAULT_PROMO_PHRASES = [
28        "I never sleep.",
29        "I have answers.",
30        "I'll solve ur ?",
31        "I know info.",
32        "Need a pointer?",
33        "Happy to help!",
34        "I share the ❤️!",
35        "I know things.",
36        "I share info.",
37        "I spark convos.",
38        "I find discounts.",
39        "I'm free to use.",
40        "I'll help you out.",
41        "I guide you to savings.",
42        "I spot sales.",
43        "I ease your day.",
44        "I handle it!",
45        "I'm here for you.",
46        "I have ideas.",
47        "Why wait?",
48        "Happy to help!",
49        "Click here now.",
50        "Get Furie™ free."
51    ];
52    
53    // 2. INITIALIZE THE WORKING LIST WITH DEFAULTS
54    
55    const getStorageKey = (key) => `${key}_${propertyId}`;
56    
57    let promoPhrases = [...DEFAULT_PROMO_PHRASES]; 
58
59    let isProductPlacementEnabled = true; 
60    let isCustomHooksActive = false; // Tracks if we are using AI/Custom messages
61    let isLiveChatActive = false; 
62    let isProactiveEnabled = false;
63    let isGdprEnabled = false; 
64    let privacyPolicyUrl = "#";
65    let isGdprConsented = localStorage.getItem(getStorageKey("furrieGdprConsent")) === "1";
66    // If GDPR is on AND they haven't consented yet, we block features
67    let isGdprBlocked = false;
68    let pendingProactivePayload = null;    
69    
70    let furieSessionId = localStorage.getItem(getStorageKey("furieSessionId")) || null;
71    let liveChatSessionId = null; // Ensure this is defined
72        
73// ── Audio (chirp) & mute state ───────────────────────
74        const agentChirp = new Audio('https://www.furie.ai/sounds/chirp.mp3');   
75        agentChirp.preload = 'auto';
76
77// Image urls
78const baseImgUrl = `https://www.furie.ai/img/${propertyId}/`;
79const defaultImgUrl = 'https://www.furie.ai/img/default/'; // Fallback URL
80
81const imageUrls = [
82    baseImgUrl + 'logo.png',
83    baseImgUrl + 'bot.png',
84    baseImgUrl + 'agent.png'
85];
86
87// Preload the three
88preloadImages(imageUrls);
89
90
91
92// --- PROTECT WIDGET SVGs FROM HOST CSS ---
93const svgProtectionStyle = document.createElement('style');
94svgProtectionStyle.textContent = `
95  /* 1. Reset SVG defaults so host CSS doesn't hide them */
96  #furrie-chatbot button svg {
97    width: 24px !important;
98    height: 24px !important;
99    min-width: 24px !important;
100    min-height: 24px !important;
101    display: block !important;
102    visibility: visible !important;
103    opacity: 1 !important;
104    fill: currentColor !important; /* Force it to use the text color */
105  }
106
107  /* 2. Ensure paths inherit the fill correctly */
108  #furrie-chatbot button svg path {
109    fill: currentColor !important;
110    stroke: none !important; /* Prevent external strokes from ruining icons */
111    opacity: 1 !important;
112    visibility: visible !important;
113  }
114
115  /* 3. Special handling for stroked icons (like the 'mute' line) */
116  #furrie-chatbot button svg line {
117    stroke: currentColor !important;
118    stroke-width: 2px !important;
119  }
120
121  /* 4. Stop buttons from being squashed by Flexbox */
122  #furrie-chatbot button {
123    flex-shrink: 0 !important; /* Don't let long titles squash buttons */
124    box-sizing: border-box !important;
125  }
126  
127  #furrie-tts-toggle svg path {
128      fill: currentColor !important;
129      stroke: none !important;
130      opacity: 1 !important; /* Force path opacity to 1, rely on button opacity */
131  }
132  
133  /* Ensure the strikethrough line is visible */
134  #furrie-tts-toggle svg line {
135      stroke: currentColor !important;
136      stroke-width: 2px !important;
137      opacity: 1 !important;
138  }
139  
140
141
142/* --- BUBBLE BASE CLASS --- */
143.furie-chat-bubble-base {
144    padding: 10px 14px !important;
145    word-wrap: break-word !important;
146    /* CRITICAL FIX FOR SPACING: Preserves spaces and line breaks */
147    white-space: pre-wrap !important; 
148    line-height: 1.4em !important;
149    border: 1px solid #EAEFF2 !important;
150    box-shadow: none !important;
151    margin: 0 !important;
152    max-width: 100% !important;
153    font-family: 'Quicksand', sans-serif !important;
154    font-size: 14px !important;
155}
156
157/* --- GUEST BUBBLE (You) --- */
158.furie-bubble-guest {
159    /* Use CSS Variable for color (set in JS below) */
160    background-color: var(--furie-guest-bg, #785CC2) !important; 
161    color: var(--furie-guest-fg, #ffffff) !important;
162    border-radius: 25px 25px 5px 25px !important;
163    margin-left: auto !important; /* Pushes to right */
164}
165
166/* --- BOT/AGENT BUBBLE --- */
167.furie-bubble-bot {
168    background-color: #ffffff !important;
169    color: #444444 !important;
170    border-radius: 25px 25px 25px 5px !important;
171    margin-right: auto !important; /* Pushes to left */
172}
173`;
174document.head.appendChild(svgProtectionStyle);
175
176
177
178
179// --- CSS ISOLATION & STYLE DEFINITIONS ---
180const furieStyle = document.createElement('style');
181furieStyle.textContent = `
182  /* 1. DEFINE VARIABLES */
183  :root {
184      --furie-guest-bg: #785CC2; 
185      --furie-guest-fg: #ffffff;
186      --furie-bot-bg: #ffffff;
187      --furie-bot-fg: #444444;
188  }
189
190  /* 2. BASE BUBBLE STYLE */
191  .furie-chat-bubble-base {
192      /* Standard spacing */
193      padding: 10px 14px !important;
194      border-radius: 25px !important;
195      border: 1px solid #EAEFF2 !important;
196      box-shadow: 0 1px 2px rgba(0,0,0,0.05) !important;
197      
198      /* LAYOUT PROTECTION */
199      max-width: 85% !important;
200      display: block !important;       
201      white-space: pre-wrap !important; 
202      word-wrap: break-word !important;
203      min-width: 85%;
204      
205      /* TYPOGRAPHY FIXES */
206      line-height: 1.5 !important;      
207      font-family: 'Quicksand', sans-serif !important;
208      
209      /* FIX 1: Use 'inherit' so the Text Size Toggle works */
210      font-size: inherit !important; 
211      
212      text-align: left !important;
213      letter-spacing: normal !important; 
214      margin: 0 !important;
215      box-sizing: border-box !important;
216      
217      /* VISIBILITY FIX: Ensure parent is visible so letters can fade in */
218      opacity: 1 !important; 
219  }
220
221  /* 3. VARIANT STYLES */
222  .furie-bubble-guest {
223      background-color: var(--furie-guest-bg) !important;
224      color: var(--furie-guest-fg) !important;
225      border-bottom-right-radius: 5px !important;
226      margin-left: auto !important; 
227      min-width: 85%;
228  }
229
230  .furie-bubble-bot {
231      background-color: var(--furie-bot-bg) !important;
232      color: var(--furie-bot-fg) !important;
233      border-bottom-left-radius: 5px !important;
234      margin-right: auto !important;
235      min-width: 85%;
236  }
237
238  /* 4. ANIMATION LOGIC */
239  .fade-in-letter {
240      opacity: 0;
241      transition: opacity 0.4s ease-in-out;
242      display: inline !important; 
243      white-space: pre-wrap !important;
244      position: static !important;
245      width: auto !important;
246  }
247
248  /* REVEAL STATE (Visible) */
249  .furie-chat-bubble-base.visible .fade-in-letter {
250      opacity: 1 !important;
251  }
252  
253  /* 5. LIST FIXES */
254  .furie-chat-bubble-base ul, 
255  .furie-chat-bubble-base ol {
256      margin: 5px 0 5px 20px !important;
257      padding: 0 !important;
258      list-style: disc !important;
259  }
260  .furie-chat-bubble-base li {
261      display: list-item !important;
262      margin-bottom: 4px !important;
263  }
264
265  /* 6. FIX 2: COMPACT THINKING BUBBLE */
266  /* This forces the bubble to shrink when showing the dots */
267  .furie-chat-bubble-base.furie-compact {
268      padding: 6px 14px !important; /* Smaller padding */
269      min-height: 0 !important;     /* Reset height */
270      height: auto !important;
271      width: fit-content !important; /* Shrink to fit dots */
272      line-height: 1 !important;
273  }
274  
275  /* Reset inner span margins for the dots */
276  .furie-chat-bubble-base.furie-compact > span {
277      margin: 0 !important; 
278      display: inline-flex !important;
279  }
280
281  .furie-dots i {
282      font-style: normal !important;
283      font-weight: 800 !important;
284      margin: 0 2px !important;
285      animation: furieWave 1.0s ease-in-out infinite;
286      display: inline-block !important;
287  }
288  
289  @keyframes furieWave {
290    0%,100% { transform: translateY(0); opacity:.35; }
291    50%     { transform: translateY(-4px); opacity:1;   }
292  }
293`;
294document.head.appendChild(furieStyle);
295
296
297
298
299
300const hexToCss = (c) => (!c ? null : (c.startsWith('#') ? c : `#${c}`));
301
302// YIQ contrast helper for readable text on custom guest bubble
303function idealTextColor(hex) {
304  const h = (hex || '').replace('#','');
305  if (!/^[0-9a-fA-F]{6}$/.test(h)) return '#ffffff';
306  const r = parseInt(h.substr(0,2),16);
307  const g = parseInt(h.substr(2,2),16);
308  const b = parseInt(h.substr(4,2),16);
309  const yiq = (r*299 + g*587 + b*114) / 1000;
310  return yiq >= 128 ? '#000000' : '#ffffff';
311}
312
313function applyUi(ui) {
314  if (!ui) return;
315
316
317// --- CAPTURE GDPR SETTINGS ---
318  if(ui.gdpr_enabled !== undefined) {
319      isGdprEnabled = (parseInt(ui.gdpr_enabled) === 1);
320  }
321  if(ui.property_privacy_policy_url) {
322      privacyPolicyUrl = ui.property_privacy_policy_url;
323  }
324
325// If the server sends a 1, enable it. Otherwise, keep it false.
326  if (ui.proactive_msgs_toggle !== undefined) {
327      isProactiveEnabled = (parseInt(ui.proactive_msgs_toggle) === 1);
328  }
329
330// --- DYNAMIC POSITIONING LOGIC ---
331if (ui.px_from_bottom !== undefined || ui.px_from_right !== undefined) {
332
333const bottom = parseInt(ui.px_from_bottom !== undefined ? ui.px_from_bottom : 72);
334const right  = parseInt(ui.px_from_right !== undefined ? ui.px_from_right : 20);
335
336
337    const containerBottom = bottom + 10; 
338
339// 1. Update Launcher Wrapper
340const wrapper = document.getElementById('furrie-launcher-wrapper');
341if (wrapper) {
342    // A. Apply the database position (while still invisible)
343    wrapper.style.bottom = `${bottom}px`;
344    wrapper.style.right = `${right}px`;
345
346    // B. Trigger the Fade-In Animation
347    // We use a tiny timeout to ensure the DOM has updated the 'bottom' position first
348    requestAnimationFrame(() => {
349        wrapper.style.opacity = "1";
350        wrapper.style.transform = "translateY(0)"; // Slide up to final position
351    });
352}
353
354const docStyle = document.documentElement.style;
355
356  if (ui.guest_color) {
357      const gb = hexToCss(ui.guest_color);
358      if (gb) {
359          docStyle.setProperty('--furie-guest-bg', gb);
360          docStyle.setProperty('--furie-guest-fg', idealTextColor(gb));
361      }
362  }
363
364// 2. Update Chat Window Container (same as before)
365const chatContainer = document.getElementById('furrie-chatbot');
366if (chatContainer) {
367    if (window.innerWidth > 768) {
368        chatContainer.style.bottom = `${bottom + 10}px`;
369        chatContainer.style.right = `${right}px`;
370        chatContainer.style.maxHeight = `calc(100vh - ${bottom + 30}px)`;
371    }
372    }
373}
374
375
376
377
378
379
380// --- PROMO PILL IMAGE LOGIC ---
381// Get the image element
382const pillImg = document.querySelector('#furrie-promo-pill img');
383
384if (pillImg) {
385    // 1. Determine if the image feature is active (default to true if undefined)
386    const isImageActive = (ui.promo_pill_image_active !== undefined) 
387                          ? (parseInt(ui.promo_pill_image_active) === 1) 
388                          : true;
389
390    if (!isImageActive) {
391        // CASE A: Feature is OFF -> Hide the image completely
392        pillImg.style.display = 'none';
393    } else {
394        // CASE B: Feature is ON -> Ensure visible
395        pillImg.style.display = 'inline-block'; 
396
397        if (ui.promo_pill_image_url) {
398            // Sub-case: Custom Image Provided -> Swap SRC and adjust styling
399            pillImg.src = ui.promo_pill_image_url;
400            pillImg.style.height = "20px"; // Larger for custom icons
401            pillImg.style.width = "auto";
402            pillImg.style.objectFit = "contain";
403            pillImg.style.marginLeft = "auto"; 
404        } else {
405            // Sub-case: Active, but no custom image -> Revert to Default Logo
406            // Note: globalImgUrl is defined at the top of your script
407            pillImg.src = globalImgUrl + 'furie_logo.png';
408            pillImg.style.height = "20px"; // Standard size
409            pillImg.style.width = "auto";
410            pillImg.style.borderRadius = "0px;";
411            pillImg.style.objectFit = "contain";
412            pillImg.style.marginLeft = "auto";
413        }
414    }
415}
416
417
418
419
420
421// --- PROMO PILL VISIBILITY TOGGLE ---
422  // Use the new DB field 'promo_pops_toggle'. Default to 1 (true) if undefined.
423  const showPill = (ui.promo_pops_toggle !== undefined) ? (ui.promo_pops_toggle == 1) : 1;
424  
425  // Update global state variable (used elsewhere in rotation logic)
426  isProductPlacementEnabled = showPill;
427        
428  if (typeof promoPill !== 'undefined') {
429       // Hide or Show based on database value
430       promoPill.style.display = showPill ? 'flex' : 'none';
431  }
432
433    if (ui.promo_pill_image_url) {
434      const pillImg = document.querySelector('#furrie-promo-pill img');
435      if (pillImg) {
436          pillImg.src = ui.promo_pill_image_url;
437          // Reset styles specific to the logo to ensure custom icon fits well
438          pillImg.style.height = "14px"; // Slightly larger for custom icons
439          pillImg.style.width = "auto";
440          pillImg.style.objectFit = "contain";
441      }
442    }
443
444
445
446
447  // Title text/content
448  const headerTitleEl = document.getElementById('furrie-title');
449  if (headerTitleEl) {
450    if (ui.chat_header_name) headerTitleEl.textContent = ui.chat_header_name;
451    const hdrTxt = hexToCss(ui.header_text_color) || '#555555';
452    headerTitleEl.style.setProperty('color', hdrTxt, 'important');
453  }
454
455  // Header/container background
456  const headerBar = container.querySelector('div[style*="justify-content: space-between"]');
457  if (ui.header_background_color) {
458    const bg = hexToCss(ui.header_background_color);
459    if (bg) {
460      container.style.backgroundColor = bg;
461      if (headerBar) headerBar.style.backgroundColor = bg;
462    }
463  }
464
465
466
467const bubbleFadeStyle = document.createElement('style');
468bubbleFadeStyle.textContent = `
469  .chat-bubble {
470    opacity: 0 !important;
471    transform: translateY(6px) !important;
472    transition: opacity 0.5s ease, transform 0.5s ease !important;
473    
474  }
475  .chat-bubble ul {
476  margin: 0 !important;
477  padding-left: 20px !important;
478}
479
480.chat-bubble li {
481  margin: 0 !important;
482  padding: 0 !important;
483  list-style: none !important; 
484  line-height: 1.2 !important; 
485}
486  .chat-bubble.visible {
487    opacity: 1 !important;
488    transform: translateY(0) !important;
489  }
490  
491  
492/* Hide native bullets; we’ll provide our own animated marker */
493#furrie-messages ul !important,
494#furrie-messages ol { list-style: none !important; padding-left: 18px !important; }
495#furrie-messages li { position: relative !important; }  
496  
497`;
498document.head.appendChild(bubbleFadeStyle);
499
500
501
502const letterFadeStyle = document.createElement('style');
503letterFadeStyle.textContent = `
504  /* Each letter will be a span with these base properties */
505  .fade-in-letter {
506    opacity: 0;
507    transition: opacity 0.4s ease-in-out;
508  }
509
510  /* When the parent bubble gets the 'visible' class, the letters fade in */
511  /* The transition-delay on each span will stagger the effect */
512  .bubble.visible .fade-in-letter {
513    opacity: 1 !important;
514  }
515`;
516document.head.appendChild(letterFadeStyle);
517    
518  // Chat window border
519  if (ui.border_color) {
520    const bc = hexToCss(ui.border_color);
521    const msg = document.getElementById('furrie-messages');
522    if (msg && bc) {
523      msg.style.borderTop = `3px solid ${bc}`;
524      msg.style.borderBottom = `3px solid ${bc}`;
525    }
526  }
527
528  // Submit (send) button color + readable text
529  if (ui.button_color) {
530    const submitBtn = document.getElementById('furrie-submit') || document.querySelector('#furrie-form button[type="submit"]');
531    const c = hexToCss(ui.button_color);
532    window.__furie_button_color = c;
533    if (submitBtn && c) {
534      submitBtn.style.setProperty('background-color', idealTextColor(c), 'important');
535      submitBtn.style.setProperty('color', c, 'important');
536    }
537  }
538
539if (ui.button_color) {
540  const micBtn = document.getElementById('furrie-mic');
541  const c = hexToCss(ui.button_color);
542  if (micBtn && c) {
543    micBtn.style.backgroundColor = idealTextColor(c);
544    micBtn.style.color = c; // auto contrast
545    micBtn.style.borderRadius = "50%"; // optional, to match send button
546  }
547}
548
549  // Guest bubble background + auto-contrast
550  if (ui.guest_color) {
551    const gb = hexToCss(ui.guest_color);
552    if (gb) {
553      window.__furie_guest_bubble_bg = gb;
554      window.__furie_guest_bubble_fg = idealTextColor(gb);
555    }
556  }
557
558  // Redirect buttons
559  if (ui.redirect_button_color) {
560    window.__furie_redirect_btn_color = hexToCss(ui.redirect_button_color);
561  }
562  
563  
564  
565  
566  // --- FORCE UPDATE BUTTON COLORS ---
567  if (ui.header_text_color) {
568    const textColor = hexToCss(ui.header_text_color);
569    
570    // 1. Update TTS Toggle
571    const ttsBtn = document.getElementById('furrie-tts-toggle');
572    if (ttsBtn) {
573        ttsBtn.style.setProperty('color', textColor, 'important');
574        // Reset opacity based on active state (stored in local var or check icon)
575        const isReadAloud = localStorage.getItem("furrieReadAloud") === "1";
576        ttsBtn.style.opacity = isReadAloud ? "1" : "0.7"; 
577    }
578
579    // 2. Update Mute Button
580    const muteBtn = document.getElementById('furrie-mute');
581    if (muteBtn) {
582        muteBtn.style.setProperty('color', textColor, 'important');
583    }
584    
585    // 3. Update Font Toggle
586    const fontBtn = document.getElementById('furrie-font-toggle');
587    if (fontBtn) {
588        fontBtn.style.setProperty('color', textColor, 'important');
589    }
590  }
591  
592  
593}
594
595
596
597
598/**
599 * Smoothly scrolls the chat window to the bottom over a set duration.
600 * @param {number} duration - in ms
601 */
602function smoothScrollBottom(duration = 700) {
603  const start = messagesDiv.scrollTop;
604  const end = messagesDiv.scrollHeight - messagesDiv.clientHeight;
605  const startTime = performance.now();
606
607  function step(now) {
608    const progress = Math.min(1, (now - startTime) / duration);
609    // easeInOutQuad
610    const eased = progress < 0.5
611      ? 2 * progress * progress
612      : -1 + (4 - 2 * progress) * progress;
613
614    messagesDiv.scrollTop = start + (end - start) * eased;
615    if (progress < 1) requestAnimationFrame(step);
616  }
617  requestAnimationFrame(step);
618}
619
620
621
622function preloadImages(urls) {
623    urls.forEach(url => {
624        const img = new Image();
625        img.src = url;
626    });
627}
628
629
630
631preloadImages(imageUrls);
632
633
634const waveStyle = document.createElement('style');
635waveStyle.textContent = `
636  .furie-compact { padding: 2px 2px !important; line-height: 1; padding: 0px; margin-bottom: 10px;}
637  .furie-dots { display:inline-flex; white-space:nowrap; line-height:1; padding:0px; margin-right: 16px; margin-left: 10px; }
638    /* soft pulse so it’s not jarring */
639  .furie-pulse { animation: furiePulse 1.6s ease-in-out infinite; }
640  @keyframes furiePulse {
641    0%,100% { opacity:.7 }
642    50%     { opacity:1 }
643  }
644  .furie-dots i {
645    display:inline-block;
646    font-style:normal;
647    font-weight: 800;
648    opacity:.35;
649    font-size: 16px;
650    animation: furieWave 1.0s ease-in-out infinite;
651    margin: 0 2px;
652    padding: 0px;
653  }
654  @keyframes furieWave {
655    0%,100% { transform: translateY(0);   opacity:.35; }
656    50%     { transform: translateY(-10px); opacity:1;   }
657  }
658`;
659document.head.appendChild(waveStyle);
660
661
662        // Live Chat State
663
664let sessionStatus = 'uninitialized'; // -> 'Browse', 'bot', 'live'
665
666
667
668        let liveChatApiEndpoint = '';
669
670        let liveChatPropertyId = null;
671
672        let pusherInstance = null;
673
674        let pusherChannel = null;
675
676        let isMuted   = (localStorage.getItem('furrieMuted') === '1');  // remember choice
677        agentChirp.muted = isMuted;
678
679        let notificationInterval = null;
680        const originalTitle = document.title;
681
682        // User Info State
683
684let userName = localStorage.getItem("furrieUserName") || "";
685let userEmail = localStorage.getItem("furrieUserEmail") || "";
686let existingGuestUid = localStorage.getItem("furrieGuestUid") || null;
687        let infoCollected = localStorage.getItem(getStorageKey("furrieAskedInfo"));
688
689
690
691
692/**
693 * Generates a random, memorable name for a guest.
694 * @returns {string} A name like 'KindFox451'
695 */
696function generateMemorableName() {
697    
698        // ============================
699    //        SAFE ADJECTIVES
700    // ============================
701    $adjectives = [
702
703        // 3-Letter (Colors / Qualities)
704        'Red','Blue','Tan','Ash','Bay','Gray','Grey','Ice','Jet','Pink','Gold',
705        'Big','New','Hot','Dry','Wet','Top','Fit','Key','Zen','Apt',
706
707        // 4-Letter Colors
708        'Aqua','Mint','Plum','Ruby','Teal','Jade','Onyx','Rose','Cyan',
709
710        // 4-Letter Qualities (Positive / Neutral)
711        'Kind','Calm','Brave','Good','Nice','Warm','Cool','Rich','Mild','Wild',
712        'Busy','Easy','Fine','Free','Full','Huge','Keen','Neat','Open','Pure',
713        'Real','Soft','Sure','Tall','Thin','Tiny','True','Vast','Wide','Wise',
714        'Deep','Fast','Pale','Bold','Chic','Cozy','Cute','Fair','Fond','Glad',
715        'Hazy','High','Just','Live','Lush','Mega','Meek','Okay','Oval',
716        'Peak','Posh','Prim','Safe','Same','Slim','Solo','Tidy','Twin','Used',
717        'Zany'
718    ];
719
720    // ============================
721    //          SAFE NOUNS
722    // ============================
723    $nouns = [
724
725        // 3-Letter Animals
726        'Fox','Owl','Dog','Cat','Bug','Ant','Ape','Bat','Bee','Elk','Emu','Pig',
727        'Cow','Hen','Ram','Rat','Yak','Cod','Ray','Eel','Fly','Gnu','Jay','Kit',
728        'Koi','Pup',
729
730        // 4-Letter Animals
731        'Lion','Puma','Wolf','Bear','Hawk','Goat','Duck','Frog','Fish','Bird',
732        'Crab','Deer','Dove','Gull','Hare','Lamb','Lark','Mole','Moth','Mule',
733        'Newt','Seal','Swan','Toad','Tuna','Wasp','Wren','Ibex','Kudu','Ling',
734        'Lynx','Boar','Colt','Coot','Crow','Dace','Dodo','Drake','Fawn','Flea',
735        'Fowl','Gnat','Goby','Hake','Hart','Kite','Kiwi','Mako','Mink','Mite',
736        'Oryx','Pony','Pupa','Rook','Sora','Stag','Tahr','Tern','Tick',
737        'Zebu',
738
739        // 3-Letter Nature/Objects (Safe Only)
740        'Sky','Ark','Axe','Bag','Bar','Bed','Bin','Box','Bud','Bus','Can','Cap',
741        'Car','Cog','Cot','Cup','Dam','Den','Dew','Dot','Egg','Era','Fan','Fig',
742        'Fin','Fir','Fog','Fun','Gap','Gas','Gem','Gum','Hat','Hay','Hut','Ice',
743        'Ink','Jar','Jet','Jug','Key','Kit','Lab','Lag','Lap','Lid','Lip','Log',
744        'Lot','Map','Mat','Mop','Mug','Net','Nib','Oak','Oar','Oil','Orb','Ore',
745        'Pad','Pal','Pan','Pea','Peg','Pen','Pet','Pie','Pin','Pit','Pod','Pop',
746        'Pot','Row','Rub','Rug','Run','Rut','Sap','Saw','Sea','Set','Sip','Sit',
747        'Sow','Spa','Spy','Sub','Sum','Sun','Tab','Tag','Tap','Tar','Tea','Ten',
748        'Tie','Tin','Tip','Toe','Ton','Toy','Tub','Tug','Urn','Use','Van','Vat',
749        'Vet','Wax','Web','Win','Yen','Yep','Yet','Zip','Zoo',
750
751        // 4-Letter Nature/Objects (Safe Only)
752        'Aura','Boat','Book','Bush','Cave','Clay','Core','Cove','Cube','Dawn',
753        'Deck','Desk','Dirt','Disk','Dock','Door','Dusk','Dust','Echo','Edge',
754        'Face','Farm','Fire','Flag','Flax','Flow','Foam','Fort','Game','Gate',
755        'Gaze','Gear','Glow','Gold','Golf','Grit','Gulf','Hail','Head','Heat',
756        'Hill','Home','Hope','Hour','Hull','Iron','Jade','Jolt','Jute','Kelp',
757        'Kite','Knot','Lake','Lamp','Lava','Leaf','Link','Loop','Mask','Maze',
758        'Mead','Milk','Mint','Mist','Moon','Moss','Mud','Myth','Nest','Node',
759        'Note','Opal','Path','Peak','Pear','Peat','Pine','Pool','Port','Post',
760        'Pond','Rain','Reef','Rice','Ring','Road','Rock','Rook','Root','Rose',
761        'Ruby','Sand','Seed','Ship','Snow','Soil','Song','Star','Stone','Sun',
762        'Tale','Team','Time','Tree','Tide','Vale','Veil','View','Wave','Way',
763        'Wind','Wood','Yarn','Year','Yurt','Zeal','Zone','Zinc','Acme','Atom',
764        'Axis','Ball','Band','Bank','Base','Beam','Bean','Belt','Bend','Bike',
765        'Bill','Blob','Blog','Bolt','Bond','Bone','Boom','Boot','Bore','Bowl',
766        'Brain','Brass','Brick','Bride','Brim','Bubble','Buck','Bulb','Bulk',
767        'Bunk','Burl','Cafe','Cake','Camp','Cane','Cape','Card','Care','Cart',
768        'Case','Cash','Cask','Cast','Chum','City','Clam','Clan','Claw','Clip',
769        'Club','Clue','Coal','Coat','Coil','Coin','Comb','Cone','Copy','Cord',
770        'Cork','Cost','Crew','Crop','Cure','Curl','Dale','Dart','Dash','Data',
771        'Date','Day','Deal','Deed','Dime','Dice','Dish','Dome','Dream','Dress',
772        'Drop','Drum','Duke','Dune','Duty','Earn','East','Ebb','Fang','Fate',
773        'Feed','Felt','Fern','Film','Fizz','Fjord','Foil','Fold','Folk','Food',
774        'Foot','Ford','Form','Fuel','Fume','Fund','Fury','Fuse','Fuzz','Gale',
775        'Gift','Glass','Glue','Goal','Gong','Gown','Grab','Gram','Grid','Grin',
776        'Grip','Gulp','Gush','Hack','Hall','Hand','Hang','Harp','Heel','Heir',
777        'Helm','Help','Herb','Hero','Hike','Hint','Hive','Hole','Hood','Hook',
778        'Hoop','Horn','Hose','Host','Hunk','Hunt','Hurd','Hymn','Icon','Idea',
779        'Idle','Idol','Inch','Ink','Isle','Item','Jail','Java','Jeep','Jest',
780        'Jewel','Jinx','Join','Joke','Juice','Jump','Jury','Lace','Lack','Lair',
781        'Lane','Lard','Lash','Last','Lath','Lawn','Lead','Leak','Leap','Lego',
782        'Lens','Liar','Lice','Life','Lift','Like','Lime','Line','List','Load',
783        'Loan','Lobe','Lock','Lode','Loom','Lore','Lure','Lute','Lyre',
784        'Mail','Make','Male','Malt','Many','Mark','Mart','Mash','Mass','Mast',
785        'Mate','Meal','Mean','Meat','Melt','Mend','Menu','Mesh','Mess','Mind',
786        'Mine','Mode','Mood','Moor','Mope','More','Muck','Muff','Name','Navy',
787        'Neck','Need','News','Nook','Noon','Nose','Null','Oath','Pace','Pack',
788        'Pact','Page','Pair','Palm','Pane','Pant','Park','Part','Pass','Pawn',
789        'Peak','Pelt','Phon','Pick','Pile','Pill','Pipe','Pith','Plan','Play',
790        'Plot','Plug','Poem','Poet','Pole','Poll','Pomp','Pork','Posh','Puck',
791        'Puff','Pull','Pulp','Pump','Punt','Punk','Pyro','Quad','Quay','Quest',
792        'Quip','Quiz','Race','Raft','Rage','Rail','Rake','Rank','Rant','Rate',
793        'Rays','Read','Rent','Rest','Ride','Rift','Rind','Riot','Robe','Roll',
794        'Rope','Rote','Rove','Rush','Rusk','Sail','Sale','Salt','Sage','Sash',
795        'Scab','Scar','Scent','Seam','Seat','Sect','Self','Send','Sense',
796        'Serf','Sets','Shack','Shade','Shaft','Sham','Shard','Shed','Shin','Shoe',
797        'Shop','Show','Side','Sigh','Sign','Silk','Sill','Silo','Silt','Site',
798        'Size','Skid','Skim','Slab','Slag','Slam','Sled','Slip',
799        'Slog','Slot','Slum','Smog','Snag','Snip','Sock','Soda','Sofa','Solo',
800        'Soot','Sort','Soul','Soup','Span','Spec','Spin','Spot','Spur','Stab',
801        'Stir','Stop','Suds','Suit','Surf','Swab','Swag','Swan','Sway',
802        'Talk','Tank','Tape','Task','Taxi','Tent','Term','Test','Text','Thaw',
803        'Thug','Tile','Tilt','Tire','Tool','Tour','Town','Track','Trade','Trap',
804        'Tray','Trek','Trim','Trio','Trip','Tuba','Tube','Tuck','Tuft','Turf',
805        'Turn','Tusk','Twine','Type','Unit','Urge','User','Vein','Vent','Vest',
806        'Vibe','Vice','Vine','Visa','Volt','Vote','Wade','Wage','Wail','Wait',
807        'Wake','Walk','Wall','Ward','Ware','Wart','Wash','Watt','Week',
808        'Weld','West','Whim','Whip','Wick','Will','Wink','Wisp','Wish',
809        'Wrap','Yank','Yard','Yawn','Yell','Yelp','Yoga','Yoke','Yolk',
810        'Zero','Zest','Zing','Zips'
811    ];
812    
813    
814    const randAdj = adjectives[Math.floor(Math.random() * adjectives.length)];
815    const randNoun = nouns[Math.floor(Math.random() * nouns.length)];
816    const randNum = Math.floor(Math.random() * 900) + 100; // 100-999
817    
818    return randAdj + randNoun + randNum;
819}
820
821
822
823
824function playChirp() {
825    if (isMuted) return; // Only check if muted, remove the visibility check
826
827    try {
828        // pause → rewind → play = “re-trigger”
829        agentChirp.pause();
830        agentChirp.currentTime = 0;
831        agentChirp.play();
832    } catch {
833        // Safari iOS sometimes refuses; fall back to a throw-away clone
834        const clone = agentChirp.cloneNode();
835        clone.play().catch(()=>{});
836    }
837}
838
839
840const quicksandLink = document.createElement("link");
841quicksandLink.href = "https://fonts.googleapis.com/css2?family=Quicksand:wght@400;500;600;700&display=swap";
842quicksandLink.rel = "stylesheet";
843document.head.appendChild(quicksandLink);
844
845const style = document.createElement('style');
846style.textContent = `
847  #furrie-chatbot * {
848    font-family: 'Quicksand', sans-serif !important;
849  }
850`;
851document.head.appendChild(style);
852
853/**
854 * Notifies the user of a new message by blinking the page title.
855 */
856function startTitleNotification() {
857    if (notificationInterval) return; // Already notifying
858
859    notificationInterval = setInterval(() => {
860        document.title = (document.title === originalTitle) 
861            ? 'New Message!' 
862            : originalTitle;
863    }, 500); // Blink every second
864}
865
866/**
867 * Stops the title-blinking notification and restores the original title.
868 */
869function stopTitleNotification() {
870    clearInterval(notificationInterval);
871    notificationInterval = null;
872    document.title = originalTitle;
873}
874
875
876/**
877 * helper: Grabs visible text from body, stripping scripts/styles
878 * to ensure the AI sees what the user sees.
879 */
880function getCleanPageText() {
881    if (!document.body) return "";
882    
883    // clone body so we don't affect the live page
884    const clone = document.body.cloneNode(true);
885    
886    // remove invisible scripts/styles
887    const scripts = clone.querySelectorAll('script, style, noscript, iframe, svg');
888    scripts.forEach(el => el.remove());
889    
890    // get text, collapse whitespace, limit to 2500 chars
891    return (clone.innerText || "").replace(/\s+/g, ' ').trim().substring(0, 30000);
892}
893
894
895
896
897
898
899/**
900 * Creates a session on page load with "Browse" status,
901 * retrieves a session ID, and initializes Pusher to listen for agent messages.
902 */
903async function createOrResumeSession() {
904  try {
905    const existingGuestUid = localStorage.getItem("furrieGuestUid") || null;
906    const cleanContent = getCleanPageText();
907
908    const response = await fetch(apiEndpoint, {
909      method: "POST",
910      headers: { "Content-Type": "application/json" },
911      body: JSON.stringify({
912        action: 'create_session',
913        propertyId: propertyId,
914        existingSessionId: furieSessionId,
915        guestName: userName,
916        guestEmail: userEmail,
917        existingGuestUid,
918        page_url: window.location.href,
919        page_title: document.title,
920        page_content: cleanContent
921      }),
922    });
923    if (!response.ok) throw new Error('Session creation failed');
924
925    const data = await response.json();
926
927    // --- 1. SETUP SESSION IDS & PUSHER (Moved Up) ---
928    // We do this FIRST so the socket is listening even if GDPR blocks the UI
929    if (data.guest_uid) {
930      localStorage.setItem("furrieGuestUid", data.guest_uid);
931    }
932
933    if (data.session_id) {
934      furieSessionId = data.session_id;
935      liveChatSessionId = data.session_id;
936      localStorage.setItem(getStorageKey("furieSessionId"), furieSessionId);
937      
938      sessionStatus = data.status || 'Browse';
939      isLiveChatActive = (sessionStatus === 'open');
940
941      if (data.pusher_key) {
942        liveChatApiEndpoint = data.live_chat_api;
943        liveChatPropertyId = data.property_id;
944        loadPusher(() => initializePusher(data));
945      }
946    }
947
948    // --- 2. APPLY UI & CHECK GDPR ---
949    if (data.ui) {
950        applyUi(data.ui);
951
952        // --- GDPR BLOCK ---
953        if (isGdprEnabled && !isGdprConsented && !isCustomHooksActive) {
954            isGdprBlocked = true;
955            
956            // 1. Hide Promo Pill immediately
957            const pill = document.getElementById('furrie-promo-pill');
958            if(pill) pill.style.display = 'none';
959
960            // 2. Wait a few seconds, then Maximize and Show Checkup
961            setTimeout(() => {
962                maximizeBtn();
963                renderGdprModal();
964            }, 1000); 
965            
966            // CRITICAL CHANGE: We do NOT return here anymore. 
967            // We just let the code finish, but we wrap the HISTORY rendering below in a check.
968        }
969        // ----------------------
970
971		promoPhrases = [...DEFAULT_PROMO_PHRASES]; 
972        isCustomHooksActive = false; 
973
974        // Promo Hooks Logic
975        if (data.ui.promo_hooks && Array.isArray(data.ui.promo_hooks) && data.ui.promo_hooks.length > 0) {
976            promoPhrases = data.ui.promo_hooks;
977            phraseIndex = -1; 
978            isCustomHooksActive = true; 
979        }
980    }
981
982
983    // --- 3. RENDER HISTORY FROM DB (Conditioned on GDPR) ---
984    // Only render history if we are NOT blocked. 
985    // (If blocked, history will load when handleGdprChoice calls this function again).
986    if (!isGdprBlocked && data.db_history && Array.isArray(data.db_history)) {
987          const msgContainer = document.getElementById("furrie-messages");
988          
989          if(msgContainer) msgContainer.innerHTML = ''; 
990
991          const cleanHistoryForStorage = data.db_history.map(msg => ({
992              sender: msg.sender,
993              text: msg.text,
994              avatarUrl: msg.avatarUrl || null
995          }));
996          localStorage.setItem(getStorageKey("furrieChatHistory"), JSON.stringify(cleanHistoryForStorage));
997
998          let lastMsgSender = null;
999          let lastMsgText = null;
1000
1001          data.db_history.forEach(msg => {
1002              if (msg.sender === lastMsgSender && msg.text === lastMsgText) {
1003                  return; 
1004              }
1005              lastMsgSender = msg.sender;
1006              lastMsgText = msg.text;
1007              appendMessage(msg.sender, msg.text, false, null, msg.time, true); 
1008          });
1009
1010          const messagesDiv = document.getElementById("furrie-messages");
1011          if(messagesDiv) messagesDiv.scrollTop = messagesDiv.scrollHeight;
1012    }
1013    
1014  } catch (error) {
1015    console.error("Could not establish a Bot session:", error);
1016    const wrapper = document.getElementById('furrie-launcher-wrapper');
1017    if (wrapper) wrapper.style.display = 'flex';
1018  }
1019}
1020
1021
1022
1023
1024        // --- UI INITIALIZATION ---
1025
1026        const container = document.createElement("div");
1027        
1028        const defaultBottom = 82;
1029
1030        container.id = "furrie-chatbot";
1031
1032        Object.assign(container.style, {
1033
1034                position: "fixed",
1035
1036                bottom: `${defaultBottom}px`,
1037
1038                right: "20px",
1039
1040                width: "400px",
1041                
1042                height: "700px",
1043                
1044                marginTop: "15px",
1045                
1046                maxHeight: `calc(100vh - ${defaultBottom + 30}px)`,
1047
1048                maxWidth: "90vw",
1049
1050                zIndex: "2147483647",
1051
1052                boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
1053
1054                borderRadius: "25px",
1055
1056                overflow: "hidden",
1057                
1058                color: "#555555",
1059
1060                backgroundColor: "#E6FEFE",
1061
1062                fontFamily: "Quicksand, sans-serif",
1063
1064                transform: "translateY(100%)",
1065
1066                fontSize: "12px",
1067
1068                paddingTop: "7px",
1069
1070                opacity: "0",
1071
1072                transition: "transform 0.3s ease, opacity 0.3s ease",
1073
1074                pointerEvents: "none"
1075
1076        });
1077
1078
1079
1080        if (window.innerWidth <= 768) {
1081
1082                Object.assign(container.style, {
1083
1084                        top: "0",
1085
1086                        bottom: "0",
1087
1088                        left: "0",
1089
1090                        right: "10px",
1091
1092                        width: "100vw",
1093
1094                        height: "100dvh",
1095
1096                        maxWidth: "100%",
1097
1098                        maxHeight: "100vh",
1099
1100                        borderRadius: "0",
1101                        
1102                        fontFamily: "Quicksand, sans-serif",
1103                        
1104                        boxSizing: "border-box", 
1105
1106                        fontSize: "14px",
1107                        
1108                                        zIndex: "2147483647"
1109
1110                });
1111
1112        } else {
1113
1114                container.style.height = "600px";
1115
1116                container.style.maxHeight = "calc(100vh - ${defaultBottom + 30}px)";
1117
1118        }
1119
1120
1121
1122        container.innerHTML = `
1123
1124        <div style="display: flex; flex-direction: column; height: 100%; margin-right: 0px;">
1125
1126<div id="furrie-chat-header" style="
1127    background-color: #E6FEFE; 
1128    padding: 10px !important; 
1129    display: flex !important; 
1130    justify-content: space-between !important; 
1131    align-items: center !important;
1132    box-sizing: border-box !important;
1133    width: 100% !important;
1134">
1135<img src="${baseImgUrl}logo.png" onerror="this.onerror=null; this.src='${defaultImgUrl}logo.png';" alt="Logo" style="height: 60px; margin-right: 12px; border-radius: 20px;">
1136    <h4 id="furrie-title" style="margin-left: 2px; margin-right: 5px; font-size: 16px; font-weight: 800;"></h4>
1137  </a>
1138  <div id="furrie-header-controls" style="
1139    display: flex !important; 
1140    left-margin: 20px !important;
1141    align-items: center !important; 
1142    gap: 2px !important; 
1143    flex-shrink: 0 !important;
1144    margin-right: 2px !important; /* Extra safety buffer */
1145">
1146  <button id="furrie-minimize" style="background: none; border: none; font-size: 20px; font-weight: bold; cursor: pointer; color: #555555; margin-right: 10px; margin-left: 5px;">−</button>
1147  </div>
1148</div>
1149
1150                <div id="furrie-messages" style="flex: 1 !important; padding: 10px !important; overflow-y: auto; border-top: 3px solid #5DF7F8 !important; border-bottom: 3px solid #5DF7F8; background-color: #F7F9FB; border-radius: 25px 0px 0px 25px !important;"></div>
1151
1152                <form id="furrie-form" style="display: flex; align-items: center; border-top: 0px solid #ccc; padding: 6px; background: #fff;">
1153
1154                        <input type="text" id="furrie-input"  name="nobot"
1155    placeholder="How can I help you?"
1156    autocomplete="off"
1157    autocorrect="off"
1158    autocapitalize="off"
1159    spellcheck="false"
1160    inputmode="text" style="flex: 1; border: none; padding: 8px; font-size: 16px; color: #5AC0DE; outline: none; box-shadow: none; min-height: 36px; max-height: 60px; font-weight: 500 !important;" />
1161
1162
1163<button id="furrie-mic" type="button"
1164  style="color: white; border: none; padding: 0 12px; margin-left: 6px; border-radius: 999px; cursor: pointer; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; font-size: 18px;">
1165  <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" 
1166       fill="currentColor" viewBox="0 0 16 16">
1167    <path d="M8 12a3 3 0 0 0 3-3V4a3 3 0 1 0-6 0v5a3 3 0 0 0 3 3z"/>
1168    <path d="M5 10.5a.5.5 0 0 1 .5.5 2.5 2.5 0 0 0 5 0 .5.5 0 0 1 1 0
1169             3.5 3.5 0 0 1-7 0 .5.5 0 0 1 .5-.5z"/>
1170    <path d="M8 14a.5.5 0 0 1 .5.5V16h-1v-1.5A.5.5 0 0 1 8 14z"/>
1171  </svg>
1172</button>
1173
1174<button id="furrie-submit" type="submit"
1175        style="color: white; border: none; padding: 0 12px; margin-left: 6px; border-radius: 999px; cursor: pointer; width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; font-size: 18px;">➤</button>
1176
1177                </form> 
1178
1179        </div>`;
1180
1181        document.body.appendChild(container);
1182
1183
1184
1185// --- 1. CREATE WRAPPER (Anchors the Launcher position) ---
1186// --- 0. INJECT STYLES FOR BUBBLE TRIANGLE ---
1187// We inject a specific style tag here to handle the "speech bubble tail" pseudo-elements
1188const bubbleTailStyle = document.createElement('style');
1189bubbleTailStyle.textContent = `
1190    #furrie-promo-pill::after {
1191        content: '';
1192        position: absolute;
1193        bottom: -6px; /* Hangs off the bottom */
1194        right: 18px;  /* Aligns with the top-right of the launcher */
1195        width: 0;
1196        height: 0;
1197        border-left: 7px solid transparent;
1198        border-right: 7px solid transparent;
1199        border-top: 7px solid #fff; /* White triangle matching background */
1200    }
1201    #furrie-promo-pill::before {
1202        content: '';
1203        position: absolute;
1204        bottom: -8px; /* Slightly lower to create the border effect */
1205        right: 18px;
1206        width: 0;
1207        height: 0;
1208        border-left: 7px solid transparent;
1209        border-right: 7px solid transparent;
1210        border-top: 7px solid #eee; /* Grey triangle matching border */
1211        z-index: -1;
1212    }
1213`;
1214document.head.appendChild(bubbleTailStyle);
1215
1216
1217// --- 1. CREATE WRAPPER (Anchors the Launcher position) ---
1218    const launcherWrapper = document.createElement("div");
1219    launcherWrapper.id = "furrie-launcher-wrapper";
1220    Object.assign(launcherWrapper.style, {
1221        position: "fixed",
1222        bottom: "72px", 
1223        right: "20px",
1224        zIndex: "2147483647",
1225        display: "flex",
1226        flexDirection: "column",
1227        alignItems: "flex-end", // Align pill to right of launcher
1228        pointerEvents: "none",   // Let clicks pass through the invisible gap
1229        // --- ANIMATION STYLES ---
1230    opacity: "0",              // ...but it is invisible
1231    transform: "translateY(20px)", // ...and starts 20px lower than final spot
1232    transition: "opacity 0.8s ease, transform 0.8s ease" // The "Gentle Fade" settings
1233    });
1234    document.body.appendChild(launcherWrapper);
1235
1236    // --- 2. LAUNCHER BUTTON (Inside Wrapper) ---
1237    const launcher = document.createElement("div");
1238    launcher.id = "furrie-launcher";
1239    Object.assign(launcher.style, {
1240        // Removed fixed positioning (wrapper handles it)
1241        position: "relative", 
1242        backgroundColor: "#ffffff",
1243        border: "1px solid #ddd",
1244        borderRadius: "999px",
1245        padding: "6px 12px 6px 6px",
1246        boxShadow: "0 4px 10px rgba(0,0,0,0.1)",
1247        cursor: "pointer",
1248        display: "flex",
1249        alignItems: "center",
1250        maxWidth: "calc(100vw - 40px)", 
1251        fontSize: "14px",
1252        fontWeight: "bold",
1253        fontFamily: "Quicksand, sans-serif",
1254        pointerEvents: "auto" // Re-enable clicks
1255    });
1256
1257    launcher.innerHTML = `
1258        <img src="${baseImgUrl}bot.png" onerror="this.onerror=null; this.src='${defaultImgUrl}bot.png';" alt="Chat Icon" style="height: 40px; margin-left: 6px; margin-right: 4px; border-radius: 15px;">
1259        <span id="furrie-launcher-text" style="display: inline-block; white-space: nowrap; overflow: hidden; transition: width 1s ease, opacity 1s ease, margin 5s ease; opacity: 1; width: auto; margin-right: 6px; color: #555555;"></span>`;
1260    
1261    launcherWrapper.appendChild(launcher); // Append to wrapper
1262
1263    // --- 3. PRODUCT PLACEMENT PILL (Now a Bubble ABOVE Launcher) ---
1264    const promoPill = document.createElement("a");
1265    promoPill.id = "furrie-promo-pill";
1266    promoPill.href = "https://www.furie.ai";
1267    promoPill.target = "_blank";
1268    Object.assign(promoPill.style, {
1269        position: "absolute", 
1270        bottom: "65px",      // MOVED: Sits above the launcher now
1271        right: "16px",          
1272        height: "auto",
1273        backgroundColor: "#fff",
1274        borderRadius: "20px", // ROUNDER: Bubble shape
1275        padding: "8px 14px",  // PLUMPER: More breathing room
1276        boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
1277        textDecoration: "none",
1278        display: "none", 
1279        alignItems: "center",
1280        textAlign: "left",
1281        justifyContent: "center",
1282        fontSize: "13px",
1283        color: "#555",
1284        fontFamily: "Quicksand, sans-serif",
1285        cursor: "pointer",
1286        border: "1px solid #eee",
1287        transition: "opacity 0.3s ease",
1288        pointerEvents: "auto",
1289        whiteSpace: "normal",
1290        overflow: "visible", // CRITICAL: Allows the triangle tail to stick out
1291        minWidth: "200px",
1292        maxWidth: "200px"
1293        
1294    });
1295    
1296    // Initial HTML with ID on span for targeting
1297    promoPill.innerHTML = `
1298        <span id="furie-promo-text" style="margin-right:5px; font-weight:600; transition: opacity 0.5s ease; opacity: 1; border-radius: 10px;">👋</span>
1299    <img src="${globalImgUrl}furie_logo.png"
1300     style="height:14px; width:auto; vertical-align:middle; margin-left:auto; border-radius: 10px;">
1301    `;
1302    launcherWrapper.appendChild(promoPill); 
1303
1304
1305
1306
1307    let phraseIndex = 0;
1308    const promoTextSpan = promoPill.querySelector('#furie-promo-text');
1309
1310// Cycle every few seconds
1311    setInterval(() => {
1312        // Only rotate if the pill is actually visible
1313        if (promoPill.style.display === 'none') return;
1314
1315        // 1. Fade out text
1316        promoTextSpan.style.opacity = '0';
1317
1318        setTimeout(() => {
1319            // 2. Swap text
1320            phraseIndex = (phraseIndex + 1) % promoPhrases.length;
1321            const rawPhrase = promoPhrases[phraseIndex];
1322            
1323            let displayText = rawPhrase;
1324            let extractedUrl = ""; // Variable to hold the found URL
1325
1326            // 1. If AI/Custom message included a URL, extract it and strip it from display text
1327            const urlRegex = /(https?:\/\/[^\s]+)/g;
1328            const urlMatch = rawPhrase.match(urlRegex);
1329            
1330            if (urlMatch && urlMatch[0]) {
1331                extractedUrl = urlMatch[0]; // Save the URL
1332                displayText = rawPhrase.replace(urlMatch[0], '').trim(); // Clean the text
1333            }
1334
1335            // 2. STORE DATA ON THE ELEMENT (This is the missing link)
1336            promoPill.dataset.pendingUrl = extractedUrl;
1337            promoPill.dataset.pendingText = displayText;
1338
1339            // 3. Update cursor behavior
1340            if (isCustomHooksActive) {
1341                // AI/Custom Mode: Don't link out via href, prepare for Chat Open
1342                promoPill.href = "javascript:void(0)"; 
1343                promoPill.style.cursor = "pointer";
1344            } else {
1345                // Default Mode: Keep default Furie link
1346                promoPill.href = "https://www.furie.ai";
1347                promoPill.style.cursor = "pointer";
1348            }
1349            
1350            // Update visual text
1351            promoTextSpan.textContent = displayText;
1352            
1353            // 4. Fade in text
1354            promoTextSpan.style.opacity = '1';
1355        }, 500);
1356
1357    }, 8000);
1358    
1359    launcherWrapper.appendChild(promoPill); // Append to wrapper
1360
1361
1362        // --- DOM ELEMENT REFERENCES ---
1363
1364        const messagesDiv = document.getElementById("furrie-messages");
1365
1366        const form = document.getElementById("furrie-form");
1367
1368        const input = document.getElementById("furrie-input");
1369
1370        const textSpan = document.getElementById("furrie-launcher-text");
1371        
1372        
1373        // Text-to-speech
1374function speakText(text) {
1375    try {
1376        // Encode the text so it safely passes through the URL
1377        const encodedText = encodeURIComponent(text.replace(/<[^>]+>/g, ""));
1378        const url = `https://www.furie.ai/furie_tts.php?text=${encodedText}`;
1379
1380        // The browser natively handles streaming playback for URLs
1381        const audio = new Audio(url);
1382        audio.playbackRate = 1.1;
1383        
1384        // Play immediately; the browser will buffer the stream naturally
1385        audio.play().catch(e => console.error("Audio play blocked/failed:", e));
1386
1387    } catch (e) {
1388        console.error("TTS error:", e);
1389    }
1390}
1391
1392        // Turn off autofill/quicktype/spell UI
1393if (form)  form.setAttribute('autocomplete', 'off');
1394
1395if (input) {
1396  input.setAttribute('autocomplete',   'off');
1397  input.setAttribute('autocorrect',    'off');
1398  input.setAttribute('autocapitalize', 'off');
1399  input.setAttribute('spellcheck',     'false');
1400
1401  // Chrome sometimes keys suggestions by name=; use an uncommon, rotating name
1402  input.setAttribute('name', 'msg_' + Math.random().toString(36).slice(2));
1403
1404  // Gently hint password managers to ignore
1405  input.setAttribute('data-lpignore', 'true');      // LastPass
1406  input.setAttribute('data-form-type','other');     // 1Password
1407}
1408
1409const listTight = document.createElement('style');
1410listTight.textContent = `
1411  #furrie-messages ul { margin: 6px 0; padding-left: 18px; }
1412  #furrie-messages li { margin: 2px 0; }
1413`;
1414document.head.appendChild(listTight);
1415
1416
1417
1418// Speech-to-text mic logic goes here:
1419let isListening = false;
1420let recognition;
1421
1422function startListening() {
1423  if (!('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)) {
1424    alert("Speech recognition not supported.");
1425    return;
1426  }
1427  const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
1428  recognition = new SR();
1429  recognition.lang = "auto"; 
1430  recognition.interimResults = false;
1431  recognition.onresult = (e) => {
1432    const transcript = e.results[0][0].transcript;
1433    input.value = transcript;
1434    form.dispatchEvent(new Event("submit", {cancelable:true, bubbles:true}));
1435  };
1436  recognition.onend = () => { isListening = false; setMicUI(false); };
1437  recognition.start();
1438  isListening = true;
1439  setMicUI(true);
1440}
1441
1442function stopListening() {
1443  if (recognition) recognition.stop();
1444  isListening = false;
1445  setMicUI(false);
1446}
1447
1448function setMicUI(active) {
1449  const micBtn = document.getElementById('furrie-mic');
1450  if (!micBtn) return;
1451  micBtn.style.opacity = active ? '1' : '0.7';
1452  micBtn.title = active ? 'Stop listening' : 'Voice input';
1453}
1454
1455document.getElementById('furrie-mic')?.addEventListener('click', () => {
1456  if (isListening) stopListening(); else startListening();
1457});
1458
1459
1460        // --- HELPER FUNCTIONS ---
1461
1462
1463function canTrackBehavior() {
1464  if (!isGdprEnabled) return true;
1465  return isGdprConsented === true;
1466}
1467
1468
1469
1470function renderGdprModal() {
1471    // 1. Clear existing chat messages to show the modal cleanly
1472    messagesDiv.innerHTML = '';
1473    
1474    // 2. Disable Input
1475    input.disabled = true;
1476    input.placeholder = "Please accept privacy policy to chat.";
1477    
1478    // 3. Create Container
1479    const modal = document.createElement("div");
1480    modal.id = "furrie-gdpr-modal";
1481    Object.assign(modal.style, {
1482        display: "flex",
1483        flexDirection: "column",
1484        justifyContent: "flex-start",
1485        justifyContent: "center",
1486        height: "100%",
1487        padding: "20px",
1488        textAlign: "left",
1489        backgroundColor: "#fff",
1490        color: "#555",
1491        fontSize: "14px",
1492        lineHeight: "1.5"
1493    });
1494
1495    const btnColor = window.__furie_button_color || '#5AC0DE';
1496    const textColor = idealTextColor(btnColor);
1497
1498    modal.innerHTML = `
1499        <div style="text-align:center; margin-bottom:15px; margin-top: 150px; font-size: 24px;">🛡️</div>
1500        <h3 style="margin:0 0 10px 0; text-align:center; font-weight:700;">Privacy Preference Center</h3>
1501        <p style="margin-bottom:15px; font-size:13px;">To provide a helpful browsing, support, and interaction experience, we need your permission to process your data.</p>
1502        
1503        <div style="margin-bottom:10px;">
1504            <strong style="display:block; margin-bottom:2px;">🤖 AI Processing (Functional)</strong>
1505            <span style="font-size:12px; color:#777;">We share your messages with AI providers (OpenAI) solely to generate accurate responses.</span>
1506        </div>
1507
1508        <div style="margin-bottom:10px;">
1509            <strong style="display:block; margin-bottom:2px;">📊 Analytics & Insights</strong>
1510            <span style="font-size:12px; color:#777;">We anonymously track your browsing activity (pages visited, scroll depth, time on site) on this website to calculate a "Helpfulness Score" and offer relevant assistance.</span>
1511        </div>
1512
1513        <div style="margin-bottom:20px;">
1514            <strong style="display:block; margin-bottom:2px;">🍪 Session Storage</strong>
1515            <span style="font-size:12px; color:#777;">We store a temporary ID in your browser to maintain conversation history across pages.</span>
1516        </div>
1517
1518        <div style="display:flex; flex-direction:column; gap:10px;">
1519            <button id="furrie-gdpr-accept" style="background-color:${btnColor}; color:${textColor}; border:none; padding:12px; border-radius:25px; font-weight:bold; cursor:pointer;">Accept & Chat</button>
1520            <button id="furrie-gdpr-decline" style="background:none; border:1px solid #ccc; padding:10px; border-radius:25px; cursor:pointer; color:#777;">Decline</button>
1521        </div>
1522        
1523        <div style="text-align:center; margin-top:15px;">
1524            <a href="${privacyPolicyUrl}" target="_blank" style="font-size:12px; color:#999; text-decoration:underline;">Read Privacy Policy</a>
1525        </div>
1526    `;
1527
1528    messagesDiv.appendChild(modal);
1529
1530    // Bind Events
1531    document.getElementById("furrie-gdpr-accept").onclick = () => handleGdprChoice(true);
1532    document.getElementById("furrie-gdpr-decline").onclick = () => handleGdprChoice(false);
1533}
1534
1535async function handleGdprChoice(accepted) {
1536    if (accepted) {
1537        // --- ACCEPTED FLOW ---
1538        
1539        // 1. Update State
1540        isGdprConsented = true;
1541        isGdprBlocked = false;
1542        localStorage.setItem(getStorageKey("furrieGdprConsent"), "1");
1543        
1544        // 2. Remove Modal immediately
1545        const modal = document.getElementById("furrie-gdpr-modal");
1546        if(modal) modal.remove();
1547        
1548        // 3. Unlock Input
1549        input.disabled = false;
1550        input.placeholder = "How can I help you?";
1551
1552        // 4. Force Show Promo Pill
1553        if (isProductPlacementEnabled) {
1554             const pill = document.getElementById('furrie-promo-pill');
1555             if(pill) pill.style.display = 'flex';
1556        }
1557
1558        // 5. Send Consent to DB (Wait for it to finish)
1559        try {
1560            await fetch(apiEndpoint, {
1561                method: "POST",
1562                headers: { "Content-Type": "application/json" },
1563                body: JSON.stringify({
1564                    action: 'log_consent',
1565                    session_id: furieSessionId,
1566                    propertyId: propertyId,
1567                    status: 'accepted'
1568                })
1569            });
1570        } catch(e) { console.error(e); }
1571
1572
1573
1574// Reset timing so dwell calculations start post-consent
1575pageStartTs = performance.now();
1576
1577// Force a post-consent tracking event
1578sendPageEvent({
1579  meta: {
1580    gdpr_accepted: true,
1581    forced: true
1582  }
1583});
1584
1585
1586        // 6. Restart Session
1587        // This will call loadPusher -> initializePusher.
1588        // Once connected, initializePusher will fire sendPageEvent automatically.
1589        createOrResumeSession(); 
1590
1591    } else {
1592        // --- DECLINED FLOW ---
1593        // (This remains unchanged from your previous working version)
1594        isCustomHooksActive = false; 
1595        promoPhrases = [...DEFAULT_PROMO_PHRASES];
1596        const pill = document.getElementById('furrie-promo-pill');
1597        if (pill) {
1598             pill.href = "https://www.furie.ai";
1599             pill.style.cursor = "pointer";
1600             delete pill.dataset.pendingUrl;
1601             delete pill.dataset.pendingText;
1602             if (isProductPlacementEnabled) {
1603                 pill.style.display = 'flex';
1604                 const txt = pill.querySelector('#furie-promo-text');
1605                 if(txt) txt.textContent = ""; 
1606             }
1607        }
1608        minimizeBtn();
1609        fetch(apiEndpoint, {
1610            method: "POST",
1611            headers: { "Content-Type": "application/json" },
1612            body: JSON.stringify({
1613                action: 'log_consent',
1614                session_id: furieSessionId,
1615                propertyId: propertyId,
1616                status: 'declined'
1617            })
1618        });
1619    }
1620}
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630/* Get visible text for prompt */
1631function getVisibleText() {
1632  // Use document.body.innerText directly. 
1633  // It gets the readable text of the page without duplicating it for every parent element.
1634  if (!document.body) return '';
1635  return document.body.innerText || '';
1636}
1637
1638
1639/**
1640 * Cleans the bot's HTML response to remove unwanted spacing,
1641 * specifically extra <br> tags around <ul> lists.
1642 * @param {string} html - The raw HTML string from the bot.
1643 * @returns {string} - The cleaned HTML string.
1644 */
1645function sanitizeHtmlResponse(html) {
1646    if (!html || typeof html !== 'string') {
1647        return '';
1648    }
1649
1650    let cleanedHtml = html;
1651
1652    // 1. Remove any <br> tags immediately before an opening <ul> tag.
1653    cleanedHtml = cleanedHtml.replace(/(?:<br\s*\/?>\s*)+(<ul)/gi, '$1');
1654
1655    // 2. Remove any <br> tags immediately after a closing </ul> tag.
1656    cleanedHtml = cleanedHtml.replace(/(<\/ul>)(?:<br\s*\/?>\s*)+/gi, '$1');
1657    
1658    // 3. Trim any leading/trailing whitespace or leftover <br> tags from the whole response.
1659    cleanedHtml = cleanedHtml.trim().replace(/^<br\s*\/?>/i, '').replace(/<br\s*\/?>$/i, '').trim();
1660
1661    return cleanedHtml;
1662}
1663
1664        /**
1665
1666           * FIX #2: Added the missing saveHistory function.
1667
1668           * Saves a message to the browser's local storage.
1669
1670           * @param {string} sender - Who sent the message ('You' or 'Furie').
1671
1672           * @param {string} text - The message content.
1673
1674           */
1675
1676
1677
1678function saveHistory(sender, text, avatarUrl = null) {
1679    const historyKey = getStorageKey("furrieChatHistory");
1680    const currentHistory = JSON.parse(localStorage.getItem(historyKey) || "[]");
1681    const messageData = { sender, text };
1682    if (avatarUrl) {
1683        messageData.avatarUrl = avatarUrl;
1684    }
1685    currentHistory.push(messageData);
1686    localStorage.setItem(historyKey, JSON.stringify(currentHistory));
1687}
1688
1689
1690
1691
1692
1693
1694// === Mute button (bell icon) ===============================================
1695function bellSVG(muted) {
1696  // simple bell with optional slash
1697  return `
1698  <svg width="22" height="22" viewBox="0 0 24 24" aria-hidden="true">
1699    <path d="M12 22a2.5 2.5 0 0 0 2.45-2h-4.9A2.5 2.5 0 0 0 12 22Z"></path>
1700    <path d="M18 16v-4.5a6 6 0 1 0-12 0V16l-1.6 1.6a1 1 0 0 0 .7 1.7h14.8a1 1 0 0 0 .7-1.7L18 16Z" fill="currentColor"></path>
1701    ${muted ? `<line x1="3" y1="3" x2="18" y2="18" stroke="currentColor" stroke-width="1" stroke-linecap="round"></line>` : ``}
1702  </svg>`;
1703}
1704
1705
1706
1707function setMuteUI(btn, muted) {
1708  btn.innerHTML = bellSVG(muted);
1709  btn.title = muted ? "Unmute notifications" : "Mute notifications";
1710  btn.setAttribute("aria-label", btn.title);
1711  btn.setAttribute("aria-pressed", String(muted));
1712  // Optional visual state
1713  btn.style.opacity = muted ? "0.5" : "1";
1714}
1715
1716function addMuteButton() {
1717    const controls = document.getElementById('furrie-header-controls');
1718    if (!controls) return;
1719
1720    const muteBtn = document.createElement("button");
1721    muteBtn.id = "furrie-mute";
1722    muteBtn.type = "button";
1723    Object.assign(muteBtn.style, {
1724        background: "none",
1725        border: "none",
1726        cursor: "pointer",
1727        width: "36px",
1728        height: "36px",
1729        borderRadius: "50%",
1730        display: "inline-flex",
1731        alignItems: "center",
1732        justifyContent: "center",
1733        marginRight: "4px",
1734        color: (window.__furie_header_text_color || "#555555") + " !important"
1735    });
1736
1737    setMuteUI(muteBtn, isMuted);
1738
1739    muteBtn.addEventListener("click", () => {
1740        isMuted = !isMuted;
1741        agentChirp.muted = isMuted;
1742        localStorage.setItem("furrieMuted", isMuted ? "1" : "0");
1743        setMuteUI(muteBtn, isMuted);
1744    });
1745
1746    // Just insert into controls. The old logic using 'header' is removed.
1747    controls.insertBefore(muteBtn, controls.firstChild);
1748}
1749
1750// Call once after DOM is ready and header exists:
1751addMuteButton();          
1752
1753
1754// Read-aloud toggle
1755function addReadAloudButton() {
1756    const controls = document.getElementById('furrie-header-controls');
1757    if (!controls) return;
1758
1759    const btn = document.createElement("button");
1760    btn.id = "furrie-tts-toggle";
1761    btn.type = "button";
1762    Object.assign(btn.style, {
1763        background: "none",
1764        border: "none",
1765        cursor: "pointer",
1766        width: "36px",
1767        height: "36px",
1768        display: "inline-flex",
1769        alignItems: "center",
1770        justifyContent: "center",
1771        marginRight: "4px",
1772        marginTop: "2px",
1773        color: (window.__furie_header_text_color || "#555555") + " !important"
1774    });
1775
1776    function renderIcon(on) {
1777        return `
1778    <svg width="24" height="24" viewBox="0 0 24 24">
1779      <path d="M3 9v6h4l5 5V4L7 9H3z" fill="currentColor"></path>
1780      <path d="M16 12c0-1.77-.77-3.37-2-4.47v8.94A5.98 5.98 0 0 0 16 12z" fill="currentColor"></path>
1781      ${on ? "" : `<line x1="3" y1="3" x2="18" y2="18" stroke="currentColor" stroke-width="1" stroke-linecap="round"></line>`}
1782    </svg>
1783  `;
1784    }
1785
1786    let readAloudOn = false;
1787    try {
1788        readAloudOn = localStorage.getItem("furrieReadAloud") === "1";
1789    } catch (e) {}
1790    
1791    btn.innerHTML = renderIcon(readAloudOn);
1792    btn.title = "Toggle Read Aloud";
1793    btn.style.opacity = readAloudOn ? "1" : "0.5";
1794
1795    btn.addEventListener("click", () => {
1796        readAloudOn = !readAloudOn;
1797        localStorage.setItem("furrieReadAloud", readAloudOn ? "1" : "0");
1798        btn.innerHTML = renderIcon(readAloudOn);
1799        btn.style.opacity = readAloudOn ? "1" : "0.5";
1800    });
1801
1802    // Clean insert into controls
1803    controls.insertBefore(btn, controls.firstChild);
1804}
1805
1806addReadAloudButton();
1807
1808
1809
1810// === Font Size Toggle ===============================================
1811function addFontSizeToggle() {
1812    const controls = document.getElementById('furrie-header-controls');
1813    if (!controls) return;
1814
1815    const btn = document.createElement("button");
1816    btn.id = "furrie-font-toggle";
1817    btn.type = "button";
1818    Object.assign(btn.style, {
1819        background: "none",
1820        border: "none",
1821        cursor: "pointer",
1822        width: "36px",
1823        height: "36px",
1824        display: "inline-flex",
1825        alignItems: "center",
1826        justifyContent: "center",
1827        marginRight: "6px",
1828        color: (window.__furie_header_text_color || "#555555") + " !important"
1829    });
1830    btn.innerHTML = `<span style="display:flex;align-items:flex-end;gap:2px;line-height:1;">
1831      <span style="font-size:18px;font-weight:800;margin-top:2px;">A</span>
1832      <span style="font-size:12px;font-weight:800;">A</span>
1833    </span>`;
1834    btn.title = "Toggle Font Size";
1835
1836    const sizes = ["12px", "14px", "16px"];
1837    let currentIndex = parseInt(localStorage.getItem("furrieFontSizeIndex") || "1", 10);
1838    container.style.fontSize = sizes[currentIndex];
1839
1840    btn.addEventListener("click", () => {
1841        currentIndex = (currentIndex + 1) % sizes.length;
1842        container.style.fontSize = sizes[currentIndex];
1843        localStorage.setItem("furrieFontSizeIndex", currentIndex.toString());
1844    });
1845
1846    // Clean insert into controls
1847    controls.insertBefore(btn, controls.firstChild);
1848}
1849
1850addFontSizeToggle();
1851
1852// Per-instance auto-scroll that cancels ONLY if the user interacts during this scroll.
1853let currentAutoScroll = null;
1854
1855/**
1856 * Smoothly scroll #furrie-messages to bottom.
1857 * Returns a cancel() function for this specific instance.
1858 */
1859function scrollToBottom(duration = 600) {
1860  const el = messagesDiv;
1861  // Cancel any existing instance first
1862  if (currentAutoScroll && typeof currentAutoScroll.cancel === 'function') {
1863    currentAutoScroll.cancel();
1864  }
1865
1866  const start = el.scrollTop;
1867  const end   = el.scrollHeight - el.clientHeight;
1868  if (end <= start) return () => {};
1869
1870  let cancelled = false;
1871  let rafId = null;
1872  const t0 = performance.now();
1873
1874  const cancel = () => { cancelled = true; cleanup(); };
1875
1876  // Cancel only on real user input, not programmatic scrolls
1877  const cancelOnWheelTouch = () => cancel();
1878  const cancelOnPointer    = () => cancel();
1879  const cancelOnKey = (e) => {
1880    // Only keys that actually scroll
1881    if (['PageUp','PageDown','ArrowUp','ArrowDown','Home','End',' '].includes(e.key)) cancel();
1882  };
1883
1884  // Attach listeners for *this* instance only
1885  el.addEventListener('wheel',       cancelOnWheelTouch, {passive:true});
1886  el.addEventListener('touchstart',  cancelOnWheelTouch, {passive:true});
1887  el.addEventListener('pointerdown', cancelOnPointer,    {passive:true});
1888  window.addEventListener('keydown', cancelOnKey,        {passive:true});
1889
1890  function cleanup() {
1891    if (rafId) cancelAnimationFrame(rafId);
1892    el.removeEventListener('wheel',       cancelOnWheelTouch, {passive:true});
1893    el.removeEventListener('touchstart',  cancelOnWheelTouch, {passive:true});
1894    el.removeEventListener('pointerdown', cancelOnPointer,    {passive:true});
1895    window.removeEventListener('keydown', cancelOnKey,        {passive:true});
1896    currentAutoScroll = null;
1897  }
1898
1899  function step(now) {
1900    if (cancelled) return;
1901    const t = Math.min(1, (now - t0) / duration);
1902    const eased = t < 0.5 ? 2*t*t : -1 + (4 - 2*t)*t;
1903    el.scrollTop = start + (end - start) * eased;
1904    if (t < 1) {
1905      rafId = requestAnimationFrame(step);
1906    } else {
1907      cleanup();
1908    }
1909  }
1910
1911  rafId = requestAnimationFrame(step);
1912  currentAutoScroll = { cancel };
1913  return currentAutoScroll;
1914}
1915
1916
1917/**
1918 * Appends a message to the chat, animating it with a letter-by-letter fade effect.
1919 * Handles complex HTML by traversing the DOM tree.
1920 * @param {string} sender - Who sent the message ('You', 'FurieBot', 'Agent').
1921 * @param {string} text - The message content, which can be a string of HTML.
1922 * @param {boolean} [animate=true] - Whether to animate the message.
1923 */
1924function appendMessage(sender, text, animate = true, avatarUrl = null, specificTimestamp = null, skipSave = false) {
1925    
1926    // --- 1. Create the Message Wrapper and Avatar ---
1927    const wrap = document.createElement('div');
1928    Object.assign(wrap.style, {
1929        display: 'flex',
1930        flexDirection: sender === 'You' ? 'row-reverse' : 'row',
1931        alignItems: 'flex-end',
1932        gap: '6px',
1933        margin: '6px 0'
1934    });
1935
1936    const showAvatar = sender === 'FurieBot' || sender === 'Agent';
1937    if (showAvatar) {
1938        const av = document.createElement('img');
1939        const isBot = sender === 'FurieBot';
1940        
1941        let primarySrc = '';
1942        if (isBot) {
1943            primarySrc = `${baseImgUrl}bot.png`;
1944        } else { // It's an Agent
1945            primarySrc = avatarUrl || `${baseImgUrl}agent.png`; 
1946        }
1947        const fallbackSrc = isBot ? `${defaultImgUrl}bot.png` : `${defaultImgUrl}agent.png`;
1948        
1949        av.src = primarySrc;
1950        av.alt = sender;
1951        av.onerror = function() {
1952            this.onerror = null; 
1953            this.src = fallbackSrc;
1954        };
1955
1956        Object.assign(av.style, {
1957            width: '58px', height: '58px', borderRadius: '50%',
1958            marginRight: sender === 'You' ? '0' : '6px',
1959            marginLeft: sender === 'You' ? '6px' : '0',
1960            objectFit: 'cover', opacity: '0', transition: 'opacity .8s ease'
1961        });
1962        wrap.appendChild(av);
1963        requestAnimationFrame(() => { av.style.opacity = '1'; });
1964    }
1965
1966    // --- 2. Create the Bubble and Timestamp ---
1967    const stack = document.createElement('div');
1968    Object.assign(stack.style, {
1969        display: 'inline-flex',
1970        flexDirection: 'column',
1971        alignItems: sender === 'You' ? 'flex-end' : 'flex-start',
1972        maxWidth: '85%'
1973    });
1974
1975    const bubble = document.createElement('div');
1976    
1977    // --- NEW CLASS-BASED STYLING (Replaces inline styles) ---
1978    // 1. Add base class for structure and text reset
1979    bubble.classList.add('furie-chat-bubble-base'); 
1980
1981    // 2. Add specific class for colors and alignment
1982    if (sender === 'You') {
1983        bubble.classList.add('furie-bubble-guest');
1984    } else {
1985        bubble.classList.add('furie-bubble-bot');
1986    }
1987    
1988    // (Note: The large Object.assign block for bubble styles is removed 
1989    // because .furie-chat-bubble-base now handles it with !important overrides)
1990
1991    const ts = document.createElement('div');
1992    Object.assign(ts.style, {
1993        fontSize: '11px', color: '#999', marginTop: '4px',
1994        textAlign: sender === 'You' ? 'right' : 'left',
1995        alignSelf: sender === 'You' ? 'flex-end' : 'flex-start',
1996        padding: '0 2px'
1997    });
1998    
1999    if (specificTimestamp) {
2000        ts.textContent = specificTimestamp;
2001    } else {
2002        ts.textContent = new Date().toLocaleString([], { 
2003            month: 'short', 
2004            day: 'numeric', 
2005            hour: 'numeric', 
2006            minute: '2-digit' 
2007        });
2008    }
2009    
2010    stack.appendChild(bubble);
2011    stack.appendChild(ts);
2012    wrap.appendChild(stack);
2013    messagesDiv.appendChild(wrap);
2014    scrollToBottom();
2015    
2016    requestAnimationFrame(() => {
2017        // If your base CSS handles visibility, this might be redundant but is safe to keep
2018        bubble.classList.add('visible');
2019    });
2020
2021    // --- 3. ANIMATION LOGIC ---
2022    // User messages are never animated
2023    if (sender === 'You' || animate === false) {
2024        bubble.innerHTML = sanitizeHtmlResponse(text);
2025        if (sender !== 'You' && !skipSave) saveHistory(sender, text, avatarUrl);
2026        return bubble;
2027    }
2028    
2029    // Bot/Agent messages are animated
2030    bubble.innerHTML = sanitizeHtmlResponse(text);
2031    if (sender !== 'You' && !skipSave) saveHistory(sender, text);
2032
2033
2034    // Insert animated bullet spans for each <li>
2035    const listItems = bubble.querySelectorAll('li');
2036    listItems.forEach(li => {
2037        if (li.firstChild && li.firstChild.classList && li.firstChild.classList.contains('fade-in-letter-bullet')) return;
2038
2039        const bullet = document.createElement('span');
2040        bullet.className = 'fade-in-letter fade-in-letter-bullet';
2041        bullet.textContent = '';
2042        bullet.style.transitionDelay = '0ms'; 
2043
2044        li.insertBefore(bullet, li.firstChild);
2045    });
2046
2047    // Text-to-speech if toggle is on 
2048    if ((sender === 'FurieBot' || sender === 'Agent') && localStorage.getItem("furrieReadAloud") === "1") {
2049        speakText(text.replace(/<[^>]+>/g, "")); 
2050    }
2051
2052    let letterCount = 0;
2053    const letterDelay = 5; 
2054
2055    function wrapTextNodes(node) {
2056        if (node.nodeType === 3) { // Text node
2057            const fragment = document.createDocumentFragment();
2058            const characters = node.textContent.split('');
2059            
2060            characters.forEach(char => {
2061                const span = document.createElement('span');
2062                span.className = 'fade-in-letter';
2063                span.textContent = char;
2064                span.style.transitionDelay = `${letterCount * letterDelay}ms`;
2065                fragment.appendChild(span);
2066                letterCount++;
2067            });
2068            node.parentNode.replaceChild(fragment, node);
2069        } else if (node.nodeType === 1) { // Element node
2070            Array.from(node.childNodes).forEach(child => wrapTextNodes(child));
2071        }
2072    }
2073
2074    wrapTextNodes(bubble);
2075
2076    requestAnimationFrame(() => {
2077        bubble.classList.add('visible');
2078    });
2079    
2080    const totalDuration = letterCount * letterDelay + 500; 
2081    scrollToBottom(totalDuration);
2082
2083    return bubble;
2084}
2085
2086let activeBusyRemover = null;
2087
2088const nauticalPhrases = [
2089  " "
2090];
2091
2092function randomNauticalPhrase() {
2093  return nauticalPhrases[Math.floor(Math.random() * nauticalPhrases.length)];
2094}
2095
2096
2097
2098function showBusyIndicator() {
2099  if (activeBusyRemover) activeBusyRemover();
2100
2101  const bubble = appendMessage('FurieBot', '', false);
2102
2103bubble.classList.add('furie-compact', 'furie-pulse', 'chat-bubble');
2104
2105// Trigger fade-in next frame
2106requestAnimationFrame(() => {
2107  bubble.classList.add('visible');
2108});
2109
2110  // Hide the timestamp row
2111  const stack = bubble.parentElement;
2112  const ts = stack && stack.children && stack.children[1];
2113  if (ts) ts.style.display = 'none';
2114
2115  bubble.innerHTML = `
2116    <span style="display:inline-flex;align-items:center;gap:2px;margin-left:-10px;">
2117      <span class="furie-dots" aria-live="polite" aria-label="Bot is thinking"></span>
2118    </span>
2119  `;
2120
2121  // Create dot wave
2122  const dots = bubble.querySelector('.furie-dots');
2123  for (let i = 0; i < 3; i++) {
2124    const d = document.createElement('i');
2125    d.textContent = '•';
2126    d.style.animationDelay = (i * 0.08) + 's';
2127    dots.appendChild(d);
2128  }
2129
2130  // Scroll smoothly to bottom so the bubble isn’t cut off
2131  scrollToBottom();
2132
2133  const wrap = stack && stack.parentElement;
2134  const remover = () => {
2135    if (wrap && wrap.remove) wrap.remove();
2136    activeBusyRemover = null;
2137    scrollToBottom();
2138  };
2139  activeBusyRemover = remover;
2140  return remover;
2141}
2142
2143
2144
2145
2146        function loadPusher(callback) {
2147
2148                if (window.Pusher) {
2149
2150                        callback();
2151
2152                        return;
2153
2154                }
2155
2156                const script = document.createElement('script');
2157
2158                script.src = 'https://js.pusher.com/8.2/pusher.min.js';
2159
2160                script.onload = callback;
2161
2162                script.onerror = () => appendMessage('FurieBot', 'Error: Could not load live chat library.');
2163
2164                document.head.appendChild(script);
2165
2166        }
2167
2168
2169
2170        function initializePusher(config) {
2171
2172
2173                if (pusherInstance) {
2174        return; // Pusher is already initialized, do nothing.
2175    }
2176
2177                pusherInstance = new Pusher(config.pusher_key, {
2178
2179                        cluster: config.pusher_cluster,
2180
2181                        encrypted: true
2182
2183                });
2184
2185 
2186
2187pusherChannel = pusherInstance.subscribe(`session-${liveChatSessionId}`);
2188
2189
2190pusherChannel.bind('pusher:subscription_succeeded', () => {
2191        // Send the beacon now that we are ready to hear the reply
2192        sendPageEvent({ meta: { subscription_ready: true } });
2193    });
2194
2195
2196
2197
2198
2199
2200
2201pusherChannel.bind('agent-message', data => {
2202    // An agent message always means the chat is live.
2203    if (!isLiveChatActive) {
2204        isLiveChatActive = true;
2205        sessionStatus = 'open';
2206        // Maximize the window if the agent sends the first message
2207        if (container.style.opacity === "0") {
2208            maximizeBtn();
2209        }
2210        addEndChatButton(); //  'End Chat' button
2211    }
2212
2213    appendMessage('Agent', data.msg, true, data.agent_avatar_url); // Use the main appendMessage function
2214    playChirp();
2215    startTitleNotification();
2216});
2217
2218 
2219 
2220
2221
2222    // --- Listen for proactive messages from the bot ---
2223    pusherChannel.bind('proactive-message', data => {
2224    
2225    
2226    // --- GDPR BLOCK ---
2227    if (isGdprBlocked) {
2228        // Save the message for later instead of ignoring it
2229        pendingProactivePayload = data; 
2230        return; 
2231    }
2232    // -----------------
2233    
2234    // 1. CHECK TOGGLE
2235    if (!isProactiveEnabled) return; 
2236
2237    // Helper: Strip HTML tags and whitespace for "fuzzy" comparison
2238    const normalize = (str) => (str || "").replace(/<[^>]+>/g, "").replace(/\s+/g, "").toLowerCase();
2239    const incomingNormalized = normalize(data.msg);
2240
2241    // 2. CHECK LOCAL STORAGE FLAG (Session Scoped)
2242    // If we specifically marked this session as having handled THIS proactive event, stop.
2243    // We use a hash or simplified version of the msg to make the key unique to the content.
2244    // This allows *different* proactive messages on other pages, but blocks *this specific one* from repeating.
2245    const msgHash = incomingNormalized.slice(0, 20); // First 20 chars as ID
2246    const storageKey = `furrie_shown_${furieSessionId}_${msgHash}`;
2247    
2248    if (localStorage.getItem(storageKey)) {
2249        return; 
2250    }
2251
2252    // 3. CHECK CHAT HISTORY (Robust)
2253    const history = JSON.parse(localStorage.getItem("furrieChatHistory") || "[]");
2254    
2255    // Check if ANY message in history (Bot OR Agent) loosely matches the incoming content.
2256    const alreadyInHistory = history.some(msg => {
2257        const isBot = (msg.sender === 'FurieBot' || msg.sender === 'bot' || msg.sender === 'Agent');
2258        return isBot && normalize(msg.text) === incomingNormalized;
2259    });
2260
2261    if (alreadyInHistory) {
2262        // It's already in the chat log (loaded from DB), so mark it as shown and exit.
2263        localStorage.setItem(storageKey, "1");
2264        return;
2265    }
2266
2267    // 4. CHECK IF USER HAS CHATTED (Optional but recommended based on your request)
2268    // "Now if the user has chatted... the message repeats" -> This fixes that specific loop.
2269    // If the chat log contains user messages, we generally assume the "opener" is no longer needed.
2270    const userHasChatted = history.some(msg => msg.sender === 'You');
2271    
2272    // If you want to BLOCK proactive messages strictly if the user has already started chatting:
2273    if (userHasChatted) {
2274         localStorage.setItem(storageKey, "1"); // Suppress it
2275         return;
2276    }
2277
2278    // 5. Mark as shown & Display
2279    localStorage.setItem(storageKey, "1");
2280
2281    appendMessage('FurieBot', data.msg, true, null); 
2282    playChirp();
2283    startTitleNotification();
2284
2285    if (data.maximize === true && container.style.opacity === "0") {
2286        maximizeBtn(); 
2287    }
2288});
2289
2290
2291
2292
2293pusherChannel.bind('session-closed', () => {
2294    if (!isLiveChatActive) return;
2295
2296    isLiveChatActive = false;
2297
2298    // Lock live-chat UI
2299    input.disabled = true;
2300    input.placeholder = 'Chat has ended.';
2301    document.getElementById('furrie-form').style.opacity = '0.5';
2302    document.getElementById('livechat-end-btn')?.remove();
2303
2304    
2305    const btnColor = window.__furie_button_color || '#5AC0DE'; // Use dynamic color with fallback
2306    const textColor = idealTextColor(btnColor); // Calculate contrasting text color
2307
2308    // Show message + CTA in ONE bubble (no typing animation)
2309    const html = `
2310    <div style="margin:-20px 0 0px 0; line-height:1; font-size:15px; padding-top:20px;">Thanks 🙏 for speaking with us! ❤️ The human agent chat session has ended.</br></br>Continue ➡️ the conversation 💬 with the AI 🤖 assistant below.</div>
2311    <div style="display:flex;flex-wrap:wrap;">
2312    
2313        <button id="new-convo-btn"
2314                style="background-color:${btnColor};color:${textColor};border:none;
2315                       padding:6px 10px;border-radius:25px;cursor:pointer;
2316                       font-size:15px;line-height:1;font-weight:600;">Start a Fresh Chat</button>
2317    </div>
2318    `;
2319  
2320    appendMessage('FurieBot', html, false);
2321
2322    // Audible cue
2323    playChirp();
2324});
2325
2326        }
2327
2328function resumeBotMode() {
2329  // Re-enable bot UI
2330  isLiveChatActive = false;                 // ensure we're in bot mode
2331  sessionStatus = 'bot';
2332  input.disabled = false;
2333  input.placeholder = 'How can I help you?';
2334  document.getElementById('furrie-form').style.opacity = '1';
2335
2336  // Tell backend this browser is back to bot mode (helps agent dashboard)
2337  if (furieSessionId) {
2338    fetch(apiEndpoint, {
2339      method: 'POST',
2340      headers: { 'Content-Type': 'application/json' },
2341      body: JSON.stringify({
2342        action: 'update_status',
2343        session_id: furieSessionId,
2344        new_status: 'bot'
2345      })
2346    }).catch(()=>{});
2347  }
2348
2349  appendMessage('FurieBot', "I'm back on-line. How can I help? 🧭");
2350  input.focus();
2351}
2352
2353function startNewConversation() {
2354  // Clear local bot history only (keeps live-chat history in your DB)
2355    localStorage.removeItem(getStorageKey('furrieChatHistory'));
2356
2357  // Also clear name/email prompt flag if you want to re-ask:
2358  // localStorage.removeItem('furrieAskedInfo');
2359
2360  // Optionally create a brand-new bot session ID:
2361  // This will ask backend to create a new 'Browse' session
2362  fetch(apiEndpoint, {
2363    method: 'POST',
2364    headers: { 'Content-Type': 'application/json' },
2365    body: JSON.stringify({
2366      action: 'create_session',
2367      propertyId: propertyId,
2368      existingSessionId: null,
2369      guestName: userName || 'Guest',
2370      guestEmail: userEmail || ''
2371    })
2372  })
2373  .then(r => r.json())
2374  .then(data => {
2375    if (data && data.session_id) {
2376      furieSessionId = data.session_id;
2377      localStorage.setItem(getStorageKey('furieSessionId'), furieSessionId);
2378    }
2379  })
2380  .catch(()=>{});
2381
2382  // Reset UI like resume
2383  input.disabled = false;
2384  input.placeholder = 'How can I help you?';
2385  document.getElementById('furrie-form').style.opacity = '1';
2386  sessionStatus = 'bot';
2387  isLiveChatActive = false;
2388
2389  // Visual reset: divider and greeting
2390  appendMessage('FurieBot', 'Starting a new conversation. How can I help?');
2391  input.focus();
2392}
2393
2394// Single delegated listener for both buttons
2395messagesDiv.addEventListener('click', (e) => {
2396  const t = e.target;
2397  if (!t) return;
2398  if (t.id === 'resume-bot-btn') {
2399    resumeBotMode();
2400  } else if (t.id === 'new-convo-btn') {
2401    startNewConversation();
2402  }
2403});          
2404
2405function appendAgentMessage(sender, text) {
2406    const bubble = appendMessage('Agent', text, false); // No typing animation
2407    if (bubble) {
2408        bubble.style.backgroundColor = '#fff';
2409        bubble.style.borderRadius = '25px 25px 25px 5px';
2410
2411        // Fade-in effect
2412        bubble.style.opacity = '0';
2413        bubble.style.transition = 'opacity .8s ease';
2414        requestAnimationFrame(() => {
2415            bubble.style.opacity = '1';
2416        });
2417    }
2418}
2419
2420
2421
2422        function startLiveChat(handoffData) {
2423
2424                isLiveChatActive = true;
2425
2426                liveChatSessionId = handoffData.session_id;
2427
2428                liveChatApiEndpoint = handoffData.live_chat_api;
2429
2430                liveChatPropertyId = handoffData.property_id;
2431
2432                loadPusher(() => initializePusher(handoffData));
2433
2434                addEndChatButton();
2435
2436        }
2437
2438
2439
2440function addEndChatButton() {
2441    const controls = document.getElementById('furrie-header-controls');
2442    if (!controls) return;
2443
2444    // Prevent duplicates
2445    if (document.getElementById('livechat-end-btn')) return;
2446
2447    const endButton = document.createElement('button');
2448    endButton.id = 'livechat-end-btn';
2449    endButton.textContent = 'End';
2450
2451    Object.assign(endButton.style, {
2452        background: '#de712c',
2453        color: 'white',
2454        border: 'none',
2455        padding: '4px 10px', // Adjusted padding for better fit
2456        borderRadius: '15px',
2457        cursor: 'pointer',
2458        fontWeight: 'bold',
2459        fontSize: '11px',
2460        marginRight: '6px',
2461        flexShrink: '0'
2462    });
2463
2464    endButton.onclick = () => {
2465        if (confirm('Are you sure you want to end this chat?')) {
2466            fetch(liveChatApiEndpoint, {
2467                method: 'POST',
2468                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2469                body: new URLSearchParams({
2470                    action: 'close-session',
2471                    session_id: liveChatSessionId,
2472                    property_id: liveChatPropertyId
2473                })
2474            }).catch(err => console.error('Failed to end session:', err));
2475        }
2476    };
2477
2478    // Insert at the very start of the controls (left of the other icons)
2479    controls.insertBefore(endButton, controls.firstChild);
2480}
2481 
2482
2483
2484async function promptForUserInfo() {
2485    return new Promise(resolve => {
2486        const wrapper = document.createElement("div");
2487        wrapper.style.opacity = "0";
2488        wrapper.style.transition = "opacity 1.5s ease";
2489        wrapper.style.margin = "0px";
2490        
2491     
2492        const btnColor = window.__furie_button_color || '#5AC0DE'; // Use dynamic color with fallback
2493        const textColor = idealTextColor(btnColor); // Calculate contrasting text color
2494        
2495        wrapper.innerHTML = `
2496            <input type="text" id="guest-name" placeholder="Your Name 🏷️" style="margin-top: 10px; margin-bottom: 5px; width: 70%; padding: 6px; border: 1px solid #ccc; border-radius: 25px;" />
2497            <input type="email" id="guest-email" placeholder="Your Email @" style="margin-top: 2px; margin-bottom: 15px; width: 70%; padding: 6px; border: 1px solid #ccc; border-radius: 25px;" />
2498            <button id="guest-submit" style="background-color: ${btnColor}; color: ${textColor}; border: none; padding: 8px 12px; border-radius: 25px; cursor: pointer; font-weight: bold;">➤️</button><br><br><span style="font-size:0.6em">I am AI-powered and can make mistakes. By continuing, you agree this chat may be logged for ticket tracking, training, or other purposes. Do not type anything here you would not share in public. By continuing, you agree to our Privacy Policy and Terms and Conditions of Use.</span><br><br>`;
2499       
2500
2501        messagesDiv.appendChild(wrapper);
2502        wrapper.scrollIntoView({ behavior: "smooth", block: "end" });
2503        requestAnimationFrame(() => (wrapper.style.opacity = "1"));
2504
2505        const nameField = wrapper.querySelector("#guest-name");
2506        const emailField = wrapper.querySelector("#guest-email");
2507        const submitButton = wrapper.querySelector("#guest-submit");
2508
2509        [nameField, emailField].forEach(field => {
2510            field.addEventListener("keydown", (event) => {
2511                if (event.key === "Enter") {
2512                    event.preventDefault();
2513                    submitButton.click();
2514                }
2515            });
2516        });
2517
2518        submitButton.onclick = () => {
2519            const nameInput = nameField.value.trim();
2520            const emailInput = emailField.value.trim();
2521            if (nameInput) {
2522                userName = nameInput;
2523                localStorage.setItem("furrieUserName", userName);
2524            }
2525            if (emailInput) {
2526                userEmail = emailInput;
2527                localStorage.setItem("furrieUserEmail", userEmail);
2528            }
2529            
2530            // --- FIX: Update the server immediately with the new Name/Email ---
2531            if (furieSessionId && (userName || userEmail)) {
2532                fetch(apiEndpoint, {
2533                    method: "POST",
2534                    headers: { "Content-Type": "application/json" },
2535                    body: JSON.stringify({
2536                        // Use the action that triggers name updates
2537                        action: 'create_session', 
2538                        session_id: furieSessionId, // Existing session
2539                        existingSessionId: furieSessionId,
2540                        propertyId: propertyId,
2541                        guestName: userName,  // Send the data we just captured
2542                        guestEmail: userEmail
2543                    })
2544                }).catch(err => console.error("Failed to sync name update:", err));
2545            }
2546            // ------------------------------------------------------------------
2547
2548            localStorage.setItem("furrieAskedInfo", "1");
2549            wrapper.remove();
2550            resolve();
2551        };
2552    });
2553}
2554
2555
2556/**
2557 * Displays a confirmation message with 'Yes' and 'No' buttons for live agent handoff.
2558 */
2559function promptForLiveAgentConfirmation() {
2560    // Get the dynamic color, with a fallback to white if it's not set
2561    const bgColor = window.__furie_button_color || '#ffffff';
2562    // Automatically calculate the best text color (black or white) for readability
2563    const fgColor = idealTextColor(bgColor);
2564
2565    // Create a wrapper for the confirmation message and buttons
2566    const confirmationWrapper = document.createElement("div");
2567    Object.assign(confirmationWrapper.style, {
2568        opacity: "0",
2569        transition: "opacity 1.0s ease",
2570        padding: "10px",
2571        textAlign: "center",
2572        backgroundColor: "#fff",
2573        borderRadius: "25px",
2574        margin: "10px 0 22px 0"
2575    });
2576
2577    const message = document.createElement("p");
2578    message.textContent = "💁‍♀️🎧?";
2579    message.style.marginBottom = "10px";
2580    confirmationWrapper.appendChild(message);
2581
2582    const buttonContainer = document.createElement("div");
2583    confirmationWrapper.appendChild(buttonContainer);
2584
2585    // --- 'Yes' Button ---
2586    const yesButton = document.createElement("button");
2587    yesButton.textContent = "👍";
2588    Object.assign(yesButton.style, {
2589        backgroundColor: bgColor,
2590        color: fgColor, 
2591        border: "none",
2592        padding: "10px 20px",
2593        borderRadius: "20px",
2594        cursor: "pointer",
2595        fontWeight: "bold",
2596        marginRight: "10px"
2597    });
2598
2599    yesButton.onclick = () => {
2600        confirmationWrapper.remove();
2601        appendMessage("You", "👍", false); // Show user's choice in the chat
2602        
2603        // This payload uses the special phrase the backend is now looking for
2604        const handoffPayload = {
2605            question: "##CONFIRM_HANDOFF##",
2606            propertyId,
2607            guestname: userName,
2608            guestemail: userEmail,
2609            history: JSON.parse(localStorage.getItem("furrieChatHistory") || "[]")
2610        };
2611        
2612        // We call the main bot request handler to trigger the handoff on the backend
2613        handleBotRequest(handoffPayload);
2614    };
2615
2616    // --- 'No' Button ---
2617    const noButton = document.createElement("button");
2618    noButton.textContent = "👎";
2619    Object.assign(noButton.style, {
2620        backgroundColor: "#9da0b1", // A clear 'no' color
2621        color: "white",
2622        border: "none",
2623        padding: "10px 20px",
2624        borderRadius: "20px",
2625        cursor: "pointer",
2626        fontWeight: "bold"
2627    });
2628
2629    noButton.onclick = () => {
2630        confirmationWrapper.remove();
2631        appendMessage("You", "No", false); // Show user's choice
2632        appendMessage("FurieBot", "🙂");
2633    };
2634    
2635    buttonContainer.appendChild(yesButton);
2636    buttonContainer.appendChild(noButton);
2637
2638    messagesDiv.appendChild(confirmationWrapper);
2639    requestAnimationFrame(() => (confirmationWrapper.style.opacity = "1"));
2640    confirmationWrapper.scrollIntoView({ behavior: "smooth", block: "end" });
2641}
2642
2643
2644        // --- UI EVENT LISTENERS AND MAIN LOGIC ---
2645
2646
2647
2648const minimizeBtn = () => {
2649            container.style.transform = "translateY(100%)";
2650            container.style.opacity = "0";
2651            container.style.pointerEvents = "none";
2652            
2653            // Show the entire wrapper (Launcher + Pill)
2654            setTimeout(() => { 
2655                launcherWrapper.style.display = "flex"; 
2656            }, 300);
2657    };
2658
2659    const maximizeBtn = () => {
2660            container.style.transform = "translateY(0)";
2661            container.style.opacity = "1";
2662            container.style.pointerEvents = "auto";
2663            
2664            // Hide the entire wrapper
2665            launcherWrapper.style.display = "none";
2666
2667            setTimeout(() => input.focus(), 300);
2668    };
2669    
2670    // --- CLICK LISTENER FOR PILL ---
2671    promoPill.addEventListener("click", (e) => {
2672        // Only hijack the click if we are in AI/Custom mode
2673        if (isCustomHooksActive) {
2674            e.preventDefault(); // Stop any href navigation
2675            maximizeBtn();      // Open the chat window
2676
2677            // 1. Retrieve the data we stored in the loop above
2678            const url = promoPill.dataset.pendingUrl;
2679            const text = promoPill.dataset.pendingText;
2680
2681            // 2. If a URL exists for this specific message, inject the bot message
2682            if (url) {
2683                // Construct the HTML message
2684                const linkMsg = `Click here: <br><a href="${url}" target="_blank" style="text-decoration: underline;
2684 color: inherit; font-weight: 700;">${text}</a><br><br>Or type below.`;
2685                
2686                // Inject into chat stream as 'FurieBot' (false = no typing animation)
2687                appendMessage('FurieBot', linkMsg, false);
2688            }
2689        }
2690        // If not custom mode, let it bubble (defaults to furie.ai href)
2691    });
2692
2693launcher.addEventListener("click", maximizeBtn);
2694const minBtnEl = container.querySelector("#furrie-minimize");
2695if (minBtnEl) minBtnEl.addEventListener("click", minimizeBtn);
2696
2697
2698
2699        // Launcher text animation
2700
2701        let isShrunk = false;
2702
2703        setTimeout(() => {
2704
2705                if (!textSpan) return;
2706
2707                textSpan.style.width = textSpan.offsetWidth + "px";
2708
2709                requestAnimationFrame(() => {
2710
2711                        setTimeout(() => {
2712
2713                                textSpan.style.opacity = "0";
2714
2715                                textSpan.style.width = "0px";
2716
2717                                textSpan.style.marginRight = "2px";
2718
2719                                isShrunk = true;
2720
2721                        }, 100);
2722
2723                });
2724
2725        }, 5000);
2726
2727        // ... (mouseenter/mouseleave for launcher remains the same) ...
2728
2729           launcher.addEventListener("mouseenter", () => {
2730
2731                if (isShrunk) {
2732
2733                        const temp = textSpan.cloneNode(true);
2734
2735                        Object.assign(temp.style, {position: "absolute", visibility: "hidden", width: "auto", opacity: "0", whiteSpace: "nowrap"});
2736
2737                        document.body.appendChild(temp);
2738
2739                        const fullWidth = temp.offsetWidth + "px";
2740
2741                        document.body.removeChild(temp);
2742
2743                        textSpan.style.width = fullWidth;
2744
2745                        textSpan.style.opacity = "1";
2746
2747                        textSpan.style.marginRight = "8px";
2748
2749                }
2750
2751        });
2752
2753        launcher.addEventListener("mouseleave", () => {
2754
2755                if (isShrunk) {
2756
2757                        requestAnimationFrame(() => {
2758
2759                                textSpan.style.opacity = "0";
2760
2761                                textSpan.style.width = "0px";
2762
2763                                textSpan.style.marginRight = "0";
2764
2765                        });
2766
2767                }
2768
2769        });
2770
2771
2772
2773        // Load initial chat history
2774
2775//        JSON.parse(localStorage.getItem("furrieChatHistory") || "[]")
2776//    .forEach(msg => appendMessage(msg.sender, msg.text, false, msg.avatarUrl));
2777
2778//        messagesDiv.scrollTop = messagesDiv.scrollHeight;
2779
2780
2781
2782
2783
2784        // --- MAIN FORM SUBMISSION LOGIC ---
2785
2786/**
2787 * Reusable function to send a payload to the bot and handle the response.
2788 * This can be called by both the main form and the confirmation buttons.
2789 * @param {object} payload The data to send to the bot API.
2790 */
2791async function handleBotRequest(payload) {
2792    
2793
2794    // The main request to the bot now handles all logic.
2795    try {
2796
2797		
2798		
2799        const stopBusy = showBusyIndicator();  // <-- show the wave dots
2800
2801
2802        const response = await fetch(apiEndpoint, {
2803            method: "POST",
2804            headers: { "Content-Type": "application/json" },
2805            body: JSON.stringify(payload),
2806        });
2807
2808        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
2809        const data = await response.json();
2810        stopBusy();
2811    
2812        // Update the local status after the bot has successfully processed the message
2813        if (sessionStatus === 'Browse') {
2814            sessionStatus = 'bot';
2815        }
2816        if (data.action === 'confirm_live_agent') {
2817            promptForLiveAgentConfirmation();
2818            return;
2819        }
2820
2821        // Case 2 (NEW): Bot's message triggered a confirmation.
2822        // We must display the bot's message FIRST, then the prompt.
2823        if (data.action === 'answer_and_confirm_handoff') {
2824            const reply = data.answer;
2825            appendMessage("FurieBot", reply);
2826
2827            // Wait for the typing animation to finish, then show the prompt.
2828   const animMs = reply.split(/\s+/).length * 80 + 200; // 80ms/word + buffer
2829   setTimeout(promptForLiveAgentConfirmation, animMs);
2830
2831            // Also handle any redirect buttons that might have been sent
2832            if (data.redirect && Array.isArray(data.buttons)) {
2833                // (This is the same redirect logic from your original code)
2834                const redirectBlock = document.createElement("div");
2835                redirectBlock.style.opacity = "0";
2836                redirectBlock.style.transition = "opacity 0.8s ease";
2837                let html = `<div style="margin-top: 4px; margin-bottom: 10px; margin-left: 22px;">${data.message || ""}</div><div>`;
2838                data.buttons.forEach(btn => {
2839const ctaColor = window.__furie_redirect_btn_color || '#5AC0DE';
2840html += `<a href="${btn.button_url}" target="_blank"
2841          style="display:inline-block;margin:4px 4px 4px 22px;
2842                 background-color:${ctaColor}; color:#fff; padding:8px 14px;
2843                 border-radius:25px;text-decoration:none;font-weight:bold;">
2844          ${btn.button_text}
2845        </a>`;
2846                });
2847                html += "</div>";
2848                redirectBlock.innerHTML = html;
2849                setTimeout(() => {
2850                    redirectBlock.classList.add('chat-bubble'); // reuse the fade-in style
2851                    messagesDiv.appendChild(redirectBlock);
2852
2853               requestAnimationFrame(() => {
2854                redirectBlock.classList.add('visible');
2855                scrollToBottom(); // gradual scroll to bottom over ~0.8s
2856                });
2857
2858  //  saveHistory("FurieBot", data.message || "");
2859}, animMs);
2860            }
2861            return; // Processing for this response is complete.
2862        }
2863
2864        // Case 3: User clicked "Yes," and backend is starting the live chat.
2865        if (data.action === 'initiate_handoff') {
2866            appendMessage("FurieBot", data.answer); // Displays "One moment..."
2867            startLiveChat(data.handoff_data);
2868            return;
2869            
2870        }
2871
2872        // --- DEFAULT BEHAVIOR ---
2873        // If no special action, just display the bot's answer and any redirect buttons.
2874        const reply = data.answer || "Oops! I had a little hiccup. Please try again.";
2875        appendMessage("FurieBot", reply);
2876
2877if (data.redirect && Array.isArray(data.buttons)) {
2878             // (This is the same redirect logic from your original code)
2879            const redirectBlock = document.createElement("div");
2880            redirectBlock.style.opacity = "0";
2881            redirectBlock.style.transition = "opacity 0.8s ease";
2882            let html = `<div style="margin-top: 4px; margin-left: 22px;">${data.message || ""}</div><div>`;
2883            data.buttons.forEach(btn => {
2884const ctaColor = window.__furie_redirect_btn_color || '#5AC0DE';
2885html += `<a href="${btn.button_url}" target="_blank"
2886            style="display:inline-block;margin:4px 4px 12px 22px;
2887                background-color:${ctaColor}; color:#fff; padding:8px 14px;
2888                border-radius:25px;text-decoration:none;font-weight:bold;">
2889            ${btn.button_text}
2890            </a>`;
2891            });
2892            html += "</div>";
2893            redirectBlock.innerHTML = html;
2894            const animMs = reply.split(/\s+/).length * 80 + 200;
2895            setTimeout(() => {
2896                messagesDiv.appendChild(redirectBlock);
2897                requestAnimationFrame(() => {
2898                    redirectBlock.style.opacity = "1";
2899                    redirectBlock.scrollIntoView({ behavior: "smooth", block: "end" });
2900                });
2901               // saveHistory("FurieBot", data.message || "");
2902            }, animMs);
2903        }
2904
2905    } catch (err) {
2906        stopBusy();        
2907        console.error("Fetch error:", err);
2908        appendMessage("FurieBot", "Sorry, I couldn’t find an answer! Please try again.");
2909    }
2910}
2911
2912
2913// --- GuestC ---
2914const trackingEndpoint = "https://www.furie.ai/guest_track.php";
2915const proactivemessageEndpoint = "https://www.furie.ai/livechat/guest_track.php";
2916let pageStartTs = performance.now();
2917
2918
2919
2920function currentUtm() {
2921  const u = new URL(location.href);
2922  const g = x => u.searchParams.get(x) || "";
2923  return {
2924    utm_source: g("utm_source"),
2925    utm_medium: g("utm_medium"),
2926    utm_campaign: g("utm_campaign"),
2927    utm_term: g("utm_term"),
2928    utm_content: g("utm_content"),
2929  };
2930}
2931function scrollPct() {
2932  const doc = document.documentElement;
2933  const denom = (doc.scrollHeight - window.innerHeight);
2934  if (denom <= 0) return 0;
2935  return Math.max(0, Math.min(100, Math.round((window.scrollY / denom) * 100)));
2936}
2937
2938function sendPageEvent(extra = {}) {
2939  if (!canTrackBehavior()) return;
2940  if (!furieSessionId) return;
2941
2942  const payload = {
2943    session_id: furieSessionId,
2944    propertyId,
2945    url: location.href.split('#')[0],
2946    title: document.title,
2947    referrer: document.referrer || "",
2948    pagecontents: getCleanPageText().slice(0, 30000),
2949    time_spent_ms: Math.round(performance.now() - pageStartTs),
2950    viewport_w: window.innerWidth,
2951    viewport_h: window.innerHeight,
2952    scroll_pct: scrollPct(),
2953    ...currentUtm(),
2954    meta: extra.meta || null
2955  };
2956
2957  navigator.sendBeacon(trackingEndpoint, JSON.stringify(payload));
2958  navigator.sendBeacon(proactivemessageEndpoint, JSON.stringify(payload));
2959}
2960// on hide/unload
2961document.addEventListener('visibilitychange', () => {
2962  if (document.visibilityState === 'hidden') sendPageEvent();
2963});
2964window.addEventListener('pagehide', () => sendPageEvent());
2965// simple heartbeat (optional): every 5s
2966setInterval(() => sendPageEvent({meta:{heartbeat:true}}), 5000);
2967
2968
2969// ---  MAIN FORM SUBMISSION LOGIC ---
2970form.addEventListener("submit", async (e) => {
2971    e.preventDefault();
2972    const userInput = input.value.trim();
2973    if (!userInput) return;
2974
2975    saveHistory('You', userInput);
2976
2977    // Live chat mode logic remains unchanged
2978    if (isLiveChatActive) {
2979        appendMessage("You", userInput, false);
2980        input.value = "";
2981        try {
2982            await fetch(liveChatApiEndpoint, {
2983                method: 'POST',
2984                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2985                body: new URLSearchParams({ session_id: liveChatSessionId, property_id: liveChatPropertyId, message: userInput, action: 'process-message' })
2986            });
2987        } catch (err) {
2988            console.error('Live chat send error:', err);
2989        }
2990        return;
2991    }
2992
2993    // Default Bot Behavior
2994    if (!infoCollected && (!userName || !userEmail)) {
2995        await promptForUserInfo();
2996        infoCollected = true;
2997    }
2998
2999    appendMessage("You", userInput, false);
3000    input.value = "";
3001
3002    // Create the payload and call the new refactored function
3003const payload = {
3004    session_id: furieSessionId, 
3005    question: userInput,
3006    propertyId,
3007    guestname: userName,
3008    guestemail: userEmail,
3009    history: JSON.parse(localStorage.getItem("furrieChatHistory") || "[]")
3010,
3011
3012    // Visible Text Passthrough:
3013    pagecontents: getVisibleText().slice(0, 500000),   // cap if you like
3014    page_url: location.href,
3015    page_title: document.title
3016  };
3017  
3018    await handleBotRequest(payload);
3019});
3020
3021document.addEventListener('visibilitychange', () => {
3022    // When the user brings the tab back into focus, stop the notification.
3023    if (document.visibilityState === 'visible') {
3024        stopTitleNotification();
3025    }
3026});
3027
3028// Only run when DOM is ready so getCleanPageText() can see the body content
3029if (document.readyState === 'loading') {
3030    document.addEventListener('DOMContentLoaded', createOrResumeSession);
3031} else {
3032    createOrResumeSession();
3033}
3034})();

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.