Forráskód Böngészése

basic chat implementation

Erwin Jacimino 8 hónapja
szülő
commit
f54bc282ae
7 módosított fájl, 610 hozzáadás és 288 törlés
  1. 2 3
      app.py
  2. 96 5
      check_printer.sh
  3. 8 26
      main.py
  4. 26 48
      public/main/index.html
  5. 381 190
      public/main/js/app.js
  6. 10 0
      public/main/styles.css
  7. 87 16
      routes/chat.py

+ 2 - 3
app.py

@@ -90,8 +90,7 @@ def setup_routes(app: FastAPI):
         app.include_router(
             chat.chat_router, 
             prefix="/api/chat",
-            tags=["Chat"], 
-            dependencies=[Depends(get_current_user)]
+            tags=["Chat"]
         )
 
         # User routes
@@ -153,4 +152,4 @@ def setup_routes(app: FastAPI):
         error_msg = f"Error setting up application routes: - {e}"
         logger.error(error_msg)
     
-        raise
+        raise e

+ 96 - 5
check_printer.sh

@@ -1,10 +1,101 @@
 #!/bin/bash
 
-PORTS=(6010 6012 6014 6016)
+echo
+
 AUTH="Authorization: Bearer PRINTER123cerveza@"
+RED=$'\e[31m'
+GREEN=$'\e[32m'
+RESET=$'\e[0m'
+
+PRINTERS=(
+  "Pedidos Express Printer (Barra)|6010|6011"
+  "Pedidos Express Printer (Cocteleria)|6012|6013"
+  "Pedidos Express Printer (Cocina Pizza)|6014|6015"
+  "Pedidos Express Printer (Cocina Burger)|6016|6017"
+)
+
+# Encabezado optimizado
+echo "╔═══════════════════════════════════════════════╦══════════════╦══════════════╦══════════════╦══════════════╦══════════════╗"
+printf "║ %-45s ║ %-12s ║ %-12s ║ %-12s ║ %-12s ║ %-12s ║\n" \
+  "Nombre" "Printer" "SSH (Port)" "Conectado" "Pendientes" "Status"
+echo "╠═══════════════════════════════════════════════╬══════════════╬══════════════╬══════════════╬══════════════╬══════════════╣"
+
+LAST_INDEX=$((${#PRINTERS[@]} - 1))
+
+for i in "${!PRINTERS[@]}"; do
+    ENTRY="${PRINTERS[$i]}"
+    NAME=$(echo "$ENTRY" | cut -d'|' -f1)
+    PORT=$(echo "$ENTRY" | cut -d'|' -f2)
+    SSH_PORT=$(echo "$ENTRY" | cut -d'|' -f3)
 
-for PORT in "${PORTS[@]}"; do
-    echo "Checking printer on port $PORT..."
-    RESPONSE=$(curl -s "http://localhost:$PORT/status" -H "$AUTH")
-    echo "Port $PORT response: $RESPONSE"
+    RESPONSE=$(curl -fsS --max-time 2 "http://localhost:$PORT/status" -H "$AUTH" 2>/dev/null || true)
+
+    # Check SSH port connectivity
+    if nc -z -w2 localhost "$SSH_PORT" 2>/dev/null; then
+        SSH_STATUS="OK"
+        SSH_COLOR="${GREEN}"
+    else
+        SSH_STATUS="ERROR"
+        SSH_COLOR="${RED}"
+    fi
+
+    if [[ -n "$RESPONSE" ]]; then
+        PRINTER_STATUS="OK"
+        PRINTER_COLOR="${GREEN}"
+        if echo "$RESPONSE" | jq -e . >/dev/null 2>&1; then
+            CONNECTED_RAW=$(echo "$RESPONSE" | jq -r '.printer_connected // "-"')
+            if [[ "$CONNECTED_RAW" == "true" ]]; then
+                CONNECTED="YES"
+                CONNECTED_COLOR="${GREEN}"
+            elif [[ "$CONNECTED_RAW" == "false" ]]; then
+                CONNECTED="NO"
+                CONNECTED_COLOR="${RED}"
+            else
+                CONNECTED="-"
+                CONNECTED_COLOR=""
+            fi
+            PENDING=$(echo "$RESPONSE" | jq -r '.pending_orders // "-"')
+            STATUS_RAW=$(echo "$RESPONSE" | jq -r '.status // "-"')
+            # Procesar el status
+            if [[ "$STATUS_RAW" == "ok" ]]; then
+                STATUS="Operativo"
+                STATUS_COLOR="${GREEN}"
+            else
+                STATUS="No Operativo"
+                STATUS_COLOR="${RED}"
+            fi
+            RESPONSE_MSG="OK"
+            RESPONSE_COLOR="${GREEN}"
+        else
+            CONNECTED="-"
+            CONNECTED_COLOR=""
+            PENDING="-"
+            STATUS="No Operativo"
+            STATUS_COLOR="${RED}"
+            RESPONSE_MSG="${RESPONSE}"
+            RESPONSE_COLOR="${GREEN}"
+        fi
+    else
+        PRINTER_STATUS="ERROR"
+        PRINTER_COLOR="${RED}"
+        RESPONSE_MSG="-"
+        CONNECTED="-"
+        CONNECTED_COLOR=""
+        PENDING="-"
+        STATUS="No Operativo"
+        STATUS_COLOR="${RED}"
+    fi
+
+    # Imprimir la fila con colores aplicados solo al texto, manteniendo el ancho fijo
+    printf "║ %-45s ║ %b%-12s%b ║ %b%-12s%b ║ %b%-12s%b ║ %-12s ║ %b%-12s%b ║\n" \
+      "$NAME" "$PRINTER_COLOR" "$PRINTER_STATUS:$PORT" "$RESET" "$SSH_COLOR" "$SSH_STATUS:$SSH_PORT" "$RESET" \
+      "$CONNECTED_COLOR" "$CONNECTED" "$RESET" \
+      "$PENDING" "$STATUS_COLOR" "$STATUS" "$RESET"
+
+    # Solo imprimir barra si no es la última
+    if [[ $i -lt $LAST_INDEX ]]; then
+        echo "╠═══════════════════════════════════════════════╬══════════════╬══════════════╬══════════════╬══════════════╬══════════════╣"
+    fi
 done
+
+echo "╚═══════════════════════════════════════════════╩══════════════╩══════════════╩══════════════╩══════════════╩══════════════╝"

+ 8 - 26
main.py

@@ -1,4 +1,6 @@
-from config.settings import PORT, OPENAI_API_KEY, BG_DATA_PATH,DEVELOPMENT, validate_config
+#!/home/superti/miniconda3/envs/pedidos_express/bin/python
+
+from config.settings import PORT, BG_DATA_PATH,  validate_config
 import os
 import uvicorn
 from app import create_app, setup_routes
@@ -10,55 +12,35 @@ logger = getLogger("main")
 
 def main():
     """Main application entry point"""
-    logger.info("Starting Pedidos Express application")
-    
-    
     try:
         # Validate configuration
         if not validate_config():
             logger.critical("FATAL: Configuration validation failed.")
-            
-            if not OPENAI_API_KEY:
-                logger.error("Please create a .env file with OPENAI_API_KEY='your_key_here'")
-                with open(".env", "w") as f:
-                    f.write("OPENAI_API_KEY='your_key_here'")
+            logger.error("Please create a .env file with OPENAI_API_KEY='your_key_here'")
+            with open(".env", "w") as f:
+                f.write("OPENAI_API_KEY='your_key_here'")
             return None
-        
-        
-        
-        # Create and configure app
         logger.info("Creating FastAPI application")
         app = create_app()
-        
         logger.info("Setting up application routes")
         setup_routes(app)
-        
-        
         # Initialize database
         logger.info("Initializing database")
         initialize_db()
-        
-        
         # Display startup information
         logger.info(f"Server starting on http://localhost:{PORT}")
-        
         if not os.path.exists(BG_DATA_PATH):
             logger.warning(f"WARNING: {BG_DATA_PATH} not found. AI assistant will not have specific menu data.")
-        
         else:
             logger.info(f"AI assistant data loaded from: {os.path.abspath(BG_DATA_PATH)}")
-        
 
 
-        
         logger.info("Pedidos Express application ready to serve requests")
         return app
-        
     except Exception as e:
         error_msg = f"Critical error during application startup: - {e}"
         logger.critical(error_msg)
-        
-        return None
+        raise e
 
 app = main()
 
@@ -71,4 +53,4 @@ logger.info("Application initialized successfully")
 
 if __name__ == "__main__":
     logger.info(f"Starting server with uvicorn on port {PORT}")
-    uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info", access_log=False)
+    uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info", access_log=False)

+ 26 - 48
public/main/index.html

@@ -113,54 +113,32 @@
     </section>
 
     <!-- ===== CHAT ===== -->
-    <section id="chatTab" data-index="2" data-tab class="flex hidden flex-col flex-1 min-h-0">
+    <section id="chatTab" data-index="2" data-tab class="flex hidden flex-col h-full flex-1 min-h-0">
         <!-- Contenedor de mensajes que puede crecer y hacer scroll -->
-        <div id="chatMessages" class="flex-1 overflow-y-auto px-5 md:px-8 py-4 flex flex-col gap-4">
-            <!-- Sugerencias y Bajada -->
-            <div class="text-center text-gray-600 px-5 text-sm leading-relaxed max-w-full">
-                <h2 class="text-md text-gray-500 mt-5">Conversa con nuestro asistente IA: descubre nuestras cervezas 🍻 y sugiere mejoras para nuestra app ✨<span class="mx-0.5">🍻</span></h2>
-                <!-- Ideas de mensajes -->
-                <div id="chatSuggestions" class="grid grid-cols-1 gap-3 max-w-2xl mx-auto mt-5">
-                    <div class="bg-white border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-gray-300 hover:shadow-sm transition-all duration-200 text-left flex items-center gap-4">
-                        <div class="text-lg mb-1">🍺</div>
-                        <div class="chat-suggestion text-gray-400 font-medium text-xs">¿Que me puedes contar de la burlesque?</div>
-                    </div>
-                    
-                    <div class="bg-white border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-gray-300 hover:shadow-sm transition-all duration-200 text-left flex items-center gap-4">
-                        <div class="text-lg mb-1">⁉️</div>
-                        <div class="chat-suggestion text-gray-400 font-medium text-xs">Mi sugerencia para la aplicacion es...</div>
-                    </div>  
-                    <div class="bg-white border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-gray-300 hover:shadow-sm transition-all duration-200 text-left flex items-center gap-4">
-                        <div class="text-lg mb-1">🍕</div>
-                        <div class="chat-suggestion text-gray-400 font-medium text-xs">¿Qué pizza queda bien con la hoppy mosh?</div>
-                    </div>
-                    <div class="bg-white border border-gray-200 rounded-xl p-4 cursor-pointer hover:border-gray-300 hover:shadow-sm transition-all duration-200 text-left flex items-center gap-4">
-                        <div class="text-lg mb-1">📱</div>
-                        <div class="chat-suggestion text-gray-400 font-medium text-xs">La orden no se envio bien</div>
-                    </div>               
-                </div>
+        <div id="chatContainer" class="flex flex-col h-full flex-1 bg-white border border-gray-200 rounded-xl shadow-sm m-4 overflow-hidden">
+          <!-- Mensajes -->
+          <div id="chatMessages" class="flex-1 overflow-y-auto px-4 py-3 space-y-1 bg-gray-50 font-mono text-sm">
+            <!-- Mensajes de ejemplo estilo IRC -->
+            <div class="text-gray-500 text-xs">*** Bienvenido al chat de Biergarten Klein ***</div>
+            <template id="chatMessageTemplate">
+              <div><span class="text-gray-400 chat-message-time">[00:36]</span> <span class="font-bold chat-message-user">&lt;JuanP_mesa5&gt;</span> <span class="chat-message-text">Yo también quiero!</span></div>
+            </template>
+            <template id="systemMessageTemplate">
+              <div class="system-container"><span class="text-gray-400 chat-message-time">[14:24]</span> <span class="font-bold chat-message-text">*** Maria_mesa2 se ha unido al chat</span></div>
+            </template>
+          </div>
+          <!-- Input y botón -->
+          <form id="chatForm" class="flex flex-col relative gap-2 border-t border-gray-200 bg-white px-3 py-2" autocomplete="off">
+              <ul id="userList" class="hidden absolute bottom-full w-3/4 rounded-lg border bg-gray-100 flex flex-col gap-2 py-2 px-2">
+                <template id="listUserName">
+                  <li class="list-user-name">Maria_mesa2</li>
+                </template>
+              </ul>
+            <div class="flex gap-2 w-full">
+              <input id="chatInput" type="text" placeholder="Escribe un mensaje..." class="flex-grow flex-1 px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#101419] text-sm bg-gray-50" autocomplete="off" maxlength="300" />
+              <button type="submit" class="bg-[#101419] flex-shrink hover:bg-[#37404a] text-white px-4 py-2 rounded-lg font-medium transition-colors duration-200">Enviar</button>
             </div>
-        </div>
-        
-        <!-- Indicador de carga - FIJO arriba del input -->
-        <div id="aiLoadingIndicator" class="hidden flex px-5 py-3 text-left text-sm text-gray-500 italic flex-shrink-0">
-            Pensando
-        </div>
-        
-        <!-- Input del chat - FIJO en la parte inferior -->
-        <div class="px-4 py-4 md:px-5 md:py-5 bg-white border-t border-gray-200 flex-shrink-0">
-            <form id="chatForm" class="flex items-center bg-gray-50 border border-gray-300 rounded-3xl px-5 py-1 transition-colors focus-within:border-gray-400">
-                <input 
-                    id="chatInput" 
-                    class="flex-1 sticky flex-shrink-0 bg-transparent border-none outline-none text-gray-900 text-sm py-3 placeholder-gray-500"
-                    autocomplete="off"
-                    placeholder="Pregunta lo que quieras"
-                    maxlength="2000"
-                >
-                <button id="sendChatButton" type="submit" class="bg-custom-dark hover:bg-custom-dark-hover text-white border-none rounded-2xl px-4 py-2.5 text-sm transition-colors ml-2 disabled:bg-gray-400 disabled:cursor-not-allowed">
-                    Enviar
-                </button>
-            </form>
+          </form>
         </div>
     </section>
 
@@ -267,7 +245,7 @@
   <!-- MODALS -->
   <!-- === MODAL INICIO DE SESIÓN === -->
 <div id="sessionModal"
-     class="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
+     class="hidden fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
   <form id="loginForm" class="bg-white w-full max-w-md p-8 rounded-xl shadow-xl space-y-6">
     <div class="text-center">
       <h2 id="loginTitle" class="text-2xl font-bold text-gray-900">¡Bienvenido!</h2>
@@ -594,4 +572,4 @@
     };
   </script>
 </body>
-</html>
+</html>

+ 381 - 190
public/main/js/app.js

@@ -1,6 +1,7 @@
+//--- Imports ---
 import { sendMessage as serviceSendMessage } from './service/chat.js';
 import { getProducts, sendOrder } from './service/product.js';
-import { login } from './service/auth.js'
+import { login } from './service/auth.js';
 import { createGlobalLoader, showGlobalLoader, hideGlobalLoader } from './utils/loader.js';
 import { updateProgress, claimReward } from './utils/progressBar.js';
 import { showError } from './utils/error.js';
@@ -8,28 +9,64 @@ import { addHistoryRow, setupShoppingCart } from './utils/shoppingCart.js';
 import { hideGUI, showGUI } from './utils/gui.js';
 import { smartSearch } from './utils/searching.js';
 import { getUserData } from './service/user.js';
-// --- Variables de Usuario ---
+
+// --- Variables Globales ---
+
+// --- User Data ---
 let userId = -1;
 let userName = "Cliente";
+let chatUserName = "prueba_mesa43"
 let userTable = null;
 let userToken = null;
-// --- Datos de Productos y Carrito ---
+let chatWebsocket = null;
+
+// --- Products & Cart ---
 let Allproducts = [];
 let cart = [];
 let itsEmpty = true;
-let cacheMode = false
+let cacheMode = false;
 
-// --- Categorias Importantes ---
+// --- Configuration ---
+const favoriteCategories = ["Shop", "Pizzas Familiares", "Pizza Medianas"];
 
-const favoriteCategories = ["Shop", "Pizzas Familiares", "Pizza Medianas" ];
-
-// --- Historial de Chat ---
+// --- Chat History ---
 let chatHistory = [
-    { role: "system", content: "¡Hola! Soy tu asistente en Biergarten Klein. ¿Te gustaría una recomendación de nuestras cervezas artesanales?" }
+    { 
+        user: "name",
+        time: "00:00",
+        content: "Hola Mundo" 
+    }
+];
+
+const userColors = [
+  // Rojos
+  "text-red-500", "text-red-600", "text-rose-500", "text-rose-600",
+  "text-orange-500", "text-orange-600", "text-amber-500", "text-amber-600",
+
+  // Amarillos / dorados
+  "text-yellow-500", "text-yellow-600", "text-lime-500", "text-lime-600",
+
+  // Verdes
+  "text-green-500", "text-green-600", "text-emerald-500", "text-emerald-600",
+  "text-teal-500", "text-teal-600", "text-cyan-500", "text-cyan-600",
+
+  // Azules
+  "text-sky-500", "text-sky-600", "text-blue-500", "text-blue-600",
+  "text-indigo-500", "text-indigo-600",
+
+  // Violetas / púrpuras    æ
+  "text-violet-500", "text-violet-600", "text-purple-500", "text-purple-600",
+  "text-fuchsia-500", "text-fuchsia-600", "text-pink-500", "text-pink-600",
+
+  // Neutros variados
+  "text-gray-500", "text-gray-600", "text-zinc-500", "text-zinc-600",
+  "text-neutral-500", "text-neutral-600", "text-stone-500", "text-stone-600"
 ];
 
 
-// --- Elementos del DOM: Productos y Carrito ---
+//--- Elementos del DOM ---
+
+// --- Product & Cart Elements ---
 const productListElement = document.getElementById("productList");
 const cartItemsElement = document.getElementById("cartItems");
 const cartTotalElement = document.getElementById("cartTotal");
@@ -38,41 +75,50 @@ const checkoutButton = document.getElementById("checkoutButton");
 const originalCheckoutButtonText = checkoutButton ? checkoutButton.textContent : "Finalizar Pedido";
 const cartCountElement = document.getElementById("cartCount");
 
-// --- Elementos del DOM: Chat ---
+// --- Chat Elements ---
 const chatMessagesElement = document.getElementById("chatMessages");
 const chatInputElement = document.getElementById("chatInput");
 const chatForm = document.getElementById("chatForm");
-const aiLoadingIndicator = document.getElementById("aiLoadingIndicator");
-const chatSuggestionsElement = document.getElementById("chatSuggestions");
+const userList = document.getElementById("userList")
+// --- Reward Elements ---
+const rewardBtn = document.getElementById('rewardBtn');
+const rewardModal = document.getElementById('rewardModal');
+const closeRewardModal = document.getElementById('closeRewardModal');
+const closeSuccessRewardModalButton = document.getElementById('closeSuccessRewardModal');
+const cancelRewardBtn = document.getElementById('cancelRewardBtn');
+const acceptTermsCheckbox = document.getElementById('acceptTermsCheckbox');
+const claimRewardBtn = document.getElementById('claimRewardBtn');
 
+//--- Inicialización y Configuración ---
 
-//#region --- Inicialización y Configuracion ---
+/**
+ * Main application initialization
+ */
 async function initializeApp() {
     showGlobalLoader("Cargando productos...");
+    chatUserName = userName.split(" ")[0].toLowerCase() + "_mesa"+userTable
+    initializeChat();
+    initializeWebSocket();
     await initializeProducts();
     await renderProducts(Allproducts);
     setupSearchListener();
     updateCartDisplay();
     setupShoppingCart(userId, userToken, userName);
-    initializeChat();
     setupBasicListeners();
     showGUI();
     hideGlobalLoader();
-
-    const chatSuggestions = Array.from(chatSuggestionsElement.children);
-
-    chatSuggestions.forEach(suggestion => {
-        suggestion.addEventListener("click", () => {
-            sendSuggestion(suggestion.querySelector(".chat-suggestion").textContent);
-        });
-    });
 }
+
+/**
+ * Initialize login modal and authentication
+ */
 function initializeLoginModal() {
     const sessionModal = document.getElementById('sessionModal');
     const loginForm = document.getElementById('loginForm');
     const logoutBtn = document.getElementById('logoutBtn');
     sessionModal.classList.remove('hidden');
 
+    // Login form submission
     loginForm.addEventListener('submit', async (event) => {
         event.preventDefault();
         event.stopPropagation();
@@ -92,7 +138,7 @@ function initializeLoginModal() {
             return;
         }
         try {
-            const { data } = await login(email, pin)
+            const { data } = await login(email, pin);
             userToken = data.token;
             userName = data.name;
             userId = data.id;
@@ -104,15 +150,15 @@ function initializeLoginModal() {
             }
 
             sessionModal.classList.add('hidden');
-
             initializeApp();
         } catch (error) {
-            console.error(error)
+            console.error(error);
         }
-    })
+    });
+    // Logout functionality
     if (logoutBtn) {
         logoutBtn.addEventListener('click', () => {
-            setCookie("userToken", "", -1); // Eliminar cookie
+            setCookie("userToken", "", -1);
             userId = -1;
             userName = "Cliente";
             userTable = null;
@@ -120,40 +166,95 @@ function initializeLoginModal() {
             cacheMode = false;
             hideGUI();
             sessionModal.classList.remove('hidden');
+            
+            // Reset UI elements
             document.querySelector("#emailInputContainer").classList.remove("hidden");
             document.querySelector("#pinInputContainer").classList.remove("hidden");
-            const loginTitle = document.getElementById("loginTitle");
-            loginTitle.textContent = "¡Bienvenido!";
-            // Restaurar atributos required a los inputs
+            document.getElementById("loginTitle").textContent = "¡Bienvenido!";
             document.querySelector("#emailInput").setAttribute("required", "");
             document.querySelector("#pinInput").setAttribute("required", "");
             logoutBtn.classList.add("hidden");
             document.querySelector("#recoveryPIN").classList.remove("hidden");
         });
     }
+}
 
-}   
+function initializeWebSocket() {
+    if (!chatWebsocket) {
+        const wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
+        chatWebsocket = new WebSocket(`${wsProtocol}://${location.host}/api/chat/ws?token=${userToken}`);
+        chatWebsocket.onopen = () => {
+            chatWebsocket.send(JSON.stringify({
+                type: "join",
+                username: chatUserName
+            }));
+        }
+        chatWebsocket.onmessage = (event) => {
+            const data = JSON.parse(event.data);
+            if (data.type === "message") {
+                displayChatMessage(data.username, data.message);
+            }
+            else if (data.type === "join") {
+                newUserInChat(data.username);
+            }
+            else if (data.type === "leave") {
+                userLeftChat(data.username);
+            }
+            else if (data.type === "ping") {
+                chatWebsocket.send(JSON.stringify({
+                    type: "pong"
+                }));
+            }
+        }
+    }
+}
+
+
+
+/**
+ * Initialize chat functionality
+ */
 function initializeChat() {
     if (!chatForm) return;
     chatForm.addEventListener("submit", (event) => {
         event.preventDefault();
         if (chatInputElement.value.trim() === "") return;
 
-        sendMessageToAI();
-        chatInputElement.addEventListener("input", () => {
-            if (chatInputElement.value.trim() === "") {
-                chatSuggestionsElement.classList.remove("hidden");
-            } else {
-                chatSuggestionsElement.classList.add("hidden");
-            }
-        });
+        // sendMessageToAI();
+        if (chatWebsocket) {
+            chatWebsocket.send(JSON.stringify({
+                type: "message",
+                username: chatUserName,
+                message: chatInputElement.value
+            }));
+            chatInputElement.value = "";
+        }
+        // displayChatMessage(chatUserName, chatInputElement.value)
+    });
+    chatInputElement.addEventListener("input", () => {
+      const lastWord = chatInputElement.value.split(" ").at(-1)
+      if (lastWord.trim().startsWith("@")) {
+        userList.classList.remove("hidden")
+
+      }else{
+        userList.classList.add("hidden")
+      }
     });
 }
