Feat: generación IA de asignaturas, navegación con confetti y ajustes de API

closes #63:
- 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).
This commit was merged in pull request #76.
This commit is contained in:
2026-02-05 13:22:16 -06:00
parent f00fabeac5
commit b1a233fa8c
6 changed files with 117 additions and 30 deletions

View File

@@ -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<Asignatura>(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<UUID>
repositoriosIds?: Array<UUID>
archivosAdhocIds?: Array<UUID>
usarMCP?: boolean
// clonInterno?: {
// facultadId?: string
// carreraId?: string
// planOrigenId?: string
// asignaturaOrigenId?: string | null
// }
// clonTradicional?: {
// archivoWordAsignaturaId: string | null
// archivosAdicionalesIds: Array<string>
// }
iaConfig?: {
descripcionEnfoqueAcademico: string
instruccionesAdicionalesIA: string
archivosReferencia: Array<string>
repositoriosReferencia?: Array<string>
archivosAdjuntos?: Array<UploadedFile>
}
}): Promise<any> {
return invokeEdge<any>(EDGE.ai_generate_subject, payload)
}
export async function ai_generate_subject(
input: AIGenerateSubjectInput,
): Promise<any> {
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<any>(
EDGE.ai_generate_subject,
edgeFunctionBody,
undefined,
supabaseBrowser(),
)
}
export async function subjects_persist_from_ai(payload: {