vueloenglobo.mx/public/assets/js/main-min.js

1099 lines
45 KiB
JavaScript

// ==================== 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 '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
if (m === '"') return '&quot;';
if (m === "'") return '&#039;';
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 `<video controls style="max-width:100%; border-radius:16px; margin:10px 0; box-shadow:0 4px 12px rgba(0,0,0,0.1);">
<source src="${src}" type="video/${src.split('.').pop().toLowerCase()}">
Tu navegador no soporta video.
</video>`;
}
return `<img src="${src}" alt="${this.escapeHtml(alt)}" style="max-width:100%; border-radius:16px; margin:10px 0; box-shadow:0 4px 12px rgba(0,0,0,0.1);" onerror="this.style.display='none'">`;
});
// Convertir Markdown enlaces a HTML
formatted = formatted.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" style="color:#04cdff; text-decoration:underline;">$1</a>');
// Convertir negrita
formatted = formatted.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Convertir itálica
formatted = formatted.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Convertir saltos de línea
formatted = formatted.replace(/\n/g, '<br>');
// Resaltar precios
formatted = formatted.replace(/\$(\d+(?:,\d+)*(?:\.\d+)?)/g, '<span class="chatbot-price">$$$1</span>');
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 = '<span></span><span></span><span></span>';
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 = '<div class="text-center p-4"><i class="fas fa-spinner fa-spin"></i> Buscando vuelos...</div>';
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 = `<div class="text-center p-8">
<i class="fas fa-search fa-3x text-gray-400 mb-4"></i>
<p class="text-gray-600">No se encontraron vuelos para "${utils.escapeHtml(query)}"</p>
<p class="text-gray-400 text-sm mt-2">Prueba con: amanecer, familiar, premium, teotihuacán</p>
</div>`;
} 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 `<div class="search-result-item" style="display: flex; gap: 1rem; padding: 1rem; cursor: pointer; background: white; border-radius: 16px; margin-bottom: 0.75rem; border: 1px solid #e5e7eb;" onclick="window.handleSearchResultClick('${utils.escapeHtml(vuelo.id || '')}', '${utils.escapeHtml(vuelo.enlace || vuelo.enlace_permanente || '')}')">
<div style="flex:0 0 100px; height:100px; border-radius:12px; overflow:hidden; background:#f3f4f6;">
<img src="${imgUrl || 'https://via.placeholder.com/100x100?text=Vuelo'}" alt="${utils.escapeHtml(vuelo.nombre || 'Vuelo')}" style="width:100%; height:100%; object-fit:cover;" onerror="this.src='https://via.placeholder.com/100x100?text=Vuelo'">
</div>
<div style="flex:1">
<h4 style="font-size:1.1rem; font-weight:700; margin-bottom:0.5rem;">${utils.escapeHtml(vuelo.nombre || 'Vuelo en Globo')}</h4>
<div class="flex gap-3 text-sm text-gray-500 mb-2">
<span><i class="far fa-clock"></i> ${utils.escapeHtml(vuelo.duracion || 'Consulta disponibilidad')}</span>
<span><i class="far fa-map"></i> ${utils.escapeHtml(vuelo.ubicacion || 'Teotihuacán')}</span>
</div>
<div>
${vuelo.precioOriginal ? `<span class="line-through text-gray-400 mr-2">$${vuelo.precioOriginal.toLocaleString()}</span>` : ''}
<span class="bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-3 py-1 rounded-full font-bold">$${(vuelo.precio || 0).toLocaleString()}</span>
${vuelo.unidad ? `<span class="text-gray-400 ml-2">/ ${utils.escapeHtml(vuelo.unidad)}</span>` : ''}
</div>
</div>
</div>`;
}).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');
});
})();