// ==================== CHATBOT VUELOS EN GLOBO - INTEGRACIÓN n8n ==================== (function() { "use strict"; // ---------- CONFIGURACIÓN CENTRAL ---------- const CONFIG = { N8N_WEBHOOK_ID: 'd9de4096-c21a-41e8-aa2c-30dd1de5c3ba/chat', N8N_BASE_URL: 'https://n8npix.pixelteo.com/', get CHAT_API_URL() { return `${this.N8N_BASE_URL}/webhook/${this.N8N_WEBHOOK_ID}`; }, TIMEOUT_MS: 45000, MAX_RETRIES: 2, SESSION_STORAGE_KEY: 'chatbotSessionGlobo', CHAT_HISTORY_KEY: 'chatbotHistoryGlobo', MAX_HISTORY_MESSAGES: 50, SESSION_MAX_IDLE_MINUTES: 30 // Tiempo en minutos para resetear conversación }; // ---------- 0. INYECTAR CSS DINÁMICAMENTE ---------- const injectStyles = () => { if (document.getElementById('chatbot-dynamic-styles')) return; const style = document.createElement('style'); style.id = 'chatbot-dynamic-styles'; style.textContent = ` .chatbot-container { position: fixed; bottom: 90px; right: 20px; width: 380px; max-width: calc(100vw - 40px); height: 550px; background: white; border-radius: 24px; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; overflow: hidden; z-index: 9999; transform: scale(0.9); opacity: 0; visibility: hidden; transition: all 0.25s ease; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, sans-serif; } .chatbot-container.active { transform: scale(1); opacity: 1; visibility: visible; } .chatbot-header { background: linear-gradient(135deg, #04cdff, #0099cc); color: white; padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; font-weight: bold; font-size: 1.1rem; } .chatbot-header h3 { margin: 0; font-size: 1.2rem; display: flex; align-items: center; gap: 8px; } .chatbot-header button { background: none; border: none; color: white; font-size: 1.4rem; cursor: pointer; padding: 0; line-height: 1; opacity: 0.8; transition: opacity 0.2s; } .chatbot-header button:hover { opacity: 1; } .chatbot-messages { flex: 1; overflow-y: auto; padding: 16px; background: #f9fafb; display: flex; flex-direction: column; gap: 12px; } .chatbot-message { max-width: 85%; padding: 10px 14px; border-radius: 18px; font-size: 0.9rem; line-height: 1.4; word-wrap: break-word; animation: fadeInUp 0.2s ease-out; } .chatbot-message.user { background: #04cdff; color: white; align-self: flex-end; border-bottom-right-radius: 4px; } .chatbot-message.bot { background: white; color: #1f2937; border: 1px solid #e5e7eb; align-self: flex-start; border-bottom-left-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } .chatbot-message.error { background: #fee2e2; color: #b91c1c; border: 1px solid #fecaca; align-self: center; text-align: center; font-size: 0.85rem; } .chatbot-message img { max-width: 100%; border-radius: 16px; margin: 10px 0; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); display: block; } .chatbot-message video { max-width: 100%; border-radius: 16px; margin: 10px 0; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); display: block; } .chatbot-message h2, .chatbot-message h3 { margin: 0.75rem 0 0.5rem; font-weight: 600; color: #1e293b; } .chatbot-message.bot h2 { font-size: 1.3rem; border-left: 4px solid #04cdff; padding-left: 12px; } .chatbot-message p { margin: 0.5rem 0; } .chatbot-message ul, .chatbot-message ol { margin: 0.5rem 0; padding-left: 1.5rem; } .chatbot-message li { margin: 0.25rem 0; } .chatbot-message a { color: #04cdff; text-decoration: underline; font-weight: 500; cursor: pointer; } .chatbot-message a:hover { color: #0099cc; } .chatbot-price { color: #10b981; font-weight: bold; background: #e0f2fe; padding: 2px 6px; border-radius: 20px; display: inline-block; } .chatbot-message .btn, .chatbot-message button, .chatbot-message a[style*="background:#ff9800"] { background: linear-gradient(135deg, #ff9800, #f57c00); color: white; border: none; padding: 12px 22px; border-radius: 10px; font-weight: bold; cursor: pointer; transition: transform 0.1s, background 0.2s; display: inline-block; margin: 8px 4px 0 0; text-decoration: none; font-size: 1rem; } .chatbot-message .btn:hover, .chatbot-message button:hover, .chatbot-message a[style*="background:#ff9800"]:hover { background: linear-gradient(135deg, #f57c00, #e65100); transform: translateY(-2px); } .chatbot-message code { background: #f1f5f9; padding: 2px 6px; border-radius: 8px; font-family: monospace; font-size: 0.85em; } .chatbot-message hr { margin: 12px 0; border: none; border-top: 1px solid #e2e8f0; } .chatbot-footer { display: flex; gap: 8px; padding: 12px; background: white; border-top: 1px solid #e5e7eb; } .chatbot-footer input { flex: 1; padding: 10px 14px; border: 1px solid #d1d5db; border-radius: 40px; font-size: 0.9rem; outline: none; transition: border 0.2s; } .chatbot-footer input:focus { border-color: #04cdff; box-shadow: 0 0 0 2px rgba(4,205,255,0.2); } .chatbot-footer button { background: #04cdff; border: none; color: white; width: 42px; border-radius: 40px; cursor: pointer; transition: background 0.2s; display: flex; align-items: center; justify-content: center; } .chatbot-footer button:hover:not(:disabled) { background: #0099cc; } .chatbot-footer button:disabled { opacity: 0.5; cursor: not-allowed; } .chatbot-status { font-size: 0.7rem; padding: 4px 12px; background: #f1f5f9; text-align: center; color: #475569; border-top: 1px solid #e2e8f0; } .chatbot-loading { display: flex; gap: 4px; align-items: center; justify-content: center; padding: 8px; background: white; border-radius: 20px; width: 50px; margin: 8px 0; align-self: flex-start; } .chatbot-loading span { width: 8px; height: 8px; background: #94a3b8; border-radius: 50%; animation: bounce 1.4s infinite ease-in-out both; } .chatbot-loading span:nth-child(1) { animation-delay: -0.32s; } .chatbot-loading span:nth-child(2) { animation-delay: -0.16s; } @keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } } @keyframes fadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } body.light-mode .chatbot-container { background: #ffffff; } body.light-mode .chatbot-messages { background: #f9fafb; } body.light-mode .chatbot-message.bot { background: #ffffff; border-color: #e2e8f0; } /* NUEVO: Botón flotante de cierre para móviles */ .chatbot-close-float { display: none; /* oculto por defecto */ position: fixed; top: 16px; right: 16px; width: 40px; height: 40px; border-radius: 50%; background: rgba(0,0,0,0.5); color: white; border: none; font-size: 1.5rem; cursor: pointer; z-index: 10000; align-items: center; justify-content: center; backdrop-filter: blur(4px); transition: transform 0.2s; } .chatbot-close-float:hover { transform: scale(1.1); } /* Ajuste para pantalla completa en móviles */ @media (max-width: 480px) { .chatbot-container { width: 100%; max-width: 100%; bottom: 0; right: 0; left: 0; height: 100vh; /* Altura completa de la ventana */ height: 100dvh; /* Soporte para navegadores modernos */ border-radius: 0; /* Sin bordes redondeados */ transform: none; transition: transform 0.25s ease, opacity 0.25s ease; } .chatbot-container.active { transform: none; opacity: 1; visibility: visible; } .chatbot-message { max-width: 90%; } .chatbot-close-float { display: flex; /* visible solo en móviles cuando chat activo */ } } .chatbot-float-btn { position: fixed; bottom: 20px; right: 20px; width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, #04cdff, #0099cc); border: none; box-shadow: 0 4px 12px rgba(0,0,0,0.2); color: white; font-size: 28px; cursor: pointer; z-index: 9998; transition: transform 0.2s; } .chatbot-float-btn:hover { transform: scale(1.05); } `; document.head.appendChild(style); }; // ==================== 1. UTILIDADES GENERALES ==================== const utils = { throttle(fn, delay) { let lastCall = 0; return function (...args) { const now = Date.now(); if (now - lastCall >= delay) { lastCall = now; fn.apply(this, args); } }; }, debounce(fn, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }, escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>"']/g, function(m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; if (m === '"') return '"'; if (m === "'") return '''; return m; }); }, generateUUID() { if (crypto && crypto.randomUUID) return crypto.randomUUID(); return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }, formatMessage(text) { if (!text) return ""; let formatted = text; // Convertir Markdown imágenes a HTML formatted = formatted.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => { const isVideo = src.toLowerCase().endsWith('.mp4') || src.toLowerCase().endsWith('.webm'); if (isVideo) { return ``; } return `${this.escapeHtml(alt)}`; }); // Convertir Markdown enlaces a HTML formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Convertir negrita formatted = formatted.replace(/\*\*(.+?)\*\*/g, '$1'); // Convertir itálica formatted = formatted.replace(/\*(.+?)\*/g, '$1'); // Convertir saltos de línea formatted = formatted.replace(/\n/g, '
'); // Resaltar precios formatted = formatted.replace(/\$(\d+(?:,\d+)*(?:\.\d+)?)/g, '$$$1'); return formatted; }, // Buscar vuelos directamente desde n8n async searchVuelosEnN8n(query = '') { try { const sessionId = localStorage.getItem(CONFIG.SESSION_STORAGE_KEY) || ''; const response = await fetch(CONFIG.CHAT_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify({ action: 'searchVuelos', query: query, sessionId: sessionId, timestamp: new Date().toISOString() }) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data = await response.json(); return data.vuelos || (Array.isArray(data) ? data : []); } catch (error) { console.error('Error buscando vuelos en n8n:', error); return []; } } }; // ==================== 2. GESTOR DE HISTORIAL DE CHAT ==================== class ChatHistory { constructor(storageKey, maxMessages, maxIdleMinutes) { this.storageKey = storageKey; this.maxMessages = maxMessages; this.maxIdleMinutes = maxIdleMinutes; this.lastActivityKey = storageKey + '_lastActivity'; } getMessages() { try { const stored = localStorage.getItem(this.storageKey); return stored ? JSON.parse(stored) : []; } catch { return []; } } addMessage(message) { const messages = this.getMessages(); messages.push(message); if (messages.length > this.maxMessages) { messages.splice(0, messages.length - this.maxMessages); } try { localStorage.setItem(this.storageKey, JSON.stringify(messages)); localStorage.setItem(this.lastActivityKey, Date.now().toString()); } catch {} } clear() { localStorage.removeItem(this.storageKey); localStorage.removeItem(this.lastActivityKey); } getLastActivityTime() { const timestamp = localStorage.getItem(this.lastActivityKey); return timestamp ? parseInt(timestamp, 10) : 0; } isExpired() { const now = Date.now(); const last = this.getLastActivityTime(); if (!last) return false; // sin actividad previa no expira const diffMinutes = (now - last) / (1000 * 60); return diffMinutes > this.maxIdleMinutes; } getContextForAPI() { return this.getMessages().slice(-10); } } // ==================== 3. CHATBOT ==================== class ChatbotManager { constructor(options) { this.isOpen = false; this.isLoading = false; this.sessionId = options.sessionId; this.apiUrl = options.apiUrl; this.dom = options.domElements; this.utils = options.utils; this.history = options.history; this.beepAudio = null; this.retryCount = 0; // NUEVO: Referencia al botón flotante de cierre this.closeFloatBtn = null; this.init(); } init() { if (!this.dom.toggle) return; this.dom.toggle.addEventListener('click', (e) => { e.stopPropagation(); this.playBeep(); this.toggle(); }); this.dom.close?.addEventListener('click', () => this.close()); this.dom.sendBtn?.addEventListener('click', () => this.sendMessage()); this.dom.input?.addEventListener('keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.sendMessage(); } }); // Cerrar al hacer clic fuera document.addEventListener('click', (e) => { if (this.isOpen && this.dom.container && this.dom.toggle && !this.dom.container.contains(e.target) && !this.dom.toggle.contains(e.target)) { this.close(); } }); // Manejar clics en enlaces dentro de mensajes this.dom.messages?.addEventListener('click', (e) => { const target = e.target.closest('a'); if (target && target.href && !target.href.startsWith('javascript')) { e.preventDefault(); window.open(target.href, '_blank', 'noopener,noreferrer'); } }); // NUEVO: Crear botón flotante de cierre para móviles this.createCloseFloatButton(); // NUEVO: Gesto swipe para cerrar en móviles this.enableSwipeToClose(); // Restaurar historial visual (con verificación de expiración) this.restoreHistory(); } // NUEVO: Método para crear el botón flotante de cierre createCloseFloatButton() { const btn = document.createElement('button'); btn.className = 'chatbot-close-float'; btn.innerHTML = '✕'; btn.setAttribute('aria-label', 'Cerrar chat'); btn.addEventListener('click', () => this.close()); document.body.appendChild(btn); this.closeFloatBtn = btn; } // NUEVO: Habilitar swipe hacia abajo para cerrar en móviles enableSwipeToClose() { let touchStartY = 0; const container = this.dom.container; if (!container) return; container.addEventListener('touchstart', (e) => { touchStartY = e.touches[0].clientY; }, { passive: true }); container.addEventListener('touchmove', (e) => { const touchY = e.touches[0].clientY; const deltaY = touchY - touchStartY; // Solo permitir swipe hacia abajo en la parte superior del chat if (deltaY > 80 && container.scrollTop <= 5 && this.isOpen) { this.close(); } }, { passive: true }); } restoreHistory() { // Verificar si la sesión ha expirado por inactividad if (this.history.isExpired()) { console.log('Sesión expirada, limpiando historial y renovando sesión'); this.history.clear(); // Regenerar sessionId para que el backend reconozca nueva conversación this.sessionId = this.utils.generateUUID(); localStorage.setItem(CONFIG.SESSION_STORAGE_KEY, this.sessionId); return; // No restaurar nada, empezar limpio } const messages = this.history.getMessages(); messages.forEach(msg => { this.addMessageToDOM(msg.text, msg.type, false); }); } playBeep() { if (!this.beepAudio) { this.beepAudio = document.getElementById('chatbotBeep') || new Audio(); if (!this.beepAudio.src || this.beepAudio.src === window.location.href) { this.beepAudio.src = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQAAAAA="; } } this.beepAudio.play().catch(() => {}); } toggle() { this.isOpen ? this.close() : this.open(); } open() { if (!this.dom.container) return; // Al abrir, verificar también expiración (por si acaso) if (this.history.isExpired()) { this.history.clear(); this.sessionId = this.utils.generateUUID(); localStorage.setItem(CONFIG.SESSION_STORAGE_KEY, this.sessionId); // Limpiar mensajes visuales si los hubiera if (this.dom.messages) this.dom.messages.innerHTML = ''; } this.dom.container.classList.add('active'); this.isOpen = true; // Mostrar botón flotante de cierre si es móvil if (window.innerWidth <= 480 && this.closeFloatBtn) { this.closeFloatBtn.style.display = 'flex'; } this.dom.messages.scrollTop = this.dom.messages.scrollHeight; setTimeout(() => this.dom.input?.focus(), 300); } close() { if (!this.dom.container) return; this.dom.container.classList.remove('active'); this.isOpen = false; // Ocultar botón flotante de cierre if (this.closeFloatBtn) { this.closeFloatBtn.style.display = 'none'; } } addMessageToDOM(text, type, saveToHistory = true) { const msgDiv = document.createElement('div'); msgDiv.className = `chatbot-message ${type}`; msgDiv.innerHTML = this.utils.formatMessage(text); this.dom.messages?.appendChild(msgDiv); this.dom.messages.scrollTop = this.dom.messages.scrollHeight; if (saveToHistory) { this.history.addMessage({ text, type, timestamp: new Date().toISOString() }); } } addMessage(text, type) { let finalText = text; try { const parsed = JSON.parse(text); if (parsed.message) finalText = parsed.message; } catch {} this.addMessageToDOM(finalText, type); } showLoading() { if (this.isLoading) return; this.isLoading = true; const loadingDiv = document.createElement('div'); loadingDiv.className = 'chatbot-loading'; loadingDiv.id = 'chatbotLoading'; loadingDiv.innerHTML = ''; this.dom.messages?.appendChild(loadingDiv); this.dom.messages.scrollTop = this.dom.messages.scrollHeight; if (this.dom.status) this.dom.status.textContent = '✍️ Escribiendo...'; if (this.dom.sendBtn) this.dom.sendBtn.disabled = true; if (this.dom.input) this.dom.input.disabled = true; } hideLoading() { const loading = document.getElementById('chatbotLoading'); if (loading) loading.remove(); this.isLoading = false; if (this.dom.status) this.dom.status.textContent = '🟢 Conectado'; if (this.dom.sendBtn) this.dom.sendBtn.disabled = false; if (this.dom.input) this.dom.input.disabled = false; if (this.isOpen) { setTimeout(() => this.dom.input?.focus(), 100); } } async sendMessage() { const input = this.dom.input; if (!input) return; const userText = input.value.trim(); if (!userText || this.isLoading) return; this.addMessage(userText, 'user'); input.value = ''; this.showLoading(); this.retryCount = 0; await this.makeRequest(userText); } async makeRequest(userText) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CONFIG.TIMEOUT_MS); // Construir payload compatible con n8n Chat Trigger const payload = { chatInput: userText, sessionId: this.sessionId, timestamp: new Date().toISOString(), // Incluir contexto de historial para el AI Agent conversationHistory: this.history.getContextForAPI(), userData: { userAgent: navigator.userAgent, language: navigator.language, platform: navigator.platform } }; console.log('Enviando a n8n:', CONFIG.CHAT_API_URL, payload); const response = await fetch(CONFIG.CHAT_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); console.log('Respuesta de n8n:', data); this.hideLoading(); this.retryCount = 0; // Extraer la respuesta del bot del formato de n8n let botReply = this.extractBotReply(data); this.addMessage(botReply, 'bot'); } catch (error) { console.error('Chatbot error:', error); this.handleError(error, userText); } } extractBotReply(data) { // El Chat Trigger de n8n devuelve la respuesta en diferentes formatos if (data.output) return data.output; if (data.message) return data.message; if (data.response) return data.response; if (data.text) return data.text; if (typeof data === 'string') return data; // Buscar en propiedades anidadas const findMessage = (obj) => { if (!obj || typeof obj !== 'object') return null; if (obj.output) return obj.output; if (obj.message) return obj.message; if (obj.text) return obj.text; for (const key of Object.keys(obj)) { if (typeof obj[key] === 'object') { const found = findMessage(obj[key]); if (found) return found; } } return null; }; const found = findMessage(data); if (found) return found; // Si no se encuentra, devolver todo como string return JSON.stringify(data); } handleError(error, userText) { this.hideLoading(); if (this.retryCount < CONFIG.MAX_RETRIES) { this.retryCount++; console.log(`Reintentando (${this.retryCount}/${CONFIG.MAX_RETRIES})...`); this.showLoading(); setTimeout(() => this.makeRequest(userText), 1000 * this.retryCount); return; } this.addMessage( '⚠️ Estoy experimentando una pequeña intermitencia momentánea. ' + 'El equipo de soporte ya está al tanto y me encargaré personalmente de dar seguimiento a tu solicitud. ' + '¿Podrías intentarlo de nuevo en unos momentos? 🙏', 'error' ); } sendPredefined(message) { if (!this.dom.input) return; if (!this.isOpen) this.open(); this.dom.input.value = message; // Pequeño delay para asegurar que el chat esté abierto setTimeout(() => this.sendMessage(), 400); } } // ==================== 4. DOM Y UI ==================== const dom = { elements: {}, cacheSelectors() { this.elements = { preloader: document.getElementById('preloader'), progressBar: document.getElementById('preloaderProgressBar'), themeToggles: document.querySelectorAll('.theme-toggle'), hamburger: document.getElementById('hamburgerMenu'), mobileMenu: document.getElementById('mobileMenu'), closeMenu: document.getElementById('closeMenu'), menuOverlay: document.getElementById('menuOverlay'), mobileNavLinks: document.querySelectorAll('.mobile-nav-link'), searchBtn: document.getElementById('searchBtn'), searchModal: document.getElementById('searchModal'), closeSearch: document.getElementById('closeSearch'), searchInput: document.getElementById('searchInput'), searchResults: document.getElementById('searchResults'), searchSuggestions: document.querySelectorAll('.search-suggestion'), notificationBtn: document.getElementById('notificationBtn'), reservarBtn: document.getElementById('reservarBtn'), heroVideo: document.getElementById('heroVideo'), chatbotToggle: document.getElementById('chatbotToggle'), chatbotContainer: document.getElementById('chatbotContainer'), chatbotClose: document.getElementById('chatbotClose'), chatbotMessages: document.getElementById('chatbotMessages'), chatbotInput: document.getElementById('chatbotInput'), chatbotSendBtn: document.getElementById('chatbotSendBtn'), chatbotStatus: document.getElementById('chatbotStatus') }; } }; const ui = { init(chatbotInstance) { this.chatbot = chatbotInstance; this.setupPreloader(); this.setupTheme(); this.setupMobileMenu(); this.setupSearchModal(); this.setupAnimations(); this.setupVideo(); this.setupBookingButtons(); this.setupResponsive(); }, setupPreloader() { const preloader = dom.elements.preloader; const progressBar = dom.elements.progressBar; if (!preloader) return; let progress = 0; const interval = setInterval(() => { progress += Math.random() * 15; if (progress >= 100) { progress = 100; clearInterval(interval); this.finishPreloader(preloader); } if (progressBar) progressBar.style.width = `${Math.min(progress, 100)}%`; }, 100); window.addEventListener('load', () => { clearInterval(interval); this.finishPreloader(preloader); }); }, finishPreloader(preloader) { if (!preloader) return; preloader.classList.add('loaded'); setTimeout(() => { if (preloader) preloader.style.display = 'none'; }, 600); }, setupTheme() { const isLightMode = localStorage.getItem('theme') === 'light'; if (isLightMode) document.body.classList.add('light-mode'); dom.elements.themeToggles?.forEach(toggle => { toggle.addEventListener('click', () => { document.body.classList.toggle('light-mode'); const nowLight = document.body.classList.contains('light-mode'); localStorage.setItem('theme', nowLight ? 'light' : 'dark'); }); }); }, setupMobileMenu() { const { hamburger, mobileMenu, closeMenu, menuOverlay, mobileNavLinks } = dom.elements; let menuOpen = false; const toggle = () => { menuOpen = !menuOpen; hamburger?.classList.toggle('active'); mobileMenu?.classList.toggle('active'); menuOverlay?.classList.toggle('active'); document.body.style.overflow = menuOpen ? 'hidden' : 'auto'; }; hamburger?.addEventListener('click', toggle); closeMenu?.addEventListener('click', toggle); menuOverlay?.addEventListener('click', toggle); mobileNavLinks?.forEach(link => link.addEventListener('click', toggle)); }, setupSearchModal() { const { searchBtn, searchModal, closeSearch, searchInput, searchResults, searchSuggestions } = dom.elements; if (!searchBtn || !searchModal) return; let searchOpen = false; const open = () => { searchOpen = true; searchModal.classList.add('active'); document.body.style.overflow = 'hidden'; setTimeout(() => searchInput?.focus(), 300); }; const close = () => { searchOpen = false; searchModal.classList.remove('active'); document.body.style.overflow = 'auto'; if (searchInput) searchInput.value = ''; if (searchResults) { searchResults.innerHTML = ''; searchResults.classList.remove('active'); } }; searchBtn.addEventListener('click', open); closeSearch?.addEventListener('click', close); document.addEventListener('keydown', (e) => e.key === 'Escape' && searchOpen && close()); const performSearch = utils.debounce(async () => { const query = searchInput?.value.toLowerCase().trim() || ''; if (query.length === 0) { if (searchResults) { searchResults.innerHTML = ''; searchResults.classList.remove('active'); } return; } if (searchResults) { searchResults.innerHTML = '
Buscando vuelos...
'; searchResults.classList.add('active'); } const vuelos = await utils.searchVuelosEnN8n(query); this.displaySearchResults(vuelos, query); }, 400); searchInput?.addEventListener('input', performSearch); searchSuggestions?.forEach(sugg => { sugg.addEventListener('click', () => { const text = sugg.getAttribute('data-search'); if (searchInput && text) { searchInput.value = text; performSearch(); searchInput.focus(); } }); }); }, displaySearchResults(results, query) { const container = dom.elements.searchResults; if (!container) return; if (!results || results.length === 0) { container.innerHTML = `

