From 073969b9bf54476e761cf09f93301f8ca11a628c Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Tue, 10 Feb 2026 15:58:51 -0600 Subject: [PATCH] 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