Spaces:
Sleeping
Sleeping
Fachada DAO, Preparacion Render
Browse files- Dockerfile +33 -0
- backend/aplicacion.py +29 -37
- backend/dao/fachada_dao.py +32 -0
- backend/main.py +6 -2
- backend/services/pipeline.py +8 -14
- backend/services/recomendacion.py +3 -3
- chatbot/.env.production +1 -0
- chatbot/src/components/AppHero.vue +3 -2
- chatbot/src/components/MessageFeed.vue +2 -1
- chatbot/src/config.js +7 -0
- chatbot/src/views/AuthView.vue +6 -5
- chatbot/src/views/ChatView.vue +7 -6
- chatbot/src/views/PreferencesView.vue +2 -1
- chatbot/vite.config.js +8 -0
- docs/singleton_bd.md +0 -2
- render.yaml +26 -0
Dockerfile
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Backend - Dockerfile
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Instalar dependencias del sistema
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
gcc \
|
| 9 |
+
g++ \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# Copiar requirements
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
# Copiar el código del backend
|
| 17 |
+
COPY backend/ ./backend/
|
| 18 |
+
COPY data/ ./data/
|
| 19 |
+
|
| 20 |
+
# Crear directorio para la base de datos
|
| 21 |
+
RUN mkdir -p /app/backend
|
| 22 |
+
|
| 23 |
+
WORKDIR /app/backend
|
| 24 |
+
|
| 25 |
+
# Exponer puerto
|
| 26 |
+
EXPOSE 5000
|
| 27 |
+
|
| 28 |
+
# Variables de entorno por defecto
|
| 29 |
+
ENV FLASK_APP=aplicacion.py
|
| 30 |
+
ENV PYTHONUNBUFFERED=1
|
| 31 |
+
|
| 32 |
+
# Comando para iniciar la app
|
| 33 |
+
CMD ["python", "main.py"]
|
backend/aplicacion.py
CHANGED
|
@@ -7,11 +7,7 @@ from flask import Flask, jsonify, request
|
|
| 7 |
from flask_cors import CORS
|
| 8 |
|
| 9 |
from config import OMDB_API_KEY
|
| 10 |
-
from dao.
|
| 11 |
-
from dao.emocion_dao import EmocionDAO
|
| 12 |
-
from dao.historial_dao import HistorialDAO
|
| 13 |
-
from dao.pelicula_dao import PeliculaDAO
|
| 14 |
-
from dao.usuario_dao import UsuarioDao
|
| 15 |
from base_datos import iniciar_historial_usuario
|
| 16 |
from vo import PeliculaVistaVO
|
| 17 |
from services.pipeline import AnalysisService
|
|
@@ -44,11 +40,7 @@ print(
|
|
| 44 |
|
| 45 |
_analysis_service = AnalysisService(_modelo, _movies_df, _media_rating_global)
|
| 46 |
|
| 47 |
-
|
| 48 |
-
_emocion_dao = EmocionDAO()
|
| 49 |
-
_ciclo_dao = CicloDAO()
|
| 50 |
-
_historial_dao = HistorialDAO()
|
| 51 |
-
_pelicula_dao = PeliculaDAO()
|
| 52 |
|
| 53 |
|
| 54 |
# ------------------------------------------------------------------
|
|
@@ -66,7 +58,7 @@ def register():
|
|
| 66 |
return jsonify({"error": "El usuario debe tener al menos 3 caracteres"}), 400
|
| 67 |
if len(password) < 6:
|
| 68 |
return jsonify({"error": "La contraseña debe tener al menos 6 caracteres"}), 400
|
| 69 |
-
usuario =
|
| 70 |
if not usuario:
|
| 71 |
return jsonify({"error": "El nombre de usuario ya existe"}), 409
|
| 72 |
return jsonify({"user_id": usuario.id, "username": usuario.username, "token": usuario.token}), 201
|
|
@@ -78,7 +70,7 @@ def login():
|
|
| 78 |
password = str(payload.get("password", "")).strip()
|
| 79 |
if not username or not password:
|
| 80 |
return jsonify({"error": "username y password son obligatorios"}), 400
|
| 81 |
-
usuario =
|
| 82 |
if not usuario:
|
| 83 |
return jsonify({"error": "Credenciales incorrectas"}), 401
|
| 84 |
return jsonify({"user_id": usuario.id, "username": usuario.username, "token": usuario.token})
|
|
@@ -87,7 +79,7 @@ def login():
|
|
| 87 |
def logout():
|
| 88 |
payload = request.json or {}
|
| 89 |
token = str(payload.get("token", "")).strip()
|
| 90 |
-
|
| 91 |
return jsonify({"ok": True})
|
| 92 |
|
| 93 |
@app.route("/auth/password", methods=["POST"])
|
|
@@ -100,8 +92,8 @@ def change_password():
|
|
| 100 |
return jsonify({"error": "token, old_password y new_password son obligatorios"}), 400
|
| 101 |
if len(new_password) < 6:
|
| 102 |
return jsonify({"error": "La nueva contraseña debe tener al menos 6 caracteres"}), 400
|
| 103 |
-
usuario =
|
| 104 |
-
if not usuario or not
|
| 105 |
return jsonify({"error": "Contraseña actual incorrecta o sesión inválida"}), 401
|
| 106 |
return jsonify({"ok": True})
|
| 107 |
|
|
@@ -111,7 +103,7 @@ def verify_token():
|
|
| 111 |
token = str(payload.get("token", "")).strip()
|
| 112 |
if not token:
|
| 113 |
return jsonify({"valid": False}), 400
|
| 114 |
-
usuario =
|
| 115 |
if not usuario:
|
| 116 |
return jsonify({"valid": False}), 401
|
| 117 |
return jsonify({"valid": True, "user_id": usuario.id, "username": usuario.username})
|
|
@@ -122,13 +114,13 @@ def delete_account():
|
|
| 122 |
token = str(payload.get("token", "")).strip()
|
| 123 |
if not token:
|
| 124 |
return jsonify({"error": "token es obligatorio"}), 400
|
| 125 |
-
usuario =
|
| 126 |
if not usuario:
|
| 127 |
return jsonify({"error": "Token inválido o cuenta no encontrada"}), 401
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
return jsonify({"ok": True, "deleted_user": usuario.username})
|
| 133 |
|
| 134 |
|
|
@@ -166,16 +158,16 @@ def seguimiento_recomendacion():
|
|
| 166 |
if not user_id or not cycle_id or not texto_posterior:
|
| 167 |
return jsonify({"error": "user_id, ciclo_recomendacion_id y texto_post son obligatorios"}), 400
|
| 168 |
|
| 169 |
-
ciclo =
|
| 170 |
if not ciclo:
|
| 171 |
return jsonify({"error": "ciclo de recomendacion no encontrado"}), 404
|
| 172 |
|
| 173 |
-
emocion_pre =
|
| 174 |
|
| 175 |
momento_analisis = datetime.now(timezone.utc).isoformat()
|
| 176 |
result_post, emocion_posterior, valencia_posterior = analizar_texto(_modelo, texto_posterior)
|
| 177 |
|
| 178 |
-
emocion_post_obj =
|
| 179 |
user_id=user_id,
|
| 180 |
texto=texto_posterior,
|
| 181 |
emocion=emocion_posterior,
|
|
@@ -184,9 +176,9 @@ def seguimiento_recomendacion():
|
|
| 184 |
)
|
| 185 |
|
| 186 |
if id_pelicula:
|
| 187 |
-
|
| 188 |
if emocion_post_obj:
|
| 189 |
-
|
| 190 |
ciclo_id=cycle_id,
|
| 191 |
user_id=user_id,
|
| 192 |
pelicula_id=id_pelicula,
|
|
@@ -238,12 +230,12 @@ def guardar_visto():
|
|
| 238 |
if rating_usuario < 1 or rating_usuario > 5:
|
| 239 |
return jsonify({"error": "rating_usuario debe estar entre 1 y 5"}), 400
|
| 240 |
|
| 241 |
-
|
| 242 |
|
| 243 |
emocion_id = None
|
| 244 |
if emocion_str:
|
| 245 |
valencia = "positiva" if emocion_str in ("alegria", "sorpresa") else "negativa"
|
| 246 |
-
emocion_obj =
|
| 247 |
user_id=user_id,
|
| 248 |
texto=texto,
|
| 249 |
emocion=emocion_str,
|
|
@@ -252,7 +244,7 @@ def guardar_visto():
|
|
| 252 |
)
|
| 253 |
emocion_id = emocion_obj.id if emocion_obj else None
|
| 254 |
|
| 255 |
-
entrada =
|
| 256 |
user_id=user_id,
|
| 257 |
pelicula_id=id_pelicula,
|
| 258 |
emocion_id=emocion_id,
|
|
@@ -280,7 +272,7 @@ def obtener_historial():
|
|
| 280 |
user_id = str(payload.get("user_id", "") or request.args.get("user_id", "")).strip()
|
| 281 |
if not user_id:
|
| 282 |
return jsonify({"error": "user_id es obligatorio"}), 400
|
| 283 |
-
deleted =
|
| 284 |
return jsonify({"ok": True, "user_id": user_id, "deleted": deleted})
|
| 285 |
|
| 286 |
user_id = str(request.args.get("user_id", "")).strip()
|
|
@@ -293,7 +285,7 @@ def obtener_historial():
|
|
| 293 |
limit = 30
|
| 294 |
limit = max(1, min(limit, 200))
|
| 295 |
|
| 296 |
-
vistas =
|
| 297 |
return jsonify({"items": [dataclasses.asdict(vo) for vo in vistas], "count": len(vistas)})
|
| 298 |
|
| 299 |
|
|
@@ -313,13 +305,13 @@ def obtener_transiciones():
|
|
| 313 |
limit = 20
|
| 314 |
limit = max(1, min(limit, 100))
|
| 315 |
|
| 316 |
-
emociones =
|
| 317 |
-
entradas_h =
|
| 318 |
|
| 319 |
peliculas_map: dict[str, str] = {}
|
| 320 |
for h in entradas_h:
|
| 321 |
if h.pelicula_id not in peliculas_map:
|
| 322 |
-
peli =
|
| 323 |
peliculas_map[h.pelicula_id] = peli.titulo if peli else ""
|
| 324 |
|
| 325 |
emociones_asc = sorted(emociones, key=lambda e: e.analizado_en)
|
|
@@ -428,7 +420,7 @@ def onboarding_historial():
|
|
| 428 |
|
| 429 |
if not user_id or not token:
|
| 430 |
return jsonify({"error": "user_id y token son obligatorios"}), 400
|
| 431 |
-
if not
|
| 432 |
return jsonify({"error": "Token inválido"}), 401
|
| 433 |
if not isinstance(peliculas, list):
|
| 434 |
return jsonify({"error": "peliculas debe ser una lista"}), 400
|
|
@@ -451,8 +443,8 @@ def onboarding_historial():
|
|
| 451 |
valoracion = v if 1.0 <= v <= 5.0 else None
|
| 452 |
except (TypeError, ValueError):
|
| 453 |
pass
|
| 454 |
-
|
| 455 |
-
entrada =
|
| 456 |
user_id=user_id,
|
| 457 |
pelicula_id=movie_id,
|
| 458 |
emocion_id=None,
|
|
|
|
| 7 |
from flask_cors import CORS
|
| 8 |
|
| 9 |
from config import OMDB_API_KEY
|
| 10 |
+
from dao.dao_facade import DAOFacade
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
from base_datos import iniciar_historial_usuario
|
| 12 |
from vo import PeliculaVistaVO
|
| 13 |
from services.pipeline import AnalysisService
|
|
|
|
| 40 |
|
| 41 |
_analysis_service = AnalysisService(_modelo, _movies_df, _media_rating_global)
|
| 42 |
|
| 43 |
+
_dao = DAOFacade.obtener_instancia()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
|
| 46 |
# ------------------------------------------------------------------
|
|
|
|
| 58 |
return jsonify({"error": "El usuario debe tener al menos 3 caracteres"}), 400
|
| 59 |
if len(password) < 6:
|
| 60 |
return jsonify({"error": "La contraseña debe tener al menos 6 caracteres"}), 400
|
| 61 |
+
usuario = _dao.usuario.registrar(username, password)
|
| 62 |
if not usuario:
|
| 63 |
return jsonify({"error": "El nombre de usuario ya existe"}), 409
|
| 64 |
return jsonify({"user_id": usuario.id, "username": usuario.username, "token": usuario.token}), 201
|
|
|
|
| 70 |
password = str(payload.get("password", "")).strip()
|
| 71 |
if not username or not password:
|
| 72 |
return jsonify({"error": "username y password son obligatorios"}), 400
|
| 73 |
+
usuario = _dao.usuario.login(username, password)
|
| 74 |
if not usuario:
|
| 75 |
return jsonify({"error": "Credenciales incorrectas"}), 401
|
| 76 |
return jsonify({"user_id": usuario.id, "username": usuario.username, "token": usuario.token})
|
|
|
|
| 79 |
def logout():
|
| 80 |
payload = request.json or {}
|
| 81 |
token = str(payload.get("token", "")).strip()
|
| 82 |
+
_dao.usuario.cerrar_sesion(token)
|
| 83 |
return jsonify({"ok": True})
|
| 84 |
|
| 85 |
@app.route("/auth/password", methods=["POST"])
|
|
|
|
| 92 |
return jsonify({"error": "token, old_password y new_password son obligatorios"}), 400
|
| 93 |
if len(new_password) < 6:
|
| 94 |
return jsonify({"error": "La nueva contraseña debe tener al menos 6 caracteres"}), 400
|
| 95 |
+
usuario = _dao.usuario.obtener_por_token(token)
|
| 96 |
+
if not usuario or not _dao.usuario.actualizar_contraseña(usuario.id, new_password):
|
| 97 |
return jsonify({"error": "Contraseña actual incorrecta o sesión inválida"}), 401
|
| 98 |
return jsonify({"ok": True})
|
| 99 |
|
|
|
|
| 103 |
token = str(payload.get("token", "")).strip()
|
| 104 |
if not token:
|
| 105 |
return jsonify({"valid": False}), 400
|
| 106 |
+
usuario = _dao.usuario.obtener_por_token(token)
|
| 107 |
if not usuario:
|
| 108 |
return jsonify({"valid": False}), 401
|
| 109 |
return jsonify({"valid": True, "user_id": usuario.id, "username": usuario.username})
|
|
|
|
| 114 |
token = str(payload.get("token", "")).strip()
|
| 115 |
if not token:
|
| 116 |
return jsonify({"error": "token es obligatorio"}), 400
|
| 117 |
+
usuario = _dao.usuario.obtener_por_token(token)
|
| 118 |
if not usuario:
|
| 119 |
return jsonify({"error": "Token inválido o cuenta no encontrada"}), 401
|
| 120 |
+
_dao.historial.borrar_por_usuario(usuario.id)
|
| 121 |
+
_dao.emocion.borrar_por_usuario(usuario.id)
|
| 122 |
+
_dao.ciclo.borrar_por_usuario(usuario.id)
|
| 123 |
+
_dao.usuario.eliminar(usuario.id)
|
| 124 |
return jsonify({"ok": True, "deleted_user": usuario.username})
|
| 125 |
|
| 126 |
|
|
|
|
| 158 |
if not user_id or not cycle_id or not texto_posterior:
|
| 159 |
return jsonify({"error": "user_id, ciclo_recomendacion_id y texto_post son obligatorios"}), 400
|
| 160 |
|
| 161 |
+
ciclo = _dao.ciclo.obtener_por_id(cycle_id, user_id)
|
| 162 |
if not ciclo:
|
| 163 |
return jsonify({"error": "ciclo de recomendacion no encontrado"}), 404
|
| 164 |
|
| 165 |
+
emocion_pre = _dao.emocion.obtener_por_id(ciclo.emocion_pre_id)
|
| 166 |
|
| 167 |
momento_analisis = datetime.now(timezone.utc).isoformat()
|
| 168 |
result_post, emocion_posterior, valencia_posterior = analizar_texto(_modelo, texto_posterior)
|
| 169 |
|
| 170 |
+
emocion_post_obj = _dao.emocion.añadir(
|
| 171 |
user_id=user_id,
|
| 172 |
texto=texto_posterior,
|
| 173 |
emocion=emocion_posterior,
|
|
|
|
| 176 |
)
|
| 177 |
|
| 178 |
if id_pelicula:
|
| 179 |
+
_dao.pelicula.guardar_si_no_existe(id_pelicula, titulo_pelicula, genero=_genero_pelicula(id_pelicula))
|
| 180 |
if emocion_post_obj:
|
| 181 |
+
_dao.ciclo.cerrar_ciclo(
|
| 182 |
ciclo_id=cycle_id,
|
| 183 |
user_id=user_id,
|
| 184 |
pelicula_id=id_pelicula,
|
|
|
|
| 230 |
if rating_usuario < 1 or rating_usuario > 5:
|
| 231 |
return jsonify({"error": "rating_usuario debe estar entre 1 y 5"}), 400
|
| 232 |
|
| 233 |
+
_dao.pelicula.guardar_si_no_existe(id_pelicula, titulo_pelicula, genero=_genero_pelicula(id_pelicula))
|
| 234 |
|
| 235 |
emocion_id = None
|
| 236 |
if emocion_str:
|
| 237 |
valencia = "positiva" if emocion_str in ("alegria", "sorpresa") else "negativa"
|
| 238 |
+
emocion_obj = _dao.emocion.añadir(
|
| 239 |
user_id=user_id,
|
| 240 |
texto=texto,
|
| 241 |
emocion=emocion_str,
|
|
|
|
| 244 |
)
|
| 245 |
emocion_id = emocion_obj.id if emocion_obj else None
|
| 246 |
|
| 247 |
+
entrada = _dao.historial.añadir_pelicula(
|
| 248 |
user_id=user_id,
|
| 249 |
pelicula_id=id_pelicula,
|
| 250 |
emocion_id=emocion_id,
|
|
|
|
| 272 |
user_id = str(payload.get("user_id", "") or request.args.get("user_id", "")).strip()
|
| 273 |
if not user_id:
|
| 274 |
return jsonify({"error": "user_id es obligatorio"}), 400
|
| 275 |
+
deleted = _dao.historial.borrar_por_usuario(user_id)
|
| 276 |
return jsonify({"ok": True, "user_id": user_id, "deleted": deleted})
|
| 277 |
|
| 278 |
user_id = str(request.args.get("user_id", "")).strip()
|
|
|
|
| 285 |
limit = 30
|
| 286 |
limit = max(1, min(limit, 200))
|
| 287 |
|
| 288 |
+
vistas = _dao.historial.obtener_vistas_por_usuario(user_id=user_id, limit=limit)
|
| 289 |
return jsonify({"items": [dataclasses.asdict(vo) for vo in vistas], "count": len(vistas)})
|
| 290 |
|
| 291 |
|
|
|
|
| 305 |
limit = 20
|
| 306 |
limit = max(1, min(limit, 100))
|
| 307 |
|
| 308 |
+
emociones = _dao.emocion.obtener_por_usuario(user_id, limit=500)
|
| 309 |
+
entradas_h = _dao.historial.obtener_por_usuario(user_id, limit=1000)
|
| 310 |
|
| 311 |
peliculas_map: dict[str, str] = {}
|
| 312 |
for h in entradas_h:
|
| 313 |
if h.pelicula_id not in peliculas_map:
|
| 314 |
+
peli = _dao.pelicula.obtener_por_id(h.pelicula_id)
|
| 315 |
peliculas_map[h.pelicula_id] = peli.titulo if peli else ""
|
| 316 |
|
| 317 |
emociones_asc = sorted(emociones, key=lambda e: e.analizado_en)
|
|
|
|
| 420 |
|
| 421 |
if not user_id or not token:
|
| 422 |
return jsonify({"error": "user_id y token son obligatorios"}), 400
|
| 423 |
+
if not _dao.usuario.obtener_por_token(token):
|
| 424 |
return jsonify({"error": "Token inválido"}), 401
|
| 425 |
if not isinstance(peliculas, list):
|
| 426 |
return jsonify({"error": "peliculas debe ser una lista"}), 400
|
|
|
|
| 443 |
valoracion = v if 1.0 <= v <= 5.0 else None
|
| 444 |
except (TypeError, ValueError):
|
| 445 |
pass
|
| 446 |
+
_dao.pelicula.guardar_si_no_existe(movie_id, titulo, genero=genero)
|
| 447 |
+
entrada = _dao.historial.añadir_pelicula(
|
| 448 |
user_id=user_id,
|
| 449 |
pelicula_id=movie_id,
|
| 450 |
emocion_id=None,
|
backend/dao/fachada_dao.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from usuario_dao import UsuarioDao
|
| 2 |
+
from pelicula_dao import PeliculaDAO
|
| 3 |
+
from emocion_dao import EmocionDAO
|
| 4 |
+
from ciclo_dao import CicloDAO
|
| 5 |
+
from historial_dao import HistorialDAO
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class DAOFacade:
|
| 9 |
+
"""Fachada centralizada para acceso a todos los DAO."""
|
| 10 |
+
|
| 11 |
+
_instance = None
|
| 12 |
+
|
| 13 |
+
def __new__(cls):
|
| 14 |
+
if cls._instance is None:
|
| 15 |
+
cls._instance = super().__new__(cls)
|
| 16 |
+
cls._instance._initialized = False
|
| 17 |
+
return cls._instance
|
| 18 |
+
|
| 19 |
+
def __init__(self):
|
| 20 |
+
if self._initialized:
|
| 21 |
+
return
|
| 22 |
+
self.usuario = UsuarioDao()
|
| 23 |
+
self.pelicula = PeliculaDAO()
|
| 24 |
+
self.emocion = EmocionDAO()
|
| 25 |
+
self.ciclo = CicloDAO()
|
| 26 |
+
self.historial = HistorialDAO()
|
| 27 |
+
self._initialized = True
|
| 28 |
+
|
| 29 |
+
@staticmethod
|
| 30 |
+
def obtener_instancia():
|
| 31 |
+
"""Obtiene la instancia única de la fachada."""
|
| 32 |
+
return DAOFacade()
|
backend/main.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
| 1 |
"""
|
| 2 |
Este archivo es el punto de entrada del servidor.
|
| 3 |
-
Importa la app Flask singleton y la ejecuta en el puerto 5000.
|
| 4 |
"""
|
| 5 |
|
|
|
|
| 6 |
from aplicacion import app
|
| 7 |
|
| 8 |
if __name__ == "__main__":
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
Este archivo es el punto de entrada del servidor.
|
| 3 |
+
Importa la app Flask singleton y la ejecuta en el puerto indicado (por defecto 5000).
|
| 4 |
"""
|
| 5 |
|
| 6 |
+
import os
|
| 7 |
from aplicacion import app
|
| 8 |
|
| 9 |
if __name__ == "__main__":
|
| 10 |
+
port = int(os.getenv("PORT", 5000))
|
| 11 |
+
host = os.getenv("HOST", "0.0.0.0")
|
| 12 |
+
debug = os.getenv("FLASK_ENV", "production") == "development"
|
| 13 |
+
app.run(host=host, port=port, debug=debug)
|
backend/services/pipeline.py
CHANGED
|
@@ -6,10 +6,7 @@ Desacopla la logica de negocio de las rutas Flask.
|
|
| 6 |
from datetime import datetime, timezone
|
| 7 |
|
| 8 |
from config import POSITIVE_EMOTIONS
|
| 9 |
-
from dao.
|
| 10 |
-
from dao.emocion_dao import EmocionDAO
|
| 11 |
-
from dao.historial_dao import HistorialDAO
|
| 12 |
-
from dao.pelicula_dao import PeliculaDAO
|
| 13 |
from modelos_servicios import ContextoEmocional, ResultadoAnalisis
|
| 14 |
from vo import EmocionVO
|
| 15 |
from services.chatbot import generar_texto_chatbot
|
|
@@ -27,23 +24,20 @@ class AnalysisService:
|
|
| 27 |
self._modelo = modelo
|
| 28 |
self._movies_df = movies_df
|
| 29 |
self._media_global_ratings = media_global_ratings
|
| 30 |
-
self.
|
| 31 |
-
self._ciclo_dao = CicloDAO()
|
| 32 |
-
self._historial_dao = HistorialDAO()
|
| 33 |
-
self._pelicula_dao = PeliculaDAO()
|
| 34 |
|
| 35 |
def analizar(self, texto: str, user_id: str, estrategia: str) -> ResultadoAnalisis:
|
| 36 |
momento_analisis = datetime.now(timezone.utc).isoformat()
|
| 37 |
|
| 38 |
# Emoción previa antes de registrar la actual
|
| 39 |
-
emocion_previa_obj = self.
|
| 40 |
emocion_previa_vo = EmocionVO.desde(emocion_previa_obj) if emocion_previa_obj else None
|
| 41 |
|
| 42 |
resultado, emocion_dominante, valencia_dominante = analizar_texto(self._modelo, texto)
|
| 43 |
arousal_actual = calculo_arousal(resultado)
|
| 44 |
valencia_continua = calcular_valencia_continua(resultado)
|
| 45 |
|
| 46 |
-
historial_eventos = self.
|
| 47 |
historico_arousal = [
|
| 48 |
estimar_arousal_emocion_es(e.emocion)
|
| 49 |
for e in historial_eventos
|
|
@@ -70,7 +64,7 @@ class AnalysisService:
|
|
| 70 |
modo_recomendacion = "diferente" if emocion_dominante in POSITIVE_EMOTIONS else "similar"
|
| 71 |
|
| 72 |
# Registrar emoción actual
|
| 73 |
-
emocion_actual = self.
|
| 74 |
user_id=user_id,
|
| 75 |
texto=texto,
|
| 76 |
emocion=emocion_dominante,
|
|
@@ -81,7 +75,7 @@ class AnalysisService:
|
|
| 81 |
# Crear ciclo vinculado a la emoción recién registrada
|
| 82 |
cycle_id = None
|
| 83 |
if emocion_actual:
|
| 84 |
-
ciclo = self.
|
| 85 |
user_id=user_id,
|
| 86 |
emocion_pre_id=emocion_actual.id,
|
| 87 |
estrategia=modo_recomendacion,
|
|
@@ -92,13 +86,13 @@ class AnalysisService:
|
|
| 92 |
# Película vista entre la emoción previa y la actual
|
| 93 |
pelicula_transicion = None
|
| 94 |
if emocion_previa_vo:
|
| 95 |
-
hp = self.
|
| 96 |
user_id=user_id,
|
| 97 |
inicio=emocion_previa_vo.analizado_en,
|
| 98 |
fin=momento_analisis,
|
| 99 |
)
|
| 100 |
if hp:
|
| 101 |
-
peli = self.
|
| 102 |
if peli:
|
| 103 |
pelicula_transicion = {
|
| 104 |
"movie_id": peli.id,
|
|
|
|
| 6 |
from datetime import datetime, timezone
|
| 7 |
|
| 8 |
from config import POSITIVE_EMOTIONS
|
| 9 |
+
from dao.dao_facade import DAOFacade
|
|
|
|
|
|
|
|
|
|
| 10 |
from modelos_servicios import ContextoEmocional, ResultadoAnalisis
|
| 11 |
from vo import EmocionVO
|
| 12 |
from services.chatbot import generar_texto_chatbot
|
|
|
|
| 24 |
self._modelo = modelo
|
| 25 |
self._movies_df = movies_df
|
| 26 |
self._media_global_ratings = media_global_ratings
|
| 27 |
+
self._dao = DAOFacade.obtener_instancia()
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
def analizar(self, texto: str, user_id: str, estrategia: str) -> ResultadoAnalisis:
|
| 30 |
momento_analisis = datetime.now(timezone.utc).isoformat()
|
| 31 |
|
| 32 |
# Emoción previa antes de registrar la actual
|
| 33 |
+
emocion_previa_obj = self._dao.emocion.obtener_ultima(user_id)
|
| 34 |
emocion_previa_vo = EmocionVO.desde(emocion_previa_obj) if emocion_previa_obj else None
|
| 35 |
|
| 36 |
resultado, emocion_dominante, valencia_dominante = analizar_texto(self._modelo, texto)
|
| 37 |
arousal_actual = calculo_arousal(resultado)
|
| 38 |
valencia_continua = calcular_valencia_continua(resultado)
|
| 39 |
|
| 40 |
+
historial_eventos = self._dao.emocion.obtener_por_usuario(user_id=user_id, limit=200)
|
| 41 |
historico_arousal = [
|
| 42 |
estimar_arousal_emocion_es(e.emocion)
|
| 43 |
for e in historial_eventos
|
|
|
|
| 64 |
modo_recomendacion = "diferente" if emocion_dominante in POSITIVE_EMOTIONS else "similar"
|
| 65 |
|
| 66 |
# Registrar emoción actual
|
| 67 |
+
emocion_actual = self._dao.emocion.añadir(
|
| 68 |
user_id=user_id,
|
| 69 |
texto=texto,
|
| 70 |
emocion=emocion_dominante,
|
|
|
|
| 75 |
# Crear ciclo vinculado a la emoción recién registrada
|
| 76 |
cycle_id = None
|
| 77 |
if emocion_actual:
|
| 78 |
+
ciclo = self._dao.ciclo.crear(
|
| 79 |
user_id=user_id,
|
| 80 |
emocion_pre_id=emocion_actual.id,
|
| 81 |
estrategia=modo_recomendacion,
|
|
|
|
| 86 |
# Película vista entre la emoción previa y la actual
|
| 87 |
pelicula_transicion = None
|
| 88 |
if emocion_previa_vo:
|
| 89 |
+
hp = self._dao.historial.obtener_entre_fechas(
|
| 90 |
user_id=user_id,
|
| 91 |
inicio=emocion_previa_vo.analizado_en,
|
| 92 |
fin=momento_analisis,
|
| 93 |
)
|
| 94 |
if hp:
|
| 95 |
+
peli = self._dao.pelicula.obtener_por_id(hp[0].pelicula_id)
|
| 96 |
if peli:
|
| 97 |
pelicula_transicion = {
|
| 98 |
"movie_id": peli.id,
|
backend/services/recomendacion.py
CHANGED
|
@@ -4,16 +4,16 @@ Combina calidad global (suavizado bayesiano), similitud de generos y preferencia
|
|
| 4 |
Adapta la estrategia segun el estado emocional usando el patron Strategy con EstrategiaFactory.
|
| 5 |
"""
|
| 6 |
|
| 7 |
-
from dao.
|
| 8 |
from modelos_servicios import ContextoEmocional
|
| 9 |
from services.estrategias_recomendacion import EstrategiaFactory
|
| 10 |
from services.calculos import construir_perfil_usuario, recomendar_calidad_aleatoria
|
| 11 |
|
| 12 |
-
|
| 13 |
|
| 14 |
|
| 15 |
def obtener_historial_usuario(user_id: str, limit: int = 200) -> list[dict]:
|
| 16 |
-
entradas =
|
| 17 |
return [{"movie_id": h.pelicula_id, "user_rating": h.valoracion} for h in entradas]
|
| 18 |
|
| 19 |
def recomendar_peliculas(
|
|
|
|
| 4 |
Adapta la estrategia segun el estado emocional usando el patron Strategy con EstrategiaFactory.
|
| 5 |
"""
|
| 6 |
|
| 7 |
+
from dao.dao_facade import DAOFacade
|
| 8 |
from modelos_servicios import ContextoEmocional
|
| 9 |
from services.estrategias_recomendacion import EstrategiaFactory
|
| 10 |
from services.calculos import construir_perfil_usuario, recomendar_calidad_aleatoria
|
| 11 |
|
| 12 |
+
_dao = DAOFacade.obtener_instancia()
|
| 13 |
|
| 14 |
|
| 15 |
def obtener_historial_usuario(user_id: str, limit: int = 200) -> list[dict]:
|
| 16 |
+
entradas = _dao.historial.obtener_por_usuario(user_id, limit=limit)
|
| 17 |
return [{"movie_id": h.pelicula_id, "user_rating": h.valoracion} for h in entradas]
|
| 18 |
|
| 19 |
def recomendar_peliculas(
|
chatbot/.env.production
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
VITE_API_BASE_URL=https://tu-api-render-aqui.onrender.com
|
chatbot/src/components/AppHero.vue
CHANGED
|
@@ -90,6 +90,7 @@
|
|
| 90 |
import { computed, ref } from "vue";
|
| 91 |
import { useRouter } from "vue-router";
|
| 92 |
import { useTheme } from "vuetify";
|
|
|
|
| 93 |
|
| 94 |
defineProps({
|
| 95 |
username: { type: String, default: "" },
|
|
@@ -111,7 +112,7 @@ function toggleTheme() {
|
|
| 111 |
async function logout() {
|
| 112 |
const token = localStorage.getItem("vs_token") || "";
|
| 113 |
try {
|
| 114 |
-
await fetch(
|
| 115 |
method: "POST",
|
| 116 |
headers: { "Content-Type": "application/json" },
|
| 117 |
body: JSON.stringify({ token }),
|
|
@@ -125,7 +126,7 @@ async function confirmDelete() {
|
|
| 125 |
deleteLoading.value = true;
|
| 126 |
const token = localStorage.getItem("vs_token") || "";
|
| 127 |
try {
|
| 128 |
-
await fetch(
|
| 129 |
method: "DELETE",
|
| 130 |
headers: { "Content-Type": "application/json" },
|
| 131 |
body: JSON.stringify({ token }),
|
|
|
|
| 90 |
import { computed, ref } from "vue";
|
| 91 |
import { useRouter } from "vue-router";
|
| 92 |
import { useTheme } from "vuetify";
|
| 93 |
+
import API_BASE_URL from "../config.js";
|
| 94 |
|
| 95 |
defineProps({
|
| 96 |
username: { type: String, default: "" },
|
|
|
|
| 112 |
async function logout() {
|
| 113 |
const token = localStorage.getItem("vs_token") || "";
|
| 114 |
try {
|
| 115 |
+
await fetch(`${API_BASE_URL}/auth/logout`, {
|
| 116 |
method: "POST",
|
| 117 |
headers: { "Content-Type": "application/json" },
|
| 118 |
body: JSON.stringify({ token }),
|
|
|
|
| 126 |
deleteLoading.value = true;
|
| 127 |
const token = localStorage.getItem("vs_token") || "";
|
| 128 |
try {
|
| 129 |
+
await fetch(`${API_BASE_URL}/auth/account`, {
|
| 130 |
method: "DELETE",
|
| 131 |
headers: { "Content-Type": "application/json" },
|
| 132 |
body: JSON.stringify({ token }),
|
chatbot/src/components/MessageFeed.vue
CHANGED
|
@@ -215,6 +215,7 @@ import { computed, nextTick, reactive, ref, watch } from "vue";
|
|
| 215 |
import { useTheme } from "vuetify";
|
| 216 |
import { dominantEmotionInfo, emotionInfo } from "../constants/emotions";
|
| 217 |
import MessageComposer from "./MessageComposer.vue";
|
|
|
|
| 218 |
|
| 219 |
const props = defineProps({
|
| 220 |
messages: { type: Array, default: () => [] },
|
|
@@ -236,7 +237,7 @@ async function loadPoster(imdbId) {
|
|
| 236 |
if (!imdbId || key in posters) return;
|
| 237 |
posters[key] = null;
|
| 238 |
try {
|
| 239 |
-
const res = await fetch(`
|
| 240 |
const data = await res.json();
|
| 241 |
posters[key] = data.poster_url || null;
|
| 242 |
} catch { /* keep null */ }
|
|
|
|
| 215 |
import { useTheme } from "vuetify";
|
| 216 |
import { dominantEmotionInfo, emotionInfo } from "../constants/emotions";
|
| 217 |
import MessageComposer from "./MessageComposer.vue";
|
| 218 |
+
import API_BASE_URL from "../config.js";
|
| 219 |
|
| 220 |
const props = defineProps({
|
| 221 |
messages: { type: Array, default: () => [] },
|
|
|
|
| 237 |
if (!imdbId || key in posters) return;
|
| 238 |
posters[key] = null;
|
| 239 |
try {
|
| 240 |
+
const res = await fetch(`${API_BASE_URL}/poster/${key}`);
|
| 241 |
const data = await res.json();
|
| 242 |
posters[key] = data.poster_url || null;
|
| 243 |
} catch { /* keep null */ }
|
chatbot/src/config.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// API Base URL configuration
|
| 2 |
+
// Uses environment variable VITE_API_BASE_URL if available
|
| 3 |
+
// Falls back to localhost:5000 for local development
|
| 4 |
+
|
| 5 |
+
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "http://localhost:5000";
|
| 6 |
+
|
| 7 |
+
export default API_BASE_URL;
|
chatbot/src/views/AuthView.vue
CHANGED
|
@@ -231,6 +231,7 @@
|
|
| 231 |
<script setup>
|
| 232 |
import { ref, onMounted } from "vue";
|
| 233 |
import { useRouter } from "vue-router";
|
|
|
|
| 234 |
|
| 235 |
const router = useRouter();
|
| 236 |
const activeTab = ref("login");
|
|
@@ -268,7 +269,7 @@ async function submitLogin() {
|
|
| 268 |
}
|
| 269 |
loginLoading.value = true;
|
| 270 |
try {
|
| 271 |
-
const res = await fetch(
|
| 272 |
method: "POST",
|
| 273 |
headers: { "Content-Type": "application/json" },
|
| 274 |
body: JSON.stringify({ username: loginForm.value.username, password: loginForm.value.password }),
|
|
@@ -302,7 +303,7 @@ async function submitRegister() {
|
|
| 302 |
}
|
| 303 |
registerLoading.value = true;
|
| 304 |
try {
|
| 305 |
-
const res = await fetch(
|
| 306 |
method: "POST",
|
| 307 |
headers: { "Content-Type": "application/json" },
|
| 308 |
body: JSON.stringify({ username: registerForm.value.username, password: registerForm.value.password }),
|
|
@@ -326,7 +327,7 @@ async function submitRegister() {
|
|
| 326 |
|
| 327 |
async function cargarPopulares() {
|
| 328 |
try {
|
| 329 |
-
const res = await fetch(
|
| 330 |
if (res.ok) {
|
| 331 |
const data = await res.json();
|
| 332 |
popularMovies.value = data.items || [];
|
|
@@ -345,7 +346,7 @@ function onSearchInput() {
|
|
| 345 |
searchTimer = setTimeout(async () => {
|
| 346 |
try {
|
| 347 |
const q = encodeURIComponent(searchQuery.value.trim());
|
| 348 |
-
const res = await fetch(`
|
| 349 |
if (res.ok) {
|
| 350 |
const data = await res.json();
|
| 351 |
searchResults.value = data.items || [];
|
|
@@ -389,7 +390,7 @@ async function finalizarOnboarding() {
|
|
| 389 |
}
|
| 390 |
onboardingLoading.value = true;
|
| 391 |
try {
|
| 392 |
-
await fetch(
|
| 393 |
method: "POST",
|
| 394 |
headers: { "Content-Type": "application/json" },
|
| 395 |
body: JSON.stringify({
|
|
|
|
| 231 |
<script setup>
|
| 232 |
import { ref, onMounted } from "vue";
|
| 233 |
import { useRouter } from "vue-router";
|
| 234 |
+
import API_BASE_URL from "../config.js";
|
| 235 |
|
| 236 |
const router = useRouter();
|
| 237 |
const activeTab = ref("login");
|
|
|
|
| 269 |
}
|
| 270 |
loginLoading.value = true;
|
| 271 |
try {
|
| 272 |
+
const res = await fetch(`${API_BASE_URL}/auth/login`, {
|
| 273 |
method: "POST",
|
| 274 |
headers: { "Content-Type": "application/json" },
|
| 275 |
body: JSON.stringify({ username: loginForm.value.username, password: loginForm.value.password }),
|
|
|
|
| 303 |
}
|
| 304 |
registerLoading.value = true;
|
| 305 |
try {
|
| 306 |
+
const res = await fetch(`${API_BASE_URL}/auth/register`, {
|
| 307 |
method: "POST",
|
| 308 |
headers: { "Content-Type": "application/json" },
|
| 309 |
body: JSON.stringify({ username: registerForm.value.username, password: registerForm.value.password }),
|
|
|
|
| 327 |
|
| 328 |
async function cargarPopulares() {
|
| 329 |
try {
|
| 330 |
+
const res = await fetch(`${API_BASE_URL}/peliculas/populares?limit=20`);
|
| 331 |
if (res.ok) {
|
| 332 |
const data = await res.json();
|
| 333 |
popularMovies.value = data.items || [];
|
|
|
|
| 346 |
searchTimer = setTimeout(async () => {
|
| 347 |
try {
|
| 348 |
const q = encodeURIComponent(searchQuery.value.trim());
|
| 349 |
+
const res = await fetch(`${API_BASE_URL}/peliculas/buscar?q=${q}&limit=15`);
|
| 350 |
if (res.ok) {
|
| 351 |
const data = await res.json();
|
| 352 |
searchResults.value = data.items || [];
|
|
|
|
| 390 |
}
|
| 391 |
onboardingLoading.value = true;
|
| 392 |
try {
|
| 393 |
+
await fetch(`${API_BASE_URL}/onboarding/historial`, {
|
| 394 |
method: "POST",
|
| 395 |
headers: { "Content-Type": "application/json" },
|
| 396 |
body: JSON.stringify({
|
chatbot/src/views/ChatView.vue
CHANGED
|
@@ -220,6 +220,7 @@ import { computed, onMounted, ref, watch } from "vue";
|
|
| 220 |
import AppHero from "../components/AppHero.vue";
|
| 221 |
import MessageFeed from "../components/MessageFeed.vue";
|
| 222 |
import { emotionInfoBySpanish } from "../constants/emotions";
|
|
|
|
| 223 |
|
| 224 |
const messages = ref([]);
|
| 225 |
const input = ref("");
|
|
@@ -255,7 +256,7 @@ onMounted(async () => {
|
|
| 255 |
|
| 256 |
if (storedToken) {
|
| 257 |
try {
|
| 258 |
-
const res = await fetch(
|
| 259 |
method: "POST",
|
| 260 |
headers: { "Content-Type": "application/json" },
|
| 261 |
body: JSON.stringify({ token: storedToken }),
|
|
@@ -281,7 +282,7 @@ watch(estrategia, v => localStorage.setItem("vs_estrategia", v));
|
|
| 281 |
watch(userId, async id => {
|
| 282 |
if (!id) return;
|
| 283 |
try {
|
| 284 |
-
const res = await fetch(`
|
| 285 |
const data = await res.json();
|
| 286 |
history.value = data.items || [];
|
| 287 |
} catch { history.value = []; }
|
|
@@ -294,7 +295,7 @@ async function analyze() {
|
|
| 294 |
input.value = "";
|
| 295 |
loading.value = true;
|
| 296 |
try {
|
| 297 |
-
const res = await fetch(
|
| 298 |
method: "POST",
|
| 299 |
headers: { "Content-Type": "application/json" },
|
| 300 |
body: JSON.stringify({ texto: text, user_id: userId.value, estrategia: estrategia.value }),
|
|
@@ -342,7 +343,7 @@ async function submitRating() {
|
|
| 342 |
ratingDialog.value.open = false;
|
| 343 |
pendingViewOp.value = null;
|
| 344 |
try {
|
| 345 |
-
const res = await fetch(
|
| 346 |
method: "POST",
|
| 347 |
headers: { "Content-Type": "application/json" },
|
| 348 |
body: JSON.stringify({
|
|
@@ -375,7 +376,7 @@ async function submitPost() {
|
|
| 375 |
postDialog.value.open = false;
|
| 376 |
if (!text.trim() || !cycleId) return;
|
| 377 |
try {
|
| 378 |
-
const res = await fetch(
|
| 379 |
method: "POST",
|
| 380 |
headers: { "Content-Type": "application/json" },
|
| 381 |
body: JSON.stringify({
|
|
@@ -396,7 +397,7 @@ async function submitPost() {
|
|
| 396 |
async function confirmClearHistory() {
|
| 397 |
clearHistoryLoading.value = true;
|
| 398 |
try {
|
| 399 |
-
const res = await fetch(
|
| 400 |
method: "DELETE",
|
| 401 |
headers: { "Content-Type": "application/json" },
|
| 402 |
body: JSON.stringify({ user_id: userId.value }),
|
|
|
|
| 220 |
import AppHero from "../components/AppHero.vue";
|
| 221 |
import MessageFeed from "../components/MessageFeed.vue";
|
| 222 |
import { emotionInfoBySpanish } from "../constants/emotions";
|
| 223 |
+
import API_BASE_URL from "../config.js";
|
| 224 |
|
| 225 |
const messages = ref([]);
|
| 226 |
const input = ref("");
|
|
|
|
| 256 |
|
| 257 |
if (storedToken) {
|
| 258 |
try {
|
| 259 |
+
const res = await fetch(`${API_BASE_URL}/auth/verify`, {
|
| 260 |
method: "POST",
|
| 261 |
headers: { "Content-Type": "application/json" },
|
| 262 |
body: JSON.stringify({ token: storedToken }),
|
|
|
|
| 282 |
watch(userId, async id => {
|
| 283 |
if (!id) return;
|
| 284 |
try {
|
| 285 |
+
const res = await fetch(`${API_BASE_URL}/historial?user_id=${encodeURIComponent(id)}&limit=20`);
|
| 286 |
const data = await res.json();
|
| 287 |
history.value = data.items || [];
|
| 288 |
} catch { history.value = []; }
|
|
|
|
| 295 |
input.value = "";
|
| 296 |
loading.value = true;
|
| 297 |
try {
|
| 298 |
+
const res = await fetch(`${API_BASE_URL}/analizar`, {
|
| 299 |
method: "POST",
|
| 300 |
headers: { "Content-Type": "application/json" },
|
| 301 |
body: JSON.stringify({ texto: text, user_id: userId.value, estrategia: estrategia.value }),
|
|
|
|
| 343 |
ratingDialog.value.open = false;
|
| 344 |
pendingViewOp.value = null;
|
| 345 |
try {
|
| 346 |
+
const res = await fetch(`${API_BASE_URL}/historial/visto`, {
|
| 347 |
method: "POST",
|
| 348 |
headers: { "Content-Type": "application/json" },
|
| 349 |
body: JSON.stringify({
|
|
|
|
| 376 |
postDialog.value.open = false;
|
| 377 |
if (!text.trim() || !cycleId) return;
|
| 378 |
try {
|
| 379 |
+
const res = await fetch(`${API_BASE_URL}/recomendacion/seguimiento`, {
|
| 380 |
method: "POST",
|
| 381 |
headers: { "Content-Type": "application/json" },
|
| 382 |
body: JSON.stringify({
|
|
|
|
| 397 |
async function confirmClearHistory() {
|
| 398 |
clearHistoryLoading.value = true;
|
| 399 |
try {
|
| 400 |
+
const res = await fetch(`${API_BASE_URL}/historial`, {
|
| 401 |
method: "DELETE",
|
| 402 |
headers: { "Content-Type": "application/json" },
|
| 403 |
body: JSON.stringify({ user_id: userId.value }),
|
chatbot/src/views/PreferencesView.vue
CHANGED
|
@@ -102,6 +102,7 @@
|
|
| 102 |
import { computed, ref } from "vue";
|
| 103 |
import { useRouter } from "vue-router";
|
| 104 |
import { useTheme } from "vuetify";
|
|
|
|
| 105 |
|
| 106 |
const router = useRouter();
|
| 107 |
const theme = useTheme();
|
|
@@ -134,7 +135,7 @@ async function changePassword() {
|
|
| 134 |
}
|
| 135 |
pwdLoading.value = true;
|
| 136 |
try {
|
| 137 |
-
const res = await fetch(
|
| 138 |
method: "POST",
|
| 139 |
headers: { "Content-Type": "application/json" },
|
| 140 |
body: JSON.stringify({
|
|
|
|
| 102 |
import { computed, ref } from "vue";
|
| 103 |
import { useRouter } from "vue-router";
|
| 104 |
import { useTheme } from "vuetify";
|
| 105 |
+
import API_BASE_URL from "../config.js";
|
| 106 |
|
| 107 |
const router = useRouter();
|
| 108 |
const theme = useTheme();
|
|
|
|
| 135 |
}
|
| 136 |
pwdLoading.value = true;
|
| 137 |
try {
|
| 138 |
+
const res = await fetch(`${API_BASE_URL}/auth/password`, {
|
| 139 |
method: "POST",
|
| 140 |
headers: { "Content-Type": "application/json" },
|
| 141 |
body: JSON.stringify({
|
chatbot/vite.config.js
CHANGED
|
@@ -9,4 +9,12 @@ export default defineConfig({
|
|
| 9 |
}),
|
| 10 |
vuetify({ autoImport: true }),
|
| 11 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
});
|
|
|
|
| 9 |
}),
|
| 10 |
vuetify({ autoImport: true }),
|
| 11 |
],
|
| 12 |
+
server: {
|
| 13 |
+
port: 5173,
|
| 14 |
+
host: '0.0.0.0',
|
| 15 |
+
},
|
| 16 |
+
preview: {
|
| 17 |
+
port: 4173,
|
| 18 |
+
host: '0.0.0.0',
|
| 19 |
+
},
|
| 20 |
});
|
docs/singleton_bd.md
CHANGED
|
@@ -7,6 +7,4 @@ classDiagram
|
|
| 7 |
+instancia() ConexionBD
|
| 8 |
+obtener_conexion() Connection
|
| 9 |
}
|
| 10 |
-
|
| 11 |
-
note for ConexionBD "Singleton clásico que garantiza una única instancia de conexión a la base de datos SQLite"
|
| 12 |
```
|
|
|
|
| 7 |
+instancia() ConexionBD
|
| 8 |
+obtener_conexion() Connection
|
| 9 |
}
|
|
|
|
|
|
|
| 10 |
```
|
render.yaml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
- type: web
|
| 3 |
+
name: valor-sentimental-backend
|
| 4 |
+
runtime: python
|
| 5 |
+
plan: standard
|
| 6 |
+
buildCommand: pip install -r requirements.txt
|
| 7 |
+
startCommand: cd backend && python main.py
|
| 8 |
+
envVars:
|
| 9 |
+
- key: PORT
|
| 10 |
+
value: 5000
|
| 11 |
+
- key: FLASK_ENV
|
| 12 |
+
value: production
|
| 13 |
+
- key: HF_TOKEN
|
| 14 |
+
scope: secret
|
| 15 |
+
- key: OMDB_API_KEY
|
| 16 |
+
scope: secret
|
| 17 |
+
|
| 18 |
+
- type: web
|
| 19 |
+
name: valor-sentimental-frontend
|
| 20 |
+
runtime: node
|
| 21 |
+
plan: standard
|
| 22 |
+
buildCommand: cd chatbot && npm install && npm run build
|
| 23 |
+
startCommand: cd chatbot && npm run preview
|
| 24 |
+
envVars:
|
| 25 |
+
- key: VITE_API_BASE_URL
|
| 26 |
+
value: https://valor-sentimental-backend.onrender.com
|