Generación de múltiples asignaturas con sugerencias #105

Merged
Guillermo.Arrieta merged 9 commits from issue/89-nueva-opcin-en-wizard-crear-asignaturas-con-sugere into main 2026-02-12 23:31:46 +00:00
3 changed files with 211 additions and 110 deletions
Showing only changes of commit 9c588cfd8f - Show all commits

View File

@@ -9,12 +9,13 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from '@/components/ui/card' } from '@/components/ui/card'
import { usePlan, useSubjectEstructuras } from '@/data' import { usePlan, usePlanLineas, useSubjectEstructuras } from '@/data'
import { formatFileSize } from '@/features/planes/utils/format-file-size' import { formatFileSize } from '@/features/planes/utils/format-file-size'
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) { export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
const { data: plan } = usePlan(wizard.plan_estudio_id) const { data: plan } = usePlan(wizard.plan_estudio_id)
const { data: estructuras } = useSubjectEstructuras() const { data: estructuras } = useSubjectEstructuras()
const { data: lineasPlan } = usePlanLineas(wizard.plan_estudio_id)
const estructuraNombre = (() => { const estructuraNombre = (() => {
const estructuraId = wizard.datosBasicos.estructuraId const estructuraId = wizard.datosBasicos.estructuraId
@@ -26,6 +27,8 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
const modoLabel = (() => { const modoLabel = (() => {
if (wizard.tipoOrigen === 'MANUAL') return 'Manual (Vacía)' if (wizard.tipoOrigen === 'MANUAL') return 'Manual (Vacía)'
if (wizard.tipoOrigen === 'IA') return 'Generada con IA' if (wizard.tipoOrigen === 'IA') return 'Generada con IA'
if (wizard.tipoOrigen === 'IA_SIMPLE') return 'Generada con IA (Simple)'
if (wizard.tipoOrigen === 'IA_MULTIPLE') return 'Generación múltiple (IA)'
if (wizard.tipoOrigen === 'CLONADO_INTERNO') return 'Clonada (Sistema)' if (wizard.tipoOrigen === 'CLONADO_INTERNO') return 'Clonada (Sistema)'
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') return 'Clonada (Archivo)' if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') return 'Clonada (Archivo)'
return '—' return '—'
@@ -41,6 +44,10 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
const repositoriosRef = wizard.iaConfig?.repositoriosReferencia ?? [] const repositoriosRef = wizard.iaConfig?.repositoriosReferencia ?? []
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? [] const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
const materiasSeleccionadas = wizard.sugerencias.filter((s) => s.selected)
const iaMultipleEnfoque = wizard.iaMultiple?.enfoque.trim() ?? ''
const iaMultipleCantidad = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
return ( return (
<Card> <Card>
<CardHeader> <CardHeader>
@@ -72,7 +79,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
{wizard.tipoOrigen === 'MANUAL' && ( {wizard.tipoOrigen === 'MANUAL' && (
<Icons.Pencil className="h-4 w-4" /> <Icons.Pencil className="h-4 w-4" />
)} )}
{wizard.tipoOrigen === 'IA' && ( {(wizard.tipoOrigen === 'IA' ||
wizard.tipoOrigen === 'IA_SIMPLE' ||
wizard.tipoOrigen === 'IA_MULTIPLE') && (
<Icons.Sparkles className="h-4 w-4" /> <Icons.Sparkles className="h-4 w-4" />
)} )}
{(wizard.tipoOrigen === 'CLONADO_INTERNO' || {(wizard.tipoOrigen === 'CLONADO_INTERNO' ||
@@ -83,6 +92,88 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
</span> </span>
</div> </div>
{wizard.tipoOrigen === 'IA_MULTIPLE' ? (
<>
<div className="border-border/60 bg-muted/30 grid gap-3 rounded-xl border p-4">
<div className="flex flex-col gap-1">
<div className="text-foreground text-base font-semibold">
Configuración
</div>
<div className="text-muted-foreground text-xs">
Se crearán {materiasSeleccionadas.length} asignatura(s) a
partir de tus selecciones.
</div>
</div>
<div className="bg-background/40 border-border/60 rounded-lg border p-3">
<div className="text-muted-foreground text-xs">
Estructura
</div>
<div className="text-foreground mt-1 text-sm font-medium">
{estructuraNombre}
</div>
</div>
</div>
<div className="border-border/60 bg-muted/30 grid gap-3 rounded-xl border p-4">
<div className="flex items-end justify-between gap-2">
<div className="text-foreground text-base font-semibold">
Materias seleccionadas
</div>
<div className="text-muted-foreground text-xs">
{materiasSeleccionadas.length} en total
</div>
</div>
{materiasSeleccionadas.length === 0 ? (
<div className="text-muted-foreground text-sm">
No hay materias seleccionadas.
</div>
) : (
<div className="grid gap-3">
{materiasSeleccionadas.map((m) => {
const lineaNombre = m.linea_plan_id
? (lineasPlan?.find((l) => l.id === m.linea_plan_id)
?.nombre ?? m.linea_plan_id)
: '—'
const cicloText =
typeof m.numero_ciclo === 'number' &&
Number.isFinite(m.numero_ciclo)
? String(m.numero_ciclo)
: '—'
return (
<div
key={m.id}
className="bg-background/40 border-border/60 grid gap-2 rounded-lg border p-3"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="text-foreground text-sm font-semibold">
{m.nombre}
</div>
<div className="text-muted-foreground flex flex-wrap items-center gap-2 text-xs">
<span className="bg-accent/30 text-accent-foreground rounded-full px-2 py-0.5">
Línea: {lineaNombre}
</span>
<span className="bg-accent/30 text-accent-foreground rounded-full px-2 py-0.5">
Ciclo: {cicloText}
</span>
</div>
</div>
<div className="text-muted-foreground text-sm whitespace-pre-wrap">
{m.descripcion || '—'}
</div>
</div>
)
})}
</div>
)}
</div>
</>
) : (
<>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="col-span-2"> <div className="col-span-2">
<span className="text-muted-foreground">Nombre: </span> <span className="text-muted-foreground">Nombre: </span>
@@ -111,7 +202,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
<span className="font-medium">{estructuraNombre}</span> <span className="font-medium">{estructuraNombre}</span>
</div> </div>
<div> <div>
<span className="text-muted-foreground">Horas académicas: </span> <span className="text-muted-foreground">
Horas académicas:{' '}
</span>
<span className="font-medium"> <span className="font-medium">
{wizard.datosBasicos.horasAcademicas ?? '—'} {wizard.datosBasicos.horasAcademicas ?? '—'}
</span> </span>
@@ -160,7 +253,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
</div> </div>
<div> <div>
<div className="font-medium">Repositorios de referencia</div> <div className="font-medium">
Repositorios de referencia
</div>
{repositoriosRef.length ? ( {repositoriosRef.length ? (
<ul className="text-muted-foreground list-disc pl-5 text-xs"> <ul className="text-muted-foreground list-disc pl-5 text-xs">
{repositoriosRef.map((id) => ( {repositoriosRef.map((id) => (
@@ -178,7 +273,9 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
<ul className="text-muted-foreground list-disc pl-5 text-xs"> <ul className="text-muted-foreground list-disc pl-5 text-xs">
{adjuntos.map((f) => ( {adjuntos.map((f) => (
<li key={f.id}> <li key={f.id}>
<span className="text-foreground">{f.file.name}</span>{' '} <span className="text-foreground">
{f.file.name}
</span>{' '}
<span>· {formatFileSize(f.file.size)}</span> <span>· {formatFileSize(f.file.size)}</span>
</li> </li>
))} ))}
@@ -189,6 +286,8 @@ export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
</div> </div>
</div> </div>
</div> </div>
</>
)}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@@ -48,15 +48,17 @@ export function useNuevaAsignaturaWizard(planId: string) {
wizard.tipoOrigen === 'CLONADO_TRADICIONAL' wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
const canContinueDesdeBasicos = const canContinueDesdeBasicos =
!!wizard.datosBasicos.nombre && (!!wizard.datosBasicos.nombre &&
wizard.datosBasicos.tipo !== null && wizard.datosBasicos.tipo !== null &&
wizard.datosBasicos.creditos !== null && wizard.datosBasicos.creditos !== null &&
wizard.datosBasicos.creditos > 0 && wizard.datosBasicos.creditos > 0 &&
!!wizard.datosBasicos.estructuraId !!wizard.datosBasicos.estructuraId) ||
(wizard.tipoOrigen === 'IA_MULTIPLE' &&
wizard.sugerencias.filter((s) => s.selected).length > 0)
const canContinueDesdeDetalles = (() => { const canContinueDesdeDetalles = (() => {
if (wizard.tipoOrigen === 'MANUAL') return true if (wizard.tipoOrigen === 'MANUAL') return true
if (wizard.tipoOrigen === 'IA') { if (wizard.tipoOrigen === 'IA_SIMPLE') {
return !!wizard.iaConfig?.descripcionEnfoqueAcademico return !!wizard.iaConfig?.descripcionEnfoqueAcademico
} }
if (wizard.tipoOrigen === 'CLONADO_INTERNO') { if (wizard.tipoOrigen === 'CLONADO_INTERNO') {

View File

@@ -7,11 +7,6 @@ export type Json =
| Array<Json> | Array<Json>
export type Database = { export type Database = {
// Allows to automatically instantiate createClient with right options
// instead of createClient<Database, { PostgrestVersion: 'XX' }>(URL, KEY)
__InternalSupabase: {
PostgrestVersion: '12.2.3 (519615d)'
}
graphql_public: { graphql_public: {
Tables: { Tables: {
[_ in never]: never [_ in never]: never
@@ -98,6 +93,7 @@ export type Database = {
creado_por: string | null creado_por: string | null
creditos: number creditos: number
datos: Json datos: Json
estado: Database['public']['Enums']['estado_asignatura']
estructura_id: string | null estructura_id: string | null
horas_academicas: number | null horas_academicas: number | null
horas_independientes: number | null horas_independientes: number | null
@@ -122,6 +118,7 @@ export type Database = {
creado_por?: string | null creado_por?: string | null
creditos: number creditos: number
datos?: Json datos?: Json
estado?: Database['public']['Enums']['estado_asignatura']
estructura_id?: string | null estructura_id?: string | null
horas_academicas?: number | null horas_academicas?: number | null
horas_independientes?: number | null horas_independientes?: number | null
@@ -146,6 +143,7 @@ export type Database = {
creado_por?: string | null creado_por?: string | null
creditos?: number creditos?: number
datos?: Json datos?: Json
estado?: Database['public']['Enums']['estado_asignatura']
estructura_id?: string | null estructura_id?: string | null
horas_academicas?: number | null horas_academicas?: number | null
horas_independientes?: number | null horas_independientes?: number | null
@@ -1089,6 +1087,7 @@ export type Database = {
unaccent_immutable: { Args: { '': string }; Returns: string } unaccent_immutable: { Args: { '': string }; Returns: string }
} }
Enums: { Enums: {
estado_asignatura: 'borrador' | 'revisada' | 'aprobada' | 'generando'
estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA' estado_tarea_revision: 'PENDIENTE' | 'COMPLETADA' | 'OMITIDA'
fuente_cambio: 'HUMANO' | 'IA' fuente_cambio: 'HUMANO' | 'IA'
nivel_plan_estudio: nivel_plan_estudio:
@@ -1261,6 +1260,7 @@ export const Constants = {
}, },
public: { public: {
Enums: { Enums: {
estado_asignatura: ['borrador', 'revisada', 'aprobada', 'generando'],
estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'], estado_tarea_revision: ['PENDIENTE', 'COMPLETADA', 'OMITIDA'],
fuente_cambio: ['HUMANO', 'IA'], fuente_cambio: ['HUMANO', 'IA'],
nivel_plan_estudio: [ nivel_plan_estudio: [