Compare commits
25 Commits
d167589c61
...
c280faef22
| Author | SHA1 | Date | |
|---|---|---|---|
| c280faef22 | |||
| d6c567195a | |||
| 9c588cfd8f | |||
| 46d8d6142e | |||
| 07d08e1b57 | |||
| ded54c18dd | |||
| 89f264bf5d | |||
| 675c76db74 | |||
| d74807c84e | |||
| 4d0f5815eb | |||
| 2f9e779bce | |||
| 0c57bdfc38 | |||
| 2250a1afd1 | |||
| 9102e756cb | |||
| e788eb788f | |||
| 2ec222694d | |||
| 58d4ee8b6e | |||
| d9a6852f43 | |||
| ba188329dc | |||
| 777be81d2a | |||
| 3afce0de77 | |||
| 4b8ec2c5ab | |||
| 0788002c9b | |||
| c7c631a701 | |||
| 9ba94f2c2c |
@@ -36,12 +36,14 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"motion": "^12.24.7",
|
"motion": "^12.24.7",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"use-debounce": "^10.1.0",
|
"use-debounce": "^10.1.0",
|
||||||
|
"vaul": "^1.1.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/devtools-vite": "^0.3.11",
|
"@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/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
|
||||||
|
|
||||||
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
|
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
|
||||||
|
|
||||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="],
|
||||||
|
|
||||||
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
|
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
|
||||||
|
|
||||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
|
"@radix-ui/react-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-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="],
|
||||||
|
|
||||||
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||||
|
|
||||||
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
|
||||||
|
|
||||||
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="],
|
||||||
|
|
||||||
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
|
||||||
|
|
||||||
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
|
||||||
@@ -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-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
|
||||||
|
|
||||||
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
|
||||||
@@ -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-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="],
|
||||||
|
|
||||||
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
@@ -1161,6 +1197,8 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="],
|
||||||
|
|
||||||
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
"react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="],
|
||||||
@@ -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=="],
|
"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": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
||||||
|
|
||||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||||
@@ -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-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||||
|
|
||||||
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
@@ -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-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-toolbar/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||||
|
|
||||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
|
||||||
@@ -1487,6 +1531,14 @@
|
|||||||
|
|
||||||
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
|
"radix-ui/@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="],
|
||||||
|
|
||||||
|
"radix-ui/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
|
||||||
|
|
||||||
|
"radix-ui/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
|
||||||
|
|
||||||
|
"radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||||
|
|
||||||
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|||||||
+3
-1
@@ -49,12 +49,14 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"motion": "^12.24.7",
|
"motion": "^12.24.7",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwindcss": "^4.0.6",
|
"tailwindcss": "^4.0.6",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"use-debounce": "^10.1.0"
|
"use-debounce": "^10.1.0",
|
||||||
|
"vaul": "^1.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tanstack/devtools-vite": "^0.3.11",
|
"@tanstack/devtools-vite": "^0.3.11",
|
||||||
|
|||||||
@@ -249,22 +249,16 @@ export default function AsignaturaDetailPage() {
|
|||||||
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
|
<div className="flex flex-wrap gap-4 text-sm text-blue-200">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<GraduationCap className="h-4 w-4 shrink-0" />
|
<GraduationCap className="h-4 w-4 shrink-0" />
|
||||||
{/* Eliminamos el max-w y dejamos que el flex-wrap haga su trabajo */}
|
<span className="text-blue-100">
|
||||||
<EditableHeaderField
|
{asignaturasApi?.planes_estudio?.datos?.nombre || ''}
|
||||||
value={asignaturasApi?.planes_estudio?.datos?.nombre || ''}
|
|
||||||
onSave={(val) => handleUpdateHeader('plan_nombre', val)}
|
|
||||||
className="min-w-[10ch] text-blue-100" // min-w para que sea clickeable si está vacío
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<EditableHeaderField
|
<span className="text-blue-100">
|
||||||
value={
|
{asignaturasApi?.planes_estudio?.carreras?.facultades
|
||||||
asignaturasApi?.planes_estudio?.carreras?.facultades
|
?.nombre || ''}
|
||||||
?.nombre || ''
|
</span>
|
||||||
}
|
|
||||||
onSave={(val) => handleUpdateHeader('facultad_nombre', val)}
|
|
||||||
className="min-w-[10ch] text-blue-100"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Plus, Search, BookOpen, Trash2, Library, Edit3 } from 'lucide-react'
|
||||||
Plus,
|
|
||||||
Search,
|
|
||||||
BookOpen,
|
|
||||||
Trash2,
|
|
||||||
Library,
|
|
||||||
Edit3,
|
|
||||||
Save,
|
|
||||||
} from 'lucide-react'
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -204,15 +196,6 @@ export function BibliographyItem({
|
|||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Button
|
|
||||||
onClick={() => onSave(entries)}
|
|
||||||
disabled={isSaving}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700"
|
|
||||||
>
|
|
||||||
<Save className="mr-2 h-4 w-4" />{' '}
|
|
||||||
{isSaving ? 'Guardando...' : 'Guardar'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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 type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
|
||||||
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '@/components/ui/accordion'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
@@ -20,6 +26,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data'
|
||||||
import {
|
import {
|
||||||
FACULTADES,
|
FACULTADES,
|
||||||
MATERIAS_MOCK,
|
MATERIAS_MOCK,
|
||||||
@@ -29,12 +36,14 @@ import {
|
|||||||
export function PasoDetallesPanel({
|
export function PasoDetallesPanel({
|
||||||
wizard,
|
wizard,
|
||||||
onChange,
|
onChange,
|
||||||
onGenerarIA: _onGenerarIA,
|
|
||||||
}: {
|
}: {
|
||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
onChange: React.Dispatch<React.SetStateAction<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') {
|
if (wizard.tipoOrigen === 'MANUAL') {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -49,7 +58,7 @@ export function PasoDetallesPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wizard.tipoOrigen === 'IA') {
|
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<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') {
|
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
|
|||||||
@@ -77,12 +77,93 @@ export function PasoMetodoCardGroup({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>Generar contenido automático.</CardDescription>
|
<CardDescription>Generar contenido automático.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
{(wizard.tipoOrigen === 'IA' ||
|
||||||
|
wizard.tipoOrigen === 'IA_SIMPLE' ||
|
||||||
|
wizard.tipoOrigen === 'IA_MULTIPLE') && (
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
tipoOrigen: 'IA_SIMPLE',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent) =>
|
||||||
|
handleKeyActivate(e, () =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
tipoOrigen: 'IA_SIMPLE',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||||
|
isSelected('IA_SIMPLE')
|
||||||
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icons.Edit3 className="h-6 w-6 flex-none" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">Una asignatura</span>
|
||||||
|
<span className="text-xs opacity-70">
|
||||||
|
Crear una asignatura con control detallado de metadatos.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
tipoOrigen: 'IA_MULTIPLE',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onKeyDown={(e: React.KeyboardEvent) =>
|
||||||
|
handleKeyActivate(e, () =>
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
tipoOrigen: 'IA_MULTIPLE',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||||
|
isSelected('IA_MULTIPLE')
|
||||||
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icons.List className="h-6 w-6 flex-none" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">Varias asignaturas</span>
|
||||||
|
<span className="text-xs opacity-70">
|
||||||
|
Generar varias asignaturas a partir de sugerencias de la IA.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
className={isSelected('OTRO') ? 'ring-ring ring-2' : ''}
|
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onChange((w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'OTRO' }))
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'CLONADO' }),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@@ -93,7 +174,7 @@ export function PasoMetodoCardGroup({
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(wizard.tipoOrigen === 'OTRO' ||
|
{(wizard.tipoOrigen === 'CLONADO' ||
|
||||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
|
||||||
<CardContent className="flex flex-col gap-3">
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import { usePlan, useSubjectEstructuras } from '@/data'
|
import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data'
|
||||||
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
||||||
|
|
||||||
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||||
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
||||||
const { data: estructuras } = useSubjectEstructuras()
|
const { data: estructuras } = useSubjectEstructuras()
|
||||||
|
const { data: lineasPlan } = usePlanLineas(wizard.plan_estudio_id)
|
||||||
|
|
||||||
const estructuraNombre = (() => {
|
const estructuraNombre = (() => {
|
||||||
const estructuraId = wizard.datosBasicos.estructuraId
|
const estructuraId = wizard.datosBasicos.estructuraId
|
||||||
@@ -26,6 +27,8 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
const modoLabel = (() => {
|
const modoLabel = (() => {
|
||||||
if (wizard.tipoOrigen === 'MANUAL') return 'Manual (Vacía)'
|
if (wizard.tipoOrigen === 'MANUAL') return 'Manual (Vacía)'
|
||||||
if (wizard.tipoOrigen === 'IA') return 'Generada con IA'
|
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_INTERNO') return 'Clonada (Sistema)'
|
||||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') return 'Clonada (Archivo)'
|
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') return 'Clonada (Archivo)'
|
||||||
return '—'
|
return '—'
|
||||||
@@ -41,6 +44,10 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
const repositoriosRef = wizard.iaConfig?.repositoriosReferencia ?? []
|
const repositoriosRef = wizard.iaConfig?.repositoriosReferencia ?? []
|
||||||
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
||||||
|
|
||||||
|
const materiasSeleccionadas = wizard.sugerencias.filter((s) => s.selected)
|
||||||
|
const iaMultipleEnfoque = wizard.iaMultiple?.enfoque.trim() ?? ''
|
||||||
|
const iaMultipleCantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -72,7 +79,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
{wizard.tipoOrigen === 'MANUAL' && (
|
{wizard.tipoOrigen === 'MANUAL' && (
|
||||||
<Icons.Pencil className="h-4 w-4" />
|
<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" />
|
<Icons.Sparkles className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
{(wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
{(wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||||
@@ -83,6 +92,88 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
</span>
|
</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-background/40 border-border/60 rounded-lg border p-3">
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
Estructura
|
||||||
|
</div>
|
||||||
|
<div className="text-foreground mt-1 text-sm font-medium">
|
||||||
|
{estructuraNombre}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-border/60 bg-muted/30 grid gap-3 rounded-xl border p-4">
|
||||||
|
<div className="flex items-end justify-between gap-2">
|
||||||
|
<div className="text-foreground text-base font-semibold">
|
||||||
|
Materias seleccionadas
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{materiasSeleccionadas.length} en total
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{materiasSeleccionadas.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
No hay materias seleccionadas.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{materiasSeleccionadas.map((m) => {
|
||||||
|
const lineaNombre = m.linea_plan_id
|
||||||
|
? (lineasPlan?.find((l) => l.id === m.linea_plan_id)
|
||||||
|
?.nombre ?? m.linea_plan_id)
|
||||||
|
: '—'
|
||||||
|
|
||||||
|
const cicloText =
|
||||||
|
typeof m.numero_ciclo === 'number' &&
|
||||||
|
Number.isFinite(m.numero_ciclo)
|
||||||
|
? String(m.numero_ciclo)
|
||||||
|
: '—'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
className="bg-background/40 border-border/60 grid gap-2 rounded-lg border p-3"
|
||||||
|
>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div className="text-foreground text-sm font-semibold">
|
||||||
|
{m.nombre}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
|
||||||
|
<span className="bg-accent/30 text-accent-foreground rounded-full px-2 py-0.5">
|
||||||
|
Línea: {lineaNombre}
|
||||||
|
</span>
|
||||||
|
<span className="bg-accent/30 text-accent-foreground rounded-full px-2 py-0.5">
|
||||||
|
Ciclo: {cicloText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground text-sm whitespace-pre-wrap">
|
||||||
|
{m.descripcion || '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<span className="text-muted-foreground">Nombre: </span>
|
<span className="text-muted-foreground">Nombre: </span>
|
||||||
@@ -111,7 +202,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
<span className="font-medium">{estructuraNombre}</span>
|
<span className="font-medium">{estructuraNombre}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Horas académicas: </span>
|
<span className="text-muted-foreground">
|
||||||
|
Horas académicas:{' '}
|
||||||
|
</span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{wizard.datosBasicos.horasAcademicas ?? '—'}
|
{wizard.datosBasicos.horasAcademicas ?? '—'}
|
||||||
</span>
|
</span>
|
||||||
@@ -160,7 +253,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">Repositorios de referencia</div>
|
<div className="font-medium">
|
||||||
|
Repositorios de referencia
|
||||||
|
</div>
|
||||||
{repositoriosRef.length ? (
|
{repositoriosRef.length ? (
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||||
{repositoriosRef.map((id) => (
|
{repositoriosRef.map((id) => (
|
||||||
@@ -178,7 +273,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||||
{adjuntos.map((f) => (
|
{adjuntos.map((f) => (
|
||||||
<li key={f.id}>
|
<li key={f.id}>
|
||||||
<span className="text-foreground">{f.file.name}</span>{' '}
|
<span className="text-foreground">
|
||||||
|
{f.file.name}
|
||||||
|
</span>{' '}
|
||||||
<span>· {formatFileSize(f.file.size)}</span>
|
<span>· {formatFileSize(f.file.size)}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -189,6 +286,8 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useNavigate } from '@tanstack/react-router'
|
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 { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
|
import type { TablesInsert } from '@/types/supabase'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { useGenerateSubjectAI } from '@/data'
|
import { supabaseBrowser, useGenerateSubjectAI, qk } from '@/data'
|
||||||
|
|
||||||
export function WizardControls({
|
export function WizardControls({
|
||||||
wizard,
|
wizard,
|
||||||
@@ -28,6 +30,7 @@ export function WizardControls({
|
|||||||
isLastStep: boolean
|
isLastStep: boolean
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const qc = useQueryClient()
|
||||||
const generateSubjectAI = useGenerateSubjectAI()
|
const generateSubjectAI = useGenerateSubjectAI()
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
setWizard((w) => ({
|
setWizard((w) => ({
|
||||||
@@ -37,7 +40,7 @@ export function WizardControls({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (wizard.tipoOrigen === 'IA') {
|
if (wizard.tipoOrigen === 'IA' || wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||||
const aiInput: AIGenerateSubjectInput = {
|
const aiInput: AIGenerateSubjectInput = {
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
@@ -77,6 +80,82 @@ export function WizardControls({
|
|||||||
})
|
})
|
||||||
return
|
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) {
|
} catch (err: any) {
|
||||||
setWizard((w) => ({
|
setWizard((w) => ({
|
||||||
...w,
|
...w,
|
||||||
@@ -94,7 +173,7 @@ export function WizardControls({
|
|||||||
Anterior
|
Anterior
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="mx-2 flex-1">
|
||||||
{(errorMessage ?? wizard.errorMessage) && (
|
{(errorMessage ?? wizard.errorMessage) && (
|
||||||
<span className="text-destructive text-sm font-medium">
|
<span className="text-destructive text-sm font-medium">
|
||||||
{errorMessage ?? wizard.errorMessage}
|
{errorMessage ?? wizard.errorMessage}
|
||||||
|
|||||||
@@ -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 }
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Drawer as DrawerPrimitive } from "vaul"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Drawer({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||||
|
return <DrawerPrimitive.Root data-slot="drawer" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||||
|
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||||
|
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||||
|
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Overlay
|
||||||
|
data-slot="drawer-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<DrawerPortal data-slot="drawer-portal">
|
||||||
|
<DrawerOverlay />
|
||||||
|
<DrawerPrimitive.Content
|
||||||
|
data-slot="drawer-content"
|
||||||
|
className={cn(
|
||||||
|
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||||
|
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||||
|
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||||
|
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||||
|
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||||
|
{children}
|
||||||
|
</DrawerPrimitive.Content>
|
||||||
|
</DrawerPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-header"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="drawer-footer"
|
||||||
|
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Title
|
||||||
|
data-slot="drawer-title"
|
||||||
|
className={cn("text-foreground font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrawerDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DrawerPrimitive.Description
|
||||||
|
data-slot="drawer-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Drawer,
|
||||||
|
DrawerPortal,
|
||||||
|
DrawerOverlay,
|
||||||
|
DrawerTrigger,
|
||||||
|
DrawerClose,
|
||||||
|
DrawerContent,
|
||||||
|
DrawerHeader,
|
||||||
|
DrawerFooter,
|
||||||
|
DrawerTitle,
|
||||||
|
DrawerDescription,
|
||||||
|
}
|
||||||
@@ -4,9 +4,11 @@ import { StepWithTooltip } from '@/components/wizard/StepWithTooltip'
|
|||||||
export function WizardResponsiveHeader({
|
export function WizardResponsiveHeader({
|
||||||
wizard,
|
wizard,
|
||||||
methods,
|
methods,
|
||||||
|
titleOverrides,
|
||||||
}: {
|
}: {
|
||||||
wizard: any
|
wizard: any
|
||||||
methods: any
|
methods: any
|
||||||
|
titleOverrides?: Record<string, string>
|
||||||
}) {
|
}) {
|
||||||
const idx = wizard.utils.getIndex(methods.current.id)
|
const idx = wizard.utils.getIndex(methods.current.id)
|
||||||
const totalSteps = wizard.steps.length
|
const totalSteps = wizard.steps.length
|
||||||
@@ -14,6 +16,8 @@ export function WizardResponsiveHeader({
|
|||||||
const hasNextStep = idx < totalSteps - 1
|
const hasNextStep = idx < totalSteps - 1
|
||||||
const nextStep = wizard.steps[currentIndex]
|
const nextStep = wizard.steps[currentIndex]
|
||||||
|
|
||||||
|
const resolveTitle = (step: any) => titleOverrides?.[step?.id] ?? step?.title
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden">
|
||||||
@@ -22,13 +26,13 @@ export function WizardResponsiveHeader({
|
|||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
<h2 className="text-lg font-bold text-slate-900">
|
<h2 className="text-lg font-bold text-slate-900">
|
||||||
<StepWithTooltip
|
<StepWithTooltip
|
||||||
title={methods.current.title}
|
title={resolveTitle(methods.current)}
|
||||||
desc={methods.current.description}
|
desc={methods.current.description}
|
||||||
/>
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
{hasNextStep && nextStep ? (
|
{hasNextStep && nextStep ? (
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-slate-400">
|
||||||
Siguiente: {nextStep.title}
|
Siguiente: {resolveTitle(nextStep)}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm font-medium text-green-500">
|
<p className="text-sm font-medium text-green-500">
|
||||||
@@ -48,7 +52,10 @@ export function WizardResponsiveHeader({
|
|||||||
className="whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<wizard.Stepper.Title>
|
<wizard.Stepper.Title>
|
||||||
<StepWithTooltip title={step.title} desc={step.description} />
|
<StepWithTooltip
|
||||||
|
title={resolveTitle(step)}
|
||||||
|
desc={step.description}
|
||||||
|
/>
|
||||||
</wizard.Stepper.Title>
|
</wizard.Stepper.Title>
|
||||||
</wizard.Stepper.Step>
|
</wizard.Stepper.Step>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ export async function plan_asignaturas_list(
|
|||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('asignaturas')
|
.from('asignaturas')
|
||||||
.select(
|
.select(
|
||||||
'id,plan_estudio_id,horas_academicas, horas_independientes,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,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)
|
.eq('plan_estudio_id', planId)
|
||||||
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
.order('numero_ciclo', { ascending: true, nullsFirst: false })
|
||||||
@@ -176,18 +176,31 @@ export async function plan_asignaturas_list(
|
|||||||
return data ?? []
|
return data ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> {
|
export async function plans_history(
|
||||||
|
planId: UUID,
|
||||||
|
page: number = 0,
|
||||||
|
pageSize: number = 4,
|
||||||
|
): Promise<{ data: Array<CambioPlan>; count: number }> {
|
||||||
|
// Cambiamos el retorno
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const { data, error } = await supabase
|
const from = page * pageSize
|
||||||
|
const to = from + pageSize - 1
|
||||||
|
|
||||||
|
const { data, error, count } = await supabase
|
||||||
.from('cambios_plan')
|
.from('cambios_plan')
|
||||||
.select(
|
.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)
|
.eq('plan_estudio_id', planId)
|
||||||
.order('cambiado_en', { ascending: false })
|
.order('cambiado_en', { ascending: false })
|
||||||
|
.range(from, to)
|
||||||
|
|
||||||
throwIfError(error)
|
throwIfError(error)
|
||||||
return data ?? []
|
return {
|
||||||
|
data: data ?? [],
|
||||||
|
count: count ?? 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Wizard: crear plan manual (Edge Function) */
|
/** Wizard: crear plan manual (Edge Function) */
|
||||||
@@ -291,7 +304,7 @@ export async function ai_generate_plan(
|
|||||||
archivosAdjuntos: undefined, // los manejamos aparte
|
archivosAdjuntos: undefined, // los manejamos aparte
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
input.iaConfig.archivosAdjuntos.forEach((file, index) => {
|
input.iaConfig.archivosAdjuntos.forEach((file) => {
|
||||||
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ import type {
|
|||||||
UUID,
|
UUID,
|
||||||
} from '../types/domain'
|
} from '../types/domain'
|
||||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||||
|
import type {
|
||||||
|
AsignaturaSugerida,
|
||||||
|
DataAsignaturaSugerida,
|
||||||
|
} from '@/features/asignaturas/nueva/types'
|
||||||
import type { Database } from '@/types/supabase'
|
import type { Database } from '@/types/supabase'
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
|
generate_subject_suggestions: 'generate-subject-suggestions',
|
||||||
subjects_create_manual: 'subjects_create_manual',
|
subjects_create_manual: 'subjects_create_manual',
|
||||||
ai_generate_subject: 'ai-generate-subject',
|
ai_generate_subject: 'ai-generate-subject',
|
||||||
subjects_persist_from_ai: 'subjects_persist_from_ai',
|
subjects_persist_from_ai: 'subjects_persist_from_ai',
|
||||||
@@ -36,7 +41,7 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
|
|||||||
.from('asignaturas')
|
.from('asignaturas')
|
||||||
.select(
|
.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(
|
planes_estudio(
|
||||||
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||||
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
||||||
@@ -133,9 +138,64 @@ export type AIGenerateSubjectInput = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function ai_generate_subject(
|
export async function ai_generate_subject(
|
||||||
input: AIGenerateSubjectInput,
|
input: AIGenerateSubjectInput | AIGenerateSubjectJsonInput,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
if ('datosBasicos' in input) {
|
||||||
const edgeFunctionBody = new FormData()
|
const edgeFunctionBody = new FormData()
|
||||||
edgeFunctionBody.append('plan_estudio_id', input.plan_estudio_id)
|
edgeFunctionBody.append('plan_estudio_id', input.plan_estudio_id)
|
||||||
edgeFunctionBody.append('datosBasicos', JSON.stringify(input.datosBasicos))
|
edgeFunctionBody.append('datosBasicos', JSON.stringify(input.datosBasicos))
|
||||||
@@ -146,7 +206,7 @@ export async function ai_generate_subject(
|
|||||||
archivosAdjuntos: undefined, // los manejamos aparte
|
archivosAdjuntos: undefined, // los manejamos aparte
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
input.iaConfig?.archivosAdjuntos?.forEach((file, index) => {
|
input.iaConfig?.archivosAdjuntos?.forEach((file) => {
|
||||||
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
||||||
})
|
})
|
||||||
return invokeEdge<any>(
|
return invokeEdge<any>(
|
||||||
@@ -155,6 +215,11 @@ export async function ai_generate_subject(
|
|||||||
undefined,
|
undefined,
|
||||||
supabaseBrowser(),
|
supabaseBrowser(),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return invokeEdge<any>(EDGE.ai_generate_subject, input, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subjects_persist_from_ai(payload: {
|
export async function subjects_persist_from_ai(payload: {
|
||||||
|
|||||||
@@ -80,11 +80,17 @@ export function usePlanAsignaturas(planId: UUID | null | undefined) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usePlanHistorial(planId: UUID | null | undefined) {
|
export function usePlanHistorial(
|
||||||
|
planId: UUID | null | undefined,
|
||||||
|
page: number,
|
||||||
|
) {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: planId ? qk.planHistorial(planId) : ['planes', 'historial', null],
|
queryKey: planId
|
||||||
queryFn: () => plans_history(planId as UUID),
|
? [...qk.planHistorial(planId), page]
|
||||||
|
: ['planes', 'historial', null, page],
|
||||||
|
queryFn: () => plans_history(planId as UUID, page),
|
||||||
enabled: Boolean(planId),
|
enabled: Boolean(planId),
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const qk = {
|
|||||||
planHistorial: (planId: string) => ['planes', planId, 'historial'] as const,
|
planHistorial: (planId: string) => ['planes', planId, 'historial'] as const,
|
||||||
planDocumento: (planId: string) => ['planes', planId, 'documento'] as const,
|
planDocumento: (planId: string) => ['planes', planId, 'documento'] as const,
|
||||||
|
|
||||||
|
sugerenciasAsignaturas: () => ['asignaturas', 'sugerencias'] as const,
|
||||||
asignatura: (asignaturaId: string) =>
|
asignatura: (asignaturaId: string) =>
|
||||||
['asignaturas', 'detail', asignaturaId] as const,
|
['asignaturas', 'detail', asignaturaId] as const,
|
||||||
asignaturaBibliografia: (asignaturaId: string) =>
|
asignaturaBibliografia: (asignaturaId: string) =>
|
||||||
|
|||||||
@@ -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 { 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 { PasoDetallesPanel } from '@/components/asignaturas/wizard/PasoDetallesPanel'
|
||||||
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
||||||
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
||||||
@@ -55,10 +55,16 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
canContinueDesdeMetodo,
|
canContinueDesdeMetodo,
|
||||||
canContinueDesdeBasicos,
|
canContinueDesdeBasicos,
|
||||||
canContinueDesdeDetalles,
|
canContinueDesdeDetalles,
|
||||||
simularGeneracionIA,
|
|
||||||
crearAsignatura,
|
|
||||||
} = useNuevaAsignaturaWizard(planId)
|
} = useNuevaAsignaturaWizard(planId)
|
||||||
|
|
||||||
|
const titleOverrides =
|
||||||
|
wizard.tipoOrigen === 'IA_MULTIPLE'
|
||||||
|
? {
|
||||||
|
basicos: 'Sugerencias',
|
||||||
|
detalles: 'Estructura',
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
||||||
}
|
}
|
||||||
@@ -99,7 +105,11 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
title="Nueva Asignatura"
|
title="Nueva Asignatura"
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
headerSlot={
|
headerSlot={
|
||||||
<WizardResponsiveHeader wizard={Wizard} methods={methods} />
|
<WizardResponsiveHeader
|
||||||
|
wizard={Wizard}
|
||||||
|
methods={methods}
|
||||||
|
titleOverrides={titleOverrides}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
footerSlot={
|
footerSlot={
|
||||||
<Wizard.Stepper.Controls>
|
<Wizard.Stepper.Controls>
|
||||||
@@ -137,11 +147,7 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
|||||||
|
|
||||||
{idx === 2 && (
|
{idx === 2 && (
|
||||||
<Wizard.Stepper.Panel>
|
<Wizard.Stepper.Panel>
|
||||||
<PasoDetallesPanel
|
<PasoDetallesPanel wizard={wizard} onChange={setWizard} />
|
||||||
wizard={wizard}
|
|
||||||
onChange={setWizard}
|
|
||||||
onGenerarIA={simularGeneracionIA}
|
|
||||||
/>
|
|
||||||
</Wizard.Stepper.Panel>
|
</Wizard.Stepper.Panel>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import type { AsignaturaPreview, NewSubjectWizardState } from '../types'
|
import type { NewSubjectWizardState } from '../types'
|
||||||
|
|
||||||
export function useNuevaAsignaturaWizard(planId: string) {
|
export function useNuevaAsignaturaWizard(planId: string) {
|
||||||
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
||||||
step: 1,
|
step: 1,
|
||||||
plan_estudio_id: planId,
|
plan_estudio_id: planId,
|
||||||
|
estructuraId: null,
|
||||||
tipoOrigen: null,
|
tipoOrigen: null,
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombre: '',
|
nombre: '',
|
||||||
@@ -16,6 +17,7 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
horasIndependientes: null,
|
horasIndependientes: null,
|
||||||
estructuraId: '',
|
estructuraId: '',
|
||||||
},
|
},
|
||||||
|
sugerencias: [],
|
||||||
clonInterno: {},
|
clonInterno: {},
|
||||||
clonTradicional: {
|
clonTradicional: {
|
||||||
archivoWordAsignaturaId: null,
|
archivoWordAsignaturaId: null,
|
||||||
@@ -28,6 +30,11 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
repositoriosReferencia: [],
|
repositoriosReferencia: [],
|
||||||
archivosAdjuntos: [],
|
archivosAdjuntos: [],
|
||||||
},
|
},
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: '',
|
||||||
|
cantidadDeSugerencias: 10,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
resumen: {},
|
resumen: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
@@ -35,20 +42,23 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
|
|
||||||
const canContinueDesdeMetodo =
|
const canContinueDesdeMetodo =
|
||||||
wizard.tipoOrigen === 'MANUAL' ||
|
wizard.tipoOrigen === 'MANUAL' ||
|
||||||
wizard.tipoOrigen === 'IA' ||
|
wizard.tipoOrigen === 'IA_SIMPLE' ||
|
||||||
|
wizard.tipoOrigen === 'IA_MULTIPLE' ||
|
||||||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
|
||||||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
|
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
|
||||||
|
|
||||||
const canContinueDesdeBasicos =
|
const canContinueDesdeBasicos =
|
||||||
!!wizard.datosBasicos.nombre &&
|
(!!wizard.datosBasicos.nombre &&
|
||||||
wizard.datosBasicos.tipo !== null &&
|
wizard.datosBasicos.tipo !== null &&
|
||||||
wizard.datosBasicos.creditos !== null &&
|
wizard.datosBasicos.creditos !== null &&
|
||||||
wizard.datosBasicos.creditos > 0 &&
|
wizard.datosBasicos.creditos > 0 &&
|
||||||
!!wizard.datosBasicos.estructuraId
|
!!wizard.datosBasicos.estructuraId) ||
|
||||||
|
(wizard.tipoOrigen === 'IA_MULTIPLE' &&
|
||||||
|
wizard.sugerencias.filter((s) => s.selected).length > 0)
|
||||||
|
|
||||||
const canContinueDesdeDetalles = (() => {
|
const canContinueDesdeDetalles = (() => {
|
||||||
if (wizard.tipoOrigen === 'MANUAL') return true
|
if (wizard.tipoOrigen === 'MANUAL') return true
|
||||||
if (wizard.tipoOrigen === 'IA') {
|
if (wizard.tipoOrigen === 'IA_SIMPLE') {
|
||||||
return !!wizard.iaConfig?.descripcionEnfoqueAcademico
|
return !!wizard.iaConfig?.descripcionEnfoqueAcademico
|
||||||
}
|
}
|
||||||
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
|
||||||
@@ -57,38 +67,17 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
|
||||||
return !!wizard.clonTradicional?.archivoWordAsignaturaId
|
return !!wizard.clonTradicional?.archivoWordAsignaturaId
|
||||||
}
|
}
|
||||||
|
if (wizard.tipoOrigen === 'IA_MULTIPLE') {
|
||||||
|
return wizard.estructuraId !== null
|
||||||
|
}
|
||||||
return false
|
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 {
|
return {
|
||||||
wizard,
|
wizard,
|
||||||
setWizard,
|
setWizard,
|
||||||
canContinueDesdeMetodo,
|
canContinueDesdeMetodo,
|
||||||
canContinueDesdeBasicos,
|
canContinueDesdeBasicos,
|
||||||
canContinueDesdeDetalles,
|
canContinueDesdeDetalles,
|
||||||
simularGeneracionIA,
|
|
||||||
crearAsignatura,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/
|
|||||||
import type { Asignatura } from '@/data'
|
import type { Asignatura } from '@/data'
|
||||||
|
|
||||||
export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO'
|
export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO'
|
||||||
export type SubModoClonado = 'INTERNO' | 'TRADICIONAL'
|
|
||||||
export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO'
|
export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO'
|
||||||
|
|
||||||
export type AsignaturaPreview = {
|
export type AsignaturaPreview = {
|
||||||
@@ -12,10 +11,34 @@ export type AsignaturaPreview = {
|
|||||||
bibliografiaCount: number
|
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 = {
|
export type NewSubjectWizardState = {
|
||||||
step: 1 | 2 | 3 | 4
|
step: 1 | 2 | 3 | 4
|
||||||
plan_estudio_id: Asignatura['plan_estudio_id']
|
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: {
|
datosBasicos: {
|
||||||
nombre: Asignatura['nombre']
|
nombre: Asignatura['nombre']
|
||||||
codigo?: Asignatura['codigo']
|
codigo?: Asignatura['codigo']
|
||||||
@@ -25,6 +48,7 @@ export type NewSubjectWizardState = {
|
|||||||
horasIndependientes?: Asignatura['horas_independientes'] | null
|
horasIndependientes?: Asignatura['horas_independientes'] | null
|
||||||
estructuraId: Asignatura['estructura_id'] | null
|
estructuraId: Asignatura['estructura_id'] | null
|
||||||
}
|
}
|
||||||
|
sugerencias: Array<AsignaturaSugerida>
|
||||||
clonInterno?: {
|
clonInterno?: {
|
||||||
facultadId?: string
|
facultadId?: string
|
||||||
carreraId?: string
|
carreraId?: string
|
||||||
@@ -42,6 +66,11 @@ export type NewSubjectWizardState = {
|
|||||||
repositoriosReferencia?: Array<string>
|
repositoriosReferencia?: Array<string>
|
||||||
archivosAdjuntos?: Array<UploadedFile>
|
archivosAdjuntos?: Array<UploadedFile>
|
||||||
}
|
}
|
||||||
|
iaMultiple?: {
|
||||||
|
enfoque: string
|
||||||
|
cantidadDeSugerencias: number
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
resumen: {
|
resumen: {
|
||||||
previewAsignatura?: AsignaturaPreview
|
previewAsignatura?: AsignaturaPreview
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
Plus,
|
Plus,
|
||||||
@@ -8,9 +9,10 @@ import {
|
|||||||
BookOpen,
|
BookOpen,
|
||||||
Loader2,
|
Loader2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useMemo } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
|
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
|
||||||
|
import type { Tables } from '@/types/supabase'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -30,13 +32,14 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table'
|
} from '@/components/ui/table'
|
||||||
import { usePlanAsignaturas, usePlanLineas } from '@/data'
|
import { qk, supabaseBrowser, usePlanAsignaturas, usePlanLineas } from '@/data'
|
||||||
|
|
||||||
// --- Configuración de Estilos ---
|
// --- Configuración de Estilos ---
|
||||||
const statusConfig: Record<
|
const statusConfig: Record<
|
||||||
AsignaturaStatus,
|
AsignaturaStatus,
|
||||||
{ label: string; className: string }
|
{ label: string; className: string }
|
||||||
> = {
|
> = {
|
||||||
|
generando: { label: 'Generando', className: 'bg-slate-100 text-slate-600' },
|
||||||
borrador: { label: 'Borrador', 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' },
|
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
|
||||||
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-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 }> =
|
const tipoConfig: Record<TipoAsignatura, { label: string; className: string }> =
|
||||||
{
|
{
|
||||||
obligatoria: {
|
OBLIGATORIA: {
|
||||||
label: 'Obligatoria',
|
label: 'Obligatoria',
|
||||||
className: 'bg-blue-100 text-blue-700',
|
className: 'bg-blue-100 text-blue-700',
|
||||||
},
|
},
|
||||||
optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
|
OPTATIVA: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
|
||||||
troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
|
TRONCAL: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
|
||||||
|
OTRA: { label: 'Otra', className: 'bg-slate-100 text-slate-700' },
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Mapeadores de API ---
|
// --- Mapeadores de API ---
|
||||||
const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
|
const mapAsignaturas = (
|
||||||
|
asigApi: Array<Tables<'asignaturas'>> = [],
|
||||||
|
): Array<Asignatura> => {
|
||||||
return asigApi.map((asig) => ({
|
return asigApi.map((asig) => ({
|
||||||
id: asig.id,
|
id: asig.id,
|
||||||
clave: asig.codigo,
|
clave: asig.codigo ?? '',
|
||||||
nombre: asig.nombre,
|
nombre: asig.nombre,
|
||||||
creditos: asig.creditos ?? 0,
|
creditos: asig.creditos,
|
||||||
ciclo: asig.numero_ciclo ?? null,
|
ciclo: asig.numero_ciclo ?? null,
|
||||||
lineaCurricularId: asig.linea_plan_id ?? null,
|
lineaCurricularId: asig.linea_plan_id ?? null,
|
||||||
tipo:
|
tipo: asig.tipo,
|
||||||
asig.tipo?.toLowerCase() === 'obligatoria' ? 'obligatoria' : 'optativa',
|
estado: asig.estado,
|
||||||
estado: 'borrador', // O el campo que venga de tu API
|
hd: asig.horas_academicas ?? 0,
|
||||||
hd: Math.floor((asig.horas_semana ?? 0) / 2),
|
hi: asig.horas_independientes ?? 0,
|
||||||
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
|
prerrequisitos: [],
|
||||||
prerrequisitos: Array.isArray(asig.prerrequisitos)
|
|
||||||
? asig.prerrequisitos
|
|
||||||
: [],
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,12 +82,38 @@ export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas')({
|
|||||||
function AsignaturasPage() {
|
function AsignaturasPage() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
// 1. Fetch de datos reales
|
// 1. Fetch de datos reales
|
||||||
const { data: asignaturasApi, isLoading: loadingAsig } =
|
const { data: asignaturasApi, isLoading: loadingAsig } =
|
||||||
usePlanAsignaturas(planId)
|
usePlanAsignaturas(planId)
|
||||||
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(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
|
// 2. Estados de filtrado
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [filterTipo, setFilterTipo] = useState<string>('all')
|
const [filterTipo, setFilterTipo] = useState<string>('all')
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ import {
|
|||||||
Eye,
|
Eye,
|
||||||
History,
|
History,
|
||||||
Calendar,
|
Calendar,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -57,15 +60,14 @@ const getEventConfig = (tipo: string, campo: string) => {
|
|||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
const { data: rawData, isLoading } = usePlanHistorial(planId)
|
const [page, setPage] = useState(0)
|
||||||
|
const pageSize = 4
|
||||||
|
const { data: response, isLoading } = usePlanHistorial(planId, page)
|
||||||
|
const rawData = response?.data ?? []
|
||||||
|
const totalRecords = response?.count ?? 0
|
||||||
|
const totalPages = Math.ceil(totalRecords / pageSize)
|
||||||
const [structure, setStructure] = useState<any>(null)
|
const [structure, setStructure] = useState<any>(null)
|
||||||
const { data } = usePlan(planId)
|
const { data } = usePlan(planId)
|
||||||
console.log('analizando estructura')
|
|
||||||
|
|
||||||
console.log(data?.estructuras_plan?.definicion?.properties)
|
|
||||||
// console.log(structure)
|
|
||||||
|
|
||||||
// ESTADOS PARA EL MODAL
|
|
||||||
const [selectedEvent, setSelectedEvent] = useState<any>(null)
|
const [selectedEvent, setSelectedEvent] = useState<any>(null)
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
|
||||||
@@ -229,6 +231,46 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
{historyEvents.length > 0 && (
|
||||||
|
<div className="mt-10 ml-20 flex items-center justify-between border-t pt-4">
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
Mostrando {rawData.length} de {totalRecords} cambios
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setPage((p) => Math.max(0, p - 1))
|
||||||
|
window.scrollTo(0, 0) // Opcional: volver arriba
|
||||||
|
}}
|
||||||
|
disabled={page === 0 || isLoading}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||||
|
Anterior
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<span className="text-sm font-medium text-slate-700">
|
||||||
|
Página {page + 1} de {totalPages || 1}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setPage((p) => p + 1)
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}}
|
||||||
|
// Ahora se deshabilita si llegamos a la última página real
|
||||||
|
disabled={page + 1 >= totalPages || isLoading}
|
||||||
|
>
|
||||||
|
Siguiente
|
||||||
|
<ChevronRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MODAL DE COMPARACIÓN CON SCROLL INTERNO */}
|
{/* MODAL DE COMPARACIÓN CON SCROLL INTERNO */}
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
import { createFileRoute, useRouterState } from '@tanstack/react-router'
|
import { createFileRoute, useRouterState } from '@tanstack/react-router'
|
||||||
import {
|
import {
|
||||||
Sparkles,
|
|
||||||
Send,
|
Send,
|
||||||
Target,
|
Target,
|
||||||
UserCheck,
|
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
FileText,
|
FileText,
|
||||||
GraduationCap,
|
GraduationCap,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Check,
|
Check,
|
||||||
X,
|
X,
|
||||||
|
MessageSquarePlus,
|
||||||
|
Archive,
|
||||||
|
RotateCcw,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useState, useEffect, useRef, useMemo } from 'react'
|
import { useState, useEffect, useRef, useMemo } from 'react'
|
||||||
|
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||||
|
|
||||||
|
import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Drawer, DrawerContent } from '@/components/ui/drawer'
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { usePlan } from '@/data/hooks/usePlans'
|
import { usePlan } from '@/data/hooks/usePlans'
|
||||||
@@ -53,22 +59,25 @@ interface SelectedField {
|
|||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatLabel = (key: string) => {
|
|
||||||
const result = key.replace(/_/g, ' ')
|
|
||||||
return result.charAt(0).toUpperCase() + result.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/$planId/_detalle/iaplan')({
|
export const Route = createFileRoute('/planes/$planId/_detalle/iaplan')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = Route.useParams()
|
const { planId } = Route.useParams()
|
||||||
// Usamos el ID dinámico del plan o el hardcoded según tu necesidad
|
|
||||||
const { data } = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f')
|
const { data } = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f')
|
||||||
const routerState = useRouterState()
|
const routerState = useRouterState()
|
||||||
|
const [openIA, setOpenIA] = useState(false)
|
||||||
|
// archivos
|
||||||
|
const [selectedArchivoIds, setSelectedArchivoIds] = useState<Array<string>>(
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const [selectedRepositorioIds, setSelectedRepositorioIds] = useState<
|
||||||
|
Array<string>
|
||||||
|
>([])
|
||||||
|
const [uploadedFiles, setUploadedFiles] = useState<Array<UploadedFile>>([])
|
||||||
|
|
||||||
// ESTADOS PRINCIPALES
|
|
||||||
const [messages, setMessages] = useState<Array<any>>([
|
const [messages, setMessages] = useState<Array<any>>([
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -84,6 +93,62 @@ function RouteComponent() {
|
|||||||
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
|
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
|
||||||
|
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [activeChatId, setActiveChatId] = useState('1')
|
||||||
|
const [chatHistory, setChatHistory] = useState([
|
||||||
|
{ id: '1', title: 'Chat inicial' },
|
||||||
|
])
|
||||||
|
const [showArchived, setShowArchived] = useState(false)
|
||||||
|
const [archivedHistory, setArchivedHistory] = useState<Array<any>>([])
|
||||||
|
const [allMessages, setAllMessages] = useState<{ [key: string]: Array<any> }>(
|
||||||
|
{
|
||||||
|
'1': [
|
||||||
|
{
|
||||||
|
id: 'm1',
|
||||||
|
role: 'assistant',
|
||||||
|
content: '¡Hola! Soy tu asistente de IA en este chat inicial.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const createNewChat = () => {
|
||||||
|
const newId = Date.now().toString()
|
||||||
|
const newChat = { id: newId, title: `Nuevo chat ${chatHistory.length + 1}` }
|
||||||
|
|
||||||
|
setChatHistory([newChat, ...chatHistory])
|
||||||
|
setAllMessages({
|
||||||
|
...allMessages,
|
||||||
|
[newId]: [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
role: 'assistant',
|
||||||
|
content: '¡Nuevo chat creado! ¿En qué puedo ayudarte?',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
setActiveChatId(newId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const archiveChat = (e: React.MouseEvent, id: string) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
const chatToArchive = chatHistory.find((chat) => chat.id === id)
|
||||||
|
if (chatToArchive) {
|
||||||
|
setArchivedHistory([chatToArchive, ...archivedHistory])
|
||||||
|
const newHistory = chatHistory.filter((chat) => chat.id !== id)
|
||||||
|
setChatHistory(newHistory)
|
||||||
|
if (activeChatId === id && newHistory.length > 0) {
|
||||||
|
setActiveChatId(newHistory[0].id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const unarchiveChat = (e: React.MouseEvent, id: string) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
const chatToRestore = archivedHistory.find((chat) => chat.id === id)
|
||||||
|
if (chatToRestore) {
|
||||||
|
setChatHistory([chatToRestore, ...chatHistory])
|
||||||
|
setArchivedHistory(archivedHistory.filter((chat) => chat.id !== id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Transformar datos de la API para el menú de selección
|
// 1. Transformar datos de la API para el menú de selección
|
||||||
const availableFields = useMemo(() => {
|
const availableFields = useMemo(() => {
|
||||||
@@ -107,12 +172,11 @@ function RouteComponent() {
|
|||||||
f.value === state.campo_edit.label || f.key === state.campo_edit.clave,
|
f.value === state.campo_edit.label || f.key === state.campo_edit.clave,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (field && !selectedFields.some((sf) => sf.key === field.key)) {
|
if (!field) return
|
||||||
setSelectedFields([field])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
setSelectedFields([field])
|
||||||
setInput((prev) =>
|
setInput((prev) =>
|
||||||
injectFieldsIntoInput(prev || 'Mejora este campo:', field ? [field] : []),
|
injectFieldsIntoInput(prev || 'Mejora este campo:', [field]),
|
||||||
)
|
)
|
||||||
}, [availableFields])
|
}, [availableFields])
|
||||||
|
|
||||||
@@ -131,21 +195,21 @@ function RouteComponent() {
|
|||||||
input: string,
|
input: string,
|
||||||
fields: Array<SelectedField>,
|
fields: Array<SelectedField>,
|
||||||
) => {
|
) => {
|
||||||
const baseText = input.replace(/\[[^\]]+]/g, '').trim()
|
// Quita cualquier bloque previo de campos
|
||||||
|
const cleaned = input.replace(/\n?\[Campos:[^\]]*]/g, '').trim()
|
||||||
|
|
||||||
const tags = fields.map((f) => `${f.label}`).join(' ')
|
if (fields.length === 0) return cleaned
|
||||||
|
|
||||||
return `${baseText} ${tags}`.trim()
|
const fieldLabels = fields.map((f) => f.label).join(', ')
|
||||||
|
|
||||||
|
return `${cleaned}\n[Campos: ${fieldLabels}]`
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleField = (field: SelectedField) => {
|
const toggleField = (field: SelectedField) => {
|
||||||
setSelectedFields((prev) => {
|
setSelectedFields((prev) => {
|
||||||
let nextFields
|
const nextFields = prev.find((f) => f.key === field.key)
|
||||||
|
? prev.filter((f) => f.key !== field.key)
|
||||||
if (prev.find((f) => f.key === field.key)) {
|
: [...prev, field]
|
||||||
nextFields = prev.filter((f) => f.key !== field.key)
|
|
||||||
} else {
|
|
||||||
nextFields = [...prev, field]
|
|
||||||
}
|
|
||||||
|
|
||||||
setInput((prevInput) =>
|
setInput((prevInput) =>
|
||||||
injectFieldsIntoInput(prevInput || 'Mejora este campo:', nextFields),
|
injectFieldsIntoInput(prevInput || 'Mejora este campo:', nextFields),
|
||||||
@@ -156,6 +220,7 @@ function RouteComponent() {
|
|||||||
|
|
||||||
setShowSuggestions(false)
|
setShowSuggestions(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildPrompt = (userInput: string) => {
|
const buildPrompt = (userInput: string) => {
|
||||||
if (selectedFields.length === 0) return userInput
|
if (selectedFields.length === 0) return userInput
|
||||||
|
|
||||||
@@ -164,11 +229,11 @@ function RouteComponent() {
|
|||||||
.join('\n')
|
.join('\n')
|
||||||
|
|
||||||
return `
|
return `
|
||||||
${userInput || 'Mejora los siguientes campos:'}
|
${userInput || 'Mejora los siguientes campos:'}
|
||||||
|
|
||||||
Campos a analizar:
|
Campos a analizar:
|
||||||
${fieldsText}
|
${fieldsText}
|
||||||
`.trim()
|
`.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSend = async (promptOverride?: string) => {
|
const handleSend = async (promptOverride?: string) => {
|
||||||
@@ -183,39 +248,157 @@ ${fieldsText}
|
|||||||
content: finalPrompt,
|
content: finalPrompt,
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessages((prev) => [...prev, userMsg])
|
// setMessages((prev) => [...prev, userMsg])
|
||||||
setInput('')
|
setInput('')
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
// setSelectedFields([])
|
||||||
|
setSelectedArchivoIds([])
|
||||||
|
setSelectedRepositorioIds([])
|
||||||
|
setUploadedFiles([])
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const mockText =
|
const suggestions = selectedFields.map((field) => ({
|
||||||
'Sugerencia generada basada en los campos seleccionados...'
|
key: field.key,
|
||||||
|
label: field.label,
|
||||||
|
newValue: field.value,
|
||||||
|
}))
|
||||||
|
|
||||||
setMessages((prev) => [
|
setMessages((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: `He analizado ${selectedFields
|
type: 'improvement-card',
|
||||||
.map((f) => f.label)
|
content:
|
||||||
.join(', ')}. Aquí tienes una propuesta:\n\n${mockText}`,
|
'He analizado los campos seleccionados. Aquí tienes mis sugerencias de mejora:',
|
||||||
|
suggestions: suggestions,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
setPendingSuggestion({ text: mockText })
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}, 1200)
|
}, 1200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... debajo de tus otros hooks
|
||||||
|
const totalReferencias = useMemo(() => {
|
||||||
|
return (
|
||||||
|
selectedArchivoIds.length +
|
||||||
|
selectedRepositorioIds.length +
|
||||||
|
uploadedFiles.length
|
||||||
|
)
|
||||||
|
}, [selectedArchivoIds, selectedRepositorioIds, uploadedFiles])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
|
<div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
|
||||||
|
{/* --- PANEL IZQUIERDO: HISTORIAL --- */}
|
||||||
|
<div className="flex w-64 flex-col border-r pr-4">
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between px-2">
|
||||||
|
<h2 className="text-xs font-bold tracking-wider text-slate-500 uppercase">
|
||||||
|
Chats
|
||||||
|
</h2>
|
||||||
|
{/* Botón de toggle archivados movido aquí arriba */}
|
||||||
|
<button
|
||||||
|
onClick={() => setShowArchived(!showArchived)}
|
||||||
|
className={`rounded-md p-1.5 transition-colors ${
|
||||||
|
showArchived
|
||||||
|
? 'bg-teal-50 text-teal-600'
|
||||||
|
: 'text-slate-400 hover:bg-slate-100'
|
||||||
|
}`}
|
||||||
|
title={showArchived ? 'Ver chats activos' : 'Ver archivados'}
|
||||||
|
>
|
||||||
|
<Archive size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={createNewChat}
|
||||||
|
variant="outline"
|
||||||
|
className="mb-4 w-full justify-start gap-2 border-slate-200 hover:bg-teal-50 hover:text-teal-700"
|
||||||
|
>
|
||||||
|
<MessageSquarePlus size={18} /> Nuevo chat
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{/* Lógica de renderizado condicional */}
|
||||||
|
{!showArchived ? (
|
||||||
|
// LISTA DE CHATS ACTIVOS
|
||||||
|
chatHistory.map((chat) => (
|
||||||
|
<div
|
||||||
|
key={chat.id}
|
||||||
|
onClick={() => setActiveChatId(chat.id)}
|
||||||
|
className={`group relative flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-3 text-sm transition-colors ${
|
||||||
|
activeChatId === chat.id
|
||||||
|
? 'bg-slate-100 font-medium text-slate-900'
|
||||||
|
: 'text-slate-600 hover:bg-slate-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText size={16} className="shrink-0 opacity-40" />
|
||||||
|
<span className="truncate pr-8">{chat.title}</span>
|
||||||
|
<button
|
||||||
|
onClick={(e) => archiveChat(e, chat.id)}
|
||||||
|
className="absolute right-2 p-1 opacity-0 transition-opacity group-hover:opacity-100 hover:text-amber-600"
|
||||||
|
title="Archivar"
|
||||||
|
>
|
||||||
|
<Archive size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
// LISTA DE CHATS ARCHIVADOS
|
||||||
|
<div className="animate-in fade-in slide-in-from-left-2">
|
||||||
|
<p className="mb-2 px-2 text-[10px] font-bold text-slate-400 uppercase">
|
||||||
|
Archivados
|
||||||
|
</p>
|
||||||
|
{archivedHistory.map((chat) => (
|
||||||
|
<div
|
||||||
|
key={chat.id}
|
||||||
|
className="group relative mb-1 flex w-full items-center gap-3 rounded-lg bg-slate-50/50 px-3 py-2 text-sm text-slate-400"
|
||||||
|
>
|
||||||
|
<Archive size={14} className="shrink-0 opacity-30" />
|
||||||
|
<span className="truncate pr-8">{chat.title}</span>
|
||||||
|
<button
|
||||||
|
onClick={(e) => unarchiveChat(e, chat.id)}
|
||||||
|
className="absolute right-2 p-1 opacity-0 group-hover:opacity-100 hover:text-teal-600"
|
||||||
|
title="Desarchivar"
|
||||||
|
>
|
||||||
|
<RotateCcw size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{archivedHistory.length === 0 && (
|
||||||
|
<div className="px-2 py-4 text-center">
|
||||||
|
<p className="text-xs text-slate-400 italic">
|
||||||
|
No hay archivados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
{/* PANEL DE CHAT PRINCIPAL */}
|
{/* PANEL DE CHAT PRINCIPAL */}
|
||||||
<div className="relative flex min-w-0 flex-[3] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm">
|
<div className="relative flex min-w-0 flex-[3] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm">
|
||||||
{/* NUEVO: Barra superior de campos seleccionados */}
|
{/* NUEVO: Barra superior de campos seleccionados */}
|
||||||
<div className="shrink-0 border-b bg-white p-3">
|
<div className="shrink-0 border-b bg-white p-3">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[10px] font-bold text-slate-400 uppercase">
|
<span className="text-[10px] font-bold text-slate-400 uppercase">
|
||||||
Mejorar con IA
|
Mejorar con IA
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setOpenIA(true)}
|
||||||
|
className="flex items-center gap-2 rounded-md bg-slate-100 px-3 py-1.5 text-xs font-medium transition hover:bg-slate-200"
|
||||||
|
>
|
||||||
|
<Archive size={14} className="text-slate-500" />
|
||||||
|
Referencias
|
||||||
|
{totalReferencias > 0 && (
|
||||||
|
<span className="flex h-4 min-w-[16px] items-center justify-center rounded-full bg-teal-600 px-1 text-[10px] text-white">
|
||||||
|
{totalReferencias}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -224,21 +407,6 @@ ${fieldsText}
|
|||||||
<ScrollArea ref={scrollRef} className="h-full w-full">
|
<ScrollArea ref={scrollRef} className="h-full w-full">
|
||||||
<div className="mx-auto max-w-3xl space-y-6 p-6">
|
<div className="mx-auto max-w-3xl space-y-6 p-6">
|
||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
<div
|
|
||||||
key={msg.id}
|
|
||||||
className={`flex ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'} items-start gap-3`}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
className={`h-8 w-8 shrink-0 border ${msg.role === 'assistant' ? 'bg-teal-50' : 'bg-slate-200'}`}
|
|
||||||
>
|
|
||||||
<AvatarFallback className="text-[10px]">
|
|
||||||
{msg.role === 'assistant' ? (
|
|
||||||
<Sparkles size={14} className="text-teal-600" />
|
|
||||||
) : (
|
|
||||||
<UserCheck size={14} />
|
|
||||||
)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div
|
<div
|
||||||
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
|
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
|
||||||
>
|
>
|
||||||
@@ -250,7 +418,19 @@ ${fieldsText}
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{msg.content}
|
{msg.content}
|
||||||
</div>
|
|
||||||
|
{msg.type === 'improvement-card' && (
|
||||||
|
<ImprovementCard
|
||||||
|
suggestions={msg.suggestions}
|
||||||
|
onApply={(key, val) => {
|
||||||
|
setSelectedFields((prev) =>
|
||||||
|
prev.filter((f) => f.key !== key),
|
||||||
|
)
|
||||||
|
console.log(`Aplicando ${val} al campo ${key}`)
|
||||||
|
// Aquí llamarías a tu función de actualización de datos real
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -285,11 +465,10 @@ ${fieldsText}
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
|
|
||||||
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
|
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
|
||||||
<div className="shrink-0 border-t bg-white p-4">
|
<div className="shrink-0 border-t bg-white p-4">
|
||||||
<div className="relative mx-auto max-w-4xl">
|
<div className="relative mx-auto max-w-4xl">
|
||||||
{/* MENÚ DE SUGERENCIAS FLOTANTE (Se mantiene igual) */}
|
{/* MENÚ DE SUGERENCIAS FLOTANTE */}
|
||||||
{showSuggestions && (
|
{showSuggestions && (
|
||||||
<div className="animate-in slide-in-from-bottom-2 absolute bottom-full z-50 mb-2 w-72 overflow-hidden rounded-xl border bg-white shadow-2xl">
|
<div className="animate-in slide-in-from-bottom-2 absolute bottom-full z-50 mb-2 w-72 overflow-hidden rounded-xl border bg-white shadow-2xl">
|
||||||
<div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold tracking-wider text-slate-500 uppercase">
|
<div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold tracking-wider text-slate-500 uppercase">
|
||||||
@@ -316,7 +495,7 @@ ${fieldsText}
|
|||||||
|
|
||||||
{/* CONTENEDOR DEL INPUT TRANSFORMADO */}
|
{/* CONTENEDOR DEL INPUT TRANSFORMADO */}
|
||||||
<div className="flex flex-col gap-2 rounded-xl border bg-slate-50 p-2 transition-all focus-within:bg-white focus-within:ring-1 focus-within:ring-teal-500">
|
<div className="flex flex-col gap-2 rounded-xl border bg-slate-50 p-2 transition-all focus-within:bg-white focus-within:ring-1 focus-within:ring-teal-500">
|
||||||
{/* 1. Visualización de campos dentro del input (Tags) */}
|
{/* 1. Visualización de campos dentro del input ) */}
|
||||||
{selectedFields.length > 0 && (
|
{selectedFields.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 px-2 pt-1">
|
<div className="flex flex-wrap gap-2 px-2 pt-1">
|
||||||
{selectedFields.map((field) => (
|
{selectedFields.map((field) => (
|
||||||
@@ -341,19 +520,13 @@ ${fieldsText}
|
|||||||
<Textarea
|
<Textarea
|
||||||
value={input}
|
value={input}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
handleSend()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={
|
placeholder={
|
||||||
selectedFields.length > 0
|
selectedFields.length > 0
|
||||||
? 'Escribe instrucciones adicionales...'
|
? 'Escribe instrucciones adicionales...'
|
||||||
: 'Escribe tu solicitud o ":" para campos...'
|
: 'Escribe tu solicitud o ":" para campos...'
|
||||||
}
|
}
|
||||||
className="max-h-[120px] min-h-[40px] flex-1 resize-none border-none bg-transparent py-2 text-sm shadow-none focus-visible:ring-0"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleSend()}
|
onClick={() => handleSend()}
|
||||||
disabled={
|
disabled={
|
||||||
@@ -369,8 +542,7 @@ ${fieldsText}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* PANEL LATERAL */}
|
||||||
{/* PANEL LATERAL (PRESETS) - SE MANTIENE COMO LO TENÍAS */}
|
|
||||||
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
|
<div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
|
||||||
<h4 className="flex items-center gap-2 text-left text-sm font-bold text-slate-800">
|
<h4 className="flex items-center gap-2 text-left text-sm font-bold text-slate-800">
|
||||||
<Lightbulb size={18} className="text-orange-500" /> Acciones rápidas
|
<Lightbulb size={18} className="text-orange-500" /> Acciones rápidas
|
||||||
@@ -392,6 +564,104 @@ ${fieldsText}
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Drawer open={openIA} onOpenChange={setOpenIA}>
|
||||||
|
<DrawerContent className="fixed inset-0 h-screen w-screen max-w-none rounded-none">
|
||||||
|
<div className="flex items-center justify-between border-b p-4">
|
||||||
|
<h2 className="text-sm font-semibold">Referencias para la IA</h2>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setOpenIA(false)}
|
||||||
|
className="text-muted-foreground hover:text-foreground text-sm"
|
||||||
|
>
|
||||||
|
Cerrar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-[calc(100vh-60px)] overflow-y-auto p-6">
|
||||||
|
<ReferenciasParaIA
|
||||||
|
selectedArchivoIds={selectedArchivoIds}
|
||||||
|
selectedRepositorioIds={selectedRepositorioIds}
|
||||||
|
uploadedFiles={uploadedFiles}
|
||||||
|
onToggleArchivo={(id, checked) => {
|
||||||
|
setSelectedArchivoIds((prev) =>
|
||||||
|
checked ? [...prev, id] : prev.filter((a) => a !== id),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onToggleRepositorio={(id, checked) => {
|
||||||
|
setSelectedRepositorioIds((prev) =>
|
||||||
|
checked ? [...prev, id] : prev.filter((r) => r !== id),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
onFilesChange={(files) => {
|
||||||
|
setUploadedFiles(files)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DrawerContent>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImprovementCard = ({
|
||||||
|
suggestions,
|
||||||
|
onApply,
|
||||||
|
}: {
|
||||||
|
suggestions: Array<any>
|
||||||
|
onApply: (key: string, value: string) => void
|
||||||
|
}) => {
|
||||||
|
// Estado para rastrear qué campos han sido aplicados
|
||||||
|
const [appliedFields, setAppliedFields] = useState<Array<string>>([])
|
||||||
|
|
||||||
|
const handleApply = (key: string, value: string) => {
|
||||||
|
onApply(key, value)
|
||||||
|
setAppliedFields((prev) => [...prev, key])
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-2 flex w-full flex-col gap-4">
|
||||||
|
{suggestions.map((sug) => {
|
||||||
|
const isApplied = appliedFields.includes(sug.key)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={sug.key}
|
||||||
|
className="rounded-2xl border border-slate-100 bg-white p-5 shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-bold text-slate-900">{sug.label}</h3>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleApply(sug.key, sug.newValue)}
|
||||||
|
disabled={isApplied}
|
||||||
|
className={`h-8 rounded-full px-4 text-xs transition-all ${
|
||||||
|
isApplied
|
||||||
|
? 'cursor-not-allowed bg-slate-100 text-slate-400'
|
||||||
|
: 'bg-[#00a189] text-white hover:bg-[#008f7a]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isApplied ? (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Check size={12} /> Aplicado
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
'Aplicar mejora'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`rounded-xl border p-3 text-sm transition-colors duration-300 ${
|
||||||
|
isApplied
|
||||||
|
? 'border-[#ccfbf1] bg-[#f0fdfa] text-slate-700'
|
||||||
|
: 'border-slate-200 bg-slate-50 text-slate-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{sug.newValue}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -504,6 +504,9 @@ function MapaCurricularPage() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
<Button className="bg-teal-700 text-white hover:bg-teal-800">
|
||||||
|
<Plus size={16} className="mr-2" /> Exportar{' '}
|
||||||
|
</Button>
|
||||||
{asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId).length >
|
{asignaturas.filter((m) => !m.ciclo || !m.lineaCurricularId).length >
|
||||||
0 && (
|
0 && (
|
||||||
<Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
|
<Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
|
||||||
|
|||||||
+4
-2
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Tables } from './supabase'
|
||||||
|
|
||||||
export type PlanStatus =
|
export type PlanStatus =
|
||||||
| 'borrador'
|
| 'borrador'
|
||||||
| 'revision'
|
| 'revision'
|
||||||
@@ -12,9 +14,9 @@ export type TipoPlan =
|
|||||||
| 'Doctorado'
|
| 'Doctorado'
|
||||||
| 'Especialidad'
|
| '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 {
|
export interface Facultad {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
+136
-11
@@ -7,11 +7,6 @@ export type Json =
|
|||||||
| Array<Json>
|
| Array<Json>
|
||||||
|
|
||||||
export type Database = {
|
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: {
|
graphql_public: {
|
||||||
Tables: {
|
Tables: {
|
||||||
[_ in never]: never
|
[_ in never]: never
|
||||||
@@ -93,11 +88,11 @@ export type Database = {
|
|||||||
asignatura_hash: string | null
|
asignatura_hash: string | null
|
||||||
codigo: string | null
|
codigo: string | null
|
||||||
contenido_tematico: Json
|
contenido_tematico: Json
|
||||||
conversation_id: string | null
|
|
||||||
creado_en: string
|
creado_en: string
|
||||||
creado_por: string | null
|
creado_por: string | null
|
||||||
creditos: number
|
creditos: number
|
||||||
datos: Json
|
datos: Json
|
||||||
|
estado: Database['public']['Enums']['estado_asignatura']
|
||||||
estructura_id: string | null
|
estructura_id: string | null
|
||||||
horas_academicas: number | null
|
horas_academicas: number | null
|
||||||
horas_independientes: number | null
|
horas_independientes: number | null
|
||||||
@@ -117,11 +112,11 @@ export type Database = {
|
|||||||
asignatura_hash?: string | null
|
asignatura_hash?: string | null
|
||||||
codigo?: string | null
|
codigo?: string | null
|
||||||
contenido_tematico?: Json
|
contenido_tematico?: Json
|
||||||
conversation_id?: string | null
|
|
||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
creditos: number
|
creditos: number
|
||||||
datos?: Json
|
datos?: Json
|
||||||
|
estado?: Database['public']['Enums']['estado_asignatura']
|
||||||
estructura_id?: string | null
|
estructura_id?: string | null
|
||||||
horas_academicas?: number | null
|
horas_academicas?: number | null
|
||||||
horas_independientes?: number | null
|
horas_independientes?: number | null
|
||||||
@@ -141,11 +136,11 @@ export type Database = {
|
|||||||
asignatura_hash?: string | null
|
asignatura_hash?: string | null
|
||||||
codigo?: string | null
|
codigo?: string | null
|
||||||
contenido_tematico?: Json
|
contenido_tematico?: Json
|
||||||
conversation_id?: string | null
|
|
||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
creditos?: number
|
creditos?: number
|
||||||
datos?: Json
|
datos?: Json
|
||||||
|
estado?: Database['public']['Enums']['estado_asignatura']
|
||||||
estructura_id?: string | null
|
estructura_id?: string | null
|
||||||
horas_academicas?: number | null
|
horas_academicas?: number | null
|
||||||
horas_independientes?: 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: {
|
estados_plan: {
|
||||||
Row: {
|
Row: {
|
||||||
clave: string
|
clave: string
|
||||||
@@ -677,7 +801,6 @@ export type Database = {
|
|||||||
actualizado_en: string
|
actualizado_en: string
|
||||||
actualizado_por: string | null
|
actualizado_por: string | null
|
||||||
carrera_id: string
|
carrera_id: string
|
||||||
conversation_id: string | null
|
|
||||||
creado_en: string
|
creado_en: string
|
||||||
creado_por: string | null
|
creado_por: string | null
|
||||||
datos: Json
|
datos: Json
|
||||||
@@ -698,7 +821,6 @@ export type Database = {
|
|||||||
actualizado_en?: string
|
actualizado_en?: string
|
||||||
actualizado_por?: string | null
|
actualizado_por?: string | null
|
||||||
carrera_id: string
|
carrera_id: string
|
||||||
conversation_id?: string | null
|
|
||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
datos?: Json
|
datos?: Json
|
||||||
@@ -719,7 +841,6 @@ export type Database = {
|
|||||||
actualizado_en?: string
|
actualizado_en?: string
|
||||||
actualizado_por?: string | null
|
actualizado_por?: string | null
|
||||||
carrera_id?: string
|
carrera_id?: string
|
||||||
conversation_id?: string | null
|
|
||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
datos?: Json
|
datos?: Json
|
||||||
@@ -1089,6 +1210,8 @@ export type Database = {
|
|||||||
unaccent_immutable: { Args: { '': string }; Returns: string }
|
unaccent_immutable: { Args: { '': string }; Returns: string }
|
||||||
}
|
}
|
||||||
Enums: {
|
Enums: {
|
||||||
|
estado_asignatura: 'borrador' | 'revisada' | 'aprobada' | 'generando'
|
||||||
|
estado_conversacion: 'ACTIVA' | 'ARCHIVANDO' | 'ARCHIVADA' | 'ERROR'
|
||||||
estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA'
|
estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA'
|
||||||
fuente_cambio: 'HUMANO' | 'IA'
|
fuente_cambio: 'HUMANO' | 'IA'
|
||||||
nivel_plan_estudio:
|
nivel_plan_estudio:
|
||||||
@@ -1261,6 +1384,8 @@ export const Constants = {
|
|||||||
},
|
},
|
||||||
public: {
|
public: {
|
||||||
Enums: {
|
Enums: {
|
||||||
|
estado_asignatura: ['borrador', 'revisada', 'aprobada', 'generando'],
|
||||||
|
estado_conversacion: ['ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR'],
|
||||||
estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'],
|
estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'],
|
||||||
fuente_cambio: ['HUMANO', 'IA'],
|
fuente_cambio: ['HUMANO', 'IA'],
|
||||||
nivel_plan_estudio: [
|
nivel_plan_estudio: [
|
||||||
|
|||||||
Reference in New Issue
Block a user