From d74807c84e5fb4eabba7773087bcf5ab7ddcea2d Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Mon, 9 Feb 2026 15:03:33 -0600 Subject: [PATCH 1/9] wip --- .../asignaturas/wizard/PasoBasicosForm.tsx | 356 ----------------- .../PasoBasicosForm/PasoBasicosForm.tsx | 362 ++++++++++++++++++ .../PasoBasicosForm/PasoSugerenciasForm.tsx | 237 ++++++++++++ .../asignaturas/wizard/PasoDetallesPanel.tsx | 8 +- .../wizard/PasoMetodoCardGroup.tsx | 87 ++++- .../wizard/WizardResponsiveHeader.tsx | 13 +- .../nueva/NuevaAsignaturaModalContainer.tsx | 24 +- .../nueva/hooks/useNuevaAsignaturaWizard.ts | 45 +-- src/features/asignaturas/nueva/types.ts | 12 +- 9 files changed, 738 insertions(+), 406 deletions(-) delete mode 100644 src/components/asignaturas/wizard/PasoBasicosForm.tsx create mode 100644 src/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm.tsx create mode 100644 src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx diff --git a/src/components/asignaturas/wizard/PasoBasicosForm.tsx b/src/components/asignaturas/wizard/PasoBasicosForm.tsx deleted file mode 100644 index 6c6c0d3..0000000 --- a/src/components/asignaturas/wizard/PasoBasicosForm.tsx +++ /dev/null @@ -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> -}) { - const { data: estructuras } = useSubjectEstructuras() - - const [creditosInput, setCreditosInput] = useState(() => { - 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 ( -
-
- - - 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" - /> -
- -
- - - 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" - /> -
- -
- - -
- -
- - { - 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) => { - 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" - /> -
- -
- - -

- Define los campos requeridos (ej. Objetivos, Temario, Evaluación). -

-
- -
- - { - if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) { - e.preventDefault() - } - }} - onChange={(e: React.ChangeEvent) => - 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" - /> -
- -
- - { - if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) { - e.preventDefault() - } - }} - onChange={(e: React.ChangeEvent) => - 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" - /> -
-
- ) -} diff --git a/src/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm.tsx b/src/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm.tsx new file mode 100644 index 0000000..cba5dfd --- /dev/null +++ b/src/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm.tsx @@ -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> +}) { + const { data: estructuras } = useSubjectEstructuras() + + const [creditosInput, setCreditosInput] = useState(() => { + 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 ( +
+
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+ +
+ + +
+ +
+ + { + 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) => { + 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" + /> +
+ +
+ + +

+ Define los campos requeridos (ej. Objetivos, Temario, Evaluación). +

+
+ +
+ + { + if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) { + e.preventDefault() + } + }} + onChange={(e: React.ChangeEvent) => + 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" + /> +
+ +
+ + { + if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) { + e.preventDefault() + } + }} + onChange={(e: React.ChangeEvent) => + 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" + /> +
+
+ ) + } + + return +} diff --git a/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx new file mode 100644 index 0000000..fcbc79f --- /dev/null +++ b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx @@ -0,0 +1,237 @@ +import { RefreshCw, Sparkles } from 'lucide-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 { cn } from '@/lib/utils' + +// Interfaces +interface Suggestion { + id: string + nombre: string + tipo: 'Obligatoria' | 'Optativa' + creditos: number + horasAcademicas: number + horasIndependientes: number + descripcion: string +} + +// Datos Mock basados en tu imagen +const MOCK_SUGGESTIONS: Array = [ + { + id: '1', + nombre: 'Propiedad Intelectual en Entornos Digitales', + tipo: 'Optativa', + creditos: 4, + horasAcademicas: 32, + horasIndependientes: 16, + descripcion: + 'Derechos de autor, patentes de software y marcas en el ecosistema digital.', + }, + { + id: '2', + nombre: 'Derecho Constitucional Digital', + tipo: 'Obligatoria', + creditos: 8, + horasAcademicas: 64, + horasIndependientes: 32, + descripcion: + 'Marco constitucional aplicado al entorno digital y derechos fundamentales en línea.', + }, + { + id: '3', + nombre: 'Gobernanza de Internet', + tipo: 'Optativa', + creditos: 4, + horasAcademicas: 32, + horasIndependientes: 16, + descripcion: + 'Políticas públicas, regulación internacional y gobernanza del ecosistema digital.', + }, + { + id: '4', + nombre: 'Protección de Datos Personales', + tipo: 'Obligatoria', + creditos: 6, + horasAcademicas: 48, + horasIndependientes: 24, + descripcion: + 'Regulación y cumplimiento de leyes de protección de datos (GDPR, LFPDPPP).', + }, + { + id: '5', + nombre: 'Inteligencia Artificial y Ética Jurídica', + tipo: 'Optativa', + creditos: 4, + horasAcademicas: 32, + horasIndependientes: 16, + descripcion: + 'Implicaciones legales y éticas del uso de IA en la práctica jurídica.', + }, + { + id: '6', + nombre: 'Ciberseguridad y Derecho Penal', + tipo: 'Obligatoria', + creditos: 6, + horasAcademicas: 48, + horasIndependientes: 24, + descripcion: + 'Delitos informáticos, evidencia digital y marco penal en el ciberespacio.', + }, +] +export default function PasoSugerenciasForm({ + wizard, + onChange, +}: { + wizard: NewSubjectWizardState + onChange: Dispatch> +}) { + const selectedIds = wizard.iaMultiple?.selectedIds ?? [] + const ciclo = wizard.iaMultiple?.ciclo ?? '' + const enfoque = wizard.iaMultiple?.enfoque ?? '' + + const setIaMultiple = ( + patch: Partial>, + ) => + onChange((w) => ({ + ...w, + iaMultiple: { + ciclo: w.iaMultiple?.ciclo ?? '', + enfoque: w.iaMultiple?.enfoque ?? '', + selectedIds: w.iaMultiple?.selectedIds ?? [], + ...patch, + }, + })) + + const toggleAsignatura = (id: string, checked: boolean) => { + const prev = selectedIds + const next = checked ? [...prev, id] : prev.filter((x) => x !== id) + setIaMultiple({ selectedIds: next }) + } + + return ( +
+ {/* --- BLOQUE SUPERIOR: PARÁMETROS --- */} +
+
+ + + Parámetros de sugerencia + +
+ +
+ {/* Input Ciclo */} +
+ + setIaMultiple({ ciclo: e.target.value })} + /> +
+ + {/* Input Enfoque */} +
+ + setIaMultiple({ enfoque: e.target.value })} + /> +
+ + {/* Botón Refrescar */} + +
+
+ + {/* --- HEADER LISTA --- */} +
+
+

+ Asignaturas sugeridas +

+

+ Basadas en el plan "Licenciatura en Derecho Digital" +

