vueloenglobo.mx/resources/views/template/chat.blade.php

716 lines
26 KiB
PHP

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SkyBalloons | Asistente de Reservas</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* Estilos mejorados con micro-interacciones */
:root {
--primary: #0ea5e9;
--primary-dark: #0284c7;
--bg-dark: #0f172a;
--bg-card: #1e293b;
--bot-msg: #334155;
--text-light: #e2e8f0;
--text-muted: #94a3b8;
--border-light: rgba(255,255,255,0.1);
--success: #10b981;
--error: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', sans-serif;
}
body {
height: 100vh;
background: linear-gradient(135deg, #0b1121, #1a2639);
display: flex;
align-items: center;
justify-content: center;
}
.chat-container {
width: 100%;
max-width: 500px;
height: 92vh;
background: rgba(30, 41, 59, 0.98);
backdrop-filter: blur(12px);
border-radius: 28px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.7);
border: 1px solid var(--border-light);
}
.chat-header {
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
padding: 20px 22px;
text-align: center;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.chat-header img {
height: 42px;
filter: brightness(1.1);
}
.chat-header h2 {
color: white;
font-size: 14px;
font-weight: 500;
margin-top: 6px;
opacity: 0.9;
letter-spacing: 0.3px;
}
.chat-messages {
flex: 1;
padding: 24px 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
scroll-behavior: smooth;
}
.message {
max-width: 85%;
padding: 14px 18px;
border-radius: 22px;
font-size: 14.5px;
line-height: 1.5;
animation: slideIn 0.3s cubic-bezier(0.2, 0.9, 0.3, 1);
word-break: break-word;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.bot {
background: var(--bot-msg);
color: var(--text-light);
align-self: flex-start;
border-bottom-left-radius: 6px;
border: 1px solid rgba(255,255,255,0.05);
}
.user {
background: var(--primary);
color: white;
align-self: flex-end;
border-bottom-right-radius: 6px;
}
.system-message {
background: rgba(16, 185, 129, 0.2);
color: var(--success);
align-self: center;
font-size: 12px;
padding: 8px 16px;
border-radius: 30px;
border: 1px solid rgba(16, 185, 129, 0.3);
max-width: 90%;
}
.error-message {
background: rgba(239, 68, 68, 0.2);
color: var(--error);
align-self: center;
font-size: 12px;
padding: 8px 16px;
border-radius: 30px;
border: 1px solid rgba(239, 68, 68, 0.3);
max-width: 90%;
}
.chat-input {
padding: 18px 16px;
background: var(--bg-card);
display: flex;
gap: 12px;
border-top: 1px solid var(--border-light);
}
.chat-input input {
flex: 1;
padding: 14px 18px;
border: none;
border-radius: 30px;
background: #0f172a;
color: white;
outline: none;
font-size: 14.5px;
transition: all 0.2s;
border: 1px solid transparent;
}
.chat-input input:focus {
border-color: var(--primary);
background: #1a2538;
}
.chat-input input::placeholder {
color: #64748b;
}
.chat-input input.error {
border-color: var(--error);
animation: shake 0.3s ease-in-out;
}
.chat-input button {
padding: 14px 24px;
border: none;
border-radius: 30px;
background: var(--primary);
color: white;
cursor: pointer;
transition: 0.2s;
font-weight: 600;
font-size: 14px;
letter-spacing: 0.3px;
box-shadow: 0 4px 10px rgba(14, 165, 233, 0.3);
}
.chat-input button:hover:not(:disabled) {
background: var(--primary-dark);
transform: scale(1.02);
}
.chat-input button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status {
text-align: center;
font-size: 12px;
padding: 8px;
color: var(--text-muted);
background: #0f172a;
border-top: 1px solid var(--border-light);
font-weight: 500;
}
.loader {
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,0.3);
border-top: 3px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 5px 0;
}
.date-picker-container {
position: fixed;
bottom: 90px;
left: 50%;
transform: translateX(-50%);
width: 90%;
max-width: 450px;
background: var(--bg-card);
border-radius: 20px;
padding: 20px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
border: 1px solid var(--border-light);
z-index: 1000;
display: none;
animation: slideUp 0.3s ease;
}
.date-picker-container.show {
display: block;
}
.date-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
color: var(--text-light);
}
.date-picker-header h3 {
font-size: 16px;
font-weight: 500;
}
.date-picker-header button {
background: none;
border: none;
color: var(--text-muted);
font-size: 20px;
cursor: pointer;
padding: 5px 10px;
}
.date-picker-header button:hover {
color: var(--text-light);
}
.date-inputs {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 20px;
}
.date-inputs select {
background: #0f172a;
color: white;
border: 1px solid var(--border-light);
border-radius: 12px;
padding: 12px;
font-size: 16px;
outline: none;
cursor: pointer;
flex: 1;
}
.date-inputs select:focus {
border-color: var(--primary);
}
.date-inputs select.error {
border-color: var(--error);
}
.confirm-date-btn {
width: 100%;
padding: 14px;
background: var(--primary);
color: white;
border: none;
border-radius: 30px;
font-weight: 600;
cursor: pointer;
transition: 0.2s;
}
.confirm-date-btn:hover {
background: var(--primary-dark);
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideUp {
from { opacity: 0; transform: translate(-50%, 20px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
@media (max-width: 600px) {
.chat-container {
height: 100vh;
border-radius: 0;
}
.date-picker-container {
width: 95%;
bottom: 80px;
}
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<img src="https://www.skyballoons.mx/assets/images/logo-original-blanco.png" alt="SkyBalloons">
<h2>Asistente Oficial de Reservas</h2>
</div>
<div class="chat-messages" id="chatMessages"></div>
<div class="status" id="statusBar">🟢 Conectado</div>
<div class="chat-input">
<input type="text" id="messageInput" placeholder="Escribe tu mensaje..." autocomplete="off">
<button id="sendButton">Enviar</button>
</div>
</div>
<!-- Selector de fecha mejorado -->
<div class="date-picker-container" id="datePickerContainer">
<div class="date-picker-header">
<h3>📅 Selecciona tu fecha de nacimiento</h3>
<button id="closeDatePicker">✕</button>
</div>
<div class="date-inputs">
<select id="daySelect" aria-label="Día">
<option value="">Día</option>
</select>
<select id="monthSelect" aria-label="Mes">
<option value="">Mes</option>
<option value="1">Enero</option>
<option value="2">Febrero</option>
<option value="3">Marzo</option>
<option value="4">Abril</option>
<option value="5">Mayo</option>
<option value="6">Junio</option>
<option value="7">Julio</option>
<option value="8">Agosto</option>
<option value="9">Septiembre</option>
<option value="10">Octubre</option>
<option value="11">Noviembre</option>
<option value="12">Diciembre</option>
</select>
<select id="yearSelect" aria-label="Año">
<option value="">Año</option>
</select>
</div>
<button class="confirm-date-btn" id="confirmDateBtn">Confirmar fecha</button>
</div>
<script>
(function() {
// Configuración
const API_URL = "https://n8n.skyballoons.cloud/webhook/sky-recopilador-72/chat";
// Generar o recuperar sessionId
let sessionId = localStorage.getItem("skySession");
if (!sessionId) {
sessionId = crypto.randomUUID ? crypto.randomUUID() : '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);
});
localStorage.setItem("skySession", sessionId);
}
// Elementos DOM
const chatMessages = document.getElementById("chatMessages");
const messageInput = document.getElementById("messageInput");
const sendButton = document.getElementById("sendButton");
const statusBar = document.getElementById("statusBar");
const datePickerContainer = document.getElementById("datePickerContainer");
const closeDatePicker = document.getElementById("closeDatePicker");
const daySelect = document.getElementById("daySelect");
const monthSelect = document.getElementById("monthSelect");
const yearSelect = document.getElementById("yearSelect");
const confirmDateBtn = document.getElementById("confirmDateBtn");
// Estado
let isLoading = false;
let awaitingDate = false;
let dateValidationCallback = null;
let currentContext = null; // Para saber qué campo estamos solicitando
// Validaciones
const validators = {
nombre: (text) => {
const nameRegex = /^[a-zA-ZáéíóúÁÉÍÓÚñÑ\s]{3,50}$/;
return nameRegex.test(text.trim()) ? null : "Por favor, ingresa un nombre válido (solo letras, mínimo 3 caracteres)";
},
email: (text) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(text.trim()) ? null : "Por favor, ingresa un correo electrónico válido";
},
telefono: (text) => {
const phoneRegex = /^[0-9]{10}$/;
return phoneRegex.test(text.replace(/\D/g, '')) ? null : "Por favor, ingresa un teléfono válido de 10 dígitos";
},
fecha: (text) => {
const dateRegex = /^\d{2}\/\d{2}\/\d{4}$/;
if (!dateRegex.test(text)) return "Formato inválido. Usa DD/MM/AAAA";
const [day, month, year] = text.split('/').map(Number);
const date = new Date(year, month - 1, day);
const isValid = date.getDate() === day && date.getMonth() === month - 1;
if (!isValid) return "Fecha inválida";
const today = new Date();
const minDate = new Date(today.getFullYear() - 100, today.getMonth(), today.getDate());
const maxDate = new Date(today.getFullYear() - 18, today.getMonth(), today.getDate());
if (date > maxDate) return "Debes ser mayor de 18 años";
if (date < minDate) return "Fecha de nacimiento no válida";
return null;
}
};
// Detectar contexto del último mensaje del bot
function detectContext(botMessage) {
if (!botMessage) return null;
const lowerMsg = botMessage.toLowerCase();
if (lowerMsg.includes("nombre") || lowerMsg.includes("nombre completo")) return "nombre";
if (lowerMsg.includes("correo") || lowerMsg.includes("email")) return "email";
if (lowerMsg.includes("teléfono") || lowerMsg.includes("telefono") || lowerMsg.includes("celular")) return "telefono";
if (lowerMsg.includes("fecha de nacimiento") || lowerMsg.includes("fecha nacimiento")) return "fecha";
return null;
}
// Inicializar selects de fecha
function initializeDatePicker() {
// Limpiar selects
daySelect.innerHTML = '<option value="">Día</option>';
yearSelect.innerHTML = '<option value="">Año</option>';
// Llenar días (1-31)
for (let i = 1; i <= 31; i++) {
const option = document.createElement('option');
option.value = i;
option.textContent = i.toString().padStart(2, '0');
daySelect.appendChild(option);
}
// Llenar años (últimos 100 años)
const currentYear = new Date().getFullYear();
for (let i = currentYear - 18; i >= currentYear - 100; i--) {
const option = document.createElement('option');
option.value = i;
option.textContent = i;
yearSelect.appendChild(option);
}
}
// Mostrar selector de fecha
function showDatePicker(callback) {
dateValidationCallback = callback;
awaitingDate = true;
datePickerContainer.classList.add('show');
messageInput.disabled = true;
sendButton.disabled = true;
// Resetear selects
daySelect.value = '';
monthSelect.value = '';
yearSelect.value = '';
[daySelect, monthSelect, yearSelect].forEach(select => {
select.classList.remove('error');
});
}
// Ocultar selector de fecha
function hideDatePicker() {
datePickerContainer.classList.remove('show');
awaitingDate = false;
dateValidationCallback = null;
messageInput.disabled = false;
sendButton.disabled = false;
messageInput.focus();
}
// Validar fecha seleccionada
function validateSelectedDate() {
const day = daySelect.value;
const month = monthSelect.value;
const year = yearSelect.value;
if (!day || !month || !year) {
[daySelect, monthSelect, yearSelect].forEach(select => {
if (!select.value) select.classList.add('error');
});
addSystemMessage("Por favor, selecciona día, mes y año", "error");
return false;
}
// Validar días por mes
const daysInMonth = new Date(year, month, 0).getDate();
if (day > daysInMonth) {
daySelect.classList.add('error');
addSystemMessage(`El mes seleccionado solo tiene ${daysInMonth} días`, "error");
return false;
}
// Validar edad mínima (18 años)
const selectedDate = new Date(year, month - 1, day);
const today = new Date();
const minAgeDate = new Date(today.getFullYear() - 18, today.getMonth(), today.getDate());
if (selectedDate > minAgeDate) {
addSystemMessage("Debes ser mayor de 18 años para continuar", "error");
return false;
}
return true;
}
// Funciones UI
function addMessage(text, type) {
const msgDiv = document.createElement("div");
msgDiv.className = `message ${type}`;
const formattedText = text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>');
msgDiv.innerHTML = formattedText;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function addSystemMessage(text, type = "system") {
const msgDiv = document.createElement("div");
msgDiv.className = type === "error" ? "error-message" : "system-message";
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function showLoadingIndicator() {
if (isLoading) return;
isLoading = true;
const loaderDiv = document.createElement("div");
loaderDiv.className = "message bot";
loaderDiv.id = "loadingIndicator";
loaderDiv.innerHTML = '<div class="loader"></div>';
chatMessages.appendChild(loaderDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
statusBar.innerText = "✍️ Escribiendo...";
sendButton.disabled = true;
messageInput.disabled = true;
}
function removeLoadingIndicator() {
const loader = document.getElementById("loadingIndicator");
if (loader) loader.remove();
isLoading = false;
if (!awaitingDate) {
statusBar.innerText = "🟢 Conectado";
sendButton.disabled = false;
messageInput.disabled = false;
messageInput.focus();
}
}
// Procesar mensaje con API
async function processMessageWithAPI(text) {
showLoadingIndicator();
try {
const response = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chatInput: text,
sessionId: sessionId
})
});
if (!response.ok) {
throw new Error(`Error ${response.status}: ${response.statusText}`);
}
const data = await response.json();
removeLoadingIndicator();
const botReply = data.output || "Lo siento, no pude procesar tu solicitud.";
addMessage(botReply, "bot");
// Actualizar contexto después de recibir respuesta del bot
currentContext = detectContext(botReply);
// Si el bot está pidiendo fecha, mostrar selector automáticamente
if (currentContext === "fecha") {
showDatePicker((formattedDate) => {
addMessage(formattedDate, "user");
processMessageWithAPI(formattedDate);
});
}
} catch (error) {
removeLoadingIndicator();
addMessage("⚠️ Error de conexión. Por favor, intenta de nuevo.", "bot");
statusBar.innerText = "🔴 Error de conexión";
console.error("Error:", error);
}
}
// Enviar mensaje principal
async function sendMessage() {
const userText = messageInput.value.trim();
if (!userText || isLoading || awaitingDate) return;
// Obtener último mensaje del bot para validar según contexto
const lastBotMessages = Array.from(chatMessages.children)
.filter(msg => msg.classList.contains('bot'));
const lastBotMessage = lastBotMessages.length > 0
? lastBotMessages[lastBotMessages.length - 1].textContent
: '';
// Si no tenemos contexto actual, detectar del último mensaje
if (!currentContext) {
currentContext = detectContext(lastBotMessage);
}
let validationError = null;
if (currentContext === "nombre") {
validationError = validators.nombre(userText);
} else if (currentContext === "email") {
validationError = validators.email(userText);
} else if (currentContext === "telefono") {
validationError = validators.telefono(userText);
} else if (currentContext === "fecha") {
// Para fecha, mostrar el selector en lugar de validar texto
showDatePicker((formattedDate) => {
addMessage(formattedDate, "user");
processMessageWithAPI(formattedDate);
});
messageInput.value = "";
return;
}
if (validationError) {
addSystemMessage(validationError, "error");
messageInput.classList.add('error');
setTimeout(() => messageInput.classList.remove('error'), 300);
return;
}
// Mostrar mensaje del usuario
addMessage(userText, "user");
messageInput.value = "";
// Limpiar contexto después de enviar (se actualizará con la respuesta)
currentContext = null;
await processMessageWithAPI(userText);
}
// Event Listeners
messageInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") sendMessage();
});
sendButton.addEventListener("click", sendMessage);
closeDatePicker.addEventListener("click", hideDatePicker);
confirmDateBtn.addEventListener("click", () => {
if (validateSelectedDate() && dateValidationCallback) {
const formattedDate = `${daySelect.value.padStart(2, '0')}/${monthSelect.value.padStart(2, '0')}/${yearSelect.value}`;
dateValidationCallback(formattedDate);
hideDatePicker();
}
});
// Remover error cuando se selecciona un valor
[daySelect, monthSelect, yearSelect].forEach(select => {
select.addEventListener('change', () => {
select.classList.remove('error');
});
});
// Inicializar
initializeDatePicker();
addMessage("👋 ¡Hola! Soy el asistente de SkyBalloons. Para comenzar con su reserva, ¿podría indicarme su **nombre completo** (nombre y apellidos)?", "bot");
currentContext = "nombre";
messageInput.focus();
})();
</script>
</body>
</html>