1099 lines
45 KiB
JavaScript
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 '&';
|
|
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 `<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');
|
|
});
|
|
})(); |