Manuals inputs implemented

Feature/input manual
This commit is contained in:
Alexis López
2026-04-30 13:20:47 +02:00
committed by GitHub
11 changed files with 392 additions and 89 deletions
+7 -3
View File
@@ -3,10 +3,14 @@ __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/
+147 -37
View File
@@ -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>
+27
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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:
+22
View File
@@ -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
+5
View File
@@ -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,3 +1,104 @@
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({ export default function Controls({
selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf, selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf,
subscales, onOpenSubscale subscales, onOpenSubscale
@@ -7,13 +108,23 @@ 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>
@@ -23,18 +134,20 @@ export default function Controls({
<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
@@ -47,18 +160,20 @@ 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
+21 -7
View File
@@ -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 -1
View File
@@ -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 (
+1 -1
View File
@@ -10,6 +10,6 @@ export default defineConfig({
], ],
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
allowedHosts: 'all' allowedHosts: true
} }
}) })