Programació de les funcionalitats bàsiques en el BackEnd i FrontEnd

Per la creació del FrontEnd hem utilitzat un codi d’HTML amb l’ajuda de la IA Gemini.

Justificació:

L’arquitectura d’aquest projecte s’ha dissenyat seguint un model de tres capes clarament diferenciades per garantir la modularitat, l’escalabilitat i la independència de la interfície respecte a la lògica de negoci. En primer lloc, disposem del FrontEnd (Widget), desenvolupat en HTML, CSS i JavaScript, encarregat exclusivament de la interacció amb l’usuari i la presentació visual. En segon lloc, trobem el BackEnd (Flask en Python), que actua com a controlador centralitzat, gestionant les peticions HTTP de tipus POST i processant la informació. Finalment, s’integra una capa de persistència de dades estructurada mitjançant un fitxer JSON, que funciona com a base de dades local i lleugera.

La gestió d’errors en aquest ecosistema és un pilar crític per garantir la robustesa del codi. El BackEnd implementa blocs de control d’excepcions (try-except) per assegurar que, davant de qualsevol fallada de lectura del fitxer o entrada inesperada de l’usuari, el servidor no es col·lapsi i pugui retornar una resposta controlada de tipus “500 Internal Server Error” en format JSON. Això evita que l’aplicació mostri errors interns tous a l’usuari final, mantenint una experiència de navegació fluida i segura.

!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chatbot LAN Party</title>
    <style>
        :root { 
            --accent-color: #ffffff; 
            --dark-bg: #1a1a1a; 
            --text-dark: #000000;
        }

        /* Botó flotant */
        #chat-launcher {
            position: fixed;
            bottom: 20px;
            right: 20px;
            width: 60px;
            height: 60px;
            background: var(--accent-color);
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            box-shadow: 0 4px 15px rgba(0,0,0,0.3);
            z-index: 1001;
            font-size: 30px;
            transition: transform 0.3s ease;
        }

        #chat-launcher:hover { transform: scale(1.1); }

        /* Contenidor del xat */
        #chat-container { 
            position: fixed; 
            bottom: 90px; 
            right: 20px; 
            width: 350px; 
            height: 500px; 
            background: var(--dark-bg); 
            border-radius: 15px; 
            box-shadow: 0 5px 25px rgba(0,0,0,0.5); 
            display: none; 
            flex-direction: column; 
            font-family: sans-serif; 
            border: 1px solid #333; 
            overflow: hidden; 
            z-index: 1000; 
        }

        #chat-header { 
            background: var(--accent-color); 
            color: var(--text-dark); 
            padding: 15px; 
            font-weight: bold; 
            text-align: center; 
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        #close-chat { cursor: pointer; font-size: 20px; padding: 0 5px; }

        #chat-box { flex: 1; padding: 15px; overflow-y: auto; color: white; display: flex; flex-direction: column; gap: 10px; }
        
        .message { max-width: 80%; padding: 10px; border-radius: 10px; font-size: 14px; line-height: 1.4; word-wrap: break-word; }
        .bot-msg { background: #333; align-self: flex-start; border-bottom-left-radius: 2px; }
        .user-msg { background: var(--accent-color); color: var(--text-dark); align-self: flex-end; border-bottom-right-radius: 2px; }

        #chat-input-area { display: flex; padding: 10px; background: #222; }
        #user-input { flex: 1; padding: 10px; border: none; border-radius: 5px; outline: none; }
        
        #send-btn { 
            background: var(--accent-color); 
            color: var(--text-dark);
            border: none; 
            padding: 10px 15px; 
            margin-left: 5px; 
            border-radius: 5px; 
            cursor: pointer; 
            font-weight: bold; 
        }

        /* Indicador de càrrega */
        .typing { font-style: italic; font-size: 12px; color: #888; margin-bottom: 10px; }
    </style>
</head>
<body>

<div id="chat-launcher" onclick="toggleChat()">🕹️</div>

<div id="chat-container">
    <div id="chat-header">
        <span>Assistent IA LAN Party</span>
        <span id="close-chat" onclick="toggleChat()">×</span>
    </div>
    <div id="chat-box">
        <div class="message bot-msg">Hola! Soc l'assistent intel·ligent. Pregunta'm el que vulguis sobre la LAN Party de Castellbisbal!</div>
    </div>
    <div id="chat-input-area">
        <input type="text" id="user-input" placeholder="Escriu aquí...">
        <button id="send-btn" onclick="handleSend()">></button>
    </div>
</div>

<script>
    // --- IMPORTANT: Aquí poses l'URL que et doni Ngrok al Colab ---
    const NGROK_URL = "https://nonexternally-squarable-suk.ngrok-free.dev"; 

    const chatContainer = document.getElementById('chat-container');
    const chatBox = document.getElementById('chat-box');
    const userInput = document.getElementById('user-input');

    function toggleChat() {
        chatContainer.style.display = (chatContainer.style.display === 'flex') ? 'none' : 'flex';
    }

    function addMessage(text, sender) {
        const msgDiv = document.createElement('div');
        msgDiv.classList.add('message', sender === 'user' ? 'user-msg' : 'bot-msg');
        msgDiv.innerText = text;
        chatBox.appendChild(msgDiv);
        chatBox.scrollTop = chatBox.scrollHeight;
    }

    async function handleSend() {
        const text = userInput.value.trim();
        if (!text) return;

        // Afegim el missatge de l'usuari a la pantalla
        addMessage(text, 'user');
        userInput.value = "";

        // Mostrem un petit indicador de que la IA està pensant
        const loadingDiv = document.createElement('div');
        loadingDiv.className = 'typing';
        loadingDiv.innerText = "L'assistent està escrivint...";
        chatBox.appendChild(loadingDiv);
        chatBox.scrollTop = chatBox.scrollHeight;

        try {
            // Cridem al Backend de Google Colab
            const response = await fetch(`${NGROK_URL}/chat`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ message: text })
            });

            const data = await response.json();
            
            // Eliminem l'indicador de càrrega i posem la resposta de Gemini
            chatBox.removeChild(loadingDiv);
            addMessage(data.reply, 'bot');

        } catch (error) {
            chatBox.removeChild(loadingDiv);
            addMessage("Error de connexió. Revisa si el servidor Colab està actiu.", 'bot');
            console.error("Error:", error);
        }
    }

    userInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') handleSend(); });