+
+
+ {selectedIds.length} seleccionadas +
+
+ + {/* --- LISTA DE ASIGNATURAS (CON EL ESTILO PEDIDO) --- */} +
+ {MOCK_SUGGESTIONS.map((asignatura) => { + const isSelected = selectedIds.includes(asignatura.id) + + return ( + + ) + })} +
+
+ ) +} diff --git a/src/components/asignaturas/wizard/PasoDetallesPanel.tsx b/src/components/asignaturas/wizard/PasoDetallesPanel.tsx index 5ce9dd1..b8d9ecd 100644 --- a/src/components/asignaturas/wizard/PasoDetallesPanel.tsx +++ b/src/components/asignaturas/wizard/PasoDetallesPanel.tsx @@ -29,11 +29,9 @@ import { export function PasoDetallesPanel({ wizard, onChange, - onGenerarIA: _onGenerarIA, }: { wizard: NewSubjectWizardState onChange: React.Dispatch> - onGenerarIA: () => void }) { if (wizard.tipoOrigen === 'MANUAL') { return ( @@ -49,7 +47,7 @@ export function PasoDetallesPanel({ ) } - if (wizard.tipoOrigen === 'IA') { + if (wizard.tipoOrigen === 'IA_SIMPLE') { return (
@@ -148,6 +146,10 @@ export function PasoDetallesPanel({ ) } + if (wizard.tipoOrigen === 'IA_MULTIPLE') { + return
Hola
+ } + if (wizard.tipoOrigen === 'CLONADO_INTERNO') { return (
diff --git a/src/components/asignaturas/wizard/PasoMetodoCardGroup.tsx b/src/components/asignaturas/wizard/PasoMetodoCardGroup.tsx index 73db993..46b3d82 100644 --- a/src/components/asignaturas/wizard/PasoMetodoCardGroup.tsx +++ b/src/components/asignaturas/wizard/PasoMetodoCardGroup.tsx @@ -77,12 +77,93 @@ export function PasoMetodoCardGroup({ Generar contenido automático. + {(wizard.tipoOrigen === 'IA' || + wizard.tipoOrigen === 'IA_SIMPLE' || + wizard.tipoOrigen === 'IA_MULTIPLE') && ( + +
{ + 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' + }`} + > + +
+ Una asignatura + + Crear una asignatura con control detallado de metadatos. + +
+
+ +
{ + 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' + }`} + > + +
+ Varias asignaturas + + Generar varias asignaturas a partir de sugerencias de la IA. + +
+
+
+ )} - onChange((w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'OTRO' })) + onChange( + (w): NewSubjectWizardState => ({ ...w, tipoOrigen: 'CLONADO' }), + ) } role="button" tabIndex={0} @@ -93,7 +174,7 @@ export function PasoMetodoCardGroup({ De otra asignatura o archivo Word. - {(wizard.tipoOrigen === 'OTRO' || + {(wizard.tipoOrigen === 'CLONADO' || wizard.tipoOrigen === 'CLONADO_INTERNO' || wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && ( diff --git a/src/components/wizard/WizardResponsiveHeader.tsx b/src/components/wizard/WizardResponsiveHeader.tsx index 7766e13..39f8427 100644 --- a/src/components/wizard/WizardResponsiveHeader.tsx +++ b/src/components/wizard/WizardResponsiveHeader.tsx @@ -4,9 +4,11 @@ import { StepWithTooltip } from '@/components/wizard/StepWithTooltip' export function WizardResponsiveHeader({ wizard, methods, + titleOverrides, }: { wizard: any methods: any + titleOverrides?: Record }) { const idx = wizard.utils.getIndex(methods.current.id) const totalSteps = wizard.steps.length @@ -14,6 +16,8 @@ export function WizardResponsiveHeader({ const hasNextStep = idx < totalSteps - 1 const nextStep = wizard.steps[currentIndex] + const resolveTitle = (step: any) => titleOverrides?.[step?.id] ?? step?.title + return ( <>
@@ -22,13 +26,13 @@ export function WizardResponsiveHeader({

{hasNextStep && nextStep ? (

- Siguiente: {nextStep.title} + Siguiente: {resolveTitle(nextStep)}

) : (

@@ -48,7 +52,10 @@ export function WizardResponsiveHeader({ className="whitespace-nowrap" > - + ))} diff --git a/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx b/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx index 9308fe2..8d48bd9 100644 --- a/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx +++ b/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx @@ -3,7 +3,7 @@ import * as Icons from 'lucide-react' import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard' -import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm' +import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm/PasoBasicosForm' import { PasoDetallesPanel } from '@/components/asignaturas/wizard/PasoDetallesPanel' import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup' import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard' @@ -55,10 +55,16 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) { canContinueDesdeMetodo, canContinueDesdeBasicos, canContinueDesdeDetalles, - simularGeneracionIA, - crearAsignatura, } = useNuevaAsignaturaWizard(planId) + const titleOverrides = + wizard.tipoOrigen === 'IA_MULTIPLE' + ? { + basicos: 'Sugerencias', + detalles: 'Estructura', + } + : undefined + const handleClose = () => { navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false }) } @@ -99,7 +105,11 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) { title="Nueva Asignatura" onClose={handleClose} headerSlot={ - + } footerSlot={ @@ -137,11 +147,7 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) { {idx === 2 && ( - + )} diff --git a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts index 4230248..4e281a4 100644 --- a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts +++ b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts @@ -1,6 +1,6 @@ import { useState } from 'react' -import type { AsignaturaPreview, NewSubjectWizardState } from '../types' +import type { NewSubjectWizardState } from '../types' export function useNuevaAsignaturaWizard(planId: string) { const [wizard, setWizard] = useState({ @@ -28,6 +28,11 @@ export function useNuevaAsignaturaWizard(planId: string) { repositoriosReferencia: [], archivosAdjuntos: [], }, + iaMultiple: { + ciclo: '', + enfoque: '', + selectedIds: ['1', '3', '6'], + }, resumen: {}, isLoading: false, errorMessage: null, @@ -35,16 +40,18 @@ export function useNuevaAsignaturaWizard(planId: string) { const canContinueDesdeMetodo = wizard.tipoOrigen === 'MANUAL' || - wizard.tipoOrigen === 'IA' || + wizard.tipoOrigen === 'IA_SIMPLE' || + wizard.tipoOrigen === 'IA_MULTIPLE' || wizard.tipoOrigen === 'CLONADO_INTERNO' || wizard.tipoOrigen === 'CLONADO_TRADICIONAL' const canContinueDesdeBasicos = - !!wizard.datosBasicos.nombre && - wizard.datosBasicos.tipo !== null && - wizard.datosBasicos.creditos !== null && - wizard.datosBasicos.creditos > 0 && - !!wizard.datosBasicos.estructuraId + (!!wizard.datosBasicos.nombre && + wizard.datosBasicos.tipo !== null && + wizard.datosBasicos.creditos !== null && + wizard.datosBasicos.creditos > 0 && + !!wizard.datosBasicos.estructuraId) || + true const canContinueDesdeDetalles = (() => { if (wizard.tipoOrigen === 'MANUAL') return true @@ -60,35 +67,11 @@ export function useNuevaAsignaturaWizard(planId: string) { return false })() - const simularGeneracionIA = async () => { - setWizard((w) => ({ ...w, isLoading: true })) - await new Promise((r) => setTimeout(r, 1500)) - setWizard((w) => ({ - ...w, - isLoading: false, - resumen: { - previewAsignatura: { - nombre: w.datosBasicos.nombre, - objetivo: - 'Aplicar los fundamentos teóricos para la resolución de problemas...', - unidades: 5, - bibliografiaCount: 3, - } as AsignaturaPreview, - }, - })) - } - - const crearAsignatura = async () => { - await new Promise((r) => setTimeout(r, 1000)) - } - return { wizard, setWizard, canContinueDesdeMetodo, canContinueDesdeBasicos, canContinueDesdeDetalles, - simularGeneracionIA, - crearAsignatura, } } diff --git a/src/features/asignaturas/nueva/types.ts b/src/features/asignaturas/nueva/types.ts index d57a9ac..064b308 100644 --- a/src/features/asignaturas/nueva/types.ts +++ b/src/features/asignaturas/nueva/types.ts @@ -15,7 +15,12 @@ export type AsignaturaPreview = { export type NewSubjectWizardState = { step: 1 | 2 | 3 | 4 plan_estudio_id: Asignatura['plan_estudio_id'] - tipoOrigen: Asignatura['tipo_origen'] | null + tipoOrigen: + | Asignatura['tipo_origen'] + | 'CLONADO' + | 'IA_SIMPLE' + | 'IA_MULTIPLE' + | null datosBasicos: { nombre: Asignatura['nombre'] codigo?: Asignatura['codigo'] @@ -42,6 +47,11 @@ export type NewSubjectWizardState = { repositoriosReferencia?: Array archivosAdjuntos?: Array } + iaMultiple?: { + ciclo: string + enfoque: string + selectedIds: Array + } resumen: { previewAsignatura?: AsignaturaPreview } -- 2.49.1 From 675c76db74fec463b27f033e1c8e733231294e12 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Tue, 10 Feb 2026 13:45:21 -0600 Subject: [PATCH 2/9] wip --- .../PasoBasicosForm/PasoSugerenciasForm.tsx | 130 +++++------------- src/data/api/subjects.api.ts | 10 ++ src/data/query/keys.ts | 1 + .../nueva/hooks/useNuevaAsignaturaWizard.ts | 5 +- src/features/asignaturas/nueva/types.ts | 24 +++- 5 files changed, 70 insertions(+), 100 deletions(-) diff --git a/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx index fcbc79f..c857844 100644 --- a/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx +++ b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx @@ -1,88 +1,19 @@ import { RefreshCw, Sparkles } from 'lucide-react' +import { useState } from 'react' -import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types' +import type { + AsignaturaSugerida, + 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 { usePlan } from '@/data' import { cn } from '@/lib/utils' -// Interfaces -interface Suggestion { - id: string - nombre: string - tipo: 'Obligatoria' | 'Optativa' - creditos: number - horasAcademicas: number - horasIndependientes: number - descripcion: string -} - -// Datos Mock basados en tu imagen -const MOCK_SUGGESTIONS: Array = [ - { - id: '1', - nombre: 'Propiedad Intelectual en Entornos Digitales', - tipo: 'Optativa', - creditos: 4, - horasAcademicas: 32, - horasIndependientes: 16, - descripcion: - 'Derechos de autor, patentes de software y marcas en el ecosistema digital.', - }, - { - id: '2', - nombre: 'Derecho Constitucional Digital', - tipo: 'Obligatoria', - creditos: 8, - horasAcademicas: 64, - horasIndependientes: 32, - descripcion: - 'Marco constitucional aplicado al entorno digital y derechos fundamentales en línea.', - }, - { - id: '3', - nombre: 'Gobernanza de Internet', - tipo: 'Optativa', - creditos: 4, - horasAcademicas: 32, - horasIndependientes: 16, - descripcion: - 'Políticas públicas, regulación internacional y gobernanza del ecosistema digital.', - }, - { - id: '4', - nombre: 'Protección de Datos Personales', - tipo: 'Obligatoria', - creditos: 6, - horasAcademicas: 48, - horasIndependientes: 24, - descripcion: - 'Regulación y cumplimiento de leyes de protección de datos (GDPR, LFPDPPP).', - }, - { - id: '5', - nombre: 'Inteligencia Artificial y Ética Jurídica', - tipo: 'Optativa', - creditos: 4, - horasAcademicas: 32, - horasIndependientes: 16, - descripcion: - 'Implicaciones legales y éticas del uso de IA en la práctica jurídica.', - }, - { - id: '6', - nombre: 'Ciberseguridad y Derecho Penal', - tipo: 'Obligatoria', - creditos: 6, - horasAcademicas: 48, - horasIndependientes: 24, - descripcion: - 'Delitos informáticos, evidencia digital y marco penal en el ciberespacio.', - }, -] export default function PasoSugerenciasForm({ wizard, onChange, @@ -90,22 +21,28 @@ export default function PasoSugerenciasForm({ wizard: NewSubjectWizardState onChange: Dispatch> }) { - const selectedIds = wizard.iaMultiple?.selectedIds ?? [] + const selectedIds = wizard.sugerencias + .filter((s) => s.selected) + .map((s) => s.id) const ciclo = wizard.iaMultiple?.ciclo ?? '' const enfoque = wizard.iaMultiple?.enfoque ?? '' + const [suggestions, setSuggestions] = useState>([]) const setIaMultiple = ( patch: Partial>, ) => - onChange((w) => ({ - ...w, - iaMultiple: { - ciclo: w.iaMultiple?.ciclo ?? '', - enfoque: w.iaMultiple?.enfoque ?? '', - selectedIds: w.iaMultiple?.selectedIds ?? [], - ...patch, - }, - })) + onChange( + (w): NewSubjectWizardState => ({ + ...w, + iaMultiple: { + ciclo: w.iaMultiple?.ciclo ?? null, + enfoque: w.iaMultiple?.enfoque ?? '', + ...patch, + }, + }), + ) + + const { data: plan } = usePlan(wizard.plan_estudio_id) const toggleAsignatura = (id: string, checked: boolean) => { const prev = selectedIds @@ -133,7 +70,8 @@ export default function PasoSugerenciasForm({ setIaMultiple({ ciclo: e.target.value })} + type="number" + onChange={(e) => setIaMultiple({ ciclo: Number(e.target.value) })} />

@@ -152,7 +90,7 @@ export default function PasoSugerenciasForm({ {/* Botón Refrescar */}
@@ -164,7 +102,8 @@ export default function PasoSugerenciasForm({ Asignaturas sugeridas

- Basadas en el plan "Licenciatura en Derecho Digital" + Basadas en el plan{' '} + {plan ? `${plan.nivel} en ${plan.nombre}` : '...'}

@@ -174,7 +113,7 @@ export default function PasoSugerenciasForm({ {/* --- LISTA DE ASIGNATURAS (CON EL ESTILO PEDIDO) --- */}
- {MOCK_SUGGESTIONS.map((asignatura) => { + {suggestions.map((asignatura) => { const isSelected = selectedIds.includes(asignatura.id) return ( @@ -203,29 +142,30 @@ export default function PasoSugerenciasForm({
- {asignatura.nombre} + {asignatura.data.nombre} {/* Badges de Tipo */} - {asignatura.tipo} + {asignatura.data.tipo} - {asignatura.creditos} cred. · {asignatura.horasAcademicas}h - acad. · {asignatura.horasIndependientes}h indep. + {asignatura.data.creditos} cred. ·{' '} + {asignatura.data.horasAcademicas}h acad. ·{' '} + {asignatura.data.horasIndependientes}h indep.

- {asignatura.descripcion} + {asignatura.data.descripcion}

diff --git a/src/data/api/subjects.api.ts b/src/data/api/subjects.api.ts index acfe531..7e99716 100644 --- a/src/data/api/subjects.api.ts +++ b/src/data/api/subjects.api.ts @@ -15,6 +15,7 @@ import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/ import type { Database } from '@/types/supabase' const EDGE = { + generate_subject_suggestions: 'generate-subject-suggestions', subjects_create_manual: 'subjects_create_manual', ai_generate_subject: 'ai-generate-subject', subjects_persist_from_ai: 'subjects_persist_from_ai', @@ -133,6 +134,15 @@ export type AIGenerateSubjectInput = { } } +export async function generate_subject_suggestions(): Promise< + Array<{ [key: string]: any }> +> { + return invokeEdge>( + EDGE.generate_subject_suggestions, + {}, + ) +} + export async function ai_generate_subject( input: AIGenerateSubjectInput, ): Promise { diff --git a/src/data/query/keys.ts b/src/data/query/keys.ts index ec61bbd..1ac5890 100644 --- a/src/data/query/keys.ts +++ b/src/data/query/keys.ts @@ -19,6 +19,7 @@ export const qk = { planHistorial: (planId: string) => ['planes', planId, 'historial'] as const, planDocumento: (planId: string) => ['planes', planId, 'documento'] as const, + sugerenciasAsignaturas: () => ['asignaturas', 'sugerencias'] as const, asignatura: (asignaturaId: string) => ['asignaturas', 'detail', asignaturaId] as const, asignaturaBibliografia: (asignaturaId: string) => diff --git a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts index 4e281a4..6085d4d 100644 --- a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts +++ b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts @@ -16,6 +16,7 @@ export function useNuevaAsignaturaWizard(planId: string) { horasIndependientes: null, estructuraId: '', }, + sugerencias: [], clonInterno: {}, clonTradicional: { archivoWordAsignaturaId: null, @@ -29,9 +30,9 @@ export function useNuevaAsignaturaWizard(planId: string) { archivosAdjuntos: [], }, iaMultiple: { - ciclo: '', + ciclo: null, enfoque: '', - selectedIds: ['1', '3', '6'], + selectedIds: [], }, resumen: {}, isLoading: false, diff --git a/src/features/asignaturas/nueva/types.ts b/src/features/asignaturas/nueva/types.ts index 064b308..48b4be9 100644 --- a/src/features/asignaturas/nueva/types.ts +++ b/src/features/asignaturas/nueva/types.ts @@ -2,7 +2,6 @@ import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/ import type { Asignatura } from '@/data' export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO' -export type SubModoClonado = 'INTERNO' | 'TRADICIONAL' export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO' export type AsignaturaPreview = { @@ -12,6 +11,24 @@ export type AsignaturaPreview = { bibliografiaCount: number } +export type DataAsignaturaSugerida = { + nombre: Asignatura['nombre'] + codigo?: Asignatura['codigo'] + tipo: Asignatura['tipo'] | null + creditos: Asignatura['creditos'] | null + horasAcademicas?: number | null + horasIndependientes?: number | null + estructuraId: Asignatura['estructura_id'] | null + descripcion?: string +} + +export type AsignaturaSugerida = { + id: string + selected: boolean + source: 'IA' | 'MANUAL' | 'CLON' + data: DataAsignaturaSugerida +} + export type NewSubjectWizardState = { step: 1 | 2 | 3 | 4 plan_estudio_id: Asignatura['plan_estudio_id'] @@ -30,6 +47,7 @@ export type NewSubjectWizardState = { horasIndependientes?: Asignatura['horas_independientes'] | null estructuraId: Asignatura['estructura_id'] | null } + sugerencias: Array clonInterno?: { facultadId?: string carreraId?: string @@ -48,9 +66,9 @@ export type NewSubjectWizardState = { archivosAdjuntos?: Array } iaMultiple?: { - ciclo: string + ciclo: number | null enfoque: string - selectedIds: Array + selectedIds?: Array } resumen: { previewAsignatura?: AsignaturaPreview -- 2.49.1 From 89f264bf5def51d3488387bc3c14b666e017aef5 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Tue, 10 Feb 2026 15:58:51 -0600 Subject: [PATCH 3/9] Primera version funcional de sugerencias --- .../PasoBasicosForm/PasoSugerenciasForm.tsx | 113 +++++++++++++----- src/data/api/subjects.api.ts | 30 ++++- .../nueva/hooks/useNuevaAsignaturaWizard.ts | 1 - src/features/asignaturas/nueva/types.ts | 6 +- 4 files changed, 114 insertions(+), 36 deletions(-) diff --git a/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx index c857844..2c0cdba 100644 --- a/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx +++ b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx @@ -1,17 +1,13 @@ import { RefreshCw, Sparkles } from 'lucide-react' -import { useState } from 'react' -import type { - AsignaturaSugerida, - NewSubjectWizardState, -} from '@/features/asignaturas/nueva/types' +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 { usePlan } from '@/data' +import { generate_subject_suggestions, usePlan } from '@/data' import { cn } from '@/lib/utils' export default function PasoSugerenciasForm({ @@ -26,7 +22,6 @@ export default function PasoSugerenciasForm({ .map((s) => s.id) const ciclo = wizard.iaMultiple?.ciclo ?? '' const enfoque = wizard.iaMultiple?.enfoque ?? '' - const [suggestions, setSuggestions] = useState>([]) const setIaMultiple = ( patch: Partial>, @@ -45,9 +40,53 @@ export default function PasoSugerenciasForm({ const { data: plan } = usePlan(wizard.plan_estudio_id) const toggleAsignatura = (id: string, checked: boolean) => { - const prev = selectedIds - const next = checked ? [...prev, id] : prev.filter((x) => x !== id) - setIaMultiple({ selectedIds: next }) + onChange((w) => ({ + ...w, + sugerencias: w.sugerencias.map((s) => + s.id === id ? { ...s, selected: checked } : s, + ), + })) + } + + const onMasSugerencias = async () => { + const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected) + + onChange((w) => ({ + ...w, + isLoading: true, + errorMessage: null, + sugerencias: sugerenciasConservadas, + })) + + try { + const nuevasSugerencias = await generate_subject_suggestions() + const merged = [...nuevasSugerencias, ...sugerenciasConservadas] + + const seen = new Set() + const deduped = merged.filter((s) => { + if (seen.has(s.id)) return false + seen.add(s.id) + return true + }) + + onChange( + (w): NewSubjectWizardState => ({ + ...w, + isLoading: false, + sugerencias: deduped, + }), + ) + } catch (err) { + const message = + err instanceof Error ? err.message : 'Error generando sugerencias.' + onChange( + (w): NewSubjectWizardState => ({ + ...w, + isLoading: false, + errorMessage: message, + }), + ) + } } return ( @@ -71,7 +110,20 @@ export default function PasoSugerenciasForm({ placeholder="Ej. 3" value={ciclo} type="number" - onChange={(e) => setIaMultiple({ ciclo: Number(e.target.value) })} + min={1} + max={999} + onChange={(e) => { + const raw = e.target.value + if (raw === '') { + setIaMultiple({ ciclo: null }) + 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, 999) + setIaMultiple({ ciclo: capped }) + }} />
@@ -88,11 +140,22 @@ export default function PasoSugerenciasForm({
{/* Botón Refrescar */} -
+ +

+ Al generar más sugerencias, solo se conservarán las asignaturas que + hayas seleccionado. +

{/* --- HEADER LISTA --- */} @@ -111,18 +174,16 @@ export default function PasoSugerenciasForm({ - {/* --- LISTA DE ASIGNATURAS (CON EL ESTILO PEDIDO) --- */} + {/* --- LISTA DE ASIGNATURAS --- */}
- {suggestions.map((asignatura) => { - const isSelected = selectedIds.includes(asignatura.id) + {wizard.sugerencias.map((asignatura) => { + const isSelected = asignatura.selected return ( diff --git a/src/data/api/subjects.api.ts b/src/data/api/subjects.api.ts index 7e99716..0d06769 100644 --- a/src/data/api/subjects.api.ts +++ b/src/data/api/subjects.api.ts @@ -12,6 +12,10 @@ import type { UUID, } from '../types/domain' import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone' +import type { + AsignaturaSugerida, + DataAsignaturaSugerida, +} from '@/features/asignaturas/nueva/types' import type { Database } from '@/types/supabase' const EDGE = { @@ -37,7 +41,7 @@ export async function subjects_get(subjectId: UUID): Promise { .from('asignaturas') .select( ` - id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, + id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,conversation_id,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, planes_estudio( id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono)) @@ -135,12 +139,30 @@ export type AIGenerateSubjectInput = { } export async function generate_subject_suggestions(): Promise< - Array<{ [key: string]: any }> + Array > { - return invokeEdge>( + const raw = await invokeEdge>( EDGE.generate_subject_suggestions, {}, ) + + const arr = 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, + estructuraId: null, + descripcion: s.descripcion, + }), + ) + + return arr } export async function ai_generate_subject( @@ -156,7 +178,7 @@ export async function ai_generate_subject( archivosAdjuntos: undefined, // los manejamos aparte }), ) - input.iaConfig?.archivosAdjuntos?.forEach((file, index) => { + input.iaConfig?.archivosAdjuntos?.forEach((file) => { edgeFunctionBody.append(`archivosAdjuntos`, file.file) }) return invokeEdge( diff --git a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts index 6085d4d..45e9546 100644 --- a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts +++ b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts @@ -32,7 +32,6 @@ export function useNuevaAsignaturaWizard(planId: string) { iaMultiple: { ciclo: null, enfoque: '', - selectedIds: [], }, resumen: {}, isLoading: false, diff --git a/src/features/asignaturas/nueva/types.ts b/src/features/asignaturas/nueva/types.ts index 48b4be9..56d9943 100644 --- a/src/features/asignaturas/nueva/types.ts +++ b/src/features/asignaturas/nueva/types.ts @@ -18,7 +18,6 @@ export type DataAsignaturaSugerida = { creditos: Asignatura['creditos'] | null horasAcademicas?: number | null horasIndependientes?: number | null - estructuraId: Asignatura['estructura_id'] | null descripcion?: string } @@ -26,8 +25,8 @@ export type AsignaturaSugerida = { id: string selected: boolean source: 'IA' | 'MANUAL' | 'CLON' - data: DataAsignaturaSugerida -} + estructuraId: Asignatura['estructura_id'] | null +} & DataAsignaturaSugerida export type NewSubjectWizardState = { step: 1 | 2 | 3 | 4 @@ -68,7 +67,6 @@ export type NewSubjectWizardState = { iaMultiple?: { ciclo: number | null enfoque: string - selectedIds?: Array } resumen: { previewAsignatura?: AsignaturaPreview -- 2.49.1 From ded54c18dd0e08ab05a00bbd9e92b0c65ab300a1 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Wed, 11 Feb 2026 13:14:54 -0600 Subject: [PATCH 4/9] Se mandan generar sugerencias de asignaturas junto con el id del plan, el enfoque que se le quiere dar, la cantidad de sugerencias, y las sugerencias conservadas --- .../PasoBasicosForm/PasoSugerenciasForm.tsx | 92 +++++++++++++++---- .../asignaturas/wizard/WizardControls.tsx | 2 +- src/data/api/subjects.api.ts | 23 +++-- .../nueva/hooks/useNuevaAsignaturaWizard.ts | 1 + src/features/asignaturas/nueva/types.ts | 3 +- 5 files changed, 91 insertions(+), 30 deletions(-) diff --git a/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx index 2c0cdba..d703a6d 100644 --- a/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx +++ b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx @@ -17,11 +17,9 @@ export default function PasoSugerenciasForm({ wizard: NewSubjectWizardState onChange: Dispatch> }) { - const selectedIds = wizard.sugerencias - .filter((s) => s.selected) - .map((s) => s.id) const ciclo = wizard.iaMultiple?.ciclo ?? '' const enfoque = wizard.iaMultiple?.enfoque ?? '' + const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 10 const setIaMultiple = ( patch: Partial>, @@ -32,6 +30,7 @@ export default function PasoSugerenciasForm({ iaMultiple: { ciclo: w.iaMultiple?.ciclo ?? null, enfoque: w.iaMultiple?.enfoque ?? '', + cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10, ...patch, }, }), @@ -48,7 +47,7 @@ export default function PasoSugerenciasForm({ })) } - const onMasSugerencias = async () => { + const onGenerarSugerencias = async () => { const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected) onChange((w) => ({ @@ -59,21 +58,44 @@ export default function PasoSugerenciasForm({ })) try { - const nuevasSugerencias = await generate_subject_suggestions() - const merged = [...nuevasSugerencias, ...sugerenciasConservadas] + const numeroCiclo = wizard.iaMultiple?.ciclo + if (!numeroCiclo || !Number.isFinite(numeroCiclo) || numeroCiclo <= 0) { + onChange((w) => ({ + ...w, + isLoading: false, + errorMessage: 'Ingresa un número de ciclo válido.', + })) + return + } - const seen = new Set() - const deduped = merged.filter((s) => { - if (seen.has(s.id)) return false - seen.add(s.id) - return true + const cantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10 + if (!Number.isFinite(cantidad) || cantidad <= 0 || cantidad > 50) { + onChange((w) => ({ + ...w, + isLoading: false, + errorMessage: 'La cantidad de sugerencias debe ser entre 1 y 50.', + })) + return + } + + const enfoqueTrim = wizard.iaMultiple?.enfoque.trim() ?? '' + + const nuevasSugerencias = await generate_subject_suggestions({ + plan_estudio_id: wizard.plan_estudio_id, + numero_de_ciclo: numeroCiclo, + enfoque: enfoqueTrim ? enfoqueTrim : undefined, + cantidad_de_sugerencias: cantidad, + sugerencias_conservadas: sugerenciasConservadas.map((s) => ({ + nombre: s.nombre, + descripcion: s.descripcion, + })), }) onChange( (w): NewSubjectWizardState => ({ ...w, isLoading: false, - sugerencias: deduped, + sugerencias: [...nuevasSugerencias, ...sugerenciasConservadas], }), ) } catch (err) { @@ -90,7 +112,7 @@ export default function PasoSugerenciasForm({ } return ( -
+ <> {/* --- BLOQUE SUPERIOR: PARÁMETROS --- */}
@@ -138,17 +160,47 @@ export default function PasoSugerenciasForm({ onChange={(e) => setIaMultiple({ enfoque: e.target.value })} />
+
+ +
+
+ + { + 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, 50) + setIaMultiple({ cantidadDeSugerencias: capped }) + }} + /> +
- {/* Botón Refrescar */}
@@ -170,12 +222,12 @@ export default function PasoSugerenciasForm({

- {selectedIds.length} seleccionadas + {wizard.sugerencias.filter((s) => s.selected).length} seleccionadas
{/* --- LISTA DE ASIGNATURAS --- */} -
+
{wizard.sugerencias.map((asignatura) => { const isSelected = asignatura.selected @@ -223,7 +275,7 @@ export default function PasoSugerenciasForm({
-

+

{asignatura.descripcion}

@@ -231,6 +283,6 @@ export default function PasoSugerenciasForm({ ) })} - + ) } diff --git a/src/components/asignaturas/wizard/WizardControls.tsx b/src/components/asignaturas/wizard/WizardControls.tsx index fd26612..9188df1 100644 --- a/src/components/asignaturas/wizard/WizardControls.tsx +++ b/src/components/asignaturas/wizard/WizardControls.tsx @@ -94,7 +94,7 @@ export function WizardControls({ Anterior -
+
{(errorMessage ?? wizard.errorMessage) && ( {errorMessage ?? wizard.errorMessage} diff --git a/src/data/api/subjects.api.ts b/src/data/api/subjects.api.ts index 0d06769..590f170 100644 --- a/src/data/api/subjects.api.ts +++ b/src/data/api/subjects.api.ts @@ -138,31 +138,38 @@ export type AIGenerateSubjectInput = { } } -export async function generate_subject_suggestions(): Promise< - Array -> { +export type GenerateSubjectSuggestionsInput = { + plan_estudio_id: UUID + numero_de_ciclo: number + enfoque?: string + cantidad_de_sugerencias: number + sugerencias_conservadas: Array<{ nombre: string; descripcion: string }> +} + +export async function generate_subject_suggestions( + input: GenerateSubjectSuggestionsInput, +): Promise> { const raw = await invokeEdge>( EDGE.generate_subject_suggestions, - {}, + input, + { headers: { 'Content-Type': 'application/json' } }, ) - const arr = raw.map( + return raw.map( (s): AsignaturaSugerida => ({ id: crypto.randomUUID(), selected: false, source: 'IA', + estructuraId: null, nombre: s.nombre, codigo: s.codigo, tipo: s.tipo ?? null, creditos: s.creditos ?? null, horasAcademicas: s.horasAcademicas ?? null, horasIndependientes: s.horasIndependientes ?? null, - estructuraId: null, descripcion: s.descripcion, }), ) - - return arr } export async function ai_generate_subject( diff --git a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts index 45e9546..d920134 100644 --- a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts +++ b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts @@ -32,6 +32,7 @@ export function useNuevaAsignaturaWizard(planId: string) { iaMultiple: { ciclo: null, enfoque: '', + cantidadDeSugerencias: 10, }, resumen: {}, isLoading: false, diff --git a/src/features/asignaturas/nueva/types.ts b/src/features/asignaturas/nueva/types.ts index 56d9943..d529f13 100644 --- a/src/features/asignaturas/nueva/types.ts +++ b/src/features/asignaturas/nueva/types.ts @@ -18,7 +18,7 @@ export type DataAsignaturaSugerida = { creditos: Asignatura['creditos'] | null horasAcademicas?: number | null horasIndependientes?: number | null - descripcion?: string + descripcion: string } export type AsignaturaSugerida = { @@ -67,6 +67,7 @@ export type NewSubjectWizardState = { iaMultiple?: { ciclo: number | null enfoque: string + cantidadDeSugerencias: number } resumen: { previewAsignatura?: AsignaturaPreview -- 2.49.1 From 07d08e1b57299fb8fc594595acd23a4bbbd5b8e3 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Wed, 11 Feb 2026 16:03:52 -0600 Subject: [PATCH 5/9] Add AI progress loader and enhance suggestion generation logic - Introduced AIProgressLoader component to display loading progress and messages during suggestion generation. - Updated PasoSugerenciasForm to manage loading state and display tooltip for preserved suggestions. - Adjusted suggestion limits and removed unused ciclo input from state. --- .../PasoBasicosForm/PasoSugerenciasForm.tsx | 148 +++++++++-------- src/data/api/subjects.api.ts | 1 - .../asignaturas/nueva/AIProgressLoader.tsx | 151 ++++++++++++++++++ .../nueva/hooks/useNuevaAsignaturaWizard.ts | 2 +- src/features/asignaturas/nueva/types.ts | 2 +- 5 files changed, 237 insertions(+), 67 deletions(-) create mode 100644 src/features/asignaturas/nueva/AIProgressLoader.tsx diff --git a/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx index d703a6d..e512e4a 100644 --- a/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx +++ b/src/components/asignaturas/wizard/PasoBasicosForm/PasoSugerenciasForm.tsx @@ -1,4 +1,5 @@ -import { RefreshCw, Sparkles } from 'lucide-react' +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' @@ -7,7 +8,14 @@ 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({ @@ -17,9 +25,11 @@ export default function PasoSugerenciasForm({ wizard: NewSubjectWizardState onChange: Dispatch> }) { - const ciclo = wizard.iaMultiple?.ciclo ?? '' 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>, @@ -28,9 +38,9 @@ export default function PasoSugerenciasForm({ (w): NewSubjectWizardState => ({ ...w, iaMultiple: { - ciclo: w.iaMultiple?.ciclo ?? null, enfoque: w.iaMultiple?.enfoque ?? '', cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10, + isLoading: w.iaMultiple?.isLoading ?? false, ...patch, }, }), @@ -48,32 +58,31 @@ export default function PasoSugerenciasForm({ } const onGenerarSugerencias = async () => { + const hadNoSugerenciasBefore = wizard.sugerencias.length === 0 const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected) onChange((w) => ({ ...w, - isLoading: true, errorMessage: null, sugerencias: sugerenciasConservadas, + iaMultiple: { + enfoque: w.iaMultiple?.enfoque ?? '', + cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10, + isLoading: true, + }, })) try { - const numeroCiclo = wizard.iaMultiple?.ciclo - if (!numeroCiclo || !Number.isFinite(numeroCiclo) || numeroCiclo <= 0) { - onChange((w) => ({ - ...w, - isLoading: false, - errorMessage: 'Ingresa un número de ciclo válido.', - })) - return - } - const cantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10 - if (!Number.isFinite(cantidad) || cantidad <= 0 || cantidad > 50) { + if (!Number.isFinite(cantidad) || cantidad <= 0 || cantidad > 15) { onChange((w) => ({ ...w, - isLoading: false, - errorMessage: 'La cantidad de sugerencias debe ser entre 1 y 50.', + errorMessage: 'La cantidad de sugerencias debe ser entre 1 y 15.', + iaMultiple: { + enfoque: w.iaMultiple?.enfoque ?? '', + cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10, + isLoading: false, + }, })) return } @@ -82,7 +91,6 @@ export default function PasoSugerenciasForm({ const nuevasSugerencias = await generate_subject_suggestions({ plan_estudio_id: wizard.plan_estudio_id, - numero_de_ciclo: numeroCiclo, enfoque: enfoqueTrim ? enfoqueTrim : undefined, cantidad_de_sugerencias: cantidad, sugerencias_conservadas: sugerenciasConservadas.map((s) => ({ @@ -91,11 +99,19 @@ export default function PasoSugerenciasForm({ })), }) + if (hadNoSugerenciasBefore && nuevasSugerencias.length > 0) { + setShowConservacionTooltip(true) + } + onChange( (w): NewSubjectWizardState => ({ ...w, - isLoading: false, sugerencias: [...nuevasSugerencias, ...sugerenciasConservadas], + iaMultiple: { + enfoque: w.iaMultiple?.enfoque ?? '', + cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10, + isLoading: false, + }, }), ) } catch (err) { @@ -104,8 +120,12 @@ export default function PasoSugerenciasForm({ onChange( (w): NewSubjectWizardState => ({ ...w, - isLoading: false, errorMessage: message, + iaMultiple: { + enfoque: w.iaMultiple?.enfoque ?? '', + cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10, + isLoading: false, + }, }), ) } @@ -122,48 +142,23 @@ export default function PasoSugerenciasForm({
-
- {/* Input Ciclo */} -
- - { - const raw = e.target.value - if (raw === '') { - setIaMultiple({ ciclo: null }) - 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, 999) - setIaMultiple({ ciclo: capped }) - }} - /> -
- - {/* Input Enfoque */} -
+
+
- setIaMultiple({ enfoque: e.target.value })} />
-
-
+
+
@@ -172,7 +167,7 @@ export default function PasoSugerenciasForm({ value={cantidadDeSugerencias} type="number" min={1} - max={50} + max={15} step={1} inputMode="numeric" onKeyDown={(e) => { @@ -186,7 +181,7 @@ export default function PasoSugerenciasForm({ const asNumber = Number(raw) if (!Number.isFinite(asNumber)) return const n = Math.floor(Math.abs(asNumber)) - const capped = Math.min(n >= 1 ? n : 1, 50) + const capped = Math.min(n >= 1 ? n : 1, 15) setIaMultiple({ cantidadDeSugerencias: capped }) }} /> @@ -197,19 +192,21 @@ export default function PasoSugerenciasForm({ variant="outline" className="h-9 gap-1.5" onClick={onGenerarSugerencias} - disabled={wizard.isLoading} + disabled={isLoading} > - Generar sugerencias + {wizard.sugerencias.length > 0 + ? 'Generar más sugerencias' + : 'Generar sugerencias'}
- -

- Al generar más sugerencias, solo se conservarán las asignaturas que - hayas seleccionado. -

+ + {/* --- HEADER LISTA --- */}
@@ -221,9 +218,32 @@ export default function PasoSugerenciasForm({ {plan ? `${plan.nivel} en ${plan.nombre}` : '...'}

-
- {wizard.sugerencias.filter((s) => s.selected).length} seleccionadas -
+ + +
+ 📌 + {wizard.sugerencias.filter((s) => s.selected).length}{' '} + seleccionadas +
+
+ +
+ + Al generar más sugerencias, se conservarán las asignaturas + seleccionadas. + + +
+
+
{/* --- LISTA DE ASIGNATURAS --- */} diff --git a/src/data/api/subjects.api.ts b/src/data/api/subjects.api.ts index 590f170..f64dd2b 100644 --- a/src/data/api/subjects.api.ts +++ b/src/data/api/subjects.api.ts @@ -140,7 +140,6 @@ export type AIGenerateSubjectInput = { export type GenerateSubjectSuggestionsInput = { plan_estudio_id: UUID - numero_de_ciclo: number enfoque?: string cantidad_de_sugerencias: number sugerencias_conservadas: Array<{ nombre: string; descripcion: string }> diff --git a/src/features/asignaturas/nueva/AIProgressLoader.tsx b/src/features/asignaturas/nueva/AIProgressLoader.tsx new file mode 100644 index 0000000..4e9177d --- /dev/null +++ b/src/features/asignaturas/nueva/AIProgressLoader.tsx @@ -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 = ({ + 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 ( +
+ {/* Contenedor de la barra */} +
+
+
+ + Generando IA + +
+
+ + {Math.floor(progress)}% + +
+
+ + {/* Barra de fondo */} +
+ {/* Barra de progreso dinámica */} +
+
+ + {/* Mensajes cambiantes */} +
+ {' '} + {/* Altura fija para evitar saltos */} +

+ {messages[currentMessageIndex]} +

+
+ + {/* Nota de tiempo estimado (Opcional, transparencia operacional) */} +

+ Tiempo estimado: ~{Math.ceil(4.07 * cantidadDeSugerencias + 10.93)}{' '} + segs +

+
+
+ ) +} diff --git a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts index d920134..bdff38d 100644 --- a/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts +++ b/src/features/asignaturas/nueva/hooks/useNuevaAsignaturaWizard.ts @@ -30,9 +30,9 @@ export function useNuevaAsignaturaWizard(planId: string) { archivosAdjuntos: [], }, iaMultiple: { - ciclo: null, enfoque: '', cantidadDeSugerencias: 10, + isLoading: false, }, resumen: {}, isLoading: false, diff --git a/src/features/asignaturas/nueva/types.ts b/src/features/asignaturas/nueva/types.ts index d529f13..8c7a8de 100644 --- a/src/features/asignaturas/nueva/types.ts +++ b/src/features/asignaturas/nueva/types.ts @@ -65,9 +65,9 @@ export type NewSubjectWizardState = { archivosAdjuntos?: Array } iaMultiple?: { - ciclo: number | null enfoque: string cantidadDeSugerencias: number + isLoading: boolean } resumen: { previewAsignatura?: AsignaturaPreview -- 2.49.1 From 46d8d6142e078bd0c82c860f02609ce9193798b9 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Wed, 11 Feb 2026 16:56:16 -0600 Subject: [PATCH 6/9] feat: integrate Radix UI Accordion component and enhance subject wizard - Added Radix UI Accordion component for better UI organization in PasoDetallesPanel. - Implemented structure selection and subject suggestions management in the wizard. - Updated subject API to initialize new subjects with null values for structure and cycle. - Modified state management in useNuevaAsignaturaWizard to include estructuraId. - Adjusted types for suggested subjects to include line and cycle information. --- bun.lock | 49 +++++ package.json | 1 + .../asignaturas/wizard/PasoDetallesPanel.tsx | 174 +++++++++++++++++- src/components/ui/accordion.tsx | 64 +++++++ src/data/api/subjects.api.ts | 3 +- .../nueva/hooks/useNuevaAsignaturaWizard.ts | 15 +- src/features/asignaturas/nueva/types.ts | 4 +- 7 files changed, 301 insertions(+), 9 deletions(-) create mode 100644 src/components/ui/accordion.tsx diff --git a/bun.lock b/bun.lock index 7118678..e2e0e87 100644 --- a/bun.lock +++ b/bun.lock @@ -36,6 +36,7 @@ "date-fns": "^4.1.0", "lucide-react": "^0.562.0", "motion": "^12.24.7", + "radix-ui": "^1.4.3", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwind-merge": "^3.4.0", @@ -251,10 +252,16 @@ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + "@radix-ui/react-accessible-icon": ["@radix-ui/react-accessible-icon@1.1.7", "", { "dependencies": { "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A=="], + + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g=="], + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], @@ -281,12 +288,24 @@ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + "@radix-ui/react-form": ["@radix-ui/react-form@0.1.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ=="], + + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="], + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="], + + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], + + "@radix-ui/react-one-time-password-field": ["@radix-ui/react-one-time-password-field@0.1.8", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg=="], + + "@radix-ui/react-password-toggle-field": ["@radix-ui/react-password-toggle-field@0.1.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-is-hydrated": "0.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw=="], + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], @@ -297,6 +316,10 @@ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.7", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="], + + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="], + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], @@ -305,10 +328,22 @@ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="], + + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], + + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="], + + "@radix-ui/react-toolbar": ["@radix-ui/react-toolbar@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-toggle-group": "1.1.11" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg=="], + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], @@ -1161,6 +1196,8 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "radix-ui": ["radix-ui@1.4.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-accessible-icon": "1.1.7", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-aspect-ratio": "1.1.7", "@radix-ui/react-avatar": "1.1.10", "@radix-ui/react-checkbox": "1.3.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-context-menu": "2.2.16", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-dropdown-menu": "2.1.16", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-form": "0.1.8", "@radix-ui/react-hover-card": "1.1.15", "@radix-ui/react-label": "2.1.7", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-menubar": "1.1.16", "@radix-ui/react-navigation-menu": "1.2.14", "@radix-ui/react-one-time-password-field": "0.1.8", "@radix-ui/react-password-toggle-field": "0.1.3", "@radix-ui/react-popover": "1.1.15", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-progress": "1.1.7", "@radix-ui/react-radio-group": "1.3.8", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-scroll-area": "1.2.10", "@radix-ui/react-select": "2.2.6", "@radix-ui/react-separator": "1.1.7", "@radix-ui/react-slider": "1.3.6", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-switch": "1.2.6", "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-toggle-group": "1.1.11", "@radix-ui/react-toolbar": "1.1.11", "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-escape-keydown": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA=="], + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], @@ -1419,6 +1456,8 @@ "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1431,6 +1470,8 @@ "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "@radix-ui/react-toolbar/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], @@ -1487,6 +1528,14 @@ "is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "radix-ui/@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + + "radix-ui/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], + + "radix-ui/@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], + + "radix-ui/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], diff --git a/package.json b/package.json index 7a9ab10..4c911fc 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "date-fns": "^4.1.0", "lucide-react": "^0.562.0", "motion": "^12.24.7", + "radix-ui": "^1.4.3", "react": "^19.2.0", "react-dom": "^19.2.0", "tailwind-merge": "^3.4.0", diff --git a/src/components/asignaturas/wizard/PasoDetallesPanel.tsx b/src/components/asignaturas/wizard/PasoDetallesPanel.tsx index b8d9ecd..e794b00 100644 --- a/src/components/asignaturas/wizard/PasoDetallesPanel.tsx +++ b/src/components/asignaturas/wizard/PasoDetallesPanel.tsx @@ -4,6 +4,12 @@ import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/ import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types' import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA' +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion' import { Card, CardDescription, @@ -20,6 +26,7 @@ import { SelectValue, } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' +import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data' import { FACULTADES, MATERIAS_MOCK, @@ -33,6 +40,10 @@ export function PasoDetallesPanel({ wizard: NewSubjectWizardState onChange: React.Dispatch> }) { + const { data: estructurasAsignatura } = useSubjectEstructuras() + const { data: plan } = usePlan(wizard.plan_estudio_id) + const { data: lineasPlan } = usePlanLineas(wizard.plan_estudio_id) + if (wizard.tipoOrigen === 'MANUAL') { return ( @@ -147,7 +158,168 @@ export function PasoDetallesPanel({ } if (wizard.tipoOrigen === 'IA_MULTIPLE') { - return
Hola
+ const maxCiclos = Math.max(1, plan?.numero_ciclos ?? 1) + const sugerenciasSeleccionadas = wizard.sugerencias.filter( + (s) => s.selected, + ) + + const patchSugerencia = ( + id: string, + patch: Partial, + ) => + onChange((w) => ({ + ...w, + sugerencias: w.sugerencias.map((s) => + s.id === id ? { ...s, ...patch } : s, + ), + })) + + return ( +
+
+
+ + +
+
+ +
+

+ Materias seleccionadas +

+ {sugerenciasSeleccionadas.length === 0 ? ( +
+ Selecciona al menos una sugerencia para configurar su descripción, + línea curricular y ciclo. +
+ ) : ( + + {sugerenciasSeleccionadas.map((asig) => ( + + + {asig.nombre} + + +
+
+ +