From 8f06bf0c6bfd93f96731795a6bda1aeed5566add Mon Sep 17 00:00:00 2001 From: Alexis Date: Sun, 22 Mar 2026 12:48:31 +0100 Subject: [PATCH 01/80] =?UTF-8?q?Configuraci=C3=B3n=20inicial:=20estructur?= =?UTF-8?q?a=20de=20carpetas=20y=20Docker=20base=20para=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/Dockerfile | 7 +++++++ docker-compose.yaml | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 backend/Dockerfile create mode 100644 docker-compose.yaml diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..b862b2b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.10-slim + +WORKDIR /app + +RUN pip install fastapi uvicorn + +CMD ["uvicorn", "api.routes:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..2dc0c35 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,10 @@ +version: '3.8' + +services: + backend: + build: + context: ./backend + ports: + - "8000:8000" + volumes: + - ./backend:/app \ No newline at end of file From 2d4779e68ca9f415c0eac1b1252c36ac892a4282 Mon Sep 17 00:00:00 2001 From: Alexis Date: Sun, 22 Mar 2026 13:10:48 +0100 Subject: [PATCH 02/80] Ruta de prueba --- backend/api/__pycache__/routes.cpython-310.pyc | Bin 0 -> 379 bytes backend/api/routes.py | 8 ++++++++ docker-compose.yaml | 2 -- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 backend/api/__pycache__/routes.cpython-310.pyc create mode 100644 backend/api/routes.py diff --git a/backend/api/__pycache__/routes.cpython-310.pyc b/backend/api/__pycache__/routes.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cd7e9549ac7c8c61abd7e436a64f21bbf335a1d7 GIT binary patch literal 379 zcmYjMJ5Iwu5Z#X*TZp8fqFg%M;0qu=LLg`&Bsw?I;+Y_W?X`C80@`p8TyhE|xP?VW z#TB4oHbF{8n$O!e`}D?)$6JhI_w%jzrT*o?W=H}jly092FyJMtc*Z$HzF?U^@ghO= z93%wKY{9bt!W)*1zG<5!e26ZzYfrDQo}#p|9p38P6PqCjoKd=%3UCkwCjmLlq6%xL z@3A{3;=R8#rB;9rQ2lG}sq<3hrf#uDFqTLKCVxPy(6t}Ens;?xm|E9hUYYfWtjo_% zs+)$gNUiBwbO+7jFtJ^$XMnSn)zG{0_F`%MJWawZn$tb>%lh|0lExtKuwC+N_0<&k WmH4nS(3N<^2`z8jW+M@c3I78RrBy5d literal 0 HcmV?d00001 diff --git a/backend/api/routes.py b/backend/api/routes.py new file mode 100644 index 0000000..f52258d --- /dev/null +++ b/backend/api/routes.py @@ -0,0 +1,8 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/prueba") +def ruta_de_prueba(): + # En FastAPI si devuelves un diccionario, se transforma a JSON + return {"mensaje": "¡Hola desde FastAPI! El contenedor de docker está funcionando"} \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 2dc0c35..6d49e5b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '3.8' - services: backend: build: From cccfe639e11df075a4f3cc303c9bf2b3f486e9ea Mon Sep 17 00:00:00 2001 From: Alexis Date: Sun, 22 Mar 2026 13:17:48 +0100 Subject: [PATCH 03/80] =?UTF-8?q?Eliminada=20cach=C3=A9=20de=20Python=20y?= =?UTF-8?q?=20a=C3=B1adido=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/__pycache__/routes.cpython-310.pyc | Bin 379 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 backend/api/__pycache__/routes.cpython-310.pyc diff --git a/backend/api/__pycache__/routes.cpython-310.pyc b/backend/api/__pycache__/routes.cpython-310.pyc deleted file mode 100644 index cd7e9549ac7c8c61abd7e436a64f21bbf335a1d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 379 zcmYjMJ5Iwu5Z#X*TZp8fqFg%M;0qu=LLg`&Bsw?I;+Y_W?X`C80@`p8TyhE|xP?VW z#TB4oHbF{8n$O!e`}D?)$6JhI_w%jzrT*o?W=H}jly092FyJMtc*Z$HzF?U^@ghO= z93%wKY{9bt!W)*1zG<5!e26ZzYfrDQo}#p|9p38P6PqCjoKd=%3UCkwCjmLlq6%xL z@3A{3;=R8#rB;9rQ2lG}sq<3hrf#uDFqTLKCVxPy(6t}Ens;?xm|E9hUYYfWtjo_% zs+)$gNUiBwbO+7jFtJ^$XMnSn)zG{0_F`%MJWawZn$tb>%lh|0lExtKuwC+N_0<&k WmH4nS(3N<^2`z8jW+M@c3I78RrBy5d From 9078a01a99bdd8678ad314c4f93e739ce7a07393 Mon Sep 17 00:00:00 2001 From: Alexis Date: Sun, 22 Mar 2026 13:18:45 +0100 Subject: [PATCH 04/80] =?UTF-8?q?Eliminada=20cach=C3=A9=20de=20Python=20y?= =?UTF-8?q?=20a=C3=B1adido=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e327dab --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Caché de Python +__pycache__/ +*.py[cod] +*$py.class + +# Variables de entorno +.env + +# Configuraciones del editor +.vscode/ +.idea/ \ No newline at end of file From a043dad1617ef99a0992776b089d2bdc002b0a38 Mon Sep 17 00:00:00 2001 From: Alexis Date: Sun, 22 Mar 2026 13:33:23 +0100 Subject: [PATCH 05/80] =?UTF-8?q?A=C3=B1adida=20descripci=C3=B3n=20al=20re?= =?UTF-8?q?adme=20para=20probar=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2e70b14..fa39cd1 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -# deck-of-cards \ No newline at end of file +# deck-of-cards + +Prueba de feature en una rama hija de develop para hacer un pull request \ No newline at end of file From df24ed46b53667faff99fdcde4fb45bedcc5a5a6 Mon Sep 17 00:00:00 2001 From: Alexis Date: Sun, 22 Mar 2026 15:28:32 +0100 Subject: [PATCH 06/80] Configurado frontend de React con Vite 8.0 y Docker --- docker-compose.yaml | 11 +- frontend/.gitignore | 24 + frontend/Dockerfile | 5 + frontend/README.md | 16 + frontend/eslint.config.js | 29 + frontend/index.html | 13 + frontend/package-lock.json | 2599 +++++++++++++++++++++++++++++++++ frontend/package.json | 27 + frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/src/App.css | 184 +++ frontend/src/App.jsx | 121 ++ frontend/src/assets/hero.png | Bin 0 -> 44919 bytes frontend/src/assets/react.svg | 1 + frontend/src/assets/vite.svg | 1 + frontend/src/index.css | 111 ++ frontend/src/main.jsx | 10 + frontend/vite.config.js | 7 + 18 files changed, 3183 insertions(+), 1 deletion(-) create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/assets/hero.png create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/vite.config.js diff --git a/docker-compose.yaml b/docker-compose.yaml index 6d49e5b..38e74c6 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,4 +5,13 @@ services: ports: - "8000:8000" volumes: - - ./backend:/app \ No newline at end of file + - ./backend:/app + + frontend: + build: + context: ./frontend + ports: + - "5173:5173" + volumes: + - ./frontend:/app + - /app/node_modules \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..82936ab --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,5 @@ +FROM node:20-alpine +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm install +CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..a36934d --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..4fa125d --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }], + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..f94d687 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + frontend + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..663adb7 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2599 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", + "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", + "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", + "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-rc.7" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", + "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.120.0", + "@rolldown/pluginutils": "1.0.0-rc.10" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-x64": "1.0.0-rc.10", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", + "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", + "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.10", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f13a5b5 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,27 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "vite": "^8.0.1" + } +} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..f90339d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,184 @@ +.counter { + font-size: 16px; + padding: 5px 10px; + border-radius: 5px; + color: var(--accent); + background: var(--accent-bg); + border: 2px solid transparent; + transition: border-color 0.3s; + margin-bottom: 24px; + + &:hover { + border-color: var(--accent-border); + } + &:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } +} + +.hero { + position: relative; + + .base, + .framework, + .vite { + inset-inline: 0; + margin: 0 auto; + } + + .base { + width: 170px; + position: relative; + z-index: 0; + } + + .framework, + .vite { + position: absolute; + } + + .framework { + z-index: 1; + top: 34px; + height: 28px; + transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) + scale(1.4); + } + + .vite { + z-index: 0; + top: 107px; + height: 26px; + width: auto; + transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) + scale(0.8); + } +} + +#center { + display: flex; + flex-direction: column; + gap: 25px; + place-content: center; + place-items: center; + flex-grow: 1; + + @media (max-width: 1024px) { + padding: 32px 20px 24px; + gap: 18px; + } +} + +#next-steps { + display: flex; + border-top: 1px solid var(--border); + text-align: left; + + & > div { + flex: 1 1 0; + padding: 32px; + @media (max-width: 1024px) { + padding: 24px 20px; + } + } + + .icon { + margin-bottom: 16px; + width: 22px; + height: 22px; + } + + @media (max-width: 1024px) { + flex-direction: column; + text-align: center; + } +} + +#docs { + border-right: 1px solid var(--border); + + @media (max-width: 1024px) { + border-right: none; + border-bottom: 1px solid var(--border); + } +} + +#next-steps ul { + list-style: none; + padding: 0; + display: flex; + gap: 8px; + margin: 32px 0 0; + + .logo { + height: 18px; + } + + a { + color: var(--text-h); + font-size: 16px; + border-radius: 6px; + background: var(--social-bg); + display: flex; + padding: 6px 12px; + align-items: center; + gap: 8px; + text-decoration: none; + transition: box-shadow 0.3s; + + &:hover { + box-shadow: var(--shadow); + } + .button-icon { + height: 18px; + width: 18px; + } + } + + @media (max-width: 1024px) { + margin-top: 20px; + flex-wrap: wrap; + justify-content: center; + + li { + flex: 1 1 calc(50% - 8px); + } + + a { + width: 100%; + justify-content: center; + box-sizing: border-box; + } + } +} + +#spacer { + height: 88px; + border-top: 1px solid var(--border); + @media (max-width: 1024px) { + height: 48px; + } +} + +.ticks { + position: relative; + width: 100%; + + &::before, + &::after { + content: ''; + position: absolute; + top: -4.5px; + border: 5px solid transparent; + } + + &::before { + left: 0; + border-left-color: var(--border); + } + &::after { + right: 0; + border-right-color: var(--border); + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..b2bf2e8 --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,121 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from './assets/vite.svg' +import heroImg from './assets/hero.png' +import './App.css' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +
+
+ + React logo + Vite logo +
+
+

Get started

+

+ Edit src/App.jsx and save to test HMR +

+
+ +
+ +
+ +
+
+ +

Documentation

+

Your questions, answered

+ +
+
+ +

Connect with us

+

Join the Vite community