</script>

</body>
</html>

Gestió de Dades Estructurades (JSON)

Per a la persistència de les dades del nostre xatbot, s’ha creat un fitxer estructurat en format JSON anomenat `respostes.json`. Aquest format ens permet emmagatzemar la informació en parelles de clau-valor (paraula clau introduïda per l’usuari i la resposta automatitzada del xatbot). És una solució òptima i lleugera que actua com a base de dades local, permetent que el BackEnd la consulti en temps real de manera eficient.

Contingut del fitxer JSON:

[
    {
        "id": 1,
        "pregunta": "¿Cómo me conecto a la red?",
        "respuesta": "Configura tu ordenador en modo DHCP (IP automática)."
    },
    {
        "id": 2,
        "pregunta": "¿Necesito traer cable de red?",
        "respuesta": "Sí, trae un cable Ethernet de al menos 5 metros."
    },
    {
        "id": 3,
        "pregunta": "¿Debo traer mis propios periféricos?",
        "respuesta": "Sí, trae tu ratón, teclado y auriculares."
    },
    {
        "id": 4,
        "pregunta": "¿Qué cables debo traer para la corriente?",
        "respuesta": "Debes traer una regleta de al menos 3 tomas para conectar tu torre, monitor y periféricos."
    },
    {
        "id": 5,
        "pregunta": "¿Puedo traer una nevera pequeña o ventilador?",
        "respuesta": "No. Solo se permite conectar el ordenador y el monitor para evitar sobrecargas eléctricas."
    },
    {
        "id": 6,
        "pregunta": "¿Dónde puedo dejar mis objetos de valor?",
        "respuesta": "Te recomendamos no dejarlos nunca solos. La organización no se hace responsable de pérdidas."
    },
    {
        "id": 7,
        "pregunta": "¿Tengo que actualizar los juegos antes de venir?",
        "respuesta": "Sí. Por favor, descarga todas las actualizaciones en casa para no saturar la red del evento."
    },
    {
        "id": 8,
        "pregunta": "¿Se puede comer en la mesa del ordenador?",
        "respuesta": "No. Por higiene y seguridad del equipo, utiliza exclusivamente las zonas de restauración habilitadas."
    },
    {
        "id": 9,
        "pregunta": "¿A qué hora se apagan las luces?",
        "respuesta": "El recinto está abierto 24h, pero se reducirá la iluminación de 00:00 a 08:00 para facilitar el descanso."
    },
    {
        "id": 10,
        "pregunta": "¿Puedo usar Wi-Fi para jugar?",
        "respuesta": "No se recomienda. El Wi-Fi es solo para móviles; para jugar debes usar cable para evitar lag."
    }
]

