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:
@@ -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>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user