+ +
+
+ +
+
+ + ) +} + +export default App diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..cc51a3d20ad4bc961b596a6adfd686685cd84bb0 GIT binary patch literal 44919 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 JPDHLkV1myZcL)Fg literal 0 HcmV?d00001 diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..2c84af0 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,111 @@ +:root { + --text: #6b6375; + --text-h: #08060d; + --bg: #fff; + --border: #e5e4e7; + --code-bg: #f4f3ec; + --accent: #aa3bff; + --accent-bg: rgba(170, 59, 255, 0.1); + --accent-border: rgba(170, 59, 255, 0.5); + --social-bg: rgba(244, 243, 236, 0.5); + --shadow: + rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; + + --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: ui-monospace, Consolas, monospace; + + font: 18px/145% var(--sans); + letter-spacing: 0.18px; + color-scheme: light dark; + color: var(--text); + background: var(--bg); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + @media (max-width: 1024px) { + font-size: 16px; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --text: #9ca3af; + --text-h: #f3f4f6; + --bg: #16171d; + --border: #2e303a; + --code-bg: #1f2028; + --accent: #c084fc; + --accent-bg: rgba(192, 132, 252, 0.15); + --accent-border: rgba(192, 132, 252, 0.5); + --social-bg: rgba(47, 48, 58, 0.5); + --shadow: + rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; + } + + #social .button-icon { + filter: invert(1) brightness(2); + } +} + +body { + margin: 0; +} + +#root { + width: 1126px; + max-width: 100%; + margin: 0 auto; + text-align: center; + border-inline: 1px solid var(--border); + min-height: 100svh; + display: flex; + flex-direction: column; + box-sizing: border-box; +} + +h1, +h2 { + font-family: var(--heading); + font-weight: 500; + color: var(--text-h); +} + +h1 { + font-size: 56px; + letter-spacing: -1.68px; + margin: 32px 0; + @media (max-width: 1024px) { + font-size: 36px; + margin: 20px 0; + } +} +h2 { + font-size: 24px; + line-height: 118%; + letter-spacing: -0.24px; + margin: 0 0 8px; + @media (max-width: 1024px) { + font-size: 20px; + } +} +p { + margin: 0; +} + +code, +.counter { + font-family: var(--mono); + display: inline-flex; + border-radius: 4px; + color: var(--text-h); +} + +code { + font-size: 15px; + line-height: 135%; + padding: 4px 8px; + background: var(--code-bg); +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/frontend/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From 7f734bd15cc31d92a82ec331ee5afe7190ad0569 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Sun, 22 Mar 2026 17:31:01 +0100 Subject: [PATCH 07/80] Prueba --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa39cd1..7014423 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # deck-of-cards -Prueba de feature en una rama hija de develop para hacer un pull request \ No newline at end of file +Prueba de feature en una rama hija de develop para hacer un pull request. V2 \ No newline at end of file From 113c20204dc8086eb99ecc5228884f9fbc3a9c23 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Mon, 23 Mar 2026 11:04:28 +0100 Subject: [PATCH 08/80] =?UTF-8?q?A=C3=B1adido=20endpoint=20para=20el=20c?= =?UTF-8?q?=C3=A1lculo=20de=20referencias?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/routes.py | 56 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/backend/api/routes.py b/backend/api/routes.py index f52258d..a5b50bb 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -1,8 +1,60 @@ from fastapi import FastAPI +from pydantic import BaseModel +from typing import List, Dict app = FastAPI() +# 1. Definimos el modelo de datos basado en el JSON que se enviará desde el frontend +class ValueFunctionRequest(BaseModel): + criterion_name: str + levels: List[str] + blank_cards: List[int] + references: Dict[str, float] + +# Ruta prueba para probar que todo funciona @app.get("/prueba") def ruta_de_prueba(): - # En FastAPI si devuelves un diccionario, se transforma a JSON - return {"mensaje": "¡Hola desde FastAPI! El contenedor de docker está funcionando"} \ No newline at end of file + return {"mensaje": "¡Hola desde FastAPI! El contenedor de docker está funcionando"} + +# 2. Endpoint POST para recibir los datos y calcular los valores +@app.post("/api/criteria/doc/value-function") +def calcular_funcion_valor(request: ValueFunctionRequest): + levels = request.levels + blank_cards = request.blank_cards + refs = request.references + + # Extraemos las claves de referencia y las convertimos a enteros + claves_ref = sorted(int(k) for k in refs.keys()) # ejemplo: [0, 4] + p, q = claves_ref # p = 0, q = 4 + + # Valores de referencia + up = refs[str(p)] + uq = refs[str(q)] + + # PASO A: Calculamos el número total de unidades elementales 'h' entre p y q + h = sum(blank_cards[r] + 1 for r in range(p, q)) + + # PASO B: Calculamos el valor fraccional de una sola unidad 'alpha' + alpha = (uq - up) / h if h != 0 else 0 + + # Inicializamos la lista de valores V con el tamaño de los niveles + V = [0.0] * len(levels) + V[p] = up + + # PASO C: Calculamos los valores hacia adelante (niveles por encima de p) + for i in range(p + 1, len(levels)): + suma_unidades = sum(blank_cards[r] + 1 for r in range(p, i)) + V[i] = up + alpha * suma_unidades + + # PASO D: Calculamos los valores hacia atrás (niveles por debajo de p) + for i in range(p - 1, -1, -1): + suma_unidades = sum(blank_cards[r] + 1 for r in range(i, p)) + V[i] = up - alpha * suma_unidades + + # Formateamos la respuesta + resultado_valores = {levels[i]: round(V[i], 4) for i in range(len(levels))} + + return { + "criterion_name": request.criterion_name, + "values": resultado_valores + } From 7deda89ad0f2667d19f3c496524ec434225a5a5f Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Mon, 23 Mar 2026 11:16:37 +0100 Subject: [PATCH 09/80] =?UTF-8?q?Optimizaci=C3=B3n=20c=C3=B3digo=20del=20P?= =?UTF-8?q?OST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/routes.py | 72 ++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/backend/api/routes.py b/backend/api/routes.py index a5b50bb..e84950b 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -4,57 +4,51 @@ from typing import List, Dict app = FastAPI() -# 1. Definimos el modelo de datos basado en el JSON que se enviará desde el frontend class ValueFunctionRequest(BaseModel): criterion_name: str levels: List[str] blank_cards: List[int] references: Dict[str, float] -# Ruta prueba para probar que todo funciona @app.get("/prueba") -def ruta_de_prueba(): +def prueba(): return {"mensaje": "¡Hola desde FastAPI! El contenedor de docker está funcionando"} -# 2. Endpoint POST para recibir los datos y calcular los valores @app.post("/api/criteria/doc/value-function") -def calcular_funcion_valor(request: ValueFunctionRequest): +def calcular(request: ValueFunctionRequest): levels = request.levels - blank_cards = request.blank_cards - refs = request.references - - # Extraemos las claves de referencia y las convertimos a enteros - claves_ref = sorted(int(k) for k in refs.keys()) # ejemplo: [0, 4] - p, q = claves_ref # p = 0, q = 4 + cards_between_levels = request.blank_cards + reference_values = request.references + + # Índices de referencia (por ejemplo 0 y 4) + ref_indices = sorted(int(k) for k in reference_values) + lower_ref, upper_ref = ref_indices + + # Valores asignados a esas referencias + lower_value = reference_values[str(lower_ref)] + upper_value = reference_values[str(upper_ref)] + + # Total de unidades entre las referencias + total_units = sum(cards_between_levels[i] + 1 for i in range(lower_ref, upper_ref)) + + # Valor por unidad + unit_value = (upper_value - lower_value) / total_units if total_units else 0 + + # Lista de valores finales + values = [0] * len(levels) + values[lower_ref] = lower_value + + # Hacia arriba + for i in range(lower_ref + 1, len(levels)): + units = sum(cards_between_levels[r] + 1 for r in range(lower_ref, i)) + values[i] = lower_value + unit_value * units + + # Hacia abajo + for i in range(lower_ref - 1, -1, -1): + units = sum(cards_between_levels[r] + 1 for r in range(i, lower_ref)) + values[i] = lower_value - unit_value * units - # Valores de referencia - up = refs[str(p)] - uq = refs[str(q)] - - # PASO A: Calculamos el número total de unidades elementales 'h' entre p y q - h = sum(blank_cards[r] + 1 for r in range(p, q)) - - # PASO B: Calculamos el valor fraccional de una sola unidad 'alpha' - alpha = (uq - up) / h if h != 0 else 0 - - # Inicializamos la lista de valores V con el tamaño de los niveles - V = [0.0] * len(levels) - V[p] = up - - # PASO C: Calculamos los valores hacia adelante (niveles por encima de p) - for i in range(p + 1, len(levels)): - suma_unidades = sum(blank_cards[r] + 1 for r in range(p, i)) - V[i] = up + alpha * suma_unidades - - # PASO D: Calculamos los valores hacia atrás (niveles por debajo de p) - for i in range(p - 1, -1, -1): - suma_unidades = sum(blank_cards[r] + 1 for r in range(i, p)) - V[i] = up - alpha * suma_unidades - - # Formateamos la respuesta - resultado_valores = {levels[i]: round(V[i], 4) for i in range(len(levels))} - return { "criterion_name": request.criterion_name, - "values": resultado_valores + "values": {levels[i]: round(values[i], 4) for i in range(len(levels))} } From d6832d08ef53fd1b07adc0d82266cf64672ede56 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 23 Mar 2026 12:27:17 +0100 Subject: [PATCH 10/80] =?UTF-8?q?Dise=C3=B1o=20vertical=20de=20las=20carta?= =?UTF-8?q?s=20y=20estado=20de=20React?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 368 +++++++++++++++++++++++++++++++------ frontend/package.json | 4 +- frontend/src/App.css | 184 ------------------- frontend/src/App.jsx | 248 ++++++++++++++----------- frontend/src/index.css | 112 +---------- frontend/vite.config.js | 6 +- 6 files changed, 461 insertions(+), 461 deletions(-) delete mode 100644 frontend/src/App.css diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 663adb7..16c9cf9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,10 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@tailwindcss/vite": "^4.2.2", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "tailwindcss": "^4.2.2" }, "devDependencies": { "@eslint/js": "^9.39.4", @@ -267,7 +269,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -279,7 +280,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -290,7 +290,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -510,7 +509,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -521,7 +519,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -532,7 +529,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -542,14 +538,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -560,7 +554,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -577,7 +570,6 @@ "version": "0.120.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -590,7 +582,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -607,7 +598,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -624,7 +614,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -641,7 +630,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -658,7 +646,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -675,7 +662,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -692,7 +678,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -709,7 +694,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -726,7 +710,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -743,7 +726,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -760,7 +742,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -777,7 +758,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -794,7 +774,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -811,7 +790,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -828,7 +806,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -845,11 +822,267 @@ "dev": true, "license": "MIT" }, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1177,7 +1410,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1190,6 +1422,19 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1422,7 +1667,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1491,7 +1735,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1538,6 +1781,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1632,6 +1881,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1727,7 +1985,6 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -1760,7 +2017,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1781,7 +2037,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1802,7 +2057,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1823,7 +2077,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1844,7 +2097,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1865,7 +2117,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1886,7 +2137,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1907,7 +2157,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1928,7 +2177,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1949,7 +2197,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1970,7 +2217,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2017,6 +2263,15 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -2041,7 +2296,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2157,14 +2411,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2177,7 +2429,6 @@ "version": "8.5.8", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2257,7 +2508,6 @@ "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.120.0", @@ -2291,7 +2541,6 @@ "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", - "dev": true, "license": "MIT" }, "node_modules/scheduler": { @@ -2337,7 +2586,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2369,11 +2617,29 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2390,7 +2656,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD", "optional": true }, @@ -2452,7 +2717,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", - "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", diff --git a/frontend/package.json b/frontend/package.json index f13a5b5..0cb6cdb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.2.2", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "tailwindcss": "^4.2.2" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b2bf2e8..e1c8458 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,121 +1,145 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import heroImg from './assets/hero.png' -import './App.css' +import { useState } from 'react'; function App() { - const [count, setCount] = useState(0) + const [criterionName, setCriterionName] = useState(''); + const [levels, setLevels] = useState(['', '', '']); + const [blankCards, setBlankCards] = useState([0, 0]); + + const handleLevelChange = (index, newValue) => { + const newLevels = [...levels]; + newLevels[index] = newValue; + setLevels(newLevels); + }; + + const handleAddLevel = () => { + setLevels([...levels, '']); + setBlankCards([...blankCards, 0]); + }; + + const handleRemoveLevel = (indexToRemove) => { + if (levels.length <= 2) return; + const newLevels = levels.filter((_, index) => index !== indexToRemove); + const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; + const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); + setLevels(newLevels); + setBlankCards(newBlankCards); + }; + + const handleBlankCardChange = (index, delta) => { + const newBlankCards = [...blankCards]; + const newValue = newBlankCards[index] + delta; + if (newValue >= 0) { + newBlankCards[index] = newValue; + setBlankCards(newBlankCards); + } + }; return ( - <> -
-
- - React logo - Vite logo -
-
-

Get started

-

- Edit src/App.jsx and save to test HMR -

-
- -
+
+ + {/* TÍTULO */} +
+ + setCriterionName(e.target.value)} + className="w-full text-3xl font-bold p-2 text-center text-slate-700 border-b-2 border-transparent hover:border-slate-200 focus:border-blue-500 outline-none transition-colors" + /> +
-
+ {/* TIMELINE VERTICAL CON CARTAS REALES */} +
+ {levels.map((level, index) => ( +
+ + {/* 1. LA CARTA DE NIVEL (Recuperando el diseño de naipe) */} +
+ + {/* Botón Eliminar */} + {levels.length > 2 && ( + + )} -
-
- -

Documentation

-

Your questions, answered

- -
-
- -

Connect with us

-

Join the Vite community

- -
-
+ {/* Detalles tipo naipe (Esquinas) */} + + {index + 1} + + + {index + 1} + -
-
- - ) + {/* Input centrado */} + handleLevelChange(index, e.target.value)} + className="w-4/5 text-center text-2xl font-bold text-slate-700 bg-transparent border-b-2 border-dashed border-slate-300 focus:border-blue-500 outline-none pb-1" + /> +
+ + {/* 2. EL CONECTOR Y CARTAS BLANCAS */} + {index < levels.length - 1 && ( +
+
+ +
+ Blancas: + + + {blankCards[index]} + + +
+ + {blankCards[index] > 0 && ( +
+ {Array.from({ length: blankCards[index] }).map((_, i) => ( +
+ ))} +
+ )} + +
+
+ )} +
+ ))} + + {/* 3. BOTÓN AÑADIR */} +
+ +
+ +
+
+ ); } -export default App +export default App; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 2c84af0..a461c50 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,111 +1 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; - - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } -} - -body { - margin: 0; -} - -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; - box-sizing: border-box; -} - -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); -} - -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } -} -p { - margin: 0; -} - -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); -} - -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); -} +@import "tailwindcss"; \ No newline at end of file diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 8b0f57b..3d15f68 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,7 +1,11 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + tailwindcss(), + ], }) From 8fe5469149c6808918600f651587eb58326e0562 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Mon, 23 Mar 2026 12:31:20 +0100 Subject: [PATCH 11/80] =?UTF-8?q?Optimizaci=C3=B3n=20c=C3=B3digo=20del=20P?= =?UTF-8?q?OST?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7014423..28e2aa5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # deck-of-cards -Prueba de feature en una rama hija de develop para hacer un pull request. V2 \ No newline at end of file +Prueba de feature en una rama hija de develop para hacer un pull request. \ No newline at end of file From 493afc99a992ceaa63a4e6d8f367a4fde80c4649 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Mon, 23 Mar 2026 14:00:24 +0100 Subject: [PATCH 12/80] Configuracion CORS y Peticiones Complejas --- backend/api/routes.py | 197 +++++++++++++++++++++++++++++++++++------- 1 file changed, 168 insertions(+), 29 deletions(-) diff --git a/backend/api/routes.py b/backend/api/routes.py index e84950b..2a50ae9 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -1,54 +1,193 @@ from fastapi import FastAPI from pydantic import BaseModel -from typing import List, Dict +from typing import List, Dict, Tuple +from fastapi.middleware.cors import CORSMiddleware app = FastAPI() +# Configuración CORS para permitir peticiones desde React +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Permite cualquier origen (perfecto para desarrollo local) + allow_credentials=True, + allow_methods=["*"], # Permite POST, GET, OPTIONS, etc. + allow_headers=["*"], # Permite cualquier cabecera +) + +# ----------------------------- +# MODELOS +# ----------------------------- + class ValueFunctionRequest(BaseModel): criterion_name: str levels: List[str] blank_cards: List[int] references: Dict[str, float] -@app.get("/prueba") -def prueba(): - return {"mensaje": "¡Hola desde FastAPI! El contenedor de docker está funcionando"} +class DoCMFRequest(BaseModel): + term: str + core: Tuple[float, float] # [a, b] + support: Tuple[float, float] # [c, d] + left_nodes_x: List[float] + left_blank_cards: List[int] + right_nodes_x: List[float] + right_blank_cards: List[int] + +class EvaluationRequest(BaseModel): + x: float + left_nodes: List[Tuple[float, float]] + right_nodes: List[Tuple[float, float]] + core: Tuple[float, float] + support: Tuple[float, float] + + +# ----------------------------- +# ENDPOINT 1: FUNCIÓN DE VALOR DoC +# ----------------------------- @app.post("/api/criteria/doc/value-function") -def calcular(request: ValueFunctionRequest): +def calcular_value_function(request: ValueFunctionRequest): levels = request.levels - cards_between_levels = request.blank_cards - reference_values = request.references + cards = request.blank_cards + refs = request.references - # Índices de referencia (por ejemplo 0 y 4) - ref_indices = sorted(int(k) for k in reference_values) - lower_ref, upper_ref = ref_indices + ref_indices = sorted(int(k) for k in refs) + p, q = ref_indices + up, uq = refs[str(p)], refs[str(q)] - # Valores asignados a esas referencias - lower_value = reference_values[str(lower_ref)] - upper_value = reference_values[str(upper_ref)] + total_units = sum(cards[i] + 1 for i in range(p, q)) + alpha = (uq - up) / total_units if total_units else 0 - # Total de unidades entre las referencias - total_units = sum(cards_between_levels[i] + 1 for i in range(lower_ref, upper_ref)) - - # Valor por unidad - unit_value = (upper_value - lower_value) / total_units if total_units else 0 - - # Lista de valores finales values = [0] * len(levels) - values[lower_ref] = lower_value + values[p] = up - # Hacia arriba - for i in range(lower_ref + 1, len(levels)): - units = sum(cards_between_levels[r] + 1 for r in range(lower_ref, i)) - values[i] = lower_value + unit_value * units + for i in range(p + 1, len(levels)): + units = sum(cards[r] + 1 for r in range(p, i)) + values[i] = up + alpha * units - # Hacia abajo - for i in range(lower_ref - 1, -1, -1): - units = sum(cards_between_levels[r] + 1 for r in range(i, lower_ref)) - values[i] = lower_value - unit_value * units + for i in range(p - 1, -1, -1): + units = sum(cards[r] + 1 for r in range(i, p)) + values[i] = up - alpha * units return { "criterion_name": request.criterion_name, "values": {levels[i]: round(values[i], 4) for i in range(len(levels))} } + + +# ----------------------------- +# ENDPOINT 2: CONSTRUIR DoC-MF +# ----------------------------- + +@app.post("/api/criteria/doc-mf/build") +def build_doc_mf(request: DoCMFRequest): + + a, b = request.core + c, d = request.support + + # ---- LADO IZQUIERDO ---- + left_x = request.left_nodes_x + left_e = request.left_blank_cards + + TL = sum(e + 1 for e in left_e) + YL = 1 / TL if TL else 0 + + left_nodes = [] + acc = 0 + for i in range(len(left_x)): + if i == 0: + left_nodes.append((left_x[i], 0.0)) + else: + acc += (left_e[i-1] + 1) + left_nodes.append((left_x[i], round(acc * YL, 4))) + + # ---- LADO DERECHO ---- + right_x = request.right_nodes_x + right_e = request.right_blank_cards + + TR = sum(e + 1 for e in right_e) + YR = 1 / TR if TR else 0 + + right_nodes = [] + acc = 0 + for i in range(len(right_x)): + if i == 0: + right_nodes.append((right_x[i], 1.0)) + else: + acc += (right_e[i-1] + 1) + right_nodes.append((right_x[i], round(1 - acc * YR, 4))) + + return { + "term": request.term, + "core": request.core, + "support": request.support, + "left_nodes": left_nodes, + "right_nodes": right_nodes + } + + +# ----------------------------- +# ENDPOINT 3: EVALUAR UN VALOR x +# ----------------------------- + +def linear_interpolation(x, nodes): + for i in range(len(nodes) - 1): + x0, y0 = nodes[i] + x1, y1 = nodes[i+1] + if x0 <= x <= x1: + t = (x - x0) / (x1 - x0) + return y0 + t * (y1 - y0) + return 0.0 + +@app.post("/api/criteria/doc-mf/evaluate") +def evaluate_doc_mf(request: EvaluationRequest): + x = request.x + a, b = request.core + c, d = request.support + + if x < c or x > d: + return {"membership": 0.0} + + if a <= x <= b: + return {"membership": 1.0} + + if c <= x < a: + return {"membership": linear_interpolation(x, request.left_nodes)} + + if b < x <= d: + return {"membership": linear_interpolation(x, request.right_nodes)} + + return {"membership": 0.0} + + +# ----------------------------- +# ENDPOINT 4: PUNTOS (x,y) DE LA FUNCIÓN DE VALOR +# ----------------------------- + +@app.post("/api/criteria/doc/value-function/points") +def value_function_points(request: ValueFunctionRequest): + levels = request.levels + cards = request.blank_cards + refs = request.references + + ref_indices = sorted(int(k) for k in refs) + p, q = ref_indices + up, uq = refs[str(p)], refs[str(q)] + + total_units = sum(cards[i] + 1 for i in range(p, q)) + alpha = (uq - up) / total_units if total_units else 0 + + values = [0] * len(levels) + values[p] = up + + for i in range(p + 1, len(levels)): + units = sum(cards[r] + 1 for r in range(p, i)) + values[i] = up + alpha * units + + for i in range(p - 1, -1, -1): + units = sum(cards[r] + 1 for r in range(i, p)) + values[i] = up - alpha * units + + points = [{"x": i, "y": round(values[i], 4)} for i in range(len(levels))] + + return {"points": points} From 45d0585c5d5945c041f04337c92cc49a93e0a081 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 23 Mar 2026 14:04:55 +0100 Subject: [PATCH 13/80] add: implementada peticion post hacia el endpoint del backend --- frontend/src/App.jsx | 69 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e1c8458..e34c01b 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,10 +1,55 @@ import { useState } from 'react'; function App() { + const [criterionName, setCriterionName] = useState(''); const [levels, setLevels] = useState(['', '', '']); const [blankCards, setBlankCards] = useState([0, 0]); + const [isLoading, setIsLoading] = useState(false); + const [result, setResult] = useState(null); + + const handleCalculate = async () => { + setIsLoading(true); + setResult(null); + + const currentReferences = [levels[0], levels[levels.length - 1]]; + + const payload = { + name: criterionName || "Criterio sin nombre", + labels: levels, + blank_cards: blankCards, + references: currentReferences + }; + + console.log("Enviando JSON al backend:", payload); + + try { + const response = await fetch('http://localhost:8000/api/criteria/doc/value-function', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new Error(`Error del servidor: ${response.status}`); + } + + const data = await response.json(); + console.log("Respuesta del backend:", data); + setResult(data); + + } catch (error) { + console.error("Error al hacer la petición:", error); + alert("No se ha podido conectar con el backend."); + } finally { + setIsLoading(false); + } + }; + + const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; @@ -138,6 +183,30 @@ function App() { + + {/* --- NUEVO: BOTÓN DE CALCULAR --- */} +
+ +
+ + {/* --- NUEVO: CAJA PARA VER EL RESULTADO (TEMPORAL PARA DEBUG) --- */} + {result && ( +
+

Respuesta del Backend:

+
+              {JSON.stringify(result, null, 2)}
+            
+
+ )} + ); } From fec5773928b75bb28ffa3e79aa3c97106b986344 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 23 Mar 2026 14:26:52 +0100 Subject: [PATCH 14/80] =?UTF-8?q?fix:=20Ajustes=20de=20payload=20para=20qu?= =?UTF-8?q?e=20encaje=20con=20lo=20que=20espera=20recibir=20la=20funci?= =?UTF-8?q?=C3=B3n=20en=20el=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.jsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e34c01b..a0f6f3d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,12 +12,15 @@ function App() { const handleCalculate = async () => { setIsLoading(true); setResult(null); - - const currentReferences = [levels[0], levels[levels.length - 1]]; + + const currentReferences = { + [levels[0]]: 0, + [levels[levels.length - 1]]: 1 + }; const payload = { - name: criterionName || "Criterio sin nombre", - labels: levels, + criterion_name: criterionName || "Criterio sin nombre", + levels: levels, blank_cards: blankCards, references: currentReferences }; From 08666eebee15a8ef51507254f9d2f58fe8b9fb57 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 24 Mar 2026 09:07:21 +0100 Subject: [PATCH 15/80] Respuesta correcta de la api --- frontend/src/App.jsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a0f6f3d..284ac4a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,10 +12,13 @@ function App() { const handleCalculate = async () => { setIsLoading(true); setResult(null); + + const firstIndex = "0"; + const lastIndex = (levels.length - 1).toString(); const currentReferences = { - [levels[0]]: 0, - [levels[levels.length - 1]]: 1 + [firstIndex]: 0, + [lastIndex]: 1 }; const payload = { @@ -200,16 +203,6 @@ function App() { - {/* --- NUEVO: CAJA PARA VER EL RESULTADO (TEMPORAL PARA DEBUG) --- */} - {result && ( -
-

Respuesta del Backend:

-
-              {JSON.stringify(result, null, 2)}
-            
-
- )} - ); } From 351745d50fb05621a53e80a789c8d5a1bdd12748 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 24 Mar 2026 09:29:27 +0100 Subject: [PATCH 16/80] =?UTF-8?q?add:=20instalar=20recharts=20y=20a=C3=B1a?= =?UTF-8?q?dir=20la=20gr=C3=A1fica=20con=20los=20datos=20recibidos=20del?= =?UTF-8?q?=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 411 ++++++++++++++++++++++++++++++++++++- frontend/package.json | 1 + frontend/src/App.jsx | 65 +++++- 3 files changed, 468 insertions(+), 9 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 16c9cf9..86a85d0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,6 +11,7 @@ "@tailwindcss/vite": "^4.2.2", "react": "^19.2.4", "react-dom": "^19.2.4", + "recharts": "^3.8.0", "tailwindcss": "^4.2.2" }, "devDependencies": { @@ -575,6 +576,42 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", @@ -822,6 +859,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", @@ -1089,6 +1138,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1107,7 +1219,7 @@ "version": "19.2.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1123,6 +1235,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -1325,6 +1443,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1378,9 +1505,130 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1399,6 +1647,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1435,6 +1689,16 @@ "node": ">=10.13.0" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1642,6 +1906,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1824,6 +2094,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -1851,6 +2131,15 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2494,6 +2783,87 @@ "react": "^19.2.4" } }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", + "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2636,6 +3006,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -2713,6 +3089,37 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0cb6cdb..32ea15a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "@tailwindcss/vite": "^4.2.2", "react": "^19.2.4", "react-dom": "^19.2.4", + "recharts": "^3.8.0", "tailwindcss": "^4.2.2" }, "devDependencies": { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 284ac4a..58eacb0 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; function App() { @@ -102,12 +103,12 @@ function App() { /> - {/* TIMELINE VERTICAL CON CARTAS REALES */} + {/* TIMELINE VERTICAL */}
{levels.map((level, index) => (
- {/* 1. LA CARTA DE NIVEL (Recuperando el diseño de naipe) */} + {/* CARTA DE NIVEL (etiqueta) */}
{/* Botón Eliminar */} @@ -121,7 +122,7 @@ function App() { )} - {/* Detalles tipo naipe (Esquinas) */} + {/* Detalles tipo naipe */} {index + 1} @@ -129,7 +130,6 @@ function App() { {index + 1} - {/* Input centrado */}
- {/* 2. EL CONECTOR Y CARTAS BLANCAS */} + {/* CONECTOR Y CARTAS BLANCAS */} {index < levels.length - 1 && (
@@ -177,7 +177,7 @@ function App() {
))} - {/* 3. BOTÓN AÑADIR */} + {/* BOTÓN AÑADIR */}
+ {/* GRÁFICA */} + {result && ( +
+

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

+ +
+ + ({ + nombre: label, + valor: value + }))} + margin={{ top: 20, right: 30, left: 20, bottom: 20 }} + > + + + + + + + [value.toFixed(4), 'Valor DoC']} + labelStyle={{ fontWeight: 'bold', color: '#1e293b', marginBottom: '4px' }} + /> + + + + +
+
+ )} +
); } From 532fa9d5d2443bdd7ed47f6a63582d5f6c51402c Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 24 Mar 2026 09:57:51 +0100 Subject: [PATCH 17/80] =?UTF-8?q?refactor:=20creada=20organizaci=C3=B3n=20?= =?UTF-8?q?de=20carpetas,=20instalado=20axios,=20implementado=20archivo=20?= =?UTF-8?q?.env=20con=20variables=20de=20entorno,=20implementado=20nuevo?= =?UTF-8?q?=20m=C3=A9todo=20calculateValueFunction=20en=20App.jsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 280 ++++++++++++++++++ frontend/package.json | 1 + frontend/src/App.jsx | 22 +- frontend/src/assets/hero.png | Bin 44919 -> 0 bytes frontend/src/assets/react.svg | 1 - frontend/src/assets/vite.svg | 1 - frontend/src/components/BlankCardsCounter.jsx | 0 frontend/src/components/CardEditor.jsx | 0 .../src/components/ValueFunctionChart.jsx | 0 frontend/src/config.js | 1 + frontend/src/lib/axios.js | 13 + frontend/src/pages/AdvancedMode.jsx | 0 frontend/src/pages/BasicMode.jsx | 0 frontend/src/services/docService.js | 11 + 14 files changed, 309 insertions(+), 21 deletions(-) delete mode 100644 frontend/src/assets/hero.png delete mode 100644 frontend/src/assets/react.svg delete mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/components/BlankCardsCounter.jsx create mode 100644 frontend/src/components/CardEditor.jsx create mode 100644 frontend/src/components/ValueFunctionChart.jsx create mode 100644 frontend/src/config.js create mode 100644 frontend/src/lib/axios.js create mode 100644 frontend/src/pages/AdvancedMode.jsx create mode 100644 frontend/src/pages/BasicMode.jsx create mode 100644 frontend/src/services/docService.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 86a85d0..0f2633d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.2.2", + "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", "recharts": "^3.8.0", @@ -1330,6 +1331,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1395,6 +1413,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1472,6 +1503,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1660,6 +1703,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1669,6 +1721,20 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.321", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", @@ -1689,6 +1755,51 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-toolkit": { "version": "1.45.1", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", @@ -2001,6 +2112,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2015,6 +2162,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2025,6 +2181,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2051,6 +2244,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2067,6 +2272,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -2561,6 +2805,36 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -2752,6 +3026,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 32ea15a..278c666 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.2.2", + "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", "recharts": "^3.8.0", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 58eacb0..292034c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; +import { calculateValueFunction } from './services/docService'; function App() { @@ -29,28 +30,11 @@ function App() { references: currentReferences }; - console.log("Enviando JSON al backend:", payload); - try { - const response = await fetch('http://localhost:8000/api/criteria/doc/value-function', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(payload), - }); - - if (!response.ok) { - throw new Error(`Error del servidor: ${response.status}`); - } - - const data = await response.json(); - console.log("Respuesta del backend:", data); + const data = await calculateValueFunction(payload); setResult(data); - } catch (error) { - console.error("Error al hacer la petición:", error); - alert("No se ha podido conectar con el backend."); + alert("No se ha podido conectar con el backend: " + error); } finally { setIsLoading(false); } diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index cc51a3d20ad4bc961b596a6adfd686685cd84bb0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44919 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 JPDHLkV1myZcL)Fg diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/components/BlankCardsCounter.jsx b/frontend/src/components/BlankCardsCounter.jsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/ValueFunctionChart.jsx b/frontend/src/components/ValueFunctionChart.jsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/config.js b/frontend/src/config.js new file mode 100644 index 0000000..837c422 --- /dev/null +++ b/frontend/src/config.js @@ -0,0 +1 @@ +export const API_BASE_URL = import.meta.env.VITE_API_URL; \ No newline at end of file diff --git a/frontend/src/lib/axios.js b/frontend/src/lib/axios.js new file mode 100644 index 0000000..5aa26f5 --- /dev/null +++ b/frontend/src/lib/axios.js @@ -0,0 +1,13 @@ +import Axios from 'axios'; +import { API_BASE_URL } from '../config'; + +const axios = Axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } +}); + + +export default axios; \ No newline at end of file diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/pages/BasicMode.jsx b/frontend/src/pages/BasicMode.jsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/services/docService.js b/frontend/src/services/docService.js new file mode 100644 index 0000000..79150d3 --- /dev/null +++ b/frontend/src/services/docService.js @@ -0,0 +1,11 @@ +import axios from '../lib/axios'; + +export const calculateValueFunction = async (payload) => { + try { + const response = await axios.post('/criteria/doc/value-function', payload); + return response.data; + } catch (error) { + console.error("Error en calculateValueFunction:", error); + throw error; + } +}; \ No newline at end of file From 9a8ddf53285ca2f1542a3a0331ceff313946f243 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Tue, 24 Mar 2026 10:55:06 +0100 Subject: [PATCH 18/80] =?UTF-8?q?A=C3=B1adida=20explicaci=C3=B3n=20de=20pe?= =?UTF-8?q?rtenencia=20en=20enpoint=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/routes.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/backend/api/routes.py b/backend/api/routes.py index 2a50ae9..10544bc 100644 --- a/backend/api/routes.py +++ b/backend/api/routes.py @@ -146,18 +146,35 @@ def evaluate_doc_mf(request: EvaluationRequest): c, d = request.support if x < c or x > d: - return {"membership": 0.0} + return { + "membership": 0.0, + "explanation": "El valor está fuera del soporte, por eso la pertenencia es 0." + } if a <= x <= b: - return {"membership": 1.0} + return { + "membership": 1.0, + "explanation": "El valor está dentro del núcleo, por eso la pertenencia es 1." + } if c <= x < a: - return {"membership": linear_interpolation(x, request.left_nodes)} + mu = linear_interpolation(x, request.left_nodes) + return { + "membership": mu, + "explanation": "El valor está en el lado izquierdo y se interpola entre nodos." + } if b < x <= d: - return {"membership": linear_interpolation(x, request.right_nodes)} + mu = linear_interpolation(x, request.right_nodes) + return { + "membership": mu, + "explanation": "El valor está en el lado derecho y se interpola entre nodos." + } - return {"membership": 0.0} + return { + "membership": 0.0, + "explanation": "No se pudo determinar la pertenencia." + } # ----------------------------- From 99a1067a3cf4b5cad41196f093ecc9be5e7f6f59 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 24 Mar 2026 11:04:01 +0100 Subject: [PATCH 19/80] =?UTF-8?q?refactor:=20componentizar=20l=C3=B3gica?= =?UTF-8?q?=20de=20App.jsx=20manteniendo=20funcionalidad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/index.html | 2 +- frontend/src/App.jsx | 238 +----------------- frontend/src/components/AddLevelButton.jsx | 13 + frontend/src/components/BlankCardsCounter.jsx | 36 +++ frontend/src/components/CardEditor.jsx | 33 +++ frontend/src/components/CriterionInput.jsx | 16 ++ .../src/components/ValueFunctionChart.jsx | 43 ++++ frontend/src/pages/BasicMode.jsx | 124 +++++++++ 8 files changed, 268 insertions(+), 237 deletions(-) create mode 100644 frontend/src/components/AddLevelButton.jsx create mode 100644 frontend/src/components/CriterionInput.jsx diff --git a/frontend/index.html b/frontend/index.html index f94d687..609e6e7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - frontend + Deck of Cards
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 292034c..0545d76 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,243 +1,9 @@ -import { useState } from 'react'; -import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; -import { calculateValueFunction } from './services/docService'; +import BasicMode from './pages/BasicMode'; function App() { - - const [criterionName, setCriterionName] = useState(''); - const [levels, setLevels] = useState(['', '', '']); - const [blankCards, setBlankCards] = useState([0, 0]); - - const [isLoading, setIsLoading] = useState(false); - const [result, setResult] = useState(null); - - const handleCalculate = async () => { - setIsLoading(true); - setResult(null); - - const firstIndex = "0"; - const lastIndex = (levels.length - 1).toString(); - - const currentReferences = { - [firstIndex]: 0, - [lastIndex]: 1 - }; - - const payload = { - criterion_name: criterionName || "Criterio sin nombre", - levels: levels, - blank_cards: blankCards, - references: currentReferences - }; - - try { - const data = await calculateValueFunction(payload); - setResult(data); - } catch (error) { - alert("No se ha podido conectar con el backend: " + error); - } finally { - setIsLoading(false); - } - }; - - - const handleLevelChange = (index, newValue) => { - const newLevels = [...levels]; - newLevels[index] = newValue; - setLevels(newLevels); - }; - - const handleAddLevel = () => { - setLevels([...levels, '']); - setBlankCards([...blankCards, 0]); - }; - - const handleRemoveLevel = (indexToRemove) => { - if (levels.length <= 2) return; - const newLevels = levels.filter((_, index) => index !== indexToRemove); - const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; - const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); - setLevels(newLevels); - setBlankCards(newBlankCards); - }; - - const handleBlankCardChange = (index, delta) => { - const newBlankCards = [...blankCards]; - const newValue = newBlankCards[index] + delta; - if (newValue >= 0) { - newBlankCards[index] = newValue; - setBlankCards(newBlankCards); - } - }; - return (
- - {/* TÍTULO */} -
- - setCriterionName(e.target.value)} - className="w-full text-3xl font-bold p-2 text-center text-slate-700 border-b-2 border-transparent hover:border-slate-200 focus:border-blue-500 outline-none transition-colors" - /> -
- - {/* TIMELINE VERTICAL */} -
- {levels.map((level, index) => ( -
- - {/* CARTA DE NIVEL (etiqueta) */} -
- - {/* Botón Eliminar */} - {levels.length > 2 && ( - - )} - - {/* Detalles tipo naipe */} - - {index + 1} - - - {index + 1} - - - handleLevelChange(index, e.target.value)} - className="w-4/5 text-center text-2xl font-bold text-slate-700 bg-transparent border-b-2 border-dashed border-slate-300 focus:border-blue-500 outline-none pb-1" - /> -
- - {/* CONECTOR Y CARTAS BLANCAS */} - {index < levels.length - 1 && ( -
-
- -
- Blancas: - - - {blankCards[index]} - - -
- - {blankCards[index] > 0 && ( -
- {Array.from({ length: blankCards[index] }).map((_, i) => ( -
- ))} -
- )} - -
-
- )} -
- ))} - - {/* BOTÓN AÑADIR */} -
- -
- -
- - {/* BOTÓN DE CALCULAR */} -
- -
- - {/* GRÁFICA */} - {result && ( -
-

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

- -
- - ({ - nombre: label, - valor: value - }))} - margin={{ top: 20, right: 30, left: 20, bottom: 20 }} - > - - - - - - - [value.toFixed(4), 'Valor DoC']} - labelStyle={{ fontWeight: 'bold', color: '#1e293b', marginBottom: '4px' }} - /> - - - - -
-
- )} - +
); } diff --git a/frontend/src/components/AddLevelButton.jsx b/frontend/src/components/AddLevelButton.jsx new file mode 100644 index 0000000..1d39d4e --- /dev/null +++ b/frontend/src/components/AddLevelButton.jsx @@ -0,0 +1,13 @@ +export default function AddLevelButton({ handleAddLevel }) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/BlankCardsCounter.jsx b/frontend/src/components/BlankCardsCounter.jsx index e69de29..2cabe8d 100644 --- a/frontend/src/components/BlankCardsCounter.jsx +++ b/frontend/src/components/BlankCardsCounter.jsx @@ -0,0 +1,36 @@ +export default function BlankCardsCounter({ index, blankCardsCount, handleBlankCardChange }) { + return ( +
+
+ +
+ Blancas: + + + {blankCardsCount} + + +
+ + {blankCardsCount > 0 && ( +
+ {Array.from({ length: blankCardsCount }).map((_, i) => ( +
+ ))} +
+ )} + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx index e69de29..77c0c73 100644 --- a/frontend/src/components/CardEditor.jsx +++ b/frontend/src/components/CardEditor.jsx @@ -0,0 +1,33 @@ +export default function CardEditor({ index, level, handleLevelChange, handleRemoveLevel, totalLevels }) { + return ( +
+ + {/* Botón Eliminar */} + {totalLevels > 2 && ( + + )} + + {/* Detalles tipo naipe */} + + {index + 1} + + + {index + 1} + + + handleLevelChange(index, e.target.value)} + className="w-4/5 text-center text-2xl font-bold text-slate-700 bg-transparent border-b-2 border-dashed border-slate-300 focus:border-blue-500 outline-none pb-1" + /> +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/CriterionInput.jsx b/frontend/src/components/CriterionInput.jsx new file mode 100644 index 0000000..300c2de --- /dev/null +++ b/frontend/src/components/CriterionInput.jsx @@ -0,0 +1,16 @@ +export default function CriterionInput({ criterionName, setCriterionName }) { + return ( +
+ + setCriterionName(e.target.value)} + className="w-full text-3xl font-bold p-2 text-center text-slate-700 border-b-2 border-transparent hover:border-slate-200 focus:border-blue-500 outline-none transition-colors" + /> +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/ValueFunctionChart.jsx b/frontend/src/components/ValueFunctionChart.jsx index e69de29..e496d46 100644 --- a/frontend/src/components/ValueFunctionChart.jsx +++ b/frontend/src/components/ValueFunctionChart.jsx @@ -0,0 +1,43 @@ +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; + +export default function ValueFunctionChart({ result }) { + if (!result) return null; + + return ( +
+

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

+ +
+ + ({ + nombre: label, + valor: value + }))} + margin={{ top: 20, right: 30, left: 20, bottom: 20 }} + > + + + + [value.toFixed(4), 'Valor DoC']} + labelStyle={{ fontWeight: 'bold', color: '#1e293b', marginBottom: '4px' }} + /> + + + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/BasicMode.jsx b/frontend/src/pages/BasicMode.jsx index e69de29..d5c7976 100644 --- a/frontend/src/pages/BasicMode.jsx +++ b/frontend/src/pages/BasicMode.jsx @@ -0,0 +1,124 @@ +import { useState } from 'react'; +import CriterionInput from '../components/CriterionInput'; +import CardEditor from '../components/CardEditor'; +import BlankCardsCounter from '../components/BlankCardsCounter'; +import AddLevelButton from '../components/AddLevelButton'; +import ValueFunctionChart from '../components/ValueFunctionChart'; +import { calculateValueFunction } from '../services/docService'; + +export default function BasicMode() { + const [criterionName, setCriterionName] = useState(''); + const [levels, setLevels] = useState(['', '', '']); + const [blankCards, setBlankCards] = useState([0, 0]); + + const [isLoading, setIsLoading] = useState(false); + const [result, setResult] = useState(null); + + const handleCalculate = async () => { + setIsLoading(true); + setResult(null); + + const firstIndex = "0"; + const lastIndex = (levels.length - 1).toString(); + + const currentReferences = { + [firstIndex]: 0, + [lastIndex]: 1 + }; + + const payload = { + criterion_name: criterionName || "Criterio sin nombre", + levels: levels, + blank_cards: blankCards, + references: currentReferences + }; + + try { + const data = await calculateValueFunction(payload); + setResult(data); + } catch (error) { + alert("No se ha podido conectar con el backend: " + error); + } finally { + setIsLoading(false); + } + }; + + const handleLevelChange = (index, newValue) => { + const newLevels = [...levels]; + newLevels[index] = newValue; + setLevels(newLevels); + }; + + const handleAddLevel = () => { + setLevels([...levels, '']); + setBlankCards([...blankCards, 0]); + }; + + const handleRemoveLevel = (indexToRemove) => { + if (levels.length <= 2) return; + const newLevels = levels.filter((_, index) => index !== indexToRemove); + const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; + const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); + setLevels(newLevels); + setBlankCards(newBlankCards); + }; + + const handleBlankCardChange = (index, delta) => { + const newBlankCards = [...blankCards]; + const newValue = newBlankCards[index] + delta; + if (newValue >= 0) { + newBlankCards[index] = newValue; + setBlankCards(newBlankCards); + } + }; + + return ( +
+ + + +
+ {levels.map((level, index) => ( +
+ + + + {index < levels.length - 1 && ( + + )} +
+ ))} + + +
+ +
+ +
+ + + +
+); +} \ No newline at end of file From 9a3c40e30e993c84e1682ca5360096432d8c45f9 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 24 Mar 2026 13:19:37 +0100 Subject: [PATCH 20/80] =?UTF-8?q?add:=20a=C3=B1adir=20enrutado=20para=20se?= =?UTF-8?q?parar=20la=20secci=C3=B3n=20"basico"=20y=20"avanzado"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 58 ++++++++++++++++ frontend/package.json | 1 + frontend/src/App.jsx | 6 +- .../src/components/ValueFunctionChart.jsx | 66 +++++++++---------- frontend/src/components/layout/MainLayout.jsx | 49 ++++++++++++++ frontend/src/pages/AdvancedMode.jsx | 7 ++ frontend/src/routers/AppRouter.jsx | 21 ++++++ 7 files changed, 171 insertions(+), 37 deletions(-) create mode 100644 frontend/src/components/layout/MainLayout.jsx create mode 100644 frontend/src/routers/AppRouter.jsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0f2633d..c0cf46f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-router-dom": "^7.13.2", "recharts": "^3.8.0", "tailwindcss": "^4.2.2" }, @@ -1529,6 +1530,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3093,6 +3107,44 @@ } } }, + "node_modules/react-router": { + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", + "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/recharts": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", @@ -3209,6 +3261,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 278c666..debc10c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-router-dom": "^7.13.2", "recharts": "^3.8.0", "tailwindcss": "^4.2.2" }, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 0545d76..df64ca3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,10 +1,8 @@ -import BasicMode from './pages/BasicMode'; +import { AppRouter } from './routers/AppRouter'; function App() { return ( -
- -
+ ); } diff --git a/frontend/src/components/ValueFunctionChart.jsx b/frontend/src/components/ValueFunctionChart.jsx index e496d46..dad2e8e 100644 --- a/frontend/src/components/ValueFunctionChart.jsx +++ b/frontend/src/components/ValueFunctionChart.jsx @@ -1,43 +1,43 @@ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; export default function ValueFunctionChart({ result }) { - if (!result) return null; + if (!result) return null; return (
-

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

+

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

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

+ Método Deck of Cards +

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

+ En construcción +

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

+ Escribe una etiqueta +

+ )} +
- {/* Botón Eliminar */} - {totalLevels > 2 && ( - - )} - - {/* Detalles tipo naipe */} - - {index + 1} - - - {index + 1} - - - handleLevelChange(index, e.target.value)} - className="w-4/5 text-center text-2xl font-bold text-slate-700 bg-transparent border-b-2 border-dashed border-slate-300 focus:border-blue-500 outline-none pb-1" - />
); } \ No newline at end of file diff --git a/frontend/src/components/CriterionInput.jsx b/frontend/src/components/CriterionInput.jsx index 300c2de..bc15f39 100644 --- a/frontend/src/components/CriterionInput.jsx +++ b/frontend/src/components/CriterionInput.jsx @@ -1,4 +1,4 @@ -export default function CriterionInput({ criterionName, setCriterionName }) { +export default function CriterionInput({ criterionName, setCriterionName, error }) { return (
); } \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index bd75055..73cbad7 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -22,7 +22,7 @@ export const MainLayout = () => { }` } > - DoC Clásico + DoC Básico { + + let hasError = false; + const newErrors = { criterion: false, levels: Array(levels.length).fill(false) }; + + if (!criterionName.trim()) { + newErrors.criterion = true; + hasError = true; + } + + levels.forEach((level, idx) => { + if (!level.trim()) { + newErrors.levels[idx] = true; + hasError = true; + } + }); + + setErrors(newErrors); + + if (hasError) return; + setIsLoading(true); setResult(null); - const firstIndex = "0"; - const lastIndex = (levels.length - 1).toString(); - - const currentReferences = { - [firstIndex]: 0, - [lastIndex]: 1 - }; - const payload = { - criterion_name: criterionName || "Criterio sin nombre", - levels: levels, + criterion_name: criterionName.trim(), + levels: levels.map(l => l.trim()), blank_cards: blankCards, - references: currentReferences + references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; try { @@ -43,15 +57,27 @@ export default function BasicMode() { } }; + const handleCriterionChange = (val) => { + setCriterionName(val); + if (errors.criterion) setErrors({ ...errors, criterion: false }); + }; + const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); + + if (errors.levels[index]) { + const newErrLevels = [...errors.levels]; + newErrLevels[index] = false; + setErrors({ ...errors, levels: newErrLevels }); + } }; const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); + setErrors({ ...errors, levels: [...errors.levels, false] }); }; const handleRemoveLevel = (indexToRemove) => { @@ -59,8 +85,12 @@ export default function BasicMode() { const newLevels = levels.filter((_, index) => index !== indexToRemove); const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); + + const newErrLevels = errors.levels.filter((_, index) => index !== indexToRemove); + setLevels(newLevels); setBlankCards(newBlankCards); + setErrors({ ...errors, levels: newErrLevels }); }; const handleBlankCardChange = (index, delta) => { @@ -74,10 +104,11 @@ export default function BasicMode() { return (
- +
@@ -90,6 +121,7 @@ export default function BasicMode() { handleLevelChange={handleLevelChange} handleRemoveLevel={handleRemoveLevel} totalLevels={levels.length} + error={errors.levels[index]} /> {index < levels.length - 1 && ( @@ -107,18 +139,18 @@ export default function BasicMode() {
-); + ); } \ No newline at end of file From 8106f40d6386bb9bde37b8a8e9919ec82765756e Mon Sep 17 00:00:00 2001 From: Alexis Date: Wed, 25 Mar 2026 09:29:52 +0100 Subject: [PATCH 22/80] fixed: manejar que minimo haya 3 cartas de etiqueta --- frontend/src/components/CardEditor.jsx | 4 ++-- frontend/src/pages/BasicMode.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx index a8fdcf7..f197cf9 100644 --- a/frontend/src/components/CardEditor.jsx +++ b/frontend/src/components/CardEditor.jsx @@ -5,7 +5,7 @@ export default function CardEditor({ index, level, handleLevelChange, handleRemo error ? 'border-red-400 shadow-red-100' : 'border-slate-200' }`}> {/* Botón para eliminar */} - {totalLevels > 2 && ( + {totalLevels > 3 && (
- +
); } \ No newline at end of file diff --git a/frontend/src/pages/BasicMode.jsx b/frontend/src/pages/BasicMode.jsx index f162493..3d913e6 100644 --- a/frontend/src/pages/BasicMode.jsx +++ b/frontend/src/pages/BasicMode.jsx @@ -81,7 +81,7 @@ export default function BasicMode() { }; const handleRemoveLevel = (indexToRemove) => { - if (levels.length <= 2) return; + if (levels.length <= 3) return; const newLevels = levels.filter((_, index) => index !== indexToRemove); const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); From e79c6df2babdd8b7cc5c1fa330af36babda25d8b Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Wed, 25 Mar 2026 13:42:09 +0100 Subject: [PATCH 23/80] =?UTF-8?q?A=C3=B1adidos=20endpoints=20de=20validaci?= =?UTF-8?q?=C3=B3n,=20organizaci=C3=B3n=20del=20backend=20en=20subcarpetas?= =?UTF-8?q?=20y=20archivos=20y=20a=C3=B1adido=20el=20control=20de=20errore?= =?UTF-8?q?s=20http.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/main.py | 25 +++ backend/api/models/docmf_models.py | 43 ++++ .../models/docmf_simple_validation_models.py | 24 ++ backend/api/models/docmf_validation_models.py | 37 +++ backend/api/models/evaluation_models.py | 35 +++ backend/api/models/value_function_models.py | 36 +++ backend/api/routers/docmf_build.py | 12 + backend/api/routers/docmf_evaluate.py | 12 + .../api/routers/docmf_simple_validation.py | 38 ++++ backend/api/routers/docmf_validation.py | 39 ++++ backend/api/routers/value_function.py | 19 ++ backend/api/routes.py | 210 ------------------ backend/api/services/docmf_build_service.py | 37 +++ .../api/services/docmf_evaluate_service.py | 22 ++ .../docmf_simple_validation_service.py | 25 +++ .../api/services/docmf_validation_service.py | 39 ++++ .../api/services/value_function_service.py | 38 ++++ backend/api/utils/interpolation.py | 8 + 18 files changed, 489 insertions(+), 210 deletions(-) create mode 100644 backend/api/main.py create mode 100644 backend/api/models/docmf_models.py create mode 100644 backend/api/models/docmf_simple_validation_models.py create mode 100644 backend/api/models/docmf_validation_models.py create mode 100644 backend/api/models/evaluation_models.py create mode 100644 backend/api/models/value_function_models.py create mode 100644 backend/api/routers/docmf_build.py create mode 100644 backend/api/routers/docmf_evaluate.py create mode 100644 backend/api/routers/docmf_simple_validation.py create mode 100644 backend/api/routers/docmf_validation.py create mode 100644 backend/api/routers/value_function.py delete mode 100644 backend/api/routes.py create mode 100644 backend/api/services/docmf_build_service.py create mode 100644 backend/api/services/docmf_evaluate_service.py create mode 100644 backend/api/services/docmf_simple_validation_service.py create mode 100644 backend/api/services/docmf_validation_service.py create mode 100644 backend/api/services/value_function_service.py create mode 100644 backend/api/utils/interpolation.py diff --git a/backend/api/main.py b/backend/api/main.py new file mode 100644 index 0000000..f7df1ae --- /dev/null +++ b/backend/api/main.py @@ -0,0 +1,25 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from routers.value_function import router as value_router +from routers.docmf_build import router as docmf_build_router +from routers.docmf_evaluate import router as docmf_eval_router +from routers.docmf_simple_validation import router as simple_validation_router +from routers.docmf_validation import router as validation_router + + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(value_router, prefix="/api/criteria/doc") +app.include_router(docmf_build_router, prefix="/api/criteria/doc-mf") +app.include_router(docmf_eval_router, prefix="/api/criteria/doc-mf") +app.include_router(simple_validation_router, prefix="/api/criteria/doc-mf") +app.include_router(validation_router, prefix="/api/criteria/doc-mf") diff --git a/backend/api/models/docmf_models.py b/backend/api/models/docmf_models.py new file mode 100644 index 0000000..a13b69c --- /dev/null +++ b/backend/api/models/docmf_models.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class DoCMFRequest(BaseModel): + term: str + core: Tuple[float, float] + support: Tuple[float, float] + left_nodes_x: List[float] + left_blank_cards: List[int] + right_nodes_x: List[float] + right_blank_cards: List[int] + + @field_validator("term") + def term_not_empty(cls, v): + if not v.strip(): + raise ValueError("El término no puede estar vacío.") + return v + + @field_validator("core") + def core_valid(cls, v): + a, b = v + if a > b: + raise ValueError("El núcleo debe cumplir a <= b.") + return v + + @field_validator("support") + def support_valid(cls, v, info): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + + core = info.data.get("core") + if core: + a, b = core + if not (c <= a < b <= d): + raise ValueError("El núcleo debe estar dentro del soporte.") + return v + + @field_validator("left_blank_cards", "right_blank_cards") + def cards_valid(cls, v): + if any(c < 0 for c in v): + raise ValueError("Las cartas no pueden ser negativas.") + return v diff --git a/backend/api/models/docmf_simple_validation_models.py b/backend/api/models/docmf_simple_validation_models.py new file mode 100644 index 0000000..246e5d5 --- /dev/null +++ b/backend/api/models/docmf_simple_validation_models.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class SimpleLevelDefinition(BaseModel): + core: Tuple[float, float] + support: Tuple[float, float] + + @field_validator("core") + def validate_core(cls, v): + a, b = v + if a >= b: + raise ValueError("El núcleo debe cumplir a < b.") + return v + + @field_validator("support") + def validate_support(cls, v): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + return v + + +class SimpleValidationRequest(BaseModel): + levels: List[SimpleLevelDefinition] diff --git a/backend/api/models/docmf_validation_models.py b/backend/api/models/docmf_validation_models.py new file mode 100644 index 0000000..e62907d --- /dev/null +++ b/backend/api/models/docmf_validation_models.py @@ -0,0 +1,37 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class LevelDefinition(BaseModel): + core: Tuple[float, float] + support: Tuple[float, float] + left_nodes: List[Tuple[float, float]] + right_nodes: List[Tuple[float, float]] + + @field_validator("core") + def validate_core(cls, v): + a, b = v + if a >= b: + raise ValueError("El núcleo debe cumplir a < b.") + return v + + @field_validator("support") + def validate_support(cls, v): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + return v + + @field_validator("left_nodes", "right_nodes") + def validate_nodes(cls, v): + if len(v) < 2: + raise ValueError("Debe haber al menos 2 nodos.") + xs = [p[0] for p in v] + if xs != sorted(xs): + raise ValueError("Los nodos deben estar ordenados por x.") + if len(xs) != len(set(xs)): + raise ValueError("Los nodos no pueden tener valores x duplicados.") + return v + + +class ValidationRequest(BaseModel): + levels: List[LevelDefinition] diff --git a/backend/api/models/evaluation_models.py b/backend/api/models/evaluation_models.py new file mode 100644 index 0000000..ca18221 --- /dev/null +++ b/backend/api/models/evaluation_models.py @@ -0,0 +1,35 @@ +from pydantic import BaseModel, field_validator +from typing import List, Tuple + +class EvaluationRequest(BaseModel): + x: float + core: Tuple[float, float] + support: Tuple[float, float] + left_nodes: List[Tuple[float, float]] + right_nodes: List[Tuple[float, float]] + + @field_validator("core") + def core_valid(cls, v): + a, b = v + if a > b: + raise ValueError("El núcleo debe cumplir a <= b.") + return v + + @field_validator("support") + def support_valid(cls, v, info): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + + core = info.data.get("core") + if core: + a, b = core + if not (c <= a < b <= d): + raise ValueError("El núcleo debe estar dentro del soporte.") + return v + + @field_validator("left_nodes", "right_nodes") + def nodes_valid(cls, v): + if len(v) < 2: + raise ValueError("Debe haber al menos 2 nodos.") + return v diff --git a/backend/api/models/value_function_models.py b/backend/api/models/value_function_models.py new file mode 100644 index 0000000..66aaed5 --- /dev/null +++ b/backend/api/models/value_function_models.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, field_validator +from typing import List, Dict + +class ValueFunctionRequest(BaseModel): + criterion_name: str + levels: List[str] + blank_cards: List[int] + references: Dict[str, float] + + @field_validator("criterion_name") + def name_not_empty(cls, v): + if not v.strip(): + raise ValueError("El nombre no puede estar vacío.") + return v + + @field_validator("levels") + def levels_not_empty(cls, v): + if len(v) < 2: + raise ValueError("Debe haber al menos 2 niveles.") + return v + + @field_validator("blank_cards") + def cards_valid(cls, v, info): + if any(c < 0 for c in v): + raise ValueError("Las cartas no pueden ser negativas.") + + levels = info.data.get("levels") + if levels and len(v) != len(levels) - 1: + raise ValueError("Debe haber uno menos de número de cartas blancas que de niveles.") + return v + + @field_validator("references") + def refs_valid(cls, v): + if len(v) != 2: + raise ValueError("Debe haber 2 referencias.") + return v diff --git a/backend/api/routers/docmf_build.py b/backend/api/routers/docmf_build.py new file mode 100644 index 0000000..f3169b2 --- /dev/null +++ b/backend/api/routers/docmf_build.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, HTTPException +from models.docmf_models import DoCMFRequest +from services.docmf_build_service import build_docmf + +router = APIRouter() + +@router.post("/build") +def build(request: DoCMFRequest): + try: + return build_docmf(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/routers/docmf_evaluate.py b/backend/api/routers/docmf_evaluate.py new file mode 100644 index 0000000..f2aae4a --- /dev/null +++ b/backend/api/routers/docmf_evaluate.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter, HTTPException +from models.evaluation_models import EvaluationRequest +from services.docmf_evaluate_service import evaluate_docmf + +router = APIRouter() + +@router.post("/evaluate") +def evaluate(request: EvaluationRequest): + try: + return evaluate_docmf(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/routers/docmf_simple_validation.py b/backend/api/routers/docmf_simple_validation.py new file mode 100644 index 0000000..800ec93 --- /dev/null +++ b/backend/api/routers/docmf_simple_validation.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, HTTPException +from models.docmf_simple_validation_models import SimpleValidationRequest +from services.docmf_simple_validation_service import validate_simple_levels + +router = APIRouter() + +@router.post("/validate-simple") +def validate_simple_docmf(request: SimpleValidationRequest): + results = validate_simple_levels(request.levels) + invalid = [r for r in results if not r["valid"]] + + # Caso: un solo nivel + if len(request.levels) == 1: + if invalid: + raise HTTPException( + status_code=400, + detail={ + "message": "El nivel es incorrecto.", + "errors": invalid[0]["errors"] + } + ) + return { + "message": "El nivel es correcto.", + "details": results[0] + } + + # Caso: varios niveles + if invalid: + return { + "message": "Validación completada.", + "valid_levels": [r for r in results if r["valid"]], + "invalid_levels": invalid + } + + return { + "message": "Todos los niveles son correctos.", + "results": results + } diff --git a/backend/api/routers/docmf_validation.py b/backend/api/routers/docmf_validation.py new file mode 100644 index 0000000..a1bb518 --- /dev/null +++ b/backend/api/routers/docmf_validation.py @@ -0,0 +1,39 @@ +from fastapi import APIRouter, HTTPException +from models.docmf_validation_models import ValidationRequest +from services.docmf_validation_service import validate_levels + +router = APIRouter() + +@router.post("/validate") +def validate_docmf_levels(request: ValidationRequest): + results = validate_levels(request.levels) + + invalid = [r for r in results if not r["valid"]] + + if len(request.levels) == 1: + # Caso de un solo nivel + if invalid: + raise HTTPException( + status_code=400, + detail={ + "message": "El nivel es incorrecto.", + "errors": invalid[0]["errors"] + } + ) + return { + "message": "El nivel es correcto.", + "details": results[0] + } + + # Caso de varios niveles + if invalid: + return { + "message": "Validación completada.", + "valid_levels": [r for r in results if r["valid"]], + "invalid_levels": invalid + } + + return { + "message": "Todos los niveles son correctos.", + "results": results + } diff --git a/backend/api/routers/value_function.py b/backend/api/routers/value_function.py new file mode 100644 index 0000000..c30022e --- /dev/null +++ b/backend/api/routers/value_function.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, HTTPException +from models.value_function_models import ValueFunctionRequest +from services.value_function_service import compute_value_function, compute_points + +router = APIRouter() + +@router.post("/value-function") +def value_function(request: ValueFunctionRequest): + try: + return compute_value_function(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/value-function/points") +def value_function_points(request: ValueFunctionRequest): + try: + return compute_points(request) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/backend/api/routes.py b/backend/api/routes.py deleted file mode 100644 index 10544bc..0000000 --- a/backend/api/routes.py +++ /dev/null @@ -1,210 +0,0 @@ -from fastapi import FastAPI -from pydantic import BaseModel -from typing import List, Dict, Tuple -from fastapi.middleware.cors import CORSMiddleware - -app = FastAPI() - -# Configuración CORS para permitir peticiones desde React -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # Permite cualquier origen (perfecto para desarrollo local) - allow_credentials=True, - allow_methods=["*"], # Permite POST, GET, OPTIONS, etc. - allow_headers=["*"], # Permite cualquier cabecera -) - -# ----------------------------- -# MODELOS -# ----------------------------- - -class ValueFunctionRequest(BaseModel): - criterion_name: str - levels: List[str] - blank_cards: List[int] - references: Dict[str, float] - -class DoCMFRequest(BaseModel): - term: str - core: Tuple[float, float] # [a, b] - support: Tuple[float, float] # [c, d] - left_nodes_x: List[float] - left_blank_cards: List[int] - right_nodes_x: List[float] - right_blank_cards: List[int] - -class EvaluationRequest(BaseModel): - x: float - left_nodes: List[Tuple[float, float]] - right_nodes: List[Tuple[float, float]] - core: Tuple[float, float] - support: Tuple[float, float] - - -# ----------------------------- -# ENDPOINT 1: FUNCIÓN DE VALOR DoC -# ----------------------------- - -@app.post("/api/criteria/doc/value-function") -def calcular_value_function(request: ValueFunctionRequest): - levels = request.levels - cards = request.blank_cards - refs = request.references - - ref_indices = sorted(int(k) for k in refs) - p, q = ref_indices - up, uq = refs[str(p)], refs[str(q)] - - total_units = sum(cards[i] + 1 for i in range(p, q)) - alpha = (uq - up) / total_units if total_units else 0 - - values = [0] * len(levels) - values[p] = up - - for i in range(p + 1, len(levels)): - units = sum(cards[r] + 1 for r in range(p, i)) - values[i] = up + alpha * units - - for i in range(p - 1, -1, -1): - units = sum(cards[r] + 1 for r in range(i, p)) - values[i] = up - alpha * units - - return { - "criterion_name": request.criterion_name, - "values": {levels[i]: round(values[i], 4) for i in range(len(levels))} - } - - -# ----------------------------- -# ENDPOINT 2: CONSTRUIR DoC-MF -# ----------------------------- - -@app.post("/api/criteria/doc-mf/build") -def build_doc_mf(request: DoCMFRequest): - - a, b = request.core - c, d = request.support - - # ---- LADO IZQUIERDO ---- - left_x = request.left_nodes_x - left_e = request.left_blank_cards - - TL = sum(e + 1 for e in left_e) - YL = 1 / TL if TL else 0 - - left_nodes = [] - acc = 0 - for i in range(len(left_x)): - if i == 0: - left_nodes.append((left_x[i], 0.0)) - else: - acc += (left_e[i-1] + 1) - left_nodes.append((left_x[i], round(acc * YL, 4))) - - # ---- LADO DERECHO ---- - right_x = request.right_nodes_x - right_e = request.right_blank_cards - - TR = sum(e + 1 for e in right_e) - YR = 1 / TR if TR else 0 - - right_nodes = [] - acc = 0 - for i in range(len(right_x)): - if i == 0: - right_nodes.append((right_x[i], 1.0)) - else: - acc += (right_e[i-1] + 1) - right_nodes.append((right_x[i], round(1 - acc * YR, 4))) - - return { - "term": request.term, - "core": request.core, - "support": request.support, - "left_nodes": left_nodes, - "right_nodes": right_nodes - } - - -# ----------------------------- -# ENDPOINT 3: EVALUAR UN VALOR x -# ----------------------------- - -def linear_interpolation(x, nodes): - for i in range(len(nodes) - 1): - x0, y0 = nodes[i] - x1, y1 = nodes[i+1] - if x0 <= x <= x1: - t = (x - x0) / (x1 - x0) - return y0 + t * (y1 - y0) - return 0.0 - -@app.post("/api/criteria/doc-mf/evaluate") -def evaluate_doc_mf(request: EvaluationRequest): - x = request.x - a, b = request.core - c, d = request.support - - if x < c or x > d: - return { - "membership": 0.0, - "explanation": "El valor está fuera del soporte, por eso la pertenencia es 0." - } - - if a <= x <= b: - return { - "membership": 1.0, - "explanation": "El valor está dentro del núcleo, por eso la pertenencia es 1." - } - - if c <= x < a: - mu = linear_interpolation(x, request.left_nodes) - return { - "membership": mu, - "explanation": "El valor está en el lado izquierdo y se interpola entre nodos." - } - - if b < x <= d: - mu = linear_interpolation(x, request.right_nodes) - return { - "membership": mu, - "explanation": "El valor está en el lado derecho y se interpola entre nodos." - } - - return { - "membership": 0.0, - "explanation": "No se pudo determinar la pertenencia." - } - - -# ----------------------------- -# ENDPOINT 4: PUNTOS (x,y) DE LA FUNCIÓN DE VALOR -# ----------------------------- - -@app.post("/api/criteria/doc/value-function/points") -def value_function_points(request: ValueFunctionRequest): - levels = request.levels - cards = request.blank_cards - refs = request.references - - ref_indices = sorted(int(k) for k in refs) - p, q = ref_indices - up, uq = refs[str(p)], refs[str(q)] - - total_units = sum(cards[i] + 1 for i in range(p, q)) - alpha = (uq - up) / total_units if total_units else 0 - - values = [0] * len(levels) - values[p] = up - - for i in range(p + 1, len(levels)): - units = sum(cards[r] + 1 for r in range(p, i)) - values[i] = up + alpha * units - - for i in range(p - 1, -1, -1): - units = sum(cards[r] + 1 for r in range(i, p)) - values[i] = up - alpha * units - - points = [{"x": i, "y": round(values[i], 4)} for i in range(len(levels))] - - return {"points": points} diff --git a/backend/api/services/docmf_build_service.py b/backend/api/services/docmf_build_service.py new file mode 100644 index 0000000..0cdc6eb --- /dev/null +++ b/backend/api/services/docmf_build_service.py @@ -0,0 +1,37 @@ +def build_docmf(request): + a, b = request.core + c, d = request.support + + # LEFT + TL = sum(e + 1 for e in request.left_blank_cards) + YL = 1 / TL + left_nodes = [] + acc = 0 + + for i, x in enumerate(request.left_nodes_x): + if i == 0: + left_nodes.append((x, 0.0)) + else: + acc += request.left_blank_cards[i-1] + 1 + left_nodes.append((x, round(acc * YL, 4))) + + # RIGHT + TR = sum(e + 1 for e in request.right_blank_cards) + YR = 1 / TR + right_nodes = [] + acc = 0 + + for i, x in enumerate(request.right_nodes_x): + if i == 0: + right_nodes.append((x, 1.0)) + else: + acc += request.right_blank_cards[i-1] + 1 + right_nodes.append((x, round(1 - acc * YR, 4))) + + return { + "term": request.term, + "core": request.core, + "support": request.support, + "left_nodes": left_nodes, + "right_nodes": right_nodes + } diff --git a/backend/api/services/docmf_evaluate_service.py b/backend/api/services/docmf_evaluate_service.py new file mode 100644 index 0000000..37a3562 --- /dev/null +++ b/backend/api/services/docmf_evaluate_service.py @@ -0,0 +1,22 @@ +from utils.interpolation import linear_interpolation + +def evaluate_docmf(request): + x = request.x + a, b = request.core + c, d = request.support + + if x < c or x > d: + return {"membership": 0.0, "explanation": "Fuera del soporte."} + + if a <= x <= b: + return {"membership": 1.0, "explanation": "Dentro del núcleo."} + + if c <= x < a: + mu = linear_interpolation(x, request.left_nodes) + return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.left_nodes} del lado izquierdo."} + + if b < x <= d: + mu = linear_interpolation(x, request.right_nodes) + return {"membership": mu, "explanation": f"El valor x={x} se interpola entre los nodos {request.right_nodes} del lado derecho."} + + raise ValueError("No se pudo evaluar el valor.") diff --git a/backend/api/services/docmf_simple_validation_service.py b/backend/api/services/docmf_simple_validation_service.py new file mode 100644 index 0000000..253f910 --- /dev/null +++ b/backend/api/services/docmf_simple_validation_service.py @@ -0,0 +1,25 @@ +def validate_simple_level(level: dict): + errors = [] + + a, b = level["core"] + c, d = level["support"] + + # Validación: núcleo dentro del soporte + if not (c <= a < b <= d): + errors.append("El núcleo debe estar completamente dentro del soporte.") + + return errors + + +def validate_simple_levels(levels): + results = [] + + for idx, level in enumerate(levels): + errors = validate_simple_level(level.dict()) + results.append({ + "level_index": idx, + "valid": len(errors) == 0, + "errors": errors + }) + + return results diff --git a/backend/api/services/docmf_validation_service.py b/backend/api/services/docmf_validation_service.py new file mode 100644 index 0000000..97ccfa5 --- /dev/null +++ b/backend/api/services/docmf_validation_service.py @@ -0,0 +1,39 @@ +def validate_single_level(level: dict): + errors = [] + + a, b = level["core"] + c, d = level["support"] + + # Core dentro del soporte + if not (c <= a < b <= d): + errors.append("El núcleo debe estar completamente dentro del soporte.") + + # Nodos cubren correctamente el soporte + left = level["left_nodes"] + right = level["right_nodes"] + + if left[0][0] != c: + errors.append("El primer nodo izquierdo debe coincidir con el inicio del soporte.") + if left[-1][0] != a: + errors.append("El último nodo izquierdo debe coincidir con el inicio del núcleo.") + + if right[0][0] != b: + errors.append("El primer nodo derecho debe coincidir con el final del núcleo.") + if right[-1][0] != d: + errors.append("El último nodo derecho debe coincidir con el final del soporte.") + + return errors + + +def validate_levels(levels): + results = [] + + for idx, level in enumerate(levels): + errors = validate_single_level(level.dict()) + results.append({ + "level_index": idx, + "valid": len(errors) == 0, + "errors": errors + }) + + return results diff --git a/backend/api/services/value_function_service.py b/backend/api/services/value_function_service.py new file mode 100644 index 0000000..07039b0 --- /dev/null +++ b/backend/api/services/value_function_service.py @@ -0,0 +1,38 @@ +def compute_value_function(request): + levels = request.levels + cards = request.blank_cards + refs = request.references + + p, q = sorted(int(k) for k in refs) + up, uq = refs[str(p)], refs[str(q)] + + if len(levels) < 3: + raise ValueError("Mínimo debe haber 3 niveles para esta funcionalidad.") + + total_units = sum(cards[i] + 1 for i in range(p, q)) + if total_units == 0: + raise ValueError("Las cartas no pueden generar 0 unidades.") + + alpha = (uq - up) / total_units + + values = [0] * len(levels) + values[p] = up + + for i in range(p + 1, len(levels)): + units = sum(cards[r] + 1 for r in range(p, i)) + values[i] = up + alpha * units + + for i in range(p - 1, -1, -1): + units = sum(cards[r] + 1 for r in range(i, p)) + values[i] = up - alpha * units + + return { + "criterion_name": request.criterion_name, + "values": {levels[i]: round(values[i], 4) for i in range(len(levels))} + } + + +def compute_points(request): + result = compute_value_function(request) + values = list(result["values"].values()) + return {"points": [{"x": i, "y": values[i]} for i in range(len(values))]} diff --git a/backend/api/utils/interpolation.py b/backend/api/utils/interpolation.py new file mode 100644 index 0000000..ccfc35f --- /dev/null +++ b/backend/api/utils/interpolation.py @@ -0,0 +1,8 @@ +def linear_interpolation(x, nodes): + for i in range(len(nodes) - 1): + x0, y0 = nodes[i] + x1, y1 = nodes[i+1] + if x0 <= x <= x1: + t = (x - x0) / (x1 - x0) + return y0 + t * (y1 - y0) + return 0.0 From 3977e11ffb71a0497f9a71b9b7b2339007cdfdb7 Mon Sep 17 00:00:00 2001 From: Alexis Date: Wed, 25 Mar 2026 13:42:19 +0100 Subject: [PATCH 24/80] =?UTF-8?q?add:=20a=C3=B1adir=20funcionalidad=20avan?= =?UTF-8?q?zada=20en=20dos=20pasos:=20el=20primero=20calcula=20la=20escala?= =?UTF-8?q?,=20el=20segundo=20deja=20seleccionar=20el=20nucleo=20y=20el=20?= =?UTF-8?q?soporte=20de=20cada=20etiqueta=20de=20forma=20visual?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/AdvancedMode.jsx | 318 +++++++++++++++++++++++++++- 1 file changed, 315 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx index 3a96f2d..2a3b9a9 100644 --- a/frontend/src/pages/AdvancedMode.jsx +++ b/frontend/src/pages/AdvancedMode.jsx @@ -1,7 +1,319 @@ +import React, { useState } from 'react'; +import { ComposedChart, Line, XAxis, YAxis, CartesianGrid, ReferenceArea, ReferenceLine, ResponsiveContainer, Tooltip } from 'recharts'; +import CriterionInput from '../components/CriterionInput'; +import CardEditor from '../components/CardEditor'; +import BlankCardsCounter from '../components/BlankCardsCounter'; +import AddLevelButton from '../components/AddLevelButton'; +import { calculateValueFunction } from '../services/docService'; + +const COLORS = [ + '#ef4444', + '#f59e0b', + '#10b981', + '#3b82f6', + '#d946ef', + '#06b6d4', + '#8b5cf6', + '#f43f5e', + '#6366f1' +]; + export default function AdvancedMode() { + + const [step, setStep] = useState(1); + const [isLoading, setIsLoading] = useState(false); + + const [criterionName, setCriterionName] = useState(''); + const [levels, setLevels] = useState(['', '', '']); + const [blankCards, setBlankCards] = useState([0, 0]); + const [errors, setErrors] = useState({ criterion: false, levels: [] }); + + const [baseScale, setBaseScale] = useState({}); + const [selectedTerm, setSelectedTerm] = useState(null); + const [mfDefinitions, setMfDefinitions] = useState({}); + + const handleCriterionChange = (val) => { + setCriterionName(val); + if (errors.criterion) setErrors({ ...errors, criterion: false }); + }; + + const handleLevelChange = (index, newValue) => { + const newLevels = [...levels]; + newLevels[index] = newValue; + setLevels(newLevels); + if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); + }; + + const handleAddLevel = () => { + setLevels([...levels, '']); + setBlankCards([...blankCards, 0]); + setErrors({ ...errors, levels: [...errors.levels, false] }); + }; + + const handleRemoveLevel = (indexToRemove) => { + if (levels.length <= 3) return; + const newLevels = levels.filter((_, index) => index !== indexToRemove); + const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; + const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); + setLevels(newLevels); + setBlankCards(newBlankCards); + setErrors({ ...errors, levels: errors.levels.filter((_, index) => index !== indexToRemove) }); + }; + + const handleBlankCardChange = (index, delta) => { + const newBlankCards = [...blankCards]; + const newValue = newBlankCards[index] + delta; + if (newValue >= 0) { + newBlankCards[index] = newValue; + setBlankCards(newBlankCards); + } + }; + + const handleGenerateBaseScale = async () => { + let hasError = false; + const newErrors = { criterion: false, levels: Array(levels.length).fill(false) }; + if (!criterionName.trim()) { newErrors.criterion = true; hasError = true; } + levels.forEach((lvl, idx) => { if (!lvl.trim()) { newErrors.levels[idx] = true; hasError = true; }}); + setErrors(newErrors); + + if (hasError) return alert("Por favor, rellena todos los campos de la escala base."); + + setIsLoading(true); + + try { + const payloadBase = { + criterion_name: criterionName.trim(), + levels: levels.map(l => l.trim()), + blank_cards: blankCards, + references: { "0": 0, [(levels.length - 1).toString()]: 1 } + }; + + const baseResult = await calculateValueFunction(payloadBase); + const calculatedValues = baseResult.values; + setBaseScale(calculatedValues); + + const initialMfs = {}; + Object.entries(calculatedValues).forEach(([name, value]) => { + initialMfs[name] = { + supportStart: value, + coreStart: value, + coreEnd: value, + supportEnd: value + }; + }); + setMfDefinitions(initialMfs); + setSelectedTerm(Object.keys(calculatedValues)[0]); + setStep(2); + + } catch (error) { + alert("Error al conectar con el backend: " + error); + } finally { + setIsLoading(false); + } + }; + + const updateCurrentMf = (field, value) => { + if (!selectedTerm) return; + const numValue = parseFloat(value); + + setMfDefinitions(prev => { + const current = { ...prev[selectedTerm], [field]: numValue }; + + // Reglas de colisión internas + if (field === 'supportStart' && current.supportStart > current.coreStart) current.coreStart = current.supportStart; + if (field === 'coreStart') { + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + } + if (field === 'coreEnd') { + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } + if (field === 'supportEnd' && current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; + + return { ...prev, [selectedTerm]: current }; + }); + }; + + const handleFinalSubmit = () => { + console.log("DATOS FINALES LISTOS PARA EL BACKEND:", { + base_scale: baseScale, + membership_functions: mfDefinitions + }); + alert("¡Mira la consola! El JSON está preparado."); + }; + + // Cálculo de límites verticales + const scaleKeys = Object.keys(baseScale); + const selectedIndex = scaleKeys.indexOf(selectedTerm); + const selectedColor = COLORS[selectedIndex % COLORS.length] || '#2563eb'; + const currentMf = selectedTerm ? mfDefinitions[selectedTerm] : null; + + let minBound = 0; + let maxBound = 1; + + if (selectedIndex > 0) { + minBound = baseScale[scaleKeys[selectedIndex - 1]]; + } + if (selectedIndex >= 0 && selectedIndex < scaleKeys.length - 1) { + maxBound = baseScale[scaleKeys[selectedIndex + 1]]; + } + + return ( -

- En construcción -

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

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

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

Paso 2: Modelar Conceptos Difusos

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

+ Ajustando franjas para: "{selectedTerm}" +

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

+ Ajustando franjas para: "{selectedTerm}" +

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

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

+

Paso 1: Escala de Referencia (Cartas)

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

- Ajustando franjas para: "{selectedTerm}" -

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

Escribe una etiqueta

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

Paso 1: Escala de Referencia (Cartas)

+
+ +
+

+ Paso 1: Escala de Referencia (Mesa) +

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

- Escribe una etiqueta -

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

Escribe una etiqueta

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

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

+ Deck of Cards Method

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

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

+ Ajustando: "{selectedTerm}"

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

+
+

Paso 1: Escala de Referencia (Mesa)

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

Paso 2: Modelar Conceptos Difusos

- +
+
+

Paso 2: Modelar Conceptos Difusos

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

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

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

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

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

Paso 2: Modelar Conceptos Difusos

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

Paso 2: Modelar Conceptos Difusos

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

Diseñar Subescala

+

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

+
+ +
+ + {/* Tablero */} +
+
+ {Array.from({ length: cardsCount }).map((_, index) => ( + +
+
+ {cardsCount > 2 && index === cardsCount - 1 && ( + + )} + {index + 1} +
+
+ {index < cardsCount - 1 && ( + + )} +
+ ))} + +
+ +
+
+
+ + {/* Botones */} +
+ + +
+ + +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/membershipFunction/Controls.jsx b/frontend/src/components/membershipFunction/Controls.jsx index fe7e785..f879c4a 100644 --- a/frontend/src/components/membershipFunction/Controls.jsx +++ b/frontend/src/components/membershipFunction/Controls.jsx @@ -1,21 +1,18 @@ -export default function Controls({ selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf }) { +export default function Controls({ + selectedTerm, currentMf, selectedColor, baseScale, mfDefinitions, updateCurrentMf, + subscales, onOpenSubscale +}) { if (!selectedTerm || !currentMf) return null; const scaleKeys = Object.keys(baseScale); const selectedIndex = scaleKeys.indexOf(selectedTerm); - let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; + let absoluteMin = 0, absoluteMax = 1; + if (selectedIndex > 0) absoluteMin = mfDefinitions[scaleKeys[selectedIndex - 1]].coreEnd; + if (selectedIndex < scaleKeys.length - 1) absoluteMax = mfDefinitions[scaleKeys[selectedIndex + 1]].coreStart; - if (selectedIndex > 0) { - const prevTerm = scaleKeys[selectedIndex - 1]; - prevCoreEnd = mfDefinitions[prevTerm].coreEnd; - prevSupportEnd = mfDefinitions[prevTerm].supportEnd; - } - if (selectedIndex < scaleKeys.length - 1) { - const nextTerm = scaleKeys[selectedIndex + 1]; - nextCoreStart = mfDefinitions[nextTerm].coreStart; - nextSupportStart = mfDefinitions[nextTerm].supportStart; - } + const leftSubscale = subscales?.[selectedTerm]?.left; + const rightSubscale = subscales?.[selectedTerm]?.right; return (
@@ -24,34 +21,56 @@ export default function Controls({ selectedTerm, currentMf, selectedColor, baseS Ajustando: "{selectedTerm}" -
-
+
+ {/* Lado izquierdo (Pendiente ascendente) */} +
- updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> + updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
- updateCurrentMf('supportStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> + updateCurrentMf('coreStart', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> +
+ + {/* Botón subescala izquierda */} +
+
-
+ {/* Lado derecho (Pendiente descendente) */} +
- updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> + updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} />
- updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> + updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} /> +
+ + {/* Botón subescala derecha */} +
+
diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 3baa1f2..34a24c9 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -1,109 +1,126 @@ import { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; +import SubscaleModal from '../components/editor/SubscaleModal'; // <-- IMPORTAMOS EL MODAL import { calculateValueFunction } from '../services/docService'; export default function DocEditor() { - const [step, setStep] = useState(1); - const [isLoading, setIsLoading] = useState(false); + const [step, setStep] = useState(1); + const [isLoading, setIsLoading] = useState(false); - // ESTADOS: FASE 1 - const [criterionName, setCriterionName] = useState(''); - const [levels, setLevels] = useState(['', '', '']); - const [blankCards, setBlankCards] = useState([0, 0]); - const [errors, setErrors] = useState({ criterion: false, levels: [] }); + // ESTADOS: FASE 1 + const [criterionName, setCriterionName] = useState(''); + const [levels, setLevels] = useState(['', '', '']); + const [blankCards, setBlankCards] = useState([0, 0]); + const [errors, setErrors] = useState({ criterion: false, levels: [] }); - // ESTADOS: FASE 2 - const [baseScale, setBaseScale] = useState({}); - const [selectedTerm, setSelectedTerm] = useState(null); - const [mfDefinitions, setMfDefinitions] = useState({}); + // ESTADOS: FASE 2 + const [baseScale, setBaseScale] = useState({}); + const [selectedTerm, setSelectedTerm] = useState(null); + const [mfDefinitions, setMfDefinitions] = useState({}); - // MANEJADORES: FASE 1 - const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; - const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; - const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); }; - const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); }; - const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } }; + // ESTADOS: SUBESCALAS (FASE 2.5) + // Formato: { "regular": { left: { cardsCount: 3, blankCards: [1, 0] }, right: null }, "bueno": ... } + const [subscales, setSubscales] = useState({}); + const [modalTarget, setModalTarget] = useState(null); // { term: 'regular', side: 'left', initialData: {...} } - const handleGenerateBaseScale = async () => { - const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) }; - if (newErrors.criterion || newErrors.levels.includes(true)) { - setErrors(newErrors); - return alert("Por favor, rellena todos los campos."); - } + // MANEJADORES: FASE 1 + const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; + const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; + const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); }; + const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); }; + const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } }; - setIsLoading(true); - try { - const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; - const baseResult = await calculateValueFunction(payloadBase); - - setBaseScale(baseResult.values); - const initialMfs = {}; - Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; }); - - setMfDefinitions(initialMfs); - setSelectedTerm(Object.keys(baseResult.values)[0]); - setStep(2); - } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } - }; + const handleGenerateBaseScale = async () => { + const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) }; + if (newErrors.criterion || newErrors.levels.includes(true)) { + setErrors(newErrors); + return alert("Por favor, rellena todos los campos."); + } - // MANEJADORES: FASE 2 - const updateCurrentMf = (field, value) => { - if (!selectedTerm) return; - let numValue = parseFloat(value); - - setMfDefinitions(prev => { - const scaleKeys = Object.keys(baseScale); - const selectedIndex = scaleKeys.indexOf(selectedTerm); - let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; + setIsLoading(true); + try { + const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; + const baseResult = await calculateValueFunction(payloadBase); + setBaseScale(baseResult.values); + const initialMfs = {}; + Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; }); + setMfDefinitions(initialMfs); + setSelectedTerm(Object.keys(baseResult.values)[0]); + setStep(2); + } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } + }; - if (selectedIndex > 0) { - prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd; - prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd; - } - if (selectedIndex < scaleKeys.length - 1) { - nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart; - nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart; - } + // MANEJADORES: FASE 2 + const updateCurrentMf = (field, value) => { + if (!selectedTerm) return; + let numValue = parseFloat(value); + setMfDefinitions(prev => { + const scaleKeys = Object.keys(baseScale); + const selectedIndex = scaleKeys.indexOf(selectedTerm); + let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; - const anchor = baseScale[selectedTerm]; + if (selectedIndex > 0) { + prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd; + prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd; + } + if (selectedIndex < scaleKeys.length - 1) { + nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart; + nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart; + } - if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd; - if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd; - if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart; - if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart; + const anchor = baseScale[selectedTerm]; - if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor; - if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor; + if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd; + if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd; + if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart; + if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart; + if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor; + if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor; - const current = { ...prev[selectedTerm], [field]: numValue }; + const current = { ...prev[selectedTerm], [field]: numValue }; - if (field === 'supportStart') { - if (current.supportStart > current.coreStart) current.coreStart = current.supportStart; - if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - } else if (field === 'coreStart') { - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - } else if (field === 'coreEnd') { - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - } else if (field === 'supportEnd') { - if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; - if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - } + if (field === 'supportStart') { + if (current.supportStart > current.coreStart) current.coreStart = current.supportStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreStart') { + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + } else if (field === 'coreEnd') { + if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } else if (field === 'supportEnd') { + if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; + if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; + if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; + } + return { ...prev, [selectedTerm]: current }; + }); + }; - return { ...prev, [selectedTerm]: current }; - }); - }; + // MANEJADORES: SUBESCALAS + const handleOpenSubscale = (term, side, initialData) => { + setModalTarget({ term, side, initialData }); + }; - const handleFinalSubmit = () => { - console.log("PAYLOAD DOC-MF:", { baseScale, mfDefinitions }); - alert("¡Mira la consola! JSON preparado."); - }; + const handleSaveSubscale = (term, side, data) => { + setSubscales(prev => ({ + ...prev, + [term]: { + ...prev[term], + [side]: data + } + })); + setModalTarget(null); + }; + + const handleFinalSubmit = () => { + console.log("PAYLOAD DOC-MF COMPLETO:", { baseScale, mfDefinitions, subscales }); + alert("JSON en consola."); + }; return (
@@ -122,6 +139,17 @@ export default function DocEditor() { selectedTerm={selectedTerm} setSelectedTerm={setSelectedTerm} updateCurrentMf={updateCurrentMf} handleFinalSubmit={handleFinalSubmit} onBack={() => setStep(1)} + subscales={subscales} + onOpenSubscale={handleOpenSubscale} + /> + )} + + {modalTarget && ( + setModalTarget(null)} + onSave={handleSaveSubscale} + targetInfo={modalTarget} /> )}
From cced6d3923567ab3ef30b11308da0f44cd608dcf Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 10:56:46 +0100 Subject: [PATCH 38/80] =?UTF-8?q?add:=20preparar=20payload=20para=20hacer?= =?UTF-8?q?=20la=20petici=C3=B3n=20al=20endpoint=20/build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/DocEditor.jsx | 40 ++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 34a24c9..88fb4c0 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -1,7 +1,7 @@ -import { useState } from 'react'; +import React, { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; -import SubscaleModal from '../components/editor/SubscaleModal'; // <-- IMPORTAMOS EL MODAL +import SubscaleModal from '../components/editor/SubscaleModal'; import { calculateValueFunction } from '../services/docService'; export default function DocEditor() { @@ -20,9 +20,8 @@ export default function DocEditor() { const [mfDefinitions, setMfDefinitions] = useState({}); // ESTADOS: SUBESCALAS (FASE 2.5) - // Formato: { "regular": { left: { cardsCount: 3, blankCards: [1, 0] }, right: null }, "bueno": ... } const [subscales, setSubscales] = useState({}); - const [modalTarget, setModalTarget] = useState(null); // { term: 'regular', side: 'left', initialData: {...} } + const [modalTarget, setModalTarget] = useState(null); // MANEJADORES: FASE 1 const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; @@ -117,9 +116,36 @@ export default function DocEditor() { setModalTarget(null); }; - const handleFinalSubmit = () => { - console.log("PAYLOAD DOC-MF COMPLETO:", { baseScale, mfDefinitions, subscales }); - alert("JSON en consola."); + const handleFinalSubmit = async () => { + const scaleKeys = Object.keys(baseScale); + + const payload = { + criterion_name: criterionName.trim(), + levels: scaleKeys.map(term => { + const mf = mfDefinitions[term]; + const sub = subscales[term] || {}; + + return { + term: term, + core: [ + Number(mf.coreStart.toFixed(4)), + Number(mf.coreEnd.toFixed(4)) + ], + support: [ + Number(mf.supportStart.toFixed(4)), + Number(mf.supportEnd.toFixed(4)) + ], + left_blank_cards: sub.left ? sub.left.blankCards : [0], + right_blank_cards: sub.right ? sub.right.blankCards : [0], + + left_nodes_count: sub.left ? sub.left.cardsCount : 2, + right_nodes_count: sub.right ? sub.right.cardsCount : 2 + }; + }) + }; + + console.log("PAYLOAD:", JSON.stringify(payload, null, 2)); + // TODO: Llamada a la API }; return ( From e19e971cd6da50640eda9d8a8933c6428134863e Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Fri, 27 Mar 2026 11:16:44 +0100 Subject: [PATCH 39/80] =?UTF-8?q?Backend=20totalmente=20hecho=20con=20mong?= =?UTF-8?q?odb,=20a=C3=B1adida=20la=20funcionalidad=20de=20usuarios=20con?= =?UTF-8?q?=20historial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/database/__init__.py | 0 backend/api/database/connection.py | 18 ------ backend/api/database/init_db.py | 5 -- backend/api/database/models.py | 14 ----- backend/api/database/mongodb.py | 9 +++ backend/api/database/session.py | 15 ----- backend/api/main.py | 18 +++--- backend/api/models/user_models.py | 44 +++++++++++++++ backend/api/routers/auth.py | 89 ++++++++++++++++++++++++++++++ backend/api/routers/history.py | 62 +++++++++++++++++++++ backend/api/routers/test_db.py | 13 ----- backend/api/routers/test_mongo.py | 12 ++++ backend/api/utils/security.py | 16 ++++++ backend/requirements.txt | 7 ++- docker-compose.yaml | 19 ++----- 15 files changed, 252 insertions(+), 89 deletions(-) delete mode 100644 backend/api/database/__init__.py delete mode 100644 backend/api/database/connection.py delete mode 100644 backend/api/database/init_db.py delete mode 100644 backend/api/database/models.py create mode 100644 backend/api/database/mongodb.py delete mode 100644 backend/api/database/session.py create mode 100644 backend/api/models/user_models.py create mode 100644 backend/api/routers/auth.py create mode 100644 backend/api/routers/history.py delete mode 100644 backend/api/routers/test_db.py create mode 100644 backend/api/routers/test_mongo.py create mode 100644 backend/api/utils/security.py diff --git a/backend/api/database/__init__.py b/backend/api/database/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/api/database/connection.py b/backend/api/database/connection.py deleted file mode 100644 index 640fac4..0000000 --- a/backend/api/database/connection.py +++ /dev/null @@ -1,18 +0,0 @@ -import os -from sqlalchemy import create_engine - -DB_USER = "root" -DB_PASSWORD = "root" -DB_HOST = "db" -DB_PORT = "3306" -DB_NAME = "deckofcards" - -DATABASE_URL = ( - f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" -) - -engine = create_engine( - DATABASE_URL, - pool_pre_ping=True, - echo=False -) diff --git a/backend/api/database/init_db.py b/backend/api/database/init_db.py deleted file mode 100644 index 4fd4e1f..0000000 --- a/backend/api/database/init_db.py +++ /dev/null @@ -1,5 +0,0 @@ -from .connection import engine -from .models import Base - -def init_db(): - Base.metadata.create_all(bind=engine) diff --git a/backend/api/database/models.py b/backend/api/database/models.py deleted file mode 100644 index 6674aab..0000000 --- a/backend/api/database/models.py +++ /dev/null @@ -1,14 +0,0 @@ -from sqlalchemy.orm import declarative_base -from sqlalchemy import Column, Integer, String, Float - -Base = declarative_base() - -class DoCMFLevel(Base): - __tablename__ = "docmf_levels" - - id = Column(Integer, primary_key=True, index=True) - term = Column(String(50), nullable=False) - core_a = Column(Float, nullable=False) - core_b = Column(Float, nullable=False) - support_c = Column(Float, nullable=False) - support_d = Column(Float, nullable=False) diff --git a/backend/api/database/mongodb.py b/backend/api/database/mongodb.py new file mode 100644 index 0000000..fbe5848 --- /dev/null +++ b/backend/api/database/mongodb.py @@ -0,0 +1,9 @@ +from motor.motor_asyncio import AsyncIOMotorClient + +MONGO_URL = "mongodb://mongo:27017" +DB_NAME = "deckofcards" + +client = AsyncIOMotorClient(MONGO_URL) +db = client[DB_NAME] + +users_collection = db["users"] diff --git a/backend/api/database/session.py b/backend/api/database/session.py deleted file mode 100644 index 9ba837b..0000000 --- a/backend/api/database/session.py +++ /dev/null @@ -1,15 +0,0 @@ -from sqlalchemy.orm import sessionmaker -from .connection import engine - -SessionLocal = sessionmaker( - autocommit=False, - autoflush=False, - bind=engine -) - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() diff --git a/backend/api/main.py b/backend/api/main.py index 897ab2e..1edd7e5 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -1,23 +1,24 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from contextlib import asynccontextmanager - -from api.database.init_db import init_db +from api.database.mongodb import db # Routers -from api.routers.test_db import router as test_db_router +from api.routers.test_mongo import router as test_mongo_router from api.routers.value_function import router as value_router from api.routers.docmf_build import router as docmf_build_router from api.routers.docmf_evaluate import router as docmf_eval_router from api.routers.docmf_simple_validation import router as simple_validation_router from api.routers.docmf_validation import router as validation_router - +from api.routers.auth import router as auth_router +from api.routers.history import router as history_router +from api.routers.test_mongo import router as test_mongo_router @asynccontextmanager async def lifespan(app: FastAPI): - init_db() + # Aquí podrías hacer comprobaciones si quieres yield - + # No hace falta cerrar nada con Motor app = FastAPI(lifespan=lifespan) @@ -29,9 +30,12 @@ app.add_middleware( allow_headers=["*"], ) -app.include_router(test_db_router, prefix="/api") +app.include_router(test_mongo_router, prefix="/api") app.include_router(value_router, prefix="/api/criteria/doc") app.include_router(docmf_build_router, prefix="/api/criteria/doc-mf") app.include_router(docmf_eval_router, prefix="/api/criteria/doc-mf") app.include_router(simple_validation_router, prefix="/api/criteria/doc-mf") app.include_router(validation_router, prefix="/api/criteria/doc-mf") +app.include_router(test_mongo_router, prefix="/api") +app.include_router(auth_router, prefix="/api") +app.include_router(history_router, prefix="/api") \ No newline at end of file diff --git a/backend/api/models/user_models.py b/backend/api/models/user_models.py new file mode 100644 index 0000000..c611c6f --- /dev/null +++ b/backend/api/models/user_models.py @@ -0,0 +1,44 @@ +from typing import List, Optional +from pydantic import BaseModel, EmailStr, Field +from datetime import datetime + + +class FuzzyTerm(BaseModel): + term: str + core: List[float] + support: List[float] + left_nodes: List[List[float]] + right_nodes: List[List[float]] + + +class HistoryItem(BaseModel): + id: Optional[str] = Field(default=None, alias="_id") + name: str + created_at: datetime + results: List[FuzzyTerm] + + +class HistoryCreateRequest(BaseModel): + name: str + results: List[FuzzyTerm] + + +class UserCreate(BaseModel): + username: str + email: EmailStr + password: str + + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +class UserInDB(BaseModel): + id: Optional[str] = Field(default=None, alias="_id") + username: str + email: EmailStr + password_hash: str + token: Optional[str] = None + history: List[HistoryItem] = [] + diff --git a/backend/api/routers/auth.py b/backend/api/routers/auth.py new file mode 100644 index 0000000..2287379 --- /dev/null +++ b/backend/api/routers/auth.py @@ -0,0 +1,89 @@ +from fastapi import APIRouter, HTTPException, status +from api.database.mongodb import users_collection +from api.models.user_models import UserCreate, UserLogin +from api.utils.security import hash_password, verify_password, generate_token +from bson import ObjectId + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/register") +async def register_user(user: UserCreate): + existing_username = await users_collection.find_one({"username": user.username}) + if existing_username: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="El nombre de usuario ya está en uso", + ) + + existing_email = await users_collection.find_one({"email": user.email}) + if existing_email: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="El email ya está registrado", + ) + + token = generate_token() + + user_doc = { + "username": user.username, + "email": user.email, + "password_hash": hash_password(user.password), + "token": token, + "history": [], + } + + result = await users_collection.insert_one(user_doc) + + return { + "message": "Usuario registrado correctamente", + "user_id": str(result.inserted_id), + "token": token, + } + + +@router.post("/login") +async def login_user(credentials: UserLogin): + user = await users_collection.find_one({"email": credentials.email}) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Credenciales inválidas", + ) + + if not verify_password(credentials.password, user["password_hash"]): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Credenciales inválidas", + ) + + new_token = generate_token() + + await users_collection.update_one( + {"_id": user["_id"]}, + {"$set": {"token": new_token}} + ) + + return { + "message": "Login correcto", + "user_id": str(user["_id"]), + "username": user["username"], + "token": new_token, + } + + +@router.post("/logout/{user_id}") +async def logout_user(user_id: str): + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Usuario no encontrado", + ) + + await users_collection.update_one( + {"_id": ObjectId(user_id)}, + {"$set": {"token": None}} + ) + + return {"message": "Sesión cerrada correctamente"} diff --git a/backend/api/routers/history.py b/backend/api/routers/history.py new file mode 100644 index 0000000..fadd769 --- /dev/null +++ b/backend/api/routers/history.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, HTTPException, status +from typing import List +from datetime import datetime +from bson import ObjectId + +from api.database.mongodb import users_collection +from api.models.user_models import FuzzyTerm, HistoryCreateRequest + +router = APIRouter(prefix="/history", tags=["history"]) + + +@router.post("/{user_id}/add") +async def add_history_item(user_id: str, data: HistoryCreateRequest): + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Usuario no encontrado", + ) + + history_item_id = ObjectId() + + history_item = { + "_id": history_item_id, + "name": data.name, # ← nuevo campo + "created_at": datetime.utcnow(), + "results": [r.dict() for r in data.results], + } + + await users_collection.update_one( + {"_id": ObjectId(user_id)}, + {"$push": {"history": history_item}}, + ) + + return { + "message": "Elemento añadido al historial", + "history_item_id": str(history_item_id), + } + + + +@router.delete("/{user_id}/delete/{history_item_id}") +async def delete_history_item(user_id: str, history_item_id: str): + user = await users_collection.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Usuario no encontrado", + ) + + result = await users_collection.update_one( + {"_id": ObjectId(user_id)}, + {"$pull": {"history": {"_id": ObjectId(history_item_id)}}}, + ) + + if result.modified_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Elemento de historial no encontrado", + ) + + return {"message": "Elemento eliminado del historial"} diff --git a/backend/api/routers/test_db.py b/backend/api/routers/test_db.py deleted file mode 100644 index d554bfd..0000000 --- a/backend/api/routers/test_db.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import APIRouter, Depends -from sqlalchemy import text -from api.database.session import get_db - -router = APIRouter() - -@router.get("/test-db") -def test_db_connection(db=Depends(get_db)): - try: - db.execute(text("SELECT 1")) - return {"status": "ok", "message": "Conexión a MySQL correcta"} - except Exception as e: - return {"status": "error", "message": str(e)} diff --git a/backend/api/routers/test_mongo.py b/backend/api/routers/test_mongo.py new file mode 100644 index 0000000..43dd15d --- /dev/null +++ b/backend/api/routers/test_mongo.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter +from api.database.mongodb import db + +router = APIRouter() + +@router.get("/test-mongo") +async def test_mongo(): + try: + await db.command("ping") + return {"status": "ok", "message": "Conexión a MongoDB correcta"} + except Exception as e: + return {"status": "error", "message": str(e)} diff --git a/backend/api/utils/security.py b/backend/api/utils/security.py new file mode 100644 index 0000000..6dd73c5 --- /dev/null +++ b/backend/api/utils/security.py @@ -0,0 +1,16 @@ +import secrets +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def generate_token() -> str: + return secrets.token_hex(32) # 64 caracteres seguros diff --git a/backend/requirements.txt b/backend/requirements.txt index bf665b8..26ebe6e 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,7 @@ fastapi uvicorn -sqlalchemy -pymysql +motor pydantic -cryptography \ No newline at end of file +passlib +bcrypt==4.0.1 +email-validator diff --git a/docker-compose.yaml b/docker-compose.yaml index 2d3b0f5..f0fa360 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,12 +11,6 @@ services: - ./backend:/app depends_on: - db - environment: - DB_HOST: db - DB_PORT: 3306 - DB_USER: root - DB_PASSWORD: root - DB_NAME: deckofcards frontend: build: @@ -29,16 +23,13 @@ services: - /app/node_modules db: - image: mysql:8.0 - container_name: mysql_db + image: mongo:6 + container_name: mongo restart: always - environment: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: deckofcards ports: - - "3306:3306" + - "27018:27017" volumes: - - mysql_data:/var/lib/mysql + - mongo_data:/data/db volumes: - mysql_data: \ No newline at end of file + mongo_data: From 7da263732c8f61aad8ad5d5752c5648186fcbd1a Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 11:19:11 +0100 Subject: [PATCH 40/80] =?UTF-8?q?add:=20preparar=20archivos=20para=20hacer?= =?UTF-8?q?=20la=20petici=C3=B3n=20al=20nuevo=20endpoint=20"build"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/lib/{axios.js => api.js} | 4 +-- frontend/src/pages/DocEditor.jsx | 50 ++++++++++++++++++--------- frontend/src/services/docService.js | 22 +++++++++--- 3 files changed, 52 insertions(+), 24 deletions(-) rename frontend/src/lib/{axios.js => api.js} (80%) diff --git a/frontend/src/lib/axios.js b/frontend/src/lib/api.js similarity index 80% rename from frontend/src/lib/axios.js rename to frontend/src/lib/api.js index 5aa26f5..dae5d65 100644 --- a/frontend/src/lib/axios.js +++ b/frontend/src/lib/api.js @@ -1,7 +1,7 @@ import Axios from 'axios'; import { API_BASE_URL } from '../config'; -const axios = Axios.create({ +const api = Axios.create({ baseURL: API_BASE_URL, headers: { 'Accept': 'application/json', @@ -10,4 +10,4 @@ const axios = Axios.create({ }); -export default axios; \ No newline at end of file +export default api; \ No newline at end of file diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 88fb4c0..b114498 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; import SubscaleModal from '../components/editor/SubscaleModal'; -import { calculateValueFunction } from '../services/docService'; +import { calculateValueFunction, buildFuzzyGraph } from '../services/docService'; export default function DocEditor() { const [step, setStep] = useState(1); @@ -18,11 +18,12 @@ export default function DocEditor() { const [baseScale, setBaseScale] = useState({}); const [selectedTerm, setSelectedTerm] = useState(null); const [mfDefinitions, setMfDefinitions] = useState({}); - - // ESTADOS: SUBESCALAS (FASE 2.5) const [subscales, setSubscales] = useState({}); const [modalTarget, setModalTarget] = useState(null); + // ESTADO: FASE 3 + const [finalResult, setFinalResult] = useState(null); + // MANEJADORES: FASE 1 const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; @@ -100,7 +101,6 @@ export default function DocEditor() { }); }; - // MANEJADORES: SUBESCALAS const handleOpenSubscale = (term, side, initialData) => { setModalTarget({ term, side, initialData }); }; @@ -116,40 +116,45 @@ export default function DocEditor() { setModalTarget(null); }; + // Petición para el endpoint "build" const handleFinalSubmit = async () => { const scaleKeys = Object.keys(baseScale); - const payload = { criterion_name: criterionName.trim(), levels: scaleKeys.map(term => { const mf = mfDefinitions[term]; const sub = subscales[term] || {}; - return { term: term, - core: [ - Number(mf.coreStart.toFixed(4)), - Number(mf.coreEnd.toFixed(4)) - ], - support: [ - Number(mf.supportStart.toFixed(4)), - Number(mf.supportEnd.toFixed(4)) - ], + core: [ Number(mf.coreStart.toFixed(4)), Number(mf.coreEnd.toFixed(4)) ], + support: [ Number(mf.supportStart.toFixed(4)), Number(mf.supportEnd.toFixed(4)) ], left_blank_cards: sub.left ? sub.left.blankCards : [0], right_blank_cards: sub.right ? sub.right.blankCards : [0], - left_nodes_count: sub.left ? sub.left.cardsCount : 2, right_nodes_count: sub.right ? sub.right.cardsCount : 2 }; }) }; - console.log("PAYLOAD:", JSON.stringify(payload, null, 2)); - // TODO: Llamada a la API + setIsLoading(true); + try { + const result = await buildFuzzyGraph(payload); + console.log("RESPUESTA DEL BACKEND:", result); + + setFinalResult(result); + setStep(3); + + } catch (error) { + console.error(error); + alert("Error del servidor: \n" + JSON.stringify(error, null, 2)); + } finally { + setIsLoading(false); + } }; return (
+ {step === 1 && ( )} + {step === 2 && ( )} + {step === 3 && ( +
+

Paso 3: Espectro Difuso Final

+

Gráfica construida correctamente. ¡Mira la consola!

+ +
+ )} + {modalTarget && ( { try { - const response = await axios.post('/criteria/doc/value-function', payload); - return response.data; + const response = await api.post('/criteria/doc/value-function', payload); + return response.data; } catch (error) { - console.error("Error en calculateValueFunction:", error); - throw error; + console.error('Error calculating value function:', error); + throw error.response?.data?.detail || error.message; } +}; + +export const buildFuzzyGraph = async (payload) => { + try { + const response = await api.post('/criteria/doc-mf/build', payload); + return response.data; + } catch (error) { + console.error('Error building fuzzy graph:', error); + throw error.response?.data?.detail || error.message; + } + + }; \ No newline at end of file From e0e1f5381b83a90123e6d5867c502a95a152ce1c Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 12:24:17 +0100 Subject: [PATCH 41/80] =?UTF-8?q?fix:=20arreglar=20petici=C3=B3n=20para=20?= =?UTF-8?q?el=20endpoint=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/DocEditor.jsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index b114498..01bf56b 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -119,19 +119,30 @@ export default function DocEditor() { // Petición para el endpoint "build" const handleFinalSubmit = async () => { const scaleKeys = Object.keys(baseScale); + const payload = { - criterion_name: criterionName.trim(), levels: scaleKeys.map(term => { const mf = mfDefinitions[term]; const sub = subscales[term] || {}; + + const leftCount = sub.left ? sub.left.cardsCount : 2; + const left_nodes_x = Array.from({ length: leftCount }).map((_, i) => + Number((mf.supportStart + (mf.coreStart - mf.supportStart) * (i / (leftCount - 1))).toFixed(4)) + ); + + const rightCount = sub.right ? sub.right.cardsCount : 2; + const right_nodes_x = Array.from({ length: rightCount }).map((_, i) => + Number((mf.coreEnd + (mf.supportEnd - mf.coreEnd) * (i / (rightCount - 1))).toFixed(4)) + ); + return { term: term, core: [ Number(mf.coreStart.toFixed(4)), Number(mf.coreEnd.toFixed(4)) ], support: [ Number(mf.supportStart.toFixed(4)), Number(mf.supportEnd.toFixed(4)) ], + left_nodes_x: left_nodes_x, left_blank_cards: sub.left ? sub.left.blankCards : [0], - right_blank_cards: sub.right ? sub.right.blankCards : [0], - left_nodes_count: sub.left ? sub.left.cardsCount : 2, - right_nodes_count: sub.right ? sub.right.cardsCount : 2 + right_nodes_x: right_nodes_x, + right_blank_cards: sub.right ? sub.right.blankCards : [0] }; }) }; From 111acc632e52a6f1253a92295674ba3e325a02a9 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Fri, 27 Mar 2026 12:46:45 +0100 Subject: [PATCH 42/80] Arreglos de seguridad e historial --- backend/api/models/docmf_models.py | 2 +- backend/api/routers/auth.py | 27 ++++++++++++++++++++ backend/api/routers/history.py | 40 +++++++++++++----------------- backend/api/utils/security.py | 24 ++++++++++++++++++ 4 files changed, 69 insertions(+), 24 deletions(-) diff --git a/backend/api/models/docmf_models.py b/backend/api/models/docmf_models.py index 0cf9342..f96e2e9 100644 --- a/backend/api/models/docmf_models.py +++ b/backend/api/models/docmf_models.py @@ -32,7 +32,7 @@ class DoCMFRequest(BaseModel): core = info.data.get("core") if core: a, b = core - if not (c <= a < b <= d): + if not (c <= a <= b <= d): raise ValueError("El núcleo debe estar dentro del soporte.") return v diff --git a/backend/api/routers/auth.py b/backend/api/routers/auth.py index 2287379..e0d6c68 100644 --- a/backend/api/routers/auth.py +++ b/backend/api/routers/auth.py @@ -3,6 +3,13 @@ from api.database.mongodb import users_collection from api.models.user_models import UserCreate, UserLogin from api.utils.security import hash_password, verify_password, generate_token from bson import ObjectId +from fastapi import APIRouter, HTTPException, status, Depends +from api.utils.security import ( + hash_password, + verify_password, + generate_token, + get_current_user, +) router = APIRouter(prefix="/auth", tags=["auth"]) @@ -87,3 +94,23 @@ async def logout_user(user_id: str): ) return {"message": "Sesión cerrada correctamente"} + + +@router.post("/logout") +async def logout_user(current_user: dict = Depends(get_current_user)): + user_id = current_user["_id"] + + await users_collection.update_one( + {"_id": user_id}, + {"$set": {"token": None}}, + ) + + return {"message": "Sesión cerrada correctamente"} + +@router.get("/me") +async def get_me(current_user: dict = Depends(get_current_user)): + return { + "user_id": str(current_user["_id"]), + "username": current_user["username"], + "email": current_user["email"], + } diff --git a/backend/api/routers/history.py b/backend/api/routers/history.py index fadd769..43a260a 100644 --- a/backend/api/routers/history.py +++ b/backend/api/routers/history.py @@ -1,34 +1,31 @@ -from fastapi import APIRouter, HTTPException, status -from typing import List +from fastapi import APIRouter, HTTPException, status, Depends from datetime import datetime from bson import ObjectId from api.database.mongodb import users_collection from api.models.user_models import FuzzyTerm, HistoryCreateRequest +from api.utils.security import get_current_user router = APIRouter(prefix="/history", tags=["history"]) -@router.post("/{user_id}/add") -async def add_history_item(user_id: str, data: HistoryCreateRequest): - user = await users_collection.find_one({"_id": ObjectId(user_id)}) - if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Usuario no encontrado", - ) +@router.post("/add") +async def add_history_item( + data: HistoryCreateRequest, + current_user: dict = Depends(get_current_user), +): + user_id = current_user["_id"] history_item_id = ObjectId() - history_item = { "_id": history_item_id, - "name": data.name, # ← nuevo campo + "name": data.name, "created_at": datetime.utcnow(), "results": [r.dict() for r in data.results], } await users_collection.update_one( - {"_id": ObjectId(user_id)}, + {"_id": user_id}, {"$push": {"history": history_item}}, ) @@ -38,18 +35,15 @@ async def add_history_item(user_id: str, data: HistoryCreateRequest): } - -@router.delete("/{user_id}/delete/{history_item_id}") -async def delete_history_item(user_id: str, history_item_id: str): - user = await users_collection.find_one({"_id": ObjectId(user_id)}) - if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Usuario no encontrado", - ) +@router.delete("/delete/{history_item_id}") +async def delete_history_item( + history_item_id: str, + current_user: dict = Depends(get_current_user), +): + user_id = current_user["_id"] result = await users_collection.update_one( - {"_id": ObjectId(user_id)}, + {"_id": user_id}, {"$pull": {"history": {"_id": ObjectId(history_item_id)}}}, ) diff --git a/backend/api/utils/security.py b/backend/api/utils/security.py index 6dd73c5..5826361 100644 --- a/backend/api/utils/security.py +++ b/backend/api/utils/security.py @@ -1,5 +1,9 @@ import secrets from passlib.context import CryptContext +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from api.database.mongodb import users_collection +from bson import ObjectId pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") @@ -14,3 +18,23 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: def generate_token() -> str: return secrets.token_hex(32) # 64 caracteres seguros + + +security_scheme = HTTPBearer() + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security_scheme), +): + token = credentials.credentials + + user = await users_collection.find_one({"token": token}) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token inválido o usuario no autenticado", + ) + + # devolvemos el documento tal cual (dict) + user["id"] = str(user["_id"]) + return user \ No newline at end of file From 89ebf99c7fb900bc3987749cc13a9d13dc484e4d Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 13:16:43 +0100 Subject: [PATCH 43/80] =?UTF-8?q?add:=20a=C3=B1adida=20gr=C3=A1fica=20fina?= =?UTF-8?q?l.=20reforzada=20la=20seguridad=20en=20el=20payload=20para=20la?= =?UTF-8?q?=20petici=C3=B3n=20a=20"build"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/editor/Step3FinalGraph.jsx | 87 +++++++++++++++++++ frontend/src/config.js | 7 +- frontend/src/pages/DocEditor.jsx | 34 +++++--- 3 files changed, 115 insertions(+), 13 deletions(-) create mode 100644 frontend/src/components/editor/Step3FinalGraph.jsx diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx new file mode 100644 index 0000000..0173abf --- /dev/null +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -0,0 +1,87 @@ +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer +} from 'recharts'; +import { CHART_COLORS } from '../../config'; + +const Step3FinalGraph = ({ data }) => { + if (!data || !data.results) return

Cargando gráfico final...

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

Espectro Difuso Final

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

Paso 3: Espectro Difuso Final

-

Gráfica construida correctamente. ¡Mira la consola!

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

Cargando gráfico final...

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

Cargando gráfico final...

; + } return ( -
-

Espectro Difuso Final

+
+

Espectro Difuso Final

- - - - - - - - [value.toFixed(3), name]} - labelFormatter={(label) => `X: ${Number(label).toFixed(3)}`} - contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} - /> - - ({ - id: item.term, - type: 'circle', - value: item.term.toUpperCase(), - color: CHART_COLORS[item.originalIndex % CHART_COLORS.length] - }))} - /> + {/* Gráfica */} +
+ + + + + + [Number(value).toFixed(3), name]} + labelFormatter={(label) => `X: ${Number(label).toFixed(3)}`} + contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} + /> - {sortedResults.map((item) => { - const lineData = [...item.left_nodes, ...item.right_nodes].map(node => ({ - x: node[0], - y: node[1] - })); + {sortedResults.map((item) => { + const lineData = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(node => ({ + x: Number(node[0]), + y: Number(node[1]) + })); + + return ( + + ); + })} + + +
+ + {/* Leyenda */} +
+ {sortedResults.map((item) => ( +
+ + + + + {item.term} + +
+ ))} +
- return ( - - ); - })} -
-
); }; From 07f1fd9ebc8a9c75246d83b2d874e57cef96252b Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 13:55:55 +0100 Subject: [PATCH 45/80] fix: cambio minimo --- .../src/components/editor/Step3FinalGraph.jsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx index 35d728f..45c9d27 100644 --- a/frontend/src/components/editor/Step3FinalGraph.jsx +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -79,20 +79,20 @@ const Step3FinalGraph = ({ data }) => { {/* Leyenda */}
{sortedResults.map((item) => ( -
- - +
+ + - - {item.term} - -
+ + {item.term} + +
))}
From e96af43990baeff60d86c6b3246833526dc397f2 Mon Sep 17 00:00:00 2001 From: Alexis Date: Fri, 27 Mar 2026 14:27:50 +0100 Subject: [PATCH 46/80] add: authContext implementado --- frontend/src/context/AuthContext.js | 7 ++++++ frontend/src/context/AuthProvider.jsx | 32 +++++++++++++++++++++++++++ frontend/src/main.jsx | 7 ++++-- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 frontend/src/context/AuthContext.js create mode 100644 frontend/src/context/AuthProvider.jsx diff --git a/frontend/src/context/AuthContext.js b/frontend/src/context/AuthContext.js new file mode 100644 index 0000000..ac7e88c --- /dev/null +++ b/frontend/src/context/AuthContext.js @@ -0,0 +1,7 @@ +import { createContext, useContext } from 'react'; + +export const AuthContext = createContext(); + +export const useAuth = () => { + return useContext(AuthContext); +}; \ No newline at end of file diff --git a/frontend/src/context/AuthProvider.jsx b/frontend/src/context/AuthProvider.jsx new file mode 100644 index 0000000..5180fb6 --- /dev/null +++ b/frontend/src/context/AuthProvider.jsx @@ -0,0 +1,32 @@ +import { useState } from 'react'; +import { AuthContext } from './AuthContext'; + +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(() => { + const storedUser = localStorage.getItem('user'); + return storedUser ? JSON.parse(storedUser) : null; + }); + + const login = (userData, token) => { + setUser(userData); + localStorage.setItem('user', JSON.stringify(userData)); + localStorage.setItem('token', token); + }; + + const logout = () => { + setUser(null); + localStorage.removeItem('user'); + localStorage.removeItem('token'); + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index b9a1a6d..33c21e7 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -2,9 +2,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.jsx' +import { AuthProvider } from './context/AuthProvider.jsx' createRoot(document.getElementById('root')).render( - + + + , -) +) \ No newline at end of file From 22ed6c107e7f0cef9077410fb8502dc33af8c4cc Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 6 Apr 2026 10:20:27 +0200 Subject: [PATCH 47/80] add: implementar login/registro --- frontend/src/components/layout/MainLayout.jsx | 57 +++-- frontend/src/lib/api.js | 7 + frontend/src/pages/AdvancedMode.jsx | 236 ------------------ frontend/src/pages/BasicMode.jsx | 156 ------------ frontend/src/pages/Login.jsx | 88 +++++++ frontend/src/pages/Register.jsx | 102 ++++++++ frontend/src/routers/AppRouter.jsx | 4 + frontend/src/services/authService.js | 13 + 8 files changed, 257 insertions(+), 406 deletions(-) delete mode 100644 frontend/src/pages/AdvancedMode.jsx delete mode 100644 frontend/src/pages/BasicMode.jsx create mode 100644 frontend/src/pages/Login.jsx create mode 100644 frontend/src/pages/Register.jsx create mode 100644 frontend/src/services/authService.js diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index c8bc184..cd5bf25 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -1,26 +1,55 @@ -import { Outlet } from 'react-router-dom'; +import { Outlet, Link } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; export default function MainLayout() { + const { user, isAuthenticated, logout } = useAuth(); + return ( -
- - {/* Cabecera */} -
-
-
- DoC +
+ +
+
+ + +
+ DoC +
+ + Deck of Cards + + + +
+ {isAuthenticated ? ( +
+
{ + if(window.confirm('¿Deseas cerrar sesión?')) { + logout(); + } + }} + > + {user?.username?.charAt(0).toUpperCase() || 'U'} +
+
+ ) : ( + + Iniciar Sesión + + )}
-

- Deck of Cards -

+
- {/* Contenido principal */} -
+
-
); } \ No newline at end of file diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index dae5d65..787616f 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -9,5 +9,12 @@ const api = Axios.create({ } }); +api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); export default api; \ No newline at end of file diff --git a/frontend/src/pages/AdvancedMode.jsx b/frontend/src/pages/AdvancedMode.jsx deleted file mode 100644 index 88a5a83..0000000 --- a/frontend/src/pages/AdvancedMode.jsx +++ /dev/null @@ -1,236 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import CriterionInput from '../components/CriterionInput'; -import CardEditor from '../components/CardEditor'; -import BlankCardsCounter from '../components/BlankCardsCounter'; -import AddLevelButton from '../components/AddLevelButton'; -import Chart from '../components/membershipFunction/Chart'; -import Controls from '../components/membershipFunction/Controls'; -import { calculateValueFunction } from '../services/docService'; - -const COLORS = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#d946ef', '#06b6d4', '#8b5cf6', '#f43f5e', '#6366f1']; - -export default function AdvancedMode() { - const [step, setStep] = useState(1); - const [isLoading, setIsLoading] = useState(false); - - const [criterionName, setCriterionName] = useState(''); - const [levels, setLevels] = useState(['', '', '']); - const [blankCards, setBlankCards] = useState([0, 0]); - const [errors, setErrors] = useState({ criterion: false, levels: [] }); - - const [isZoomActive, setIsZoomActive] = useState(true); - - const containerRef = useRef(null); - const tableRef = useRef(null); - const [dimensions, setDimensions] = useState({ container: 1000, table: 0 }); - - useEffect(() => { - const updateMeasurements = () => { - if (containerRef.current && tableRef.current) { - setDimensions({ - container: containerRef.current.offsetWidth, - table: tableRef.current.scrollWidth - }); - } - }; - const timeoutId = setTimeout(updateMeasurements, 50); - window.addEventListener('resize', updateMeasurements); - return () => { - clearTimeout(timeoutId); - window.removeEventListener('resize', updateMeasurements); - }; - }, [levels, blankCards, step]); - - // Estados Fase 2 (Franjas) - const [baseScale, setBaseScale] = useState({}); - const [selectedTerm, setSelectedTerm] = useState(null); - const [mfDefinitions, setMfDefinitions] = useState({}); - - // Manejadores de Escala - const handleCriterionChange = (val) => { setCriterionName(val); if (errors.criterion) setErrors({ ...errors, criterion: false }); }; - const handleLevelChange = (index, newValue) => { const newLevels = [...levels]; newLevels[index] = newValue; setLevels(newLevels); if (errors.levels[index]) setErrors({ ...errors, levels: errors.levels.map((e, i) => i === index ? false : e) }); }; - const handleAddLevel = () => { setLevels([...levels, '']); setBlankCards([...blankCards, 0]); setErrors({ ...errors, levels: [...errors.levels, false] }); }; - const handleRemoveLevel = (indexToRemove) => { if (levels.length <= 3) return; setLevels(levels.filter((_, i) => i !== indexToRemove)); setBlankCards(blankCards.filter((_, i) => i !== (indexToRemove === 0 ? 0 : indexToRemove - 1))); setErrors({ ...errors, levels: errors.levels.filter((_, i) => i !== indexToRemove) }); }; - const handleBlankCardChange = (index, delta) => { const newCards = [...blankCards]; if (newCards[index] + delta >= 0) { newCards[index] += delta; setBlankCards(newCards); } }; - - const handleGenerateBaseScale = async () => { - const newErrors = { criterion: !criterionName.trim(), levels: levels.map(l => !l.trim()) }; - if (newErrors.criterion || newErrors.levels.includes(true)) { - setErrors(newErrors); - return alert("Por favor, rellena todos los campos."); - } - - setIsLoading(true); - try { - const payloadBase = { criterion_name: criterionName.trim(), levels: levels.map(l => l.trim()), blank_cards: blankCards, references: { "0": 0, [(levels.length - 1).toString()]: 1 } }; - const baseResult = await calculateValueFunction(payloadBase); - - setBaseScale(baseResult.values); - const initialMfs = {}; - Object.entries(baseResult.values).forEach(([name, value]) => { initialMfs[name] = { supportStart: value, coreStart: value, coreEnd: value, supportEnd: value }; }); - - setMfDefinitions(initialMfs); - setSelectedTerm(Object.keys(baseResult.values)[0]); - setStep(2); - } catch (error) { alert("Error: " + error); } finally { setIsLoading(false); } - }; - - const updateCurrentMf = (field, value) => { - if (!selectedTerm) return; - let numValue = parseFloat(value); - - setMfDefinitions(prev => { - const scaleKeys = Object.keys(baseScale); - const selectedIndex = scaleKeys.indexOf(selectedTerm); - let prevCoreEnd = 0, prevSupportEnd = 0, nextCoreStart = 1, nextSupportStart = 1; - - if (selectedIndex > 0) { - prevCoreEnd = prev[scaleKeys[selectedIndex - 1]].coreEnd; - prevSupportEnd = prev[scaleKeys[selectedIndex - 1]].supportEnd; - } - if (selectedIndex < scaleKeys.length - 1) { - nextCoreStart = prev[scaleKeys[selectedIndex + 1]].coreStart; - nextSupportStart = prev[scaleKeys[selectedIndex + 1]].supportStart; - } - - const anchor = baseScale[selectedTerm]; - - if (field === 'supportStart' && numValue < prevCoreEnd) numValue = prevCoreEnd; - if (field === 'coreStart' && numValue < prevSupportEnd) numValue = prevSupportEnd; - if (field === 'coreEnd' && numValue > nextSupportStart) numValue = nextSupportStart; - if (field === 'supportEnd' && numValue > nextCoreStart) numValue = nextCoreStart; - - if ((field === 'supportStart' || field === 'coreStart') && numValue > anchor) numValue = anchor; - if ((field === 'supportEnd' || field === 'coreEnd') && numValue < anchor) numValue = anchor; - - const current = { ...prev[selectedTerm], [field]: numValue }; - - if (field === 'supportStart') { - if (current.supportStart > current.coreStart) current.coreStart = current.supportStart; - if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - } else if (field === 'coreStart') { - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - if (current.coreStart > current.coreEnd) current.coreEnd = current.coreStart; - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - } else if (field === 'coreEnd') { - if (current.coreEnd > current.supportEnd) current.supportEnd = current.coreEnd; - if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - } else if (field === 'supportEnd') { - if (current.supportEnd < current.coreEnd) current.coreEnd = current.supportEnd; - if (current.coreEnd < current.coreStart) current.coreStart = current.coreEnd; - if (current.coreStart < current.supportStart) current.supportStart = current.coreStart; - } - - return { ...prev, [selectedTerm]: current }; - }); - }; - - const handleFinalSubmit = () => { - console.log("PAYLOAD DOC-MF:", { base_scale: baseScale, membership_functions: mfDefinitions }); - alert("¡Mira la consola! JSON preparado."); - }; - - const scaleKeys = Object.keys(baseScale); - const selectedColor = COLORS[scaleKeys.indexOf(selectedTerm) % COLORS.length] || '#2563eb'; - - const needsZoom = dimensions.table > dimensions.container; - const dynamicScale = needsZoom ? (dimensions.container / dimensions.table) * 0.95 : 1; - const currentScale = isZoomActive && needsZoom ? dynamicScale : 1; - - return ( -
- - {/* PASO 1 */} - {step === 1 && ( -
- -
-

- Paso 1: Establecer escala -

- {needsZoom && ( - - )} -
- - - -
-
- -
- - {levels.map((level, index) => ( - -
- 3} /> -
- {index < levels.length - 1 && ( - - )} -
- ))} - -
-
-
- - -
- -
-
- -
- -
-
- )} - - {/* PASO 2 */} - {step === 2 && ( -
-
-

Paso 2: Modelar Conceptos Difusos

- -
- -
- {scaleKeys.map((name, index) => { - const color = COLORS[index % COLORS.length]; - const isSelected = selectedTerm === name; - return ( - - ); - })} -
- - - - - -
- -
-
- )} -
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/BasicMode.jsx b/frontend/src/pages/BasicMode.jsx deleted file mode 100644 index 3d913e6..0000000 --- a/frontend/src/pages/BasicMode.jsx +++ /dev/null @@ -1,156 +0,0 @@ -import { useState } from 'react'; -import CriterionInput from '../components/CriterionInput'; -import CardEditor from '../components/CardEditor'; -import BlankCardsCounter from '../components/BlankCardsCounter'; -import AddLevelButton from '../components/AddLevelButton'; -import ValueFunctionChart from '../components/ValueFunctionChart'; -import { calculateValueFunction } from '../services/docService'; - -export default function BasicMode() { - const [criterionName, setCriterionName] = useState(''); - const [levels, setLevels] = useState(['', '', '']); - const [blankCards, setBlankCards] = useState([0, 0]); - - const [isLoading, setIsLoading] = useState(false); - const [result, setResult] = useState(null); - - const [errors, setErrors] = useState({ criterion: false, levels: [] }); - - const handleCalculate = async () => { - - let hasError = false; - const newErrors = { criterion: false, levels: Array(levels.length).fill(false) }; - - if (!criterionName.trim()) { - newErrors.criterion = true; - hasError = true; - } - - levels.forEach((level, idx) => { - if (!level.trim()) { - newErrors.levels[idx] = true; - hasError = true; - } - }); - - setErrors(newErrors); - - if (hasError) return; - - setIsLoading(true); - setResult(null); - - const payload = { - criterion_name: criterionName.trim(), - levels: levels.map(l => l.trim()), - blank_cards: blankCards, - references: { "0": 0, [(levels.length - 1).toString()]: 1 } - }; - - try { - const data = await calculateValueFunction(payload); - setResult(data); - } catch (error) { - alert("No se ha podido conectar con el backend: " + error); - } finally { - setIsLoading(false); - } - }; - - const handleCriterionChange = (val) => { - setCriterionName(val); - if (errors.criterion) setErrors({ ...errors, criterion: false }); - }; - - const handleLevelChange = (index, newValue) => { - const newLevels = [...levels]; - newLevels[index] = newValue; - setLevels(newLevels); - - if (errors.levels[index]) { - const newErrLevels = [...errors.levels]; - newErrLevels[index] = false; - setErrors({ ...errors, levels: newErrLevels }); - } - }; - - const handleAddLevel = () => { - setLevels([...levels, '']); - setBlankCards([...blankCards, 0]); - setErrors({ ...errors, levels: [...errors.levels, false] }); - }; - - const handleRemoveLevel = (indexToRemove) => { - if (levels.length <= 3) return; - const newLevels = levels.filter((_, index) => index !== indexToRemove); - const blankIndexToRemove = indexToRemove === 0 ? 0 : indexToRemove - 1; - const newBlankCards = blankCards.filter((_, index) => index !== blankIndexToRemove); - - const newErrLevels = errors.levels.filter((_, index) => index !== indexToRemove); - - setLevels(newLevels); - setBlankCards(newBlankCards); - setErrors({ ...errors, levels: newErrLevels }); - }; - - const handleBlankCardChange = (index, delta) => { - const newBlankCards = [...blankCards]; - const newValue = newBlankCards[index] + delta; - if (newValue >= 0) { - newBlankCards[index] = newValue; - setBlankCards(newBlankCards); - } - }; - - return ( -
- - - -
- {levels.map((level, index) => ( -
- - - - {index < levels.length - 1 && ( - - )} -
- ))} - - -
- -
- -
- - - -
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx new file mode 100644 index 0000000..e6a48ff --- /dev/null +++ b/frontend/src/pages/Login.jsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { authService } from '../services/authService'; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const navigate = useNavigate(); + const { login } = useAuth(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + + try { + const data = await authService.login(email, password); + + const userData = { + id: data.user_id, + username: data.username, + email: email + }; + + login(userData, data.token); + navigate('/'); + } catch (err) { + setError(err.response?.data?.detail || 'Error al iniciar sesión. Revisa tus credenciales.'); + } + }; + + return ( +
+
+
+

Bienvenido

+

Inicia sesión para guardar tus espectros difusos

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + placeholder="tu@email.com" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + /> +
+ + +
+ +

+ ¿No tienes cuenta? Regístrate aquí +

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/Register.jsx b/frontend/src/pages/Register.jsx new file mode 100644 index 0000000..3055c9d --- /dev/null +++ b/frontend/src/pages/Register.jsx @@ -0,0 +1,102 @@ +import { useState } from 'react'; +import { useNavigate, Link } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { authService } from '../services/authService'; + +export default function Register() { + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const navigate = useNavigate(); + const { login } = useAuth(); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(''); + + try { + const data = await authService.register(username, email, password); + + const userData = { + id: data.user_id, + username: username, + email: email + }; + + login(userData, data.token); + navigate('/'); + } catch (err) { + setError(err.response?.data?.detail || 'Error al registrar el usuario.'); + } + }; + + return ( +
+
+
+

Crear Cuenta

+

Únete para guardar tu progreso

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setUsername(e.target.value)} + placeholder="Ej: alexis99" + /> +
+ +
+ + setEmail(e.target.value)} + placeholder="tu@email.com" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + /> +
+ + +
+ +

+ ¿Ya tienes cuenta? Inicia sesión aquí +

+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx index 5cc0534..679ca28 100644 --- a/frontend/src/routers/AppRouter.jsx +++ b/frontend/src/routers/AppRouter.jsx @@ -1,6 +1,8 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import MainLayout from '../components/layout/MainLayout'; import DocEditor from '../pages/DocEditor'; +import Login from '../pages/Login'; +import Register from '../pages/Register'; export function AppRouter() { return ( @@ -8,6 +10,8 @@ export function AppRouter() { }> } /> + } /> + } /> } /> diff --git a/frontend/src/services/authService.js b/frontend/src/services/authService.js new file mode 100644 index 0000000..be72b64 --- /dev/null +++ b/frontend/src/services/authService.js @@ -0,0 +1,13 @@ +import api from '../lib/api'; + +export const authService = { + login: async (email, password) => { + const response = await api.post('/auth/login', { email, password }); + return response.data; + }, + + register: async (username, email, password) => { + const response = await api.post('/auth/register', { username, email, password }); + return response.data; + } +}; \ No newline at end of file From 5d3de4e27f248a5eb54187b3e6e83241a4ccac95 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Mon, 6 Apr 2026 10:42:42 +0200 Subject: [PATCH 48/80] =?UTF-8?q?A=C3=B1adida=20funcionalidad=20de=20inter?= =?UTF-8?q?valos=20en=20cartas=20blancas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/main.py | 4 +- backend/api/models/docit2mf_models.py | 69 +++++++++++++++++++ backend/api/models/docmf_models.py | 1 + backend/api/models/user_models.py | 25 +++++-- backend/api/routers/docit2mf_build.py | 24 +++++++ backend/api/routers/history.py | 5 +- .../api/services/docit2mf_build_service.py | 61 ++++++++++++++++ backend/api/services/docmf_build_service.py | 28 +++++++- 8 files changed, 207 insertions(+), 10 deletions(-) create mode 100644 backend/api/models/docit2mf_models.py create mode 100644 backend/api/routers/docit2mf_build.py create mode 100644 backend/api/services/docit2mf_build_service.py diff --git a/backend/api/main.py b/backend/api/main.py index 1edd7e5..d4c740d 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -13,6 +13,7 @@ from api.routers.docmf_validation import router as validation_router from api.routers.auth import router as auth_router from api.routers.history import router as history_router from api.routers.test_mongo import router as test_mongo_router +from api.routers.docit2mf_build import router as docit2mf_router @asynccontextmanager async def lifespan(app: FastAPI): @@ -38,4 +39,5 @@ app.include_router(simple_validation_router, prefix="/api/criteria/doc-mf") app.include_router(validation_router, prefix="/api/criteria/doc-mf") app.include_router(test_mongo_router, prefix="/api") app.include_router(auth_router, prefix="/api") -app.include_router(history_router, prefix="/api") \ No newline at end of file +app.include_router(history_router, prefix="/api") +app.include_router(docit2mf_router, prefix="/api") diff --git a/backend/api/models/docit2mf_models.py b/backend/api/models/docit2mf_models.py new file mode 100644 index 0000000..d8069cb --- /dev/null +++ b/backend/api/models/docit2mf_models.py @@ -0,0 +1,69 @@ +# models/docit2mf_models.py + +from pydantic import BaseModel, field_validator +from typing import List, Tuple, Union + + +BlankCardInput = Union[int, Tuple[int, int], List[int]] + + +class DoCIT2MFRequest(BaseModel): + term: str + core: Tuple[float, float] + support: Tuple[float, float] + + left_nodes_x: List[float] + left_blank_cards: List[BlankCardInput] + + right_nodes_x: List[float] + right_blank_cards: List[BlankCardInput] + + @field_validator("term") + def term_not_empty(cls, v): + if not v.strip(): + raise ValueError("El término no puede estar vacío.") + return v + + @field_validator("core") + def core_valid(cls, v): + a, b = v + if a > b: + raise ValueError("El núcleo debe cumplir a <= b.") + return v + + @field_validator("support") + def support_valid(cls, v, info): + c, d = v + if c >= d: + raise ValueError("El soporte debe cumplir c < d.") + + core = info.data.get("core") + if core: + a, b = core + if not (c <= a <= b <= d): + raise ValueError("El núcleo debe estar dentro del soporte.") + return v + + @field_validator("left_blank_cards", "right_blank_cards") + def validate_cards(cls, v): + for item in v: + # Caso 1: entero + if isinstance(item, int): + if item < 0: + raise ValueError("Las cartas no pueden ser negativas.") + # Caso 2: lista o tupla [min,max] + elif isinstance(item, (list, tuple)): + if len(item) != 2: + raise ValueError("Los intervalos deben ser [min, max].") + lo, hi = item + if lo < 0 or hi < 0: + raise ValueError("Las cartas no pueden ser negativas.") + if lo > hi: + raise ValueError("Debe cumplirse min <= max.") + else: + raise ValueError("Formato inválido para cartas blancas.") + return v + + +class DoCIT2MFMultiRequest(BaseModel): + levels: List[DoCIT2MFRequest] diff --git a/backend/api/models/docmf_models.py b/backend/api/models/docmf_models.py index f96e2e9..8239d45 100644 --- a/backend/api/models/docmf_models.py +++ b/backend/api/models/docmf_models.py @@ -45,3 +45,4 @@ class DoCMFRequest(BaseModel): class DoCMFMultiRequest(BaseModel): levels: List[DoCMFRequest] + diff --git a/backend/api/models/user_models.py b/backend/api/models/user_models.py index c611c6f..4b20fc0 100644 --- a/backend/api/models/user_models.py +++ b/backend/api/models/user_models.py @@ -1,8 +1,12 @@ -from typing import List, Optional +from typing import List, Optional, Union from pydantic import BaseModel, EmailStr, Field from datetime import datetime +# ----------------------------- +# MODELOS DE FUNCIONES DIFUSAS +# ----------------------------- + class FuzzyTerm(BaseModel): term: str core: List[float] @@ -11,18 +15,32 @@ class FuzzyTerm(BaseModel): right_nodes: List[List[float]] +class IT2FuzzyTerm(BaseModel): + term: str + lower: FuzzyTerm + upper: FuzzyTerm + + +# ----------------------------- +# HISTORIAL +# ----------------------------- + class HistoryItem(BaseModel): id: Optional[str] = Field(default=None, alias="_id") name: str created_at: datetime - results: List[FuzzyTerm] + results: List[Union[FuzzyTerm, IT2FuzzyTerm]] class HistoryCreateRequest(BaseModel): name: str - results: List[FuzzyTerm] + results: List[Union[FuzzyTerm, IT2FuzzyTerm]] +# ----------------------------- +# USUARIOS +# ----------------------------- + class UserCreate(BaseModel): username: str email: EmailStr @@ -41,4 +59,3 @@ class UserInDB(BaseModel): password_hash: str token: Optional[str] = None history: List[HistoryItem] = [] - diff --git a/backend/api/routers/docit2mf_build.py b/backend/api/routers/docit2mf_build.py new file mode 100644 index 0000000..7b132b4 --- /dev/null +++ b/backend/api/routers/docit2mf_build.py @@ -0,0 +1,24 @@ +# routers/docit2mf_build.py + +from fastapi import APIRouter, Depends, HTTPException +from api.models.docit2mf_models import DoCIT2MFMultiRequest +from api.services.docit2mf_build_service import build_it2mf_from_level +from api.utils.security import get_current_user + +router = APIRouter(prefix="/criteria", tags=["criteria"]) + + +@router.post("/doc-it2mf/build") +async def build_doc_it2mf( + request: DoCIT2MFMultiRequest, + current_user: dict = Depends(get_current_user) +): + results = [] + + try: + for level in request.levels: + results.append(build_it2mf_from_level(level)) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + + return {"levels": results} diff --git a/backend/api/routers/history.py b/backend/api/routers/history.py index 43a260a..0110241 100644 --- a/backend/api/routers/history.py +++ b/backend/api/routers/history.py @@ -3,7 +3,7 @@ from datetime import datetime from bson import ObjectId from api.database.mongodb import users_collection -from api.models.user_models import FuzzyTerm, HistoryCreateRequest +from api.models.user_models import FuzzyTerm, IT2FuzzyTerm, HistoryCreateRequest from api.utils.security import get_current_user router = APIRouter(prefix="/history", tags=["history"]) @@ -21,7 +21,7 @@ async def add_history_item( "_id": history_item_id, "name": data.name, "created_at": datetime.utcnow(), - "results": [r.dict() for r in data.results], + "results": [r.dict() for r in data.results], # ahora soporta IT2MF } await users_collection.update_one( @@ -35,6 +35,7 @@ async def add_history_item( } + @router.delete("/delete/{history_item_id}") async def delete_history_item( history_item_id: str, diff --git a/backend/api/services/docit2mf_build_service.py b/backend/api/services/docit2mf_build_service.py new file mode 100644 index 0000000..a18d5d8 --- /dev/null +++ b/backend/api/services/docit2mf_build_service.py @@ -0,0 +1,61 @@ +# services/docit2mf_build_service.py + +from typing import List, Union +from api.models.docit2mf_models import DoCIT2MFRequest +from api.models.docmf_models import DoCMFRequest +from api.services.docmf_build_service import build_doc_mf_level + + +def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> List[int]: + """ + Devuelve una lista de enteros: + - Si el valor es un entero → se usa tal cual para LMF y UMF + - Si es un intervalo [min,max] → se usa min o max según mode + """ + result = [] + for item in values: + if isinstance(item, int): + # valor fijo → mismo para LMF y UMF + result.append(item) + else: + lo, hi = item + result.append(lo if mode == "min" else hi) + return result + + +def build_it2mf_from_level(level: DoCIT2MFRequest): + # LMF (mínimos) + left_min = _extract_bounds(level.left_blank_cards, "min") + right_min = _extract_bounds(level.right_blank_cards, "min") + + lower_level = DoCMFRequest( + term=level.term, + core=level.core, + support=level.support, + left_nodes_x=level.left_nodes_x, + left_blank_cards=left_min, + right_nodes_x=level.right_nodes_x, + right_blank_cards=right_min, + ) + lower = build_doc_mf_level(lower_level) + + # UMF (máximos) + left_max = _extract_bounds(level.left_blank_cards, "max") + right_max = _extract_bounds(level.right_blank_cards, "max") + + upper_level = DoCMFRequest( + term=level.term, + core=level.core, + support=level.support, + left_nodes_x=level.left_nodes_x, + left_blank_cards=left_max, + right_nodes_x=level.right_nodes_x, + right_blank_cards=right_max, + ) + upper = build_doc_mf_level(upper_level) + + return { + "term": level.term, + "lower": lower, + "upper": upper + } diff --git a/backend/api/services/docmf_build_service.py b/backend/api/services/docmf_build_service.py index 7c88660..0d4c406 100644 --- a/backend/api/services/docmf_build_service.py +++ b/backend/api/services/docmf_build_service.py @@ -1,4 +1,10 @@ -def build_single_docmf(request): +# services/docmf_build_service.py + +from api.models.docmf_models import DoCMFRequest +from api.models.user_models import FuzzyTerm + + +def build_single_docmf(request: DoCMFRequest): a, b = request.core c, d = request.support @@ -12,7 +18,7 @@ def build_single_docmf(request): if i == 0: left_nodes.append((x, 0.0)) else: - acc += request.left_blank_cards[i-1] + 1 + acc += request.left_blank_cards[i - 1] + 1 left_nodes.append((x, round(acc * YL, 4))) # RIGHT @@ -25,7 +31,7 @@ def build_single_docmf(request): if i == 0: right_nodes.append((x, 1.0)) else: - acc += request.right_blank_cards[i-1] + 1 + acc += request.right_blank_cards[i - 1] + 1 right_nodes.append((x, round(1 - acc * YR, 4))) return { @@ -43,3 +49,19 @@ def build_docmf_multi(request): result = build_single_docmf(level) results.append(result) return {"results": results} + + +def build_doc_mf_level(level: DoCMFRequest) -> FuzzyTerm: + """ + Adaptador para reutilizar build_single_docmf con el modelo DoCMFRequest. + Devuelve un FuzzyTerm, que es lo que espera el sistema IT2MF. + """ + result = build_single_docmf(level) + + return FuzzyTerm( + term=result["term"], + core=list(result["core"]), + support=list(result["support"]), + left_nodes=[list(p) for p in result["left_nodes"]], + right_nodes=[list(p) for p in result["right_nodes"]], + ) From 2a237a51db0a57b0de83695d5d1a4140d78efee0 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 6 Apr 2026 12:34:42 +0200 Subject: [PATCH 49/80] =?UTF-8?q?add:=20a=C3=B1adir=20modo=20rango=20en=20?= =?UTF-8?q?el=20modal=20para=20la=20subescala?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/editor/SubscaleModal.jsx | 114 ++++++++++++++++-- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx index 70bb4ce..0980b37 100644 --- a/frontend/src/components/editor/SubscaleModal.jsx +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -4,11 +4,20 @@ import BlankCardsCounter from '../BlankCardsCounter'; export default function SubscaleModal({ onClose, onSave, targetInfo }) { const [cardsCount, setCardsCount] = useState(targetInfo?.initialData?.cardsCount || 2); - const [blankCards, setBlankCards] = useState(targetInfo?.initialData?.blankCards || [0]); + + const [blankCards, setBlankCards] = useState(() => { + const initialBlanks = targetInfo?.initialData?.blankCards || [0]; + return initialBlanks.map(b => { + if (Array.isArray(b)) { + return { min: b[0], max: b[1], isRange: true }; + } + return { min: b, max: b, isRange: false }; + }); + }); const handleAddCard = () => { setCardsCount(prev => prev + 1); - setBlankCards([...blankCards, 0]); + setBlankCards([...blankCards, { min: 0, max: 0, isRange: false }]); }; const handleRemoveCard = () => { @@ -17,16 +26,46 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { setBlankCards(blankCards.slice(0, -1)); }; - const handleBlankCardChange = (index, delta) => { + const handleExactChange = (index, delta) => { const newBlanks = [...blankCards]; - if (newBlanks[index] + delta >= 0) { - newBlanks[index] += delta; + const newVal = newBlanks[index].min + delta; + if (newVal >= 0) { + newBlanks[index].min = newVal; + newBlanks[index].max = newVal; setBlankCards(newBlanks); } }; + const handleMinChange = (index, delta) => { + const newBlanks = [...blankCards]; + const newVal = newBlanks[index].min + delta; + if (newVal >= 0 && newVal <= newBlanks[index].max) { + newBlanks[index].min = newVal; + setBlankCards(newBlanks); + } + }; + + const handleMaxChange = (index, delta) => { + const newBlanks = [...blankCards]; + const newVal = newBlanks[index].max + delta; + if (newVal >= newBlanks[index].min) { + newBlanks[index].max = newVal; + setBlankCards(newBlanks); + } + }; + + const toggleRangeMode = (index) => { + const newBlanks = [...blankCards]; + newBlanks[index].isRange = !newBlanks[index].isRange; + if (!newBlanks[index].isRange) { + newBlanks[index].max = newBlanks[index].min; + } + setBlankCards(newBlanks); + }; + const handleSave = () => { - onSave(targetInfo.term, targetInfo.side, { cardsCount, blankCards }); + const payloadBlanks = blankCards.map(b => b.isRange ? [b.min, b.max] : b.min); + onSave(targetInfo.term, targetInfo.side, { cardsCount, blankCards: payloadBlanks }); }; const handleDelete = () => { @@ -35,7 +74,7 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { return (
-
+
@@ -47,34 +86,83 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) {
- {/* Tablero */} + {/* Tablero de Cartas */}
+ {Array.from({ length: cardsCount }).map((_, index) => ( + {/* CARTA DE REFERENCIA */}
{cardsCount > 2 && index === cardsCount - 1 && ( - + )} {index + 1}
+ + {/* HUECO ENTRE CARTAS: Representación y Controles */} {index < cardsCount - 1 && ( - +
+ + {/* Representación visual de las cartas blancas Sólidas / Fantasmas */} +
+ {Array.from({ length: blankCards[index].min }).map((_, i) => ( +
+ ))} + {blankCards[index].isRange && Array.from({ length: blankCards[index].max - blankCards[index].min }).map((_, i) => ( +
+ ? +
+ ))} +
+ + {/* Controles de números */} + {blankCards[index].isRange ? ( + // MODO RANGO +
+
+ MÍN + handleMinChange(idx, delta)} /> +
+
+ MÁX + handleMaxChange(idx, delta)} /> +
+
+ ) : ( + // MODO EXACTO +
+ CARTAS + handleExactChange(idx, delta)} /> +
+ )} + + {/* Botón Toggle */} + + +
)} ))} + {/* Botón Añadir Carta */}
-
+
- {/* Botones */} + {/* Botones de Acción */}
- {/* Lado derecho (Pendiente descendente) */}
-
- - updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> -
updateCurrentMf('supportEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor, opacity: 0.7 }} />
+
+ + updateCurrentMf('coreEnd', e.target.value)} className="w-full cursor-pointer h-1.5" style={{ accentColor: selectedColor }} /> +
- {/* Botón subescala derecha */}
+ + {/* Dropdown Menu (Solo Usuario y Cerrar Sesión) */} + {isDropdownOpen && ( + <> +
setIsDropdownOpen(false)} + >
+ +
+
+

Usuario

+

{user?.username}

+
+ + +
+ + )}
) : ( - - Iniciar Sesión - + // BOTONES PARA USUARIO NO LOGUEADO +
+ + Iniciar sesión + + + Registrarse + +
)}
-
-
- + {/* CONTENIDO PRINCIPAL */} +
+ {children}
); diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index 1ebf5f9..dd393b5 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import Step1BaseScale from '../components/editor/Step1BaseScale'; import Step2FuzzyModeling from '../components/editor/Step2FuzzyModeling'; import SubscaleModal from '../components/editor/SubscaleModal'; -import { calculateValueFunction, buildFuzzyGraph } from '../services/docService'; +import { calculateValueFunction, buildFuzzyGraph, saveToHistory } from '../services/docService'; import Step3FinalGraph from '../components/editor/Step3FinalGraph'; export default function DocEditor() { @@ -193,6 +193,38 @@ export default function DocEditor() { } }; + // Petición para guardar en el historial + const handleSaveToHistory = async () => { + const token = localStorage.getItem('token'); + if (!token) { + alert("Para guardar tu modelo debes iniciar sesión primero. Puedes seguir visualizando la gráfica sin problema."); + return; + } + + const defaultName = criterionName ? `Modelo de ${criterionName}` : "Mi nueva gráfica DoC-IT2MF"; + const historyName = prompt("Dale un nombre a este modelo para guardarlo en tu historial:", defaultName); + + if (!historyName) return; + + setIsLoading(true); + try { + const payload = { + name: historyName, + results: finalResult.levels || finalResult.results + }; + + await saveToHistory(payload); + + alert("¡Gráfica guardada con éxito en tu historial!"); + + } catch (error) { + console.error("Error al guardar en el historial:", error); + alert("Hubo un problema al guardar el modelo: " + error); + } finally { + setIsLoading(false); + } + }; + return (
@@ -223,10 +255,13 @@ export default function DocEditor() {
)} diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx new file mode 100644 index 0000000..e5df8c4 --- /dev/null +++ b/frontend/src/pages/History.jsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { getUserHistory, deleteHistoryItem } from '../services/docService'; +import Step3FinalGraph from '../components/editor/Step3FinalGraph'; + +export default function History() { + const [historyItems, setHistoryItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [expandedId, setExpandedId] = useState(null); + + useEffect(() => { + fetchHistory(); + }, []); + + const fetchHistory = async () => { + setIsLoading(true); + try { + const data = await getUserHistory(); + const items = Array.isArray(data) ? data : data.history || data.items || []; + + setHistoryItems(items.reverse()); + } catch (error) { + console.error("Error fetching history:", error); + alert("Hubo un problema al cargar el historial."); + } finally { + setIsLoading(false); + } + }; + + const handleDelete = async (id) => { + if (!window.confirm('¿Seguro que quieres borrar este modelo definitivamente?')) return; + + try { + await deleteHistoryItem(id); + setHistoryItems(prev => prev.filter(item => item._id !== id && item.id !== id)); + if (expandedId === id) setExpandedId(null); + } catch (error) { + alert("Error al borrar: " + error); + } + }; + + const toggleExpand = (id) => { + setExpandedId(expandedId === id ? null : id); + }; + + return ( +
+ + {/* Cabecera */} +
+
+

Mi Historial

+

+ Aquí están todas las gráficas y modelos que has guardado. +

+
+ + + Nuevo Modelo + +
+ + {/* Lista de Historial */} + {isLoading ? ( +
+
+

Cargando tus gráficas...

+
+ ) : historyItems.length === 0 ? ( +
+ 📭 +

Aún no has guardado ningún modelo.

+

Ve al editor, crea una gráfica y dale a "Finalizar y Guardar".

+
+ ) : ( +
+ {historyItems.map((item) => { + const itemId = item._id || item.id; + const isExpanded = expandedId === itemId; + + return ( +
+ + {/* Cabecera de la Card (Siempre visible) */} +
+
+
+ 📊 +
+
+

{item.name || 'Modelo sin título'}

+

+ Guardado en el historial +

+
+
+ +
+ + +
+
+ + {/* Contenido Desplegable (La Gráfica) */} + {isExpanded && ( +
+ +
+ )} +
+ ); + })} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/routers/AppRouter.jsx b/frontend/src/routers/AppRouter.jsx index 679ca28..3337675 100644 --- a/frontend/src/routers/AppRouter.jsx +++ b/frontend/src/routers/AppRouter.jsx @@ -1,20 +1,22 @@ -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import MainLayout from '../components/layout/MainLayout'; import DocEditor from '../pages/DocEditor'; import Login from '../pages/Login'; import Register from '../pages/Register'; +import History from '../pages/History'; -export function AppRouter() { +export default function AppRouter() { return ( - - - }> - } /> - } /> - } /> - } /> - - - + + + + } /> + } /> + } /> + } /> + } /> + + + ); } \ No newline at end of file diff --git a/frontend/src/services/docService.js b/frontend/src/services/docService.js index 16ea154..ecf3686 100644 --- a/frontend/src/services/docService.js +++ b/frontend/src/services/docService.js @@ -18,4 +18,34 @@ export const buildFuzzyGraph = async (payload) => { console.error('Error building fuzzy graph:', error); throw error.response?.data?.detail || error.message; } +}; + +export const saveToHistory = async (payload) => { + try { + const response = await api.post('/history/add', payload); + return response.data; + } catch (error) { + console.error('Error saving to history:', error); + throw error.response?.data?.detail || error.message; + } +}; + +export const getUserHistory = async () => { + try { + const response = await api.get('/history/user'); + return response.data; + } catch (error) { + console.error('Error fetching history:', error); + throw error.response?.data?.detail || error.message; + } +}; + +export const deleteHistoryItem = async (id) => { + try { + const response = await api.delete(`/history/delete/${id}`); + return response.data; + } catch (error) { + console.error('Error deleting history item:', error); + throw error.response?.data?.detail || error.message; + } }; \ No newline at end of file From 62a4db33a661dad83d77949426509b0471d66ee8 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 09:39:48 +0200 Subject: [PATCH 54/80] =?UTF-8?q?refactor:=20mejorar=20dise=C3=B1o=20de=20?= =?UTF-8?q?interfaz=20y=20UX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/layout/MainLayout.jsx | 14 ++++++++++---- frontend/src/pages/History.jsx | 14 ++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index 0a127ec..2c164a0 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -102,11 +102,17 @@ export default function MainLayout({ children }) {
) : ( // BOTONES PARA USUARIO NO LOGUEADO -
- - Iniciar sesión +
+ + Entrar - + Registrarse
diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index e5df8c4..12da8e0 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -44,8 +44,7 @@ export default function History() { }; return ( -
- +
{/* Cabecera */}
@@ -83,7 +82,7 @@ export default function History() { return (
- {/* Cabecera de la Card (Siempre visible) */} + {/* Cabecera de la Card */}
@@ -92,7 +91,10 @@ export default function History() {

{item.name || 'Modelo sin título'}

- Guardado en el historial + {item.created_at + ? `Guardado el ${new Date(item.created_at).toLocaleDateString('es-ES', { day: '2-digit', month: 'long', year: 'numeric' })}` + : 'Guardado en el historial' + }

@@ -109,12 +111,12 @@ export default function History() { className="px-4 py-2.5 bg-white border border-red-200 text-red-500 font-bold rounded-xl hover:bg-red-50 transition-colors shadow-sm" title="Borrar modelo" > - 🗑️ + Borrar
- {/* Contenido Desplegable (La Gráfica) */} + {/* Contenido Desplegable (La gráfica) */} {isExpanded && (
From ffab8ccce69c9d4bb96587ce6db91f6d76ff63be Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Tue, 7 Apr 2026 09:52:36 +0200 Subject: [PATCH 55/80] =?UTF-8?q?A=C3=B1adidas=20modificaciones=20de=20seg?= =?UTF-8?q?uridad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/routers/docit2mf_build.py | 12 +++++++++++- backend/requirements.txt | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/backend/api/routers/docit2mf_build.py b/backend/api/routers/docit2mf_build.py index 75660f4..ba4c06a 100644 --- a/backend/api/routers/docit2mf_build.py +++ b/backend/api/routers/docit2mf_build.py @@ -1,13 +1,19 @@ # api/routers/docit2mf_build.py +import logging from fastapi import APIRouter, HTTPException +from slowapi import Limiter +from slowapi.util import get_remote_address from api.models.docit2mf_models import DoCIT2MFMultiRequest from api.services.docit2mf_build_service import build_it2mf_from_level router = APIRouter(prefix="/criteria", tags=["criteria"]) +limiter = Limiter(key_func=get_remote_address) +logger = logging.getLogger(__name__) @router.post("/doc-it2mf/build") +@limiter.limit("10/minute") async def build_doc_it2mf(request: DoCIT2MFMultiRequest): results = [] @@ -15,6 +21,10 @@ async def build_doc_it2mf(request: DoCIT2MFMultiRequest): for level in request.levels: results.append(build_it2mf_from_level(level)) except ValueError as e: - raise HTTPException(status_code=400, detail=str(e)) + logger.warning(f"Validation error in doc-it2mf/build: {str(e)}") + raise HTTPException(status_code=400, detail="Invalid input data") + except Exception as e: + logger.error(f"Unexpected error in doc-it2mf/build: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail="Internal server error") return {"levels": results} diff --git a/backend/requirements.txt b/backend/requirements.txt index 26ebe6e..bb4e897 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -5,3 +5,4 @@ pydantic passlib bcrypt==4.0.1 email-validator +slowapi From 66c350c8a49ecf0b0f43647d4b38732de2fad4e3 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 10:04:34 +0200 Subject: [PATCH 56/80] =?UTF-8?q?add:=20a=C3=B1adir=20logo=20y=20favicon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/public/favicon.svg | 2 +- frontend/src/components/layout/MainLayout.jsx | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 6893eb1..38aaf3c 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1 +1 @@ - \ No newline at end of file +logo-doc \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index 2c164a0..9ff0053 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -29,8 +29,15 @@ export default function MainLayout({ children }) {
{/* Logo / Título */} - - + + Deck of Cards Logo + + {/* Texto del título */} + Deck of Cards From 392a1fb36c5670407c0fc67d88d614b7ee722673 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 10:48:22 +0200 Subject: [PATCH 57/80] proceso rangos --- .../src/components/editor/SubscaleModal.jsx | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx index 0980b37..3a751f1 100644 --- a/frontend/src/components/editor/SubscaleModal.jsx +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -102,49 +102,51 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) {
- {/* HUECO ENTRE CARTAS: Representación y Controles */} + {/* HUECO ENTRE CARTAS */} {index < cardsCount - 1 && ( -
- - {/* Representación visual de las cartas blancas Sólidas / Fantasmas */} -
- {Array.from({ length: blankCards[index].min }).map((_, i) => ( -
- ))} - {blankCards[index].isRange && Array.from({ length: blankCards[index].max - blankCards[index].min }).map((_, i) => ( -
- ? -
- ))} -
+
{/* Controles de números */} {blankCards[index].isRange ? ( - // MODO RANGO -
+ // MODO RANGO +
- MÍN - handleMinChange(idx, delta)} /> + MÍN + handleMinChange(idx, delta)} + />
+ {/* Guión separador para unificar visualmente el rango */} + -
- MÁX - handleMaxChange(idx, delta)} /> + MÁX + handleMaxChange(idx, delta)} + />
) : ( // MODO EXACTO -
- CARTAS - handleExactChange(idx, delta)} /> +
+ CARTAS + handleExactChange(idx, delta)} + />
)} {/* Botón Toggle */}
From 9602c4f5096ce3909c3aeead2b3ab88d79f1b209 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 12:47:13 +0200 Subject: [PATCH 58/80] =?UTF-8?q?refactor:=20mejorar=20dise=C3=B1o=20y=20f?= =?UTF-8?q?uncionalidad=20de=20componentes=20en=20el=20editor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/BlankCardsCounter.jsx | 40 +++--- frontend/src/components/CardEditor.jsx | 4 +- .../src/components/editor/Step1BaseScale.jsx | 32 +++-- .../components/editor/Step2FuzzyModeling.jsx | 2 +- .../src/components/editor/SubscaleModal.jsx | 115 ++++++++++-------- 5 files changed, 107 insertions(+), 86 deletions(-) diff --git a/frontend/src/components/BlankCardsCounter.jsx b/frontend/src/components/BlankCardsCounter.jsx index b35b7ed..3292eea 100644 --- a/frontend/src/components/BlankCardsCounter.jsx +++ b/frontend/src/components/BlankCardsCounter.jsx @@ -1,3 +1,5 @@ +import React from 'react'; + export default function BlankCardsCounter({ index, blankCardsCount, handleBlankCardChange }) { const maxCardsPerRow = 7; @@ -7,35 +9,29 @@ export default function BlankCardsCounter({ index, blankCardsCount, handleBlankC } return ( -
+
-
+ {/* Bloque de botones */} +
+ - {/* Línea conectora horizontal */} -
- - {/* Botones - y + */} -
- - -
- Blancas - {blankCardsCount} -
- - +
+ Blancas + {blankCardsCount}
+ +
{/* Cartas blancas */} {blankCardsCount > 0 && ( -
+
{rows.map((row, rowIndex) => (
{row.map((_, colIndex) => ( diff --git a/frontend/src/components/CardEditor.jsx b/frontend/src/components/CardEditor.jsx index c5e887f..4783869 100644 --- a/frontend/src/components/CardEditor.jsx +++ b/frontend/src/components/CardEditor.jsx @@ -10,9 +10,9 @@ export default function CardEditor({ index, level, handleLevelChange, handleRemo )} {index + 1} {index + 1} - handleLevelChange(index, e.target.value)} className={`w-10/12 text-center text-lg font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500'}`} /> + handleLevelChange(index, e.target.value)} className={`w-10/12 text-center text-lg font-bold text-slate-700 bg-transparent border-b-2 border-dashed outline-none pb-1 ${error ? 'border-red-300 focus:border-red-500 placeholder:text-red-200' : 'border-slate-300 focus:border-blue-500'}`} />
-
{error &&

Escribe una etiqueta

}
+
{error &&

Escribe un término

}
); } \ No newline at end of file diff --git a/frontend/src/components/editor/Step1BaseScale.jsx b/frontend/src/components/editor/Step1BaseScale.jsx index 115b450..2ee5755 100644 --- a/frontend/src/components/editor/Step1BaseScale.jsx +++ b/frontend/src/components/editor/Step1BaseScale.jsx @@ -59,30 +59,48 @@ export default function Step1BaseScale({ -
+
{levels.map((level, index) => ( -
+ + {/* CARTA DE NIVEL */} +
3} />
+ + {/* HUECO ENTRE CARTAS Y CONTADOR */} {index < levels.length - 1 && ( - +
+
+ +
+ +
+
)} ))} -
-
+ + {/* LÍNEA HACIA EL BOTÓN DE AÑADIR */} +
+
- + + {/* BOTÓN AÑADIR NIVEL */} +
+ +
+
-
+ {/* Generar Gráfica Continua */} +
diff --git a/frontend/src/components/editor/Step2FuzzyModeling.jsx b/frontend/src/components/editor/Step2FuzzyModeling.jsx index dcc3d18..2369c54 100644 --- a/frontend/src/components/editor/Step2FuzzyModeling.jsx +++ b/frontend/src/components/editor/Step2FuzzyModeling.jsx @@ -79,7 +79,7 @@ export default function Step2FuzzyModeling({
diff --git a/frontend/src/components/editor/SubscaleModal.jsx b/frontend/src/components/editor/SubscaleModal.jsx index 3a751f1..6f70c3a 100644 --- a/frontend/src/components/editor/SubscaleModal.jsx +++ b/frontend/src/components/editor/SubscaleModal.jsx @@ -3,10 +3,19 @@ import BlankCardsCounter from '../BlankCardsCounter'; export default function SubscaleModal({ onClose, onSave, targetInfo }) { - const [cardsCount, setCardsCount] = useState(targetInfo?.initialData?.cardsCount || 2); + const initialCount = Math.max(3, targetInfo?.initialData?.cardsCount || 3); + const [cardsCount, setCardsCount] = useState(initialCount); const [blankCards, setBlankCards] = useState(() => { - const initialBlanks = targetInfo?.initialData?.blankCards || [0]; + let initialBlanks = targetInfo?.initialData?.blankCards; + + if (!initialBlanks || initialBlanks.length === 0) { + initialBlanks = [0, 0]; + } else if (initialBlanks.length < initialCount - 1) { + const padding = Array(initialCount - 1 - initialBlanks.length).fill(0); + initialBlanks = [...initialBlanks, ...padding]; + } + return initialBlanks.map(b => { if (Array.isArray(b)) { return { min: b[0], max: b[1], isRange: true }; @@ -21,7 +30,7 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { }; const handleRemoveCard = () => { - if (cardsCount <= 2) return; + if (cardsCount <= 3) return; setCardsCount(prev => prev - 1); setBlankCards(blankCards.slice(0, -1)); }; @@ -73,10 +82,10 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { }; return ( -
-
+
+
-
+

Diseñar Subescala

@@ -86,16 +95,15 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) {

- {/* Tablero de Cartas */} -
+
{Array.from({ length: cardsCount }).map((_, index) => ( {/* CARTA DE REFERENCIA */} -
+
- {cardsCount > 2 && index === cardsCount - 1 && ( + {cardsCount > 3 && index === cardsCount - 1 && ( )} {index + 1} @@ -104,58 +112,57 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) { {/* HUECO ENTRE CARTAS */} {index < cardsCount - 1 && ( -
- - {/* Controles de números */} - {blankCards[index].isRange ? ( - // MODO RANGO -
-
- MÍN - handleMinChange(idx, delta)} - /> +
+
+ +
+ {blankCards[index].isRange ? ( +
+
+ MÍN + handleMinChange(idx, delta)} + /> +
+ +
-
+ +
+ MÁX + handleMaxChange(idx, delta)} + /> +
- {/* Guión separador para unificar visualmente el rango */} - - -
- MÁX - handleMaxChange(idx, delta)} - /> + ) : ( +
+ CARTAS + handleExactChange(idx, delta)} + />
-
- ) : ( - // MODO EXACTO -
- CARTAS - handleExactChange(idx, delta)} - /> -
- )} - - {/* Botón Toggle */} - + )} + +
)} ))} {/* Botón Añadir Carta */} -
+
@@ -165,7 +172,7 @@ export default function SubscaleModal({ onClose, onSave, targetInfo }) {
{/* Botones de Acción */} -
+
From 5fbf08cdc192f8ac5a053fb0649fb6bea6c11a26 Mon Sep 17 00:00:00 2001 From: Alexis Date: Tue, 7 Apr 2026 13:02:13 +0200 Subject: [PATCH 59/80] refactor: mejorar logo favicon --- frontend/public/favicon.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 38aaf3c..cd55914 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1 +1 @@ -logo-doc \ No newline at end of file +logo doc \ No newline at end of file From 03e3b69ae3902d258547c58c2a1170fc72264711 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 9 Apr 2026 10:50:43 +0200 Subject: [PATCH 60/80] =?UTF-8?q?refactor:=20cambios=20m=C3=ADnimos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/DocEditor.jsx | 2 +- frontend/src/pages/History.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/DocEditor.jsx b/frontend/src/pages/DocEditor.jsx index dd393b5..1ea2b5b 100644 --- a/frontend/src/pages/DocEditor.jsx +++ b/frontend/src/pages/DocEditor.jsx @@ -261,7 +261,7 @@ export default function DocEditor() { isLoading ? 'bg-slate-400 text-slate-100 cursor-not-allowed' : 'bg-blue-600 text-white hover:bg-blue-700' }`} > - {isLoading ? 'Guardando...' : 'Finalizar y Guardar'} + {isLoading ? 'Guardando...' : 'Guardar'}
)} diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 12da8e0..10529fa 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -71,7 +71,7 @@ export default function History() {
📭

Aún no has guardado ningún modelo.

-

Ve al editor, crea una gráfica y dale a "Finalizar y Guardar".

+

Ve al editor, crea una gráfica y dale a "Guardar".

) : (
From ed44d2f9fd0672a562b619c4cde638e1bb87effd Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Thu, 9 Apr 2026 11:39:00 +0200 Subject: [PATCH 61/80] =?UTF-8?q?A=C3=B1adido=20soporte=20para=20autentica?= =?UTF-8?q?ci=C3=B3n=20con=20Google=20y=20mejoras=20en=20la=20gesti=C3=B3n?= =?UTF-8?q?=20de=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + backend/api/main.py | 8 ++- backend/api/routers/google_auth.py | 101 +++++++++++++++++++++++++++++ backend/api/utils/security.py | 8 ++- backend/requirements.txt | 2 + 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 backend/api/routers/google_auth.py diff --git a/.gitignore b/.gitignore index e327dab..a8ebeb5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ __pycache__/ # Variables de entorno .env +.env* + # Configuraciones del editor .vscode/ diff --git a/backend/api/main.py b/backend/api/main.py index d4c740d..639fca8 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -14,15 +14,19 @@ from api.routers.auth import router as auth_router from api.routers.history import router as history_router from api.routers.test_mongo import router as test_mongo_router from api.routers.docit2mf_build import router as docit2mf_router +from api.routers.google_auth import router as google_auth_router + + + @asynccontextmanager async def lifespan(app: FastAPI): - # Aquí podrías hacer comprobaciones si quieres yield - # No hace falta cerrar nada con Motor app = FastAPI(lifespan=lifespan) +app.include_router(google_auth_router) + app.add_middleware( CORSMiddleware, allow_origins=["*"], diff --git a/backend/api/routers/google_auth.py b/backend/api/routers/google_auth.py new file mode 100644 index 0000000..44dc4cf --- /dev/null +++ b/backend/api/routers/google_auth.py @@ -0,0 +1,101 @@ +# api/routers/google_auth.py + +from fastapi import APIRouter, HTTPException, Depends +from fastapi.responses import RedirectResponse +from pydantic import BaseModel +from bson import ObjectId +import httpx +import os +import jwt + +from api.database.mongodb import users_collection +from api.utils.security import create_access_token + +router = APIRouter(prefix="/auth/google", tags=["auth"]) + + +GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") +GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") +REDIRECT_URI = "http://localhost:8000/api/auth/google/callback" + + +# ----------------------------- +# 1. LOGIN → REDIRECCIÓN A GOOGLE +# ----------------------------- +@router.get("/login") +async def google_login(): + google_auth_url = ( + "https://accounts.google.com/o/oauth2/v2/auth" + "?response_type=code" + f"&client_id={GOOGLE_CLIENT_ID}" + f"&redirect_uri={REDIRECT_URI}" + "&scope=openid%20email%20profile" + "&access_type=offline" + "&prompt=consent" + ) + + return RedirectResponse(google_auth_url) + + +# ----------------------------- +# 2. CALLBACK → GOOGLE DEVUELVE EL CODE +# ----------------------------- +@router.get("/callback") +async def google_callback(code: str): + + # 1. Intercambiar code por access_token + token_url = "https://oauth2.googleapis.com/token" + + data = { + "code": code, + "client_id": GOOGLE_CLIENT_ID, + "client_secret": GOOGLE_CLIENT_SECRET, + "redirect_uri": REDIRECT_URI, + "grant_type": "authorization_code", + } + + async with httpx.AsyncClient() as client: + token_response = await client.post(token_url, data=data) + token_json = token_response.json() + + if "access_token" not in token_json: + raise HTTPException(status_code=400, detail="Error obteniendo token de Google") + + access_token = token_json["access_token"] + + # 2. Obtener datos del usuario desde Google + async with httpx.AsyncClient() as client: + userinfo = await client.get( + "https://www.googleapis.com/oauth2/v2/userinfo", + headers={"Authorization": f"Bearer {access_token}"} + ) + + user_data = userinfo.json() + + google_id = user_data["id"] + email = user_data["email"] + name = user_data.get("name", "Usuario") + + # 3. Buscar usuario en MongoDB + user = await users_collection.find_one({"email": email}) + + if not user: + # Crear usuario nuevo + new_user = { + "username": name, + "email": email, + "password_hash": None, # No hay contraseña + "google_id": google_id, + "history": [], + } + + result = await users_collection.insert_one(new_user) + user_id = result.inserted_id + else: + user_id = user["_id"] + + # 4. Crear JWT de tu sistema + token = create_access_token({"user_id": str(user_id)}) + + # 5. Redirigir al frontend con el token + return {"message": "Login con Google exitoso", "token": token} diff --git a/backend/api/utils/security.py b/backend/api/utils/security.py index 5826361..eb3ad3c 100644 --- a/backend/api/utils/security.py +++ b/backend/api/utils/security.py @@ -17,7 +17,7 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: def generate_token() -> str: - return secrets.token_hex(32) # 64 caracteres seguros + return secrets.token_hex(32) security_scheme = HTTPBearer() @@ -35,6 +35,8 @@ async def get_current_user( detail="Token inválido o usuario no autenticado", ) - # devolvemos el documento tal cual (dict) user["id"] = str(user["_id"]) - return user \ No newline at end of file + return user + +def create_access_token(data: dict): + return jwt.encode(data, SECRET_KEY, algorithm="HS256") diff --git a/backend/requirements.txt b/backend/requirements.txt index bb4e897..6387273 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -6,3 +6,5 @@ passlib bcrypt==4.0.1 email-validator slowapi +httpx +PyJWT From d3e44a624977a4666188eef32660a87bd2460aca Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 9 Apr 2026 13:00:46 +0200 Subject: [PATCH 62/80] =?UTF-8?q?refactor:=20optimizar=20la=20generaci?= =?UTF-8?q?=C3=B3n=20de=20datos=20en=20el=20gr=C3=A1fico=20final,=20mostra?= =?UTF-8?q?ndo=20el=20grado=20de=20pertenencia=20en=20cualquier=20punto=20?= =?UTF-8?q?de=20la=20gr=C3=A1fica.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/editor/Step3FinalGraph.jsx | 226 +++++++++++++++--- 1 file changed, 192 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx index 99884dd..fd6edf6 100644 --- a/frontend/src/components/editor/Step3FinalGraph.jsx +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -2,87 +2,245 @@ import React, { useMemo } from 'react'; import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { CHART_COLORS } from '../../config'; +// 1. Función auxiliar +const interpolateY = (x, nodes) => { + if (!nodes || nodes.length === 0) return null; + const EPSILON = 1e-5; + const MICRO_STEP = 0.0001; + + const firstX = nodes[0][0]; + const lastX = nodes[nodes.length - 1][0]; + + if (x < firstX - MICRO_STEP - EPSILON) return null; + if (x > lastX + MICRO_STEP + EPSILON) return null; + + if (x < firstX - EPSILON) return 0; + if (x > lastX + EPSILON) return 0; + + for (let i = nodes.length - 1; i >= 0; i--) { + if (Math.abs(nodes[i][0] - x) < EPSILON) { + return nodes[i][1]; + } + } + + for (let i = 0; i < nodes.length - 1; i++) { + const x1 = nodes[i][0]; + const x2 = nodes[i + 1][0]; + + if (Math.abs(x2 - x1) < EPSILON) continue; + + if (x >= x1 && x <= x2) { + const y1 = nodes[i][1]; + const y2 = nodes[i + 1][1]; + return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1); + } + } + return null; +}; + const Step3FinalGraph = ({ data, criterionName }) => { + + // Extracción de Nodos Base const sortedResults = useMemo(() => { const rawItems = data?.levels || data?.results || []; const processed = rawItems.map((item, index) => { const isType2 = !!item.lower && !!item.upper; const color = CHART_COLORS[index % CHART_COLORS.length] || '#333'; - - let lineData = []; - let coreVal = 0; let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`; if (isType2) { - const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])]; - const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])]; - - lineData = lowerNodes.map((lNode, i) => { - const uNode = upperNodes[i]; - const lowerY = Number(lNode[1]); - const upperY = Number(uNode ? uNode[1] : lNode[1]); - return { x: Number(lNode[0]), lowerY, upperY, range: [lowerY, upperY] }; - }); - coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; + const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; + return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal }; } else { - const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])]; - lineData = nodes.map(node => ({ x: Number(node[0]), y: Number(node[1]) })); - coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; + const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; + return { ...item, term: termName, isType2, nodes, color, coreVal }; } - - return { ...item, term: termName, isType2, lineData, color, coreVal }; }); return processed.sort((a, b) => a.coreVal - b.coreVal); }, [data]); + // Generación inteligente de datos + const denseData = useMemo(() => { + const xSet = new Set(); + const steps = 1000; + + for (let i = 0; i <= steps; i++) { + xSet.add(Number((i / steps).toFixed(4))); + } + + sortedResults.forEach(item => { + const addNodes = (nodes) => { + nodes.forEach(n => { + const x = n[0]; + xSet.add(Number((x - 0.0001).toFixed(4))); + xSet.add(Number(x.toFixed(4))); + xSet.add(Number((x + 0.0001).toFixed(4))); + }); + }; + if (item.isType2) { + addNodes(item.lowerNodes); + addNodes(item.upperNodes); + } else { + addNodes(item.nodes); + } + }); + + const xValues = Array.from(xSet).sort((a, b) => a - b); + + const dataPoints = []; + xValues.forEach(x => { + const point = { x }; + + sortedResults.forEach(item => { + if (item.isType2) { + const lowerRaw = interpolateY(x, item.lowerNodes); + const upperRaw = interpolateY(x, item.upperNodes); + + point[`${item.term}_lower`] = lowerRaw; + point[`${item.term}_upper`] = upperRaw; + + if (lowerRaw === null && upperRaw === null) { + point[`${item.term}_range`] = null; + } else { + point[`${item.term}_range`] = [lowerRaw !== null ? lowerRaw : 0, upperRaw !== null ? upperRaw : 0]; + } + } else { + point[item.term] = interpolateY(x, item.nodes); + } + }); + dataPoints.push(point); + }); + return dataPoints; + }, [sortedResults]); + + // Tooltip + const renderCustomTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + const dataPoint = payload[0].payload; + + const activeTerms = sortedResults.filter(item => { + if (item.isType2) { + return dataPoint[`${item.term}_upper`] !== null && dataPoint[`${item.term}_upper`] > 0; + } else { + return dataPoint[item.term] !== null && dataPoint[item.term] > 0; + } + }); + + if (activeTerms.length === 0) return null; + + return ( +
+

+ Punto X: + {Number(label).toFixed(3)} +

+
+ {activeTerms.map(item => { + if (item.isType2) { + const lower = dataPoint[`${item.term}_lower`] !== null ? dataPoint[`${item.term}_lower`] : 0; + const upper = dataPoint[`${item.term}_upper`] !== null ? dataPoint[`${item.term}_upper`] : 0; + const range = Math.abs(upper - lower); + + if (range <= 0.001) { + return ( +
+ {item.term} + + Pertenencia: {Number(upper).toFixed(3)} + +
+ ); + } + + return ( +
+ {item.term} + Mínimo: {Number(lower).toFixed(3)} + Máximo: {Number(upper).toFixed(3)} + + Incertidumbre: {Number(range).toFixed(3)} + +
+ ); + } else { + const val = dataPoint[item.term]; + return ( +
+ {item.term} + + Pertenencia: {Number(val).toFixed(3)} + +
+ ); + } + })} +
+
+ ); + } + return null; + }; + if (!data || (!data.levels && !data.results)) { return

Cargando gráfico final...

; } return ( -
+
+ + - {/* Título */}

{criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'}

- {/* Gráfica */}
- + - - Array.isArray(value) ? [`[${Number(value[0]).toFixed(3)}, ${Number(value[1]).toFixed(3)}]`, name] : [Number(value).toFixed(3), name]} - labelFormatter={(label) => `X: ${Number(label).toFixed(3)}`} - contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} + Number(val.toFixed(2))} + tick={{ fill: '#475569', fontSize: 14 }} /> + + {sortedResults.map((item) => { if (item.isType2) { return ( - - - + + + ); } else { - return ; + return ; } })}
- {/* Leyenda */}
{sortedResults.map((item) => (
From f216ddea80a2fbe29e4d844cb4b1449d951721a8 Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Thu, 9 Apr 2026 13:18:00 +0200 Subject: [PATCH 63/80] =?UTF-8?q?A=C3=B1adida=20funcionalidad=20para=20ord?= =?UTF-8?q?enar=20nodos=20y=20garantizar=20que=20UMF=20no=20sea=20menor=20?= =?UTF-8?q?que=20LMF=20en=20la=20construcci=C3=B3n=20de=20funciones=20IT2M?= =?UTF-8?q?F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/services/docit2mf_build_service.py | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/backend/api/services/docit2mf_build_service.py b/backend/api/services/docit2mf_build_service.py index a18d5d8..82083ac 100644 --- a/backend/api/services/docit2mf_build_service.py +++ b/backend/api/services/docit2mf_build_service.py @@ -15,7 +15,6 @@ def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> Li result = [] for item in values: if isinstance(item, int): - # valor fijo → mismo para LMF y UMF result.append(item) else: lo, hi = item @@ -23,8 +22,45 @@ def _extract_bounds(values: List[Union[int, List[int], tuple]], mode: str) -> Li return result +def _sort_nodes(nodes): + """Ordena los nodos por su coordenada X.""" + return sorted(nodes, key=lambda p: p[0]) + + +def _enforce_upper_ge_lower(lower, upper): + """ + Garantiza que la UMF (upper) nunca quede por debajo de la LMF (lower). + Ajusta los valores de pertenencia si es necesario. + """ + # left nodes + for i in range(len(lower["left_nodes"])): + lx, ly = lower["left_nodes"][i] + ux, uy = upper["left_nodes"][i] + upper["left_nodes"][i][1] = max(uy, ly) + + # right nodes + for i in range(len(lower["right_nodes"])): + lx, ly = lower["right_nodes"][i] + ux, uy = upper["right_nodes"][i] + upper["right_nodes"][i][1] = max(uy, ly) + + return upper + + def build_it2mf_from_level(level: DoCIT2MFRequest): + """ + Construye una función IT2MF a partir de un nivel con intervalos de cartas blancas. + Devuelve: + { + "term": ..., + "lower": {...}, + "upper": {...} + } + """ + + # ------------------------- # LMF (mínimos) + # ------------------------- left_min = _extract_bounds(level.left_blank_cards, "min") right_min = _extract_bounds(level.right_blank_cards, "min") @@ -39,7 +75,13 @@ def build_it2mf_from_level(level: DoCIT2MFRequest): ) lower = build_doc_mf_level(lower_level) + # Ordenar nodos LMF + lower["left_nodes"] = _sort_nodes(lower["left_nodes"]) + lower["right_nodes"] = _sort_nodes(lower["right_nodes"]) + + # ------------------------- # UMF (máximos) + # ------------------------- left_max = _extract_bounds(level.left_blank_cards, "max") right_max = _extract_bounds(level.right_blank_cards, "max") @@ -54,6 +96,15 @@ def build_it2mf_from_level(level: DoCIT2MFRequest): ) upper = build_doc_mf_level(upper_level) + # Ordenar nodos UMF + upper["left_nodes"] = _sort_nodes(upper["left_nodes"]) + upper["right_nodes"] = _sort_nodes(upper["right_nodes"]) + + # ------------------------- + # FIX: evitar inversión vertical (UMF < LMF) + # ------------------------- + upper = _enforce_upper_ge_lower(lower, upper) + return { "term": level.term, "lower": lower, From b6402f2d59f16f7486625ac3004905c5bc2d8588 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 13 Apr 2026 09:56:38 +0200 Subject: [PATCH 64/80] =?UTF-8?q?refactor:=20mejorar=20la=20estructura=20y?= =?UTF-8?q?=20funcionalidad=20del=20gr=C3=A1fico=20final,=20optimizando=20?= =?UTF-8?q?la=20carga=20de=20datos=20y=20la=20visualizaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/editor/Step3FinalGraph.jsx | 302 ++++-------------- .../editor/finalGraph/GraphTooltip.jsx | 53 +++ .../editor/finalGraph/useGraphData.js | 89 ++++++ frontend/src/pages/History.jsx | 21 +- 4 files changed, 226 insertions(+), 239 deletions(-) create mode 100644 frontend/src/components/editor/finalGraph/GraphTooltip.jsx create mode 100644 frontend/src/components/editor/finalGraph/useGraphData.js diff --git a/frontend/src/components/editor/Step3FinalGraph.jsx b/frontend/src/components/editor/Step3FinalGraph.jsx index fd6edf6..49d3e47 100644 --- a/frontend/src/components/editor/Step3FinalGraph.jsx +++ b/frontend/src/components/editor/Step3FinalGraph.jsx @@ -1,256 +1,92 @@ -import React, { useMemo } from 'react'; +import React, { useState, useEffect, memo } from 'react'; import { ComposedChart, Area, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; -import { CHART_COLORS } from '../../config'; +import { useGraphData } from './finalGraph/useGraphData'; +import { GraphTooltip } from './finalGraph/GraphTooltip'; -// 1. Función auxiliar -const interpolateY = (x, nodes) => { - if (!nodes || nodes.length === 0) return null; - const EPSILON = 1e-5; - const MICRO_STEP = 0.0001; +const Step3FinalGraph = memo(({ data, criterionName }) => { + const { sortedResults, denseData } = useGraphData(data); + const [isReady, setIsReady] = useState(false); - const firstX = nodes[0][0]; - const lastX = nodes[nodes.length - 1][0]; - - if (x < firstX - MICRO_STEP - EPSILON) return null; - if (x > lastX + MICRO_STEP + EPSILON) return null; - - if (x < firstX - EPSILON) return 0; - if (x > lastX + EPSILON) return 0; - - for (let i = nodes.length - 1; i >= 0; i--) { - if (Math.abs(nodes[i][0] - x) < EPSILON) { - return nodes[i][1]; - } - } - - for (let i = 0; i < nodes.length - 1; i++) { - const x1 = nodes[i][0]; - const x2 = nodes[i + 1][0]; - - if (Math.abs(x2 - x1) < EPSILON) continue; - - if (x >= x1 && x <= x2) { - const y1 = nodes[i][1]; - const y2 = nodes[i + 1][1]; - return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1); - } - } - return null; -}; - -const Step3FinalGraph = ({ data, criterionName }) => { - - // Extracción de Nodos Base - const sortedResults = useMemo(() => { - const rawItems = data?.levels || data?.results || []; - - const processed = rawItems.map((item, index) => { - const isType2 = !!item.lower && !!item.upper; - const color = CHART_COLORS[index % CHART_COLORS.length] || '#333'; - let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`; - - if (isType2) { - const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); - const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); - const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; - return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal }; - } else { - const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); - const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; - return { ...item, term: termName, isType2, nodes, color, coreVal }; - } - }); - - return processed.sort((a, b) => a.coreVal - b.coreVal); - }, [data]); - - // Generación inteligente de datos - const denseData = useMemo(() => { - const xSet = new Set(); - const steps = 1000; - - for (let i = 0; i <= steps; i++) { - xSet.add(Number((i / steps).toFixed(4))); - } - - sortedResults.forEach(item => { - const addNodes = (nodes) => { - nodes.forEach(n => { - const x = n[0]; - xSet.add(Number((x - 0.0001).toFixed(4))); - xSet.add(Number(x.toFixed(4))); - xSet.add(Number((x + 0.0001).toFixed(4))); - }); - }; - if (item.isType2) { - addNodes(item.lowerNodes); - addNodes(item.upperNodes); - } else { - addNodes(item.nodes); - } - }); - - const xValues = Array.from(xSet).sort((a, b) => a - b); - - const dataPoints = []; - xValues.forEach(x => { - const point = { x }; - - sortedResults.forEach(item => { - if (item.isType2) { - const lowerRaw = interpolateY(x, item.lowerNodes); - const upperRaw = interpolateY(x, item.upperNodes); - - point[`${item.term}_lower`] = lowerRaw; - point[`${item.term}_upper`] = upperRaw; - - if (lowerRaw === null && upperRaw === null) { - point[`${item.term}_range`] = null; - } else { - point[`${item.term}_range`] = [lowerRaw !== null ? lowerRaw : 0, upperRaw !== null ? upperRaw : 0]; - } - } else { - point[item.term] = interpolateY(x, item.nodes); - } - }); - dataPoints.push(point); - }); - return dataPoints; - }, [sortedResults]); - - // Tooltip - const renderCustomTooltip = ({ active, payload, label }) => { - if (active && payload && payload.length) { - const dataPoint = payload[0].payload; - - const activeTerms = sortedResults.filter(item => { - if (item.isType2) { - return dataPoint[`${item.term}_upper`] !== null && dataPoint[`${item.term}_upper`] > 0; - } else { - return dataPoint[item.term] !== null && dataPoint[item.term] > 0; - } - }); - - if (activeTerms.length === 0) return null; - - return ( -
-

- Punto X: - {Number(label).toFixed(3)} -

-
- {activeTerms.map(item => { - if (item.isType2) { - const lower = dataPoint[`${item.term}_lower`] !== null ? dataPoint[`${item.term}_lower`] : 0; - const upper = dataPoint[`${item.term}_upper`] !== null ? dataPoint[`${item.term}_upper`] : 0; - const range = Math.abs(upper - lower); - - if (range <= 0.001) { - return ( -
- {item.term} - - Pertenencia: {Number(upper).toFixed(3)} - -
- ); - } - - return ( -
- {item.term} - Mínimo: {Number(lower).toFixed(3)} - Máximo: {Number(upper).toFixed(3)} - - Incertidumbre: {Number(range).toFixed(3)} - -
- ); - } else { - const val = dataPoint[item.term]; - return ( -
- {item.term} - - Pertenencia: {Number(val).toFixed(3)} - -
- ); - } - })} -
-
- ); - } - return null; - }; + useEffect(() => { + const timer = setTimeout(() => { + setIsReady(true); + }, 400); + return () => clearTimeout(timer); + }, []); if (!data || (!data.levels && !data.results)) { - return

Cargando gráfico final...

; + return

Cargando datos...

; } return ( -
- - - -

+
+ + +

{criterionName ? `Criterio: ${criterionName}` : 'Espectro Difuso Final'}

-
- - - - - Number(val.toFixed(2))} - tick={{ fill: '#475569', fontSize: 14 }} - /> - - +
+ + {!isReady && ( +
+
+ Generando gráfica... +
+ )} - {sortedResults.map((item) => { - if (item.isType2) { - return ( + {/* Gráfica */} +
+ {isReady && ( + + + + + Number(val.toFixed(2))} + tick={{ fill: '#475569', fontSize: 14 }} + /> + + } + cursor={{ stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5 5' }} + isAnimationActive={false} + /> + + {sortedResults.map((item) => ( - - - + {item.isType2 ? ( + <> + + + + + ) : ( + + )} - ); - } else { - return ; - } - })} - - + ))} + + + )} +
-
+ {/* Leyenda */} +
{sortedResults.map((item) => ( -
- - {item.term} -
+
+ + {item.term} +
))}
); -}; +}); export default Step3FinalGraph; \ No newline at end of file diff --git a/frontend/src/components/editor/finalGraph/GraphTooltip.jsx b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx new file mode 100644 index 0000000..bc95ac9 --- /dev/null +++ b/frontend/src/components/editor/finalGraph/GraphTooltip.jsx @@ -0,0 +1,53 @@ +const TermInfo = ({ title, color, children }) => ( +
+ {title} + {children} +
+); + +export const GraphTooltip = ({ active, payload, label, sortedResults }) => { + if (!active || !payload || !payload.length) return null; + const dataPoint = payload[0].payload; + + const activeTerms = sortedResults.filter(item => + item.isType2 ? (dataPoint[`${item.term}_upper`] ?? 0) > 0 : (dataPoint[item.term] ?? 0) > 0 + ); + + if (activeTerms.length === 0) return null; + + return ( +
+

+ Punto X: {Number(label).toFixed(3)} +

+
+ {activeTerms.map(item => { + if (item.isType2) { + const lower = dataPoint[`${item.term}_lower`] ?? 0; + const upper = dataPoint[`${item.term}_upper`] ?? 0; + const range = Math.abs(upper - lower); + + return range <= 0.001 ? ( + + Pertenencia: {Number(upper).toFixed(3)} + + ) : ( + + Mínimo: {Number(lower).toFixed(3)} + Máximo: {Number(upper).toFixed(3)} + + Incertidumbre: {Number(range).toFixed(3)} + + + ); + } + return ( + + Pertenencia: {Number(dataPoint[item.term]).toFixed(3)} + + ); + })} +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/editor/finalGraph/useGraphData.js b/frontend/src/components/editor/finalGraph/useGraphData.js new file mode 100644 index 0000000..408cdfc --- /dev/null +++ b/frontend/src/components/editor/finalGraph/useGraphData.js @@ -0,0 +1,89 @@ +import { useMemo } from 'react'; +import { CHART_COLORS } from '../../../config'; + +const interpolateY = (x, nodes) => { + if (!nodes || nodes.length === 0) return null; + const EPSILON = 1e-5; + const MICRO_STEP = 0.0001; + const firstX = nodes[0][0]; + const lastX = nodes[nodes.length - 1][0]; + + if (x < firstX - MICRO_STEP - EPSILON) return null; + if (x > lastX + MICRO_STEP + EPSILON) return null; + if (x < firstX - EPSILON) return 0; + if (x > lastX + EPSILON) return 0; + + for (let i = nodes.length - 1; i >= 0; i--) { + if (Math.abs(nodes[i][0] - x) < EPSILON) return nodes[i][1]; + } + + for (let i = 0; i < nodes.length - 1; i++) { + const x1 = nodes[i][0]; + const x2 = nodes[i + 1][0]; + if (Math.abs(x2 - x1) < EPSILON) continue; + if (x >= x1 && x <= x2) { + const y1 = nodes[i][1]; + const y2 = nodes[i + 1][1]; + return y1 + ((x - x1) * (y2 - y1)) / (x2 - x1); + } + } + return null; +}; + +export const useGraphData = (data) => { + const sortedResults = useMemo(() => { + const rawItems = data?.levels || data?.results || []; + const processed = rawItems.map((item, index) => { + const isType2 = !!item.lower && !!item.upper; + const color = CHART_COLORS[index % CHART_COLORS.length] || '#333'; + let termName = item.term || (item.lower && item.lower.term) || `Termino ${index}`; + + if (isType2) { + const lowerNodes = [...(item.lower.left_nodes || []), ...(item.lower.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const upperNodes = [...(item.upper.left_nodes || []), ...(item.upper.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.lower.core) ? Number(item.lower.core[0]) : 0; + return { ...item, term: termName, isType2, lowerNodes, upperNodes, color, coreVal }; + } else { + const nodes = [...(item.left_nodes || []), ...(item.right_nodes || [])].map(n => [Number(n[0]), Number(n[1])]).sort((a,b)=>a[0]-b[0]); + const coreVal = Array.isArray(item.core) ? Number(item.core[0]) : 0; + return { ...item, term: termName, isType2, nodes, color, coreVal }; + } + }); + return processed.sort((a, b) => a.coreVal - b.coreVal); + }, [data]); + + const denseData = useMemo(() => { + const xSet = new Set(); + const steps = 1000; + for (let i = 0; i <= steps; i++) xSet.add(Number((i / steps).toFixed(4))); + + sortedResults.forEach(item => { + const addNodes = (nodes) => nodes.forEach(n => { + const x = n[0]; + xSet.add(Number((x - 0.0001).toFixed(4))); + xSet.add(Number(x.toFixed(4))); + xSet.add(Number((x + 0.0001).toFixed(4))); + }); + item.isType2 ? (addNodes(item.lowerNodes), addNodes(item.upperNodes)) : addNodes(item.nodes); + }); + + const xValues = Array.from(xSet).sort((a, b) => a - b); + return xValues.map(x => { + const point = { x }; + sortedResults.forEach(item => { + if (item.isType2) { + const lowerRaw = interpolateY(x, item.lowerNodes); + const upperRaw = interpolateY(x, item.upperNodes); + point[`${item.term}_lower`] = lowerRaw; + point[`${item.term}_upper`] = upperRaw; + point[`${item.term}_range`] = (lowerRaw === null && upperRaw === null) ? null : [lowerRaw ?? 0, upperRaw ?? 0]; + } else { + point[item.term] = interpolateY(x, item.nodes); + } + }); + return point; + }); + }, [sortedResults]); + + return { sortedResults, denseData }; +}; \ No newline at end of file diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 10529fa..e7067d9 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; import { getUserHistory, deleteHistoryItem } from '../services/docService'; import Step3FinalGraph from '../components/editor/Step3FinalGraph'; @@ -116,12 +116,21 @@ export default function History() {
- {/* Contenido Desplegable (La gráfica) */} - {isExpanded && ( -
- + {/* Contenido Desplegable (La gráfica)*/} +
+
+ {isExpanded ? ( + + ) : ( +
+ )}
- )} +
+
); })} From 2be291ca130033e40087255d98dc28940bab5d6e Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 13 Apr 2026 10:52:28 +0200 Subject: [PATCH 65/80] =?UTF-8?q?add:=20implementar=20footer=20con=20infor?= =?UTF-8?q?maci=C3=B3n=20del=20proyecto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/layout/Footer.jsx | 41 +++++++++++++++++++ frontend/src/components/layout/MainLayout.jsx | 13 +++--- 2 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/layout/Footer.jsx diff --git a/frontend/src/components/layout/Footer.jsx b/frontend/src/components/layout/Footer.jsx new file mode 100644 index 0000000..05091c4 --- /dev/null +++ b/frontend/src/components/layout/Footer.jsx @@ -0,0 +1,41 @@ +import React from 'react'; + +export default function Footer() { + return ( +
+
+
+ + {/* Información del Proyecto */} +
+ + Deck of Cards + + + Herramienta Científica de Modelado Difuso + +
+ + {/* Enlaces y Redes */} + + +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/layout/MainLayout.jsx b/frontend/src/components/layout/MainLayout.jsx index 9ff0053..60dc8f8 100644 --- a/frontend/src/components/layout/MainLayout.jsx +++ b/frontend/src/components/layout/MainLayout.jsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; +import Footer from './Footer'; export default function MainLayout({ children }) { const [isDropdownOpen, setIsDropdownOpen] = useState(false); @@ -23,9 +24,10 @@ export default function MainLayout({ children }) { }; return ( -
+ // IMPORTANTE: flex y flex-col son la clave para que el footer se quede abajo +
{/* HEADER */} -
+
{/* Logo / Título */} @@ -35,8 +37,6 @@ export default function MainLayout({ children }) { alt="Deck of Cards Logo" className="w-10 h-10 shadow-sm rounded-xl object-contain" /> - - {/* Texto del título */} Deck of Cards @@ -129,9 +129,12 @@ export default function MainLayout({ children }) {
{/* CONTENIDO PRINCIPAL */} -
+
{children}
+ + {/* FOOTER */} +
); } \ No newline at end of file From 040989bdfc43c9c9a224554574a28c621cac786a Mon Sep 17 00:00:00 2001 From: Mireya Cueto Garrido Date: Mon, 13 Apr 2026 11:08:14 +0200 Subject: [PATCH 66/80] =?UTF-8?q?A=C3=B1adido=20soporte=20para=20autentica?= =?UTF-8?q?ci=C3=B3n=20con=20Google=20y=20ajustes=20en=20la=20configuraci?= =?UTF-8?q?=C3=B3n=20del=20entorno?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api/main.py | 3 +-- backend/api/routers/google_auth.py | 25 +++++++++++-------------- backend/api/utils/security.py | 4 ++++ docker-compose.yaml | 4 ++-- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/backend/api/main.py b/backend/api/main.py index 639fca8..775db40 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -25,8 +25,6 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) -app.include_router(google_auth_router) - app.add_middleware( CORSMiddleware, allow_origins=["*"], @@ -45,3 +43,4 @@ app.include_router(test_mongo_router, prefix="/api") app.include_router(auth_router, prefix="/api") app.include_router(history_router, prefix="/api") app.include_router(docit2mf_router, prefix="/api") +app.include_router(google_auth_router, prefix="/api") \ No newline at end of file diff --git a/backend/api/routers/google_auth.py b/backend/api/routers/google_auth.py index 44dc4cf..3fec964 100644 --- a/backend/api/routers/google_auth.py +++ b/backend/api/routers/google_auth.py @@ -1,6 +1,6 @@ # api/routers/google_auth.py -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, Request from fastapi.responses import RedirectResponse from pydantic import BaseModel from bson import ObjectId @@ -13,10 +13,9 @@ from api.utils.security import create_access_token router = APIRouter(prefix="/auth/google", tags=["auth"]) - GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID") GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET") -REDIRECT_URI = "http://localhost:8000/api/auth/google/callback" +REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI") # ----------------------------- @@ -25,7 +24,7 @@ REDIRECT_URI = "http://localhost:8000/api/auth/google/callback" @router.get("/login") async def google_login(): google_auth_url = ( - "https://accounts.google.com/o/oauth2/v2/auth" + "https://accounts.google.com/o/oauth2/auth" "?response_type=code" f"&client_id={GOOGLE_CLIENT_ID}" f"&redirect_uri={REDIRECT_URI}" @@ -37,13 +36,17 @@ async def google_login(): return RedirectResponse(google_auth_url) + # ----------------------------- # 2. CALLBACK → GOOGLE DEVUELVE EL CODE # ----------------------------- @router.get("/callback") -async def google_callback(code: str): +async def google_callback(request: Request): + + code = request.query_params.get("code") + if not code: + raise HTTPException(status_code=400, detail="Missing code parameter") - # 1. Intercambiar code por access_token token_url = "https://oauth2.googleapis.com/token" data = { @@ -59,11 +62,10 @@ async def google_callback(code: str): token_json = token_response.json() if "access_token" not in token_json: - raise HTTPException(status_code=400, detail="Error obteniendo token de Google") + raise HTTPException(status_code=400, detail=token_json) access_token = token_json["access_token"] - # 2. Obtener datos del usuario desde Google async with httpx.AsyncClient() as client: userinfo = await client.get( "https://www.googleapis.com/oauth2/v2/userinfo", @@ -76,26 +78,21 @@ async def google_callback(code: str): email = user_data["email"] name = user_data.get("name", "Usuario") - # 3. Buscar usuario en MongoDB user = await users_collection.find_one({"email": email}) if not user: - # Crear usuario nuevo new_user = { "username": name, "email": email, - "password_hash": None, # No hay contraseña + "password_hash": None, "google_id": google_id, "history": [], } - result = await users_collection.insert_one(new_user) user_id = result.inserted_id else: user_id = user["_id"] - # 4. Crear JWT de tu sistema token = create_access_token({"user_id": str(user_id)}) - # 5. Redirigir al frontend con el token return {"message": "Login con Google exitoso", "token": token} diff --git a/backend/api/utils/security.py b/backend/api/utils/security.py index eb3ad3c..f6abe1c 100644 --- a/backend/api/utils/security.py +++ b/backend/api/utils/security.py @@ -4,6 +4,10 @@ from fastapi import Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from api.database.mongodb import users_collection from bson import ObjectId +import os +import jwt + +SECRET_KEY = os.getenv("SECRET_KEY") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") diff --git a/docker-compose.yaml b/docker-compose.yaml index f0fa360..aead62e 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,3 @@ -version: "3.9" - services: backend: build: @@ -11,6 +9,8 @@ services: - ./backend:/app depends_on: - db + env_file: + - backend\.env frontend: build: From ed9c608884e9a6e37cfe866bec261ec1945dedba Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 13 Apr 2026 12:46:15 +0200 Subject: [PATCH 67/80] =?UTF-8?q?refactor:=20mejorar=20y=20expandir=20info?= =?UTF-8?q?rmaci=C3=B3n=20de=20footer=20y=20componentizaci=C3=B3n=20de=20h?= =?UTF-8?q?eader.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/public/uja-logo.png | Bin 0 -> 74499 bytes frontend/src/components/layout/Footer.jsx | 100 ++++++++++--- frontend/src/components/layout/Header.jsx | 80 +++++++++++ frontend/src/components/layout/MainLayout.jsx | 134 +----------------- 4 files changed, 163 insertions(+), 151 deletions(-) create mode 100644 frontend/public/uja-logo.png create mode 100644 frontend/src/components/layout/Header.jsx diff --git a/frontend/public/uja-logo.png b/frontend/public/uja-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..77d17fda40748131e22ddd4eb75ad501fe859837 GIT binary patch literal 74499 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�bS7O`N!80=hY?TB*|QRg;I1TY#Mmv0;~7*g%14_)Sfe?lxV@0A%rkC1mbj;q@a8Io(b?* z6$)UT{MqVI%2p#tz|P+;9-7T%3p)_tV%7$f+1XVa*c3i0?&WvzwAj99{M|^0e zLAPw@eTuk>H?QC0`KiYISib0N@txM&FUq_RW2V+e|AeXaokE{G)_|DJ1yt)jD%dF8 z4#|CBlVcZf3#v0D15fjdsV(E)w@(aDV+nE48cw)kGj`eNRTcdF`71Ry%f`#wrwdJ= zm&r`mFOi0QUmDwev5Yulwkx)ej(hdYuXXxPFi9;F9*~sZyUO*J`D<&PQC|Y|Rq*~2 zf#@N0*RfAWsGsoNzb)FW#0tR{kDF;gL2mU&rlJkBp^yg#t7Nwh&n9N8Kj3}cjJsbM zrXVgUw=VBS1$RkG2;AdWY^yE6m(8+IUo+G(Tlae{;GTZ3drMiaJJzj3&SSZW=-Fr)__rL) z7ENlkOK!!i*8tLzen)MNep8lKa#e5-m|=N9F-KaDOfm2Qdyla@}` z)mi2U!d!Cz!Fl;!oxGDb7`xblmM6#Wy&HQF$5qZuX<@mz`1ZTkH>{|fS5c52-&nu) zcFE$&D&JgN2hSb-MUU7|69Hd!dEr(Y0lYXjNzz^XIs0Y3odyYdaKBU?m#WA8$vWH( z{fzmo!VY|`Moo$YLAy1BbcP+0^xOcbkZa7n` z#vbHttn=Z|MNw^Wes&`&XzVZt|5Ol@G8=2NEceB7dEsAf{$B)lA|MzMBH+4OEH5vV z%i?OZ`hLr9$?f9NQ|TCp#@fJLk5%{&2;{maSzo(0k9L{T!u_707>kcYPDB&G{@_EaAG_FkAkWkn!jn3vXszRp>4|;S zJw=-`wE+p-cA}H#m@bc*ng14|sSyk~`>8WtqQu4}DZv9hP7Ud!OJi>|9@-xm8@e zJHNqu#RzxoAnLq7GgEQtTfS#+T22&r$%k9#qX+KC1-2r;U&GxadC_aje`lI{5^0Xf zRu>UJ<*nj=r;2+%-4wq9{WFmnufcAMFHP^bCvf|X$xPp=;eNxI&S&$@?E@IIuEh5Y z_Yx1x6lY2meSPyygmmC2#iH;YOv2NDzKwa_LHo`hM3L9yZQ)Vhq0Wu9>$BHdXQ!sO zyN)$q6vw%pQNq2AekfBn>v(SJGW22@3u%k#y~XEM$81ZM#EP~Nwm z<R?aSqLU+~b0KveYKV4$Kic)yLeFW>Z_S&BbtU27xG z)=fo?I%-6ubxjvoO(3*?R9BM!37miQK~5e-fE^#^4D;00d`L2 z)#09^fNUz;T;73jArq@7vi*&~y)<*1!n&Z9r|S_khQVNPwtTAy+;6|j%jr0O8^EgQ z&gk{|Hu`GIZ(IACNOpVM#9i;NbsM`T?pJ>{+j~4va<-Pn?WzJkWxhJ?k>5yD*3%-!v zEB_^bhL_Qjn*Z@&@5onWIjQdu&tqYgZmYzF4ynH~*9yUHilAU&tJ~~l#NyM7S6Ip72CJ|A

m#Tr>pH-Tqz?q~gXI{0eGmLFI_qEgd8C6mL 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 literal 0 HcmV?d00001 diff --git a/frontend/src/components/layout/Footer.jsx b/frontend/src/components/layout/Footer.jsx index 05091c4..4bb3a2f 100644 --- a/frontend/src/components/layout/Footer.jsx +++ b/frontend/src/components/layout/Footer.jsx @@ -1,40 +1,94 @@ -import React from 'react'; - export default function Footer() { return ( -