diff --git a/frontend/src/components/AuthDemoPanel.jsx b/frontend/src/components/AuthDemoPanel.jsx
new file mode 100644
index 0000000..05efddd
--- /dev/null
+++ b/frontend/src/components/AuthDemoPanel.jsx
@@ -0,0 +1,407 @@
+import React, { useState, useEffect, useRef, useCallback } from 'react';
+import {
+ ComposedChart, Area, Line,
+ XAxis, YAxis, CartesianGrid,
+ ReferenceArea, ReferenceLine,
+ ResponsiveContainer,
+} from 'recharts';
+
+// ── Fake demo data ──────────────────────────────────────────────────────────
+
+const STEP1_CARDS = ['Bajo', 'Medio', 'Alto', 'Perfecto'];
+const STEP1_BLANKS = [3, 1, 4]; // huecos asimétricos entre cartas
+
+const STEP2_TERMS = [
+ { name: 'Bajo', xVal: 0.08, mf: { supportStart: 0.00, coreStart: 0.00, coreEnd: 0.06, supportEnd: 0.30 } },
+ { name: 'Medio', xVal: 0.38, mf: { supportStart: 0.18, coreStart: 0.38, coreEnd: 0.38, supportEnd: 0.59 } }, // pico/triángulo
+ { name: 'Alto', xVal: 0.65, mf: { supportStart: 0.52, coreStart: 0.60, coreEnd: 0.69, supportEnd: 0.77 } },
+ { name: 'Perfecto', xVal: 0.92, mf: { supportStart: 0.74, coreStart: 0.88, coreEnd: 1.00, supportEnd: 1.00 } },
+];
+const STEP2_COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6'];
+
+// Igual que interpolateY en useGraphData.js:
+// · Zona buffer justo fuera del soporte → 0 (ancla la línea en y=0)
+// · Más allá del buffer → null (corte, sin línea horizontal)
+const TRAP_BUF = 1.1e-4;
+function trapVal(x, s0, c0, c1, s1) {
+ if (x < s0 - TRAP_BUF || x > s1 + TRAP_BUF) return null;
+ if (x < s0 || x > s1) return 0;
+ if (x >= c0 && x <= c1) return 1;
+ if (x < c0 && c0 > s0) return (x - s0) / (c0 - s0);
+ if (x > c1 && s1 > c1) return (s1 - x) / (s1 - c1);
+ return 0;
+}
+
+// Solo 'Alto' es IT2 (tiene subescala). LMF es triángulo → banda "lente" visible.
+const STEP3_TERMS = [
+ { name: 'Bajo', color: '#ef4444', type: 't1', pts: [0.00, 0.00, 0.06, 0.30] },
+ { name: 'Medio', color: '#f59e0b', type: 't1', pts: [0.18, 0.38, 0.38, 0.59] },
+ { name: 'Alto', color: '#10b981', type: 't2', u: [0.49, 0.57, 0.71, 0.81], l: [0.56, 0.64, 0.64, 0.76] },
+ { name: 'Perfecto', color: '#3b82f6', type: 't1', pts: [0.74, 0.88, 1.00, 1.00] },
+];
+
+// Puntos clave del trapecio (piezas lineales → basta con vértices, como en el paso 2).
+// Recharts interpola el trazo entre ellos con animación fluida.
+function getTermLineData(term) {
+ const name = term.name;
+
+ if (term.type === 't1') {
+ const [s0, c0, c1, s1] = term.pts;
+ const xs = new Set([s0, c0, c1, s1]);
+ if (s0 <= 0.001) xs.add(s0 - TRAP_BUF);
+ if (s1 >= 0.999) xs.add(s1 + TRAP_BUF);
+ return Array.from(xs).sort((a, b) => a - b).map(x => ({
+ x,
+ [name]: trapVal(x, s0, c0, c1, s1),
+ }));
+ }
+
+ const xs = new Set([...term.u, ...term.l]);
+ return Array.from(xs).sort((a, b) => a - b).map(x => {
+ const upper = trapVal(x, ...term.u);
+ const lower = trapVal(x, ...term.l);
+ return {
+ x,
+ [`${name}_upper`]: upper,
+ [`${name}_lower`]: lower,
+ [`${name}_range`]: (lower === null && upper === null)
+ ? null
+ : [lower ?? 0, upper ?? 0],
+ };
+ });
+}
+
+const STEP_LABELS = [
+ { n: 1, label: 'Escala' },
+ { n: 2, label: 'Modelado' },
+ { n: 3, label: 'Espectro IT2' },
+];
+
+// ── Step sub-components ─────────────────────────────────────────────────────
+
+function Step1Content({ count }) {
+ const done = count >= STEP1_CARDS.length;
+ return (
+
+
Criterio: Calidad Investigadora
+
+ {STEP1_CARDS.map((name, i) => (
+
+ {i > 0 && (
+
+
+
+ ×{STEP1_BLANKS[i - 1]}
+
+
+ )}
+
+
+ {i + 1}
+ {i + 1}
+ {name}
+
+
+
+ ))}
+
+
+
✓ {STEP1_CARDS.length} niveles definidos
+
+
+
+ );
+}
+
+function Step2Content({ count }) {
+ const visibleTerms = STEP2_TERMS.slice(0, count);
+ const activeIndex = count - 1;
+ const showSubscale = count >= 3;
+
+ return (
+
+
+ {STEP2_TERMS.map((term, i) => {
+ const color = STEP2_COLORS[i % STEP2_COLORS.length];
+ const isVisible = i < count;
+ const isActive = i === activeIndex;
+ return (
+
+ {term.name}
+ {i === 2 && showSubscale && (
+ IT2
+ )}
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ {visibleTerms.map((term, i) => {
+ const color = STEP2_COLORS[i % STEP2_COLORS.length];
+ const isActive = i === activeIndex;
+ return (
+
+ );
+ })}
+ {visibleTerms.map((term, i) => {
+ const color = STEP2_COLORS[i % STEP2_COLORS.length];
+ const isActive = i === activeIndex;
+ return (
+
+ );
+ })}
+ {visibleTerms.map((term, i) => {
+ const color = STEP2_COLORS[i % STEP2_COLORS.length];
+ const isActive = i === activeIndex;
+ const trapezeData = [
+ { x: term.mf.supportStart, y: 0 },
+ { x: term.mf.coreStart, y: 1 },
+ { x: term.mf.coreEnd, y: 1 },
+ { x: term.mf.supportEnd, y: 0 },
+ ];
+ return (
+
+ );
+ })}
+
+
+
+
+ {/* Mini SubscaleModal inline */}
+
+
+ Diseñar Subescala
+ ·
+ Alto
+ ·
+ Pendiente Descendente
+
+
+
+
+ 1
+
+
+
+
+ MÍN
+ 2
+
+
—
+
+ MÁX
+ 5
+
+
+
¿Dudas? Rango ✓
+
+
+ 2
+
+
+
+ CARTAS
+ 3
+
+
Distancia exacta
+
+
+ 3
+
+
+
+
+ );
+}
+
+function Step3Content({ count }) {
+ return (
+
+
Espectro difuso · Calidad Investigadora
+
+
+
+
+
+
+ {STEP3_TERMS.slice(0, count).map((term, i) => {
+ const isNewest = i === count - 1;
+ const data = getTermLineData(term);
+ return (
+
+ {term.type === 't1' ? (
+
+ ) : (
+ <>
+
+
+
+ >
+ )}
+
+ );
+ })}
+
+
+
+ {STEP3_TERMS.map((term, i) => (
+
+
+ {term.name}
+ {term.type === 't2' && i < count && (
+ IT2
+ )}
+
+ ))}
+
+
+
+ );
+}
+
+// ── Main component ───────────────────────────────────────────────────────────
+
+export default function AuthDemoPanel() {
+ const [step, setStep] = useState(1);
+ const [count, setCount] = useState(0);
+ const [fading, setFading] = useState(false);
+ const innerTimerRef = useRef(null);
+
+ const transitionTo = useCallback((nextStep) => {
+ if (innerTimerRef.current) clearTimeout(innerTimerRef.current);
+ setFading(true);
+ innerTimerRef.current = setTimeout(() => {
+ setStep(nextStep);
+ setCount(0);
+ setFading(false);
+ }, 440);
+ }, []);
+
+ useEffect(() => {
+ let timeout;
+ if (step === 1) {
+ if (count < STEP1_CARDS.length) {
+ timeout = setTimeout(() => setCount(c => c + 1), 580);
+ } else {
+ timeout = setTimeout(() => transitionTo(2), 950);
+ }
+ } else if (step === 2) {
+ if (count < STEP2_TERMS.length) {
+ timeout = setTimeout(() => setCount(c => c + 1), 920);
+ } else {
+ timeout = setTimeout(() => transitionTo(3), 1300);
+ }
+ } else if (step === 3) {
+ if (count < STEP3_TERMS.length) {
+ timeout = setTimeout(() => setCount(c => c + 1), 920);
+ } else {
+ timeout = setTimeout(() => transitionTo(1), 2800);
+ }
+ }
+ return () => clearTimeout(timeout);
+ }, [step, count, transitionTo]);
+
+ useEffect(() => {
+ return () => { if (innerTimerRef.current) clearTimeout(innerTimerRef.current); };
+ }, []);
+
+ return (
+
+ {/* Step breadcrumb */}
+
+ {STEP_LABELS.map((s, i) => (
+
+
+ s.n ? 'bg-emerald-500 text-white' : 'bg-slate-200 text-slate-400'}`}>
+ {step > s.n ? '✓' : s.n}
+
+ s.n ? 'text-emerald-600' : 'text-slate-400'}`}>
+ {s.label}
+
+
+ {i < STEP_LABELS.length - 1 && (
+ ›
+ )}
+
+ ))}
+
+
+ En vivo
+
+
+
+
+ {step === 1 && }
+ {step === 2 && }
+ {step === 3 && }
+
+
+ );
+}
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx
index c91df6a..2fd4743 100644
--- a/frontend/src/pages/Login.jsx
+++ b/frontend/src/pages/Login.jsx
@@ -4,194 +4,7 @@ import { useAuth } from '../context/AuthContext';
import { authService } from '../services/authService';
import { API_BASE_URL } from '../config';
import { FiArrowLeft, FiEye, FiEyeOff } from 'react-icons/fi';
-import { ComposedChart, Line, XAxis, YAxis, CartesianGrid, ReferenceArea, ReferenceLine, ResponsiveContainer } from 'recharts';
-
-const DEMO_TERMS = [
- { name: 'Muy Bajo', xVal: 0.10, mf: { supportStart: 0.00, coreStart: 0.00, coreEnd: 0.10, supportEnd: 0.22 } },
- { name: 'Bajo', xVal: 0.28, mf: { supportStart: 0.12, coreStart: 0.22, coreEnd: 0.33, supportEnd: 0.44 } },
- { name: 'Medio', xVal: 0.50, mf: { supportStart: 0.35, coreStart: 0.44, coreEnd: 0.56, supportEnd: 0.65 } },
- { name: 'Alto', xVal: 0.72, mf: { supportStart: 0.56, coreStart: 0.67, coreEnd: 0.78, supportEnd: 0.88 } },
- { name: 'Muy Alto', xVal: 0.90, mf: { supportStart: 0.78, coreStart: 0.90, coreEnd: 1.00, supportEnd: 1.00 } },
-];
-const DEMO_COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#d946ef'];
-
-function FakeDemoPanel() {
- const [visibleCount, setVisibleCount] = useState(0);
- const [fading, setFading] = useState(false);
-
- useEffect(() => {
- let timeout;
- if (visibleCount < DEMO_TERMS.length) {
- timeout = setTimeout(() => setVisibleCount(v => v + 1), 1300);
- } else {
- timeout = setTimeout(() => {
- setFading(true);
- setTimeout(() => {
- setVisibleCount(0);
- setFading(false);
- }, 500);
- }, 2400);
- }
- return () => clearTimeout(timeout);
- }, [visibleCount]);
-
- const visibleTerms = DEMO_TERMS.slice(0, visibleCount);
- const activeIndex = visibleCount - 1;
-
- return (
-
- {/* Mini header badge */}
-
- Paso 2
- ·
- Modelar Conceptos Difusos
-
-
- En vivo
-
-
-
- {/* Term pills */}
-
- {DEMO_TERMS.map((term, index) => {
- const color = DEMO_COLORS[index % DEMO_COLORS.length];
- const isVisible = index < visibleCount;
- const isActive = index === activeIndex;
- return (
-
- {term.name}
-
- );
- })}
-
-
- {/* Chart */}
-
-
-
-
-
-
- {visibleTerms.map((term, index) => {
- const color = DEMO_COLORS[index % DEMO_COLORS.length];
- const isActive = index === activeIndex;
- return (
-
- );
- })}
- {visibleTerms.map((term, index) => {
- const color = DEMO_COLORS[index % DEMO_COLORS.length];
- const isActive = index === activeIndex;
- return (
-
- );
- })}
- {visibleTerms.map((term, index) => {
- const color = DEMO_COLORS[index % DEMO_COLORS.length];
- const isActive = index === activeIndex;
- const trapezeData = [
- { x: term.mf.supportStart, y: 0 },
- { x: term.mf.coreStart, y: 1 },
- { x: term.mf.coreEnd, y: 1 },
- { x: term.mf.supportEnd, y: 0 },
- ];
- return (
-
- );
- })}
-
-
-
-
- {/* Terms table */}
-
-
- Términos modelados
- {visibleCount} / {DEMO_TERMS.length}
-
-
- {DEMO_TERMS.map((term, index) => {
- const isVisible = index < visibleCount;
- const isActive = index === activeIndex;
- const color = DEMO_COLORS[index % DEMO_COLORS.length];
- return (
-
-
-
- {term.name}
-
- {isVisible && (
-
- [{term.mf.coreStart.toFixed(2)}, {term.mf.coreEnd.toFixed(2)}]
-
- )}
- {isActive && (
-
- )}
-
- );
- })}
-
-
-
- );
-}
+import AuthDemoPanel from '../components/AuthDemoPanel';
export default function Login() {
const [email, setEmail] = useState('');
@@ -261,7 +74,8 @@ export default function Login() {
return (
-
+
+
Deck of Cards
Modela y compara de forma visual
@@ -274,7 +88,7 @@ export default function Login() {
Ir al editor principal
-
+
@@ -290,41 +104,41 @@ export default function Login() {
)}
);
-}
\ No newline at end of file
+}
diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx
index 09174f4..f4b195f 100644
--- a/frontend/src/pages/Register.jsx
+++ b/frontend/src/pages/Register.jsx
@@ -3,6 +3,7 @@ import { useNavigate, Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { authService } from '../services/authService';
import { FiArrowLeft, FiEye, FiEyeOff } from 'react-icons/fi';
+import AuthDemoPanel from '../components/AuthDemoPanel';
export default function Register() {
const [username, setUsername] = useState('');
@@ -97,21 +98,22 @@ export default function Register() {
return (
-
+
Deck of Cards
Crea tu cuenta y guarda cada modelo
-
+
Registra tus criterios, conserva resultados en el historial y retoma tus análisis cuando quieras.
Ir al editor principal
+