diff --git a/src/components/asignaturas/wizard/WizardControls.tsx b/src/components/asignaturas/wizard/WizardControls.tsx index 1b6cd8e..fd26612 100644 --- a/src/components/asignaturas/wizard/WizardControls.tsx +++ b/src/components/asignaturas/wizard/WizardControls.tsx @@ -1,6 +1,10 @@ +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, @@ -12,7 +16,6 @@ export function WizardControls({ disableNext, disableCreate, isLastStep, - onCreate, }: { wizard: NewSubjectWizardState setWizard: React.Dispatch> @@ -23,8 +26,9 @@ export function WizardControls({ disableNext: boolean disableCreate: boolean isLastStep: boolean - onCreate: () => Promise | void }) { + const navigate = useNavigate() + const generateSubjectAI = useGenerateSubjectAI() const handleCreate = async () => { setWizard((w) => ({ ...w, @@ -33,7 +37,46 @@ export function WizardControls({ })) try { - await onCreate() + 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 + } } catch (err: any) { setWizard((w) => ({ ...w, diff --git a/src/components/planes/wizard/WizardControls.tsx b/src/components/planes/wizard/WizardControls.tsx index 24ea695..446fd53 100644 --- a/src/components/planes/wizard/WizardControls.tsx +++ b/src/components/planes/wizard/WizardControls.tsx @@ -1,5 +1,6 @@ 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' @@ -54,11 +55,11 @@ export function WizardControls({ ? wizard.datosBasicos.numCiclos : 1 - const aiInput = { + const aiInput: AIGeneratePlanInput = { datosBasicos: { nombrePlan: wizard.datosBasicos.nombrePlan, - carreraId: wizard.datosBasicos.carrera.id || undefined, - facultadId: wizard.datosBasicos.facultad.id || undefined, + carreraId: wizard.datosBasicos.carrera.id, + facultadId: wizard.datosBasicos.facultad.id, 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 40b3293..acfe531 100644 --- a/src/data/api/subjects.api.ts +++ b/src/data/api/subjects.api.ts @@ -11,11 +11,12 @@ 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', @@ -102,26 +103,58 @@ export async function subjects_create_manual( return invokeEdge(EDGE.subjects_create_manual, payload) } -export async function ai_generate_subject(payload: { - planId: UUID +export type AIGenerateSubjectInput = { + plan_estudio_id: Asignatura['plan_estudio_id'] datosBasicos: { - nombre: string - clave?: string - tipo: TipoAsignatura - creditos: number - horasSemana?: number - estructuraId: UUID + 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 } - iaConfig: { - descripcionEnfoque: string - notasAdicionales?: string - archivosExistentesIds?: Array - repositoriosIds?: Array - archivosAdhocIds?: Array - usarMCP?: boolean + // 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 } -}): Promise { - return invokeEdge(EDGE.ai_generate_subject, payload) +} + +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(), + ) } export async function subjects_persist_from_ai(payload: { diff --git a/src/data/hooks/useSubjects.ts b/src/data/hooks/useSubjects.ts index 843edbc..35a8b77 100644 --- a/src/data/hooks/useSubjects.ts +++ b/src/data/hooks/useSubjects.ts @@ -97,7 +97,10 @@ export function useCreateSubjectManual() { } export function useGenerateSubjectAI() { - return useMutation({ mutationFn: ai_generate_subject }) + const qc = useQueryClient() + 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 0ac8e81..9308fe2 100644 --- a/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx +++ b/src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx @@ -118,10 +118,6 @@ 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 bfa039d..e227155 100644 --- a/src/routes/planes/$planId/asignaturas/$asignaturaId.tsx +++ b/src/routes/planes/$planId/asignaturas/$asignaturaId.tsx @@ -1,6 +1,8 @@ -import { createFileRoute, notFound } from '@tanstack/react-router' +import { createFileRoute, notFound, useLocation } from '@tanstack/react-router' +import { useEffect } from 'react' 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' @@ -35,6 +37,15 @@ 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 (