Compare commits
2 Commits
main
...
52d36fe9f6
| Author | SHA1 | Date | |
|---|---|---|---|
| 52d36fe9f6 | |||
| 7782c59278 |
@@ -1,37 +0,0 @@
|
|||||||
name: Deploy to Azure Static Web Apps
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Bun
|
|
||||||
uses: oven-sh/setup-bun@v2
|
|
||||||
with:
|
|
||||||
bun-version: latest
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: bun install
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL }}
|
|
||||||
VITE_SUPABASE_ANON_KEY: ${{ vars.VITE_SUPABASE_ANON_KEY }}
|
|
||||||
run: bunx --bun vite build
|
|
||||||
|
|
||||||
# No hace falta instalar el CLI globalmente, usamos bunx
|
|
||||||
- name: Deploy to Azure Static Web Apps
|
|
||||||
env:
|
|
||||||
AZURE_SWA_DEPLOYMENT_TOKEN: ${{ secrets.AZURE_SWA_DEPLOYMENT_TOKEN }}
|
|
||||||
run: |
|
|
||||||
bunx @azure/static-web-apps-cli deploy ./dist \
|
|
||||||
--env production \
|
|
||||||
--deployment-token "$AZURE_SWA_DEPLOYMENT_TOKEN"
|
|
||||||
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@@ -1 +0,0 @@
|
|||||||
Ignora los problemas de imports de eslint
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,4 +8,3 @@ count.txt
|
|||||||
.nitro
|
.nitro
|
||||||
.tanstack
|
.tanstack
|
||||||
.wrangler
|
.wrangler
|
||||||
diff.txt
|
|
||||||
7
.vscode/extensions.json
vendored
7
.vscode/extensions.json
vendored
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"bradlc.vscode-tailwindcss"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
@@ -2,7 +2,6 @@
|
|||||||
// Use IntelliSense to learn about possible attributes.
|
// Use IntelliSense to learn about possible attributes.
|
||||||
// Hover to view descriptions of existing attributes.
|
// Hover to view descriptions of existing attributes.
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
// close #40
|
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
|
|||||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@@ -22,11 +22,5 @@
|
|||||||
],
|
],
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
"*.css": "tailwindcss"
|
"*.css": "tailwindcss"
|
||||||
},
|
}
|
||||||
"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
||||||
"[javascriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
||||||
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
||||||
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
||||||
"[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
|
||||||
"prettier.requireConfig": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
FROM oven/bun:1 AS build
|
FROM oven/bun:1 AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN bun install
|
RUN bun install --frozen-lockfile
|
||||||
RUN bunx --bun vite build
|
RUN bunx --bun vite build
|
||||||
|
|
||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|||||||
320
bun.lock
320
bun.lock
@@ -4,12 +4,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "acad-ia-2",
|
"name": "acad-ia-2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/react": "^0.3.2",
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
@@ -29,30 +26,22 @@
|
|||||||
"@tanstack/react-router": "^1.132.0",
|
"@tanstack/react-router": "^1.132.0",
|
||||||
"@tanstack/react-router-devtools": "^1.132.0",
|
"@tanstack/react-router-devtools": "^1.132.0",
|
||||||
"@tanstack/router-plugin": "^1.132.0",
|
"@tanstack/router-plugin": "^1.132.0",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
|
||||||
"canvas-confetti": "^1.9.4",
|
|
||||||
"citeproc": "^2.4.63",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.561.0",
|
||||||
"motion": "^12.24.7",
|
|
||||||
"radix-ui": "^1.4.3",
|
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"use-debounce": "^10.1.0",
|
|
||||||
"vaul": "^1.1.2",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/devtools-vite": "^0.3.11",
|
"@tanstack/devtools-vite": "^0.3.11",
|
||||||
"@tanstack/eslint-config": "^0.3.0",
|
"@tanstack/eslint-config": "^0.3.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/bun": "^1.3.6",
|
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^19.2.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
@@ -61,7 +50,6 @@
|
|||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-unused-imports": "^4.3.0",
|
"eslint-plugin-unused-imports": "^4.3.0",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
@@ -75,7 +63,7 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="],
|
"@acemir/cssom": ["@acemir/cssom@0.9.30", "", {}, "sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg=="],
|
||||||
|
|
||||||
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="],
|
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="],
|
||||||
|
|
||||||
@@ -83,23 +71,23 @@
|
|||||||
|
|
||||||
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
|
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
|
||||||
|
|
||||||
"@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="],
|
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||||
|
|
||||||
"@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="],
|
"@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
|
||||||
|
|
||||||
"@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@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" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="],
|
"@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@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" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
|
||||||
|
|
||||||
"@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="],
|
"@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
|
||||||
|
|
||||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "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" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
|
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||||
|
|
||||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||||
|
|
||||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
|
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||||
|
|
||||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
|
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
|
||||||
|
|
||||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="],
|
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||||
|
|
||||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||||
|
|
||||||
@@ -107,25 +95,25 @@
|
|||||||
|
|
||||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||||
|
|
||||||
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="],
|
"@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
|
||||||
|
|
||||||
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="],
|
"@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||||
|
|
||||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="],
|
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
|
||||||
|
|
||||||
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="],
|
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||||
|
|
||||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||||
|
|
||||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
"@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||||
|
|
||||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||||
|
|
||||||
"@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="],
|
"@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
|
||||||
|
|
||||||
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="],
|
"@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||||
|
|
||||||
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
|
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
|
||||||
|
|
||||||
@@ -135,22 +123,10 @@
|
|||||||
|
|
||||||
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
|
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
|
||||||
|
|
||||||
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.26", "", {}, "sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA=="],
|
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.22", "", {}, "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw=="],
|
||||||
|
|
||||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
|
||||||
|
|
||||||
"@dnd-kit/abstract": ["@dnd-kit/abstract@0.3.2", "", { "dependencies": { "@dnd-kit/geometry": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-uvPVK+SZYD6Viddn9M0K0JQdXknuVSxA/EbMlFRanve3P/XTc18oLa5zGftKSGjfQGmuzkZ34E26DSbly1zi3Q=="],
|
|
||||||
|
|
||||||
"@dnd-kit/collision": ["@dnd-kit/collision@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/geometry": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-pNmNSLCI8S9fNQ7QJ3fBCDjiT0sqBhUFcKgmyYaGvGCAU+kq0AP8OWlh0JSisc9k5mFyxmRpmFQcnJpILz/RPA=="],
|
|
||||||
|
|
||||||
"@dnd-kit/dom": ["@dnd-kit/dom@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/collision": "^0.3.2", "@dnd-kit/geometry": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-cIUAVgt2szQyz6JRy7I+0r+xeyOAGH21Y15hb5bIyHoDEaZBvIDH+OOlD9eoLjCbsxDLN9WloU2CBi3OE6LYDg=="],
|
|
||||||
|
|
||||||
"@dnd-kit/geometry": ["@dnd-kit/geometry@0.3.2", "", { "dependencies": { "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" } }, "sha512-3UBPuIS7E3oGiHxOE8h810QA+0pnrnCtGxl4Os1z3yy5YkC/BEYGY+TxWPTQaY1/OMV7GCX7ZNMlama2QN3n3w=="],
|
|
||||||
|
|
||||||
"@dnd-kit/react": ["@dnd-kit/react@0.3.2", "", { "dependencies": { "@dnd-kit/abstract": "^0.3.2", "@dnd-kit/dom": "^0.3.2", "@dnd-kit/state": "^0.3.2", "tslib": "^2.6.2" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-1Opg1xw6I75Z95c+rF2NJa0pdGb8rLAENtuopKtJ1J0PudWlz+P6yL137xy/6DV43uaRmNGtsdbMbR0yRYJ72g=="],
|
|
||||||
|
|
||||||
"@dnd-kit/state": ["@dnd-kit/state@0.3.2", "", { "dependencies": { "@preact/signals-core": "^1.10.0", "tslib": "^2.6.2" } }, "sha512-dLUIkoYrIJhGXfF2wGLTfb46vUokEsO/OoE21TSfmahYrx7ysTmnwbePsznFaHlwgZhQEh6AlLvthLCeY21b1A=="],
|
|
||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
"@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||||
@@ -227,7 +203,7 @@
|
|||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||||
|
|
||||||
"@exodus/bytes": ["@exodus/bytes@1.10.0", "", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-tf8YdcbirXdPnJ+Nd4UN1EXnz+IP2DI45YVEr3vvzcVTOyrApkmIB4zvOQVd3XPr7RXnfBtAx+PXImXOIU0Ajg=="],
|
"@exodus/bytes": ["@exodus/bytes@1.8.0", "", { "peerDependencies": { "@exodus/crypto": "^1.0.0-rc.4" }, "optionalPeers": ["@exodus/crypto"] }, "sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ=="],
|
||||||
|
|
||||||
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||||
|
|
||||||
@@ -263,26 +239,16 @@
|
|||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
"@preact/signals-core": ["@preact/signals-core@1.14.0", "", {}, "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
"@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
|
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
|
||||||
|
|
||||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||||
|
|
||||||
"@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
|
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
|
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
|
||||||
|
|
||||||
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
|
||||||
@@ -291,8 +257,6 @@
|
|||||||
|
|
||||||
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
|
||||||
|
|
||||||
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
|
||||||
|
|
||||||
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||||
@@ -305,24 +269,12 @@
|
|||||||
|
|
||||||
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||||
|
|
||||||
"@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||||
|
|
||||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
||||||
|
|
||||||
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
||||||
|
|
||||||
"@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
||||||
|
|
||||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||||
@@ -333,10 +285,6 @@
|
|||||||
|
|
||||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||||
|
|
||||||
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
||||||
@@ -345,22 +293,10 @@
|
|||||||
|
|
||||||
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
||||||
|
|
||||||
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||||
|
|
||||||
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
@@ -387,55 +323,55 @@
|
|||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="],
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="],
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="],
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="],
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="],
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="],
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="],
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="],
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="],
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="],
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="],
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="],
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="],
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="],
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="],
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="],
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="],
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="],
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="],
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="],
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="],
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="],
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="],
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="],
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="],
|
||||||
|
|
||||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||||
|
|
||||||
@@ -455,19 +391,19 @@
|
|||||||
|
|
||||||
"@stepperize/react": ["@stepperize/react@5.1.9", "", { "dependencies": { "@stepperize/core": "1.2.7" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yBgw1I5Tx6/qZB4xTdVBaPGfTqH5aYS1WFB5vtR8+fwPeqd3YNuOnQ1pJM6w/xV/gvryuy31hbFw080lZc+/hw=="],
|
"@stepperize/react": ["@stepperize/react@5.1.9", "", { "dependencies": { "@stepperize/core": "1.2.7" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yBgw1I5Tx6/qZB4xTdVBaPGfTqH5aYS1WFB5vtR8+fwPeqd3YNuOnQ1pJM6w/xV/gvryuy31hbFw080lZc+/hw=="],
|
||||||
|
|
||||||
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.7.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.53.1", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-zjTUwIsEfT+k9BmXwq1QEFYsb4afBlsI1AXFyWQBgggMzwBFOuu92pGrE5OFx90IOjNl+lUbQoTG7f8S0PkOdg=="],
|
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.6.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.47.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw=="],
|
||||||
|
|
||||||
"@supabase/auth-js": ["@supabase/auth-js@2.93.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-pC0Ek4xk4z6q7A/3+UuZ/eYgfFUUQTg3DhapzrAgJnFGDJDFDyGCj6v9nIz8+3jfLqSZ3QKGe6AoEodYjShghg=="],
|
"@supabase/auth-js": ["@supabase/auth-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-vxb66dgo6h3yyPbR06735Ps+dK3hj0JwS8w9fdQPVZQmocSTlKUW5MfxSy99mN0XqCCuLMQ3jCEiIIUU23e9ng=="],
|
||||||
|
|
||||||
"@supabase/functions-js": ["@supabase/functions-js@2.93.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-Ott2IcIXHGupaC0nX9WNEiJAX4OdlGRu9upkkURaQHbaLdz9JuCcHxlwTERgtgjMpikbIWHfMM1M9QTQFYABiA=="],
|
"@supabase/functions-js": ["@supabase/functions-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-x9mV9dF1Lam9qL3zlpP6mSM5C9iqMPtF5B/tU1Jj/F0ufX5mjDf9ghVBaErVxmrQJRL4+iMKWKY2GnODkpS8tw=="],
|
||||||
|
|
||||||
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.93.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-uRKKQJBDnfi6XFNFPNMh9+u3HT2PCgp065PcMPmG7e0xGuqvLtN89QxO2/SZcGbw2y1+mNBz0yUs5KmyNqF2fA=="],
|
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-jh6vqzaYzoFn3raaC0hcFt9h+Bt+uxNRBSdc7PfToQeRGk7PDPoweHsbdiPWREtDVTGKfu+PyPW9e2jbK+BCgQ=="],
|
||||||
|
|
||||||
"@supabase/realtime-js": ["@supabase/realtime-js@2.93.1", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-2WaP/KVHPlQDjWM6qe4wOZz6zSRGaXw1lfXf4thbfvk3C3zPPKqXRyspyYnk3IhphyxSsJ2hQ/cXNOz48008tg=="],
|
"@supabase/realtime-js": ["@supabase/realtime-js@2.90.1", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-PWbnEMkcQRuor8jhObp4+Snufkq8C6fBp+MchVp2qBPY1NXk/c3Iv3YyiFYVzo0Dzuw4nAlT4+ahuPggy4r32w=="],
|
||||||
|
|
||||||
"@supabase/storage-js": ["@supabase/storage-js@2.93.1", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-3KVwd4S1i1BVPL6KIywe5rnruNQXSkLyvrdiJmwnqwbCcDujQumARdGWBPesqCjOPKEU2M9ORWKAsn+2iLzquA=="],
|
"@supabase/storage-js": ["@supabase/storage-js@2.90.1", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-GHY+Ps/K/RBfRj7kwx+iVf2HIdqOS43rM2iDOIDpapyUnGA9CCBFzFV/XvfzznGykd//z2dkGZhlZZprsVFqGg=="],
|
||||||
|
|
||||||
"@supabase/supabase-js": ["@supabase/supabase-js@2.93.1", "", { "dependencies": { "@supabase/auth-js": "2.93.1", "@supabase/functions-js": "2.93.1", "@supabase/postgrest-js": "2.93.1", "@supabase/realtime-js": "2.93.1", "@supabase/storage-js": "2.93.1" } }, "sha512-FJTgS5s0xEgRQ3u7gMuzGObwf3jA4O5Ki/DgCDXx94w1pihLM4/WG3XFa4BaCJYfuzLxLcv6zPPA5tDvBUjAUg=="],
|
"@supabase/supabase-js": ["@supabase/supabase-js@2.90.1", "", { "dependencies": { "@supabase/auth-js": "2.90.1", "@supabase/functions-js": "2.90.1", "@supabase/postgrest-js": "2.90.1", "@supabase/realtime-js": "2.90.1", "@supabase/storage-js": "2.90.1" } }, "sha512-U8KaKGLUgTIFHtwEW1dgw1gK7XrdpvvYo7nzzqPx721GqPe8WZbAiLh/hmyKLGBYQ/mmQNr20vU9tWSDZpii3w=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
|
||||||
|
|
||||||
@@ -513,41 +449,41 @@
|
|||||||
|
|
||||||
"@tanstack/eslint-config": ["@tanstack/eslint-config@0.3.4", "", { "dependencies": { "@eslint/js": "^9.37.0", "@stylistic/eslint-plugin": "^5.4.0", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-n": "^17.23.1", "globals": "^16.5.0", "typescript-eslint": "^8.46.0", "vue-eslint-parser": "^10.2.0" }, "peerDependencies": { "eslint": "^8.0.0 || ^9.0.0" } }, "sha512-5Ou1XWJRCTx5G8WoCbT7+6nQ4iNdsISzBAc4lXpFy2fEOO7xioOSPvcPIv+r9V0drPPETou2tr6oLGZZ909FKg=="],
|
"@tanstack/eslint-config": ["@tanstack/eslint-config@0.3.4", "", { "dependencies": { "@eslint/js": "^9.37.0", "@stylistic/eslint-plugin": "^5.4.0", "eslint-plugin-import-x": "^4.16.1", "eslint-plugin-n": "^17.23.1", "globals": "^16.5.0", "typescript-eslint": "^8.46.0", "vue-eslint-parser": "^10.2.0" }, "peerDependencies": { "eslint": "^8.0.0 || ^9.0.0" } }, "sha512-5Ou1XWJRCTx5G8WoCbT7+6nQ4iNdsISzBAc4lXpFy2fEOO7xioOSPvcPIv+r9V0drPPETou2tr6oLGZZ909FKg=="],
|
||||||
|
|
||||||
"@tanstack/history": ["@tanstack/history@1.154.14", "", {}, "sha512-xyIfof8eHBuub1CkBnbKNKQXeRZC4dClhmzePHVOEel4G7lk/dW+TQ16da7CFdeNLv6u6Owf5VoBQxoo6DFTSA=="],
|
"@tanstack/history": ["@tanstack/history@1.145.7", "", {}, "sha512-gMo/ReTUp0a3IOcZoI3hH6PLDC2R/5ELQ7P2yu9F6aEkA0wSQh+Q4qzMrtcKvF2ut0oE+16xWCGDo/TdYd6cEQ=="],
|
||||||
|
|
||||||
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
|
"@tanstack/query-core": ["@tanstack/query-core@5.90.16", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="],
|
||||||
|
|
||||||
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.92.0", "", {}, "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ=="],
|
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.92.0", "", {}, "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ=="],
|
||||||
|
|
||||||
"@tanstack/react-devtools": ["@tanstack/react-devtools@0.7.11", "", { "dependencies": { "@tanstack/devtools": "0.7.0" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-a2Lmz8x+JoDrsU6f7uKRcyyY+k8mA/n5mb9h7XJ3Fz/y3+sPV9t7vAW1s5lyNkQyyDt6V1Oim99faLthoJSxMw=="],
|
"@tanstack/react-devtools": ["@tanstack/react-devtools@0.7.11", "", { "dependencies": { "@tanstack/devtools": "0.7.0" }, "peerDependencies": { "@types/react": ">=16.8", "@types/react-dom": ">=16.8", "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-a2Lmz8x+JoDrsU6f7uKRcyyY+k8mA/n5mb9h7XJ3Fz/y3+sPV9t7vAW1s5lyNkQyyDt6V1Oim99faLthoJSxMw=="],
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.90.20", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.90.16", "", { "dependencies": { "@tanstack/query-core": "5.90.16" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ=="],
|
||||||
|
|
||||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.2", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="],
|
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.2", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="],
|
||||||
|
|
||||||
"@tanstack/react-router": ["@tanstack/react-router@1.157.15", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.157.15", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-dVHX3Ann1rxLkXCrB9ctNKveGOrkmlKMo5fDIaaPCqqkDN/aC1gZ9O93i0OQVPUNekpkdXijmpHkxw12WqMTRQ=="],
|
"@tanstack/react-router": ["@tanstack/react-router@1.145.7", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.145.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-0O+a4TjJSPXd2BsvDPwDPBKRQKYqNIBg5TAg9NzCteqJ0NXRxwohyqCksHqCEEtJe/uItwqmHoqkK4q5MDhEsA=="],
|
||||||
|
|
||||||
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.157.15", "", { "dependencies": { "@tanstack/router-devtools-core": "1.157.15" }, "peerDependencies": { "@tanstack/react-router": "^1.157.15", "@tanstack/router-core": "^1.157.15", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-WNxsQaoVz1MDINKbWJ7xGYg0xyG9UAnRq7cYNFypDFyX6gqfiQUTxpFMVZfaw1sv+/fI/6E+hd7WChu1rrfBqQ=="],
|
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.145.7", "", { "dependencies": { "@tanstack/router-devtools-core": "1.145.7" }, "peerDependencies": { "@tanstack/react-router": "^1.145.7", "@tanstack/router-core": "^1.145.7", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-crzHSQ/rcGX7RfuYsmm1XG5quurNMDTIApU7jfwDx5J9HnUxCOSJrbFX0L3w0o0VRCw5xhrL2EdCnW78Ic86hg=="],
|
||||||
|
|
||||||
"@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
|
"@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
|
||||||
|
|
||||||
"@tanstack/router-core": ["@tanstack/router-core@1.157.15", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-KaYz6s+wYcg92kRQ7HXlTJLhBaBXOYiiqRBv5tsRbKRIqqhWNyeGz5+NfDwaYFHg5XLSDs3DvN0elMtxcj4dTg=="],
|
"@tanstack/router-core": ["@tanstack/router-core@1.145.7", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-v6jx6JqVUBM0/FcBq1tX22xiPq8Ufc0PDEP582/4deYoq2/RYd+bZstANp3mGSsqdxE/luhoLYuuSQiwi/j1wA=="],
|
||||||
|
|
||||||
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.157.15", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.157.15", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-udqDYuJUtVfPmk/4yhtOZl1dYlze/rMqaj3v/jQRS8TeGqWYal48Q18hM3A5Bd2YqORvaAkOQsI7JWKYnvxCiQ=="],
|
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.145.7", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.145.7", "csstype": "^3.0.10", "solid-js": ">=1.9.5" }, "optionalPeers": ["csstype"] }, "sha512-oKeq/6QvN49THCh++FJyPv1X65i20qGS4aJHQTNsl4cu1piW1zWUhab2L3DZVr3G8C40FW3xb6hVw92N/fzZbQ=="],
|
||||||
|
|
||||||
"@tanstack/router-generator": ["@tanstack/router-generator@1.157.15", "", { "dependencies": { "@tanstack/router-core": "1.157.15", "@tanstack/router-utils": "1.154.7", "@tanstack/virtual-file-routes": "1.154.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-zGac6tyRFz/X86fk9/CAmS6z8lyZf4p9lhAqLBCKVkFiFPmU4eAJp1ODvs81EtV0uJdRL1/rb+uvgHLGUsmQ0g=="],
|
"@tanstack/router-generator": ["@tanstack/router-generator@1.145.7", "", { "dependencies": { "@tanstack/router-core": "1.145.7", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-xg71c1WTku0ro0rgpJWh3Dt+ognV9qWe2KJHAPzrqfOYdUYu9sGq7Ri4jo8Rk0luXWZrWsrFdBP+9Jx6JH6zWA=="],
|
||||||
|
|
||||||
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.157.15", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.157.15", "@tanstack/router-generator": "1.157.15", "@tanstack/router-utils": "1.154.7", "@tanstack/virtual-file-routes": "1.154.7", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.157.15", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-EpRYRb35//sVJ8GPBhthqfPt9HNhx1xAaejiQ8i4vkG37et6qaSGAO+Woq91WjnpmxMYs4+sNJpGioPuVLBBqQ=="],
|
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.145.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.145.7", "@tanstack/router-generator": "1.145.7", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.145.7", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-Rimo0NragYKHwjoYX9JBLS8VkZD4D/LqzzLIlX9yz93lmWFRu/DbuS7fDZNqX1Ea8naNvo18DlySszYLzC8XDg=="],
|
||||||
|
|
||||||
"@tanstack/router-utils": ["@tanstack/router-utils@1.154.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-61bGx32tMKuEpVRseu2sh1KQe8CfB7793Mch/kyQt0EP3tD7X0sXmimCl3truRiDGUtI0CaSoQV1NPjAII1RBA=="],
|
"@tanstack/router-utils": ["@tanstack/router-utils@1.143.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA=="],
|
||||||
|
|
||||||
"@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
|
"@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
|
||||||
|
|
||||||
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.154.7", "", {}, "sha512-cHHDnewHozgjpI+MIVp9tcib6lYEQK5MyUr0ChHpHFGBl8Xei55rohFK0I0ve/GKoHeioaK42Smd8OixPp6CTg=="],
|
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.145.4", "", {}, "sha512-CI75JrfqSluhdGwLssgVeQBaCphgfkMQpi8MCY3UJX1hoGzXa8kHYJcUuIFMOLs1q7zqHy++EVVtMK03osR5wQ=="],
|
||||||
|
|
||||||
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
"@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
|
||||||
|
|
||||||
"@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
|
"@testing-library/react": ["@testing-library/react@16.3.1", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw=="],
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
@@ -561,10 +497,6 @@
|
|||||||
|
|
||||||
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
|
|
||||||
|
|
||||||
"@types/canvas-confetti": ["@types/canvas-confetti@1.9.0", "", {}, "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg=="],
|
|
||||||
|
|
||||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||||
|
|
||||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||||
@@ -575,35 +507,35 @@
|
|||||||
|
|
||||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="],
|
"@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="],
|
||||||
|
|
||||||
"@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="],
|
"@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
|
"@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="],
|
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="],
|
||||||
|
|
||||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="],
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="],
|
||||||
|
|
||||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="],
|
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="],
|
||||||
|
|
||||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="],
|
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="],
|
||||||
|
|
||||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="],
|
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="],
|
||||||
|
|
||||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="],
|
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
|
"@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="],
|
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="],
|
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="],
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="],
|
||||||
|
|
||||||
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
||||||
|
|
||||||
@@ -703,15 +635,15 @@
|
|||||||
|
|
||||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||||
|
|
||||||
"axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="],
|
"axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="],
|
||||||
|
|
||||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.12", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig=="],
|
"babel-dead-code-elimination": ["babel-dead-code-elimination@1.0.11", "", { "dependencies": { "@babel/core": "^7.23.7", "@babel/parser": "^7.23.6", "@babel/traverse": "^7.23.7", "@babel/types": "^7.23.6" } }, "sha512-mwq3W3e/pKSI6TG8lXMiDWvEi1VXYlSBlJlB3l+I0bAb5u1RNUl88udos85eOPNK3m5EXK9uO7d2g08pesTySQ=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA=="],
|
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
|
||||||
|
|
||||||
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
|
||||||
|
|
||||||
@@ -725,8 +657,6 @@
|
|||||||
|
|
||||||
"browserslist": ["browserslist@4.28.1", "", { "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" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
"browserslist": ["browserslist@4.28.1", "", { "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" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
|
|
||||||
|
|
||||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||||
|
|
||||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||||
@@ -737,9 +667,7 @@
|
|||||||
|
|
||||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="],
|
||||||
|
|
||||||
"canvas-confetti": ["canvas-confetti@1.9.4", "", {}, "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw=="],
|
|
||||||
|
|
||||||
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
|
||||||
|
|
||||||
@@ -751,8 +679,6 @@
|
|||||||
|
|
||||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||||
|
|
||||||
"citeproc": ["citeproc@2.4.63", "", {}, "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q=="],
|
|
||||||
|
|
||||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
@@ -765,7 +691,7 @@
|
|||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
"comment-parser": ["comment-parser@1.4.5", "", {}, "sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw=="],
|
"comment-parser": ["comment-parser@1.4.1", "", {}, "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg=="],
|
||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
@@ -777,7 +703,7 @@
|
|||||||
|
|
||||||
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
|
||||||
|
|
||||||
"cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="],
|
"cssstyle": ["cssstyle@5.3.6", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-legscpSpgSAeGEe0TNcai97DKt9Vd9AsAdOL7Uoetb52Ar/8eJm3LIa39qpv8wWzLFlNG4vVvppQM+teaMPj3A=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
@@ -785,7 +711,7 @@
|
|||||||
|
|
||||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||||
|
|
||||||
"data-urls": ["data-urls@6.0.1", "", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^15.1.0" } }, "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ=="],
|
"data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="],
|
||||||
|
|
||||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||||
|
|
||||||
@@ -813,7 +739,7 @@
|
|||||||
|
|
||||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="],
|
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||||
|
|
||||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
@@ -821,7 +747,7 @@
|
|||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
"electron-to-chromium": ["electron-to-chromium@1.5.279", "", {}, "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg=="],
|
"electron-to-chromium": ["electron-to-chromium@1.5.267", "", {}, "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
@@ -873,9 +799,7 @@
|
|||||||
|
|
||||||
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
|
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
|
||||||
|
|
||||||
"eslint-plugin-n": ["eslint-plugin-n@17.23.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.5.0", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", "globals": "^15.11.0", "globrex": "^0.1.2", "ignore": "^5.3.2", "semver": "^7.6.3", "ts-declaration-location": "^1.0.6" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-RhWBeb7YVPmNa2eggvJooiuehdL76/bbfj/OJewyoGT80qn5PXdz8zMOTO6YHOsI7byPt7+Ighh/i/4a5/v7hw=="],
|
"eslint-plugin-n": ["eslint-plugin-n@17.23.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.5.0", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", "globals": "^15.11.0", "globrex": "^0.1.2", "ignore": "^5.3.2", "semver": "^7.6.3", "ts-declaration-location": "^1.0.6" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-68PealUpYoHOBh332JLLD9Sj7OQUDkFpmcfqt8R9sySfFSeuGJjMTJQvCRRB96zO3A/PELRLkPrzsHmzEFQQ5A=="],
|
||||||
|
|
||||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "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" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
|
||||||
|
|
||||||
"eslint-plugin-unused-imports": ["eslint-plugin-unused-imports@4.3.0", "", { "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin"] }, "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA=="],
|
"eslint-plugin-unused-imports": ["eslint-plugin-unused-imports@4.3.0", "", { "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^9.0.0 || ^8.0.0" }, "optionalPeers": ["@typescript-eslint/eslint-plugin"] }, "sha512-ZFBmXMGBYfHttdRtOG9nFFpmUvMtbHSjsKrS20vdWdbfiVYsO3yA2SGYy9i9XmZJDfMGBflZGBCm70SEnFQtOA=="],
|
||||||
|
|
||||||
@@ -923,8 +847,6 @@
|
|||||||
|
|
||||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||||
|
|
||||||
"framer-motion": ["framer-motion@12.29.2", "", { "dependencies": { "motion-dom": "^12.29.2", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lSNRzBJk4wuIy0emYQ/nfZ7eWhqud2umPKw2QAQki6uKhZPKm2hRQHeQoHTG9MIvfobb+A/LbEWPJU794ZUKrg=="],
|
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
@@ -975,10 +897,6 @@
|
|||||||
|
|
||||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
"hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
|
|
||||||
|
|
||||||
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
|
||||||
|
|
||||||
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||||
|
|
||||||
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
|
||||||
@@ -1053,7 +971,7 @@
|
|||||||
|
|
||||||
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
"isbot": ["isbot@5.1.34", "", {}, "sha512-aCMIBSKd/XPRYdiCQTLC8QHH4YT8B3JUADu+7COgYIZPvkeoMcUHMRjZLM9/7V8fCj+l7FSREc1lOPNjzogo/A=="],
|
"isbot": ["isbot@5.1.32", "", {}, "sha512-VNfjM73zz2IBZmdShMfAUg10prm6t7HFUQmNAEOAVS4YH92ZrZcvkMcGX6cIgBJAzWDzPent/EeAtYEHNPNPBQ=="],
|
||||||
|
|
||||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
@@ -1117,9 +1035,9 @@
|
|||||||
|
|
||||||
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
"loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@11.2.5", "", {}, "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw=="],
|
"lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
|
"lucide-react": ["lucide-react@0.561.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A=="],
|
||||||
|
|
||||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||||
|
|
||||||
@@ -1137,12 +1055,6 @@
|
|||||||
|
|
||||||
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="],
|
||||||
|
|
||||||
"motion": ["motion@12.29.2", "", { "dependencies": { "framer-motion": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-jMpHdAzEDF1QQ055cB+1lOBLdJ6ialVWl6QQzpJI2OvmHequ7zFVHM2mx0HNAy+Tu4omUlApfC+4vnkX0geEOg=="],
|
|
||||||
|
|
||||||
"motion-dom": ["motion-dom@12.29.2", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-/k+NuycVV8pykxyiTCoFzIVLA95Nb1BFIVvfSu9L50/6K6qNeAYtkxXILy/LRutt7AzaYDc2myj0wkCVVYAPPA=="],
|
|
||||||
|
|
||||||
"motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
|
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
@@ -1205,7 +1117,7 @@
|
|||||||
|
|
||||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
"prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
|
||||||
|
|
||||||
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="],
|
"prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="],
|
||||||
|
|
||||||
@@ -1215,8 +1127,6 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
"radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
|
|
||||||
|
|
||||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||||
@@ -1249,7 +1159,7 @@
|
|||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="],
|
"rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="],
|
||||||
|
|
||||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||||
|
|
||||||
@@ -1263,9 +1173,9 @@
|
|||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
"seroval": ["seroval@1.5.0", "", {}, "sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw=="],
|
"seroval": ["seroval@1.4.2", "", {}, "sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ=="],
|
||||||
|
|
||||||
"seroval-plugins": ["seroval-plugins@1.5.0", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA=="],
|
"seroval-plugins": ["seroval-plugins@1.4.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-X7p4MEDTi+60o2sXZ4bnDBhgsUYDSkQEvzYZuJyFqWg9jcoPsHts5nrg5O956py2wyt28lUrBxk0M0/wU8URpA=="],
|
||||||
|
|
||||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||||
|
|
||||||
@@ -1291,7 +1201,7 @@
|
|||||||
|
|
||||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|
||||||
"solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="],
|
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
|
||||||
|
|
||||||
@@ -1319,7 +1229,7 @@
|
|||||||
|
|
||||||
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
|
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
|
||||||
|
|
||||||
"supabase": ["supabase@2.72.8", "", { "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.5.3" }, "bin": { "supabase": "bin/supabase" } }, "sha512-3Wymv/QjmndLB9ACQA31VvJ7+KXmDqj7s8g7y+ldAcCaHBMbj+I7x0j/UBGkNbtSh0BG7kRicGA3Xc3jQlccNQ=="],
|
"supabase": ["supabase@2.72.2", "", { "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.5.2" }, "bin": { "supabase": "bin/supabase" } }, "sha512-vnLeNqLGJnuqs4XkE1dYCGF6dSiRV4/BGExUEexUnyEhiR9+VDvTVt1/LM45hFbG4WF2hiVgePu5JDuwcpY8Tg=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
@@ -1333,7 +1243,7 @@
|
|||||||
|
|
||||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||||
|
|
||||||
"tar": ["tar@7.5.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-ENg5JUHUm2rDD7IvKNFGzyElLXNjachNLp6RaGf4+JOgxXHkqA+gq81ZAMCUmtMtqBsoU62lcp6S27g1LCYGGQ=="],
|
"tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="],
|
||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
@@ -1385,7 +1295,7 @@
|
|||||||
|
|
||||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="],
|
"typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="],
|
||||||
|
|
||||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||||
|
|
||||||
@@ -1401,15 +1311,11 @@
|
|||||||
|
|
||||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||||
|
|
||||||
"use-debounce": ["use-debounce@10.1.0", "", { "peerDependencies": { "react": "*" } }, "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg=="],
|
|
||||||
|
|
||||||
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
|
||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
"vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
|
"vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
|
||||||
|
|
||||||
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
|
||||||
|
|
||||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||||
|
|
||||||
@@ -1439,7 +1345,7 @@
|
|||||||
|
|
||||||
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||||
|
|
||||||
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
|
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||||
|
|
||||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||||
|
|
||||||
@@ -1457,9 +1363,7 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||||
|
|
||||||
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
|
|
||||||
|
|
||||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
@@ -1477,8 +1381,6 @@
|
|||||||
|
|
||||||
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
@@ -1491,8 +1393,6 @@
|
|||||||
|
|
||||||
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
"@radix-ui/react-toolbar/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
|
||||||
|
|
||||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
@@ -1509,10 +1409,6 @@
|
|||||||
|
|
||||||
"@tanstack/devtools/@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.3", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3" } }, "sha512-kl0r6N5iIL3t9gGDRAv55VRM3UIyMKVH83esRGq7xBjYsRLe/BeCIN2HqrlJkObUXQMKhy7i8ejuGOn+bDqDBw=="],
|
"@tanstack/devtools/@tanstack/devtools-client": ["@tanstack/devtools-client@0.0.3", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3" } }, "sha512-kl0r6N5iIL3t9gGDRAv55VRM3UIyMKVH83esRGq7xBjYsRLe/BeCIN2HqrlJkObUXQMKhy7i8ejuGOn+bDqDBw=="],
|
||||||
|
|
||||||
"@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
||||||
|
|
||||||
"@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||||
@@ -1525,8 +1421,6 @@
|
|||||||
|
|
||||||
"cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
"data-urls/whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
|
|
||||||
|
|
||||||
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"eslint-compat-utils/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"eslint-compat-utils/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
@@ -1549,18 +1443,14 @@
|
|||||||
|
|
||||||
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
"radix-ui/@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
|
|
||||||
|
|
||||||
"radix-ui/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
|
||||||
|
|
||||||
"radix-ui/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
|
||||||
|
|
||||||
"radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
|
||||||
|
|
||||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
|
"solid-js/seroval": ["seroval@1.3.2", "", {}, "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ=="],
|
||||||
|
|
||||||
|
"solid-js/seroval-plugins": ["seroval-plugins@1.3.3", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-16OL3NnUBw8JG1jBLUoZJsLnQq0n5Ua6aHalhJK4fMQkz1lqR7Osz1sA30trBtd9VUDc2NgkuRCn8+/pBwqZ+w=="],
|
||||||
|
|
||||||
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
"strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||||
|
|
||||||
"tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
"tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||||
|
|||||||
@@ -17,11 +17,5 @@
|
|||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
},
|
},
|
||||||
"iconLibrary": "lucide",
|
"iconLibrary": "lucide"
|
||||||
"registries": {
|
|
||||||
"@shadcn-studio": "https://shadcnstudio.com/r/{name}.json",
|
|
||||||
"@ss-components": "https://shadcnstudio.com/r/components/{name}.json",
|
|
||||||
"@ss-blocks": "https://shadcnstudio.com/r/blocks/{name}.json",
|
|
||||||
"@ss-themes": "https://shadcnstudio.com/r/themes/{name}.json"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import { tanstackConfig } from '@tanstack/eslint-config'
|
import { tanstackConfig } from '@tanstack/eslint-config'
|
||||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||||
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
import jsxA11y from 'eslint-plugin-jsx-a11y'
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import unusedImports from 'eslint-plugin-unused-imports'
|
import unusedImports from 'eslint-plugin-unused-imports'
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
@@ -25,12 +24,9 @@ export default [
|
|||||||
|
|
||||||
// 3. TUS REGLAS Y CONFIGURACIÓN "PRO"
|
// 3. TUS REGLAS Y CONFIGURACIÓN "PRO"
|
||||||
{
|
{
|
||||||
// Opcional: Puedes ser explícito sobre dónde aplicar esto
|
|
||||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
|
||||||
plugins: {
|
plugins: {
|
||||||
'jsx-a11y': jsxA11y,
|
'jsx-a11y': jsxA11y,
|
||||||
'unused-imports': unusedImports,
|
'unused-imports': unusedImports,
|
||||||
'react-hooks': reactHooks,
|
|
||||||
},
|
},
|
||||||
// Configuración robusta del Resolver (La versión de Copilot)
|
// Configuración robusta del Resolver (La versión de Copilot)
|
||||||
settings: {
|
settings: {
|
||||||
@@ -48,8 +44,7 @@ export default [
|
|||||||
// --- REGLAS DE ACCESIBILIDAD (A11Y) ---
|
// --- REGLAS DE ACCESIBILIDAD (A11Y) ---
|
||||||
// Activamos las recomendadas manualmente
|
// Activamos las recomendadas manualmente
|
||||||
...jsxA11y.configs.recommended.rules,
|
...jsxA11y.configs.recommended.rules,
|
||||||
'react-hooks/rules-of-hooks': 'error',
|
|
||||||
'react-hooks/exhaustive-deps': 'warn',
|
|
||||||
// --- ORDEN DE IMPORTS ---
|
// --- ORDEN DE IMPORTS ---
|
||||||
'sort-imports': 'off', // Apagamos el nativo
|
'sort-imports': 'off', // Apagamos el nativo
|
||||||
'import/order': [
|
'import/order': [
|
||||||
|
|||||||
18
package.json
18
package.json
@@ -17,12 +17,9 @@
|
|||||||
"ci:verify": "prettier --check . && eslint . && tsc --noEmit"
|
"ci:verify": "prettier --check . && eslint . && tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/react": "^0.3.2",
|
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
|
||||||
"@radix-ui/react-collapsible": "^1.1.12",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-context-menu": "^2.2.16",
|
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
@@ -42,30 +39,22 @@
|
|||||||
"@tanstack/react-router": "^1.132.0",
|
"@tanstack/react-router": "^1.132.0",
|
||||||
"@tanstack/react-router-devtools": "^1.132.0",
|
"@tanstack/react-router-devtools": "^1.132.0",
|
||||||
"@tanstack/router-plugin": "^1.132.0",
|
"@tanstack/router-plugin": "^1.132.0",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
|
||||||
"canvas-confetti": "^1.9.4",
|
|
||||||
"citeproc": "^2.4.63",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.561.0",
|
||||||
"motion": "^12.24.7",
|
|
||||||
"radix-ui": "^1.4.3",
|
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6"
|
||||||
"use-debounce": "^10.1.0",
|
|
||||||
"vaul": "^1.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/devtools-vite": "^0.3.11",
|
"@tanstack/devtools-vite": "^0.3.11",
|
||||||
"@tanstack/eslint-config": "^0.3.0",
|
"@tanstack/eslint-config": "^0.3.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/bun": "^1.3.6",
|
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^19.2.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
@@ -74,7 +63,6 @@
|
|||||||
"eslint-import-resolver-typescript": "^4.4.4",
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
"eslint-plugin-import": "^2.32.0",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-unused-imports": "^4.3.0",
|
"eslint-plugin-unused-imports": "^4.3.0",
|
||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
|
|||||||
@@ -1,757 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<locale xmlns="http://purl.org/net/xbiblio/csl" version="1.0" xml:lang="es-MX">
|
|
||||||
<info>
|
|
||||||
<translator>
|
|
||||||
<name>Juan Ignacio Flores Salgado</name>
|
|
||||||
<uri>https://www.mendeley.com/profiles/juan-ignacio-flores-salgado/</uri>
|
|
||||||
</translator>
|
|
||||||
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
|
||||||
<updated>2025-10-16T03:24:00+00:00</updated>
|
|
||||||
</info>
|
|
||||||
<style-options punctuation-in-quote="false"/>
|
|
||||||
<date form="text">
|
|
||||||
<date-part name="day" prefix="el " suffix=" de "/>
|
|
||||||
<date-part name="month" suffix=" de "/>
|
|
||||||
<date-part name="year"/>
|
|
||||||
</date>
|
|
||||||
<date form="numeric">
|
|
||||||
<date-part name="day" form="numeric-leading-zeros" suffix="/"/>
|
|
||||||
<date-part name="month" form="numeric-leading-zeros" suffix="/"/>
|
|
||||||
<date-part name="year"/>
|
|
||||||
</date>
|
|
||||||
<terms>
|
|
||||||
<!-- LONG GENERAL TERMS -->
|
|
||||||
<term name="accessed">consultado</term>
|
|
||||||
<term name="advance-online-publication">advance online publication</term>
|
|
||||||
<term name="album">album</term>
|
|
||||||
<term name="and">y</term>
|
|
||||||
<term name="and others">et al.</term>
|
|
||||||
<term name="anonymous">anónimo</term>
|
|
||||||
<term name="at">en</term>
|
|
||||||
<term name="audio-recording">audio recording</term>
|
|
||||||
<term name="available at">disponible en</term>
|
|
||||||
<term name="by">de</term>
|
|
||||||
<term name="circa">circa</term>
|
|
||||||
<term name="cited">citado</term>
|
|
||||||
<term name="et-al">et al.</term>
|
|
||||||
<term name="film">film</term>
|
|
||||||
<term name="forthcoming">en preparación</term>
|
|
||||||
<term name="from">a partir de</term>
|
|
||||||
<term name="henceforth">henceforth</term>
|
|
||||||
<term name="ibid">ibid.</term>
|
|
||||||
<term name="in">en</term>
|
|
||||||
<term name="in press">en imprenta</term>
|
|
||||||
<term name="internet">internet</term>
|
|
||||||
<term name="letter">carta</term>
|
|
||||||
<term name="loc-cit">loc. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
|
|
||||||
<term name="no date">sin fecha</term>
|
|
||||||
<term name="no-place">no place</term>
|
|
||||||
<term name="no-publisher">no publisher</term> <!-- sine nomine -->
|
|
||||||
<term name="on">on</term>
|
|
||||||
<term name="online">en línea</term>
|
|
||||||
<term name="op-cit">op. cit.</term> <!-- like ibid., the abbreviated form is the regular form -->
|
|
||||||
<term name="original-work-published">obra original publicada en</term>
|
|
||||||
<term name="personal-communication">comunicación personal</term>
|
|
||||||
<term name="podcast">podcast</term>
|
|
||||||
<term name="podcast-episode">podcast episode</term>
|
|
||||||
<term name="preprint">preprint</term>
|
|
||||||
<term name="presented at">presentado en</term>
|
|
||||||
<term name="radio-broadcast">radio broadcast</term>
|
|
||||||
<term name="radio-series">radio series</term>
|
|
||||||
<term name="radio-series-episode">radio series episode</term>
|
|
||||||
<term name="reference">
|
|
||||||
<single>referencia</single>
|
|
||||||
<multiple>referencias</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="retrieved">recuperado</term>
|
|
||||||
<term name="review-of">review of</term>
|
|
||||||
<term name="scale">escala</term>
|
|
||||||
<term name="special-issue">special issue</term>
|
|
||||||
<term name="special-section">special section</term>
|
|
||||||
<term name="television-broadcast">television broadcast</term>
|
|
||||||
<term name="television-series">television series</term>
|
|
||||||
<term name="television-series-episode">television series episode</term>
|
|
||||||
<term name="video">video</term>
|
|
||||||
<term name="working-paper">working paper</term>
|
|
||||||
|
|
||||||
<!-- SHORT GENERAL TERMS -->
|
|
||||||
<term name="anonymous" form="short">anón.</term>
|
|
||||||
<term name="circa" form="short">c.</term>
|
|
||||||
<term name="no date" form="short">s/f</term>
|
|
||||||
<term name="no-place" form="short">n.p.</term>
|
|
||||||
<term name="no-publisher" form="short">n.p.</term>
|
|
||||||
<term name="reference" form="short">
|
|
||||||
<single>ref.</single>
|
|
||||||
<multiple>refs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="review-of" form="short">rev. of</term>
|
|
||||||
|
|
||||||
<!-- SYMBOLIC GENERAL FORMS -->
|
|
||||||
|
|
||||||
<!-- LONG ITEM TYPE FORMS -->
|
|
||||||
<term name="article">preprint</term>
|
|
||||||
<term name="article-journal">journal article</term>
|
|
||||||
<term name="article-magazine">magazine article</term>
|
|
||||||
<term name="article-newspaper">newspaper article</term>
|
|
||||||
<term name="bill">bill</term>
|
|
||||||
<!-- book is in the list of locator terms -->
|
|
||||||
<term name="broadcast">broadcast</term>
|
|
||||||
<!-- chapter is in the list of locator terms -->
|
|
||||||
<term name="classic">classic</term>
|
|
||||||
<term name="collection">collection</term>
|
|
||||||
<term name="dataset">dataset</term>
|
|
||||||
<term name="document">document</term>
|
|
||||||
<term name="entry">entry</term>
|
|
||||||
<term name="entry-dictionary">dictionary entry</term>
|
|
||||||
<term name="entry-encyclopedia">encyclopedia entry</term>
|
|
||||||
<term name="event">event</term>
|
|
||||||
<!-- figure is in the list of locator terms -->
|
|
||||||
<term name="graphic">graphic</term>
|
|
||||||
<term name="hearing">hearing</term>
|
|
||||||
<term name="interview">entrevista</term>
|
|
||||||
<term name="legal_case">legal case</term>
|
|
||||||
<term name="legislation">legislation</term>
|
|
||||||
<term name="manuscript">manuscript</term>
|
|
||||||
<term name="map">map</term>
|
|
||||||
<term name="motion_picture">video recording</term>
|
|
||||||
<term name="musical_score">musical score</term>
|
|
||||||
<term name="pamphlet">pamphlet</term>
|
|
||||||
<term name="paper-conference">conference paper</term>
|
|
||||||
<term name="patent">patent</term>
|
|
||||||
<term name="performance">performance</term>
|
|
||||||
<term name="periodical">periodical</term>
|
|
||||||
<term name="personal_communication">comunicación personal</term>
|
|
||||||
<term name="post">post</term>
|
|
||||||
<term name="post-weblog">blog post</term>
|
|
||||||
<term name="regulation">regulation</term>
|
|
||||||
<term name="report">report</term>
|
|
||||||
<term name="review">review</term>
|
|
||||||
<term name="review-book">book review</term>
|
|
||||||
<term name="software">software</term>
|
|
||||||
<term name="song">audio recording</term>
|
|
||||||
<term name="speech">presentation</term>
|
|
||||||
<term name="standard">standard</term>
|
|
||||||
<term name="thesis">thesis</term>
|
|
||||||
<term name="treaty">treaty</term>
|
|
||||||
<term name="webpage">webpage</term>
|
|
||||||
|
|
||||||
<!-- SHORT ITEM TYPE FORMS -->
|
|
||||||
<term name="article-journal" form="short">journal art.</term>
|
|
||||||
<term name="article-magazine" form="short">mag. art.</term>
|
|
||||||
<term name="article-newspaper" form="short">newspaper art.</term>
|
|
||||||
<!-- book is in the list of locator terms -->
|
|
||||||
<!-- chapter is in the list of locator terms -->
|
|
||||||
<term name="document" form="short">doc.</term>
|
|
||||||
<!-- figure is in the list of locator terms -->
|
|
||||||
<term name="graphic" form="short">graph.</term>
|
|
||||||
<term name="interview" form="short">interv.</term>
|
|
||||||
<term name="manuscript" form="short">MS</term>
|
|
||||||
<term name="motion_picture" form="short">video rec.</term>
|
|
||||||
<term name="report" form="short">rep.</term>
|
|
||||||
<term name="review" form="short">rev.</term>
|
|
||||||
<term name="review-book" form="short">bk. rev.</term>
|
|
||||||
<term name="song" form="short">audio rec.</term>
|
|
||||||
|
|
||||||
<!-- LONG VERB ITEM TYPE FORMS -->
|
|
||||||
<!-- Only where applicable -->
|
|
||||||
<term name="hearing" form="verb">testimony of</term>
|
|
||||||
<term name="review" form="verb">review of</term>
|
|
||||||
<term name="review-book" form="verb">review of the book</term>
|
|
||||||
|
|
||||||
<!-- SHORT VERB ITEM TYPE FORMS -->
|
|
||||||
|
|
||||||
<!-- HISTORICAL ERA TERMS -->
|
|
||||||
<term name="ad">d. C.</term>
|
|
||||||
<term name="bc">a. C.</term>
|
|
||||||
<term name="bce">BCE</term>
|
|
||||||
<term name="ce">CE</term>
|
|
||||||
|
|
||||||
<!-- PUNCTUATION -->
|
|
||||||
<term name="open-quote">“</term>
|
|
||||||
<term name="close-quote">”</term>
|
|
||||||
<term name="open-inner-quote">‘</term>
|
|
||||||
<term name="close-inner-quote">’</term>
|
|
||||||
<term name="page-range-delimiter">–</term>
|
|
||||||
<term name="colon">:</term>
|
|
||||||
<term name="comma">,</term>
|
|
||||||
<term name="semicolon">;</term>
|
|
||||||
|
|
||||||
<!-- ORDINALS -->
|
|
||||||
<term name="ordinal">a</term>
|
|
||||||
<term name="ordinal-01" gender-form="feminine" match="whole-number">a</term>
|
|
||||||
<term name="ordinal-01" gender-form="masculine" match="whole-number">o</term>
|
|
||||||
|
|
||||||
<!-- LONG ORDINALS -->
|
|
||||||
<term name="long-ordinal-01">primera</term>
|
|
||||||
<term name="long-ordinal-02">segunda</term>
|
|
||||||
<term name="long-ordinal-03">tercera</term>
|
|
||||||
<term name="long-ordinal-04">cuarta</term>
|
|
||||||
<term name="long-ordinal-05">quinta</term>
|
|
||||||
<term name="long-ordinal-06">sexta</term>
|
|
||||||
<term name="long-ordinal-07">séptima</term>
|
|
||||||
<term name="long-ordinal-08">octava</term>
|
|
||||||
<term name="long-ordinal-09">novena</term>
|
|
||||||
<term name="long-ordinal-10">décima</term>
|
|
||||||
|
|
||||||
<!-- LONG LOCATOR FORMS -->
|
|
||||||
<term name="act">
|
|
||||||
<single>act</single>
|
|
||||||
<multiple>acts</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="appendix">
|
|
||||||
<single>appendix</single>
|
|
||||||
<multiple>appendices</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="article-locator">
|
|
||||||
<single>article</single>
|
|
||||||
<multiple>articles</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="book">
|
|
||||||
<single>libro</single>
|
|
||||||
<multiple>libros</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="canon">
|
|
||||||
<single>canon</single>
|
|
||||||
<multiple>canons</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="chapter">
|
|
||||||
<single>capítulo</single>
|
|
||||||
<multiple>capítulos</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="column">
|
|
||||||
<single>columna</single>
|
|
||||||
<multiple>columnas</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="elocation">
|
|
||||||
<single>location</single>
|
|
||||||
<multiple>locations</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="equation">
|
|
||||||
<single>equation</single>
|
|
||||||
<multiple>equations</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="figure">
|
|
||||||
<single>figura</single>
|
|
||||||
<multiple>figuras</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="folio">
|
|
||||||
<single>folio</single>
|
|
||||||
<multiple>folios</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="issue">
|
|
||||||
<single>número</single>
|
|
||||||
<multiple>números</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="line">
|
|
||||||
<single>línea</single>
|
|
||||||
<multiple>líneas</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="note">
|
|
||||||
<single>nota</single>
|
|
||||||
<multiple>notas</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="opus">
|
|
||||||
<single>opus</single>
|
|
||||||
<multiple>opera</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="page">
|
|
||||||
<single>página</single>
|
|
||||||
<multiple>páginas</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="paragraph">
|
|
||||||
<single>párrafo</single>
|
|
||||||
<multiple>párrafos</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="part">
|
|
||||||
<single>parte</single>
|
|
||||||
<multiple>partes</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="rule">
|
|
||||||
<single>rule</single>
|
|
||||||
<multiple>rules</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="scene">
|
|
||||||
<single>scene</single>
|
|
||||||
<multiple>scenes</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="section">
|
|
||||||
<single>sección</single>
|
|
||||||
<multiple>secciones</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="sub-verbo">
|
|
||||||
<single>sub voce</single>
|
|
||||||
<multiple>sub vocibus</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="supplement">
|
|
||||||
<single>supplement</single>
|
|
||||||
<multiple>supplements</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="table">
|
|
||||||
<single>table</single>
|
|
||||||
<multiple>tables</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="timestamp"> <!-- generally blank -->
|
|
||||||
<single/>
|
|
||||||
<multiple/>
|
|
||||||
</term>
|
|
||||||
<term name="title-locator">
|
|
||||||
<single>title</single>
|
|
||||||
<multiple>titles</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="verse">
|
|
||||||
<single>verso</single>
|
|
||||||
<multiple>versos</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="volume">
|
|
||||||
<single>volumen</single>
|
|
||||||
<multiple>volúmenes</multiple>
|
|
||||||
</term>
|
|
||||||
|
|
||||||
<!-- SHORT LOCATOR FORMS -->
|
|
||||||
<term name="appendix" form="short">
|
|
||||||
<single>app.</single>
|
|
||||||
<multiple>apps.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="article-locator" form="short">
|
|
||||||
<single>art.</single>
|
|
||||||
<multiple>arts.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="book" form="short">
|
|
||||||
<single>lib.</single>
|
|
||||||
<multiple>libs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="chapter" form="short">
|
|
||||||
<single>cap.</single>
|
|
||||||
<multiple>caps.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="column" form="short">
|
|
||||||
<single>col.</single>
|
|
||||||
<multiple>cols.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="elocation" form="short">
|
|
||||||
<single>loc.</single>
|
|
||||||
<multiple>locs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="equation" form="short">
|
|
||||||
<single>eq.</single>
|
|
||||||
<multiple>eqs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="figure" form="short">
|
|
||||||
<single>fig.</single>
|
|
||||||
<multiple>figs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="folio" form="short">
|
|
||||||
<single>f.</single>
|
|
||||||
<multiple>ff.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="issue" form="short">
|
|
||||||
<single>núm.</single>
|
|
||||||
<multiple>núms.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="line" form="short">
|
|
||||||
<single>l.</single>
|
|
||||||
<multiple>ls.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="note" form="short">
|
|
||||||
<single>n.</single>
|
|
||||||
<multiple>nn.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="opus" form="short">
|
|
||||||
<single>op.</single>
|
|
||||||
<multiple>opp.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="page" form="short">
|
|
||||||
<single>p.</single>
|
|
||||||
<multiple>pp.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="paragraph" form="short">
|
|
||||||
<single>párr.</single>
|
|
||||||
<multiple>párrs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="part" form="short">
|
|
||||||
<single>pt.</single>
|
|
||||||
<multiple>pts.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="rule" form="short">
|
|
||||||
<single>r.</single>
|
|
||||||
<multiple>rr.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="scene" form="short">
|
|
||||||
<single>sc.</single>
|
|
||||||
<multiple>scs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="section" form="short">
|
|
||||||
<single>sec.</single>
|
|
||||||
<multiple>secs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="sub-verbo" form="short">
|
|
||||||
<single>s. v.</single>
|
|
||||||
<multiple>s. vv.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="supplement" form="short">
|
|
||||||
<single>supp.</single>
|
|
||||||
<multiple>supps.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="table" form="short">
|
|
||||||
<single>tbl.</single>
|
|
||||||
<multiple>tbls.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="timestamp" form="short"> <!-- generally blank -->
|
|
||||||
<single/>
|
|
||||||
<multiple/>
|
|
||||||
</term>
|
|
||||||
<term name="title-locator" form="short">
|
|
||||||
<single>tit.</single>
|
|
||||||
<multiple>tits.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="verse" form="short">
|
|
||||||
<single>v.</single>
|
|
||||||
<multiple>vv.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="volume" form="short">
|
|
||||||
<single>vol.</single>
|
|
||||||
<multiple>vols.</multiple>
|
|
||||||
</term>
|
|
||||||
|
|
||||||
<!-- SYMBOLIC LOCATOR FORMS -->
|
|
||||||
<term name="paragraph" form="symbol">
|
|
||||||
<single>¶</single>
|
|
||||||
<multiple>¶</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="section" form="symbol">
|
|
||||||
<single>§</single>
|
|
||||||
<multiple>§</multiple>
|
|
||||||
</term>
|
|
||||||
|
|
||||||
<!-- LONG NUMBER VARIABLE FORMS -->
|
|
||||||
<term name="chapter-number">
|
|
||||||
<single>chapter</single>
|
|
||||||
<multiple>chapters</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="citation-number">
|
|
||||||
<single>citation</single>
|
|
||||||
<multiple>citations</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="collection-number">
|
|
||||||
<single>número</single>
|
|
||||||
<multiple>números</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="edition">
|
|
||||||
<single>edición</single>
|
|
||||||
<multiple>ediciones</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="first-reference-note-number">
|
|
||||||
<single>reference</single>
|
|
||||||
<multiple>references</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="number">
|
|
||||||
<single>number</single>
|
|
||||||
<multiple>numbers</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="number-of-pages">
|
|
||||||
<single>página</single>
|
|
||||||
<multiple>páginas</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="number-of-volumes">
|
|
||||||
<single>volume</single>
|
|
||||||
<multiple>volumes</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="page-first">
|
|
||||||
<single>page</single>
|
|
||||||
<multiple>pages</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="printing">
|
|
||||||
<single>printing</single>
|
|
||||||
<multiple>printings</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="version">versión</term>
|
|
||||||
|
|
||||||
<!-- SHORT NUMBER VARIABLE FORMS -->
|
|
||||||
<term name="chapter-number" form="short">
|
|
||||||
<single>chap.</single>
|
|
||||||
<multiple>chaps.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="citation-number" form="short">
|
|
||||||
<single>cit.</single>
|
|
||||||
<multiple>cits.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="collection-number" form="short">
|
|
||||||
<single>núm.</single>
|
|
||||||
<multiple>núms.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="edition" form="short">
|
|
||||||
<single>ed.</single>
|
|
||||||
<multiple>eds.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="first-reference-note-number" form="short">
|
|
||||||
<single>ref.</single>
|
|
||||||
<multiple>refs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="number" form="short">
|
|
||||||
<single>no.</single>
|
|
||||||
<multiple>nos.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="number-of-pages" form="short">
|
|
||||||
<single>p.</single>
|
|
||||||
<multiple>pp.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="number-of-volumes" form="short">
|
|
||||||
<single>vol.</single>
|
|
||||||
<multiple>vols.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="page-first" form="short">
|
|
||||||
<single>p.</single>
|
|
||||||
<multiple>pp.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="printing" form="short">
|
|
||||||
<single>print.</single>
|
|
||||||
<multiple>prints.</multiple>
|
|
||||||
</term>
|
|
||||||
|
|
||||||
<!-- LONG ROLE FORMS -->
|
|
||||||
<term name="author"/> <!-- generally blank -->
|
|
||||||
<term name="chair">
|
|
||||||
<single>chair</single>
|
|
||||||
<multiple>chairs</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="collection-editor">
|
|
||||||
<single>ed.</single>
|
|
||||||
<multiple>eds.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="compiler">
|
|
||||||
<single>compiler</single>
|
|
||||||
<multiple>compilers</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="composer"/> <!-- generally blank -->
|
|
||||||
<term name="container-author"/> <!-- generally blank -->
|
|
||||||
<term name="contributor">
|
|
||||||
<single>contributor</single>
|
|
||||||
<multiple>contributors</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="curator">
|
|
||||||
<single>curator</single>
|
|
||||||
<multiple>curators</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="director">
|
|
||||||
<single>director</single>
|
|
||||||
<multiple>directores</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="editor">
|
|
||||||
<single>editor</single>
|
|
||||||
<multiple>editores</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="editor-translator">
|
|
||||||
<single>editor y traductor</single>
|
|
||||||
<multiple>editores y traductores</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="editortranslator">
|
|
||||||
<single>editor y traductor</single>
|
|
||||||
<multiple>editores y traductores</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="editorial-director">
|
|
||||||
<single>coordinador</single>
|
|
||||||
<multiple>coordinadores</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="executive-producer">
|
|
||||||
<single>executive producer</single>
|
|
||||||
<multiple>executive producers</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="guest">
|
|
||||||
<single>guest</single>
|
|
||||||
<multiple>guests</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="host">
|
|
||||||
<single>host</single>
|
|
||||||
<multiple>hosts</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="illustrator">
|
|
||||||
<single>ilustrador</single>
|
|
||||||
<multiple>ilustradores</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="interviewer"/> <!-- generally blank -->
|
|
||||||
<term name="narrator">
|
|
||||||
<single>narrator</single>
|
|
||||||
<multiple>narrators</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="organizer">
|
|
||||||
<single>organizer</single>
|
|
||||||
<multiple>organizers</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="original-author"/> <!-- generally blank -->
|
|
||||||
<term name="performer">
|
|
||||||
<single>performer</single>
|
|
||||||
<multiple>performers</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="producer">
|
|
||||||
<single>producer</single>
|
|
||||||
<multiple>producers</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="recipient"/> <!-- generally blank -->
|
|
||||||
<term name="reviewed-author"/> <!-- generally blank -->
|
|
||||||
<term name="script-writer">
|
|
||||||
<single>writer</single>
|
|
||||||
<multiple>writers</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="series-creator">
|
|
||||||
<single>series creator</single>
|
|
||||||
<multiple>series creators</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="translator">
|
|
||||||
<single>traductor</single>
|
|
||||||
<multiple>traductores</multiple>
|
|
||||||
</term>
|
|
||||||
|
|
||||||
<!-- SHORT ROLE FORMS -->
|
|
||||||
<term name="compiler" form="short">
|
|
||||||
<single>comp.</single>
|
|
||||||
<multiple>comps.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="contributor" form="short">
|
|
||||||
<single>contrib.</single>
|
|
||||||
<multiple>contribs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="curator" form="short">
|
|
||||||
<single>cur.</single>
|
|
||||||
<multiple>curs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="director" form="short">
|
|
||||||
<single>dir.</single>
|
|
||||||
<multiple>dirs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="editor" form="short">
|
|
||||||
<single>ed.</single>
|
|
||||||
<multiple>eds.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="editor-translator" form="short">
|
|
||||||
<single>ed. y trad.</single>
|
|
||||||
<multiple>eds. y trads.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="editortranslator" form="short">
|
|
||||||
<single>ed. y trad.</single>
|
|
||||||
<multiple>eds. y trads.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="editorial-director" form="short">
|
|
||||||
<single>coord.</single>
|
|
||||||
<multiple>coords.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="executive-producer" form="short">
|
|
||||||
<single>exec. prod.</single>
|
|
||||||
<multiple>exec. prods.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="illustrator" form="short">
|
|
||||||
<single>ilust.</single>
|
|
||||||
<multiple>ilusts.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="narrator" form="short">
|
|
||||||
<single>narr.</single>
|
|
||||||
<multiple>narrs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="organizer" form="short">
|
|
||||||
<single>org.</single>
|
|
||||||
<multiple>orgs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="performer" form="short">
|
|
||||||
<single>perf.</single>
|
|
||||||
<multiple>perfs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="producer" form="short">
|
|
||||||
<single>prod.</single>
|
|
||||||
<multiple>prods.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="script-writer" form="short">
|
|
||||||
<single>writ.</single>
|
|
||||||
<multiple>writs.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="series-creator" form="short">
|
|
||||||
<single>cre.</single>
|
|
||||||
<multiple>cres.</multiple>
|
|
||||||
</term>
|
|
||||||
<term name="translator" form="short">
|
|
||||||
<single>trad.</single>
|
|
||||||
<multiple>trads.</multiple>
|
|
||||||
</term>
|
|
||||||
|
|
||||||
<!-- VERB ROLE FORMS -->
|
|
||||||
<term name="chair" form="verb">chaired by</term>
|
|
||||||
<term name="collection-editor" form="verb">edited by</term>
|
|
||||||
<term name="compiler" form="verb">compiled by</term>
|
|
||||||
<term name="container-author" form="verb">de</term>
|
|
||||||
<term name="contributor" form="verb">with</term>
|
|
||||||
<term name="curator" form="verb">curated by</term>
|
|
||||||
<term name="director" form="verb">dirigido por</term>
|
|
||||||
<term name="editor" form="verb">editado por</term>
|
|
||||||
<term name="editor-translator" form="verb">editado y traducido por</term>
|
|
||||||
<term name="editortranslator" form="verb">editado y traducido por</term>
|
|
||||||
<term name="editorial-director" form="verb">coordinado por</term>
|
|
||||||
<term name="executive-producer" form="verb">executive produced by</term>
|
|
||||||
<term name="guest" form="verb">with guest</term>
|
|
||||||
<term name="host" form="verb">hosted by</term>
|
|
||||||
<term name="illustrator" form="verb">ilustrado por</term>
|
|
||||||
<term name="interviewer" form="verb">entrevistado por</term>
|
|
||||||
<term name="narrator" form="verb">narrated by</term>
|
|
||||||
<term name="organizer" form="verb">organized by</term>
|
|
||||||
<term name="performer" form="verb">performed by</term>
|
|
||||||
<term name="producer" form="verb">produced by</term>
|
|
||||||
<term name="recipient" form="verb">a</term>
|
|
||||||
<term name="reviewed-author" form="verb">por</term>
|
|
||||||
<term name="script-writer" form="verb">written by</term>
|
|
||||||
<term name="series-creator" form="verb">created by</term>
|
|
||||||
<term name="translator" form="verb">traducido por</term>
|
|
||||||
|
|
||||||
<!-- SHORT VERB ROLE FORMS -->
|
|
||||||
<term name="collection-editor" form="verb-short">ed. by</term>
|
|
||||||
<term name="compiler" form="verb-short">comp. by</term>
|
|
||||||
<term name="contributor" form="verb-short">w.</term>
|
|
||||||
<term name="curator" form="verb-short">cur. by</term>
|
|
||||||
<term name="director" form="verb-short">dir.</term>
|
|
||||||
<term name="editor" form="verb-short">ed.</term>
|
|
||||||
<term name="editor-translator" form="verb-short">ed. y trad.</term>
|
|
||||||
<term name="editortranslator" form="verb-short">ed. y trad.</term>
|
|
||||||
<term name="editorial-director" form="verb-short">coord.</term>
|
|
||||||
<term name="executive-producer" form="verb-short">exec. prod. by</term>
|
|
||||||
<term name="guest" form="verb-short">w. guest</term>
|
|
||||||
<term name="host" form="verb-short">hosted by</term>
|
|
||||||
<term name="illustrator" form="verb-short">ilust.</term>
|
|
||||||
<term name="narrator" form="verb-short">narr. by</term>
|
|
||||||
<term name="organizer" form="verb-short">org. by</term>
|
|
||||||
<term name="performer" form="verb-short">perf. by</term>
|
|
||||||
<term name="producer" form="verb-short">prod. by</term>
|
|
||||||
<term name="script-writer" form="verb-short">writ. by</term>
|
|
||||||
<term name="series-creator" form="verb-short">cre. by</term>
|
|
||||||
<term name="translator" form="verb-short">trad.</term>
|
|
||||||
|
|
||||||
<!-- LONG MONTH FORMS -->
|
|
||||||
<term name="month-01">enero</term>
|
|
||||||
<term name="month-02">febrero</term>
|
|
||||||
<term name="month-03">marzo</term>
|
|
||||||
<term name="month-04">abril</term>
|
|
||||||
<term name="month-05">mayo</term>
|
|
||||||
<term name="month-06">junio</term>
|
|
||||||
<term name="month-07">julio</term>
|
|
||||||
<term name="month-08">agosto</term>
|
|
||||||
<term name="month-09">septiembre</term>
|
|
||||||
<term name="month-10">octubre</term>
|
|
||||||
<term name="month-11">noviembre</term>
|
|
||||||
<term name="month-12">diciembre</term>
|
|
||||||
|
|
||||||
<!-- SHORT MONTH FORMS -->
|
|
||||||
<term name="month-01" form="short">ene.</term>
|
|
||||||
<term name="month-02" form="short">feb.</term>
|
|
||||||
<term name="month-03" form="short">mar.</term>
|
|
||||||
<term name="month-04" form="short">abr.</term>
|
|
||||||
<term name="month-05" form="short">may</term>
|
|
||||||
<term name="month-06" form="short">jun.</term>
|
|
||||||
<term name="month-07" form="short">jul.</term>
|
|
||||||
<term name="month-08" form="short">ago.</term>
|
|
||||||
<term name="month-09" form="short">sep.</term>
|
|
||||||
<term name="month-10" form="short">oct.</term>
|
|
||||||
<term name="month-11" form="short">nov.</term>
|
|
||||||
<term name="month-12" form="short">dic.</term>
|
|
||||||
|
|
||||||
<!-- SEASONS -->
|
|
||||||
<term name="season-01">primavera</term>
|
|
||||||
<term name="season-02">verano</term>
|
|
||||||
<term name="season-03">otoño</term>
|
|
||||||
<term name="season-04">invierno</term>
|
|
||||||
</terms>
|
|
||||||
</locale>
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,519 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" version="1.0" demote-non-dropping-particle="sort-only">
|
|
||||||
<info>
|
|
||||||
<title>IEEE Reference Guide version 11.29.2023</title>
|
|
||||||
<title-short>Institute of Electrical and Electronics Engineers</title-short>
|
|
||||||
<id>http://www.zotero.org/styles/ieee</id>
|
|
||||||
<link href="http://www.zotero.org/styles/ieee" rel="self"/>
|
|
||||||
<link href="https://journals.ieeeauthorcenter.ieee.org/your-role-in-article-production/ieee-editorial-style-manual/" rel="documentation"/>
|
|
||||||
<author>
|
|
||||||
<name>Michael Berkowitz</name>
|
|
||||||
<email>mberkowi@gmu.edu</email>
|
|
||||||
</author>
|
|
||||||
<contributor>
|
|
||||||
<name>Julian Onions</name>
|
|
||||||
<email>julian.onions@gmail.com</email>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Rintze Zelle</name>
|
|
||||||
<uri>http://twitter.com/rintzezelle</uri>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Stephen Frank</name>
|
|
||||||
<uri>http://www.zotero.org/sfrank</uri>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Sebastian Karcher</name>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Giuseppe Silano</name>
|
|
||||||
<email>g.silano89@gmail.com</email>
|
|
||||||
<uri>http://giuseppesilano.net</uri>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Patrick O'Brien</name>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Brenton M. Wiernik</name>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Oliver Couch</name>
|
|
||||||
<email>oliver.couch@gmail.com</email>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Andrew Dunning</name>
|
|
||||||
<uri>https://orcid.org/0000-0003-0464-5036</uri>
|
|
||||||
</contributor>
|
|
||||||
<category citation-format="numeric"/>
|
|
||||||
<category field="engineering"/>
|
|
||||||
<category field="generic-base"/>
|
|
||||||
<summary>IEEE style as per the 2023 guidelines.</summary>
|
|
||||||
<updated>2024-03-27T11:41:27+00:00</updated>
|
|
||||||
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
|
||||||
</info>
|
|
||||||
<locale xml:lang="en">
|
|
||||||
<date form="text">
|
|
||||||
<date-part name="month" form="short" suffix=" "/>
|
|
||||||
<date-part name="day" form="numeric-leading-zeros" suffix=", "/>
|
|
||||||
<date-part name="year"/>
|
|
||||||
</date>
|
|
||||||
<terms>
|
|
||||||
<term name="chapter" form="short">ch.</term>
|
|
||||||
<term name="chapter-number" form="short">ch.</term>
|
|
||||||
<term name="presented at">presented at the</term>
|
|
||||||
<term name="available at">available</term>
|
|
||||||
<!-- always use three-letter abbreviations for months -->
|
|
||||||
<term name="month-06" form="short">Jun.</term>
|
|
||||||
<term name="month-07" form="short">Jul.</term>
|
|
||||||
<term name="month-09" form="short">Sep.</term>
|
|
||||||
</terms>
|
|
||||||
</locale>
|
|
||||||
<!-- Macros -->
|
|
||||||
<macro name="status">
|
|
||||||
<choose>
|
|
||||||
<if variable="page issue volume" match="none">
|
|
||||||
<text variable="status" text-case="capitalize-first" suffix="" font-weight="bold"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="edition">
|
|
||||||
<choose>
|
|
||||||
<if type="bill book chapter graphic legal_case legislation motion_picture paper-conference report song" match="any">
|
|
||||||
<choose>
|
|
||||||
<if is-numeric="edition">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<number variable="edition" form="ordinal"/>
|
|
||||||
<text term="edition" form="short"/>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<text variable="edition" text-case="capitalize-first" suffix="."/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="issued">
|
|
||||||
<choose>
|
|
||||||
<if type="article-journal report" match="any">
|
|
||||||
<date variable="issued">
|
|
||||||
<date-part name="month" form="short" suffix=" "/>
|
|
||||||
<date-part name="year" form="long"/>
|
|
||||||
</date>
|
|
||||||
</if>
|
|
||||||
<else-if type="bill book chapter graphic legal_case legislation song thesis" match="any">
|
|
||||||
<date variable="issued">
|
|
||||||
<date-part name="year" form="long"/>
|
|
||||||
</date>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="paper-conference" match="any">
|
|
||||||
<date variable="issued">
|
|
||||||
<date-part name="month" form="short"/>
|
|
||||||
<date-part name="year" prefix=" "/>
|
|
||||||
</date>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="motion_picture" match="any">
|
|
||||||
<date variable="issued" form="text" prefix="(" suffix=")"/>
|
|
||||||
</else-if>
|
|
||||||
<else>
|
|
||||||
<date variable="issued" form="text"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="author">
|
|
||||||
<names variable="author">
|
|
||||||
<name and="text" et-al-min="7" et-al-use-first="1" initialize-with=". "/>
|
|
||||||
<label form="short" prefix=", " text-case="capitalize-first"/>
|
|
||||||
<et-al font-style="italic"/>
|
|
||||||
<substitute>
|
|
||||||
<names variable="editor"/>
|
|
||||||
<names variable="translator"/>
|
|
||||||
<text macro="director"/>
|
|
||||||
</substitute>
|
|
||||||
</names>
|
|
||||||
</macro>
|
|
||||||
<macro name="editor">
|
|
||||||
<names variable="editor">
|
|
||||||
<name initialize-with=". " delimiter=", " and="text"/>
|
|
||||||
<label form="short" prefix=", " text-case="capitalize-first"/>
|
|
||||||
</names>
|
|
||||||
</macro>
|
|
||||||
<macro name="director">
|
|
||||||
<names variable="director">
|
|
||||||
<name and="text" et-al-min="7" et-al-use-first="1" initialize-with=". "/>
|
|
||||||
<et-al font-style="italic"/>
|
|
||||||
</names>
|
|
||||||
</macro>
|
|
||||||
<macro name="locators">
|
|
||||||
<group delimiter=", ">
|
|
||||||
<text macro="edition"/>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text term="volume" form="short"/>
|
|
||||||
<number variable="volume" form="numeric"/>
|
|
||||||
</group>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<number variable="number-of-volumes" form="numeric"/>
|
|
||||||
<text term="volume" form="short" plural="true"/>
|
|
||||||
</group>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text term="issue" form="short"/>
|
|
||||||
<number variable="issue" form="numeric"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="title">
|
|
||||||
<choose>
|
|
||||||
<if type="bill book graphic legal_case legislation motion_picture song standard software" match="any">
|
|
||||||
<text variable="title" font-style="italic"/>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<text variable="title" quotes="true"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="publisher">
|
|
||||||
<choose>
|
|
||||||
<if type="bill book chapter graphic legal_case legislation motion_picture paper-conference song" match="any">
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text variable="publisher-place"/>
|
|
||||||
<text variable="publisher"/>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<group delimiter=", ">
|
|
||||||
<text variable="publisher"/>
|
|
||||||
<text variable="publisher-place"/>
|
|
||||||
</group>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="event">
|
|
||||||
<choose>
|
|
||||||
<!-- Published Conference Paper -->
|
|
||||||
<if type="paper-conference speech" match="any">
|
|
||||||
<choose>
|
|
||||||
<if variable="container-title" match="any">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text term="in"/>
|
|
||||||
<text variable="container-title" font-style="italic"/>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
<!-- Unpublished Conference Paper -->
|
|
||||||
<else>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text term="presented at"/>
|
|
||||||
<text variable="event"/>
|
|
||||||
</group>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="access">
|
|
||||||
<choose>
|
|
||||||
<if type="webpage post post-weblog" match="any">
|
|
||||||
<!-- https://url.com/ (accessed Mon. DD, YYYY). -->
|
|
||||||
<choose>
|
|
||||||
<if variable="URL">
|
|
||||||
<group delimiter=". " prefix=" ">
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text term="accessed" text-case="capitalize-first"/>
|
|
||||||
<date variable="accessed" form="text"/>
|
|
||||||
</group>
|
|
||||||
<text term="online" prefix="[" suffix="]" text-case="capitalize-first"/>
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text term="available at" text-case="capitalize-first"/>
|
|
||||||
<text variable="URL"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</if>
|
|
||||||
<else-if match="any" variable="DOI">
|
|
||||||
<!-- doi: 10.1000/xyz123. -->
|
|
||||||
<text variable="DOI" prefix=" doi: " suffix="."/>
|
|
||||||
</else-if>
|
|
||||||
<else-if variable="URL">
|
|
||||||
<!-- Accessed: Mon. DD, YYYY. [Medium]. Available: https://URL.com/ -->
|
|
||||||
<group delimiter=". " prefix=" " suffix=". ">
|
|
||||||
<!-- Accessed: Mon. DD, YYYY. -->
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text term="accessed" text-case="capitalize-first"/>
|
|
||||||
<date variable="accessed" form="text"/>
|
|
||||||
</group>
|
|
||||||
<!-- [Online Video]. -->
|
|
||||||
<group prefix="[" suffix="]" delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if variable="medium" match="any">
|
|
||||||
<text variable="medium" text-case="capitalize-first"/>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<text term="online" text-case="capitalize-first"/>
|
|
||||||
<choose>
|
|
||||||
<if type="motion_picture">
|
|
||||||
<text term="video" text-case="capitalize-first"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<!-- Available: https://URL.com/ -->
|
|
||||||
<group delimiter=": " prefix=" ">
|
|
||||||
<text term="available at" text-case="capitalize-first"/>
|
|
||||||
<text variable="URL"/>
|
|
||||||
</group>
|
|
||||||
</else-if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="page">
|
|
||||||
<choose>
|
|
||||||
<if type="article-journal" variable="number" match="all">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text value="Art."/>
|
|
||||||
<text term="issue" form="short"/>
|
|
||||||
<text variable="number"/>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<label variable="page" form="short"/>
|
|
||||||
<text variable="page"/>
|
|
||||||
</group>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="citation-locator">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if locator="page">
|
|
||||||
<label variable="locator" form="short"/>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<label variable="locator" form="short" text-case="capitalize-first"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
<text variable="locator"/>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="geographic-location">
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<choose>
|
|
||||||
<if variable="publisher-place">
|
|
||||||
<text variable="publisher-place" text-case="title"/>
|
|
||||||
</if>
|
|
||||||
<else-if variable="event-place">
|
|
||||||
<text variable="event-place" text-case="title"/>
|
|
||||||
</else-if>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<!-- Series -->
|
|
||||||
<macro name="collection">
|
|
||||||
<choose>
|
|
||||||
<if variable="collection-title" match="any">
|
|
||||||
<text term="in" suffix=" "/>
|
|
||||||
<group delimiter=", " suffix=". ">
|
|
||||||
<text variable="collection-title"/>
|
|
||||||
<text variable="collection-number" prefix="no. "/>
|
|
||||||
<text variable="volume" prefix="vol. "/>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<!-- Citation -->
|
|
||||||
<citation>
|
|
||||||
<sort>
|
|
||||||
<key variable="citation-number"/>
|
|
||||||
</sort>
|
|
||||||
<layout delimiter=", ">
|
|
||||||
<group prefix="[" suffix="]" delimiter=", ">
|
|
||||||
<text variable="citation-number"/>
|
|
||||||
<text macro="citation-locator"/>
|
|
||||||
</group>
|
|
||||||
</layout>
|
|
||||||
</citation>
|
|
||||||
<!-- Bibliography -->
|
|
||||||
<bibliography entry-spacing="0" second-field-align="flush">
|
|
||||||
<layout>
|
|
||||||
<!-- Citation Number -->
|
|
||||||
<text variable="citation-number" prefix="[" suffix="]"/>
|
|
||||||
<!-- Author(s) -->
|
|
||||||
<text macro="author" suffix=", "/>
|
|
||||||
<!-- Rest of Citation -->
|
|
||||||
<choose>
|
|
||||||
<!-- Specific Formats -->
|
|
||||||
<if type="article-journal">
|
|
||||||
<group delimiter=", ">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text variable="container-title" font-style="italic" form="short"/>
|
|
||||||
<text macro="locators"/>
|
|
||||||
<text macro="page"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
<text macro="status"/>
|
|
||||||
</group>
|
|
||||||
<choose>
|
|
||||||
<if variable="URL DOI" match="none">
|
|
||||||
<text value="."/>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<text value=","/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
<text macro="access"/>
|
|
||||||
</if>
|
|
||||||
<else-if type="paper-conference speech" match="any">
|
|
||||||
<group delimiter=", " suffix=", ">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text macro="event"/>
|
|
||||||
<text macro="editor"/>
|
|
||||||
</group>
|
|
||||||
<text macro="collection"/>
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="publisher"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
<text macro="page"/>
|
|
||||||
<text macro="status"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="chapter">
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="title"/>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text term="in" suffix=" "/>
|
|
||||||
<text variable="container-title" font-style="italic"/>
|
|
||||||
</group>
|
|
||||||
<text macro="locators"/>
|
|
||||||
<text macro="editor"/>
|
|
||||||
<text macro="collection"/>
|
|
||||||
<text macro="publisher"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<label variable="chapter-number" form="short"/>
|
|
||||||
<text variable="chapter-number"/>
|
|
||||||
</group>
|
|
||||||
<text macro="page"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="report">
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text macro="publisher"/>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text variable="genre"/>
|
|
||||||
<text variable="number"/>
|
|
||||||
</group>
|
|
||||||
<text macro="issued"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="thesis">
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text variable="genre"/>
|
|
||||||
<text macro="publisher"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="software">
|
|
||||||
<group delimiter=". " suffix=".">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text macro="issued" prefix="(" suffix=")"/>
|
|
||||||
<text variable="genre"/>
|
|
||||||
<text macro="publisher"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="article">
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text macro="publisher" font-style="italic"/>
|
|
||||||
<text variable="number"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="webpage post-weblog post" match="any">
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text variable="container-title"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="patent">
|
|
||||||
<group delimiter=", ">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text variable="number"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<!-- Online Video -->
|
|
||||||
<else-if type="motion_picture">
|
|
||||||
<text macro="geographic-location" suffix=". "/>
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="standard">
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="title"/>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text variable="genre"/>
|
|
||||||
<text variable="number"/>
|
|
||||||
</group>
|
|
||||||
<text macro="geographic-location"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<!-- Generic/Fallback Formats -->
|
|
||||||
<else-if type="bill book graphic legal_case legislation report song" match="any">
|
|
||||||
<group delimiter=", " suffix=". ">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text macro="locators"/>
|
|
||||||
</group>
|
|
||||||
<text macro="collection"/>
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="publisher"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
<text macro="page"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="article-magazine article-newspaper broadcast interview manuscript map patent personal_communication song speech thesis webpage" match="any">
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text variable="container-title" font-style="italic"/>
|
|
||||||
<text macro="locators"/>
|
|
||||||
<text macro="publisher"/>
|
|
||||||
<text macro="page"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else-if>
|
|
||||||
<else>
|
|
||||||
<group delimiter=", " suffix=". ">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text variable="container-title" font-style="italic"/>
|
|
||||||
<text macro="locators"/>
|
|
||||||
</group>
|
|
||||||
<text macro="collection"/>
|
|
||||||
<group delimiter=", " suffix=".">
|
|
||||||
<text macro="publisher"/>
|
|
||||||
<text macro="page"/>
|
|
||||||
<text macro="issued"/>
|
|
||||||
</group>
|
|
||||||
<text macro="access"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</layout>
|
|
||||||
</bibliography>
|
|
||||||
</style>
|
|
||||||
@@ -1,520 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<style xmlns="http://purl.org/net/xbiblio/csl" class="in-text" delimiter-precedes-last="always" demote-non-dropping-particle="sort-only" initialize-with="" initialize-with-hyphen="false" name-as-sort-order="all" name-delimiter=", " names-delimiter=", " page-range-format="minimal" sort-separator=" " version="1.0">
|
|
||||||
<!-- This file was generated by the Style Variant Builder <https://github.com/citation-style-language/style-variant-builder>. To contribute changes, modify the template and regenerate variants. -->
|
|
||||||
<info>
|
|
||||||
<title>NLM/Vancouver: Citing Medicine 2nd edition (citation-sequence)</title>
|
|
||||||
<title-short>National Library of Medicine, ANSI/NISO Z39.29-2005 (R2010), ICMJE Recommendations/URMs (C-S)</title-short>
|
|
||||||
<id>http://www.zotero.org/styles/nlm-citation-sequence</id>
|
|
||||||
<link href="http://www.zotero.org/styles/nlm-citation-sequence" rel="self"/>
|
|
||||||
<link href="https://www.nlm.nih.gov/citingmedicine" rel="documentation"/>
|
|
||||||
<link href="https://www.nlm.nih.gov/bsd/uniform_requirements.html" rel="documentation"/>
|
|
||||||
<link href="https://www.icmje.org/recommendations/" rel="documentation"/>
|
|
||||||
<author>
|
|
||||||
<name>Michael Berkowitz</name>
|
|
||||||
<email>mberkowi@gmu.edu</email>
|
|
||||||
</author>
|
|
||||||
<author>
|
|
||||||
<name>Andrew Dunning</name>
|
|
||||||
<uri>https://orcid.org/0000-0003-0464-5036</uri>
|
|
||||||
</author>
|
|
||||||
<contributor>
|
|
||||||
<name>Petr Hlustik</name>
|
|
||||||
<uri>https://orcid.org/0000-0002-1951-0671</uri>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Sebastian Karcher</name>
|
|
||||||
<uri>https://orcid.org/0000-0001-8249-7388</uri>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Charles Parnot</name>
|
|
||||||
<uri>https://orcid.org/0000-0002-7346-5883</uri>
|
|
||||||
</contributor>
|
|
||||||
<contributor>
|
|
||||||
<name>Sean Takats</name>
|
|
||||||
<uri>https://orcid.org/0000-0002-7851-5069</uri>
|
|
||||||
</contributor>
|
|
||||||
<category citation-format="numeric"/>
|
|
||||||
<category field="generic-base"/>
|
|
||||||
<category field="medicine"/>
|
|
||||||
<category field="science"/>
|
|
||||||
<summary>Citing Medicine: The NLM Style Guide for Authors, Editors, and Publishers, 2nd edition (2015), based on ANSI/NISO Z39.29-2005 (R2010); citation-sequence system.</summary>
|
|
||||||
<updated>2026-02-18T15:24:08+00:00</updated>
|
|
||||||
<rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
|
|
||||||
</info>
|
|
||||||
<locale xml:lang="en">
|
|
||||||
<date delimiter=" " form="text">
|
|
||||||
<date-part name="year"/>
|
|
||||||
<date-part form="short" name="month" strip-periods="true"/>
|
|
||||||
<date-part name="day"/>
|
|
||||||
</date>
|
|
||||||
<terms>
|
|
||||||
<term name="available at">available from</term>
|
|
||||||
<term name="collection-editor">
|
|
||||||
<single>editor</single>
|
|
||||||
<multiple>editors</multiple>
|
|
||||||
</term>
|
|
||||||
<term form="short" name="month-06">Jun.</term>
|
|
||||||
<term form="short" name="month-07">Jul.</term>
|
|
||||||
<term form="short" name="month-09">Sep.</term>
|
|
||||||
<term name="presented at">presented at</term>
|
|
||||||
<term form="short" name="section">
|
|
||||||
<single>sect.</single>
|
|
||||||
<multiple>sects.</multiple>
|
|
||||||
</term>
|
|
||||||
<term form="short" name="supplement">
|
|
||||||
<single>suppl.</single>
|
|
||||||
<multiple>suppls.</multiple>
|
|
||||||
</term>
|
|
||||||
</terms>
|
|
||||||
</locale>
|
|
||||||
<locale xml:lang="fr">
|
|
||||||
<date delimiter=" " form="text">
|
|
||||||
<date-part name="day"/>
|
|
||||||
<date-part form="short" name="month" strip-periods="true"/>
|
|
||||||
<date-part name="year"/>
|
|
||||||
</date>
|
|
||||||
</locale>
|
|
||||||
<!-- Variable labels -->
|
|
||||||
<macro name="label-collection-number">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if is-numeric="collection-number">
|
|
||||||
<label form="short" variable="collection-number"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
<text variable="collection-number"/>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="label-edition">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if is-numeric="edition">
|
|
||||||
<number form="ordinal" variable="edition"/>
|
|
||||||
<label form="short" variable="edition"/>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<text variable="edition"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="label-number-of-pages">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text variable="number-of-pages"/>
|
|
||||||
<choose>
|
|
||||||
<if is-numeric="number-of-pages">
|
|
||||||
<label form="short" plural="never" variable="number-of-pages"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="label-page">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<label form="short" plural="never" variable="page"/>
|
|
||||||
<text variable="page"/>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="label-part-number-capitalized">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if is-numeric="part-number">
|
|
||||||
<!-- TODO: Replace with `part-number` label when CSL provides one -->
|
|
||||||
<text form="short" term="part" text-case="capitalize-first"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
<text variable="part-number"/>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="label-supplement-number">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if is-numeric="supplement-number">
|
|
||||||
<!-- TODO: Replace with `supplement-number` label when CSL provides one -->
|
|
||||||
<text form="short" strip-periods="true" term="supplement" text-case="capitalize-first"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
<text text-case="capitalize-first" variable="supplement-number"/>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="label-volume-capitalized">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if is-numeric="volume">
|
|
||||||
<label form="short" text-case="capitalize-first" variable="volume"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
<text variable="volume"/>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="author">
|
|
||||||
<names variable="author">
|
|
||||||
<label prefix=", "/>
|
|
||||||
<substitute>
|
|
||||||
<names variable="editor-translator"/>
|
|
||||||
<names variable="editor translator"/>
|
|
||||||
<names variable="editor"/>
|
|
||||||
<names variable="collection-editor"/>
|
|
||||||
</substitute>
|
|
||||||
</names>
|
|
||||||
</macro>
|
|
||||||
<macro name="title">
|
|
||||||
<choose>
|
|
||||||
<if type="webpage" variable="container-title">
|
|
||||||
<!-- `webpage` listed under `container-title` (Citing Medicine, ch. 25) -->
|
|
||||||
<text variable="container-title"/>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<text variable="title"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="content-type">
|
|
||||||
<text variable="genre"/>
|
|
||||||
</macro>
|
|
||||||
<macro name="type-of-medium">
|
|
||||||
<choose>
|
|
||||||
<if variable="medium">
|
|
||||||
<text text-case="capitalize-first" variable="medium"/>
|
|
||||||
</if>
|
|
||||||
<else-if match="any" type="chapter entry-dictionary entry-encyclopedia paper-conference"/>
|
|
||||||
<else-if variable="URL">
|
|
||||||
<text term="internet" text-case="capitalize-first"/>
|
|
||||||
</else-if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="container-preposition">
|
|
||||||
<choose>
|
|
||||||
<if match="any" type="chapter paper-conference entry-dictionary entry-encyclopedia">
|
|
||||||
<text term="in" text-case="capitalize-first"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="secondary-authors">
|
|
||||||
<names variable="editor">
|
|
||||||
<label prefix=", "/>
|
|
||||||
</names>
|
|
||||||
</macro>
|
|
||||||
<macro name="container-title">
|
|
||||||
<group delimiter=", ">
|
|
||||||
<choose>
|
|
||||||
<if type="webpage"/>
|
|
||||||
<else-if variable="container-title">
|
|
||||||
<group delimiter=". ">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if match="any" type="article-journal review review-book">
|
|
||||||
<text form="short" strip-periods="true" variable="container-title"/>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<text variable="container-title"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
<choose>
|
|
||||||
<if type="article-journal" variable="DOI"/>
|
|
||||||
<else-if type="article-journal" variable="PMID"/>
|
|
||||||
<else-if type="article-journal" variable="PMCID"/>
|
|
||||||
<else-if variable="URL">
|
|
||||||
<text prefix="[" suffix="]" term="internet" text-case="capitalize-first"/>
|
|
||||||
</else-if>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
<text macro="label-edition"/>
|
|
||||||
</group>
|
|
||||||
</else-if>
|
|
||||||
<!-- TODO: add `event-name` and `event-place` -->
|
|
||||||
<else-if match="any" type="bill legislation">
|
|
||||||
<group delimiter=". ">
|
|
||||||
<text variable="container-title"/>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text form="short" term="section" text-case="capitalize-first"/>
|
|
||||||
<text variable="section"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<text variable="number"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="speech">
|
|
||||||
<group delimiter=": ">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text text-case="capitalize-first" variable="genre"/>
|
|
||||||
<text term="presented at"/>
|
|
||||||
</group>
|
|
||||||
<text variable="event"/>
|
|
||||||
</group>
|
|
||||||
</else-if>
|
|
||||||
<else>
|
|
||||||
<group delimiter=", ">
|
|
||||||
<text macro="label-volume-capitalized"/>
|
|
||||||
<text variable="volume-title"/>
|
|
||||||
</group>
|
|
||||||
<group delimiter=", ">
|
|
||||||
<text macro="label-part-number-capitalized"/>
|
|
||||||
<text variable="part-title"/>
|
|
||||||
</group>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="place-of-publication">
|
|
||||||
<choose>
|
|
||||||
<if type="thesis">
|
|
||||||
<text prefix="[" suffix="]" variable="publisher-place"/>
|
|
||||||
</if>
|
|
||||||
<else-if type="speech"/>
|
|
||||||
<else>
|
|
||||||
<text variable="publisher-place"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="publisher">
|
|
||||||
<choose>
|
|
||||||
<!-- discard publisher for serial publications -->
|
|
||||||
<if match="none" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book">
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text macro="place-of-publication"/>
|
|
||||||
<text variable="publisher"/>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="date">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if match="any" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book">
|
|
||||||
<group delimiter=":">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<date form="text" variable="issued"/>
|
|
||||||
<choose>
|
|
||||||
<if type="article-journal" variable="DOI"/>
|
|
||||||
<else-if type="article-journal" variable="PMID"/>
|
|
||||||
<else-if type="article-journal" variable="PMCID"/>
|
|
||||||
<else>
|
|
||||||
<text macro="date-of-citation"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
<choose>
|
|
||||||
<if type="article-newspaper">
|
|
||||||
<text variable="page"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
<else-if match="any" type="bill legislation">
|
|
||||||
<date form="text" variable="issued"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="report">
|
|
||||||
<date date-parts="year-month" form="text" variable="issued"/>
|
|
||||||
<text macro="date-of-citation"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="patent">
|
|
||||||
<group delimiter=", ">
|
|
||||||
<text variable="number"/>
|
|
||||||
<date date-parts="year" form="numeric" variable="issued"/>
|
|
||||||
</group>
|
|
||||||
<text macro="date-of-citation"/>
|
|
||||||
</else-if>
|
|
||||||
<else-if type="speech">
|
|
||||||
<group delimiter="; ">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<date form="text" variable="issued"/>
|
|
||||||
<text macro="date-of-citation"/>
|
|
||||||
</group>
|
|
||||||
<text variable="event-place"/>
|
|
||||||
</group>
|
|
||||||
</else-if>
|
|
||||||
<else>
|
|
||||||
<date date-parts="year" form="numeric" variable="issued"/>
|
|
||||||
<text macro="date-of-citation"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="identifier-serial">
|
|
||||||
<choose>
|
|
||||||
<if match="any" type="article-journal article-magazine periodical post-weblog review review-book">
|
|
||||||
<group delimiter=":">
|
|
||||||
<group>
|
|
||||||
<text variable="collection-title"/>
|
|
||||||
<text variable="volume"/>
|
|
||||||
<group delimiter=" " prefix="(" suffix=")">
|
|
||||||
<text variable="issue"/>
|
|
||||||
<text macro="label-supplement-number"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<text macro="location-pagination-serial"/>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="date-of-citation">
|
|
||||||
<choose>
|
|
||||||
<if variable="URL">
|
|
||||||
<group delimiter=" " prefix="[" suffix="]">
|
|
||||||
<text term="cited"/>
|
|
||||||
<date form="text" variable="accessed"/>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="location-pagination-monographic">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if match="any" type="article-journal article-magazine article-newspaper review review-book"/>
|
|
||||||
<else-if type="book">
|
|
||||||
<text macro="label-number-of-pages"/>
|
|
||||||
</else-if>
|
|
||||||
<else>
|
|
||||||
<text macro="label-page"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="location-pagination-serial">
|
|
||||||
<choose>
|
|
||||||
<if variable="number">
|
|
||||||
<text variable="number"/>
|
|
||||||
</if>
|
|
||||||
<else>
|
|
||||||
<text variable="page"/>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="webpage-part">
|
|
||||||
<choose>
|
|
||||||
<if type="webpage" variable="container-title">
|
|
||||||
<text variable="title"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="series">
|
|
||||||
<choose>
|
|
||||||
<if match="any" type="article-journal article-magazine article-newspaper periodical post-weblog review review-book"/>
|
|
||||||
<else-if variable="collection-title">
|
|
||||||
<group delimiter=". " prefix="(" suffix=")">
|
|
||||||
<names variable="collection-editor">
|
|
||||||
<label prefix=", "/>
|
|
||||||
</names>
|
|
||||||
<group delimiter="; ">
|
|
||||||
<text variable="collection-title"/>
|
|
||||||
<text macro="label-collection-number"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</else-if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="report-number">
|
|
||||||
<choose>
|
|
||||||
<if type="report">
|
|
||||||
<group delimiter=": ">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text term="report" text-case="capitalize-first"/>
|
|
||||||
<label form="short" text-case="capitalize-first" variable="number"/>
|
|
||||||
</group>
|
|
||||||
<text variable="number"/>
|
|
||||||
</group>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
</macro>
|
|
||||||
<macro name="availability">
|
|
||||||
<group delimiter=". ">
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text text-case="capitalize-first" value="located at"/>
|
|
||||||
<group delimiter="; ">
|
|
||||||
<group delimiter=", ">
|
|
||||||
<text variable="archive_collection"/>
|
|
||||||
<text variable="archive"/>
|
|
||||||
<text variable="archive-place"/>
|
|
||||||
</group>
|
|
||||||
<text variable="archive_location"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<choose>
|
|
||||||
<if type="article-journal" variable="DOI"/>
|
|
||||||
<else-if type="article-journal" variable="PMID"/>
|
|
||||||
<else-if type="article-journal" variable="PMCID"/>
|
|
||||||
<else>
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text term="available at" text-case="capitalize-first"/>
|
|
||||||
<text variable="URL"/>
|
|
||||||
</group>
|
|
||||||
</else>
|
|
||||||
</choose>
|
|
||||||
<text prefix="doi:" variable="DOI"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<macro name="notes">
|
|
||||||
<group delimiter=". " suffix=".">
|
|
||||||
<group delimiter="; ">
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text value="PubMed PMID"/>
|
|
||||||
<text variable="PMID"/>
|
|
||||||
</group>
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text value="PubMed Central PMCID"/>
|
|
||||||
<text variable="PMCID"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<text variable="references"/>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<citation collapse="citation-number">
|
|
||||||
<sort>
|
|
||||||
<key variable="citation-number"/>
|
|
||||||
</sort>
|
|
||||||
<layout delimiter="," prefix="(" suffix=")">
|
|
||||||
<text variable="citation-number"/>
|
|
||||||
</layout>
|
|
||||||
</citation>
|
|
||||||
<macro name="bibliography">
|
|
||||||
<group delimiter=" ">
|
|
||||||
<group delimiter=". " suffix=".">
|
|
||||||
<text macro="author"/>
|
|
||||||
<group delimiter=" ">
|
|
||||||
<text macro="title"/>
|
|
||||||
<text macro="content-type" prefix="[" suffix="]"/>
|
|
||||||
<choose>
|
|
||||||
<if type="webpage" variable="container-title">
|
|
||||||
<text macro="type-of-medium" prefix="[" suffix="]"/>
|
|
||||||
</if>
|
|
||||||
<else-if match="none" variable="container-title">
|
|
||||||
<text macro="type-of-medium" prefix="[" suffix="]"/>
|
|
||||||
</else-if>
|
|
||||||
</choose>
|
|
||||||
</group>
|
|
||||||
<choose>
|
|
||||||
<if match="none" variable="container-title">
|
|
||||||
<text macro="label-edition"/>
|
|
||||||
</if>
|
|
||||||
</choose>
|
|
||||||
<group delimiter=": ">
|
|
||||||
<text macro="container-preposition"/>
|
|
||||||
<group delimiter=". ">
|
|
||||||
<text macro="secondary-authors"/>
|
|
||||||
<text macro="container-title"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<group delimiter="; ">
|
|
||||||
<text macro="publisher"/>
|
|
||||||
<group delimiter=";">
|
|
||||||
<text macro="date"/>
|
|
||||||
<text macro="identifier-serial"/>
|
|
||||||
</group>
|
|
||||||
</group>
|
|
||||||
<text macro="location-pagination-monographic"/>
|
|
||||||
<text macro="webpage-part"/>
|
|
||||||
<text macro="series"/>
|
|
||||||
<text macro="report-number"/>
|
|
||||||
</group>
|
|
||||||
<text macro="availability"/>
|
|
||||||
<text macro="notes"/>
|
|
||||||
</group>
|
|
||||||
</macro>
|
|
||||||
<bibliography et-al-min="7" et-al-use-first="6" second-field-align="flush">
|
|
||||||
<layout>
|
|
||||||
<text suffix="." variable="citation-number"/>
|
|
||||||
<text macro="bibliography"/>
|
|
||||||
</layout>
|
|
||||||
</bibliography>
|
|
||||||
</style>
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,118 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<svg version="1.1" id="logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 192.3 63.4" style="enable-background:new 0 0 192.3 63.4;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill:#FFFFFF;}
|
|
||||||
.st1{fill:#FFFFFF;}
|
|
||||||
.st2{fill:#FFFFFF;}
|
|
||||||
</style>
|
|
||||||
<g>
|
|
||||||
<g id="Group_1247_1_">
|
|
||||||
<path id="Path_477_1_" class="st0" d="M50.7,50.6l4.4-7.8h-8.9l-12-21l-4.4,7.8l12,21C41.8,50.6,50.7,50.6,50.7,50.6z"/>
|
|
||||||
<path id="Path_478_1_" class="st0" d="M34.3,1h-9l4.4,7.8l-12,20.8h9.1l12-20.8L34.3,1z"/>
|
|
||||||
<path id="Path_479_1_" class="st0" d="M0,40.1l4.4,7.8l4.4-7.8h23.9l-4.4-7.8H4.4L0,40.1z"/>
|
|
||||||
<path id="Path_480_1_" class="st1" d="M56.7,40.1l4.4-7.8h-9L40.3,11.4l-4.4,7.8l12,20.8H56.7z"/>
|
|
||||||
<path id="Path_481_1_" class="st1" d="M22.3,1h-8.9l4.4,7.8l-12,20.8h9l12-20.8L22.3,1z"/>
|
|
||||||
<path id="Path_482_1_" class="st1" d="M5.9,50.6l4.4,7.8l4.4-7.8h23.9l-4.4-7.8H10.5L5.9,50.6z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="st1" d="M67.9,3.9c0-0.8,0-1.6-0.1-2.4l1.7-0.1l0.1,0.1v6.3c0,0.7,0.1,1.2,0.5,1.6C70.6,9.8,71,10,71.7,10
|
|
||||||
c0.5,0,1.1-0.1,1.3-0.5c0.4-0.4,0.5-0.9,0.5-1.6V3.5c0-0.8,0-1.5-0.1-2.2l1.9-0.1v6.7c0,1.1-0.4,2-1.1,2.6
|
|
||||||
c-0.7,0.5-1.6,0.9-2.7,0.9c-1.1,0-2-0.3-2.7-0.9C68.2,10,67.8,9,67.8,7.9L67.9,3.9L67.9,3.9z"/>
|
|
||||||
<path class="st1" d="M83,11.3h-1.7V6.6c0-0.7-0.1-1.5-0.3-2.3L82.6,4c0.1,0.4,0.3,0.7,0.3,1.1C83.5,4.4,84.3,4,85.1,4
|
|
||||||
c0.7,0,1.1,0.1,1.5,0.5C87,5,87.1,5.5,87.1,6.2v5.1h-1.7V6.6c0-0.8-0.4-1.3-1.1-1.3c-0.4,0-0.9,0.1-1.3,0.5L83,11.3L83,11.3z"/>
|
|
||||||
<path class="st1" d="M95.1,1.6c0,0.3-0.1,0.7-0.4,0.8c-0.3,0.3-0.5,0.4-0.8,0.4S93.3,2.7,93,2.6c-0.1-0.1-0.3-0.4-0.3-0.7
|
|
||||||
s0.1-0.7,0.4-0.8c0.3-0.1,0.5-0.4,0.9-0.4c0.3,0,0.5,0.1,0.7,0.3S95.1,1.4,95.1,1.6z M93,11.3V6.6c0-0.8,0-1.6-0.1-2.4l1.7-0.3
|
|
||||||
L94.8,4v7.1L93,11.3L93,11.3z"/>
|
|
||||||
<path class="st1" d="M106.4,4.3l-2.3,7h-1.9l-2.3-7.1l1.9-0.1l0.9,3.6c0.3,1.1,0.5,1.9,0.5,2.4l0,0c0.1-0.5,0.3-1.3,0.7-2.4
|
|
||||||
l0.9-3.6L106.4,4.3L106.4,4.3z"/>
|
|
||||||
<path class="st1" d="M116.7,7.7l-0.3,0.3h-4c0,0.8,0.3,1.3,0.7,1.7c0.4,0.4,0.8,0.5,1.5,0.5c0.5,0,1.2-0.1,1.7-0.5l0.1,1.2
|
|
||||||
c-0.7,0.4-1.3,0.7-2.4,0.7c-1.1,0-1.9-0.3-2.6-0.9s-0.9-1.5-0.9-2.7s0.3-2,0.9-2.8s1.5-1.1,2.4-1.1c0.8,0,1.5,0.3,2,0.8
|
|
||||||
c0.5,0.5,0.8,1.2,0.8,2.2C116.7,7.3,116.7,7.5,116.7,7.7z M113.9,5.1c-0.4,0-0.8,0.1-0.9,0.5c-0.3,0.4-0.4,0.9-0.4,1.6l2.6-0.1
|
|
||||||
c0-0.1,0-0.3,0-0.5c0-0.4-0.1-0.8-0.3-1.1C114.6,5.3,114.3,5.1,113.9,5.1z"/>
|
|
||||||
<path class="st1" d="M124,11.3h-1.7V6.6c0-0.7-0.1-1.5-0.3-2.3l1.6-0.3c0.1,0.5,0.3,0.9,0.4,1.5c0.5-0.9,1.2-1.5,1.9-1.5
|
|
||||||
c0.3,0,0.5,0,0.7,0.1l-0.1,1.7c-0.3-0.1-0.5-0.1-0.8-0.1c-0.5,0-1.1,0.1-1.6,0.5C124,6.3,124,11.3,124,11.3z"/>
|
|
||||||
<path class="st1" d="M135.8,4.4l-0.1,1.3c-0.7-0.4-1.3-0.5-1.9-0.5c-0.4,0-0.7,0.1-0.8,0.3c-0.3,0.1-0.3,0.3-0.3,0.5
|
|
||||||
c0,0.3,0.1,0.4,0.4,0.7c0.3,0.1,0.5,0.4,0.8,0.5c0.3,0.1,0.7,0.3,1.1,0.4c0.4,0.1,0.7,0.4,0.8,0.7c0.3,0.3,0.4,0.7,0.4,1.1
|
|
||||||
c0,0.7-0.3,1.2-0.8,1.5c-0.5,0.4-1.2,0.5-2.2,0.5s-1.7-0.1-2.4-0.5l0.1-1.3c0.8,0.4,1.5,0.7,2.3,0.7c0.4,0,0.7-0.1,0.8-0.3
|
|
||||||
c0.1-0.1,0.3-0.3,0.3-0.5c0-0.3-0.1-0.4-0.4-0.7c-0.3-0.1-0.5-0.4-0.8-0.5s-0.7-0.3-0.9-0.4s-0.7-0.4-0.8-0.7
|
|
||||||
c-0.3-0.3-0.4-0.7-0.4-1.1c0-0.7,0.3-1.2,0.8-1.6c0.5-0.4,1.2-0.5,2-0.5C134.5,4,135.1,4.2,135.8,4.4z"/>
|
|
||||||
<path class="st1" d="M143.3,1.6c0,0.3-0.1,0.7-0.4,0.8c-0.3,0.1-0.5,0.4-0.8,0.4c-0.3,0-0.5-0.1-0.8-0.3c-0.1-0.1-0.1-0.4-0.1-0.7
|
|
||||||
s0.1-0.7,0.4-0.8c0.3-0.3,0.5-0.4,0.9-0.4c0.3,0,0.5,0.1,0.7,0.3S143.3,1.4,143.3,1.6z M141.3,11.3V6.6c0-0.8,0-1.6-0.1-2.4
|
|
||||||
l1.7-0.3l0.1,0.1v7.1L141.3,11.3L141.3,11.3z"/>
|
|
||||||
<path class="st1" d="M153,0.7l1.7-0.3l0.1,0.1v8.7c0,0.7,0.1,1.3,0.3,1.9l-1.6,0.1c-0.1-0.1-0.3-0.5-0.4-0.9l0,0
|
|
||||||
c-0.5,0.7-1.1,0.9-2,0.9c-0.9,0-1.5-0.3-2-0.9c-0.5-0.7-0.8-1.5-0.8-2.6c0-1.2,0.4-2.2,1.1-3c0.7-0.7,1.6-1.1,2.7-1.1
|
|
||||||
c0.3,0,0.5,0,0.9,0.1V3C153.2,2,153.2,1.4,153,0.7z M150.5,7.7c0,0.8,0.1,1.5,0.4,1.9c0.3,0.5,0.7,0.7,1.2,0.7
|
|
||||||
c0.4,0,0.8-0.1,1.1-0.4V5c-0.3,0-0.5-0.1-0.7-0.1c-0.7,0-1.1,0.3-1.5,0.7C150.6,6.2,150.5,6.9,150.5,7.7z"/>
|
|
||||||
<path class="st1" d="M166.1,9.3c0,0.7,0.1,1.3,0.3,2l-1.5,0.1c-0.1-0.3-0.3-0.5-0.4-0.9l0,0c-0.3,0.3-0.5,0.5-0.9,0.7
|
|
||||||
c-0.4,0.1-0.8,0.3-1.2,0.3c-0.5,0-1.1-0.1-1.3-0.4c-0.4-0.3-0.5-0.7-0.5-1.2c0-0.8,0.4-1.3,1.1-1.7c0.7-0.4,1.6-0.7,2.8-0.7V6.7
|
|
||||||
c0-0.8-0.4-1.3-1.3-1.3c-0.7,0-1.5,0.3-2.2,0.7l-0.1-1.3c0.9-0.4,1.7-0.5,2.7-0.5s1.6,0.3,2,0.7c0.4,0.4,0.7,0.9,0.7,1.7
|
|
||||||
c0,0.4,0,0.8,0,1.5C166.1,8.6,166.1,9,166.1,9.3z M162.2,9.4c0,0.3,0.1,0.5,0.3,0.7c0.1,0.1,0.4,0.3,0.7,0.3
|
|
||||||
c0.4,0,0.8-0.1,1.2-0.5V7.9c-0.7,0-1.1,0.3-1.5,0.4C162.3,8.6,162.2,9,162.2,9.4z"/>
|
|
||||||
<path class="st1" d="M175.6,0.7l1.7-0.3l0.1,0.1v8.7c0,0.7,0.1,1.3,0.3,1.9l-1.6,0.1c-0.1-0.1-0.3-0.5-0.4-0.9l0,0
|
|
||||||
c-0.5,0.7-1.1,0.9-2,0.9s-1.5-0.3-2-0.9c-0.5-0.7-0.8-1.5-0.8-2.6c0-1.2,0.4-2.2,1.1-3c0.7-0.7,1.6-1.1,2.7-1.1
|
|
||||||
c0.3,0,0.5,0,0.9,0.1V3C175.7,2,175.7,1.4,175.6,0.7z M173.1,7.7c0,0.8,0.1,1.5,0.4,1.9c0.3,0.5,0.7,0.7,1.2,0.7
|
|
||||||
c0.4,0,0.8-0.1,1.1-0.4V5c-0.3,0-0.5-0.1-0.7-0.1c-0.7,0-1.1,0.3-1.5,0.7C173.2,6.2,173.1,6.9,173.1,7.7z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="st1" d="M78.8,51.2l0.3,0.3l1.2,11h-2l-0.4-4c-0.1-1.2-0.3-3-0.4-5l0,0c-0.3,1.2-0.7,2.8-1.2,5l-1.2,4h-2.3l-1.1-4
|
|
||||||
c-0.5-1.9-0.9-3.5-1.2-5l0,0c-0.1,1.2-0.3,2.8-0.4,5l-0.4,4h-1.7l1.2-11.2l2.7-0.1l1.3,4.6c0.4,1.6,0.8,3.2,1.1,4.8l0,0
|
|
||||||
c0.3-1.6,0.5-3.2,1.1-4.8l1.3-4.4L78.8,51.2z"/>
|
|
||||||
<path class="st1" d="M89.4,58.5l-0.3,0.3h-4.4c0.1,0.8,0.3,1.5,0.8,1.9c0.5,0.4,0.9,0.7,1.6,0.7s1.3-0.1,2-0.5l0.1,1.3
|
|
||||||
c-0.7,0.4-1.6,0.7-2.7,0.7c-1.2,0-2.2-0.4-3-1.1c-0.7-0.7-1.1-1.7-1.1-3c0-1.3,0.4-2.3,1.1-3.1c0.7-0.8,1.6-1.2,2.7-1.2
|
|
||||||
c0.9,0,1.7,0.3,2.3,0.9c0.5,0.5,0.8,1.3,0.8,2.4C89.4,58,89.4,58.4,89.4,58.5z M87.7,50.4l0.1,0.4c-0.7,0.9-1.5,1.9-2.6,2.7
|
|
||||||
l-0.8-0.1c0.7-1.1,1.1-2.2,1.3-3H87.7z M86.3,55.5c-0.4,0-0.8,0.3-1.1,0.7c-0.3,0.4-0.4,1.1-0.5,1.7l2.8-0.1c0-0.1,0-0.3,0-0.5
|
|
||||||
c0-0.5-0.1-0.9-0.3-1.2C87,55.7,86.7,55.5,86.3,55.5z"/>
|
|
||||||
<path class="st1" d="M96.4,62.5l-1.7-3.1L93,62.5h-1.6l-0.1-0.3l2.3-3.8l-2.3-4l2-0.3l1.7,3.4l1.6-3.4l1.6,0.1l0.1,0.3L96,58.4
|
|
||||||
l2.4,4h-2V62.5z"/>
|
|
||||||
<path class="st1" d="M103.1,51.8c0,0.4-0.1,0.7-0.4,0.9s-0.5,0.4-0.9,0.4c-0.4,0-0.7-0.1-0.8-0.4c-0.3-0.3-0.3-0.5-0.3-0.8
|
|
||||||
c0-0.4,0.1-0.7,0.4-0.9c0.3-0.3,0.5-0.4,0.9-0.4c0.3,0,0.5,0.1,0.8,0.3C103,51.1,103.1,51.4,103.1,51.8z M100.8,62.5v-5.2
|
|
||||||
c0-0.9,0-1.9-0.1-2.7l2-0.3l0.3,0.3v7.9H100.8z"/>
|
|
||||||
<path class="st1" d="M112.3,55l-0.4,1.6c-0.7-0.4-1.2-0.7-1.9-0.7c-0.5,0-1.1,0.3-1.5,0.7c-0.4,0.4-0.5,1.1-0.5,1.9
|
|
||||||
c0,0.9,0.1,1.6,0.7,2c0.4,0.5,0.9,0.8,1.7,0.8c0.5,0,1.2-0.1,1.7-0.5l0.1,1.3c-0.7,0.4-1.5,0.7-2.4,0.7c-1.2,0-2.2-0.4-2.8-1.1
|
|
||||||
s-1.1-1.7-1.1-3c0-1.3,0.4-2.3,1.1-3.1c0.8-0.8,1.7-1.2,2.8-1.2C110.8,54.3,111.6,54.6,112.3,55z"/>
|
|
||||||
<path class="st1" d="M121.4,58.5c0,1.3-0.4,2.4-1.1,3.1s-1.6,1.1-2.7,1.1c-1.1,0-1.9-0.4-2.6-1.1s-1.1-1.7-1.1-3
|
|
||||||
c0-1.3,0.4-2.4,1.1-3.1s1.6-1.2,2.7-1.2c1.1,0,2,0.4,2.7,1.1C121.1,56.2,121.4,57.3,121.4,58.5z M116,58.5c0,2,0.5,3,1.6,3
|
|
||||||
c0.5,0,0.9-0.3,1.2-0.8c0.3-0.5,0.4-1.2,0.4-2.2c0-2-0.5-3.1-1.6-3.1c-0.5,0-0.9,0.3-1.2,0.8C116.3,56.8,116,57.6,116,58.5z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="st2" d="M83.5,41v3.8H68.3V25.3c0-2.4-0.1-4.6-0.4-6.5l4.6-0.4L73,19v22.2L83.5,41z"/>
|
|
||||||
<path class="st2" d="M100.4,39.5c0,1.9,0.3,3.6,0.8,5.1l-3.9,0.4c-0.4-0.7-0.8-1.6-1.1-2.6h-0.1c-0.5,0.7-1.3,1.3-2.3,1.9
|
|
||||||
c-0.9,0.5-2.2,0.8-3.2,0.8c-1.5,0-2.7-0.4-3.6-1.2c-0.9-0.8-1.3-1.9-1.3-3.4c0-2,0.9-3.6,2.8-4.6c1.9-1.1,4.3-1.6,7.4-1.7v-1.7
|
|
||||||
c0-2.3-1.2-3.4-3.5-3.4c-1.9,0-3.8,0.5-5.6,1.6l-0.3-3.6c2.4-0.9,4.7-1.5,7.1-1.5c2.3,0,4,0.5,5.2,1.6c1.2,1.1,1.7,2.6,1.7,4.6
|
|
||||||
c0,0.9,0,2.3,0,4C100.4,37.7,100.4,38.9,100.4,39.5z M90.2,39.8c0,0.7,0.3,1.3,0.7,1.7c0.4,0.5,1.1,0.7,1.7,0.7
|
|
||||||
c1.2,0,2.2-0.4,3.1-1.3v-5.1c-1.6,0.1-3,0.5-4,1.2C90.8,37.8,90.2,38.7,90.2,39.8z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path class="st2" d="M126.3,37.3c0,2.4-0.8,4.2-2.6,5.5c-1.7,1.3-4,2-6.9,2c-3.1,0-5.5-0.5-7.3-1.5L110,39c0.9,0.5,2,1.1,3.5,1.3
|
|
||||||
c1.3,0.3,2.6,0.4,3.6,0.4c1.3,0,2.4-0.3,3.2-0.9c0.8-0.5,1.1-1.3,1.1-2.4c0-0.3,0-0.5-0.1-0.8c0-0.3-0.1-0.5-0.3-0.7
|
|
||||||
s-0.3-0.4-0.4-0.7c-0.1-0.3-0.4-0.4-0.4-0.5c-0.1-0.1-0.3-0.3-0.7-0.5c-0.3-0.3-0.5-0.4-0.7-0.4c-0.1-0.1-0.4-0.3-0.8-0.4
|
|
||||||
c-0.4-0.3-0.7-0.4-0.8-0.4c-0.1-0.1-0.4-0.3-0.9-0.4c-0.4-0.3-0.7-0.4-0.8-0.4c-0.9-0.4-1.6-0.8-2.2-1.2c-0.5-0.4-1.2-0.8-1.7-1.5
|
|
||||||
c-0.7-0.5-1.1-1.2-1.3-2c-0.3-0.8-0.4-1.6-0.4-2.7c0-2.4,0.9-4.2,2.7-5.5c1.7-1.3,4.2-2,7-2c2.4,0,4.4,0.4,6.2,1.1l-0.7,4.3
|
|
||||||
c-1.7-1.1-3.6-1.5-5.6-1.5c-1.5,0-2.6,0.3-3.4,0.9c-0.8,0.7-1.2,1.3-1.2,2.4c0,0.4,0,0.7,0.1,1.1c0.1,0.3,0.3,0.7,0.5,0.9
|
|
||||||
c0.3,0.3,0.5,0.5,0.7,0.8c0.1,0.1,0.5,0.4,0.9,0.7c0.5,0.3,0.8,0.5,1.1,0.5c0.1,0.1,0.5,0.3,1.2,0.7c0.7,0.3,1.1,0.5,1.2,0.5
|
|
||||||
c0.8,0.4,1.5,0.8,2.2,1.2c0.5,0.4,1.2,0.8,1.7,1.5c0.7,0.7,1.1,1.3,1.3,2.2C126.1,35.4,126.3,36.3,126.3,37.3z"/>
|
|
||||||
<path class="st2" d="M143.9,39.1c0,1.9,0.3,3.8,0.8,5.4l-4,0.4c-0.4-0.7-0.8-1.6-1.1-2.7h-0.1c-0.5,0.8-1.3,1.3-2.4,1.9
|
|
||||||
c-1.1,0.5-2.2,0.8-3.4,0.8c-1.6,0-2.8-0.4-3.8-1.2c-0.9-0.8-1.3-2-1.3-3.5c0-2.2,0.9-3.6,3-4.7c1.9-1.1,4.4-1.6,7.7-1.7v-1.9
|
|
||||||
c0-2.3-1.2-3.5-3.6-3.5c-2,0-3.9,0.5-5.9,1.7l-0.3-3.8c2.4-1.1,4.8-1.5,7.4-1.5c2.4,0,4.2,0.5,5.4,1.6c1.2,1.1,1.9,2.7,1.9,4.8
|
|
||||||
c0,0.9,0,2.3,0,4.2C143.9,37.1,143.9,38.3,143.9,39.1z M133.4,39.3c0,0.7,0.3,1.3,0.7,1.9c0.4,0.5,1.1,0.8,1.9,0.8
|
|
||||||
c1.2,0,2.3-0.4,3.2-1.3v-5.2c-1.7,0.1-3.1,0.5-4.2,1.2S133.4,38.2,133.4,39.3z"/>
|
|
||||||
<path class="st2" d="M153.7,44.4h-4.8V21.9c0-2-0.1-4.2-0.4-6.5l4.8-0.5l0.5,0.5L153.7,44.4L153.7,44.4z"/>
|
|
||||||
<path class="st2" d="M163.4,44.4h-4.8V21.9c0-2-0.1-4.2-0.4-6.5l4.8-0.5l0.5,0.5L163.4,44.4L163.4,44.4z"/>
|
|
||||||
<path class="st2" d="M183.7,34.7l-0.5,0.5h-10.9c0.1,2,0.8,3.6,1.7,4.6c0.9,0.9,2.4,1.5,4,1.5c1.6,0,3.2-0.4,4.8-1.3l0.3,3.2
|
|
||||||
c-1.7,1.1-3.9,1.6-6.5,1.6c-3,0-5.2-0.8-7-2.6s-2.6-4.2-2.6-7.3c0-3.1,0.8-5.6,2.6-7.5c1.7-1.9,3.9-2.8,6.6-2.8
|
|
||||||
c2.3,0,4.2,0.7,5.5,2.2c1.3,1.5,2,3.4,2,5.6C183.8,33.5,183.8,34.2,183.7,34.7z M176,27.6c-1.1,0-2,0.5-2.7,1.6
|
|
||||||
c-0.7,1.1-1.1,2.6-1.2,4.3l6.7-0.3c0-0.3,0-0.8,0-1.3c0-1.2-0.3-2.3-0.8-3.1S176.9,27.6,176,27.6z"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path id="Path_483" class="st1" d="M187,39.4h1.5c0.3,0,0.7,0.1,1,0.2c0.3,0.2,0.5,0.5,0.5,0.8c0,0.3-0.1,0.6-0.3,0.8
|
|
||||||
c-0.1,0.1-0.3,0.2-0.5,0.3c0.4,0.1,0.6,0.3,0.6,0.8c0,0.4,0.1,0.9,0.3,1.3h-0.6c-0.1-0.4-0.2-0.7-0.2-1.1
|
|
||||||
c-0.1-0.6-0.2-0.8-0.9-0.8h-0.7v1.9H187V39.4 M187.5,41.2h0.9c0.2,0,0.4,0,0.6-0.1c0.2-0.1,0.3-0.3,0.3-0.6c0-0.7-0.6-0.7-0.8-0.7
|
|
||||||
h-0.9V41.2z"/>
|
|
||||||
<path id="Path_484" class="st1" d="M191.9,41.5c0,1.9-1.6,3.4-3.4,3.3s-3.4-1.6-3.3-3.4c0-1.9,1.5-3.3,3.4-3.3
|
|
||||||
C190.4,38.1,191.9,39.6,191.9,41.5z M188.5,37.8c-2.1,0-3.7,1.6-3.8,3.7c0,2.1,1.6,3.7,3.7,3.8c2.1,0,3.7-1.6,3.8-3.7c0,0,0,0,0,0
|
|
||||||
C192.2,39.4,190.5,37.8,188.5,37.8z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 10 KiB |
@@ -1,19 +0,0 @@
|
|||||||
// scripts/update-types.ts
|
|
||||||
/* Uso:
|
|
||||||
bun run scripts/update-types.ts
|
|
||||||
*/
|
|
||||||
import { $ } from "bun";
|
|
||||||
|
|
||||||
console.log("🔄 Generando tipos de Supabase...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Ejecutamos el comando y capturamos la salida como texto
|
|
||||||
const output = await $`supabase gen types typescript --linked`.text();
|
|
||||||
|
|
||||||
// Escribimos el archivo directamente con Bun (garantiza UTF-8)
|
|
||||||
await Bun.write("src/types/supabase.ts", output);
|
|
||||||
|
|
||||||
console.log("✅ Tipos actualizados correctamente con acentos.");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Error generando tipos:", error);
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,11 @@ export default function Header() {
|
|||||||
</button>
|
</button>
|
||||||
<h1 className="ml-4 text-xl font-semibold">
|
<h1 className="ml-4 text-xl font-semibold">
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<img src="/lasalle-logo.svg" alt="La Salle Logo" className="h-10" />
|
<img
|
||||||
|
src="/tanstack-word-logo-white.svg"
|
||||||
|
alt="TanStack Logo"
|
||||||
|
className="h-10"
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,798 +0,0 @@
|
|||||||
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
|
||||||
import { Minus, Pencil, Plus, Sparkles } from 'lucide-react'
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
import type { AsignaturaDetail } from '@/data'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects'
|
|
||||||
import { columnParsers } from '@/lib/asignaturaColumnParsers'
|
|
||||||
|
|
||||||
export interface BibliografiaEntry {
|
|
||||||
id: string
|
|
||||||
tipo: 'BASICA' | 'COMPLEMENTARIA'
|
|
||||||
cita: string
|
|
||||||
fuenteBibliotecaId?: string
|
|
||||||
fuenteBiblioteca?: any
|
|
||||||
}
|
|
||||||
export interface BibliografiaTabProps {
|
|
||||||
id: string
|
|
||||||
bibliografia: Array<BibliografiaEntry>
|
|
||||||
onSave: (bibliografia: Array<BibliografiaEntry>) => void
|
|
||||||
isSaving: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsignaturaDatos {
|
|
||||||
[key: string]: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AsignaturaResponse {
|
|
||||||
datos: AsignaturaDatos
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CriterioEvaluacionRow = {
|
|
||||||
criterio: string
|
|
||||||
porcentaje: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type CriterioEvaluacionRowDraft = {
|
|
||||||
id: string
|
|
||||||
criterio: string
|
|
||||||
porcentaje: string // allow empty while editing
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
|
||||||
'/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
)({
|
|
||||||
component: AsignaturaDetailPage,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default function AsignaturaDetailPage() {
|
|
||||||
const { asignaturaId } = useParams({
|
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
})
|
|
||||||
const { data: asignaturaApi } = useSubject(asignaturaId)
|
|
||||||
|
|
||||||
const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null)
|
|
||||||
const updateAsignatura = useUpdateAsignatura()
|
|
||||||
|
|
||||||
const handlePersistDatoGeneral = (clave: string, value: string) => {
|
|
||||||
const baseDatos = asignatura?.datos ?? (asignaturaApi as any)?.datos ?? {}
|
|
||||||
const mergedDatos = { ...baseDatos, [clave]: value }
|
|
||||||
|
|
||||||
// Mantener estado local coherente para merges posteriores.
|
|
||||||
setAsignatura((prev) => ({
|
|
||||||
...((prev ?? asignaturaApi ?? {}) as any),
|
|
||||||
datos: mergedDatos,
|
|
||||||
}))
|
|
||||||
|
|
||||||
updateAsignatura.mutate({
|
|
||||||
asignaturaId,
|
|
||||||
patch: {
|
|
||||||
datos: mergedDatos,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/* ---------- sincronizar API ---------- */
|
|
||||||
useEffect(() => {
|
|
||||||
if (asignaturaApi) setAsignatura(asignaturaApi)
|
|
||||||
}, [asignaturaApi])
|
|
||||||
|
|
||||||
return <DatosGenerales onPersistDato={handlePersistDatoGeneral} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DatosGenerales({
|
|
||||||
onPersistDato,
|
|
||||||
}: {
|
|
||||||
onPersistDato: (clave: string, value: string) => void
|
|
||||||
}) {
|
|
||||||
const { asignaturaId, planId } = useParams({
|
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
})
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const { data: data, isLoading: isLoading } = useSubject(asignaturaId)
|
|
||||||
const updateAsignatura = useUpdateAsignatura()
|
|
||||||
|
|
||||||
const evaluationCardRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
const [evaluationForceEditToken, setEvaluationForceEditToken] =
|
|
||||||
useState<number>(0)
|
|
||||||
const [evaluationHighlightToken, setEvaluationHighlightToken] =
|
|
||||||
useState<number>(0)
|
|
||||||
|
|
||||||
// 1. Extraemos la definición de la estructura (los metadatos)
|
|
||||||
const definicionRaw = data?.estructuras_asignatura?.definicion
|
|
||||||
const definicion = isRecord(definicionRaw)
|
|
||||||
? (definicionRaw as Record<string, unknown>)
|
|
||||||
: null
|
|
||||||
|
|
||||||
const propertiesRaw = definicion ? (definicion as any).properties : undefined
|
|
||||||
const structureProps = isRecord(propertiesRaw)
|
|
||||||
? (propertiesRaw as Record<string, any>)
|
|
||||||
: {}
|
|
||||||
|
|
||||||
// 2. Extraemos los valores reales (el contenido redactado)
|
|
||||||
const datosRaw = data?.datos
|
|
||||||
const valoresActuales = isRecord(datosRaw)
|
|
||||||
? (datosRaw as Record<string, any>)
|
|
||||||
: {}
|
|
||||||
|
|
||||||
const criteriosEvaluacion: Array<CriterioEvaluacionRow> = useMemo(() => {
|
|
||||||
const raw = (data as any)?.criterios_de_evaluacion
|
|
||||||
console.log(raw)
|
|
||||||
|
|
||||||
if (!Array.isArray(raw)) return []
|
|
||||||
|
|
||||||
const rows: Array<CriterioEvaluacionRow> = []
|
|
||||||
for (const item of raw) {
|
|
||||||
if (!isRecord(item)) continue
|
|
||||||
const criterio = typeof item.criterio === 'string' ? item.criterio : ''
|
|
||||||
const porcentajeNum =
|
|
||||||
typeof item.porcentaje === 'number'
|
|
||||||
? item.porcentaje
|
|
||||||
: typeof item.porcentaje === 'string'
|
|
||||||
? Number(item.porcentaje)
|
|
||||||
: NaN
|
|
||||||
|
|
||||||
if (!criterio.trim()) continue
|
|
||||||
if (!Number.isFinite(porcentajeNum)) continue
|
|
||||||
const porcentaje = Math.trunc(porcentajeNum)
|
|
||||||
if (porcentaje < 1 || porcentaje > 100) continue
|
|
||||||
|
|
||||||
rows.push({ criterio: criterio.trim(), porcentaje: porcentaje })
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
const openEvaluationEditor = () => {
|
|
||||||
evaluationCardRef.current?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start',
|
|
||||||
})
|
|
||||||
|
|
||||||
const now = Date.now()
|
|
||||||
setEvaluationForceEditToken(now)
|
|
||||||
setEvaluationHighlightToken(now)
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistCriteriosEvaluacion = async (
|
|
||||||
rows: Array<CriterioEvaluacionRow>,
|
|
||||||
) => {
|
|
||||||
await updateAsignatura.mutateAsync({
|
|
||||||
asignaturaId: asignaturaId as any,
|
|
||||||
patch: {
|
|
||||||
criterios_de_evaluacion: rows,
|
|
||||||
} as any,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (isLoading) return <p>Cargando información...</p>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
|
|
||||||
{/* Encabezado de la Sección */}
|
|
||||||
<div className="flex flex-col justify-between gap-4 border-b pb-6 md:flex-row md:items-center">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-slate-900">
|
|
||||||
Datos Generales
|
|
||||||
</h2>
|
|
||||||
<p className="mt-1 text-slate-500">
|
|
||||||
Información oficial estructurada bajo los lineamientos de la SEP.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Grid de Información */}
|
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
|
||||||
{/* Columna Principal (Más ancha) */}
|
|
||||||
<div className="space-y-6 md:col-span-2">
|
|
||||||
{Object.entries(structureProps).map(
|
|
||||||
([key, config]: [string, any]) => {
|
|
||||||
const cardTitle = config.title || key
|
|
||||||
const description = config.description || ''
|
|
||||||
|
|
||||||
const xColumn =
|
|
||||||
typeof config?.['x-column'] === 'string'
|
|
||||||
? config['x-column']
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
// Obtenemos el placeholder del arreglo 'examples' de la estructura
|
|
||||||
const placeholder =
|
|
||||||
config.examples && config.examples.length > 0
|
|
||||||
? config.examples[0]
|
|
||||||
: ''
|
|
||||||
|
|
||||||
const valActual = valoresActuales[key]
|
|
||||||
|
|
||||||
let currentContent = valActual ?? ''
|
|
||||||
|
|
||||||
if (xColumn) {
|
|
||||||
const rawValue = (data as any)?.[xColumn]
|
|
||||||
const parser = columnParsers[xColumn]
|
|
||||||
currentContent = parser
|
|
||||||
? parser(rawValue)
|
|
||||||
: String(rawValue ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InfoCard
|
|
||||||
asignaturaId={asignaturaId}
|
|
||||||
key={key}
|
|
||||||
clave={key}
|
|
||||||
title={cardTitle}
|
|
||||||
initialContent={currentContent}
|
|
||||||
placeholder={placeholder}
|
|
||||||
description={description}
|
|
||||||
onPersist={({ clave, value }) =>
|
|
||||||
onPersistDato(String(clave ?? key), String(value ?? ''))
|
|
||||||
}
|
|
||||||
onClickEditButton={({ startEditing }) => {
|
|
||||||
switch (xColumn) {
|
|
||||||
case 'contenido_tematico': {
|
|
||||||
navigate({
|
|
||||||
to: '/planes/$planId/asignaturas/$asignaturaId/contenido',
|
|
||||||
params: { planId, asignaturaId },
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case 'criterios_de_evaluacion': {
|
|
||||||
openEvaluationEditor()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
startEditing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Columna Lateral (Información Secundaria) */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Tarjeta de Requisitos */}
|
|
||||||
<InfoCard
|
|
||||||
title="Requisitos y Seriación"
|
|
||||||
type="requirements"
|
|
||||||
initialContent={[
|
|
||||||
{
|
|
||||||
type: 'Pre-requisito',
|
|
||||||
code: 'PA-301',
|
|
||||||
name: 'Programación Avanzada',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'Co-requisito',
|
|
||||||
code: 'MAT-201',
|
|
||||||
name: 'Matemáticas Discretas',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Tarjeta de Evaluación */}
|
|
||||||
<InfoCard
|
|
||||||
title="Sistema de Evaluación"
|
|
||||||
type="evaluation"
|
|
||||||
initialContent={criteriosEvaluacion}
|
|
||||||
containerRef={evaluationCardRef}
|
|
||||||
forceEditToken={evaluationForceEditToken}
|
|
||||||
highlightToken={evaluationHighlightToken}
|
|
||||||
onPersist={({ value }) => persistCriteriosEvaluacion(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InfoCardProps {
|
|
||||||
asignaturaId?: string
|
|
||||||
clave?: string
|
|
||||||
title: string
|
|
||||||
initialContent: any
|
|
||||||
placeholder?: string
|
|
||||||
description?: string
|
|
||||||
required?: boolean // Nueva prop para el asterisco
|
|
||||||
type?: 'text' | 'requirements' | 'evaluation'
|
|
||||||
onEnhanceAI?: (content: any) => void
|
|
||||||
onPersist?: (payload: {
|
|
||||||
type: NonNullable<InfoCardProps['type']>
|
|
||||||
clave?: string
|
|
||||||
value: any
|
|
||||||
}) => void | Promise<void>
|
|
||||||
onClickEditButton?: (helpers: { startEditing: () => void }) => void
|
|
||||||
|
|
||||||
containerRef?: React.RefObject<HTMLDivElement | null>
|
|
||||||
forceEditToken?: number
|
|
||||||
highlightToken?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
function InfoCard({
|
|
||||||
asignaturaId,
|
|
||||||
clave,
|
|
||||||
title,
|
|
||||||
initialContent,
|
|
||||||
placeholder,
|
|
||||||
description,
|
|
||||||
required,
|
|
||||||
type = 'text',
|
|
||||||
onPersist,
|
|
||||||
onClickEditButton,
|
|
||||||
containerRef,
|
|
||||||
forceEditToken,
|
|
||||||
highlightToken,
|
|
||||||
}: InfoCardProps) {
|
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
|
||||||
const [isHighlighted, setIsHighlighted] = useState(false)
|
|
||||||
const [data, setData] = useState(initialContent)
|
|
||||||
const [tempText, setTempText] = useState(initialContent)
|
|
||||||
|
|
||||||
const [evalRows, setEvalRows] = useState<Array<CriterioEvaluacionRowDraft>>(
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { planId } = useParams({
|
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setData(initialContent)
|
|
||||||
setTempText(initialContent)
|
|
||||||
|
|
||||||
if (type === 'evaluation') {
|
|
||||||
const raw = Array.isArray(initialContent) ? initialContent : []
|
|
||||||
const rows: Array<CriterioEvaluacionRowDraft> = raw
|
|
||||||
.map((r: any): CriterioEvaluacionRowDraft | null => {
|
|
||||||
const criterio = typeof r?.criterio === 'string' ? r.criterio : ''
|
|
||||||
const porcentajeNum =
|
|
||||||
typeof r?.porcentaje === 'number'
|
|
||||||
? r.porcentaje
|
|
||||||
: typeof r?.porcentaje === 'string'
|
|
||||||
? Number(r.porcentaje)
|
|
||||||
: NaN
|
|
||||||
|
|
||||||
const porcentaje = Number.isFinite(porcentajeNum)
|
|
||||||
? String(Math.trunc(porcentajeNum))
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
criterio,
|
|
||||||
porcentaje,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean) as Array<CriterioEvaluacionRowDraft>
|
|
||||||
|
|
||||||
setEvalRows(rows)
|
|
||||||
}
|
|
||||||
}, [initialContent, type])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!forceEditToken) return
|
|
||||||
setIsEditing(true)
|
|
||||||
}, [forceEditToken])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!highlightToken) return
|
|
||||||
setIsHighlighted(true)
|
|
||||||
const t = window.setTimeout(() => setIsHighlighted(false), 900)
|
|
||||||
return () => window.clearTimeout(t)
|
|
||||||
}, [highlightToken])
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
console.log('clave, valor:', clave, String(tempText ?? ''))
|
|
||||||
|
|
||||||
if (type === 'evaluation') {
|
|
||||||
const cleaned: Array<CriterioEvaluacionRow> = []
|
|
||||||
for (const r of evalRows) {
|
|
||||||
const criterio = String(r.criterio).trim()
|
|
||||||
const porcentajeStr = String(r.porcentaje).trim()
|
|
||||||
if (!criterio) continue
|
|
||||||
if (!porcentajeStr) continue
|
|
||||||
|
|
||||||
const n = Number(porcentajeStr)
|
|
||||||
if (!Number.isFinite(n)) continue
|
|
||||||
const porcentaje = Math.trunc(n)
|
|
||||||
if (porcentaje < 1 || porcentaje > 100) continue
|
|
||||||
|
|
||||||
cleaned.push({ criterio, porcentaje })
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(cleaned)
|
|
||||||
setEvalRows(
|
|
||||||
cleaned.map((x) => ({
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
criterio: x.criterio,
|
|
||||||
porcentaje: String(x.porcentaje),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
setIsEditing(false)
|
|
||||||
|
|
||||||
void onPersist?.({ type, clave, value: cleaned })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setData(tempText)
|
|
||||||
setIsEditing(false)
|
|
||||||
|
|
||||||
if (type === 'text') {
|
|
||||||
void onPersist?.({ type, clave, value: String(tempText ?? '') })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleIARequest = (campoClave: string) => {
|
|
||||||
console.log(placeholder)
|
|
||||||
|
|
||||||
// Añadimos un timestamp a la state para forzar que la navegación
|
|
||||||
// genere una nueva ubicación incluso si la ruta y los params son iguales.
|
|
||||||
navigate({
|
|
||||||
to: '/planes/$planId/asignaturas/$asignaturaId/iaasignatura',
|
|
||||||
params: { planId, asignaturaId: asignaturaId! },
|
|
||||||
state: {
|
|
||||||
activeTab: 'ia',
|
|
||||||
prefillCampo: campoClave,
|
|
||||||
prefillContenido: data,
|
|
||||||
_ts: Date.now(),
|
|
||||||
} as any,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const evaluationTotal = useMemo(() => {
|
|
||||||
if (type !== 'evaluation') return 0
|
|
||||||
return evalRows.reduce((acc, r) => {
|
|
||||||
const v = String(r.porcentaje).trim()
|
|
||||||
if (!v) return acc
|
|
||||||
const n = Number(v)
|
|
||||||
if (!Number.isFinite(n)) return acc
|
|
||||||
const porcentaje = Math.trunc(n)
|
|
||||||
if (porcentaje < 1 || porcentaje > 100) return acc
|
|
||||||
return acc + porcentaje
|
|
||||||
}, 0)
|
|
||||||
}, [type, evalRows])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={containerRef as any}>
|
|
||||||
<Card
|
|
||||||
className={
|
|
||||||
'overflow-hidden transition-all hover:border-slate-300 ' +
|
|
||||||
(isHighlighted ? 'ring-primary/40 ring-2' : '')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TooltipProvider>
|
|
||||||
<CardHeader className="border-b bg-slate-50/50 px-5 py-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<CardTitle className="cursor-help text-sm font-bold text-slate-700">
|
|
||||||
{title}
|
|
||||||
</CardTitle>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="max-w-xs text-xs">
|
|
||||||
{description || 'Información del campo'}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
{required && (
|
|
||||||
<span
|
|
||||||
className="text-sm font-bold text-red-500"
|
|
||||||
title="Requerido"
|
|
||||||
>
|
|
||||||
*
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!isEditing && (
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-blue-500 hover:bg-blue-100"
|
|
||||||
onClick={() => clave && handleIARequest(clave)}
|
|
||||||
>
|
|
||||||
<Sparkles className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Mejorar con IA</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-slate-400"
|
|
||||||
onClick={() => {
|
|
||||||
const startEditing = () => setIsEditing(true)
|
|
||||||
|
|
||||||
if (onClickEditButton) {
|
|
||||||
onClickEditButton({ startEditing })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
startEditing()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Pencil className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Editar campo</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<CardContent className="pt-4">
|
|
||||||
{isEditing ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{type === 'evaluation' ? (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{evalRows.map((row) => (
|
|
||||||
<div
|
|
||||||
key={row.id}
|
|
||||||
className="grid grid-cols-[2fr_1fr_1ch_32px] items-center gap-2"
|
|
||||||
>
|
|
||||||
<Input
|
|
||||||
value={row.criterio}
|
|
||||||
placeholder="Criterio"
|
|
||||||
onChange={(e) => {
|
|
||||||
const nextCriterio = e.target.value
|
|
||||||
setEvalRows((prev) =>
|
|
||||||
prev.map((r) =>
|
|
||||||
r.id === row.id
|
|
||||||
? { ...r, criterio: nextCriterio }
|
|
||||||
: r,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
|
||||||
value={row.porcentaje}
|
|
||||||
placeholder="%"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={100}
|
|
||||||
step={1}
|
|
||||||
inputMode="numeric"
|
|
||||||
onChange={(e) => {
|
|
||||||
const raw = e.target.value
|
|
||||||
// Solo permitir '' o dígitos
|
|
||||||
if (raw !== '' && !/^\d+$/.test(raw)) return
|
|
||||||
|
|
||||||
if (raw === '') {
|
|
||||||
setEvalRows((prev) =>
|
|
||||||
prev.map((r) =>
|
|
||||||
r.id === row.id
|
|
||||||
? {
|
|
||||||
id: r.id,
|
|
||||||
criterio: r.criterio,
|
|
||||||
porcentaje: '',
|
|
||||||
}
|
|
||||||
: r,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const n = Number(raw)
|
|
||||||
if (!Number.isFinite(n)) return
|
|
||||||
const porcentaje = Math.trunc(n)
|
|
||||||
if (porcentaje < 1 || porcentaje > 100) return
|
|
||||||
|
|
||||||
// No permitir suma > 100
|
|
||||||
setEvalRows((prev) => {
|
|
||||||
const next = prev.map((r) =>
|
|
||||||
r.id === row.id
|
|
||||||
? {
|
|
||||||
id: r.id,
|
|
||||||
criterio: r.criterio,
|
|
||||||
porcentaje: raw,
|
|
||||||
}
|
|
||||||
: r,
|
|
||||||
)
|
|
||||||
|
|
||||||
const total = next.reduce((acc, r) => {
|
|
||||||
const v = String(r.porcentaje).trim()
|
|
||||||
if (!v) return acc
|
|
||||||
const nn = Number(v)
|
|
||||||
if (!Number.isFinite(nn)) return acc
|
|
||||||
const vv = Math.trunc(nn)
|
|
||||||
if (vv < 1 || vv > 100) return acc
|
|
||||||
return acc + vv
|
|
||||||
}, 0)
|
|
||||||
|
|
||||||
return total > 100 ? prev : next
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="flex w-[1ch] items-center justify-center text-sm text-slate-600"
|
|
||||||
aria-hidden
|
|
||||||
>
|
|
||||||
%
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-red-600 hover:bg-red-50"
|
|
||||||
onClick={() => {
|
|
||||||
setEvalRows((prev) =>
|
|
||||||
prev.filter((r) => r.id !== row.id),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
aria-label="Quitar renglón"
|
|
||||||
title="Quitar"
|
|
||||||
>
|
|
||||||
<Minus className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
'text-sm ' +
|
|
||||||
(evaluationTotal === 100
|
|
||||||
? 'text-muted-foreground'
|
|
||||||
: 'text-destructive font-semibold')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Total: {evaluationTotal}/100
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="text-emerald-700 hover:bg-emerald-50"
|
|
||||||
onClick={() => {
|
|
||||||
// Agregar una fila vacía (siempre permitido)
|
|
||||||
setEvalRows((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
criterio: '',
|
|
||||||
porcentaje: '',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="mr-2 h-4 w-4" /> Agregar renglón
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Textarea
|
|
||||||
value={tempText}
|
|
||||||
placeholder={placeholder}
|
|
||||||
onChange={(e) => setTempText(e.target.value)}
|
|
||||||
className="min-h-30 text-sm leading-relaxed"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => {
|
|
||||||
setIsEditing(false)
|
|
||||||
if (type === 'evaluation') {
|
|
||||||
const raw = Array.isArray(data) ? data : []
|
|
||||||
setEvalRows(
|
|
||||||
raw.map((r: CriterioEvaluacionRow) => ({
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
criterio:
|
|
||||||
typeof r.criterio === 'string' ? r.criterio : '',
|
|
||||||
porcentaje:
|
|
||||||
typeof r.porcentaje === 'number'
|
|
||||||
? String(Math.trunc(r.porcentaje))
|
|
||||||
: typeof r.porcentaje === 'string'
|
|
||||||
? String(Math.trunc(Number(r.porcentaje)))
|
|
||||||
: '',
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="bg-[#00a878] hover:bg-[#008f66]"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={type === 'evaluation' && evaluationTotal > 100}
|
|
||||||
>
|
|
||||||
Guardar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="text-sm leading-relaxed text-slate-600">
|
|
||||||
{type === 'text' &&
|
|
||||||
(data ? (
|
|
||||||
<p className="whitespace-pre-wrap">{data}</p>
|
|
||||||
) : (
|
|
||||||
<p className="text-slate-400 italic">Sin información.</p>
|
|
||||||
))}
|
|
||||||
{type === 'requirements' && <RequirementsView items={data} />}
|
|
||||||
{type === 'evaluation' && (
|
|
||||||
<EvaluationView items={data as Array<CriterioEvaluacionRow>} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vista de Requisitos
|
|
||||||
function RequirementsView({ items }: { items: Array<any> }) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{items.map((req, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="rounded-lg border border-slate-100 bg-slate-50 p-3"
|
|
||||||
>
|
|
||||||
<p className="text-[10px] font-bold tracking-tight text-slate-400 uppercase">
|
|
||||||
{req.type}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm font-medium text-slate-700">
|
|
||||||
{req.code} {req.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vista de Evaluación
|
|
||||||
function EvaluationView({ items }: { items: Array<CriterioEvaluacionRow> }) {
|
|
||||||
const porcentajeTotal = items.reduce(
|
|
||||||
(total, item) => total + Number(item.porcentaje),
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{items.map((item, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
className="flex justify-between border-b border-slate-50 pb-1.5 text-sm italic"
|
|
||||||
>
|
|
||||||
<span className="text-slate-500">{item.criterio}</span>
|
|
||||||
<span className="font-bold text-blue-600">{item.porcentaje}%</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{porcentajeTotal < 100 && (
|
|
||||||
<p className="text-destructive text-sm font-medium">
|
|
||||||
El porcentaje total es menor a 100%.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,177 +1,130 @@
|
|||||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
import { useState } from 'react';
|
||||||
|
import { Plus, Search, BookOpen, Trash2, Library, Edit3, Save } from 'lucide-react';
|
||||||
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
//import { toast } from 'sonner';
|
||||||
|
//import { mockLibraryResources } from '@/data/mockMateriaData';
|
||||||
|
|
||||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
export const mockLibraryResources = [
|
||||||
import { useNavigate, useParams } from '@tanstack/react-router'
|
{
|
||||||
import { Plus, Search, BookOpen, Trash2, Library, Edit3 } from 'lucide-react'
|
id: 'lib-1',
|
||||||
import { useState } from 'react'
|
titulo: 'Deep Learning',
|
||||||
|
autor: 'Goodfellow, I., Bengio, Y., & Courville, A.',
|
||||||
import {
|
editorial: 'MIT Press',
|
||||||
AlertDialog,
|
anio: 2016,
|
||||||
AlertDialogAction,
|
isbn: '9780262035613',
|
||||||
AlertDialogCancel,
|
disponible: true
|
||||||
AlertDialogContent,
|
},
|
||||||
AlertDialogDescription,
|
{
|
||||||
AlertDialogFooter,
|
id: 'lib-2',
|
||||||
AlertDialogHeader,
|
titulo: 'Artificial Intelligence: A Modern Approach',
|
||||||
AlertDialogTitle,
|
autor: 'Russell, S., & Norvig, P.',
|
||||||
} from '@/components/ui/alert-dialog'
|
editorial: 'Pearson',
|
||||||
import { Badge } from '@/components/ui/badge'
|
anio: 2020,
|
||||||
import { Button } from '@/components/ui/button'
|
isbn: '9780134610993',
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
disponible: true
|
||||||
import {
|
},
|
||||||
Dialog,
|
{
|
||||||
DialogContent,
|
id: 'lib-3',
|
||||||
DialogHeader,
|
titulo: 'Hands-On Machine Learning',
|
||||||
DialogTitle,
|
autor: 'Aurélien Géron',
|
||||||
DialogTrigger,
|
editorial: 'O\'Reilly Media',
|
||||||
} from '@/components/ui/dialog'
|
anio: 2019,
|
||||||
import { Input } from '@/components/ui/input'
|
isbn: '9781492032649',
|
||||||
import {
|
disponible: false
|
||||||
Select,
|
}
|
||||||
SelectContent,
|
];
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import {
|
|
||||||
useCreateBibliografia,
|
|
||||||
useDeleteBibliografia,
|
|
||||||
useSubjectBibliografia,
|
|
||||||
useUpdateBibliografia,
|
|
||||||
} from '@/data/hooks/useSubjects'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
// --- Interfaces ---
|
// --- Interfaces ---
|
||||||
export interface BibliografiaEntry {
|
export interface BibliografiaEntry {
|
||||||
id: string
|
id: string;
|
||||||
tipo: 'BASICA' | 'COMPLEMENTARIA'
|
tipo: 'BASICA' | 'COMPLEMENTARIA';
|
||||||
cita: string
|
cita: string;
|
||||||
tipo_fuente?: 'MANUAL' | 'BIBLIOTECA'
|
fuenteBibliotecaId?: string;
|
||||||
biblioteca_item_id?: string | null
|
fuenteBiblioteca?: any;
|
||||||
fuenteBibliotecaId?: string
|
|
||||||
fuenteBiblioteca?: any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BibliographyItem() {
|
interface BibliografiaTabProps {
|
||||||
const navigate = useNavigate()
|
bibliografia: BibliografiaEntry[];
|
||||||
const { planId, asignaturaId } = useParams({
|
onSave: (bibliografia: BibliografiaEntry[]) => void;
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
isSaving: boolean;
|
||||||
})
|
}
|
||||||
|
|
||||||
// --- 1. Única fuente de verdad: La Query ---
|
export function BibliographyItem({ bibliografia, onSave, isSaving }: BibliografiaTabProps) {
|
||||||
const { data: bibliografia = [], isLoading } =
|
const [entries, setEntries] = useState<BibliografiaEntry[]>(bibliografia);
|
||||||
useSubjectBibliografia(asignaturaId)
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false);
|
||||||
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
|
const [newEntryType, setNewEntryType] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA');
|
||||||
|
|
||||||
// --- 2. Mutaciones ---
|
const basicaEntries = entries.filter(e => e.tipo === 'BASICA');
|
||||||
const { mutate: crearBibliografia } = useCreateBibliografia()
|
const complementariaEntries = entries.filter(e => e.tipo === 'COMPLEMENTARIA');
|
||||||
const { mutate: actualizarBibliografia } = useUpdateBibliografia(asignaturaId)
|
|
||||||
const { mutate: eliminarBibliografia } = useDeleteBibliografia(asignaturaId)
|
|
||||||
|
|
||||||
// --- 3. Estados de UI (Solo para diálogos y edición) ---
|
const handleAddManual = (cita: string) => {
|
||||||
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
|
const newEntry: BibliografiaEntry = { id: `manual-${Date.now()}`, tipo: newEntryType, cita };
|
||||||
const [deleteId, setDeleteId] = useState<string | null>(null)
|
setEntries([...entries, newEntry]);
|
||||||
const [editingId, setEditingId] = useState<string | null>(null)
|
setIsAddDialogOpen(false);
|
||||||
|
//toast.success('Referencia manual añadida');
|
||||||
|
};
|
||||||
|
|
||||||
console.log('Datos actuales en el front:', bibliografia)
|
const handleAddFromLibrary = (resource: any, tipo: 'BASICA' | 'COMPLEMENTARIA') => {
|
||||||
// --- 4. Derivación de datos (Se calculan en cada render) ---
|
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`;
|
||||||
const basicaEntries = bibliografia.filter((e) => e.tipo === 'BASICA')
|
const newEntry: BibliografiaEntry = {
|
||||||
const complementariaEntries = bibliografia.filter(
|
id: `lib-ref-${Date.now()}`,
|
||||||
(e) => e.tipo === 'COMPLEMENTARIA',
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- Handlers Conectados a la Base de Datos ---
|
|
||||||
|
|
||||||
const handleAddFromLibrary = (
|
|
||||||
resource: any,
|
|
||||||
tipo: 'BASICA' | 'COMPLEMENTARIA',
|
|
||||||
) => {
|
|
||||||
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`
|
|
||||||
crearBibliografia(
|
|
||||||
{
|
|
||||||
asignatura_id: asignaturaId,
|
|
||||||
tipo,
|
tipo,
|
||||||
cita,
|
cita,
|
||||||
tipo_fuente: 'BIBLIOTECA',
|
fuenteBibliotecaId: resource.id,
|
||||||
biblioteca_item_id: resource.id,
|
fuenteBiblioteca: resource,
|
||||||
},
|
};
|
||||||
{
|
setEntries([...entries, newEntry]);
|
||||||
onSuccess: () => setIsLibraryDialogOpen(false),
|
setIsLibraryDialogOpen(false);
|
||||||
},
|
//toast.success('Añadido desde biblioteca');
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const handleUpdateCita = (id: string, nuevaCita: string) => {
|
const handleUpdateCita = (id: string, cita: string) => {
|
||||||
actualizarBibliografia(
|
setEntries(entries.map(e => e.id === id ? { ...e, cita } : e));
|
||||||
{
|
};
|
||||||
id,
|
|
||||||
updates: { cita: nuevaCita },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => setEditingId(null),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onConfirmDelete = () => {
|
|
||||||
if (deleteId) {
|
|
||||||
eliminarBibliografia(deleteId, {
|
|
||||||
onSuccess: () => setDeleteId(null),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading)
|
|
||||||
return <div className="p-10 text-center">Cargando bibliografía...</div>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-in fade-in mx-auto max-w-5xl space-y-8 py-10 duration-500">
|
<div className="max-w-5xl mx-auto py-10 space-y-8 animate-in fade-in duration-500">
|
||||||
<div className="flex items-center justify-between border-b pb-4">
|
<div className="flex items-center justify-between border-b pb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold tracking-tight text-slate-900">
|
<h2 className="text-2xl font-bold text-slate-900 tracking-tight">Bibliografía</h2>
|
||||||
Bibliografía
|
<p className="text-sm text-slate-500 mt-1">
|
||||||
</h2>
|
{basicaEntries.length} básica • {complementariaEntries.length} complementaria
|
||||||
<p className="mt-1 text-sm text-slate-500">
|
|
||||||
{basicaEntries.length} básica • {complementariaEntries.length}{' '}
|
|
||||||
complementaria
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Dialog
|
<Dialog open={isLibraryDialogOpen} onOpenChange={setIsLibraryDialogOpen}>
|
||||||
open={isLibraryDialogOpen}
|
|
||||||
onOpenChange={setIsLibraryDialogOpen}
|
|
||||||
>
|
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button variant="outline" className="border-blue-200 text-blue-700 hover:bg-blue-50">
|
||||||
variant="outline"
|
<Library className="w-4 h-4 mr-2" /> Buscar en biblioteca
|
||||||
className="border-blue-200 text-blue-700 hover:bg-blue-50"
|
|
||||||
>
|
|
||||||
<Library className="mr-2 h-4 w-4" /> Buscar en biblioteca
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
<LibrarySearchDialog
|
<LibrarySearchDialog onSelect={handleAddFromLibrary} existingIds={entries.map(e => e.fuenteBibliotecaId || '')} />
|
||||||
// CORRECCIÓN: Usamos 'bibliografia' en lugar de 'bibliografia2'
|
|
||||||
resources={[]} // Aquí deberías pasar el catálogo general, no la bibliografía de la asignatura
|
|
||||||
onSelect={handleAddFromLibrary}
|
|
||||||
// CORRECCIÓN: Usamos 'bibliografia' en lugar de 'entries'
|
|
||||||
existingIds={bibliografia.map(
|
|
||||||
(e) => e.biblioteca_item_id || '',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Button
|
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
|
||||||
onClick={() =>
|
<DialogTrigger asChild>
|
||||||
navigate({
|
<Button variant="outline"><Plus className="w-4 h-4 mr-2" /> Añadir manual</Button>
|
||||||
to: `/planes/${planId}/asignaturas/${asignaturaId}/bibliografia/nueva`,
|
</DialogTrigger>
|
||||||
resetScroll: false,
|
<DialogContent>
|
||||||
})
|
<AddManualDialog tipo={newEntryType} onTypeChange={setNewEntryType} onAdd={handleAddManual} />
|
||||||
}
|
</DialogContent>
|
||||||
className="ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-11 items-center justify-center gap-2 rounded-md px-8 text-sm font-medium shadow-md transition-colors"
|
</Dialog>
|
||||||
>
|
|
||||||
<Plus className="mr-2 h-4 w-4" /> Agregar Bibliografía
|
<Button onClick={() => onSave(entries)} disabled={isSaving} className="bg-blue-600 hover:bg-blue-700">
|
||||||
|
<Save className="w-4 h-4 mr-2" /> {isSaving ? 'Guardando...' : 'Guardar'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -180,13 +133,11 @@ export function BibliographyItem() {
|
|||||||
{/* BASICA */}
|
{/* BASICA */}
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-4 w-1 rounded-full bg-blue-600" />
|
<div className="h-4 w-1 bg-blue-600 rounded-full" />
|
||||||
<h3 className="font-semibold text-slate-800">
|
<h3 className="font-semibold text-slate-800">Bibliografía Básica</h3>
|
||||||
Bibliografía Básica
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{basicaEntries.map((entry) => (
|
{basicaEntries.map(entry => (
|
||||||
<BibliografiaCard
|
<BibliografiaCard
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
@@ -203,13 +154,11 @@ export function BibliographyItem() {
|
|||||||
{/* COMPLEMENTARIA */}
|
{/* COMPLEMENTARIA */}
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-4 w-1 rounded-full bg-slate-400" />
|
<div className="h-4 w-1 bg-slate-400 rounded-full" />
|
||||||
<h3 className="font-semibold text-slate-800">
|
<h3 className="font-semibold text-slate-800">Bibliografía Complementaria</h3>
|
||||||
Bibliografía Complementaria
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3">
|
<div className="grid gap-3">
|
||||||
{complementariaEntries.map((entry) => (
|
{complementariaEntries.map(entry => (
|
||||||
<BibliografiaCard
|
<BibliografiaCard
|
||||||
key={entry.id}
|
key={entry.id}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
@@ -228,175 +177,115 @@ export function BibliographyItem() {
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>¿Eliminar referencia?</AlertDialogTitle>
|
<AlertDialogTitle>¿Eliminar referencia?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>La referencia será quitada del plan de estudios.</AlertDialogDescription>
|
||||||
La referencia será quitada del plan de estudios.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={onConfirmDelete} className="bg-red-600">
|
<AlertDialogAction onClick={() => { setEntries(entries.filter(e => e.id !== deleteId)); setDeleteId(null); }} className="bg-red-600">Eliminar</AlertDialogAction>
|
||||||
Eliminar
|
|
||||||
</AlertDialogAction>
|
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Subcomponentes ---
|
// --- Subcomponentes ---
|
||||||
|
|
||||||
function BibliografiaCard({
|
function BibliografiaCard({ entry, isEditing, onEdit, onStopEditing, onUpdateCita, onDelete }: any) {
|
||||||
entry,
|
const [localCita, setLocalCita] = useState(entry.cita);
|
||||||
isEditing,
|
|
||||||
onEdit,
|
|
||||||
onStopEditing,
|
|
||||||
onUpdateCita,
|
|
||||||
onDelete,
|
|
||||||
}: any) {
|
|
||||||
const [localCita, setLocalCita] = useState(entry.cita)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card className={cn("group transition-all hover:shadow-md", isEditing && "ring-2 ring-blue-500")}>
|
||||||
className={cn(
|
|
||||||
'group transition-all hover:shadow-md',
|
|
||||||
isEditing && 'ring-2 ring-blue-500',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<BookOpen
|
<BookOpen className={cn("w-5 h-5 mt-1", entry.tipo === 'BASICA' ? "text-blue-600" : "text-slate-400")} />
|
||||||
className={cn(
|
<div className="flex-1 min-w-0">
|
||||||
'mt-1 h-5 w-5',
|
|
||||||
entry.tipo === 'BASICA' ? 'text-blue-600' : 'text-slate-400',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Textarea
|
<Textarea value={localCita} onChange={(e) => setLocalCita(e.target.value)} className="min-h-[80px]" />
|
||||||
value={localCita}
|
|
||||||
onChange={(e) => setLocalCita(e.target.value)}
|
|
||||||
className="min-h-[80px]"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button variant="ghost" size="sm" onClick={onStopEditing}>
|
<Button variant="ghost" size="sm" onClick={onStopEditing}>Cancelar</Button>
|
||||||
Cancelar
|
<Button size="sm" className="bg-emerald-600" onClick={() => { onUpdateCita(entry.id, localCita); onStopEditing(); }}>Guardar</Button>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="bg-emerald-600"
|
|
||||||
onClick={() => {
|
|
||||||
onUpdateCita(entry.id, localCita)
|
|
||||||
onStopEditing()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Guardar
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div onClick={onEdit} className="cursor-pointer">
|
<div onClick={onEdit} className="cursor-pointer">
|
||||||
<p className="text-sm leading-relaxed text-slate-700">
|
<p className="text-sm leading-relaxed text-slate-700">{entry.cita}</p>
|
||||||
{entry.cita}
|
|
||||||
</p>
|
|
||||||
{entry.fuenteBiblioteca && (
|
{entry.fuenteBiblioteca && (
|
||||||
<div className="mt-2 flex gap-2">
|
<div className="flex gap-2 mt-2">
|
||||||
<Badge
|
<Badge variant="secondary" className="text-[10px] bg-slate-100 text-slate-600">Biblioteca</Badge>
|
||||||
variant="secondary"
|
{entry.fuenteBiblioteca.disponible && <Badge className="text-[10px] bg-emerald-50 text-emerald-700 border-emerald-100">Disponible</Badge>}
|
||||||
className="bg-slate-100 text-[10px] text-slate-600"
|
|
||||||
>
|
|
||||||
Biblioteca
|
|
||||||
</Badge>
|
|
||||||
{entry.fuenteBiblioteca.disponible && (
|
|
||||||
<Badge className="border-emerald-100 bg-emerald-50 text-[10px] text-emerald-700">
|
|
||||||
Disponible
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<div className="flex opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="flex opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<Button
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-blue-600" onClick={onEdit}><Edit3 className="w-4 h-4" /></Button>
|
||||||
variant="ghost"
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-red-500" onClick={onDelete}><Trash2 className="w-4 h-4" /></Button>
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-slate-400 hover:text-blue-600"
|
|
||||||
onClick={onEdit}
|
|
||||||
>
|
|
||||||
<Edit3 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-slate-400 hover:text-red-500"
|
|
||||||
onClick={onDelete}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function LibrarySearchDialog({ resources, onSelect, existingIds }: any) {
|
function AddManualDialog({ tipo, onTypeChange, onAdd }: any) {
|
||||||
const [search, setSearch] = useState('')
|
const [cita, setCita] = useState('');
|
||||||
const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA')
|
|
||||||
const filtered = (resources || []).filter(
|
|
||||||
(r: any) =>
|
|
||||||
!existingIds.includes(r.id) &&
|
|
||||||
r.titulo?.toLowerCase().includes(search.toLowerCase()),
|
|
||||||
)
|
|
||||||
console.log(filtered)
|
|
||||||
console.log(resources)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-4">
|
||||||
<DialogHeader>
|
<DialogHeader><DialogTitle>Referencia Manual</DialogTitle></DialogHeader>
|
||||||
<DialogTitle>Catálogo de Biblioteca</DialogTitle>
|
<div className="space-y-2">
|
||||||
</DialogHeader>
|
<label className="text-xs font-bold uppercase text-slate-500">Tipo</label>
|
||||||
<div className="flex gap-2">
|
<Select value={tipo} onValueChange={onTypeChange}>
|
||||||
<div className="relative flex-1">
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
|
||||||
<Input
|
|
||||||
value={search}
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
placeholder="Buscar por título o autor..."
|
|
||||||
className="pl-10"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Select value={tipo} onValueChange={(v: any) => setTipo(v)}>
|
|
||||||
<SelectTrigger className="w-36">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="BASICA">Básica</SelectItem>
|
<SelectItem value="BASICA">Básica</SelectItem>
|
||||||
<SelectItem value="COMPLEMENTARIA">Complem.</SelectItem>
|
<SelectItem value="COMPLEMENTARIA">Complementaria</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="max-h-[300px] space-y-2 overflow-y-auto pr-2">
|
<div className="space-y-2">
|
||||||
{filtered.map((res: any) => (
|
<label className="text-xs font-bold uppercase text-slate-500">Cita APA</label>
|
||||||
<div
|
<Textarea value={cita} onChange={(e) => setCita(e.target.value)} placeholder="Autor, A. (Año). Título..." className="min-h-[120px]" />
|
||||||
key={res.id}
|
</div>
|
||||||
onClick={() => onSelect(res, tipo)}
|
<Button onClick={() => onAdd(cita)} disabled={!cita.trim()} className="w-full bg-blue-600">Añadir a la lista</Button>
|
||||||
className="group flex cursor-pointer items-center justify-between rounded-lg border p-3 hover:bg-slate-50"
|
</div>
|
||||||
>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LibrarySearchDialog({ onSelect, existingIds }: any) {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA');
|
||||||
|
const filtered = mockLibraryResources.filter(r =>
|
||||||
|
!existingIds.includes(r.id) && r.titulo.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 py-2">
|
||||||
|
<DialogHeader><DialogTitle>Catálogo de Biblioteca</DialogTitle></DialogHeader>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Buscar por título o autor..." className="pl-10" />
|
||||||
|
</div>
|
||||||
|
<Select value={tipo} onValueChange={(v:any) => setTipo(v)}><SelectTrigger className="w-36"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent><SelectItem value="BASICA">Básica</SelectItem><SelectItem value="COMPLEMENTARIA">Complem.</SelectItem></SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="max-h-[300px] overflow-y-auto pr-2 space-y-2">
|
||||||
|
{filtered.map(res => (
|
||||||
|
<div key={res.id} onClick={() => onSelect(res, tipo)} className="p-3 border rounded-lg hover:bg-slate-50 cursor-pointer flex justify-between items-center group">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-slate-700">
|
<p className="text-sm font-semibold text-slate-700">{res.titulo}</p>
|
||||||
{res.titulo}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-slate-500">{res.autor}</p>
|
<p className="text-xs text-slate-500">{res.autor}</p>
|
||||||
</div>
|
</div>
|
||||||
<Plus className="h-4 w-4 text-blue-600 opacity-0 transition-opacity group-hover:opacity-100" />
|
<Plus className="w-4 h-4 text-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
|||||||
import { FileCheck, Download, RefreshCw, Loader2 } from 'lucide-react'
|
import { useState } from 'react';
|
||||||
import { useState } from 'react'
|
import { FileText, Download, RefreshCw, Calendar, FileCheck, AlertTriangle, Loader2 } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -11,78 +13,75 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog';
|
||||||
import { Button } from '@/components/ui/button'
|
import type { DocumentoMateria, Materia, MateriaStructure } from '@/types/materia';
|
||||||
import { Card } from '@/components/ui/card'
|
import { cn } from '@/lib/utils';
|
||||||
|
//import { toast } from 'sonner';
|
||||||
|
//import { format } from 'date-fns';
|
||||||
|
//import { es } from 'date-fns/locale';
|
||||||
|
|
||||||
interface DocumentoSEPTabProps {
|
interface DocumentoSEPTabProps {
|
||||||
pdfUrl: string | null
|
documento: DocumentoMateria | null;
|
||||||
isLoading: boolean
|
materia: Materia;
|
||||||
onDownloadPdf: () => void
|
estructura: MateriaStructure;
|
||||||
onDownloadWord: () => void
|
datosGenerales: Record<string, any>;
|
||||||
onRegenerate: () => void
|
onRegenerate: () => void;
|
||||||
isRegenerating: boolean
|
isRegenerating: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DocumentoSEPTab({
|
export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales, onRegenerate, isRegenerating }: DocumentoSEPTabProps) {
|
||||||
pdfUrl,
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
isLoading,
|
|
||||||
onDownloadPdf,
|
// Check completeness
|
||||||
onDownloadWord,
|
const camposObligatorios = estructura.campos.filter(c => c.obligatorio);
|
||||||
onRegenerate,
|
const camposCompletos = camposObligatorios.filter(c => datosGenerales[c.id]?.trim());
|
||||||
isRegenerating,
|
const completeness = Math.round((camposCompletos.length / camposObligatorios.length) * 100);
|
||||||
}: DocumentoSEPTabProps) {
|
const isComplete = completeness === 100;
|
||||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
|
|
||||||
|
|
||||||
const handleRegenerate = () => {
|
const handleRegenerate = () => {
|
||||||
setShowConfirmDialog(false)
|
setShowConfirmDialog(false);
|
||||||
onRegenerate()
|
onRegenerate();
|
||||||
}
|
//toast.success('Regenerando documento...');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in space-y-6">
|
<div className="space-y-6 animate-fade-in">
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
|
<h2 className="font-display text-2xl font-semibold text-foreground flex items-center gap-2">
|
||||||
<FileCheck className="text-accent h-6 w-6" />
|
<FileCheck className="w-6 h-6 text-accent" />
|
||||||
Documento SEP
|
Documento SEP
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
Previsualización del documento oficial generado
|
Previsualización del documento oficial para la SEP
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AlertDialog
|
{documento?.estado === 'listo' && (
|
||||||
open={showConfirmDialog}
|
<Button variant="outline" onClick={() => console.log("descargando") /*toast.info('Descarga iniciada')*/}>
|
||||||
onOpenChange={setShowConfirmDialog}
|
<Download className="w-4 h-4 mr-2" />
|
||||||
>
|
Descargar
|
||||||
<AlertDialogTrigger asChild>
|
</Button>
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="gap-2"
|
|
||||||
disabled={isRegenerating}
|
|
||||||
>
|
|
||||||
{isRegenerating ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
)}
|
)}
|
||||||
{isRegenerating ? 'Generando...' : 'Regenerar'}
|
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button disabled={isRegenerating || !isComplete}>
|
||||||
|
{isRegenerating ? (
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{isRegenerating ? 'Generando...' : 'Regenerar documento'}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
|
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>¿Regenerar documento SEP?</AlertDialogTitle>
|
<AlertDialogTitle>¿Regenerar documento SEP?</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
Se generará una nueva versión del documento con la información
|
Se creará una nueva versión del documento con los datos actuales de la materia.
|
||||||
actual.
|
La versión anterior quedará en el historial.
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
|
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleRegenerate}>
|
<AlertDialogAction onClick={handleRegenerate}>
|
||||||
@@ -91,47 +90,232 @@ export function DocumentoSEPTab({
|
|||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{pdfUrl && !isLoading && (
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
<>
|
{/* Document preview */}
|
||||||
<Button
|
<div className="lg:col-span-2">
|
||||||
size="sm"
|
<Card className="card-elevated h-[700px] overflow-hidden">
|
||||||
className="gap-2 bg-teal-700 hover:bg-teal-800"
|
{documento?.estado === 'listo' ? (
|
||||||
onClick={onDownloadWord}
|
<div className="h-full bg-muted/30 flex flex-col">
|
||||||
>
|
{/* Simulated document header */}
|
||||||
<Download className="h-4 w-4" /> Descargar Word
|
<div className="bg-card border-b p-4">
|
||||||
</Button>
|
<div className="flex items-center justify-between">
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
variant="outline"
|
<FileText className="w-5 h-5 text-primary" />
|
||||||
size="sm"
|
<span className="font-medium text-foreground">
|
||||||
className="gap-2"
|
Programa de Estudios - {materia.clave}
|
||||||
onClick={onDownloadPdf}
|
</span>
|
||||||
>
|
</div>
|
||||||
<Download className="h-4 w-4" /> Descargar PDF
|
<Badge variant="outline">Versión {documento.version}</Badge>
|
||||||
</Button>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
|
||||||
|
{/* Document content simulation */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
|
<div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 space-y-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="text-center border-b pb-6">
|
||||||
|
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-2">
|
||||||
|
Secretaría de Educación Pública
|
||||||
|
</p>
|
||||||
|
<h1 className="font-display text-2xl font-bold text-primary mb-1">
|
||||||
|
{materia.nombre}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Clave: {materia.clave} | Créditos: {materia.creditos || 'N/A'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Datos de la institución */}
|
||||||
|
<div className="space-y-1 text-sm">
|
||||||
|
<p><strong>Carrera:</strong> {materia.carrera}</p>
|
||||||
|
<p><strong>Facultad:</strong> {materia.facultad}</p>
|
||||||
|
<p><strong>Plan de estudios:</strong> {materia.planNombre}</p>
|
||||||
|
{materia.ciclo && <p><strong>Ciclo:</strong> {materia.ciclo}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Campos del documento */}
|
||||||
|
{estructura.campos.map((campo) => {
|
||||||
|
const valor = datosGenerales[campo.id];
|
||||||
|
if (!valor) return null;
|
||||||
|
return (
|
||||||
|
<div key={campo.id} className="space-y-2">
|
||||||
|
<h3 className="font-semibold text-foreground border-b pb-1">
|
||||||
|
{campo.nombre}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-foreground whitespace-pre-wrap leading-relaxed">
|
||||||
|
{valor}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<div className="border-t pt-6 mt-8 text-center text-xs text-muted-foreground">
|
||||||
|
<p>Documento generado el {/*format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/}</p>
|
||||||
|
<p className="mt-1">Universidad La Salle</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : documento?.estado === 'generando' ? (
|
||||||
|
<div className="h-full flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className="w-12 h-12 mx-auto text-accent animate-spin mb-4" />
|
||||||
|
<p className="text-muted-foreground">Generando documento...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full flex items-center justify-center">
|
||||||
|
<div className="text-center max-w-sm">
|
||||||
|
<FileText className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
No hay documento generado aún
|
||||||
|
</p>
|
||||||
|
{!isComplete && (
|
||||||
|
<div className="p-4 bg-warning/10 rounded-lg text-sm text-warning-foreground">
|
||||||
|
<AlertTriangle className="w-4 h-4 inline mr-2" />
|
||||||
|
Completa todos los campos obligatorios para generar el documento
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* PDF Preview */}
|
|
||||||
<Card className="h-200 overflow-hidden">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex h-full items-center justify-center">
|
|
||||||
<Loader2 className="h-10 w-10 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : pdfUrl ? (
|
|
||||||
<iframe
|
|
||||||
src={`${pdfUrl}#toolbar=0`}
|
|
||||||
className="h-full w-full border-none"
|
|
||||||
title="Documento SEP"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="text-muted-foreground flex h-full items-center justify-center">
|
|
||||||
No se pudo cargar el documento.
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
|
||||||
|
{/* Info sidebar */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Status */}
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Estado del documento</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{documento && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Versión</span>
|
||||||
|
<Badge variant="outline">{documento.version}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Generado</span>
|
||||||
|
<span className="text-sm">
|
||||||
|
{/*format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm text-muted-foreground">Estado</span>
|
||||||
|
<Badge className={cn(
|
||||||
|
documento.estado === 'listo' && "bg-success text-success-foreground",
|
||||||
|
documento.estado === 'generando' && "bg-info text-info-foreground",
|
||||||
|
documento.estado === 'error' && "bg-destructive text-destructive-foreground"
|
||||||
|
)}>
|
||||||
|
{documento.estado === 'listo' && 'Listo'}
|
||||||
|
{documento.estado === 'generando' && 'Generando'}
|
||||||
|
{documento.estado === 'error' && 'Error'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Completeness */}
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Completitud de datos</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Campos obligatorios</span>
|
||||||
|
<span className="font-medium">{camposCompletos.length}/{camposObligatorios.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-full transition-all duration-500",
|
||||||
|
completeness === 100 ? "bg-success" : "bg-accent"
|
||||||
|
)}
|
||||||
|
style={{ width: `${completeness}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className={cn(
|
||||||
|
"text-xs",
|
||||||
|
completeness === 100 ? "text-success" : "text-muted-foreground"
|
||||||
|
)}>
|
||||||
|
{completeness === 100
|
||||||
|
? 'Todos los campos obligatorios están completos'
|
||||||
|
: `Faltan ${camposObligatorios.length - camposCompletos.length} campos por completar`
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Missing fields */}
|
||||||
|
{!isComplete && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground">Campos faltantes:</p>
|
||||||
|
{camposObligatorios.filter(c => !datosGenerales[c.id]?.trim()).map((campo) => (
|
||||||
|
<div key={campo.id} className="flex items-center gap-2 text-sm">
|
||||||
|
<AlertTriangle className="w-3 h-3 text-warning" />
|
||||||
|
<span className="text-foreground">{campo.nombre}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Requirements */}
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Requisitos SEP</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="space-y-2 text-sm">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className={cn(
|
||||||
|
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5",
|
||||||
|
datosGenerales['objetivo_general'] ? "bg-success/20" : "bg-muted"
|
||||||
|
)}>
|
||||||
|
{datosGenerales['objetivo_general'] && <Check className="w-3 h-3 text-success" />}
|
||||||
|
</div>
|
||||||
|
<span className="text-muted-foreground">Objetivo general definido</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className={cn(
|
||||||
|
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5",
|
||||||
|
datosGenerales['competencias'] ? "bg-success/20" : "bg-muted"
|
||||||
|
)}>
|
||||||
|
{datosGenerales['competencias'] && <Check className="w-3 h-3 text-success" />}
|
||||||
|
</div>
|
||||||
|
<span className="text-muted-foreground">Competencias especificadas</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<div className={cn(
|
||||||
|
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5",
|
||||||
|
datosGenerales['evaluacion'] ? "bg-success/20" : "bg-muted"
|
||||||
|
)}>
|
||||||
|
{datosGenerales['evaluacion'] && <Check className="w-3 h-3 text-success" />}
|
||||||
|
</div>
|
||||||
|
<span className="text-muted-foreground">Criterios de evaluación</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Check({ className }: { className?: string }) {
|
||||||
|
return (
|
||||||
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,293 +1,181 @@
|
|||||||
import { useParams } from '@tanstack/react-router'
|
import { useState } from 'react';
|
||||||
import { format, parseISO } from 'date-fns'
|
import { History, FileText, List, BookMarked, Sparkles, FileCheck, User, Filter, Calendar } from 'lucide-react';
|
||||||
import { es } from 'date-fns/locale'
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import {
|
import { Button } from '@/components/ui/button';
|
||||||
History,
|
import { Badge } from '@/components/ui/badge';
|
||||||
FileText,
|
|
||||||
List,
|
|
||||||
BookMarked,
|
|
||||||
Sparkles,
|
|
||||||
FileCheck,
|
|
||||||
Filter,
|
|
||||||
Calendar,
|
|
||||||
Loader2,
|
|
||||||
Eye,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useState, useMemo } from 'react'
|
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useSubjectHistorial } from '@/data/hooks/useSubjects'
|
import type { CambioMateria } from '@/types/materia';
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils';
|
||||||
|
import { format, formatDistanceToNow } from 'date-fns';
|
||||||
|
import { es } from 'date-fns/locale';
|
||||||
|
|
||||||
const tipoConfig: Record<string, { label: string; icon: any; color: string }> =
|
interface HistorialTabProps {
|
||||||
{
|
historial: CambioMateria[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const tipoConfig: Record<string, { label: string; icon: React.ComponentType<{ className?: string }>; color: string }> = {
|
||||||
datos: { label: 'Datos generales', icon: FileText, color: 'text-info' },
|
datos: { label: 'Datos generales', icon: FileText, color: 'text-info' },
|
||||||
contenido: {
|
contenido: { label: 'Contenido temático', icon: List, color: 'text-accent' },
|
||||||
label: 'Contenido temático',
|
bibliografia: { label: 'Bibliografía', icon: BookMarked, color: 'text-success' },
|
||||||
icon: List,
|
|
||||||
color: 'text-accent',
|
|
||||||
},
|
|
||||||
bibliografia: {
|
|
||||||
label: 'Bibliografía',
|
|
||||||
icon: BookMarked,
|
|
||||||
color: 'text-success',
|
|
||||||
},
|
|
||||||
ia: { label: 'IA', icon: Sparkles, color: 'text-amber-500' },
|
ia: { label: 'IA', icon: Sparkles, color: 'text-amber-500' },
|
||||||
documento: {
|
documento: { label: 'Documento SEP', icon: FileCheck, color: 'text-primary' },
|
||||||
label: 'Documento SEP',
|
};
|
||||||
icon: FileCheck,
|
|
||||||
color: 'text-primary',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export function HistorialTab() {
|
export function HistorialTab({ historial }: HistorialTabProps) {
|
||||||
const { asignaturaId } = useParams({
|
const [filtros, setFiltros] = useState<Set<string>>(new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento']));
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId/historial',
|
|
||||||
})
|
|
||||||
// 1. Obtenemos los datos directamente dentro del componente
|
|
||||||
const { data: rawData, isLoading } = useSubjectHistorial(asignaturaId)
|
|
||||||
|
|
||||||
const [filtros, setFiltros] = useState<Set<string>>(
|
|
||||||
new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento']),
|
|
||||||
)
|
|
||||||
|
|
||||||
// ESTADOS PARA EL MODAL
|
|
||||||
const [selectedChange, setSelectedChange] = useState<any>(null)
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
||||||
|
|
||||||
const RenderValue = ({ value }: { value: any }) => {
|
|
||||||
// 1. Caso: Nulo o vacío
|
|
||||||
if (
|
|
||||||
value === null ||
|
|
||||||
value === undefined ||
|
|
||||||
value === 'Sin información previa'
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<span className="text-muted-foreground italic">Sin información</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Caso: Es un ARRAY (como tu lista de unidades)
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{value.map((item, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="rounded-lg border bg-white/50 p-3 shadow-sm"
|
|
||||||
>
|
|
||||||
<RenderValue value={item} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Caso: Es un OBJETO (como cada unidad con titulo, temas, etc.)
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
return (
|
|
||||||
<div className="grid gap-2">
|
|
||||||
{Object.entries(value).map(([key, val]) => (
|
|
||||||
<div key={key} className="flex flex-col">
|
|
||||||
<span className="text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
|
||||||
{key.replace(/_/g, ' ')}
|
|
||||||
</span>
|
|
||||||
<div className="text-sm text-slate-700">
|
|
||||||
{/* Llamada recursiva para manejar lo que haya dentro del valor */}
|
|
||||||
{typeof val === 'object' ? (
|
|
||||||
<div className="mt-1 border-l-2 border-slate-100 pl-2">
|
|
||||||
<RenderValue value={val} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
String(val)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Caso: Texto o número simple
|
|
||||||
return <span className="text-sm leading-relaxed">{String(value)}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
const historialTransformado = useMemo(() => {
|
|
||||||
if (!rawData) return []
|
|
||||||
return rawData.map((item: any) => ({
|
|
||||||
id: item.id,
|
|
||||||
tipo: item.campo === 'contenido_tematico' ? 'contenido' : 'datos',
|
|
||||||
descripcion: `Se actualizó el campo ${item.campo.replace('_', ' ')}`,
|
|
||||||
fecha: parseISO(item.cambiado_en),
|
|
||||||
usuario: item.fuente === 'HUMANO' ? 'Usuario Staff' : 'Sistema IA',
|
|
||||||
detalles: {
|
|
||||||
campo: item.campo,
|
|
||||||
valor_anterior: item.valor_anterior || 'Sin datos previos', // Asumiendo que existe en tu API
|
|
||||||
valor_nuevo: item.valor_nuevo,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}, [rawData])
|
|
||||||
|
|
||||||
const openCompareModal = (cambio: any) => {
|
|
||||||
setSelectedChange(cambio)
|
|
||||||
setIsModalOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleFiltro = (tipo: string) => {
|
const toggleFiltro = (tipo: string) => {
|
||||||
const newFiltros = new Set(filtros)
|
const newFiltros = new Set(filtros);
|
||||||
if (newFiltros.has(tipo)) newFiltros.delete(tipo)
|
if (newFiltros.has(tipo)) {
|
||||||
else newFiltros.add(tipo)
|
newFiltros.delete(tipo);
|
||||||
setFiltros(newFiltros)
|
} else {
|
||||||
|
newFiltros.add(tipo);
|
||||||
}
|
}
|
||||||
|
setFiltros(newFiltros);
|
||||||
|
};
|
||||||
|
|
||||||
// 3. Aplicamos filtros y agrupamiento sobre los datos transformados
|
const filteredHistorial = historial.filter(cambio => filtros.has(cambio.tipo));
|
||||||
const filteredHistorial = historialTransformado.filter((cambio) =>
|
|
||||||
filtros.has(cambio.tipo),
|
|
||||||
)
|
|
||||||
|
|
||||||
const groupedHistorial = filteredHistorial.reduce(
|
// Group by date
|
||||||
(groups, cambio) => {
|
const groupedHistorial = filteredHistorial.reduce((groups, cambio) => {
|
||||||
const dateKey = format(cambio.fecha, 'yyyy-MM-dd')
|
const dateKey = format(cambio.fecha, 'yyyy-MM-dd');
|
||||||
if (!groups[dateKey]) groups[dateKey] = []
|
if (!groups[dateKey]) {
|
||||||
groups[dateKey].push(cambio)
|
groups[dateKey] = [];
|
||||||
return groups
|
|
||||||
},
|
|
||||||
{} as Record<string, Array<any>>,
|
|
||||||
)
|
|
||||||
|
|
||||||
const sortedDates = Object.keys(groupedHistorial).sort((a, b) =>
|
|
||||||
b.localeCompare(a),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex h-48 items-center justify-center">
|
|
||||||
<Loader2 className="text-primary h-8 w-8 animate-spin" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
groups[dateKey].push(cambio);
|
||||||
|
return groups;
|
||||||
|
}, {} as Record<string, CambioMateria[]>);
|
||||||
|
|
||||||
|
const sortedDates = Object.keys(groupedHistorial).sort((a, b) => b.localeCompare(a));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in space-y-6">
|
<div className="space-y-6 animate-fade-in">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
|
<h2 className="font-display text-2xl font-semibold text-foreground flex items-center gap-2">
|
||||||
<History className="text-accent h-6 w-6" />
|
<History className="w-6 h-6 text-accent" />
|
||||||
Historial de cambios
|
Historial de cambios
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
{historialTransformado.length} cambios registrados
|
{historial.length} cambios registrados
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dropdown de Filtros (Igual al anterior) */}
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Filter className="mr-2 h-4 w-4" />
|
<Filter className="w-4 h-4 mr-2" />
|
||||||
Filtrar ({filtros.size})
|
Filtrar ({filtros.size})
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-48">
|
<DropdownMenuContent align="end" className="w-48">
|
||||||
{Object.entries(tipoConfig).map(([tipo, config]) => (
|
{Object.entries(tipoConfig).map(([tipo, config]) => {
|
||||||
|
const Icon = config.icon;
|
||||||
|
return (
|
||||||
<DropdownMenuCheckboxItem
|
<DropdownMenuCheckboxItem
|
||||||
key={tipo}
|
key={tipo}
|
||||||
checked={filtros.has(tipo)}
|
checked={filtros.has(tipo)}
|
||||||
onCheckedChange={() => toggleFiltro(tipo)}
|
onCheckedChange={() => toggleFiltro(tipo)}
|
||||||
>
|
>
|
||||||
<config.icon className={cn('mr-2 h-4 w-4', config.color)} />
|
<Icon className={cn("w-4 h-4 mr-2", config.color)} />
|
||||||
{config.label}
|
{config.label}
|
||||||
</DropdownMenuCheckboxItem>
|
</DropdownMenuCheckboxItem>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredHistorial.length === 0 ? (
|
{filteredHistorial.length === 0 ? (
|
||||||
<Card>
|
<Card className="card-elevated">
|
||||||
<CardContent className="py-12 text-center">
|
<CardContent className="py-12 text-center">
|
||||||
<History className="text-muted-foreground/50 mx-auto mb-4 h-12 w-12" />
|
<History className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||||
<p className="text-muted-foreground">No se encontraron cambios.</p>
|
<p className="text-muted-foreground">
|
||||||
|
{historial.length === 0
|
||||||
|
? 'No hay cambios registrados aún'
|
||||||
|
: 'No hay cambios con los filtros seleccionados'
|
||||||
|
}
|
||||||
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{sortedDates.map((dateKey) => (
|
{sortedDates.map((dateKey) => {
|
||||||
|
const cambios = groupedHistorial[dateKey];
|
||||||
|
const date = new Date(dateKey);
|
||||||
|
const isToday = format(new Date(), 'yyyy-MM-dd') === dateKey;
|
||||||
|
const isYesterday = format(new Date(Date.now() - 86400000), 'yyyy-MM-dd') === dateKey;
|
||||||
|
|
||||||
|
return (
|
||||||
<div key={dateKey}>
|
<div key={dateKey}>
|
||||||
<div className="mb-4 flex items-center gap-3">
|
{/* Date header */}
|
||||||
<Calendar className="text-muted-foreground h-4 w-4" />
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<h3 className="text-foreground font-semibold">
|
<div className="p-2 rounded-lg bg-muted">
|
||||||
{format(parseISO(dateKey), "EEEE, d 'de' MMMM", {
|
<Calendar className="w-4 h-4 text-muted-foreground" />
|
||||||
locale: es,
|
</div>
|
||||||
})}
|
<div>
|
||||||
|
<h3 className="font-semibold text-foreground">
|
||||||
|
{isToday ? 'Hoy' : isYesterday ? 'Ayer' : format(date, "EEEE, d 'de' MMMM", { locale: es })}
|
||||||
</h3>
|
</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{cambios.length} {cambios.length === 1 ? 'cambio' : 'cambios'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-border ml-4 space-y-4 border-l-2 pl-6">
|
{/* Timeline */}
|
||||||
{groupedHistorial[dateKey].map((cambio) => {
|
<div className="ml-4 border-l-2 border-border pl-6 space-y-4">
|
||||||
const config = tipoConfig[cambio.tipo] || tipoConfig.datos
|
{cambios.map((cambio) => {
|
||||||
const Icon = config.icon
|
const config = tipoConfig[cambio.tipo];
|
||||||
|
const Icon = config.icon;
|
||||||
return (
|
return (
|
||||||
<div key={cambio.id} className="relative">
|
<div key={cambio.id} className="relative">
|
||||||
<div
|
{/* Timeline dot */}
|
||||||
className={cn(
|
<div className={cn(
|
||||||
'border-background absolute -left-[31px] h-4 w-4 rounded-full border-2',
|
"absolute -left-[31px] w-4 h-4 rounded-full border-2 border-background",
|
||||||
`bg-current ${config.color}`,
|
`bg-current ${config.color}`
|
||||||
)}
|
)} />
|
||||||
/>
|
|
||||||
<Card className="card-interactive">
|
<Card className="card-interactive">
|
||||||
<CardContent className="py-4">
|
<CardContent className="py-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div
|
<div className={cn(
|
||||||
className={cn(
|
"p-2 rounded-lg bg-muted flex-shrink-0",
|
||||||
'bg-muted rounded-lg p-2',
|
config.color
|
||||||
config.color,
|
)}>
|
||||||
)}
|
<Icon className="w-4 h-4" />
|
||||||
>
|
|
||||||
<Icon className="h-4 w-4" />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex justify-between">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<p className="font-medium">
|
<div>
|
||||||
|
<p className="font-medium text-foreground">
|
||||||
{cambio.descripcion}
|
{cambio.descripcion}
|
||||||
</p>
|
</p>
|
||||||
{/* BOTÓN PARA VER CAMBIOS */}
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<Button
|
<Badge variant="outline" className="text-xs">
|
||||||
variant="ghost"
|
{config.label}
|
||||||
size="sm"
|
</Badge>
|
||||||
className="gap-2 text-blue-600 hover:bg-blue-50 hover:text-blue-700"
|
{cambio.detalles?.campo && (
|
||||||
onClick={() => openCompareModal(cambio)}
|
<span className="text-xs text-muted-foreground">
|
||||||
>
|
Campo: {cambio.detalles.campo}
|
||||||
<Eye className="h-4 w-4" />
|
</span>
|
||||||
Ver cambios
|
)}
|
||||||
</Button>
|
</div>
|
||||||
<span className="text-muted-foreground text-xs">
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground whitespace-nowrap">
|
||||||
{format(cambio.fecha, 'HH:mm')}
|
{format(cambio.fecha, 'HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="flex items-center gap-2 mt-3 text-xs text-muted-foreground">
|
||||||
<Badge
|
<User className="w-3 h-3" />
|
||||||
variant="outline"
|
<span>{cambio.usuario}</span>
|
||||||
className="text-[10px]"
|
<span className="text-muted-foreground/50">•</span>
|
||||||
>
|
<span>
|
||||||
{config.label}
|
{formatDistanceToNow(cambio.fecha, { addSuffix: true, locale: es })}
|
||||||
</Badge>
|
|
||||||
<span className="text-muted-foreground text-xs italic">
|
|
||||||
por {cambio.usuario}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,62 +183,14 @@ export function HistorialTab() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* MODAL DE COMPARACIÓN */}
|
|
||||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
||||||
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col overflow-hidden">
|
|
||||||
<DialogHeader className="flex-shrink-0">
|
|
||||||
<DialogTitle className="flex items-center gap-2 text-xl">
|
|
||||||
<History className="h-5 w-5 text-blue-500" />
|
|
||||||
Comparación de cambios
|
|
||||||
</DialogTitle>
|
|
||||||
{/* ... info de usuario y fecha */}
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="custom-scrollbar mt-4 flex-1 overflow-y-auto pr-2">
|
|
||||||
<div className="grid h-full grid-cols-2 gap-6">
|
|
||||||
{/* Lado Antes */}
|
|
||||||
<div className="flex flex-col space-y-3">
|
|
||||||
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white pb-2">
|
|
||||||
<div className="h-2 w-2 rounded-full bg-red-400" />
|
|
||||||
<span className="text-xs font-bold text-slate-500 uppercase">
|
|
||||||
Versión Anterior
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 rounded-xl border border-red-100 bg-red-50/30 p-4">
|
);
|
||||||
<RenderValue
|
|
||||||
value={selectedChange?.detalles.valor_anterior}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lado Después */}
|
|
||||||
<div className="flex flex-col space-y-3">
|
|
||||||
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white pb-2">
|
|
||||||
<div className="h-2 w-2 rounded-full bg-emerald-400" />
|
|
||||||
<span className="text-xs font-bold text-slate-500 uppercase">
|
|
||||||
Nueva Versión
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 rounded-xl border border-emerald-100 bg-emerald-50/40 p-4">
|
|
||||||
<RenderValue value={selectedChange?.detalles.valor_nuevo} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 flex flex-shrink-0 items-center justify-center gap-2 rounded-lg border border-slate-100 bg-slate-50 p-3 text-xs text-slate-500">
|
|
||||||
Campo modificado:{' '}
|
|
||||||
<Badge variant="secondary">{selectedChange?.detalles.campo}</Badge>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,795 +0,0 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query'
|
|
||||||
import { useParams } from '@tanstack/react-router'
|
|
||||||
import {
|
|
||||||
Sparkles,
|
|
||||||
Send,
|
|
||||||
Target,
|
|
||||||
UserCheck,
|
|
||||||
Lightbulb,
|
|
||||||
FileText,
|
|
||||||
GraduationCap,
|
|
||||||
BookOpen,
|
|
||||||
Check,
|
|
||||||
X,
|
|
||||||
MessageSquarePlus,
|
|
||||||
Archive,
|
|
||||||
History,
|
|
||||||
Edit2, // Agregado
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
|
||||||
|
|
||||||
import { ImprovementCard } from './SaveAsignatura/ImprovementCardProps'
|
|
||||||
|
|
||||||
import type { IASugerencia } from '@/types/asignatura'
|
|
||||||
|
|
||||||
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Drawer, DrawerContent } from '@/components/ui/drawer'
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
import {
|
|
||||||
useAISubjectChat,
|
|
||||||
useConversationBySubject,
|
|
||||||
useMessagesBySubjectChat,
|
|
||||||
useSubject,
|
|
||||||
useUpdateSubjectConversationName,
|
|
||||||
useUpdateSubjectConversationStatus,
|
|
||||||
} from '@/data'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface SelectedField {
|
|
||||||
key: string
|
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IAAsignaturaTabProps {
|
|
||||||
asignatura?: Record<string, any>
|
|
||||||
onAcceptSuggestion: (sugerencia: IASugerencia) => void
|
|
||||||
onRejectSuggestion: (messageId: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function IAAsignaturaTab({
|
|
||||||
onAcceptSuggestion,
|
|
||||||
onRejectSuggestion,
|
|
||||||
}: IAAsignaturaTabProps) {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const { asignaturaId } = useParams({
|
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
|
||||||
})
|
|
||||||
|
|
||||||
// --- ESTADOS ---
|
|
||||||
const [activeChatId, setActiveChatId] = useState<string | undefined>(
|
|
||||||
undefined,
|
|
||||||
)
|
|
||||||
const [showArchived, setShowArchived] = useState(false)
|
|
||||||
const [input, setInput] = useState('')
|
|
||||||
const [selectedFields, setSelectedFields] = useState<Array<SelectedField>>([])
|
|
||||||
const [showSuggestions, setShowSuggestions] = useState(false)
|
|
||||||
const [isSending, setIsSending] = useState(false)
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
// --- DATA QUERIES ---
|
|
||||||
const { data: datosGenerales } = useSubject(asignaturaId)
|
|
||||||
const { data: todasConversaciones, isLoading: loadingConv } =
|
|
||||||
useConversationBySubject(asignaturaId)
|
|
||||||
const { data: rawMessages } = useMessagesBySubjectChat(activeChatId, {
|
|
||||||
enabled: !!activeChatId,
|
|
||||||
})
|
|
||||||
const { mutateAsync: sendMessage } = useAISubjectChat()
|
|
||||||
const { mutate: updateStatus } = useUpdateSubjectConversationStatus()
|
|
||||||
const [isCreatingNewChat, setIsCreatingNewChat] = useState(false)
|
|
||||||
const hasInitialSelected = useRef(false)
|
|
||||||
const { mutate: updateName } = useUpdateSubjectConversationName()
|
|
||||||
const [editingId, setEditingId] = useState<string | null>(null)
|
|
||||||
const [tempName, setTempName] = useState('')
|
|
||||||
const [openIA, setOpenIA] = useState(false)
|
|
||||||
|
|
||||||
const [selectedArchivoIds, setSelectedArchivoIds] = useState<Array<string>>(
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
const [selectedRepositorioIds, setSelectedRepositorioIds] = useState<
|
|
||||||
Array<string>
|
|
||||||
>([])
|
|
||||||
const [uploadedFiles, setUploadedFiles] = useState<Array<File>>([])
|
|
||||||
|
|
||||||
// Cálculo del total para el Badge del botón
|
|
||||||
const totalReferencias =
|
|
||||||
selectedArchivoIds.length +
|
|
||||||
selectedRepositorioIds.length +
|
|
||||||
uploadedFiles.length
|
|
||||||
|
|
||||||
const isAiThinking = useMemo(() => {
|
|
||||||
if (isSending) return true
|
|
||||||
if (!rawMessages || rawMessages.length === 0) return false
|
|
||||||
|
|
||||||
// Verificamos si el último mensaje está en estado de procesamiento
|
|
||||||
const lastMessage = rawMessages[rawMessages.length - 1]
|
|
||||||
return (
|
|
||||||
lastMessage.estado === 'PROCESANDO' || lastMessage.estado === 'PENDIENTE'
|
|
||||||
)
|
|
||||||
}, [isSending, rawMessages])
|
|
||||||
|
|
||||||
// --- AUTO-SCROLL ---
|
|
||||||
useEffect(() => {
|
|
||||||
const viewport = scrollRef.current?.querySelector(
|
|
||||||
'[data-radix-scroll-area-viewport]',
|
|
||||||
)
|
|
||||||
if (viewport) {
|
|
||||||
viewport.scrollTop = viewport.scrollHeight
|
|
||||||
}
|
|
||||||
}, [rawMessages, isSending])
|
|
||||||
|
|
||||||
// --- FILTRADO DE CHATS ---
|
|
||||||
const { activeChats, archivedChats } = useMemo(() => {
|
|
||||||
const chats = todasConversaciones || []
|
|
||||||
return {
|
|
||||||
activeChats: chats.filter((c: any) => c.estado === 'ACTIVA'),
|
|
||||||
archivedChats: chats.filter((c: any) => c.estado === 'ARCHIVADA'),
|
|
||||||
}
|
|
||||||
}, [todasConversaciones])
|
|
||||||
|
|
||||||
const availableFields = useMemo(() => {
|
|
||||||
// 1. Obtenemos los campos dinámicos de la DB
|
|
||||||
const dynamicFields = datosGenerales?.datos
|
|
||||||
? Object.keys(datosGenerales.datos).map((key) => {
|
|
||||||
const estructuraProps =
|
|
||||||
datosGenerales?.estructuras_asignatura?.definicion?.properties || {}
|
|
||||||
return {
|
|
||||||
key,
|
|
||||||
label:
|
|
||||||
estructuraProps[key]?.title ||
|
|
||||||
key.replace(/_/g, ' ').toUpperCase(),
|
|
||||||
value: String(datosGenerales.datos[key] || ''),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: []
|
|
||||||
|
|
||||||
// 2. Definimos tus campos manuales (hardcoded)
|
|
||||||
const hardcodedFields = [
|
|
||||||
{
|
|
||||||
key: 'contenido_tematico',
|
|
||||||
label: 'Contenido temático',
|
|
||||||
value: '', // Puedes dejarlo vacío o buscarlo en datosGenerales si existiera
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'criterios_de_evaluacion',
|
|
||||||
label: 'Criterios de evaluación',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// 3. Unimos ambos, filtrando duplicados por si acaso el backend ya los envía
|
|
||||||
const combined = [...dynamicFields]
|
|
||||||
|
|
||||||
hardcodedFields.forEach((hf) => {
|
|
||||||
if (!combined.some((f) => f.key === hf.key)) {
|
|
||||||
combined.push(hf)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return combined
|
|
||||||
}, [datosGenerales])
|
|
||||||
|
|
||||||
// --- PROCESAMIENTO DE MENSAJES ---
|
|
||||||
// --- PROCESAMIENTO DE MENSAJES ---
|
|
||||||
const messages = useMemo(() => {
|
|
||||||
const msgs: Array<any> = []
|
|
||||||
|
|
||||||
// 1. Mensajes existentes de la DB
|
|
||||||
if (rawMessages) {
|
|
||||||
rawMessages.forEach((m) => {
|
|
||||||
// Mensaje del usuario
|
|
||||||
msgs.push({ id: `${m.id}-user`, role: 'user', content: m.mensaje })
|
|
||||||
|
|
||||||
// Respuesta de la IA (si existe)
|
|
||||||
if (m.respuesta) {
|
|
||||||
const sugerencias =
|
|
||||||
m.propuesta?.recommendations?.map((rec: any, index: number) => ({
|
|
||||||
id: `${m.id}-sug-${index}`,
|
|
||||||
messageId: m.id,
|
|
||||||
campoKey: rec.campo_afectado,
|
|
||||||
campoNombre: rec.campo_afectado.replace(/_/g, ' '),
|
|
||||||
valorSugerido: rec.texto_mejora,
|
|
||||||
aceptada: rec.aplicada,
|
|
||||||
})) || []
|
|
||||||
|
|
||||||
msgs.push({
|
|
||||||
id: `${m.id}-ai`,
|
|
||||||
role: 'assistant',
|
|
||||||
content: m.respuesta,
|
|
||||||
sugerencias: sugerencias,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. INYECCIÓN OPTIMISTA: Si estamos enviando, mostramos el texto actual del input como mensaje de usuario
|
|
||||||
if (isSending && input.trim()) {
|
|
||||||
msgs.push({
|
|
||||||
id: 'optimistic-user-msg',
|
|
||||||
role: 'user',
|
|
||||||
content: input,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return msgs
|
|
||||||
}, [rawMessages, isSending, input])
|
|
||||||
|
|
||||||
// Auto-selección inicial
|
|
||||||
useEffect(() => {
|
|
||||||
// Si ya hay un chat, o si el usuario ya interactuó (hasInitialSelected), abortamos.
|
|
||||||
if (activeChatId || hasInitialSelected.current) return
|
|
||||||
|
|
||||||
if (activeChats.length > 0 && !loadingConv) {
|
|
||||||
setActiveChatId(activeChats[0].id)
|
|
||||||
hasInitialSelected.current = true
|
|
||||||
}
|
|
||||||
}, [activeChats, loadingConv])
|
|
||||||
|
|
||||||
const filteredFields = useMemo(() => {
|
|
||||||
if (!showSuggestions) return availableFields
|
|
||||||
|
|
||||||
// Extraemos lo que hay después del último ':' para filtrar
|
|
||||||
const lastColonIndex = input.lastIndexOf(':')
|
|
||||||
const query = input.slice(lastColonIndex + 1).toLowerCase()
|
|
||||||
|
|
||||||
return availableFields.filter(
|
|
||||||
(f) =>
|
|
||||||
f.label.toLowerCase().includes(query) ||
|
|
||||||
f.key.toLowerCase().includes(query),
|
|
||||||
)
|
|
||||||
}, [availableFields, input, showSuggestions])
|
|
||||||
|
|
||||||
// 2. Efecto para cerrar con ESC
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape') setShowSuggestions(false)
|
|
||||||
}
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 3. Función para insertar el campo y limpiar el prompt
|
|
||||||
const handleSelectField = (field: SelectedField) => {
|
|
||||||
// 1. Agregamos al array de objetos (para tu lógica de API)
|
|
||||||
if (!selectedFields.find((f) => f.key === field.key)) {
|
|
||||||
setSelectedFields((prev) => [...prev, field])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Lógica de autocompletado en el texto
|
|
||||||
const lastColonIndex = input.lastIndexOf(':')
|
|
||||||
if (lastColonIndex !== -1) {
|
|
||||||
// Tomamos lo que había antes del ":" + el Nombre del Campo + un espacio
|
|
||||||
const nuevoTexto = input.slice(0, lastColonIndex) + `${field.label} `
|
|
||||||
setInput(nuevoTexto)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Cerramos el buscador y devolvemos el foco al textarea
|
|
||||||
setShowSuggestions(false)
|
|
||||||
|
|
||||||
// Opcional: Si tienes una ref del textarea, puedes hacer:
|
|
||||||
// textareaRef.current?.focus()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveName = (id: string) => {
|
|
||||||
if (tempName.trim()) {
|
|
||||||
updateName({ id, nombre: tempName })
|
|
||||||
}
|
|
||||||
setEditingId(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSend = async (promptOverride?: string) => {
|
|
||||||
const text = promptOverride || input
|
|
||||||
if (!text.trim() && selectedFields.length === 0) return
|
|
||||||
|
|
||||||
setIsSending(true)
|
|
||||||
try {
|
|
||||||
const response = await sendMessage({
|
|
||||||
subjectId: asignaturaId as any, // Importante: se usa para crear la conv si activeChatId es undefined
|
|
||||||
content: text,
|
|
||||||
campos: selectedFields.map((f) => f.key),
|
|
||||||
conversacionId: activeChatId, // Si es undefined, la mutación crea el chat automáticamente
|
|
||||||
})
|
|
||||||
|
|
||||||
// IMPORTANTE: Después de la respuesta, actualizamos el ID activo con el que creó el backend
|
|
||||||
if (response.conversacionId) {
|
|
||||||
setActiveChatId(response.conversacionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
setInput('')
|
|
||||||
// setSelectedFields([])
|
|
||||||
|
|
||||||
// Invalidamos la lista de conversaciones para que el nuevo chat aparezca en el historial (panel izquierdo)
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ['conversation-by-subject', asignaturaId],
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al enviar mensaje:', error)
|
|
||||||
} finally {
|
|
||||||
setIsSending(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleField = (field: SelectedField) => {
|
|
||||||
setSelectedFields((prev) =>
|
|
||||||
prev.find((f) => f.key === field.key)
|
|
||||||
? prev.filter((f) => f.key !== field.key)
|
|
||||||
: [...prev, field],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createNewChat = () => {
|
|
||||||
setActiveChatId(undefined) // Al ser undefined, el próximo mensaje creará la charla en el backend
|
|
||||||
setInput('')
|
|
||||||
setSelectedFields([])
|
|
||||||
// Opcional: podrías forzar el foco al textarea aquí con una ref
|
|
||||||
}
|
|
||||||
|
|
||||||
const PRESETS = [
|
|
||||||
{
|
|
||||||
id: 'mejorar-obj',
|
|
||||||
label: 'Mejorar objetivo',
|
|
||||||
icon: Target,
|
|
||||||
prompt: 'Mejora la redacción del objetivo...',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sugerir-cont',
|
|
||||||
label: 'Sugerir contenido',
|
|
||||||
icon: BookOpen,
|
|
||||||
prompt: 'Genera un desglose de temas...',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'actividades',
|
|
||||||
label: 'Actividades',
|
|
||||||
icon: GraduationCap,
|
|
||||||
prompt: 'Sugiere actividades prácticas...',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
|
|
||||||
{/* PANEL IZQUIERDO */}
|
|
||||||
<div className="flex w-64 flex-col border-r pr-4">
|
|
||||||
<div className="mb-4 flex items-center justify-between px-2">
|
|
||||||
<h2 className="flex items-center gap-2 text-xs font-bold text-slate-500 uppercase">
|
|
||||||
<History size={14} /> Historial
|
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className={cn(
|
|
||||||
'h-8 w-8',
|
|
||||||
showArchived && 'bg-teal-50 text-teal-600',
|
|
||||||
)}
|
|
||||||
onClick={() => setShowArchived(!showArchived)}
|
|
||||||
>
|
|
||||||
<Archive size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
setActiveChatId(undefined)
|
|
||||||
hasInitialSelected.current = true
|
|
||||||
setIsCreatingNewChat(true)
|
|
||||||
setInput('')
|
|
||||||
setSelectedFields([])
|
|
||||||
|
|
||||||
// 4. Opcional: Limpiar el caché de mensajes actual para que la pantalla se vea vacía al instante
|
|
||||||
queryClient.setQueryData(['subject-messages', undefined], [])
|
|
||||||
}}
|
|
||||||
variant="outline"
|
|
||||||
className="mb-4 w-full justify-start gap-2 border-dashed border-slate-300 hover:border-teal-500"
|
|
||||||
>
|
|
||||||
<MessageSquarePlus size={18} /> Nuevo Chat
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* PANEL IZQUIERDO - Cambios en ScrollArea y contenedor */}
|
|
||||||
<ScrollArea className="flex-1">
|
|
||||||
<div className="flex flex-col gap-1 pr-3">
|
|
||||||
{' '}
|
|
||||||
{/* Eliminado space-y-1 para mejor control con gap */}
|
|
||||||
{(showArchived ? archivedChats : activeChats).map((chat: any) => (
|
|
||||||
<div
|
|
||||||
key={chat.id}
|
|
||||||
className={cn(
|
|
||||||
// Agregamos 'overflow-hidden' para que nada salga de este cuadro
|
|
||||||
'group relative flex w-full min-w-0 items-center justify-between gap-2 overflow-hidden rounded-lg px-3 py-2 text-sm transition-all',
|
|
||||||
activeChatId === chat.id
|
|
||||||
? 'bg-teal-50 text-teal-900'
|
|
||||||
: 'text-slate-600 hover:bg-slate-100',
|
|
||||||
)}
|
|
||||||
onDoubleClick={() => {
|
|
||||||
setEditingId(chat.id)
|
|
||||||
setTempName(chat.nombre || chat.titulo || 'Conversacion')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{editingId === chat.id ? (
|
|
||||||
<div className="flex min-w-0 flex-1 items-center">
|
|
||||||
<input
|
|
||||||
autoFocus
|
|
||||||
className="w-full rounded border-none bg-white px-1 text-xs ring-1 ring-teal-400 outline-none"
|
|
||||||
value={tempName}
|
|
||||||
onChange={(e) => setTempName(e.target.value)}
|
|
||||||
onBlur={() => handleSaveName(chat.id)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') handleSaveName(chat.id)
|
|
||||||
if (e.key === 'Escape') setEditingId(null)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* CLAVE 2: 'truncate' y 'min-w-0' en el span para que ceda ante los botones */}
|
|
||||||
<span
|
|
||||||
onClick={() => setActiveChatId(chat.id)}
|
|
||||||
className="block max-w-[140px] min-w-0 flex-1 cursor-pointer truncate pr-1"
|
|
||||||
title={chat.nombre || chat.titulo}
|
|
||||||
>
|
|
||||||
{chat.nombre || chat.titulo || 'Conversación'}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* CLAVE 3: 'shrink-0' asegura que los botones NUNCA desaparezcan */}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'z-10 flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100',
|
|
||||||
activeChatId === chat.id
|
|
||||||
? 'bg-teal-50'
|
|
||||||
: 'bg-slate-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TooltipProvider delayDuration={300}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setEditingId(chat.id)
|
|
||||||
setTempName(chat.nombre || chat.titulo || '')
|
|
||||||
}}
|
|
||||||
className="rounded-md p-1 transition-colors hover:bg-slate-200 hover:text-teal-600"
|
|
||||||
>
|
|
||||||
<Edit2 size={14} />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="text-[10px]">
|
|
||||||
Editar nombre
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
const nuevoEstado =
|
|
||||||
chat.estado === 'ACTIVA'
|
|
||||||
? 'ARCHIVADA'
|
|
||||||
: 'ACTIVA'
|
|
||||||
updateStatus({
|
|
||||||
id: chat.id,
|
|
||||||
estado: nuevoEstado,
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
className={cn(
|
|
||||||
'rounded-md p-1 transition-colors hover:bg-slate-200',
|
|
||||||
chat.estado === 'ACTIVA'
|
|
||||||
? 'hover:text-red-500'
|
|
||||||
: 'hover:text-teal-600',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{chat.estado === 'ACTIVA' ? (
|
|
||||||
<Archive size={14} />
|
|
||||||
) : (
|
|
||||||
<History size={14} className="scale-x-[-1]" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="top" className="text-[10px]">
|
|
||||||
{chat.estado === 'ACTIVA'
|
|
||||||
? 'Archivar'
|
|
||||||
: 'Desarchivar'}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PANEL CENTRAL */}
|
|
||||||
<div className="relative flex min-w-0 flex-[3] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm">
|
|
||||||
<div className="flex shrink-0 items-center justify-between border-b bg-white p-3">
|
|
||||||
<span className="text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
|
||||||
Asistente IA
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => setOpenIA(true)}
|
|
||||||
className="flex items-center gap-2 rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium transition hover:bg-slate-200"
|
|
||||||
>
|
|
||||||
<FileText size={14} className="text-slate-500" />
|
|
||||||
Referencias
|
|
||||||
{totalReferencias > 0 && (
|
|
||||||
<span className="animate-in zoom-in flex h-4 min-w-[16px] items-center justify-center rounded-full bg-teal-600 px-1 text-[10px] text-white">
|
|
||||||
{totalReferencias}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative min-h-0 flex-1">
|
|
||||||
<ScrollArea ref={scrollRef} className="h-full w-full">
|
|
||||||
<div className="mx-auto max-w-3xl space-y-8 p-6">
|
|
||||||
{messages.map((msg) => (
|
|
||||||
<div
|
|
||||||
key={msg.id}
|
|
||||||
className={cn(
|
|
||||||
'flex gap-4',
|
|
||||||
msg.role === 'user' ? 'flex-row-reverse' : 'flex-row',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
className={cn(
|
|
||||||
'h-9 w-9 shrink-0 border shadow-sm',
|
|
||||||
msg.role === 'assistant'
|
|
||||||
? 'bg-teal-600 text-white'
|
|
||||||
: 'bg-slate-100',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AvatarFallback>
|
|
||||||
{msg.role === 'assistant' ? (
|
|
||||||
<Sparkles size={16} />
|
|
||||||
) : (
|
|
||||||
<UserCheck size={16} />
|
|
||||||
)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex max-w-[85%] flex-col gap-3',
|
|
||||||
msg.role === 'user' ? 'items-end' : 'items-start',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'relative overflow-hidden rounded-2xl border shadow-sm',
|
|
||||||
msg.role === 'user'
|
|
||||||
? 'rounded-tr-none border-teal-700 bg-teal-600 px-4 py-3 text-white'
|
|
||||||
: 'w-full rounded-tl-none border-slate-200 bg-white text-slate-800',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{/* Texto del mensaje principal */}
|
|
||||||
<div
|
|
||||||
style={{ whiteSpace: 'pre-line' }}
|
|
||||||
className={cn(
|
|
||||||
'text-sm leading-relaxed',
|
|
||||||
msg.role === 'assistant' && 'p-4',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{msg.content}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* CONTENEDOR DE SUGERENCIAS INTEGRADO */}
|
|
||||||
{msg.role === 'assistant' &&
|
|
||||||
msg.sugerencias &&
|
|
||||||
msg.sugerencias.length > 0 && (
|
|
||||||
<div className="space-y-3 border-t bg-slate-50/50 p-3">
|
|
||||||
<p className="mb-1 text-[10px] font-bold text-slate-400 uppercase">
|
|
||||||
Mejoras disponibles:
|
|
||||||
</p>
|
|
||||||
{msg.sugerencias.map((sug: any) => (
|
|
||||||
<ImprovementCard
|
|
||||||
key={sug.id}
|
|
||||||
sug={sug}
|
|
||||||
asignaturaId={asignaturaId}
|
|
||||||
onApplied={(campoFinalizado) => {
|
|
||||||
// Filtramos el array para conservar todos MENOS el que se aplicó
|
|
||||||
console.log(campoFinalizado)
|
|
||||||
console.log('campos:', selectedFields)
|
|
||||||
|
|
||||||
setSelectedFields((prev) =>
|
|
||||||
prev.filter((fieldObj) => {
|
|
||||||
// Accedemos a .key porque fieldObj es { key: "...", label: "..." }
|
|
||||||
return fieldObj.key !== campoFinalizado
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{isAiThinking && (
|
|
||||||
<div className="animate-in fade-in slide-in-from-bottom-2 flex gap-4">
|
|
||||||
<Avatar className="h-9 w-9 shrink-0 border bg-teal-600 text-white shadow-sm">
|
|
||||||
<AvatarFallback>
|
|
||||||
<Sparkles size={16} className="animate-pulse" />
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col items-start gap-2">
|
|
||||||
<div className="rounded-2xl rounded-tl-none border border-slate-200 bg-white p-4 shadow-sm">
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400 [animation-delay:-0.3s]"></span>
|
|
||||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400 [animation-delay:-0.15s]"></span>
|
|
||||||
<span className="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] font-medium text-slate-400 italic">
|
|
||||||
La IA está analizando tu solicitud...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Espacio extra al final para que el scroll no tape el último mensaje */}
|
|
||||||
<div className="h-4" />
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* INPUT */}
|
|
||||||
<div className="shrink-0 border-t bg-white p-4">
|
|
||||||
<div className="relative mx-auto max-w-4xl">
|
|
||||||
{showSuggestions && (
|
|
||||||
<div className="animate-in fade-in slide-in-from-bottom-2 absolute bottom-full left-0 z-50 mb-2 w-72 overflow-hidden rounded-xl border bg-white shadow-2xl">
|
|
||||||
<div className="flex justify-between border-b bg-slate-50 px-3 py-2 text-[10px] font-bold text-slate-500 uppercase">
|
|
||||||
<span>Filtrando campos...</span>
|
|
||||||
<span className="rounded bg-slate-200 px-1 text-[9px] text-slate-400">
|
|
||||||
ESC para cerrar
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="max-h-60 overflow-y-auto p-1">
|
|
||||||
{filteredFields.length > 0 ? (
|
|
||||||
filteredFields.map((field) => (
|
|
||||||
<button
|
|
||||||
key={field.key}
|
|
||||||
onClick={() => handleSelectField(field)}
|
|
||||||
className="flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-teal-50"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="font-medium text-slate-700">
|
|
||||||
{field.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{selectedFields.find((f) => f.key === field.key) && (
|
|
||||||
<Check size={14} className="text-teal-600" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="p-4 text-center text-xs text-slate-400 italic">
|
|
||||||
No se encontraron coincidencias
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 rounded-xl border bg-slate-50 p-2 transition-all focus-within:bg-white focus-within:ring-1 focus-within:ring-teal-500">
|
|
||||||
{selectedFields.length > 0 && (
|
|
||||||
<div className="flex flex-wrap gap-1.5 px-2 pt-1">
|
|
||||||
{selectedFields.map((field) => (
|
|
||||||
<div
|
|
||||||
key={field.key}
|
|
||||||
className="animate-in zoom-in-95 flex items-center gap-1 rounded-md border border-teal-200 bg-teal-50 px-2 py-0.5 text-[11px] font-bold text-teal-700 shadow-sm"
|
|
||||||
>
|
|
||||||
<Target size={10} />
|
|
||||||
{field.label}
|
|
||||||
<button
|
|
||||||
onClick={() => toggleField(field)}
|
|
||||||
className="ml-1 rounded-full p-0.5 transition-colors hover:bg-teal-200/50"
|
|
||||||
>
|
|
||||||
<X size={10} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-end gap-2">
|
|
||||||
<Textarea
|
|
||||||
value={input}
|
|
||||||
onChange={(e) => {
|
|
||||||
setInput(e.target.value)
|
|
||||||
if (e.target.value.endsWith(':')) setShowSuggestions(true)
|
|
||||||
else if (showSuggestions && !e.target.value.includes(':'))
|
|
||||||
setShowSuggestions(false)
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
handleSend()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder='Escribe ":" para referenciar un campo...'
|
|
||||||
className="max-h-[120px] min-h-[40px] flex-1 resize-none border-none bg-transparent text-sm shadow-none focus-visible:ring-0"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleSend()}
|
|
||||||
disabled={
|
|
||||||
(!input.trim() && selectedFields.length === 0) || isSending
|
|
||||||
}
|
|
||||||
size="icon"
|
|
||||||
className="h-9 w-9 bg-teal-600 hover:bg-teal-700"
|
|
||||||
>
|
|
||||||
<Send size={16} className="text-white" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PANEL DERECHO ACCIONES */}
|
|
||||||
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
|
|
||||||
<h4 className="flex items-center gap-2 text-sm font-bold text-slate-800">
|
|
||||||
<Lightbulb size={18} className="text-orange-500" /> Atajos
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{PRESETS.map((preset) => (
|
|
||||||
<button
|
|
||||||
key={preset.id}
|
|
||||||
onClick={() => handleSend(preset.prompt)}
|
|
||||||
className="group flex w-full items-center gap-3 rounded-xl border bg-white p-3 text-left text-sm transition-all hover:border-teal-500 hover:bg-teal-50"
|
|
||||||
>
|
|
||||||
<div className="rounded-lg bg-slate-100 p-2 group-hover:bg-teal-100 group-hover:text-teal-600">
|
|
||||||
<preset.icon size={16} />
|
|
||||||
</div>
|
|
||||||
<span className="font-medium text-slate-700">{preset.label}</span>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* --- DRAWER DE REFERENCIAS --- */}
|
|
||||||
<Drawer open={openIA} onOpenChange={setOpenIA}>
|
|
||||||
<DrawerContent className="fixed inset-x-0 bottom-0 mx-auto mb-4 flex h-[80vh] w-full max-w-2xl flex-col overflow-hidden rounded-2xl border bg-white shadow-2xl">
|
|
||||||
<div className="flex items-center justify-between border-b bg-slate-50/50 px-4 py-3">
|
|
||||||
<h2 className="text-xs font-bold tracking-wider text-slate-500 uppercase">
|
|
||||||
Referencias para la IA
|
|
||||||
</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setOpenIA(false)}
|
|
||||||
className="text-slate-400 hover:text-slate-600"
|
|
||||||
>
|
|
||||||
<X size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4">
|
|
||||||
<ReferenciasParaIA
|
|
||||||
selectedArchivoIds={selectedArchivoIds}
|
|
||||||
selectedRepositorioIds={selectedRepositorioIds}
|
|
||||||
uploadedFiles={uploadedFiles}
|
|
||||||
onToggleArchivo={(id, checked) => {
|
|
||||||
setSelectedArchivoIds((prev) =>
|
|
||||||
checked ? [...prev, id] : prev.filter((a) => a !== id),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
onToggleRepositorio={(id, checked) => {
|
|
||||||
setSelectedRepositorioIds((prev) =>
|
|
||||||
checked ? [...prev, id] : prev.filter((r) => r !== id),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
onFilesChange={(files) => setUploadedFiles(files)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
357
src/components/asignaturas/detalle/IAMateriaTab.tsx
Normal file
357
src/components/asignaturas/detalle/IAMateriaTab.tsx
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Send, Sparkles, Bot, User, Check, X, RefreshCw, Lightbulb, Wand2 } from 'lucide-react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from '@/components/ui/command';
|
||||||
|
import type { IAMessage, IASugerencia, CampoEstructura } from '@/types/materia';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
//import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface IAMateriaTabProps {
|
||||||
|
campos: CampoEstructura[];
|
||||||
|
datosGenerales: Record<string, any>;
|
||||||
|
messages: IAMessage[];
|
||||||
|
onSendMessage: (message: string, campoId?: string) => void;
|
||||||
|
onAcceptSuggestion: (sugerencia: IASugerencia) => void;
|
||||||
|
onRejectSuggestion: (messageId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quickActions = [
|
||||||
|
{ id: 'mejorar-objetivos', label: 'Mejorar objetivos', icon: Wand2, prompt: 'Mejora el :objetivo_general para que sea más específico y medible' },
|
||||||
|
{ id: 'generar-contenido', label: 'Generar contenido temático', icon: Lightbulb, prompt: 'Sugiere un contenido temático completo basado en los objetivos y competencias' },
|
||||||
|
{ id: 'alinear-perfil', label: 'Alinear con perfil de egreso', icon: RefreshCw, prompt: 'Revisa las :competencias y alinéalas con el perfil de egreso del plan' },
|
||||||
|
{ id: 'ajustar-biblio', label: 'Recomendar bibliografía', icon: Sparkles, prompt: 'Recomienda bibliografía actualizada basándote en el contenido temático' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function IAMateriaTab({ campos, datosGenerales, messages, onSendMessage, onAcceptSuggestion, onRejectSuggestion }: IAMateriaTabProps) {
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showFieldSelector, setShowFieldSelector] = useState(false);
|
||||||
|
const [fieldSelectorPosition, setFieldSelectorPosition] = useState({ top: 0, left: 0 });
|
||||||
|
const [cursorPosition, setCursorPosition] = useState(0);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const pos = e.target.selectionStart;
|
||||||
|
setInput(value);
|
||||||
|
setCursorPosition(pos);
|
||||||
|
|
||||||
|
// Check for : character to trigger field selector
|
||||||
|
const lastChar = value.charAt(pos - 1);
|
||||||
|
if (lastChar === ':') {
|
||||||
|
const rect = textareaRef.current?.getBoundingClientRect();
|
||||||
|
if (rect) {
|
||||||
|
setFieldSelectorPosition({ top: rect.bottom + 8, left: rect.left });
|
||||||
|
setShowFieldSelector(true);
|
||||||
|
}
|
||||||
|
} else if (showFieldSelector && (lastChar === ' ' || !value.includes(':'))) {
|
||||||
|
setShowFieldSelector(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertFieldMention = (campoId: string) => {
|
||||||
|
const beforeCursor = input.slice(0, cursorPosition);
|
||||||
|
const afterCursor = input.slice(cursorPosition);
|
||||||
|
const lastColonIndex = beforeCursor.lastIndexOf(':');
|
||||||
|
const newInput = beforeCursor.slice(0, lastColonIndex) + `:${campoId}` + afterCursor;
|
||||||
|
setInput(newInput);
|
||||||
|
setShowFieldSelector(false);
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!input.trim() || isLoading) return;
|
||||||
|
|
||||||
|
// Extract field mention if any
|
||||||
|
const fieldMatch = input.match(/:(\w+)/);
|
||||||
|
const campoId = fieldMatch ? fieldMatch[1] : undefined;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
onSendMessage(input, campoId);
|
||||||
|
setInput('');
|
||||||
|
|
||||||
|
// Simulate AI response delay
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuickAction = (prompt: string) => {
|
||||||
|
setInput(prompt);
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMessageContent = (content: string) => {
|
||||||
|
// Render field mentions as styled badges
|
||||||
|
return content.split(/(:[\w_]+)/g).map((part, i) => {
|
||||||
|
if (part.startsWith(':')) {
|
||||||
|
const campo = campos.find(c => c.id === part.slice(1));
|
||||||
|
return (
|
||||||
|
<span key={i} className="field-mention mx-0.5">
|
||||||
|
{campo?.nombre || part}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 animate-fade-in">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="font-display text-2xl font-semibold text-foreground flex items-center gap-2">
|
||||||
|
<Sparkles className="w-6 h-6 text-accent" />
|
||||||
|
IA de la materia
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Usa <kbd className="px-1.5 py-0.5 bg-muted rounded text-xs font-mono">:</kbd> para mencionar campos específicos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* Chat area */}
|
||||||
|
<Card className="lg:col-span-2 card-elevated flex flex-col h-[600px]">
|
||||||
|
<CardHeader className="pb-2 border-b">
|
||||||
|
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||||
|
Conversación
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-1 flex flex-col p-0">
|
||||||
|
<ScrollArea className="flex-1 p-4" ref={scrollRef}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Bot className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" />
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Inicia una conversación para mejorar tu materia con IA
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
messages.map((message) => (
|
||||||
|
<div key={message.id} className={cn(
|
||||||
|
"flex gap-3",
|
||||||
|
message.role === 'user' ? "justify-end" : "justify-start"
|
||||||
|
)}>
|
||||||
|
{message.role === 'assistant' && (
|
||||||
|
<div className="w-8 h-8 rounded-full bg-accent/20 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Bot className="w-4 h-4 text-accent" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={cn(
|
||||||
|
"max-w-[80%] rounded-lg px-4 py-3",
|
||||||
|
message.role === 'user'
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "bg-muted"
|
||||||
|
)}>
|
||||||
|
<p className="text-sm whitespace-pre-wrap">
|
||||||
|
{renderMessageContent(message.content)}
|
||||||
|
</p>
|
||||||
|
{message.sugerencia && !message.sugerencia.aceptada && (
|
||||||
|
<div className="mt-3 p-3 bg-background/80 rounded-md border">
|
||||||
|
<p className="text-xs font-medium text-muted-foreground mb-2">
|
||||||
|
Sugerencia para: {message.sugerencia.campoNombre}
|
||||||
|
</p>
|
||||||
|
<div className="text-sm text-foreground bg-accent/10 p-2 rounded mb-3 max-h-32 overflow-y-auto">
|
||||||
|
{message.sugerencia.valorSugerido}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onAcceptSuggestion(message.sugerencia!)}
|
||||||
|
className="bg-success hover:bg-success/90 text-success-foreground"
|
||||||
|
>
|
||||||
|
<Check className="w-3 h-3 mr-1" />
|
||||||
|
Aplicar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onRejectSuggestion(message.id)}
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3 mr-1" />
|
||||||
|
Rechazar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{message.sugerencia?.aceptada && (
|
||||||
|
<Badge className="mt-2 badge-library">
|
||||||
|
<Check className="w-3 h-3 mr-1" />
|
||||||
|
Sugerencia aplicada
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{message.role === 'user' && (
|
||||||
|
<div className="w-8 h-8 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
|
||||||
|
<User className="w-4 h-4 text-primary-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-accent/20 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Bot className="w-4 h-4 text-accent animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div className="bg-muted rounded-lg px-4 py-3">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="w-2 h-2 bg-accent rounded-full animate-bounce [animation-delay:-0.3s]" />
|
||||||
|
<div className="w-2 h-2 bg-accent rounded-full animate-bounce [animation-delay:-0.15s]" />
|
||||||
|
<div className="w-2 h-2 bg-accent rounded-full animate-bounce" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Input area */}
|
||||||
|
<div className="p-4 border-t">
|
||||||
|
<div className="relative">
|
||||||
|
<Textarea
|
||||||
|
ref={textareaRef}
|
||||||
|
value={input}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Escribe tu mensaje... Usa : para mencionar campos"
|
||||||
|
className="min-h-[80px] pr-12 resize-none"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!input.trim() || isLoading}
|
||||||
|
className="absolute bottom-3 right-3 h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<Send className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Field selector popover */}
|
||||||
|
{showFieldSelector && (
|
||||||
|
<div className="absolute z-50 mt-1 w-64 bg-popover border rounded-lg shadow-lg">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Buscar campo..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No se encontró el campo</CommandEmpty>
|
||||||
|
<CommandGroup heading="Campos disponibles">
|
||||||
|
{campos.map((campo) => (
|
||||||
|
<CommandItem
|
||||||
|
key={campo.id}
|
||||||
|
value={campo.id}
|
||||||
|
onSelect={() => insertFieldMention(campo.id)}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="font-mono text-xs text-accent mr-2">
|
||||||
|
:{campo.id}
|
||||||
|
</span>
|
||||||
|
<span>{campo.nombre}</span>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Sidebar with quick actions and fields */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Quick actions */}
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Acciones rápidas</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
{quickActions.map((action) => {
|
||||||
|
const Icon = action.icon;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={action.id}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start text-left h-auto py-3"
|
||||||
|
onClick={() => handleQuickAction(action.prompt)}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4 mr-2 text-accent flex-shrink-0" />
|
||||||
|
<span className="text-sm">{action.label}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Available fields */}
|
||||||
|
<Card className="card-elevated">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Campos de la materia</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ScrollArea className="h-[280px]">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{campos.map((campo) => {
|
||||||
|
const hasValue = !!datosGenerales[campo.id];
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={campo.id}
|
||||||
|
className={cn(
|
||||||
|
"p-2 rounded-md border cursor-pointer transition-colors hover:bg-muted/50",
|
||||||
|
hasValue ? "border-success/30" : "border-warning/30"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setInput(prev => prev + `:${campo.id} `);
|
||||||
|
textareaRef.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-mono text-accent">:{campo.id}</span>
|
||||||
|
{hasValue ? (
|
||||||
|
<Badge variant="outline" className="text-xs text-success border-success/30">
|
||||||
|
Completo
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" className="text-xs text-warning border-warning/30">
|
||||||
|
Vacío
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-foreground mt-1">{campo.nombre}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
396
src/components/asignaturas/detalle/MateriaDetailPage.tsx
Normal file
396
src/components/asignaturas/detalle/MateriaDetailPage.tsx
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Separator } from '@/components/ui/separator'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
GraduationCap,
|
||||||
|
Edit2, Save,
|
||||||
|
Pencil
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { ContenidoTematico } from './ContenidoTematico'
|
||||||
|
import { BibliographyItem } from './BibliographyItem'
|
||||||
|
import { IAMateriaTab } from './IAMateriaTab'
|
||||||
|
import type {
|
||||||
|
CampoEstructura,
|
||||||
|
IAMessage,
|
||||||
|
IASugerencia,
|
||||||
|
UnidadTematica,
|
||||||
|
} from '@/types/materia';
|
||||||
|
import {
|
||||||
|
mockMateria,
|
||||||
|
mockEstructura,
|
||||||
|
mockDocumentoSep,
|
||||||
|
mockHistorial
|
||||||
|
} from '@/data/mockMateriaData';
|
||||||
|
import { DocumentoSEPTab } from './DocumentoSEPTab'
|
||||||
|
import { HistorialTab } from './HistorialTab'
|
||||||
|
|
||||||
|
export interface BibliografiaEntry {
|
||||||
|
id: string;
|
||||||
|
tipo: 'BASICA' | 'COMPLEMENTARIA';
|
||||||
|
cita: string;
|
||||||
|
fuenteBibliotecaId?: string;
|
||||||
|
fuenteBiblioteca?: any;
|
||||||
|
}
|
||||||
|
export interface BibliografiaTabProps {
|
||||||
|
bibliografia: BibliografiaEntry[];
|
||||||
|
onSave: (bibliografia: BibliografiaEntry[]) => void;
|
||||||
|
isSaving: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MateriaDetailPage() {
|
||||||
|
|
||||||
|
// 1. Asegúrate de tener estos estados en tu componente principal
|
||||||
|
const [messages, setMessages] = useState<IAMessage[]>([]);
|
||||||
|
const [datosGenerales, setDatosGenerales] = useState({});
|
||||||
|
const [campos, setCampos] = useState<CampoEstructura[]>([]);
|
||||||
|
|
||||||
|
// 2. Funciones de manejo para la IA
|
||||||
|
const handleSendMessage = (text: string, campoId?: string) => {
|
||||||
|
const newMessage: IAMessage = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
role: 'user',
|
||||||
|
content: text,
|
||||||
|
timestamp: new Date(),
|
||||||
|
campoAfectado: campoId
|
||||||
|
};
|
||||||
|
setMessages([...messages, newMessage]);
|
||||||
|
|
||||||
|
// Aquí llamarías a tu API de OpenAI/Claude
|
||||||
|
//toast.info("Enviando consulta a la IA...");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAcceptSuggestion = (sugerencia: IASugerencia) => {
|
||||||
|
// Lógica para actualizar el valor del campo en tu estado de datosGenerales
|
||||||
|
//toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dentro de tu componente principal (donde están los Tabs)
|
||||||
|
const [bibliografia, setBibliografia] = useState<BibliografiaEntry[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
tipo: 'BASICA',
|
||||||
|
cita: 'Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach. Pearson.'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const handleSaveBibliografia = (data: BibliografiaEntry[]) => {
|
||||||
|
setIsSaving(true);
|
||||||
|
// Aquí iría tu llamada a la API
|
||||||
|
setBibliografia(data);
|
||||||
|
|
||||||
|
// Simulamos un guardado
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsSaving(false);
|
||||||
|
//toast.success("Cambios guardados");
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isRegenerating, setIsRegenerating] = useState(false);
|
||||||
|
|
||||||
|
const handleRegenerateDocument = useCallback(() => {
|
||||||
|
setIsRegenerating(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsRegenerating(false);
|
||||||
|
}, 2000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{/* ================= HEADER ================= */}
|
||||||
|
<section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
|
||||||
|
<div className="max-w-7xl mx-auto px-6 py-10">
|
||||||
|
<Link
|
||||||
|
to="/planes"
|
||||||
|
className="flex items-center gap-2 text-sm text-blue-200 hover:text-white mb-4"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" />
|
||||||
|
Volver al plan
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="flex items-start justify-between gap-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Badge className="bg-blue-900/50 border border-blue-700">
|
||||||
|
IA-401
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<h1 className="text-3xl font-bold">
|
||||||
|
Inteligencia Artificial Aplicada
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<GraduationCap className="w-4 h-4" />
|
||||||
|
Ingeniería en Sistemas Computacionales
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span>Facultad de Ingeniería</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-blue-300">
|
||||||
|
Pertenece al plan:{' '}
|
||||||
|
<span className="underline cursor-pointer">
|
||||||
|
Licenciatura en Ingeniería en Sistemas Computacionales 2024
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 items-end">
|
||||||
|
<Badge variant="secondary">8 créditos</Badge>
|
||||||
|
<Badge variant="secondary">7° semestre</Badge>
|
||||||
|
<Badge variant="secondary">Sistemas Inteligentes</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ================= TABS ================= */}
|
||||||
|
<section className="bg-white border-b">
|
||||||
|
<div className="max-w-7xl mx-auto px-6">
|
||||||
|
<Tabs defaultValue="datos">
|
||||||
|
<TabsList className="h-auto bg-transparent p-0 gap-6">
|
||||||
|
<TabsTrigger value="datos">Datos generales</TabsTrigger>
|
||||||
|
<TabsTrigger value="contenido">Contenido temático</TabsTrigger>
|
||||||
|
<TabsTrigger value="bibliografia">Bibliografía</TabsTrigger>
|
||||||
|
<TabsTrigger value="ia">IA de la materia</TabsTrigger>
|
||||||
|
<TabsTrigger value="sep">Documento SEP</TabsTrigger>
|
||||||
|
<TabsTrigger value="historial">Historial</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<Separator className="mt-2" />
|
||||||
|
|
||||||
|
{/* ================= TAB: DATOS GENERALES ================= */}
|
||||||
|
<TabsContent value="datos">
|
||||||
|
<DatosGenerales />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="contenido">
|
||||||
|
<ContenidoTematico></ContenidoTematico>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="bibliografia">
|
||||||
|
<BibliographyItem
|
||||||
|
bibliografia={bibliografia}
|
||||||
|
onSave={handleSaveBibliografia}
|
||||||
|
isSaving={isSaving}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="ia">
|
||||||
|
<IAMateriaTab
|
||||||
|
campos={campos}
|
||||||
|
datosGenerales={datosGenerales}
|
||||||
|
messages={messages}
|
||||||
|
onSendMessage={handleSendMessage}
|
||||||
|
onAcceptSuggestion={handleAcceptSuggestion}
|
||||||
|
onRejectSuggestion={(id) => console.log("Rechazada") /*toast.error("Sugerencia rechazada")*/}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="sep">
|
||||||
|
<DocumentoSEPTab
|
||||||
|
documento={mockDocumentoSep}
|
||||||
|
materia={mockMateria}
|
||||||
|
estructura={mockEstructura}
|
||||||
|
datosGenerales={datosGenerales}
|
||||||
|
onRegenerate={handleRegenerateDocument}
|
||||||
|
isRegenerating={isRegenerating}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="historial">
|
||||||
|
<HistorialTab historial={mockHistorial} />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================= TAB CONTENT ================= */
|
||||||
|
|
||||||
|
function DatosGenerales() {
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto py-8 px-4 space-y-8 animate-in fade-in duration-500">
|
||||||
|
|
||||||
|
{/* Encabezado de la Sección */}
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b pb-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight text-slate-900">Datos Generales</h2>
|
||||||
|
<p className="text-slate-500 mt-1">
|
||||||
|
Información oficial estructurada bajo los lineamientos de la SEP.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
|
<Edit2 className="w-4 h-4" /> Editar borrador
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" className="gap-2 bg-blue-600 hover:bg-blue-700">
|
||||||
|
<Save className="w-4 h-4" /> Guardar cambios
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid de Información */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
{/* Columna Principal (Más ancha) */}
|
||||||
|
<div className="md:col-span-2 space-y-6">
|
||||||
|
<div className="md:col-span-2 space-y-6">
|
||||||
|
<InfoCard
|
||||||
|
title="Competencias a Desarrollar"
|
||||||
|
subtitle="Competencias profesionales que se desarrollarán"
|
||||||
|
isList={true}
|
||||||
|
initialContent={`• Diseñar algoritmos de machine learning para clasificación y predicción\n• Implementar redes neuronales profundas para procesamiento de imágenes\n• Evaluar y optimizar modelos de IA considerando métricas`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InfoCard
|
||||||
|
title="Objetivo General"
|
||||||
|
initialContent="Formar profesionales capaces de diseñar, implementar y evaluar sistemas de inteligencia artificial que resuelvan problemas complejos."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<InfoCard
|
||||||
|
title="Justificación"
|
||||||
|
initialContent="La inteligencia artificial es una de las tecnologías más disruptivas..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Columna Lateral (Información Secundaria) */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Tarjeta de Requisitos */}
|
||||||
|
<InfoCard
|
||||||
|
title="Requisitos y Seriación"
|
||||||
|
type="requirements"
|
||||||
|
initialContent={[
|
||||||
|
{ type: "Pre-requisito", code: "PA-301", name: "Programación Avanzada" },
|
||||||
|
{ type: "Co-requisito", code: "MAT-201", name: "Matemáticas Discretas" }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Tarjeta de Evaluación */}
|
||||||
|
<InfoCard
|
||||||
|
title="Sistema de Evaluación"
|
||||||
|
type="evaluation"
|
||||||
|
initialContent={[
|
||||||
|
{ label: "Exámenes parciales", value: "30%" },
|
||||||
|
{ label: "Proyecto integrador", value: "35%" },
|
||||||
|
{ label: "Prácticas de laboratorio", value: "20%" },
|
||||||
|
{ label: "Participación", value: "15%" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InfoCardProps {
|
||||||
|
title: string,
|
||||||
|
subtitle?: string
|
||||||
|
isList?:boolean
|
||||||
|
initialContent: any // Puede ser string o array de objetos
|
||||||
|
type?: 'text' | 'list' | 'requirements' | 'evaluation'
|
||||||
|
}
|
||||||
|
|
||||||
|
function InfoCard({ title, initialContent, type = 'text' }: InfoCardProps) {
|
||||||
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [data, setData] = useState(initialContent)
|
||||||
|
// Estado temporal para el área de texto (siempre editamos como texto por simplicidad)
|
||||||
|
const [tempText, setTempText] = useState(
|
||||||
|
type === 'text' || type === 'list'
|
||||||
|
? initialContent
|
||||||
|
: JSON.stringify(initialContent, null, 2) // O un formato legible
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// Aquí podrías parsear el texto de vuelta si es necesario
|
||||||
|
setData(tempText)
|
||||||
|
setIsEditing(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="transition-all hover:border-slate-300">
|
||||||
|
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0">
|
||||||
|
<CardTitle className="text-sm font-bold text-slate-700">{title}</CardTitle>
|
||||||
|
{!isEditing && (
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={() => setIsEditing(true)}>
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Textarea
|
||||||
|
value={tempText}
|
||||||
|
onChange={(e) => setTempText(e.target.value)}
|
||||||
|
className="text-xs min-h-[100px]"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button size="sm" variant="ghost" onClick={() => setIsEditing(false)}>Cancelar</Button>
|
||||||
|
<Button size="sm" className="bg-[#00a878]" onClick={handleSave}>Guardar</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm">
|
||||||
|
{type === 'requirements' && <RequirementsView items={data} />}
|
||||||
|
{type === 'evaluation' && <EvaluationView items={data} />}
|
||||||
|
{type === 'text' && <p className="text-slate-600">{data}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vista de Requisitos
|
||||||
|
function RequirementsView({ items }: { items: any[] }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{items.map((req, i) => (
|
||||||
|
<div key={i} className="p-3 bg-slate-50 rounded-lg border border-slate-100">
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-tight">{req.type}</p>
|
||||||
|
<p className="text-sm font-medium text-slate-700">{req.code} {req.name}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vista de Evaluación
|
||||||
|
function EvaluationView({ items }: { items: any[] }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<div key={i} className="flex justify-between text-sm border-b border-slate-50 pb-1.5 italic">
|
||||||
|
<span className="text-slate-500">{item.label}</span>
|
||||||
|
<span className="font-bold text-blue-600">{item.value}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function EmptyTab({ title }: { title: string }) {
|
||||||
|
return (
|
||||||
|
<div className="py-16 text-center text-muted-foreground">
|
||||||
|
{title} (pendiente)
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
import { Check, Loader2, BookOpen, Clock, ListChecks } from 'lucide-react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import type { IASugerencia } from '@/types/asignatura'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
useUpdateAsignatura,
|
|
||||||
useSubject,
|
|
||||||
useUpdateSubjectRecommendation,
|
|
||||||
} from '@/data'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
interface ImprovementCardProps {
|
|
||||||
sug: IASugerencia
|
|
||||||
asignaturaId: string
|
|
||||||
onApplied: (campoKey: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ImprovementCard({
|
|
||||||
sug,
|
|
||||||
asignaturaId,
|
|
||||||
onApplied,
|
|
||||||
}: ImprovementCardProps) {
|
|
||||||
const { data: asignatura } = useSubject(asignaturaId)
|
|
||||||
const updateAsignatura = useUpdateAsignatura()
|
|
||||||
const updateRecommendation = useUpdateSubjectRecommendation()
|
|
||||||
|
|
||||||
const [isApplying, setIsApplying] = useState(false)
|
|
||||||
|
|
||||||
const handleApply = async () => {
|
|
||||||
if (!asignatura) return
|
|
||||||
|
|
||||||
setIsApplying(true)
|
|
||||||
try {
|
|
||||||
// 1. Identificar a qué columna debe ir el guardado
|
|
||||||
let patchData = {}
|
|
||||||
|
|
||||||
if (sug.campoKey === 'contenido_tematico') {
|
|
||||||
// Se guarda directamente en la columna contenido_tematico
|
|
||||||
patchData = { contenido_tematico: sug.valorSugerido }
|
|
||||||
} else if (sug.campoKey === 'criterios_de_evaluacion') {
|
|
||||||
// Se guarda directamente en la columna criterios_de_evaluacion
|
|
||||||
patchData = { criterios_de_evaluacion: sug.valorSugerido }
|
|
||||||
} else {
|
|
||||||
// Otros campos (ciclo, fines, etc.) se siguen guardando en el JSON de la columna 'datos'
|
|
||||||
patchData = {
|
|
||||||
datos: {
|
|
||||||
...asignatura.datos,
|
|
||||||
[sug.campoKey]: sug.valorSugerido,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Ejecutar la actualización con la estructura correcta
|
|
||||||
await updateAsignatura.mutateAsync({
|
|
||||||
asignaturaId: asignaturaId as any,
|
|
||||||
patch: patchData as any,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 3. Marcar la recomendación como aplicada
|
|
||||||
await updateRecommendation.mutateAsync({
|
|
||||||
mensajeId: sug.messageId,
|
|
||||||
campoAfectado: sug.campoKey,
|
|
||||||
})
|
|
||||||
console.log(sug.campoKey)
|
|
||||||
|
|
||||||
onApplied(sug.campoKey)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al aplicar mejora:', error)
|
|
||||||
} finally {
|
|
||||||
setIsApplying(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- FUNCIÓN PARA RENDERIZAR EL CONTENIDO DE FORMA SEGURA ---
|
|
||||||
const renderContenido = (valor: any) => {
|
|
||||||
// Si no es un array, es texto simple
|
|
||||||
if (!Array.isArray(valor)) {
|
|
||||||
return <p className="italic">"{String(valor)}"</p>
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CASO 1: CONTENIDO TEMÁTICO (Detectamos si el primer objeto tiene 'unidad') ---
|
|
||||||
if (valor[0]?.hasOwnProperty('unidad')) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{valor.map((u: any, idx: number) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="rounded-md border border-teal-100 bg-white p-2 shadow-sm"
|
|
||||||
>
|
|
||||||
<div className="mb-1 flex items-center gap-2 border-b border-slate-50 pb-1 text-[11px] font-bold text-teal-800">
|
|
||||||
<BookOpen size={12} /> Unidad {u.unidad}: {u.titulo}
|
|
||||||
</div>
|
|
||||||
<ul className="space-y-1">
|
|
||||||
{u.temas?.map((t: any, tidx: number) => (
|
|
||||||
<li
|
|
||||||
key={tidx}
|
|
||||||
className="flex items-start justify-between gap-2 text-[10px] text-slate-600"
|
|
||||||
>
|
|
||||||
<span className="leading-tight">• {t.nombre}</span>
|
|
||||||
<span className="flex shrink-0 items-center gap-0.5 font-mono text-slate-400">
|
|
||||||
<Clock size={10} /> {t.horasEstimadas}h
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CASO 2: CRITERIOS DE EVALUACIÓN (Detectamos si tiene 'criterio') ---
|
|
||||||
if (valor[0]?.hasOwnProperty('criterio')) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="mb-1 flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase">
|
|
||||||
<ListChecks size={12} /> Desglose de evaluación
|
|
||||||
</div>
|
|
||||||
{valor.map((c: any, idx: number) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="flex items-center justify-between gap-3 rounded-md border border-slate-100 bg-white p-2 shadow-sm"
|
|
||||||
>
|
|
||||||
<span className="text-[11px] leading-tight text-slate-700">
|
|
||||||
{c.criterio}
|
|
||||||
</span>
|
|
||||||
<div className="flex shrink-0 items-center gap-1 rounded-full border border-orange-100 bg-orange-50 px-2 py-0.5 text-[10px] font-bold text-orange-600">
|
|
||||||
{c.porcentaje}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{/* Opcional: Suma total para verificar que de 100% */}
|
|
||||||
<div className="pt-1 text-right text-[9px] font-medium text-slate-400">
|
|
||||||
Total:{' '}
|
|
||||||
{valor.reduce(
|
|
||||||
(acc: number, curr: any) => acc + (curr.porcentaje || 0),
|
|
||||||
0,
|
|
||||||
)}
|
|
||||||
%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caso por defecto (Array genérico)
|
|
||||||
return (
|
|
||||||
<pre className="text-[10px]">
|
|
||||||
{/* JSON.stringify(valor, null, 2)*/ 'hola'}
|
|
||||||
</pre>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ESTADO APLICADO ---
|
|
||||||
if (sug.aceptada) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col rounded-xl border border-slate-100 bg-white p-3 opacity-80 shadow-sm">
|
|
||||||
<div className="mb-3 flex items-center justify-between gap-4">
|
|
||||||
<span className="text-sm font-bold text-slate-800">
|
|
||||||
{sug.campoNombre}
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center gap-1.5 rounded-full border border-slate-100 bg-slate-50 px-3 py-1 text-xs font-medium text-slate-400">
|
|
||||||
<Check size={14} />
|
|
||||||
Aplicado
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="rounded-lg border border-teal-100 bg-teal-50/30 p-3 text-xs leading-relaxed text-slate-500">
|
|
||||||
{renderContenido(sug.valorSugerido)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- ESTADO PENDIENTE ---
|
|
||||||
return (
|
|
||||||
<div className="group flex flex-col rounded-xl border border-teal-100 bg-white p-3 shadow-sm transition-all hover:border-teal-200">
|
|
||||||
<div className="mb-3 flex items-center justify-between gap-4">
|
|
||||||
<span className="max-w-[150px] truncate rounded-lg border border-teal-100 bg-teal-50/50 px-2.5 py-1 text-[10px] font-bold tracking-wider text-teal-700 uppercase">
|
|
||||||
{sug.campoNombre}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
disabled={isApplying || !asignatura}
|
|
||||||
className="h-8 w-auto bg-teal-600 px-4 text-xs font-semibold shadow-sm hover:bg-teal-700"
|
|
||||||
onClick={handleApply}
|
|
||||||
>
|
|
||||||
{isApplying ? (
|
|
||||||
<Loader2 size={14} className="mr-1.5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Check size={14} className="mr-1.5" />
|
|
||||||
)}
|
|
||||||
{isApplying ? 'Aplicando...' : 'Aplicar mejora'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'rounded-lg border border-dashed border-slate-200 bg-slate-50/50 p-3 text-xs leading-relaxed text-slate-600',
|
|
||||||
!Array.isArray(sug.valorSugerido) && 'line-clamp-4 italic',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{renderContenido(sug.valorSugerido)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
155
src/components/asignaturas/wizard/PasoBasicosForm.tsx
Normal file
155
src/components/asignaturas/wizard/PasoBasicosForm.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import type {
|
||||||
|
NewSubjectWizardState,
|
||||||
|
TipoAsignatura,
|
||||||
|
} from '@/features/asignaturas/new/types'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import {
|
||||||
|
ESTRUCTURAS_SEP,
|
||||||
|
TIPOS_MATERIA,
|
||||||
|
} from '@/features/asignaturas/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoBasicosForm({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="grid gap-1 sm:col-span-2">
|
||||||
|
<Label htmlFor="nombre">Nombre de la asignatura</Label>
|
||||||
|
<Input
|
||||||
|
id="nombre"
|
||||||
|
placeholder="Ej. Matemáticas Discretas"
|
||||||
|
value={wizard.datosBasicos.nombre}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, nombre: e.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="clave">Clave (Opcional)</Label>
|
||||||
|
<Input
|
||||||
|
id="clave"
|
||||||
|
placeholder="Ej. MAT-101"
|
||||||
|
value={wizard.datosBasicos.clave || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, clave: e.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="tipo">Tipo</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.tipo}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, tipo: val as TipoAsignatura },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="tipo"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TIPOS_MATERIA.map((t) => (
|
||||||
|
<SelectItem key={t.value} value={t.value}>
|
||||||
|
{t.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="creditos">Créditos</Label>
|
||||||
|
<Input
|
||||||
|
id="creditos"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={wizard.datosBasicos.creditos}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
creditos: Number(e.target.value || 0),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="horas">Horas / Semana</Label>
|
||||||
|
<Input
|
||||||
|
id="horas"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={wizard.datosBasicos.horasSemana || 0}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
horasSemana: Number(e.target.value || 0),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1 sm:col-span-2">
|
||||||
|
<Label htmlFor="estructura">Estructura de la asignatura</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.estructuraId}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="estructura"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona plantilla..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{ESTRUCTURAS_SEP.map((e) => (
|
||||||
|
<SelectItem key={e.id} value={e.id}>
|
||||||
|
{e.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
Define los campos requeridos (ej. Objetivos, Temario, Evaluación).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,362 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import PasoSugerenciasForm from './PasoSugerenciasForm'
|
|
||||||
|
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
|
||||||
import type { Database } from '@/types/supabase'
|
|
||||||
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { useSubjectEstructuras } from '@/data'
|
|
||||||
import { TIPOS_MATERIA } from '@/features/asignaturas/nueva/catalogs'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
export function PasoBasicosForm({
|
|
||||||
wizard,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
wizard: NewSubjectWizardState
|
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
|
||||||
}) {
|
|
||||||
const { data: estructuras } = useSubjectEstructuras()
|
|
||||||
|
|
||||||
const [creditosInput, setCreditosInput] = useState<string>(() => {
|
|
||||||
const c = Number(wizard.datosBasicos.creditos ?? 0)
|
|
||||||
let newC = c
|
|
||||||
console.log('antes', newC)
|
|
||||||
|
|
||||||
if (Number.isFinite(c) && c > 999) {
|
|
||||||
newC = 999
|
|
||||||
}
|
|
||||||
console.log('desp', newC)
|
|
||||||
return newC > 0 ? newC.toFixed(2) : ''
|
|
||||||
})
|
|
||||||
const [creditosFocused, setCreditosFocused] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (creditosFocused) return
|
|
||||||
const c = Number(wizard.datosBasicos.creditos ?? 0)
|
|
||||||
let newC = c
|
|
||||||
if (Number.isFinite(c) && c > 999) {
|
|
||||||
newC = 999
|
|
||||||
}
|
|
||||||
setCreditosInput(newC > 0 ? newC.toFixed(2) : '')
|
|
||||||
}, [wizard.datosBasicos.creditos, creditosFocused])
|
|
||||||
|
|
||||||
if (wizard.tipoOrigen !== 'IA_MULTIPLE') {
|
|
||||||
return (
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="grid gap-1 sm:col-span-2">
|
|
||||||
<Label htmlFor="nombre">Nombre de la asignatura</Label>
|
|
||||||
<Input
|
|
||||||
id="nombre"
|
|
||||||
placeholder="Ej. Matemáticas Discretas"
|
|
||||||
maxLength={200}
|
|
||||||
value={wizard.datosBasicos.nombre}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, nombre: e.target.value },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="codigo">
|
|
||||||
Código
|
|
||||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
|
||||||
(Opcional)
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="codigo"
|
|
||||||
placeholder="Ej. MAT-101"
|
|
||||||
maxLength={200}
|
|
||||||
value={wizard.datosBasicos.codigo || ''}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, codigo: e.target.value },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 placeholder:italicplaceholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="tipo">Tipo</Label>
|
|
||||||
<Select
|
|
||||||
value={(wizard.datosBasicos.tipo ?? '') as string}
|
|
||||||
onValueChange={(value: string) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
tipo: value as NewSubjectWizardState['datosBasicos']['tipo'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="tipo"
|
|
||||||
className={cn(
|
|
||||||
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
|
||||||
!wizard.datosBasicos.tipo
|
|
||||||
? 'text-muted-foreground font-normal italic opacity-70'
|
|
||||||
: 'font-medium not-italic',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Ej. Obligatoria" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{TIPOS_MATERIA.map((t) => (
|
|
||||||
<SelectItem key={t.value} value={t.value}>
|
|
||||||
{t.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="creditos">Créditos</Label>
|
|
||||||
<Input
|
|
||||||
id="creditos"
|
|
||||||
type="text"
|
|
||||||
inputMode="decimal"
|
|
||||||
maxLength={6}
|
|
||||||
pattern="^\\d*(?:[.,]\\d{0,2})?$"
|
|
||||||
value={creditosInput}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (['-', 'e', 'E', '+'].includes(e.key)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFocus={() => setCreditosFocused(true)}
|
|
||||||
onBlur={() => {
|
|
||||||
setCreditosFocused(false)
|
|
||||||
|
|
||||||
const raw = creditosInput.trim()
|
|
||||||
if (!raw) {
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalized = raw.replace(',', '.')
|
|
||||||
let asNumber = Number.parseFloat(normalized)
|
|
||||||
if (!Number.isFinite(asNumber) || asNumber <= 0) {
|
|
||||||
setCreditosInput('')
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cap to 999
|
|
||||||
if (asNumber > 999) asNumber = 999
|
|
||||||
|
|
||||||
const fixed = asNumber.toFixed(2)
|
|
||||||
setCreditosInput(fixed)
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, creditos: Number(fixed) },
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const nextRaw = e.target.value
|
|
||||||
if (nextRaw === '') {
|
|
||||||
setCreditosInput('')
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, creditos: 0 },
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!/^\d*(?:[.,]\d{0,2})?$/.test(nextRaw)) return
|
|
||||||
|
|
||||||
// If typed number exceeds 999, cap it immediately (prevents entering >999)
|
|
||||||
const asNumberRaw = Number.parseFloat(nextRaw.replace(',', '.'))
|
|
||||||
if (Number.isFinite(asNumberRaw) && asNumberRaw > 999) {
|
|
||||||
// show capped value to the user
|
|
||||||
const cappedStr = '999.00'
|
|
||||||
setCreditosInput(cappedStr)
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
creditos: 999,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setCreditosInput(nextRaw)
|
|
||||||
|
|
||||||
const asNumber = Number.parseFloat(nextRaw.replace(',', '.'))
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
creditos:
|
|
||||||
Number.isFinite(asNumber) && asNumber > 0 ? asNumber : 0,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
placeholder="Ej. 4.50"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="estructura">Estructura de la asignatura</Label>
|
|
||||||
<Select
|
|
||||||
value={wizard.datosBasicos.estructuraId as string}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="estructura"
|
|
||||||
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Selecciona plantilla..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{estructuras?.map(
|
|
||||||
(
|
|
||||||
e: Database['public']['Tables']['estructuras_asignatura']['Row'],
|
|
||||||
) => (
|
|
||||||
<SelectItem key={e.id} value={e.id}>
|
|
||||||
{e.nombre}
|
|
||||||
</SelectItem>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Define los campos requeridos (ej. Objetivos, Temario, Evaluación).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="horasAcademicas">
|
|
||||||
Horas Académicas
|
|
||||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
|
||||||
(Opcional)
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="horasAcademicas"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={999}
|
|
||||||
step={1}
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={wizard.datosBasicos.horasAcademicas ?? ''}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
horasAcademicas: (() => {
|
|
||||||
const raw = e.target.value
|
|
||||||
if (raw === '') return null
|
|
||||||
const asNumber = Number(raw)
|
|
||||||
if (Number.isNaN(asNumber)) return null
|
|
||||||
// Coerce to positive integer (natural numbers without zero)
|
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
|
||||||
const capped = Math.min(n >= 1 ? n : 1, 999)
|
|
||||||
return capped
|
|
||||||
})(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
placeholder="Ej. 48"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="horasIndependientes">
|
|
||||||
Horas Independientes
|
|
||||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
|
||||||
(Opcional)
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="horasIndependientes"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={999}
|
|
||||||
step={1}
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={wizard.datosBasicos.horasIndependientes ?? ''}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
horasIndependientes: (() => {
|
|
||||||
const raw = e.target.value
|
|
||||||
if (raw === '') return null
|
|
||||||
const asNumber = Number(raw)
|
|
||||||
if (Number.isNaN(asNumber)) return null
|
|
||||||
// Coerce to positive integer (natural numbers without zero)
|
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
|
||||||
const capped = Math.min(n >= 1 ? n : 1, 999)
|
|
||||||
return capped
|
|
||||||
})(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
placeholder="Ej. 24"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <PasoSugerenciasForm wizard={wizard} onChange={onChange} />
|
|
||||||
}
|
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
import { RefreshCw, Sparkles, X } from 'lucide-react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
|
||||||
import type { Dispatch, SetStateAction } from 'react'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
import { generate_subject_suggestions, usePlan } from '@/data'
|
|
||||||
import { AIProgressLoader } from '@/features/asignaturas/nueva/AIProgressLoader'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
export default function PasoSugerenciasForm({
|
|
||||||
wizard,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
wizard: NewSubjectWizardState
|
|
||||||
onChange: Dispatch<SetStateAction<NewSubjectWizardState>>
|
|
||||||
}) {
|
|
||||||
const enfoque = wizard.iaMultiple?.enfoque ?? ''
|
|
||||||
const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 5
|
|
||||||
const isLoading = wizard.iaMultiple?.isLoading ?? false
|
|
||||||
|
|
||||||
const [showConservacionTooltip, setShowConservacionTooltip] = useState(false)
|
|
||||||
|
|
||||||
const setIaMultiple = (
|
|
||||||
patch: Partial<NonNullable<NewSubjectWizardState['iaMultiple']>>,
|
|
||||||
) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
iaMultiple: {
|
|
||||||
enfoque: w.iaMultiple?.enfoque ?? '',
|
|
||||||
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
|
||||||
isLoading: w.iaMultiple?.isLoading ?? false,
|
|
||||||
...patch,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
|
||||||
|
|
||||||
const toggleAsignatura = (id: string, checked: boolean) => {
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
sugerencias: w.sugerencias.map((s) =>
|
|
||||||
s.id === id ? { ...s, selected: checked } : s,
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const onGenerarSugerencias = async () => {
|
|
||||||
const hadNoSugerenciasBefore = wizard.sugerencias.length === 0
|
|
||||||
const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected)
|
|
||||||
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
errorMessage: null,
|
|
||||||
sugerencias: sugerenciasConservadas,
|
|
||||||
iaMultiple: {
|
|
||||||
enfoque: w.iaMultiple?.enfoque ?? '',
|
|
||||||
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
|
||||||
isLoading: true,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
try {
|
|
||||||
const cantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
|
||||||
if (!Number.isFinite(cantidad) || cantidad <= 0 || cantidad > 15) {
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
errorMessage: 'La cantidad de sugerencias debe ser entre 1 y 15.',
|
|
||||||
iaMultiple: {
|
|
||||||
enfoque: w.iaMultiple?.enfoque ?? '',
|
|
||||||
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
|
||||||
isLoading: false,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const enfoqueTrim = wizard.iaMultiple?.enfoque.trim() ?? ''
|
|
||||||
|
|
||||||
const nuevasSugerencias = await generate_subject_suggestions({
|
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
|
||||||
enfoque: enfoqueTrim ? enfoqueTrim : undefined,
|
|
||||||
cantidad_de_sugerencias: cantidad,
|
|
||||||
sugerencias_conservadas: sugerenciasConservadas.map((s) => ({
|
|
||||||
nombre: s.nombre,
|
|
||||||
descripcion: s.descripcion,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (hadNoSugerenciasBefore && nuevasSugerencias.length > 0) {
|
|
||||||
setShowConservacionTooltip(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
sugerencias: [...nuevasSugerencias, ...sugerenciasConservadas],
|
|
||||||
iaMultiple: {
|
|
||||||
enfoque: w.iaMultiple?.enfoque ?? '',
|
|
||||||
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
|
||||||
isLoading: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
const message =
|
|
||||||
err instanceof Error ? err.message : 'Error generando sugerencias.'
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
errorMessage: message,
|
|
||||||
iaMultiple: {
|
|
||||||
enfoque: w.iaMultiple?.enfoque ?? '',
|
|
||||||
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
|
||||||
isLoading: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* --- BLOQUE SUPERIOR: PARÁMETROS --- */}
|
|
||||||
<div className="border-border/60 bg-muted/30 mb-4 rounded-xl border p-4">
|
|
||||||
<div className="mb-3 flex items-center gap-2">
|
|
||||||
<Sparkles className="text-primary h-4 w-4" />
|
|
||||||
<span className="text-sm font-semibold">
|
|
||||||
Parámetros de sugerencia
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="w-full">
|
|
||||||
<Label className="text-muted-foreground mb-1 block text-xs">
|
|
||||||
Enfoque (opcional)
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
placeholder="Ej. Enfocado en normativa mexicana y tecnología"
|
|
||||||
value={enfoque}
|
|
||||||
maxLength={7000}
|
|
||||||
rows={4}
|
|
||||||
onChange={(e) => setIaMultiple({ enfoque: e.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-3 flex w-full flex-col items-end justify-between gap-3 sm:flex-row">
|
|
||||||
<div className="w-full sm:w-44">
|
|
||||||
<Label className="text-muted-foreground mb-1 block text-xs">
|
|
||||||
Cantidad de sugerencias
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Ej. 5"
|
|
||||||
value={cantidadDeSugerencias}
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={15}
|
|
||||||
step={1}
|
|
||||||
inputMode="numeric"
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(e) => {
|
|
||||||
const raw = e.target.value
|
|
||||||
if (raw === '') return
|
|
||||||
const asNumber = Number(raw)
|
|
||||||
if (!Number.isFinite(asNumber)) return
|
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
|
||||||
const capped = Math.min(n >= 1 ? n : 1, 15)
|
|
||||||
setIaMultiple({ cantidadDeSugerencias: capped })
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
className="h-9 gap-1.5"
|
|
||||||
onClick={onGenerarSugerencias}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
|
||||||
{wizard.sugerencias.length > 0
|
|
||||||
? 'Generar más sugerencias'
|
|
||||||
: 'Generar sugerencias'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AIProgressLoader
|
|
||||||
isLoading={isLoading}
|
|
||||||
cantidadDeSugerencias={cantidadDeSugerencias}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* --- HEADER LISTA --- */}
|
|
||||||
<div className="mb-3 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-foreground text-base font-semibold">
|
|
||||||
Asignaturas sugeridas
|
|
||||||
</h3>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
Basadas en el plan{' '}
|
|
||||||
{plan ? `${plan.nivel} en ${plan.nombre}` : '...'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Tooltip open={showConservacionTooltip}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="bg-muted text-foreground inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-sm font-semibold">
|
|
||||||
<span aria-hidden>📌</span>
|
|
||||||
{wizard.sugerencias.filter((s) => s.selected).length}{' '}
|
|
||||||
seleccionadas
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" sideOffset={8} className="max-w-xs">
|
|
||||||
<div className="flex items-start gap-2">
|
|
||||||
<span className="flex-1 text-sm">
|
|
||||||
Al generar más sugerencias, se conservarán las asignaturas
|
|
||||||
seleccionadas.
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-5 w-5"
|
|
||||||
onClick={() => setShowConservacionTooltip(false)}
|
|
||||||
>
|
|
||||||
<X className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* --- LISTA DE ASIGNATURAS --- */}
|
|
||||||
<div className="max-h-100 space-y-1 overflow-y-auto pr-1">
|
|
||||||
{wizard.sugerencias.map((asignatura) => {
|
|
||||||
const isSelected = asignatura.selected
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={asignatura.id}
|
|
||||||
aria-checked={isSelected}
|
|
||||||
className={cn(
|
|
||||||
'border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={isSelected}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
toggleAsignatura(asignatura.id, !!checked)
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring mt-0.5 h-5 w-5 shrink-0 border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
||||||
// isSelected ? '' : 'invisible',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Contenido de la tarjeta */}
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<span className="text-foreground text-sm font-medium">
|
|
||||||
{asignatura.nombre}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Badges de Tipo */}
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
|
||||||
asignatura.tipo === 'OBLIGATORIA'
|
|
||||||
? 'border-blue-200 bg-transparent text-blue-700 dark:border-blue-800 dark:text-blue-300'
|
|
||||||
: 'border-yellow-200 bg-transparent text-yellow-700 dark:border-yellow-800 dark:text-yellow-300',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{asignatura.tipo}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400">
|
|
||||||
{asignatura.creditos} cred. · {asignatura.horasAcademicas}h
|
|
||||||
acad. · {asignatura.horasIndependientes}h indep.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-1 text-sm">
|
|
||||||
{asignatura.descripcion}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
286
src/components/asignaturas/wizard/PasoConfiguracionPanel.tsx
Normal file
286
src/components/asignaturas/wizard/PasoConfiguracionPanel.tsx
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import type { NewSubjectWizardState } from '@/features/asignaturas/new/types'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import {
|
||||||
|
ARCHIVOS_SISTEMA_MOCK,
|
||||||
|
FACULTADES,
|
||||||
|
MATERIAS_MOCK,
|
||||||
|
PLANES_MOCK,
|
||||||
|
} from '@/features/asignaturas/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoConfiguracionPanel({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
onGenerarIA,
|
||||||
|
}: {
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
|
onGenerarIA: () => void
|
||||||
|
}) {
|
||||||
|
if (wizard.modoCreacion === 'MANUAL') {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Configuración Manual</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
La asignatura se creará vacía. Podrás editar el contenido detallado
|
||||||
|
en la siguiente pantalla.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.modoCreacion === 'IA') {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label>Descripción del enfoque</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Ej. Asignatura teórica-práctica enfocada en patrones de diseño..."
|
||||||
|
value={wizard.iaConfig?.descripcionEnfoque}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...w.iaConfig!,
|
||||||
|
descripcionEnfoque: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="min-h-25"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label>Notas adicionales</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Restricciones, bibliografía sugerida, etc."
|
||||||
|
value={wizard.iaConfig?.notasAdicionales}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: { ...w.iaConfig!, notasAdicionales: e.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Archivos de contexto (Opcional)</Label>
|
||||||
|
<div className="flex flex-col gap-2 rounded-md border p-3">
|
||||||
|
{ARCHIVOS_SISTEMA_MOCK.map((file) => (
|
||||||
|
<div key={file.id} className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={file.id}
|
||||||
|
checked={wizard.iaConfig?.archivosExistentesIds.includes(
|
||||||
|
file.id,
|
||||||
|
)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const checked = e.target.checked
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...w.iaConfig!,
|
||||||
|
archivosExistentesIds: checked
|
||||||
|
? [
|
||||||
|
...(w.iaConfig?.archivosExistentesIds || []),
|
||||||
|
file.id,
|
||||||
|
]
|
||||||
|
: w.iaConfig?.archivosExistentesIds.filter(
|
||||||
|
(id) => id !== file.id,
|
||||||
|
) || [],
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={file.id} className="font-normal">
|
||||||
|
{file.name}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={onGenerarIA} disabled={wizard.isLoading}>
|
||||||
|
{wizard.isLoading ? (
|
||||||
|
<>
|
||||||
|
<Icons.Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Generando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icons.Sparkles className="mr-2 h-4 w-4" /> Generar Preview
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{wizard.resumen.previewAsignatura && (
|
||||||
|
<Card className="bg-muted/50 border-dashed">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Vista previa generada</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-muted-foreground text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>Objetivo:</strong>{' '}
|
||||||
|
{wizard.resumen.previewAsignatura.objetivo}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2">
|
||||||
|
Se detectaron {wizard.resumen.previewAsignatura.unidades}{' '}
|
||||||
|
unidades temáticas y{' '}
|
||||||
|
{wizard.resumen.previewAsignatura.bibliografiaCount} fuentes
|
||||||
|
bibliográficas.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.subModoClonado === 'INTERNO') {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-2 sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<Label>Facultad</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonInterno: { ...w.clonInterno, facultadId: val },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Todas" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FACULTADES.map((f) => (
|
||||||
|
<SelectItem key={f.id} value={f.id}>
|
||||||
|
{f.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Plan</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonInterno: { ...w.clonInterno, planOrigenId: val },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Todos" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PLANES_MOCK.map((p) => (
|
||||||
|
<SelectItem key={p.id} value={p.id}>
|
||||||
|
{p.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Buscar</Label>
|
||||||
|
<Input placeholder="Nombre..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid max-h-75 gap-2 overflow-y-auto">
|
||||||
|
{MATERIAS_MOCK.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonInterno: { ...w.clonInterno, asignaturaOrigenId: m.id },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className={`hover:bg-accent flex cursor-pointer items-center justify-between rounded-md border p-3 ${
|
||||||
|
wizard.clonInterno?.asignaturaOrigenId === m.id
|
||||||
|
? 'border-primary bg-primary/5 ring-primary ring-1'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{m.nombre}</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{m.clave} • {m.creditos} créditos
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{wizard.clonInterno?.asignaturaOrigenId === m.id && (
|
||||||
|
<Icons.CheckCircle2 className="text-primary h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.subModoClonado === 'TRADICIONAL') {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="rounded-lg border border-dashed p-8 text-center">
|
||||||
|
<Icons.Upload className="text-muted-foreground mx-auto mb-4 h-10 w-10" />
|
||||||
|
<h3 className="mb-1 text-sm font-medium">
|
||||||
|
Sube el Word de la asignatura
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mb-4 text-xs">
|
||||||
|
Arrastra el archivo o haz clic para buscar (.doc, .docx)
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept=".doc,.docx"
|
||||||
|
className="mx-auto max-w-xs"
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonTradicional: {
|
||||||
|
...w.clonTradicional!,
|
||||||
|
archivoWordAsignaturaId:
|
||||||
|
e.target.files?.[0]?.name || 'mock_file',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{wizard.clonTradicional?.archivoWordAsignaturaId && (
|
||||||
|
<div className="flex items-center gap-2 rounded-md bg-green-50 p-3 text-sm text-green-700">
|
||||||
|
<Icons.FileText className="h-4 w-4" />
|
||||||
|
Archivo cargado listo para procesar.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,459 +0,0 @@
|
|||||||
import * as Icons from 'lucide-react'
|
|
||||||
|
|
||||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
|
||||||
|
|
||||||
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from '@/components/ui/accordion'
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
|
||||||
import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data'
|
|
||||||
import {
|
|
||||||
FACULTADES,
|
|
||||||
MATERIAS_MOCK,
|
|
||||||
PLANES_MOCK,
|
|
||||||
} from '@/features/asignaturas/nueva/catalogs'
|
|
||||||
|
|
||||||
export function PasoDetallesPanel({
|
|
||||||
wizard,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
wizard: NewSubjectWizardState
|
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
|
||||||
}) {
|
|
||||||
const { data: estructurasAsignatura } = useSubjectEstructuras()
|
|
||||||
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
|
||||||
const { data: lineasPlan } = usePlanLineas(wizard.plan_estudio_id)
|
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'MANUAL') {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Configuración Manual</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
La asignatura se creará vacía. Podrás editar el contenido detallado
|
|
||||||
en la siguiente pantalla.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Label>Descripción del enfoque académico</Label>
|
|
||||||
<Textarea
|
|
||||||
placeholder="Describe el enfoque, alcance y público objetivo. Ej.: Teórica-práctica enfocada en patrones de diseño, con proyectos semanales..."
|
|
||||||
maxLength={7000}
|
|
||||||
value={wizard.iaConfig?.descripcionEnfoqueAcademico}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
iaConfig: {
|
|
||||||
...w.iaConfig!,
|
|
||||||
descripcionEnfoqueAcademico: e.target.value,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 min-h-25 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<Label>
|
|
||||||
Instrucciones adicionales para la IA
|
|
||||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
|
||||||
(Opcional)
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
placeholder="Opcional: restricciones y preferencias. Ej.: incluye bibliografía en español, evita contenido avanzado, prioriza evaluación por proyectos..."
|
|
||||||
maxLength={7000}
|
|
||||||
value={wizard.iaConfig?.instruccionesAdicionalesIA}
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
iaConfig: {
|
|
||||||
...w.iaConfig!,
|
|
||||||
instruccionesAdicionalesIA: e.target.value,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ReferenciasParaIA
|
|
||||||
selectedArchivoIds={wizard.iaConfig?.archivosReferencia || []}
|
|
||||||
selectedRepositorioIds={wizard.iaConfig?.repositoriosReferencia || []}
|
|
||||||
uploadedFiles={wizard.iaConfig?.archivosAdjuntos || []}
|
|
||||||
onToggleArchivo={(id, checked) =>
|
|
||||||
onChange((w): NewSubjectWizardState => {
|
|
||||||
const prev = w.iaConfig?.archivosReferencia || []
|
|
||||||
const next = checked
|
|
||||||
? [...prev, id]
|
|
||||||
: prev.filter((a) => a !== id)
|
|
||||||
return {
|
|
||||||
...w,
|
|
||||||
iaConfig: {
|
|
||||||
...w.iaConfig!,
|
|
||||||
archivosReferencia: next,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onToggleRepositorio={(id, checked) =>
|
|
||||||
onChange((w): NewSubjectWizardState => {
|
|
||||||
const prev = w.iaConfig?.repositoriosReferencia || []
|
|
||||||
const next = checked
|
|
||||||
? [...prev, id]
|
|
||||||
: prev.filter((r) => r !== id)
|
|
||||||
return {
|
|
||||||
...w,
|
|
||||||
iaConfig: {
|
|
||||||
...w.iaConfig!,
|
|
||||||
repositoriosReferencia: next,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onFilesChange={(files: Array<UploadedFile>) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
iaConfig: {
|
|
||||||
...w.iaConfig!,
|
|
||||||
archivosAdjuntos: files,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'IA_MULTIPLE') {
|
|
||||||
const maxCiclos = Math.max(1, plan?.numero_ciclos ?? 1)
|
|
||||||
const sugerenciasSeleccionadas = wizard.sugerencias.filter(
|
|
||||||
(s) => s.selected,
|
|
||||||
)
|
|
||||||
|
|
||||||
const patchSugerencia = (
|
|
||||||
id: string,
|
|
||||||
patch: Partial<NewSubjectWizardState['sugerencias'][number]>,
|
|
||||||
) =>
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
sugerencias: w.sugerencias.map((s) =>
|
|
||||||
s.id === id ? { ...s, ...patch } : s,
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<div className="border-border/60 bg-muted/30 rounded-xl border p-4">
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label className="text-muted-foreground text-xs">
|
|
||||||
Estructura de la asignatura
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={wizard.datosBasicos.estructuraId ?? undefined}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
estructuraId: val,
|
|
||||||
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Selecciona una estructura" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{(estructurasAsignatura ?? []).map((e) => (
|
|
||||||
<SelectItem key={e.id} value={e.id}>
|
|
||||||
{e.nombre}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-border/60 bg-muted/30 rounded-xl border p-4">
|
|
||||||
<h3 className="text-foreground mx-3 mb-2 text-lg font-semibold">
|
|
||||||
Materias seleccionadas
|
|
||||||
</h3>
|
|
||||||
{sugerenciasSeleccionadas.length === 0 ? (
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
Selecciona al menos una sugerencia para configurar su descripción,
|
|
||||||
línea curricular y ciclo.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Accordion type="multiple" className="w-full space-y-2">
|
|
||||||
{sugerenciasSeleccionadas.map((asig) => (
|
|
||||||
<AccordionItem
|
|
||||||
key={asig.id}
|
|
||||||
value={asig.id}
|
|
||||||
className="border-border/60 bg-background/40 rounded-lg border border-b-0 px-3"
|
|
||||||
>
|
|
||||||
<AccordionTrigger className="hover:bg-accent/30 data-[state=open]:bg-accent/20 data-[state=open]:text-accent-foreground -mx-3 px-3">
|
|
||||||
{asig.nombre}
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="text-muted-foreground">
|
|
||||||
<div className="mx-1 grid gap-3 sm:grid-cols-2">
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label className="text-muted-foreground text-xs">
|
|
||||||
Descripción
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
value={asig.descripcion}
|
|
||||||
maxLength={7000}
|
|
||||||
rows={6}
|
|
||||||
onChange={(e) =>
|
|
||||||
patchSugerencia(asig.id, {
|
|
||||||
descripcion: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid content-start gap-3">
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label className="text-muted-foreground text-xs">
|
|
||||||
Ciclo (opcional)
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={maxCiclos}
|
|
||||||
step={1}
|
|
||||||
inputMode="numeric"
|
|
||||||
placeholder={`1-${maxCiclos}`}
|
|
||||||
value={asig.numero_ciclo ?? ''}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (
|
|
||||||
['.', ',', '-', 'e', 'E', '+'].includes(e.key)
|
|
||||||
) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(e) => {
|
|
||||||
const raw = e.target.value
|
|
||||||
if (raw === '') {
|
|
||||||
patchSugerencia(asig.id, { numero_ciclo: null })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const asNumber = Number(raw)
|
|
||||||
if (!Number.isFinite(asNumber)) return
|
|
||||||
|
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
|
||||||
const capped = Math.min(
|
|
||||||
Math.max(n >= 1 ? n : 1, 1),
|
|
||||||
maxCiclos,
|
|
||||||
)
|
|
||||||
|
|
||||||
patchSugerencia(asig.id, { numero_ciclo: capped })
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label className="text-muted-foreground text-xs">
|
|
||||||
Línea curricular (opcional)
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={asig.linea_plan_id ?? '__none__'}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
patchSugerencia(asig.id, {
|
|
||||||
linea_plan_id: val === '__none__' ? null : val,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Sin línea" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="__none__">Ninguna</SelectItem>
|
|
||||||
{(lineasPlan ?? []).map((l) => (
|
|
||||||
<SelectItem key={l.id} value={l.id}>
|
|
||||||
{l.nombre}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
|
||||||
return (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="grid gap-2 sm:grid-cols-3">
|
|
||||||
<div>
|
|
||||||
<Label>Facultad</Label>
|
|
||||||
<Select
|
|
||||||
onValueChange={(val) =>
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
clonInterno: { ...w.clonInterno, facultadId: val },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Todas" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{FACULTADES.map((f) => (
|
|
||||||
<SelectItem key={f.id} value={f.id}>
|
|
||||||
{f.nombre}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>Plan</Label>
|
|
||||||
<Select
|
|
||||||
onValueChange={(val) =>
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
clonInterno: { ...w.clonInterno, planOrigenId: val },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Todos" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{PLANES_MOCK.map((p) => (
|
|
||||||
<SelectItem key={p.id} value={p.id}>
|
|
||||||
{p.nombre}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>Buscar</Label>
|
|
||||||
<Input placeholder="Nombre..." />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid max-h-75 gap-2 overflow-y-auto">
|
|
||||||
{MATERIAS_MOCK.map((m) => (
|
|
||||||
<div
|
|
||||||
key={m.id}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() =>
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
clonInterno: { ...w.clonInterno, asignaturaOrigenId: m.id },
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key !== 'Enter' && e.key !== ' ') return
|
|
||||||
e.preventDefault()
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
clonInterno: { ...w.clonInterno, asignaturaOrigenId: m.id },
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
className={`hover:bg-accent flex cursor-pointer items-center justify-between rounded-md border p-3 ${
|
|
||||||
wizard.clonInterno?.asignaturaOrigenId === m.id
|
|
||||||
? 'border-primary bg-primary/5 ring-primary ring-1'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">{m.nombre}</div>
|
|
||||||
<div className="text-muted-foreground text-xs">
|
|
||||||
{m.clave} • {m.creditos} créditos
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{wizard.clonInterno?.asignaturaOrigenId === m.id && (
|
|
||||||
<Icons.CheckCircle2 className="text-primary h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
|
||||||
return (
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<div className="rounded-lg border border-dashed p-8 text-center">
|
|
||||||
<Icons.Upload className="text-muted-foreground mx-auto mb-4 h-10 w-10" />
|
|
||||||
<h3 className="mb-1 text-sm font-medium">
|
|
||||||
Sube el Word de la asignatura
|
|
||||||
</h3>
|
|
||||||
<p className="text-muted-foreground mb-4 text-xs">
|
|
||||||
Arrastra el archivo o haz clic para buscar (.doc, .docx)
|
|
||||||
</p>
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
accept=".doc,.docx"
|
|
||||||
className="mx-auto max-w-xs"
|
|
||||||
onChange={(e) =>
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
clonTradicional: {
|
|
||||||
...w.clonTradicional!,
|
|
||||||
archivoWordAsignaturaId:
|
|
||||||
e.target.files?.[0]?.name || 'mock_file',
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{wizard.clonTradicional?.archivoWordAsignaturaId && (
|
|
||||||
<div className="flex items-center gap-2 rounded-md bg-green-50 p-3 text-sm text-green-700">
|
|
||||||
<Icons.FileText className="h-4 w-4" />
|
|
||||||
Archivo cargado listo para procesar.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
import type {
|
||||||
|
ModoCreacion,
|
||||||
|
NewSubjectWizardState,
|
||||||
|
SubModoClonado,
|
||||||
|
} from '@/features/asignaturas/new/types'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -17,33 +21,19 @@ export function PasoMetodoCardGroup({
|
|||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
}) {
|
}) {
|
||||||
const isSelected = (modo: NewSubjectWizardState['tipoOrigen']) =>
|
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
|
||||||
wizard.tipoOrigen === modo
|
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
|
||||||
const handleKeyActivate = (e: React.KeyboardEvent, cb: () => void) => {
|
|
||||||
const key = e.key
|
|
||||||
if (
|
|
||||||
key === 'Enter' ||
|
|
||||||
key === ' ' ||
|
|
||||||
key === 'Spacebar' ||
|
|
||||||
key === 'Space'
|
|
||||||
) {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
<Card
|
<Card
|
||||||
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange(
|
onChange((w) => ({
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
...w,
|
||||||
tipoOrigen: 'MANUAL',
|
modoCreacion: 'MANUAL',
|
||||||
}),
|
subModoClonado: undefined,
|
||||||
)
|
}))
|
||||||
}
|
}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -61,12 +51,11 @@ export function PasoMetodoCardGroup({
|
|||||||
<Card
|
<Card
|
||||||
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange(
|
onChange((w) => ({
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
...w,
|
||||||
tipoOrigen: 'IA',
|
modoCreacion: 'IA',
|
||||||
}),
|
subModoClonado: undefined,
|
||||||
)
|
}))
|
||||||
}
|
}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -77,94 +66,11 @@ export function PasoMetodoCardGroup({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Generar contenido automático.</CardDescription>
|
<CardDescription>Generar contenido automático.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(wizard.tipoOrigen === 'IA' ||
|
|
||||||
wizard.tipoOrigen === 'IA_SIMPLE' ||
|
|
||||||
wizard.tipoOrigen === 'IA_MULTIPLE') && (
|
|
||||||
<CardContent className="flex flex-col gap-3">
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'IA_SIMPLE',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
onKeyDown={(e: React.KeyboardEvent) =>
|
|
||||||
handleKeyActivate(e, () =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'IA_SIMPLE',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
|
||||||
isSelected('IA_SIMPLE')
|
|
||||||
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
|
||||||
: 'border-border text-muted-foreground'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icons.Edit3 className="h-6 w-6 flex-none" />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-sm font-medium">Una asignatura</span>
|
|
||||||
<span className="text-xs opacity-70">
|
|
||||||
Crear una asignatura con control detallado de metadatos.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'IA_MULTIPLE',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
onKeyDown={(e: React.KeyboardEvent) =>
|
|
||||||
handleKeyActivate(e, () =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'IA_MULTIPLE',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
|
||||||
isSelected('IA_MULTIPLE')
|
|
||||||
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
|
||||||
: 'border-border text-muted-foreground'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Icons.List className="h-6 w-6 flex-none" />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-sm font-medium">Varias asignaturas</span>
|
|
||||||
<span className="text-xs opacity-70">
|
|
||||||
Generar varias asignaturas a partir de sugerencias de la IA.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'CLONADO' }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@@ -174,34 +80,18 @@ export function PasoMetodoCardGroup({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(wizard.tipoOrigen === 'CLONADO' ||
|
{wizard.modoCreacion === 'CLONADO' && (
|
||||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
<CardContent>
|
||||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
<div className="flex flex-col gap-3">
|
||||||
<CardContent className="flex flex-col gap-3">
|
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onChange(
|
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' }))
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'CLONADO_INTERNO',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e: React.KeyboardEvent) =>
|
|
||||||
handleKeyActivate(e, () =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'CLONADO_INTERNO',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||||
isSelected('CLONADO_INTERNO')
|
isSubSelected('INTERNO')
|
||||||
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
: 'border-border text-muted-foreground'
|
: 'border-border text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -220,25 +110,10 @@ export function PasoMetodoCardGroup({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onChange(
|
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' }))
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'CLONADO_TRADICIONAL',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e: React.KeyboardEvent) =>
|
|
||||||
handleKeyActivate(e, () =>
|
|
||||||
onChange(
|
|
||||||
(w): NewSubjectWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'CLONADO_TRADICIONAL',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||||
isSelected('CLONADO_TRADICIONAL')
|
isSubSelected('TRADICIONAL')
|
||||||
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
: 'border-border text-muted-foreground'
|
: 'border-border text-muted-foreground'
|
||||||
}`}
|
}`}
|
||||||
@@ -246,7 +121,10 @@ export function PasoMetodoCardGroup({
|
|||||||
<Icons.Upload className="h-6 w-6 flex-none" />
|
<Icons.Upload className="h-6 w-6 flex-none" />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-medium">Desde archivos</span>
|
<span className="text-sm font-medium">Desde archivos</span>
|
||||||
<span className="text-xs opacity-70">Subir Word existente</span>
|
<span className="text-xs opacity-70">
|
||||||
|
Subir Word existente
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
import type { NewSubjectWizardState } from '@/features/asignaturas/new/types'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -9,45 +9,9 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data'
|
import { ESTRUCTURAS_SEP } from '@/features/asignaturas/new/catalogs'
|
||||||
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
|
||||||
|
|
||||||
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||||
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
|
||||||
const { data: estructuras } = useSubjectEstructuras()
|
|
||||||
const { data: lineasPlan } = usePlanLineas(wizard.plan_estudio_id)
|
|
||||||
|
|
||||||
const estructuraNombre = (() => {
|
|
||||||
const estructuraId = wizard.datosBasicos.estructuraId
|
|
||||||
if (!estructuraId) return '—'
|
|
||||||
const hit = estructuras?.find((e) => e.id === estructuraId)
|
|
||||||
return hit?.nombre ?? estructuraId
|
|
||||||
})()
|
|
||||||
|
|
||||||
const modoLabel = (() => {
|
|
||||||
if (wizard.tipoOrigen === 'MANUAL') return 'Manual (Vacía)'
|
|
||||||
if (wizard.tipoOrigen === 'IA') return 'Generada con IA'
|
|
||||||
if (wizard.tipoOrigen === 'IA_SIMPLE') return 'Generada con IA (Simple)'
|
|
||||||
if (wizard.tipoOrigen === 'IA_MULTIPLE') return 'Generación múltiple (IA)'
|
|
||||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') return 'Clonada (Sistema)'
|
|
||||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') return 'Clonada (Archivo)'
|
|
||||||
return '—'
|
|
||||||
})()
|
|
||||||
|
|
||||||
const creditosText =
|
|
||||||
typeof wizard.datosBasicos.creditos === 'number' &&
|
|
||||||
Number.isFinite(wizard.datosBasicos.creditos)
|
|
||||||
? wizard.datosBasicos.creditos.toFixed(2)
|
|
||||||
: '—'
|
|
||||||
|
|
||||||
const archivosRef = wizard.iaConfig?.archivosReferencia ?? []
|
|
||||||
const repositoriosRef = wizard.iaConfig?.repositoriosReferencia ?? []
|
|
||||||
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
|
||||||
|
|
||||||
const materiasSeleccionadas = wizard.sugerencias.filter((s) => s.selected)
|
|
||||||
const iaMultipleEnfoque = wizard.iaMultiple?.enfoque.trim() ?? ''
|
|
||||||
const iaMultipleCantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -56,238 +20,54 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
Verifica los datos antes de crear la asignatura.
|
Verifica los datos antes de crear la asignatura.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="grid gap-4 text-sm">
|
||||||
<div className="grid gap-4 text-sm">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="grid gap-2">
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Plan de estudios: </span>
|
<span className="text-muted-foreground">Nombre:</span>
|
||||||
<span className="font-medium">
|
<div className="font-medium">{wizard.datosBasicos.nombre}</div>
|
||||||
{plan?.nombre || wizard.plan_estudio_id || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{plan?.carreras?.nombre ? (
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Carrera: </span>
|
<span className="text-muted-foreground">Tipo:</span>
|
||||||
<span className="font-medium">{plan.carreras.nombre}</span>
|
<div className="font-medium">{wizard.datosBasicos.tipo}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Créditos:</span>
|
||||||
|
<div className="font-medium">{wizard.datosBasicos.creditos}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Estructura:</span>
|
||||||
|
<div className="font-medium">
|
||||||
|
{
|
||||||
|
ESTRUCTURAS_SEP.find(
|
||||||
|
(e) => e.id === wizard.datosBasicos.estructuraId,
|
||||||
|
)?.label
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-muted rounded-md p-3">
|
<div className="bg-muted rounded-md p-3">
|
||||||
<span className="text-muted-foreground">Tipo de origen: </span>
|
<span className="text-muted-foreground">Modo de creación:</span>
|
||||||
<span className="inline-flex items-center gap-2 font-medium">
|
<div className="flex items-center gap-2 font-medium">
|
||||||
{wizard.tipoOrigen === 'MANUAL' && (
|
{wizard.modoCreacion === 'MANUAL' && (
|
||||||
<Icons.Pencil className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{(wizard.tipoOrigen === 'IA' ||
|
|
||||||
wizard.tipoOrigen === 'IA_SIMPLE' ||
|
|
||||||
wizard.tipoOrigen === 'IA_MULTIPLE') && (
|
|
||||||
<Icons.Sparkles className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{(wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
|
||||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
|
||||||
<Icons.Copy className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{modoLabel}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{wizard.tipoOrigen === 'IA_MULTIPLE' ? (
|
|
||||||
<>
|
<>
|
||||||
<div className="border-border/60 bg-muted/30 grid gap-3 rounded-xl border p-4">
|
<Icons.Pencil className="h-4 w-4" /> Manual (Vacía)
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="text-foreground text-base font-semibold">
|
|
||||||
Configuración
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-xs">
|
|
||||||
Se crearán {materiasSeleccionadas.length} asignatura(s) a
|
|
||||||
partir de tus selecciones.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-background/40 border-border/60 rounded-lg border p-3">
|
|
||||||
<div className="text-muted-foreground text-xs">
|
|
||||||
Estructura
|
|
||||||
</div>
|
|
||||||
<div className="text-foreground mt-1 text-sm font-medium">
|
|
||||||
{estructuraNombre}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-border/60 bg-muted/30 grid gap-3 rounded-xl border p-4">
|
|
||||||
<div className="flex items-end justify-between gap-2">
|
|
||||||
<div className="text-foreground text-base font-semibold">
|
|
||||||
Materias seleccionadas
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground text-xs">
|
|
||||||
{materiasSeleccionadas.length} en total
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{materiasSeleccionadas.length === 0 ? (
|
|
||||||
<div className="text-muted-foreground text-sm">
|
|
||||||
No hay materias seleccionadas.
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="grid gap-3">
|
|
||||||
{materiasSeleccionadas.map((m) => {
|
|
||||||
const lineaNombre = m.linea_plan_id
|
|
||||||
? (lineasPlan?.find((l) => l.id === m.linea_plan_id)
|
|
||||||
?.nombre ?? m.linea_plan_id)
|
|
||||||
: '—'
|
|
||||||
|
|
||||||
const cicloText =
|
|
||||||
typeof m.numero_ciclo === 'number' &&
|
|
||||||
Number.isFinite(m.numero_ciclo)
|
|
||||||
? String(m.numero_ciclo)
|
|
||||||
: '—'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={m.id}
|
|
||||||
className="bg-background/40 border-border/60 grid gap-2 rounded-lg border p-3"
|
|
||||||
>
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
||||||
<div className="text-foreground text-sm font-semibold">
|
|
||||||
{m.nombre}
|
|
||||||
</div>
|
|
||||||
<div className="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
|
|
||||||
<span className="bg-accent/30 text-accent-foreground rounded-full px-2 py-0.5">
|
|
||||||
Línea: {lineaNombre}
|
|
||||||
</span>
|
|
||||||
<span className="bg-accent/30 text-accent-foreground rounded-full px-2 py-0.5">
|
|
||||||
Ciclo: {cicloText}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-muted-foreground text-sm whitespace-pre-wrap">
|
|
||||||
{m.descripcion || '—'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="col-span-2">
|
|
||||||
<span className="text-muted-foreground">Nombre: </span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.datosBasicos.nombre || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Código: </span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.datosBasicos.codigo || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Tipo: </span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.datosBasicos.tipo || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Créditos: </span>
|
|
||||||
<span className="font-medium">{creditosText}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Estructura: </span>
|
|
||||||
<span className="font-medium">{estructuraNombre}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Horas académicas:{' '}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.datosBasicos.horasAcademicas ?? '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Horas independientes:{' '}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.datosBasicos.horasIndependientes ?? '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-muted/50 rounded-md p-3">
|
|
||||||
<div className="font-medium">Configuración IA</div>
|
|
||||||
<div className="mt-2 grid gap-2">
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Enfoque académico:{' '}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.iaConfig?.descripcionEnfoqueAcademico || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Instrucciones adicionales:{' '}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.iaConfig?.instruccionesAdicionalesIA || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="font-medium">Archivos de referencia</div>
|
|
||||||
{archivosRef.length ? (
|
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
|
||||||
{archivosRef.map((id) => (
|
|
||||||
<li key={id}>{id}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<div className="text-muted-foreground text-xs">—</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">
|
|
||||||
Repositorios de referencia
|
|
||||||
</div>
|
|
||||||
{repositoriosRef.length ? (
|
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
|
||||||
{repositoriosRef.map((id) => (
|
|
||||||
<li key={id}>{id}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<div className="text-muted-foreground text-xs">—</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">Archivos adjuntos</div>
|
|
||||||
{adjuntos.length ? (
|
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
|
||||||
{adjuntos.map((f) => (
|
|
||||||
<li key={f.id}>
|
|
||||||
<span className="text-foreground">
|
|
||||||
{f.file.name}
|
|
||||||
</span>{' '}
|
|
||||||
<span>· {formatFileSize(f.file.size)}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : (
|
|
||||||
<div className="text-muted-foreground text-xs">—</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{wizard.modoCreacion === 'IA' && (
|
||||||
|
<>
|
||||||
|
<Icons.Sparkles className="h-4 w-4" /> Generada con IA
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{wizard.modoCreacion === 'CLONADO' && (
|
||||||
|
<>
|
||||||
|
<Icons.Copy className="h-4 w-4" /> Clonada
|
||||||
|
{wizard.subModoClonado === 'INTERNO'
|
||||||
|
? ' (Sistema)'
|
||||||
|
: ' (Archivo)'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,477 +1,66 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query'
|
import type { NewSubjectWizardState } from '@/features/asignaturas/new/types'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
|
||||||
import { Loader2 } from 'lucide-react'
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
import type { AISubjectUnifiedInput } from '@/data'
|
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
|
||||||
import type { TablesInsert } from '@/types/supabase'
|
|
||||||
import type { RealtimeChannel } from '@supabase/supabase-js'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
|
||||||
supabaseBrowser,
|
|
||||||
useGenerateSubjectAI,
|
|
||||||
qk,
|
|
||||||
useCreateSubjectManual,
|
|
||||||
subjects_get_maybe,
|
|
||||||
} from '@/data'
|
|
||||||
|
|
||||||
export function WizardControls({
|
export function WizardControls({
|
||||||
|
Wizard,
|
||||||
|
methods,
|
||||||
wizard,
|
wizard,
|
||||||
setWizard,
|
canContinueDesdeMetodo,
|
||||||
errorMessage,
|
canContinueDesdeBasicos,
|
||||||
onPrev,
|
canContinueDesdeConfig,
|
||||||
onNext,
|
onCreate,
|
||||||
disablePrev,
|
|
||||||
disableNext,
|
|
||||||
disableCreate,
|
|
||||||
isLastStep,
|
|
||||||
}: {
|
}: {
|
||||||
|
Wizard: any
|
||||||
|
methods: any
|
||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
setWizard: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
canContinueDesdeMetodo: boolean
|
||||||
errorMessage?: string | null
|
canContinueDesdeBasicos: boolean
|
||||||
onPrev: () => void
|
canContinueDesdeConfig: boolean
|
||||||
onNext: () => void
|
onCreate: () => void
|
||||||
disablePrev: boolean
|
|
||||||
disableNext: boolean
|
|
||||||
disableCreate: boolean
|
|
||||||
isLastStep: boolean
|
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
const idx = Wizard.utils.getIndex(methods.current.id)
|
||||||
const qc = useQueryClient()
|
const isLast = idx >= Wizard.steps.length - 1
|
||||||
const generateSubjectAI = useGenerateSubjectAI()
|
|
||||||
const createSubjectManual = useCreateSubjectManual()
|
|
||||||
const [isSpinningIA, setIsSpinningIA] = useState(false)
|
|
||||||
const cancelledRef = useRef(false)
|
|
||||||
const realtimeChannelRef = useRef<RealtimeChannel | null>(null)
|
|
||||||
const watchSubjectIdRef = useRef<string | null>(null)
|
|
||||||
const watchTimeoutRef = useRef<number | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
cancelledRef.current = false
|
|
||||||
return () => {
|
|
||||||
cancelledRef.current = true
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const stopSubjectWatch = useCallback(() => {
|
|
||||||
if (watchTimeoutRef.current) {
|
|
||||||
window.clearTimeout(watchTimeoutRef.current)
|
|
||||||
watchTimeoutRef.current = null
|
|
||||||
}
|
|
||||||
|
|
||||||
watchSubjectIdRef.current = null
|
|
||||||
|
|
||||||
const ch = realtimeChannelRef.current
|
|
||||||
if (ch) {
|
|
||||||
realtimeChannelRef.current = null
|
|
||||||
try {
|
|
||||||
supabaseBrowser().removeChannel(ch)
|
|
||||||
} catch {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
stopSubjectWatch()
|
|
||||||
}
|
|
||||||
}, [stopSubjectWatch])
|
|
||||||
|
|
||||||
const handleSubjectReady = (args: {
|
|
||||||
id: string
|
|
||||||
plan_estudio_id: string
|
|
||||||
estado?: unknown
|
|
||||||
}) => {
|
|
||||||
if (cancelledRef.current) return
|
|
||||||
|
|
||||||
const estado = String(args.estado ?? '').toLowerCase()
|
|
||||||
if (estado === 'generando') return
|
|
||||||
|
|
||||||
stopSubjectWatch()
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
setWizard((w) => ({ ...w, isLoading: false }))
|
|
||||||
|
|
||||||
navigate({
|
|
||||||
to: `/planes/${args.plan_estudio_id}/asignaturas/${args.id}`,
|
|
||||||
state: { showConfetti: true },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const beginSubjectWatch = (args: { subjectId: string; planId: string }) => {
|
|
||||||
stopSubjectWatch()
|
|
||||||
|
|
||||||
watchSubjectIdRef.current = args.subjectId
|
|
||||||
|
|
||||||
// Timeout de seguridad (mismo límite que teníamos con polling)
|
|
||||||
watchTimeoutRef.current = window.setTimeout(
|
|
||||||
() => {
|
|
||||||
if (cancelledRef.current) return
|
|
||||||
if (watchSubjectIdRef.current !== args.subjectId) return
|
|
||||||
|
|
||||||
stopSubjectWatch()
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
setWizard((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage:
|
|
||||||
'La generación está tardando demasiado. Intenta de nuevo en unos minutos.',
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
6 * 60 * 1000,
|
|
||||||
)
|
|
||||||
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const channel = supabase.channel(`asignaturas-status-${args.subjectId}`)
|
|
||||||
realtimeChannelRef.current = channel
|
|
||||||
|
|
||||||
channel.on(
|
|
||||||
'postgres_changes',
|
|
||||||
{
|
|
||||||
event: 'UPDATE',
|
|
||||||
schema: 'public',
|
|
||||||
table: 'asignaturas',
|
|
||||||
filter: `id=eq.${args.subjectId}`,
|
|
||||||
},
|
|
||||||
(payload) => {
|
|
||||||
if (cancelledRef.current) return
|
|
||||||
|
|
||||||
const next: any = (payload as any)?.new
|
|
||||||
if (!next?.id || !next?.plan_estudio_id) return
|
|
||||||
handleSubjectReady({
|
|
||||||
id: String(next.id),
|
|
||||||
plan_estudio_id: String(next.plan_estudio_id),
|
|
||||||
estado: next.estado,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
channel.subscribe((status) => {
|
|
||||||
if (cancelledRef.current) return
|
|
||||||
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
|
|
||||||
stopSubjectWatch()
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
setWizard((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage:
|
|
||||||
'No se pudo suscribir al estado de la asignatura. Intenta de nuevo.',
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadAiAttachments = async (args: {
|
|
||||||
planId: string
|
|
||||||
files: Array<{ file: File }>
|
|
||||||
}): Promise<Array<string>> => {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
if (!args.files.length) return []
|
|
||||||
|
|
||||||
const runId = crypto.randomUUID()
|
|
||||||
const basePath = `planes/${args.planId}/asignaturas/ai/${runId}`
|
|
||||||
|
|
||||||
const keys: Array<string> = []
|
|
||||||
for (const f of args.files) {
|
|
||||||
const safeName = (f.file.name || 'archivo').replace(/[\\/]+/g, '_')
|
|
||||||
const key = `${basePath}/${crypto.randomUUID()}-${safeName}`
|
|
||||||
|
|
||||||
const { error } = await supabase.storage
|
|
||||||
.from('ai-storage')
|
|
||||||
.upload(key, f.file, {
|
|
||||||
contentType: f.file.type || undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) throw new Error(error.message)
|
|
||||||
keys.push(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
|
||||||
setWizard((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: true,
|
|
||||||
errorMessage: null,
|
|
||||||
}))
|
|
||||||
|
|
||||||
let startedWaiting = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
|
||||||
if (!wizard.plan_estudio_id) {
|
|
||||||
throw new Error('Plan de estudio inválido.')
|
|
||||||
}
|
|
||||||
if (!wizard.datosBasicos.estructuraId) {
|
|
||||||
throw new Error('Estructura inválida.')
|
|
||||||
}
|
|
||||||
if (!wizard.datosBasicos.nombre.trim()) {
|
|
||||||
throw new Error('Nombre inválido.')
|
|
||||||
}
|
|
||||||
if (wizard.datosBasicos.creditos == null) {
|
|
||||||
throw new Error('Créditos inválidos.')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`${new Date().toISOString()} - Insertando asignatura IA`)
|
|
||||||
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const placeholder: TablesInsert<'asignaturas'> = {
|
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
|
||||||
estructura_id: wizard.datosBasicos.estructuraId,
|
|
||||||
nombre: wizard.datosBasicos.nombre,
|
|
||||||
codigo: wizard.datosBasicos.codigo ?? null,
|
|
||||||
tipo: wizard.datosBasicos.tipo ?? undefined,
|
|
||||||
creditos: wizard.datosBasicos.creditos,
|
|
||||||
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
|
|
||||||
horas_independientes: wizard.datosBasicos.horasIndependientes ?? null,
|
|
||||||
estado: 'generando',
|
|
||||||
tipo_origen: 'IA',
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: inserted, error: insertError } = await supabase
|
|
||||||
.from('asignaturas')
|
|
||||||
.insert(placeholder)
|
|
||||||
.select('id,plan_estudio_id')
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (insertError) throw new Error(insertError.message)
|
|
||||||
const subjectId = inserted.id
|
|
||||||
|
|
||||||
setIsSpinningIA(true)
|
|
||||||
|
|
||||||
// Inicia watch realtime antes de disparar la Edge para no perder updates.
|
|
||||||
startedWaiting = true
|
|
||||||
beginSubjectWatch({ subjectId, planId: wizard.plan_estudio_id })
|
|
||||||
|
|
||||||
const archivosAdjuntos = await uploadAiAttachments({
|
|
||||||
planId: wizard.plan_estudio_id,
|
|
||||||
files: (wizard.iaConfig?.archivosAdjuntos ?? []).map((x) => ({
|
|
||||||
file: x.file,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
|
||||||
const payload: AISubjectUnifiedInput = {
|
|
||||||
datosUpdate: {
|
|
||||||
id: subjectId,
|
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
|
||||||
estructura_id: wizard.datosBasicos.estructuraId,
|
|
||||||
nombre: wizard.datosBasicos.nombre,
|
|
||||||
codigo: wizard.datosBasicos.codigo ?? null,
|
|
||||||
tipo: wizard.datosBasicos.tipo ?? null,
|
|
||||||
creditos: wizard.datosBasicos.creditos,
|
|
||||||
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
|
|
||||||
horas_independientes:
|
|
||||||
wizard.datosBasicos.horasIndependientes ?? null,
|
|
||||||
},
|
|
||||||
iaConfig: {
|
|
||||||
descripcionEnfoqueAcademico:
|
|
||||||
wizard.iaConfig?.descripcionEnfoqueAcademico ?? undefined,
|
|
||||||
instruccionesAdicionalesIA:
|
|
||||||
wizard.iaConfig?.instruccionesAdicionalesIA ?? undefined,
|
|
||||||
archivosAdjuntos,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`${new Date().toISOString()} - Disparando Edge IA asignatura (unified)`,
|
|
||||||
)
|
|
||||||
|
|
||||||
await generateSubjectAI.mutateAsync(payload as any)
|
|
||||||
|
|
||||||
// Fallback: una lectura puntual por si el UPDATE llegó antes de suscribir.
|
|
||||||
const latest = await subjects_get_maybe(subjectId)
|
|
||||||
if (latest) {
|
|
||||||
handleSubjectReady({
|
|
||||||
id: latest.id as any,
|
|
||||||
plan_estudio_id: latest.plan_estudio_id as any,
|
|
||||||
estado: (latest as any).estado,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'IA_MULTIPLE') {
|
|
||||||
const selected = wizard.sugerencias.filter((s) => s.selected)
|
|
||||||
|
|
||||||
if (selected.length === 0) {
|
|
||||||
throw new Error('Selecciona al menos una sugerencia.')
|
|
||||||
}
|
|
||||||
if (!wizard.plan_estudio_id) {
|
|
||||||
throw new Error('Plan de estudio inválido.')
|
|
||||||
}
|
|
||||||
if (!wizard.estructuraId) {
|
|
||||||
throw new Error('Selecciona una estructura para continuar.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
setIsSpinningIA(true)
|
|
||||||
|
|
||||||
const archivosAdjuntos = await uploadAiAttachments({
|
|
||||||
planId: wizard.plan_estudio_id,
|
|
||||||
files: (wizard.iaConfig?.archivosAdjuntos ?? []).map((x) => ({
|
|
||||||
file: x.file,
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
|
|
||||||
const placeholders: Array<TablesInsert<'asignaturas'>> = selected.map(
|
|
||||||
(s): TablesInsert<'asignaturas'> => ({
|
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
|
||||||
estructura_id: wizard.estructuraId,
|
|
||||||
estado: 'generando',
|
|
||||||
nombre: s.nombre,
|
|
||||||
codigo: s.codigo ?? null,
|
|
||||||
tipo: s.tipo ?? undefined,
|
|
||||||
creditos: s.creditos ?? 0,
|
|
||||||
horas_academicas: s.horasAcademicas ?? null,
|
|
||||||
horas_independientes: s.horasIndependientes ?? null,
|
|
||||||
linea_plan_id: s.linea_plan_id ?? null,
|
|
||||||
numero_ciclo: s.numero_ciclo ?? null,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const { data: inserted, error: insertError } = await supabase
|
|
||||||
.from('asignaturas')
|
|
||||||
.insert(placeholders)
|
|
||||||
.select('id')
|
|
||||||
|
|
||||||
if (insertError) {
|
|
||||||
throw new Error(insertError.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertedIds = inserted.map((r) => r.id)
|
|
||||||
if (insertedIds.length !== selected.length) {
|
|
||||||
throw new Error('No se pudieron crear todas las asignaturas.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disparar generación en paralelo (no bloquear navegación)
|
|
||||||
insertedIds.forEach((id, idx) => {
|
|
||||||
const s = selected[idx]
|
|
||||||
const creditosForEdge =
|
|
||||||
typeof s.creditos === 'number' && s.creditos > 0
|
|
||||||
? s.creditos
|
|
||||||
: undefined
|
|
||||||
const payload: AISubjectUnifiedInput = {
|
|
||||||
datosUpdate: {
|
|
||||||
id,
|
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
|
||||||
estructura_id: wizard.estructuraId ?? undefined,
|
|
||||||
nombre: s.nombre,
|
|
||||||
codigo: s.codigo ?? null,
|
|
||||||
tipo: s.tipo ?? null,
|
|
||||||
creditos: creditosForEdge,
|
|
||||||
horas_academicas: s.horasAcademicas ?? null,
|
|
||||||
horas_independientes: s.horasIndependientes ?? null,
|
|
||||||
numero_ciclo: s.numero_ciclo ?? null,
|
|
||||||
linea_plan_id: s.linea_plan_id ?? null,
|
|
||||||
},
|
|
||||||
iaConfig: {
|
|
||||||
descripcionEnfoqueAcademico: s.descripcion,
|
|
||||||
instruccionesAdicionalesIA:
|
|
||||||
wizard.iaConfig?.instruccionesAdicionalesIA ?? undefined,
|
|
||||||
archivosAdjuntos,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
void generateSubjectAI.mutateAsync(payload as any).catch((e) => {
|
|
||||||
console.error('Error generando asignatura IA (multiple):', e)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Invalidar la query del listado del plan (una vez) para que la lista
|
|
||||||
// muestre el estado actualizado y recargue cuando lleguen updates.
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planAsignaturas(wizard.plan_estudio_id),
|
|
||||||
})
|
|
||||||
|
|
||||||
navigate({
|
|
||||||
to: `/planes/${wizard.plan_estudio_id}/asignaturas`,
|
|
||||||
resetScroll: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'MANUAL') {
|
|
||||||
if (!wizard.plan_estudio_id) {
|
|
||||||
throw new Error('Plan de estudio inválido.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const asignatura = await createSubjectManual.mutateAsync({
|
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
|
||||||
estructura_id: wizard.datosBasicos.estructuraId!,
|
|
||||||
nombre: wizard.datosBasicos.nombre,
|
|
||||||
codigo: wizard.datosBasicos.codigo ?? null,
|
|
||||||
tipo: wizard.datosBasicos.tipo ?? undefined,
|
|
||||||
creditos: wizard.datosBasicos.creditos ?? 0,
|
|
||||||
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
|
|
||||||
horas_independientes: wizard.datosBasicos.horasIndependientes ?? null,
|
|
||||||
linea_plan_id: null,
|
|
||||||
numero_ciclo: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
navigate({
|
|
||||||
to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`,
|
|
||||||
state: { showConfetti: true },
|
|
||||||
resetScroll: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
stopSubjectWatch()
|
|
||||||
setWizard((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage: err?.message ?? 'Error creando la asignatura',
|
|
||||||
}))
|
|
||||||
} finally {
|
|
||||||
if (!startedWaiting) {
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
setWizard((w) => ({ ...w, isLoading: false }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex grow items-center justify-between">
|
<div className="flex-none border-t bg-white p-6">
|
||||||
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
<div className="flex items-center justify-between">
|
||||||
Anterior
|
<div className="flex-1">
|
||||||
</Button>
|
{wizard.errorMessage && (
|
||||||
|
|
||||||
<div className="mx-2 flex-1">
|
|
||||||
{(errorMessage ?? wizard.errorMessage) && (
|
|
||||||
<span className="text-destructive text-sm font-medium">
|
<span className="text-destructive text-sm font-medium">
|
||||||
{errorMessage ?? wizard.errorMessage}
|
{wizard.errorMessage}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-2 flex w-5 items-center justify-center">
|
<div className="flex gap-4">
|
||||||
<Loader2
|
<Button
|
||||||
className={
|
variant="secondary"
|
||||||
wizard.tipoOrigen === 'IA_SIMPLE' && isSpinningIA
|
onClick={() => methods.prev()}
|
||||||
? 'text-muted-foreground h-6 w-6 animate-spin'
|
disabled={idx === 0 || wizard.isLoading}
|
||||||
: 'h-6 w-6 opacity-0'
|
>
|
||||||
}
|
Anterior
|
||||||
aria-hidden={!(wizard.tipoOrigen === 'IA_SIMPLE' && isSpinningIA)}
|
</Button>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLastStep ? (
|
{!isLast ? (
|
||||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
<Button
|
||||||
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
onClick={() => methods.next()}
|
||||||
|
disabled={
|
||||||
|
wizard.isLoading ||
|
||||||
|
(idx === 0 && !canContinueDesdeMetodo) ||
|
||||||
|
(idx === 1 && !canContinueDesdeBasicos) ||
|
||||||
|
(idx === 2 && !canContinueDesdeConfig)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Siguiente
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button onClick={onNext} disabled={disableNext}>
|
<Button onClick={onCreate} disabled={wizard.isLoading}>
|
||||||
Siguiente
|
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
import { Check, Loader2 } from 'lucide-react'
|
|
||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { useUpdatePlanFields, useUpdateRecommendationApplied } from '@/data'
|
|
||||||
|
|
||||||
export const ImprovementCard = ({
|
|
||||||
suggestions,
|
|
||||||
onApply,
|
|
||||||
planId,
|
|
||||||
dbMessageId,
|
|
||||||
currentDatos,
|
|
||||||
activeChatId,
|
|
||||||
onApplySuccess,
|
|
||||||
}: {
|
|
||||||
suggestions: Array<any>
|
|
||||||
onApply?: (key: string, value: string) => void
|
|
||||||
planId: string
|
|
||||||
currentDatos: any
|
|
||||||
dbMessageId: string
|
|
||||||
activeChatId: any
|
|
||||||
onApplySuccess?: (key: string) => void
|
|
||||||
}) => {
|
|
||||||
const [localApplied, setLocalApplied] = useState<Array<string>>([])
|
|
||||||
const updatePlan = useUpdatePlanFields()
|
|
||||||
const updateAppliedStatus = useUpdateRecommendationApplied()
|
|
||||||
|
|
||||||
const handleApply = (key: string, newValue: string) => {
|
|
||||||
if (!currentDatos) return
|
|
||||||
const currentValue = currentDatos[key]
|
|
||||||
let finalValue: any
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof currentValue === 'object' &&
|
|
||||||
currentValue !== null &&
|
|
||||||
'description' in currentValue
|
|
||||||
) {
|
|
||||||
finalValue = { ...currentValue, description: newValue }
|
|
||||||
} else {
|
|
||||||
finalValue = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
const datosActualizados = {
|
|
||||||
...currentDatos,
|
|
||||||
[key]: finalValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePlan.mutate(
|
|
||||||
{
|
|
||||||
planId: planId as any,
|
|
||||||
patch: { datos: datosActualizados },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
setLocalApplied((prev) => [...prev, key])
|
|
||||||
|
|
||||||
if (onApplySuccess) onApplySuccess(key)
|
|
||||||
|
|
||||||
// --- CAMBIO AQUÍ: Ahora enviamos el ID del mensaje ---
|
|
||||||
if (dbMessageId) {
|
|
||||||
updateAppliedStatus.mutate({
|
|
||||||
conversacionId: dbMessageId, // Cambiamos el nombre de la propiedad si es necesario
|
|
||||||
campoAfectado: key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onApply) onApply(key, newValue)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-2 flex w-full flex-col gap-4">
|
|
||||||
{suggestions.map((sug) => {
|
|
||||||
const isApplied = sug.applied === true || localApplied.includes(sug.key)
|
|
||||||
const isUpdating =
|
|
||||||
updatePlan.isPending &&
|
|
||||||
updatePlan.variables.patch.datos?.[sug.key] !== undefined
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={sug.key}
|
|
||||||
className={`rounded-2xl border bg-white p-5 shadow-sm transition-all ${
|
|
||||||
isApplied ? 'border-teal-200 bg-teal-50/20' : 'border-slate-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<h3 className="text-sm font-bold text-slate-900">{sug.label}</h3>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleApply(sug.key, sug.newValue)}
|
|
||||||
disabled={isApplied || !!isUpdating}
|
|
||||||
className={`h-8 rounded-full px-4 text-xs transition-all ${
|
|
||||||
isApplied
|
|
||||||
? 'cursor-not-allowed bg-slate-100 text-slate-400'
|
|
||||||
: 'bg-[#00a189] text-white hover:bg-[#008f7a]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{isUpdating ? (
|
|
||||||
<Loader2 size={12} className="animate-spin" />
|
|
||||||
) : isApplied ? (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Check size={12} /> Aplicado
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
'Aplicar mejora'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={`rounded-xl border p-3 text-sm transition-colors duration-300 ${
|
|
||||||
isApplied
|
|
||||||
? 'border-teal-100 bg-teal-50/50 text-slate-700'
|
|
||||||
: 'border-slate-200 bg-slate-50 text-slate-500'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{sug.newValue}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
181
src/components/planes/wizard/PasoBasicosForm.tsx
Normal file
181
src/components/planes/wizard/PasoBasicosForm.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import type { CARRERAS } from '@/features/planes/new/catalogs'
|
||||||
|
import type { NewPlanWizardState } from '@/features/planes/new/types'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import {
|
||||||
|
FACULTADES,
|
||||||
|
NIVELES,
|
||||||
|
TIPOS_CICLO,
|
||||||
|
} from '@/features/planes/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoBasicosForm({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
carrerasFiltradas,
|
||||||
|
}: {
|
||||||
|
wizard: NewPlanWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
|
carrerasFiltradas: typeof CARRERAS
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="grid gap-1 sm:col-span-2">
|
||||||
|
<Label htmlFor="nombrePlan">Nombre del plan</Label>
|
||||||
|
<Input
|
||||||
|
id="nombrePlan"
|
||||||
|
placeholder="Ej. Ingeniería en Sistemas 2026"
|
||||||
|
value={wizard.datosBasicos.nombrePlan}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="facultad">Facultad</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.facultadId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
facultadId: value,
|
||||||
|
carreraId: '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="facultad"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona facultad…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FACULTADES.map((f) => (
|
||||||
|
<SelectItem key={f.id} value={f.id}>
|
||||||
|
{f.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="carrera">Carrera</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.carreraId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, carreraId: value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={!wizard.datosBasicos.facultadId}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="carrera"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona carrera…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{carrerasFiltradas.map((c) => (
|
||||||
|
<SelectItem key={c.id} value={c.id}>
|
||||||
|
{c.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="nivel">Nivel</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.nivel}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, nivel: value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="nivel"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona nivel…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{NIVELES.map((n) => (
|
||||||
|
<SelectItem key={n} value={n}>
|
||||||
|
{n}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="tipoCiclo">Tipo de ciclo</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.tipoCiclo}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
tipoCiclo: value as any,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="tipoCiclo"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TIPOS_CICLO.map((t) => (
|
||||||
|
<SelectItem key={t.value} value={t.value}>
|
||||||
|
{t.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="numCiclos">Número de ciclos</Label>
|
||||||
|
<Input
|
||||||
|
id="numCiclos"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={wizard.datosBasicos.numCiclos}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
numCiclos: Number(e.target.value || 1),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,344 +0,0 @@
|
|||||||
import type {
|
|
||||||
EstructuraPlanRow,
|
|
||||||
FacultadRow,
|
|
||||||
NivelPlanEstudio,
|
|
||||||
TipoCiclo,
|
|
||||||
} from '@/data/types/domain'
|
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
|
||||||
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { useCatalogosPlanes } from '@/data/hooks/usePlans'
|
|
||||||
import { NIVELES, TIPOS_CICLO } from '@/features/planes/nuevo/catalogs'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
export function PasoBasicosForm({
|
|
||||||
wizard,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
wizard: NewPlanWizardState
|
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
|
||||||
}) {
|
|
||||||
const { data: catalogos } = useCatalogosPlanes()
|
|
||||||
|
|
||||||
// Preferir los catálogos remotos si están disponibles; si no, usar los locales
|
|
||||||
const facultadesList = catalogos?.facultades ?? []
|
|
||||||
const rawCarreras = catalogos?.carreras ?? []
|
|
||||||
const estructurasPlanList = catalogos?.estructurasPlan ?? []
|
|
||||||
|
|
||||||
const filteredCarreras = rawCarreras.filter((c: any) => {
|
|
||||||
const facId = wizard.datosBasicos.facultad.id
|
|
||||||
if (!facId) return true
|
|
||||||
// soportar ambos shapes: `facultad_id` (BD) o `facultadId` (local)
|
|
||||||
return c.facultad_id ? c.facultad_id === facId : c.facultadId === facId
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<div className="grid gap-1 sm:col-span-2">
|
|
||||||
<Label htmlFor="nombrePlan">
|
|
||||||
Nombre del plan {/* <span className="text-destructive">*</span> */}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="nombrePlan"
|
|
||||||
placeholder="Ej. Ingeniería en Sistemas (2026)"
|
|
||||||
value={wizard.datosBasicos.nombrePlan}
|
|
||||||
maxLength={200}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
nombrePlan: e.target.value,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="facultad">Facultad</Label>
|
|
||||||
<Select
|
|
||||||
value={wizard.datosBasicos.facultad.id}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
facultad: {
|
|
||||||
id: value,
|
|
||||||
nombre:
|
|
||||||
facultadesList.find((f) => f.id === value)?.nombre ||
|
|
||||||
'',
|
|
||||||
},
|
|
||||||
carrera: { id: '', nombre: '' },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="facultad"
|
|
||||||
className={cn(
|
|
||||||
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
|
||||||
!wizard.datosBasicos.facultad.id
|
|
||||||
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
|
|
||||||
: 'font-medium not-italic', // Tiene Valor (Medium)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Ej. Facultad de Ingeniería" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{facultadesList.map((f: FacultadRow) => (
|
|
||||||
<SelectItem key={f.id} value={f.id}>
|
|
||||||
{f.nombre}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="carrera">Carrera</Label>
|
|
||||||
<Select
|
|
||||||
value={wizard.datosBasicos.carrera.id}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
carrera: {
|
|
||||||
id: value,
|
|
||||||
nombre:
|
|
||||||
filteredCarreras.find((c) => c.id === value)?.nombre ||
|
|
||||||
'',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={!wizard.datosBasicos.facultad.id}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="carrera"
|
|
||||||
className={cn(
|
|
||||||
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
|
||||||
!wizard.datosBasicos.carrera.id
|
|
||||||
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
|
|
||||||
: 'font-medium not-italic', // Tiene Valor (Medium)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Ej. Ingeniería en Cibernética y Sistemas Computacionales" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{filteredCarreras.map((c: any) => (
|
|
||||||
<SelectItem key={c.id} value={c.id}>
|
|
||||||
{c.nombre}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="nivel">Nivel</Label>
|
|
||||||
<Select
|
|
||||||
value={wizard.datosBasicos.nivel}
|
|
||||||
onValueChange={(value: NivelPlanEstudio) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: { ...w.datosBasicos, nivel: value },
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="nivel"
|
|
||||||
className={cn(
|
|
||||||
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
|
||||||
!wizard.datosBasicos.nivel
|
|
||||||
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
|
|
||||||
: 'font-medium not-italic', // Tiene Valor (Medium)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Ej. Licenciatura" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{NIVELES.map((n) => (
|
|
||||||
<SelectItem key={n} value={n}>
|
|
||||||
{n}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="tipoCiclo">Tipo de ciclo</Label>
|
|
||||||
<Select
|
|
||||||
value={wizard.datosBasicos.tipoCiclo}
|
|
||||||
onValueChange={(value: TipoCiclo) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
tipoCiclo: value as any,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="tipoCiclo"
|
|
||||||
className={cn(
|
|
||||||
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
|
||||||
!wizard.datosBasicos.tipoCiclo
|
|
||||||
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
|
|
||||||
: 'font-medium not-italic', // Tiene Valor (Medium)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Ej. Semestre" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{TIPOS_CICLO.map((t) => (
|
|
||||||
<SelectItem key={t} value={t}>
|
|
||||||
{t}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="numCiclos">Número de ciclos</Label>
|
|
||||||
<Input
|
|
||||||
id="numCiclos"
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={99}
|
|
||||||
step={1}
|
|
||||||
inputMode="numeric"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
value={wizard.datosBasicos.numCiclos ?? ''}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
// Keep undefined when the input is empty so the field stays optional
|
|
||||||
numCiclos: (() => {
|
|
||||||
const raw = e.target.value
|
|
||||||
if (raw === '') return null
|
|
||||||
const asNumber = Number(raw)
|
|
||||||
if (Number.isNaN(asNumber)) return null
|
|
||||||
// Coerce to positive integer (natural numbers without zero)
|
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
|
||||||
const capped = Math.min(n >= 1 ? n : 1, 99)
|
|
||||||
return capped
|
|
||||||
})(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
|
||||||
placeholder="Ej. 8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-1">
|
|
||||||
<Label htmlFor="estructuraPlan">Estructura de plan de estudios</Label>
|
|
||||||
<Select
|
|
||||||
value={wizard.datosBasicos.estructuraPlanId ?? ''}
|
|
||||||
onValueChange={(value: string) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
estructuraPlanId: value,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="tipoCiclo"
|
|
||||||
className={cn(
|
|
||||||
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
|
||||||
!wizard.datosBasicos.estructuraPlanId
|
|
||||||
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
|
|
||||||
: 'font-medium not-italic', // Tiene Valor (Medium)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Ej. Plan base SEP/ULSA (2026)" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{estructurasPlanList.map((t: EstructuraPlanRow) => (
|
|
||||||
<SelectItem key={t.id} value={t.id}>
|
|
||||||
{t.nombre}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <Separator className="my-3" />
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
|
||||||
<TemplateSelectorCard
|
|
||||||
cardTitle="Plantilla de plan de estudios"
|
|
||||||
cardDescription="Selecciona el Word para tu nuevo plan."
|
|
||||||
templatesData={PLANTILLAS_ANEXO_1}
|
|
||||||
selectedTemplateId={wizard.datosBasicos.plantillaPlanId || ''}
|
|
||||||
selectedVersion={wizard.datosBasicos.plantillaPlanVersion || ''}
|
|
||||||
onChange={({ templateId, version }) =>
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
plantillaPlanId: templateId,
|
|
||||||
plantillaPlanVersion: version,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TemplateSelectorCard
|
|
||||||
cardTitle="Plantilla de mapa curricular"
|
|
||||||
cardDescription="Selecciona el Excel para tu mapa curricular."
|
|
||||||
templatesData={PLANTILLAS_ANEXO_2}
|
|
||||||
selectedTemplateId={wizard.datosBasicos.plantillaMapaId || ''}
|
|
||||||
selectedVersion={wizard.datosBasicos.plantillaMapaVersion || ''}
|
|
||||||
onChange={({ templateId, version }) =>
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
datosBasicos: {
|
|
||||||
...w.datosBasicos,
|
|
||||||
plantillaMapaId: templateId,
|
|
||||||
plantillaMapaVersion: version,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
import { useMemo, useState } from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
} from '@/components/ui/card'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
type TemplateData = {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
versions: Array<string>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default data (kept for backward compatibility if caller doesn't pass templates)
|
|
||||||
const DEFAULT_TEMPLATES_DATA: Array<TemplateData> = [
|
|
||||||
{
|
|
||||||
id: 'sep-2025',
|
|
||||||
name: 'Licenciatura RVOE SEP',
|
|
||||||
versions: ['v2025.2 (Vigente)', 'v2025.1', 'v2024.Final'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'interno-mix',
|
|
||||||
name: 'Estándar Institucional Mixto',
|
|
||||||
versions: ['v2.0', 'v1.5', 'v1.0-beta'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'conacyt',
|
|
||||||
name: 'Formato Posgrado CONAHCYT',
|
|
||||||
versions: ['v3.0 (2025)', 'v2.8'],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
cardTitle?: string
|
|
||||||
cardDescription?: string
|
|
||||||
templatesData?: Array<TemplateData>
|
|
||||||
// Controlled selection (optional). If not provided, component manages its own state
|
|
||||||
selectedTemplateId?: string
|
|
||||||
selectedVersion?: string
|
|
||||||
onChange?: (sel: { templateId: string; version: string }) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TemplateSelectorCard({
|
|
||||||
cardTitle = 'Configuración del Documento',
|
|
||||||
cardDescription = 'Selecciona la base para tu nuevo plan.',
|
|
||||||
templatesData = DEFAULT_TEMPLATES_DATA,
|
|
||||||
selectedTemplateId,
|
|
||||||
selectedVersion,
|
|
||||||
onChange,
|
|
||||||
}: Props) {
|
|
||||||
const [internalTemplate, setInternalTemplate] = useState<string>('')
|
|
||||||
const [internalVersion, setInternalVersion] = useState<string>('')
|
|
||||||
|
|
||||||
const selectedTemplate = selectedTemplateId ?? internalTemplate
|
|
||||||
const version = selectedVersion ?? internalVersion
|
|
||||||
|
|
||||||
// Buscamos las versiones de la plantilla seleccionada
|
|
||||||
const currentTemplateData = useMemo(
|
|
||||||
() => templatesData.find((t) => t.id === selectedTemplate),
|
|
||||||
[templatesData, selectedTemplate],
|
|
||||||
)
|
|
||||||
const availableVersions = currentTemplateData?.versions || []
|
|
||||||
|
|
||||||
const handleTemplateChange = (value: string) => {
|
|
||||||
const template = templatesData.find((t) => t.id === value)
|
|
||||||
const firstVersion = template?.versions[0] ?? ''
|
|
||||||
if (onChange) {
|
|
||||||
onChange({ templateId: value, version: firstVersion })
|
|
||||||
} else {
|
|
||||||
setInternalTemplate(value)
|
|
||||||
setInternalVersion(firstVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleVersionChange = (value: string) => {
|
|
||||||
if (onChange) {
|
|
||||||
onChange({ templateId: selectedTemplate, version: value })
|
|
||||||
} else {
|
|
||||||
setInternalVersion(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="w-full max-w-lg gap-2 overflow-hidden">
|
|
||||||
<CardHeader className="px-4 pb-2 sm:px-6 sm:pb-4">
|
|
||||||
<CardTitle className="text-lg">{cardTitle}</CardTitle>
|
|
||||||
<CardDescription>{cardDescription}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
|
||||||
{/* SELECT 1: PRIMARIO (Llamativo) */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label
|
|
||||||
htmlFor="template-select"
|
|
||||||
className="text-foreground text-base font-semibold"
|
|
||||||
>
|
|
||||||
Plantilla
|
|
||||||
</Label>
|
|
||||||
<Select value={selectedTemplate} onValueChange={handleTemplateChange}>
|
|
||||||
<SelectTrigger
|
|
||||||
id="template-select"
|
|
||||||
className="bg-background border-primary/40 focus:ring-primary/20 focus:border-primary flex h-11 w-full min-w-0 items-center justify-between gap-2 text-base shadow-sm [&>span]:block! [&>span]:truncate! [&>span]:text-left"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Selecciona una plantilla..." />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{templatesData.map((t) => (
|
|
||||||
<SelectItem key={t.id} value={t.id} className="font-medium">
|
|
||||||
{t.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SELECT 2: SECUNDARIO (Sutil) */}
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label
|
|
||||||
htmlFor="version-select"
|
|
||||||
className={cn(
|
|
||||||
'text-xs tracking-wider uppercase transition-colors',
|
|
||||||
!selectedTemplate
|
|
||||||
? 'text-muted-foreground/50'
|
|
||||||
: 'text-muted-foreground',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
Versión
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Select
|
|
||||||
value={version}
|
|
||||||
onValueChange={handleVersionChange}
|
|
||||||
disabled={!selectedTemplate}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id="version-select"
|
|
||||||
className={cn(
|
|
||||||
'flex h-9 min-w-0 items-center justify-between gap-2 text-sm transition-all duration-300',
|
|
||||||
/* AQUÍ ESTÁ EL CAMBIO DE ANCHO: */
|
|
||||||
'w-full max-w-full sm:w-55',
|
|
||||||
|
|
||||||
/* Las correcciones vitales para truncado que ya teníamos: */
|
|
||||||
'min-w-0 [&>span]:block! [&>span]:truncate! [&>span]:text-left',
|
|
||||||
'[&>span]:block [&>span]:min-w-0 [&>span]:truncate [&>span]:text-left',
|
|
||||||
|
|
||||||
!selectedTemplate
|
|
||||||
? 'bg-muted/50 cursor-not-allowed border-transparent opacity-50'
|
|
||||||
: 'bg-muted/20 border-border hover:bg-background hover:border-primary/30',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={
|
|
||||||
!selectedTemplate
|
|
||||||
? '— Esperando plantilla —'
|
|
||||||
: 'Selecciona versión'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{availableVersions.map((v) => (
|
|
||||||
<SelectItem
|
|
||||||
key={v}
|
|
||||||
value={v}
|
|
||||||
className="text-muted-foreground focus:text-foreground text-sm"
|
|
||||||
>
|
|
||||||
{v}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { FileDropzone } from './FileDropZone'
|
import type { NewPlanWizardState } from '@/features/planes/new/types'
|
||||||
import ReferenciasParaIA from './ReferenciasParaIA'
|
|
||||||
|
|
||||||
import type { UploadedFile } from './FileDropZone'
|
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
@@ -16,18 +14,20 @@ import {
|
|||||||
CARRERAS,
|
CARRERAS,
|
||||||
FACULTADES,
|
FACULTADES,
|
||||||
PLANES_EXISTENTES,
|
PLANES_EXISTENTES,
|
||||||
} from '@/features/planes/nuevo/catalogs'
|
} from '@/features/planes/new/catalogs'
|
||||||
|
|
||||||
export function PasoDetallesPanel({
|
export function PasoDetallesPanel({
|
||||||
wizard,
|
wizard,
|
||||||
onChange,
|
onChange,
|
||||||
|
onGenerarIA,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: {
|
}: {
|
||||||
wizard: NewPlanWizardState
|
wizard: NewPlanWizardState
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
|
onGenerarIA: () => void
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
}) {
|
}) {
|
||||||
if (wizard.tipoOrigen === 'MANUAL') {
|
if (wizard.modoCreacion === 'MANUAL') {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -40,104 +40,102 @@ export function PasoDetallesPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'IA') {
|
if (wizard.modoCreacion === 'IA') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div>
|
||||||
<Label htmlFor="desc">Descripción del enfoque académico</Label>
|
<Label htmlFor="desc">Descripción del enfoque</Label>
|
||||||
<textarea
|
<textarea
|
||||||
id="desc"
|
id="desc"
|
||||||
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
placeholder="Define el perfil de egreso, visión pedagógica y sector profesional. Ej.: Programa semestral orientado a la Industria 4.0, con enfoque en competencias directivas y emprendimiento tecnológico..."
|
placeholder="Describe el enfoque del programa…"
|
||||||
maxLength={7000}
|
value={wizard.iaConfig?.descripcionEnfoque || ''}
|
||||||
value={wizard.iaConfig?.descripcionEnfoqueAcademico || ''}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
onChange((w) => ({
|
onChange((w) => ({
|
||||||
...w,
|
...w,
|
||||||
iaConfig: {
|
iaConfig: {
|
||||||
...(w.iaConfig || ({} as any)),
|
...(w.iaConfig || ({} as any)),
|
||||||
descripcionEnfoqueAcademico: e.target.value,
|
descripcionEnfoque: e.target.value,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<div className="flex flex-col gap-1">
|
<Label htmlFor="poblacion">Población objetivo</Label>
|
||||||
<Label htmlFor="notas">
|
<Input
|
||||||
Instrucciones adicionales para la IA
|
id="poblacion"
|
||||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
placeholder="Ej. Egresados de bachillerato con perfil STEM"
|
||||||
(Opcional)
|
value={wizard.iaConfig?.poblacionObjetivo || ''}
|
||||||
</span>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
</Label>
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...(w.iaConfig || ({} as any)),
|
||||||
|
poblacionObjetivo: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="notas">Notas adicionales</Label>
|
||||||
<textarea
|
<textarea
|
||||||
id="notas"
|
id="notas"
|
||||||
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
placeholder="Opcional: Estándares, estructura y limitaciones. Ej.: Estructura de 9 ciclos, carga pesada en ciencias básicas, sigue normativa CACEI, incluye 15% de materias optativas..."
|
placeholder="Lineamientos institucionales, restricciones, etc."
|
||||||
maxLength={7000}
|
value={wizard.iaConfig?.notasAdicionales || ''}
|
||||||
value={wizard.iaConfig?.instruccionesAdicionalesIA || ''}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
onChange((w) => ({
|
onChange((w) => ({
|
||||||
...w,
|
...w,
|
||||||
iaConfig: {
|
iaConfig: {
|
||||||
...(w.iaConfig || ({} as any)),
|
...(w.iaConfig || ({} as any)),
|
||||||
instruccionesAdicionalesIA: e.target.value,
|
notasAdicionales: e.target.value,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ReferenciasParaIA
|
<div className="flex items-center justify-between">
|
||||||
selectedArchivoIds={wizard.iaConfig?.archivosReferencia || []}
|
<div className="text-muted-foreground text-sm">
|
||||||
selectedRepositorioIds={wizard.iaConfig?.repositoriosReferencia || []}
|
Opcional: se pueden adjuntar recursos IA más adelante.
|
||||||
uploadedFiles={wizard.iaConfig?.archivosAdjuntos || []}
|
</div>
|
||||||
onToggleArchivo={(id, checked) =>
|
<Button onClick={onGenerarIA} disabled={isLoading}>
|
||||||
onChange((w): NewPlanWizardState => {
|
{isLoading ? 'Generando…' : 'Generar borrador con IA'}
|
||||||
const prev = w.iaConfig?.archivosReferencia || []
|
</Button>
|
||||||
const next = checked
|
</div>
|
||||||
? [...prev, id]
|
|
||||||
: prev.filter((x) => x !== id)
|
{wizard.resumen.previewPlan && (
|
||||||
return {
|
<Card>
|
||||||
...w,
|
<CardHeader>
|
||||||
iaConfig: {
|
<CardTitle>Preview IA</CardTitle>
|
||||||
...(w.iaConfig || ({} as any)),
|
<CardDescription>
|
||||||
archivosReferencia: next,
|
Asignaturas aprox.:{' '}
|
||||||
},
|
{wizard.resumen.previewPlan.numAsignaturasAprox}
|
||||||
}
|
</CardDescription>
|
||||||
})
|
</CardHeader>
|
||||||
}
|
<CardContent>
|
||||||
onToggleRepositorio={(id, checked) =>
|
<ul className="text-muted-foreground list-disc pl-5 text-sm">
|
||||||
onChange((w): NewPlanWizardState => {
|
{wizard.resumen.previewPlan.secciones?.map((s) => (
|
||||||
const prev = w.iaConfig?.repositoriosReferencia || []
|
<li key={s.id}>
|
||||||
const next = checked
|
<span className="text-foreground font-medium">
|
||||||
? [...prev, id]
|
{s.titulo}:
|
||||||
: prev.filter((x) => x !== id)
|
</span>{' '}
|
||||||
return {
|
{s.resumen}
|
||||||
...w,
|
</li>
|
||||||
iaConfig: {
|
))}
|
||||||
...(w.iaConfig || ({} as any)),
|
</ul>
|
||||||
repositoriosReferencia: next,
|
</CardContent>
|
||||||
},
|
</Card>
|
||||||
}
|
)}
|
||||||
})
|
|
||||||
}
|
|
||||||
onFilesChange={(files: Array<UploadedFile>) =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
iaConfig: {
|
|
||||||
...(w.iaConfig || ({} as any)),
|
|
||||||
archivosAdjuntos: files,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
if (
|
||||||
|
wizard.modoCreacion === 'CLONADO' &&
|
||||||
|
wizard.subModoClonado === 'INTERNO'
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="grid gap-3 sm:grid-cols-3">
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
@@ -146,7 +144,6 @@ export function PasoDetallesPanel({
|
|||||||
<select
|
<select
|
||||||
id="clonFacultad"
|
id="clonFacultad"
|
||||||
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
aria-label="Facultad"
|
|
||||||
value={wizard.datosBasicos.facultadId}
|
value={wizard.datosBasicos.facultadId}
|
||||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||||
onChange((w) => ({
|
onChange((w) => ({
|
||||||
@@ -171,7 +168,6 @@ export function PasoDetallesPanel({
|
|||||||
<select
|
<select
|
||||||
id="clonCarrera"
|
id="clonCarrera"
|
||||||
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
aria-label="Carrera"
|
|
||||||
value={wizard.datosBasicos.carreraId}
|
value={wizard.datosBasicos.carreraId}
|
||||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||||
onChange((w) => ({
|
onChange((w) => ({
|
||||||
@@ -241,12 +237,15 @@ export function PasoDetallesPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
if (
|
||||||
|
wizard.modoCreacion === 'CLONADO' &&
|
||||||
|
wizard.subModoClonado === 'TRADICIONAL'
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="grid gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div>
|
||||||
<Label htmlFor="word">Word del plan (obligatorio)</Label>
|
<Label htmlFor="word">Word del plan (obligatorio)</Label>
|
||||||
{/* <input
|
<input
|
||||||
id="word"
|
id="word"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".doc,.docx"
|
accept=".doc,.docx"
|
||||||
@@ -262,21 +261,6 @@ export function PasoDetallesPanel({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/> */}
|
|
||||||
|
|
||||||
<FileDropzone
|
|
||||||
acceptedTypes=".doc,.docx"
|
|
||||||
maxFiles={1}
|
|
||||||
onFilesChange={(files) => {
|
|
||||||
const f = files[0] || null
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
clonTradicional: {
|
|
||||||
...(w.clonTradicional || ({} as any)),
|
|
||||||
archivoWordPlanId: f,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -285,32 +269,17 @@ export function PasoDetallesPanel({
|
|||||||
id="mapa"
|
id="mapa"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".xls,.xlsx"
|
accept=".xls,.xlsx"
|
||||||
title="Subir mapa curricular"
|
|
||||||
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChange((w) => {
|
onChange((w) => ({
|
||||||
const file = e.target.files?.[0] || null
|
|
||||||
const next = file
|
|
||||||
? {
|
|
||||||
id:
|
|
||||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
||||||
? (crypto as any).randomUUID()
|
|
||||||
: `file-${Date.now()}-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substr(2, 9)}`,
|
|
||||||
name: file.name,
|
|
||||||
size: formatFileSize(file.size),
|
|
||||||
type: file.name.split('.').pop() || 'file',
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
return {
|
|
||||||
...w,
|
...w,
|
||||||
clonTradicional: {
|
clonTradicional: {
|
||||||
...(w.clonTradicional || ({} as any)),
|
...(w.clonTradicional || ({} as any)),
|
||||||
archivoMapaExcelId: next,
|
archivoMapaExcelId: e.target.files?.[0]
|
||||||
|
? `file_${e.target.files[0].name}`
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
}
|
}))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -320,32 +289,17 @@ export function PasoDetallesPanel({
|
|||||||
id="asignaturas"
|
id="asignaturas"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".xls,.xlsx,.csv"
|
accept=".xls,.xlsx,.csv"
|
||||||
title="Subir listado de asignaturas"
|
|
||||||
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChange((w) => {
|
onChange((w) => ({
|
||||||
const file = e.target.files?.[0] || null
|
|
||||||
const next = file
|
|
||||||
? {
|
|
||||||
id:
|
|
||||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
||||||
? (crypto as any).randomUUID()
|
|
||||||
: `file-${Date.now()}-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substr(2, 9)}`,
|
|
||||||
name: file.name,
|
|
||||||
size: formatFileSize(file.size),
|
|
||||||
type: file.name.split('.').pop() || 'file',
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
return {
|
|
||||||
...w,
|
...w,
|
||||||
clonTradicional: {
|
clonTradicional: {
|
||||||
...(w.clonTradicional || ({} as any)),
|
...(w.clonTradicional || ({} as any)),
|
||||||
archivoAsignaturasExcelId: next,
|
archivoAsignaturasExcelId: e.target.files?.[0]
|
||||||
|
? `file_${e.target.files[0].name}`
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
}
|
}))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -367,9 +321,3 @@ export function PasoDetallesPanel({
|
|||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFileSize(bytes: number): string {
|
|
||||||
if (bytes < 1024) return bytes + ' B'
|
|
||||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
||||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
import { Upload, File, X, FileText } from 'lucide-react'
|
|
||||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
export interface UploadedFile {
|
|
||||||
id: string // Necesario para React (key)
|
|
||||||
file: File // La fuente de verdad (contiene name, size, type)
|
|
||||||
preview?: string // Opcional: si fueran imágenes
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FileDropzoneProps {
|
|
||||||
persistentFiles?: Array<UploadedFile>
|
|
||||||
onFilesChange?: (files: Array<UploadedFile>) => void
|
|
||||||
acceptedTypes?: string
|
|
||||||
maxFiles?: number
|
|
||||||
title?: string
|
|
||||||
description?: string
|
|
||||||
autoScrollToDropzone?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FileDropzone({
|
|
||||||
persistentFiles,
|
|
||||||
onFilesChange,
|
|
||||||
acceptedTypes = '.doc,.docx,.pdf',
|
|
||||||
maxFiles = 5,
|
|
||||||
title = 'Arrastra archivos aquí',
|
|
||||||
description = 'o haz clic para seleccionar',
|
|
||||||
autoScrollToDropzone = false,
|
|
||||||
}: FileDropzoneProps) {
|
|
||||||
const [isDragging, setIsDragging] = useState(false)
|
|
||||||
const [files, setFiles] = useState<Array<UploadedFile>>(persistentFiles ?? [])
|
|
||||||
const onFilesChangeRef = useRef<typeof onFilesChange>(onFilesChange)
|
|
||||||
const bottomRef = useRef<HTMLDivElement>(null)
|
|
||||||
const prevFilesLengthRef = useRef(files.length)
|
|
||||||
|
|
||||||
const addFiles = useCallback(
|
|
||||||
(incomingFiles: Array<File>) => {
|
|
||||||
console.log(
|
|
||||||
'incoming files:',
|
|
||||||
incomingFiles.map((file) => file.name),
|
|
||||||
)
|
|
||||||
|
|
||||||
setFiles((previousFiles) => {
|
|
||||||
console.log(
|
|
||||||
'previous files',
|
|
||||||
previousFiles.map((f) => f.file.name),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Evitar duplicados por nombre (comprobación global en los archivos existentes)
|
|
||||||
const existingFileNames = new Set(
|
|
||||||
previousFiles.map((uploaded) => uploaded.file.name),
|
|
||||||
)
|
|
||||||
const uniqueNewFiles = incomingFiles.filter(
|
|
||||||
(incomingFile) => !existingFileNames.has(incomingFile.name),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Convertir archivos a objetos con ID único para manejo en React
|
|
||||||
const filesToUpload: Array<UploadedFile> = uniqueNewFiles.map(
|
|
||||||
(incomingFile) => ({
|
|
||||||
id:
|
|
||||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
|
||||||
? (crypto as any).randomUUID()
|
|
||||||
: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
file: incomingFile,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Calcular espacio disponible respetando el límite máximo
|
|
||||||
const room = Math.max(0, maxFiles - previousFiles.length)
|
|
||||||
const nextFiles = [
|
|
||||||
...previousFiles,
|
|
||||||
...filesToUpload.slice(0, room),
|
|
||||||
].slice(0, maxFiles)
|
|
||||||
return nextFiles
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[maxFiles],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Manejador para cuando se arrastran archivos sobre la zona
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setIsDragging(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Manejador para cuando se sale de la zona de arrastre
|
|
||||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setIsDragging(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Manejador para cuando se sueltan los archivos
|
|
||||||
const handleDrop = useCallback(
|
|
||||||
(e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setIsDragging(false)
|
|
||||||
const droppedFiles = Array.from(e.dataTransfer.files)
|
|
||||||
addFiles(droppedFiles)
|
|
||||||
},
|
|
||||||
[addFiles],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Manejador para la selección de archivos mediante el input nativo
|
|
||||||
const handleFileInput = useCallback(
|
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (e.target.files) {
|
|
||||||
const selectedFiles = Array.from(e.target.files)
|
|
||||||
addFiles(selectedFiles)
|
|
||||||
// Corrección de bug: Limpiar el valor para permitir seleccionar el mismo archivo nuevamente si fue eliminado
|
|
||||||
e.target.value = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[addFiles],
|
|
||||||
)
|
|
||||||
|
|
||||||
// Función para eliminar un archivo específico por su ID
|
|
||||||
const removeFile = useCallback((fileId: string) => {
|
|
||||||
setFiles((previousFiles) => {
|
|
||||||
console.log(
|
|
||||||
'previous files',
|
|
||||||
previousFiles.map((f) => f.file.name),
|
|
||||||
)
|
|
||||||
const remainingFiles = previousFiles.filter(
|
|
||||||
(uploadedFile) => uploadedFile.id !== fileId,
|
|
||||||
)
|
|
||||||
return remainingFiles
|
|
||||||
})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Mantener la referencia actualizada de la función callback externa para evitar loops en useEffect
|
|
||||||
useEffect(() => {
|
|
||||||
onFilesChangeRef.current = onFilesChange
|
|
||||||
}, [onFilesChange])
|
|
||||||
|
|
||||||
// Notificar al componente padre cuando cambia la lista de archivos
|
|
||||||
useEffect(() => {
|
|
||||||
if (onFilesChangeRef.current) onFilesChangeRef.current(files)
|
|
||||||
}, [files])
|
|
||||||
|
|
||||||
// Scroll automático hacia abajo solo cuando se pasa de 0 a 1 o más archivos
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
autoScrollToDropzone &&
|
|
||||||
prevFilesLengthRef.current === 0 &&
|
|
||||||
files.length > 0
|
|
||||||
) {
|
|
||||||
// Usar un pequeño timeout para asegurar que el renderizado se complete
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
bottomRef.current?.scrollIntoView({
|
|
||||||
behavior: 'smooth',
|
|
||||||
block: 'start',
|
|
||||||
})
|
|
||||||
}, 100)
|
|
||||||
|
|
||||||
// Actualizar la referencia
|
|
||||||
prevFilesLengthRef.current = files.length
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mantener sincronizada la referencia en otros casos
|
|
||||||
prevFilesLengthRef.current = files.length
|
|
||||||
}, [files.length, autoScrollToDropzone])
|
|
||||||
|
|
||||||
// Determinar el icono a mostrar según la extensión del archivo
|
|
||||||
const getFileIcon = (type: string) => {
|
|
||||||
switch (type.toLowerCase()) {
|
|
||||||
case 'pdf':
|
|
||||||
return <FileText className="text-destructive h-4 w-4" />
|
|
||||||
case 'doc':
|
|
||||||
case 'docx':
|
|
||||||
return <FileText className="text-info h-4 w-4" />
|
|
||||||
default:
|
|
||||||
return <File className="text-muted-foreground h-4 w-4" />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* Elemento invisible para referencia de scroll */}
|
|
||||||
<div ref={bottomRef} />
|
|
||||||
|
|
||||||
{/* Área principal de dropzone */}
|
|
||||||
<div
|
|
||||||
onDragOver={handleDragOver}
|
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
onDrop={handleDrop}
|
|
||||||
className={cn(
|
|
||||||
'cursor-pointer rounded-xl border-2 border-dashed p-7 text-center transition-all duration-300',
|
|
||||||
// Siempre usar borde por defecto a menos que se esté arrastrando
|
|
||||||
'border-border hover:border-primary/50',
|
|
||||||
isDragging && 'ring-primary ring-2 ring-offset-2',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept={acceptedTypes}
|
|
||||||
multiple
|
|
||||||
onChange={handleFileInput}
|
|
||||||
className="hidden"
|
|
||||||
id="file-upload"
|
|
||||||
disabled={files.length >= maxFiles}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="file-upload"
|
|
||||||
className="cursor-pointer"
|
|
||||||
aria-label="Seleccionar archivos"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center gap-3">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex h-12 w-12 items-center justify-center rounded-xl transition-colors',
|
|
||||||
isDragging
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-accent text-accent-foreground',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Upload className="h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-foreground text-sm font-medium">{title}</p>
|
|
||||||
{/* <p className="text-muted-foreground mt-1 text-xs">
|
|
||||||
{description}
|
|
||||||
</p> */}
|
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
|
||||||
Formatos:{' '}
|
|
||||||
{acceptedTypes
|
|
||||||
.replace(/\./g, '')
|
|
||||||
.toUpperCase()
|
|
||||||
.replace(/,/g, ', ')}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-2 flex items-center justify-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'text-primary text-xl font-bold',
|
|
||||||
files.length >= maxFiles ? 'text-destructive' : '',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{files.length}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'text-sm font-medium transition-colors',
|
|
||||||
files.length >= maxFiles
|
|
||||||
? 'text-destructive'
|
|
||||||
: 'text-muted-foreground/80',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
/ {maxFiles} archivos (máximo)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lista de archivos subidos (Orden inverso: más recientes primero) */}
|
|
||||||
<div className="h-56 overflow-y-auto">
|
|
||||||
{files.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{[...files].reverse().map((uploadedFile) => (
|
|
||||||
<div
|
|
||||||
key={uploadedFile.id}
|
|
||||||
className="bg-accent/50 border-border fade-in flex items-center gap-3 rounded-lg border p-3"
|
|
||||||
>
|
|
||||||
{getFileIcon(uploadedFile.file.type)}
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-foreground truncate text-sm font-medium">
|
|
||||||
{uploadedFile.file.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{formatFileSize(uploadedFile.file.size)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="text-muted-foreground hover:text-destructive h-8 w-8"
|
|
||||||
onClick={() => removeFile(uploadedFile.id)}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
import { FileText, FolderOpen, Upload } from 'lucide-react'
|
|
||||||
import { useMemo, useState } from 'react'
|
|
||||||
|
|
||||||
import BarraBusqueda from '../../BarraBusqueda'
|
|
||||||
|
|
||||||
import { FileDropzone } from './FileDropZone'
|
|
||||||
|
|
||||||
import type { UploadedFile } from './FileDropZone'
|
|
||||||
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
import {
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
TabsContents,
|
|
||||||
} from '@/components/ui/motion-tabs'
|
|
||||||
import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs'
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
const ReferenciasParaIA = ({
|
|
||||||
selectedArchivoIds = [],
|
|
||||||
selectedRepositorioIds = [],
|
|
||||||
uploadedFiles = [],
|
|
||||||
onToggleArchivo,
|
|
||||||
onToggleRepositorio,
|
|
||||||
onFilesChange,
|
|
||||||
}: {
|
|
||||||
selectedArchivoIds?: Array<string>
|
|
||||||
selectedRepositorioIds?: Array<string>
|
|
||||||
uploadedFiles?: Array<UploadedFile>
|
|
||||||
onToggleArchivo?: (id: string, checked: boolean) => void
|
|
||||||
onToggleRepositorio?: (id: string, checked: boolean) => void
|
|
||||||
onFilesChange?: (files: Array<UploadedFile>) => void
|
|
||||||
}) => {
|
|
||||||
const [busquedaArchivos, setBusquedaArchivos] = useState('')
|
|
||||||
const [busquedaRepositorios, setBusquedaRepositorios] = useState('')
|
|
||||||
|
|
||||||
const cleanText = (text: string) => {
|
|
||||||
return text
|
|
||||||
.normalize('NFD') // Descompone "á" en "a" + "´"
|
|
||||||
.replace(/[\u0300-\u036f]/g, '') // Elimina los símbolos diacríticos
|
|
||||||
.toLowerCase() // Convierte a minúsculas
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtrado de archivos y de repositorios
|
|
||||||
const archivosFiltrados = useMemo(() => {
|
|
||||||
// Función helper para limpiar texto (quita acentos y hace minúsculas)
|
|
||||||
|
|
||||||
const term = cleanText(busquedaArchivos)
|
|
||||||
return ARCHIVOS.filter((archivo) =>
|
|
||||||
cleanText(archivo.nombre).includes(term),
|
|
||||||
)
|
|
||||||
}, [busquedaArchivos])
|
|
||||||
|
|
||||||
const repositoriosFiltrados = useMemo(() => {
|
|
||||||
const term = cleanText(busquedaRepositorios)
|
|
||||||
return REPOSITORIOS.filter((repositorio) =>
|
|
||||||
cleanText(repositorio.nombre).includes(term),
|
|
||||||
)
|
|
||||||
}, [busquedaRepositorios])
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
name: 'Archivos existentes',
|
|
||||||
|
|
||||||
value: 'archivos-existentes',
|
|
||||||
|
|
||||||
icon: FileText,
|
|
||||||
|
|
||||||
content: (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<BarraBusqueda
|
|
||||||
value={busquedaArchivos}
|
|
||||||
onChange={setBusquedaArchivos}
|
|
||||||
placeholder="Buscar archivo existente..."
|
|
||||||
className="m-1 mb-1.5"
|
|
||||||
/>
|
|
||||||
<div className="flex h-96 flex-col gap-0.5 overflow-y-auto">
|
|
||||||
{archivosFiltrados.map((archivo) => (
|
|
||||||
<Label
|
|
||||||
key={archivo.id}
|
|
||||||
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedArchivoIds.includes(archivo.id)}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
onToggleArchivo?.(archivo.id, !!checked)
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
||||||
selectedArchivoIds.includes(archivo.id) ? '' : 'invisible',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FileText className="text-muted-foreground h-4 w-4" />
|
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-foreground truncate text-sm font-medium">
|
|
||||||
{archivo.nombre}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{archivo.tamaño}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'Repositorios',
|
|
||||||
|
|
||||||
value: 'repositorios',
|
|
||||||
|
|
||||||
icon: FolderOpen,
|
|
||||||
|
|
||||||
content: (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<BarraBusqueda
|
|
||||||
value={busquedaRepositorios}
|
|
||||||
onChange={setBusquedaRepositorios}
|
|
||||||
placeholder="Buscar repositorio..."
|
|
||||||
className="m-1 mb-1.5"
|
|
||||||
/>
|
|
||||||
<div className="flex h-96 flex-col gap-0.5 overflow-y-auto">
|
|
||||||
{repositoriosFiltrados.map((repositorio) => (
|
|
||||||
<Label
|
|
||||||
key={repositorio.id}
|
|
||||||
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950"
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedRepositorioIds.includes(repositorio.id)}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
onToggleRepositorio?.(repositorio.id, !!checked)
|
|
||||||
}
|
|
||||||
className={cn(
|
|
||||||
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
||||||
selectedRepositorioIds.includes(repositorio.id)
|
|
||||||
? ''
|
|
||||||
: 'invisible',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FolderOpen className="text-muted-foreground h-4 w-4" />
|
|
||||||
<div className="min-w-0 flex-1">
|
|
||||||
<p className="text-foreground text-sm font-medium">
|
|
||||||
{repositorio.nombre}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="text-muted-foreground text-xs">
|
|
||||||
{repositorio.descripcion} · {repositorio.cantidadArchivos}{' '}
|
|
||||||
archivos
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'Subir archivos',
|
|
||||||
|
|
||||||
value: 'subir-archivos',
|
|
||||||
|
|
||||||
icon: Upload,
|
|
||||||
|
|
||||||
content: (
|
|
||||||
<div className="p-1">
|
|
||||||
<FileDropzone
|
|
||||||
persistentFiles={uploadedFiles}
|
|
||||||
onFilesChange={onFilesChange}
|
|
||||||
title="Sube archivos de referencia"
|
|
||||||
description="Documentos que serán usados como contexto para la generación"
|
|
||||||
autoScrollToDropzone={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full flex-col gap-1">
|
|
||||||
<Label>
|
|
||||||
Referencias para la IA{' '}
|
|
||||||
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
|
|
||||||
(Opcional)
|
|
||||||
</span>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
<Tabs defaultValue="archivos-existentes" className="gap-4">
|
|
||||||
<TabsList className="w-full">
|
|
||||||
{tabs.map(({ icon: Icon, name, value }) => (
|
|
||||||
<TabsTrigger
|
|
||||||
key={value}
|
|
||||||
value={value}
|
|
||||||
className="flex items-center gap-1 px-2.5 sm:px-3"
|
|
||||||
>
|
|
||||||
<Icon />
|
|
||||||
|
|
||||||
<span className="hidden sm:inline">{name}</span>
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContents className="bg-background mx-1 -mt-2 mb-1 h-full rounded-sm">
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<TabsContent
|
|
||||||
key={tab.value}
|
|
||||||
value={tab.value}
|
|
||||||
className="animate-in fade-in duration-300 ease-out"
|
|
||||||
>
|
|
||||||
{tab.content}
|
|
||||||
</TabsContent>
|
|
||||||
))}
|
|
||||||
</TabsContents>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ReferenciasParaIA
|
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
import type { TipoOrigen } from '@/data/types/domain'
|
import type {
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
NewPlanWizardState,
|
||||||
|
ModoCreacion,
|
||||||
|
SubModoClonado,
|
||||||
|
} from '@/features/planes/new/types'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -18,41 +21,18 @@ export function PasoModoCardGroup({
|
|||||||
wizard: NewPlanWizardState
|
wizard: NewPlanWizardState
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
}) {
|
}) {
|
||||||
const isSelected = (m: TipoOrigen) => wizard.tipoOrigen === m
|
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
|
||||||
const handleKeyActivate = (e: React.KeyboardEvent, cb: () => void) => {
|
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
|
||||||
const key = e.key
|
|
||||||
if (
|
|
||||||
key === 'Enter' ||
|
|
||||||
key === ' ' ||
|
|
||||||
key === 'Spacebar' ||
|
|
||||||
key === 'Space'
|
|
||||||
) {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
cb()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 sm:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
<Card
|
<Card
|
||||||
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange(
|
onChange((w) => ({
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
...w,
|
||||||
tipoOrigen: 'MANUAL',
|
modoCreacion: 'MANUAL',
|
||||||
}),
|
subModoClonado: undefined,
|
||||||
)
|
}))
|
||||||
}
|
|
||||||
onKeyDown={(e: React.KeyboardEvent) =>
|
|
||||||
handleKeyActivate(e, () =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'MANUAL',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -68,22 +48,11 @@ export function PasoModoCardGroup({
|
|||||||
<Card
|
<Card
|
||||||
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange(
|
onChange((w) => ({
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
...w,
|
||||||
tipoOrigen: 'IA',
|
modoCreacion: 'IA',
|
||||||
}),
|
subModoClonado: undefined,
|
||||||
)
|
}))
|
||||||
}
|
|
||||||
onKeyDown={(e: React.KeyboardEvent) =>
|
|
||||||
handleKeyActivate(e, () =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'IA',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -99,15 +68,8 @@ export function PasoModoCardGroup({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
className={isSelected('OTRO') ? 'ring-ring ring-2' : ''}
|
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
|
||||||
onChange((w): NewPlanWizardState => ({ ...w, tipoOrigen: 'OTRO' }))
|
|
||||||
}
|
|
||||||
onKeyDown={(e: React.KeyboardEvent) =>
|
|
||||||
handleKeyActivate(e, () =>
|
|
||||||
onChange((w): NewPlanWizardState => ({ ...w, tipoOrigen: 'OTRO' })),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
@@ -117,34 +79,17 @@ export function PasoModoCardGroup({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Desde un plan existente o archivos.</CardDescription>
|
<CardDescription>Desde un plan existente o archivos.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(wizard.tipoOrigen === 'OTRO' ||
|
{wizard.modoCreacion === 'CLONADO' && (
|
||||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
|
||||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
|
||||||
<CardContent className="flex flex-col gap-3">
|
<CardContent className="flex flex-col gap-3">
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onChange(
|
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' }))
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'CLONADO_INTERNO',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e: React.KeyboardEvent) =>
|
|
||||||
handleKeyActivate(e, () =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'CLONADO_INTERNO',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
||||||
isSelected('CLONADO_INTERNO')
|
isSubSelected('INTERNO')
|
||||||
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
||||||
: 'border-border text-muted-foreground'
|
: 'border-border text-muted-foreground'
|
||||||
} `}
|
} `}
|
||||||
@@ -158,25 +103,10 @@ export function PasoModoCardGroup({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onChange(
|
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' }))
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'CLONADO_TRADICIONAL',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e: React.KeyboardEvent) =>
|
|
||||||
handleKeyActivate(e, () =>
|
|
||||||
onChange(
|
|
||||||
(w): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
tipoOrigen: 'CLONADO_TRADICIONAL',
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
||||||
isSelected('CLONADO_TRADICIONAL')
|
isSubSelected('TRADICIONAL')
|
||||||
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
||||||
: 'border-border text-muted-foreground'
|
: 'border-border text-muted-foreground'
|
||||||
} `}
|
} `}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { UploadedFile } from './PasoDetallesPanel/FileDropZone'
|
import type { NewPlanWizardState } from '@/features/planes/new/types'
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -8,14 +7,10 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import {
|
|
||||||
PLANES_EXISTENTES,
|
|
||||||
ARCHIVOS,
|
|
||||||
REPOSITORIOS,
|
|
||||||
} from '@/features/planes/nuevo/catalogs'
|
|
||||||
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
|
||||||
|
|
||||||
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
||||||
|
const modo = wizard.modoCreacion
|
||||||
|
const sub = wizard.subModoClonado
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -26,14 +21,6 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid gap-2 text-sm">
|
<div className="grid gap-2 text-sm">
|
||||||
{(() => {
|
|
||||||
// Precompute common derived values to avoid unnecessary optional chaining warnings
|
|
||||||
const archivosRef = wizard.iaConfig?.archivosReferencia ?? []
|
|
||||||
const repositoriosRef =
|
|
||||||
wizard.iaConfig?.repositoriosReferencia ?? []
|
|
||||||
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
|
||||||
const contenido = (
|
|
||||||
<>
|
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Nombre: </span>
|
<span className="text-muted-foreground">Nombre: </span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
@@ -41,12 +28,10 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">Facultad/Carrera: </span>
|
||||||
Facultad/Carrera:{' '}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{wizard.datosBasicos.facultad.nombre || '—'} /{' '}
|
{wizard.datosBasicos.facultadId || '—'} /{' '}
|
||||||
{wizard.datosBasicos.carrera.nombre || '—'}
|
{wizard.datosBasicos.carreraId || '—'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -58,128 +43,22 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
|||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Ciclos: </span>
|
<span className="text-muted-foreground">Ciclos: </span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{wizard.datosBasicos.numCiclos} (
|
{wizard.datosBasicos.numCiclos} ({wizard.datosBasicos.tipoCiclo})
|
||||||
{wizard.datosBasicos.tipoCiclo})
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<span className="text-muted-foreground">Modo: </span>
|
<span className="text-muted-foreground">Modo: </span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{wizard.tipoOrigen === 'MANUAL' && 'Manual'}
|
{modo === 'MANUAL' && 'Manual'}
|
||||||
{wizard.tipoOrigen === 'IA' && 'Generado con IA'}
|
{modo === 'IA' && 'Generado con IA'}
|
||||||
{wizard.tipoOrigen === 'CLONADO_INTERNO' &&
|
{modo === 'CLONADO' &&
|
||||||
|
sub === 'INTERNO' &&
|
||||||
'Clonado desde plan del sistema'}
|
'Clonado desde plan del sistema'}
|
||||||
{wizard.tipoOrigen === 'CLONADO_TRADICIONAL' &&
|
{modo === 'CLONADO' &&
|
||||||
|
sub === 'TRADICIONAL' &&
|
||||||
'Importado desde documentos tradicionales'}
|
'Importado desde documentos tradicionales'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{wizard.tipoOrigen === 'CLONADO_INTERNO' && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<span className="text-muted-foreground">Plan origen: </span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{(() => {
|
|
||||||
const p = PLANES_EXISTENTES.find(
|
|
||||||
(x) => x.id === wizard.clonInterno?.planOrigenId,
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
p?.nombre || wizard.clonInterno?.planOrigenId || '—'
|
|
||||||
)
|
|
||||||
})()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{wizard.tipoOrigen === 'CLONADO_TRADICIONAL' && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="font-medium">Documentos adjuntos</div>
|
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
|
||||||
<li>
|
|
||||||
<span className="text-foreground">Word del plan:</span>{' '}
|
|
||||||
{wizard.clonTradicional?.archivoWordPlanId?.name || '—'}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span className="text-foreground">
|
|
||||||
Mapa curricular:
|
|
||||||
</span>{' '}
|
|
||||||
{wizard.clonTradicional?.archivoMapaExcelId?.name ||
|
|
||||||
'—'}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<span className="text-foreground">Asignaturas:</span>{' '}
|
|
||||||
{wizard.clonTradicional?.archivoAsignaturasExcelId
|
|
||||||
?.name || '—'}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{wizard.tipoOrigen === 'IA' && (
|
|
||||||
<div className="bg-muted/50 mt-2 rounded-md p-3">
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Enfoque: </span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.iaConfig?.descripcionEnfoqueAcademico || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">Notas: </span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{wizard.iaConfig?.instruccionesAdicionalesIA || '—'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{archivosRef.length > 0 && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="font-medium">Archivos existentes</div>
|
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
|
||||||
{archivosRef.map((id) => {
|
|
||||||
const a = ARCHIVOS.find((x) => x.id === id)
|
|
||||||
return (
|
|
||||||
<li key={id}>
|
|
||||||
<span className="text-foreground">
|
|
||||||
{a?.nombre || id}
|
|
||||||
</span>{' '}
|
|
||||||
{a?.tamaño ? <span>· {a.tamaño}</span> : null}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{repositoriosRef.length > 0 && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="font-medium">Repositorios</div>
|
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
|
||||||
{repositoriosRef.map((id) => {
|
|
||||||
const r = REPOSITORIOS.find((x) => x.id === id)
|
|
||||||
return (
|
|
||||||
<li key={id}>
|
|
||||||
<span className="text-foreground">
|
|
||||||
{r?.nombre || id}
|
|
||||||
</span>{' '}
|
|
||||||
{r?.cantidadArchivos ? (
|
|
||||||
<span>· {r.cantidadArchivos} archivos</span>
|
|
||||||
) : null}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{adjuntos.length > 0 && (
|
|
||||||
<div className="mt-2">
|
|
||||||
<div className="font-medium">Adjuntos</div>
|
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
|
||||||
{adjuntos.map((f: UploadedFile) => (
|
|
||||||
<li key={f.id}>
|
|
||||||
<span className="text-foreground">
|
|
||||||
{f.file.name}
|
|
||||||
</span>{' '}
|
|
||||||
<span>· {formatFileSize(f.file.size)}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{wizard.resumen.previewPlan && (
|
{wizard.resumen.previewPlan && (
|
||||||
<div className="bg-muted mt-2 rounded-md p-3">
|
<div className="bg-muted mt-2 rounded-md p-3">
|
||||||
<div className="font-medium">Preview IA</div>
|
<div className="font-medium">Preview IA</div>
|
||||||
@@ -189,10 +68,6 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)
|
|
||||||
return contenido
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,306 +1,39 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router'
|
|
||||||
import { Loader2 } from 'lucide-react'
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
import type { AIGeneratePlanInput } from '@/data'
|
|
||||||
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
|
||||||
// import type { Database } from '@/types/supabase'
|
|
||||||
import type { RealtimeChannel } from '@supabase/supabase-js'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { plans_get_maybe } from '@/data/api/plans.api'
|
|
||||||
import {
|
|
||||||
useCreatePlanManual,
|
|
||||||
useDeletePlanEstudio,
|
|
||||||
useGeneratePlanAI,
|
|
||||||
} from '@/data/hooks/usePlans'
|
|
||||||
import { supabaseBrowser } from '@/data/supabase/client'
|
|
||||||
|
|
||||||
export function WizardControls({
|
export function WizardControls({
|
||||||
errorMessage,
|
errorMessage,
|
||||||
onPrev,
|
onPrev,
|
||||||
onNext,
|
onNext,
|
||||||
|
onCreate,
|
||||||
disablePrev,
|
disablePrev,
|
||||||
disableNext,
|
disableNext,
|
||||||
disableCreate,
|
disableCreate,
|
||||||
isLastStep,
|
isLastStep,
|
||||||
wizard,
|
|
||||||
setWizard,
|
|
||||||
}: {
|
}: {
|
||||||
errorMessage?: string | null
|
errorMessage?: string | null
|
||||||
onPrev: () => void
|
onPrev: () => void
|
||||||
onNext: () => void
|
onNext: () => void
|
||||||
|
onCreate: () => void
|
||||||
disablePrev: boolean
|
disablePrev: boolean
|
||||||
disableNext: boolean
|
disableNext: boolean
|
||||||
disableCreate: boolean
|
disableCreate: boolean
|
||||||
isLastStep: boolean
|
isLastStep: boolean
|
||||||
wizard: NewPlanWizardState
|
|
||||||
setWizard: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
|
||||||
const generatePlanAI = useGeneratePlanAI()
|
|
||||||
const createPlanManual = useCreatePlanManual()
|
|
||||||
const deletePlan = useDeletePlanEstudio()
|
|
||||||
const [isSpinningIA, setIsSpinningIA] = useState(false)
|
|
||||||
const cancelledRef = useRef(false)
|
|
||||||
const realtimeChannelRef = useRef<RealtimeChannel | null>(null)
|
|
||||||
const watchPlanIdRef = useRef<string | null>(null)
|
|
||||||
const watchTimeoutRef = useRef<number | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
cancelledRef.current = false
|
|
||||||
return () => {
|
|
||||||
cancelledRef.current = true
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const stopPlanWatch = useCallback(() => {
|
|
||||||
if (watchTimeoutRef.current) {
|
|
||||||
window.clearTimeout(watchTimeoutRef.current)
|
|
||||||
watchTimeoutRef.current = null
|
|
||||||
}
|
|
||||||
|
|
||||||
watchPlanIdRef.current = null
|
|
||||||
|
|
||||||
const ch = realtimeChannelRef.current
|
|
||||||
if (ch) {
|
|
||||||
realtimeChannelRef.current = null
|
|
||||||
try {
|
|
||||||
supabaseBrowser().removeChannel(ch)
|
|
||||||
} catch {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
stopPlanWatch()
|
|
||||||
}
|
|
||||||
}, [stopPlanWatch])
|
|
||||||
|
|
||||||
const checkPlanStateAndAct = useCallback(
|
|
||||||
async (planId: string) => {
|
|
||||||
if (cancelledRef.current) return
|
|
||||||
if (watchPlanIdRef.current !== planId) return
|
|
||||||
|
|
||||||
const plan = await plans_get_maybe(planId as any)
|
|
||||||
if (!plan) return
|
|
||||||
|
|
||||||
const clave = String(plan.estados_plan?.clave ?? '').toUpperCase()
|
|
||||||
|
|
||||||
if (clave.startsWith('GENERANDO')) return
|
|
||||||
|
|
||||||
if (clave.startsWith('BORRADOR')) {
|
|
||||||
stopPlanWatch()
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
setWizard((w) => ({ ...w, isLoading: false }))
|
|
||||||
navigate({
|
|
||||||
to: `/planes/${plan.id}`,
|
|
||||||
state: { showConfetti: true },
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clave.startsWith('FALLID')) {
|
|
||||||
stopPlanWatch()
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
|
|
||||||
deletePlan
|
|
||||||
.mutateAsync(plan.id)
|
|
||||||
.catch(() => {
|
|
||||||
// Si falla el borrado, igual mostramos el error.
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setWizard((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage: 'La generación del plan falló',
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[deletePlan, navigate, setWizard, stopPlanWatch],
|
|
||||||
)
|
|
||||||
|
|
||||||
const beginPlanWatch = useCallback(
|
|
||||||
(planId: string) => {
|
|
||||||
stopPlanWatch()
|
|
||||||
watchPlanIdRef.current = planId
|
|
||||||
|
|
||||||
watchTimeoutRef.current = window.setTimeout(
|
|
||||||
() => {
|
|
||||||
if (cancelledRef.current) return
|
|
||||||
if (watchPlanIdRef.current !== planId) return
|
|
||||||
|
|
||||||
stopPlanWatch()
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
setWizard((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage:
|
|
||||||
'La generación está tardando demasiado. Intenta de nuevo en unos minutos.',
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
6 * 60 * 1000,
|
|
||||||
)
|
|
||||||
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const channel = supabase.channel(`planes-status-${planId}`)
|
|
||||||
realtimeChannelRef.current = channel
|
|
||||||
|
|
||||||
channel.on(
|
|
||||||
'postgres_changes',
|
|
||||||
{
|
|
||||||
event: '*',
|
|
||||||
schema: 'public',
|
|
||||||
table: 'planes_estudio',
|
|
||||||
filter: `id=eq.${planId}`,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
void checkPlanStateAndAct(planId)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
channel.subscribe((status) => {
|
|
||||||
const st = status as
|
|
||||||
| 'SUBSCRIBED'
|
|
||||||
| 'TIMED_OUT'
|
|
||||||
| 'CLOSED'
|
|
||||||
| 'CHANNEL_ERROR'
|
|
||||||
if (cancelledRef.current) return
|
|
||||||
if (st === 'CHANNEL_ERROR' || st === 'TIMED_OUT') {
|
|
||||||
stopPlanWatch()
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
setWizard((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage:
|
|
||||||
'No se pudo suscribir al estado del plan. Intenta de nuevo.',
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fallback inmediato por si el plan ya cambió antes de suscribir.
|
|
||||||
void checkPlanStateAndAct(planId)
|
|
||||||
},
|
|
||||||
[checkPlanStateAndAct, setWizard, stopPlanWatch],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleCreate = async () => {
|
|
||||||
// Start loading
|
|
||||||
setWizard(
|
|
||||||
(w: NewPlanWizardState): NewPlanWizardState => ({
|
|
||||||
...w,
|
|
||||||
isLoading: true,
|
|
||||||
errorMessage: null,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (wizard.tipoOrigen === 'IA') {
|
|
||||||
const tipoCicloSafe = (wizard.datosBasicos.tipoCiclo ||
|
|
||||||
'Semestre') as any
|
|
||||||
const numCiclosSafe =
|
|
||||||
typeof wizard.datosBasicos.numCiclos === 'number'
|
|
||||||
? wizard.datosBasicos.numCiclos
|
|
||||||
: 1
|
|
||||||
|
|
||||||
const aiInput: AIGeneratePlanInput = {
|
|
||||||
datosBasicos: {
|
|
||||||
nombrePlan: wizard.datosBasicos.nombrePlan,
|
|
||||||
carreraId: wizard.datosBasicos.carrera.id,
|
|
||||||
facultadId: wizard.datosBasicos.facultad.id,
|
|
||||||
nivel: wizard.datosBasicos.nivel as string,
|
|
||||||
tipoCiclo: tipoCicloSafe,
|
|
||||||
numCiclos: numCiclosSafe,
|
|
||||||
estructuraPlanId: wizard.datosBasicos.estructuraPlanId as string,
|
|
||||||
},
|
|
||||||
iaConfig: {
|
|
||||||
descripcionEnfoqueAcademico:
|
|
||||||
wizard.iaConfig?.descripcionEnfoqueAcademico || '',
|
|
||||||
instruccionesAdicionalesIA:
|
|
||||||
wizard.iaConfig?.instruccionesAdicionalesIA || '',
|
|
||||||
archivosReferencia: wizard.iaConfig?.archivosReferencia || [],
|
|
||||||
repositoriosIds: wizard.iaConfig?.repositoriosReferencia || [],
|
|
||||||
archivosAdjuntos: wizard.iaConfig?.archivosAdjuntos || [],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
|
|
||||||
|
|
||||||
setIsSpinningIA(true)
|
|
||||||
const resp: any = await generatePlanAI.mutateAsync(aiInput as any)
|
|
||||||
const planId = resp?.plan?.id ?? resp?.id
|
|
||||||
console.log(`${new Date().toISOString()} - Plan IA generado`, resp)
|
|
||||||
|
|
||||||
if (!planId) {
|
|
||||||
throw new Error('No se pudo obtener el id del plan generado por IA')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inicia realtime; los efectos navegan o marcan error.
|
|
||||||
beginPlanWatch(String(planId))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'MANUAL') {
|
|
||||||
// Crear plan vacío manualmente usando el hook
|
|
||||||
const plan = await createPlanManual.mutateAsync({
|
|
||||||
carreraId: wizard.datosBasicos.carrera.id,
|
|
||||||
estructuraId: wizard.datosBasicos.estructuraPlanId as string,
|
|
||||||
nombre: wizard.datosBasicos.nombrePlan,
|
|
||||||
nivel: wizard.datosBasicos.nivel as NivelPlanEstudio,
|
|
||||||
tipoCiclo: wizard.datosBasicos.tipoCiclo as TipoCiclo,
|
|
||||||
numCiclos: (wizard.datosBasicos.numCiclos as number) || 1,
|
|
||||||
datos: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Navegar al nuevo plan
|
|
||||||
navigate({
|
|
||||||
to: `/planes/${plan.id}`,
|
|
||||||
state: { showConfetti: true },
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
setIsSpinningIA(false)
|
|
||||||
stopPlanWatch()
|
|
||||||
setWizard((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage: err?.message ?? 'Error generando el plan',
|
|
||||||
}))
|
|
||||||
} finally {
|
|
||||||
// Si entramos en watch realtime, el loading se corta desde checkPlanStateAndAct.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex grow items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
<div className="flex-1">
|
||||||
Anterior
|
|
||||||
</Button>
|
|
||||||
<div className="mx-2 flex-1">
|
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<span className="text-destructive text-sm font-medium">
|
<span className="text-destructive text-sm font-medium">
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
<div className="mx-2 flex w-5 items-center justify-center">
|
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
||||||
<Loader2
|
Anterior
|
||||||
className={
|
</Button>
|
||||||
wizard.tipoOrigen === 'IA' && isSpinningIA
|
|
||||||
? 'text-muted-foreground h-6 w-6 animate-spin'
|
|
||||||
: 'h-6 w-6 opacity-0'
|
|
||||||
}
|
|
||||||
aria-hidden={!(wizard.tipoOrigen === 'IA' && isSpinningIA)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{isLastStep ? (
|
{isLastStep ? (
|
||||||
<Button onClick={handleCreate} disabled={disableCreate}>
|
<Button onClick={onCreate} disabled={disableCreate}>
|
||||||
Crear plan
|
Crear plan
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
@@ -309,5 +42,6 @@ export function WizardControls({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import { Label } from '@/components/ui/label'
|
|
||||||
|
|
||||||
const CheckboxCardDemo = () => {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="border-border hover:border-primary/30 hover:bg-accent/50 flex cursor-pointer items-center items-start gap-2 gap-3 rounded-lg border p-3 transition-colors has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950">
|
|
||||||
<Checkbox
|
|
||||||
defaultChecked
|
|
||||||
className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
/>
|
|
||||||
<div className="grid gap-1.5 font-normal">
|
|
||||||
<p className="text-sm leading-none font-medium">Auto Start</p>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Starting with your OS.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
<Label className="hover:bg-accent/50 flex items-start gap-2 rounded-lg border p-3 has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950">
|
|
||||||
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
|
|
||||||
<div className="grid gap-1.5 font-normal">
|
|
||||||
<p className="text-sm leading-none font-medium">Auto update</p>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Download and install new version
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CheckboxCardDemo
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { BookIcon, GiftIcon, HeartIcon } from 'lucide-react'
|
|
||||||
|
|
||||||
import CheckboxCardDemo from '../checkbox/checkbox-13'
|
|
||||||
|
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
name: 'Explore',
|
|
||||||
value: 'explore',
|
|
||||||
icon: BookIcon,
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<CheckboxCardDemo />
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Favorites',
|
|
||||||
value: 'favorites',
|
|
||||||
icon: HeartIcon,
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
All your{' '}
|
|
||||||
<span className="text-foreground font-semibold">favorites</span> are
|
|
||||||
saved here. Revisit articles, collections, and moments you love, any
|
|
||||||
time you want a little inspiration.
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Surprise',
|
|
||||||
value: 'surprise',
|
|
||||||
icon: GiftIcon,
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<span className="text-foreground font-semibold">Surprise!</span>{' '}
|
|
||||||
Here's something unexpected—a fun fact, a quirky tip, or a daily
|
|
||||||
challenge. Come back for a new surprise every day!
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const TabsWithIconDemo = () => {
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<Tabs defaultValue="explore" className="gap-4">
|
|
||||||
<TabsList className="w-full">
|
|
||||||
{tabs.map(({ icon: Icon, name, value }) => (
|
|
||||||
<TabsTrigger
|
|
||||||
key={value}
|
|
||||||
value={value}
|
|
||||||
className="flex items-center gap-1 px-2.5 sm:px-3"
|
|
||||||
>
|
|
||||||
<Icon />
|
|
||||||
{name}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<TabsContent
|
|
||||||
key={tab.value}
|
|
||||||
value={tab.value}
|
|
||||||
className="animate-in fade-in duration-300 ease-out"
|
|
||||||
>
|
|
||||||
<p className="text-muted-foreground text-sm">{tab.content}</p>
|
|
||||||
</TabsContent>
|
|
||||||
))}
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TabsWithIconDemo
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { Tabs, TabsContent, TabsContents, TabsList, TabsTrigger } from '@/components/ui/motion-tabs'
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
name: 'Explore',
|
|
||||||
value: 'explore',
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
Discover <span className='text-foreground font-semibold'>fresh ideas</span>, trending topics, and hidden gems
|
|
||||||
curated just for you. Start exploring and let your curiosity lead the way!
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Favorites',
|
|
||||||
value: 'favorites',
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
All your <span className='text-foreground font-semibold'>favorites</span> are saved here. Revisit articles,
|
|
||||||
collections, and moments you love, any time you want a little inspiration.
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Surprise Me',
|
|
||||||
value: 'surprise',
|
|
||||||
content: (
|
|
||||||
<>
|
|
||||||
<span className='text-foreground font-semibold'>Surprise!</span> Here's something unexpected—a fun fact, a
|
|
||||||
quirky tip, or a daily challenge. Come back for a new surprise every day!
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const AnimatedTabsDemo = () => {
|
|
||||||
return (
|
|
||||||
<div className='w-full max-w-md'>
|
|
||||||
<Tabs defaultValue='explore' className='gap-4'>
|
|
||||||
<TabsList>
|
|
||||||
{tabs.map(tab => (
|
|
||||||
<TabsTrigger key={tab.value} value={tab.value}>
|
|
||||||
{tab.name}
|
|
||||||
</TabsTrigger>
|
|
||||||
))}
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContents className='bg-background mx-1 -mt-2 mb-1 h-full rounded-sm'>
|
|
||||||
{tabs.map(tab => (
|
|
||||||
<TabsContent key={tab.value} value={tab.value}>
|
|
||||||
<p className='text-muted-foreground text-sm'>{tab.content}</p>
|
|
||||||
</TabsContent>
|
|
||||||
))}
|
|
||||||
</TabsContents>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<p className='text-muted-foreground mt-4 text-center text-xs'>
|
|
||||||
Inspired by{' '}
|
|
||||||
<a
|
|
||||||
className='hover:text-foreground underline'
|
|
||||||
href='https://animate-ui.com/docs/components/tabs'
|
|
||||||
target='_blank'
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
Animate UI
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AnimatedTabsDemo
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Link, useRouter } from '@tanstack/react-router'
|
|
||||||
import { FileQuestion, Home, ArrowLeft } from 'lucide-react'
|
|
||||||
|
|
||||||
import { Button } from './button'
|
|
||||||
|
|
||||||
interface NotFoundPageProps {
|
|
||||||
title?: string
|
|
||||||
message?: string
|
|
||||||
children?: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotFoundPage({
|
|
||||||
title = 'Página no encontrada',
|
|
||||||
message = 'Lo sentimos, no pudimos encontrar lo que buscabas. Es posible que la página haya sido movida o eliminada.',
|
|
||||||
children,
|
|
||||||
}: NotFoundPageProps) {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-[60vh] flex-col items-center justify-center p-4 text-center">
|
|
||||||
<div className="bg-muted mb-6 rounded-full p-6">
|
|
||||||
<FileQuestion className="text-muted-foreground h-12 w-12" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1 className="mb-2 text-3xl font-bold tracking-tight">{title}</h1>
|
|
||||||
<p className="text-muted-foreground mb-8 max-w-125">{message}</p>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
|
||||||
<Button variant="outline" onClick={() => router.history.back()}>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
Regresar
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button asChild>
|
|
||||||
<Link to="/">
|
|
||||||
<Home className="mr-2 h-4 w-4" />
|
|
||||||
Ir al inicio
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { ChevronDownIcon } from "lucide-react"
|
|
||||||
import { Accordion as AccordionPrimitive } from "radix-ui"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Accordion({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
|
||||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccordionItem({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<AccordionPrimitive.Item
|
|
||||||
data-slot="accordion-item"
|
|
||||||
className={cn("border-b last:border-b-0", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccordionTrigger({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<AccordionPrimitive.Header className="flex">
|
|
||||||
<AccordionPrimitive.Trigger
|
|
||||||
data-slot="accordion-trigger"
|
|
||||||
className={cn(
|
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
|
||||||
</AccordionPrimitive.Trigger>
|
|
||||||
</AccordionPrimitive.Header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AccordionContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<AccordionPrimitive.Content
|
|
||||||
data-slot="accordion-content"
|
|
||||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
|
||||||
</AccordionPrimitive.Content>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
||||||
import { CheckIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Checkbox({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<CheckboxPrimitive.Root
|
|
||||||
data-slot="checkbox"
|
|
||||||
className={cn(
|
|
||||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<CheckboxPrimitive.Indicator
|
|
||||||
data-slot="checkbox-indicator"
|
|
||||||
className="grid place-content-center text-current transition-none"
|
|
||||||
>
|
|
||||||
<CheckIcon className="size-3.5" />
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Checkbox }
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
|
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function ContextMenu({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
|
||||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSub({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
|
||||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuRadioGroup({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.RadioGroup
|
|
||||||
data-slot="context-menu-radio-group"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSubTrigger({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.SubTrigger
|
|
||||||
data-slot="context-menu-sub-trigger"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<ChevronRightIcon className="ml-auto" />
|
|
||||||
</ContextMenuPrimitive.SubTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSubContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.SubContent
|
|
||||||
data-slot="context-menu-sub-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuContent({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Portal>
|
|
||||||
<ContextMenuPrimitive.Content
|
|
||||||
data-slot="context-menu-content"
|
|
||||||
className={cn(
|
|
||||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</ContextMenuPrimitive.Portal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuItem({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
variant = "default",
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
|
||||||
inset?: boolean
|
|
||||||
variant?: "default" | "destructive"
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Item
|
|
||||||
data-slot="context-menu-item"
|
|
||||||
data-inset={inset}
|
|
||||||
data-variant={variant}
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuCheckboxItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
checked,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.CheckboxItem
|
|
||||||
data-slot="context-menu-checkbox-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
checked={checked}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="size-4" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuRadioItem({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.RadioItem
|
|
||||||
data-slot="context-menu-radio-item"
|
|
||||||
className={cn(
|
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
||||||
<ContextMenuPrimitive.ItemIndicator>
|
|
||||||
<CircleIcon className="size-2 fill-current" />
|
|
||||||
</ContextMenuPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
{children}
|
|
||||||
</ContextMenuPrimitive.RadioItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuLabel({
|
|
||||||
className,
|
|
||||||
inset,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
|
||||||
inset?: boolean
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Label
|
|
||||||
data-slot="context-menu-label"
|
|
||||||
data-inset={inset}
|
|
||||||
className={cn(
|
|
||||||
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuSeparator({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
|
||||||
return (
|
|
||||||
<ContextMenuPrimitive.Separator
|
|
||||||
data-slot="context-menu-separator"
|
|
||||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuShortcut({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"span">) {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
data-slot="context-menu-shortcut"
|
|
||||||
className={cn(
|
|
||||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ContextMenu,
|
|
||||||
ContextMenuTrigger,
|
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
|
||||||
ContextMenuCheckboxItem,
|
|
||||||
ContextMenuRadioItem,
|
|
||||||
ContextMenuLabel,
|
|
||||||
ContextMenuSeparator,
|
|
||||||
ContextMenuShortcut,
|
|
||||||
ContextMenuGroup,
|
|
||||||
ContextMenuPortal,
|
|
||||||
ContextMenuSub,
|
|
||||||
ContextMenuSubContent,
|
|
||||||
ContextMenuSubTrigger,
|
|
||||||
ContextMenuRadioGroup,
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { Drawer as DrawerPrimitive } from "vaul"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Drawer({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
|
||||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerTrigger({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
|
||||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerPortal({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
|
||||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerClose({
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
|
||||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerOverlay({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
|
||||||
return (
|
|
||||||
<DrawerPrimitive.Overlay
|
|
||||||
data-slot="drawer-overlay"
|
|
||||||
className={cn(
|
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerContent({
|
|
||||||
className,
|
|
||||||
children,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<DrawerPortal data-slot="drawer-portal">
|
|
||||||
<DrawerOverlay />
|
|
||||||
<DrawerPrimitive.Content
|
|
||||||
data-slot="drawer-content"
|
|
||||||
className={cn(
|
|
||||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
|
||||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
|
||||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
|
||||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
|
||||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
|
||||||
{children}
|
|
||||||
</DrawerPrimitive.Content>
|
|
||||||
</DrawerPortal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="drawer-header"
|
|
||||||
className={cn(
|
|
||||||
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="drawer-footer"
|
|
||||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerTitle({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
|
||||||
return (
|
|
||||||
<DrawerPrimitive.Title
|
|
||||||
data-slot="drawer-title"
|
|
||||||
className={cn("text-foreground font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawerDescription({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
|
||||||
return (
|
|
||||||
<DrawerPrimitive.Description
|
|
||||||
data-slot="drawer-description"
|
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Drawer,
|
|
||||||
DrawerPortal,
|
|
||||||
DrawerOverlay,
|
|
||||||
DrawerTrigger,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerDescription,
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// src/components/ui/lateral-confetti.tsx
|
|
||||||
|
|
||||||
import confetti from 'canvas-confetti'
|
|
||||||
|
|
||||||
export function lateralConfetti() {
|
|
||||||
// 1. Reset para limpiar cualquier configuración vieja pegada en memoria
|
|
||||||
confetti.reset()
|
|
||||||
|
|
||||||
const duration = 1500
|
|
||||||
const end = Date.now() + duration
|
|
||||||
|
|
||||||
// 2. Colores vibrantes (cálidos primero)
|
|
||||||
const vibrantColors = [
|
|
||||||
'#FF0000', // Rojo puro
|
|
||||||
'#fcff42', // Amarillo
|
|
||||||
'#88ff5a', // Verde
|
|
||||||
'#26ccff', // Azul
|
|
||||||
'#a25afd', // Morado
|
|
||||||
]
|
|
||||||
|
|
||||||
;(function frame() {
|
|
||||||
const commonSettings = {
|
|
||||||
particleCount: 5,
|
|
||||||
spread: 55,
|
|
||||||
// origin: { x: 0.5 }, // No necesario si definimos origin abajo, pero útil en otros contextos
|
|
||||||
colors: vibrantColors,
|
|
||||||
zIndex: 99999,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cañón izquierdo
|
|
||||||
confetti({
|
|
||||||
...commonSettings,
|
|
||||||
angle: 60,
|
|
||||||
origin: { x: 0, y: 0.6 },
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cañón derecho
|
|
||||||
confetti({
|
|
||||||
...commonSettings,
|
|
||||||
angle: 120,
|
|
||||||
origin: { x: 1, y: 0.6 },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (Date.now() < end) {
|
|
||||||
requestAnimationFrame(frame)
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
@@ -1,549 +0,0 @@
|
|||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import type { Transition } from 'motion/react'
|
|
||||||
import { AnimatePresence, motion } from 'motion/react'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
|
|
||||||
type MotionHighlightMode = 'children' | 'parent'
|
|
||||||
|
|
||||||
type Bounds = {
|
|
||||||
top: number
|
|
||||||
left: number
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type MotionHighlightContextType<T extends string> = {
|
|
||||||
mode: MotionHighlightMode
|
|
||||||
activeValue: T | null
|
|
||||||
setActiveValue: (value: T | null) => void
|
|
||||||
setBounds: (bounds: DOMRect) => void
|
|
||||||
clearBounds: () => void
|
|
||||||
id: string
|
|
||||||
hover: boolean
|
|
||||||
className?: string
|
|
||||||
activeClassName?: string
|
|
||||||
setActiveClassName: (className: string) => void
|
|
||||||
transition?: Transition
|
|
||||||
disabled?: boolean
|
|
||||||
enabled?: boolean
|
|
||||||
exitDelay?: number
|
|
||||||
forceUpdateBounds?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const MotionHighlightContext = React.createContext<
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
MotionHighlightContextType<any> | undefined
|
|
||||||
>(undefined)
|
|
||||||
|
|
||||||
function useMotionHighlight<T extends string>(): MotionHighlightContextType<T> {
|
|
||||||
const context = React.useContext(MotionHighlightContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useMotionHighlight must be used within a MotionHighlightProvider')
|
|
||||||
}
|
|
||||||
|
|
||||||
return context as unknown as MotionHighlightContextType<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseMotionHighlightProps<T extends string> = {
|
|
||||||
mode?: MotionHighlightMode
|
|
||||||
value?: T | null
|
|
||||||
defaultValue?: T | null
|
|
||||||
onValueChange?: (value: T | null) => void
|
|
||||||
className?: string
|
|
||||||
transition?: Transition
|
|
||||||
hover?: boolean
|
|
||||||
disabled?: boolean
|
|
||||||
enabled?: boolean
|
|
||||||
exitDelay?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type ParentModeMotionHighlightProps = {
|
|
||||||
boundsOffset?: Partial<Bounds>
|
|
||||||
containerClassName?: string
|
|
||||||
forceUpdateBounds?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type ControlledParentModeMotionHighlightProps<T extends string> = BaseMotionHighlightProps<T> &
|
|
||||||
ParentModeMotionHighlightProps & {
|
|
||||||
mode: 'parent'
|
|
||||||
controlledItems: true
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type ControlledChildrenModeMotionHighlightProps<T extends string> = BaseMotionHighlightProps<T> & {
|
|
||||||
mode?: 'children' | undefined
|
|
||||||
controlledItems: true
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type UncontrolledParentModeMotionHighlightProps<T extends string> = BaseMotionHighlightProps<T> &
|
|
||||||
ParentModeMotionHighlightProps & {
|
|
||||||
mode: 'parent'
|
|
||||||
controlledItems?: false
|
|
||||||
itemsClassName?: string
|
|
||||||
children: React.ReactElement | React.ReactElement[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type UncontrolledChildrenModeMotionHighlightProps<T extends string> = BaseMotionHighlightProps<T> & {
|
|
||||||
mode?: 'children'
|
|
||||||
controlledItems?: false
|
|
||||||
itemsClassName?: string
|
|
||||||
children: React.ReactElement | React.ReactElement[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type MotionHighlightProps<T extends string> = React.ComponentProps<'div'> &
|
|
||||||
(
|
|
||||||
| ControlledParentModeMotionHighlightProps<T>
|
|
||||||
| ControlledChildrenModeMotionHighlightProps<T>
|
|
||||||
| UncontrolledParentModeMotionHighlightProps<T>
|
|
||||||
| UncontrolledChildrenModeMotionHighlightProps<T>
|
|
||||||
)
|
|
||||||
|
|
||||||
function MotionHighlight<T extends string>({ ref, ...props }: MotionHighlightProps<T>) {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
value,
|
|
||||||
defaultValue,
|
|
||||||
onValueChange,
|
|
||||||
className,
|
|
||||||
transition = { type: 'spring', stiffness: 350, damping: 35 },
|
|
||||||
hover = false,
|
|
||||||
enabled = true,
|
|
||||||
controlledItems,
|
|
||||||
disabled = false,
|
|
||||||
exitDelay = 0.2,
|
|
||||||
mode = 'children'
|
|
||||||
} = props
|
|
||||||
|
|
||||||
const localRef = React.useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement)
|
|
||||||
|
|
||||||
const [activeValue, setActiveValue] = React.useState<T | null>(value ?? defaultValue ?? null)
|
|
||||||
const [boundsState, setBoundsState] = React.useState<Bounds | null>(null)
|
|
||||||
const [activeClassNameState, setActiveClassNameState] = React.useState<string>('')
|
|
||||||
|
|
||||||
const safeSetActiveValue = React.useCallback(
|
|
||||||
(id: T | null) => {
|
|
||||||
setActiveValue(prev => (prev === id ? prev : id))
|
|
||||||
if (id !== activeValue) onValueChange?.(id as T)
|
|
||||||
},
|
|
||||||
[activeValue, onValueChange]
|
|
||||||
)
|
|
||||||
|
|
||||||
const safeSetBounds = React.useCallback(
|
|
||||||
(bounds: DOMRect) => {
|
|
||||||
if (!localRef.current) return
|
|
||||||
|
|
||||||
const boundsOffset = (props as ParentModeMotionHighlightProps)?.boundsOffset ?? {
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerRect = localRef.current.getBoundingClientRect()
|
|
||||||
|
|
||||||
const newBounds: Bounds = {
|
|
||||||
top: bounds.top - containerRect.top + (boundsOffset.top ?? 0),
|
|
||||||
left: bounds.left - containerRect.left + (boundsOffset.left ?? 0),
|
|
||||||
width: bounds.width + (boundsOffset.width ?? 0),
|
|
||||||
height: bounds.height + (boundsOffset.height ?? 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
setBoundsState(prev => {
|
|
||||||
if (
|
|
||||||
prev &&
|
|
||||||
prev.top === newBounds.top &&
|
|
||||||
prev.left === newBounds.left &&
|
|
||||||
prev.width === newBounds.width &&
|
|
||||||
prev.height === newBounds.height
|
|
||||||
) {
|
|
||||||
return prev
|
|
||||||
}
|
|
||||||
|
|
||||||
return newBounds
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[props]
|
|
||||||
)
|
|
||||||
|
|
||||||
const clearBounds = React.useCallback(() => {
|
|
||||||
setBoundsState(prev => (prev === null ? prev : null))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (value !== undefined) setActiveValue(value)
|
|
||||||
else if (defaultValue !== undefined) setActiveValue(defaultValue)
|
|
||||||
}, [value, defaultValue])
|
|
||||||
|
|
||||||
const id = React.useId()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (mode !== 'parent') return
|
|
||||||
const container = localRef.current
|
|
||||||
|
|
||||||
if (!container) return
|
|
||||||
|
|
||||||
const onScroll = () => {
|
|
||||||
if (!activeValue) return
|
|
||||||
const activeEl = container.querySelector<HTMLElement>(`[data-value="${activeValue}"][data-highlight="true"]`)
|
|
||||||
|
|
||||||
if (activeEl) safeSetBounds(activeEl.getBoundingClientRect())
|
|
||||||
}
|
|
||||||
|
|
||||||
container.addEventListener('scroll', onScroll, { passive: true })
|
|
||||||
|
|
||||||
return () => container.removeEventListener('scroll', onScroll)
|
|
||||||
}, [mode, activeValue, safeSetBounds])
|
|
||||||
|
|
||||||
const render = React.useCallback(
|
|
||||||
(children: React.ReactNode) => {
|
|
||||||
if (mode === 'parent') {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={localRef}
|
|
||||||
data-slot='motion-highlight-container'
|
|
||||||
className={cn('relative', (props as ParentModeMotionHighlightProps)?.containerClassName)}
|
|
||||||
>
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{boundsState && (
|
|
||||||
<motion.div
|
|
||||||
data-slot='motion-highlight'
|
|
||||||
animate={{
|
|
||||||
top: boundsState.top,
|
|
||||||
left: boundsState.left,
|
|
||||||
width: boundsState.width,
|
|
||||||
height: boundsState.height,
|
|
||||||
opacity: 1
|
|
||||||
}}
|
|
||||||
initial={{
|
|
||||||
top: boundsState.top,
|
|
||||||
left: boundsState.left,
|
|
||||||
width: boundsState.width,
|
|
||||||
height: boundsState.height,
|
|
||||||
opacity: 0
|
|
||||||
}}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
transition: {
|
|
||||||
...transition,
|
|
||||||
delay: (transition?.delay ?? 0) + (exitDelay ?? 0)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
transition={transition}
|
|
||||||
className={cn('bg-muted absolute z-0', className, activeClassNameState)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return children
|
|
||||||
},
|
|
||||||
[mode, props, boundsState, transition, exitDelay, className, activeClassNameState]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MotionHighlightContext.Provider
|
|
||||||
value={{
|
|
||||||
mode,
|
|
||||||
activeValue,
|
|
||||||
setActiveValue: safeSetActiveValue,
|
|
||||||
id,
|
|
||||||
hover,
|
|
||||||
className,
|
|
||||||
transition,
|
|
||||||
disabled,
|
|
||||||
enabled,
|
|
||||||
exitDelay,
|
|
||||||
setBounds: safeSetBounds,
|
|
||||||
clearBounds,
|
|
||||||
activeClassName: activeClassNameState,
|
|
||||||
setActiveClassName: setActiveClassNameState,
|
|
||||||
forceUpdateBounds: (props as ParentModeMotionHighlightProps)?.forceUpdateBounds
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{enabled
|
|
||||||
? controlledItems
|
|
||||||
? render(children)
|
|
||||||
: render(
|
|
||||||
React.Children.map(children, (child, index) => (
|
|
||||||
<MotionHighlightItem key={index} className={props?.itemsClassName}>
|
|
||||||
{child}
|
|
||||||
</MotionHighlightItem>
|
|
||||||
))
|
|
||||||
)
|
|
||||||
: children}
|
|
||||||
</MotionHighlightContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNonOverridingDataAttributes(
|
|
||||||
element: React.ReactElement,
|
|
||||||
dataAttributes: Record<string, unknown>
|
|
||||||
): Record<string, unknown> {
|
|
||||||
return Object.keys(dataAttributes).reduce<Record<string, unknown>>((acc, key) => {
|
|
||||||
if ((element.props as Record<string, unknown>)[key] === undefined) {
|
|
||||||
acc[key] = dataAttributes[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExtendedChildProps = React.ComponentProps<'div'> & {
|
|
||||||
id?: string
|
|
||||||
ref?: React.Ref<HTMLElement>
|
|
||||||
'data-active'?: string
|
|
||||||
'data-value'?: string
|
|
||||||
'data-disabled'?: boolean
|
|
||||||
'data-highlight'?: boolean
|
|
||||||
'data-slot'?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MotionHighlightItemProps = React.ComponentProps<'div'> & {
|
|
||||||
children: React.ReactElement
|
|
||||||
id?: string
|
|
||||||
value?: string
|
|
||||||
className?: string
|
|
||||||
transition?: Transition
|
|
||||||
activeClassName?: string
|
|
||||||
disabled?: boolean
|
|
||||||
exitDelay?: number
|
|
||||||
asChild?: boolean
|
|
||||||
forceUpdateBounds?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
function MotionHighlightItem({
|
|
||||||
ref,
|
|
||||||
children,
|
|
||||||
id,
|
|
||||||
value,
|
|
||||||
className,
|
|
||||||
transition,
|
|
||||||
disabled = false,
|
|
||||||
activeClassName,
|
|
||||||
exitDelay,
|
|
||||||
asChild = false,
|
|
||||||
forceUpdateBounds,
|
|
||||||
...props
|
|
||||||
}: MotionHighlightItemProps) {
|
|
||||||
const itemId = React.useId()
|
|
||||||
|
|
||||||
const {
|
|
||||||
activeValue,
|
|
||||||
setActiveValue,
|
|
||||||
mode,
|
|
||||||
setBounds,
|
|
||||||
clearBounds,
|
|
||||||
hover,
|
|
||||||
enabled,
|
|
||||||
className: contextClassName,
|
|
||||||
transition: contextTransition,
|
|
||||||
id: contextId,
|
|
||||||
disabled: contextDisabled,
|
|
||||||
exitDelay: contextExitDelay,
|
|
||||||
forceUpdateBounds: contextForceUpdateBounds,
|
|
||||||
setActiveClassName
|
|
||||||
} = useMotionHighlight()
|
|
||||||
|
|
||||||
const element = children as React.ReactElement<ExtendedChildProps>
|
|
||||||
const childValue = id ?? value ?? element.props?.['data-value'] ?? element.props?.id ?? itemId
|
|
||||||
const isActive = activeValue === childValue
|
|
||||||
const isDisabled = disabled === undefined ? contextDisabled : disabled
|
|
||||||
const itemTransition = transition ?? contextTransition
|
|
||||||
|
|
||||||
const localRef = React.useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (mode !== 'parent') return
|
|
||||||
let rafId: number
|
|
||||||
let previousBounds: Bounds | null = null
|
|
||||||
const shouldUpdateBounds = forceUpdateBounds === true || (contextForceUpdateBounds && forceUpdateBounds !== false)
|
|
||||||
|
|
||||||
const updateBounds = () => {
|
|
||||||
if (!localRef.current) return
|
|
||||||
|
|
||||||
const bounds = localRef.current.getBoundingClientRect()
|
|
||||||
|
|
||||||
if (shouldUpdateBounds) {
|
|
||||||
if (
|
|
||||||
previousBounds &&
|
|
||||||
previousBounds.top === bounds.top &&
|
|
||||||
previousBounds.left === bounds.left &&
|
|
||||||
previousBounds.width === bounds.width &&
|
|
||||||
previousBounds.height === bounds.height
|
|
||||||
) {
|
|
||||||
rafId = requestAnimationFrame(updateBounds)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
previousBounds = bounds
|
|
||||||
rafId = requestAnimationFrame(updateBounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
setBounds(bounds)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
updateBounds()
|
|
||||||
setActiveClassName(activeClassName ?? '')
|
|
||||||
} else if (!activeValue) clearBounds()
|
|
||||||
|
|
||||||
if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId)
|
|
||||||
}, [
|
|
||||||
mode,
|
|
||||||
isActive,
|
|
||||||
activeValue,
|
|
||||||
setBounds,
|
|
||||||
clearBounds,
|
|
||||||
activeClassName,
|
|
||||||
setActiveClassName,
|
|
||||||
forceUpdateBounds,
|
|
||||||
contextForceUpdateBounds
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!React.isValidElement(children)) return children
|
|
||||||
|
|
||||||
const dataAttributes = {
|
|
||||||
'data-active': isActive ? 'true' : 'false',
|
|
||||||
'aria-selected': isActive,
|
|
||||||
'data-disabled': isDisabled,
|
|
||||||
'data-value': childValue,
|
|
||||||
'data-highlight': true
|
|
||||||
}
|
|
||||||
|
|
||||||
const commonHandlers = hover
|
|
||||||
? {
|
|
||||||
onMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
setActiveValue(childValue)
|
|
||||||
element.props.onMouseEnter?.(e)
|
|
||||||
},
|
|
||||||
onMouseLeave: (e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
setActiveValue(null)
|
|
||||||
element.props.onMouseLeave?.(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
onClick: (e: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
setActiveValue(childValue)
|
|
||||||
element.props.onClick?.(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (asChild) {
|
|
||||||
if (mode === 'children') {
|
|
||||||
return React.cloneElement(
|
|
||||||
element,
|
|
||||||
{
|
|
||||||
key: childValue,
|
|
||||||
ref: localRef,
|
|
||||||
className: cn('relative', element.props.className),
|
|
||||||
...getNonOverridingDataAttributes(element, {
|
|
||||||
...dataAttributes,
|
|
||||||
'data-slot': 'motion-highlight-item-container'
|
|
||||||
}),
|
|
||||||
...commonHandlers,
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
<>
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{isActive && !isDisabled && (
|
|
||||||
<motion.div
|
|
||||||
layoutId={`transition-background-${contextId}`}
|
|
||||||
data-slot='motion-highlight'
|
|
||||||
className={cn('bg-muted absolute inset-0 z-0', contextClassName, activeClassName)}
|
|
||||||
transition={itemTransition}
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
transition: {
|
|
||||||
...itemTransition,
|
|
||||||
delay: (itemTransition?.delay ?? 0) + (exitDelay ?? contextExitDelay ?? 0)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...dataAttributes}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
<div data-slot='motion-highlight-item' className={cn('relative z-[1]', className)} {...dataAttributes}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return React.cloneElement(element, {
|
|
||||||
ref: localRef,
|
|
||||||
...getNonOverridingDataAttributes(element, {
|
|
||||||
...dataAttributes,
|
|
||||||
'data-slot': 'motion-highlight-item'
|
|
||||||
}),
|
|
||||||
...commonHandlers
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return enabled ? (
|
|
||||||
<div
|
|
||||||
key={childValue}
|
|
||||||
ref={localRef}
|
|
||||||
data-slot='motion-highlight-item-container'
|
|
||||||
className={cn(mode === 'children' && 'relative', className)}
|
|
||||||
{...dataAttributes}
|
|
||||||
{...props}
|
|
||||||
{...commonHandlers}
|
|
||||||
>
|
|
||||||
{mode === 'children' && (
|
|
||||||
<AnimatePresence initial={false}>
|
|
||||||
{isActive && !isDisabled && (
|
|
||||||
<motion.div
|
|
||||||
layoutId={`transition-background-${contextId}`}
|
|
||||||
data-slot='motion-highlight'
|
|
||||||
className={cn('bg-muted absolute inset-0 z-0', contextClassName, activeClassName)}
|
|
||||||
transition={itemTransition}
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{
|
|
||||||
opacity: 0,
|
|
||||||
transition: {
|
|
||||||
...itemTransition,
|
|
||||||
delay: (itemTransition?.delay ?? 0) + (exitDelay ?? contextExitDelay ?? 0)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
{...dataAttributes}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{React.cloneElement(element, {
|
|
||||||
className: cn('relative z-[1]', element.props.className),
|
|
||||||
...getNonOverridingDataAttributes(element, {
|
|
||||||
...dataAttributes,
|
|
||||||
'data-slot': 'motion-highlight-item'
|
|
||||||
})
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
MotionHighlight,
|
|
||||||
MotionHighlightItem,
|
|
||||||
useMotionHighlight,
|
|
||||||
type MotionHighlightProps,
|
|
||||||
type MotionHighlightItemProps
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import * as React from 'react'
|
|
||||||
|
|
||||||
import { motion, type Transition, type HTMLMotionProps } from 'motion/react'
|
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { MotionHighlight, MotionHighlightItem } from '@/components/ui/motion-highlight'
|
|
||||||
|
|
||||||
type TabsContextType<T extends string> = {
|
|
||||||
activeValue: T
|
|
||||||
handleValueChange: (value: T) => void
|
|
||||||
registerTrigger: (value: T, node: HTMLElement | null) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const TabsContext = React.createContext<TabsContextType<any> | undefined>(undefined)
|
|
||||||
|
|
||||||
function useTabs<T extends string = string>(): TabsContextType<T> {
|
|
||||||
const context = React.useContext(TabsContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useTabs must be used within a TabsProvider')
|
|
||||||
}
|
|
||||||
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseTabsProps = React.ComponentProps<'div'> & {
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnControlledTabsProps<T extends string = string> = BaseTabsProps & {
|
|
||||||
defaultValue?: T
|
|
||||||
value?: never
|
|
||||||
onValueChange?: never
|
|
||||||
}
|
|
||||||
|
|
||||||
type ControlledTabsProps<T extends string = string> = BaseTabsProps & {
|
|
||||||
value: T
|
|
||||||
onValueChange?: (value: T) => void
|
|
||||||
defaultValue?: never
|
|
||||||
}
|
|
||||||
|
|
||||||
type TabsProps<T extends string = string> = UnControlledTabsProps<T> | ControlledTabsProps<T>
|
|
||||||
|
|
||||||
function Tabs<T extends string = string>({
|
|
||||||
defaultValue,
|
|
||||||
value,
|
|
||||||
onValueChange,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: TabsProps<T>) {
|
|
||||||
const [activeValue, setActiveValue] = React.useState<T | undefined>(defaultValue ?? undefined)
|
|
||||||
const triggersRef = React.useRef(new Map<string, HTMLElement>())
|
|
||||||
const initialSet = React.useRef(false)
|
|
||||||
const isControlled = value !== undefined
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!isControlled && activeValue === undefined && triggersRef.current.size > 0 && !initialSet.current) {
|
|
||||||
const firstTab = Array.from(triggersRef.current.keys())[0]
|
|
||||||
|
|
||||||
setActiveValue(firstTab as T)
|
|
||||||
initialSet.current = true
|
|
||||||
}
|
|
||||||
}, [activeValue, isControlled])
|
|
||||||
|
|
||||||
const registerTrigger = (value: string, node: HTMLElement | null) => {
|
|
||||||
if (node) {
|
|
||||||
triggersRef.current.set(value, node)
|
|
||||||
|
|
||||||
if (!isControlled && activeValue === undefined && !initialSet.current) {
|
|
||||||
setActiveValue(value as T)
|
|
||||||
initialSet.current = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
triggersRef.current.delete(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleValueChange = (val: T) => {
|
|
||||||
if (!isControlled) setActiveValue(val)
|
|
||||||
else onValueChange?.(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TabsContext.Provider
|
|
||||||
value={{
|
|
||||||
activeValue: (value ?? activeValue)!,
|
|
||||||
handleValueChange,
|
|
||||||
registerTrigger
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div data-slot='tabs' className={cn('flex flex-col gap-2', className)} {...props}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</TabsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TabsListProps = React.ComponentProps<'div'> & {
|
|
||||||
children: React.ReactNode
|
|
||||||
activeClassName?: string
|
|
||||||
transition?: Transition
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsList({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
activeClassName,
|
|
||||||
transition = {
|
|
||||||
type: 'spring',
|
|
||||||
stiffness: 200,
|
|
||||||
damping: 25
|
|
||||||
},
|
|
||||||
...props
|
|
||||||
}: TabsListProps) {
|
|
||||||
const { activeValue } = useTabs()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MotionHighlight
|
|
||||||
controlledItems
|
|
||||||
className={cn('bg-background rounded-sm shadow-sm', activeClassName)}
|
|
||||||
value={activeValue}
|
|
||||||
transition={transition}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
role='tablist'
|
|
||||||
data-slot='tabs-list'
|
|
||||||
className={cn(
|
|
||||||
'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-[4px]',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</MotionHighlight>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TabsTriggerProps = HTMLMotionProps<'button'> & {
|
|
||||||
value: string
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsTrigger({ ref, value, children, className, ...props }: TabsTriggerProps) {
|
|
||||||
const { activeValue, handleValueChange, registerTrigger } = useTabs()
|
|
||||||
|
|
||||||
const localRef = React.useRef<HTMLButtonElement | null>(null)
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
registerTrigger(value, localRef.current)
|
|
||||||
|
|
||||||
return () => registerTrigger(value, null)
|
|
||||||
}, [value, registerTrigger])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MotionHighlightItem value={value} className='size-full'>
|
|
||||||
<motion.button
|
|
||||||
ref={localRef}
|
|
||||||
data-slot='tabs-trigger'
|
|
||||||
role='tab'
|
|
||||||
onClick={() => handleValueChange(value)}
|
|
||||||
data-state={activeValue === value ? 'active' : 'inactive'}
|
|
||||||
className={cn(
|
|
||||||
'ring-offset-background focus-visible:ring-ring data-[state=active]:text-foreground z-[1] inline-flex size-full cursor-pointer items-center justify-center rounded-sm px-2 py-1 text-sm font-medium whitespace-nowrap transition-transform focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</motion.button>
|
|
||||||
</MotionHighlightItem>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TabsContentsProps = React.ComponentProps<'div'> & {
|
|
||||||
children: React.ReactNode
|
|
||||||
transition?: Transition
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsContents({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
transition = {
|
|
||||||
type: 'spring',
|
|
||||||
stiffness: 300,
|
|
||||||
damping: 30,
|
|
||||||
bounce: 0,
|
|
||||||
restDelta: 0.01
|
|
||||||
},
|
|
||||||
...props
|
|
||||||
}: TabsContentsProps) {
|
|
||||||
const { activeValue } = useTabs()
|
|
||||||
const childrenArray = React.Children.toArray(children)
|
|
||||||
|
|
||||||
const activeIndex = childrenArray.findIndex(
|
|
||||||
(child): child is React.ReactElement<{ value: string }> =>
|
|
||||||
React.isValidElement(child) &&
|
|
||||||
typeof child.props === 'object' &&
|
|
||||||
child.props !== null &&
|
|
||||||
'value' in child.props &&
|
|
||||||
child.props.value === activeValue
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div data-slot='tabs-contents' className={cn('overflow-hidden', className)} {...props}>
|
|
||||||
<motion.div className='-mx-2 flex' animate={{ x: activeIndex * -100 + '%' }} transition={transition}>
|
|
||||||
{childrenArray.map((child, index) => (
|
|
||||||
<div key={index} className='w-full shrink-0 px-2'>
|
|
||||||
{child}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TabsContentProps = HTMLMotionProps<'div'> & {
|
|
||||||
value: string
|
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
function TabsContent({ children, value, className, ...props }: TabsContentProps) {
|
|
||||||
const { activeValue } = useTabs()
|
|
||||||
const isActive = activeValue === value
|
|
||||||
|
|
||||||
return (
|
|
||||||
<motion.div
|
|
||||||
role='tabpanel'
|
|
||||||
data-slot='tabs-content'
|
|
||||||
className={cn('overflow-hidden', className)}
|
|
||||||
initial={{ filter: 'blur(0px)' }}
|
|
||||||
animate={{ filter: isActive ? 'blur(0px)' : 'blur(2px)' }}
|
|
||||||
exit={{ filter: 'blur(0px)' }}
|
|
||||||
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</motion.div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
Tabs,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
TabsContents,
|
|
||||||
TabsContent,
|
|
||||||
useTabs,
|
|
||||||
type TabsContextType,
|
|
||||||
type TabsProps,
|
|
||||||
type TabsListProps,
|
|
||||||
type TabsTriggerProps,
|
|
||||||
type TabsContentsProps,
|
|
||||||
type TabsContentProps
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { CircleIcon } from "lucide-react"
|
|
||||||
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function RadioGroup({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<RadioGroupPrimitive.Root
|
|
||||||
data-slot="radio-group"
|
|
||||||
className={cn("grid gap-3", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function RadioGroupItem({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
|
||||||
return (
|
|
||||||
<RadioGroupPrimitive.Item
|
|
||||||
data-slot="radio-group-item"
|
|
||||||
className={cn(
|
|
||||||
"aspect-square size-4 shrink-0 rounded-full border border-input text-primary shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<RadioGroupPrimitive.Indicator
|
|
||||||
data-slot="radio-group-indicator"
|
|
||||||
className="relative flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<CircleIcon className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 fill-primary" />
|
|
||||||
</RadioGroupPrimitive.Indicator>
|
|
||||||
</RadioGroupPrimitive.Item>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { RadioGroup, RadioGroupItem }
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-slot="skeleton"
|
|
||||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Skeleton }
|
|
||||||
@@ -1,24 +1,18 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react"
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const Textarea = React.forwardRef<
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
HTMLTextAreaElement,
|
|
||||||
React.ComponentProps<'textarea'>
|
|
||||||
>(({ className, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<textarea
|
<textarea
|
||||||
ref={ref}
|
|
||||||
data-slot="textarea"
|
data-slot="textarea"
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
Textarea.displayName = 'Textarea'
|
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea }
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from '@/components/ui/tooltip'
|
|
||||||
|
|
||||||
export function StepWithTooltip({
|
|
||||||
title,
|
|
||||||
desc,
|
|
||||||
}: {
|
|
||||||
title: string
|
|
||||||
desc: string
|
|
||||||
}) {
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span
|
|
||||||
className="cursor-help decoration-dotted underline-offset-4 hover:underline"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setIsOpen((prev) => !prev)
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setIsOpen(true)}
|
|
||||||
onMouseLeave={() => setIsOpen(false)}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="max-w-50 text-xs">
|
|
||||||
<p>{desc}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import * as Icons from 'lucide-react'
|
|
||||||
|
|
||||||
import { CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
|
||||||
|
|
||||||
export function WizardLayout({
|
|
||||||
title,
|
|
||||||
onClose,
|
|
||||||
headerSlot,
|
|
||||||
footerSlot,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
title: string
|
|
||||||
onClose: () => void
|
|
||||||
headerSlot?: React.ReactNode
|
|
||||||
footerSlot?: React.ReactNode
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Dialog open={true} onOpenChange={(open) => !open && onClose()}>
|
|
||||||
<DialogContent
|
|
||||||
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"
|
|
||||||
onInteractOutside={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="z-10 flex-none border-b bg-white">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-4 p-6 pb-4">
|
|
||||||
<CardTitle>{title}</CardTitle>
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none"
|
|
||||||
>
|
|
||||||
<Icons.X className="h-4 w-4" />
|
|
||||||
<span className="sr-only">Cerrar</span>
|
|
||||||
</button>
|
|
||||||
</CardHeader>
|
|
||||||
|
|
||||||
{headerSlot ? <div className="px-6 pb-6">{headerSlot}</div> : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{footerSlot ? (
|
|
||||||
<div className="flex-none border-t bg-white p-6">{footerSlot}</div>
|
|
||||||
) : null}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { CircularProgress } from '@/components/CircularProgress'
|
|
||||||
import { StepWithTooltip } from '@/components/wizard/StepWithTooltip'
|
|
||||||
|
|
||||||
export function WizardResponsiveHeader({
|
|
||||||
wizard,
|
|
||||||
methods,
|
|
||||||
titleOverrides,
|
|
||||||
hiddenStepIds,
|
|
||||||
}: {
|
|
||||||
wizard: any
|
|
||||||
methods: any
|
|
||||||
titleOverrides?: Record<string, string>
|
|
||||||
hiddenStepIds?: Array<string>
|
|
||||||
}) {
|
|
||||||
const hidden = new Set(hiddenStepIds ?? [])
|
|
||||||
const visibleSteps = (wizard.steps as Array<any>).filter(
|
|
||||||
(s) => s && !hidden.has(s.id),
|
|
||||||
)
|
|
||||||
|
|
||||||
const idx = visibleSteps.findIndex((s) => s.id === methods.current.id)
|
|
||||||
const safeIdx = idx >= 0 ? idx : 0
|
|
||||||
const totalSteps = visibleSteps.length
|
|
||||||
const currentIndex = Math.min(safeIdx + 1, totalSteps)
|
|
||||||
const hasNextStep = safeIdx < totalSteps - 1
|
|
||||||
const nextStep = visibleSteps[safeIdx + 1]
|
|
||||||
|
|
||||||
const resolveTitle = (step: any) => titleOverrides?.[step?.id] ?? step?.title
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="block sm:hidden">
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<CircularProgress current={currentIndex} total={totalSteps} />
|
|
||||||
<div className="flex flex-col justify-center">
|
|
||||||
<h2 className="text-lg font-bold text-slate-900">
|
|
||||||
<StepWithTooltip
|
|
||||||
title={resolveTitle(methods.current)}
|
|
||||||
desc={methods.current.description}
|
|
||||||
/>
|
|
||||||
</h2>
|
|
||||||
{hasNextStep && nextStep ? (
|
|
||||||
<p className="text-sm text-slate-400">
|
|
||||||
Siguiente: {resolveTitle(nextStep)}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm font-medium text-green-500">
|
|
||||||
¡Último paso!
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="hidden sm:block">
|
|
||||||
<wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2">
|
|
||||||
{visibleSteps.map((step: any, visibleIdx: number) => (
|
|
||||||
<wizard.Stepper.Step
|
|
||||||
key={step.id}
|
|
||||||
of={step.id}
|
|
||||||
icon={visibleIdx + 1}
|
|
||||||
className="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<wizard.Stepper.Title>
|
|
||||||
<StepWithTooltip
|
|
||||||
title={resolveTitle(step)}
|
|
||||||
desc={step.description}
|
|
||||||
/>
|
|
||||||
</wizard.Stepper.Title>
|
|
||||||
</wizard.Stepper.Step>
|
|
||||||
))}
|
|
||||||
</wizard.Stepper.Navigation>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,56 +1,45 @@
|
|||||||
import type { Database } from '../types/database'
|
import type { PostgrestError, AuthError, SupabaseClient } from "@supabase/supabase-js";
|
||||||
import type {
|
import type { Database } from "../types/database";
|
||||||
PostgrestError,
|
|
||||||
AuthError,
|
|
||||||
SupabaseClient,
|
|
||||||
} from '@supabase/supabase-js'
|
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
public readonly code?: string,
|
public readonly code?: string,
|
||||||
public readonly details?: unknown,
|
public readonly details?: unknown,
|
||||||
public readonly hint?: string,
|
public readonly hint?: string
|
||||||
) {
|
) {
|
||||||
super(message)
|
super(message);
|
||||||
this.name = 'ApiError'
|
this.name = "ApiError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function throwIfError(error: PostgrestError | AuthError | null): void {
|
export function throwIfError(error: PostgrestError | AuthError | null): void {
|
||||||
if (!error) return
|
if (!error) return;
|
||||||
const anyErr = error as any
|
|
||||||
|
const anyErr = error as any;
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
anyErr.message ?? 'Error inesperado',
|
anyErr.message ?? "Error inesperado",
|
||||||
anyErr.code,
|
anyErr.code,
|
||||||
anyErr.details,
|
anyErr.details,
|
||||||
anyErr.hint,
|
anyErr.hint
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requireData<T>(
|
export function requireData<T>(data: T | null | undefined, message = "Respuesta vacía"): T {
|
||||||
data: T | null | undefined,
|
if (data === null || data === undefined) throw new ApiError(message);
|
||||||
message = 'Respuesta vacía',
|
return data;
|
||||||
): T {
|
|
||||||
if (data === null || data === undefined) throw new ApiError(message)
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserIdOrThrow(
|
export async function getUserIdOrThrow(supabase: SupabaseClient<Database>): Promise<string> {
|
||||||
supabase: SupabaseClient<Database>,
|
const { data, error } = await supabase.auth.getUser();
|
||||||
): Promise<string> {
|
throwIfError(error);
|
||||||
const { data, error } = await supabase.auth.getUser()
|
if (!data?.user?.id) throw new ApiError("No hay sesión activa (auth).");
|
||||||
throwIfError(error)
|
return data.user.id;
|
||||||
if (!data?.user?.id) throw new ApiError('No hay sesión activa (auth).')
|
|
||||||
return data.user.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildRange(
|
export function buildRange(limit?: number, offset?: number): { from?: number; to?: number } {
|
||||||
limit?: number,
|
if (!limit) return {};
|
||||||
offset?: number,
|
const from = Math.max(0, offset ?? 0);
|
||||||
): { from?: number; to?: number } {
|
const to = from + Math.max(1, limit) - 1;
|
||||||
if (!limit) return {}
|
return { from, to };
|
||||||
const from = Math.max(0, offset ?? 0)
|
|
||||||
const to = from + Math.max(1, limit) - 1
|
|
||||||
return { from, to }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,377 +1,81 @@
|
|||||||
import { supabaseBrowser } from '../supabase/client'
|
import { invokeEdge } from "../supabase/invokeEdge";
|
||||||
import { invokeEdge } from '../supabase/invokeEdge'
|
import type { InteraccionIA, UUID } from "../types/domain";
|
||||||
|
|
||||||
import type { InteraccionIA, UUID } from '../types/domain'
|
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
ai_plan_improve: 'ai_plan_improve',
|
ai_plan_improve: "ai_plan_improve",
|
||||||
ai_plan_chat: 'ai_plan_chat',
|
ai_plan_chat: "ai_plan_chat",
|
||||||
ai_subject_improve: 'ai_subject_improve',
|
ai_subject_improve: "ai_subject_improve",
|
||||||
ai_subject_chat: 'ai_subject_chat',
|
ai_subject_chat: "ai_subject_chat",
|
||||||
|
|
||||||
library_search: 'library_search',
|
library_search: "library_search",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export async function ai_plan_improve(payload: {
|
export async function ai_plan_improve(payload: {
|
||||||
planId: UUID
|
planId: UUID;
|
||||||
sectionKey: string // ej: "perfil_de_egreso" o tu key interna
|
sectionKey: string; // ej: "perfil_de_egreso" o tu key interna
|
||||||
prompt: string
|
prompt: string;
|
||||||
context?: Record<string, any>
|
context?: Record<string, any>;
|
||||||
fuentes?: {
|
fuentes?: {
|
||||||
archivosIds?: Array<UUID>
|
archivosIds?: UUID[];
|
||||||
vectorStoresIds?: Array<UUID>
|
vectorStoresIds?: UUID[];
|
||||||
usarMCP?: boolean
|
usarMCP?: boolean;
|
||||||
conversacionId?: string
|
conversacionId?: string;
|
||||||
}
|
};
|
||||||
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
|
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
|
||||||
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(
|
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(EDGE.ai_plan_improve, payload);
|
||||||
EDGE.ai_plan_improve,
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ai_plan_chat(payload: {
|
export async function ai_plan_chat(payload: {
|
||||||
planId: UUID
|
planId: UUID;
|
||||||
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>
|
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
|
||||||
fuentes?: {
|
fuentes?: {
|
||||||
archivosIds?: Array<UUID>
|
archivosIds?: UUID[];
|
||||||
vectorStoresIds?: Array<UUID>
|
vectorStoresIds?: UUID[];
|
||||||
usarMCP?: boolean
|
usarMCP?: boolean;
|
||||||
conversacionId?: string
|
conversacionId?: string;
|
||||||
}
|
};
|
||||||
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
|
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
|
||||||
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(
|
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(EDGE.ai_plan_chat, payload);
|
||||||
EDGE.ai_plan_chat,
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ai_subject_improve(payload: {
|
export async function ai_subject_improve(payload: {
|
||||||
subjectId: UUID
|
subjectId: UUID;
|
||||||
sectionKey: string
|
sectionKey: string;
|
||||||
prompt: string
|
prompt: string;
|
||||||
context?: Record<string, any>
|
context?: Record<string, any>;
|
||||||
fuentes?: {
|
fuentes?: {
|
||||||
archivosIds?: Array<UUID>
|
archivosIds?: UUID[];
|
||||||
vectorStoresIds?: Array<UUID>
|
vectorStoresIds?: UUID[];
|
||||||
usarMCP?: boolean
|
usarMCP?: boolean;
|
||||||
conversacionId?: string
|
conversacionId?: string;
|
||||||
}
|
};
|
||||||
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
|
}): Promise<{ interaccion: InteraccionIA; propuesta: any }> {
|
||||||
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(
|
return invokeEdge<{ interaccion: InteraccionIA; propuesta: any }>(EDGE.ai_subject_improve, payload);
|
||||||
EDGE.ai_subject_improve,
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ai_subject_chat(payload: {
|
export async function ai_subject_chat(payload: {
|
||||||
subjectId: UUID
|
subjectId: UUID;
|
||||||
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>
|
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
|
||||||
fuentes?: {
|
fuentes?: {
|
||||||
archivosIds?: Array<UUID>
|
archivosIds?: UUID[];
|
||||||
vectorStoresIds?: Array<UUID>
|
vectorStoresIds?: UUID[];
|
||||||
usarMCP?: boolean
|
usarMCP?: boolean;
|
||||||
conversacionId?: string
|
conversacionId?: string;
|
||||||
}
|
};
|
||||||
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
|
}): Promise<{ interaccion: InteraccionIA; reply: string; meta?: any }> {
|
||||||
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(
|
return invokeEdge<{ interaccion: InteraccionIA; reply: string; meta?: any }>(EDGE.ai_subject_chat, payload);
|
||||||
EDGE.ai_subject_chat,
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Biblioteca (Edge; adapta a tu API real) */
|
/** Biblioteca (Edge; adapta a tu API real) */
|
||||||
export type LibraryItem = {
|
export type LibraryItem = {
|
||||||
id: string
|
id: string;
|
||||||
titulo: string
|
titulo: string;
|
||||||
autor?: string
|
autor?: string;
|
||||||
isbn?: string
|
isbn?: string;
|
||||||
citaSugerida?: string
|
citaSugerida?: string;
|
||||||
disponibilidad?: string
|
disponibilidad?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function library_search(payload: {
|
export async function library_search(payload: { query: string; limit?: number }): Promise<LibraryItem[]> {
|
||||||
query: string
|
return invokeEdge<LibraryItem[]>(EDGE.library_search, payload);
|
||||||
limit?: number
|
|
||||||
}): Promise<Array<LibraryItem>> {
|
|
||||||
return invokeEdge<Array<LibraryItem>>(EDGE.library_search, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function create_conversation(planId: string) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase.functions.invoke(
|
|
||||||
'create-chat-conversation/plan/conversations',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
plan_estudio_id: planId, // O el nombre que confirmamos que funciona
|
|
||||||
instanciador: 'alex',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function get_chat_history(conversacionId: string) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase.functions.invoke(
|
|
||||||
`create-chat-conversation/conversations/${conversacionId}/messages`,
|
|
||||||
{ method: 'GET' },
|
|
||||||
)
|
|
||||||
if (error) throw error
|
|
||||||
return data // Retorna Array de mensajes
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function update_conversation_status(
|
|
||||||
conversacionId: string,
|
|
||||||
nuevoEstado: 'ARCHIVADA' | 'ACTIVA',
|
|
||||||
) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('conversaciones_plan') // Asegúrate que el nombre de la tabla sea exacto
|
|
||||||
.update({ estado: nuevoEstado })
|
|
||||||
.eq('id', conversacionId)
|
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modificamos la función de chat para que use la ruta de mensajes
|
|
||||||
export async function ai_plan_chat_v2(payload: {
|
|
||||||
conversacionId: string
|
|
||||||
content: string
|
|
||||||
campos?: Array<string>
|
|
||||||
}): Promise<{ reply: string; meta?: any }> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase.functions.invoke(
|
|
||||||
`create-chat-conversation/conversations/plan/${payload.conversacionId}/messages`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
content: payload.content,
|
|
||||||
campos: payload.campos || [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getConversationByPlan(planId: string) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('conversaciones_plan')
|
|
||||||
.select('*')
|
|
||||||
.eq('plan_estudio_id', planId)
|
|
||||||
.order('creado_en', { ascending: false })
|
|
||||||
if (error) throw error
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
||||||
return data ?? []
|
|
||||||
}
|
|
||||||
export async function getMessagesByConversation(conversationId: string) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('plan_mensajes_ia')
|
|
||||||
.select('*')
|
|
||||||
.eq('conversacion_plan_id', conversationId)
|
|
||||||
.order('fecha_creacion', { ascending: true }) // Ascendente para que el chat fluya en orden cronológico
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error('Error al obtener mensajes:', error.message)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return data ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function update_conversation_title(
|
|
||||||
conversacionId: string,
|
|
||||||
nuevoTitulo: string,
|
|
||||||
) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('conversaciones_plan')
|
|
||||||
.update({ nombre: nuevoTitulo }) // Asegúrate que la columna se llame 'title' o 'nombre'
|
|
||||||
.eq('id', conversacionId)
|
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function update_recommendation_applied_status(
|
|
||||||
mensajeId: string, // Ahora es más eficiente usar el ID del mensaje directamente
|
|
||||||
campoAfectado: string,
|
|
||||||
) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
// 1. Obtener la propuesta actual de ese mensaje específico
|
|
||||||
const { data: msgData, error: fetchError } = await supabase
|
|
||||||
.from('plan_mensajes_ia')
|
|
||||||
.select('propuesta')
|
|
||||||
.eq('id', mensajeId)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (fetchError) throw fetchError
|
|
||||||
if (!msgData?.propuesta)
|
|
||||||
throw new Error('No se encontró la propuesta en el mensaje')
|
|
||||||
|
|
||||||
const propuestaActual = msgData.propuesta as any
|
|
||||||
|
|
||||||
// 2. Modificar el array de recommendations dentro de la propuesta
|
|
||||||
// Mantenemos el resto de la propuesta (prompt, respuesta, etc.) intacto
|
|
||||||
const nuevaPropuesta = {
|
|
||||||
...propuestaActual,
|
|
||||||
recommendations: (propuestaActual.recommendations || []).map((rec: any) =>
|
|
||||||
rec.campo_afectado === campoAfectado ? { ...rec, aplicada: true } : rec,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Actualizar la base de datos con el nuevo objeto JSON
|
|
||||||
const { error: updateError } = await supabase
|
|
||||||
.from('plan_mensajes_ia')
|
|
||||||
.update({ propuesta: nuevaPropuesta })
|
|
||||||
.eq('id', mensajeId)
|
|
||||||
|
|
||||||
if (updateError) throw updateError
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- FUNCIONES DE ASIGNATURA ---
|
|
||||||
|
|
||||||
export async function create_subject_conversation(subjectId: string) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase.functions.invoke(
|
|
||||||
'create-chat-conversation/asignatura/conversations', // Ruta corregida
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
asignatura_id: subjectId,
|
|
||||||
instanciador: 'alex',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (error) throw error
|
|
||||||
return data // Retorna { conversation_asignatura: { id, ... } }
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ai_subject_chat_v2(payload: {
|
|
||||||
conversacionId: string
|
|
||||||
content: string
|
|
||||||
campos?: Array<string>
|
|
||||||
}) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase.functions.invoke(
|
|
||||||
`create-chat-conversation/conversations/asignatura/${payload.conversacionId}/messages`, // Ruta corregida
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: {
|
|
||||||
content: payload.content,
|
|
||||||
campos: payload.campos || [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getConversationBySubject(subjectId: string) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('conversaciones_asignatura') // Tabla corregida
|
|
||||||
.select('*')
|
|
||||||
.eq('asignatura_id', subjectId)
|
|
||||||
.order('creado_en', { ascending: false })
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getMessagesBySubjectConversation(conversationId: string) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('asignatura_mensajes_ia' as any)
|
|
||||||
.select('*')
|
|
||||||
.eq('conversacion_asignatura_id', conversationId)
|
|
||||||
.order('fecha_creacion', { ascending: true })
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function update_subject_recommendation_applied(
|
|
||||||
mensajeId: string,
|
|
||||||
campoAfectado: string,
|
|
||||||
) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
// 1. Obtener propuesta actual
|
|
||||||
const { data: msgData, error: fetchError } = await supabase
|
|
||||||
.from('asignatura_mensajes_ia')
|
|
||||||
.select('propuesta')
|
|
||||||
.eq('id', mensajeId)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (fetchError) throw fetchError
|
|
||||||
const propuestaActual = msgData?.propuesta as any
|
|
||||||
|
|
||||||
// 2. Marcar como aplicada
|
|
||||||
const nuevaPropuesta = {
|
|
||||||
...propuestaActual,
|
|
||||||
recommendations: (propuestaActual.recommendations || []).map((rec: any) =>
|
|
||||||
rec.campo_afectado === campoAfectado ? { ...rec, aplicada: true } : rec,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Update
|
|
||||||
const { error: updateError } = await supabase
|
|
||||||
.from('asignatura_mensajes_ia')
|
|
||||||
.update({ propuesta: nuevaPropuesta })
|
|
||||||
.eq('id', mensajeId)
|
|
||||||
|
|
||||||
if (updateError) throw updateError
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function update_subject_conversation_status(
|
|
||||||
conversacionId: string,
|
|
||||||
nuevoEstado: 'ARCHIVADA' | 'ACTIVA',
|
|
||||||
) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('conversaciones_asignatura')
|
|
||||||
.update({ estado: nuevoEstado })
|
|
||||||
.eq('id', conversacionId)
|
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function update_subject_conversation_name(
|
|
||||||
conversacionId: string,
|
|
||||||
nuevoNombre: string,
|
|
||||||
) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('conversaciones_asignatura')
|
|
||||||
.update({ nombre: nuevoNombre }) // Asumiendo que la columna es 'titulo' según tu código previo, o cambia a 'nombre'
|
|
||||||
.eq('id', conversacionId)
|
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
// document.api.ts
|
|
||||||
|
|
||||||
import { supabaseBrowser } from '../supabase/client'
|
|
||||||
import { invokeEdge } from '../supabase/invokeEdge'
|
|
||||||
|
|
||||||
import { requireData, throwIfError } from './_helpers'
|
|
||||||
|
|
||||||
import type { Tables } from '@/types/supabase'
|
|
||||||
|
|
||||||
const EDGE = {
|
|
||||||
carbone_io_wrapper: 'carbone-io-wrapper',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
interface GeneratePdfParams {
|
|
||||||
plan_estudio_id: string
|
|
||||||
convertTo?: 'pdf'
|
|
||||||
}
|
|
||||||
interface GeneratePdfParamsAsignatura {
|
|
||||||
asignatura_id: string
|
|
||||||
convertTo?: 'pdf'
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchPlanPdf({
|
|
||||||
plan_estudio_id,
|
|
||||||
convertTo,
|
|
||||||
}: GeneratePdfParams): Promise<Blob> {
|
|
||||||
return await invokeEdge<Blob>(
|
|
||||||
EDGE.carbone_io_wrapper,
|
|
||||||
{
|
|
||||||
action: 'downloadReport',
|
|
||||||
plan_estudio_id,
|
|
||||||
body: convertTo ? { convertTo } : {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
responseType: 'blob',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchAsignaturaPdf({
|
|
||||||
asignatura_id,
|
|
||||||
convertTo,
|
|
||||||
}: GeneratePdfParamsAsignatura): Promise<Blob> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('asignaturas')
|
|
||||||
.select('*')
|
|
||||||
.eq('id', asignatura_id)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
throwIfError(error)
|
|
||||||
|
|
||||||
const row = requireData(
|
|
||||||
data as Pick<
|
|
||||||
Tables<'asignaturas'>,
|
|
||||||
'datos' | 'contenido_tematico' | 'criterios_de_evaluacion'
|
|
||||||
>,
|
|
||||||
'Asignatura no encontrada',
|
|
||||||
)
|
|
||||||
|
|
||||||
const body: Record<string, unknown> = {
|
|
||||||
data: row,
|
|
||||||
}
|
|
||||||
if (convertTo) body.convertTo = convertTo
|
|
||||||
|
|
||||||
return await invokeEdge<Blob>(
|
|
||||||
EDGE.carbone_io_wrapper,
|
|
||||||
{
|
|
||||||
action: 'downloadReport',
|
|
||||||
asignatura_id,
|
|
||||||
body: {
|
|
||||||
...body,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
responseType: 'blob',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
import { invokeEdge } from '../supabase/invokeEdge'
|
import { invokeEdge } from "../supabase/invokeEdge";
|
||||||
import type { UUID } from '../types/domain'
|
import type { UUID } from "../types/domain";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata “canónica” para UI (archivo OpenAI + espejo en Supabase)
|
* Metadata “canónica” para UI (archivo OpenAI + espejo en Supabase)
|
||||||
* Se apoya en tu tabla `archivos`.
|
* Se apoya en tu tabla `archivos`.
|
||||||
*/
|
*/
|
||||||
export type AppFile = {
|
export type AppFile = {
|
||||||
id: UUID // id interno (tabla archivos)
|
id: UUID; // id interno (tabla archivos)
|
||||||
openai_file_id: string // id OpenAI
|
openai_file_id: string; // id OpenAI
|
||||||
nombre: string
|
nombre: string;
|
||||||
mime_type: string | null
|
mime_type: string | null;
|
||||||
bytes: number | null
|
bytes: number | null;
|
||||||
|
|
||||||
// espejo Supabase para preview/descarga
|
// espejo Supabase para preview/descarga
|
||||||
ruta_storage: string | null // "bucket/path"
|
ruta_storage: string | null; // "bucket/path"
|
||||||
signed_url?: string | null
|
signed_url?: string | null;
|
||||||
|
|
||||||
// auditoría/evidencia
|
// auditoría/evidencia
|
||||||
temporal: boolean
|
temporal: boolean;
|
||||||
notas?: string | null
|
notas?: string | null;
|
||||||
|
|
||||||
subido_en: string
|
subido_en: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
upload: 'openai_files_upload',
|
upload: "openai_files_upload",
|
||||||
remove: 'openai_files_delete',
|
remove: "openai_files_delete",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sube archivo a OpenAI y (opcional) crea espejo en Storage
|
* Sube archivo a OpenAI y (opcional) crea espejo en Storage
|
||||||
@@ -37,28 +37,28 @@ export async function openai_files_upload(payload: {
|
|||||||
* Si tu Edge soporta multipart: manda File/Blob directo.
|
* Si tu Edge soporta multipart: manda File/Blob directo.
|
||||||
* Si no, manda base64/bytes (según tu implementación).
|
* Si no, manda base64/bytes (según tu implementación).
|
||||||
*/
|
*/
|
||||||
file: File
|
file: File;
|
||||||
|
|
||||||
/** “temporal” = evidencia usada para generar plan/asignatura */
|
/** “temporal” = evidencia usada para generar plan/materia */
|
||||||
temporal?: boolean
|
temporal?: boolean;
|
||||||
|
|
||||||
/** contexto para auditoría */
|
/** contexto para auditoría */
|
||||||
contexto?: {
|
contexto?: {
|
||||||
planId?: UUID
|
planId?: UUID;
|
||||||
asignaturaId?: UUID
|
asignaturaId?: UUID;
|
||||||
motivo?: 'WIZARD_PLAN' | 'WIZARD_MATERIA' | 'ADHOC'
|
motivo?: "WIZARD_PLAN" | "WIZARD_MATERIA" | "ADHOC";
|
||||||
}
|
};
|
||||||
|
|
||||||
/** si quieres forzar espejo para preview siempre */
|
/** si quieres forzar espejo para preview siempre */
|
||||||
mirrorToSupabase?: boolean
|
mirrorToSupabase?: boolean;
|
||||||
}): Promise<AppFile> {
|
}): Promise<AppFile> {
|
||||||
return invokeEdge<AppFile>(EDGE.upload, payload)
|
return invokeEdge<AppFile>(EDGE.upload, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openai_files_delete(payload: {
|
export async function openai_files_delete(payload: {
|
||||||
openaiFileId: string
|
openaiFileId: string;
|
||||||
/** si quieres borrar también espejo y registro */
|
/** si quieres borrar también espejo y registro */
|
||||||
hardDelete?: boolean
|
hardDelete?: boolean;
|
||||||
}): Promise<{ ok: true }> {
|
}): Promise<{ ok: true }> {
|
||||||
return invokeEdge<{ ok: true }>(EDGE.remove, payload)
|
return invokeEdge<{ ok: true }>(EDGE.remove, payload);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { supabaseBrowser } from '../supabase/client'
|
import { supabaseBrowser } from "../supabase/client";
|
||||||
import { invokeEdge } from '../supabase/invokeEdge'
|
import { invokeEdge } from "../supabase/invokeEdge";
|
||||||
|
|
||||||
import { buildRange, requireData, throwIfError } from './_helpers'
|
import { buildRange, requireData, throwIfError } from "./_helpers";
|
||||||
|
|
||||||
import type { Database } from '../../types/supabase'
|
|
||||||
import type {
|
import type {
|
||||||
Asignatura,
|
Asignatura,
|
||||||
CambioPlan,
|
CambioPlan,
|
||||||
@@ -14,121 +13,89 @@ import type {
|
|||||||
PlanEstudio,
|
PlanEstudio,
|
||||||
TipoCiclo,
|
TipoCiclo,
|
||||||
UUID,
|
UUID,
|
||||||
} from '../types/domain'
|
} from "../types/domain";
|
||||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
plans_create_manual: 'plans_create_manual',
|
plans_create_manual: "plans_create_manual",
|
||||||
ai_generate_plan: 'ai-generate-plan',
|
ai_generate_plan: "ai_generate_plan",
|
||||||
plans_persist_from_ai: 'plans_persist_from_ai',
|
plans_persist_from_ai: "plans_persist_from_ai",
|
||||||
plans_clone_from_existing: 'plans_clone_from_existing',
|
plans_clone_from_existing: "plans_clone_from_existing",
|
||||||
|
|
||||||
plans_import_from_files: 'plans_import_from_files',
|
plans_import_from_files: "plans_import_from_files",
|
||||||
|
|
||||||
// plans_update_fields: 'plans_update_fields',
|
plans_update_fields: "plans_update_fields",
|
||||||
plans_update_map: 'plans_update_map',
|
plans_update_map: "plans_update_map",
|
||||||
plans_transition_state: 'plans_transition_state',
|
plans_transition_state: "plans_transition_state",
|
||||||
|
|
||||||
plans_generate_document: 'plans_generate_document',
|
plans_generate_document: "plans_generate_document",
|
||||||
plans_get_document: 'plans_get_document',
|
plans_get_document: "plans_get_document",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type PlanListFilters = {
|
export type PlanListFilters = {
|
||||||
search?: string
|
search?: string;
|
||||||
carreraId?: UUID
|
carreraId?: UUID;
|
||||||
facultadId?: UUID // filtra por carreras.facultad_id
|
facultadId?: UUID; // filtra por carreras.facultad_id
|
||||||
estadoId?: UUID
|
estadoId?: UUID;
|
||||||
activo?: boolean
|
activo?: boolean;
|
||||||
|
|
||||||
limit?: number
|
limit?: number;
|
||||||
offset?: number
|
offset?: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Helper para limpiar texto (lo movemos fuera para reutilizar o lo dejas en un utils)
|
|
||||||
const cleanText = (text: string) => {
|
|
||||||
return text
|
|
||||||
.normalize('NFD')
|
|
||||||
.replace(/[\u0300-\u036f]/g, '')
|
|
||||||
.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function plans_list(
|
export async function plans_list(
|
||||||
filters: PlanListFilters = {},
|
filters: PlanListFilters = {},
|
||||||
): Promise<Paged<PlanEstudio>> {
|
): Promise<Paged<PlanEstudio>> {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser();
|
||||||
|
|
||||||
// 1. Construimos la query base
|
|
||||||
// NOTA IMPORTANTE: Para filtrar planes basados en facultad (que está en carreras),
|
|
||||||
// necesitamos hacer un INNER JOIN. En Supabase se usa "!inner".
|
|
||||||
// Si filters.facultadId existe, forzamos el inner join, si no, lo dejamos normal.
|
|
||||||
|
|
||||||
const carreraModifier =
|
|
||||||
filters.facultadId && filters.facultadId !== 'todas' ? '!inner' : ''
|
|
||||||
|
|
||||||
|
// 1. Construimos la query.
|
||||||
|
// TypeScript validará que "planes_estudio" existe en Database
|
||||||
let q = supabase
|
let q = supabase
|
||||||
.from('planes_estudio')
|
.from("planes_estudio")
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
*,
|
*,
|
||||||
carreras${carreraModifier} (
|
carreras (
|
||||||
*,
|
*,
|
||||||
facultades (*)
|
facultades (*)
|
||||||
),
|
),
|
||||||
estructuras_plan (*),
|
estructuras_plan (*),
|
||||||
estados_plan (*)
|
estados_plan (*)
|
||||||
`,
|
`,
|
||||||
{ count: 'exact' },
|
{ count: "exact" },
|
||||||
)
|
)
|
||||||
.order('creado_en', { ascending: false })
|
.order("actualizado_en", { ascending: false });
|
||||||
|
|
||||||
// 2. Aplicamos filtros dinámicos
|
// 2. Aplicamos filtros dinámicos
|
||||||
|
|
||||||
// SOLUCIÓN SEARCH: Limpiamos el input y buscamos en la columna generada
|
|
||||||
if (filters.search?.trim()) {
|
if (filters.search?.trim()) {
|
||||||
const cleanTerm = cleanText(filters.search.trim())
|
q = q.ilike("nombre", `%${filters.search.trim()}%`);
|
||||||
// Usamos la columna nueva creada en el Paso 1
|
|
||||||
q = q.ilike('nombre_search', `%${cleanTerm}%`)
|
|
||||||
}
|
}
|
||||||
|
if (filters.carreraId) q = q.eq("carrera_id", filters.carreraId);
|
||||||
|
if (filters.estadoId) q = q.eq("estado_actual_id", filters.estadoId);
|
||||||
|
if (typeof filters.activo === "boolean") q = q.eq("activo", filters.activo);
|
||||||
|
|
||||||
if (filters.carreraId && filters.carreraId !== 'todas') {
|
// filtro por FK “hacia arriba” (PostgREST soporta filtros en recursos embebidos)
|
||||||
q = q.eq('carrera_id', filters.carreraId)
|
if (filters.facultadId) q = q.eq("carreras.facultad_id", filters.facultadId);
|
||||||
}
|
|
||||||
|
|
||||||
if (filters.estadoId && filters.estadoId !== 'todos') {
|
|
||||||
q = q.eq('estado_actual_id', filters.estadoId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof filters.activo === 'boolean') {
|
|
||||||
q = q.eq('activo', filters.activo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtro por facultad (gracias al !inner arriba, esto filtrará los planes)
|
|
||||||
if (filters.facultadId && filters.facultadId !== 'todas') {
|
|
||||||
q = q.eq('carreras.facultad_id', filters.facultadId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Paginación
|
// 3. Paginación
|
||||||
const { from, to } = buildRange(filters.limit, filters.offset)
|
const { from, to } = buildRange(filters.limit, filters.offset);
|
||||||
if (from !== undefined && to !== undefined) q = q.range(from, to)
|
if (from !== undefined && to !== undefined) q = q.range(from, to);
|
||||||
|
|
||||||
const { data, error, count } = await q
|
const { data, error, count } = await q;
|
||||||
throwIfError(error)
|
throwIfError(error);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 1. Si data es null, usa [].
|
// 1. Si data es null, usa [].
|
||||||
// 2. Luego dile a TS que el resultado es tu Array tipado.
|
// 2. Luego dile a TS que el resultado es tu Array tipado.
|
||||||
data: (data ?? []) as unknown as Array<PlanEstudio>,
|
data: (data ?? []) as unknown as Array<PlanEstudio>,
|
||||||
count: count ?? 0,
|
count: count ?? 0,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_get(planId: UUID): Promise<PlanEstudio> {
|
export async function plans_get(planId: UUID): Promise<PlanEstudio> {
|
||||||
console.log('plans_get')
|
const supabase = supabaseBrowser();
|
||||||
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('planes_estudio')
|
.from("planes_estudio")
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
*,
|
*,
|
||||||
@@ -137,340 +104,198 @@ export async function plans_get(planId: UUID): Promise<PlanEstudio> {
|
|||||||
estados_plan (*)
|
estados_plan (*)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.eq('id', planId)
|
.eq("id", planId)
|
||||||
.single()
|
.single();
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error);
|
||||||
return requireData(data, 'Plan no encontrado.')
|
return requireData(data, "Plan no encontrado.");
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Variante de `plans_get` que NO lanza si no existe (devuelve null).
|
|
||||||
* Útil para flujos de polling donde el plan puede tardar en aparecer.
|
|
||||||
*/
|
|
||||||
export async function plans_get_maybe(
|
|
||||||
planId: UUID,
|
|
||||||
): Promise<PlanEstudio | null> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('planes_estudio')
|
|
||||||
.select(
|
|
||||||
`
|
|
||||||
*,
|
|
||||||
carreras (*, facultades(*)),
|
|
||||||
estructuras_plan (*),
|
|
||||||
estados_plan (*)
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.eq('id', planId)
|
|
||||||
.maybeSingle()
|
|
||||||
|
|
||||||
throwIfError(error)
|
|
||||||
return (data ?? null) as unknown as PlanEstudio | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function plans_delete(planId: UUID): Promise<{ id: UUID }> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('planes_estudio')
|
|
||||||
.delete()
|
|
||||||
.eq('id', planId)
|
|
||||||
.select('id')
|
|
||||||
.maybeSingle()
|
|
||||||
|
|
||||||
throwIfError(error)
|
|
||||||
|
|
||||||
// Si por alguna razón no retorna fila (RLS / triggers), devolvemos el id solicitado.
|
|
||||||
return { id: ((data as any)?.id ?? planId) as UUID }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plan_lineas_list(
|
export async function plan_lineas_list(
|
||||||
planId: UUID,
|
planId: UUID,
|
||||||
): Promise<Array<LineaPlan>> {
|
): Promise<Array<LineaPlan>> {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser();
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('lineas_plan')
|
.from("lineas_plan")
|
||||||
.select('id,plan_estudio_id,nombre,orden,area,creado_en,actualizado_en')
|
.select("id,plan_estudio_id,nombre,orden,area,creado_en,actualizado_en")
|
||||||
.eq('plan_estudio_id', planId)
|
.eq("plan_estudio_id", planId)
|
||||||
.order('orden', { ascending: true })
|
.order("orden", { ascending: true });
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error);
|
||||||
return data ?? []
|
return data ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plan_asignaturas_list(
|
export async function plan_asignaturas_list(
|
||||||
planId: UUID,
|
planId: UUID,
|
||||||
): Promise<Array<Asignatura>> {
|
): Promise<Array<Asignatura>> {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser();
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('asignaturas')
|
.from("asignaturas")
|
||||||
.select(
|
.select(
|
||||||
'id,plan_estudio_id,horas_academicas,horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,prerrequisito_asignatura_id',
|
"id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en",
|
||||||
)
|
)
|
||||||
.eq('plan_estudio_id', planId)
|
.eq("plan_estudio_id", planId)
|
||||||
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
.order("numero_ciclo", { ascending: true, nullsFirst: false })
|
||||||
.order('orden_celda', { ascending: true, nullsFirst: false })
|
.order("orden_celda", { ascending: true, nullsFirst: false })
|
||||||
.order('nombre', { ascending: true })
|
.order("nombre", { ascending: true });
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error);
|
||||||
return data ?? []
|
return data ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_history(
|
export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> {
|
||||||
planId: UUID,
|
const supabase = supabaseBrowser();
|
||||||
page: number = 0,
|
const { data, error } = await supabase
|
||||||
pageSize: number = 4,
|
.from("cambios_plan")
|
||||||
): Promise<{ data: Array<CambioPlan>; count: number }> {
|
|
||||||
// Cambiamos el retorno
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const from = page * pageSize
|
|
||||||
const to = from + pageSize - 1
|
|
||||||
|
|
||||||
const { data, error, count } = await supabase
|
|
||||||
.from('cambios_plan')
|
|
||||||
.select(
|
.select(
|
||||||
'id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,response_id',
|
"id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,interaccion_ia_id",
|
||||||
{ count: 'exact' }, // <--- Pedimos el conteo exacto
|
|
||||||
)
|
)
|
||||||
.eq('plan_estudio_id', planId)
|
.eq("plan_estudio_id", planId)
|
||||||
.order('cambiado_en', { ascending: false })
|
.order("cambiado_en", { ascending: false });
|
||||||
.range(from, to)
|
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error);
|
||||||
return {
|
return data ?? [];
|
||||||
data: data ?? [],
|
|
||||||
count: count ?? 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wizard: crear plan manual (Edge Function) */
|
/** Wizard: crear plan manual (Edge Function) */
|
||||||
export type PlansCreateManualInput = {
|
export type PlansCreateManualInput = {
|
||||||
carreraId: UUID
|
carreraId: UUID;
|
||||||
estructuraId: UUID
|
estructuraId: UUID;
|
||||||
nombre: string
|
nombre: string;
|
||||||
nivel: NivelPlanEstudio
|
nivel: NivelPlanEstudio;
|
||||||
tipoCiclo: TipoCiclo
|
tipoCiclo: TipoCiclo;
|
||||||
numCiclos: number
|
numCiclos: number;
|
||||||
datos?: Partial<PlanDatosSep> & Record<string, any>
|
datos?: Partial<PlanDatosSep> & Record<string, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function plans_create_manual(
|
export async function plans_create_manual(
|
||||||
input: PlansCreateManualInput,
|
input: PlansCreateManualInput,
|
||||||
): Promise<PlanEstudio> {
|
): Promise<PlanEstudio> {
|
||||||
const supabase = supabaseBrowser()
|
return invokeEdge<PlanEstudio>(EDGE.plans_create_manual, input);
|
||||||
|
|
||||||
// 1. Obtener estado 'BORRADOR'
|
|
||||||
const { data: estado, error: estadoError } = await supabase
|
|
||||||
.from('estados_plan')
|
|
||||||
.select('id,clave,orden')
|
|
||||||
.ilike('clave', 'BORRADOR%')
|
|
||||||
.order('orden', { ascending: true })
|
|
||||||
.limit(1)
|
|
||||||
.maybeSingle()
|
|
||||||
|
|
||||||
if (estadoError) {
|
|
||||||
throw new Error(estadoError.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Preparar insert
|
|
||||||
const planInsert: Database['public']['Tables']['planes_estudio']['Insert'] = {
|
|
||||||
activo: true,
|
|
||||||
actualizado_en: new Date().toISOString(),
|
|
||||||
carrera_id: input.carreraId,
|
|
||||||
creado_en: new Date().toISOString(),
|
|
||||||
datos: input.datos || {},
|
|
||||||
estado_actual_id: estado?.id || null,
|
|
||||||
estructura_id: input.estructuraId,
|
|
||||||
nivel: input.nivel,
|
|
||||||
nombre: input.nombre,
|
|
||||||
numero_ciclos: input.numCiclos,
|
|
||||||
tipo_ciclo: input.tipoCiclo,
|
|
||||||
tipo_origen: 'MANUAL',
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Insertar
|
|
||||||
const { data: nuevoPlan, error: planError } = await supabase
|
|
||||||
.from('planes_estudio')
|
|
||||||
.insert([planInsert])
|
|
||||||
.select(
|
|
||||||
`
|
|
||||||
*,
|
|
||||||
carreras (*, facultades(*)),
|
|
||||||
estructuras_plan (*),
|
|
||||||
estados_plan (*)
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (planError) {
|
|
||||||
throw new Error(planError.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nuevoPlan as unknown as PlanEstudio
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wizard: IA genera preview JSON (Edge Function) */
|
/** Wizard: IA genera preview JSON (Edge Function) */
|
||||||
export type AIGeneratePlanInput = {
|
export type AIGeneratePlanInput = {
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombrePlan: string
|
nombrePlan: string;
|
||||||
carreraId: UUID
|
carreraId: UUID;
|
||||||
facultadId?: UUID
|
facultadId?: UUID;
|
||||||
nivel: string
|
nivel: string;
|
||||||
tipoCiclo: TipoCiclo
|
tipoCiclo: TipoCiclo;
|
||||||
numCiclos: number
|
numCiclos: number;
|
||||||
estructuraPlanId: UUID
|
};
|
||||||
}
|
|
||||||
iaConfig: {
|
iaConfig: {
|
||||||
descripcionEnfoqueAcademico: string
|
descripcionEnfoque: string;
|
||||||
instruccionesAdicionalesIA?: string
|
poblacionObjetivo?: string;
|
||||||
archivosReferencia?: Array<UUID>
|
notasAdicionales?: string;
|
||||||
repositoriosIds?: Array<UUID>
|
archivosReferencia?: Array<UUID>;
|
||||||
archivosAdjuntos: Array<UploadedFile>
|
repositoriosIds?: Array<UUID>;
|
||||||
usarMCP?: boolean
|
usarMCP?: boolean;
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function ai_generate_plan(
|
export async function ai_generate_plan(
|
||||||
input: AIGeneratePlanInput,
|
input: AIGeneratePlanInput,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
console.log('input ai generate', input)
|
return invokeEdge<any>(EDGE.ai_generate_plan, input);
|
||||||
|
|
||||||
const edgeFunctionBody = new FormData()
|
|
||||||
edgeFunctionBody.append('datosBasicos', JSON.stringify(input.datosBasicos))
|
|
||||||
edgeFunctionBody.append(
|
|
||||||
'iaConfig',
|
|
||||||
JSON.stringify({
|
|
||||||
...input.iaConfig,
|
|
||||||
archivosAdjuntos: undefined, // los manejamos aparte
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
input.iaConfig.archivosAdjuntos.forEach((file) => {
|
|
||||||
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
|
||||||
})
|
|
||||||
|
|
||||||
return invokeEdge<any>(
|
|
||||||
EDGE.ai_generate_plan,
|
|
||||||
edgeFunctionBody,
|
|
||||||
undefined,
|
|
||||||
supabaseBrowser(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_persist_from_ai(payload: {
|
export async function plans_persist_from_ai(
|
||||||
jsonPlan: any
|
payload: { jsonPlan: any },
|
||||||
}): Promise<PlanEstudio> {
|
): Promise<PlanEstudio> {
|
||||||
return invokeEdge<PlanEstudio>(EDGE.plans_persist_from_ai, payload)
|
return invokeEdge<PlanEstudio>(EDGE.plans_persist_from_ai, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_clone_from_existing(payload: {
|
export async function plans_clone_from_existing(payload: {
|
||||||
planOrigenId: UUID
|
planOrigenId: UUID;
|
||||||
overrides: Partial<
|
overrides:
|
||||||
Pick<PlanEstudio, 'nombre' | 'nivel' | 'tipo_ciclo' | 'numero_ciclos'>
|
& Partial<
|
||||||
> & {
|
Pick<PlanEstudio, "nombre" | "nivel" | "tipo_ciclo" | "numero_ciclos">
|
||||||
carrera_id?: UUID
|
>
|
||||||
estructura_id?: UUID
|
& {
|
||||||
datos?: Partial<PlanDatosSep> & Record<string, any>
|
carrera_id?: UUID;
|
||||||
}
|
estructura_id?: UUID;
|
||||||
|
datos?: Partial<PlanDatosSep> & Record<string, any>;
|
||||||
|
};
|
||||||
}): Promise<PlanEstudio> {
|
}): Promise<PlanEstudio> {
|
||||||
return invokeEdge<PlanEstudio>(EDGE.plans_clone_from_existing, payload)
|
return invokeEdge<PlanEstudio>(EDGE.plans_clone_from_existing, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_import_from_files(payload: {
|
export async function plans_import_from_files(payload: {
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombrePlan: string
|
nombrePlan: string;
|
||||||
carreraId: UUID
|
carreraId: UUID;
|
||||||
estructuraId: UUID
|
estructuraId: UUID;
|
||||||
nivel: string
|
nivel: string;
|
||||||
tipoCiclo: TipoCiclo
|
tipoCiclo: TipoCiclo;
|
||||||
numCiclos: number
|
numCiclos: number;
|
||||||
}
|
};
|
||||||
archivoWordPlanId: UUID
|
archivoWordPlanId: UUID;
|
||||||
archivoMapaExcelId?: UUID | null
|
archivoMapaExcelId?: UUID | null;
|
||||||
archivoAsignaturasExcelId?: UUID | null
|
archivoMateriasExcelId?: UUID | null;
|
||||||
}): Promise<PlanEstudio> {
|
}): Promise<PlanEstudio> {
|
||||||
return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload)
|
return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update de tarjetas/fields del plan (Edge Function: merge server-side) */
|
/** Update de tarjetas/fields del plan (Edge Function: merge server-side) */
|
||||||
export type PlansUpdateFieldsPatch = {
|
export type PlansUpdateFieldsPatch = {
|
||||||
nombre?: string
|
nombre?: string;
|
||||||
nivel?: NivelPlanEstudio
|
nivel?: NivelPlanEstudio;
|
||||||
tipo_ciclo?: TipoCiclo
|
tipo_ciclo?: TipoCiclo;
|
||||||
numero_ciclos?: number
|
numero_ciclos?: number;
|
||||||
datos?: Partial<PlanDatosSep> & Record<string, any>
|
datos?: Partial<PlanDatosSep> & Record<string, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function plans_update_fields(
|
export async function plans_update_fields(
|
||||||
planId: UUID,
|
planId: UUID,
|
||||||
patch: PlansUpdateFieldsPatch,
|
patch: PlansUpdateFieldsPatch,
|
||||||
): Promise<PlanEstudio> {
|
): Promise<PlanEstudio> {
|
||||||
const supabase = supabaseBrowser()
|
return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch });
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('planes_estudio')
|
|
||||||
.update(patch)
|
|
||||||
.eq('id', planId)
|
|
||||||
.select(
|
|
||||||
`
|
|
||||||
*,
|
|
||||||
carreras (*, facultades(*)),
|
|
||||||
estructuras_plan (*),
|
|
||||||
estados_plan (*)
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
.single()
|
|
||||||
|
|
||||||
throwIfError(error)
|
|
||||||
return requireData(data, 'No se pudo actualizar el plan.')
|
|
||||||
// Alternativa Edge Function:
|
|
||||||
// return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Operaciones del mapa curricular (mover/reordenar) */
|
/** Operaciones del mapa curricular (mover/reordenar) */
|
||||||
export type PlanMapOperation =
|
export type PlanMapOperation =
|
||||||
| {
|
| {
|
||||||
op: 'MOVE_ASIGNATURA'
|
op: "MOVE_ASIGNATURA";
|
||||||
asignaturaId: UUID
|
asignaturaId: UUID;
|
||||||
numero_ciclo: number | null
|
numero_ciclo: number | null;
|
||||||
linea_plan_id: UUID | null
|
linea_plan_id: UUID | null;
|
||||||
orden_celda?: number | null
|
orden_celda?: number | null;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
op: 'REORDER_CELDA'
|
op: "REORDER_CELDA";
|
||||||
linea_plan_id: UUID
|
linea_plan_id: UUID;
|
||||||
numero_ciclo: number
|
numero_ciclo: number;
|
||||||
asignaturaIdsOrdenados: Array<UUID>
|
asignaturaIdsOrdenados: Array<UUID>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function plans_update_map(
|
export async function plans_update_map(
|
||||||
planId: UUID,
|
planId: UUID,
|
||||||
ops: Array<PlanMapOperation>,
|
ops: Array<PlanMapOperation>,
|
||||||
): Promise<{ ok: true }> {
|
): Promise<{ ok: true }> {
|
||||||
return invokeEdge<{ ok: true }>(EDGE.plans_update_map, { planId, ops })
|
return invokeEdge<{ ok: true }>(EDGE.plans_update_map, { planId, ops });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_transition_state(payload: {
|
export async function plans_transition_state(payload: {
|
||||||
planId: UUID
|
planId: UUID;
|
||||||
haciaEstadoId: UUID
|
haciaEstadoId: UUID;
|
||||||
comentario?: string
|
comentario?: string;
|
||||||
}): Promise<{ ok: true }> {
|
}): Promise<{ ok: true }> {
|
||||||
return invokeEdge<{ ok: true }>(EDGE.plans_transition_state, payload)
|
return invokeEdge<{ ok: true }>(EDGE.plans_transition_state, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Documento (Edge Function: genera y devuelve URL firmada o metadata) */
|
/** Documento (Edge Function: genera y devuelve URL firmada o metadata) */
|
||||||
export type DocumentoResult = {
|
export type DocumentoResult = {
|
||||||
archivoId: UUID
|
archivoId: UUID;
|
||||||
signedUrl: string
|
signedUrl: string;
|
||||||
mimeType?: string
|
mimeType?: string;
|
||||||
nombre?: string
|
nombre?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export async function plans_generate_document(
|
export async function plans_generate_document(
|
||||||
planId: UUID,
|
planId: UUID,
|
||||||
): Promise<DocumentoResult> {
|
): Promise<DocumentoResult> {
|
||||||
return invokeEdge<DocumentoResult>(EDGE.plans_generate_document, { planId })
|
return invokeEdge<DocumentoResult>(EDGE.plans_generate_document, { planId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_get_document(
|
export async function plans_get_document(
|
||||||
@@ -478,26 +303,5 @@ export async function plans_get_document(
|
|||||||
): Promise<DocumentoResult | null> {
|
): Promise<DocumentoResult | null> {
|
||||||
return invokeEdge<DocumentoResult | null>(EDGE.plans_get_document, {
|
return invokeEdge<DocumentoResult | null>(EDGE.plans_get_document, {
|
||||||
planId,
|
planId,
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export async function getCatalogos() {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const [facultadesRes, carrerasRes, estadosRes, estructurasPlanRes] =
|
|
||||||
await Promise.all([
|
|
||||||
supabase.from('facultades').select('*').order('nombre'),
|
|
||||||
supabase.from('carreras').select('*').order('nombre'),
|
|
||||||
supabase.from('estados_plan').select('*').order('orden'),
|
|
||||||
supabase.from('estructuras_plan').select('*').order('creado_en', {
|
|
||||||
ascending: true,
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
return {
|
|
||||||
facultades: facultadesRes.data ?? [],
|
|
||||||
carreras: carrerasRes.data ?? [],
|
|
||||||
estados: estadosRes.data ?? [],
|
|
||||||
estructurasPlan: estructurasPlanRes.data ?? [],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,441 +1,181 @@
|
|||||||
import { supabaseBrowser } from '../supabase/client'
|
import { supabaseBrowser } from "../supabase/client";
|
||||||
import { invokeEdge } from '../supabase/invokeEdge'
|
import { invokeEdge } from "../supabase/invokeEdge";
|
||||||
|
import { throwIfError, requireData } from "./_helpers";
|
||||||
import { throwIfError, requireData } from './_helpers'
|
|
||||||
|
|
||||||
import type { DocumentoResult } from './plans.api'
|
|
||||||
import type {
|
import type {
|
||||||
Asignatura,
|
Asignatura,
|
||||||
BibliografiaAsignatura,
|
BibliografiaAsignatura,
|
||||||
CarreraRow,
|
|
||||||
CambioAsignatura,
|
CambioAsignatura,
|
||||||
EstructuraAsignatura,
|
|
||||||
FacultadRow,
|
|
||||||
PlanEstudioRow,
|
|
||||||
TipoAsignatura,
|
TipoAsignatura,
|
||||||
UUID,
|
UUID,
|
||||||
} from '../types/domain'
|
} from "../types/domain";
|
||||||
import type {
|
import type { DocumentoResult } from "./plans.api";
|
||||||
AsignaturaSugerida,
|
|
||||||
DataAsignaturaSugerida,
|
|
||||||
} from '@/features/asignaturas/nueva/types'
|
|
||||||
import type { Database, Tables, TablesInsert } from '@/types/supabase'
|
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
generate_subject_suggestions: 'generate-subject-suggestions',
|
subjects_create_manual: "subjects_create_manual",
|
||||||
subjects_create_manual: 'subjects_create_manual',
|
ai_generate_subject: "ai_generate_subject",
|
||||||
ai_generate_subject: 'ai-generate-subject',
|
subjects_persist_from_ai: "subjects_persist_from_ai",
|
||||||
subjects_persist_from_ai: 'subjects_persist_from_ai',
|
subjects_clone_from_existing: "subjects_clone_from_existing",
|
||||||
subjects_clone_from_existing: 'subjects_clone_from_existing',
|
subjects_import_from_file: "subjects_import_from_file",
|
||||||
subjects_import_from_file: 'subjects_import_from_file',
|
|
||||||
|
|
||||||
// Bibliografía
|
subjects_update_fields: "subjects_update_fields",
|
||||||
buscar_bibliografia: 'buscar-bibliografia',
|
subjects_update_contenido: "subjects_update_contenido",
|
||||||
|
subjects_update_bibliografia: "subjects_update_bibliografia",
|
||||||
|
|
||||||
subjects_update_fields: 'subjects_update_fields',
|
subjects_generate_document: "subjects_generate_document",
|
||||||
subjects_update_bibliografia: 'subjects_update_bibliografia',
|
subjects_get_document: "subjects_get_document",
|
||||||
|
} as const;
|
||||||
|
|
||||||
subjects_generate_document: 'subjects_generate_document',
|
export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
|
||||||
subjects_get_document: 'subjects_get_document',
|
const supabase = supabaseBrowser();
|
||||||
} as const
|
|
||||||
|
|
||||||
export type BuscarBibliografiaRequest = {
|
|
||||||
searchTerms: {
|
|
||||||
q: string
|
|
||||||
}
|
|
||||||
|
|
||||||
google: {
|
|
||||||
orderBy?: 'newest' | 'relevance'
|
|
||||||
langRestrict?: string
|
|
||||||
startIndex?: number
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
openLibrary: {
|
|
||||||
language?: string
|
|
||||||
page?: number
|
|
||||||
sort?: string
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GoogleBooksVolume = {
|
|
||||||
kind?: 'books#volume'
|
|
||||||
id: string
|
|
||||||
etag?: string
|
|
||||||
selfLink?: string
|
|
||||||
volumeInfo?: {
|
|
||||||
title?: string
|
|
||||||
subtitle?: string
|
|
||||||
authors?: Array<string>
|
|
||||||
publisher?: string
|
|
||||||
publishedDate?: string
|
|
||||||
description?: string
|
|
||||||
industryIdentifiers?: Array<{ type?: string; identifier?: string }>
|
|
||||||
pageCount?: number
|
|
||||||
categories?: Array<string>
|
|
||||||
language?: string
|
|
||||||
previewLink?: string
|
|
||||||
infoLink?: string
|
|
||||||
canonicalVolumeLink?: string
|
|
||||||
imageLinks?: {
|
|
||||||
smallThumbnail?: string
|
|
||||||
thumbnail?: string
|
|
||||||
small?: string
|
|
||||||
medium?: string
|
|
||||||
large?: string
|
|
||||||
extraLarge?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
searchInfo?: {
|
|
||||||
textSnippet?: string
|
|
||||||
}
|
|
||||||
[k: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OpenLibraryDoc = Record<string, unknown>
|
|
||||||
|
|
||||||
export type EndpointResult =
|
|
||||||
| { endpoint: 'google'; item: GoogleBooksVolume }
|
|
||||||
| { endpoint: 'open_library'; item: OpenLibraryDoc }
|
|
||||||
|
|
||||||
export async function buscar_bibliografia(
|
|
||||||
input: BuscarBibliografiaRequest,
|
|
||||||
): Promise<Array<EndpointResult>> {
|
|
||||||
const q = input.searchTerms.q
|
|
||||||
|
|
||||||
if (typeof q !== 'string' || q.trim().length < 1) {
|
|
||||||
throw new Error('q es requerido')
|
|
||||||
}
|
|
||||||
|
|
||||||
return await invokeEdge<Array<EndpointResult>>(
|
|
||||||
EDGE.buscar_bibliografia,
|
|
||||||
input,
|
|
||||||
{ headers: { 'Content-Type': 'application/json' } },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ContenidoTemaApi =
|
|
||||||
| string
|
|
||||||
| {
|
|
||||||
nombre: string
|
|
||||||
horasEstimadas?: number
|
|
||||||
descripcion?: string
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estructura persistida en `asignaturas.contenido_tematico`.
|
|
||||||
* La BDD guarda un arreglo de unidades, cada una con temas (strings u objetos).
|
|
||||||
*/
|
|
||||||
export type ContenidoApi = {
|
|
||||||
unidad: number
|
|
||||||
titulo: string
|
|
||||||
temas: Array<ContenidoTemaApi>
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FacultadInSubject = Pick<
|
|
||||||
FacultadRow,
|
|
||||||
'id' | 'nombre' | 'nombre_corto' | 'color' | 'icono'
|
|
||||||
>
|
|
||||||
|
|
||||||
export type CarreraInSubject = Pick<
|
|
||||||
CarreraRow,
|
|
||||||
'id' | 'facultad_id' | 'nombre' | 'nombre_corto' | 'clave_sep' | 'activa'
|
|
||||||
> & {
|
|
||||||
facultades: FacultadInSubject | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PlanEstudioInSubject = Pick<
|
|
||||||
PlanEstudioRow,
|
|
||||||
| 'id'
|
|
||||||
| 'carrera_id'
|
|
||||||
| 'estructura_id'
|
|
||||||
| 'nombre'
|
|
||||||
| 'nivel'
|
|
||||||
| 'tipo_ciclo'
|
|
||||||
| 'numero_ciclos'
|
|
||||||
| 'datos'
|
|
||||||
| 'estado_actual_id'
|
|
||||||
| 'activo'
|
|
||||||
| 'tipo_origen'
|
|
||||||
| 'meta_origen'
|
|
||||||
| 'creado_por'
|
|
||||||
| 'actualizado_por'
|
|
||||||
| 'creado_en'
|
|
||||||
| 'actualizado_en'
|
|
||||||
> & {
|
|
||||||
carreras: CarreraInSubject | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EstructuraAsignaturaInSubject = Pick<
|
|
||||||
EstructuraAsignatura,
|
|
||||||
'id' | 'nombre' | 'definicion'
|
|
||||||
>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tipo real que devuelve `subjects_get` (asignatura + relaciones seleccionadas).
|
|
||||||
* Nota: `asignaturas_update` (update directo) NO devuelve estas relaciones.
|
|
||||||
*/
|
|
||||||
export type AsignaturaDetail = Omit<Asignatura, 'contenido_tematico'> & {
|
|
||||||
contenido_tematico: Array<ContenidoApi> | null
|
|
||||||
planes_estudio: PlanEstudioInSubject | null
|
|
||||||
estructuras_asignatura: EstructuraAsignaturaInSubject | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function subjects_get(subjectId: UUID): Promise<AsignaturaDetail> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('asignaturas')
|
.from("asignaturas")
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion,
|
id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||||
planes_estudio(
|
planes_estudio(
|
||||||
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||||
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
||||||
),
|
),
|
||||||
estructuras_asignatura(id,nombre,definicion)
|
estructuras_asignatura(id,nombre,version,definicion)
|
||||||
`,
|
`
|
||||||
)
|
)
|
||||||
.eq('id', subjectId)
|
.eq("id", subjectId)
|
||||||
.single()
|
.single();
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error);
|
||||||
return requireData(
|
return requireData(data, "Materia no encontrada.");
|
||||||
data,
|
|
||||||
'Asignatura no encontrada.',
|
|
||||||
) as unknown as AsignaturaDetail
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_history(
|
export async function subjects_history(subjectId: UUID): Promise<CambioAsignatura[]> {
|
||||||
subjectId: UUID,
|
const supabase = supabaseBrowser();
|
||||||
): Promise<Array<CambioAsignatura>> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('cambios_asignatura')
|
.from("cambios_asignatura")
|
||||||
.select(
|
.select(
|
||||||
'id,asignatura_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,fuente,interaccion_ia_id',
|
"id,asignatura_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,fuente,interaccion_ia_id"
|
||||||
)
|
)
|
||||||
.eq('asignatura_id', subjectId)
|
.eq("asignatura_id", subjectId)
|
||||||
.order('cambiado_en', { ascending: false })
|
.order("cambiado_en", { ascending: false });
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error);
|
||||||
return data ?? []
|
return data ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_bibliografia_list(
|
export async function subjects_bibliografia_list(subjectId: UUID): Promise<BibliografiaAsignatura[]> {
|
||||||
subjectId: UUID,
|
const supabase = supabaseBrowser();
|
||||||
): Promise<Array<BibliografiaAsignatura>> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('bibliografia_asignatura')
|
.from("bibliografia_asignatura")
|
||||||
.select(
|
.select("id,asignatura_id,tipo,cita,tipo_fuente,biblioteca_item_id,creado_por,creado_en,actualizado_en")
|
||||||
'id,asignatura_id,tipo,cita,referencia_biblioteca,referencia_en_linea,creado_por,creado_en,actualizado_en',
|
.eq("asignatura_id", subjectId)
|
||||||
)
|
.order("tipo", { ascending: true })
|
||||||
.eq('asignatura_id', subjectId)
|
.order("creado_en", { ascending: true });
|
||||||
.order('tipo', { ascending: true })
|
|
||||||
.order('creado_en', { ascending: true })
|
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error);
|
||||||
return data ?? []
|
return data ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_create_manual(
|
/** Wizard: crear materia manual (Edge Function) */
|
||||||
payload: TablesInsert<'asignaturas'>,
|
export type SubjectsCreateManualInput = {
|
||||||
): Promise<Asignatura> {
|
planId: UUID;
|
||||||
const supabase = supabaseBrowser()
|
datosBasicos: {
|
||||||
const { data, error } = await supabase
|
nombre: string;
|
||||||
.from('asignaturas')
|
clave?: string;
|
||||||
.insert(payload)
|
tipo: TipoAsignatura;
|
||||||
.select()
|
creditos: number;
|
||||||
.single()
|
horasSemana?: number;
|
||||||
|
estructuraId: UUID;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
throwIfError(error)
|
export async function subjects_create_manual(payload: SubjectsCreateManualInput): Promise<Asignatura> {
|
||||||
return requireData(data, 'No se pudo crear la asignatura.')
|
return invokeEdge<Asignatura>(EDGE.subjects_create_manual, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function ai_generate_subject(payload: {
|
||||||
* Nuevo payload unificado (JSON) para la Edge `ai_generate_subject`.
|
planId: UUID;
|
||||||
* - Siempre incluye `datosUpdate.plan_estudio_id`.
|
datosBasicos: {
|
||||||
* - `datosUpdate.id` es opcional (si no existe, la Edge puede crear).
|
nombre: string;
|
||||||
* En el frontend, insertamos primero y usamos `id` para actualizar.
|
clave?: string;
|
||||||
*/
|
tipo: TipoAsignatura;
|
||||||
export type AISubjectUnifiedInput = {
|
creditos: number;
|
||||||
datosUpdate: Partial<{
|
horasSemana?: number;
|
||||||
id: string
|
estructuraId: UUID;
|
||||||
plan_estudio_id: string
|
};
|
||||||
estructura_id: string
|
iaConfig: {
|
||||||
nombre: string
|
descripcionEnfoque: string;
|
||||||
codigo: string | null
|
notasAdicionales?: string;
|
||||||
tipo: string | null
|
archivosExistentesIds?: UUID[];
|
||||||
creditos: number
|
repositoriosIds?: UUID[];
|
||||||
horas_academicas: number | null
|
archivosAdhocIds?: UUID[];
|
||||||
horas_independientes: number | null
|
usarMCP?: boolean;
|
||||||
numero_ciclo: number | null
|
};
|
||||||
linea_plan_id: string | null
|
}): Promise<any> {
|
||||||
orden_celda: number | null
|
return invokeEdge<any>(EDGE.ai_generate_subject, payload);
|
||||||
}> & {
|
|
||||||
plan_estudio_id: string
|
|
||||||
}
|
|
||||||
iaConfig?: {
|
|
||||||
descripcionEnfoqueAcademico?: string
|
|
||||||
instruccionesAdicionalesIA?: string
|
|
||||||
archivosAdjuntos?: Array<string>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_get_maybe(
|
export async function subjects_persist_from_ai(payload: { planId: UUID; jsonMateria: any }): Promise<Asignatura> {
|
||||||
subjectId: UUID,
|
return invokeEdge<Asignatura>(EDGE.subjects_persist_from_ai, payload);
|
||||||
): Promise<Asignatura | null> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('asignaturas')
|
|
||||||
.select('id,plan_estudio_id,estado')
|
|
||||||
.eq('id', subjectId)
|
|
||||||
.maybeSingle()
|
|
||||||
|
|
||||||
throwIfError(error)
|
|
||||||
return (data ?? null) as unknown as Asignatura | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GenerateSubjectSuggestionsInput = {
|
|
||||||
plan_estudio_id: UUID
|
|
||||||
enfoque?: string
|
|
||||||
cantidad_de_sugerencias: number
|
|
||||||
sugerencias_conservadas: Array<{ nombre: string; descripcion: string }>
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generate_subject_suggestions(
|
|
||||||
input: GenerateSubjectSuggestionsInput,
|
|
||||||
): Promise<Array<AsignaturaSugerida>> {
|
|
||||||
const raw = await invokeEdge<Array<DataAsignaturaSugerida>>(
|
|
||||||
EDGE.generate_subject_suggestions,
|
|
||||||
input,
|
|
||||||
{ headers: { 'Content-Type': 'application/json' } },
|
|
||||||
)
|
|
||||||
|
|
||||||
return raw.map(
|
|
||||||
(s): AsignaturaSugerida => ({
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
selected: false,
|
|
||||||
source: 'IA',
|
|
||||||
nombre: s.nombre,
|
|
||||||
codigo: s.codigo,
|
|
||||||
tipo: s.tipo ?? null,
|
|
||||||
creditos: s.creditos ?? null,
|
|
||||||
horasAcademicas: s.horasAcademicas ?? null,
|
|
||||||
horasIndependientes: s.horasIndependientes ?? null,
|
|
||||||
descripcion: s.descripcion,
|
|
||||||
linea_plan_id: null,
|
|
||||||
numero_ciclo: null,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ai_generate_subject(
|
|
||||||
input: AISubjectUnifiedInput,
|
|
||||||
): Promise<any> {
|
|
||||||
return invokeEdge<any>(EDGE.ai_generate_subject, input, {
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function subjects_persist_from_ai(payload: {
|
|
||||||
planId: UUID
|
|
||||||
jsonAsignatura: any
|
|
||||||
}): Promise<Asignatura> {
|
|
||||||
return invokeEdge<Asignatura>(EDGE.subjects_persist_from_ai, payload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_clone_from_existing(payload: {
|
export async function subjects_clone_from_existing(payload: {
|
||||||
asignaturaOrigenId: UUID
|
materiaOrigenId: UUID;
|
||||||
planDestinoId: UUID
|
planDestinoId: UUID;
|
||||||
overrides?: Partial<{
|
overrides?: Partial<{
|
||||||
nombre: string
|
nombre: string;
|
||||||
codigo: string
|
codigo: string;
|
||||||
tipo: TipoAsignatura
|
tipo: TipoAsignatura;
|
||||||
creditos: number
|
creditos: number;
|
||||||
horas_semana: number
|
horas_semana: number;
|
||||||
}>
|
}>;
|
||||||
}): Promise<Asignatura> {
|
}): Promise<Asignatura> {
|
||||||
return invokeEdge<Asignatura>(EDGE.subjects_clone_from_existing, payload)
|
return invokeEdge<Asignatura>(EDGE.subjects_clone_from_existing, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_import_from_file(payload: {
|
export async function subjects_import_from_file(payload: {
|
||||||
planId: UUID
|
planId: UUID;
|
||||||
archivoWordAsignaturaId: UUID
|
archivoWordMateriaId: UUID;
|
||||||
archivosAdicionalesIds?: Array<UUID>
|
archivosAdicionalesIds?: UUID[];
|
||||||
}): Promise<Asignatura> {
|
}): Promise<Asignatura> {
|
||||||
return invokeEdge<Asignatura>(EDGE.subjects_import_from_file, payload)
|
return invokeEdge<Asignatura>(EDGE.subjects_import_from_file, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Guardado de tarjetas/fields (Edge: merge server-side en asignaturas.datos y columnas) */
|
/** Guardado de tarjetas/fields (Edge: merge server-side en asignaturas.datos y columnas) */
|
||||||
export type SubjectsUpdateFieldsPatch = Partial<{
|
export type SubjectsUpdateFieldsPatch = Partial<{
|
||||||
codigo: string | null
|
codigo: string | null;
|
||||||
nombre: string
|
nombre: string;
|
||||||
tipo: TipoAsignatura
|
tipo: TipoAsignatura;
|
||||||
creditos: number
|
creditos: number;
|
||||||
horas_semana: number | null
|
horas_semana: number | null;
|
||||||
numero_ciclo: number | null
|
numero_ciclo: number | null;
|
||||||
linea_plan_id: UUID | null
|
linea_plan_id: UUID | null;
|
||||||
|
|
||||||
datos: Record<string, any>
|
datos: Record<string, any>;
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
export async function subjects_update_fields(
|
export async function subjects_update_fields(subjectId: UUID, patch: SubjectsUpdateFieldsPatch): Promise<Asignatura> {
|
||||||
subjectId: UUID,
|
return invokeEdge<Asignatura>(EDGE.subjects_update_fields, { subjectId, patch });
|
||||||
patch: SubjectsUpdateFieldsPatch,
|
|
||||||
): Promise<Asignatura> {
|
|
||||||
return invokeEdge<Asignatura>(EDGE.subjects_update_fields, {
|
|
||||||
subjectId,
|
|
||||||
patch,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_update_contenido(
|
export async function subjects_update_contenido(subjectId: UUID, unidades: any[]): Promise<Asignatura> {
|
||||||
subjectId: UUID,
|
return invokeEdge<Asignatura>(EDGE.subjects_update_contenido, { subjectId, unidades });
|
||||||
unidades: Array<ContenidoApi>,
|
|
||||||
): Promise<Asignatura> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
type AsignaturaUpdate = Database['public']['Tables']['asignaturas']['Update']
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('asignaturas')
|
|
||||||
.update({
|
|
||||||
contenido_tematico:
|
|
||||||
unidades as unknown as AsignaturaUpdate['contenido_tematico'],
|
|
||||||
})
|
|
||||||
.eq('id', subjectId)
|
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
throwIfError(error)
|
|
||||||
return requireData(data, 'No se pudo actualizar la asignatura.')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BibliografiaUpsertInput = Array<{
|
export type BibliografiaUpsertInput = Array<{
|
||||||
id?: UUID
|
id?: UUID;
|
||||||
tipo: 'BASICA' | 'COMPLEMENTARIA'
|
tipo: "BASICA" | "COMPLEMENTARIA";
|
||||||
cita: string
|
cita: string;
|
||||||
tipo_fuente?: 'MANUAL' | 'BIBLIOTECA'
|
tipo_fuente?: "MANUAL" | "BIBLIOTECA";
|
||||||
biblioteca_item_id?: string | null
|
biblioteca_item_id?: string | null;
|
||||||
}>
|
}>;
|
||||||
|
|
||||||
export async function subjects_update_bibliografia(
|
export async function subjects_update_bibliografia(
|
||||||
subjectId: UUID,
|
subjectId: UUID,
|
||||||
entries: BibliografiaUpsertInput,
|
entries: BibliografiaUpsertInput
|
||||||
): Promise<{ ok: true }> {
|
): Promise<{ ok: true }> {
|
||||||
return invokeEdge<{ ok: true }>(EDGE.subjects_update_bibliografia, {
|
return invokeEdge<{ ok: true }>(EDGE.subjects_update_bibliografia, { subjectId, entries });
|
||||||
subjectId,
|
|
||||||
entries,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Documento SEP asignatura */
|
/** Documento SEP materia */
|
||||||
/* export type DocumentoResult = {
|
/* export type DocumentoResult = {
|
||||||
archivoId: UUID;
|
archivoId: UUID;
|
||||||
signedUrl: string;
|
signedUrl: string;
|
||||||
@@ -443,145 +183,10 @@ export async function subjects_update_bibliografia(
|
|||||||
nombre?: string;
|
nombre?: string;
|
||||||
}; */
|
}; */
|
||||||
|
|
||||||
export async function subjects_generate_document(
|
export async function subjects_generate_document(subjectId: UUID): Promise<DocumentoResult> {
|
||||||
subjectId: UUID,
|
return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, { subjectId });
|
||||||
): Promise<DocumentoResult> {
|
|
||||||
return invokeEdge<DocumentoResult>(EDGE.subjects_generate_document, {
|
|
||||||
subjectId,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_get_document(
|
export async function subjects_get_document(subjectId: UUID): Promise<DocumentoResult | null> {
|
||||||
subjectId: UUID,
|
return invokeEdge<DocumentoResult | null>(EDGE.subjects_get_document, { subjectId });
|
||||||
): Promise<DocumentoResult | null> {
|
|
||||||
return invokeEdge<DocumentoResult | null>(EDGE.subjects_get_document, {
|
|
||||||
subjectId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function subjects_get_structure_catalog(): Promise<
|
|
||||||
Array<Database['public']['Tables']['estructuras_asignatura']['Row']>
|
|
||||||
> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('estructuras_asignatura')
|
|
||||||
.select('*')
|
|
||||||
.order('nombre', { ascending: true })
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function asignaturas_update(
|
|
||||||
asignaturaId: UUID,
|
|
||||||
patch: Partial<Asignatura>, // O tu tipo específico para el Patch de materias
|
|
||||||
): Promise<Asignatura> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('asignaturas')
|
|
||||||
.update(patch)
|
|
||||||
.eq('id', asignaturaId)
|
|
||||||
.select() // Trae la materia actualizada
|
|
||||||
.single()
|
|
||||||
|
|
||||||
throwIfError(error)
|
|
||||||
return requireData(data, 'No se pudo actualizar la asignatura.')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insertar una nueva línea
|
|
||||||
export async function lineas_insert(linea: {
|
|
||||||
nombre: string
|
|
||||||
plan_estudio_id: string
|
|
||||||
orden: number
|
|
||||||
area?: string
|
|
||||||
}) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('lineas_plan') // Asegúrate que el nombre de la tabla sea correcto
|
|
||||||
.insert([linea])
|
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualizar una línea existente
|
|
||||||
export async function lineas_update(
|
|
||||||
lineaId: string,
|
|
||||||
patch: { nombre?: string; orden?: number; area?: string },
|
|
||||||
) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('lineas_plan')
|
|
||||||
.update(patch)
|
|
||||||
.eq('id', lineaId)
|
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function lineas_delete(lineaId: string) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
// Nota: Si configuraste "ON DELETE SET NULL" en tu base de datos,
|
|
||||||
// las asignaturas se desvincularán solas. Si no, Supabase podría dar error.
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('lineas_plan')
|
|
||||||
.delete()
|
|
||||||
.eq('id', lineaId)
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return lineaId
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bibliografia_insert(
|
|
||||||
entry: TablesInsert<'bibliografia_asignatura'>,
|
|
||||||
): Promise<Tables<'bibliografia_asignatura'>> {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('bibliografia_asignatura')
|
|
||||||
.insert([entry])
|
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data as Tables<'bibliografia_asignatura'>
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bibliografia_update(
|
|
||||||
id: string,
|
|
||||||
updates: {
|
|
||||||
cita?: string
|
|
||||||
tipo?: 'BASICA' | 'COMPLEMENTARIA'
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('bibliografia_asignatura')
|
|
||||||
.update(updates) // Ahora 'updates' es compatible con lo que espera Supabase
|
|
||||||
.eq('id', id)
|
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function bibliografia_delete(id: string) {
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const { error } = await supabase
|
|
||||||
.from('bibliografia_asignatura')
|
|
||||||
.delete()
|
|
||||||
.eq('id', id)
|
|
||||||
|
|
||||||
if (error) throw error
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,337 +1,29 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ai_plan_chat_v2,
|
ai_plan_chat,
|
||||||
ai_plan_improve,
|
ai_plan_improve,
|
||||||
|
ai_subject_chat,
|
||||||
ai_subject_improve,
|
ai_subject_improve,
|
||||||
create_conversation,
|
|
||||||
get_chat_history,
|
|
||||||
getConversationByPlan,
|
|
||||||
library_search,
|
library_search,
|
||||||
update_conversation_status,
|
} from "../api/ai.api";
|
||||||
update_recommendation_applied_status,
|
|
||||||
update_conversation_title,
|
|
||||||
getMessagesByConversation,
|
|
||||||
update_subject_conversation_status,
|
|
||||||
update_subject_recommendation_applied,
|
|
||||||
getMessagesBySubjectConversation,
|
|
||||||
getConversationBySubject,
|
|
||||||
ai_subject_chat_v2,
|
|
||||||
create_subject_conversation,
|
|
||||||
update_subject_conversation_name,
|
|
||||||
} from '../api/ai.api'
|
|
||||||
import { supabaseBrowser } from '../supabase/client'
|
|
||||||
|
|
||||||
import type { UUID } from 'node:crypto'
|
|
||||||
|
|
||||||
export function useAIPlanImprove() {
|
export function useAIPlanImprove() {
|
||||||
return useMutation({ mutationFn: ai_plan_improve })
|
return useMutation({ mutationFn: ai_plan_improve });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAIPlanChat() {
|
export function useAIPlanChat() {
|
||||||
return useMutation({
|
return useMutation({ mutationFn: ai_plan_chat });
|
||||||
mutationFn: async (payload: {
|
|
||||||
planId: UUID
|
|
||||||
content: string
|
|
||||||
campos?: Array<string>
|
|
||||||
conversacionId?: string
|
|
||||||
}) => {
|
|
||||||
let currentId = payload.conversacionId
|
|
||||||
|
|
||||||
// 1. Si no hay ID, creamos la conversación
|
|
||||||
if (!currentId) {
|
|
||||||
const response = await create_conversation(payload.planId)
|
|
||||||
|
|
||||||
// CAMBIO AQUÍ: Accedemos a la estructura correcta según tu consola
|
|
||||||
currentId = response.conversation_plan.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Ahora enviamos el mensaje con el ID garantizado
|
|
||||||
const result = await ai_plan_chat_v2({
|
|
||||||
conversacionId: currentId!,
|
|
||||||
content: payload.content,
|
|
||||||
campos: payload.campos,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Retornamos el resultado del chat y el ID para el estado del componente
|
|
||||||
return { ...result, conversacionId: currentId }
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useChatHistory(conversacionId?: string) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['chat-history', conversacionId],
|
|
||||||
queryFn: async () => {
|
|
||||||
return get_chat_history(conversacionId!)
|
|
||||||
},
|
|
||||||
enabled: Boolean(conversacionId),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateConversationStatus() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: ({
|
|
||||||
id,
|
|
||||||
estado,
|
|
||||||
}: {
|
|
||||||
id: string
|
|
||||||
estado: 'ARCHIVADA' | 'ACTIVA'
|
|
||||||
}) => update_conversation_status(id, estado),
|
|
||||||
onSuccess: () => {
|
|
||||||
// Esto refresca las listas automáticamente
|
|
||||||
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useConversationByPlan(planId: string | null) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['conversation-by-plan', planId],
|
|
||||||
queryFn: () => getConversationByPlan(planId!),
|
|
||||||
enabled: !!planId, // solo ejecuta si existe planId
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMessagesByChat(conversationId: string | null) {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
const query = useQuery({
|
|
||||||
queryKey: ['conversation-messages', conversationId],
|
|
||||||
queryFn: () => {
|
|
||||||
if (!conversationId) throw new Error('Conversation ID is required')
|
|
||||||
return getMessagesByConversation(conversationId)
|
|
||||||
},
|
|
||||||
enabled: !!conversationId,
|
|
||||||
placeholderData: (previousData) => previousData,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!conversationId) return
|
|
||||||
|
|
||||||
// Suscribirse a cambios en los mensajes de ESTA conversación
|
|
||||||
const channel = supabase
|
|
||||||
.channel(`realtime-messages-${conversationId}`)
|
|
||||||
.on(
|
|
||||||
'postgres_changes',
|
|
||||||
{
|
|
||||||
event: '*', // Escuchamos INSERT y UPDATE
|
|
||||||
schema: 'public',
|
|
||||||
table: 'plan_mensajes_ia',
|
|
||||||
filter: `conversacion_plan_id=eq.${conversationId}`,
|
|
||||||
},
|
|
||||||
(payload) => {
|
|
||||||
// Opción A: Invalidar la query para que React Query haga refetch (más seguro)
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ['conversation-messages', conversationId],
|
|
||||||
})
|
|
||||||
|
|
||||||
/* Opción B: Actualización manual del caché (más rápido/fluido)
|
|
||||||
if (payload.eventType === 'INSERT') {
|
|
||||||
queryClient.setQueryData(['conversation-messages', conversationId], (old: any) => [...old, payload.new])
|
|
||||||
} else if (payload.eventType === 'UPDATE') {
|
|
||||||
queryClient.setQueryData(['conversation-messages', conversationId], (old: any) =>
|
|
||||||
old.map((m: any) => m.id === payload.new.id ? payload.new : m)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.subscribe()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
supabase.removeChannel(channel)
|
|
||||||
}
|
|
||||||
}, [conversationId, queryClient, supabase])
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateRecommendationApplied() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: ({
|
|
||||||
conversacionId,
|
|
||||||
campoAfectado,
|
|
||||||
}: {
|
|
||||||
conversacionId: string
|
|
||||||
campoAfectado: string
|
|
||||||
}) => update_recommendation_applied_status(conversacionId, campoAfectado),
|
|
||||||
|
|
||||||
onSuccess: (_, variables) => {
|
|
||||||
// Invalidamos la query para que useConversationByPlan refresque el JSON
|
|
||||||
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
|
|
||||||
console.log(
|
|
||||||
`Recomendación ${variables.campoAfectado} marcada como aplicada.`,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error('Error al actualizar el estado de la recomendación:', error)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAISubjectImprove() {
|
export function useAISubjectImprove() {
|
||||||
return useMutation({ mutationFn: ai_subject_improve })
|
return useMutation({ mutationFn: ai_subject_improve });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAISubjectChat() {
|
||||||
|
return useMutation({ mutationFn: ai_subject_chat });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLibrarySearch() {
|
export function useLibrarySearch() {
|
||||||
return useMutation({ mutationFn: library_search })
|
return useMutation({ mutationFn: library_search });
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateConversationTitle() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: ({ id, nombre }: { id: string; nombre: string }) =>
|
|
||||||
update_conversation_title(id, nombre),
|
|
||||||
onSuccess: (_, variables) => {
|
|
||||||
// Invalidamos para que la lista de chats se refresque
|
|
||||||
qc.invalidateQueries({ queryKey: ['conversation-by-plan'] })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asignaturas
|
|
||||||
|
|
||||||
export function useAISubjectChat() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: async (payload: {
|
|
||||||
subjectId: UUID
|
|
||||||
content: string
|
|
||||||
campos?: Array<string>
|
|
||||||
conversacionId?: string
|
|
||||||
}) => {
|
|
||||||
let currentId = payload.conversacionId
|
|
||||||
|
|
||||||
// 1. Si no hay ID, creamos la conversación de asignatura
|
|
||||||
if (!currentId) {
|
|
||||||
const response = await create_subject_conversation(payload.subjectId)
|
|
||||||
currentId = response.conversation_asignatura.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Enviamos mensaje al endpoint de asignatura
|
|
||||||
const result = await ai_subject_chat_v2({
|
|
||||||
conversacionId: currentId!,
|
|
||||||
content: payload.content,
|
|
||||||
campos: payload.campos,
|
|
||||||
})
|
|
||||||
|
|
||||||
return { ...result, conversacionId: currentId }
|
|
||||||
},
|
|
||||||
onSuccess: (data) => {
|
|
||||||
// Invalidamos mensajes para que se refresque el chat
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: ['subject-messages', data.conversacionId],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useConversationBySubject(subjectId: string | null) {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ['conversation-by-subject', subjectId],
|
|
||||||
queryFn: () => getConversationBySubject(subjectId!),
|
|
||||||
enabled: !!subjectId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMessagesBySubjectChat(conversationId: string | null) {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
const query = useQuery({
|
|
||||||
queryKey: ['subject-messages', conversationId],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!conversationId) throw new Error('Conversation ID is required')
|
|
||||||
return getMessagesBySubjectConversation(conversationId)
|
|
||||||
},
|
|
||||||
enabled: !!conversationId,
|
|
||||||
placeholderData: (previousData) => previousData,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!conversationId) return
|
|
||||||
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
|
|
||||||
// Suscripción a cambios en la tabla específica para esta conversación
|
|
||||||
const channel = supabase
|
|
||||||
.channel(`subject_messages_${conversationId}`)
|
|
||||||
.on(
|
|
||||||
'postgres_changes',
|
|
||||||
{
|
|
||||||
event: 'UPDATE', // Solo nos interesan las actualizaciones (cuando pasa de PROCESANDO a COMPLETADO)
|
|
||||||
schema: 'public',
|
|
||||||
table: 'asignatura_mensajes_ia',
|
|
||||||
filter: `conversacion_asignatura_id=eq.${conversationId}`,
|
|
||||||
},
|
|
||||||
(payload) => {
|
|
||||||
// Si el mensaje se completó o dio error, invalidamos la caché para traer los datos nuevos
|
|
||||||
if (
|
|
||||||
payload.new.estado === 'COMPLETADO' ||
|
|
||||||
payload.new.estado === 'ERROR'
|
|
||||||
) {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ['subject-messages', conversationId],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.subscribe()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
supabase.removeChannel(channel)
|
|
||||||
}
|
|
||||||
}, [conversationId, queryClient])
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateSubjectRecommendation() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (payload: { mensajeId: string; campoAfectado: string }) =>
|
|
||||||
update_subject_recommendation_applied(
|
|
||||||
payload.mensajeId,
|
|
||||||
payload.campoAfectado,
|
|
||||||
),
|
|
||||||
onSuccess: () => {
|
|
||||||
// Refrescamos los mensajes para ver el check de "aplicado"
|
|
||||||
qc.invalidateQueries({ queryKey: ['subject-messages'] })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateSubjectConversationStatus() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (payload: { id: string; estado: 'ARCHIVADA' | 'ACTIVA' }) =>
|
|
||||||
update_subject_conversation_status(payload.id, payload.estado),
|
|
||||||
onSuccess: () => {
|
|
||||||
qc.invalidateQueries({ queryKey: ['conversation-by-subject'] })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateSubjectConversationName() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (payload: { id: string; nombre: string }) =>
|
|
||||||
update_subject_conversation_name(payload.id, payload.nombre),
|
|
||||||
onSuccess: () => {
|
|
||||||
qc.invalidateQueries({ queryKey: ['conversation-by-subject'] })
|
|
||||||
// También invalidamos los mensajes si el título se muestra en la cabecera
|
|
||||||
qc.invalidateQueries({ queryKey: ['subject-messages'] })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,14 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query'
|
} from "@tanstack/react-query";
|
||||||
import { useEffect } from 'react'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ai_generate_plan,
|
ai_generate_plan,
|
||||||
getCatalogos,
|
|
||||||
plan_asignaturas_list,
|
plan_asignaturas_list,
|
||||||
plan_lineas_list,
|
plan_lineas_list,
|
||||||
plans_clone_from_existing,
|
plans_clone_from_existing,
|
||||||
plans_create_manual,
|
plans_create_manual,
|
||||||
plans_delete,
|
|
||||||
plans_generate_document,
|
plans_generate_document,
|
||||||
plans_get,
|
plans_get,
|
||||||
plans_get_document,
|
plans_get_document,
|
||||||
@@ -24,18 +21,16 @@ import {
|
|||||||
plans_transition_state,
|
plans_transition_state,
|
||||||
plans_update_fields,
|
plans_update_fields,
|
||||||
plans_update_map,
|
plans_update_map,
|
||||||
} from '../api/plans.api'
|
} from "../api/plans.api";
|
||||||
import { lineas_delete } from '../api/subjects.api'
|
import { qk } from "../query/keys";
|
||||||
import { qk } from '../query/keys'
|
|
||||||
import { supabaseBrowser } from '../supabase/client'
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
PlanListFilters,
|
PlanListFilters,
|
||||||
PlanMapOperation,
|
PlanMapOperation,
|
||||||
PlansCreateManualInput,
|
PlansCreateManualInput,
|
||||||
PlansUpdateFieldsPatch,
|
PlansUpdateFieldsPatch,
|
||||||
} from '../api/plans.api'
|
} from "../api/plans.api";
|
||||||
import type { UUID } from '../types/domain'
|
import type { UUID } from "../types/domain";
|
||||||
|
|
||||||
export function usePlanes(filters: PlanListFilters) {
|
export function usePlanes(filters: PlanListFilters) {
|
||||||
// 🧠 Tip: memoiza "filters" (useMemo) para que queryKey sea estable.
|
// 🧠 Tip: memoiza "filters" (useMemo) para que queryKey sea estable.
|
||||||
@@ -51,223 +46,124 @@ export function usePlanes(filters: PlanListFilters) {
|
|||||||
|
|
||||||
// Opcional: Tiempo que la data se considera fresca
|
// Opcional: Tiempo que la data se considera fresca
|
||||||
staleTime: 1000 * 60 * 5, // 5 minutos
|
staleTime: 1000 * 60 * 5, // 5 minutos
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlan(planId: UUID | null | undefined) {
|
export function usePlan(planId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: planId ? qk.plan(planId) : ['planes', 'detail', null],
|
queryKey: planId ? qk.plan(planId) : ["planes", "detail", null],
|
||||||
queryFn: () => {
|
queryFn: () => plans_get(planId as UUID),
|
||||||
console.log('usePlan')
|
|
||||||
return plans_get(planId as UUID)
|
|
||||||
},
|
|
||||||
enabled: Boolean(planId),
|
enabled: Boolean(planId),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlanLineas(planId: UUID | null | undefined) {
|
export function usePlanLineas(planId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: planId ? qk.planLineas(planId) : ['planes', 'lineas', null],
|
queryKey: planId ? qk.planLineas(planId) : ["planes", "lineas", null],
|
||||||
queryFn: () => plan_lineas_list(planId as UUID),
|
queryFn: () => plan_lineas_list(planId as UUID),
|
||||||
enabled: Boolean(planId),
|
enabled: Boolean(planId),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
const query = useQuery({
|
|
||||||
queryKey: planId
|
|
||||||
? qk.planAsignaturas(planId)
|
|
||||||
: ['planes', 'asignaturas', null],
|
|
||||||
queryFn: () => plan_asignaturas_list(planId as UUID),
|
|
||||||
enabled: Boolean(planId),
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!planId) return
|
|
||||||
|
|
||||||
const supabase = supabaseBrowser()
|
|
||||||
const channel = supabase.channel(`plan-asignaturas-${planId}`)
|
|
||||||
|
|
||||||
channel.on(
|
|
||||||
'postgres_changes',
|
|
||||||
{
|
|
||||||
event: '*',
|
|
||||||
schema: 'public',
|
|
||||||
table: 'asignaturas',
|
|
||||||
filter: `plan_estudio_id=eq.${planId}`,
|
|
||||||
},
|
|
||||||
(payload: {
|
|
||||||
eventType?: 'INSERT' | 'UPDATE' | 'DELETE'
|
|
||||||
new?: any
|
|
||||||
old?: any
|
|
||||||
}) => {
|
|
||||||
const eventType = payload.eventType
|
|
||||||
|
|
||||||
if (eventType === 'DELETE') {
|
|
||||||
const oldRow: any = payload.old
|
|
||||||
const deletedId = oldRow?.id
|
|
||||||
if (!deletedId) return
|
|
||||||
|
|
||||||
qc.setQueryData(qk.planAsignaturas(planId), (prev) => {
|
|
||||||
if (!Array.isArray(prev)) return prev
|
|
||||||
return prev.filter((a: any) => String(a?.id) !== String(deletedId))
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const newRow: any = payload.new
|
|
||||||
if (!newRow?.id) return
|
|
||||||
|
|
||||||
qc.setQueryData(qk.planAsignaturas(planId), (prev) => {
|
|
||||||
if (!Array.isArray(prev)) return prev
|
|
||||||
|
|
||||||
const idx = prev.findIndex(
|
|
||||||
(a: any) => String(a?.id) === String(newRow.id),
|
|
||||||
)
|
|
||||||
if (idx === -1) return [...prev, newRow]
|
|
||||||
|
|
||||||
const next = [...prev]
|
|
||||||
next[idx] = { ...prev[idx], ...newRow }
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
channel.subscribe()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
try {
|
|
||||||
supabase.removeChannel(channel)
|
|
||||||
} catch {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [planId, qc])
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePlanHistorial(
|
|
||||||
planId: UUID | null | undefined,
|
|
||||||
page: number,
|
|
||||||
) {
|
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: planId
|
queryKey: planId
|
||||||
? [...qk.planHistorial(planId), page]
|
? qk.planAsignaturas(planId)
|
||||||
: ['planes', 'historial', null, page],
|
: ["planes", "asignaturas", null],
|
||||||
queryFn: () => plans_history(planId as UUID, page),
|
queryFn: () => plan_asignaturas_list(planId as UUID),
|
||||||
enabled: Boolean(planId),
|
enabled: Boolean(planId),
|
||||||
placeholderData: (previousData) => previousData,
|
});
|
||||||
})
|
}
|
||||||
|
|
||||||
|
export function usePlanHistorial(planId: UUID | null | undefined) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: planId ? qk.planHistorial(planId) : ["planes", "historial", null],
|
||||||
|
queryFn: () => plans_history(planId as UUID),
|
||||||
|
enabled: Boolean(planId),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlanDocumento(planId: UUID | null | undefined) {
|
export function usePlanDocumento(planId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: planId ? qk.planDocumento(planId) : ['planes', 'documento', null],
|
queryKey: planId ? qk.planDocumento(planId) : ["planes", "documento", null],
|
||||||
queryFn: () => plans_get_document(planId as UUID),
|
queryFn: () => plans_get_document(planId as UUID),
|
||||||
enabled: Boolean(planId),
|
enabled: Boolean(planId),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function useCatalogosPlanes() {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: qk.estructurasPlan(),
|
|
||||||
queryFn: getCatalogos,
|
|
||||||
staleTime: 1000 * 60 * 60, // 1 hora de caché (estos datos casi no cambian)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------ Mutations ------------------ */
|
/* ------------------ Mutations ------------------ */
|
||||||
|
|
||||||
export function useCreatePlanManual() {
|
export function useCreatePlanManual() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (input: PlansCreateManualInput) => plans_create_manual(input),
|
mutationFn: (input: PlansCreateManualInput) => plans_create_manual(input),
|
||||||
onSuccess: (plan) => {
|
onSuccess: (plan) => {
|
||||||
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
qc.setQueryData(qk.plan(plan.id), plan)
|
qc.setQueryData(qk.plan(plan.id), plan);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGeneratePlanAI() {
|
export function useGeneratePlanAI() {
|
||||||
const qc = useQueryClient()
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ai_generate_plan,
|
mutationFn: ai_generate_plan,
|
||||||
onSuccess: (data) => {
|
});
|
||||||
// Asumiendo que la Edge Function devuelve { ok: true, plan: { id: ... } }
|
|
||||||
console.log('success de ai_generate_plan')
|
|
||||||
|
|
||||||
const newPlan = data.plan
|
|
||||||
|
|
||||||
if (newPlan) {
|
|
||||||
// 1. Invalidar la lista para que aparezca el nuevo plan
|
|
||||||
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
|
||||||
|
|
||||||
// 2. (Opcional) Pre-cargar el dato individual para que la navegación sea instantánea
|
|
||||||
// qc.setQueryData(["planes", "detail", newPlan.id], newPlan);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Funcion obsoleta porque ahora el plan se persiste directamente en useGeneratePlanAI
|
|
||||||
export function usePersistPlanFromAI() {
|
export function usePersistPlanFromAI() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: { jsonPlan: any }) => plans_persist_from_ai(payload),
|
mutationFn: (payload: { jsonPlan: any }) => plans_persist_from_ai(payload),
|
||||||
onSuccess: (plan) => {
|
onSuccess: (plan) => {
|
||||||
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
qc.setQueryData(qk.plan(plan.id), plan)
|
qc.setQueryData(qk.plan(plan.id), plan);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useClonePlan() {
|
export function useClonePlan() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: plans_clone_from_existing,
|
mutationFn: plans_clone_from_existing,
|
||||||
onSuccess: (plan) => {
|
onSuccess: (plan) => {
|
||||||
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
qc.setQueryData(qk.plan(plan.id), plan)
|
qc.setQueryData(qk.plan(plan.id), plan);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useImportPlanFromFiles() {
|
export function useImportPlanFromFiles() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: plans_import_from_files,
|
mutationFn: plans_import_from_files,
|
||||||
onSuccess: (plan) => {
|
onSuccess: (plan) => {
|
||||||
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
qc.setQueryData(qk.plan(plan.id), plan)
|
qc.setQueryData(qk.plan(plan.id), plan);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdatePlanFields() {
|
export function useUpdatePlanFields() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (vars: { planId: UUID; patch: PlansUpdateFieldsPatch }) =>
|
mutationFn: (vars: { planId: UUID; patch: PlansUpdateFieldsPatch }) =>
|
||||||
plans_update_fields(vars.planId, vars.patch),
|
plans_update_fields(vars.planId, vars.patch),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
qc.setQueryData(qk.plan(updated.id), updated)
|
qc.setQueryData(qk.plan(updated.id), updated);
|
||||||
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
qc.invalidateQueries({ queryKey: qk.planHistorial(updated.id) })
|
qc.invalidateQueries({ queryKey: qk.planHistorial(updated.id) });
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdatePlanMapa() {
|
export function useUpdatePlanMapa() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (vars: { planId: UUID; ops: Array<PlanMapOperation> }) =>
|
mutationFn: (vars: { planId: UUID; ops: Array<PlanMapOperation> }) =>
|
||||||
@@ -275,90 +171,61 @@ export function useUpdatePlanMapa() {
|
|||||||
|
|
||||||
// ✅ Optimista (rápida) para el caso MOVE_ASIGNATURA
|
// ✅ Optimista (rápida) para el caso MOVE_ASIGNATURA
|
||||||
onMutate: async (vars) => {
|
onMutate: async (vars) => {
|
||||||
await qc.cancelQueries({ queryKey: qk.planAsignaturas(vars.planId) })
|
await qc.cancelQueries({ queryKey: qk.planAsignaturas(vars.planId) });
|
||||||
const prev = qc.getQueryData<any>(qk.planAsignaturas(vars.planId))
|
const prev = qc.getQueryData<any>(qk.planAsignaturas(vars.planId));
|
||||||
|
|
||||||
// solo optimizamos MOVEs simples
|
// solo optimizamos MOVEs simples
|
||||||
const moves = vars.ops.filter((x) => x.op === 'MOVE_ASIGNATURA')
|
const moves = vars.ops.filter((x) => x.op === "MOVE_ASIGNATURA");
|
||||||
|
|
||||||
if (prev && Array.isArray(prev) && moves.length) {
|
if (prev && Array.isArray(prev) && moves.length) {
|
||||||
const next = prev.map((a: any) => {
|
const next = prev.map((a: any) => {
|
||||||
const m = moves.find((x) => x.asignaturaId === a.id)
|
const m = moves.find((x) => x.asignaturaId === a.id);
|
||||||
if (!m) return a
|
if (!m) return a;
|
||||||
return {
|
return {
|
||||||
...a,
|
...a,
|
||||||
numero_ciclo: m.numero_ciclo,
|
numero_ciclo: m.numero_ciclo,
|
||||||
linea_plan_id: m.linea_plan_id,
|
linea_plan_id: m.linea_plan_id,
|
||||||
orden_celda: m.orden_celda ?? a.orden_celda,
|
orden_celda: m.orden_celda ?? a.orden_celda,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
qc.setQueryData(qk.planAsignaturas(vars.planId), next)
|
qc.setQueryData(qk.planAsignaturas(vars.planId), next);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { prev }
|
return { prev };
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: (_err, vars, ctx) => {
|
onError: (_err, vars, ctx) => {
|
||||||
if (ctx?.prev) qc.setQueryData(qk.planAsignaturas(vars.planId), ctx.prev)
|
if (ctx?.prev) qc.setQueryData(qk.planAsignaturas(vars.planId), ctx.prev);
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: (_ok, vars) => {
|
onSuccess: (_ok, vars) => {
|
||||||
qc.invalidateQueries({ queryKey: qk.planAsignaturas(vars.planId) })
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(vars.planId) });
|
||||||
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) })
|
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) });
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTransitionPlanEstado() {
|
export function useTransitionPlanEstado() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: plans_transition_state,
|
mutationFn: plans_transition_state,
|
||||||
onSuccess: (_ok, vars) => {
|
onSuccess: (_ok, vars) => {
|
||||||
qc.invalidateQueries({ queryKey: qk.plan(vars.planId) })
|
qc.invalidateQueries({ queryKey: qk.plan(vars.planId) });
|
||||||
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) })
|
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) });
|
||||||
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
qc.invalidateQueries({ queryKey: ["planes", "list"] });
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function useDeletePlanEstudio() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (planId: UUID) => plans_delete(planId),
|
|
||||||
onSuccess: (_ok, planId) => {
|
|
||||||
qc.invalidateQueries({ queryKey: ['planes', 'list'] })
|
|
||||||
qc.removeQueries({ queryKey: qk.plan(planId) })
|
|
||||||
qc.removeQueries({ queryKey: qk.planMaybe(planId) })
|
|
||||||
qc.removeQueries({ queryKey: qk.planAsignaturas(planId) })
|
|
||||||
qc.removeQueries({ queryKey: qk.planLineas(planId) })
|
|
||||||
qc.removeQueries({ queryKey: qk.planHistorial(planId) })
|
|
||||||
qc.removeQueries({ queryKey: qk.planDocumento(planId) })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGeneratePlanDocumento() {
|
export function useGeneratePlanDocumento() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (planId: UUID) => plans_generate_document(planId),
|
mutationFn: (planId: UUID) => plans_generate_document(planId),
|
||||||
onSuccess: (_doc, planId) => {
|
onSuccess: (_doc, planId) => {
|
||||||
qc.invalidateQueries({ queryKey: qk.planDocumento(planId) })
|
qc.invalidateQueries({ queryKey: qk.planDocumento(planId) });
|
||||||
qc.invalidateQueries({ queryKey: qk.planHistorial(planId) })
|
qc.invalidateQueries({ queryKey: qk.planHistorial(planId) });
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function useDeleteLinea() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: lineas_delete,
|
|
||||||
onSuccess: (_idEliminado) => {
|
|
||||||
// Invalidamos para que las materias y líneas se refresquen
|
|
||||||
qc.invalidateQueries({ queryKey: ['plan_lineas'] })
|
|
||||||
qc.invalidateQueries({ queryKey: ['plan_asignaturas'] })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,319 +1,166 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { qk } from "../query/keys";
|
||||||
|
import type { UUID } from "../types/domain";
|
||||||
|
import type {
|
||||||
|
BibliografiaUpsertInput,
|
||||||
|
SubjectsCreateManualInput,
|
||||||
|
SubjectsUpdateFieldsPatch,
|
||||||
|
} from "../api/subjects.api";
|
||||||
import {
|
import {
|
||||||
ai_generate_subject,
|
ai_generate_subject,
|
||||||
asignaturas_update,
|
|
||||||
bibliografia_delete,
|
|
||||||
bibliografia_insert,
|
|
||||||
bibliografia_update,
|
|
||||||
lineas_insert,
|
|
||||||
lineas_update,
|
|
||||||
subjects_bibliografia_list,
|
subjects_bibliografia_list,
|
||||||
subjects_clone_from_existing,
|
subjects_clone_from_existing,
|
||||||
subjects_create_manual,
|
subjects_create_manual,
|
||||||
subjects_generate_document,
|
subjects_generate_document,
|
||||||
subjects_get,
|
subjects_get,
|
||||||
subjects_get_document,
|
subjects_get_document,
|
||||||
subjects_get_structure_catalog,
|
|
||||||
subjects_history,
|
subjects_history,
|
||||||
subjects_import_from_file,
|
subjects_import_from_file,
|
||||||
subjects_persist_from_ai,
|
subjects_persist_from_ai,
|
||||||
subjects_update_bibliografia,
|
subjects_update_bibliografia,
|
||||||
subjects_update_contenido,
|
subjects_update_contenido,
|
||||||
subjects_update_fields,
|
subjects_update_fields,
|
||||||
} from '../api/subjects.api'
|
} from "../api/subjects.api";
|
||||||
import { qk } from '../query/keys'
|
|
||||||
|
|
||||||
import type {
|
|
||||||
BibliografiaUpsertInput,
|
|
||||||
ContenidoApi,
|
|
||||||
SubjectsUpdateFieldsPatch,
|
|
||||||
} from '../api/subjects.api'
|
|
||||||
import type { UUID } from '../types/domain'
|
|
||||||
import type { TablesInsert } from '@/types/supabase'
|
|
||||||
|
|
||||||
export function useSubject(subjectId: UUID | null | undefined) {
|
export function useSubject(subjectId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: subjectId
|
queryKey: subjectId ? qk.asignatura(subjectId) : ["asignaturas", "detail", null],
|
||||||
? qk.asignatura(subjectId)
|
|
||||||
: ['asignaturas', 'detail', null],
|
|
||||||
queryFn: () => subjects_get(subjectId as UUID),
|
queryFn: () => subjects_get(subjectId as UUID),
|
||||||
enabled: Boolean(subjectId),
|
enabled: Boolean(subjectId),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubjectBibliografia(subjectId: UUID | null | undefined) {
|
export function useSubjectBibliografia(subjectId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: subjectId
|
queryKey: subjectId ? qk.asignaturaBibliografia(subjectId) : ["asignaturas", "bibliografia", null],
|
||||||
? qk.asignaturaBibliografia(subjectId)
|
|
||||||
: ['asignaturas', 'bibliografia', null],
|
|
||||||
queryFn: () => subjects_bibliografia_list(subjectId as UUID),
|
queryFn: () => subjects_bibliografia_list(subjectId as UUID),
|
||||||
enabled: Boolean(subjectId),
|
enabled: Boolean(subjectId),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubjectHistorial(subjectId: UUID | null | undefined) {
|
export function useSubjectHistorial(subjectId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: subjectId
|
queryKey: subjectId ? qk.asignaturaHistorial(subjectId) : ["asignaturas", "historial", null],
|
||||||
? qk.asignaturaHistorial(subjectId)
|
|
||||||
: ['asignaturas', 'historial', null],
|
|
||||||
queryFn: () => subjects_history(subjectId as UUID),
|
queryFn: () => subjects_history(subjectId as UUID),
|
||||||
enabled: Boolean(subjectId),
|
enabled: Boolean(subjectId),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubjectDocumento(subjectId: UUID | null | undefined) {
|
export function useSubjectDocumento(subjectId: UUID | null | undefined) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: subjectId
|
queryKey: subjectId ? qk.asignaturaDocumento(subjectId) : ["asignaturas", "documento", null],
|
||||||
? qk.asignaturaDocumento(subjectId)
|
|
||||||
: ['asignaturas', 'documento', null],
|
|
||||||
queryFn: () => subjects_get_document(subjectId as UUID),
|
queryFn: () => subjects_get_document(subjectId as UUID),
|
||||||
enabled: Boolean(subjectId),
|
enabled: Boolean(subjectId),
|
||||||
staleTime: 30_000,
|
staleTime: 30_000,
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function useSubjectEstructuras() {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: qk.estructurasAsignatura(),
|
|
||||||
queryFn: () => subjects_get_structure_catalog(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------ Mutations ------------------ */
|
/* ------------------ Mutations ------------------ */
|
||||||
|
|
||||||
export function useCreateSubjectManual() {
|
export function useCreateSubjectManual() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: TablesInsert<'asignaturas'>) =>
|
mutationFn: (payload: SubjectsCreateManualInput) => subjects_create_manual(payload),
|
||||||
subjects_create_manual(payload),
|
|
||||||
onSuccess: (subject) => {
|
onSuccess: (subject) => {
|
||||||
qc.setQueryData(qk.asignatura(subject.id), subject)
|
qc.setQueryData(qk.asignatura(subject.id), subject);
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) });
|
||||||
queryKey: qk.planAsignaturas(subject.plan_estudio_id),
|
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) });
|
||||||
})
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planHistorial(subject.plan_estudio_id),
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGenerateSubjectAI() {
|
export function useGenerateSubjectAI() {
|
||||||
return useMutation({
|
return useMutation({ mutationFn: ai_generate_subject });
|
||||||
mutationFn: ai_generate_subject,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePersistSubjectFromAI() {
|
export function usePersistSubjectFromAI() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: { planId: UUID; jsonAsignatura: any }) =>
|
mutationFn: (payload: { planId: UUID; jsonMateria: any }) => subjects_persist_from_ai(payload),
|
||||||
subjects_persist_from_ai(payload),
|
|
||||||
onSuccess: (subject) => {
|
onSuccess: (subject) => {
|
||||||
qc.setQueryData(qk.asignatura(subject.id), subject)
|
qc.setQueryData(qk.asignatura(subject.id), subject);
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) });
|
||||||
queryKey: qk.planAsignaturas(subject.plan_estudio_id),
|
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) });
|
||||||
})
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planHistorial(subject.plan_estudio_id),
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCloneSubject() {
|
export function useCloneSubject() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: subjects_clone_from_existing,
|
mutationFn: subjects_clone_from_existing,
|
||||||
onSuccess: (subject) => {
|
onSuccess: (subject) => {
|
||||||
qc.setQueryData(qk.asignatura(subject.id), subject)
|
qc.setQueryData(qk.asignatura(subject.id), subject);
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) });
|
||||||
queryKey: qk.planAsignaturas(subject.plan_estudio_id),
|
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) });
|
||||||
})
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planHistorial(subject.plan_estudio_id),
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useImportSubjectFromFile() {
|
export function useImportSubjectFromFile() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: subjects_import_from_file,
|
mutationFn: subjects_import_from_file,
|
||||||
onSuccess: (subject) => {
|
onSuccess: (subject) => {
|
||||||
qc.setQueryData(qk.asignatura(subject.id), subject)
|
qc.setQueryData(qk.asignatura(subject.id), subject);
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) });
|
||||||
queryKey: qk.planAsignaturas(subject.plan_estudio_id),
|
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) });
|
||||||
})
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planHistorial(subject.plan_estudio_id),
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateSubjectFields() {
|
export function useUpdateSubjectFields() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) =>
|
mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) =>
|
||||||
subjects_update_fields(vars.subjectId, vars.patch),
|
subjects_update_fields(vars.subjectId, vars.patch),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
qc.setQueryData(qk.asignatura(updated.id), updated);
|
||||||
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
qc.invalidateQueries({ queryKey: qk.planAsignaturas(updated.plan_estudio_id) });
|
||||||
)
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) });
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
|
||||||
})
|
|
||||||
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateSubjectContenido() {
|
export function useUpdateSubjectContenido() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (vars: { subjectId: UUID; unidades: Array<ContenidoApi> }) =>
|
mutationFn: (vars: { subjectId: UUID; unidades: any[] }) =>
|
||||||
subjects_update_contenido(vars.subjectId, vars.unidades),
|
subjects_update_contenido(vars.subjectId, vars.unidades),
|
||||||
onSuccess: (updated) => {
|
onSuccess: (updated) => {
|
||||||
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
qc.setQueryData(qk.asignatura(updated.id), updated);
|
||||||
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) });
|
||||||
)
|
|
||||||
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
|
||||||
})
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planHistorial(updated.plan_estudio_id),
|
|
||||||
})
|
|
||||||
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUpdateSubjectBibliografia() {
|
export function useUpdateSubjectBibliografia() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (vars: { subjectId: UUID; entries: BibliografiaUpsertInput }) =>
|
mutationFn: (vars: { subjectId: UUID; entries: BibliografiaUpsertInput }) =>
|
||||||
subjects_update_bibliografia(vars.subjectId, vars.entries),
|
subjects_update_bibliografia(vars.subjectId, vars.entries),
|
||||||
onSuccess: (_ok, vars) => {
|
onSuccess: (_ok, vars) => {
|
||||||
qc.invalidateQueries({
|
qc.invalidateQueries({ queryKey: qk.asignaturaBibliografia(vars.subjectId) });
|
||||||
queryKey: qk.asignaturaBibliografia(vars.subjectId),
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(vars.subjectId) });
|
||||||
})
|
|
||||||
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(vars.subjectId) })
|
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGenerateSubjectDocumento() {
|
export function useGenerateSubjectDocumento() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (subjectId: UUID) => subjects_generate_document(subjectId),
|
mutationFn: (subjectId: UUID) => subjects_generate_document(subjectId),
|
||||||
onSuccess: (_doc, subjectId) => {
|
onSuccess: (_doc, subjectId) => {
|
||||||
qc.invalidateQueries({ queryKey: qk.asignaturaDocumento(subjectId) })
|
qc.invalidateQueries({ queryKey: qk.asignaturaDocumento(subjectId) });
|
||||||
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(subjectId) })
|
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(subjectId) });
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateAsignatura() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (vars: {
|
|
||||||
asignaturaId: UUID
|
|
||||||
patch: Partial<SubjectsUpdateFieldsPatch>
|
|
||||||
}) => asignaturas_update(vars.asignaturaId, vars.patch),
|
|
||||||
|
|
||||||
onSuccess: (updated) => {
|
|
||||||
// ✅ Mantener consistencia con las query keys centralizadas (qk)
|
|
||||||
// 1) Actualiza el detalle (esto evita volver a entrar con caché vieja)
|
|
||||||
qc.setQueryData(qk.asignatura(updated.id), (prev) =>
|
|
||||||
prev ? { ...(prev as any), ...(updated as any) } : updated,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 2) Refresca vistas derivadas del plan
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planAsignaturas(updated.plan_estudio_id),
|
|
||||||
})
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.planHistorial(updated.plan_estudio_id),
|
|
||||||
})
|
|
||||||
|
|
||||||
// 3) Refresca historial de la asignatura si existe
|
|
||||||
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCreateLinea() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: lineas_insert,
|
|
||||||
onSuccess: (nuevaLinea) => {
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: ['plan_lineas', nuevaLinea.plan_estudio_id],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateLinea() {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (vars: { lineaId: string; patch: any }) =>
|
|
||||||
lineas_update(vars.lineaId, vars.patch),
|
|
||||||
onSuccess: (updated) => {
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: ['plan_lineas', updated.plan_estudio_id],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCreateBibliografia() {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: bibliografia_insert,
|
|
||||||
onSuccess: (data) => {
|
|
||||||
// USAR LA MISMA LLAVE QUE EL HOOK DE LECTURA
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: qk.asignaturaBibliografia(data.asignatura_id),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUpdateBibliografia(asignaturaId: string) {
|
|
||||||
const qc = useQueryClient()
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: ({ id, updates }: { id: string; updates: any }) =>
|
|
||||||
bibliografia_update(id, updates),
|
|
||||||
onSuccess: () => {
|
|
||||||
qc.invalidateQueries({
|
|
||||||
queryKey: qk.asignaturaBibliografia(asignaturaId),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDeleteBibliografia(asignaturaId: string) {
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: (id: string) => bibliografia_delete(id),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: qk.asignaturaBibliografia(asignaturaId),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
import type {
|
|
||||||
Asignatura,
|
|
||||||
AsignaturaStructure,
|
|
||||||
UnidadTematica,
|
|
||||||
BibliografiaEntry,
|
|
||||||
CambioAsignatura,
|
|
||||||
DocumentoAsignatura,
|
|
||||||
} from '@/types/asignatura'
|
|
||||||
|
|
||||||
export const mockAsignatura: Asignatura = {
|
|
||||||
id: '1',
|
|
||||||
nombre: 'Inteligencia Artificial Aplicada',
|
|
||||||
clave: 'IAA-401',
|
|
||||||
creditos: 8,
|
|
||||||
lineaCurricular: 'Sistemas Inteligentes',
|
|
||||||
ciclo: '7° Semestre',
|
|
||||||
planId: 'plan-1',
|
|
||||||
planNombre: 'Licenciatura en Ingeniería en Sistemas Computacionales 2024',
|
|
||||||
carrera: 'Ingeniería en Sistemas Computacionales',
|
|
||||||
facultad: 'Facultad de Ingeniería',
|
|
||||||
estructuraId: 'estructura-1',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mockEstructura: AsignaturaStructure = {
|
|
||||||
id: 'estructura-1',
|
|
||||||
nombre: 'Plantilla SEP Licenciatura',
|
|
||||||
campos: [
|
|
||||||
{
|
|
||||||
id: 'objetivo_general',
|
|
||||||
nombre: 'Objetivo General',
|
|
||||||
tipo: 'texto_largo',
|
|
||||||
obligatorio: true,
|
|
||||||
descripcion: 'Describe el propósito principal de la asignatura',
|
|
||||||
placeholder: 'Al finalizar el curso, el estudiante será capaz de...',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'competencias',
|
|
||||||
nombre: 'Competencias a Desarrollar',
|
|
||||||
tipo: 'texto_largo',
|
|
||||||
obligatorio: true,
|
|
||||||
descripcion: 'Competencias profesionales que se desarrollarán',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'justificacion',
|
|
||||||
nombre: 'Justificación',
|
|
||||||
tipo: 'texto_largo',
|
|
||||||
obligatorio: true,
|
|
||||||
descripcion: 'Relevancia de la asignatura en el plan de estudios',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'requisitos',
|
|
||||||
nombre: 'Requisitos / Seriación',
|
|
||||||
tipo: 'texto',
|
|
||||||
obligatorio: false,
|
|
||||||
descripcion: 'Asignaturas previas requeridas',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'estrategias_didacticas',
|
|
||||||
nombre: 'Estrategias Didácticas',
|
|
||||||
tipo: 'texto_largo',
|
|
||||||
obligatorio: true,
|
|
||||||
descripcion: 'Métodos de enseñanza-aprendizaje',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'evaluacion',
|
|
||||||
nombre: 'Sistema de Evaluación',
|
|
||||||
tipo: 'texto_largo',
|
|
||||||
obligatorio: true,
|
|
||||||
descripcion: 'Criterios y porcentajes de evaluación',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'perfil_docente',
|
|
||||||
nombre: 'Perfil del Docente',
|
|
||||||
tipo: 'texto_largo',
|
|
||||||
obligatorio: false,
|
|
||||||
descripcion: 'Características requeridas del profesor',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mockDatosGenerales: Record<string, any> = {
|
|
||||||
objetivo_general:
|
|
||||||
'Formar profesionales capaces de diseñar, implementar y evaluar sistemas de inteligencia artificial que resuelvan problemas complejos del mundo real, aplicando principios éticos y metodologías actuales en el campo.',
|
|
||||||
competencias:
|
|
||||||
'• Diseñar algoritmos de machine learning para clasificación y predicción\n• Implementar redes neuronales profundas para procesamiento de imágenes y texto\n• Evaluar y optimizar modelos de IA considerando métricas de rendimiento\n• Aplicar principios éticos en el desarrollo de sistemas inteligentes',
|
|
||||||
justificacion:
|
|
||||||
'La inteligencia artificial es una de las tecnologías más disruptivas del siglo XXI. Su integración en diversos sectores demanda profesionales con sólidas bases teóricas y prácticas. Esta asignatura proporciona las competencias necesarias para que el egresado pueda innovar y contribuir al desarrollo tecnológico del país.',
|
|
||||||
requisitos:
|
|
||||||
'Programación Avanzada (PAV-301), Matemáticas Discretas (MAT-201)',
|
|
||||||
estrategias_didacticas:
|
|
||||||
'• Aprendizaje basado en proyectos\n• Talleres prácticos con datasets reales\n• Exposiciones y discusiones grupales\n• Análisis de casos de estudio\n• Desarrollo de prototipo integrador',
|
|
||||||
evaluacion:
|
|
||||||
'• Exámenes parciales: 30%\n• Proyecto integrador: 35%\n• Prácticas de laboratorio: 20%\n• Participación y tareas: 15%',
|
|
||||||
perfil_docente:
|
|
||||||
'Profesional con maestría o doctorado en áreas afines a la inteligencia artificial, con experiencia mínima de 3 años en docencia y desarrollo de proyectos de IA.',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mockContenidoTematico: Array<UnidadTematica> = [
|
|
||||||
{
|
|
||||||
id: 'unidad-1',
|
|
||||||
nombre: 'Fundamentos de Inteligencia Artificial',
|
|
||||||
numero: 1,
|
|
||||||
temas: [
|
|
||||||
{
|
|
||||||
id: 'tema-1-1',
|
|
||||||
nombre: 'Historia y evolución de la IA',
|
|
||||||
descripcion: 'Desde los orígenes hasta la actualidad',
|
|
||||||
horasEstimadas: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tema-1-2',
|
|
||||||
nombre: 'Tipos de IA y aplicaciones',
|
|
||||||
descripcion: 'IA débil, fuerte y superinteligencia',
|
|
||||||
horasEstimadas: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tema-1-3',
|
|
||||||
nombre: 'Ética en IA',
|
|
||||||
descripcion: 'Consideraciones éticas y responsabilidad',
|
|
||||||
horasEstimadas: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'unidad-2',
|
|
||||||
nombre: 'Machine Learning',
|
|
||||||
numero: 2,
|
|
||||||
temas: [
|
|
||||||
{
|
|
||||||
id: 'tema-2-1',
|
|
||||||
nombre: 'Aprendizaje supervisado',
|
|
||||||
descripcion: 'Regresión y clasificación',
|
|
||||||
horasEstimadas: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tema-2-2',
|
|
||||||
nombre: 'Aprendizaje no supervisado',
|
|
||||||
descripcion: 'Clustering y reducción de dimensionalidad',
|
|
||||||
horasEstimadas: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tema-2-3',
|
|
||||||
nombre: 'Evaluación de modelos',
|
|
||||||
descripcion: 'Métricas y validación cruzada',
|
|
||||||
horasEstimadas: 4,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'unidad-3',
|
|
||||||
nombre: 'Deep Learning',
|
|
||||||
numero: 3,
|
|
||||||
temas: [
|
|
||||||
{
|
|
||||||
id: 'tema-3-1',
|
|
||||||
nombre: 'Redes neuronales artificiales',
|
|
||||||
descripcion: 'Perceptrón y backpropagation',
|
|
||||||
horasEstimadas: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tema-3-2',
|
|
||||||
nombre: 'Redes convolucionales (CNN)',
|
|
||||||
descripcion: 'Procesamiento de imágenes',
|
|
||||||
horasEstimadas: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tema-3-3',
|
|
||||||
nombre: 'Redes recurrentes (RNN)',
|
|
||||||
descripcion: 'Procesamiento de secuencias',
|
|
||||||
horasEstimadas: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tema-3-4',
|
|
||||||
nombre: 'Transformers y atención',
|
|
||||||
descripcion: 'Arquitecturas modernas',
|
|
||||||
horasEstimadas: 6,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'unidad-4',
|
|
||||||
nombre: 'Aplicaciones Prácticas',
|
|
||||||
numero: 4,
|
|
||||||
temas: [
|
|
||||||
{
|
|
||||||
id: 'tema-4-1',
|
|
||||||
nombre: 'Procesamiento de lenguaje natural',
|
|
||||||
descripcion: 'NLP y chatbots',
|
|
||||||
horasEstimadas: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tema-4-2',
|
|
||||||
nombre: 'Visión por computadora',
|
|
||||||
descripcion: 'Detección y reconocimiento',
|
|
||||||
horasEstimadas: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'tema-4-3',
|
|
||||||
nombre: 'Sistemas de recomendación',
|
|
||||||
descripcion: 'Filtrado colaborativo y contenido',
|
|
||||||
horasEstimadas: 4,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const mockBibliografia: Array<BibliografiaEntry> = [
|
|
||||||
{
|
|
||||||
id: 'bib-1',
|
|
||||||
tipo: 'BASICA',
|
|
||||||
cita: 'Russell, S., & Norvig, P. (2021). Artificial Intelligence: A Modern Approach (4th ed.). Pearson.',
|
|
||||||
fuenteBibliotecaId: 'lib-1',
|
|
||||||
fuenteBiblioteca: {
|
|
||||||
id: 'lib-1',
|
|
||||||
titulo: 'Artificial Intelligence: A Modern Approach',
|
|
||||||
autor: 'Stuart Russell, Peter Norvig',
|
|
||||||
editorial: 'Pearson',
|
|
||||||
anio: 2021,
|
|
||||||
isbn: '978-0134610993',
|
|
||||||
tipo: 'libro',
|
|
||||||
disponible: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bib-2',
|
|
||||||
tipo: 'BASICA',
|
|
||||||
cita: "Géron, A. (2022). Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (3rd ed.). O'Reilly Media.",
|
|
||||||
fuenteBibliotecaId: 'lib-2',
|
|
||||||
fuenteBiblioteca: {
|
|
||||||
id: 'lib-2',
|
|
||||||
titulo:
|
|
||||||
'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
|
|
||||||
autor: 'Aurélien Géron',
|
|
||||||
editorial: "O'Reilly Media",
|
|
||||||
anio: 2022,
|
|
||||||
isbn: '978-1098125974',
|
|
||||||
tipo: 'libro',
|
|
||||||
disponible: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bib-3',
|
|
||||||
tipo: 'COMPLEMENTARIA',
|
|
||||||
cita: 'Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'bib-4',
|
|
||||||
tipo: 'COMPLEMENTARIA',
|
|
||||||
cita: 'Chollet, F. (2021). Deep Learning with Python (2nd ed.). Manning Publications.',
|
|
||||||
fuenteBibliotecaId: 'lib-4',
|
|
||||||
fuenteBiblioteca: {
|
|
||||||
id: 'lib-4',
|
|
||||||
titulo: 'Deep Learning with Python',
|
|
||||||
autor: 'François Chollet',
|
|
||||||
editorial: 'Manning Publications',
|
|
||||||
anio: 2021,
|
|
||||||
isbn: '978-1617296864',
|
|
||||||
tipo: 'libro',
|
|
||||||
disponible: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const mockHistorial: Array<CambioAsignatura> = [
|
|
||||||
{
|
|
||||||
id: 'cambio-1',
|
|
||||||
tipo: 'datos',
|
|
||||||
descripcion: 'Actualización del objetivo general',
|
|
||||||
usuario: 'Dr. Carlos Méndez',
|
|
||||||
fecha: new Date('2024-12-10T14:30:00'),
|
|
||||||
detalles: { campo: 'objetivo_general' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'cambio-2',
|
|
||||||
tipo: 'contenido',
|
|
||||||
descripcion: 'Agregada Unidad 4: Aplicaciones Prácticas',
|
|
||||||
usuario: 'Dr. Carlos Méndez',
|
|
||||||
fecha: new Date('2024-12-09T10:15:00'),
|
|
||||||
detalles: { unidad: 'Unidad 4' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'cambio-3',
|
|
||||||
tipo: 'ia',
|
|
||||||
descripcion: 'IA mejoró las competencias a desarrollar',
|
|
||||||
usuario: 'Dra. María López',
|
|
||||||
fecha: new Date('2024-12-08T16:45:00'),
|
|
||||||
detalles: { campo: 'competencias', accion: 'mejora' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'cambio-4',
|
|
||||||
tipo: 'bibliografia',
|
|
||||||
descripcion: 'Añadida referencia: Deep Learning with Python',
|
|
||||||
usuario: 'Biblioteca Central',
|
|
||||||
fecha: new Date('2024-12-07T09:00:00'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'cambio-5',
|
|
||||||
tipo: 'documento',
|
|
||||||
descripcion: 'Documento SEP regenerado (versión 3)',
|
|
||||||
usuario: 'Sistema',
|
|
||||||
fecha: new Date('2024-12-06T11:30:00'),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const mockDocumentoSep: DocumentoAsignatura = {
|
|
||||||
id: 'doc-1',
|
|
||||||
asignaturaId: '1',
|
|
||||||
version: 3,
|
|
||||||
fechaGeneracion: new Date('2024-12-06T11:30:00'),
|
|
||||||
estado: 'listo',
|
|
||||||
}
|
|
||||||
302
src/data/mockMateriaData.ts
Normal file
302
src/data/mockMateriaData.ts
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
import type {
|
||||||
|
Materia,
|
||||||
|
MateriaStructure,
|
||||||
|
UnidadTematica,
|
||||||
|
BibliografiaEntry,
|
||||||
|
CambioMateria,
|
||||||
|
DocumentoMateria,
|
||||||
|
LibraryResource
|
||||||
|
} from '@/types/materia';
|
||||||
|
|
||||||
|
export const mockMateria: Materia = {
|
||||||
|
id: '1',
|
||||||
|
nombre: 'Inteligencia Artificial Aplicada',
|
||||||
|
clave: 'IAA-401',
|
||||||
|
creditos: 8,
|
||||||
|
lineaCurricular: 'Sistemas Inteligentes',
|
||||||
|
ciclo: '7° Semestre',
|
||||||
|
planId: 'plan-1',
|
||||||
|
planNombre: 'Licenciatura en Ingeniería en Sistemas Computacionales 2024',
|
||||||
|
carrera: 'Ingeniería en Sistemas Computacionales',
|
||||||
|
facultad: 'Facultad de Ingeniería',
|
||||||
|
estructuraId: 'estructura-1',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockEstructura: MateriaStructure = {
|
||||||
|
id: 'estructura-1',
|
||||||
|
nombre: 'Plantilla SEP Licenciatura',
|
||||||
|
campos: [
|
||||||
|
{
|
||||||
|
id: 'objetivo_general',
|
||||||
|
nombre: 'Objetivo General',
|
||||||
|
tipo: 'texto_largo',
|
||||||
|
obligatorio: true,
|
||||||
|
descripcion: 'Describe el propósito principal de la materia',
|
||||||
|
placeholder: 'Al finalizar el curso, el estudiante será capaz de...',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'competencias',
|
||||||
|
nombre: 'Competencias a Desarrollar',
|
||||||
|
tipo: 'texto_largo',
|
||||||
|
obligatorio: true,
|
||||||
|
descripcion: 'Competencias profesionales que se desarrollarán',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'justificacion',
|
||||||
|
nombre: 'Justificación',
|
||||||
|
tipo: 'texto_largo',
|
||||||
|
obligatorio: true,
|
||||||
|
descripcion: 'Relevancia de la materia en el plan de estudios',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'requisitos',
|
||||||
|
nombre: 'Requisitos / Seriación',
|
||||||
|
tipo: 'texto',
|
||||||
|
obligatorio: false,
|
||||||
|
descripcion: 'Materias previas requeridas',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'estrategias_didacticas',
|
||||||
|
nombre: 'Estrategias Didácticas',
|
||||||
|
tipo: 'texto_largo',
|
||||||
|
obligatorio: true,
|
||||||
|
descripcion: 'Métodos de enseñanza-aprendizaje',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evaluacion',
|
||||||
|
nombre: 'Sistema de Evaluación',
|
||||||
|
tipo: 'texto_largo',
|
||||||
|
obligatorio: true,
|
||||||
|
descripcion: 'Criterios y porcentajes de evaluación',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perfil_docente',
|
||||||
|
nombre: 'Perfil del Docente',
|
||||||
|
tipo: 'texto_largo',
|
||||||
|
obligatorio: false,
|
||||||
|
descripcion: 'Características requeridas del profesor',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockDatosGenerales: Record<string, any> = {
|
||||||
|
objetivo_general: 'Formar profesionales capaces de diseñar, implementar y evaluar sistemas de inteligencia artificial que resuelvan problemas complejos del mundo real, aplicando principios éticos y metodologías actuales en el campo.',
|
||||||
|
competencias: '• Diseñar algoritmos de machine learning para clasificación y predicción\n• Implementar redes neuronales profundas para procesamiento de imágenes y texto\n• Evaluar y optimizar modelos de IA considerando métricas de rendimiento\n• Aplicar principios éticos en el desarrollo de sistemas inteligentes',
|
||||||
|
justificacion: 'La inteligencia artificial es una de las tecnologías más disruptivas del siglo XXI. Su integración en diversos sectores demanda profesionales con sólidas bases teóricas y prácticas. Esta materia proporciona las competencias necesarias para que el egresado pueda innovar y contribuir al desarrollo tecnológico del país.',
|
||||||
|
requisitos: 'Programación Avanzada (PAV-301), Matemáticas Discretas (MAT-201)',
|
||||||
|
estrategias_didacticas: '• Aprendizaje basado en proyectos\n• Talleres prácticos con datasets reales\n• Exposiciones y discusiones grupales\n• Análisis de casos de estudio\n• Desarrollo de prototipo integrador',
|
||||||
|
evaluacion: '• Exámenes parciales: 30%\n• Proyecto integrador: 35%\n• Prácticas de laboratorio: 20%\n• Participación y tareas: 15%',
|
||||||
|
perfil_docente: 'Profesional con maestría o doctorado en áreas afines a la inteligencia artificial, con experiencia mínima de 3 años en docencia y desarrollo de proyectos de IA.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockContenidoTematico: UnidadTematica[] = [
|
||||||
|
{
|
||||||
|
id: 'unidad-1',
|
||||||
|
nombre: 'Fundamentos de Inteligencia Artificial',
|
||||||
|
numero: 1,
|
||||||
|
temas: [
|
||||||
|
{ id: 'tema-1-1', nombre: 'Historia y evolución de la IA', descripcion: 'Desde los orígenes hasta la actualidad', horasEstimadas: 2 },
|
||||||
|
{ id: 'tema-1-2', nombre: 'Tipos de IA y aplicaciones', descripcion: 'IA débil, fuerte y superinteligencia', horasEstimadas: 3 },
|
||||||
|
{ id: 'tema-1-3', nombre: 'Ética en IA', descripcion: 'Consideraciones éticas y responsabilidad', horasEstimadas: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'unidad-2',
|
||||||
|
nombre: 'Machine Learning',
|
||||||
|
numero: 2,
|
||||||
|
temas: [
|
||||||
|
{ id: 'tema-2-1', nombre: 'Aprendizaje supervisado', descripcion: 'Regresión y clasificación', horasEstimadas: 6 },
|
||||||
|
{ id: 'tema-2-2', nombre: 'Aprendizaje no supervisado', descripcion: 'Clustering y reducción de dimensionalidad', horasEstimadas: 5 },
|
||||||
|
{ id: 'tema-2-3', nombre: 'Evaluación de modelos', descripcion: 'Métricas y validación cruzada', horasEstimadas: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'unidad-3',
|
||||||
|
nombre: 'Deep Learning',
|
||||||
|
numero: 3,
|
||||||
|
temas: [
|
||||||
|
{ id: 'tema-3-1', nombre: 'Redes neuronales artificiales', descripcion: 'Perceptrón y backpropagation', horasEstimadas: 5 },
|
||||||
|
{ id: 'tema-3-2', nombre: 'Redes convolucionales (CNN)', descripcion: 'Procesamiento de imágenes', horasEstimadas: 6 },
|
||||||
|
{ id: 'tema-3-3', nombre: 'Redes recurrentes (RNN)', descripcion: 'Procesamiento de secuencias', horasEstimadas: 5 },
|
||||||
|
{ id: 'tema-3-4', nombre: 'Transformers y atención', descripcion: 'Arquitecturas modernas', horasEstimadas: 6 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'unidad-4',
|
||||||
|
nombre: 'Aplicaciones Prácticas',
|
||||||
|
numero: 4,
|
||||||
|
temas: [
|
||||||
|
{ id: 'tema-4-1', nombre: 'Procesamiento de lenguaje natural', descripcion: 'NLP y chatbots', horasEstimadas: 6 },
|
||||||
|
{ id: 'tema-4-2', nombre: 'Visión por computadora', descripcion: 'Detección y reconocimiento', horasEstimadas: 5 },
|
||||||
|
{ id: 'tema-4-3', nombre: 'Sistemas de recomendación', descripcion: 'Filtrado colaborativo y contenido', horasEstimadas: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockBibliografia: BibliografiaEntry[] = [
|
||||||
|
{
|
||||||
|
id: 'bib-1',
|
||||||
|
tipo: 'BASICA',
|
||||||
|
cita: 'Russell, S., & Norvig, P. (2021). Artificial Intelligence: A Modern Approach (4th ed.). Pearson.',
|
||||||
|
fuenteBibliotecaId: 'lib-1',
|
||||||
|
fuenteBiblioteca: {
|
||||||
|
id: 'lib-1',
|
||||||
|
titulo: 'Artificial Intelligence: A Modern Approach',
|
||||||
|
autor: 'Stuart Russell, Peter Norvig',
|
||||||
|
editorial: 'Pearson',
|
||||||
|
anio: 2021,
|
||||||
|
isbn: '978-0134610993',
|
||||||
|
tipo: 'libro',
|
||||||
|
disponible: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bib-2',
|
||||||
|
tipo: 'BASICA',
|
||||||
|
cita: 'Géron, A. (2022). Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow (3rd ed.). O\'Reilly Media.',
|
||||||
|
fuenteBibliotecaId: 'lib-2',
|
||||||
|
fuenteBiblioteca: {
|
||||||
|
id: 'lib-2',
|
||||||
|
titulo: 'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
|
||||||
|
autor: 'Aurélien Géron',
|
||||||
|
editorial: 'O\'Reilly Media',
|
||||||
|
anio: 2022,
|
||||||
|
isbn: '978-1098125974',
|
||||||
|
tipo: 'libro',
|
||||||
|
disponible: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bib-3',
|
||||||
|
tipo: 'COMPLEMENTARIA',
|
||||||
|
cita: 'Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning. MIT Press.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bib-4',
|
||||||
|
tipo: 'COMPLEMENTARIA',
|
||||||
|
cita: 'Chollet, F. (2021). Deep Learning with Python (2nd ed.). Manning Publications.',
|
||||||
|
fuenteBibliotecaId: 'lib-4',
|
||||||
|
fuenteBiblioteca: {
|
||||||
|
id: 'lib-4',
|
||||||
|
titulo: 'Deep Learning with Python',
|
||||||
|
autor: 'François Chollet',
|
||||||
|
editorial: 'Manning Publications',
|
||||||
|
anio: 2021,
|
||||||
|
isbn: '978-1617296864',
|
||||||
|
tipo: 'libro',
|
||||||
|
disponible: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockHistorial: CambioMateria[] = [
|
||||||
|
{
|
||||||
|
id: 'cambio-1',
|
||||||
|
tipo: 'datos',
|
||||||
|
descripcion: 'Actualización del objetivo general',
|
||||||
|
usuario: 'Dr. Carlos Méndez',
|
||||||
|
fecha: new Date('2024-12-10T14:30:00'),
|
||||||
|
detalles: { campo: 'objetivo_general' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cambio-2',
|
||||||
|
tipo: 'contenido',
|
||||||
|
descripcion: 'Agregada Unidad 4: Aplicaciones Prácticas',
|
||||||
|
usuario: 'Dr. Carlos Méndez',
|
||||||
|
fecha: new Date('2024-12-09T10:15:00'),
|
||||||
|
detalles: { unidad: 'Unidad 4' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cambio-3',
|
||||||
|
tipo: 'ia',
|
||||||
|
descripcion: 'IA mejoró las competencias a desarrollar',
|
||||||
|
usuario: 'Dra. María López',
|
||||||
|
fecha: new Date('2024-12-08T16:45:00'),
|
||||||
|
detalles: { campo: 'competencias', accion: 'mejora' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cambio-4',
|
||||||
|
tipo: 'bibliografia',
|
||||||
|
descripcion: 'Añadida referencia: Deep Learning with Python',
|
||||||
|
usuario: 'Biblioteca Central',
|
||||||
|
fecha: new Date('2024-12-07T09:00:00'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cambio-5',
|
||||||
|
tipo: 'documento',
|
||||||
|
descripcion: 'Documento SEP regenerado (versión 3)',
|
||||||
|
usuario: 'Sistema',
|
||||||
|
fecha: new Date('2024-12-06T11:30:00'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const mockDocumentoSep: DocumentoMateria = {
|
||||||
|
id: 'doc-1',
|
||||||
|
materiaId: '1',
|
||||||
|
version: 3,
|
||||||
|
fechaGeneracion: new Date('2024-12-06T11:30:00'),
|
||||||
|
estado: 'listo',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockLibraryResources: LibraryResource[] = [
|
||||||
|
{
|
||||||
|
id: 'lib-1',
|
||||||
|
titulo: 'Artificial Intelligence: A Modern Approach',
|
||||||
|
autor: 'Stuart Russell, Peter Norvig',
|
||||||
|
editorial: 'Pearson',
|
||||||
|
anio: 2021,
|
||||||
|
isbn: '978-0134610993',
|
||||||
|
tipo: 'libro',
|
||||||
|
disponible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lib-2',
|
||||||
|
titulo: 'Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow',
|
||||||
|
autor: 'Aurélien Géron',
|
||||||
|
editorial: 'O\'Reilly Media',
|
||||||
|
anio: 2022,
|
||||||
|
isbn: '978-1098125974',
|
||||||
|
tipo: 'libro',
|
||||||
|
disponible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lib-3',
|
||||||
|
titulo: 'Pattern Recognition and Machine Learning',
|
||||||
|
autor: 'Christopher Bishop',
|
||||||
|
editorial: 'Springer',
|
||||||
|
anio: 2006,
|
||||||
|
isbn: '978-0387310732',
|
||||||
|
tipo: 'libro',
|
||||||
|
disponible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lib-4',
|
||||||
|
titulo: 'Deep Learning with Python',
|
||||||
|
autor: 'François Chollet',
|
||||||
|
editorial: 'Manning Publications',
|
||||||
|
anio: 2021,
|
||||||
|
isbn: '978-1617296864',
|
||||||
|
tipo: 'libro',
|
||||||
|
disponible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lib-5',
|
||||||
|
titulo: 'Neural Networks and Deep Learning: A Textbook',
|
||||||
|
autor: 'Charu C. Aggarwal',
|
||||||
|
editorial: 'Springer',
|
||||||
|
anio: 2023,
|
||||||
|
isbn: '978-3031296413',
|
||||||
|
tipo: 'libro',
|
||||||
|
disponible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lib-6',
|
||||||
|
titulo: 'Machine Learning: A Probabilistic Perspective',
|
||||||
|
autor: 'Kevin Murphy',
|
||||||
|
editorial: 'MIT Press',
|
||||||
|
anio: 2012,
|
||||||
|
isbn: '978-0262018029',
|
||||||
|
tipo: 'libro',
|
||||||
|
disponible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,37 +1,31 @@
|
|||||||
export const qk = {
|
export const qk = {
|
||||||
auth: ['auth'] as const,
|
auth: ["auth"] as const,
|
||||||
session: () => ['auth', 'session'] as const,
|
session: () => ["auth", "session"] as const,
|
||||||
meProfile: () => ['auth', 'meProfile'] as const,
|
meProfile: () => ["auth", "meProfile"] as const,
|
||||||
|
|
||||||
facultades: () => ['meta', 'facultades'] as const,
|
facultades: () => ["meta", "facultades"] as const,
|
||||||
carreras: (facultadId?: string | null) =>
|
carreras: (facultadId?: string | null) =>
|
||||||
['meta', 'carreras', { facultadId: facultadId ?? null }] as const,
|
["meta", "carreras", { facultadId: facultadId ?? null }] as const,
|
||||||
estructurasPlan: (nivel?: string | null) =>
|
estructurasPlan: (nivel?: string | null) =>
|
||||||
['meta', 'estructurasPlan', { nivel: nivel ?? null }] as const,
|
["meta", "estructurasPlan", { nivel: nivel ?? null }] as const,
|
||||||
estructurasAsignatura: () => ['meta', 'estructurasAsignatura'] as const,
|
estructurasAsignatura: () => ["meta", "estructurasAsignatura"] as const,
|
||||||
estadosPlan: () => ['meta', 'estadosPlan'] as const,
|
estadosPlan: () => ["meta", "estadosPlan"] as const,
|
||||||
|
|
||||||
planesList: (filters: unknown) => ['planes', 'list', filters] as const,
|
planesList: (filters: unknown) => ["planes", "list", filters] as const,
|
||||||
plan: (planId: string) => ['planes', 'detail', planId] as const,
|
plan: (planId: string) => ["planes", "detail", planId] as const,
|
||||||
planMaybe: (planId: string) => ['planes', 'detail-maybe', planId] as const,
|
planLineas: (planId: string) => ["planes", planId, "lineas"] as const,
|
||||||
planLineas: (planId: string) => ['planes', planId, 'lineas'] as const,
|
planAsignaturas: (planId: string) => ["planes", planId, "asignaturas"] as const,
|
||||||
planAsignaturas: (planId: string) =>
|
planHistorial: (planId: string) => ["planes", planId, "historial"] as const,
|
||||||
['planes', planId, 'asignaturas'] as const,
|
planDocumento: (planId: string) => ["planes", planId, "documento"] as const,
|
||||||
planHistorial: (planId: string) => ['planes', planId, 'historial'] as const,
|
|
||||||
planDocumento: (planId: string) => ['planes', planId, 'documento'] as const,
|
|
||||||
|
|
||||||
sugerenciasAsignaturas: () => ['asignaturas', 'sugerencias'] as const,
|
asignatura: (asignaturaId: string) => ["asignaturas", "detail", asignaturaId] as const,
|
||||||
asignatura: (asignaturaId: string) =>
|
|
||||||
['asignaturas', 'detail', asignaturaId] as const,
|
|
||||||
asignaturaMaybe: (asignaturaId: string) =>
|
|
||||||
['asignaturas', 'detail-maybe', asignaturaId] as const,
|
|
||||||
asignaturaBibliografia: (asignaturaId: string) =>
|
asignaturaBibliografia: (asignaturaId: string) =>
|
||||||
['asignaturas', asignaturaId, 'bibliografia'] as const,
|
["asignaturas", asignaturaId, "bibliografia"] as const,
|
||||||
asignaturaHistorial: (asignaturaId: string) =>
|
asignaturaHistorial: (asignaturaId: string) =>
|
||||||
['asignaturas', asignaturaId, 'historial'] as const,
|
["asignaturas", asignaturaId, "historial"] as const,
|
||||||
asignaturaDocumento: (asignaturaId: string) =>
|
asignaturaDocumento: (asignaturaId: string) =>
|
||||||
['asignaturas', asignaturaId, 'documento'] as const,
|
["asignaturas", asignaturaId, "documento"] as const,
|
||||||
|
|
||||||
tareas: () => ['tareas', 'mias'] as const,
|
tareas: () => ["tareas", "mias"] as const,
|
||||||
notificaciones: () => ['notificaciones', 'mias'] as const,
|
notificaciones: () => ["notificaciones", "mias"] as const,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createClient } from "@supabase/supabase-js";
|
|||||||
import { getEnv } from "./env";
|
import { getEnv } from "./env";
|
||||||
|
|
||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
import type { Database } from "src/types/supabase";
|
import type { Database } from "src/types/supabase.js";
|
||||||
|
|
||||||
let _client: SupabaseClient<Database> | null = null;
|
let _client: SupabaseClient<Database> | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,179 +1,47 @@
|
|||||||
import {
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
FunctionsFetchError,
|
import type { Database } from "../types/database";
|
||||||
FunctionsHttpError,
|
import { supabaseBrowser } from "./client";
|
||||||
FunctionsRelayError,
|
|
||||||
} from '@supabase/supabase-js'
|
|
||||||
|
|
||||||
import { supabaseBrowser } from './client'
|
|
||||||
|
|
||||||
import type { Database } from '@/types/supabase'
|
|
||||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
||||||
|
|
||||||
export type EdgeInvokeOptions = {
|
export type EdgeInvokeOptions = {
|
||||||
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>;
|
||||||
responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer'
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export class EdgeFunctionError extends Error {
|
export class EdgeFunctionError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
public readonly functionName: string,
|
public readonly functionName: string,
|
||||||
public readonly status?: number,
|
public readonly status?: number,
|
||||||
public readonly details?: unknown,
|
public readonly details?: unknown
|
||||||
) {
|
) {
|
||||||
super(message)
|
super(message);
|
||||||
this.name = 'EdgeFunctionError'
|
this.name = "EdgeFunctionError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soporta base64 puro o data:...;base64,...
|
|
||||||
function decodeBase64ToUint8Array(input: string): Uint8Array {
|
|
||||||
const trimmed = input.trim()
|
|
||||||
const base64 = trimmed.startsWith('data:')
|
|
||||||
? trimmed.slice(trimmed.indexOf(',') + 1)
|
|
||||||
: trimmed
|
|
||||||
|
|
||||||
const bin = atob(base64)
|
|
||||||
const bytes = new Uint8Array(bin.length)
|
|
||||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripDataUrlPrefix(input: string): string {
|
|
||||||
const trimmed = input.trim()
|
|
||||||
if (!trimmed.startsWith('data:')) return trimmed
|
|
||||||
const commaIdx = trimmed.indexOf(',')
|
|
||||||
return commaIdx >= 0 ? trimmed.slice(commaIdx + 1) : trimmed
|
|
||||||
}
|
|
||||||
|
|
||||||
function looksLikeBase64(s: string): boolean {
|
|
||||||
const t = stripDataUrlPrefix(s).replace(/\s+/g, '').replace(/=+$/g, '')
|
|
||||||
|
|
||||||
// base64 típico: solo chars permitidos y longitud razonable
|
|
||||||
if (t.length < 64) return false
|
|
||||||
return /^[A-Za-z0-9+/]+$/.test(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
function startsWithZip(bytes: Uint8Array): boolean {
|
|
||||||
return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b // "PK"
|
|
||||||
}
|
|
||||||
|
|
||||||
function startsWithPdf(bytes: Uint8Array): boolean {
|
|
||||||
return (
|
|
||||||
bytes.length >= 5 &&
|
|
||||||
bytes[0] === 0x25 &&
|
|
||||||
bytes[1] === 0x50 &&
|
|
||||||
bytes[2] === 0x44 &&
|
|
||||||
bytes[3] === 0x46 &&
|
|
||||||
bytes[4] === 0x2d
|
|
||||||
) // "%PDF-"
|
|
||||||
}
|
|
||||||
|
|
||||||
function binaryStringToUint8Array(input: string): Uint8Array {
|
|
||||||
const bytes = new Uint8Array(input.length)
|
|
||||||
for (let i = 0; i < input.length; i++) bytes[i] = input.charCodeAt(i) & 0xff
|
|
||||||
return bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function invokeEdge<TOut>(
|
export async function invokeEdge<TOut>(
|
||||||
functionName: string,
|
functionName: string,
|
||||||
body?:
|
body?: unknown,
|
||||||
| string
|
|
||||||
| File
|
|
||||||
| Blob
|
|
||||||
| ArrayBuffer
|
|
||||||
| FormData
|
|
||||||
| ReadableStream<Uint8Array<ArrayBufferLike>>
|
|
||||||
| Record<string, unknown>
|
|
||||||
| undefined,
|
|
||||||
opts: EdgeInvokeOptions = {},
|
opts: EdgeInvokeOptions = {},
|
||||||
client?: SupabaseClient<Database>,
|
client?: SupabaseClient<Database>
|
||||||
): Promise<TOut> {
|
): Promise<TOut> {
|
||||||
const supabase = client ?? supabaseBrowser()
|
const supabase = client ?? supabaseBrowser();
|
||||||
|
|
||||||
// Nota: algunas versiones/defs de @supabase/supabase-js no tipan `responseType`
|
const { data, error } = await supabase.functions.invoke(functionName, {
|
||||||
// aunque el runtime lo soporte. Usamos `any` para no bloquear el uso de Blob.
|
|
||||||
const invoke: any = (supabase.functions as any).invoke.bind(
|
|
||||||
supabase.functions,
|
|
||||||
)
|
|
||||||
const { data, error } = await invoke(functionName, {
|
|
||||||
body,
|
body,
|
||||||
method: opts.method ?? 'POST',
|
method: opts.method ?? "POST",
|
||||||
headers: opts.headers,
|
headers: opts.headers,
|
||||||
responseType: opts.responseType,
|
});
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
// Valores por defecto (por si falla el parseo o es otro tipo de error)
|
const anyErr = error as any;
|
||||||
let message = error.message // El genérico "returned a non-2xx status code"
|
|
||||||
let status = undefined
|
|
||||||
let details: unknown = error
|
|
||||||
|
|
||||||
// 2. Verificamos si es un error HTTP (4xx o 5xx) que trae cuerpo JSON
|
|
||||||
if (error instanceof FunctionsHttpError) {
|
|
||||||
try {
|
|
||||||
// Obtenemos el status real (ej. 404, 400)
|
|
||||||
status = error.context.status
|
|
||||||
|
|
||||||
// ¡LA CLAVE! Leemos el JSON que tu Edge Function envió
|
|
||||||
const errorBody = await error.context.json()
|
|
||||||
details = errorBody
|
|
||||||
|
|
||||||
// Intentamos extraer el mensaje humano según tu estructura { error: { message: "..." } }
|
|
||||||
// o la estructura simple { error: "..." }
|
|
||||||
if (errorBody && typeof errorBody === 'object') {
|
|
||||||
// Caso 1: Estructura anidada (la que definimos hace poco: { error: { message: "..." } })
|
|
||||||
if (
|
|
||||||
'error' in errorBody &&
|
|
||||||
typeof errorBody.error === 'object' &&
|
|
||||||
errorBody.error !== null &&
|
|
||||||
'message' in errorBody.error
|
|
||||||
) {
|
|
||||||
message = (errorBody.error as { message: string }).message
|
|
||||||
}
|
|
||||||
// Caso 2: Estructura simple ({ error: "Mensaje de error" })
|
|
||||||
else if (
|
|
||||||
'error' in errorBody &&
|
|
||||||
typeof errorBody.error === 'string'
|
|
||||||
) {
|
|
||||||
message = errorBody.error
|
|
||||||
}
|
|
||||||
// Caso 3: Propiedad message directa ({ message: "..." })
|
|
||||||
else if (
|
|
||||||
'message' in errorBody &&
|
|
||||||
typeof errorBody.message === 'string'
|
|
||||||
) {
|
|
||||||
message = errorBody.message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('No se pudo parsear el error JSON de la Edge Function', e)
|
|
||||||
}
|
|
||||||
} else if (error instanceof FunctionsRelayError) {
|
|
||||||
message = `Error de Relay Supabase: ${error.message}`
|
|
||||||
} else if (error instanceof FunctionsFetchError) {
|
|
||||||
message = `Error de conexión (Fetch): ${error.message}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Lanzamos tu error personalizado con los datos reales extraídos
|
|
||||||
throw new EdgeFunctionError(message, functionName, status, details)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts.responseType === 'blob') {
|
|
||||||
const anyData: unknown = data
|
|
||||||
|
|
||||||
if (anyData instanceof Blob) {
|
|
||||||
return anyData as TOut
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new EdgeFunctionError(
|
throw new EdgeFunctionError(
|
||||||
'La Edge Function no devolvió un binario (Blob) válido.',
|
anyErr.message ?? "Error en Edge Function",
|
||||||
functionName,
|
functionName,
|
||||||
undefined,
|
anyErr.status,
|
||||||
{ receivedType: typeof anyData, received: anyData },
|
anyErr
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data as TOut
|
return data as TOut;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,17 +51,6 @@ export type PlanDatosSep = {
|
|||||||
propuesta_de_evaluacion_periodica_del_plan_de_estudios?: string | null;
|
propuesta_de_evaluacion_periodica_del_plan_de_estudios?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PlanEstudioWithRel =
|
|
||||||
& Tables<"planes_estudio">
|
|
||||||
& {
|
|
||||||
carreras:
|
|
||||||
| Tables<"carreras"> & {
|
|
||||||
facultades: Tables<"facultades"> | null;
|
|
||||||
}
|
|
||||||
| null;
|
|
||||||
estados_plan: Tables<"estados_plan"> | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Paged<T> = { data: Array<T>; count: number | null };
|
export type Paged<T> = { data: Array<T>; count: number | null };
|
||||||
|
|
||||||
export type FacultadRow = Tables<"facultades">;
|
export type FacultadRow = Tables<"facultades">;
|
||||||
|
|||||||
127
src/features/asignaturas/new/NuevaAsignaturaModalContainer.tsx
Normal file
127
src/features/asignaturas/new/NuevaAsignaturaModalContainer.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
|
||||||
|
|
||||||
|
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm'
|
||||||
|
import { PasoConfiguracionPanel } from '@/components/asignaturas/wizard/PasoConfiguracionPanel'
|
||||||
|
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
||||||
|
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
||||||
|
import { VistaSinPermisos } from '@/components/asignaturas/wizard/VistaSinPermisos'
|
||||||
|
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
|
||||||
|
import { WizardHeader } from '@/components/asignaturas/wizard/WizardHeader'
|
||||||
|
import { defineStepper } from '@/components/stepper'
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
const Wizard = defineStepper(
|
||||||
|
{
|
||||||
|
id: 'metodo',
|
||||||
|
title: 'Método',
|
||||||
|
description: 'Manual, IA o Clonado',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'basicos',
|
||||||
|
title: 'Datos básicos',
|
||||||
|
description: 'Nombre y estructura',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'configuracion',
|
||||||
|
title: 'Configuración',
|
||||||
|
description: 'Detalles según modo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'resumen',
|
||||||
|
title: 'Resumen',
|
||||||
|
description: 'Confirmar creación',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
|
||||||
|
|
||||||
|
export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const role = auth_get_current_user_role()
|
||||||
|
|
||||||
|
const {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
canContinueDesdeMetodo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeConfig,
|
||||||
|
simularGeneracionIA,
|
||||||
|
crearAsignatura,
|
||||||
|
} = useNuevaAsignaturaWizard(planId)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{role !== 'JEFE_CARRERA' ? (
|
||||||
|
<VistaSinPermisos onClose={handleClose} />
|
||||||
|
) : (
|
||||||
|
<Wizard.Stepper.Provider
|
||||||
|
initialStep={Wizard.utils.getFirst().id}
|
||||||
|
className="flex h-full flex-col"
|
||||||
|
>
|
||||||
|
{({ methods }) => (
|
||||||
|
<>
|
||||||
|
<WizardHeader
|
||||||
|
title="Nueva Asignatura"
|
||||||
|
Wizard={Wizard}
|
||||||
|
methods={{ ...methods, onClose: handleClose }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 0 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoMetodoCardGroup
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 1 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 2 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoConfiguracionPanel
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
onGenerarIA={simularGeneracionIA}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 3 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoResumenCard wizard={wizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WizardControls
|
||||||
|
Wizard={Wizard}
|
||||||
|
methods={methods}
|
||||||
|
wizard={wizard}
|
||||||
|
canContinueDesdeMetodo={canContinueDesdeMetodo}
|
||||||
|
canContinueDesdeBasicos={canContinueDesdeBasicos}
|
||||||
|
canContinueDesdeConfig={canContinueDesdeConfig}
|
||||||
|
onCreate={() => crearAsignatura(handleClose)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Wizard.Stepper.Provider>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import type { AsignaturaPreview, NewSubjectWizardState } from "../types";
|
||||||
|
|
||||||
|
export function useNuevaAsignaturaWizard(planId: string) {
|
||||||
|
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
||||||
|
step: 1,
|
||||||
|
planId,
|
||||||
|
modoCreacion: null,
|
||||||
|
datosBasicos: {
|
||||||
|
nombre: "",
|
||||||
|
clave: "",
|
||||||
|
tipo: "OBLIGATORIA",
|
||||||
|
creditos: 0,
|
||||||
|
horasSemana: 0,
|
||||||
|
estructuraId: "",
|
||||||
|
},
|
||||||
|
clonInterno: {},
|
||||||
|
clonTradicional: {
|
||||||
|
archivoWordAsignaturaId: null,
|
||||||
|
archivosAdicionalesIds: [],
|
||||||
|
},
|
||||||
|
iaConfig: {
|
||||||
|
descripcionEnfoque: "",
|
||||||
|
notasAdicionales: "",
|
||||||
|
archivosExistentesIds: [],
|
||||||
|
},
|
||||||
|
resumen: {},
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canContinueDesdeMetodo = wizard.modoCreacion === "MANUAL" ||
|
||||||
|
wizard.modoCreacion === "IA" ||
|
||||||
|
(wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
|
||||||
|
|
||||||
|
const canContinueDesdeBasicos = !!wizard.datosBasicos.nombre &&
|
||||||
|
wizard.datosBasicos.creditos > 0 &&
|
||||||
|
!!wizard.datosBasicos.estructuraId;
|
||||||
|
|
||||||
|
const canContinueDesdeConfig = (() => {
|
||||||
|
if (wizard.modoCreacion === "MANUAL") return true;
|
||||||
|
if (wizard.modoCreacion === "IA") {
|
||||||
|
return !!wizard.iaConfig?.descripcionEnfoque;
|
||||||
|
}
|
||||||
|
if (wizard.modoCreacion === "CLONADO") {
|
||||||
|
if (wizard.subModoClonado === "INTERNO") {
|
||||||
|
return !!wizard.clonInterno?.asignaturaOrigenId;
|
||||||
|
}
|
||||||
|
if (wizard.subModoClonado === "TRADICIONAL") {
|
||||||
|
return !!wizard.clonTradicional?.archivoWordAsignaturaId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const simularGeneracionIA = async () => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true }));
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
resumen: {
|
||||||
|
previewAsignatura: {
|
||||||
|
nombre: w.datosBasicos.nombre,
|
||||||
|
objetivo:
|
||||||
|
"Aplicar los fundamentos teóricos para la resolución de problemas...",
|
||||||
|
unidades: 5,
|
||||||
|
bibliografiaCount: 3,
|
||||||
|
} as AsignaturaPreview,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const crearAsignatura = async (onCreated: () => void) => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true }));
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
onCreated();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
canContinueDesdeMetodo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeConfig,
|
||||||
|
simularGeneracionIA,
|
||||||
|
crearAsignatura,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
src/features/asignaturas/new/types.ts
Normal file
45
src/features/asignaturas/new/types.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
|
||||||
|
export type SubModoClonado = "INTERNO" | "TRADICIONAL";
|
||||||
|
export type TipoAsignatura = "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRO";
|
||||||
|
|
||||||
|
export type AsignaturaPreview = {
|
||||||
|
nombre: string;
|
||||||
|
objetivo: string;
|
||||||
|
unidades: number;
|
||||||
|
bibliografiaCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewSubjectWizardState = {
|
||||||
|
step: 1 | 2 | 3 | 4;
|
||||||
|
planId: string;
|
||||||
|
modoCreacion: ModoCreacion | null;
|
||||||
|
subModoClonado?: SubModoClonado;
|
||||||
|
datosBasicos: {
|
||||||
|
nombre: string;
|
||||||
|
clave?: string;
|
||||||
|
tipo: TipoAsignatura;
|
||||||
|
creditos: number;
|
||||||
|
horasSemana?: number;
|
||||||
|
estructuraId: string;
|
||||||
|
};
|
||||||
|
clonInterno?: {
|
||||||
|
facultadId?: string;
|
||||||
|
carreraId?: string;
|
||||||
|
planOrigenId?: string;
|
||||||
|
asignaturaOrigenId?: string | null;
|
||||||
|
};
|
||||||
|
clonTradicional?: {
|
||||||
|
archivoWordAsignaturaId: string | null;
|
||||||
|
archivosAdicionalesIds: Array<string>;
|
||||||
|
};
|
||||||
|
iaConfig?: {
|
||||||
|
descripcionEnfoque: string;
|
||||||
|
notasAdicionales: string;
|
||||||
|
archivosExistentesIds: Array<string>;
|
||||||
|
};
|
||||||
|
resumen: {
|
||||||
|
previewAsignatura?: AsignaturaPreview;
|
||||||
|
};
|
||||||
|
isLoading: boolean;
|
||||||
|
errorMessage: string | null;
|
||||||
|
};
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react'
|
|
||||||
|
|
||||||
// --- DEFINICIÓN DE MENSAJES ---
|
|
||||||
const MENSAJES_CORTOS = [
|
|
||||||
// Hasta 5 sugerencias (6 mensajes)
|
|
||||||
'Analizando el plan de estudios...',
|
|
||||||
'Identificando áreas de oportunidad...',
|
|
||||||
'Consultando bases de datos académicas...',
|
|
||||||
'Redactando competencias específicas...',
|
|
||||||
'Calculando créditos y horas...',
|
|
||||||
'Afinando los últimos detalles...',
|
|
||||||
]
|
|
||||||
|
|
||||||
const MENSAJES_MEDIOS = [
|
|
||||||
// Hasta 10 sugerencias (10 mensajes)
|
|
||||||
'Conectando con el motor de IA...',
|
|
||||||
'Analizando estructura curricular...',
|
|
||||||
'Buscando asignaturas compatibles...',
|
|
||||||
'Verificando prerrequisitos...',
|
|
||||||
'Generando descripciones detalladas...',
|
|
||||||
'Balanceando cargas académicas...',
|
|
||||||
'Asignando horas independientes...',
|
|
||||||
'Validando coherencia temática...',
|
|
||||||
'Formateando resultados...',
|
|
||||||
'Finalizando generación...',
|
|
||||||
]
|
|
||||||
|
|
||||||
const MENSAJES_LARGOS = [
|
|
||||||
// Más de 10 sugerencias (14 mensajes)
|
|
||||||
'Iniciando procesamiento masivo...',
|
|
||||||
'Escaneando retícula completa...',
|
|
||||||
'Detectando líneas de investigación...',
|
|
||||||
'Generando primer bloque de asignaturas...',
|
|
||||||
'Evaluando pertinencia académica...',
|
|
||||||
'Optimizando créditos por ciclo...',
|
|
||||||
'Redactando objetivos de aprendizaje...',
|
|
||||||
'Generando segundo bloque...',
|
|
||||||
'Revisando duplicidad de contenidos...',
|
|
||||||
'Ajustando tiempos teóricos y prácticos...',
|
|
||||||
'Verificando normatividad...',
|
|
||||||
'Compilando sugerencias...',
|
|
||||||
'Aplicando formato final...',
|
|
||||||
'Casi listo, gracias por tu paciencia...',
|
|
||||||
]
|
|
||||||
|
|
||||||
interface AIProgressLoaderProps {
|
|
||||||
isLoading: boolean
|
|
||||||
cantidadDeSugerencias: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AIProgressLoader: React.FC<AIProgressLoaderProps> = ({
|
|
||||||
isLoading,
|
|
||||||
cantidadDeSugerencias,
|
|
||||||
}) => {
|
|
||||||
const [progress, setProgress] = useState(0)
|
|
||||||
const [currentMessageIndex, setCurrentMessageIndex] = useState(0)
|
|
||||||
|
|
||||||
// 1. Seleccionar el grupo de mensajes según la cantidad
|
|
||||||
const messages = useMemo(() => {
|
|
||||||
if (cantidadDeSugerencias <= 5) return MENSAJES_CORTOS
|
|
||||||
if (cantidadDeSugerencias <= 10) return MENSAJES_MEDIOS
|
|
||||||
return MENSAJES_LARGOS
|
|
||||||
}, [cantidadDeSugerencias])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isLoading) {
|
|
||||||
setProgress(0)
|
|
||||||
setCurrentMessageIndex(0)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- CÁLCULO DEL TIEMPO TOTAL ---
|
|
||||||
// y = 4.07x + 10.93 (en segundos)
|
|
||||||
const estimatedSeconds = 4.07 * cantidadDeSugerencias + 10.93
|
|
||||||
const durationMs = estimatedSeconds * 1000
|
|
||||||
|
|
||||||
// Intervalo de actualización de la barra (cada 50ms para suavidad)
|
|
||||||
const updateInterval = 50
|
|
||||||
const totalSteps = durationMs / updateInterval
|
|
||||||
const incrementPerStep = 99 / totalSteps // Llegamos al 99% para esperar la respuesta real
|
|
||||||
|
|
||||||
// --- TIMER 1: BARRA DE PROGRESO ---
|
|
||||||
const progressTimer = setInterval(() => {
|
|
||||||
setProgress((prev) => {
|
|
||||||
const next = prev + incrementPerStep
|
|
||||||
return next >= 99 ? 99 : next // Topar en 99%
|
|
||||||
})
|
|
||||||
}, updateInterval)
|
|
||||||
|
|
||||||
// --- TIMER 2: MENSAJES (CADA 5 SEGUNDOS) ---
|
|
||||||
const messagesTimer = setInterval(() => {
|
|
||||||
setCurrentMessageIndex((prev) => {
|
|
||||||
// Si ya es el último mensaje, no avanzar más (no ciclar)
|
|
||||||
if (prev >= messages.length - 1) return prev
|
|
||||||
return prev + 1
|
|
||||||
})
|
|
||||||
}, 5000)
|
|
||||||
|
|
||||||
// Cleanup al desmontar o cuando isLoading cambie
|
|
||||||
return () => {
|
|
||||||
clearInterval(progressTimer)
|
|
||||||
clearInterval(messagesTimer)
|
|
||||||
}
|
|
||||||
}, [isLoading, cantidadDeSugerencias, messages])
|
|
||||||
|
|
||||||
if (!isLoading) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="animate-in fade-in zoom-in m-2 mx-auto w-full max-w-md duration-300">
|
|
||||||
{/* Contenedor de la barra */}
|
|
||||||
<div className="relative pt-1">
|
|
||||||
<div className="mb-2 flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<span className="inline-block rounded-full bg-blue-200 px-2 py-1 text-xs font-semibold text-blue-600 uppercase">
|
|
||||||
Generando IA
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<span className="inline-block text-xs font-semibold text-blue-600">
|
|
||||||
{Math.floor(progress)}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Barra de fondo */}
|
|
||||||
<div className="mb-4 flex h-2 overflow-hidden rounded bg-blue-100 text-xs">
|
|
||||||
{/* Barra de progreso dinámica */}
|
|
||||||
<div
|
|
||||||
style={{ width: `${progress}%` }}
|
|
||||||
className="flex flex-col justify-center bg-blue-500 text-center whitespace-nowrap text-white shadow-none transition-all duration-75 ease-linear"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mensajes cambiantes */}
|
|
||||||
<div className="h-6 text-center">
|
|
||||||
{' '}
|
|
||||||
{/* Altura fija para evitar saltos */}
|
|
||||||
<p className="text-sm text-slate-500 italic transition-opacity duration-500">
|
|
||||||
{messages[currentMessageIndex]}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nota de tiempo estimado (Opcional, transparencia operacional) */}
|
|
||||||
<p className="mt-2 text-center text-[10px] text-slate-400">
|
|
||||||
Tiempo estimado: ~{Math.ceil(4.07 * cantidadDeSugerencias + 10.93)}{' '}
|
|
||||||
segs
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router'
|
|
||||||
import * as Icons from 'lucide-react'
|
|
||||||
|
|
||||||
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
|
|
||||||
|
|
||||||
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm'
|
|
||||||
import { PasoDetallesPanel } from '@/components/asignaturas/wizard/PasoDetallesPanel'
|
|
||||||
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
|
||||||
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
|
||||||
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
|
|
||||||
import { defineStepper } from '@/components/stepper'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from '@/components/ui/card'
|
|
||||||
import { WizardLayout } from '@/components/wizard/WizardLayout'
|
|
||||||
import { WizardResponsiveHeader } from '@/components/wizard/WizardResponsiveHeader'
|
|
||||||
|
|
||||||
const auth_get_current_user_role = (): string => 'JEFE_CARRERA'
|
|
||||||
|
|
||||||
const Wizard = defineStepper(
|
|
||||||
{
|
|
||||||
id: 'metodo',
|
|
||||||
title: 'Método',
|
|
||||||
description: 'Manual, IA o Clonado',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'basicos',
|
|
||||||
title: 'Datos básicos',
|
|
||||||
description: 'Nombre y estructura',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'detalles',
|
|
||||||
title: 'Detalles',
|
|
||||||
description: 'Detalles según modo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'resumen',
|
|
||||||
title: 'Resumen',
|
|
||||||
description: 'Confirmar creación',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const role = auth_get_current_user_role()
|
|
||||||
|
|
||||||
const {
|
|
||||||
wizard,
|
|
||||||
setWizard,
|
|
||||||
canContinueDesdeMetodo,
|
|
||||||
canContinueDesdeBasicos,
|
|
||||||
canContinueDesdeDetalles,
|
|
||||||
} = useNuevaAsignaturaWizard(planId)
|
|
||||||
|
|
||||||
const titleOverrides =
|
|
||||||
wizard.tipoOrigen === 'IA_MULTIPLE'
|
|
||||||
? {
|
|
||||||
basicos: 'Sugerencias',
|
|
||||||
detalles: 'Estructura',
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (role !== 'JEFE_CARRERA') {
|
|
||||||
return (
|
|
||||||
<WizardLayout title="Nueva Asignatura" onClose={handleClose}>
|
|
||||||
<Card className="border-destructive/40">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-destructive flex items-center gap-2">
|
|
||||||
<Icons.ShieldAlert className="h-5 w-5" />
|
|
||||||
Sin permisos
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Solo el Jefe de Carrera puede crear asignaturas.
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="flex justify-end">
|
|
||||||
<Button variant="secondary" onClick={handleClose}>
|
|
||||||
Volver
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</WizardLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wizard.Stepper.Provider
|
|
||||||
initialStep={Wizard.utils.getFirst().id}
|
|
||||||
className="flex h-full flex-col"
|
|
||||||
>
|
|
||||||
{({ methods }) => {
|
|
||||||
const idx = Wizard.utils.getIndex(methods.current.id)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WizardLayout
|
|
||||||
title="Nueva Asignatura"
|
|
||||||
onClose={handleClose}
|
|
||||||
headerSlot={
|
|
||||||
<WizardResponsiveHeader
|
|
||||||
wizard={Wizard}
|
|
||||||
methods={methods}
|
|
||||||
titleOverrides={titleOverrides}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
footerSlot={
|
|
||||||
<Wizard.Stepper.Controls>
|
|
||||||
<WizardControls
|
|
||||||
errorMessage={wizard.errorMessage}
|
|
||||||
onPrev={() => methods.prev()}
|
|
||||||
onNext={() => methods.next()}
|
|
||||||
disablePrev={idx === 0 || wizard.isLoading}
|
|
||||||
disableNext={
|
|
||||||
wizard.isLoading ||
|
|
||||||
(idx === 0 && !canContinueDesdeMetodo) ||
|
|
||||||
(idx === 1 && !canContinueDesdeBasicos) ||
|
|
||||||
(idx === 2 && !canContinueDesdeDetalles)
|
|
||||||
}
|
|
||||||
disableCreate={wizard.isLoading}
|
|
||||||
isLastStep={idx >= Wizard.steps.length - 1}
|
|
||||||
wizard={wizard}
|
|
||||||
setWizard={setWizard}
|
|
||||||
/>
|
|
||||||
</Wizard.Stepper.Controls>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="mx-auto max-w-3xl">
|
|
||||||
{idx === 0 && (
|
|
||||||
<Wizard.Stepper.Panel>
|
|
||||||
<PasoMetodoCardGroup wizard={wizard} onChange={setWizard} />
|
|
||||||
</Wizard.Stepper.Panel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{idx === 1 && (
|
|
||||||
<Wizard.Stepper.Panel>
|
|
||||||
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
|
|
||||||
</Wizard.Stepper.Panel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{idx === 2 && (
|
|
||||||
<Wizard.Stepper.Panel>
|
|
||||||
<PasoDetallesPanel wizard={wizard} onChange={setWizard} />
|
|
||||||
</Wizard.Stepper.Panel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{idx === 3 && (
|
|
||||||
<Wizard.Stepper.Panel>
|
|
||||||
<PasoResumenCard wizard={wizard} />
|
|
||||||
</Wizard.Stepper.Panel>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</WizardLayout>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Wizard.Stepper.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user