From dca9a354f6be489c0a55dcfe5d72dd1610a86b05 Mon Sep 17 00:00:00 2001 From: Pablo Occhiuzzi <104530403+opablon@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:19:26 +0000 Subject: [PATCH 1/8] refactor: implement best practices for scalability, maintainability and performance - Add modular architecture with organized folder structure - Implement custom hooks (useAutoencoder, useErrorHandler) - Add centralized configuration and utilities - Optimize performance with lazy loading, throttling, and memory management - Add comprehensive error handling with ErrorBoundary - Improve accessibility with ARIA attributes and semantic HTML - Optimize build configuration with code splitting and minification - Add ESLint configuration and code quality tools - Enhance CI/CD pipeline with quality gates - Improve SEO with meta tags and structured HTML --- .eslintrc.cjs | 43 ++++ .github/workflows/deploy.yml | 46 +++- .gitignore | 102 +++++++- index.html | 40 ++- package-lock.json | 60 +++++ package.json | 5 +- postcss.config.js | 2 - src/App.jsx | 90 +++++-- src/components/ControlPanel.jsx | 96 ++++++-- src/components/ErrorBoundary.jsx | 85 +++++++ src/components/GeneratedCharacter.jsx | 6 +- src/components/Header.jsx | 2 - src/components/LatentSpacePlot.jsx | 227 ++++++++++------- src/components/Spinner.jsx | 2 - src/components/TheorySection.jsx | 1 - src/config/constants.js | 57 +++++ src/hooks/useAutoencoder.js | 341 ++++++++++---------------- src/hooks/useErrorHandler.js | 59 +++++ src/main.jsx | 5 +- src/utils/math.js | 76 ++++++ src/utils/performance.js | 82 +++++++ src/utils/validation.js | 54 ++++ vite.config.js | 20 ++ 23 files changed, 1141 insertions(+), 360 deletions(-) create mode 100644 .eslintrc.cjs create mode 100644 src/components/ErrorBoundary.jsx create mode 100644 src/config/constants.js create mode 100644 src/hooks/useErrorHandler.js create mode 100644 src/utils/math.js create mode 100644 src/utils/performance.js create mode 100644 src/utils/validation.js diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..855b9b3 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,43 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true, node: true }, + globals: { + process: 'readonly', + tf: 'readonly' + }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + }, + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + 'react/prop-types': 'off', + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-console': 'warn', + 'prefer-const': 'error', + 'no-var': 'error', + 'react/jsx-key': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'react/no-unescaped-entities': 'off', + 'react/display-name': 'off' + }, + settings: { + react: { + version: 'detect', + }, + }, +} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 65cf716..9c67ba4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,40 +2,62 @@ name: Deploy to GitHub Pages on: push: - branches: - - main + branches: [ main ] + pull_request: + branches: [ main ] +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + jobs: - build-and-deploy: + # Build job + build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' - + cache: 'npm' + - name: Install dependencies - run: npm install - - - name: Build + run: npm ci + + - name: Lint code + run: npm run lint + + - name: Build for production run: npm run build - + - name: Setup Pages uses: actions/configure-pages@v5 - + - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: './dist' - + path: ./dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' + steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 3c3629e..5ddd652 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,101 @@ -node_modules +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Vite build output +dist +dist-ssr +*.local + +# Rollup build output +build/ + +# Webpack build output +/dist/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# OS generated files +Thumbs.db +.DS_Store? +ehthumbs.db +Icon? + +# Temporary files +tmp/ +temp/ + +# IDE files +*.swp +*.swo +*~ + +# Storybook build outputs +storybook-static diff --git a/index.html b/index.html index ebbf157..4d5df57 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,48 @@ - Explorador de Espacio Latente de Caracteres + + + Explorador de Espacio Latente - Autoencoder Interactivo + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/package-lock.json b/package-lock.json index dfce4a7..7a2e2af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "eslint-plugin-react-refresh": "^0.4.3", "postcss": "^8.4.27", "tailwindcss": "^3.3.3", + "terser": "^5.43.1", "vite": "^7.0.6" } }, @@ -939,6 +940,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", @@ -1867,6 +1878,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -4988,6 +5005,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -4997,6 +5023,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -5326,6 +5362,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/package.json b/package.json index b6aa6cb..561fb36 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "vite build", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "lint:fix": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0 --fix", + "preview": "vite preview", + "analyze": "npm run build && npx vite-bundle-analyzer dist/stats.html" }, "dependencies": { "@tensorflow/tfjs": "^4.11.0", @@ -25,6 +27,7 @@ "eslint-plugin-react-refresh": "^0.4.3", "postcss": "^8.4.27", "tailwindcss": "^3.3.3", + "terser": "^5.43.1", "vite": "^7.0.6" } } diff --git a/postcss.config.js b/postcss.config.js index 75c78c1..2aa7205 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,5 +1,3 @@ -import postcss from 'postcss'; - export default { plugins: { tailwindcss: {}, diff --git a/src/App.jsx b/src/App.jsx index ed96412..69f67eb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,11 +1,14 @@ -import React, { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, Suspense, lazy } from 'react'; import Spinner from './components/Spinner'; import Header from './components/Header'; import LatentSpacePlot from './components/LatentSpacePlot'; import GeneratedCharacter from './components/GeneratedCharacter'; import ControlPanel from './components/ControlPanel'; -import TheorySection from './components/TheorySection'; import { useAutoencoder } from './hooks/useAutoencoder'; +import { throttle } from './utils/performance'; + +// Lazy loading para componentes pesados +const TheorySection = lazy(() => import('./components/TheorySection')); function App() { const { @@ -27,19 +30,24 @@ function App() { const [showScrollButton, setShowScrollButton] = useState(false); const [isFooterVisible, setIsFooterVisible] = useState(false); - const handleScroll = () => { + // Función throttled para manejar el scroll + const handleScroll = throttle(() => { if (mainSectionRef.current) { const { bottom } = mainSectionRef.current.getBoundingClientRect(); setShowScrollButton(bottom < 0); } - }; + }, 100); useEffect(() => { + // Observer para el footer con mejores opciones const observer = new IntersectionObserver( ([entry]) => { setIsFooterVisible(entry.isIntersecting); }, - { threshold: 0.1 } + { + threshold: 0.1, + rootMargin: '50px' // Detectar antes de que sea totalmente visible + } ); const currentFooterRef = footerRef.current; @@ -47,7 +55,8 @@ function App() { observer.observe(currentFooterRef); } - window.addEventListener('scroll', handleScroll); + // Listener optimizado para scroll + window.addEventListener('scroll', handleScroll, { passive: true }); return () => { if (currentFooterRef) { @@ -55,26 +64,39 @@ function App() { } window.removeEventListener('scroll', handleScroll); }; - }, []); + }, [handleScroll]); const scrollToMainSection = () => { mainSectionRef.current?.scrollIntoView({ behavior: 'smooth', + block: 'start' }); }; + // Renderizar loading spinner de manera optimizada + if (isLoading) { + return ; + } + return ( <> - {isLoading && }
+
-
-

Espacio Latente de Referencia

