Merge pull request 'Generación de múltiples asignaturas con sugerencias' (#105) from issue/89-nueva-opcin-en-wizard-crear-asignaturas-con-sugere into main
Reviewed-on: #105
This commit was merged in pull request #105.
This commit is contained in:
52
bun.lock
52
bun.lock
@@ -36,12 +36,14 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.24.7",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.0.6",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"use-debounce": "^10.1.0",
|
||||
"vaul": "^1.1.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tanstack/devtools-vite": "^0.3.11",
|
||||
@@ -251,10 +253,16 @@
|
||||
|
||||
"@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-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-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=="],
|
||||
@@ -281,12 +289,24 @@
|
||||
|
||||
"@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-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-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-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=="],
|
||||
@@ -297,6 +317,10 @@
|
||||
|
||||
"@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-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=="],
|
||||
@@ -305,10 +329,22 @@
|
||||
|
||||
"@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-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-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-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=="],
|
||||
@@ -1161,6 +1197,8 @@
|
||||
|
||||
"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-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||
@@ -1351,6 +1389,8 @@
|
||||
|
||||
"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.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=="],
|
||||
@@ -1419,6 +1459,8 @@
|
||||
|
||||
"@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-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=="],
|
||||
@@ -1431,6 +1473,8 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -1487,6 +1531,14 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"motion": "^12.24.7",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
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])
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
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} />
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
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 ?? 10
|
||||
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. 10"
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,12 @@ import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/
|
||||
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,
|
||||
@@ -20,6 +26,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data'
|
||||
import {
|
||||
FACULTADES,
|
||||
MATERIAS_MOCK,
|
||||
@@ -29,12 +36,14 @@ import {
|
||||
export function PasoDetallesPanel({
|
||||
wizard,
|
||||
onChange,
|
||||
onGenerarIA: _onGenerarIA,
|
||||
}: {
|
||||
wizard: NewSubjectWizardState
|
||||
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||
onGenerarIA: () => void
|
||||
}) {
|
||||
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>
|
||||
@@ -49,7 +58,7 @@ export function PasoDetallesPanel({
|
||||
)
|
||||
}
|
||||
|
||||
if (wizard.tipoOrigen === 'IA') {
|
||||
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
@@ -148,6 +157,171 @@ export function PasoDetallesPanel({
|
||||
)
|
||||
}
|
||||
|
||||
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">
|
||||
|
||||
@@ -77,12 +77,93 @@ export function PasoMetodoCardGroup({
|
||||
</CardTitle>
|
||||
<CardDescription>Generar contenido automático.</CardDescription>
|
||||
</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
|
||||
className={isSelected('OTRO') ? 'ring-ring ring-2' : ''}
|
||||
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||
onClick={() =>
|
||||
onChange((w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'OTRO' }))
|
||||
onChange(
|
||||
(w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'CLONADO' }),
|
||||
)
|
||||
}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -93,7 +174,7 @@ export function PasoMetodoCardGroup({
|
||||
</CardTitle>
|
||||
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
||||
</CardHeader>
|
||||
{(wizard.tipoOrigen === 'OTRO' ||
|
||||
{(wizard.tipoOrigen === 'CLONADO' ||
|
||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
|
||||
@@ -9,12 +9,13 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card'
|
||||
import { usePlan, useSubjectEstructuras } from '@/data'
|
||||
import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data'
|
||||
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
||||
|
||||
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
|
||||
@@ -26,6 +27,8 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||
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 '—'
|
||||
@@ -41,6 +44,10 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -72,7 +79,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||
{wizard.tipoOrigen === 'MANUAL' && (
|
||||
<Icons.Pencil className="h-4 w-4" />
|
||||
)}
|
||||
{wizard.tipoOrigen === 'IA' && (
|
||||
{(wizard.tipoOrigen === 'IA' ||
|
||||
wizard.tipoOrigen === 'IA_SIMPLE' ||
|
||||
wizard.tipoOrigen === 'IA_MULTIPLE') && (
|
||||
<Icons.Sparkles className="h-4 w-4" />
|
||||
)}
|
||||
{(wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||
@@ -83,112 +92,202 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||
</span>
|
||||
</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>
|
||||
{wizard.tipoOrigen === 'IA_MULTIPLE' ? (
|
||||
<>
|
||||
<div className="border-border/60 bg-muted/30 grid gap-3 rounded-xl border p-4">
|
||||
<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-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 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="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="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="text-muted-foreground text-xs">—</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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
|
||||
import type { AIGenerateSubjectInput } from '@/data'
|
||||
import type { AIGenerateSubjectInput, AIGenerateSubjectJsonInput } from '@/data'
|
||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||
import type { TablesInsert } from '@/types/supabase'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useGenerateSubjectAI } from '@/data'
|
||||
import { supabaseBrowser, useGenerateSubjectAI, qk } from '@/data'
|
||||
|
||||
export function WizardControls({
|
||||
wizard,
|
||||
@@ -28,6 +30,7 @@ export function WizardControls({
|
||||
isLastStep: boolean
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const qc = useQueryClient()
|
||||
const generateSubjectAI = useGenerateSubjectAI()
|
||||
const handleCreate = async () => {
|
||||
setWizard((w) => ({
|
||||
@@ -37,7 +40,7 @@ export function WizardControls({
|
||||
}))
|
||||
|
||||
try {
|
||||
if (wizard.tipoOrigen === 'IA') {
|
||||
if (wizard.tipoOrigen === 'IA' || wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||
const aiInput: AIGenerateSubjectInput = {
|
||||
plan_estudio_id: wizard.plan_estudio_id,
|
||||
datosBasicos: {
|
||||
@@ -77,6 +80,82 @@ export function WizardControls({
|
||||
})
|
||||
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()
|
||||
|
||||
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 payload: AIGenerateSubjectJsonInput = {
|
||||
id,
|
||||
descripcionEnfoqueAcademico: s.descripcion,
|
||||
// (opcionales) parches directos si el edge los usa
|
||||
estructura_id: wizard.estructuraId,
|
||||
linea_plan_id: s.linea_plan_id,
|
||||
numero_ciclo: s.numero_ciclo,
|
||||
}
|
||||
|
||||
void generateSubjectAI.mutateAsync(payload).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,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
} catch (err: any) {
|
||||
setWizard((w) => ({
|
||||
...w,
|
||||
@@ -94,7 +173,7 @@ export function WizardControls({
|
||||
Anterior
|
||||
</Button>
|
||||
|
||||
<div className="flex-1">
|
||||
<div className="mx-2 flex-1">
|
||||
{(errorMessage ?? wizard.errorMessage) && (
|
||||
<span className="text-destructive text-sm font-medium">
|
||||
{errorMessage ?? wizard.errorMessage}
|
||||
|
||||
64
src/components/ui/accordion.tsx
Normal file
64
src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
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 }
|
||||
@@ -4,9 +4,11 @@ import { StepWithTooltip } from '@/components/wizard/StepWithTooltip'
|
||||
export function WizardResponsiveHeader({
|
||||
wizard,
|
||||
methods,
|
||||
titleOverrides,
|
||||
}: {
|
||||
wizard: any
|
||||
methods: any
|
||||
titleOverrides?: Record<string, string>
|
||||
}) {
|
||||
const idx = wizard.utils.getIndex(methods.current.id)
|
||||
const totalSteps = wizard.steps.length
|
||||
@@ -14,6 +16,8 @@ export function WizardResponsiveHeader({
|
||||
const hasNextStep = idx < totalSteps - 1
|
||||
const nextStep = wizard.steps[currentIndex]
|
||||
|
||||
const resolveTitle = (step: any) => titleOverrides?.[step?.id] ?? step?.title
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="block sm:hidden">
|
||||
@@ -22,13 +26,13 @@ export function WizardResponsiveHeader({
|
||||
<div className="flex flex-col justify-center">
|
||||
<h2 className="text-lg font-bold text-slate-900">
|
||||
<StepWithTooltip
|
||||
title={methods.current.title}
|
||||
title={resolveTitle(methods.current)}
|
||||
desc={methods.current.description}
|
||||
/>
|
||||
</h2>
|
||||
{hasNextStep && nextStep ? (
|
||||
<p className="text-sm text-slate-400">
|
||||
Siguiente: {nextStep.title}
|
||||
Siguiente: {resolveTitle(nextStep)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm font-medium text-green-500">
|
||||
@@ -48,7 +52,10 @@ export function WizardResponsiveHeader({
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
<wizard.Stepper.Title>
|
||||
<StepWithTooltip title={step.title} desc={step.description} />
|
||||
<StepWithTooltip
|
||||
title={resolveTitle(step)}
|
||||
desc={step.description}
|
||||
/>
|
||||
</wizard.Stepper.Title>
|
||||
</wizard.Stepper.Step>
|
||||
))}
|
||||
|
||||
@@ -165,7 +165,7 @@ export async function plan_asignaturas_list(
|
||||
const { data, error } = await supabase
|
||||
.from('asignaturas')
|
||||
.select(
|
||||
'id,plan_estudio_id,horas_academicas, horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
|
||||
'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',
|
||||
)
|
||||
.eq('plan_estudio_id', planId)
|
||||
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
||||
@@ -189,7 +189,7 @@ export async function plans_history(
|
||||
const { data, error, count } = await supabase
|
||||
.from('cambios_plan')
|
||||
.select(
|
||||
'id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo',
|
||||
'id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,response_id',
|
||||
{ count: 'exact' }, // <--- Pedimos el conteo exacto
|
||||
)
|
||||
.eq('plan_estudio_id', planId)
|
||||
@@ -304,7 +304,7 @@ export async function ai_generate_plan(
|
||||
archivosAdjuntos: undefined, // los manejamos aparte
|
||||
}),
|
||||
)
|
||||
input.iaConfig.archivosAdjuntos.forEach((file, index) => {
|
||||
input.iaConfig.archivosAdjuntos.forEach((file) => {
|
||||
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
||||
})
|
||||
|
||||
|
||||
@@ -12,9 +12,14 @@ import type {
|
||||
UUID,
|
||||
} from '../types/domain'
|
||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||
import type {
|
||||
AsignaturaSugerida,
|
||||
DataAsignaturaSugerida,
|
||||
} from '@/features/asignaturas/nueva/types'
|
||||
import type { Database } from '@/types/supabase'
|
||||
|
||||
const EDGE = {
|
||||
generate_subject_suggestions: 'generate-subject-suggestions',
|
||||
subjects_create_manual: 'subjects_create_manual',
|
||||
ai_generate_subject: 'ai-generate-subject',
|
||||
subjects_persist_from_ai: 'subjects_persist_from_ai',
|
||||
@@ -36,7 +41,7 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
|
||||
.from('asignaturas')
|
||||
.select(
|
||||
`
|
||||
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||
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,
|
||||
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,
|
||||
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
||||
@@ -133,28 +138,88 @@ export type AIGenerateSubjectInput = {
|
||||
}
|
||||
}
|
||||
|
||||
export async function ai_generate_subject(
|
||||
input: AIGenerateSubjectInput,
|
||||
): Promise<any> {
|
||||
const edgeFunctionBody = new FormData()
|
||||
edgeFunctionBody.append('plan_estudio_id', input.plan_estudio_id)
|
||||
edgeFunctionBody.append('datosBasicos', JSON.stringify(input.datosBasicos))
|
||||
edgeFunctionBody.append(
|
||||
'iaConfig',
|
||||
JSON.stringify({
|
||||
...input.iaConfig,
|
||||
archivosAdjuntos: undefined, // los manejamos aparte
|
||||
/**
|
||||
* Edge (JSON): actualizar/llenar una asignatura existente por id.
|
||||
* Nota: este flujo NO acepta `instruccionesAdicionalesIA` (solo FormData lo usa).
|
||||
*/
|
||||
export type AIGenerateSubjectJsonInput = Partial<{
|
||||
plan_estudio_id: Asignatura['plan_estudio_id']
|
||||
nombre: Asignatura['nombre']
|
||||
codigo: Asignatura['codigo']
|
||||
tipo: Asignatura['tipo'] | null
|
||||
creditos: Asignatura['creditos']
|
||||
horas_academicas: Asignatura['horas_academicas'] | null
|
||||
horas_independientes: Asignatura['horas_independientes'] | null
|
||||
estructura_id: Asignatura['estructura_id'] | null
|
||||
linea_plan_id: Asignatura['linea_plan_id'] | null
|
||||
numero_ciclo: Asignatura['numero_ciclo'] | null
|
||||
descripcionEnfoqueAcademico: string
|
||||
}> & {
|
||||
id: Asignatura['id']
|
||||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
)
|
||||
input.iaConfig?.archivosAdjuntos?.forEach((file, index) => {
|
||||
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
||||
}
|
||||
|
||||
export async function ai_generate_subject(
|
||||
input: AIGenerateSubjectInput | AIGenerateSubjectJsonInput,
|
||||
): Promise<any> {
|
||||
if ('datosBasicos' in input) {
|
||||
const edgeFunctionBody = new FormData()
|
||||
edgeFunctionBody.append('plan_estudio_id', input.plan_estudio_id)
|
||||
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_subject,
|
||||
edgeFunctionBody,
|
||||
undefined,
|
||||
supabaseBrowser(),
|
||||
)
|
||||
}
|
||||
|
||||
return invokeEdge<any>(EDGE.ai_generate_subject, input, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
return invokeEdge<any>(
|
||||
EDGE.ai_generate_subject,
|
||||
edgeFunctionBody,
|
||||
undefined,
|
||||
supabaseBrowser(),
|
||||
)
|
||||
}
|
||||
|
||||
export async function subjects_persist_from_ai(payload: {
|
||||
|
||||
@@ -19,6 +19,7 @@ export const qk = {
|
||||
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,
|
||||
asignaturaBibliografia: (asignaturaId: string) =>
|
||||
|
||||
151
src/features/asignaturas/nueva/AIProgressLoader.tsx
Normal file
151
src/features/asignaturas/nueva/AIProgressLoader.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import * as Icons from 'lucide-react'
|
||||
|
||||
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
|
||||
|
||||
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm'
|
||||
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'
|
||||
@@ -55,10 +55,16 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
||||
canContinueDesdeMetodo,
|
||||
canContinueDesdeBasicos,
|
||||
canContinueDesdeDetalles,
|
||||
simularGeneracionIA,
|
||||
crearAsignatura,
|
||||
} = useNuevaAsignaturaWizard(planId)
|
||||
|
||||
const titleOverrides =
|
||||
wizard.tipoOrigen === 'IA_MULTIPLE'
|
||||
? {
|
||||
basicos: 'Sugerencias',
|
||||
detalles: 'Estructura',
|
||||
}
|
||||
: undefined
|
||||
|
||||
const handleClose = () => {
|
||||
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
||||
}
|
||||
@@ -99,7 +105,11 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
||||
title="Nueva Asignatura"
|
||||
onClose={handleClose}
|
||||
headerSlot={
|
||||
<WizardResponsiveHeader wizard={Wizard} methods={methods} />
|
||||
<WizardResponsiveHeader
|
||||
wizard={Wizard}
|
||||
methods={methods}
|
||||
titleOverrides={titleOverrides}
|
||||
/>
|
||||
}
|
||||
footerSlot={
|
||||
<Wizard.Stepper.Controls>
|
||||
@@ -137,11 +147,7 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
||||
|
||||
{idx === 2 && (
|
||||
<Wizard.Stepper.Panel>
|
||||
<PasoDetallesPanel
|
||||
wizard={wizard}
|
||||
onChange={setWizard}
|
||||
onGenerarIA={simularGeneracionIA}
|
||||
/>
|
||||
<PasoDetallesPanel wizard={wizard} onChange={setWizard} />
|
||||
</Wizard.Stepper.Panel>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import type { AsignaturaPreview, NewSubjectWizardState } from '../types'
|
||||
import type { NewSubjectWizardState } from '../types'
|
||||
|
||||
export function useNuevaAsignaturaWizard(planId: string) {
|
||||
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
||||
step: 1,
|
||||
plan_estudio_id: planId,
|
||||
estructuraId: null,
|
||||
tipoOrigen: null,
|
||||
datosBasicos: {
|
||||
nombre: '',
|
||||
@@ -16,6 +17,7 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
||||
horasIndependientes: null,
|
||||
estructuraId: '',
|
||||
},
|
||||
sugerencias: [],
|
||||
clonInterno: {},
|
||||
clonTradicional: {
|
||||
archivoWordAsignaturaId: null,
|
||||
@@ -28,6 +30,11 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
||||
repositoriosReferencia: [],
|
||||
archivosAdjuntos: [],
|
||||
},
|
||||
iaMultiple: {
|
||||
enfoque: '',
|
||||
cantidadDeSugerencias: 10,
|
||||
isLoading: false,
|
||||
},
|
||||
resumen: {},
|
||||
isLoading: false,
|
||||
errorMessage: null,
|
||||
@@ -35,20 +42,23 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
||||
|
||||
const canContinueDesdeMetodo =
|
||||
wizard.tipoOrigen === 'MANUAL' ||
|
||||
wizard.tipoOrigen === 'IA' ||
|
||||
wizard.tipoOrigen === 'IA_SIMPLE' ||
|
||||
wizard.tipoOrigen === 'IA_MULTIPLE' ||
|
||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
|
||||
|
||||
const canContinueDesdeBasicos =
|
||||
!!wizard.datosBasicos.nombre &&
|
||||
wizard.datosBasicos.tipo !== null &&
|
||||
wizard.datosBasicos.creditos !== null &&
|
||||
wizard.datosBasicos.creditos > 0 &&
|
||||
!!wizard.datosBasicos.estructuraId
|
||||
(!!wizard.datosBasicos.nombre &&
|
||||
wizard.datosBasicos.tipo !== null &&
|
||||
wizard.datosBasicos.creditos !== null &&
|
||||
wizard.datosBasicos.creditos > 0 &&
|
||||
!!wizard.datosBasicos.estructuraId) ||
|
||||
(wizard.tipoOrigen === 'IA_MULTIPLE' &&
|
||||
wizard.sugerencias.filter((s) => s.selected).length > 0)
|
||||
|
||||
const canContinueDesdeDetalles = (() => {
|
||||
if (wizard.tipoOrigen === 'MANUAL') return true
|
||||
if (wizard.tipoOrigen === 'IA') {
|
||||
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||
return !!wizard.iaConfig?.descripcionEnfoqueAcademico
|
||||
}
|
||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||
@@ -57,38 +67,17 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
||||
return !!wizard.clonTradicional?.archivoWordAsignaturaId
|
||||
}
|
||||
if (wizard.tipoOrigen === 'IA_MULTIPLE') {
|
||||
return wizard.estructuraId !== null
|
||||
}
|
||||
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 () => {
|
||||
await new Promise((r) => setTimeout(r, 1000))
|
||||
}
|
||||
|
||||
return {
|
||||
wizard,
|
||||
setWizard,
|
||||
canContinueDesdeMetodo,
|
||||
canContinueDesdeBasicos,
|
||||
canContinueDesdeDetalles,
|
||||
simularGeneracionIA,
|
||||
crearAsignatura,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/
|
||||
import type { Asignatura } from '@/data'
|
||||
|
||||
export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO'
|
||||
export type SubModoClonado = 'INTERNO' | 'TRADICIONAL'
|
||||
export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO'
|
||||
|
||||
export type AsignaturaPreview = {
|
||||
@@ -12,10 +11,34 @@ export type AsignaturaPreview = {
|
||||
bibliografiaCount: number
|
||||
}
|
||||
|
||||
export type DataAsignaturaSugerida = {
|
||||
nombre: Asignatura['nombre']
|
||||
codigo?: Asignatura['codigo']
|
||||
tipo: Asignatura['tipo'] | null
|
||||
creditos: Asignatura['creditos'] | null
|
||||
horasAcademicas?: number | null
|
||||
horasIndependientes?: number | null
|
||||
descripcion: string
|
||||
}
|
||||
|
||||
export type AsignaturaSugerida = {
|
||||
id: string
|
||||
selected: boolean
|
||||
source: 'IA' | 'MANUAL' | 'CLON'
|
||||
linea_plan_id: string | null
|
||||
numero_ciclo: number | null
|
||||
} & DataAsignaturaSugerida
|
||||
|
||||
export type NewSubjectWizardState = {
|
||||
step: 1 | 2 | 3 | 4
|
||||
plan_estudio_id: Asignatura['plan_estudio_id']
|
||||
tipoOrigen: Asignatura['tipo_origen'] | null
|
||||
estructuraId: Asignatura['estructura_id'] | null
|
||||
tipoOrigen:
|
||||
| Asignatura['tipo_origen']
|
||||
| 'CLONADO'
|
||||
| 'IA_SIMPLE'
|
||||
| 'IA_MULTIPLE'
|
||||
| null
|
||||
datosBasicos: {
|
||||
nombre: Asignatura['nombre']
|
||||
codigo?: Asignatura['codigo']
|
||||
@@ -25,6 +48,7 @@ export type NewSubjectWizardState = {
|
||||
horasIndependientes?: Asignatura['horas_independientes'] | null
|
||||
estructuraId: Asignatura['estructura_id'] | null
|
||||
}
|
||||
sugerencias: Array<AsignaturaSugerida>
|
||||
clonInterno?: {
|
||||
facultadId?: string
|
||||
carreraId?: string
|
||||
@@ -42,6 +66,11 @@ export type NewSubjectWizardState = {
|
||||
repositoriosReferencia?: Array<string>
|
||||
archivosAdjuntos?: Array<UploadedFile>
|
||||
}
|
||||
iaMultiple?: {
|
||||
enfoque: string
|
||||
cantidadDeSugerencias: number
|
||||
isLoading: boolean
|
||||
}
|
||||
resumen: {
|
||||
previewAsignatura?: AsignaturaPreview
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
||||
import {
|
||||
Plus,
|
||||
@@ -8,9 +9,10 @@ import {
|
||||
BookOpen,
|
||||
Loader2,
|
||||
} from 'lucide-react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
|
||||
import type { Tables } from '@/types/supabase'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -30,13 +32,14 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { usePlanAsignaturas, usePlanLineas } from '@/data'
|
||||
import { qk, supabaseBrowser, usePlanAsignaturas, usePlanLineas } from '@/data'
|
||||
|
||||
// --- Configuración de Estilos ---
|
||||
const statusConfig: Record<
|
||||
AsignaturaStatus,
|
||||
{ label: string; className: string }
|
||||
> = {
|
||||
generando: { label: 'Generando', className: 'bg-slate-100 text-slate-600' },
|
||||
borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
|
||||
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
|
||||
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
|
||||
@@ -44,31 +47,31 @@ const statusConfig: Record<
|
||||
|
||||
const tipoConfig: Record<TipoAsignatura, { label: string; className: string }> =
|
||||
{
|
||||
obligatoria: {
|
||||
OBLIGATORIA: {
|
||||
label: 'Obligatoria',
|
||||
className: 'bg-blue-100 text-blue-700',
|
||||
},
|
||||
optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
|
||||
troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
|
||||
OPTATIVA: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
|
||||
TRONCAL: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
|
||||
OTRA: { label: 'Otra', className: 'bg-slate-100 text-slate-700' },
|
||||
}
|
||||
|
||||
// --- Mapeadores de API ---
|
||||
const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
|
||||
const mapAsignaturas = (
|
||||
asigApi: Array<Tables<'asignaturas'>> = [],
|
||||
): Array<Asignatura> => {
|
||||
return asigApi.map((asig) => ({
|
||||
id: asig.id,
|
||||
clave: asig.codigo,
|
||||
clave: asig.codigo ?? '',
|
||||
nombre: asig.nombre,
|
||||
creditos: asig.creditos ?? 0,
|
||||
creditos: asig.creditos,
|
||||
ciclo: asig.numero_ciclo ?? null,
|
||||
lineaCurricularId: asig.linea_plan_id ?? null,
|
||||
tipo:
|
||||
asig.tipo?.toLowerCase() === 'obligatoria' ? 'obligatoria' : 'optativa',
|
||||
estado: 'borrador', // O el campo que venga de tu API
|
||||
hd: Math.floor((asig.horas_semana ?? 0) / 2),
|
||||
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
|
||||
prerrequisitos: Array.isArray(asig.prerrequisitos)
|
||||
? asig.prerrequisitos
|
||||
: [],
|
||||
tipo: asig.tipo,
|
||||
estado: asig.estado,
|
||||
hd: asig.horas_academicas ?? 0,
|
||||
hi: asig.horas_independientes ?? 0,
|
||||
prerrequisitos: [],
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -79,12 +82,38 @@ export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas')({
|
||||
function AsignaturasPage() {
|
||||
const { planId } = Route.useParams()
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
// 1. Fetch de datos reales
|
||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
||||
usePlanAsignaturas(planId)
|
||||
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
|
||||
|
||||
useEffect(() => {
|
||||
const supabase = supabaseBrowser()
|
||||
const channel = supabase
|
||||
.channel(`plan:${planId}:asignaturas:updates`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'asignaturas',
|
||||
filter: `plan_estudio_id=eq.${planId}`,
|
||||
},
|
||||
() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: qk.planAsignaturas(planId),
|
||||
})
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel)
|
||||
}
|
||||
}, [planId, queryClient])
|
||||
|
||||
// 2. Estados de filtrado
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [filterTipo, setFilterTipo] = useState<string>('all')
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Tables } from './supabase'
|
||||
|
||||
export type PlanStatus =
|
||||
| 'borrador'
|
||||
| 'revision'
|
||||
@@ -12,9 +14,9 @@ export type TipoPlan =
|
||||
| 'Doctorado'
|
||||
| 'Especialidad'
|
||||
|
||||
export type TipoAsignatura = 'obligatoria' | 'optativa' | 'troncal'
|
||||
export type TipoAsignatura = Tables<'asignaturas'>['tipo']
|
||||
|
||||
export type AsignaturaStatus = 'borrador' | 'revisada' | 'aprobada'
|
||||
export type AsignaturaStatus = Tables<'asignaturas'>['estado']
|
||||
|
||||
export interface Facultad {
|
||||
id: string
|
||||
|
||||
@@ -7,11 +7,6 @@ export type Json =
|
||||
| Array<Json>
|
||||
|
||||
export type Database = {
|
||||
// Allows to automatically instantiate createClient with right options
|
||||
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
|
||||
__InternalSupabase: {
|
||||
PostgrestVersion: '12.2.3 (519615d)'
|
||||
}
|
||||
graphql_public: {
|
||||
Tables: {
|
||||
[_ in never]: never
|
||||
@@ -93,11 +88,11 @@ export type Database = {
|
||||
asignatura_hash: string | null
|
||||
codigo: string | null
|
||||
contenido_tematico: Json
|
||||
conversation_id: string | null
|
||||
creado_en: string
|
||||
creado_por: string | null
|
||||
creditos: number
|
||||
datos: Json
|
||||
estado: Database['public']['Enums']['estado_asignatura']
|
||||
estructura_id: string | null
|
||||
horas_academicas: number | null
|
||||
horas_independientes: number | null
|
||||
@@ -117,11 +112,11 @@ export type Database = {
|
||||
asignatura_hash?: string | null
|
||||
codigo?: string | null
|
||||
contenido_tematico?: Json
|
||||
conversation_id?: string | null
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
creditos: number
|
||||
datos?: Json
|
||||
estado?: Database['public']['Enums']['estado_asignatura']
|
||||
estructura_id?: string | null
|
||||
horas_academicas?: number | null
|
||||
horas_independientes?: number | null
|
||||
@@ -141,11 +136,11 @@ export type Database = {
|
||||
asignatura_hash?: string | null
|
||||
codigo?: string | null
|
||||
contenido_tematico?: Json
|
||||
conversation_id?: string | null
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
creditos?: number
|
||||
datos?: Json
|
||||
estado?: Database['public']['Enums']['estado_asignatura']
|
||||
estructura_id?: string | null
|
||||
horas_academicas?: number | null
|
||||
horas_independientes?: number | null
|
||||
@@ -394,6 +389,135 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
conversaciones_asignatura: {
|
||||
Row: {
|
||||
archivado_en: string | null
|
||||
archivado_por: string | null
|
||||
asignatura_id: string
|
||||
conversacion_json: Json
|
||||
creado_en: string
|
||||
creado_por: string | null
|
||||
estado: Database['public']['Enums']['estado_conversacion']
|
||||
id: string
|
||||
intento_archivado: number
|
||||
openai_conversation_id: string
|
||||
}
|
||||
Insert: {
|
||||
archivado_en?: string | null
|
||||
archivado_por?: string | null
|
||||
asignatura_id: string
|
||||
conversacion_json?: Json
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
estado?: Database['public']['Enums']['estado_conversacion']
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
openai_conversation_id: string
|
||||
}
|
||||
Update: {
|
||||
archivado_en?: string | null
|
||||
archivado_por?: string | null
|
||||
asignatura_id?: string
|
||||
conversacion_json?: Json
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
estado?: Database['public']['Enums']['estado_conversacion']
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
openai_conversation_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'conversaciones_asignatura_archivado_por_fkey'
|
||||
columns: ['archivado_por']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'usuarios_app'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_asignatura_asignatura_id_fkey'
|
||||
columns: ['asignatura_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'asignaturas'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_asignatura_creado_por_fkey'
|
||||
columns: ['creado_por']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'usuarios_app'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
]
|
||||
}
|
||||
conversaciones_plan: {
|
||||
Row: {
|
||||
archivado_en: string | null
|
||||
archivado_por: string | null
|
||||
conversacion_json: Json
|
||||
creado_en: string
|
||||
creado_por: string | null
|
||||
estado: Database['public']['Enums']['estado_conversacion']
|
||||
id: string
|
||||
intento_archivado: number
|
||||
openai_conversation_id: string
|
||||
plan_estudio_id: string
|
||||
}
|
||||
Insert: {
|
||||
archivado_en?: string | null
|
||||
archivado_por?: string | null
|
||||
conversacion_json?: Json
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
estado?: Database['public']['Enums']['estado_conversacion']
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
openai_conversation_id: string
|
||||
plan_estudio_id: string
|
||||
}
|
||||
Update: {
|
||||
archivado_en?: string | null
|
||||
archivado_por?: string | null
|
||||
conversacion_json?: Json
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
estado?: Database['public']['Enums']['estado_conversacion']
|
||||
id?: string
|
||||
intento_archivado?: number
|
||||
openai_conversation_id?: string
|
||||
plan_estudio_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: 'conversaciones_plan_archivado_por_fkey'
|
||||
columns: ['archivado_por']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'usuarios_app'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_plan_creado_por_fkey'
|
||||
columns: ['creado_por']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'usuarios_app'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_plan_plan_estudio_id_fkey'
|
||||
columns: ['plan_estudio_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'planes_estudio'
|
||||
referencedColumns: ['id']
|
||||
},
|
||||
{
|
||||
foreignKeyName: 'conversaciones_plan_plan_estudio_id_fkey'
|
||||
columns: ['plan_estudio_id']
|
||||
isOneToOne: false
|
||||
referencedRelation: 'plantilla_plan'
|
||||
referencedColumns: ['plan_estudio_id']
|
||||
},
|
||||
]
|
||||
}
|
||||
estados_plan: {
|
||||
Row: {
|
||||
clave: string
|
||||
@@ -677,7 +801,6 @@ export type Database = {
|
||||
actualizado_en: string
|
||||
actualizado_por: string | null
|
||||
carrera_id: string
|
||||
conversation_id: string | null
|
||||
creado_en: string
|
||||
creado_por: string | null
|
||||
datos: Json
|
||||
@@ -698,7 +821,6 @@ export type Database = {
|
||||
actualizado_en?: string
|
||||
actualizado_por?: string | null
|
||||
carrera_id: string
|
||||
conversation_id?: string | null
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
datos?: Json
|
||||
@@ -719,7 +841,6 @@ export type Database = {
|
||||
actualizado_en?: string
|
||||
actualizado_por?: string | null
|
||||
carrera_id?: string
|
||||
conversation_id?: string | null
|
||||
creado_en?: string
|
||||
creado_por?: string | null
|
||||
datos?: Json
|
||||
@@ -1089,6 +1210,8 @@ export type Database = {
|
||||
unaccent_immutable: { Args: { '': string }; Returns: string }
|
||||
}
|
||||
Enums: {
|
||||
estado_asignatura: 'borrador' | 'revisada' | 'aprobada' | 'generando'
|
||||
estado_conversacion: 'ACTIVA' | 'ARCHIVANDO' | 'ARCHIVADA' | 'ERROR'
|
||||
estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA'
|
||||
fuente_cambio: 'HUMANO' | 'IA'
|
||||
nivel_plan_estudio:
|
||||
@@ -1261,6 +1384,8 @@ export const Constants = {
|
||||
},
|
||||
public: {
|
||||
Enums: {
|
||||
estado_asignatura: ['borrador', 'revisada', 'aprobada', 'generando'],
|
||||
estado_conversacion: ['ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR'],
|
||||
estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'],
|
||||
fuente_cambio: ['HUMANO', 'IA'],
|
||||
nivel_plan_estudio: [
|
||||
|
||||
Reference in New Issue
Block a user