From 00369df78685b2d2dc4e173aa8a6c38f1f9321d4 Mon Sep 17 00:00:00 2001 From: Guillermo Arrieta Medina Date: Thu, 5 Feb 2026 13:24:36 -0600 Subject: [PATCH] =?UTF-8?q?Feat:=20generaci=C3=B3n=20IA=20de=20asignaturas?= =?UTF-8?q?,=20navegaci=C3=B3n=20con=20confetti=20y=20ajustes=20de=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit closes #45: - Añadido AIGenerateSubjectInput y nueva implementación ai_generate_subject que envía FormData (soporta archivosAdjuntos) al Edge Function. - Creado hook useGenerateSubjectAI (mutation) y usado en WizardControls de asignaturas para generar la asignatura vía IA. - WizardControls (asignaturas) construye el payload IA, invoca la mutación y navega al detalle de la asignatura creada pasando state.showConfetti para lanzar confetti. - Ajustes en subjects.api.ts (nombres de endpoint, tipos y envío de datos) y sincronización de tipos en WizardControls (plan y campos básicos). - Ruta de detalle de asignatura ($asignaturaId) ahora lee location.state.showConfetti y dispara lateralConfetti al entrar. - Eliminado el prop onCreate del modal de nueva asignatura (la creación IA se gestiona internamente). --- .../asignaturas/wizard/WizardControls.tsx | 49 +------------ .../planes/wizard/WizardControls.tsx | 7 +- src/data/api/subjects.api.ts | 69 +++++-------------- src/data/hooks/useSubjects.ts | 5 +- .../nueva/NuevaAsignaturaModalContainer.tsx | 4 ++ .../$planId/asignaturas/$asignaturaId.tsx | 13 +--- 6 files changed, 30 insertions(+), 117 deletions(-) diff --git a/src/components/asignaturas/wizard/WizardControls.tsx b/src/components/asignaturas/wizard/WizardControls.tsx index fd26612..1b6cd8e 100644 --- a/src/components/asignaturas/wizard/WizardControls.tsx +++ b/src/components/asignaturas/wizard/WizardControls.tsx @@ -1,10 +1,6 @@ -import { useNavigate } from '@tanstack/react-router' - -import type { AIGenerateSubjectInput } from '@/data' import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types' import { Button } from '@/components/ui/button' -import { useGenerateSubjectAI } from '@/data' export function WizardControls({ wizard, @@ -16,6 +12,7 @@ export function WizardControls({ disableNext, disableCreate, isLastStep, + onCreate, }: { wizard: NewSubjectWizardState setWizard: React.Dispatch> @@ -26,9 +23,8 @@ export function WizardControls({ disableNext: boolean disableCreate: boolean isLastStep: boolean + onCreate: () => Promise | void }) { - const navigate = useNavigate() - const generateSubjectAI = useGenerateSubjectAI() const handleCreate = async () => { setWizard((w) => ({ ...w, @@ -37,46 +33,7 @@ export function WizardControls({ })) try { - if (wizard.tipoOrigen === 'IA') { - const aiInput: AIGenerateSubjectInput = { - plan_estudio_id: wizard.plan_estudio_id, - datosBasicos: { - nombre: wizard.datosBasicos.nombre, - codigo: wizard.datosBasicos.codigo, - tipo: wizard.datosBasicos.tipo!, - creditos: wizard.datosBasicos.creditos!, - horasIndependientes: wizard.datosBasicos.horasIndependientes, - horasAcademicas: wizard.datosBasicos.horasAcademicas, - estructuraId: wizard.datosBasicos.estructuraId!, - }, - iaConfig: { - descripcionEnfoqueAcademico: - wizard.iaConfig!.descripcionEnfoqueAcademico, - instruccionesAdicionalesIA: - wizard.iaConfig!.instruccionesAdicionalesIA, - archivosReferencia: wizard.iaConfig!.archivosReferencia, - repositoriosReferencia: - wizard.iaConfig!.repositoriosReferencia || [], - archivosAdjuntos: wizard.iaConfig!.archivosAdjuntos || [], - }, - } - - console.log( - `${new Date().toISOString()} - Enviando a generar asignatura con IA`, - ) - - const asignatura = await generateSubjectAI.mutateAsync(aiInput) - console.log( - `${new Date().toISOString()} - Asignatura IA generada`, - asignatura, - ) - - navigate({ - to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`, - state: { showConfetti: true }, - }) - return - } + await onCreate() } catch (err: any) { setWizard((w) => ({ ...w, diff --git a/src/components/planes/wizard/WizardControls.tsx b/src/components/planes/wizard/WizardControls.tsx index 446fd53..24ea695 100644 --- a/src/components/planes/wizard/WizardControls.tsx +++ b/src/components/planes/wizard/WizardControls.tsx @@ -1,6 +1,5 @@ import { useNavigate } from '@tanstack/react-router' -import type { AIGeneratePlanInput } from '@/data' import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain' import type { NewPlanWizardState } from '@/features/planes/nuevo/types' // import type { Database } from '@/types/supabase' @@ -55,11 +54,11 @@ export function WizardControls({ ? wizard.datosBasicos.numCiclos : 1 - const aiInput: AIGeneratePlanInput = { + const aiInput = { datosBasicos: { nombrePlan: wizard.datosBasicos.nombrePlan, - carreraId: wizard.datosBasicos.carrera.id, - facultadId: wizard.datosBasicos.facultad.id, + carreraId: wizard.datosBasicos.carrera.id || undefined, + facultadId: wizard.datosBasicos.facultad.id || undefined, nivel: wizard.datosBasicos.nivel as string, tipoCiclo: tipoCicloSafe, numCiclos: numCiclosSafe, diff --git a/src/data/api/subjects.api.ts b/src/data/api/subjects.api.ts index 7488aab..b0b5f2a 100644 --- a/src/data/api/subjects.api.ts +++ b/src/data/api/subjects.api.ts @@ -11,12 +11,11 @@ import type { TipoAsignatura, UUID, } from '../types/domain' -import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone' import type { Database } from '@/types/supabase' const EDGE = { subjects_create_manual: 'subjects_create_manual', - ai_generate_subject: 'ai-generate-subject', + ai_generate_subject: 'ai_generate_subject', subjects_persist_from_ai: 'subjects_persist_from_ai', subjects_clone_from_existing: 'subjects_clone_from_existing', subjects_import_from_file: 'subjects_import_from_file', @@ -103,58 +102,26 @@ export async function subjects_create_manual( return invokeEdge(EDGE.subjects_create_manual, payload) } -export type AIGenerateSubjectInput = { - plan_estudio_id: Asignatura['plan_estudio_id'] +export async function ai_generate_subject(payload: { + planId: UUID datosBasicos: { - nombre: Asignatura['nombre'] - codigo?: Asignatura['codigo'] - tipo: Asignatura['tipo'] | null - creditos: Asignatura['creditos'] | null - horasAcademicas?: Asignatura['horas_academicas'] | null - horasIndependientes?: Asignatura['horas_independientes'] | null - estructuraId: Asignatura['estructura_id'] | null + nombre: string + clave?: string + tipo: TipoAsignatura + creditos: number + horasSemana?: number + estructuraId: UUID } - // clonInterno?: { - // facultadId?: string - // carreraId?: string - // planOrigenId?: string - // asignaturaOrigenId?: string | null - // } - // clonTradicional?: { - // archivoWordAsignaturaId: string | null - // archivosAdicionalesIds: Array - // } - iaConfig?: { - descripcionEnfoqueAcademico: string - instruccionesAdicionalesIA: string - archivosReferencia: Array - repositoriosReferencia?: Array - archivosAdjuntos?: Array + iaConfig: { + descripcionEnfoque: string + notasAdicionales?: string + archivosExistentesIds?: Array + repositoriosIds?: Array + archivosAdhocIds?: Array + usarMCP?: boolean } -} - -export async function ai_generate_subject( - input: AIGenerateSubjectInput, -): Promise { - const edgeFunctionBody = new FormData() - edgeFunctionBody.append('plan_estudio_id', input.plan_estudio_id) - edgeFunctionBody.append('datosBasicos', JSON.stringify(input.datosBasicos)) - edgeFunctionBody.append( - 'iaConfig', - JSON.stringify({ - ...input.iaConfig, - archivosAdjuntos: undefined, // los manejamos aparte - }), - ) - input.iaConfig?.archivosAdjuntos?.forEach((file, index) => { - edgeFunctionBody.append(`archivosAdjuntos`, file.file) - }) - return invokeEdge( - EDGE.ai_generate_subject, - edgeFunctionBody, - undefined, - supabaseBrowser(), - ) +}): Promise { + return invokeEdge(EDGE.ai_generate_subject, payload) } export async function subjects_persist_from_ai(payload: { diff --git a/src/data/hooks/useSubjects.ts b/src/data/hooks/useSubjects.ts index a5870d4..c52f15b 100644 --- a/src/data/hooks/useSubjects.ts +++ b/src/data/hooks/useSubjects.ts @@ -94,10 +94,7 @@ export function useCreateSubjectManual() { } export function useGenerateSubjectAI() { - const qc = useQueryClient() - return useMutation({ - mutationFn: ai_generate_subject, - }) + return useMutation({ mutationFn: ai_generate_subject }) } export function usePersistSubjectFromAI() { diff --git a/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx b/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx index 9308fe2..0ac8e81 100644 --- a/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx +++ b/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx @@ -118,6 +118,10 @@ export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) { isLastStep={idx >= Wizard.steps.length - 1} wizard={wizard} setWizard={setWizard} + onCreate={async () => { + await crearAsignatura() + handleClose() + }} /> } diff --git a/src/routes/planes/$planId/asignaturas/$asignaturaId.tsx b/src/routes/planes/$planId/asignaturas/$asignaturaId.tsx index e227155..bfa039d 100644 --- a/src/routes/planes/$planId/asignaturas/$asignaturaId.tsx +++ b/src/routes/planes/$planId/asignaturas/$asignaturaId.tsx @@ -1,8 +1,6 @@ -import { createFileRoute, notFound, useLocation } from '@tanstack/react-router' -import { useEffect } from 'react' +import { createFileRoute, notFound } from '@tanstack/react-router' import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage' -import { lateralConfetti } from '@/components/ui/lateral-confetti' import { NotFoundPage } from '@/components/ui/NotFoundPage' import { subjects_get } from '@/data/api/subjects.api' import { qk } from '@/data/query/keys' @@ -37,15 +35,6 @@ export const Route = createFileRoute( function RouteComponent() { // const { planId, asignaturaId } = Route.useParams() - const location = useLocation() - - // Confetti al llegar desde creación - useEffect(() => { - if ((location.state as any)?.showConfetti) { - lateralConfetti() - window.history.replaceState({}, document.title) // Limpiar el estado para que no se repita - } - }, [location.state]) return (