Arxiu el qual demana el codi més endavant en aquesta part:

    try:
        user_input = request.json.get("message")
        response = chat.send_message(user_input)
        return jsonify({"reply": response.text})
    except Exception as e:
        return jsonify({"reply": f"Error: {str(e)}"}), 500

L’Endpoint de Flask i el retorn de dades (Python)

Això es troba al teu fitxer de Colab. És la part que “escolta” les preguntes i “retorna” les respostes.

El BackEnd s’encarrega de rebre les peticions de la interfície mitjançant un endpoint de tipus POST configurat a la ruta `/api/chat`. Quan es rep un missatge, el codi executa una funció que obre de manera segura el fitxer `respostes.json`, processa el text en minúscules per evitar errors de coincidència i cerca la paraula clau. Finalment, retorna un objecte JSON utilitzant la funció `jsonify()` amb un codi d’estat HTTP 200 (Èxit) o un codi 400/500 en cas que es produeixi una excepció o error intern.

import json
from flask import Flask, request, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # Evita problemes de CORS amb el FrontEnd

def obtenir_resposta_json(missatge_usuari):
    try:
        # Obrim el fitxer faqs.json que tenim al repositori de GitHub
        with open('faqs.json', 'r', encoding='utf-8') as f:
            faqs = json.load(f)
        
        msg = missatge_usuari.lower()
        id_buscada = None
        
        # Lògica intel·ligent de mapatge de paraules clau de la LAN Party
        if "conect" in msg or "red" in msg or "xarxa" in msg:
            id_buscada = 1
        elif "cable" in msg:
            id_buscada = 2
        elif "periferic" in msg or "raton" in msg or "teclado" in msg or "cascos" in msg:
            id_buscada = 3
        elif "corriente" in msg or "enchufe" in msg or "regleta" in msg or "electricitat" in msg:
            id_buscada = 4
        elif "nevera" in msg or "ventilador" in msg:
            id_buscada = 5
        elif "valor" in msg or "segur" in msg or "robar" in msg or "perdre" in msg:
            id_buscada = 6
            
        # Busquem la resposta corresponent a la ID trobada dins del JSON array
        if id_buscada is not None:
            for faq in faqs:
                if faq["id"] == id_buscada:
                    return faq["respuesta"]
                    
        return "Ho sento, no tinc informació exacta sobre aquesta consulta de la LAN Party. Prova preguntant per 'red', 'cable', 'perifèrics' o 'corrent'."
            
    except FileNotFoundError:
        return "Error intern: No s'ha trobat el fitxer faqs.json al servidor."
    except json.JSONDecodeError:
        return "Error intern: El fitxer JSON de la LAN Party té un format erroni."
    except Exception as e:
        return f"Error inesperat: {str(e)}"

@app.route('/api/chat', methods=['POST'])
def chat_endpoint():
    try:
        dades = request.get_json()
        if not dades or 'message' not in dades:
            return jsonify({'reply': 'Error: Petició buida o incorrecta.'}), 400
            
        missatge_usuari = dades['message']
        
        # Truquem a la funció que llegeix el fitxer de dades JSON de GitHub
        resposta_final = obtenir_resposta_json(missatge_usuari)
        
        return jsonify({'reply': resposta_final}), 200
        
    except Exception as e:
        return jsonify({'reply': 'S’ha produït un error crític processant la petició.'}), 500

if __name__ == '__main__':
    app.run(port=5000, debug=True)

El Túnel de ngrok (Python)

Això es troba al final del teu codi de Colab. És el que permet que la teva web de Google Sites (que està a internet) pugui “parlar” amb el teu ordinador (que és privat).

# --- NGROK ---
ngrok.kill()
ngrok.set_auth_token(NGROK_TOKEN)
public_url = ngrok.connect(5000).public_url
print(f"\n🚀 BACKEND ACTIU!")
print(f"🔗 URL per al teu HTML: {public_url}")

app.run(port=5000)

Documentació a github: