diff --git a/frontend/src/components/AuthDemoPanel.jsx b/frontend/src/components/AuthDemoPanel.jsx
new file mode 100644
index 0000000..ea53ebf
--- /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). UMF más ancha; LMF con el mismo núcleo trapecial que el paso 2.
+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.60, 0.69, 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 0afb43b..2fd4743 100644
--- a/frontend/src/pages/Login.jsx
+++ b/frontend/src/pages/Login.jsx
@@ -3,7 +3,8 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { authService } from '../services/authService';
import { API_BASE_URL } from '../config';
-import { FiEye, FiEyeOff } from 'react-icons/fi';
+import { FiArrowLeft, FiEye, FiEyeOff } from 'react-icons/fi';
+import AuthDemoPanel from '../components/AuthDemoPanel';
export default function Login() {
const [email, setEmail] = useState('');
@@ -71,70 +72,89 @@ export default function Login() {
};
return (
-
-
-
-
-
Deck of Cards
-
Accede a tu historial y gráficas guardadas
+
+
+
+
+
Deck of Cards
+
Modela y compara de forma visual
+
+ Construye funciones de pertenencia difusa, guarda tu historial y vuelve a trabajar donde lo dejaste.
+
+
+
+ Ir al editor principal
+
+
- {error && (
-
- {error}
+
+
+
Deck of Cards
+
Accede a tu historial y gráficas guardadas
- )}
-