No se encontraron vuelos para "${utils.escapeHtml(query)}"

Prueba con: amanecer, familiar, premium, teotihuacán

`; } else { container.innerHTML = results.map(vuelo => { let imgUrl = vuelo.imagen || vuelo.image || vuelo.foto || ''; if (imgUrl && !imgUrl.startsWith('http') && !imgUrl.startsWith('//') && !imgUrl.startsWith('data:')) { imgUrl = window.location.origin + '/' + imgUrl.replace(/^\.?\//, ''); } return `
${utils.escapeHtml(vuelo.nombre || 'Vuelo')}

${utils.escapeHtml(vuelo.nombre || 'Vuelo en Globo')}

${utils.escapeHtml(vuelo.duracion || 'Consulta disponibilidad')} ${utils.escapeHtml(vuelo.ubicacion || 'Teotihuacán')}
${vuelo.precioOriginal ? `$${vuelo.precioOriginal.toLocaleString()}` : ''} $${(vuelo.precio || 0).toLocaleString()} ${vuelo.unidad ? `/ ${utils.escapeHtml(vuelo.unidad)}` : ''}
`; }).join(''); } container.classList.add('active'); }, setupAnimations() { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.style.animation = 'fadeInUp 0.8s ease-out forwards'; observer.unobserve(entry.target); } }); }, { threshold: 0.1, rootMargin: '0px 0px -100px 0px' }); document.querySelectorAll('.feature-card, .tour-one__single').forEach(el => observer.observe(el)); }, setupVideo() { const video = dom.elements.heroVideo; if (video) { video.play().catch(() => { video.style.display = 'none'; const container = document.querySelector('.video-container'); if (container) { container.style.backgroundImage = 'url("assets/img/banner/globopuertoteotihuacan-video.jpg")'; container.style.backgroundSize = 'cover'; container.style.backgroundPosition = 'center'; } }); } }, setupResponsive() { const handler = utils.throttle(() => { const w = window.innerWidth; document.body.classList.remove('mobile-small', 'mobile-large', 'tablet', 'desktop'); if (w <= 480) document.body.classList.add('mobile-small'); else if (w <= 768) document.body.classList.add('mobile-large'); else if (w <= 1024) document.body.classList.add('tablet'); else document.body.classList.add('desktop'); }, 200); window.addEventListener('resize', handler); handler(); }, setupBookingButtons() { const { notificationBtn, reservarBtn } = dom.elements; notificationBtn?.addEventListener('click', () => { alert("Notificaciones:\n\n• Tu reserva para el 15 de mayo está confirmada\n• Nuevo destino disponible: Valle de la Luna\n• Oferta especial: 20% de descuento en vuelos nocturnos"); }); reservarBtn?.addEventListener('click', (e) => { e.preventDefault(); this.chatbot.sendPredefined("Quiero reservar un vuelo en globo"); }); } }; // Función global para manejar clics en resultados de búsqueda window.handleSearchResultClick = function(id, enlace) { const modal = dom.elements.searchModal; if (modal) modal.classList.remove('active'); document.body.style.overflow = 'auto'; if (enlace) { window.open(enlace, '_blank', 'noopener,noreferrer'); } }; // ==================== 5. INICIALIZACIÓN ==================== document.addEventListener('DOMContentLoaded', () => { console.log('🚀 Inicializando Chatbot Vuelos en Globo...'); injectStyles(); dom.cacheSelectors(); // Obtener o crear sessionId persistente let sessionId = localStorage.getItem(CONFIG.SESSION_STORAGE_KEY); if (!sessionId) { sessionId = utils.generateUUID(); localStorage.setItem(CONFIG.SESSION_STORAGE_KEY, sessionId); } console.log('Session ID:', sessionId); // Inicializar historial con tiempo de expiración const history = new ChatHistory( CONFIG.CHAT_HISTORY_KEY, CONFIG.MAX_HISTORY_MESSAGES, CONFIG.SESSION_MAX_IDLE_MINUTES ); // Inicializar chatbot const chatbot = new ChatbotManager({ sessionId: sessionId, apiUrl: CONFIG.CHAT_API_URL, utils: utils, history: history, domElements: { toggle: dom.elements.chatbotToggle, container: dom.elements.chatbotContainer, close: dom.elements.chatbotClose, messages: dom.elements.chatbotMessages, input: dom.elements.chatbotInput, sendBtn: dom.elements.chatbotSendBtn, status: dom.elements.chatbotStatus } }); // Inicializar UI ui.init(chatbot); console.log('✅ Chatbot Vuelos en Globo inicializado correctamente'); }); })();