Procházet zdrojové kódy

fixed response iaklein

Erwin Jacimino před 8 měsíci
rodič
revize
9d5b4c956a
5 změnil soubory, kde provedl 176 přidání a 120 odebrání
  1. 1 1
      app.py
  2. 0 69
      public/main/index.html
  3. 135 8
      public/main/js/app.js
  4. 29 26
      routes/chat.py
  5. 11 16
      services/openai_service/openai_service.py

+ 1 - 1
app.py

@@ -41,7 +41,7 @@ def create_app() -> FastAPI:
         logger.info("Adding CORS middleware")
         app.add_middleware(
             CORSMiddleware,
-            allow_origins=["https://admin.kleinexpress.store"],
+            allow_origins=["https://admin.kleinexpress.store", "https://kleinexpress.store", "http://localhost:8000"],
             allow_credentials=True,
             allow_methods=["*"],
             allow_headers=["*"],

+ 0 - 69
public/main/index.html

@@ -496,75 +496,6 @@
 </div>
               <!-- ---------- JS: conmutar tabs + toast ---------- -->
   <script>
-
-    const animation_time = 200
-    let transitioning = false;
-    // conmutar pestañas
-    const buttons = document.querySelectorAll('.tab-btn')
-    buttons.forEach(btn => {
-      btn.addEventListener('click', () => {
-        const target = btn.dataset.target;
-        if (target === "chatTab") {
-          window.hideMentioned();
-        }
-        const active = document.querySelector(':not(.hidden)[data-tab]');
-        const activeIndex = active.dataset.index;
-        const to = document.querySelector(`#${target}[data-tab]`);
-        const toIndex = to.dataset.index;
-        const height = to.offsetHeight;
-        
-        if (activeIndex === toIndex || transitioning) return;
-        buttons.forEach(button => {
-          button.classList.remove('active')
-        })
-        btn.classList.add('active')
-        
-        active.style.height = "100%";
-        active.style.width = "100vw"
-        to.style.height = "100%";
-        to.style.width = "100vw"
-        to.style.zIndex = "1";
-        active.style.zIndex = "0";
-        transitioning = true;
-        const otherTabs = document.querySelectorAll('[data-tab]');
-        otherTabs.forEach(tab => {
-          if (tab !== active && tab !== to) {
-            tab.classList.add('hidden');
-            tab.classList.remove(`animate-[slideLeft_${animation_time}ms_ease-out]`, `animate-[slideRight_${animation_time}ms_ease-out]`);
-            tab.classList.remove(`animate-[slideLeftIn_${animation_time}ms_ease-out]`, `animate-[slideRightIn_${animation_time}ms_ease-out]`);
-          }
-        });
-        to.classList.remove('hidden');
-        // Animate tab transition
-        if (activeIndex < toIndex) {
-          // Slide left
-          active.classList.add(`animate-[slideLeft_${animation_time}ms_ease-out]`);
-          to.classList.add(`animate-[slideRightIn_${animation_time}ms_ease-out]`);
-        } else if (activeIndex > toIndex) {
-          // Slide right
-          active.classList.add(`animate-[slideRight_${animation_time}ms_ease-out]`);
-          to.classList.add(`animate-[slideLeftIn_${animation_time}ms_ease-out]`);
-        }
-
-        setTimeout(() => {
-          active.classList.remove(`animate-[slideLeft_${animation_time}ms_ease-out]`, `animate-[slideRight_${animation_time}ms_ease-out]`);
-          active.classList.add('hidden');
-          to.classList.remove(`animate-[slideLeftIn_${animation_time}ms_ease-out]`, `animate-[slideRightIn_${animation_time}ms_ease-out]`);
-          transitioning = false;
-        }, animation_time);
-
-        // Update header title if needed
-        const title = btn.dataset.title;
-        if (title) {
-          document.getElementById('mainTitle').textContent = title;
-        }
-
-
-
-
-      });
-    });
-
     // toast simple
     window.showToast = (msg) => {
       const toast = document.getElementById('toastCart');

+ 135 - 8
public/main/js/app.js

@@ -120,6 +120,7 @@ async function initializeApp() {
     await initializeProducts();
     await renderProducts(Allproducts);
     setupSearchListener();
+    setupTabSwitching();
     updateCartDisplay();
     setupShoppingCart(userId, userToken, userName);
     setupBasicListeners();
@@ -344,6 +345,23 @@ function setupBasicListeners() {
     
     checkoutButton.addEventListener("click", processOrder);
     initializeRewards();
+
+        // Close model - Back button on phone
+    window.addEventListener('popstate', function (event) {
+        alert("back");
+        this.history.pushState(null, null, this.location.href);
+        if (!rewardModal.classList.contains('hidden')) {
+            closeModal();
+        }else {
+            window.switchTab('menuTab');
+        }
+    });
+
+    window.addEventListener('beforeunload', function (event) {
+        // Show a confirmation dialog
+        event.preventDefault();
+        event.returnValue = 'Estas saliendo de la aplicacion, estas seguro?';
+    });
 }
 
 /**
@@ -376,6 +394,123 @@ function setupSearchListener() {
         }, 200);
     });
 }
+
+// Tab switching variables
+const animation_time = 200;
+let transitioning = false;
+
+/**
+ * Switch to a specific tab programmatically
+ * @param {string} targetTabId - The ID of the target tab (e.g., 'chatTab', 'productsTab')
+ */
+window.switchTab = function(targetTabId) {
+    const targetButton = document.querySelector(`[data-target="${targetTabId}"]`);
+    if (!targetButton) {
+        console.warn(`Tab button with target "${targetTabId}" not found`);
+        return;
+    }
+    
+    if (targetTabId === "chatTab") {
+        window.hideMentioned();
+    }
+    
+    const active = document.querySelector(':not(.hidden)[data-tab]');
+    if (!active) {
+        console.warn('No active tab found');
+        return;
+    }
+    
+    const activeIndex = active.dataset.index;
+    const to = document.querySelector(`#${targetTabId}[data-tab]`);
+    if (!to) {
+        console.warn(`Target tab "${targetTabId}" not found`);
+        return;
+    }
+    
+    const toIndex = to.dataset.index;
+    
+    if (activeIndex === toIndex || transitioning) return;
+    
+    // Update button states
+    const buttons = document.querySelectorAll('.tab-btn');
+    buttons.forEach(button => {
+        button.classList.remove('active');
+    });
+    targetButton.classList.add('active');
+    
+    // Perform tab transition
+    performTabTransition(active, to, activeIndex, toIndex, targetButton);
+};
+
+/**
+ * Perform the actual tab transition with animations
+ * @param {Element} active - Currently active tab
+ * @param {Element} to - Target tab to switch to
+ * @param {string} activeIndex - Index of active tab
+ * @param {string} toIndex - Index of target tab
+ * @param {Element} targetButton - Button that triggered the switch
+ */
+function performTabTransition(active, to, activeIndex, toIndex, targetButton) {
+    active.style.height = "100%";
+    active.style.width = "100vw";
+    to.style.height = "100%";
+    to.style.width = "100vw";
+    to.style.zIndex = "1";
+    active.style.zIndex = "0";
+    transitioning = true;
+    
+    const otherTabs = document.querySelectorAll('[data-tab]');
+    otherTabs.forEach(tab => {
+        if (tab !== active && tab !== to) {
+            tab.classList.add('hidden');
+            tab.classList.remove(`animate-[slideLeft_${animation_time}ms_ease-out]`, `animate-[slideRight_${animation_time}ms_ease-out]`);
+            tab.classList.remove(`animate-[slideLeftIn_${animation_time}ms_ease-out]`, `animate-[slideRightIn_${animation_time}ms_ease-out]`);
+        }
+    });
+    
+    to.classList.remove('hidden');
+    
+    // Animate tab transition
+    if (activeIndex < toIndex) {
+        // Slide left
+        active.classList.add(`animate-[slideLeft_${animation_time}ms_ease-out]`);
+        to.classList.add(`animate-[slideRightIn_${animation_time}ms_ease-out]`);
+    } else if (activeIndex > toIndex) {
+        // Slide right
+        active.classList.add(`animate-[slideRight_${animation_time}ms_ease-out]`);
+        to.classList.add(`animate-[slideLeftIn_${animation_time}ms_ease-out]`);
+    }
+
+    setTimeout(() => {
+        active.classList.remove(`animate-[slideLeft_${animation_time}ms_ease-out]`, `animate-[slideRight_${animation_time}ms_ease-out]`);
+        active.classList.add('hidden');
+        to.classList.remove(`animate-[slideLeftIn_${animation_time}ms_ease-out]`, `animate-[slideRightIn_${animation_time}ms_ease-out]`);
+        transitioning = false;
+    }, animation_time);
+
+    // Update header title if needed
+    const title = targetButton.dataset.title;
+    if (title) {
+        const mainTitle = document.getElementById('mainTitle');
+        if (mainTitle) {
+            mainTitle.textContent = title;
+        }
+    }
+}
+
+/**
+ * Setup tab switching functionality with animations
+ */
+function setupTabSwitching() {
+    // Tab switching functionality
+    const buttons = document.querySelectorAll('.tab-btn');
+    buttons.forEach(btn => {
+        btn.addEventListener('click', () => {
+            const target = btn.dataset.target;
+            window.switchTab(target);
+        });
+    });
+}
 //--- Funciones de Utilidad ---
 
 /**
@@ -1013,14 +1148,6 @@ function initializeRewards() {
         closeModal();
     });
 
-    // Close model - Back button on phone
-    window.onpopstate = function (event) {
-        if (!rewardModal.classList.contains('hidden')) {
-            event.preventDefault();
-            event.stopPropagation();
-            closeModal();
-        }
-    }
 
     // Close modal - click outside
     rewardModal.addEventListener('click', function (e) {

+ 29 - 26
routes/chat.py

@@ -17,12 +17,10 @@ logger = logging.getLogger(__name__)
 chat_router = APIRouter()
 
 redis_client = Redis(host='localhost', port=6379, db=1 if DEVELOPMENT else 0, decode_responses=True)
-
+broadcast_channel = "chat" if not DEVELOPMENT else "chat_dev"
 @chat_router.websocket("/ws")
 async def chat_irc_endpoint(websocket: WebSocket):
     """WebSocket endpoint for real-time chat interactions"""
-    global connected_users
-    
     
     # Backend de broadcast con Redis
     broadcast = Broadcast("redis://localhost:6379")
@@ -46,15 +44,15 @@ async def chat_irc_endpoint(websocket: WebSocket):
     async def reader():
         """Lee mensajes desde Redis y los manda al WebSocket"""
         logger.info("Iniciando lector de mensajes")
-        async with broadcast.subscribe(channel="chat") as subscriber:
-            logger.info("Subscribed to Redis channel 'chat'")
+        async with broadcast.subscribe(channel=broadcast_channel) as subscriber:
+            logger.info(f"Subscribed to Redis channel '{broadcast_channel}'")
             logger.debug("Starting message read loop")
-            async for event in subscriber:
+            async for event in subscriber:  # type: ignore
                 try:
                     logger.debug(f"Broadcasting message to WebSocket: {event.message}")
                     await websocket.send_text(event.message)
-                except Exception:
-                    logger.error("Error sending message to WebSocket, closing connection.")
+                except Exception as e:
+                    logger.error(f"Error sending message to WebSocket: {e}, closing connection.")
                     break
     await broadcast.connect()
     reader_task = asyncio.create_task(reader())
@@ -65,6 +63,7 @@ async def chat_irc_endpoint(websocket: WebSocket):
             try:
                 
                 data = await asyncio.wait_for(websocket.receive_text(), timeout=30.0)
+                logger.debug(f"Received data from WebSocket: {data}")
                 payload = json.loads(data)
                 event_type = payload.get("type")
                 logger.debug(f"Received event: {event_type} with payload: {payload}")
@@ -89,7 +88,7 @@ async def chat_irc_endpoint(websocket: WebSocket):
                     message_username = payload["username"]
                     
                     users = redis_client.smembers("connected_users")
-                    for user in users:
+                    for user in users:  # type: ignore
                         user_data = json.loads(user)
                         if user_data["username"] == message_username:
                             redis_client.srem("connected_users", user)
@@ -99,31 +98,23 @@ async def chat_irc_endpoint(websocket: WebSocket):
                     break
                 elif event_type == "ai_message":
                     messages = redis_client.lrange("chat_history", -15, -1)
-                    parsed_messages = [json.loads(msg) for msg in messages]
+                    parsed_messages = [json.loads(msg) for msg in messages]  # type: ignore
                     logger.debug(f"IA Message from {payload['username']}")
                     message_username = payload["username"]
                     response_content = await generate_completion(parsed_messages, current_user)
                     response = {"type": "message", "username": "IAKlein", "message": response_content}
-                elif event_type == "notification":
-                    logger.debug(f"Notification to chat")
-                    notification_message = payload["message"]
-                    messages = redis_client.lrange("chat_history", -15, -1)
-                    parsed_messages = [json.loads(msg) for msg in messages]
-                    response_content = admin_completion(notification_message, parsed_messages)
-                    response = {"type": "message", "username": "IAKlein", "message": response_content}
-                    
                 elif event_type == "mention":
                     logger.debug(f"Mention to {payload['username']}")
                     mention_username = payload["username"]
                     logger.debug(f"Mention username: {mention_username}, Current username: {username}")
-                    await broadcast.publish(channel="chat", message=json.dumps({"type": "mentioned", "username": mention_username}))
+                    await broadcast.publish(channel=broadcast_channel, message=json.dumps({"type": "mentioned", "username": mention_username}))
                     continue
                 elif event_type == "pong":
                     logger.debug(f"Received pong from user {current_user.email}")
                     continue
 
                 # Publicar en Redis
-                await broadcast.publish(channel="chat", message=json.dumps(response))
+                await broadcast.publish(channel=broadcast_channel, message=json.dumps(response))  # type: ignore
 
             except asyncio.TimeoutError:
                 await websocket.send_text(json.dumps({"type": "ping"}))
@@ -132,13 +123,13 @@ async def chat_irc_endpoint(websocket: WebSocket):
         logger.info(f"User {current_user.email} disconnected from WebSocket chat.")
         
         users = redis_client.smembers("connected_users")
-        for user in users:
+        for user in users:  # type: ignore
             user_data = json.loads(user)
             if user_data["mail"] == current_user.email:
                 redis_client.srem("connected_users", user)
                 break
-        response = {"type": "leave", "username": user_data["username"]}
-        await broadcast.publish(channel="chat", message=json.dumps(response))
+        response = {"type": "leave", "username": user_data["username"]}  # type: ignore
+        await broadcast.publish(channel=broadcast_channel, message=json.dumps(response))
 
     finally:
         reader_task.cancel()
@@ -149,7 +140,19 @@ async def notify_users(message: NotifyRequest, _: User = Depends(get_current_use
     """Send a notification message to all connected users"""
     broadcast = Broadcast("redis://localhost:6379")
     await broadcast.connect()
-    await broadcast.publish(channel="chat", message=json.dumps({"type": "notification", "message": message.message}))
+    
+    # Obtener historial de mensajes para generar respuesta de IA
+    messages = redis_client.lrange("chat_history", -15, -1)
+    parsed_messages = [json.loads(msg) for msg in messages]  # type: ignore
+    
+    # Generar respuesta de IA para la notificación
+    logger.debug(f"Processing notification: {message.message}")
+    response_content = admin_completion(message.message, parsed_messages)
+    
+    # Enviar directamente la respuesta de IA como mensaje
+    response = {"type": "message", "username": "IAKlein", "message": response_content}
+    await broadcast.publish(channel=broadcast_channel, message=json.dumps(response))
+    
     await broadcast.disconnect()
     return {"status": "Notification sent"}
 
@@ -158,7 +161,7 @@ async def get_connected_users(q: Optional[str] = Query(None), _: User = Depends(
     """Get a list of connected users (solo local al worker)"""
     # return {"users": [user.username for user in connected_users if q.lower() in user.username.lower()]}
     all_users = redis_client.smembers("connected_users")
-    all_users = [json.loads(user)["username"] for user in all_users]
+    all_users = [json.loads(user)["username"] for user in all_users]  # type: ignore
     if q is None or q.strip() == "":
         return {"users": all_users}
     filtered_users = [user for user in all_users if q.lower() in user.lower()]
@@ -168,4 +171,4 @@ async def get_connected_users(q: Optional[str] = Query(None), _: User = Depends(
 async def get_online_user_count(_: User = Depends(get_current_user)):
     """Get the count of online users (solo local al worker)"""
     all_users = redis_client.smembers("connected_users")
-    return {"count": len(all_users)}
+    return {"count": len(all_users)}  # type: ignore

+ 11 - 16
services/openai_service/openai_service.py

@@ -2,6 +2,7 @@ import json
 from typing import List
 from fastapi import HTTPException
 from openai import OpenAI
+from openai.types.chat.chat_completion_message_function_tool_call import ChatCompletionMessageFunctionToolCall
 from config.settings import OPENAI_API_KEY
 from models.chat import Message
 from models.user import User
@@ -22,7 +23,8 @@ data_string = "\n".join(data_for_prompt)
 
 async def generate_completion(messages_array: List[dict], user: User) -> str:
 
-    messages = list(map(lambda x: f'<{x.get("username", "unknown")}> {x.get("content", "")}', messages_array))
+    messages = list(map(lambda x: f'<{x.get("username", "unknown")}> {x.get("message", "")}', messages_array))
+    messages.reverse()
 
     """Generate OpenAI chat completion"""
     if not OPENAI_API_KEY:
@@ -67,6 +69,8 @@ Estilo:
         if calls:
             logger.info(f"Tool calls: {calls}")
             for call in calls:
+                if not isinstance(call, ChatCompletionMessageFunctionToolCall):
+                    continue
                 if call.function.name in tools:
                     tool_function = tools[call.function.name]
                     tool_args = json.loads(call.function.arguments)
@@ -86,7 +90,8 @@ Estilo:
 
 def admin_completion(prompt: str, messages_array: List[dict]) -> str:
     """Generate OpenAI admin completion"""
-    messages = list(map(lambda x: f'<{x.get("username", "unknown")}> {x.get("content", "")}', messages_array))
+    messages = list(map(lambda x: f'<{x.get("username", "unknown")}> {x.get("message", "")}', messages_array))
+    messages.reverse()
     if not OPENAI_API_KEY:
         logger.error("Error: OpenAI API key is not configured.")
         raise HTTPException(status_code=500, detail="OpenAI API key not configured on server.")
@@ -95,21 +100,11 @@ def admin_completion(prompt: str, messages_array: List[dict]) -> str:
         completion = openai_client.chat.completions.create(
             model="gpt-4o-mini",
             messages=[
-                {"role": "system", "content": """
-                 
-Eres IAKlein, el asistente oficial del bar Klein 🍻.
-Hablas en estilo de chat corto (como en mensajería o IRC), usando emojis y mucho carisma.
-Tu rol:
-- Responder preguntas sobre el menú del bar Klein con la info de {data_string}.
-- Hacer bromas y charlar, pero siempre llevás la conversación de vuelta al bar Klein.
-- Dar recomendaciones de comidas y tragos.
-
-- El siguiente mensaje es del Biergarten Klein y debes entregarselo a los usuarios usando tu carisma.
-- Usa los mensajes anteriores como contexto para entrar de forma sutil a la conversación.
-- Mantené tu personalidad carismática y siempre enfocate en el bar Klein.
-
+                {"role": "system", "content": f"""
+IAKlein es el asistente oficial del bar Klein 🍻 y siempre se comunica en un estilo de chat corto, tipo mensajería o IRC, con emojis y mucho carisma. Su única función es entregar avisos oficiales del Biergarten Klein y no debe inventar ni agregar información extra. Los mensajes siempre deben integrarse a la conversación en curso, usando únicamente el chat como contexto para sonar naturales y sorpresivos. Cada aviso debe anunciarse con frases coloquiales como: “desde arriba me cuentan que…”, “me dicen que les pase el dato…”, “me informan que…”, o “me están pidiendo que diga que…” 😉. El tono tiene que ser siempre claro, positivo y cercano, asegurándose de transmitir toda la información sin omitir nada.
 
-                 """},
+escribe el mensaje usando los ultimos mensajes de la siguiente lista
+mensajes: {messages}"""},
                 {"role": "user", "content": prompt}
             ],
             temperature=0.7,