Manuals inputs implemented
Feature/input manual
This commit is contained in:
+8
-4
@@ -3,11 +3,15 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# Variables de entorno
|
# Variables de entorno (solo ignoramos las locales/sobre-escrituras)
|
||||||
.env
|
# Mantenemos .env y .env.example versionados para compartir la configuración
|
||||||
.env*
|
# de producción y el esqueleto de variables. Nunca subimos .env.local.
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Override de Docker Compose para desarrollo local (no debe llegar a producción)
|
||||||
|
docker-compose.override.yml
|
||||||
|
|
||||||
# Configuraciones del editor
|
# Configuraciones del editor
|
||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
# Deck of Cards <img align="right" width="60" height="40" alt="image" src="https://github.com/user-attachments/assets/e654df77-be36-4d38-8b5a-a3d3eac462e3" />
|
# Deck of Cards <img align="right" width="38" src="frontend/public/favicon.svg" alt="Logo de Deck of Cards" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@@ -28,11 +26,76 @@ Incluye:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
# 📸 Capturas de Pantalla
|
||||||
|
|
||||||
|
## 🛠️ Proceso de Modelado (3 Pasos)
|
||||||
|
El núcleo de la aplicación guía al experto a través de tres fases intuitivas para transformar cartas físicas en modelos matemáticos.
|
||||||
|
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<p><b>Paso 1: Definición de la Escala Base</b></p>
|
||||||
|
<img src="https://github.com/user-attachments/assets/65b7ab8a-732e-461d-a418-41721d8f0810" width="100%" alt="Paso 1">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<p><b>Paso 2: Modelado Difuso</b></p>
|
||||||
|
<img src="https://github.com/user-attachments/assets/cfc166b6-6c6d-4eed-8c18-ea65463bedcd" width="100%" alt="Paso 2 - Gráfica">
|
||||||
|
<br><br>
|
||||||
|
<p><b>Paso 2: Definición de Subescalas e Intervalos</b></p>
|
||||||
|
<img src="https://github.com/user-attachments/assets/58bf477d-6f45-451e-81cf-76a5d7d9edc2" width="100%" alt="Paso 2 - Subescalas">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<p><b>Paso 3: Visualización de la Función Final</b></p>
|
||||||
|
<img src="https://github.com/user-attachments/assets/eab13dd3-0d3b-4441-babe-b1dfee49f431" width="100%" alt="Paso 3">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 📂 Historial
|
||||||
|
|
||||||
|
<table width="100%">
|
||||||
|
<tr>
|
||||||
|
<td align="center">
|
||||||
|
<p><b>Listado del Historial de Modelos</b></p>
|
||||||
|
<img src="https://github.com/user-attachments/assets/3502c9d5-e566-476b-a98a-406397ec7eb4" width="100%" alt="Historial 1">
|
||||||
|
<br><br>
|
||||||
|
<p><b>Vista de Detalle de un Modelo Guardado</b></p>
|
||||||
|
<img src="https://github.com/user-attachments/assets/059d5512-3e9d-4429-8b18-81df9afeccfe" width="100%" alt="Historial 2">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## 🔐 Acceso a la Plataforma
|
||||||
|
|
||||||
|
<table width="100%">
|
||||||
|
<tr valign="top">
|
||||||
|
<td width="50%" align="center">
|
||||||
|
<p><b>Login</b></p>
|
||||||
|
<img src="https://github.com/user-attachments/assets/d30767ab-7968-47f9-a033-8eb2a622f271" width="100%" alt="Login">
|
||||||
|
</td>
|
||||||
|
<td width="50%" align="center">
|
||||||
|
<p><b>Registro</b></p>
|
||||||
|
<img src="https://github.com/user-attachments/assets/88500610-1f39-4b8f-851e-1d768eee320e" width="100%" alt="Registro">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# ⚡ 0. ¿En qué consiste?
|
# ⚡ 0. ¿En qué consiste?
|
||||||
|
|
||||||
**Deck of Cards – TFG** es una herramienta completa diseñada para construir, validar y evaluar **funciones de pertenencia difusas** mediante el método **Deck of Cards (DoC)**, tanto en su versión **T1MF** (tipo-1) como **IT2MF** (tipo-2 intervalar).
|
**Deck of Cards** es una herramienta completa diseñada para construir, validar y evaluar **funciones de pertenencia difusas** mediante el método **Deck of Cards (DoC)**, tanto en su versión **T1MF** (tipo-1) como **IT2MF** (tipo-2 intervalar).
|
||||||
|
|
||||||
> [!INFO]
|
|
||||||
> El sistema combina un backend robusto en **FastAPI + MongoDB**, un frontend moderno en **React + Vite**, autenticación por email y **Google OAuth 2.0**, y un sistema de historial por usuario para guardar y recuperar trabajos anteriores.
|
> El sistema combina un backend robusto en **FastAPI + MongoDB**, un frontend moderno en **React + Vite**, autenticación por email y **Google OAuth 2.0**, y un sistema de historial por usuario para guardar y recuperar trabajos anteriores.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -111,6 +174,13 @@ docker compose up --build
|
|||||||
>- Frontend (react) → http://localhost:5173
|
>- Frontend (react) → http://localhost:5173
|
||||||
>- Base de Datos (mongodb) → puerto 27017
|
>- Base de Datos (mongodb) → puerto 27017
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **Nota para desarrolladores:** Gracias a Docker, no necesitas instalar nada localmente para que la app funcione. Sin embargo, para que tu editor de código reconozca las librerías, tenga autocompletado y no muestre errores de importación, te recomendamos entrar a las carpetas y descargar las dependencias en tu máquina local:
|
||||||
|
> ```bash
|
||||||
|
> cd frontend && npm install
|
||||||
|
> cd ../backend && pip install -r requirements.txt
|
||||||
|
> ```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🔌 3. Endpoints principales del proyecto
|
# 🔌 3. Endpoints principales del proyecto
|
||||||
@@ -148,11 +218,12 @@ deck-of-cards/
|
|||||||
│ ├── routers/
|
│ ├── routers/
|
||||||
│ ├── models/
|
│ ├── models/
|
||||||
│ ├── utils/
|
│ ├── utils/
|
||||||
│ ├── .env
|
│ ├── .env → !!!
|
||||||
│ └── main.py
|
│ └── main.py
|
||||||
│
|
│
|
||||||
├── frontend/ → Vite + React
|
├── frontend/ → Vite + React
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
|
│ ├── .env → !!!
|
||||||
│ └── ...
|
│ └── ...
|
||||||
│
|
│
|
||||||
├── docker-compose.yaml
|
├── docker-compose.yaml
|
||||||
@@ -161,7 +232,28 @@ deck-of-cards/
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 🔐 5. Configuración del archivo .env
|
# 🔐 5. Configuración de los archivos .env
|
||||||
|
|
||||||
|
El frontend necesita un archivo `.env` para especificar el puerto del backend
|
||||||
|
|
||||||
|
📍 **Este archivo debe estar dentro de la carpeta `frontend/`**, así:
|
||||||
|
|
||||||
|
```
|
||||||
|
deck-of-cards/
|
||||||
|
│
|
||||||
|
├── frontend/
|
||||||
|
│ ├── src/
|
||||||
|
│ ├── .env ← AQUÍ
|
||||||
|
│ └── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Contenido obligatorio del `.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_API_URL=http://localhost:8000/api
|
||||||
|
```
|
||||||
|
|
||||||
|
Por otro lado,
|
||||||
|
|
||||||
El backend necesita un archivo `.env` para funcionar correctamente, especialmente para el login con Google.
|
El backend necesita un archivo `.env` para funcionar correctamente, especialmente para el login con Google.
|
||||||
|
|
||||||
@@ -177,7 +269,7 @@ deck-of-cards/
|
|||||||
│ └── ...
|
│ └── ...
|
||||||
```
|
```
|
||||||
|
|
||||||
Contenido mínimo del `.env`:
|
Contenido obligatorio del `.env`:
|
||||||
|
|
||||||
```
|
```
|
||||||
GOOGLE_CLIENT_ID=tu_client_id_de_google //id del cliente, debe ser el mismo del proyecto de google cloud.
|
GOOGLE_CLIENT_ID=tu_client_id_de_google //id del cliente, debe ser el mismo del proyecto de google cloud.
|
||||||
@@ -200,37 +292,21 @@ Un ejemplo de página de donde sacar las tres primeras claves:
|
|||||||
Para activar el login con Google, debes crear credenciales OAuth 2.0.
|
Para activar el login con Google, debes crear credenciales OAuth 2.0.
|
||||||
Para ello sigue estos pasos:
|
Para ello sigue estos pasos:
|
||||||
|
|
||||||
### 🟦 Paso 1 — Entra en Google Cloud Console
|
### 🟦 Paso 1 — Entra en Google Cloud Console y haz login
|
||||||
https://console.cloud.google.com
|
https://console.cloud.google.com
|
||||||
|
|
||||||
### 🟩 Paso 2 — Crea un proyecto
|
### 🟩 Paso 2 — Crea un proyecto
|
||||||
Menú superior → “Seleccionar proyecto” → “Nuevo proyecto”.
|
Menú superior → “Seleccionar proyecto” → “Nuevo proyecto” → Elige un nombre para tu proyecto.
|
||||||
|
|
||||||
### 🟧 Paso 3 — Configura la pantalla de consentimiento OAuth
|
### 🟨 Paso 3 — Crea las credenciales OAuth
|
||||||
|
En el menú lateral:
|
||||||
<img width="750" height="731" alt="image" src="https://github.com/user-attachments/assets/fb06952a-ddde-4a15-87f8-162b53882d00" />
|
**APIs y Servicios → Credenciales → Crear Credenciales → ID de Cliente OAuth**
|
||||||
|
|
||||||
|
|
||||||
En el buscador escribe: **OAuth consent screen** ó ve a la página que se muestra en la captura superior.
|
|
||||||
|
|
||||||
Selecciona **Información de la página**:
|
|
||||||
|
|
||||||
→ Rellena los datos necesarios (no hace falta poner un dominio si lo tienes en local) → Guardar
|
|
||||||
|
|
||||||
|
|
||||||
<img width="1398" height="470" alt="image" src="https://github.com/user-attachments/assets/d38ff3b4-5ee7-4608-ad62-515e38114392" />
|
|
||||||
|
|
||||||
|
|
||||||
### 🟨 Paso 4 — Crea las credenciales OAuth
|
|
||||||
Menú lateral:
|
|
||||||
**APIs & Services → Credentials → Create Credentials → OAuth Client ID**
|
|
||||||
|
|
||||||
<img width="1003" height="815" alt="image" src="https://github.com/user-attachments/assets/8a9e61fd-f4dc-4b33-aac3-3e793ceac8b3" />
|
<img width="1003" height="815" alt="image" src="https://github.com/user-attachments/assets/8a9e61fd-f4dc-4b33-aac3-3e793ceac8b3" />
|
||||||
|
<img width="1247" height="347" alt="image" src="https://github.com/user-attachments/assets/900c8236-63e5-4a2b-b642-7c901108e09e" />
|
||||||
|
|
||||||
|
|
||||||
Tipo de aplicación:
|
Tipo de aplicación:
|
||||||
✔ **Web application**
|
✔ **Aplicación Web**
|
||||||
|
|
||||||
Añade en `Orígenes autorizados de javascript`:
|
Añade en `Orígenes autorizados de javascript`:
|
||||||
http://localhost:8000
|
http://localhost:8000
|
||||||
@@ -238,16 +314,37 @@ http://localhost:8000
|
|||||||
Añade este Redirect URI obligatorio:
|
Añade este Redirect URI obligatorio:
|
||||||
http://localhost:8000/api/auth/google/callback
|
http://localhost:8000/api/auth/google/callback
|
||||||
|
|
||||||
|
Después te saldrá una ventana para descargarte las claves en formato json (hazlo para que no se pierdan)
|
||||||
|
|
||||||
|
Finalmente podrás ver la pantalla final (desde aquí no se puede acceder al secreto del cliente, solo puedes generar uno nuevo):
|
||||||
|
|
||||||
<img width="1911" height="847" alt="image" src="https://github.com/user-attachments/assets/c63454ac-00a5-43e3-9f53-25b4862292df" />
|
<img width="1911" height="847" alt="image" src="https://github.com/user-attachments/assets/c63454ac-00a5-43e3-9f53-25b4862292df" />
|
||||||
|
|
||||||
|
### 🟧 Paso 4 — Copia tus claves
|
||||||
### 🟥 Paso 5 — Copia tus claves
|
|
||||||
Google te mostrará:
|
Google te mostrará:
|
||||||
|
|
||||||
- **Client ID**
|
- **Client ID**
|
||||||
- **Client Secret**
|
- **Client Secret**
|
||||||
|
|
||||||
Pégalos en tu `.env` dentro de `backend/`. Asegúrate de que las copias correctamente.
|
Pégalos en tu `.env` dentro de `backend/`. Asegúrate de que las copias correctamente cada una en su lugar.
|
||||||
|
|
||||||
|
### 🟥 Paso 5 — Configura la pantalla de consentimiento OAuth
|
||||||
|
|
||||||
|
En el buscador escribe: **OAuth consent screen** o busca en el menu lateral la siguiente opción:
|
||||||
|
|
||||||
|
<img width="750" height="731" alt="image" src="https://github.com/user-attachments/assets/fb06952a-ddde-4a15-87f8-162b53882d00" />
|
||||||
|
|
||||||
|
Configura los datos de tu proyecto seleccionando **Información de la página**:
|
||||||
|
|
||||||
|
→ Rellena los datos necesarios (no hace falta poner un dominio si lo tienes en local) → Guardar
|
||||||
|
|
||||||
|
<img width="1398" height="470" alt="image" src="https://github.com/user-attachments/assets/d38ff3b4-5ee7-4608-ad62-515e38114392" />
|
||||||
|
|
||||||
|
### 🟪 Paso 6 — Publicar la app
|
||||||
|
|
||||||
|
Publica tu app accediendo a: (Este paso es muy importante para permitir acceder a cualquier correo)
|
||||||
|
|
||||||
|
<img width="777" height="440" alt="image" src="https://github.com/user-attachments/assets/b06fabc9-9b36-46dd-a071-7ce7f28b8bca" />
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -261,8 +358,21 @@ Con esto, ya puedes:
|
|||||||
- Levantar el sistema con Docker
|
- Levantar el sistema con Docker
|
||||||
- Usar login normal y login con Google
|
- Usar login normal y login con Google
|
||||||
|
|
||||||
Y ...
|
Y... ¡Proyecto listo para ejecutarse en local!
|
||||||
|
|
||||||
¡Proyecto listo para ejecutarse en local!
|
## 👥 Autores y Equipo
|
||||||
|
|
||||||
Gracias por llegar hasta aquí ;)
|
Este proyecto es fruto de la colaboración académica con la **Universidad de Jaén**.
|
||||||
|
|
||||||
|
| Rol | Desarrollador | GitHub |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Frontend** | Alexis López Moral | [@AlexisLopez-Dev](https://github.com/AlexisLopez-Dev) |
|
||||||
|
| **Backend** | Mireya Cueto Garrido | [@MireyaCueto](https://github.com/MireyaCueto) |
|
||||||
|
|
||||||
|
### 🎓 Dirección y Tutorización
|
||||||
|
* **Director del proyecto:** Luis Martínez López
|
||||||
|
|
||||||
|
---
|
||||||
|
<p align="center">
|
||||||
|
Realizado con ❤️ en la Universidad de Jaén
|
||||||
|
</p>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Backend environment variables (FastAPI)
|
||||||
|
# ----------------------------------------
|
||||||
|
# Copia este archivo como `.env` para desarrollo local y rellena los valores.
|
||||||
|
#
|
||||||
|
# docker-compose levanta el backend con `env_file: backend/.env`.
|
||||||
|
|
||||||
|
# Google OAuth (https://console.cloud.google.com/apis/credentials)
|
||||||
|
# IMPORTANTE: la REDIRECT_URI es la URL a la que Google devuelve al usuario,
|
||||||
|
# por tanto debe coincidir con la URL pública del backend tal como la ve el
|
||||||
|
# navegador. Usando docker-compose, el backend está expuesto en el host en
|
||||||
|
# el puerto 8070, así que:
|
||||||
|
# http://localhost:8070/api/auth/google/callback
|
||||||
|
# Si ejecutas el backend fuera de Docker en el puerto 8000, usa:
|
||||||
|
# http://localhost:8000/api/auth/google/callback
|
||||||
|
# Esta URI debe estar registrada también en la consola de Google Cloud.
|
||||||
|
GOOGLE_CLIENT_ID=tu-client-id.apps.googleusercontent.com
|
||||||
|
GOOGLE_CLIENT_SECRET=tu-client-secret
|
||||||
|
GOOGLE_REDIRECT_URI=http://localhost:8070/api/auth/google/callback
|
||||||
|
|
||||||
|
# Clave para firmar los JWT (usa algo largo y aleatorio en producción)
|
||||||
|
SECRET_KEY=cambia-esta-clave-en-produccion
|
||||||
|
|
||||||
|
# URL del frontend a la que se redirige tras el login con Google
|
||||||
|
# Con docker-compose en local: http://localhost:8071
|
||||||
|
# Con Vite directo en local: http://localhost:5173
|
||||||
|
# En producción: https://tu-dominio.com
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
@@ -13,6 +13,7 @@ GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
|
|||||||
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
|
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
|
||||||
REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI")
|
REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI")
|
||||||
SECRET_KEY = os.getenv("SECRET_KEY")
|
SECRET_KEY = os.getenv("SECRET_KEY")
|
||||||
|
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173")
|
||||||
|
|
||||||
@router.get("/login")
|
@router.get("/login")
|
||||||
async def google_login():
|
async def google_login():
|
||||||
@@ -97,4 +98,4 @@ async def google_callback(request: Request):
|
|||||||
{"$set": {"token": token}}
|
{"$set": {"token": token}}
|
||||||
)
|
)
|
||||||
|
|
||||||
return RedirectResponse(f"http://localhost:5173/login?token={token}")
|
return RedirectResponse(f"{FRONTEND_URL}/login?token={token}")
|
||||||
+8
-4
@@ -4,26 +4,30 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
container_name: backend
|
container_name: backend
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8070:8000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
depends_on:
|
depends_on:
|
||||||
- db
|
- db
|
||||||
env_file:
|
env_file:
|
||||||
- backend\.env
|
- backend/.env
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: ./frontend
|
context: ./frontend
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
ports:
|
ports:
|
||||||
- "5173:5173"
|
- "8071:5173"
|
||||||
volumes:
|
volumes:
|
||||||
- ./frontend:/app
|
- ./frontend:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
|
environment:
|
||||||
|
- VITE_API_URL=http://localhost:8070/api
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: mongo:6
|
image: mongo:4.4
|
||||||
container_name: mongo
|
container_name: mongo
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Frontend environment variables (Vite)
|
||||||
|
# --------------------------------------
|
||||||
|
# Copia este archivo como `.env.local` para desarrollo local.
|
||||||
|
# Vite carga automáticamente `.env.local` encima de `.env`, así que
|
||||||
|
# los valores aquí definidos sobrescribirán los de producción en dev.
|
||||||
|
#
|
||||||
|
# cp .env.example .env.local
|
||||||
|
#
|
||||||
|
# IMPORTANTE: Sólo las variables con prefijo VITE_ se exponen al cliente.
|
||||||
|
|
||||||
|
# URL base de la API del backend (la ve el NAVEGADOR, no el contenedor).
|
||||||
|
#
|
||||||
|
# - Dev con docker-compose (recomendado): el backend está expuesto en el
|
||||||
|
# puerto 8070 del host (mapea 8070 -> 8000 del contenedor).
|
||||||
|
# VITE_API_URL=http://localhost:8070/api
|
||||||
|
#
|
||||||
|
# - Backend ejecutado fuera de Docker (uvicorn --port 8000):
|
||||||
|
# VITE_API_URL=http://localhost:8000/api
|
||||||
|
#
|
||||||
|
# - Producción (ya definido en .env):
|
||||||
|
# VITE_API_URL=http://sinbad2.ujaen.es:8070/api
|
||||||
|
VITE_API_URL=http://localhost:8070/api
|
||||||
@@ -12,6 +12,11 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
# Variables de entorno locales de Vite
|
||||||
|
# (.env y .env.example sí se versionan)
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|||||||
@@ -1,4 +1,105 @@
|
|||||||
export default function Controls({
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
function SliderInput({ label, value, min, max, step, color, onChange, decimals = 3 }) {
|
||||||
|
const [draft, setDraft] = useState(null);
|
||||||
|
|
||||||
|
const numericValue = Number(value);
|
||||||
|
const displayValue = draft !== null ? draft : numericValue.toFixed(decimals);
|
||||||
|
|
||||||
|
const commitDraft = () => {
|
||||||
|
if (draft === null) return;
|
||||||
|
|
||||||
|
const trimmed = draft.trim();
|
||||||
|
if (trimmed === '' || trimmed === '-' || trimmed === '.' || trimmed === '-.') {
|
||||||
|
setDraft(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = parseFloat(trimmed);
|
||||||
|
if (Number.isNaN(parsed)) {
|
||||||
|
setDraft(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamped = Math.min(max, Math.max(min, parsed));
|
||||||
|
onChange(clamped);
|
||||||
|
setDraft(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNumberChange = (e) => {
|
||||||
|
const raw = e.target.value;
|
||||||
|
|
||||||
|
if (raw === '') {
|
||||||
|
setDraft('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = e.target.valueAsNumber;
|
||||||
|
|
||||||
|
if (Number.isNaN(num)) {
|
||||||
|
setDraft(raw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num > max) {
|
||||||
|
setDraft(max.toFixed(decimals));
|
||||||
|
onChange(max);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num < min) {
|
||||||
|
setDraft(raw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraft(raw);
|
||||||
|
onChange(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
setDraft(null);
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-bold text-slate-600 mb-1">{label}</label>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={numericValue}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="flex-1 cursor-pointer h-1.5"
|
||||||
|
style={{ accentColor: color }}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={displayValue}
|
||||||
|
onChange={handleNumberChange}
|
||||||
|
onBlur={commitDraft}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="w-20 px-2 py-1 text-xs font-semibold text-slate-700 bg-white border border-slate-200 rounded-md text-center shadow-sm focus:outline-none focus:ring-2 focus:border-transparent transition-shadow tabular-nums"
|
||||||
|
style={{ '--tw-ring-color': color, borderColor: draft !== null ? color : undefined }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MF_STEP = 0.001;
|
||||||
|
const snapToMfStep = (v) => Math.round(Number(v) / MF_STEP) * MF_STEP;
|
||||||
|
|
||||||
|
export default function Controls({
|
||||||
selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf,
|
selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf,
|
||||||
subscales, onOpenSubscale
|
subscales, onOpenSubscale
|
||||||
}) {
|
}) {
|
||||||
@@ -6,38 +107,50 @@ export default function Controls({
|
|||||||
|
|
||||||
const scaleKeys = Object.keys(baseScale);
|
const scaleKeys = Object.keys(baseScale);
|
||||||
const selectedIndex = scaleKeys.indexOf(selectedTerm);
|
const selectedIndex = scaleKeys.indexOf(selectedTerm);
|
||||||
|
|
||||||
let absoluteMin = 0, absoluteMax = 1;
|
const prevTerm = selectedIndex > 0 ? mfDefinitions[scaleKeys[selectedIndex - 1]] : null;
|
||||||
if (selectedIndex > 0) absoluteMin = mfDefinitions[scaleKeys[selectedIndex - 1]].coreEnd;
|
const nextTerm = selectedIndex < scaleKeys.length - 1 ? mfDefinitions[scaleKeys[selectedIndex + 1]] : null;
|
||||||
if (selectedIndex < scaleKeys.length - 1) absoluteMax = mfDefinitions[scaleKeys[selectedIndex + 1]].coreStart;
|
|
||||||
|
const anchor = snapToMfStep(baseScale[selectedTerm]);
|
||||||
|
|
||||||
|
const bounds = {
|
||||||
|
supportStart: { min: snapToMfStep(prevTerm?.coreEnd ?? 0), max: anchor },
|
||||||
|
coreStart: { min: snapToMfStep(prevTerm?.supportEnd ?? 0), max: anchor },
|
||||||
|
coreEnd: { min: anchor, max: snapToMfStep(nextTerm?.supportStart ?? 1) },
|
||||||
|
supportEnd: { min: anchor, max: snapToMfStep(nextTerm?.coreStart ?? 1) },
|
||||||
|
};
|
||||||
|
|
||||||
const leftSubscale = subscales?.[selectedTerm]?.left;
|
const leftSubscale = subscales?.[selectedTerm]?.left;
|
||||||
const rightSubscale = subscales?.[selectedTerm]?.right;
|
const rightSubscale = subscales?.[selectedTerm]?.right;
|
||||||
|
|
||||||
|
const commonProps = { step: 0.001, color: selectedColor };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-md relative overflow-hidden">
|
<div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-md relative overflow-hidden">
|
||||||
<div className="absolute top-0 left-0 w-full h-1.5" style={{ backgroundColor: selectedColor }}></div>
|
<div className="absolute top-0 left-0 w-full h-1.5" style={{ backgroundColor: selectedColor }}></div>
|
||||||
<h3 className="text-xl font-bold text-slate-800 mb-4 flex items-center gap-2">
|
<h3 className="text-xl font-bold text-slate-800 mb-4 flex items-center gap-2">
|
||||||
Ajustando: <span style={{ color: selectedColor }}>"{selectedTerm}"</span>
|
Ajustando: <span style={{ color: selectedColor }}>"{selectedTerm}"</span>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
<div className="space-y-4 bg-slate-50 p-4 rounded-xl border border-slate-100">
|
<div className="space-y-4 bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||||
<div>
|
<SliderInput
|
||||||
<label className="flex justify-between text-xs font-bold text-slate-600 mb-1">
|
{...commonProps}
|
||||||
<span>Inicio del Soporte (Punto inferior)</span><span style={{ color: selectedColor }}>{currentMf.supportStart.toFixed(3)}</span>
|
{...bounds.supportStart}
|
||||||
</label>
|
label="Inicio del Soporte (Punto inferior)"
|
||||||
<input type="range" min={absoluteMin} max={absoluteMax} step="0.001" value={currentMf.supportStart} onChange={(e) => updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
|
value={currentMf.supportStart}
|
||||||
</div>
|
onChange={(v) => updateCurrentMf('supportStart', v)}
|
||||||
<div>
|
/>
|
||||||
<label className="flex justify-between text-xs font-bold text-slate-600 mb-1">
|
<SliderInput
|
||||||
<span>Inicio del Núcleo (Punto superior)</span><span style={{ color: selectedColor }}>{currentMf.coreStart.toFixed(3)}</span>
|
{...commonProps}
|
||||||
</label>
|
{...bounds.coreStart}
|
||||||
<input type="range" min={absoluteMin} max={absoluteMax} step="0.001" value={currentMf.coreStart} onChange={(e) => updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} />
|
label="Inicio del Núcleo (Punto superior)"
|
||||||
</div>
|
value={currentMf.coreStart}
|
||||||
|
onChange={(v) => updateCurrentMf('coreStart', v)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="pt-2 border-t border-slate-200 flex justify-end">
|
<div className="pt-2 border-t border-slate-200 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => onOpenSubscale(selectedTerm, 'left', leftSubscale)}
|
onClick={() => onOpenSubscale(selectedTerm, 'left', leftSubscale)}
|
||||||
className={`text-sm font-bold px-4 py-2 rounded-lg transition-all border ${leftSubscale ? 'bg-blue-50 text-blue-700 border-blue-200' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50'}`}
|
className={`text-sm font-bold px-4 py-2 rounded-lg transition-all border ${leftSubscale ? 'bg-blue-50 text-blue-700 border-blue-200' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50'}`}
|
||||||
>
|
>
|
||||||
@@ -47,21 +160,23 @@ export default function Controls({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 bg-slate-50 p-4 rounded-xl border border-slate-100">
|
<div className="space-y-4 bg-slate-50 p-4 rounded-xl border border-slate-100">
|
||||||
<div>
|
<SliderInput
|
||||||
<label className="flex justify-between text-xs font-bold text-slate-600 mb-1">
|
{...commonProps}
|
||||||
<span>Fin del Soporte (Punto inferior)</span><span style={{ color: selectedColor }}>{currentMf.supportEnd.toFixed(3)}</span>
|
{...bounds.supportEnd}
|
||||||
</label>
|
label="Fin del Soporte (Punto inferior)"
|
||||||
<input type="range" min={absoluteMin} max={absoluteMax} step="0.001" value={currentMf.supportEnd} onChange={(e) => updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
|
value={currentMf.supportEnd}
|
||||||
</div>
|
onChange={(v) => updateCurrentMf('supportEnd', v)}
|
||||||
<div>
|
/>
|
||||||
<label className="flex justify-between text-xs font-bold text-slate-600 mb-1">
|
<SliderInput
|
||||||
<span>Fin del Núcleo (Punto superior)</span><span style={{ color: selectedColor }}>{currentMf.coreEnd.toFixed(3)}</span>
|
{...commonProps}
|
||||||
</label>
|
{...bounds.coreEnd}
|
||||||
<input type="range" min={absoluteMin} max={absoluteMax} step="0.001" value={currentMf.coreEnd} onChange={(e) => updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} />
|
label="Fin del Núcleo (Punto superior)"
|
||||||
</div>
|
value={currentMf.coreEnd}
|
||||||
|
onChange={(v) => updateCurrentMf('coreEnd', v)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="pt-2 border-t border-slate-200 flex justify-end">
|
<div className="pt-2 border-t border-slate-200 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => onOpenSubscale(selectedTerm, 'right', rightSubscale)}
|
onClick={() => onOpenSubscale(selectedTerm, 'right', rightSubscale)}
|
||||||
className={`text-sm font-bold px-4 py-2 rounded-lg transition-all border ${rightSubscale ? 'bg-blue-50 text-blue-700 border-blue-200' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50'}`}
|
className={`text-sm font-bold px-4 py-2 rounded-lg transition-all border ${rightSubscale ? 'bg-blue-50 text-blue-700 border-blue-200' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50'}`}
|
||||||
>
|
>
|
||||||
@@ -72,4 +187,4 @@ export default function Controls({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,15 @@ import SubscaleModal from '../components/editor/SubscaleModal';
|
|||||||
import { calculateValueFunction, buildFuzzyGraph, saveToHistory } from '../services/docService';
|
import { calculateValueFunction, buildFuzzyGraph, saveToHistory } from '../services/docService';
|
||||||
import Step3FinalGraph from '../components/editor/Step3FinalGraph';
|
import Step3FinalGraph from '../components/editor/Step3FinalGraph';
|
||||||
|
|
||||||
|
// Step de la rejilla numérica de los puntos de la función de pertenencia.
|
||||||
|
// El <input type="number"> de Controls usa este mismo step (0.001), así que
|
||||||
|
// guardamos SIEMPRE en este grid: evita que valores 4-decimales del backend
|
||||||
|
// (p.ej. 0.3017) contaminen el estado, lo cual disparaba dos bugs en el UI:
|
||||||
|
// · stepMismatch en hover ("los valores válidos más aproximados son…")
|
||||||
|
// · flechas que no llegan al máximo (paraban en 0.9997 con max=1).
|
||||||
|
const MF_STEP = 0.001;
|
||||||
|
const snapToMfStep = (v) => Math.round(Number(v) / MF_STEP) * MF_STEP;
|
||||||
|
|
||||||
export default function DocEditor() {
|
export default function DocEditor() {
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -46,7 +55,10 @@ export default function DocEditor() {
|
|||||||
const baseResult = await calculateValueFunction(payloadBase);
|
const baseResult = await calculateValueFunction(payloadBase);
|
||||||
setBaseScale(baseResult.values);
|
setBaseScale(baseResult.values);
|
||||||
const initialMfs = {};
|
const initialMfs = {};
|
||||||
Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; });
|
Object.entries(baseResult.values).forEach(([name, value]) => {
|
||||||
|
const v = snapToMfStep(value);
|
||||||
|
initialMfs[name] = { supportStart: v, coreStart: v, coreEnd: v, supportEnd: v };
|
||||||
|
});
|
||||||
setMfDefinitions(initialMfs);
|
setMfDefinitions(initialMfs);
|
||||||
setSelectedTerm(Object.keys(baseResult.values)[0]);
|
setSelectedTerm(Object.keys(baseResult.values)[0]);
|
||||||
setStep(2);
|
setStep(2);
|
||||||
@@ -56,22 +68,24 @@ export default function DocEditor() {
|
|||||||
// MANEJADORES: FASE 2
|
// MANEJADORES: FASE 2
|
||||||
const updateCurrentMf = (field, value) => {
|
const updateCurrentMf = (field, value) => {
|
||||||
if (!selectedTerm) return;
|
if (!selectedTerm) return;
|
||||||
let numValue = parseFloat(value);
|
// Snap al grid de 3 decimales antes de hacer cualquier comparación,
|
||||||
|
// para que el estado nunca guarde más precisión que la que muestra el input.
|
||||||
|
let numValue = snapToMfStep(value);
|
||||||
setMfDefinitions(prev => {
|
setMfDefinitions(prev => {
|
||||||
const scaleKeys = Object.keys(baseScale);
|
const scaleKeys = Object.keys(baseScale);
|
||||||
const selectedIndex = scaleKeys.indexOf(selectedTerm);
|
const selectedIndex = scaleKeys.indexOf(selectedTerm);
|
||||||
let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1;
|
let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1;
|
||||||
|
|
||||||
if (selectedIndex > 0) {
|
if (selectedIndex > 0) {
|
||||||
prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd;
|
prevCoreEnd = snapToMfStep(prev[scaleKeys[selectedIndex - 1]].coreEnd);
|
||||||
prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd;
|
prevSupportEnd = snapToMfStep(prev[scaleKeys[selectedIndex - 1]].supportEnd);
|
||||||
}
|
}
|
||||||
if (selectedIndex < scaleKeys.length - 1) {
|
if (selectedIndex < scaleKeys.length - 1) {
|
||||||
nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart;
|
nextCoreStart = snapToMfStep(prev[scaleKeys[selectedIndex + 1]].coreStart);
|
||||||
nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart;
|
nextSupportStart = snapToMfStep(prev[scaleKeys[selectedIndex + 1]].supportStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchor = baseScale[selectedTerm];
|
const anchor = snapToMfStep(baseScale[selectedTerm]);
|
||||||
|
|
||||||
if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd;
|
if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd;
|
||||||
if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd;
|
if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
|||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { authService } from '../services/authService';
|
import { authService } from '../services/authService';
|
||||||
|
import { API_BASE_URL } from '../config';
|
||||||
import { FiEye, FiEyeOff } from 'react-icons/fi';
|
import { FiEye, FiEyeOff } from 'react-icons/fi';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
@@ -66,7 +67,7 @@ export default function Login() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGoogleLogin = () => {
|
const handleGoogleLogin = () => {
|
||||||
window.location.href = "http://localhost:8000/api/auth/google/login";
|
window.location.href = `${API_BASE_URL}/auth/google/login`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
allowedHosts: 'all'
|
allowedHosts: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user