Creación de planes de estudio con polling debido a mandar crear los datos en segundo plano

This commit is contained in:
2026-02-25 17:37:06 -06:00
parent f28804bb5b
commit 4d1f102acb
5 changed files with 534 additions and 373 deletions

View File

@@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router'
import { Loader2 } from 'lucide-react'
import { useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import type { AIGeneratePlanInput } from '@/data'
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
@@ -8,8 +9,13 @@ import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
// import type { Database } from '@/types/supabase'
import { Button } from '@/components/ui/button'
// import { supabaseBrowser } from '@/data'
import { useCreatePlanManual, useGeneratePlanAI } from '@/data/hooks/usePlans'
import { plans_get_maybe } from '@/data/api/plans.api'
import {
useCreatePlanManual,
useDeletePlanEstudio,
useGeneratePlanAI,
} from '@/data/hooks/usePlans'
import { qk } from '@/data/query/keys'
export function WizardControls({
errorMessage,
@@ -35,10 +41,88 @@ export function WizardControls({
const navigate = useNavigate()
const generatePlanAI = useGeneratePlanAI()
const createPlanManual = useCreatePlanManual()
const deletePlan = useDeletePlanEstudio()
const [isSpinningIA, setIsSpinningIA] = useState(false)
const [pollPlanId, setPollPlanId] = useState<string | null>(null)
const cancelledRef = useRef(false)
// const supabaseClient = supabaseBrowser()
// const persistPlanFromAI = usePersistPlanFromAI()
useEffect(() => {
cancelledRef.current = false
return () => {
cancelledRef.current = true
}
}, [])
const planQuery = useQuery({
queryKey: pollPlanId
? qk.planMaybe(pollPlanId)
: ['planes', 'detail-maybe', null],
queryFn: () => plans_get_maybe(pollPlanId as string),
enabled: Boolean(pollPlanId),
refetchInterval: pollPlanId ? 3000 : false,
refetchIntervalInBackground: true,
staleTime: 0,
})
useEffect(() => {
if (!pollPlanId) return
if (cancelledRef.current) return
// Si aún no existe en BDD, seguimos esperando.
const plan = planQuery.data
if (!plan) return
const clave = String(plan.estados_plan?.clave ?? '').toUpperCase()
if (clave.startsWith('GENERANDO')) return
if (clave.startsWith('BORRADOR')) {
setPollPlanId(null)
setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false }))
navigate({
to: `/planes/${plan.id}`,
state: { showConfetti: true },
})
return
}
if (clave.startsWith('FALLID')) {
// Detenemos el polling primero para evitar loops.
setPollPlanId(null)
setIsSpinningIA(false)
deletePlan
.mutateAsync(plan.id)
.catch(() => {
// Si falla el borrado, igual mostramos el error.
})
.finally(() => {
setWizard((w) => ({
...w,
isLoading: false,
errorMessage: 'La generación del plan falló',
}))
})
}
}, [pollPlanId, planQuery.data, navigate, setWizard, deletePlan])
useEffect(() => {
if (!pollPlanId) return
if (!planQuery.isError) return
setPollPlanId(null)
setIsSpinningIA(false)
setWizard((w) => ({
...w,
isLoading: false,
errorMessage:
(planQuery.error as any)?.message ??
'Error consultando el estado del plan',
}))
}, [pollPlanId, planQuery.isError, planQuery.error, setWizard])
const handleCreate = async () => {
// Start loading
setWizard(
@@ -82,14 +166,16 @@ export function WizardControls({
console.log(`${new Date().toISOString()} - Enviando a generar plan IA`)
setIsSpinningIA(true)
const plan = await generatePlanAI.mutateAsync(aiInput as any)
setIsSpinningIA(false)
console.log(`${new Date().toISOString()} - Plan IA generado`, plan)
const resp: any = await generatePlanAI.mutateAsync(aiInput as any)
const planId = resp?.plan?.id ?? resp?.id
console.log(`${new Date().toISOString()} - Plan IA generado`, resp)
navigate({
to: `/planes/${plan.id}`,
state: { showConfetti: true },
})
if (!planId) {
throw new Error('No se pudo obtener el id del plan generado por IA')
}
// Inicia polling con React Query; el efecto navega o marca error.
setPollPlanId(String(planId))
return
}
@@ -114,14 +200,18 @@ export function WizardControls({
}
} catch (err: any) {
setIsSpinningIA(false)
setPollPlanId(null)
setWizard((w) => ({
...w,
isLoading: false,
errorMessage: err?.message ?? 'Error generando el plan',
}))
} finally {
setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false }))
// Si entramos en polling, el loading se corta desde el efecto terminal.
if (!pollPlanId) {
setIsSpinningIA(false)
setWizard((w) => ({ ...w, isLoading: false }))
}
}
}