Generación de asignaturas funcional

This commit is contained in:
2026-02-26 16:20:21 -06:00
parent 4d1f102acb
commit d7d4eff523
8 changed files with 291 additions and 127 deletions

View File

@@ -26,7 +26,7 @@ export default function PasoSugerenciasForm({
onChange: Dispatch<SetStateAction<NewSubjectWizardState>> onChange: Dispatch<SetStateAction<NewSubjectWizardState>>
}) { }) {
const enfoque = wizard.iaMultiple?.enfoque ?? '' const enfoque = wizard.iaMultiple?.enfoque ?? ''
const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 10 const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 5
const isLoading = wizard.iaMultiple?.isLoading ?? false const isLoading = wizard.iaMultiple?.isLoading ?? false
const [showConservacionTooltip, setShowConservacionTooltip] = useState(false) const [showConservacionTooltip, setShowConservacionTooltip] = useState(false)
@@ -163,7 +163,7 @@ export default function PasoSugerenciasForm({
Cantidad de sugerencias Cantidad de sugerencias
</Label> </Label>
<Input <Input
placeholder="Ej. 10" placeholder="Ej. 5"
value={cantidadDeSugerencias} value={cantidadDeSugerencias}
type="number" type="number"
min={1} min={1}

View File

@@ -1,9 +1,9 @@
import { useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import { Loader2 } from 'lucide-react' import { Loader2 } from 'lucide-react'
import { useState } from 'react' import { useEffect, useRef, useState } from 'react'
import type { AIGenerateSubjectInput, AIGenerateSubjectJsonInput } from '@/data' import type { AISubjectUnifiedInput } from '@/data'
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types' import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
import type { TablesInsert } from '@/types/supabase' import type { TablesInsert } from '@/types/supabase'
@@ -13,6 +13,7 @@ import {
useGenerateSubjectAI, useGenerateSubjectAI,
qk, qk,
useCreateSubjectManual, useCreateSubjectManual,
subjects_get_maybe,
} from '@/data' } from '@/data'
export function WizardControls({ export function WizardControls({
@@ -41,6 +42,101 @@ export function WizardControls({
const generateSubjectAI = useGenerateSubjectAI() const generateSubjectAI = useGenerateSubjectAI()
const createSubjectManual = useCreateSubjectManual() const createSubjectManual = useCreateSubjectManual()
const [isSpinningIA, setIsSpinningIA] = useState(false) const [isSpinningIA, setIsSpinningIA] = useState(false)
const [pollSubjectId, setPollSubjectId] = useState<string | null>(null)
const cancelledRef = useRef(false)
const pollStartedAtRef = useRef<number | null>(null)
useEffect(() => {
cancelledRef.current = false
return () => {
cancelledRef.current = true
}
}, [])
const subjectQuery = useQuery({
queryKey: pollSubjectId
? qk.asignaturaMaybe(pollSubjectId)
: ['asignaturas', 'detail-maybe', null],
queryFn: () => subjects_get_maybe(pollSubjectId as string),
enabled: Boolean(pollSubjectId),
refetchInterval: () => {
if (!pollSubjectId) return false
const startedAt = pollStartedAtRef.current ?? Date.now()
if (!pollStartedAtRef.current) pollStartedAtRef.current = startedAt
const elapsedMs = Date.now() - startedAt
return elapsedMs >= 6 * 60 * 1000 ? false : 3000
},
refetchIntervalInBackground: true,
staleTime: 0,
})
useEffect(() => {
if (!pollSubjectId) return
if (cancelledRef.current) return
const asig = subjectQuery.data
if (!asig) return
const estado = String(asig.estado).toLowerCase()
if (estado === 'generando') return
setPollSubjectId(null)
pollStartedAtRef.current = null
setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false }))
navigate({
to: `/planes/${asig.plan_estudio_id}/asignaturas/${asig.id}`,
state: { showConfetti: true },
})
}, [pollSubjectId, subjectQuery.data, navigate, setWizard])
useEffect(() => {
if (!pollSubjectId) return
if (!subjectQuery.isError) return
setPollSubjectId(null)
pollStartedAtRef.current = null
setIsSpinningIA(false)
setWizard((w) => ({
...w,
isLoading: false,
errorMessage:
(subjectQuery.error as any)?.message ??
'Error consultando el estado de la asignatura',
}))
}, [pollSubjectId, subjectQuery.isError, subjectQuery.error, setWizard])
const uploadAiAttachments = async (args: {
planId: string
files: Array<{ file: File }>
}): Promise<Array<string>> => {
const supabase = supabaseBrowser()
if (!args.files.length) return []
const runId = crypto.randomUUID()
const basePath = `planes/${args.planId}/asignaturas/ai/${runId}`
const keys: Array<string> = []
for (const f of args.files) {
const safeName = (f.file.name || 'archivo').replace(/[\\/]+/g, '_')
const key = `${basePath}/${crypto.randomUUID()}-${safeName}`
const { error } = await supabase.storage
.from('ai-storage')
.upload(key, f.file, {
contentType: f.file.type || undefined,
})
if (error) throw new Error(error.message)
keys.push(key)
}
return keys
}
const handleCreate = async () => { const handleCreate = async () => {
setWizard((w) => ({ setWizard((w) => ({
...w, ...w,
@@ -48,48 +144,89 @@ export function WizardControls({
errorMessage: null, errorMessage: null,
})) }))
let startedPolling = false
try { try {
if (wizard.tipoOrigen === 'IA_SIMPLE') { if (wizard.tipoOrigen === 'IA_SIMPLE') {
const aiInput: AIGenerateSubjectInput = { if (!wizard.plan_estudio_id) {
throw new Error('Plan de estudio inválido.')
}
if (!wizard.datosBasicos.estructuraId) {
throw new Error('Estructura inválida.')
}
if (!wizard.datosBasicos.nombre.trim()) {
throw new Error('Nombre inválido.')
}
if (wizard.datosBasicos.creditos == null) {
throw new Error('Créditos inválidos.')
}
console.log(`${new Date().toISOString()} - Insertando asignatura IA`)
const supabase = supabaseBrowser()
const placeholder: TablesInsert<'asignaturas'> = {
plan_estudio_id: wizard.plan_estudio_id, plan_estudio_id: wizard.plan_estudio_id,
datosBasicos: { estructura_id: wizard.datosBasicos.estructuraId,
nombre: wizard.datosBasicos.nombre, nombre: wizard.datosBasicos.nombre,
codigo: wizard.datosBasicos.codigo, codigo: wizard.datosBasicos.codigo ?? null,
tipo: wizard.datosBasicos.tipo!, tipo: wizard.datosBasicos.tipo ?? undefined,
creditos: wizard.datosBasicos.creditos!, creditos: wizard.datosBasicos.creditos,
horasIndependientes: wizard.datosBasicos.horasIndependientes, horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
horasAcademicas: wizard.datosBasicos.horasAcademicas, horas_independientes: wizard.datosBasicos.horasIndependientes ?? null,
estructuraId: wizard.datosBasicos.estructuraId!, estado: 'generando',
tipo_origen: 'IA',
}
const { data: inserted, error: insertError } = await supabase
.from('asignaturas')
.insert(placeholder)
.select('id,plan_estudio_id')
.single()
if (insertError) throw new Error(insertError.message)
const subjectId = inserted.id
setIsSpinningIA(true)
const archivosAdjuntos = await uploadAiAttachments({
planId: wizard.plan_estudio_id,
files: (wizard.iaConfig?.archivosAdjuntos ?? []).map((x) => ({
file: x.file,
})),
})
const payload: AISubjectUnifiedInput = {
datosUpdate: {
id: subjectId,
plan_estudio_id: wizard.plan_estudio_id,
estructura_id: wizard.datosBasicos.estructuraId,
nombre: wizard.datosBasicos.nombre,
codigo: wizard.datosBasicos.codigo ?? null,
tipo: wizard.datosBasicos.tipo ?? null,
creditos: wizard.datosBasicos.creditos,
horas_academicas: wizard.datosBasicos.horasAcademicas ?? null,
horas_independientes:
wizard.datosBasicos.horasIndependientes ?? null,
}, },
iaConfig: { iaConfig: {
descripcionEnfoqueAcademico: descripcionEnfoqueAcademico:
wizard.iaConfig!.descripcionEnfoqueAcademico, wizard.iaConfig?.descripcionEnfoqueAcademico ?? undefined,
instruccionesAdicionalesIA: instruccionesAdicionalesIA:
wizard.iaConfig!.instruccionesAdicionalesIA, wizard.iaConfig?.instruccionesAdicionalesIA ?? undefined,
archivosReferencia: wizard.iaConfig!.archivosReferencia, archivosAdjuntos,
repositoriosReferencia:
wizard.iaConfig!.repositoriosReferencia || [],
archivosAdjuntos: wizard.iaConfig!.archivosAdjuntos || [],
}, },
} }
console.log( console.log(
`${new Date().toISOString()} - Enviando a generar asignatura con IA`, `${new Date().toISOString()} - Disparando Edge IA asignatura (unified)`,
) )
setIsSpinningIA(true) await generateSubjectAI.mutateAsync(payload as any)
const asignatura = await generateSubjectAI.mutateAsync(aiInput)
// await new Promise((resolve) => setTimeout(resolve, 20000)) // debug
setIsSpinningIA(false)
// console.log(
// `${new Date().toISOString()} - Asignatura IA generada`,
// asignatura,
// )
navigate({ // Inicia polling; el efecto navega cuando deje de estar "generando".
to: `/planes/${wizard.plan_estudio_id}/asignaturas/${asignatura.id}`, startedPolling = true
state: { showConfetti: true }, pollStartedAtRef.current = Date.now()
}) setPollSubjectId(subjectId)
return return
} }
@@ -108,6 +245,15 @@ export function WizardControls({
const supabase = supabaseBrowser() const supabase = supabaseBrowser()
setIsSpinningIA(true)
const archivosAdjuntos = await uploadAiAttachments({
planId: wizard.plan_estudio_id,
files: (wizard.iaConfig?.archivosAdjuntos ?? []).map((x) => ({
file: x.file,
})),
})
const placeholders: Array<TablesInsert<'asignaturas'>> = selected.map( const placeholders: Array<TablesInsert<'asignaturas'>> = selected.map(
(s): TablesInsert<'asignaturas'> => ({ (s): TablesInsert<'asignaturas'> => ({
plan_estudio_id: wizard.plan_estudio_id, plan_estudio_id: wizard.plan_estudio_id,
@@ -141,16 +287,33 @@ export function WizardControls({
// Disparar generación en paralelo (no bloquear navegación) // Disparar generación en paralelo (no bloquear navegación)
insertedIds.forEach((id, idx) => { insertedIds.forEach((id, idx) => {
const s = selected[idx] const s = selected[idx]
const payload: AIGenerateSubjectJsonInput = { const creditosForEdge =
typeof s.creditos === 'number' && s.creditos > 0
? s.creditos
: undefined
const payload: AISubjectUnifiedInput = {
datosUpdate: {
id, id,
plan_estudio_id: wizard.plan_estudio_id,
estructura_id: wizard.estructuraId ?? undefined,
nombre: s.nombre,
codigo: s.codigo ?? null,
tipo: s.tipo ?? null,
creditos: creditosForEdge,
horas_academicas: s.horasAcademicas ?? null,
horas_independientes: s.horasIndependientes ?? null,
numero_ciclo: s.numero_ciclo ?? null,
linea_plan_id: s.linea_plan_id ?? null,
},
iaConfig: {
descripcionEnfoqueAcademico: s.descripcion, descripcionEnfoqueAcademico: s.descripcion,
// (opcionales) parches directos si el edge los usa instruccionesAdicionalesIA:
estructura_id: wizard.estructuraId, wizard.iaConfig?.instruccionesAdicionalesIA ?? undefined,
linea_plan_id: s.linea_plan_id, archivosAdjuntos,
numero_ciclo: s.numero_ciclo, },
} }
void generateSubjectAI.mutateAsync(payload).catch((e) => { void generateSubjectAI.mutateAsync(payload as any).catch((e) => {
console.error('Error generando asignatura IA (multiple):', e) console.error('Error generando asignatura IA (multiple):', e)
}) })
}) })
@@ -166,6 +329,8 @@ export function WizardControls({
resetScroll: false, resetScroll: false,
}) })
setIsSpinningIA(false)
return return
} }
@@ -195,16 +360,19 @@ export function WizardControls({
} }
} catch (err: any) { } catch (err: any) {
setIsSpinningIA(false) setIsSpinningIA(false)
setPollSubjectId(null)
setWizard((w) => ({ setWizard((w) => ({
...w, ...w,
isLoading: false, isLoading: false,
errorMessage: err?.message ?? 'Error creando la asignatura', errorMessage: err?.message ?? 'Error creando la asignatura',
})) }))
} finally { } finally {
if (!startedPolling) {
setIsSpinningIA(false) setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false })) setWizard((w) => ({ ...w, isLoading: false }))
} }
} }
}
return ( return (
<div className="flex grow items-center justify-between"> <div className="flex grow items-center justify-between">

View File

@@ -45,6 +45,7 @@ export function WizardControls({
const [isSpinningIA, setIsSpinningIA] = useState(false) const [isSpinningIA, setIsSpinningIA] = useState(false)
const [pollPlanId, setPollPlanId] = useState<string | null>(null) const [pollPlanId, setPollPlanId] = useState<string | null>(null)
const cancelledRef = useRef(false) const cancelledRef = useRef(false)
const pollStartedAtRef = useRef<number | null>(null)
// const supabaseClient = supabaseBrowser() // const supabaseClient = supabaseBrowser()
// const persistPlanFromAI = usePersistPlanFromAI() // const persistPlanFromAI = usePersistPlanFromAI()
@@ -61,7 +62,15 @@ export function WizardControls({
: ['planes', 'detail-maybe', null], : ['planes', 'detail-maybe', null],
queryFn: () => plans_get_maybe(pollPlanId as string), queryFn: () => plans_get_maybe(pollPlanId as string),
enabled: Boolean(pollPlanId), enabled: Boolean(pollPlanId),
refetchInterval: pollPlanId ? 3000 : false, refetchInterval: () => {
if (!pollPlanId) return false
const startedAt = pollStartedAtRef.current ?? Date.now()
if (!pollStartedAtRef.current) pollStartedAtRef.current = startedAt
const elapsedMs = Date.now() - startedAt
return elapsedMs >= 6 * 60 * 1000 ? false : 3000
},
refetchIntervalInBackground: true, refetchIntervalInBackground: true,
staleTime: 0, staleTime: 0,
}) })
@@ -80,6 +89,7 @@ export function WizardControls({
if (clave.startsWith('BORRADOR')) { if (clave.startsWith('BORRADOR')) {
setPollPlanId(null) setPollPlanId(null)
pollStartedAtRef.current = null
setIsSpinningIA(false) setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false })) setWizard((w) => ({ ...w, isLoading: false }))
navigate({ navigate({
@@ -92,6 +102,7 @@ export function WizardControls({
if (clave.startsWith('FALLID')) { if (clave.startsWith('FALLID')) {
// Detenemos el polling primero para evitar loops. // Detenemos el polling primero para evitar loops.
setPollPlanId(null) setPollPlanId(null)
pollStartedAtRef.current = null
setIsSpinningIA(false) setIsSpinningIA(false)
deletePlan deletePlan
@@ -113,6 +124,7 @@ export function WizardControls({
if (!pollPlanId) return if (!pollPlanId) return
if (!planQuery.isError) return if (!planQuery.isError) return
setPollPlanId(null) setPollPlanId(null)
pollStartedAtRef.current = null
setIsSpinningIA(false) setIsSpinningIA(false)
setWizard((w) => ({ setWizard((w) => ({
...w, ...w,
@@ -175,6 +187,7 @@ export function WizardControls({
} }
// Inicia polling con React Query; el efecto navega o marca error. // Inicia polling con React Query; el efecto navega o marca error.
pollStartedAtRef.current = Date.now()
setPollPlanId(String(planId)) setPollPlanId(String(planId))
return return
} }

View File

@@ -15,7 +15,6 @@ import type {
TipoAsignatura, TipoAsignatura,
UUID, UUID,
} from '../types/domain' } from '../types/domain'
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
import type { import type {
AsignaturaSugerida, AsignaturaSugerida,
DataAsignaturaSugerida, DataAsignaturaSugerida,
@@ -178,54 +177,49 @@ export async function subjects_create_manual(
return requireData(data, 'No se pudo crear la asignatura.') return requireData(data, 'No se pudo crear la asignatura.')
} }
export type AIGenerateSubjectInput = { /**
plan_estudio_id: Asignatura['plan_estudio_id'] * Nuevo payload unificado (JSON) para la Edge `ai_generate_subject`.
datosBasicos: { * - Siempre incluye `datosUpdate.plan_estudio_id`.
nombre: Asignatura['nombre'] * - `datosUpdate.id` es opcional (si no existe, la Edge puede crear).
codigo?: Asignatura['codigo'] * En el frontend, insertamos primero y usamos `id` para actualizar.
tipo: Asignatura['tipo'] | null */
creditos: Asignatura['creditos'] | null export type AISubjectUnifiedInput = {
horasAcademicas?: Asignatura['horas_academicas'] | null datosUpdate: Partial<{
horasIndependientes?: Asignatura['horas_independientes'] | null id: string
estructuraId: Asignatura['estructura_id'] | null plan_estudio_id: string
estructura_id: string
nombre: string
codigo: string | null
tipo: string | null
creditos: number
horas_academicas: number | null
horas_independientes: number | null
numero_ciclo: number | null
linea_plan_id: string | null
orden_celda: number | null
}> & {
plan_estudio_id: string
} }
// clonInterno?: {
// facultadId?: string
// carreraId?: string
// planOrigenId?: string
// asignaturaOrigenId?: string | null
// }
// clonTradicional?: {
// archivoWordAsignaturaId: string | null
// archivosAdicionalesIds: Array<string>
// }
iaConfig?: { iaConfig?: {
descripcionEnfoqueAcademico: string descripcionEnfoqueAcademico?: string
instruccionesAdicionalesIA: string instruccionesAdicionalesIA?: string
archivosReferencia: Array<string> archivosAdjuntos?: Array<string>
repositoriosReferencia?: Array<string>
archivosAdjuntos?: Array<UploadedFile>
} }
} }
/** export async function subjects_get_maybe(
* Edge (JSON): actualizar/llenar una asignatura existente por id. subjectId: UUID,
* Nota: este flujo NO acepta `instruccionesAdicionalesIA` (solo FormData lo usa). ): Promise<Asignatura | null> {
*/ const supabase = supabaseBrowser()
export type AIGenerateSubjectJsonInput = Partial<{
plan_estudio_id: Asignatura['plan_estudio_id'] const { data, error } = await supabase
nombre: Asignatura['nombre'] .from('asignaturas')
codigo: Asignatura['codigo'] .select('id,plan_estudio_id,estado')
tipo: Asignatura['tipo'] | null .eq('id', subjectId)
creditos: Asignatura['creditos'] .maybeSingle()
horas_academicas: Asignatura['horas_academicas'] | null
horas_independientes: Asignatura['horas_independientes'] | null throwIfError(error)
estructura_id: Asignatura['estructura_id'] | null return (data ?? null) as unknown as Asignatura | null
linea_plan_id: Asignatura['linea_plan_id'] | null
numero_ciclo: Asignatura['numero_ciclo'] | null
descripcionEnfoqueAcademico: string
}> & {
id: Asignatura['id']
} }
export type GenerateSubjectSuggestionsInput = { export type GenerateSubjectSuggestionsInput = {
@@ -263,30 +257,8 @@ export async function generate_subject_suggestions(
} }
export async function ai_generate_subject( export async function ai_generate_subject(
input: AIGenerateSubjectInput | AIGenerateSubjectJsonInput, input: AISubjectUnifiedInput,
): Promise<any> { ): Promise<any> {
if ('datosBasicos' in input) {
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) => {
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
})
return invokeEdge<any>(
EDGE.ai_generate_subject,
edgeFunctionBody,
undefined,
supabaseBrowser(),
)
}
return invokeEdge<any>(EDGE.ai_generate_subject, input, { return invokeEdge<any>(EDGE.ai_generate_subject, input, {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
}) })

View File

@@ -85,7 +85,18 @@ export function usePlanAsignaturas(planId: UUID | null | undefined) {
const hayGenerando = data.some( const hayGenerando = data.some(
(a: any) => (a as { estado?: unknown }).estado === 'generando', (a: any) => (a as { estado?: unknown }).estado === 'generando',
) )
return hayGenerando ? 500 : false
const qAny = query as any
if (!hayGenerando) {
qAny.__generandoSince = null
return false
}
const startedAt = qAny.__generandoSince ?? Date.now()
if (!qAny.__generandoSince) qAny.__generandoSince = startedAt
const elapsedMs = Date.now() - startedAt
return elapsedMs >= 6 * 60 * 1000 ? false : 3000
}, },
refetchIntervalInBackground: true, refetchIntervalInBackground: true,
}) })

View File

@@ -23,6 +23,8 @@ export const qk = {
sugerenciasAsignaturas: () => ['asignaturas', 'sugerencias'] as const, sugerenciasAsignaturas: () => ['asignaturas', 'sugerencias'] as const,
asignatura: (asignaturaId: string) => asignatura: (asignaturaId: string) =>
['asignaturas', 'detail', asignaturaId] as const, ['asignaturas', 'detail', asignaturaId] as const,
asignaturaMaybe: (asignaturaId: string) =>
['asignaturas', 'detail-maybe', asignaturaId] as const,
asignaturaBibliografia: (asignaturaId: string) => asignaturaBibliografia: (asignaturaId: string) =>
['asignaturas', asignaturaId, 'bibliografia'] as const, ['asignaturas', asignaturaId, 'bibliografia'] as const,
asignaturaHistorial: (asignaturaId: string) => asignaturaHistorial: (asignaturaId: string) =>

View File

@@ -2,6 +2,7 @@ import {
createFileRoute, createFileRoute,
Outlet, Outlet,
Link, Link,
useLocation,
useParams, useParams,
useRouterState, useRouterState,
} from '@tanstack/react-router' } from '@tanstack/react-router'
@@ -9,6 +10,7 @@ import { ArrowLeft, GraduationCap } from 'lucide-react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { lateralConfetti } from '@/components/ui/lateral-confetti'
import { useSubject, useUpdateAsignatura } from '@/data' import { useSubject, useUpdateAsignatura } from '@/data'
export const Route = createFileRoute( export const Route = createFileRoute(
@@ -62,8 +64,7 @@ interface DatosPlan {
} }
function AsignaturaLayout() { function AsignaturaLayout() {
const routerState = useRouterState() const location = useLocation()
const state = routerState.location.state as any
const { asignaturaId } = useParams({ const { asignaturaId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId', from: '/planes/$planId/asignaturas/$asignaturaId',
}) })
@@ -117,6 +118,14 @@ function AsignaturaLayout() {
select: (state) => state.location.pathname, select: (state) => state.location.pathname,
}) })
// Confetti al llegar desde creación IA
useEffect(() => {
if ((location.state as any)?.showConfetti) {
lateralConfetti()
window.history.replaceState({}, document.title)
}
}, [location.state])
if (loadingAsig) { if (loadingAsig) {
return ( return (
<div className="flex h-screen items-center justify-center bg-[#0b1d3a] text-white"> <div className="flex h-screen items-center justify-center bg-[#0b1d3a] text-white">
@@ -130,7 +139,7 @@ function AsignaturaLayout() {
return ( return (
<div> <div>
<section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white"> <section className="bg-linear-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
<div className="mx-auto max-w-7xl px-6 py-10"> <div className="mx-auto max-w-7xl px-6 py-10">
<Link <Link
to="/planes/$planId/asignaturas" to="/planes/$planId/asignaturas"

View File

@@ -1213,12 +1213,7 @@ export type Database = {
unaccent_immutable: { Args: { '': string }; Returns: string } unaccent_immutable: { Args: { '': string }; Returns: string }
} }
Enums: { Enums: {
estado_asignatura: estado_asignatura: 'borrador' | 'revisada' | 'aprobada' | 'generando'
| 'borrador'
| 'revisada'
| 'aprobada'
| 'generando'
| 'fallida'
estado_conversacion: 'ACTIVA' | 'ARCHIVANDO' | 'ARCHIVADA' | 'ERROR' estado_conversacion: 'ACTIVA' | 'ARCHIVANDO' | 'ARCHIVADA' | 'ERROR'
estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA' estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA'
fuente_cambio: 'HUMANO' | 'IA' fuente_cambio: 'HUMANO' | 'IA'
@@ -1392,13 +1387,7 @@ export const Constants = {
}, },
public: { public: {
Enums: { Enums: {
estado_asignatura: [ estado_asignatura: ['borrador', 'revisada', 'aprobada', 'generando'],
'borrador',
'revisada',
'aprobada',
'generando',
'fallida',
],
estado_conversacion: ['ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR'], estado_conversacion: ['ACTIVA', 'ARCHIVANDO', 'ARCHIVADA', 'ERROR'],
estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'], estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'],
fuente_cambio: ['HUMANO', 'IA'], fuente_cambio: ['HUMANO', 'IA'],