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 }} /> -
-
-
-
- )} +
-
)}