Add AI progress loader and enhance suggestion generation logic
- Introduced AIProgressLoader component to display loading progress and messages during suggestion generation. - Updated PasoSugerenciasForm to manage loading state and display tooltip for preserved suggestions. - Adjusted suggestion limits and removed unused ciclo input from state.
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { RefreshCw, Sparkles } from 'lucide-react'
|
import { RefreshCw, Sparkles, X } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
|
||||||
import type { Dispatch, SetStateAction } from 'react'
|
import type { Dispatch, SetStateAction } from 'react'
|
||||||
@@ -7,7 +8,14 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
import { generate_subject_suggestions, usePlan } from '@/data'
|
import { generate_subject_suggestions, usePlan } from '@/data'
|
||||||
|
import { AIProgressLoader } from '@/features/asignaturas/nueva/AIProgressLoader'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export default function PasoSugerenciasForm({
|
export default function PasoSugerenciasForm({
|
||||||
@@ -17,9 +25,11 @@ export default function PasoSugerenciasForm({
|
|||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
onChange: Dispatch<SetStateAction<NewSubjectWizardState>>
|
onChange: Dispatch<SetStateAction<NewSubjectWizardState>>
|
||||||
}) {
|
}) {
|
||||||
const ciclo = wizard.iaMultiple?.ciclo ?? ''
|
|
||||||
const enfoque = wizard.iaMultiple?.enfoque ?? ''
|
const enfoque = wizard.iaMultiple?.enfoque ?? ''
|
||||||
const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
||||||
|
const isLoading = wizard.iaMultiple?.isLoading ?? false
|
||||||
|
|
||||||
|
const [showConservacionTooltip, setShowConservacionTooltip] = useState(false)
|
||||||
|
|
||||||
const setIaMultiple = (
|
const setIaMultiple = (
|
||||||
patch: Partial<NonNullable<NewSubjectWizardState['iaMultiple']>>,
|
patch: Partial<NonNullable<NewSubjectWizardState['iaMultiple']>>,
|
||||||
@@ -28,9 +38,9 @@ export default function PasoSugerenciasForm({
|
|||||||
(w): NewSubjectWizardState => ({
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...w,
|
||||||
iaMultiple: {
|
iaMultiple: {
|
||||||
ciclo: w.iaMultiple?.ciclo ?? null,
|
|
||||||
enfoque: w.iaMultiple?.enfoque ?? '',
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
|
isLoading: w.iaMultiple?.isLoading ?? false,
|
||||||
...patch,
|
...patch,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -48,32 +58,31 @@ export default function PasoSugerenciasForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onGenerarSugerencias = async () => {
|
const onGenerarSugerencias = async () => {
|
||||||
|
const hadNoSugerenciasBefore = wizard.sugerencias.length === 0
|
||||||
const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected)
|
const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected)
|
||||||
|
|
||||||
onChange((w) => ({
|
onChange((w) => ({
|
||||||
...w,
|
...w,
|
||||||
isLoading: true,
|
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
sugerencias: sugerenciasConservadas,
|
sugerencias: sugerenciasConservadas,
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const numeroCiclo = wizard.iaMultiple?.ciclo
|
|
||||||
if (!numeroCiclo || !Number.isFinite(numeroCiclo) || numeroCiclo <= 0) {
|
|
||||||
onChange((w) => ({
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
errorMessage: 'Ingresa un número de ciclo válido.',
|
|
||||||
}))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const cantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
const cantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
||||||
if (!Number.isFinite(cantidad) || cantidad <= 0 || cantidad > 50) {
|
if (!Number.isFinite(cantidad) || cantidad <= 0 || cantidad > 15) {
|
||||||
onChange((w) => ({
|
onChange((w) => ({
|
||||||
...w,
|
...w,
|
||||||
|
errorMessage: 'La cantidad de sugerencias debe ser entre 1 y 15.',
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: 'La cantidad de sugerencias debe ser entre 1 y 50.',
|
},
|
||||||
}))
|
}))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -82,7 +91,6 @@ export default function PasoSugerenciasForm({
|
|||||||
|
|
||||||
const nuevasSugerencias = await generate_subject_suggestions({
|
const nuevasSugerencias = await generate_subject_suggestions({
|
||||||
plan_estudio_id: wizard.plan_estudio_id,
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
numero_de_ciclo: numeroCiclo,
|
|
||||||
enfoque: enfoqueTrim ? enfoqueTrim : undefined,
|
enfoque: enfoqueTrim ? enfoqueTrim : undefined,
|
||||||
cantidad_de_sugerencias: cantidad,
|
cantidad_de_sugerencias: cantidad,
|
||||||
sugerencias_conservadas: sugerenciasConservadas.map((s) => ({
|
sugerencias_conservadas: sugerenciasConservadas.map((s) => ({
|
||||||
@@ -91,11 +99,19 @@ export default function PasoSugerenciasForm({
|
|||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (hadNoSugerenciasBefore && nuevasSugerencias.length > 0) {
|
||||||
|
setShowConservacionTooltip(true)
|
||||||
|
}
|
||||||
|
|
||||||
onChange(
|
onChange(
|
||||||
(w): NewSubjectWizardState => ({
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...w,
|
||||||
isLoading: false,
|
|
||||||
sugerencias: [...nuevasSugerencias, ...sugerenciasConservadas],
|
sugerencias: [...nuevasSugerencias, ...sugerenciasConservadas],
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -104,8 +120,12 @@ export default function PasoSugerenciasForm({
|
|||||||
onChange(
|
onChange(
|
||||||
(w): NewSubjectWizardState => ({
|
(w): NewSubjectWizardState => ({
|
||||||
...w,
|
...w,
|
||||||
isLoading: false,
|
|
||||||
errorMessage: message,
|
errorMessage: message,
|
||||||
|
iaMultiple: {
|
||||||
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
|
isLoading: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -122,48 +142,23 @@ export default function PasoSugerenciasForm({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-end gap-3 md:flex-row">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Input Ciclo */}
|
<div className="w-full">
|
||||||
<div className="w-full md:w-36">
|
|
||||||
<Label className="text-muted-foreground mb-1 block text-xs">
|
|
||||||
Número de ciclo
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Ej. 3"
|
|
||||||
value={ciclo}
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={999}
|
|
||||||
onChange={(e) => {
|
|
||||||
const raw = e.target.value
|
|
||||||
if (raw === '') {
|
|
||||||
setIaMultiple({ ciclo: null })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const asNumber = Number(raw)
|
|
||||||
if (!Number.isFinite(asNumber)) return
|
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
|
||||||
const capped = Math.min(n >= 1 ? n : 1, 999)
|
|
||||||
setIaMultiple({ ciclo: capped })
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Input Enfoque */}
|
|
||||||
<div className="w-full flex-1">
|
|
||||||
<Label className="text-muted-foreground mb-1 block text-xs">
|
<Label className="text-muted-foreground mb-1 block text-xs">
|
||||||
Enfoque (opcional)
|
Enfoque (opcional)
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Textarea
|
||||||
placeholder="Ej. Enfocado en normativa mexicana y tecnología"
|
placeholder="Ej. Enfocado en normativa mexicana y tecnología"
|
||||||
value={enfoque}
|
value={enfoque}
|
||||||
|
maxLength={7000}
|
||||||
|
rows={4}
|
||||||
onChange={(e) => setIaMultiple({ enfoque: e.target.value })}
|
onChange={(e) => setIaMultiple({ enfoque: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 flex w-full flex-col items-end gap-3 md:flex-row">
|
<div className="mt-3 flex w-full flex-col items-end justify-between gap-3 sm:flex-row">
|
||||||
<div className="w-full md:w-44">
|
<div className="w-full sm:w-44">
|
||||||
<Label className="text-muted-foreground mb-1 block text-xs">
|
<Label className="text-muted-foreground mb-1 block text-xs">
|
||||||
Cantidad de sugerencias
|
Cantidad de sugerencias
|
||||||
</Label>
|
</Label>
|
||||||
@@ -172,7 +167,7 @@ export default function PasoSugerenciasForm({
|
|||||||
value={cantidadDeSugerencias}
|
value={cantidadDeSugerencias}
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
max={50}
|
max={15}
|
||||||
step={1}
|
step={1}
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@@ -186,7 +181,7 @@ export default function PasoSugerenciasForm({
|
|||||||
const asNumber = Number(raw)
|
const asNumber = Number(raw)
|
||||||
if (!Number.isFinite(asNumber)) return
|
if (!Number.isFinite(asNumber)) return
|
||||||
const n = Math.floor(Math.abs(asNumber))
|
const n = Math.floor(Math.abs(asNumber))
|
||||||
const capped = Math.min(n >= 1 ? n : 1, 50)
|
const capped = Math.min(n >= 1 ? n : 1, 15)
|
||||||
setIaMultiple({ cantidadDeSugerencias: capped })
|
setIaMultiple({ cantidadDeSugerencias: capped })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -197,19 +192,21 @@ export default function PasoSugerenciasForm({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className="h-9 gap-1.5"
|
className="h-9 gap-1.5"
|
||||||
onClick={onGenerarSugerencias}
|
onClick={onGenerarSugerencias}
|
||||||
disabled={wizard.isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
Generar sugerencias
|
{wizard.sugerencias.length > 0
|
||||||
|
? 'Generar más sugerencias'
|
||||||
|
: 'Generar sugerencias'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-2 text-xs">
|
|
||||||
Al generar más sugerencias, solo se conservarán las asignaturas que
|
|
||||||
hayas seleccionado.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AIProgressLoader
|
||||||
|
isLoading={isLoading}
|
||||||
|
cantidadDeSugerencias={cantidadDeSugerencias}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* --- HEADER LISTA --- */}
|
{/* --- HEADER LISTA --- */}
|
||||||
<div className="mb-3 flex items-center justify-between">
|
<div className="mb-3 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -221,9 +218,32 @@ export default function PasoSugerenciasForm({
|
|||||||
{plan ? `${plan.nivel} en ${plan.nombre}` : '...'}
|
{plan ? `${plan.nivel} en ${plan.nombre}` : '...'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted text-foreground inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-semibold">
|
<Tooltip open={showConservacionTooltip}>
|
||||||
{wizard.sugerencias.filter((s) => s.selected).length} seleccionadas
|
<TooltipTrigger asChild>
|
||||||
|
<div className="bg-muted text-foreground inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-sm font-semibold">
|
||||||
|
<span aria-hidden>📌</span>
|
||||||
|
{wizard.sugerencias.filter((s) => s.selected).length}{' '}
|
||||||
|
seleccionadas
|
||||||
</div>
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" sideOffset={8} className="max-w-xs">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="flex-1 text-sm">
|
||||||
|
Al generar más sugerencias, se conservarán las asignaturas
|
||||||
|
seleccionadas.
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-5 w-5"
|
||||||
|
onClick={() => setShowConservacionTooltip(false)}
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* --- LISTA DE ASIGNATURAS --- */}
|
{/* --- LISTA DE ASIGNATURAS --- */}
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ export type AIGenerateSubjectInput = {
|
|||||||
|
|
||||||
export type GenerateSubjectSuggestionsInput = {
|
export type GenerateSubjectSuggestionsInput = {
|
||||||
plan_estudio_id: UUID
|
plan_estudio_id: UUID
|
||||||
numero_de_ciclo: number
|
|
||||||
enfoque?: string
|
enfoque?: string
|
||||||
cantidad_de_sugerencias: number
|
cantidad_de_sugerencias: number
|
||||||
sugerencias_conservadas: Array<{ nombre: string; descripcion: string }>
|
sugerencias_conservadas: Array<{ nombre: string; descripcion: string }>
|
||||||
|
|||||||
151
src/features/asignaturas/nueva/AIProgressLoader.tsx
Normal file
151
src/features/asignaturas/nueva/AIProgressLoader.tsx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from 'react'
|
||||||
|
|
||||||
|
// --- DEFINICIÓN DE MENSAJES ---
|
||||||
|
const MENSAJES_CORTOS = [
|
||||||
|
// Hasta 5 sugerencias (6 mensajes)
|
||||||
|
'Analizando el plan de estudios...',
|
||||||
|
'Identificando áreas de oportunidad...',
|
||||||
|
'Consultando bases de datos académicas...',
|
||||||
|
'Redactando competencias específicas...',
|
||||||
|
'Calculando créditos y horas...',
|
||||||
|
'Afinando los últimos detalles...',
|
||||||
|
]
|
||||||
|
|
||||||
|
const MENSAJES_MEDIOS = [
|
||||||
|
// Hasta 10 sugerencias (10 mensajes)
|
||||||
|
'Conectando con el motor de IA...',
|
||||||
|
'Analizando estructura curricular...',
|
||||||
|
'Buscando asignaturas compatibles...',
|
||||||
|
'Verificando prerrequisitos...',
|
||||||
|
'Generando descripciones detalladas...',
|
||||||
|
'Balanceando cargas académicas...',
|
||||||
|
'Asignando horas independientes...',
|
||||||
|
'Validando coherencia temática...',
|
||||||
|
'Formateando resultados...',
|
||||||
|
'Finalizando generación...',
|
||||||
|
]
|
||||||
|
|
||||||
|
const MENSAJES_LARGOS = [
|
||||||
|
// Más de 10 sugerencias (14 mensajes)
|
||||||
|
'Iniciando procesamiento masivo...',
|
||||||
|
'Escaneando retícula completa...',
|
||||||
|
'Detectando líneas de investigación...',
|
||||||
|
'Generando primer bloque de asignaturas...',
|
||||||
|
'Evaluando pertinencia académica...',
|
||||||
|
'Optimizando créditos por ciclo...',
|
||||||
|
'Redactando objetivos de aprendizaje...',
|
||||||
|
'Generando segundo bloque...',
|
||||||
|
'Revisando duplicidad de contenidos...',
|
||||||
|
'Ajustando tiempos teóricos y prácticos...',
|
||||||
|
'Verificando normatividad...',
|
||||||
|
'Compilando sugerencias...',
|
||||||
|
'Aplicando formato final...',
|
||||||
|
'Casi listo, gracias por tu paciencia...',
|
||||||
|
]
|
||||||
|
|
||||||
|
interface AIProgressLoaderProps {
|
||||||
|
isLoading: boolean
|
||||||
|
cantidadDeSugerencias: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AIProgressLoader: React.FC<AIProgressLoaderProps> = ({
|
||||||
|
isLoading,
|
||||||
|
cantidadDeSugerencias,
|
||||||
|
}) => {
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
|
const [currentMessageIndex, setCurrentMessageIndex] = useState(0)
|
||||||
|
|
||||||
|
// 1. Seleccionar el grupo de mensajes según la cantidad
|
||||||
|
const messages = useMemo(() => {
|
||||||
|
if (cantidadDeSugerencias <= 5) return MENSAJES_CORTOS
|
||||||
|
if (cantidadDeSugerencias <= 10) return MENSAJES_MEDIOS
|
||||||
|
return MENSAJES_LARGOS
|
||||||
|
}, [cantidadDeSugerencias])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading) {
|
||||||
|
setProgress(0)
|
||||||
|
setCurrentMessageIndex(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CÁLCULO DEL TIEMPO TOTAL ---
|
||||||
|
// y = 4.07x + 10.93 (en segundos)
|
||||||
|
const estimatedSeconds = 4.07 * cantidadDeSugerencias + 10.93
|
||||||
|
const durationMs = estimatedSeconds * 1000
|
||||||
|
|
||||||
|
// Intervalo de actualización de la barra (cada 50ms para suavidad)
|
||||||
|
const updateInterval = 50
|
||||||
|
const totalSteps = durationMs / updateInterval
|
||||||
|
const incrementPerStep = 99 / totalSteps // Llegamos al 99% para esperar la respuesta real
|
||||||
|
|
||||||
|
// --- TIMER 1: BARRA DE PROGRESO ---
|
||||||
|
const progressTimer = setInterval(() => {
|
||||||
|
setProgress((prev) => {
|
||||||
|
const next = prev + incrementPerStep
|
||||||
|
return next >= 99 ? 99 : next // Topar en 99%
|
||||||
|
})
|
||||||
|
}, updateInterval)
|
||||||
|
|
||||||
|
// --- TIMER 2: MENSAJES (CADA 5 SEGUNDOS) ---
|
||||||
|
const messagesTimer = setInterval(() => {
|
||||||
|
setCurrentMessageIndex((prev) => {
|
||||||
|
// Si ya es el último mensaje, no avanzar más (no ciclar)
|
||||||
|
if (prev >= messages.length - 1) return prev
|
||||||
|
return prev + 1
|
||||||
|
})
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
// Cleanup al desmontar o cuando isLoading cambie
|
||||||
|
return () => {
|
||||||
|
clearInterval(progressTimer)
|
||||||
|
clearInterval(messagesTimer)
|
||||||
|
}
|
||||||
|
}, [isLoading, cantidadDeSugerencias, messages])
|
||||||
|
|
||||||
|
if (!isLoading) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="animate-in fade-in zoom-in m-2 mx-auto w-full max-w-md duration-300">
|
||||||
|
{/* Contenedor de la barra */}
|
||||||
|
<div className="relative pt-1">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<span className="inline-block rounded-full bg-blue-200 px-2 py-1 text-xs font-semibold text-blue-600 uppercase">
|
||||||
|
Generando IA
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<span className="inline-block text-xs font-semibold text-blue-600">
|
||||||
|
{Math.floor(progress)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Barra de fondo */}
|
||||||
|
<div className="mb-4 flex h-2 overflow-hidden rounded bg-blue-100 text-xs">
|
||||||
|
{/* Barra de progreso dinámica */}
|
||||||
|
<div
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
className="flex flex-col justify-center bg-blue-500 text-center whitespace-nowrap text-white shadow-none transition-all duration-75 ease-linear"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mensajes cambiantes */}
|
||||||
|
<div className="h-6 text-center">
|
||||||
|
{' '}
|
||||||
|
{/* Altura fija para evitar saltos */}
|
||||||
|
<p className="text-sm text-slate-500 italic transition-opacity duration-500">
|
||||||
|
{messages[currentMessageIndex]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nota de tiempo estimado (Opcional, transparencia operacional) */}
|
||||||
|
<p className="mt-2 text-center text-[10px] text-slate-400">
|
||||||
|
Tiempo estimado: ~{Math.ceil(4.07 * cantidadDeSugerencias + 10.93)}{' '}
|
||||||
|
segs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,9 +30,9 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
archivosAdjuntos: [],
|
archivosAdjuntos: [],
|
||||||
},
|
},
|
||||||
iaMultiple: {
|
iaMultiple: {
|
||||||
ciclo: null,
|
|
||||||
enfoque: '',
|
enfoque: '',
|
||||||
cantidadDeSugerencias: 10,
|
cantidadDeSugerencias: 10,
|
||||||
|
isLoading: false,
|
||||||
},
|
},
|
||||||
resumen: {},
|
resumen: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ export type NewSubjectWizardState = {
|
|||||||
archivosAdjuntos?: Array<UploadedFile>
|
archivosAdjuntos?: Array<UploadedFile>
|
||||||
}
|
}
|
||||||
iaMultiple?: {
|
iaMultiple?: {
|
||||||
ciclo: number | null
|
|
||||||
enfoque: string
|
enfoque: string
|
||||||
cantidadDeSugerencias: number
|
cantidadDeSugerencias: number
|
||||||
|
isLoading: boolean
|
||||||
}
|
}
|
||||||
resumen: {
|
resumen: {
|
||||||
previewAsignatura?: AsignaturaPreview
|
previewAsignatura?: AsignaturaPreview
|
||||||
|
|||||||
Reference in New Issue
Block a user