+
+/**
+ * Setup basic event listeners
+ */
 function setupBasicListeners() {
     if (!checkoutButton) return;
+    
     checkoutButton.addEventListener("click", processOrder);
     initializeRewards();
 }
+
+/**
+ * Setup search functionality with debouncing
+ */
 function setupSearchListener() {
     const searchInput = document.getElementById("searchInput");
     if (!searchInput) return;
@@ -161,38 +262,39 @@ function setupSearchListener() {
     let debounceTimer;
     
     searchInput.addEventListener("input", () => {
-        // Limpiar el timer anterior si existe
         clearTimeout(debounceTimer);
-        const specialCases = {
-            "shop": ["cerveza"]
-        }
-        // Agregar una clase de "buscando" para el feedback visual
+        
+        // Visual feedback during search
         productListElement.style.opacity = "0.7";
         productListElement.style.transform = "scale(0.98)";
         productListElement.style.transition = "opacity 0.2s ease, transform 0.2s ease";
         
-        // Debounce de 200ms para evitar muchas llamadas
+        // Debounce search for 200ms
         debounceTimer = setTimeout(() => {
             const searchTerm = searchInput.value.toLowerCase();
             if (searchTerm.trim() === "") {
                 renderProductsWithAnimation(Allproducts);
                 return;
             }
-            const finded = smartSearch(Allproducts, searchTerm);
-            // Renderizar con animación
-            renderProductsWithAnimation(finded, false, searchTerm);
+            
+            const foundProducts = smartSearch(Allproducts, searchTerm);
+            renderProductsWithAnimation(foundProducts, false, searchTerm);
         }, 200);
     });
 }
-//#endregion
-//#region ===== Utilidad =====
-
-
+//--- Funciones de Utilidad ---
 
+/**
+ * Set a cookie with expiration
+ */
 function setCookie(name, value, days) {
     const expires = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toUTCString();
     document.cookie = `${name}=${value}; expires=${expires}; path=/`;
 }
+
+/**
+ * Get a cookie value by name
+ */
 function getCookie(name) {
     const cookies = document.cookie.split('; ');
     for (const cookie of cookies) {
@@ -203,28 +305,42 @@ function getCookie(name) {
     }
     return null;
 }
+/**
 
+ * Check and validate cached user data
+ */
 async function checkCache() {
     const tokenCache = getCookie("userToken");
-    let user = null
+    let user = null;
+    
     if (tokenCache) {
         userToken = tokenCache;
-        try{
+        try {
             const data = await getUserData(userToken);
-            user = data.data
-        }catch (error) {
+            user = data.data;
+        } catch (error) {
             console.error("Error fetching user data:", error);
             console.error("Invalid user token, clearing cache.");
-            setCookie("userToken", "", -1); // Eliminar cookie si es inválida
+            setCookie("userToken", "", -1);
             return false;
         }
+        
         if (user) {
             userId = user.id;
             userName = user.name;
             updateProgress(user.reward_progress || 0);
-            const [emailInputContainer, pinInputContainer] = [document.getElementById("emailInputContainer"), document.getElementById("pinInputContainer")];
-            const [emailInput, pinInput] = [document.getElementById("emailInput"), document.getElementById("pinInput")];
+            
+            // Update UI for cached user
+            const [emailInputContainer, pinInputContainer] = [
+                document.getElementById("emailInputContainer"), 
+                document.getElementById("pinInputContainer")
+            ];
+            const [emailInput, pinInput] = [
+                document.getElementById("emailInput"), 
+                document.getElementById("pinInput")
+            ];
             const loginTitle = document.getElementById("loginTitle");
+            
             loginTitle.textContent = `¡Bienvenido ${user.name.split(" ")[0]}!`;
             emailInputContainer.classList.add("hidden");
             emailInput.removeAttribute("required");
@@ -232,45 +348,61 @@ async function checkCache() {
             pinInput.removeAttribute("required");
             document.querySelector("#logoutBtn").classList.remove("hidden");
             document.querySelector("#recoveryPIN").classList.add("hidden");
-            cacheMode = true;     
-            return true
+            cacheMode = true;
+            return true;
         }
     }
-    return cacheMode
+    return cacheMode;
 }
 
+/**
+ * Format price to Chilean peso currency
+ */
 function formatPrice(price) {
     return price.toLocaleString("es-CL", { style: "currency", currency: "CLP" });
 }
-//#endregion
-//#region ===== Productos =====
+//--- Gestión de Productos ---
 
+/**
+ * Initialize products from API
+ */
 async function initializeProducts() {
     Allproducts = await getProducts(userToken);
 }
 
+/**
+ * Create category containers for products
+ */
 async function createCategories(products) {
     let categories = new Set(products.map(product => product.type || "Sin categoría"));
     categories = Array.from(categories).sort((a, b) => a.localeCompare(b));
     
-    for (const category of favoriteCategories.reverse() ) {
+    // Prioritize favorite categories
+    for (const category of favoriteCategories.reverse()) {
         if (categories.includes(category)) {
             categories = categories.filter(cat => cat !== category);
-            categories.unshift(category); // Mover la categoría favorita al inicio
+            categories.unshift(category);
         }
     }
+    
     if (!productListElement) return;
 
     const categoryContainers = categories.map(category => {
         const container = document.createElement("div");
-        container.classList.add("category-container");
-        container.classList.add("mb-8", "p-4");
+        container.classList.add("category-container", "mb-8", "p-4");
+        
         const title = document.createElement("h2");
         title.classList.add("category-title", "text-3xl", "font-bold", "border-b-2", "pb-3", "mb-4");
-        let titleText = ["a", "e", "i", "o", "u", "á", "é", "í", "ó", "ú", "p"].includes(category.charAt(category.length - 1).toLowerCase()) ? "s" : 
-            category.charAt(category.length - 1).toLowerCase() === "s" ? "" : "es";
-        title.textContent = category + titleText;
+        
+        // Pluralize category name
+        const lastChar = category.charAt(category.length - 1).toLowerCase();
+        const pluralSuffix = ["a", "e", "i", "o", "u", "á", "é", "í", "ó", "ú", "p"].includes(lastChar) 
+            ? "s" 
+            : lastChar === "s" ? "" : "es";
+        title.textContent = category + pluralSuffix;
+        
         container.appendChild(title);
+        
         const productList = document.createElement("div");
         productList.classList.add("product-list", "space-y-6");
         productList.id = `productList-${category}`;
@@ -283,6 +415,9 @@ async function createCategories(products) {
     return categoryContainers;
 }
 
+/**
+ * Render products in their respective categories
+ */
 async function renderProducts(products, groupInCategories = true, searchTerm = "") {
     if (!productListElement) return;
 
@@ -291,11 +426,13 @@ async function renderProducts(products, groupInCategories = true, searchTerm = "
 
     productListElement.innerHTML = "";
     let categoryContainers = [];
+    
     if (groupInCategories) {
         categoryContainers = await createCategories(products);
-    }else{
-        categoryContainers = [ { category: searchTerm, container: productListElement } ];
+    } else {
+        categoryContainers = [{ category: searchTerm, container: productListElement }];
     }
+    
     if (products.length === 0) {
         const noProductsMessage = document.createElement("p");
         noProductsMessage.textContent = "No hay productos disponibles.";
@@ -304,12 +441,9 @@ async function renderProducts(products, groupInCategories = true, searchTerm = "
     }
 
     for (const { category, container } of categoryContainers) {
-        let productsInCategory
-        if (groupInCategories){
-            productsInCategory = products.filter(product => product.type === category);
-        }else{
-            productsInCategory = products;
-        }
+        let productsInCategory = groupInCategories 
+            ? products.filter(product => product.type === category)
+            : products;
 
         if (productsInCategory.length === 0) continue;
 
@@ -323,8 +457,7 @@ async function renderProducts(products, groupInCategories = true, searchTerm = "
             clone.querySelector(".product-image").style.backgroundImage = `url('${product.image}')`;
 
             const addBtn = clone.querySelector(".add-to-cart-btn");
-            addBtn.dataset.productId = product.id;   // el listener usa esta info
-            // Agregar event listener directamente al botón clonado
+            addBtn.dataset.productId = product.id;
             addBtn.addEventListener('click', (event) => {
                 const productId = parseInt(event.target.dataset.productId);
                 addToCart(productId, event.target);
@@ -335,24 +468,29 @@ async function renderProducts(products, groupInCategories = true, searchTerm = "
     }
 }
 
+/**
+ * Render products with smooth animations
+ */
 async function renderProductsWithAnimation(products, groupInCategories = true, searchTerm = "") {
     if (!productListElement) return;
 
     const template = document.getElementById("product-card-template");
     if (!template) return;
 
-    // Fade out actual content
+    // Fade out current content
     productListElement.style.opacity = "0";
     productListElement.style.transform = "scale(0.95)";
     
     setTimeout(async () => {
         productListElement.innerHTML = "";
         let categoryContainers = [];
-        if (groupInCategories){
+        
+        if (groupInCategories) {
             categoryContainers = await createCategories(products);
-        }else {
-            categoryContainers = [ { category: searchTerm, container: productListElement } ];
+        } else {
+            categoryContainers = [{ category: searchTerm, container: productListElement }];
         }
+        
         if (products.length === 0) {
             const noProductsMessage = document.createElement("p");
             noProductsMessage.textContent = "No hay productos disponibles.";
@@ -375,12 +513,9 @@ async function renderProductsWithAnimation(products, groupInCategories = true, s
 
         let animationDelay = 0;
         for (const { category, container } of categoryContainers) {
-            let productsInCategory = [];
-            if (groupInCategories){
-                productsInCategory = products.filter(product => product.type === category);
-            }else {
-                productsInCategory = products
-            }
+            let productsInCategory = groupInCategories 
+                ? products.filter(product => product.type === category)
+                : products;
 
             if (productsInCategory.length === 0) continue;
 
@@ -426,13 +561,15 @@ async function renderProductsWithAnimation(products, groupInCategories = true, s
         
     }, 150); // Wait for fade out to complete
 }
-//#endregion
-//#region ===== Carrito =====
-
+//--- Gestión del Carrito ---
 
+/**
+ * Add product to cart with visual feedback
+ */
 window.addToCart = function(productId, buttonElement = null) {
     const product = Allproducts.find(p => p.id === productId);
     if (!product) return;
+    
     const cartItem = cart.find(item => item.id === productId);
     if (cartItem) {
         cartItem.quantity++;
@@ -440,6 +577,7 @@ window.addToCart = function(productId, buttonElement = null) {
         cart.push({ ...product, quantity: 1 });
     }
 
+    // Visual feedback for button
     if (buttonElement) {
         const originalHTML = buttonElement.innerHTML;
         buttonElement.textContent = "✔ Agregado!";
@@ -449,12 +587,18 @@ window.addToCart = function(productId, buttonElement = null) {
             buttonElement.disabled = false;
         }, 300);
     }
+    
     updateCartDisplay();
-    // Dentro de addToCart (después de updateCartDisplay())
-    if (typeof showToast === "function") showToast(`${product.name} agregado al carrito`);
-
+    
+    // Show toast notification if available
+    if (typeof showToast === "function") {
+        showToast(`${product.name} agregado al carrito`);
+    }
 };
 
+/**
+ * Remove product from cart
+ */
 window.removeFromCart = function(productId, removeAll = false) {
     const itemIndex = cart.findIndex(item => item.id === productId);
     if (itemIndex > -1) {
@@ -467,16 +611,24 @@ window.removeFromCart = function(productId, removeAll = false) {
     updateCartDisplay();
 };
 
+/**
+ * Calculate total cart amount
+ */
 function calculateTotal() {
     if (!cartTotalElement) return;
     const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
     cartTotalElement.textContent = formatPrice(total);
 }
 
+/**
+ * Update cart display with animations
+ */
 function updateCartDisplay() {
     if (!cartItemsElement || !emptyCartTextElement || !checkoutButton || !cartCountElement) return;
+    
     cartItemsElement.innerHTML = "";
     cartCountElement.textContent = cart.reduce((sum, item) => sum + item.quantity, 0);
+    
     if (cart.length === 0) {
         cartCountElement.classList.add("hidden");
         emptyCartTextElement.classList.remove("hidden");
@@ -484,6 +636,8 @@ function updateCartDisplay() {
         itsEmpty = true;
     } else {
         cartCountElement.classList.remove("hidden");
+        
+        // Animate cart count badge
         if (cartCountElement && itsEmpty) {
             itsEmpty = false;
             cartCountElement.animate([
@@ -493,21 +647,23 @@ function updateCartDisplay() {
                 duration: 300,
                 iterations: 1,
                 easing: 'ease-in-out'
-            })
+            });
         } else {
             cartCountElement.animate([
                 { transform: 'scale(1) rotate(0deg)' },
                 { transform: 'scale(1.2) rotate(180deg)' },
                 { transform: 'scale(1) rotate(360deg)' }
-
             ], {
                 duration: 300,
                 iterations: 1,
                 easing: 'ease-in-out'
-            })
+            });
         }
+        
         emptyCartTextElement.classList.add("hidden");
         checkoutButton.disabled = false;
+        
+        // Render cart items
         cart.forEach(item => {
             const cartItemHTML = `
                 <div class="product-cart flex justify-between items-center border-b border-gray-700 pb-2 last:border-b-0 mb-2">
@@ -526,17 +682,20 @@ function updateCartDisplay() {
             `;
             cartItemsElement.innerHTML += cartItemHTML;
         });
-    
     }
+    
     calculateTotal();
 }
-//#endregion
-//#region ===== Pedidos =====
-
+//--- Procesamiento de Pedidos ---
 
+/**
+ * Process and send order to backend
+ */
 async function processOrder() {
     if (cart.length === 0) return;
+    
     showGlobalLoader();
+    
     if (checkoutButton) {
         checkoutButton.disabled = true;
         checkoutButton.textContent = "Procesando...";
@@ -546,15 +705,24 @@ async function processOrder() {
         const orderData = {
             customerId: userId,
             table: userTable,
-            items: cart.map(item => ({ id: item.id, price: item.price, quantity: item.quantity })),
+            items: cart.map(item => ({ 
+                id: item.id, 
+                price: item.price, 
+                quantity: item.quantity 
+            })),
             totalAmount: cart.reduce((sum, item) => sum + item.price * item.quantity, 0),
             orderDate: new Date().toLocaleString('sv-SE').replace(' ', 'T')
         };
+        
         const data = await sendOrder(orderData, userToken);
+        
         if (data && data.new_progress) {
             updateProgress(data.new_progress);
         }
+        
         alert("Pedido enviado con éxito.");
+        
+        // Add items to history
         cart.forEach(item => {
             addHistoryRow({
                 productName: item.name,
@@ -562,41 +730,72 @@ async function processOrder() {
                 price: item.price,
             });
         });
-        cart = []
+        
+        // Clear cart
+        cart = [];
         updateCartDisplay();
+        
     } catch (error) {
         console.error("Error al procesar la orden:", error);
         alert(`Hubo un problema: ${error.message || "Por favor, inténtalo de nuevo."}`);
-
-
     } finally {
         hideGlobalLoader();
         checkoutButton.disabled = cart.length === 0;
-        checkoutButton.textContent = originalCheckoutButtonText
-
+        checkoutButton.textContent = originalCheckoutButtonText;
     }
 }
-//#endregion
-//#region ===== Chat =====
-function displayChatMessage(sender, message) {
-    if (!chatMessagesElement) return;
-    const bubbleClass = sender === "user" ? "chat-bubble-user" : "chat-bubble-ai";
-    const messageDiv = document.createElement("div");
-    messageDiv.classList.add("chat-bubble", bubbleClass);
-    messageDiv.innerHTML = sender === "ai" && window.marked ? marked.parse(message) : message;
-    chatMessagesElement.appendChild(messageDiv);
+//--- Funcionalidad del Chat ---
+
+/**
+ * Display chat message with proper styling
+ */
+
+function getUserColor(username) {
+  const hash = [...username].reduce((acc, char) => acc + char.charCodeAt(0), 0);
+  return userColors[hash % userColors.length];
+}
+
+function displayChatMessage(sender, message, time = undefined) {
+    console.log(`[${sender.toUpperCase()}] ${message}`);
+    let realtime = `[${time ? time : new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}] `;
+    let messageTemplate = chatMessagesElement.querySelector("#chatMessageTemplate");
+    if (!messageTemplate) return;
+
+    let messageClone = messageTemplate.content.cloneNode(true).firstElementChild;
+    messageClone.querySelector(".chat-message-text").innerHTML = message;
+    messageClone.querySelector(".chat-message-time").innerHTML = realtime;
+    messageClone.querySelector(".chat-message-user").innerHTML = sender
+    messageClone.querySelector(".chat-message-user").classList.add(getUserColor(sender));
+    chatMessagesElement.appendChild(messageClone);
     chatMessagesElement.scrollTop = chatMessagesElement.scrollHeight;
 }
 
-async function sendSuggestion(suggestion) {
-    if (!chatInputElement || !aiLoadingIndicator) return;
-    chatInputElement.value = suggestion;
-    chatInputElement.focus();
+function newUserInChat(userName) {
+    let userTemplate = chatMessagesElement.querySelector("#systemMessageTemplate");
+    if (!userTemplate) return;
+
+    let userClone = userTemplate.content.cloneNode(true).firstElementChild;
+    userClone.classList.add("text-green-600");
+    userClone.querySelector(".chat-message-text").innerHTML = `*** ${userName} se ha unido al chat`;
+    chatMessagesElement.appendChild(userClone);
+}
+
+function removeUserFromChat(userName) {
+    let userTemplate = chatMessagesElement.querySelector("#systemMessageTemplate");
+    if (!userTemplate) return;
 
+    let userClone = userTemplate.content.cloneNode(true).firstElementChild;
+    userClone.classList.add("text-red-600");
+    userClone.querySelector(".chat-message-text").innerHTML = `*** ${userName} se ha ido del chat`;
+    chatMessagesElement.appendChild(userClone);
 }
 
+/**
+ * Send message to AI service
+ */
 async function sendMessageToAI() {
     if (!chatInputElement || !aiLoadingIndicator) return;
+    
     const userInput = chatInputElement.value.trim();
     if (!userInput) return;
 
@@ -607,14 +806,15 @@ async function sendMessageToAI() {
 
     try {
         const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
+        
         if (!response) {
             displayChatMessage("ai", "Hubo un problema al conectar con el Chef IA.");
         } else if (response === "not_init") {
             if (await initializeService()) {
-                const response = await serviceSendMessage(userInput, chatHistory, userName, userToken);
-                if (response) {
-                    chatHistory = response.messageList;
-                    displayChatMessage("ai", response.assistantResponse);
+                const retryResponse = await serviceSendMessage(userInput, chatHistory, userName, userToken);
+                if (retryResponse) {
+                    chatHistory = retryResponse.messageList;
+                    displayChatMessage("ai", retryResponse.assistantResponse);
                 } else {
                     displayChatMessage("ai", "Hubo un problema al enviar el mensaje.");
                 }
@@ -633,120 +833,108 @@ async function sendMessageToAI() {
         if (chatInputElement) chatInputElement.focus();
     }
 }
+//--- Sistema de Recompensas ---
 
-//#endregion
-//#region ===== Rewards =====
-// Referencias a elementos del DOM
-const rewardBtn = document.getElementById('rewardBtn');
-const rewardModal = document.getElementById('rewardModal');
-const closeRewardModal = document.getElementById('closeRewardModal');
-const closeSuccessRewardModalButton = document.getElementById('closeSuccessRewardModal');
-const cancelRewardBtn = document.getElementById('cancelRewardBtn');
-const acceptTermsCheckbox = document.getElementById('acceptTermsCheckbox');
-const claimRewardBtn = document.getElementById('claimRewardBtn');
-
-
+/**
+ * Initialize rewards functionality
+ */
 function initializeRewards() {
-    // Abrir modal cuando se hace clic en el botón de recompensa
-
+    // Event listeners for reward system
     closeSuccessRewardModalButton.addEventListener("click", closeSuccessRewardModal);
 
     rewardBtn.addEventListener('click', function () {
         if (!rewardBtn.disabled) {
             rewardModal.classList.remove('hidden');
-            document.body.style.overflow = 'hidden'; // Evitar scroll del fondo
+            document.body.style.overflow = 'hidden';
         }
     });
 
-    // Cerrar modal - botón X
+    // Close modal - X button
     closeRewardModal.addEventListener('click', function () {
         closeModal();
     });
 
-    // Cerrar modal - botón Cancelar
+    // Close modal - Cancel button
     cancelRewardBtn.addEventListener('click', function () {
         closeModal();
     });
 
-    // Cerrar modal haciendo clic fuera de él
+    // Close modal - click outside
     rewardModal.addEventListener('click', function (e) {
         if (e.target === rewardModal) {
             closeModal();
         }
     });
 
-    // Cerrar modal con tecla Escape
+    // Close modal - Escape key
     document.addEventListener('keydown', function (e) {
         if (e.key === 'Escape' && !rewardModal.classList.contains('hidden')) {
             closeModal();
         }
     });
 
-    // Manejar el reclamo del premio
+    // Handle reward claim
     claimRewardBtn.addEventListener('click', function () {
         if (!this.disabled && acceptTermsCheckbox.checked) {
-            // Generar código de premio (puedes cambiar esta lógica)
-
-            // Mostrar mensaje de éxito
             claimReward(userToken, userTable);
-            // Cerrar modal
             closeModal();
             updateProgress(0);
         }
     });
 
+    // Handle terms checkbox
+    acceptTermsCheckbox.addEventListener('change', function () {
+        if (this.checked) {
+            claimRewardBtn.disabled = false;
+            claimRewardBtn.classList.remove('opacity-50', 'cursor-not-allowed');
+        } else {
+            claimRewardBtn.disabled = true;
+            claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
+        }
+    });
 }
 
+/**
+ * Close success reward modal
+ */
 function closeSuccessRewardModal() {
     const successRewardModal = document.getElementById("successRewardModal");
     successRewardModal.classList.add("hidden");
-    document.body.style.overflow = ''; // Restaurar scroll
+    document.body.style.overflow = '';
 }
 
-// Función para cerrar el modal
+/**
+ * Close reward modal and reset form
+ */
 function closeModal() {
     rewardModal.classList.add('hidden');
-    document.body.style.overflow = ''; // Restaurar scroll
+    document.body.style.overflow = '';
 
-    // Reset del formulario al cerrar
+    // Reset form
     acceptTermsCheckbox.checked = false;
     claimRewardBtn.disabled = true;
     claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
 }
 
-// Manejar el checkbox de términos y condiciones
-acceptTermsCheckbox.addEventListener('change', function () {
-    if (this.checked) {
-        // Habilitar botón de reclamar
-        claimRewardBtn.disabled = false;
-        claimRewardBtn.classList.remove('opacity-50', 'cursor-not-allowed');
-    } else {
-        // Deshabilitar botón de reclamar
-        claimRewardBtn.disabled = true;
-        claimRewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
-    }
-});
-
-
-
-// Función para mostrar mensaje de éxito con el código
+/**
+ * Show reward success notification
+ */
 function showRewardSuccess(code) {
-    // Crear elemento de notificación de éxito
     const successToast = document.createElement('div');
     successToast.className = `
-            fixed top-4 left-1/2 transform -translate-x-1/2 
-            bg-green-500 text-white text-sm rounded-lg px-6 py-4 
-            shadow-lg z-50 max-w-sm text-center
-        `;
+        fixed top-4 left-1/2 transform -translate-x-1/2 
+        bg-green-500 text-white text-sm rounded-lg px-6 py-4 
+        shadow-lg z-50 max-w-sm text-center
+    `;
     successToast.innerHTML = `
-            <div class="font-bold mb-1">🎉 ¡Premio Reclamado!</div>
-            <div class="text-xs opacity-90">Tu código: <strong>${code}</strong></div>
-            <div class="text-xs mt-1 opacity-80">Muéstralo al mesero</div>
-        `;
+        <div class="font-bold mb-1">🎉 ¡Premio Reclamado!</div>
+        <div class="text-xs opacity-90">Tu código: <strong>${code}</strong></div>
+        <div class="text-xs mt-1 opacity-80">Muéstralo al mesero</div>
+    `;
 
     document.body.appendChild(successToast);
 
-    // Remover después de 5 segundos
+    // Remove after 5 seconds
     setTimeout(() => {
         if (successToast.parentNode) {
             successToast.remove();
@@ -754,34 +942,37 @@ function showRewardSuccess(code) {
     }, 5000);
 }
 
-// Función para resetear el progreso de recompensa
+/**
+ * Reset reward progress to 0%
+ */
 function resetRewardProgress() {
     const progressBar = document.getElementById('progressBar');
     const progressText = document.getElementById('progressText');
     const rewardBtn = document.getElementById('rewardBtn');
 
-    // Resetear a 0%
     progressBar.style.width = '0%';
     progressText.textContent = '0%';
 
-    // Deshabilitar botón de recompensa
     rewardBtn.disabled = true;
     rewardBtn.classList.add('opacity-50', 'cursor-not-allowed');
     rewardBtn.classList.remove('bg-yellow-500', 'hover:bg-yellow-600');
     rewardBtn.textContent = '🎉 ¡Reclamar!';
 }
+//--- Inicialización de la Aplicación ---
 
-//#endregion
-// --- APP initialization ---
+/**
+ * Initialize the application when DOM is loaded
+ */
 document.addEventListener("DOMContentLoaded", async () => {
     const isCacheValid = await checkCache();
     if (!isCacheValid) {
-        // Si la caché no es válida, redirigir al usuario o mostrar un mensaje
         console.warn("Cache is invalid");
     }
+    
     createGlobalLoader();
+    // Uncomment these lines when ready to initialize the full app:
     initializeLoginModal();
     hideGUI();
-    // initializeApp()
-
+    // initializeApp();
 });
+

+ 10 - 0
public/main/styles.css

@@ -52,6 +52,16 @@ footer.bg-black {
     color: white;
 }
 
+#userList {
+}
+
+.list-user-name {
+  &:not(:last-child){
+    border-bottom: 1px solid #ccc;
+    font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
+  }
+}
+
 .active {
     background-color: #0a0a0a08 !important;
     box-shadow: 0px 0px 0px 2px #0a0a0a08;

+ 87 - 16
routes/chat.py

@@ -1,50 +1,121 @@
+import asyncio
+import json
 from fastapi import Request, HTTPException, Depends
 from fastapi.responses import JSONResponse
-from httpx import get
+from pydantic import BaseModel, ConfigDict
 from models.chat import ChatCompletionRequest
 from models.user import User
 from services.openai_service.openai_service import generate_completion
 from auth.security import get_current_user
 import logging
-from fastapi import APIRouter
+from fastapi import APIRouter, WebSocket, WebSocketDisconnect
+from fastapi.security import HTTPAuthorizationCredentials
 from config.messages import SuccessResponse
 
 logger = logging.getLogger(__name__)
 
 chat_router = APIRouter()
 
+class ConnectedUser(BaseModel):
+    
+    model_config = ConfigDict(arbitrary_types_allowed=True)
+    websocket: WebSocket
+    username: str
+
+connected_users: list[ConnectedUser] = []
+
+@chat_router.websocket("/ws")
+async def chat_irc_endpoint(websocket: WebSocket):
+    """WebSocket endpoint for real-time chat interactions"""
+    global connected_users
+    logger.info("New WebSocket connection attempt")
+    logger.debug(f"WebSocket query params: {websocket.query_params}")
+    current_user = await get_current_user(HTTPAuthorizationCredentials(scheme="Bearer", credentials=websocket.query_params.get("token", "")))
+    if not current_user:
+        await websocket.close(code=1008)  # Policy Violation
+        return
+    
+    await websocket.accept()
+    logger.info(f"User {current_user.email} connected to WebSocket chat.")
+    username = None
+    try:
+        while True:
+            try:
+                data = await asyncio.wait_for(websocket.receive_text(), timeout=30.0)  # 5 minutes timeout
+                payload= json.loads(data)
+                event_type = payload.get("type")
+
+                if event_type == "join":
+                    username = payload["username"]
+                    connected_users.append(ConnectedUser(websocket=websocket, username=username))
+                    response = {
+                        "type": "join",
+                        "username": username
+                    }
+                
+                elif event_type == "message":
+                    username = payload["username"]
+                    message = payload["message"]
+                    response = {
+                        "type": "message",
+                        "username": username,
+                        "message": message
+                    }
+                
+                elif event_type == "leave":
+                    response = {
+                        "type": "leave",
+                        "username": payload["username"]
+                    }
+                    connected_users = [user for user in connected_users if user.websocket != websocket]
+                    await websocket.close()
+                    logger.info(f"User {current_user.email} disconnected from WebSocket chat.")
+                    break
+                elif event_type == "pong":
+                    logger.info(f"Received pong from user {current_user.email}")
+                    continue
+
+                # Broadcast a todos los conectados
+                for client in connected_users:
+                    await client.websocket.send_text(json.dumps(response))
+                # Broadcast the received message to all connected users
+            except asyncio.TimeoutError:
+                websocket_text = {
+                    "type": "ping"
+                }
+                await websocket.send_text(json.dumps(websocket_text))
+    except WebSocketDisconnect:
+        connected_users = [user for user in connected_users if user.websocket != websocket]
+        logger.info(f"User {current_user.email} disconnected from WebSocket chat.")
+        response = {
+            "type": "leave",
+            "username": username if username else "unknown"
+        }
+        for client in connected_users:
+            await client.websocket.send_text(json.dumps(response))
+
 
 @chat_router.post("/completions")
 async def chat_completions(request_data: ChatCompletionRequest, request: Request, current_user: User = Depends(get_current_user)):
     """Get chat completions from OpenAI"""
     # Uses session_token (which is the antiAbuseToken) as an identifier for logging
     session_identifier = request.session.get("antiAbuseToken", "unknown_session")
-    
     logger.info(f"Chat completion request from user {current_user.email}")
-    
 
     try:
         openai_response = await generate_completion(
-            request_data.messages, 
-            session_identifier, 
-            current_user.name, 
+            request_data.messages,
+            session_identifier,
+            current_user.name,
             current_user.email
         )
-        
-        logger.info(f"OpenAI response generated for user {current_user.email}")
-        
-        # Legacy logging function
 
-        
         logger.info(f"Chat completion successful for user {current_user.email}")
         return JSONResponse({"response": openai_response, "message": SuccessResponse.CHAT_RESPONSE_SUCCESS})
-        
     except HTTPException as e:
         logger.error(f"HTTP error in chat completion for user {current_user.email}: {e.detail}")
         raise
-        
     except Exception as e:
         error_msg = f"Unexpected error in /api/chat/completions for user {current_user.email}: {e}"
         logger.error(error_msg)
-        
-        raise HTTPException(status_code=500, detail="Error interno del servidor al procesar el chat.")
+        raise HTTPException(status_code=500, detail="Error interno del servidor al procesar el chat.")