- {isLoading ? ( - - ) : latentCoords && latentSpaceBounds && latentData ? ( + {/* Sección del plot del espacio latente */} +
+

+ Espacio Latente de Referencia +

+ + {latentCoords && latentSpaceBounds && latentData ? ( ) : ( -
Error: No se pudo cargar el espacio latente.
+
+ Error: No se pudo cargar el espacio latente. +
)} -
-
+ + + {/* Sección de controles y carácter generado */} +
+
- + + {/* Lazy loading de la sección de teoría */} + +
Cargando contenido teórico...
+
+ }> + + + + {/* Footer */}
+ + {/* Botón de scroll optimizado */} diff --git a/src/components/ControlPanel.jsx b/src/components/ControlPanel.jsx index d9ef8de..103ff48 100644 --- a/src/components/ControlPanel.jsx +++ b/src/components/ControlPanel.jsx @@ -1,38 +1,75 @@ -import React from 'react'; +import { memo } from 'react'; +import { CONFIG } from '../config/constants'; +import { isValidBounds } from '../utils/validation'; -const ControlPanel = ({ latentCoords, latentSpaceBounds, onSliderChange, onReset, onCoordInputChange }) => { - const { xMin, xMax, yMin, yMax } = latentSpaceBounds || { xMin: -3, xMax: 3, yMin: -3, yMax: 3 }; +const ControlPanel = memo(({ latentCoords, latentSpaceBounds, onSliderChange, onReset, onCoordInputChange }) => { + // Valores por defecto seguros + const bounds = isValidBounds(latentSpaceBounds) + ? latentSpaceBounds + : { xMin: -3, xMax: 3, yMin: -3, yMax: 3 }; + + const { xMin, xMax, yMin, yMax } = bounds; + + // Formatear coordenadas con la precisión configurada + const formatCoord = (value) => value.toFixed(CONFIG.COORDINATE_PRECISION); return (
-

Ajuste Fino

+

+ Ajuste Fino +

+ {/* Inputs de coordenadas directas */}
- + onCoordInputChange('x', e.target.value)} - className="w-24 p-2 text-center border border-gray-300 rounded-md font-mono" + value={formatCoord(latentCoords.x)} + onChange={(e) => onCoordInputChange('x', e.target.value)} + className="w-24 p-2 text-center border border-gray-300 rounded-md font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + step="0.01" + min={xMin} + max={xMax} + aria-label="Coordenada X del espacio latente" />
- + onCoordInputChange('y', e.target.value)} - className="w-24 p-2 text-center border border-gray-300 rounded-md font-mono" + value={formatCoord(latentCoords.y)} + onChange={(e) => onCoordInputChange('y', e.target.value)} + className="w-24 p-2 text-center border border-gray-300 rounded-md font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + step="0.01" + min={yMin} + max={yMax} + aria-label="Coordenada Y del espacio latente" />
+ {/* Sliders para ajuste fino */}
- + onSliderChange('x', e.target.value)} - className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + onChange={(e) => onSliderChange('x', e.target.value)} + className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" + aria-label="Slider para coordenada X" />
+
- + onSliderChange('y', e.target.value)} - className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" + onChange={(e) => onSliderChange('y', e.target.value)} + className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" + aria-label="Slider para coordenada Y" />
-
); -}; +}); + +ControlPanel.displayName = 'ControlPanel'; export default ControlPanel; diff --git a/src/components/ErrorBoundary.jsx b/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..aa44062 --- /dev/null +++ b/src/components/ErrorBoundary.jsx @@ -0,0 +1,85 @@ +import React from 'react'; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(_error) { + // Actualiza el estado para mostrar la UI de error + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + // Registra el error + // console.error('ErrorBoundary caught an error:', error, errorInfo); + + this.setState({ + error: error, + errorInfo: errorInfo + }); + + // Aquí podrías enviar el error a un servicio de logging + // this.reportError(error, errorInfo); + } + + render() { + if (this.state.hasError) { + // UI de fallback personalizada + return ( +
+
+
+ + + +
+ +

+ ¡Oops! Algo salió mal +

+ +

+ La aplicación encontró un error inesperado. Por favor, intenta recargar la página. +

+ +
+ + + +
+ + {process.env.NODE_ENV === 'development' && this.state.error && ( +
+ + Detalles del error (desarrollo) + +
+
Error:
+
{this.state.error && this.state.error.toString()}
+
Stack trace:
+
{this.state.errorInfo.componentStack}
+
+
+ )} +
+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/components/GeneratedCharacter.jsx b/src/components/GeneratedCharacter.jsx index 0bb7c2f..2922312 100644 --- a/src/components/GeneratedCharacter.jsx +++ b/src/components/GeneratedCharacter.jsx @@ -1,6 +1,6 @@ -import React from 'react'; +import { forwardRef } from 'react'; -const GeneratedCharacter = React.forwardRef((props, ref) => { +const GeneratedCharacter = forwardRef((props, ref) => { return (

Carácter Generado

@@ -9,4 +9,6 @@ const GeneratedCharacter = React.forwardRef((props, ref) => { ); }); +GeneratedCharacter.displayName = 'GeneratedCharacter'; + export default GeneratedCharacter; diff --git a/src/components/Header.jsx b/src/components/Header.jsx index b14f22f..b12789b 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,5 +1,3 @@ -import React from 'react'; - const Header = () => { return (
diff --git a/src/components/LatentSpacePlot.jsx b/src/components/LatentSpacePlot.jsx index 89f5558..4e0f5da 100644 --- a/src/components/LatentSpacePlot.jsx +++ b/src/components/LatentSpacePlot.jsx @@ -1,134 +1,189 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef, useCallback } from 'react'; +import { CONFIG } from '../config/constants'; +import { isValidLatentCoords, isValidBounds, isValidLatentData } from '../utils/validation'; +import { worldToPixel } from '../utils/math'; +import { throttle } from '../utils/performance'; -const PLOT_MARGIN = 40; - -function drawGridAndAxes(ctx, width, height, bounds) { - if (!bounds) return; +/** + * Dibuja la grilla y los ejes del gráfico + */ +const drawGridAndAxes = (ctx, width, height, bounds) => { + if (!isValidBounds(bounds)) return; + const { xMin, xMax, yMin, yMax } = bounds; - ctx.strokeStyle = '#e0e0e0'; + const effectiveWidth = width - 2 * CONFIG.PLOT_MARGIN - 2 * CONFIG.PLOT_OFFSET; + const effectiveHeight = height - 2 * CONFIG.PLOT_MARGIN - 2 * CONFIG.PLOT_OFFSET; + const xOffset = CONFIG.PLOT_MARGIN + CONFIG.PLOT_OFFSET; + const yOffset = CONFIG.PLOT_MARGIN + CONFIG.PLOT_OFFSET; + + // Configurar estilo de grilla + ctx.strokeStyle = CONFIG.COLORS.GRID; ctx.lineWidth = 0.5; - const numGridLines = 10; - const effectiveWidth = width - 2 * PLOT_MARGIN - 20; - const effectiveHeight = height - 2 * PLOT_MARGIN - 20; - const xScale = effectiveWidth / (xMax - xMin); - const yScale = effectiveHeight / (yMax - yMin); - const xOffset = PLOT_MARGIN + 10; - const yOffset = PLOT_MARGIN + 10; - - for (let i = 0; i <= numGridLines; i++) { - const xVal = xMin + (i / numGridLines) * (xMax - xMin); - const xPixel = xOffset + (xVal - xMin) * xScale; + + // Dibujar líneas de grilla + for (let i = 0; i <= CONFIG.PLOT_GRID_LINES; i++) { + const xVal = xMin + (i / CONFIG.PLOT_GRID_LINES) * (xMax - xMin); + const xPixel = xOffset + (xVal - xMin) * (effectiveWidth / (xMax - xMin)); + ctx.beginPath(); ctx.moveTo(xPixel, yOffset); ctx.lineTo(xPixel, height - yOffset); ctx.stroke(); } - for (let i = 0; i <= numGridLines; i++) { - const yVal = yMin + (i / numGridLines) * (yMax - yMin); - const yPixel = height - yOffset - (yVal - yMin) * yScale; + + for (let i = 0; i <= CONFIG.PLOT_GRID_LINES; i++) { + const yVal = yMin + (i / CONFIG.PLOT_GRID_LINES) * (yMax - yMin); + const yPixel = height - yOffset - (yVal - yMin) * (effectiveHeight / (yMax - yMin)); + ctx.beginPath(); ctx.moveTo(xOffset, yPixel); ctx.lineTo(width - xOffset, yPixel); ctx.stroke(); } - ctx.strokeStyle = '#666'; + + // Dibujar ejes principales + ctx.strokeStyle = CONFIG.COLORS.AXES; ctx.lineWidth = 1; + + // Eje X ctx.beginPath(); ctx.moveTo(xOffset, height - yOffset); ctx.lineTo(width - xOffset, height - yOffset); ctx.stroke(); + + // Eje Y ctx.beginPath(); ctx.moveTo(xOffset, yOffset); ctx.lineTo(xOffset, height - yOffset); ctx.stroke(); - ctx.fillStyle = '#333'; - ctx.font = '10px Arial'; + + // Etiquetas de los ejes + ctx.fillStyle = CONFIG.COLORS.TEXT; + ctx.font = CONFIG.FONTS.AXIS_LABEL; + + // Etiquetas del eje X ctx.textAlign = 'center'; ctx.textBaseline = 'top'; const xStep = (xMax - xMin) / 5; for (let i = 0; i <= 5; i++) { const val = xMin + i * xStep; - const xPixel = xOffset + (val - xMin) * xScale; - ctx.fillText(val.toFixed(1), xPixel, height - yOffset + 5); + const xPixel = xOffset + (val - xMin) * (effectiveWidth / (xMax - xMin)); + ctx.fillText(val.toFixed(CONFIG.DISPLAY_PRECISION), xPixel, height - yOffset + 5); } + + // Etiquetas del eje Y ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; const yStep = (yMax - yMin) / 5; for (let i = 0; i <= 5; i++) { const val = yMin + i * yStep; - const yPixel = height - yOffset - (val - yMin) * yScale; - ctx.fillText(val.toFixed(1), xOffset - 5, yPixel); + const yPixel = height - yOffset - (val - yMin) * (effectiveHeight / (yMax - yMin)); + ctx.fillText(val.toFixed(CONFIG.DISPLAY_PRECISION), xOffset - 5, yPixel); } -} +}; const LatentSpacePlot = React.forwardRef((props, ref) => { const { latentCoords, latentSpaceBounds, latentData } = props; + const animationFrameRef = useRef(); - useEffect(() => { - function draw() { - if ( - ref.current && - latentCoords && - typeof latentCoords.x === 'number' && - typeof latentCoords.y === 'number' && - latentSpaceBounds && - latentData - ) { - const canvas = ref.current; - const parent = canvas.parentElement; - const visualWidth = parent.clientWidth; - const visualHeight = visualWidth; - canvas.width = visualWidth; - canvas.height = visualHeight; - const ctx = canvas.getContext('2d'); - ctx.clearRect(0, 0, visualWidth, visualHeight); - drawGridAndAxes(ctx, visualWidth, visualHeight, latentSpaceBounds); - const { xMin, xMax, yMin, yMax } = latentSpaceBounds; - const effectiveWidth = visualWidth - 2 * PLOT_MARGIN - 20; - const effectiveHeight = visualHeight - 2 * PLOT_MARGIN - 20; - const xOffset = PLOT_MARGIN + 10; - const yOffset = PLOT_MARGIN + 10; - const xScale = effectiveWidth / (xMax - xMin); - const yScale = effectiveHeight / (yMax - yMin); - - // Dibujar puntos y etiquetas - latentData.forEach(({ x, y, label }) => { - const xPixel = xOffset + (x - xMin) * xScale; - const yPixel = visualHeight - yOffset - (y - yMin) * yScale; - ctx.beginPath(); - ctx.arc(xPixel, yPixel, 3, 0, 2 * Math.PI); - ctx.fillStyle = 'blue'; - ctx.fill(); - ctx.font = '12px Arial'; - ctx.fillStyle = 'black'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'bottom'; - ctx.fillText(label, xPixel, yPixel - 5); - }); - - // Dibujar marcador (cruz) en la misma escala - const markerX = xOffset + (latentCoords.x - xMin) * xScale; - const markerY = visualHeight - yOffset - (latentCoords.y - yMin) * yScale; - ctx.beginPath(); - ctx.moveTo(markerX - 6, markerY); - ctx.lineTo(markerX + 6, markerY); - ctx.moveTo(markerX, markerY - 6); - ctx.lineTo(markerX, markerY + 6); - ctx.strokeStyle = 'red'; - ctx.lineWidth = 2; - ctx.stroke(); - } + const draw = useCallback(() => { + if ( + !ref.current || + !isValidLatentCoords(latentCoords) || + !isValidBounds(latentSpaceBounds) || + !isValidLatentData(latentData) + ) { + return; } + + const canvas = ref.current; + const parent = canvas.parentElement; + const visualWidth = parent.clientWidth; + const visualHeight = visualWidth; // Mantener aspecto cuadrado + + // Actualizar dimensiones del canvas + canvas.width = visualWidth; + canvas.height = visualHeight; + + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, visualWidth, visualHeight); + + // Dibujar grilla y ejes + drawGridAndAxes(ctx, visualWidth, visualHeight, latentSpaceBounds); + + const canvasInfo = { + width: visualWidth, + height: visualHeight, + xOffset: CONFIG.PLOT_MARGIN + CONFIG.PLOT_OFFSET, + yOffset: CONFIG.PLOT_MARGIN + CONFIG.PLOT_OFFSET + }; + + // Dibujar puntos de datos y etiquetas + latentData.forEach(({ x, y, label }) => { + const pixelCoords = worldToPixel({ x, y }, latentSpaceBounds, canvasInfo); + + // Dibujar punto + ctx.beginPath(); + ctx.arc(pixelCoords.x, pixelCoords.y, CONFIG.PLOT_POINT_SIZE, 0, 2 * Math.PI); + ctx.fillStyle = CONFIG.COLORS.POINT; + ctx.fill(); + ctx.strokeStyle = CONFIG.COLORS.PRIMARY_HOVER; + ctx.lineWidth = 1; + ctx.stroke(); + + // Dibujar etiqueta + ctx.font = CONFIG.FONTS.POINT_LABEL; + ctx.fillStyle = CONFIG.COLORS.TEXT; + ctx.textAlign = 'center'; + ctx.textBaseline = 'bottom'; + ctx.fillText(label, pixelCoords.x, pixelCoords.y - CONFIG.PLOT_POINT_SIZE - 2); + }); + + // Dibujar marcador actual (cruz roja) + const markerPixels = worldToPixel(latentCoords, latentSpaceBounds, canvasInfo); + ctx.beginPath(); + ctx.moveTo(markerPixels.x - 6, markerPixels.y); + ctx.lineTo(markerPixels.x + 6, markerPixels.y); + ctx.moveTo(markerPixels.x, markerPixels.y - 6); + ctx.lineTo(markerPixels.x, markerPixels.y + 6); + ctx.strokeStyle = CONFIG.COLORS.MARKER; + ctx.lineWidth = 2; + ctx.stroke(); + }, [latentCoords, latentSpaceBounds, latentData, ref]); + + // Versión throttled del dibujo para resize + const throttledDraw = useCallback(throttle(() => { + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + animationFrameRef.current = requestAnimationFrame(draw); + }, 16), [draw]); // ~60fps + + useEffect(() => { + // Dibujar inmediatamente cuando cambien las dependencias draw(); - window.addEventListener('resize', draw); + + // Configurar listener para resize + window.addEventListener('resize', throttledDraw); + return () => { - window.removeEventListener('resize', draw); + window.removeEventListener('resize', throttledDraw); + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } }; - }, [latentCoords, latentSpaceBounds, latentData, ref]); + }, [draw, throttledDraw]); return ( - + ); }); +LatentSpacePlot.displayName = 'LatentSpacePlot'; + export default LatentSpacePlot; diff --git a/src/components/Spinner.jsx b/src/components/Spinner.jsx index 20292e7..4294ea8 100644 --- a/src/components/Spinner.jsx +++ b/src/components/Spinner.jsx @@ -1,5 +1,3 @@ -import React from 'react'; - const Spinner = () => { return (
diff --git a/src/components/TheorySection.jsx b/src/components/TheorySection.jsx index 76ad986..b1c3a5b 100644 --- a/src/components/TheorySection.jsx +++ b/src/components/TheorySection.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import redNeuronalBasica from '../assets/red_neuronal_basica.png'; import operacionConvolucion from '../assets/operacion_convolucion.png'; import autoencoder from '../assets/autoencoder.png'; diff --git a/src/config/constants.js b/src/config/constants.js new file mode 100644 index 0000000..da87615 --- /dev/null +++ b/src/config/constants.js @@ -0,0 +1,57 @@ +// Constantes de configuración de la aplicación +export const CONFIG = { + // Modelo y datos + MODEL_PATH: 'tfjs_decoder_model_20250724_135134/model.json', + LATENT_DATA_PATH: 'latent_space_data_20250724_130013.json', + + // Dimensiones del canvas + IMAGE_SIZE: 64, + + // Configuración del plot + PLOT_POINT_SIZE: 5, + PLOT_MARGIN: 40, + PLOT_GRID_LINES: 10, + PLOT_OFFSET: 10, + + // Factores de padding y escala + PADDING_FACTOR: 0.05, + SLIDER_PADDING: 5, + + // Configuración de UI + TRANSITION_DELAY: 50, + + // Precisión numérica + COORDINATE_PRECISION: 2, + DISPLAY_PRECISION: 1, + + // Configuración de estilos + COLORS: { + PRIMARY: '#007bff', + PRIMARY_HOVER: '#0056b3', + GRID: '#e0e0e0', + AXES: '#666', + TEXT: '#333', + MARKER: 'red', + POINT: 'blue' + }, + + // Configuración de fuentes + FONTS: { + AXIS_LABEL: '10px Arial', + POINT_LABEL: '12px Arial' + } +}; + +// Tipos de datos para mejor tipado +export const DATA_TYPES = { + LATENT_COORDS: 'latentCoords', + LATENT_BOUNDS: 'latentBounds', + LATENT_DATA: 'latentData' +}; + +// Estados de la aplicación +export const APP_STATES = { + LOADING: 'loading', + READY: 'ready', + ERROR: 'error' +}; diff --git a/src/hooks/useAutoencoder.js b/src/hooks/useAutoencoder.js index 3db5b1b..095e592 100644 --- a/src/hooks/useAutoencoder.js +++ b/src/hooks/useAutoencoder.js @@ -1,11 +1,10 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import * as tf from '@tensorflow/tfjs'; - -const MODEL_PATH = 'tfjs_decoder_model_20250724_135134/model.json'; -const LATENT_DATA_PATH = 'latent_space_data_20250724_130013.json'; -const IMAGE_SIZE = 64; -const PLOT_POINT_SIZE = 5; -const PLOT_MARGIN = 40; +import { CONFIG } from '../config/constants'; +import { isValidLatentCoords, isValidBounds } from '../utils/validation'; +import { clamp, pixelToWorld } from '../utils/math'; +import { cleanupTensors, throttle } from '../utils/performance'; +import { useErrorHandler } from './useErrorHandler'; export const useAutoencoder = () => { const [isLoading, setIsLoading] = useState(true); @@ -17,264 +16,194 @@ export const useAutoencoder = () => { const plotCanvasRef = useRef(null); const generatedCanvasRef = useRef(null); + const { reportError } = useErrorHandler(); + + // Función para calcular los límites del espacio latente + const calculateBounds = useCallback((plotData) => { + const xCoords = plotData.map(p => p.x); + const yCoords = plotData.map(p => p.y); + const xMin = Math.min(...xCoords); + const xMax = Math.max(...xCoords); + const yMin = Math.min(...yCoords); + const yMax = Math.max(...yCoords); + + const xRange = xMax - xMin; + const yRange = yMax - yMin; + + return { + xMin: xMin - xRange * CONFIG.PADDING_FACTOR - CONFIG.SLIDER_PADDING, + xMax: xMax + xRange * CONFIG.PADDING_FACTOR + CONFIG.SLIDER_PADDING, + yMin: yMin - yRange * CONFIG.PADDING_FACTOR - CONFIG.SLIDER_PADDING, + yMax: yMax + yRange * CONFIG.PADDING_FACTOR + CONFIG.SLIDER_PADDING, + }; + }, []); - // Carga del modelo y los datos + // Carga del modelo y los datos con manejo de errores mejorado useEffect(() => { const loadResources = async () => { try { - const model = await tf.loadGraphModel(MODEL_PATH); + // Cargar modelo + const model = await tf.loadGraphModel(CONFIG.MODEL_PATH); setDecoderModel(model); - const response = await fetch(LATENT_DATA_PATH); + // Cargar datos latentes + const response = await fetch(CONFIG.LATENT_DATA_PATH); + if (!response.ok) { + throw new Error(`Error al cargar datos: ${response.status} ${response.statusText}`); + } + const data = await response.json(); + // Validar estructura de datos + if (!data.latent_coords || !Array.isArray(data.latent_coords) || !data.labels) { + throw new Error('Estructura de datos inválida'); + } + const plotData = data.latent_coords.map((coord, index) => ({ x: coord[0], y: coord[1], label: data.labels[index], })); + setLatentData(plotData); - - const xCoords = plotData.map(p => p.x); - const yCoords = plotData.map(p => p.y); - const xMin = Math.min(...xCoords); - const xMax = Math.max(...xCoords); - const yMin = Math.min(...yCoords); - const yMax = Math.max(...yCoords); - const paddingFactor = 0.05; - const xRange = xMax - xMin; - const yRange = yMax - yMin; - const sliderPadding = 5; - - setLatentSpaceBounds({ - xMin: xMin - xRange * paddingFactor - sliderPadding, - xMax: xMax + xRange * paddingFactor + sliderPadding, - yMin: yMin - yRange * paddingFactor - sliderPadding, - yMax: yMax + yRange * paddingFactor + sliderPadding, - }); + setLatentSpaceBounds(calculateBounds(plotData)); } catch (error) { - console.error("Error loading resources:", error); - // En caso de error, detenemos la carga para evitar un spinner infinito + reportError(error, 'Error al cargar recursos del autoencoder'); setIsLoading(false); } - // No ponemos setIsLoading(false) aquí para esperar al primer dibujado }; + loadResources(); - }, []); + }, [calculateBounds, reportError]); - const drawGridAndAxes = useCallback((ctx, width, height, bounds) => { - if (!bounds) return; - const { xMin, xMax, yMin, yMax } = bounds; + // Función optimizada para generar letras con limpieza de memoria + const generateLetter = useCallback(async (latentVector) => { + if (!decoderModel || !generatedCanvasRef.current) return; - ctx.strokeStyle = '#e0e0e0'; - ctx.lineWidth = 0.5; - const numGridLines = 10; - - const effectiveWidth = width - 2 * PLOT_MARGIN - 20; - const effectiveHeight = height - 2 * PLOT_MARGIN - 20; - const xScale = effectiveWidth / (xMax - xMin); - const yScale = effectiveHeight / (yMax - yMin); - const xOffset = PLOT_MARGIN + 10; - const yOffset = PLOT_MARGIN + 10; - - for (let i = 0; i <= numGridLines; i++) { - const xVal = xMin + (i / numGridLines) * (xMax - xMin); - const xPixel = xOffset + (xVal - xMin) * xScale; - ctx.beginPath(); - ctx.moveTo(xPixel, yOffset); - ctx.lineTo(xPixel, height - yOffset); - ctx.stroke(); - } - - for (let i = 0; i <= numGridLines; i++) { - const yVal = yMin + (i / numGridLines) * (yMax - yMin); - const yPixel = height - yOffset - (yVal - yMin) * yScale; - ctx.beginPath(); - ctx.moveTo(xOffset, yPixel); - ctx.lineTo(width - xOffset, yPixel); - ctx.stroke(); - } - - ctx.strokeStyle = '#666'; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(xOffset, height - yOffset); - ctx.lineTo(width - xOffset, height - yOffset); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(xOffset, yOffset); - ctx.lineTo(xOffset, height - yOffset); - ctx.stroke(); - - ctx.fillStyle = '#333'; - ctx.font = '10px Arial'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'top'; - const xStep = (xMax - xMin) / 5; - for (let i = 0; i <= 5; i++) { - const val = xMin + i * xStep; - const xPixel = xOffset + (val - xMin) * xScale; - ctx.fillText(val.toFixed(1), xPixel, height - yOffset + 5); - } - - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - const yStep = (yMax - yMin) / 5; - for (let i = 0; i <= 5; i++) { - const val = yMin + i * yStep; - const yPixel = height - yOffset - (val - yMin) * yScale; - ctx.fillText(val.toFixed(1), xOffset - 5, yPixel); - } - }, []); - - const drawLatentSpace = useCallback(() => { - const canvas = plotCanvasRef.current; - if (!canvas || !latentData || !latentSpaceBounds) return; - const ctx = canvas.getContext('2d'); + let latentTensor, outputTensor, imageTensor, normalizedImageTensor; - const visualWidth = canvas.clientWidth; - const visualHeight = visualWidth; - canvas.width = visualWidth; - canvas.height = visualHeight; - - ctx.clearRect(0, 0, visualWidth, visualHeight); - drawGridAndAxes(ctx, visualWidth, visualHeight, latentSpaceBounds); - - const { xMin, xMax, yMin, yMax } = latentSpaceBounds; - const effectiveWidth = visualWidth - 2 * PLOT_MARGIN - 20; - const effectiveHeight = visualHeight - 2 * PLOT_MARGIN - 20; - const xOffset = PLOT_MARGIN + 10; - const yOffset = PLOT_MARGIN + 10; - const xScale = effectiveWidth / (xMax - xMin); - const yScale = effectiveHeight / (yMax - yMin); - - latentData.forEach(point => { - const xPixel = xOffset + (point.x - xMin) * xScale; - const yPixel = visualHeight - yOffset - (point.y - yMin) * yScale; - - ctx.beginPath(); - ctx.arc(xPixel, yPixel, PLOT_POINT_SIZE, 0, 2 * Math.PI); - ctx.fillStyle = '#007bff'; - ctx.fill(); - ctx.strokeStyle = '#0056b3'; - ctx.lineWidth = 1; - ctx.stroke(); - - ctx.font = '12px Arial'; - ctx.fillStyle = '#333'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'bottom'; - ctx.fillText(point.label, xPixel, yPixel - PLOT_POINT_SIZE - 2); - }); - }, [latentData, latentSpaceBounds, drawGridAndAxes]); + try { + const canvas = generatedCanvasRef.current; + const ctx = canvas.getContext('2d'); + + // Crear tensores + latentTensor = tf.tensor2d([latentVector]); + outputTensor = decoderModel.predict(latentTensor); + imageTensor = outputTensor.squeeze(); + normalizedImageTensor = imageTensor.mul(255).round().clipByValue(0, 255).cast('int32'); + + // Obtener datos de imagen + const imageDataArray = await normalizedImageTensor.data(); + + // Crear datos RGBA + const rgbaData = new Uint8ClampedArray(CONFIG.IMAGE_SIZE * CONFIG.IMAGE_SIZE * 4); + for (let i = 0; i < CONFIG.IMAGE_SIZE * CONFIG.IMAGE_SIZE; i++) { + const pixelValue = imageDataArray[i]; + const baseIndex = i * 4; + rgbaData[baseIndex] = pixelValue; // R + rgbaData[baseIndex + 1] = pixelValue; // G + rgbaData[baseIndex + 2] = pixelValue; // B + rgbaData[baseIndex + 3] = 255; // A + } - const generateLetter = useCallback(async (latentVector) => { - const canvas = generatedCanvasRef.current; - if (!decoderModel || !canvas) return; - const ctx = canvas.getContext('2d'); + // Renderizar en canvas + ctx.clearRect(0, 0, canvas.width, canvas.height); + const imageDataObject = new ImageData(rgbaData, CONFIG.IMAGE_SIZE, CONFIG.IMAGE_SIZE); + ctx.putImageData(imageDataObject, 0, 0); - const latentTensor = tf.tensor2d([latentVector]); - const outputTensor = decoderModel.predict(latentTensor); - const imageTensor = outputTensor.squeeze(); - const normalizedImageTensor = imageTensor.mul(255).round().clipByValue(0, 255).cast('int32'); - const imageDataArray = await normalizedImageTensor.data(); - - const rgbaData = new Uint8ClampedArray(IMAGE_SIZE * IMAGE_SIZE * 4); - for (let i = 0; i < IMAGE_SIZE * IMAGE_SIZE; i++) { - const pixelValue = imageDataArray[i]; - rgbaData[i * 4 + 0] = pixelValue; - rgbaData[i * 4 + 1] = pixelValue; - rgbaData[i * 4 + 2] = pixelValue; - rgbaData[i * 4 + 3] = 255; + } catch (error) { + reportError(error, 'Error al generar letra'); + } finally { + // Limpiar tensores para evitar memory leaks + cleanupTensors([latentTensor, outputTensor, imageTensor, normalizedImageTensor]); } + }, [decoderModel, reportError]); - ctx.clearRect(0, 0, canvas.width, canvas.height); - const imageDataObject = new ImageData(rgbaData, IMAGE_SIZE, IMAGE_SIZE); - ctx.putImageData(imageDataObject, 0, 0); - - tf.dispose([latentTensor, outputTensor, imageTensor, normalizedImageTensor]); - }, [decoderModel]); - - // Efecto para dibujar el espacio latente cuando los datos están listos + // Efecto para inicializar la aplicación cuando los recursos están listos useEffect(() => { - if (decoderModel && latentData) { - // drawLatentSpace(); // ELIMINADO: solo LatentSpacePlot.jsx debe dibujar + if (decoderModel && latentData && latentSpaceBounds) { generateLetter([latentCoords.x, latentCoords.y]); setIsLoading(false); - const timer = setTimeout(() => setIsAppReady(true), 50); // Pequeño delay para la transición - const handleResize = () => {/* nada, el canvas se redibuja por React */}; - window.addEventListener('resize', handleResize); + + const timer = setTimeout(() => setIsAppReady(true), CONFIG.TRANSITION_DELAY); + return () => { clearTimeout(timer); - window.removeEventListener('resize', handleResize); }; } - }, [decoderModel, latentData, generateLetter]); + }, [decoderModel, latentData, latentSpaceBounds, latentCoords, generateLetter]); // Efecto para generar la letra cuando las coordenadas cambian useEffect(() => { - if (isAppReady) { + if (isAppReady && isValidLatentCoords(latentCoords)) { generateLetter([latentCoords.x, latentCoords.y]); } }, [latentCoords, generateLetter, isAppReady]); - const handlePlotClick = (event) => { + // Función throttled para manejar clics en el plot + const handlePlotClick = useCallback(throttle((event) => { const canvas = plotCanvasRef.current; - if (!canvas || !latentSpaceBounds) return; + if (!canvas || !isValidBounds(latentSpaceBounds)) return; const rect = canvas.getBoundingClientRect(); - // Usar el tamaño visual real del canvas - const visualWidth = rect.width; - const visualHeight = rect.height; - const xPixel = event.clientX - rect.left; - const yPixel = event.clientY - rect.top; - const { xMin, xMax, yMin, yMax } = latentSpaceBounds; + const pixelCoords = { + x: event.clientX - rect.left, + y: event.clientY - rect.top + }; - // Usar los mismos offsets y escalas que en el renderizado - const xOffset = PLOT_MARGIN + 10; - const yOffset = PLOT_MARGIN + 10; - const effectiveWidth = visualWidth - 2 * PLOT_MARGIN - 20; - const effectiveHeight = visualHeight - 2 * PLOT_MARGIN - 20; - const xScale = effectiveWidth / (xMax - xMin); - const yScale = effectiveHeight / (yMax - yMin); + const canvasInfo = { + width: rect.width, + height: rect.height, + xOffset: CONFIG.PLOT_MARGIN + CONFIG.PLOT_OFFSET, + yOffset: CONFIG.PLOT_MARGIN + CONFIG.PLOT_OFFSET + }; // Validar que el clic esté dentro del área del gráfico if ( - xPixel < xOffset || xPixel > (visualWidth - xOffset) || - yPixel < yOffset || yPixel > (visualHeight - yOffset) + pixelCoords.x < canvasInfo.xOffset || + pixelCoords.x > (canvasInfo.width - canvasInfo.xOffset) || + pixelCoords.y < canvasInfo.yOffset || + pixelCoords.y > (canvasInfo.height - canvasInfo.yOffset) ) { return; } - // Mapeo idéntico al renderizado - const latentX = xMin + (xPixel - xOffset) / xScale; - const latentY = yMin + ((visualHeight - yOffset - yPixel) / yScale); + const worldCoords = pixelToWorld(pixelCoords, latentSpaceBounds, canvasInfo); + setLatentCoords(worldCoords); + }, 50), [latentSpaceBounds]); - setLatentCoords({ x: latentX, y: latentY }); - }; - - const handleSliderChange = (dim, value) => { - setLatentCoords(prev => ({ ...prev, [dim]: parseFloat(value) })); - }; + const handleSliderChange = useCallback((dim, value) => { + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + setLatentCoords(prev => ({ ...prev, [dim]: numValue })); + } + }, []); - const handleCoordInputChange = (dim, value) => { + const handleCoordInputChange = useCallback((dim, value) => { const parsedValue = parseFloat(value); - if (!isNaN(parsedValue)) { - const { xMin, xMax, yMin, yMax } = latentSpaceBounds; - let clampedValue = parsedValue; - if (dim === 'x') { - clampedValue = Math.max(xMin, Math.min(xMax, parsedValue)); - } else { - clampedValue = Math.max(yMin, Math.min(yMax, parsedValue)); - } - setLatentCoords(prev => ({ ...prev, [dim]: clampedValue })); + if (!isNaN(parsedValue) && isValidBounds(latentSpaceBounds)) { + const { xMin, xMax, yMin, yMax } = latentSpaceBounds; + const clampedValue = dim === 'x' + ? clamp(parsedValue, xMin, xMax) + : clamp(parsedValue, yMin, yMax); + + setLatentCoords(prev => ({ ...prev, [dim]: clampedValue })); } - }; + }, [latentSpaceBounds]); - const handleReset = () => { + const handleReset = useCallback(() => { setLatentCoords({ x: 0, y: 0 }); - }; + }, []); + + // Cleanup al desmontar el componente + useEffect(() => { + return () => { + if (decoderModel) { + decoderModel.dispose(); + } + }; + }, [decoderModel]); return { isLoading, diff --git a/src/hooks/useErrorHandler.js b/src/hooks/useErrorHandler.js new file mode 100644 index 0000000..2c85f64 --- /dev/null +++ b/src/hooks/useErrorHandler.js @@ -0,0 +1,59 @@ +import { useState, useCallback } from 'react'; + +/** + * Hook personalizado para manejo de errores + * @returns {Object} + */ +export const useErrorHandler = () => { + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + + const handleAsync = useCallback(async (asyncFunction, errorMessage = 'Ha ocurrido un error') => { + try { + setIsLoading(true); + setError(null); + const result = await asyncFunction(); + return result; + } catch (err) { + console.error(errorMessage, err); + setError({ + message: errorMessage, + originalError: err, + timestamp: new Date().toISOString() + }); + throw err; + } finally { + setIsLoading(false); + } + }, []); + + const clearError = useCallback(() => { + setError(null); + }, []); + + const reportError = useCallback((error, context = '') => { + const errorInfo = { + message: error.message || 'Error desconocido', + stack: error.stack, + context, + timestamp: new Date().toISOString(), + userAgent: navigator.userAgent, + url: window.location.href + }; + + console.error('Error reportado:', errorInfo); + + // Aquí podrías enviar el error a un servicio de logging como Sentry + // sendToErrorService(errorInfo); + + setError(errorInfo); + }, []); + + return { + error, + isLoading, + handleAsync, + clearError, + reportError + }; +}; diff --git a/src/main.jsx b/src/main.jsx index 54b39dd..bd74c6b 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,10 +1,13 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App.jsx' +import ErrorBoundary from './components/ErrorBoundary.jsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( - + + + , ) diff --git a/src/utils/math.js b/src/utils/math.js new file mode 100644 index 0000000..3ee0d17 --- /dev/null +++ b/src/utils/math.js @@ -0,0 +1,76 @@ +/** + * Utilidades matemáticas para el proyecto + */ + +/** + * Limita un valor entre un mínimo y máximo + * @param {number} value - Valor a limitar + * @param {number} min - Valor mínimo + * @param {number} max - Valor máximo + * @returns {number} + */ +export const clamp = (value, min, max) => { + return Math.min(Math.max(value, min), max); +}; + +/** + * Mapea un valor de un rango a otro + * @param {number} value - Valor a mapear + * @param {number} fromMin - Mínimo del rango origen + * @param {number} fromMax - Máximo del rango origen + * @param {number} toMin - Mínimo del rango destino + * @param {number} toMax - Máximo del rango destino + * @returns {number} + */ +export const mapRange = (value, fromMin, fromMax, toMin, toMax) => { + return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin); +}; + +/** + * Calcula la escala para convertir coordenadas del mundo a píxeles + * @param {Object} bounds - Límites del mundo {xMin, xMax, yMin, yMax} + * @param {number} width - Ancho disponible en píxeles + * @param {number} height - Alto disponible en píxeles + * @returns {Object} - Escalas {xScale, yScale} + */ +export const calculateScale = (bounds, width, height) => { + const xScale = width / (bounds.xMax - bounds.xMin); + const yScale = height / (bounds.yMax - bounds.yMin); + return { xScale, yScale }; +}; + +/** + * Convierte coordenadas del mundo a coordenadas de píxeles + * @param {Object} worldCoords - Coordenadas del mundo {x, y} + * @param {Object} bounds - Límites del mundo + * @param {Object} canvasInfo - Información del canvas {width, height, xOffset, yOffset} + * @returns {Object} - Coordenadas en píxeles {x, y} + */ +export const worldToPixel = (worldCoords, bounds, canvasInfo) => { + const { width, height, xOffset, yOffset } = canvasInfo; + const effectiveWidth = width - 2 * xOffset; + const effectiveHeight = height - 2 * yOffset; + + const xPixel = xOffset + mapRange(worldCoords.x, bounds.xMin, bounds.xMax, 0, effectiveWidth); + const yPixel = height - yOffset - mapRange(worldCoords.y, bounds.yMin, bounds.yMax, 0, effectiveHeight); + + return { x: xPixel, y: yPixel }; +}; + +/** + * Convierte coordenadas de píxeles a coordenadas del mundo + * @param {Object} pixelCoords - Coordenadas en píxeles {x, y} + * @param {Object} bounds - Límites del mundo + * @param {Object} canvasInfo - Información del canvas {width, height, xOffset, yOffset} + * @returns {Object} - Coordenadas del mundo {x, y} + */ +export const pixelToWorld = (pixelCoords, bounds, canvasInfo) => { + const { width, height, xOffset, yOffset } = canvasInfo; + const effectiveWidth = width - 2 * xOffset; + const effectiveHeight = height - 2 * yOffset; + + const worldX = mapRange(pixelCoords.x - xOffset, 0, effectiveWidth, bounds.xMin, bounds.xMax); + const worldY = mapRange(height - yOffset - pixelCoords.y, 0, effectiveHeight, bounds.yMin, bounds.yMax); + + return { x: worldX, y: worldY }; +}; diff --git a/src/utils/performance.js b/src/utils/performance.js new file mode 100644 index 0000000..4a12fc9 --- /dev/null +++ b/src/utils/performance.js @@ -0,0 +1,82 @@ +/** + * Utilidades para manejo de performance y recursos + */ + +/** + * Debounce function para limitar la frecuencia de llamadas + * @param {Function} func - Función a ejecutar + * @param {number} delay - Retraso en milisegundos + * @returns {Function} + */ +export const debounce = (func, delay) => { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => func.apply(null, args), delay); + }; +}; + +/** + * Throttle function para limitar la frecuencia de ejecución + * @param {Function} func - Función a ejecutar + * @param {number} limit - Límite en milisegundos + * @returns {Function} + */ +export const throttle = (func, limit) => { + let inThrottle; + return (...args) => { + if (!inThrottle) { + func.apply(null, args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; +}; + +/** + * Hook personalizado para medir el rendimiento + * @param {string} name - Nombre de la medición + * @returns {Object} + */ +export const usePerformance = (name) => { + const start = () => { + if (typeof performance !== 'undefined' && performance.mark) { + performance.mark(`${name}-start`); + } + }; + + const end = () => { + if (typeof performance !== 'undefined' && performance.mark && performance.measure) { + performance.mark(`${name}-end`); + performance.measure(name, `${name}-start`, `${name}-end`); + } + }; + + return { start, end }; +}; + +/** + * Limpia los recursos de TensorFlow.js + * @param {Array} tensors - Array de tensores a limpiar + */ +export const cleanupTensors = (tensors) => { + if (tensors && Array.isArray(tensors)) { + tensors.forEach(tensor => { + if (tensor && typeof tensor.dispose === 'function') { + tensor.dispose(); + } + }); + } +}; + +/** + * Verifica si hay memory leaks en TensorFlow.js + */ +export const checkMemoryLeaks = () => { + if (typeof window !== 'undefined' && window.tf && window.tf.memory) { + const memInfo = window.tf.memory(); + if (memInfo.numTensors > 100) { // Umbral configurable + console.warn('Posible memory leak detectado. Tensores en memoria:', memInfo.numTensors); + } + } +}; diff --git a/src/utils/validation.js b/src/utils/validation.js new file mode 100644 index 0000000..0cbdae3 --- /dev/null +++ b/src/utils/validation.js @@ -0,0 +1,54 @@ +/** + * Utilidades para validación de datos + */ + +/** + * Valida si un valor es un número válido + * @param {any} value - Valor a validar + * @returns {boolean} + */ +export const isValidNumber = (value) => { + return typeof value === 'number' && !isNaN(value) && isFinite(value); +}; + +/** + * Valida si las coordenadas latentes son válidas + * @param {Object} coords - Coordenadas {x, y} + * @returns {boolean} + */ +export const isValidLatentCoords = (coords) => { + return coords && + isValidNumber(coords.x) && + isValidNumber(coords.y); +}; + +/** + * Valida si los límites del espacio latente son válidos + * @param {Object} bounds - Límites {xMin, xMax, yMin, yMax} + * @returns {boolean} + */ +export const isValidBounds = (bounds) => { + return bounds && + isValidNumber(bounds.xMin) && + isValidNumber(bounds.xMax) && + isValidNumber(bounds.yMin) && + isValidNumber(bounds.yMax) && + bounds.xMax > bounds.xMin && + bounds.yMax > bounds.yMin; +}; + +/** + * Valida si los datos latentes son válidos + * @param {Array} data - Array de datos latentes + * @returns {boolean} + */ +export const isValidLatentData = (data) => { + return Array.isArray(data) && + data.length > 0 && + data.every(point => + point && + isValidNumber(point.x) && + isValidNumber(point.y) && + typeof point.label === 'string' + ); +}; diff --git a/vite.config.js b/vite.config.js index 491ab1d..c5e7775 100644 --- a/vite.config.js +++ b/vite.config.js @@ -5,4 +5,24 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], base: '/cae/', + build: { + outDir: 'dist', + sourcemap: false, + minify: 'terser', + rollupOptions: { + output: { + manualChunks: { + vendor: ['react', 'react-dom'], + tensorflow: ['@tensorflow/tfjs'] + } + } + } + }, + optimizeDeps: { + include: ['@tensorflow/tfjs'] + }, + server: { + port: 3000, + open: true + } }) From 79f865057089b52294327de03d431a3ad1bb2965 Mon Sep 17 00:00:00 2001 From: Pablo Occhiuzzi <104530403+opablon@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:33:01 +0000 Subject: [PATCH 2/8] fix: resolve loading spinner timing issue - Fix gap between spinner disappearing and app being ready - Spinner now shows until both loading is complete AND app is ready - Remove opacity transition dependency on isAppReady state - Ensure smooth loading experience without blank screen --- src/App.jsx | 6 +++--- src/hooks/useAutoencoder.js | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 69f67eb..b4a9791 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -73,14 +73,14 @@ function App() { }); }; - // Renderizar loading spinner de manera optimizada - if (isLoading) { + // Renderizar loading spinner hasta que todo esté completamente listo + if (isLoading || !isAppReady) { return ; } return ( <> -
+
diff --git a/src/hooks/useAutoencoder.js b/src/hooks/useAutoencoder.js index 095e592..59bcdbb 100644 --- a/src/hooks/useAutoencoder.js +++ b/src/hooks/useAutoencoder.js @@ -124,15 +124,18 @@ export const useAutoencoder = () => { useEffect(() => { if (decoderModel && latentData && latentSpaceBounds) { generateLetter([latentCoords.x, latentCoords.y]); - setIsLoading(false); - const timer = setTimeout(() => setIsAppReady(true), CONFIG.TRANSITION_DELAY); + // Pequeño delay para asegurar que la primera letra se genera antes de mostrar la UI + const timer = setTimeout(() => { + setIsLoading(false); + setIsAppReady(true); + }, CONFIG.TRANSITION_DELAY); return () => { clearTimeout(timer); }; } - }, [decoderModel, latentData, latentSpaceBounds, latentCoords, generateLetter]); + }, [decoderModel, latentData, latentSpaceBounds, generateLetter]); // Efecto para generar la letra cuando las coordenadas cambian useEffect(() => { From 41bc4700f755d299c56fd9e4ce4750cc0791a6bc Mon Sep 17 00:00:00 2001 From: Pablo Occhiuzzi <104530403+opablon@users.noreply.github.com> Date: Tue, 5 Aug 2025 12:58:27 +0000 Subject: [PATCH 3/8] fix: improve TensorFlow.js initialization and loading sequence - Add robust backend initialization with fallback options (webgl -> webgpu -> cpu) - Improve loading spinner synchronization with actual ML model readiness - Add comprehensive logging for debugging initialization issues - Wait for successful first letter generation before hiding spinner - Add favicon.svg to resolve 404 error - Update favicon reference in index.html - Add retry mechanism for failed model initialization - Enhance error handling with detailed TensorFlow.js backend information --- index.html | 2 +- public/favicon.svg | 4 ++ src/config/constants.js | 6 +++ src/hooks/useAutoencoder.js | 75 +++++++++++++++++++++++++++++++------ 4 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 public/favicon.svg diff --git a/index.html b/index.html index 4d5df57..721d72d 100644 --- a/index.html +++ b/index.html @@ -27,7 +27,7 @@ - + diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..304e886 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,4 @@ + + + A + diff --git a/src/config/constants.js b/src/config/constants.js index da87615..da1cde4 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -4,6 +4,12 @@ export const CONFIG = { MODEL_PATH: 'tfjs_decoder_model_20250724_135134/model.json', LATENT_DATA_PATH: 'latent_space_data_20250724_130013.json', + // Configuración TensorFlow.js + TENSORFLOW: { + BACKEND_PREFERENCES: ['webgl', 'webgpu', 'cpu'], + INITIALIZATION_TIMEOUT: 5000, + }, + // Dimensiones del canvas IMAGE_SIZE: 64, diff --git a/src/hooks/useAutoencoder.js b/src/hooks/useAutoencoder.js index 59bcdbb..269b0a6 100644 --- a/src/hooks/useAutoencoder.js +++ b/src/hooks/useAutoencoder.js @@ -42,11 +42,36 @@ export const useAutoencoder = () => { useEffect(() => { const loadResources = async () => { try { + // Inicializar TensorFlow.js con backend apropiado + console.log('Inicializando TensorFlow.js...'); + + // Intentar diferentes backends en orden de preferencia + let backendInitialized = false; + for (const backend of CONFIG.TENSORFLOW.BACKEND_PREFERENCES) { + try { + await tf.setBackend(backend); + await tf.ready(); + console.log(`TensorFlow.js inicializado con backend: ${tf.getBackend()}`); + backendInitialized = true; + break; + } catch (error) { + console.warn(`No se pudo inicializar backend ${backend}:`, error.message); + continue; + } + } + + if (!backendInitialized) { + throw new Error('No se pudo inicializar ningún backend de TensorFlow.js'); + } + // Cargar modelo + console.log('Cargando modelo...'); const model = await tf.loadGraphModel(CONFIG.MODEL_PATH); setDecoderModel(model); + console.log('Modelo cargado exitosamente'); // Cargar datos latentes + console.log('Cargando datos latentes...'); const response = await fetch(CONFIG.LATENT_DATA_PATH); if (!response.ok) { throw new Error(`Error al cargar datos: ${response.status} ${response.statusText}`); @@ -67,8 +92,10 @@ export const useAutoencoder = () => { setLatentData(plotData); setLatentSpaceBounds(calculateBounds(plotData)); + console.log('Datos latentes cargados exitosamente'); } catch (error) { + console.error('Error en loadResources:', error); reportError(error, 'Error al cargar recursos del autoencoder'); setIsLoading(false); } @@ -79,11 +106,16 @@ export const useAutoencoder = () => { // Función optimizada para generar letras con limpieza de memoria const generateLetter = useCallback(async (latentVector) => { - if (!decoderModel || !generatedCanvasRef.current) return; + if (!decoderModel || !generatedCanvasRef.current) return false; let latentTensor, outputTensor, imageTensor, normalizedImageTensor; try { + // Verificar que TensorFlow.js esté listo + if (!tf.getBackend()) { + await tf.ready(); + } + const canvas = generatedCanvasRef.current; const ctx = canvas.getContext('2d'); @@ -112,8 +144,11 @@ export const useAutoencoder = () => { const imageDataObject = new ImageData(rgbaData, CONFIG.IMAGE_SIZE, CONFIG.IMAGE_SIZE); ctx.putImageData(imageDataObject, 0, 0); + return true; // Éxito + } catch (error) { reportError(error, 'Error al generar letra'); + return false; // Error } finally { // Limpiar tensores para evitar memory leaks cleanupTensors([latentTensor, outputTensor, imageTensor, normalizedImageTensor]); @@ -123,19 +158,35 @@ export const useAutoencoder = () => { // Efecto para inicializar la aplicación cuando los recursos están listos useEffect(() => { if (decoderModel && latentData && latentSpaceBounds) { - generateLetter([latentCoords.x, latentCoords.y]); - - // Pequeño delay para asegurar que la primera letra se genera antes de mostrar la UI - const timer = setTimeout(() => { - setIsLoading(false); - setIsAppReady(true); - }, CONFIG.TRANSITION_DELAY); - - return () => { - clearTimeout(timer); + const initializeApp = async () => { + try { + // Intentar generar la primera letra + const success = await generateLetter([latentCoords.x, latentCoords.y]); + + if (success) { + // Pequeño delay adicional para asegurar renderizado + setTimeout(() => { + setIsLoading(false); + setIsAppReady(true); + }, 300); + } else { + // Si falla, intentar de nuevo después de un momento + setTimeout(() => { + initializeApp(); + }, 1000); + } + } catch (error) { + reportError(error, 'Error al inicializar aplicación'); + // Intentar de nuevo + setTimeout(() => { + initializeApp(); + }, 1000); + } }; + + initializeApp(); } - }, [decoderModel, latentData, latentSpaceBounds, generateLetter]); + }, [decoderModel, latentData, latentSpaceBounds, generateLetter, latentCoords.x, latentCoords.y, reportError]); // Efecto para generar la letra cuando las coordenadas cambian useEffect(() => { From 651c122265aabae32cdf87976650e1fe23dd9a07 Mon Sep 17 00:00:00 2001 From: Pablo Occhiuzzi <104530403+opablon@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:05:04 +0000 Subject: [PATCH 4/8] debug: temporarily disable first letter generation to isolate loading issue - Remove StrictMode to prevent double execution - Add resourcesLoaded flag to prevent duplicate resource loading - Simplify initialization logic for debugging - Add extensive logging to track initialization process --- public/favicon.ico | 1 + src/hooks/useAutoencoder.js | 59 ++++++++++++++++--------------------- src/main.jsx | 8 ++--- 3 files changed, 30 insertions(+), 38 deletions(-) create mode 100644 public/favicon.ico diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..24d85ae --- /dev/null +++ b/public/favicon.ico @@ -0,0 +1 @@ +data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAD06AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAA== diff --git a/src/hooks/useAutoencoder.js b/src/hooks/useAutoencoder.js index 269b0a6..419c8bc 100644 --- a/src/hooks/useAutoencoder.js +++ b/src/hooks/useAutoencoder.js @@ -13,6 +13,7 @@ export const useAutoencoder = () => { const [latentData, setLatentData] = useState(null); const [latentSpaceBounds, setLatentSpaceBounds] = useState(null); const [latentCoords, setLatentCoords] = useState({ x: 0, y: 0 }); + const [resourcesLoaded, setResourcesLoaded] = useState(false); const plotCanvasRef = useRef(null); const generatedCanvasRef = useRef(null); @@ -38,11 +39,12 @@ export const useAutoencoder = () => { }; }, []); - // Carga del modelo y los datos con manejo de errores mejorado + // Carga del modelo y los datos con manejo de errores mejorado useEffect(() => { + if (resourcesLoaded) return; // Evitar doble carga + const loadResources = async () => { try { - // Inicializar TensorFlow.js con backend apropiado console.log('Inicializando TensorFlow.js...'); // Intentar diferentes backends en orden de preferencia @@ -92,6 +94,7 @@ export const useAutoencoder = () => { setLatentData(plotData); setLatentSpaceBounds(calculateBounds(plotData)); + setResourcesLoaded(true); console.log('Datos latentes cargados exitosamente'); } catch (error) { @@ -102,20 +105,27 @@ export const useAutoencoder = () => { }; loadResources(); - }, [calculateBounds, reportError]); + }, [calculateBounds, reportError, resourcesLoaded]); // Función optimizada para generar letras con limpieza de memoria const generateLetter = useCallback(async (latentVector) => { - if (!decoderModel || !generatedCanvasRef.current) return false; + console.log('generateLetter llamada con:', latentVector); + + if (!decoderModel || !generatedCanvasRef.current) { + console.log('generateLetter: modelo o canvas no disponible'); + return false; + } let latentTensor, outputTensor, imageTensor, normalizedImageTensor; try { // Verificar que TensorFlow.js esté listo if (!tf.getBackend()) { + console.log('generateLetter: inicializando backend...'); await tf.ready(); } + console.log('generateLetter: generando letra...'); const canvas = generatedCanvasRef.current; const ctx = canvas.getContext('2d'); @@ -144,9 +154,11 @@ export const useAutoencoder = () => { const imageDataObject = new ImageData(rgbaData, CONFIG.IMAGE_SIZE, CONFIG.IMAGE_SIZE); ctx.putImageData(imageDataObject, 0, 0); + console.log('generateLetter: letra generada exitosamente'); return true; // Éxito } catch (error) { + console.error('generateLetter: error:', error); reportError(error, 'Error al generar letra'); return false; // Error } finally { @@ -157,36 +169,17 @@ export const useAutoencoder = () => { // Efecto para inicializar la aplicación cuando los recursos están listos useEffect(() => { - if (decoderModel && latentData && latentSpaceBounds) { - const initializeApp = async () => { - try { - // Intentar generar la primera letra - const success = await generateLetter([latentCoords.x, latentCoords.y]); - - if (success) { - // Pequeño delay adicional para asegurar renderizado - setTimeout(() => { - setIsLoading(false); - setIsAppReady(true); - }, 300); - } else { - // Si falla, intentar de nuevo después de un momento - setTimeout(() => { - initializeApp(); - }, 1000); - } - } catch (error) { - reportError(error, 'Error al inicializar aplicación'); - // Intentar de nuevo - setTimeout(() => { - initializeApp(); - }, 1000); - } - }; - - initializeApp(); + if (decoderModel && latentData && latentSpaceBounds && !isAppReady) { + console.log('Recursos listos, inicializando aplicación...'); + + // Por ahora, inicializar sin generar letra para debug + setTimeout(() => { + console.log('Aplicación lista (sin primera letra)'); + setIsLoading(false); + setIsAppReady(true); + }, 500); } - }, [decoderModel, latentData, latentSpaceBounds, generateLetter, latentCoords.x, latentCoords.y, reportError]); + }, [decoderModel, latentData, latentSpaceBounds, isAppReady]); // Efecto para generar la letra cuando las coordenadas cambian useEffect(() => { diff --git a/src/main.jsx b/src/main.jsx index bd74c6b..8ebd85b 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -5,9 +5,7 @@ import ErrorBoundary from './components/ErrorBoundary.jsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( - - - - - , + + + ) From c585e9834f0efc8a1dd40b108f0bbae9b96826c6 Mon Sep 17 00:00:00 2001 From: Pablo Occhiuzzi <104530403+opablon@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:05:41 +0000 Subject: [PATCH 5/8] fix: separate initialization and first letter generation - Split app initialization and first letter generation into separate effects - App now shows immediately when resources are loaded - First letter generation happens asynchronously after app is ready - Prevents infinite loading spinner by decoupling UI readiness from letter generation - Add comprehensive logging for debugging - Remove dependency on letter generation success for app initialization --- src/hooks/useAutoencoder.js | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/hooks/useAutoencoder.js b/src/hooks/useAutoencoder.js index 419c8bc..d110032 100644 --- a/src/hooks/useAutoencoder.js +++ b/src/hooks/useAutoencoder.js @@ -172,15 +172,33 @@ export const useAutoencoder = () => { if (decoderModel && latentData && latentSpaceBounds && !isAppReady) { console.log('Recursos listos, inicializando aplicación...'); - // Por ahora, inicializar sin generar letra para debug - setTimeout(() => { - console.log('Aplicación lista (sin primera letra)'); - setIsLoading(false); - setIsAppReady(true); - }, 500); + // Inicializar aplicación inmediatamente + setIsLoading(false); + setIsAppReady(true); + console.log('Aplicación lista'); } }, [decoderModel, latentData, latentSpaceBounds, isAppReady]); + // Efecto separado para generar la primera letra después de que la app esté lista + useEffect(() => { + if (isAppReady && decoderModel && generatedCanvasRef.current) { + console.log('Generando primera letra...'); + + const generateInitialLetter = async () => { + try { + await generateLetter([latentCoords.x, latentCoords.y]); + console.log('Primera letra generada exitosamente'); + } catch (error) { + console.error('Error al generar primera letra:', error); + } + }; + + // Pequeño delay para asegurar que el canvas esté completamente renderizado + const timer = setTimeout(generateInitialLetter, 100); + return () => clearTimeout(timer); + } + }, [isAppReady, decoderModel, generateLetter, latentCoords.x, latentCoords.y]); + // Efecto para generar la letra cuando las coordenadas cambian useEffect(() => { if (isAppReady && isValidLatentCoords(latentCoords)) { From 59b93581e200ee043eaa1fdcebad7b02e09c56ad Mon Sep 17 00:00:00 2001 From: Pablo Occhiuzzi <104530403+opablon@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:09:07 +0000 Subject: [PATCH 6/8] polish: clean up debugging logs and restore production settings - Re-enable React.StrictMode for production - Remove excessive logging while keeping essential error logs - Optimize resource loading with Promise.all for parallel execution - Simplify console output for better user experience - Maintain robust error handling without verbose debugging - Ready for production deployment --- src/hooks/useAutoencoder.js | 38 ++++++++++++------------------------- src/main.jsx | 8 +++++--- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/hooks/useAutoencoder.js b/src/hooks/useAutoencoder.js index d110032..a0a1a49 100644 --- a/src/hooks/useAutoencoder.js +++ b/src/hooks/useAutoencoder.js @@ -53,11 +53,11 @@ export const useAutoencoder = () => { try { await tf.setBackend(backend); await tf.ready(); - console.log(`TensorFlow.js inicializado con backend: ${tf.getBackend()}`); + console.log(`TensorFlow.js backend: ${tf.getBackend()}`); backendInitialized = true; break; } catch (error) { - console.warn(`No se pudo inicializar backend ${backend}:`, error.message); + console.warn(`Backend ${backend} no disponible:`, error.message); continue; } } @@ -66,15 +66,12 @@ export const useAutoencoder = () => { throw new Error('No se pudo inicializar ningún backend de TensorFlow.js'); } - // Cargar modelo - console.log('Cargando modelo...'); - const model = await tf.loadGraphModel(CONFIG.MODEL_PATH); - setDecoderModel(model); - console.log('Modelo cargado exitosamente'); + // Cargar modelo y datos + const [model, response] = await Promise.all([ + tf.loadGraphModel(CONFIG.MODEL_PATH), + fetch(CONFIG.LATENT_DATA_PATH) + ]); - // Cargar datos latentes - console.log('Cargando datos latentes...'); - const response = await fetch(CONFIG.LATENT_DATA_PATH); if (!response.ok) { throw new Error(`Error al cargar datos: ${response.status} ${response.statusText}`); } @@ -92,13 +89,14 @@ export const useAutoencoder = () => { label: data.labels[index], })); + setDecoderModel(model); setLatentData(plotData); setLatentSpaceBounds(calculateBounds(plotData)); setResourcesLoaded(true); - console.log('Datos latentes cargados exitosamente'); + console.log('Recursos cargados exitosamente'); } catch (error) { - console.error('Error en loadResources:', error); + console.error('Error al cargar recursos:', error); reportError(error, 'Error al cargar recursos del autoencoder'); setIsLoading(false); } @@ -109,10 +107,7 @@ export const useAutoencoder = () => { // Función optimizada para generar letras con limpieza de memoria const generateLetter = useCallback(async (latentVector) => { - console.log('generateLetter llamada con:', latentVector); - if (!decoderModel || !generatedCanvasRef.current) { - console.log('generateLetter: modelo o canvas no disponible'); return false; } @@ -121,11 +116,9 @@ export const useAutoencoder = () => { try { // Verificar que TensorFlow.js esté listo if (!tf.getBackend()) { - console.log('generateLetter: inicializando backend...'); await tf.ready(); } - console.log('generateLetter: generando letra...'); const canvas = generatedCanvasRef.current; const ctx = canvas.getContext('2d'); @@ -154,11 +147,10 @@ export const useAutoencoder = () => { const imageDataObject = new ImageData(rgbaData, CONFIG.IMAGE_SIZE, CONFIG.IMAGE_SIZE); ctx.putImageData(imageDataObject, 0, 0); - console.log('generateLetter: letra generada exitosamente'); return true; // Éxito } catch (error) { - console.error('generateLetter: error:', error); + console.error('Error al generar letra:', error); reportError(error, 'Error al generar letra'); return false; // Error } finally { @@ -170,24 +162,18 @@ export const useAutoencoder = () => { // Efecto para inicializar la aplicación cuando los recursos están listos useEffect(() => { if (decoderModel && latentData && latentSpaceBounds && !isAppReady) { - console.log('Recursos listos, inicializando aplicación...'); - - // Inicializar aplicación inmediatamente + console.log('Aplicación inicializada exitosamente'); setIsLoading(false); setIsAppReady(true); - console.log('Aplicación lista'); } }, [decoderModel, latentData, latentSpaceBounds, isAppReady]); // Efecto separado para generar la primera letra después de que la app esté lista useEffect(() => { if (isAppReady && decoderModel && generatedCanvasRef.current) { - console.log('Generando primera letra...'); - const generateInitialLetter = async () => { try { await generateLetter([latentCoords.x, latentCoords.y]); - console.log('Primera letra generada exitosamente'); } catch (error) { console.error('Error al generar primera letra:', error); } diff --git a/src/main.jsx b/src/main.jsx index 8ebd85b..bd74c6b 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -5,7 +5,9 @@ import ErrorBoundary from './components/ErrorBoundary.jsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( - - - + + + + + , ) From 57f12efadb0d1339765e467e0ac232869c42a22c Mon Sep 17 00:00:00 2001 From: Pablo Occhiuzzi <104530403+opablon@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:14:39 +0000 Subject: [PATCH 7/8] cleanup: remove duplicate favicon and minimize console logging - Remove duplicate favicon.ico, keep only favicon.svg - Reduce console logging to only essential error messages - Use useRef for resourcesLoaded to prevent double execution in StrictMode - Remove verbose initialization messages - Keep only warning for TensorFlow.js backend fallbacks - Cleaner console output for production use --- public/favicon.ico | 1 - src/hooks/useAutoencoder.js | 24 +++++++++++------------- 2 files changed, 11 insertions(+), 14 deletions(-) delete mode 100644 public/favicon.ico diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 24d85ae..0000000 --- a/public/favicon.ico +++ /dev/null @@ -1 +0,0 @@ -data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAD06AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAA== diff --git a/src/hooks/useAutoencoder.js b/src/hooks/useAutoencoder.js index a0a1a49..2bae823 100644 --- a/src/hooks/useAutoencoder.js +++ b/src/hooks/useAutoencoder.js @@ -13,8 +13,8 @@ export const useAutoencoder = () => { const [latentData, setLatentData] = useState(null); const [latentSpaceBounds, setLatentSpaceBounds] = useState(null); const [latentCoords, setLatentCoords] = useState({ x: 0, y: 0 }); - const [resourcesLoaded, setResourcesLoaded] = useState(false); - + + const resourcesLoaded = useRef(false); const plotCanvasRef = useRef(null); const generatedCanvasRef = useRef(null); const { reportError } = useErrorHandler(); @@ -41,23 +41,23 @@ export const useAutoencoder = () => { // Carga del modelo y los datos con manejo de errores mejorado useEffect(() => { - if (resourcesLoaded) return; // Evitar doble carga + if (resourcesLoaded.current) return; // Evitar doble carga const loadResources = async () => { try { - console.log('Inicializando TensorFlow.js...'); - // Intentar diferentes backends en orden de preferencia let backendInitialized = false; for (const backend of CONFIG.TENSORFLOW.BACKEND_PREFERENCES) { try { await tf.setBackend(backend); await tf.ready(); - console.log(`TensorFlow.js backend: ${tf.getBackend()}`); backendInitialized = true; break; } catch (error) { - console.warn(`Backend ${backend} no disponible:`, error.message); + // Solo mostrar warning si es el último backend que falla + if (backend === CONFIG.TENSORFLOW.BACKEND_PREFERENCES[CONFIG.TENSORFLOW.BACKEND_PREFERENCES.length - 1]) { + console.warn('Algunos backends de TensorFlow.js no están disponibles, usando fallback'); + } continue; } } @@ -66,7 +66,7 @@ export const useAutoencoder = () => { throw new Error('No se pudo inicializar ningún backend de TensorFlow.js'); } - // Cargar modelo y datos + // Cargar modelo y datos en paralelo const [model, response] = await Promise.all([ tf.loadGraphModel(CONFIG.MODEL_PATH), fetch(CONFIG.LATENT_DATA_PATH) @@ -92,18 +92,17 @@ export const useAutoencoder = () => { setDecoderModel(model); setLatentData(plotData); setLatentSpaceBounds(calculateBounds(plotData)); - setResourcesLoaded(true); - console.log('Recursos cargados exitosamente'); + resourcesLoaded.current = true; } catch (error) { - console.error('Error al cargar recursos:', error); + console.error('Error al cargar recursos del autoencoder:', error); reportError(error, 'Error al cargar recursos del autoencoder'); setIsLoading(false); } }; loadResources(); - }, [calculateBounds, reportError, resourcesLoaded]); + }, [calculateBounds, reportError]); // Función optimizada para generar letras con limpieza de memoria const generateLetter = useCallback(async (latentVector) => { @@ -162,7 +161,6 @@ export const useAutoencoder = () => { // Efecto para inicializar la aplicación cuando los recursos están listos useEffect(() => { if (decoderModel && latentData && latentSpaceBounds && !isAppReady) { - console.log('Aplicación inicializada exitosamente'); setIsLoading(false); setIsAppReady(true); } From edf8f799cd6fedd70d9ce276214e4ab24e2f8214 Mon Sep 17 00:00:00 2001 From: Pablo Occhiuzzi <104530403+opablon@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:22:34 +0000 Subject: [PATCH 8/8] fix: improve coordinate input fields functionality - Change input type from 'number' to 'text' to allow decimal and negative input - Add local state management in ControlPanel for coordinate inputs - Implement onBlur and onKeyPress (Enter) validation - Allow users to type partial values like '-' or '1.' without immediate validation - Add placeholder text showing valid range for each coordinate - Add tooltips with precise min/max values - Simplify handleCoordInputChange in useAutoencoder hook - Ensure values are clamped within latent space bounds - Better UX for manual coordinate entry --- src/components/ControlPanel.jsx | 85 ++++++++++++++++++++++++++++----- src/hooks/useAutoencoder.js | 13 ++--- 2 files changed, 75 insertions(+), 23 deletions(-) diff --git a/src/components/ControlPanel.jsx b/src/components/ControlPanel.jsx index 103ff48..e75f4f9 100644 --- a/src/components/ControlPanel.jsx +++ b/src/components/ControlPanel.jsx @@ -1,8 +1,23 @@ -import { memo } from 'react'; +import { memo, useState, useEffect } from 'react'; import { CONFIG } from '../config/constants'; import { isValidBounds } from '../utils/validation'; +import { clamp } from '../utils/math'; const ControlPanel = memo(({ latentCoords, latentSpaceBounds, onSliderChange, onReset, onCoordInputChange }) => { + // Estado local para los inputs de coordenadas + const [localCoords, setLocalCoords] = useState({ + x: latentCoords.x.toFixed(CONFIG.COORDINATE_PRECISION), + y: latentCoords.y.toFixed(CONFIG.COORDINATE_PRECISION) + }); + + // Sincronizar el estado local cuando cambien las coordenadas externas + useEffect(() => { + setLocalCoords({ + x: latentCoords.x.toFixed(CONFIG.COORDINATE_PRECISION), + y: latentCoords.y.toFixed(CONFIG.COORDINATE_PRECISION) + }); + }, [latentCoords.x, latentCoords.y]); + // Valores por defecto seguros const bounds = isValidBounds(latentSpaceBounds) ? latentSpaceBounds @@ -13,6 +28,48 @@ const ControlPanel = memo(({ latentCoords, latentSpaceBounds, onSliderChange, on // Formatear coordenadas con la precisión configurada const formatCoord = (value) => value.toFixed(CONFIG.COORDINATE_PRECISION); + // Manejar cambios en los inputs de coordenadas + const handleInputChange = (dim, value) => { + // Actualizar el estado local inmediatamente para permitir escritura + setLocalCoords(prev => ({ ...prev, [dim]: value })); + }; + + // Manejar cuando el usuario termina de editar (onBlur o Enter) + const handleInputCommit = (dim, value) => { + const parsedValue = parseFloat(value); + + if (!isNaN(parsedValue) && isValidBounds(latentSpaceBounds)) { + // Clamp del valor dentro de los límites + const { xMin, xMax, yMin, yMax } = latentSpaceBounds; + const clampedValue = dim === 'x' + ? clamp(parsedValue, xMin, xMax) + : clamp(parsedValue, yMin, yMax); + + // Actualizar las coordenadas reales + onCoordInputChange(dim, clampedValue); + + // Actualizar el estado local con el valor formateado + setLocalCoords(prev => ({ + ...prev, + [dim]: clampedValue.toFixed(CONFIG.COORDINATE_PRECISION) + })); + } else { + // Si el valor no es válido, revertir al valor anterior + setLocalCoords(prev => ({ + ...prev, + [dim]: latentCoords[dim].toFixed(CONFIG.COORDINATE_PRECISION) + })); + } + }; + + // Manejar Enter en los inputs + const handleKeyPress = (e, dim, value) => { + if (e.key === 'Enter') { + handleInputCommit(dim, value); + e.target.blur(); // Quitar el foco para activar onBlur también + } + }; + return (

@@ -29,15 +86,16 @@ const ControlPanel = memo(({ latentCoords, latentSpaceBounds, onSliderChange, on X: onCoordInputChange('x', e.target.value)} + value={localCoords.x} + onChange={(e) => handleInputChange('x', e.target.value)} + onBlur={(e) => handleInputCommit('x', e.target.value)} + onKeyPress={(e) => handleKeyPress(e, 'x', e.target.value)} className="w-24 p-2 text-center border border-gray-300 rounded-md font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - step="0.01" - min={xMin} - max={xMax} + placeholder={`${xMin.toFixed(1)} a ${xMax.toFixed(1)}`} aria-label="Coordenada X del espacio latente" + title={`Ingrese un valor entre ${xMin.toFixed(2)} y ${xMax.toFixed(2)}`} />

@@ -48,15 +106,16 @@ const ControlPanel = memo(({ latentCoords, latentSpaceBounds, onSliderChange, on Y: onCoordInputChange('y', e.target.value)} + value={localCoords.y} + onChange={(e) => handleInputChange('y', e.target.value)} + onBlur={(e) => handleInputCommit('y', e.target.value)} + onKeyPress={(e) => handleKeyPress(e, 'y', e.target.value)} className="w-24 p-2 text-center border border-gray-300 rounded-md font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" - step="0.01" - min={yMin} - max={yMax} + placeholder={`${yMin.toFixed(1)} a ${yMax.toFixed(1)}`} aria-label="Coordenada Y del espacio latente" + title={`Ingrese un valor entre ${yMin.toFixed(2)} y ${yMax.toFixed(2)}`} />
diff --git a/src/hooks/useAutoencoder.js b/src/hooks/useAutoencoder.js index 2bae823..99a34fb 100644 --- a/src/hooks/useAutoencoder.js +++ b/src/hooks/useAutoencoder.js @@ -230,16 +230,9 @@ export const useAutoencoder = () => { }, []); const handleCoordInputChange = useCallback((dim, value) => { - const parsedValue = parseFloat(value); - if (!isNaN(parsedValue) && isValidBounds(latentSpaceBounds)) { - const { xMin, xMax, yMin, yMax } = latentSpaceBounds; - const clampedValue = dim === 'x' - ? clamp(parsedValue, xMin, xMax) - : clamp(parsedValue, yMin, yMax); - - setLatentCoords(prev => ({ ...prev, [dim]: clampedValue })); - } - }, [latentSpaceBounds]); + // El valor ya viene procesado desde el ControlPanel + setLatentCoords(prev => ({ ...prev, [dim]: value })); + }, []); const handleReset = useCallback(() => { setLatentCoords({ x: 0, y: 0 });