Compare commits
3 Commits
852564776a
...
691b8911c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 691b8911c5 | |||
| 073969b9bf | |||
| e173c3097c |
@@ -7,82 +7,9 @@ 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 { generate_subject_suggestions, usePlan } from '@/data'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
// Interfaces
|
|
||||||
interface Suggestion {
|
|
||||||
id: string
|
|
||||||
nombre: string
|
|
||||||
tipo: 'Obligatoria' | 'Optativa'
|
|
||||||
creditos: number
|
|
||||||
horasAcademicas: number
|
|
||||||
horasIndependientes: number
|
|
||||||
descripcion: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Datos Mock basados en tu imagen
|
|
||||||
const MOCK_SUGGESTIONS: Array<Suggestion> = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
nombre: 'Propiedad Intelectual en Entornos Digitales',
|
|
||||||
tipo: 'Optativa',
|
|
||||||
creditos: 4,
|
|
||||||
horasAcademicas: 32,
|
|
||||||
horasIndependientes: 16,
|
|
||||||
descripcion:
|
|
||||||
'Derechos de autor, patentes de software y marcas en el ecosistema digital.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
nombre: 'Derecho Constitucional Digital',
|
|
||||||
tipo: 'Obligatoria',
|
|
||||||
creditos: 8,
|
|
||||||
horasAcademicas: 64,
|
|
||||||
horasIndependientes: 32,
|
|
||||||
descripcion:
|
|
||||||
'Marco constitucional aplicado al entorno digital y derechos fundamentales en línea.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
nombre: 'Gobernanza de Internet',
|
|
||||||
tipo: 'Optativa',
|
|
||||||
creditos: 4,
|
|
||||||
horasAcademicas: 32,
|
|
||||||
horasIndependientes: 16,
|
|
||||||
descripcion:
|
|
||||||
'Políticas públicas, regulación internacional y gobernanza del ecosistema digital.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
nombre: 'Protección de Datos Personales',
|
|
||||||
tipo: 'Obligatoria',
|
|
||||||
creditos: 6,
|
|
||||||
horasAcademicas: 48,
|
|
||||||
horasIndependientes: 24,
|
|
||||||
descripcion:
|
|
||||||
'Regulación y cumplimiento de leyes de protección de datos (GDPR, LFPDPPP).',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
nombre: 'Inteligencia Artificial y Ética Jurídica',
|
|
||||||
tipo: 'Optativa',
|
|
||||||
creditos: 4,
|
|
||||||
horasAcademicas: 32,
|
|
||||||
horasIndependientes: 16,
|
|
||||||
descripcion:
|
|
||||||
'Implicaciones legales y éticas del uso de IA en la práctica jurídica.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
nombre: 'Ciberseguridad y Derecho Penal',
|
|
||||||
tipo: 'Obligatoria',
|
|
||||||
creditos: 6,
|
|
||||||
horasAcademicas: 48,
|
|
||||||
horasIndependientes: 24,
|
|
||||||
descripcion:
|
|
||||||
'Delitos informáticos, evidencia digital y marco penal en el ciberespacio.',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
export default function PasoSugerenciasForm({
|
export default function PasoSugerenciasForm({
|
||||||
wizard,
|
wizard,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -90,31 +17,102 @@ export default function PasoSugerenciasForm({
|
|||||||
wizard: NewSubjectWizardState
|
wizard: NewSubjectWizardState
|
||||||
onChange: Dispatch<SetStateAction<NewSubjectWizardState>>
|
onChange: Dispatch<SetStateAction<NewSubjectWizardState>>
|
||||||
}) {
|
}) {
|
||||||
const selectedIds = wizard.iaMultiple?.selectedIds ?? []
|
|
||||||
const ciclo = wizard.iaMultiple?.ciclo ?? ''
|
const ciclo = wizard.iaMultiple?.ciclo ?? ''
|
||||||
const enfoque = wizard.iaMultiple?.enfoque ?? ''
|
const enfoque = wizard.iaMultiple?.enfoque ?? ''
|
||||||
|
const cantidadDeSugerencias = wizard.iaMultiple?.cantidadDeSugerencias ?? 10
|
||||||
|
|
||||||
const setIaMultiple = (
|
const setIaMultiple = (
|
||||||
patch: Partial<NonNullable<NewSubjectWizardState['iaMultiple']>>,
|
patch: Partial<NonNullable<NewSubjectWizardState['iaMultiple']>>,
|
||||||
) =>
|
) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
...w,
|
(w): NewSubjectWizardState => ({
|
||||||
iaMultiple: {
|
...w,
|
||||||
ciclo: w.iaMultiple?.ciclo ?? '',
|
iaMultiple: {
|
||||||
enfoque: w.iaMultiple?.enfoque ?? '',
|
ciclo: w.iaMultiple?.ciclo ?? null,
|
||||||
selectedIds: w.iaMultiple?.selectedIds ?? [],
|
enfoque: w.iaMultiple?.enfoque ?? '',
|
||||||
...patch,
|
cantidadDeSugerencias: w.iaMultiple?.cantidadDeSugerencias ?? 10,
|
||||||
},
|
...patch,
|
||||||
}))
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data: plan } = usePlan(wizard.plan_estudio_id)
|
||||||
|
|
||||||
const toggleAsignatura = (id: string, checked: boolean) => {
|
const toggleAsignatura = (id: string, checked: boolean) => {
|
||||||
const prev = selectedIds
|
onChange((w) => ({
|
||||||
const next = checked ? [...prev, id] : prev.filter((x) => x !== id)
|
...w,
|
||||||
setIaMultiple({ selectedIds: next })
|
sugerencias: w.sugerencias.map((s) =>
|
||||||
|
s.id === id ? { ...s, selected: checked } : s,
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGenerarSugerencias = async () => {
|
||||||
|
const sugerenciasConservadas = wizard.sugerencias.filter((s) => s.selected)
|
||||||
|
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: true,
|
||||||
|
errorMessage: null,
|
||||||
|
sugerencias: sugerenciasConservadas,
|
||||||
|
}))
|
||||||
|
|
||||||
|
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
|
||||||
|
if (!Number.isFinite(cantidad) || cantidad <= 0 || cantidad > 50) {
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: 'La cantidad de sugerencias debe ser entre 1 y 50.',
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const enfoqueTrim = wizard.iaMultiple?.enfoque.trim() ?? ''
|
||||||
|
|
||||||
|
const nuevasSugerencias = await generate_subject_suggestions({
|
||||||
|
plan_estudio_id: wizard.plan_estudio_id,
|
||||||
|
numero_de_ciclo: numeroCiclo,
|
||||||
|
enfoque: enfoqueTrim ? enfoqueTrim : undefined,
|
||||||
|
cantidad_de_sugerencias: cantidad,
|
||||||
|
sugerencias_conservadas: sugerenciasConservadas.map((s) => ({
|
||||||
|
nombre: s.nombre,
|
||||||
|
descripcion: s.descripcion,
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
sugerencias: [...nuevasSugerencias, ...sugerenciasConservadas],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : 'Error generando sugerencias.'
|
||||||
|
onChange(
|
||||||
|
(w): NewSubjectWizardState => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: message,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<>
|
||||||
{/* --- BLOQUE SUPERIOR: PARÁMETROS --- */}
|
{/* --- BLOQUE SUPERIOR: PARÁMETROS --- */}
|
||||||
<div className="border-border/60 bg-muted/30 mb-4 rounded-xl border p-4">
|
<div className="border-border/60 bg-muted/30 mb-4 rounded-xl border p-4">
|
||||||
<div className="mb-3 flex items-center gap-2">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
@@ -133,7 +131,21 @@ export default function PasoSugerenciasForm({
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Ej. 3"
|
placeholder="Ej. 3"
|
||||||
value={ciclo}
|
value={ciclo}
|
||||||
onChange={(e) => setIaMultiple({ ciclo: e.target.value })}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -148,13 +160,54 @@ export default function PasoSugerenciasForm({
|
|||||||
onChange={(e) => setIaMultiple({ enfoque: e.target.value })}
|
onChange={(e) => setIaMultiple({ enfoque: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Botón Refrescar */}
|
<div className="mt-3 flex w-full flex-col items-end gap-3 md:flex-row">
|
||||||
<Button type="button" variant="outline" className="h-9 gap-1.5">
|
<div className="w-full md:w-44">
|
||||||
|
<Label className="text-muted-foreground mb-1 block text-xs">
|
||||||
|
Cantidad de sugerencias
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Ej. 10"
|
||||||
|
value={cantidadDeSugerencias}
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={50}
|
||||||
|
step={1}
|
||||||
|
inputMode="numeric"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value
|
||||||
|
if (raw === '') 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, 50)
|
||||||
|
setIaMultiple({ cantidadDeSugerencias: capped })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-9 gap-1.5"
|
||||||
|
onClick={onGenerarSugerencias}
|
||||||
|
disabled={wizard.isLoading}
|
||||||
|
>
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
<RefreshCw className="h-3.5 w-3.5" />
|
||||||
Nuevas 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>
|
||||||
|
|
||||||
{/* --- HEADER LISTA --- */}
|
{/* --- HEADER LISTA --- */}
|
||||||
@@ -164,26 +217,25 @@ export default function PasoSugerenciasForm({
|
|||||||
Asignaturas sugeridas
|
Asignaturas sugeridas
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
Basadas en el plan "Licenciatura en Derecho Digital"
|
Basadas en el plan{' '}
|
||||||
|
{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">
|
<div className="bg-muted text-foreground inline-flex items-center rounded-full px-2.5 py-0.5 text-sm font-semibold">
|
||||||
{selectedIds.length} seleccionadas
|
{wizard.sugerencias.filter((s) => s.selected).length} seleccionadas
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* --- LISTA DE ASIGNATURAS (CON EL ESTILO PEDIDO) --- */}
|
{/* --- LISTA DE ASIGNATURAS --- */}
|
||||||
<div className="max-h-80 space-y-1 overflow-y-auto pr-1">
|
<div className="max-h-100 space-y-1 overflow-y-auto pr-1">
|
||||||
{MOCK_SUGGESTIONS.map((asignatura) => {
|
{wizard.sugerencias.map((asignatura) => {
|
||||||
const isSelected = selectedIds.includes(asignatura.id)
|
const isSelected = asignatura.selected
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
key={asignatura.id}
|
key={asignatura.id}
|
||||||
// Para que funcione el selector css `has-aria-checked` que tenías en tu snippet
|
|
||||||
aria-checked={isSelected}
|
aria-checked={isSelected}
|
||||||
className={cn(
|
className={cn(
|
||||||
// Igual al patrón de ReferenciasParaIA
|
|
||||||
'border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950',
|
'border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -193,9 +245,8 @@ export default function PasoSugerenciasForm({
|
|||||||
toggleAsignatura(asignatura.id, !!checked)
|
toggleAsignatura(asignatura.id, !!checked)
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
// Igual al patrón de ReferenciasParaIA: invisible si no está seleccionado
|
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring mt-0.5 h-5 w-5 shrink-0 border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
'peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring mt-0.5 h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
// isSelected ? '' : 'invisible',
|
||||||
isSelected ? '' : 'invisible',
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -210,7 +261,7 @@ export default function PasoSugerenciasForm({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
||||||
asignatura.tipo === 'Obligatoria'
|
asignatura.tipo === 'OBLIGATORIA'
|
||||||
? 'border-blue-200 bg-transparent text-blue-700 dark:border-blue-800 dark:text-blue-300'
|
? 'border-blue-200 bg-transparent text-blue-700 dark:border-blue-800 dark:text-blue-300'
|
||||||
: 'border-yellow-200 bg-transparent text-yellow-700 dark:border-yellow-800 dark:text-yellow-300',
|
: 'border-yellow-200 bg-transparent text-yellow-700 dark:border-yellow-800 dark:text-yellow-300',
|
||||||
)}
|
)}
|
||||||
@@ -224,7 +275,7 @@ export default function PasoSugerenciasForm({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground mt-1 text-xs">
|
<p className="text-muted-foreground mt-1 text-sm">
|
||||||
{asignatura.descripcion}
|
{asignatura.descripcion}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,6 +283,6 @@ export default function PasoSugerenciasForm({
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function WizardControls({
|
|||||||
Anterior
|
Anterior
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex-1">
|
<div className="mx-2 flex-1">
|
||||||
{(errorMessage ?? wizard.errorMessage) && (
|
{(errorMessage ?? wizard.errorMessage) && (
|
||||||
<span className="text-destructive text-sm font-medium">
|
<span className="text-destructive text-sm font-medium">
|
||||||
{errorMessage ?? wizard.errorMessage}
|
{errorMessage ?? wizard.errorMessage}
|
||||||
|
|||||||
@@ -12,9 +12,14 @@ import type {
|
|||||||
UUID,
|
UUID,
|
||||||
} from '../types/domain'
|
} from '../types/domain'
|
||||||
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
|
||||||
|
import type {
|
||||||
|
AsignaturaSugerida,
|
||||||
|
DataAsignaturaSugerida,
|
||||||
|
} from '@/features/asignaturas/nueva/types'
|
||||||
import type { Database } from '@/types/supabase'
|
import type { Database } from '@/types/supabase'
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
|
generate_subject_suggestions: 'generate-subject-suggestions',
|
||||||
subjects_create_manual: 'subjects_create_manual',
|
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_persist_from_ai: 'subjects_persist_from_ai',
|
||||||
@@ -36,7 +41,7 @@ export async function subjects_get(subjectId: UUID): Promise<Asignatura> {
|
|||||||
.from('asignaturas')
|
.from('asignaturas')
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,conversation_id,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||||
planes_estudio(
|
planes_estudio(
|
||||||
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
|
||||||
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))
|
||||||
@@ -133,6 +138,40 @@ export type AIGenerateSubjectInput = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GenerateSubjectSuggestionsInput = {
|
||||||
|
plan_estudio_id: UUID
|
||||||
|
numero_de_ciclo: number
|
||||||
|
enfoque?: string
|
||||||
|
cantidad_de_sugerencias: number
|
||||||
|
sugerencias_conservadas: Array<{ nombre: string; descripcion: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generate_subject_suggestions(
|
||||||
|
input: GenerateSubjectSuggestionsInput,
|
||||||
|
): Promise<Array<AsignaturaSugerida>> {
|
||||||
|
const raw = await invokeEdge<Array<DataAsignaturaSugerida>>(
|
||||||
|
EDGE.generate_subject_suggestions,
|
||||||
|
input,
|
||||||
|
{ headers: { 'Content-Type': 'application/json' } },
|
||||||
|
)
|
||||||
|
|
||||||
|
return raw.map(
|
||||||
|
(s): AsignaturaSugerida => ({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
selected: false,
|
||||||
|
source: 'IA',
|
||||||
|
estructuraId: null,
|
||||||
|
nombre: s.nombre,
|
||||||
|
codigo: s.codigo,
|
||||||
|
tipo: s.tipo ?? null,
|
||||||
|
creditos: s.creditos ?? null,
|
||||||
|
horasAcademicas: s.horasAcademicas ?? null,
|
||||||
|
horasIndependientes: s.horasIndependientes ?? null,
|
||||||
|
descripcion: s.descripcion,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export async function ai_generate_subject(
|
export async function ai_generate_subject(
|
||||||
input: AIGenerateSubjectInput,
|
input: AIGenerateSubjectInput,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
@@ -146,7 +185,7 @@ export async function ai_generate_subject(
|
|||||||
archivosAdjuntos: undefined, // los manejamos aparte
|
archivosAdjuntos: undefined, // los manejamos aparte
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
input.iaConfig?.archivosAdjuntos?.forEach((file, index) => {
|
input.iaConfig?.archivosAdjuntos?.forEach((file) => {
|
||||||
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
edgeFunctionBody.append(`archivosAdjuntos`, file.file)
|
||||||
})
|
})
|
||||||
return invokeEdge<any>(
|
return invokeEdge<any>(
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const qk = {
|
|||||||
planHistorial: (planId: string) => ['planes', planId, 'historial'] as const,
|
planHistorial: (planId: string) => ['planes', planId, 'historial'] as const,
|
||||||
planDocumento: (planId: string) => ['planes', planId, 'documento'] as const,
|
planDocumento: (planId: string) => ['planes', planId, 'documento'] as const,
|
||||||
|
|
||||||
|
sugerenciasAsignaturas: () => ['asignaturas', 'sugerencias'] as const,
|
||||||
asignatura: (asignaturaId: string) =>
|
asignatura: (asignaturaId: string) =>
|
||||||
['asignaturas', 'detail', asignaturaId] as const,
|
['asignaturas', 'detail', asignaturaId] as const,
|
||||||
asignaturaBibliografia: (asignaturaId: string) =>
|
asignaturaBibliografia: (asignaturaId: string) =>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
horasIndependientes: null,
|
horasIndependientes: null,
|
||||||
estructuraId: '',
|
estructuraId: '',
|
||||||
},
|
},
|
||||||
|
sugerencias: [],
|
||||||
clonInterno: {},
|
clonInterno: {},
|
||||||
clonTradicional: {
|
clonTradicional: {
|
||||||
archivoWordAsignaturaId: null,
|
archivoWordAsignaturaId: null,
|
||||||
@@ -29,9 +30,9 @@ export function useNuevaAsignaturaWizard(planId: string) {
|
|||||||
archivosAdjuntos: [],
|
archivosAdjuntos: [],
|
||||||
},
|
},
|
||||||
iaMultiple: {
|
iaMultiple: {
|
||||||
ciclo: '',
|
ciclo: null,
|
||||||
enfoque: '',
|
enfoque: '',
|
||||||
selectedIds: ['1', '3', '6'],
|
cantidadDeSugerencias: 10,
|
||||||
},
|
},
|
||||||
resumen: {},
|
resumen: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/
|
|||||||
import type { Asignatura } from '@/data'
|
import type { Asignatura } from '@/data'
|
||||||
|
|
||||||
export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO'
|
export type ModoCreacion = 'MANUAL' | 'IA' | 'CLONADO'
|
||||||
export type SubModoClonado = 'INTERNO' | 'TRADICIONAL'
|
|
||||||
export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO'
|
export type TipoAsignatura = 'OBLIGATORIA' | 'OPTATIVA' | 'TRONCAL' | 'OTRO'
|
||||||
|
|
||||||
export type AsignaturaPreview = {
|
export type AsignaturaPreview = {
|
||||||
@@ -12,6 +11,23 @@ export type AsignaturaPreview = {
|
|||||||
bibliografiaCount: number
|
bibliografiaCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DataAsignaturaSugerida = {
|
||||||
|
nombre: Asignatura['nombre']
|
||||||
|
codigo?: Asignatura['codigo']
|
||||||
|
tipo: Asignatura['tipo'] | null
|
||||||
|
creditos: Asignatura['creditos'] | null
|
||||||
|
horasAcademicas?: number | null
|
||||||
|
horasIndependientes?: number | null
|
||||||
|
descripcion: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AsignaturaSugerida = {
|
||||||
|
id: string
|
||||||
|
selected: boolean
|
||||||
|
source: 'IA' | 'MANUAL' | 'CLON'
|
||||||
|
estructuraId: Asignatura['estructura_id'] | null
|
||||||
|
} & DataAsignaturaSugerida
|
||||||
|
|
||||||
export type NewSubjectWizardState = {
|
export type NewSubjectWizardState = {
|
||||||
step: 1 | 2 | 3 | 4
|
step: 1 | 2 | 3 | 4
|
||||||
plan_estudio_id: Asignatura['plan_estudio_id']
|
plan_estudio_id: Asignatura['plan_estudio_id']
|
||||||
@@ -30,6 +46,7 @@ export type NewSubjectWizardState = {
|
|||||||
horasIndependientes?: Asignatura['horas_independientes'] | null
|
horasIndependientes?: Asignatura['horas_independientes'] | null
|
||||||
estructuraId: Asignatura['estructura_id'] | null
|
estructuraId: Asignatura['estructura_id'] | null
|
||||||
}
|
}
|
||||||
|
sugerencias: Array<AsignaturaSugerida>
|
||||||
clonInterno?: {
|
clonInterno?: {
|
||||||
facultadId?: string
|
facultadId?: string
|
||||||
carreraId?: string
|
carreraId?: string
|
||||||
@@ -48,9 +65,9 @@ export type NewSubjectWizardState = {
|
|||||||
archivosAdjuntos?: Array<UploadedFile>
|
archivosAdjuntos?: Array<UploadedFile>
|
||||||
}
|
}
|
||||||
iaMultiple?: {
|
iaMultiple?: {
|
||||||
ciclo: string
|
ciclo: number | null
|
||||||
enfoque: string
|
enfoque: string
|
||||||
selectedIds: Array<string>
|
cantidadDeSugerencias: number
|
||||||
}
|
}
|
||||||
resumen: {
|
resumen: {
|
||||||
previewAsignatura?: AsignaturaPreview
|
previewAsignatura?: AsignaturaPreview
|
||||||
|
|||||||
Reference in New Issue
Block a user