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

This commit is contained in:
2026-02-11 13:14:54 -06:00
parent 89f264bf5d
commit ded54c18dd
5 changed files with 91 additions and 30 deletions

View File

@@ -17,11 +17,9 @@ export default function PasoSugerenciasForm({
wizard: NewSubjectWizardState wizard: NewSubjectWizardState
onChange: Dispatch<SetStateAction<NewSubjectWizardState>> onChange: Dispatch<SetStateAction<NewSubjectWizardState>>
}) { }) {
const selectedIds = wizard.sugerencias
.filter((s) => s.selected)
.map((s) => s.id)
const ciclo = wizard.iaMultiple?.ciclo ?? '' const ciclo = wizard.iaMultiple?.ciclo ?? ''
const enfoque = wizard.iaMultiple?.enfoque ?? '' const enfoque = wizard.iaMultiple?.enfoque ?? ''
const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
const setIaMultiple = ( const setIaMultiple = (
patch: Partial<NonNullable<NewSubjectWizardState['iaMultiple']>>, patch: Partial<NonNullable<NewSubjectWizardState['iaMultiple']>>,
@@ -32,6 +30,7 @@ export default function PasoSugerenciasForm({
iaMultiple: { iaMultiple: {
ciclo: w.iaMultiple?.ciclo ?? null, ciclo: w.iaMultiple?.ciclo ?? null,
enfoque: w.iaMultiple?.enfoque ?? '', enfoque: w.iaMultiple?.enfoque ?? '',
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
...patch, ...patch,
}, },
}), }),
@@ -48,7 +47,7 @@ export default function PasoSugerenciasForm({
})) }))
} }
const onMasSugerencias = async () => { const onGenerarSugerencias = async () => {
const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected) const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected)
onChange((w) => ({ onChange((w) => ({
@@ -59,21 +58,44 @@ export default function PasoSugerenciasForm({
})) }))
try { try {
const nuevasSugerencias = await generate_subject_suggestions() const numeroCiclo = wizard.iaMultiple?.ciclo
const merged = [...nuevasSugerencias, ...sugerenciasConservadas] 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<string>() const cantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
const deduped = merged.filter((s) => { if (!Number.isFinite(cantidad) || cantidad <= 0 || cantidad > 50) {
if (seen.has(s.id)) return false onChange((w) => ({
seen.add(s.id) ...w,
return true 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( onChange(
(w): NewSubjectWizardState => ({ (w): NewSubjectWizardState => ({
...w, ...w,
isLoading: false, isLoading: false,
sugerencias: deduped, sugerencias: [...nuevasSugerencias, ...sugerenciasConservadas],
}), }),
) )
} catch (err) { } catch (err) {
@@ -90,7 +112,7 @@ export default function PasoSugerenciasForm({
} }
return ( return (
<div className="p-6"> <>
{/* --- BLOQUE SUPERIOR: PARÁMETROS --- */} {/* --- BLOQUE SUPERIOR: PARÁMETROS --- */}
<div className="border-border/60 bg-muted/30 mb-4 rounded-xl border p-4"> <div className="border-border/60 bg-muted/30 mb-4 rounded-xl border p-4">
<div className="mb-3 flex items-center gap-2"> <div className="mb-3 flex items-center gap-2">
@@ -138,17 +160,47 @@ export default function PasoSugerenciasForm({
onChange={(e) => setIaMultiple({ enfoque: e.target.value })} onChange={(e) => setIaMultiple({ enfoque: e.target.value })}
/> />
</div> </div>
</div>
<div className="mt-3 flex w-full flex-col items-end gap-3 md:flex-row">
<div className="w-full md:w-44">
<Label className="text-muted-foreground mb-1 block text-xs">
Cantidad de sugerencias
</Label>
<Input
placeholder="Ej. 10"
value={cantidadDeSugerencias}
type="number"
min={1}
max={50}
step={1}
inputMode="numeric"
onKeyDown={(e) => {
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
e.preventDefault()
}
}}
onChange={(e) => {
const raw = e.target.value
if (raw === '') return
const asNumber = Number(raw)
if (!Number.isFinite(asNumber)) return
const n = Math.floor(Math.abs(asNumber))
const capped = Math.min(n >= 1 ? n : 1, 50)
setIaMultiple({ cantidadDeSugerencias: capped })
}}
/>
</div>
{/* Botón Refrescar */}
<Button <Button
type="button" type="button"
variant="outline" variant="outline"
className="h-9 gap-1.5" className="h-9 gap-1.5"
onClick={onMasSugerencias} onClick={onGenerarSugerencias}
disabled={wizard.isLoading} disabled={wizard.isLoading}
> >
<RefreshCw className="h-3.5 w-3.5" /> <RefreshCw className="h-3.5 w-3.5" />
Más sugerencias Generar sugerencias
</Button> </Button>
</div> </div>
@@ -170,12 +222,12 @@ export default function PasoSugerenciasForm({
</p> </p>
</div> </div>
<div className="bg-muted text-foreground inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-semibold"> <div className="bg-muted text-foreground inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-semibold">
{selectedIds.length} seleccionadas {wizard.sugerencias.filter((s) => s.selected).length} seleccionadas
</div> </div>
</div> </div>
{/* --- LISTA DE ASIGNATURAS --- */} {/* --- LISTA DE ASIGNATURAS --- */}
<div className="max-h-80 space-y-1 overflow-y-auto pr-1"> <div className="max-h-100 space-y-1 overflow-y-auto pr-1">
{wizard.sugerencias.map((asignatura) => { {wizard.sugerencias.map((asignatura) => {
const isSelected = asignatura.selected const isSelected = asignatura.selected
@@ -223,7 +275,7 @@ export default function PasoSugerenciasForm({
</span> </span>
</div> </div>
<p className="text-muted-foreground mt-1 text-xs"> <p className="text-muted-foreground mt-1 text-sm">
{asignatura.descripcion} {asignatura.descripcion}
</p> </p>
</div> </div>
@@ -231,6 +283,6 @@ export default function PasoSugerenciasForm({
) )
})} })}
</div> </div>
</div> </>
) )
} }

View File

@@ -94,7 +94,7 @@ export function WizardControls({
Anterior Anterior
</Button> </Button>
<div className="flex-1"> <div className="mx-2 flex-1">
{(errorMessage ?? wizard.errorMessage) && ( {(errorMessage ?? wizard.errorMessage) && (
<span className="text-destructive text-sm font-medium"> <span className="text-destructive text-sm font-medium">
{errorMessage ?? wizard.errorMessage} {errorMessage ?? wizard.errorMessage}

View File

@@ -138,31 +138,38 @@ export type AIGenerateSubjectInput = {
} }
} }
export async function generate_subject_suggestions(): Promise< export type GenerateSubjectSuggestionsInput = {
Array<AsignaturaSugerida> 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<Array<AsignaturaSugerida>> {
const raw = await invokeEdge<Array<DataAsignaturaSugerida>>( const raw = await invokeEdge<Array<DataAsignaturaSugerida>>(
EDGE.generate_subject_suggestions, EDGE.generate_subject_suggestions,
{}, input,
{ headers: { 'Content-Type': 'application/json' } },
) )
const arr = raw.map( return raw.map(
(s): AsignaturaSugerida => ({ (s): AsignaturaSugerida => ({
id: crypto.randomUUID(), id: crypto.randomUUID(),
selected: false, selected: false,
source: 'IA', source: 'IA',
estructuraId: null,
nombre: s.nombre, nombre: s.nombre,
codigo: s.codigo, codigo: s.codigo,
tipo: s.tipo ?? null, tipo: s.tipo ?? null,
creditos: s.creditos ?? null, creditos: s.creditos ?? null,
horasAcademicas: s.horasAcademicas ?? null, horasAcademicas: s.horasAcademicas ?? null,
horasIndependientes: s.horasIndependientes ?? null, horasIndependientes: s.horasIndependientes ?? null,
estructuraId: null,
descripcion: s.descripcion, descripcion: s.descripcion,
}), }),
) )
return arr
} }
export async function ai_generate_subject( export async function ai_generate_subject(

View File

@@ -32,6 +32,7 @@ export function useNuevaAsignaturaWizard(planId: string) {
iaMultiple: { iaMultiple: {
ciclo: null, ciclo: null,
enfoque: '', enfoque: '',
cantidadDeSugerencias: 10,
}, },
resumen: {}, resumen: {},
isLoading: false, isLoading: false,

View File

@@ -18,7 +18,7 @@ export type DataAsignaturaSugerida = {
creditos: Asignatura['creditos'] | null creditos: Asignatura['creditos'] | null
horasAcademicas?: number | null horasAcademicas?: number | null
horasIndependientes?: number | null horasIndependientes?: number | null
descripcion?: string descripcion: string
} }
export type AsignaturaSugerida = { export type AsignaturaSugerida = {
@@ -67,6 +67,7 @@ export type NewSubjectWizardState = {
iaMultiple?: { iaMultiple?: {
ciclo: number | null ciclo: number | null
enfoque: string enfoque: string
cantidadDeSugerencias: number
} }
resumen: { resumen: {
previewAsignatura?: AsignaturaPreview previewAsignatura?: AsignaturaPreview