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: