From 9a3c40e30e993c84e1682ca5360096432d8c45f9 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 24 Mar 2026 13:19:37 +0100 Subject: [PATCH 01/20] =?UTF-8?q?add:=20a=C3=B1adir=20enrutado=20para=20se?= =?UTF-8?q?parar=20la=20secci=C3=B3n=20"basico"=20y=20"avanzado"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 58 ++++++++++++++++ frontend/package.json | 1 + frontend/src/App.jsx | 6 +- .../src/components/ValueFunctionChart.jsx | 66 +++++++++---------- frontend/src/components/layout/MainLayout.jsx | 49 ++++++++++++++ frontend/src/pages/AdvancedMode.jsx | 7 ++ frontend/src/routers/AppRouter.jsx | 21 ++++++ 7 files changed, 171 insertions(+), 37 deletions(-) create mode 100644 frontend/src/components/layout/MainLayout.jsx create mode 100644 frontend/src/routers/AppRouter.jsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0f2633d..c0cf46f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-router-dom": "^7.13.2", "recharts": "^3.8.0", "tailwindcss": "^4.2.2" }, @@ -1529,6 +1530,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3093,6 +3107,44 @@ } } }, + "node_modules/react-router": { + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", + "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/recharts": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", @@ -3209,6 +3261,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 278c666..debc10c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-router-dom": "^7.13.2", "recharts": "^3.8.0", "tailwindcss": "^4.2.2" }, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0545d76..df64ca3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,10 +1,8 @@ -import BasicMode from './pages/BasicMode'; +import { AppRouter } from './routers/AppRouter'; function App() { return ( -
- -
+ ); } diff --git a/frontend/src/components/ValueFunctionChart.jsx b/frontend/src/components/ValueFunctionChart.jsx index e496d46..dad2e8e 100644 --- a/frontend/src/components/ValueFunctionChart.jsx +++ b/frontend/src/components/ValueFunctionChart.jsx @@ -1,43 +1,43 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; export default function ValueFunctionChart({ result }) { - if (!result) return null; + if (!result) return null; return (
-

- Función de Valor: {result.criterion_name} -

+

+ Función de Valor: {result.criterion_name} +

-
- - ({ - nombre: label, - valor: value - }))} - margin={{ top: 20, right: 30, left: 20, bottom: 20 }} - > - - - - [value.toFixed(4), 'Valor DoC']} - labelStyle={{ fontWeight: 'bold', color: '#1e293b', marginBottom: '4px' }} - /> - - - -
+
+ + ({ + nombre: label, + valor: value + }))} + margin={{ top: 20, right: 30, left: 20, bottom: 20 }} + > + + + + [value.toFixed(4), 'Valor DoC']} + labelStyle={{ fontWeight: 'bold', color: '#1e293b', marginBottom: '4px' }} + /> + + + +
); } \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx new file mode 100644 index 0000000..bd75055 --- /dev/null +++ b/frontend/src/components/layout/MainLayout.jsx @@ -0,0 +1,49 @@ +import { Outlet, NavLink } from 'react-router-dom'; + +export const MainLayout = () => { + return ( +
+ +
+

+ Método Deck of Cards +

+
+ +
+
+ + `px-8 py-3 rounded-xl font-bold transition-all duration-300 ${ + isActive + ? 'bg-white text-blue-600 shadow-md transform scale-105' + : 'text-slate-500 hover:text-slate-700 hover:bg-slate-300/50' + }` + } + > + DoC Clásico + + + + `px-8 py-3 rounded-xl font-bold transition-all duration-300 ${ + isActive + ? 'bg-white text-blue-600 shadow-md transform scale-105' + : 'text-slate-500 hover:text-slate-700 hover:bg-slate-300/50' + }` + } + > + DoC-MF Avanzado + +
+
+ +
+ +
+ +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx index e69de29..3a96f2d 100644 --- a/frontend/src/pages/AdvancedMode.jsx +++ b/frontend/src/pages/AdvancedMode.jsx @@ -0,0 +1,7 @@ +export default function AdvancedMode() { + return ( +

+ En construcción +

+ ); +} \ No newline at end of file diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx new file mode 100644 index 0000000..b59dc41 --- /dev/null +++ b/frontend/src/routers/AppRouter.jsx @@ -0,0 +1,21 @@ +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import BasicMode from '../pages/BasicMode'; +import AdvancedMode from '../pages/AdvancedMode'; +import { MainLayout } from '../components/layout/MainLayout'; + +export const AppRouter = () => { + return ( + + + + }> + } /> + } /> + + + } /> + + + + ); +}; \ No newline at end of file From 5ba0fe6711a802db65d3a6ae1878c03b65ed134d Mon Sep 17 00:00:00 2001 From: Alexis Date: Wed, 25 Mar 2026 09:21:09 +0100 Subject: [PATCH 02/20] =?UTF-8?q?add:=20manejar=20errores=20para=20que=20n?= =?UTF-8?q?o=20lleguen=20datos=20vac=C3=ADos=20al=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/CardEditor.jsx | 67 ++++++++++-------- frontend/src/components/CriterionInput.jsx | 15 +++- frontend/src/components/layout/MainLayout.jsx | 2 +- frontend/src/pages/BasicMode.jsx | 70 ++++++++++++++----- 4 files changed, 104 insertions(+), 50 deletions(-) diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx index 77c0c73..a8fdcf7 100644 --- a/frontend/src/components/CardEditor.jsx +++ b/frontend/src/components/CardEditor.jsx @@ -1,33 +1,44 @@ -export default function CardEditor({ index, level, handleLevelChange, handleRemoveLevel, totalLevels }) { +export default function CardEditor({ index, level, handleLevelChange, handleRemoveLevel, totalLevels, error }) { return ( -
+
+
+ {/* Botón para eliminar */} + {totalLevels > 2 && ( + + )} + + {/* Detalles tipo naipe */} + {index + 1} + {index + 1} + + handleLevelChange(index, e.target.value)} + className={`w-4/5 text-center text-2xl font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${ + error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500' + }`} + /> +
+ + {/* Mensaje de error */} +
+ {error && ( +

+ Escribe una etiqueta +

+ )} +
- {/* Botón Eliminar */} - {totalLevels > 2 && ( - - )} - - {/* Detalles tipo naipe */} - - {index + 1} - - - {index + 1} - - - handleLevelChange(index, e.target.value)} - className="w-4/5 text-center text-2xl font-bold text-slate-700 bg-transparent border-b-2 border-dashed border-slate-300 focus:border-blue-500 outline-none pb-1" - />
); } \ No newline at end of file diff --git a/frontend/src/components/CriterionInput.jsx b/frontend/src/components/CriterionInput.jsx index 300c2de..bc15f39 100644 --- a/frontend/src/components/CriterionInput.jsx +++ b/frontend/src/components/CriterionInput.jsx @@ -1,4 +1,4 @@ -export default function CriterionInput({ criterionName, setCriterionName }) { +export default function CriterionInput({ criterionName, setCriterionName, error }) { return (
); } \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index bd75055..73cbad7 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -22,7 +22,7 @@ export const MainLayout = () => { }` } > - DoC Clásico + DoC Básico { + + let hasError = false; + const newErrors = { criterion: false, levels: Array(levels.length).fill(false) }; + + if (!criterionName.trim()) { + newErrors.criterion = true; + hasError = true; + } + + levels.forEach((level, idx) => { + if (!level.trim()) { + newErrors.levels[idx] = true; + hasError = true; + } + }); + + setErrors(newErrors); + + if (hasError) return; + setIsLoading(true); setResult(null); - const firstIndex = "0"; - const lastIndex = (levels.length - 1).toString(); - - const currentReferences = { - [firstIndex]: 0, - [lastIndex]: 1 - }; - const payload = { - criterion_name: criterionName || "Criterio sin nombre", - levels: levels, + criterion_name: criterionName.trim(), + levels: levels.map(l => l.trim()), blank_cards: blankCards, - references: currentReferences + references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; try { @@ -43,15 +57,27 @@ export default function BasicMode() { } }; + const handleCriterionChange = (val) => { + setCriterionName(val); + if (errors.criterion) setErrors({ ...errors, criterion: false }); + }; + const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); + + if (errors.levels[index]) { + const newErrLevels = [...errors.levels]; + newErrLevels[index] = false; + setErrors({ ...errors, levels: newErrLevels }); + } }; const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); + setErrors({ ...errors, levels: [...errors.levels, false] }); }; const handleRemoveLevel = (indexToRemove) => { @@ -59,8 +85,12 @@ export default function BasicMode() { const newLevels = levels.filter((_, index) => index !== indexToRemove); const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); + + const newErrLevels = errors.levels.filter((_, index) => index !== indexToRemove); + setLevels(newLevels); setBlankCards(newBlankCards); + setErrors({ ...errors, levels: newErrLevels }); }; const handleBlankCardChange = (index, delta) => { @@ -74,10 +104,11 @@ export default function BasicMode() { return (
- +
@@ -90,6 +121,7 @@ export default function BasicMode() { handleLevelChange={handleLevelChange} handleRemoveLevel={handleRemoveLevel} totalLevels={levels.length} + error={errors.levels[index]} /> {index < levels.length - 1 && ( @@ -107,18 +139,18 @@ export default function BasicMode() {
-); + ); } \ No newline at end of file From 8106f40d6386bb9bde37b8a8e9919ec82765756e Mon Sep 17 00:00:00 2001 From: Alexis Date: Wed, 25 Mar 2026 09:29:52 +0100 Subject: [PATCH 03/20] fixed: manejar que minimo haya 3 cartas de etiqueta --- frontend/src/components/CardEditor.jsx | 4 ++-- frontend/src/pages/BasicMode.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx index a8fdcf7..f197cf9 100644 --- a/frontend/src/components/CardEditor.jsx +++ b/frontend/src/components/CardEditor.jsx @@ -5,7 +5,7 @@ export default function CardEditor({ index, level, handleLevelChange, handleRemo error ? 'border-red-400 shadow-red-100' : 'border-slate-200' }`}> {/* Botón para eliminar */} - {totalLevels > 2 && ( + {totalLevels > 3 && (
- +
); } \ No newline at end of file diff --git a/frontend/src/pages/BasicMode.jsx b/frontend/src/pages/BasicMode.jsx index f162493..3d913e6 100644 --- a/frontend/src/pages/BasicMode.jsx +++ b/frontend/src/pages/BasicMode.jsx @@ -81,7 +81,7 @@ export default function BasicMode() { }; const handleRemoveLevel = (indexToRemove) => { - if (levels.length <= 2) return; + if (levels.length <= 3) return; const newLevels = levels.filter((_, index) => index !== indexToRemove); const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); From 3977e11ffb71a0497f9a71b9b7b2339007cdfdb7 Mon Sep 17 00:00:00 2001 From: Alexis Date: Wed, 25 Mar 2026 13:42:19 +0100 Subject: [PATCH 04/20] =?UTF-8?q?add:=20a=C3=B1adir=20funcionalidad=20avan?= =?UTF-8?q?zada=20en=20dos=20pasos:=20el=20primero=20calcula=20la=20escala?= =?UTF-8?q?,=20el=20segundo=20deja=20seleccionar=20el=20nucleo=20y=20el=20?= =?UTF-8?q?soporte=20de=20cada=20etiqueta=20de=20forma=20visual?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/AdvancedMode.jsx | 318 +++++++++++++++++++++++++++- 1 file changed, 315 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx index 3a96f2d..2a3b9a9 100644 --- a/frontend/src/pages/AdvancedMode.jsx +++ b/frontend/src/pages/AdvancedMode.jsx @@ -1,7 +1,319 @@ +import React, { useState } from 'react'; +import { ComposedChart, Line, XAxis, YAxis, CartesianGrid, ReferenceArea, ReferenceLine, ResponsiveContainer, Tooltip } from 'recharts'; +import CriterionInput from '../components/CriterionInput'; +import CardEditor from '../components/CardEditor'; +import BlankCardsCounter from '../components/BlankCardsCounter'; +import AddLevelButton from '../components/AddLevelButton'; +import { calculateValueFunction } from '../services/docService'; + +const COLORS = [ + '#ef4444', + '#f59e0b', + '#10b981', + '#3b82f6', + '#d946ef', + '#06b6d4', + '#8b5cf6', + '#f43f5e', + '#6366f1' +]; + export default function AdvancedMode() { + + const [step, setStep] = useState(1); + const [isLoading, setIsLoading] = useState(false); + + const [criterionName, setCriterionName] = useState(''); + const [levels, setLevels] = useState(['', '', '']); + const [blankCards, setBlankCards] = useState([0, 0]); + const [errors, setErrors] = useState({ criterion: false, levels: [] }); + + const [baseScale, setBaseScale] = useState({}); + const [selectedTerm, setSelectedTerm] = useState(null); + const [mfDefinitions, setMfDefinitions] = useState({}); + + const handleCriterionChange = (val) => { + setCriterionName(val); + if (errors.criterion) setErrors({ ...errors, criterion: false }); + }; + + const handleLevelChange = (index, newValue) => { + const newLevels = [...levels]; + newLevels[index] = newValue; + setLevels(newLevels); + if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); + }; + + const handleAddLevel = () => { + setLevels([...levels, '']); + setBlankCards([...blankCards, 0]); + setErrors({ ...errors, levels: [...errors.levels, false] }); + }; + + const handleRemoveLevel = (indexToRemove) => { + if (levels.length <= 3) return; + const newLevels = levels.filter((_, index) => index !== indexToRemove); + const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; + const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); + setLevels(newLevels); + setBlankCards(newBlankCards); + setErrors({ ...errors, levels: errors.levels.filter((_, index) => index !== indexToRemove) }); + }; + + const handleBlankCardChange = (index, delta) => { + const newBlankCards = [...blankCards]; + const newValue = newBlankCards[index] + delta; + if (newValue >= 0) { + newBlankCards[index] = newValue; + setBlankCards(newBlankCards); + } + }; + + const handleGenerateBaseScale = async () => { + let hasError = false; + const newErrors = { criterion: false, levels: Array(levels.length).fill(false) }; + if (!criterionName.trim()) { newErrors.criterion = true; hasError = true; } + levels.forEach((lvl, idx) => { if (!lvl.trim()) { newErrors.levels[idx] = true; hasError = true; }}); + setErrors(newErrors); + + if (hasError) return alert("Por favor, rellena todos los campos de la escala base."); + + setIsLoading(true); + + try { + const payloadBase = { + criterion_name: criterionName.trim(), + levels: levels.map(l => l.trim()), + blank_cards: blankCards, + references: { "0": 0, [(levels.length - 1).toString()]: 1 } + }; + + const baseResult = await calculateValueFunction(payloadBase); + const calculatedValues = baseResult.values; + setBaseScale(calculatedValues); + + const initialMfs = {}; + Object.entries(calculatedValues).forEach(([name, value]) => { + initialMfs[name] = { + supportStart: value, + coreStart: value, + coreEnd: value, + supportEnd: value + }; + }); + setMfDefinitions(initialMfs); + setSelectedTerm(Object.keys(calculatedValues)[0]); + setStep(2); + + } catch (error) { + alert("Error al conectar con el backend: " + error); + } finally { + setIsLoading(false); + } + }; + + const updateCurrentMf = (field, value) => { + if (!selectedTerm) return; + const numValue = parseFloat(value); + + setMfDefinitions(prev => { + const current = { ...prev[selectedTerm], [field]: numValue }; + + // Reglas de colisión internas + if (field === 'supportStart' && current.supportStart > current.coreStart) current.coreStart = current.supportStart; + if (field === 'coreStart') { + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + } + if (field === 'coreEnd') { + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } + if (field === 'supportEnd' && current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; + + return { ...prev, [selectedTerm]: current }; + }); + }; + + const handleFinalSubmit = () => { + console.log("DATOS FINALES LISTOS PARA EL BACKEND:", { + base_scale: baseScale, + membership_functions: mfDefinitions + }); + alert("¡Mira la consola! El JSON está preparado."); + }; + + // Cálculo de límites verticales + const scaleKeys = Object.keys(baseScale); + const selectedIndex = scaleKeys.indexOf(selectedTerm); + const selectedColor = COLORS[selectedIndex % COLORS.length] || '#2563eb'; + const currentMf = selectedTerm ? mfDefinitions[selectedTerm] : null; + + let minBound = 0; + let maxBound = 1; + + if (selectedIndex > 0) { + minBound = baseScale[scaleKeys[selectedIndex - 1]]; + } + if (selectedIndex >= 0 && selectedIndex < scaleKeys.length - 1) { + maxBound = baseScale[scaleKeys[selectedIndex + 1]]; + } + + return ( -

- En construcción -

+
+ + {/* Paso 1: Definir la escala */} + {step === 1 && ( +
+

+ Paso 1: Definir Escala de Referencia (Cartas) +

+ +
+ {levels.map((level, index) => ( +
+ 3} /> + {index < levels.length - 1 && ( + + )} +
+ ))} + +
+
+ +
+
+ )} + + {/* Paso 2: conceptos difusos */} + {step === 2 && ( +
+
+

Paso 2: Modelar Conceptos Difusos

+ +
+ + {/* Selectores de etiqueta */} +
+ {scaleKeys.map((name, index) => { + const val = baseScale[name]; + const color = COLORS[index % COLORS.length]; + const isSelected = selectedTerm === name; + return ( + + ); + })} +
+ + {/* Gráfica */} +
+ + + + + + typeof value === 'number' ? value.toFixed(2) : value} /> + + {scaleKeys.map((name, index) => { + const val = baseScale[name]; + const mf = mfDefinitions[name]; + if (!mf) return null; + + const color = COLORS[index % COLORS.length]; + const isSelected = selectedTerm === name; + + const trapezeData = [ + { x: mf.supportStart, y: 0 }, + { x: mf.coreStart, y: 1 }, + { x: mf.coreEnd, y: 1 }, + { x: mf.supportEnd, y: 0 }, + ]; + + return ( + + + + + + + + + + ); + })} + + +
+ + {/* Sliders con restricciones vecinales */} + {selectedTerm && currentMf && ( +
+
+

+ Ajustando franjas para: "{selectedTerm}" +

+ +
+
+
+ + updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} /> +
+
+ + updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} /> +
+
+
+
+ + updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} /> +
+
+ + updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} /> +
+
+
+
+ )} + +
+ +
+ +
+ )} +
); } \ No newline at end of file From 04a84e0f36117e8197494fe5b27cb82c4637dc52 Mon Sep 17 00:00:00 2001 From: Alexis Date: Wed, 25 Mar 2026 13:56:53 +0100 Subject: [PATCH 05/20] =?UTF-8?q?refactor:=20componentizar=20la=20gr=C3=A1?= =?UTF-8?q?fica=20y=20sliders=20del=20modo=20avanzado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/membershipFunction/Chart.jsx | 50 ++++ .../membershipFunction/Controls.jsx | 63 +++++ frontend/src/pages/AdvancedMode.jsx | 255 +++--------------- 3 files changed, 150 insertions(+), 218 deletions(-) create mode 100644 frontend/src/components/membershipFunction/Chart.jsx create mode 100644 frontend/src/components/membershipFunction/Controls.jsx diff --git a/frontend/src/components/membershipFunction/Chart.jsx b/frontend/src/components/membershipFunction/Chart.jsx new file mode 100644 index 0000000..ac96c84 --- /dev/null +++ b/frontend/src/components/membershipFunction/Chart.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { ComposedChart, Line, XAxis, YAxis, CartesianGrid, ReferenceArea, ReferenceLine, ResponsiveContainer, Tooltip } from 'recharts'; + +export default function MembershipFunctionChart({ baseScale, mfDefinitions, selectedTerm, colors }) { + const scaleKeys = Object.keys(baseScale); + + return ( +
+ + + + + + typeof value === 'number' ? value.toFixed(2) : value} /> + + {scaleKeys.map((name, index) => { + const val = baseScale[name]; + const mf = mfDefinitions[name]; + if (!mf) return null; + + const color = colors[index % colors.length]; + const isSelected = selectedTerm === name; + + const trapezeData = [ + { x: mf.supportStart, y: 0 }, + { x: mf.coreStart, y: 1 }, + { x: mf.coreEnd, y: 1 }, + { x: mf.supportEnd, y: 0 }, + ]; + + return ( + + + + + + + + + ); + })} + + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx new file mode 100644 index 0000000..6457562 --- /dev/null +++ b/frontend/src/components/membershipFunction/Controls.jsx @@ -0,0 +1,63 @@ +import React from 'react'; + +export default function MembershipFunctionControls({ selectedTerm, currentMf, selectedColor, baseScale, updateCurrentMf }) { + if (!selectedTerm || !currentMf) return null; + + const scaleKeys = Object.keys(baseScale); + const selectedIndex = scaleKeys.indexOf(selectedTerm); + + let minBound = 0; + let maxBound = 1; + + if (selectedIndex > 0) { + minBound = baseScale[scaleKeys[selectedIndex - 1]]; + } + if (selectedIndex >= 0 && selectedIndex < scaleKeys.length - 1) { + maxBound = baseScale[scaleKeys[selectedIndex + 1]]; + } + + return ( +
+
+

+ Ajustando franjas para: "{selectedTerm}" +

+ +
+ +
+
+ + updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} /> +
+ +
+ + updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} /> +
+
+ +
+
+ + updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} /> +
+ +
+ + updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} /> +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx index 2a3b9a9..9119856 100644 --- a/frontend/src/pages/AdvancedMode.jsx +++ b/frontend/src/pages/AdvancedMode.jsx @@ -1,186 +1,99 @@ import React, { useState } from 'react'; -import { ComposedChart, Line, XAxis, YAxis, CartesianGrid, ReferenceArea, ReferenceLine, ResponsiveContainer, Tooltip } from 'recharts'; import CriterionInput from '../components/CriterionInput'; import CardEditor from '../components/CardEditor'; import BlankCardsCounter from '../components/BlankCardsCounter'; import AddLevelButton from '../components/AddLevelButton'; +import MembershipFunctionChart from '../components/membershipFunction/Chart'; +import MembershipFunctionControls from '../components/membershipFunction/Controls'; import { calculateValueFunction } from '../services/docService'; -const COLORS = [ - '#ef4444', - '#f59e0b', - '#10b981', - '#3b82f6', - '#d946ef', - '#06b6d4', - '#8b5cf6', - '#f43f5e', - '#6366f1' -]; +const COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1']; export default function AdvancedMode() { - const [step, setStep] = useState(1); const [isLoading, setIsLoading] = useState(false); + // Estados Fase 1 (Escala) const [criterionName, setCriterionName] = useState(''); const [levels, setLevels] = useState(['', '', '']); const [blankCards, setBlankCards] = useState([0, 0]); const [errors, setErrors] = useState({ criterion: false, levels: [] }); + // Estados Fase 2 (Franjas) const [baseScale, setBaseScale] = useState({}); const [selectedTerm, setSelectedTerm] = useState(null); const [mfDefinitions, setMfDefinitions] = useState({}); - const handleCriterionChange = (val) => { - setCriterionName(val); - if (errors.criterion) setErrors({ ...errors, criterion: false }); - }; - - const handleLevelChange = (index, newValue) => { - const newLevels = [...levels]; - newLevels[index] = newValue; - setLevels(newLevels); - if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); - }; - - const handleAddLevel = () => { - setLevels([...levels, '']); - setBlankCards([...blankCards, 0]); - setErrors({ ...errors, levels: [...errors.levels, false] }); - }; - - const handleRemoveLevel = (indexToRemove) => { - if (levels.length <= 3) return; - const newLevels = levels.filter((_, index) => index !== indexToRemove); - const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; - const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); - setLevels(newLevels); - setBlankCards(newBlankCards); - setErrors({ ...errors, levels: errors.levels.filter((_, index) => index !== indexToRemove) }); - }; - - const handleBlankCardChange = (index, delta) => { - const newBlankCards = [...blankCards]; - const newValue = newBlankCards[index] + delta; - if (newValue >= 0) { - newBlankCards[index] = newValue; - setBlankCards(newBlankCards); - } - }; + // --- Manejadores de Escala --- + const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; + const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; + const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); }; + const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); }; + const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } }; const handleGenerateBaseScale = async () => { let hasError = false; - const newErrors = { criterion: false, levels: Array(levels.length).fill(false) }; - if (!criterionName.trim()) { newErrors.criterion = true; hasError = true; } - levels.forEach((lvl, idx) => { if (!lvl.trim()) { newErrors.levels[idx] = true; hasError = true; }}); - setErrors(newErrors); - - if (hasError) return alert("Por favor, rellena todos los campos de la escala base."); + const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) }; + if (newErrors.criterion || newErrors.levels.includes(true)) { setErrors(newErrors); return alert("Por favor, rellena todos los campos."); } setIsLoading(true); - try { - const payloadBase = { - criterion_name: criterionName.trim(), - levels: levels.map(l => l.trim()), - blank_cards: blankCards, - references: { "0": 0, [(levels.length - 1).toString()]: 1 } - }; - + const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; const baseResult = await calculateValueFunction(payloadBase); - const calculatedValues = baseResult.values; - setBaseScale(calculatedValues); - + + setBaseScale(baseResult.values); const initialMfs = {}; - Object.entries(calculatedValues).forEach(([name, value]) => { - initialMfs[name] = { - supportStart: value, - coreStart: value, - coreEnd: value, - supportEnd: value - }; - }); + Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; }); + setMfDefinitions(initialMfs); - setSelectedTerm(Object.keys(calculatedValues)[0]); + setSelectedTerm(Object.keys(baseResult.values)[0]); setStep(2); - - } catch (error) { - alert("Error al conectar con el backend: " + error); - } finally { - setIsLoading(false); - } + } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } }; + // --- Manejadores de Franjas --- const updateCurrentMf = (field, value) => { if (!selectedTerm) return; const numValue = parseFloat(value); setMfDefinitions(prev => { const current = { ...prev[selectedTerm], [field]: numValue }; - - // Reglas de colisión internas if (field === 'supportStart' && current.supportStart > current.coreStart) current.coreStart = current.supportStart; - if (field === 'coreStart') { - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; - } - if (field === 'coreEnd') { - if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - } + if (field === 'coreStart') { if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; } + if (field === 'coreEnd') { if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; } if (field === 'supportEnd' && current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; - return { ...prev, [selectedTerm]: current }; }); }; const handleFinalSubmit = () => { - console.log("DATOS FINALES LISTOS PARA EL BACKEND:", { - base_scale: baseScale, - membership_functions: mfDefinitions - }); - alert("¡Mira la consola! El JSON está preparado."); + console.log("PAYLOAD DOC-MF:", { base_scale: baseScale, membership_functions: mfDefinitions }); + alert("¡Mira la consola! JSON preparado."); }; - // Cálculo de límites verticales + // Variables calculadas const scaleKeys = Object.keys(baseScale); - const selectedIndex = scaleKeys.indexOf(selectedTerm); - const selectedColor = COLORS[selectedIndex % COLORS.length] || '#2563eb'; - const currentMf = selectedTerm ? mfDefinitions[selectedTerm] : null; + const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb'; - let minBound = 0; - let maxBound = 1; - - if (selectedIndex > 0) { - minBound = baseScale[scaleKeys[selectedIndex - 1]]; - } - if (selectedIndex >= 0 && selectedIndex < scaleKeys.length - 1) { - maxBound = baseScale[scaleKeys[selectedIndex + 1]]; - } - - return (
- {/* Paso 1: Definir la escala */} + {/* --- PASO 1 --- */} {step === 1 && (
-

- Paso 1: Definir Escala de Referencia (Cartas) -

+

Paso 1: Escala de Referencia (Cartas)

+
{levels.map((level, index) => (
3} /> - {index < levels.length - 1 && ( - - )} + {index < levels.length - 1 && }
))}
+
)} - {/* Paso 2: conceptos difusos */} + {/* --- PASO 2 --- */} {step === 2 && (
@@ -197,121 +110,27 @@ export default function AdvancedMode() {
- {/* Selectores de etiqueta */}
{scaleKeys.map((name, index) => { - const val = baseScale[name]; const color = COLORS[index % COLORS.length]; const isSelected = selectedTerm === name; return ( - ); })}
- {/* Gráfica */} -
- - - - - - typeof value === 'number' ? value.toFixed(2) : value} /> + - {scaleKeys.map((name, index) => { - const val = baseScale[name]; - const mf = mfDefinitions[name]; - if (!mf) return null; - - const color = COLORS[index % COLORS.length]; - const isSelected = selectedTerm === name; - - const trapezeData = [ - { x: mf.supportStart, y: 0 }, - { x: mf.coreStart, y: 1 }, - { x: mf.coreEnd, y: 1 }, - { x: mf.supportEnd, y: 0 }, - ]; - - return ( - - - - - - - - - - ); - })} - - -
- - {/* Sliders con restricciones vecinales */} - {selectedTerm && currentMf && ( -
-
-

- Ajustando franjas para: "{selectedTerm}" -

- -
-
-
- - updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} /> -
-
- - updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} /> -
-
-
-
- - updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} /> -
-
- - updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} /> -
-
-
-
- )} +
-
)}
From cf838837a0e5720b5bda1d6e20b6309d45aba653 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 26 Mar 2026 09:11:15 +0100 Subject: [PATCH 06/20] =?UTF-8?q?fix:=20arreglar=20que=20el=20n=C3=BAcleo?= =?UTF-8?q?=20de=20una=20etiqueta=20no=20pueda=20entrar=20en=20el=20soport?= =?UTF-8?q?e=20de=20sus=20etiquetas=20adyacentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/membershipFunction/Chart.jsx | 2 +- .../membershipFunction/Controls.jsx | 42 ++++++++++------ frontend/src/pages/AdvancedMode.jsx | 50 +++++++++++++++---- 3 files changed, 67 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/membershipFunction/Chart.jsx b/frontend/src/components/membershipFunction/Chart.jsx index ac96c84..7c0134b 100644 --- a/frontend/src/components/membershipFunction/Chart.jsx +++ b/frontend/src/components/membershipFunction/Chart.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { ComposedChart, Line, XAxis, YAxis, CartesianGrid, ReferenceArea, ReferenceLine, ResponsiveContainer, Tooltip } from 'recharts'; -export default function MembershipFunctionChart({ baseScale, mfDefinitions, selectedTerm, colors }) { +export default function Chart({ baseScale, mfDefinitions, selectedTerm, colors }) { const scaleKeys = Object.keys(baseScale); return ( diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx index 6457562..9b653ae 100644 --- a/frontend/src/components/membershipFunction/Controls.jsx +++ b/frontend/src/components/membershipFunction/Controls.jsx @@ -1,19 +1,23 @@ -import React from 'react'; - -export default function MembershipFunctionControls({ selectedTerm, currentMf, selectedColor, baseScale, updateCurrentMf }) { +export default function Controls({ selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf }) { if (!selectedTerm || !currentMf) return null; const scaleKeys = Object.keys(baseScale); const selectedIndex = scaleKeys.indexOf(selectedTerm); - let minBound = 0; - let maxBound = 1; + let prevCoreEnd = 0; + let prevSupportEnd = 0; + let nextCoreStart = 1; + let nextSupportStart = 1; if (selectedIndex > 0) { - minBound = baseScale[scaleKeys[selectedIndex - 1]]; + const prevTerm = scaleKeys[selectedIndex - 1]; + prevCoreEnd = mfDefinitions[prevTerm].coreEnd; + prevSupportEnd = mfDefinitions[prevTerm].supportEnd; } - if (selectedIndex >= 0 && selectedIndex < scaleKeys.length - 1) { - maxBound = baseScale[scaleKeys[selectedIndex + 1]]; + if (selectedIndex < scaleKeys.length - 1) { + const nextTerm = scaleKeys[selectedIndex + 1]; + nextCoreStart = mfDefinitions[nextTerm].coreStart; + nextSupportStart = mfDefinitions[nextTerm].supportStart; } return ( @@ -25,35 +29,41 @@ export default function MembershipFunctionControls({ selectedTerm, currentMf, se
+ {/* Columna izquierda: Inicios */}
- updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} /> + updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} />
- updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} /> + updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} />
+ {/* Columna derecha: Fines */}
- updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} /> + updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor }} />
- updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} /> + updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer" style={{ accentColor: selectedColor, opacity: 0.7 }} />
diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx index 9119856..fb0b57b 100644 --- a/frontend/src/pages/AdvancedMode.jsx +++ b/frontend/src/pages/AdvancedMode.jsx @@ -3,8 +3,8 @@ import CriterionInput from '../components/CriterionInput'; import CardEditor from '../components/CardEditor'; import BlankCardsCounter from '../components/BlankCardsCounter'; import AddLevelButton from '../components/AddLevelButton'; -import MembershipFunctionChart from '../components/membershipFunction/Chart'; -import MembershipFunctionControls from '../components/membershipFunction/Controls'; +import Chart from '../components/membershipFunction/Chart'; +import Controls from '../components/membershipFunction/Controls'; import { calculateValueFunction } from '../services/docService'; const COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1']; @@ -24,7 +24,7 @@ export default function AdvancedMode() { const [selectedTerm, setSelectedTerm] = useState(null); const [mfDefinitions, setMfDefinitions] = useState({}); - // --- Manejadores de Escala --- + // Manejadores de Escala const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); }; @@ -32,9 +32,11 @@ export default function AdvancedMode() { const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } }; const handleGenerateBaseScale = async () => { - let hasError = false; const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) }; - if (newErrors.criterion || newErrors.levels.includes(true)) { setErrors(newErrors); return alert("Por favor, rellena todos los campos."); } + if (newErrors.criterion || newErrors.levels.includes(true)) { + setErrors(newErrors); + return alert("Por favor, rellena todos los campos."); + } setIsLoading(true); try { @@ -51,17 +53,38 @@ export default function AdvancedMode() { } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } }; - // --- Manejadores de Franjas --- + // Manejadores de Franjas const updateCurrentMf = (field, value) => { if (!selectedTerm) return; - const numValue = parseFloat(value); + let numValue = parseFloat(value); setMfDefinitions(prev => { + const scaleKeys = Object.keys(baseScale); + const selectedIndex = scaleKeys.indexOf(selectedTerm); + + let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; + + if (selectedIndex > 0) { + prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd; + prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd; + } + if (selectedIndex < scaleKeys.length - 1) { + nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart; + nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart; + } + + if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd; + if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd; + if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart; + if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart; + const current = { ...prev[selectedTerm], [field]: numValue }; + if (field === 'supportStart' && current.supportStart > current.coreStart) current.coreStart = current.supportStart; if (field === 'coreStart') { if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; } if (field === 'coreEnd') { if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; } if (field === 'supportEnd' && current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; + return { ...prev, [selectedTerm]: current }; }); }; @@ -75,6 +98,7 @@ export default function AdvancedMode() { const scaleKeys = Object.keys(baseScale); const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb'; + return (
@@ -122,10 +146,16 @@ export default function AdvancedMode() { })}
- - - + +
+
); } \ No newline at end of file diff --git a/frontend/src/components/BlankCardsCounter.jsx b/frontend/src/components/BlankCardsCounter.jsx index 2cabe8d..3048c02 100644 --- a/frontend/src/components/BlankCardsCounter.jsx +++ b/frontend/src/components/BlankCardsCounter.jsx @@ -1,36 +1,40 @@ export default function BlankCardsCounter({ index, blankCardsCount, handleBlankCardChange }) { return ( -
-
+
-
- Blancas: +
+ + {/* Botones de - y + */} +
- - {blankCardsCount} - + +
+ Blancas + {blankCardsCount} +
+
+ {/* Cartas blancas */} {blankCardsCount > 0 && ( -
+
{Array.from({ length: blankCardsCount }).map((_, i) => (
))}
)} -
); } \ No newline at end of file diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx index f197cf9..efb5ad7 100644 --- a/frontend/src/components/CardEditor.jsx +++ b/frontend/src/components/CardEditor.jsx @@ -1,10 +1,9 @@ export default function CardEditor({ index, level, handleLevelChange, handleRemoveLevel, totalLevels, error }) { return ( -
-
+
- {/* Botón para eliminar */} {totalLevels > 3 && ( )} - {/* Detalles tipo naipe */} - {index + 1} - {index + 1} + {index + 1} + {index + 1} handleLevelChange(index, e.target.value)} - className={`w-4/5 text-center text-2xl font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${ + className={`w-10/12 text-center text-xl font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${ error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500' }`} />
- {/* Mensaje de error */} -
+
{error && (

Escribe una etiqueta

)}
-
); } \ No newline at end of file diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx index fb0b57b..1ed2711 100644 --- a/frontend/src/pages/AdvancedMode.jsx +++ b/frontend/src/pages/AdvancedMode.jsx @@ -19,6 +19,9 @@ export default function AdvancedMode() { const [blankCards, setBlankCards] = useState([0, 0]); const [errors, setErrors] = useState({ criterion: false, levels: [] }); + // Estado para controlar la lupa (Zoom) + const [isZoomActive, setIsZoomActive] = useState(true); + // Estados Fase 2 (Franjas) const [baseScale, setBaseScale] = useState({}); const [selectedTerm, setSelectedTerm] = useState(null); @@ -94,31 +97,68 @@ export default function AdvancedMode() { alert("¡Mira la consola! JSON preparado."); }; - // Variables calculadas const scaleKeys = Object.keys(baseScale); const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb'; + const totalElements = levels.length; + const dynamicScale = totalElements > 6 ? 6.2 / totalElements : 1; + const currentScale = isZoomActive ? Math.max(0.4, dynamicScale) : 1; return (
- {/* --- PASO 1 --- */} + {/* PASO 1 */} {step === 1 && ( -
-

Paso 1: Escala de Referencia (Cartas)

+
+ +
+

+ Paso 1: Escala de Referencia (Mesa) +

+ + {totalElements > 6 && ( + + )} +
+ -
- {levels.map((level, index) => ( -
- 3} /> - {index < levels.length - 1 && } -
- ))} - +
+ +
+ {levels.map((level, index) => ( + + 3} /> + + {index < levels.length - 1 && ( + + )} + + ))} + +
+ +
+
-
+
@@ -126,7 +166,7 @@ export default function AdvancedMode() {
)} - {/* --- PASO 2 --- */} + {/* PASO 2 */} {step === 2 && (
From 62070970c8cb3b724f586dcf143248cc0f6e0951 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 26 Mar 2026 13:06:19 +0100 Subject: [PATCH 08/20] fix: arreglar errores menores y ajustes visuales --- frontend/src/components/AddLevelButton.jsx | 11 +- frontend/src/components/BlankCardsCounter.jsx | 63 +++++---- frontend/src/components/CardEditor.jsx | 34 +---- frontend/src/components/layout/MainLayout.jsx | 55 ++------ .../components/membershipFunction/Chart.jsx | 29 ++-- .../membershipFunction/Controls.jsx | 53 +++---- frontend/src/pages/AdvancedMode.jsx | 132 ++++++++++-------- frontend/src/routers/AppRouter.jsx | 29 ++-- 8 files changed, 175 insertions(+), 231 deletions(-) diff --git a/frontend/src/components/AddLevelButton.jsx b/frontend/src/components/AddLevelButton.jsx index 555a169..67fa95f 100644 --- a/frontend/src/components/AddLevelButton.jsx +++ b/frontend/src/components/AddLevelButton.jsx @@ -1,14 +1,11 @@ export default function AddLevelButton({ handleAddLevel }) { return ( -
- -
+
); } \ No newline at end of file diff --git a/frontend/src/components/BlankCardsCounter.jsx b/frontend/src/components/BlankCardsCounter.jsx index 3048c02..b35b7ed 100644 --- a/frontend/src/components/BlankCardsCounter.jsx +++ b/frontend/src/components/BlankCardsCounter.jsx @@ -1,36 +1,51 @@ export default function BlankCardsCounter({ index, blankCardsCount, handleBlankCardChange }) { + + const maxCardsPerRow = 7; + const rows = []; + for (let i = 0; i < blankCardsCount; i += maxCardsPerRow) { + rows.push(Array.from({ length: Math.min(maxCardsPerRow, blankCardsCount - i) })); + } + return ( -
+
-
- - {/* Botones de - y + */} -
- +
-
- Blancas - {blankCardsCount} + {/* Línea conectora horizontal */} +
+ + {/* Botones - y + */} +
+ + +
+ Blancas + {blankCardsCount} +
+ +
- -
{/* Cartas blancas */} {blankCardsCount > 0 && ( -
- {Array.from({ length: blankCardsCount }).map((_, i) => ( -
+
+ {rows.map((row, rowIndex) => ( +
+ {row.map((_, colIndex) => ( +
+ ))} +
))}
)} diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx index efb5ad7..c5e887f 100644 --- a/frontend/src/components/CardEditor.jsx +++ b/frontend/src/components/CardEditor.jsx @@ -1,40 +1,18 @@ + export default function CardEditor({ index, level, handleLevelChange, handleRemoveLevel, totalLevels, error }) { return ( -
-
+
{totalLevels > 3 && ( - + )} - {index + 1} {index + 1} - - handleLevelChange(index, e.target.value)} - className={`w-10/12 text-center text-xl font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${ - error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500' - }`} - /> -
- -
- {error && ( -

- Escribe una etiqueta -

- )} + handleLevelChange(index, e.target.value)} className={`w-10/12 text-center text-lg font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500'}`} />
+
{error &&

Escribe una etiqueta

}
); } \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index 73cbad7..fc1996d 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -1,49 +1,24 @@ -import { Outlet, NavLink } from 'react-router-dom'; +import { Outlet } from 'react-router-dom'; -export const MainLayout = () => { +export default function MainLayout() { return ( -
- -
-

- Método Deck of Cards +
+ {/* Cabecera */} +
+
+
+ DoC +
+

+ Deck of Cards Method

+
-
-
- - `px-8 py-3 rounded-xl font-bold transition-all duration-300 ${ - isActive - ? 'bg-white text-blue-600 shadow-md transform scale-105' - : 'text-slate-500 hover:text-slate-700 hover:bg-slate-300/50' - }` - } - > - DoC Básico - - - - `px-8 py-3 rounded-xl font-bold transition-all duration-300 ${ - isActive - ? 'bg-white text-blue-600 shadow-md transform scale-105' - : 'text-slate-500 hover:text-slate-700 hover:bg-slate-300/50' - }` - } - > - DoC-MF Avanzado - -
-
- -
+ {/* Contenido principal */} +
-
); -}; \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/components/membershipFunction/Chart.jsx b/frontend/src/components/membershipFunction/Chart.jsx index 7c0134b..9844798 100644 --- a/frontend/src/components/membershipFunction/Chart.jsx +++ b/frontend/src/components/membershipFunction/Chart.jsx @@ -5,41 +5,28 @@ export default function Chart({ baseScale, mfDefinitions, selectedTerm, colors } const scaleKeys = Object.keys(baseScale); return ( -
- - +
+ + - - + + typeof value === 'number' ? value.toFixed(2) : value} /> {scaleKeys.map((name, index) => { const val = baseScale[name]; const mf = mfDefinitions[name]; if (!mf) return null; - const color = colors[index % colors.length]; const isSelected = selectedTerm === name; - - const trapezeData = [ - { x: mf.supportStart, y: 0 }, - { x: mf.coreStart, y: 1 }, - { x: mf.coreEnd, y: 1 }, - { x: mf.supportEnd, y: 0 }, - ]; + const trapezeData = [ { x: mf.supportStart, y: 0 }, { x: mf.coreStart, y: 1 }, { x: mf.coreEnd, y: 1 }, { x: mf.supportEnd, y: 0 } ]; return ( - - + - - + ); })} diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx index 9b653ae..10ff96b 100644 --- a/frontend/src/components/membershipFunction/Controls.jsx +++ b/frontend/src/components/membershipFunction/Controls.jsx @@ -4,10 +4,7 @@ export default function Controls({ selectedTerm, currentMf, selectedColor, baseS const scaleKeys = Object.keys(baseScale); const selectedIndex = scaleKeys.indexOf(selectedTerm); - let prevCoreEnd = 0; - let prevSupportEnd = 0; - let nextCoreStart = 1; - let nextSupportStart = 1; + let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; if (selectedIndex > 0) { const prevTerm = scaleKeys[selectedIndex - 1]; @@ -21,52 +18,42 @@ export default function Controls({ selectedTerm, currentMf, selectedColor, baseS } return ( -
-
-

- Ajustando franjas para: "{selectedTerm}" +
+
+

+ Ajustando: "{selectedTerm}"

-
- - {/* Columna izquierda: Inicios */} -
+
+
-
-
-
- {/* Columna derecha: Fines */} -
+
-
-
-
-
); diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx index 1ed2711..8ae287b 100644 --- a/frontend/src/pages/AdvancedMode.jsx +++ b/frontend/src/pages/AdvancedMode.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import CriterionInput from '../components/CriterionInput'; import CardEditor from '../components/CardEditor'; import BlankCardsCounter from '../components/BlankCardsCounter'; @@ -22,6 +22,28 @@ export default function AdvancedMode() { // Estado para controlar la lupa (Zoom) const [isZoomActive, setIsZoomActive] = useState(true); + // Medición exacta con Refs + const containerRef = useRef(null); + const tableRef = useRef(null); + const [dimensions, setDimensions] = useState({ container: 1000, table: 0 }); + + useEffect(() => { + const updateMeasurements = () => { + if (containerRef.current && tableRef.current) { + setDimensions({ + container: containerRef.current.offsetWidth, + table: tableRef.current.scrollWidth + }); + } + }; + const timeoutId = setTimeout(updateMeasurements, 50); + window.addEventListener('resize', updateMeasurements); + return () => { + clearTimeout(timeoutId); + window.removeEventListener('resize', updateMeasurements); + }; + }, [levels, blankCards, step]); + // Estados Fase 2 (Franjas) const [baseScale, setBaseScale] = useState({}); const [selectedTerm, setSelectedTerm] = useState(null); @@ -56,7 +78,6 @@ export default function AdvancedMode() { } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } }; - // Manejadores de Franjas const updateCurrentMf = (field, value) => { if (!selectedTerm) return; let numValue = parseFloat(value); @@ -64,7 +85,6 @@ export default function AdvancedMode() { setMfDefinitions(prev => { const scaleKeys = Object.keys(baseScale); const selectedIndex = scaleKeys.indexOf(selectedTerm); - let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; if (selectedIndex > 0) { @@ -100,66 +120,62 @@ export default function AdvancedMode() { const scaleKeys = Object.keys(baseScale); const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb'; - const totalElements = levels.length; - const dynamicScale = totalElements > 6 ? 6.2 / totalElements : 1; - const currentScale = isZoomActive ? Math.max(0.4, dynamicScale) : 1; + const needsZoom = dimensions.table > dimensions.container; + const dynamicScale = needsZoom ? (dimensions.container / dimensions.table) * 0.95 : 1; + const currentScale = isZoomActive && needsZoom ? dynamicScale : 1; return (
{/* PASO 1 */} {step === 1 && ( -
+
-
-

+
+

Paso 1: Escala de Referencia (Mesa)

- - {totalElements > 6 && ( - )}
-
- -
- {levels.map((level, index) => ( - - 3} /> - - {index < levels.length - 1 && ( - - )} - - ))} +
+
-
- -
+
+ + {levels.map((level, index) => ( + + +
+ 3} /> +
+ + {index < levels.length - 1 && ( + + )} +
+ ))} + +
+
+
+ + +
+ +
-
-
@@ -168,19 +184,19 @@ export default function AdvancedMode() { {/* PASO 2 */} {step === 2 && ( -
-
-

Paso 2: Modelar Conceptos Difusos

- +
+
+

Paso 2: Modelar Conceptos Difusos

+
-
+
{scaleKeys.map((name, index) => { const color = COLORS[index % COLORS.length]; const isSelected = selectedTerm === name; return ( - ); })} @@ -188,16 +204,10 @@ export default function AdvancedMode() { - -
-
diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx index b59dc41..3259cfb 100644 --- a/frontend/src/routers/AppRouter.jsx +++ b/frontend/src/routers/AppRouter.jsx @@ -1,21 +1,16 @@ -import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; -import BasicMode from '../pages/BasicMode'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import MainLayout from '../components/layout/MainLayout'; import AdvancedMode from '../pages/AdvancedMode'; -import { MainLayout } from '../components/layout/MainLayout'; -export const AppRouter = () => { +export function AppRouter() { return ( - - - - }> - } /> - } /> - - - } /> - - - + + + }> + } /> + } /> + + + ); -}; \ No newline at end of file +} \ No newline at end of file From 23b80def5ad16bda2d096753f9be55e5712302c2 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 26 Mar 2026 13:08:49 +0100 Subject: [PATCH 09/20] fix: arreglar bug visual en el zoom --- frontend/src/pages/AdvancedMode.jsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx index 8ae287b..81aeb4a 100644 --- a/frontend/src/pages/AdvancedMode.jsx +++ b/frontend/src/pages/AdvancedMode.jsx @@ -13,16 +13,13 @@ export default function AdvancedMode() { const [step, setStep] = useState(1); const [isLoading, setIsLoading] = useState(false); - // Estados Fase 1 (Escala) const [criterionName, setCriterionName] = useState(''); const [levels, setLevels] = useState(['', '', '']); const [blankCards, setBlankCards] = useState([0, 0]); const [errors, setErrors] = useState({ criterion: false, levels: [] }); - // Estado para controlar la lupa (Zoom) const [isZoomActive, setIsZoomActive] = useState(true); - // Medición exacta con Refs const containerRef = useRef(null); const tableRef = useRef(null); const [dimensions, setDimensions] = useState({ container: 1000, table: 0 }); @@ -136,7 +133,13 @@ export default function AdvancedMode() { Paso 1: Escala de Referencia (Mesa)

{needsZoom && ( - @@ -152,15 +155,12 @@ export default function AdvancedMode() { {levels.map((level, index) => ( -
3} />
- {index < levels.length - 1 && ( )} -
))} From ddddfaef73a066de7da6fa3bb5744f364959eea3 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 26 Mar 2026 13:19:06 +0100 Subject: [PATCH 10/20] =?UTF-8?q?fix:=20arreglar=20tama=C3=B1o=20del=20Cri?= =?UTF-8?q?terionInput=20y=20m=C3=A1rgenes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/CriterionInput.jsx | 42 +++++++++++----------- frontend/src/pages/AdvancedMode.jsx | 5 ++- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/CriterionInput.jsx b/frontend/src/components/CriterionInput.jsx index bc15f39..b636ede 100644 --- a/frontend/src/components/CriterionInput.jsx +++ b/frontend/src/components/CriterionInput.jsx @@ -1,27 +1,29 @@ export default function CriterionInput({ criterionName, setCriterionName, error }) { return ( -
-

diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx index 10ff96b..fe7e785 100644 --- a/frontend/src/components/membershipFunction/Controls.jsx +++ b/frontend/src/components/membershipFunction/Controls.jsx @@ -28,13 +28,13 @@ export default function Controls({ selectedTerm, currentMf, selectedColor, baseS
updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} />
updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
@@ -43,13 +43,13 @@ export default function Controls({ selectedTerm, currentMf, selectedColor, baseS
updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} />
updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx index 04f9a54..88a5a83 100644 --- a/frontend/src/pages/AdvancedMode.jsx +++ b/frontend/src/pages/AdvancedMode.jsx @@ -13,16 +13,13 @@ export default function AdvancedMode() { const [step, setStep] = useState(1); const [isLoading, setIsLoading] = useState(false); - // Estados Fase 1 (Escala) const [criterionName, setCriterionName] = useState(''); const [levels, setLevels] = useState(['', '', '']); const [blankCards, setBlankCards] = useState([0, 0]); const [errors, setErrors] = useState({ criterion: false, levels: [] }); - // Estado para controlar la lupa (Zoom) const [isZoomActive, setIsZoomActive] = useState(true); - // Medición exacta con Refs const containerRef = useRef(null); const tableRef = useRef(null); const [dimensions, setDimensions] = useState({ container: 1000, table: 0 }); @@ -96,17 +93,35 @@ export default function AdvancedMode() { nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart; } + const anchor = baseScale[selectedTerm]; + if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd; if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd; if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart; if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart; + if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor; + if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor; + const current = { ...prev[selectedTerm], [field]: numValue }; - - if (field === 'supportStart' && current.supportStart > current.coreStart) current.coreStart = current.supportStart; - if (field === 'coreStart') { if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; } - if (field === 'coreEnd') { if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; } - if (field === 'supportEnd' && current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; + + if (field === 'supportStart') { + if (current.supportStart > current.coreStart) current.coreStart = current.supportStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreStart') { + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreEnd') { + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } else if (field === 'supportEnd') { + if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } return { ...prev, [selectedTerm]: current }; }); @@ -133,7 +148,7 @@ export default function AdvancedMode() {

- Paso 1: Escala de Referencia (Mesa) + Paso 1: Establecer escala

{needsZoom && ( + )} +
+ + + +
+
+ +
+ {levels.map((level, index) => ( + +
+ 3} /> +
+ {index < levels.length - 1 && ( + + )} +
+ ))} +
+
+
+ +
+ +
+
+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/editor/Step2FuzzyModeling.jsx b/frontend/src/components/editor/Step2FuzzyModeling.jsx new file mode 100644 index 0000000..8412d36 --- /dev/null +++ b/frontend/src/components/editor/Step2FuzzyModeling.jsx @@ -0,0 +1,43 @@ +import Chart from '../membershipFunction/Chart'; +import Controls from '../membershipFunction/Controls'; + +const COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1']; + +export default function Step2FuzzyModeling({ + baseScale, mfDefinitions, selectedTerm, setSelectedTerm, updateCurrentMf, handleFinalSubmit, onBack +}) { + const scaleKeys = Object.keys(baseScale); + const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb'; + + return ( +
+
+

Paso 2: Modelar Conceptos Difusos

+ +
+ +
+ {scaleKeys.map((name, index) => { + const color = COLORS[index % COLORS.length]; + const isSelected = selectedTerm === name; + return ( + + ); + })} +
+ + + + + +
+ +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx new file mode 100644 index 0000000..3baa1f2 --- /dev/null +++ b/frontend/src/pages/DocEditor.jsx @@ -0,0 +1,129 @@ +import { useState } from 'react'; +import Step1BaseScale from '../components/editor/Step1BaseScale'; +import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; +import { calculateValueFunction } from '../services/docService'; + +export default function DocEditor() { + const [step, setStep] = useState(1); + const [isLoading, setIsLoading] = useState(false); + + // ESTADOS: FASE 1 + const [criterionName, setCriterionName] = useState(''); + const [levels, setLevels] = useState(['', '', '']); + const [blankCards, setBlankCards] = useState([0, 0]); + const [errors, setErrors] = useState({ criterion: false, levels: [] }); + + // ESTADOS: FASE 2 + const [baseScale, setBaseScale] = useState({}); + const [selectedTerm, setSelectedTerm] = useState(null); + const [mfDefinitions, setMfDefinitions] = useState({}); + + // MANEJADORES: FASE 1 + const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; + const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; + const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); }; + const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); }; + const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } }; + + const handleGenerateBaseScale = async () => { + const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) }; + if (newErrors.criterion || newErrors.levels.includes(true)) { + setErrors(newErrors); + return alert("Por favor, rellena todos los campos."); + } + + setIsLoading(true); + try { + const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; + const baseResult = await calculateValueFunction(payloadBase); + + setBaseScale(baseResult.values); + const initialMfs = {}; + Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; }); + + setMfDefinitions(initialMfs); + setSelectedTerm(Object.keys(baseResult.values)[0]); + setStep(2); + } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } + }; + + // MANEJADORES: FASE 2 + const updateCurrentMf = (field, value) => { + if (!selectedTerm) return; + let numValue = parseFloat(value); + + setMfDefinitions(prev => { + const scaleKeys = Object.keys(baseScale); + const selectedIndex = scaleKeys.indexOf(selectedTerm); + let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; + + if (selectedIndex > 0) { + prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd; + prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd; + } + if (selectedIndex < scaleKeys.length - 1) { + nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart; + nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart; + } + + const anchor = baseScale[selectedTerm]; + + if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd; + if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd; + if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart; + if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart; + + if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor; + if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor; + + const current = { ...prev[selectedTerm], [field]: numValue }; + + if (field === 'supportStart') { + if (current.supportStart > current.coreStart) current.coreStart = current.supportStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreStart') { + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreEnd') { + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } else if (field === 'supportEnd') { + if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } + + return { ...prev, [selectedTerm]: current }; + }); + }; + + const handleFinalSubmit = () => { + console.log("PAYLOAD DOC-MF:", { baseScale, mfDefinitions }); + alert("¡Mira la consola! JSON preparado."); + }; + + return ( +
+ {step === 1 && ( + + )} + {step === 2 && ( + setStep(1)} + /> + )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx index 3259cfb..5cc0534 100644 --- a/frontend/src/routers/AppRouter.jsx +++ b/frontend/src/routers/AppRouter.jsx @@ -1,13 +1,13 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import MainLayout from '../components/layout/MainLayout'; -import AdvancedMode from '../pages/AdvancedMode'; +import DocEditor from '../pages/DocEditor'; export function AppRouter() { return ( }> - } /> + } /> } /> From 5444894bbfdba56ddff4b2b849b9dc6669f7953f Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 10:36:37 +0100 Subject: [PATCH 14/20] =?UTF-8?q?add:=20a=C3=B1adir=20modal=20emergente=20?= =?UTF-8?q?para=20gestionar=20subescalas=20con=20cartas=20dentro=20de=20ca?= =?UTF-8?q?da=20funci=C3=B3n.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/editor/Step2FuzzyModeling.jsx | 25 ++- .../src/components/editor/SubscaleModal.jsx | 96 +++++++++ .../membershipFunction/Controls.jsx | 65 ++++-- frontend/src/pages/DocEditor.jsx | 200 ++++++++++-------- 4 files changed, 271 insertions(+), 115 deletions(-) create mode 100644 frontend/src/components/editor/SubscaleModal.jsx diff --git a/frontend/src/components/editor/Step2FuzzyModeling.jsx b/frontend/src/components/editor/Step2FuzzyModeling.jsx index 8412d36..ee9841e 100644 --- a/frontend/src/components/editor/Step2FuzzyModeling.jsx +++ b/frontend/src/components/editor/Step2FuzzyModeling.jsx @@ -3,14 +3,13 @@ import Controls from '../membershipFunction/Controls'; const COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1']; -export default function Step2FuzzyModeling({ - baseScale, mfDefinitions, selectedTerm, setSelectedTerm, updateCurrentMf, handleFinalSubmit, onBack -}) { +export default function Step2FuzzyModeling({baseScale, mfDefinitions, selectedTerm, setSelectedTerm, updateCurrentMf, handleFinalSubmit, onBack, subscales, onOpenSubscale}) { const scaleKeys = Object.keys(baseScale); const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb'; return (
+

Paso 2: Modelar Conceptos Difusos

@@ -28,13 +27,27 @@ export default function Step2FuzzyModeling({ })}
- + - +
diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx new file mode 100644 index 0000000..70bb4ce --- /dev/null +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import BlankCardsCounter from '../BlankCardsCounter'; + +export default function SubscaleModal({ onClose, onSave, targetInfo }) { + + const [cardsCount, setCardsCount] = useState(targetInfo?.initialData?.cardsCount || 2); + const [blankCards, setBlankCards] = useState(targetInfo?.initialData?.blankCards || [0]); + + const handleAddCard = () => { + setCardsCount(prev => prev + 1); + setBlankCards([...blankCards, 0]); + }; + + const handleRemoveCard = () => { + if (cardsCount <= 2) return; + setCardsCount(prev => prev - 1); + setBlankCards(blankCards.slice(0, -1)); + }; + + const handleBlankCardChange = (index, delta) => { + const newBlanks = [...blankCards]; + if (newBlanks[index] + delta >= 0) { + newBlanks[index] += delta; + setBlankCards(newBlanks); + } + }; + + const handleSave = () => { + onSave(targetInfo.term, targetInfo.side, { cardsCount, blankCards }); + }; + + const handleDelete = () => { + onSave(targetInfo.term, targetInfo.side, null); + }; + + return ( +
+
+ +
+
+

Diseñar Subescala

+

+ Ajustando pendiente {targetInfo.side === 'left' ? 'Izquierda (Ascendente)' : 'Derecha (Descendente)'} del término "{targetInfo.term}" +

+
+ +
+ + {/* Tablero */} +
+
+ {Array.from({ length: cardsCount }).map((_, index) => ( + +
+
+ {cardsCount > 2 && index === cardsCount - 1 && ( + + )} + {index + 1} +
+
+ {index < cardsCount - 1 && ( + + )} +
+ ))} + +
+ +
+
+
+ + {/* Botones */} +
+ + +
+ + +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx index fe7e785..f879c4a 100644 --- a/frontend/src/components/membershipFunction/Controls.jsx +++ b/frontend/src/components/membershipFunction/Controls.jsx @@ -1,21 +1,18 @@ -export default function Controls({ selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf }) { +export default function Controls({ + selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf, + subscales, onOpenSubscale +}) { if (!selectedTerm || !currentMf) return null; const scaleKeys = Object.keys(baseScale); const selectedIndex = scaleKeys.indexOf(selectedTerm); - let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; + let absoluteMin = 0, absoluteMax = 1; + if (selectedIndex > 0) absoluteMin = mfDefinitions[scaleKeys[selectedIndex - 1]].coreEnd; + if (selectedIndex < scaleKeys.length - 1) absoluteMax = mfDefinitions[scaleKeys[selectedIndex + 1]].coreStart; - if (selectedIndex > 0) { - const prevTerm = scaleKeys[selectedIndex - 1]; - prevCoreEnd = mfDefinitions[prevTerm].coreEnd; - prevSupportEnd = mfDefinitions[prevTerm].supportEnd; - } - if (selectedIndex < scaleKeys.length - 1) { - const nextTerm = scaleKeys[selectedIndex + 1]; - nextCoreStart = mfDefinitions[nextTerm].coreStart; - nextSupportStart = mfDefinitions[nextTerm].supportStart; - } + const leftSubscale = subscales?.[selectedTerm]?.left; + const rightSubscale = subscales?.[selectedTerm]?.right; return (
@@ -24,34 +21,56 @@ export default function Controls({ selectedTerm, currentMf, selectedColor, baseS Ajustando: "{selectedTerm}" -
-
+
+ {/* Lado izquierdo (Pendiente ascendente) */} +
- updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> + updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
- updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> + updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> +
+ + {/* Botón subescala izquierda */} +
+
-
+ {/* Lado derecho (Pendiente descendente) */} +
- updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> + updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} />
- updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> + updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> +
+ + {/* Botón subescala derecha */} +
+
diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 3baa1f2..34a24c9 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -1,109 +1,126 @@ import { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; +import SubscaleModal from '../components/editor/SubscaleModal'; // <-- IMPORTAMOS EL MODAL import { calculateValueFunction } from '../services/docService'; export default function DocEditor() { - const [step, setStep] = useState(1); - const [isLoading, setIsLoading] = useState(false); + const [step, setStep] = useState(1); + const [isLoading, setIsLoading] = useState(false); - // ESTADOS: FASE 1 - const [criterionName, setCriterionName] = useState(''); - const [levels, setLevels] = useState(['', '', '']); - const [blankCards, setBlankCards] = useState([0, 0]); - const [errors, setErrors] = useState({ criterion: false, levels: [] }); + // ESTADOS: FASE 1 + const [criterionName, setCriterionName] = useState(''); + const [levels, setLevels] = useState(['', '', '']); + const [blankCards, setBlankCards] = useState([0, 0]); + const [errors, setErrors] = useState({ criterion: false, levels: [] }); - // ESTADOS: FASE 2 - const [baseScale, setBaseScale] = useState({}); - const [selectedTerm, setSelectedTerm] = useState(null); - const [mfDefinitions, setMfDefinitions] = useState({}); + // ESTADOS: FASE 2 + const [baseScale, setBaseScale] = useState({}); + const [selectedTerm, setSelectedTerm] = useState(null); + const [mfDefinitions, setMfDefinitions] = useState({}); - // MANEJADORES: FASE 1 - const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; - const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; - const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); }; - const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); }; - const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } }; + // ESTADOS: SUBESCALAS (FASE 2.5) + // Formato: { "regular": { left: { cardsCount: 3, blankCards: [1, 0] }, right: null }, "bueno": ... } + const [subscales, setSubscales] = useState({}); + const [modalTarget, setModalTarget] = useState(null); // { term: 'regular', side: 'left', initialData: {...} } - const handleGenerateBaseScale = async () => { - const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) }; - if (newErrors.criterion || newErrors.levels.includes(true)) { - setErrors(newErrors); - return alert("Por favor, rellena todos los campos."); - } + // MANEJADORES: FASE 1 + const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; + const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; + const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); }; + const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); }; + const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } }; - setIsLoading(true); - try { - const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; - const baseResult = await calculateValueFunction(payloadBase); - - setBaseScale(baseResult.values); - const initialMfs = {}; - Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; }); - - setMfDefinitions(initialMfs); - setSelectedTerm(Object.keys(baseResult.values)[0]); - setStep(2); - } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } - }; + const handleGenerateBaseScale = async () => { + const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) }; + if (newErrors.criterion || newErrors.levels.includes(true)) { + setErrors(newErrors); + return alert("Por favor, rellena todos los campos."); + } - // MANEJADORES: FASE 2 - const updateCurrentMf = (field, value) => { - if (!selectedTerm) return; - let numValue = parseFloat(value); - - setMfDefinitions(prev => { - const scaleKeys = Object.keys(baseScale); - const selectedIndex = scaleKeys.indexOf(selectedTerm); - let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; + setIsLoading(true); + try { + const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; + const baseResult = await calculateValueFunction(payloadBase); + setBaseScale(baseResult.values); + const initialMfs = {}; + Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; }); + setMfDefinitions(initialMfs); + setSelectedTerm(Object.keys(baseResult.values)[0]); + setStep(2); + } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } + }; - if (selectedIndex > 0) { - prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd; - prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd; - } - if (selectedIndex < scaleKeys.length - 1) { - nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart; - nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart; - } + // MANEJADORES: FASE 2 + const updateCurrentMf = (field, value) => { + if (!selectedTerm) return; + let numValue = parseFloat(value); + setMfDefinitions(prev => { + const scaleKeys = Object.keys(baseScale); + const selectedIndex = scaleKeys.indexOf(selectedTerm); + let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; - const anchor = baseScale[selectedTerm]; + if (selectedIndex > 0) { + prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd; + prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd; + } + if (selectedIndex < scaleKeys.length - 1) { + nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart; + nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart; + } - if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd; - if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd; - if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart; - if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart; + const anchor = baseScale[selectedTerm]; - if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor; - if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor; + if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd; + if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd; + if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart; + if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart; + if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor; + if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor; - const current = { ...prev[selectedTerm], [field]: numValue }; + const current = { ...prev[selectedTerm], [field]: numValue }; - if (field === 'supportStart') { - if (current.supportStart > current.coreStart) current.coreStart = current.supportStart; - if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - } else if (field === 'coreStart') { - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - } else if (field === 'coreEnd') { - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - } else if (field === 'supportEnd') { - if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; - if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - } + if (field === 'supportStart') { + if (current.supportStart > current.coreStart) current.coreStart = current.supportStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreStart') { + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreEnd') { + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } else if (field === 'supportEnd') { + if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } + return { ...prev, [selectedTerm]: current }; + }); + }; - return { ...prev, [selectedTerm]: current }; - }); - }; + // MANEJADORES: SUBESCALAS + const handleOpenSubscale = (term, side, initialData) => { + setModalTarget({ term, side, initialData }); + }; - const handleFinalSubmit = () => { - console.log("PAYLOAD DOC-MF:", { baseScale, mfDefinitions }); - alert("¡Mira la consola! JSON preparado."); - }; + const handleSaveSubscale = (term, side, data) => { + setSubscales(prev => ({ + ...prev, + [term]: { + ...prev[term], + [side]: data + } + })); + setModalTarget(null); + }; + + const handleFinalSubmit = () => { + console.log("PAYLOAD DOC-MF COMPLETO:", { baseScale, mfDefinitions, subscales }); + alert("JSON en consola."); + }; return (
@@ -122,6 +139,17 @@ export default function DocEditor() { selectedTerm={selectedTerm} setSelectedTerm={setSelectedTerm} updateCurrentMf={updateCurrentMf} handleFinalSubmit={handleFinalSubmit} onBack={() => setStep(1)} + subscales={subscales} + onOpenSubscale={handleOpenSubscale} + /> + )} + + {modalTarget && ( + setModalTarget(null)} + onSave={handleSaveSubscale} + targetInfo={modalTarget} /> )}
From cced6d3923567ab3ef30b11308da0f44cd608dcf Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 10:56:46 +0100 Subject: [PATCH 15/20] =?UTF-8?q?add:=20preparar=20payload=20para=20hacer?= =?UTF-8?q?=20la=20petici=C3=B3n=20al=20endpoint=20/build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/DocEditor.jsx | 40 ++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 34a24c9..88fb4c0 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -1,7 +1,7 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; -import SubscaleModal from '../components/editor/SubscaleModal'; // <-- IMPORTAMOS EL MODAL +import SubscaleModal from '../components/editor/SubscaleModal'; import { calculateValueFunction } from '../services/docService'; export default function DocEditor() { @@ -20,9 +20,8 @@ export default function DocEditor() { const [mfDefinitions, setMfDefinitions] = useState({}); // ESTADOS: SUBESCALAS (FASE 2.5) - // Formato: { "regular": { left: { cardsCount: 3, blankCards: [1, 0] }, right: null }, "bueno": ... } const [subscales, setSubscales] = useState({}); - const [modalTarget, setModalTarget] = useState(null); // { term: 'regular', side: 'left', initialData: {...} } + const [modalTarget, setModalTarget] = useState(null); // MANEJADORES: FASE 1 const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; @@ -117,9 +116,36 @@ export default function DocEditor() { setModalTarget(null); }; - const handleFinalSubmit = () => { - console.log("PAYLOAD DOC-MF COMPLETO:", { baseScale, mfDefinitions, subscales }); - alert("JSON en consola."); + const handleFinalSubmit = async () => { + const scaleKeys = Object.keys(baseScale); + + const payload = { + criterion_name: criterionName.trim(), + levels: scaleKeys.map(term => { + const mf = mfDefinitions[term]; + const sub = subscales[term] || {}; + + return { + term: term, + core: [ + Number(mf.coreStart.toFixed(4)), + Number(mf.coreEnd.toFixed(4)) + ], + support: [ + Number(mf.supportStart.toFixed(4)), + Number(mf.supportEnd.toFixed(4)) + ], + left_blank_cards: sub.left ? sub.left.blankCards : [0], + right_blank_cards: sub.right ? sub.right.blankCards : [0], + + left_nodes_count: sub.left ? sub.left.cardsCount : 2, + right_nodes_count: sub.right ? sub.right.cardsCount : 2 + }; + }) + }; + + console.log("PAYLOAD:", JSON.stringify(payload, null, 2)); + // TODO: Llamada a la API }; return ( From 7da263732c8f61aad8ad5d5752c5648186fcbd1a Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 11:19:11 +0100 Subject: [PATCH 16/20] =?UTF-8?q?add:=20preparar=20archivos=20para=20hacer?= =?UTF-8?q?=20la=20petici=C3=B3n=20al=20nuevo=20endpoint=20"build"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/{axios.js => api.js} | 4 +-- frontend/src/pages/DocEditor.jsx | 50 ++++++++++++++++++--------- frontend/src/services/docService.js | 22 +++++++++--- 3 files changed, 52 insertions(+), 24 deletions(-) rename frontend/src/lib/{axios.js => api.js} (80%) diff --git a/frontend/src/lib/axios.js b/frontend/src/lib/api.js similarity index 80% rename from frontend/src/lib/axios.js rename to frontend/src/lib/api.js index 5aa26f5..dae5d65 100644 --- a/frontend/src/lib/axios.js +++ b/frontend/src/lib/api.js @@ -1,7 +1,7 @@ import Axios from 'axios'; import { API_BASE_URL } from '../config'; -const axios = Axios.create({ +const api = Axios.create({ baseURL: API_BASE_URL, headers: { 'Accept': 'application/json', @@ -10,4 +10,4 @@ const axios = Axios.create({ }); -export default axios; \ No newline at end of file +export default api; \ No newline at end of file diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 88fb4c0..b114498 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; import SubscaleModal from '../components/editor/SubscaleModal'; -import { calculateValueFunction } from '../services/docService'; +import { calculateValueFunction, buildFuzzyGraph } from '../services/docService'; export default function DocEditor() { const [step, setStep] = useState(1); @@ -18,11 +18,12 @@ export default function DocEditor() { const [baseScale, setBaseScale] = useState({}); const [selectedTerm, setSelectedTerm] = useState(null); const [mfDefinitions, setMfDefinitions] = useState({}); - - // ESTADOS: SUBESCALAS (FASE 2.5) const [subscales, setSubscales] = useState({}); const [modalTarget, setModalTarget] = useState(null); + // ESTADO: FASE 3 + const [finalResult, setFinalResult] = useState(null); + // MANEJADORES: FASE 1 const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; @@ -100,7 +101,6 @@ export default function DocEditor() { }); }; - // MANEJADORES: SUBESCALAS const handleOpenSubscale = (term, side, initialData) => { setModalTarget({ term, side, initialData }); }; @@ -116,40 +116,45 @@ export default function DocEditor() { setModalTarget(null); }; + // Petición para el endpoint "build" const handleFinalSubmit = async () => { const scaleKeys = Object.keys(baseScale); - const payload = { criterion_name: criterionName.trim(), levels: scaleKeys.map(term => { const mf = mfDefinitions[term]; const sub = subscales[term] || {}; - return { term: term, - core: [ - Number(mf.coreStart.toFixed(4)), - Number(mf.coreEnd.toFixed(4)) - ], - support: [ - Number(mf.supportStart.toFixed(4)), - Number(mf.supportEnd.toFixed(4)) - ], + core: [ Number(mf.coreStart.toFixed(4)), Number(mf.coreEnd.toFixed(4)) ], + support: [ Number(mf.supportStart.toFixed(4)), Number(mf.supportEnd.toFixed(4)) ], left_blank_cards: sub.left ? sub.left.blankCards : [0], right_blank_cards: sub.right ? sub.right.blankCards : [0], - left_nodes_count: sub.left ? sub.left.cardsCount : 2, right_nodes_count: sub.right ? sub.right.cardsCount : 2 }; }) }; - console.log("PAYLOAD:", JSON.stringify(payload, null, 2)); - // TODO: Llamada a la API + setIsLoading(true); + try { + const result = await buildFuzzyGraph(payload); + console.log("RESPUESTA DEL BACKEND:", result); + + setFinalResult(result); + setStep(3); + + } catch (error) { + console.error(error); + alert("Error del servidor: \n" + JSON.stringify(error, null, 2)); + } finally { + setIsLoading(false); + } }; return (
+ {step === 1 && ( )} + {step === 2 && ( )} + {step === 3 && ( +
+

Paso 3: Espectro Difuso Final

+

Gráfica construida correctamente. ¡Mira la consola!

+ +
+ )} + {modalTarget && ( { try { - const response = await axios.post('/criteria/doc/value-function', payload); - return response.data; + const response = await api.post('/criteria/doc/value-function', payload); + return response.data; } catch (error) { - console.error("Error en calculateValueFunction:", error); - throw error; + console.error('Error calculating value function:', error); + throw error.response?.data?.detail || error.message; } +}; + +export const buildFuzzyGraph = async (payload) => { + try { + const response = await api.post('/criteria/doc-mf/build', payload); + return response.data; + } catch (error) { + console.error('Error building fuzzy graph:', error); + throw error.response?.data?.detail || error.message; + } + + }; \ No newline at end of file From e0e1f5381b83a90123e6d5867c502a95a152ce1c Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 12:24:17 +0100 Subject: [PATCH 17/20] =?UTF-8?q?fix:=20arreglar=20petici=C3=B3n=20para=20?= =?UTF-8?q?el=20endpoint=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/DocEditor.jsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index b114498..01bf56b 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -119,19 +119,30 @@ export default function DocEditor() { // Petición para el endpoint "build" const handleFinalSubmit = async () => { const scaleKeys = Object.keys(baseScale); + const payload = { - criterion_name: criterionName.trim(), levels: scaleKeys.map(term => { const mf = mfDefinitions[term]; const sub = subscales[term] || {}; + + const leftCount = sub.left ? sub.left.cardsCount : 2; + const left_nodes_x = Array.from({ length: leftCount }).map((_, i) => + Number((mf.supportStart + (mf.coreStart - mf.supportStart) * (i / (leftCount - 1))).toFixed(4)) + ); + + const rightCount = sub.right ? sub.right.cardsCount : 2; + const right_nodes_x = Array.from({ length: rightCount }).map((_, i) => + Number((mf.coreEnd + (mf.supportEnd - mf.coreEnd) * (i / (rightCount - 1))).toFixed(4)) + ); + return { term: term, core: [ Number(mf.coreStart.toFixed(4)), Number(mf.coreEnd.toFixed(4)) ], support: [ Number(mf.supportStart.toFixed(4)), Number(mf.supportEnd.toFixed(4)) ], + left_nodes_x: left_nodes_x, left_blank_cards: sub.left ? sub.left.blankCards : [0], - right_blank_cards: sub.right ? sub.right.blankCards : [0], - left_nodes_count: sub.left ? sub.left.cardsCount : 2, - right_nodes_count: sub.right ? sub.right.cardsCount : 2 + right_nodes_x: right_nodes_x, + right_blank_cards: sub.right ? sub.right.blankCards : [0] }; }) }; From 89ebf99c7fb900bc3987749cc13a9d13dc484e4d Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 13:16:43 +0100 Subject: [PATCH 18/20] =?UTF-8?q?add:=20a=C3=B1adida=20gr=C3=A1fica=20fina?= =?UTF-8?q?l.=20reforzada=20la=20seguridad=20en=20el=20payload=20para=20la?= =?UTF-8?q?=20petici=C3=B3n=20a=20"build"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/editor/Step3FinalGraph.jsx | 87 +++++++++++++++++++ frontend/src/config.js | 7 +- frontend/src/pages/DocEditor.jsx | 34 +++++--- 3 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/editor/Step3FinalGraph.jsx diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx new file mode 100644 index 0000000..0173abf --- /dev/null +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -0,0 +1,87 @@ +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer +} from 'recharts'; +import { CHART_COLORS } from '../../config'; + +const Step3FinalGraph = ({ data }) => { + if (!data || !data.results) return

Cargando gráfico final...

; + + const resultsWithOriginalIndex = data.results.map((item, index) => ({ + ...item, + originalIndex: index + })); + + const sortedResults = [...resultsWithOriginalIndex].sort((a, b) => { + const valA = a.core ? a.core[0] : 0; + const valB = b.core ? b.core[0] : 0; + return valA - valB; + }); + + return ( +
+

Espectro Difuso Final

+ + + + + + + + + [value.toFixed(3), name]} + labelFormatter={(label) => `X: ${Number(label).toFixed(3)}`} + contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} + /> + + ({ + id: item.term, + type: 'circle', + value: item.term.toUpperCase(), + color: CHART_COLORS[item.originalIndex % CHART_COLORS.length] + }))} + /> + + {sortedResults.map((item) => { + const lineData = [...item.left_nodes, ...item.right_nodes].map(node => ({ + x: node[0], + y: node[1] + })); + + return ( + + ); + })} + + +
+ ); +}; + +export default Step3FinalGraph; \ No newline at end of file diff --git a/frontend/src/config.js b/frontend/src/config.js index 837c422..91fea1e 100644 --- a/frontend/src/config.js +++ b/frontend/src/config.js @@ -1 +1,6 @@ -export const API_BASE_URL = import.meta.env.VITE_API_URL; \ No newline at end of file +export const API_BASE_URL = import.meta.env.VITE_API_URL; + +export const CHART_COLORS = [ + '#ef4444', '#f59e0b', '#10b981', '#3b82f6', + '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1' +]; \ No newline at end of file diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 01bf56b..4cdc508 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -1,8 +1,9 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; import SubscaleModal from '../components/editor/SubscaleModal'; import { calculateValueFunction, buildFuzzyGraph } from '../services/docService'; +import Step3FinalGraph from '../components/editor/Step3FinalGraph'; export default function DocEditor() { const [step, setStep] = useState(1); @@ -125,20 +126,26 @@ export default function DocEditor() { const mf = mfDefinitions[term]; const sub = subscales[term] || {}; + const c_start = Number(mf.coreStart.toFixed(4)); + const c_end = Number(mf.coreEnd.toFixed(4)); + + const s_start = Math.min(Number(mf.supportStart.toFixed(4)), c_start); + const s_end = Math.max(Number(mf.supportEnd.toFixed(4)), c_end); + const leftCount = sub.left ? sub.left.cardsCount : 2; const left_nodes_x = Array.from({ length: leftCount }).map((_, i) => - Number((mf.supportStart + (mf.coreStart - mf.supportStart) * (i / (leftCount - 1))).toFixed(4)) + Number((s_start + (c_start - s_start) * (i / (leftCount - 1))).toFixed(4)) ); const rightCount = sub.right ? sub.right.cardsCount : 2; const right_nodes_x = Array.from({ length: rightCount }).map((_, i) => - Number((mf.coreEnd + (mf.supportEnd - mf.coreEnd) * (i / (rightCount - 1))).toFixed(4)) + Number((c_end + (s_end - c_end) * (i / (rightCount - 1))).toFixed(4)) ); return { term: term, - core: [ Number(mf.coreStart.toFixed(4)), Number(mf.coreEnd.toFixed(4)) ], - support: [ Number(mf.supportStart.toFixed(4)), Number(mf.supportEnd.toFixed(4)) ], + core: [ c_start, c_end ], + support: [ s_start, s_end ], left_nodes_x: left_nodes_x, left_blank_cards: sub.left ? sub.left.blankCards : [0], right_nodes_x: right_nodes_x, @@ -187,13 +194,16 @@ export default function DocEditor() { /> )} - {step === 3 && ( -
-

Paso 3: Espectro Difuso Final

-

Gráfica construida correctamente. ¡Mira la consola!

- + {step === 3 && finalResult && ( +
+ + +
)} From 46250af1eba7efc75b93db94327c88bc7ddcd744 Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 13:47:55 +0100 Subject: [PATCH 19/20] fix: arreglar leyenda, utilizar colores importados desde config en step2 y step3 --- .../components/editor/Step2FuzzyModeling.jsx | 36 ++-- .../src/components/editor/Step3FinalGraph.jsx | 160 ++++++++++-------- 2 files changed, 114 insertions(+), 82 deletions(-) diff --git a/frontend/src/components/editor/Step2FuzzyModeling.jsx b/frontend/src/components/editor/Step2FuzzyModeling.jsx index ee9841e..34ba6f8 100644 --- a/frontend/src/components/editor/Step2FuzzyModeling.jsx +++ b/frontend/src/components/editor/Step2FuzzyModeling.jsx @@ -1,11 +1,21 @@ import Chart from '../membershipFunction/Chart'; import Controls from '../membershipFunction/Controls'; +import { CHART_COLORS } from '../../config'; -const COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1']; - -export default function Step2FuzzyModeling({baseScale, mfDefinitions, selectedTerm, setSelectedTerm, updateCurrentMf, handleFinalSubmit, onBack, subscales, onOpenSubscale}) { +export default function Step2FuzzyModeling({ + baseScale, + mfDefinitions, + selectedTerm, + setSelectedTerm, + updateCurrentMf, + handleFinalSubmit, + onBack, + subscales, + onOpenSubscale +}) { const scaleKeys = Object.keys(baseScale); - const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb'; + + const selectedColor = CHART_COLORS[scaleKeys.indexOf(selectedTerm) % CHART_COLORS.length] || '#2563eb'; return (
@@ -17,11 +27,18 @@ export default function Step2FuzzyModeling({baseScale, mfDefinitions, selectedTe
{scaleKeys.map((name, index) => { - const color = COLORS[index % COLORS.length]; const isSelected = selectedTerm === name; + const color = CHART_COLORS[index % CHART_COLORS.length]; + return ( - ); })} @@ -31,7 +48,7 @@ export default function Step2FuzzyModeling({baseScale, mfDefinitions, selectedTe baseScale={baseScale} mfDefinitions={mfDefinitions} selectedTerm={selectedTerm} - colors={COLORS} + colors={CHART_COLORS} />
-
-
); } \ No newline at end of file diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx index 0173abf..35d728f 100644 --- a/frontend/src/components/editor/Step3FinalGraph.jsx +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -1,85 +1,101 @@ -import { - LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer -} from 'recharts'; +import { useMemo } from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { CHART_COLORS } from '../../config'; const Step3FinalGraph = ({ data }) => { - if (!data || !data.results) return

Cargando gráfico final...

; + const sortedResults = useMemo(() => { + if (!data || !data.results) return []; - const resultsWithOriginalIndex = data.results.map((item, index) => ({ - ...item, - originalIndex: index - })); + const withPermanentColors = data.results.map((item, index) => ({ + ...item, + color: CHART_COLORS[index % CHART_COLORS.length] + })); - const sortedResults = [...resultsWithOriginalIndex].sort((a, b) => { - const valA = a.core ? a.core[0] : 0; - const valB = b.core ? b.core[0] : 0; - return valA - valB; - }); + return withPermanentColors.sort((a, b) => { + const coreA = Array.isArray(a.core) ? Number(a.core[0]) : 0; + const coreB = Array.isArray(b.core) ? Number(b.core[0]) : 0; + return coreA - coreB; + }); + }, [data]); + + if (!data || !data.results) { + return

Cargando gráfico final...

; + } return ( -
-

Espectro Difuso Final

+
+

Espectro Difuso Final

- - - - - - - - [value.toFixed(3), name]} - labelFormatter={(label) => `X: ${Number(label).toFixed(3)}`} - contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} - /> - - ({ - id: item.term, - type: 'circle', - value: item.term.toUpperCase(), - color: CHART_COLORS[item.originalIndex % CHART_COLORS.length] - }))} - /> + {/* Gráfica */} +
+ + + + + + [Number(value).toFixed(3), name]} + labelFormatter={(label) => `X: ${Number(label).toFixed(3)}`} + contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} + /> - {sortedResults.map((item) => { - const lineData = [...item.left_nodes, ...item.right_nodes].map(node => ({ - x: node[0], - y: node[1] - })); + {sortedResults.map((item) => { + const lineData = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(node => ({ + x: Number(node[0]), + y: Number(node[1]) + })); + + return ( + + ); + })} + + +
+ + {/* Leyenda */} +
+ {sortedResults.map((item) => ( +
+ + + + + {item.term} + +
+ ))} +
- return ( - - ); - })} -
-
); }; From 07f1fd9ebc8a9c75246d83b2d874e57cef96252b Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 13:55:55 +0100 Subject: [PATCH 20/20] fix: cambio minimo --- .../src/components/editor/Step3FinalGraph.jsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx index 35d728f..45c9d27 100644 --- a/frontend/src/components/editor/Step3FinalGraph.jsx +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -79,20 +79,20 @@ const Step3FinalGraph = ({ data }) => { {/* Leyenda */}
{sortedResults.map((item) => ( -
- - +
+ + - - {item.term} - -
+ + {item.term} + +
))}