diff --git a/src/components/planes/wizard/PasoDetallesPanel/FileDropZone.tsx b/src/components/planes/wizard/PasoDetallesPanel/FileDropZone.tsx index 2e70ae1..4e638ba 100644 --- a/src/components/planes/wizard/PasoDetallesPanel/FileDropZone.tsx +++ b/src/components/planes/wizard/PasoDetallesPanel/FileDropZone.tsx @@ -12,6 +12,7 @@ export interface UploadedFile { } interface FileDropzoneProps { + persistentFiles?: Array onFilesChange?: (files: Array) => void acceptedTypes?: string maxFiles?: number @@ -20,6 +21,7 @@ interface FileDropzoneProps { } export function FileDropzone({ + persistentFiles, onFilesChange, acceptedTypes = '.doc,.docx,.pdf', maxFiles = 5, @@ -27,7 +29,7 @@ export function FileDropzone({ description = 'o haz clic para seleccionar', }: FileDropzoneProps) { const [isDragging, setIsDragging] = useState(false) - const [files, setFiles] = useState>([]) + const [files, setFiles] = useState>(persistentFiles ?? []) const onFilesChangeRef = useRef(onFilesChange) const addFiles = useCallback( diff --git a/src/components/planes/wizard/PasoDetallesPanel/PasoDetallesPanel.tsx b/src/components/planes/wizard/PasoDetallesPanel/PasoDetallesPanel.tsx index b538938..37c5727 100644 --- a/src/components/planes/wizard/PasoDetallesPanel/PasoDetallesPanel.tsx +++ b/src/components/planes/wizard/PasoDetallesPanel/PasoDetallesPanel.tsx @@ -23,12 +23,10 @@ import { export function PasoDetallesPanel({ wizard, onChange, - onGenerarIA, isLoading, }: { wizard: NewPlanWizardState onChange: React.Dispatch> - onGenerarIA: () => void isLoading: boolean }) { if (wizard.tipoOrigen === 'MANUAL') { @@ -87,6 +85,7 @@ export function PasoDetallesPanel({ onChange((w) => { const prev = w.iaConfig?.archivosReferencia || [] @@ -133,7 +132,7 @@ export function PasoDetallesPanel({
Opcional: se pueden adjuntar recursos IA más adelante.
- diff --git a/src/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA.tsx b/src/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA.tsx index 528e32f..93fbd93 100644 --- a/src/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA.tsx +++ b/src/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA.tsx @@ -21,12 +21,14 @@ import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs' const ReferenciasParaIA = ({ selectedArchivoIds = [], selectedRepositorioIds = [], + uploadedFiles = [], onToggleArchivo, onToggleRepositorio, onFilesChange, }: { selectedArchivoIds?: Array selectedRepositorioIds?: Array + uploadedFiles?: Array onToggleArchivo?: (id: string, checked: boolean) => void onToggleRepositorio?: (id: string, checked: boolean) => void onFilesChange?: (files: Array) => void @@ -163,6 +165,7 @@ const ReferenciasParaIA = ({ content: (
Adjuntos
    - {adjuntos.map((f) => ( + {adjuntos.map((f: UploadedFile) => (
  • {f.file.name} diff --git a/src/data/supabase/invokeEdge.ts b/src/data/supabase/invokeEdge.ts index 289a873..627b6eb 100644 --- a/src/data/supabase/invokeEdge.ts +++ b/src/data/supabase/invokeEdge.ts @@ -1,7 +1,8 @@ -import type { SupabaseClient } from "@supabase/supabase-js"; -import type { Database } from "../types/database"; import { supabaseBrowser } from "./client"; +import type { Database } from "@/types/supabase"; +import type { SupabaseClient } from "@supabase/supabase-js"; + export type EdgeInvokeOptions = { method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; headers?: Record; @@ -12,7 +13,7 @@ export class EdgeFunctionError extends Error { message: string, public readonly functionName: string, public readonly status?: number, - public readonly details?: unknown + public readonly details?: unknown, ) { super(message); this.name = "EdgeFunctionError"; @@ -21,9 +22,17 @@ export class EdgeFunctionError extends Error { export async function invokeEdge( functionName: string, - body?: unknown, + body?: + | string + | File + | Blob + | ArrayBuffer + | FormData + | ReadableStream> + | Record + | undefined, opts: EdgeInvokeOptions = {}, - client?: SupabaseClient + client?: SupabaseClient, ): Promise { const supabase = client ?? supabaseBrowser(); @@ -34,12 +43,12 @@ export async function invokeEdge( }); if (error) { - const anyErr = error as any; + const anyErr = error; throw new EdgeFunctionError( anyErr.message ?? "Error en Edge Function", functionName, anyErr.status, - anyErr + anyErr, ); } diff --git a/src/features/planes/nuevo/NuevoPlanModalContainer.tsx b/src/features/planes/nuevo/NuevoPlanModalContainer.tsx index cb560fe..5e1e35f 100644 --- a/src/features/planes/nuevo/NuevoPlanModalContainer.tsx +++ b/src/features/planes/nuevo/NuevoPlanModalContainer.tsx @@ -25,6 +25,7 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' +import { useGeneratePlanAI } from '@/data/hooks/usePlans' // Mock de permisos/rol const auth_get_current_user_role = () => 'JEFE_CARRERA' as const @@ -47,6 +48,8 @@ const Wizard = defineStepper( export default function NuevoPlanModalContainer() { const navigate = useNavigate() const role = auth_get_current_user_role() + const generatePlanAI = useGeneratePlanAI() + // const persistPlanFromAI = usePersistPlanFromAI() const { wizard, @@ -54,7 +57,6 @@ export default function NuevoPlanModalContainer() { canContinueDesdeModo, canContinueDesdeBasicos, canContinueDesdeDetalles, - generarPreviewIA, } = useNuevoPlanWizard() const handleClose = () => { @@ -62,23 +64,72 @@ export default function NuevoPlanModalContainer() { } const crearPlan = async () => { - setWizard((w: NewPlanWizardState) => ({ - ...w, - isLoading: true, - errorMessage: null, - })) - await new Promise((r) => setTimeout(r, 900)) - const nuevoId = (() => { - if (wizard.tipoOrigen === 'MANUAL') return 'plan_new_manual_001' - if (wizard.tipoOrigen === 'IA') return 'plan_new_ai_001' - if ( - wizard.tipoOrigen === 'CLONADO_INTERNO' || - wizard.tipoOrigen === 'CLONADO_TRADICIONAL' - ) - return 'plan_new_clone_001' - return 'plan_new_import_001' - })() - navigate({ to: `/planes/${nuevoId}` }) + setWizard( + (w: NewPlanWizardState): NewPlanWizardState => ({ + ...w, + isLoading: true, + errorMessage: null, + }), + ) + + try { + if (wizard.tipoOrigen === 'IA') { + const tipoCicloSafe = (wizard.datosBasicos.tipoCiclo || + 'Semestre') as any + const numCiclosSafe = + typeof wizard.datosBasicos.numCiclos === 'number' + ? wizard.datosBasicos.numCiclos + : 1 + + const aiInput = { + datosBasicos: { + nombrePlan: wizard.datosBasicos.nombrePlan, + carreraId: wizard.datosBasicos.carreraId, + facultadId: wizard.datosBasicos.facultadId || undefined, + nivel: wizard.datosBasicos.nivel as string, + tipoCiclo: tipoCicloSafe, + numCiclos: numCiclosSafe, + estructuraPlanId: wizard.datosBasicos.estructuraPlanId as string, + }, + iaConfig: { + descripcionEnfoque: wizard.iaConfig?.descripcionEnfoque || '', + notasAdicionales: wizard.iaConfig?.notasAdicionales || '', + archivosReferencia: wizard.iaConfig?.archivosReferencia || [], + repositoriosIds: wizard.iaConfig?.repositoriosReferencia || [], + archivosAdjuntos: wizard.iaConfig?.archivosAdjuntos || [], + }, + } + + const response = await generatePlanAI.mutateAsync(aiInput as any) + // const createdPlan = await persistPlanFromAI.mutateAsync({ + // jsonPlan: generatedJson, + // }) + // navigate({ to: `/planes/${createdPlan.id}` }) + console.log('Plan generado por IA:', response) + return + } + + // Fallback: comportamiento previo para otros modos (mock IDs) + await new Promise((r) => setTimeout(r, 900)) + const nuevoId = (() => { + if (wizard.tipoOrigen === 'MANUAL') return 'plan_new_manual_001' + if ( + wizard.tipoOrigen === 'CLONADO_INTERNO' || + wizard.tipoOrigen === 'CLONADO_TRADICIONAL' + ) + return 'plan_new_clone_001' + return 'plan_new_import_001' + })() + navigate({ to: `/planes/${nuevoId}` }) + } catch (err: any) { + setWizard((w) => ({ + ...w, + isLoading: false, + errorMessage: err?.message ?? 'Error generando el plan con IA', + })) + } finally { + setWizard((w) => ({ ...w, isLoading: false })) + } } return ( @@ -164,7 +215,6 @@ export default function NuevoPlanModalContainer() { diff --git a/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts b/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts index ff25daf..6455b32 100644 --- a/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts +++ b/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts @@ -1,7 +1,6 @@ import { useState } from "react"; -import type { NewPlanWizardState, PlanPreview } from "../types"; -import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain"; +import type { NewPlanWizardState } from "../types"; export function useNuevoPlanWizard() { const [wizard, setWizard] = useState({ @@ -81,45 +80,11 @@ export function useNuevoPlanWizard() { return false; })(); - const generarPreviewIA = async () => { - setWizard((w) => ({ ...w, isLoading: true, errorMessage: null })); - await new Promise((r) => setTimeout(r, 800)); - // Ensure preview has the stricter types required by `PlanPreview`. - let tipoCicloSafe: TipoCiclo; - if (wizard.datosBasicos.tipoCiclo === "") { - tipoCicloSafe = "Semestre"; - } else { - tipoCicloSafe = wizard.datosBasicos.tipoCiclo; - } - const numCiclosSafe: number = - typeof wizard.datosBasicos.numCiclos === "number" - ? wizard.datosBasicos.numCiclos - : 1; - - const preview: PlanPreview = { - nombrePlan: wizard.datosBasicos.nombrePlan || "Plan sin nombre", - nivel: wizard.datosBasicos.nivel as NivelPlanEstudio, - tipoCiclo: tipoCicloSafe, - numCiclos: numCiclosSafe, - numAsignaturasAprox: numCiclosSafe * 6, - secciones: [ - { id: "obj", titulo: "Objetivos", resumen: "Borrador de objetivos…" }, - { id: "perfil", titulo: "Perfil de egreso", resumen: "Borrador…" }, - ], - }; - setWizard((w: NewPlanWizardState) => ({ - ...w, - isLoading: false, - resumen: { previewPlan: preview }, - })); - }; - return { wizard, setWizard, canContinueDesdeModo, canContinueDesdeBasicos, canContinueDesdeDetalles, - generarPreviewIA, }; }