Compare commits
10 Commits
issue/147-
...
37fab3ead6
| Author | SHA1 | Date | |
|---|---|---|---|
| 37fab3ead6 | |||
| fa200acbfd | |||
| 020caf4e68 | |||
| 896c694a85 | |||
| 990daf5786 | |||
| c1197413db | |||
| bf2b8a9b6e | |||
| d6ecee7549 | |||
| 66bbf8ae17 | |||
| 6012d0ced8 |
@@ -1,11 +1,12 @@
|
|||||||
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
|
||||||
import { Pencil, Sparkles } from 'lucide-react'
|
import { Minus, Pencil, Plus, Sparkles } from 'lucide-react'
|
||||||
import { useState, useEffect } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
import type { AsignaturaDetail } from '@/data'
|
import type { AsignaturaDetail } from '@/data'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -37,54 +38,15 @@ export interface AsignaturaResponse {
|
|||||||
datos: AsignaturaDatos
|
datos: AsignaturaDatos
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
type CriterioEvaluacionRow = {
|
||||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
criterio: string
|
||||||
|
porcentaje: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseContenidoTematicoToPlainText(value: unknown): string {
|
type CriterioEvaluacionRowDraft = {
|
||||||
if (!Array.isArray(value)) return ''
|
id: string
|
||||||
|
criterio: string
|
||||||
const blocks: Array<string> = []
|
porcentaje: string // allow empty while editing
|
||||||
|
|
||||||
for (const item of value) {
|
|
||||||
if (!isRecord(item)) continue
|
|
||||||
|
|
||||||
const unidad =
|
|
||||||
typeof item.unidad === 'number' && Number.isFinite(item.unidad)
|
|
||||||
? item.unidad
|
|
||||||
: undefined
|
|
||||||
const titulo = typeof item.titulo === 'string' ? item.titulo : ''
|
|
||||||
|
|
||||||
const header = `${unidad ?? ''}${unidad ? '.' : ''} ${titulo}`.trim()
|
|
||||||
if (!header) continue
|
|
||||||
|
|
||||||
const lines: Array<string> = [header]
|
|
||||||
|
|
||||||
const temas = Array.isArray(item.temas) ? item.temas : []
|
|
||||||
temas.forEach((tema, idx) => {
|
|
||||||
const temaNombre =
|
|
||||||
typeof tema === 'string'
|
|
||||||
? tema
|
|
||||||
: isRecord(tema) && typeof tema.nombre === 'string'
|
|
||||||
? tema.nombre
|
|
||||||
: ''
|
|
||||||
if (!temaNombre) return
|
|
||||||
|
|
||||||
if (unidad != null) {
|
|
||||||
lines.push(`${unidad}.${idx + 1} ${temaNombre}`.trim())
|
|
||||||
} else {
|
|
||||||
lines.push(`${idx + 1}. ${temaNombre}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
blocks.push(lines.join('\n'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return blocks.join('\n\n').trimEnd()
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnParsers: Partial<Record<string, (value: unknown) => string>> = {
|
|
||||||
contenido_tematico: parseContenidoTematicoToPlainText,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
@@ -132,11 +94,19 @@ function DatosGenerales({
|
|||||||
}: {
|
}: {
|
||||||
onPersistDato: (clave: string, value: string) => void
|
onPersistDato: (clave: string, value: string) => void
|
||||||
}) {
|
}) {
|
||||||
const { asignaturaId } = useParams({
|
const { asignaturaId, planId } = useParams({
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
from: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
})
|
})
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { data: data, isLoading: isLoading } = useSubject(asignaturaId)
|
const { data: data, isLoading: isLoading } = useSubject(asignaturaId)
|
||||||
|
const updateAsignatura = useUpdateAsignatura()
|
||||||
|
|
||||||
|
const evaluationCardRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const [evaluationForceEditToken, setEvaluationForceEditToken] =
|
||||||
|
useState<number>(0)
|
||||||
|
const [evaluationHighlightToken, setEvaluationHighlightToken] =
|
||||||
|
useState<number>(0)
|
||||||
|
|
||||||
// 1. Extraemos la definición de la estructura (los metadatos)
|
// 1. Extraemos la definición de la estructura (los metadatos)
|
||||||
const definicionRaw = data?.estructuras_asignatura?.definicion
|
const definicionRaw = data?.estructuras_asignatura?.definicion
|
||||||
@@ -154,6 +124,56 @@ function DatosGenerales({
|
|||||||
const valoresActuales = isRecord(datosRaw)
|
const valoresActuales = isRecord(datosRaw)
|
||||||
? (datosRaw as Record<string, any>)
|
? (datosRaw as Record<string, any>)
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
|
const criteriosEvaluacion: Array<CriterioEvaluacionRow> = useMemo(() => {
|
||||||
|
const raw = (data as any)?.criterios_de_evaluacion
|
||||||
|
console.log(raw)
|
||||||
|
|
||||||
|
if (!Array.isArray(raw)) return []
|
||||||
|
|
||||||
|
const rows: Array<CriterioEvaluacionRow> = []
|
||||||
|
for (const item of raw) {
|
||||||
|
if (!isRecord(item)) continue
|
||||||
|
const criterio = typeof item.criterio === 'string' ? item.criterio : ''
|
||||||
|
const porcentajeNum =
|
||||||
|
typeof item.porcentaje === 'number'
|
||||||
|
? item.porcentaje
|
||||||
|
: typeof item.porcentaje === 'string'
|
||||||
|
? Number(item.porcentaje)
|
||||||
|
: NaN
|
||||||
|
|
||||||
|
if (!criterio.trim()) continue
|
||||||
|
if (!Number.isFinite(porcentajeNum)) continue
|
||||||
|
const porcentaje = Math.trunc(porcentajeNum)
|
||||||
|
if (porcentaje < 1 || porcentaje > 100) continue
|
||||||
|
|
||||||
|
rows.push({ criterio: criterio.trim(), porcentaje: porcentaje })
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
const openEvaluationEditor = () => {
|
||||||
|
evaluationCardRef.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
})
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
setEvaluationForceEditToken(now)
|
||||||
|
setEvaluationHighlightToken(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistCriteriosEvaluacion = async (
|
||||||
|
rows: Array<CriterioEvaluacionRow>,
|
||||||
|
) => {
|
||||||
|
await updateAsignatura.mutateAsync({
|
||||||
|
asignaturaId: asignaturaId as any,
|
||||||
|
patch: {
|
||||||
|
criterios_de_evaluacion: rows,
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
}
|
||||||
if (isLoading) return <p>Cargando información...</p>
|
if (isLoading) return <p>Cargando información...</p>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -209,10 +229,29 @@ function DatosGenerales({
|
|||||||
clave={key}
|
clave={key}
|
||||||
title={cardTitle}
|
title={cardTitle}
|
||||||
initialContent={currentContent}
|
initialContent={currentContent}
|
||||||
xColumn={xColumn}
|
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
description={description}
|
description={description}
|
||||||
onPersist={(clave, value) => onPersistDato(clave, value)}
|
onPersist={({ clave, value }) =>
|
||||||
|
onPersistDato(String(clave ?? key), String(value ?? ''))
|
||||||
|
}
|
||||||
|
onClickEditButton={({ startEditing }) => {
|
||||||
|
switch (xColumn) {
|
||||||
|
case 'contenido_tematico': {
|
||||||
|
navigate({
|
||||||
|
to: '/planes/$planId/asignaturas/$asignaturaId/contenido',
|
||||||
|
params: { planId, asignaturaId },
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case 'criterios_de_evaluacion': {
|
||||||
|
openEvaluationEditor()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
startEditing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -244,12 +283,11 @@ function DatosGenerales({
|
|||||||
<InfoCard
|
<InfoCard
|
||||||
title="Sistema de Evaluación"
|
title="Sistema de Evaluación"
|
||||||
type="evaluation"
|
type="evaluation"
|
||||||
initialContent={[
|
initialContent={criteriosEvaluacion}
|
||||||
{ label: 'Exámenes parciales', value: '30%' },
|
containerRef={evaluationCardRef}
|
||||||
{ label: 'Proyecto integrador', value: '35%' },
|
forceEditToken={evaluationForceEditToken}
|
||||||
{ label: 'Prácticas de laboratorio', value: '20%' },
|
highlightToken={evaluationHighlightToken}
|
||||||
{ label: 'Participación', value: '15%' },
|
onPersist={({ value }) => persistCriteriosEvaluacion(value)}
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -265,11 +303,19 @@ interface InfoCardProps {
|
|||||||
initialContent: any
|
initialContent: any
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
description?: string
|
description?: string
|
||||||
xColumn?: string
|
|
||||||
required?: boolean // Nueva prop para el asterisco
|
required?: boolean // Nueva prop para el asterisco
|
||||||
type?: 'text' | 'requirements' | 'evaluation'
|
type?: 'text' | 'requirements' | 'evaluation'
|
||||||
onEnhanceAI?: (content: any) => void
|
onEnhanceAI?: (content: any) => void
|
||||||
onPersist?: (clave: string, value: string) => void
|
onPersist?: (payload: {
|
||||||
|
type: NonNullable<InfoCardProps['type']>
|
||||||
|
clave?: string
|
||||||
|
value: any
|
||||||
|
}) => void | Promise<void>
|
||||||
|
onClickEditButton?: (helpers: { startEditing: () => void }) => void
|
||||||
|
|
||||||
|
containerRef?: React.RefObject<HTMLDivElement | null>
|
||||||
|
forceEditToken?: number
|
||||||
|
highlightToken?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function InfoCard({
|
function InfoCard({
|
||||||
@@ -279,14 +325,22 @@ function InfoCard({
|
|||||||
initialContent,
|
initialContent,
|
||||||
placeholder,
|
placeholder,
|
||||||
description,
|
description,
|
||||||
xColumn,
|
|
||||||
required,
|
required,
|
||||||
type = 'text',
|
type = 'text',
|
||||||
onPersist,
|
onPersist,
|
||||||
|
onClickEditButton,
|
||||||
|
containerRef,
|
||||||
|
forceEditToken,
|
||||||
|
highlightToken,
|
||||||
}: InfoCardProps) {
|
}: InfoCardProps) {
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
|
const [isHighlighted, setIsHighlighted] = useState(false)
|
||||||
const [data, setData] = useState(initialContent)
|
const [data, setData] = useState(initialContent)
|
||||||
const [tempText, setTempText] = useState(initialContent)
|
const [tempText, setTempText] = useState(initialContent)
|
||||||
|
|
||||||
|
const [evalRows, setEvalRows] = useState<Array<CriterioEvaluacionRowDraft>>(
|
||||||
|
[],
|
||||||
|
)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { planId } = useParams({
|
const { planId } = useParams({
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId',
|
from: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
@@ -295,16 +349,85 @@ function InfoCard({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setData(initialContent)
|
setData(initialContent)
|
||||||
setTempText(initialContent)
|
setTempText(initialContent)
|
||||||
}, [initialContent])
|
|
||||||
|
if (type === 'evaluation') {
|
||||||
|
const raw = Array.isArray(initialContent) ? initialContent : []
|
||||||
|
const rows: Array<CriterioEvaluacionRowDraft> = raw
|
||||||
|
.map((r: any): CriterioEvaluacionRowDraft | null => {
|
||||||
|
const criterio = typeof r?.criterio === 'string' ? r.criterio : ''
|
||||||
|
const porcentajeNum =
|
||||||
|
typeof r?.porcentaje === 'number'
|
||||||
|
? r.porcentaje
|
||||||
|
: typeof r?.porcentaje === 'string'
|
||||||
|
? Number(r.porcentaje)
|
||||||
|
: NaN
|
||||||
|
|
||||||
|
const porcentaje = Number.isFinite(porcentajeNum)
|
||||||
|
? String(Math.trunc(porcentajeNum))
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
criterio,
|
||||||
|
porcentaje,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean) as Array<CriterioEvaluacionRowDraft>
|
||||||
|
|
||||||
|
setEvalRows(rows)
|
||||||
|
}
|
||||||
|
}, [initialContent, type])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!forceEditToken) return
|
||||||
|
setIsEditing(true)
|
||||||
|
}, [forceEditToken])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!highlightToken) return
|
||||||
|
setIsHighlighted(true)
|
||||||
|
const t = window.setTimeout(() => setIsHighlighted(false), 900)
|
||||||
|
return () => window.clearTimeout(t)
|
||||||
|
}, [highlightToken])
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
console.log('clave, valor:', clave, String(tempText ?? ''))
|
console.log('clave, valor:', clave, String(tempText ?? ''))
|
||||||
|
|
||||||
|
if (type === 'evaluation') {
|
||||||
|
const cleaned: Array<CriterioEvaluacionRow> = []
|
||||||
|
for (const r of evalRows) {
|
||||||
|
const criterio = String(r.criterio).trim()
|
||||||
|
const porcentajeStr = String(r.porcentaje).trim()
|
||||||
|
if (!criterio) continue
|
||||||
|
if (!porcentajeStr) continue
|
||||||
|
|
||||||
|
const n = Number(porcentajeStr)
|
||||||
|
if (!Number.isFinite(n)) continue
|
||||||
|
const porcentaje = Math.trunc(n)
|
||||||
|
if (porcentaje < 1 || porcentaje > 100) continue
|
||||||
|
|
||||||
|
cleaned.push({ criterio, porcentaje })
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(cleaned)
|
||||||
|
setEvalRows(
|
||||||
|
cleaned.map((x) => ({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
criterio: x.criterio,
|
||||||
|
porcentaje: String(x.porcentaje),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
setIsEditing(false)
|
||||||
|
|
||||||
|
void onPersist?.({ type, clave, value: cleaned })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setData(tempText)
|
setData(tempText)
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
|
|
||||||
if (type === 'text' && clave && onPersist) {
|
if (type === 'text') {
|
||||||
onPersist(clave, String(tempText ?? ''))
|
void onPersist?.({ type, clave, value: String(tempText ?? '') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,8 +448,27 @@ function InfoCard({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const evaluationTotal = useMemo(() => {
|
||||||
|
if (type !== 'evaluation') return 0
|
||||||
|
return evalRows.reduce((acc, r) => {
|
||||||
|
const v = String(r.porcentaje).trim()
|
||||||
|
if (!v) return acc
|
||||||
|
const n = Number(v)
|
||||||
|
if (!Number.isFinite(n)) return acc
|
||||||
|
const porcentaje = Math.trunc(n)
|
||||||
|
if (porcentaje < 1 || porcentaje > 100) return acc
|
||||||
|
return acc + porcentaje
|
||||||
|
}, 0)
|
||||||
|
}, [type, evalRows])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden transition-all hover:border-slate-300">
|
<div ref={containerRef as any}>
|
||||||
|
<Card
|
||||||
|
className={
|
||||||
|
'overflow-hidden transition-all hover:border-slate-300 ' +
|
||||||
|
(isHighlighted ? 'ring-primary/40 ring-2' : '')
|
||||||
|
}
|
||||||
|
>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<CardHeader className="border-b bg-slate-50/50 px-5 py-3">
|
<CardHeader className="border-b bg-slate-50/50 px-5 py-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -375,19 +517,14 @@ function InfoCard({
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 text-slate-400"
|
className="h-8 w-8 text-slate-400"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Si esta InfoCard proviene de una columna externa (ej: contenido_tematico),
|
const startEditing = () => setIsEditing(true)
|
||||||
// redirigimos a la pestaña de Contenido en vez de editar inline.
|
|
||||||
if (xColumn === 'contenido_tematico') {
|
if (onClickEditButton) {
|
||||||
// Agregamos un timestamp para forzar la actualización
|
onClickEditButton({ startEditing })
|
||||||
// de la location.state aunque la ruta sea la misma.
|
|
||||||
navigate({
|
|
||||||
to: '/planes/$planId/asignaturas/$asignaturaId/contenido',
|
|
||||||
params: { planId, asignaturaId: asignaturaId! },
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsEditing(true)
|
startEditing()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
@@ -404,17 +541,177 @@ function InfoCard({
|
|||||||
<CardContent className="pt-4">
|
<CardContent className="pt-4">
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
{type === 'evaluation' ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{evalRows.map((row) => (
|
||||||
|
<div
|
||||||
|
key={row.id}
|
||||||
|
className="grid grid-cols-[2fr_1fr_1ch_32px] items-center gap-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={row.criterio}
|
||||||
|
placeholder="Criterio"
|
||||||
|
onChange={(e) => {
|
||||||
|
const nextCriterio = e.target.value
|
||||||
|
setEvalRows((prev) =>
|
||||||
|
prev.map((r) =>
|
||||||
|
r.id === row.id
|
||||||
|
? { ...r, criterio: nextCriterio }
|
||||||
|
: r,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
value={row.porcentaje}
|
||||||
|
placeholder="%"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
|
inputMode="numeric"
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value
|
||||||
|
// Solo permitir '' o dígitos
|
||||||
|
if (raw !== '' && !/^\d+$/.test(raw)) return
|
||||||
|
|
||||||
|
if (raw === '') {
|
||||||
|
setEvalRows((prev) =>
|
||||||
|
prev.map((r) =>
|
||||||
|
r.id === row.id
|
||||||
|
? {
|
||||||
|
id: r.id,
|
||||||
|
criterio: r.criterio,
|
||||||
|
porcentaje: '',
|
||||||
|
}
|
||||||
|
: r,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = Number(raw)
|
||||||
|
if (!Number.isFinite(n)) return
|
||||||
|
const porcentaje = Math.trunc(n)
|
||||||
|
if (porcentaje < 1 || porcentaje > 100) return
|
||||||
|
|
||||||
|
// No permitir suma > 100
|
||||||
|
setEvalRows((prev) => {
|
||||||
|
const next = prev.map((r) =>
|
||||||
|
r.id === row.id
|
||||||
|
? {
|
||||||
|
id: r.id,
|
||||||
|
criterio: r.criterio,
|
||||||
|
porcentaje: raw,
|
||||||
|
}
|
||||||
|
: r,
|
||||||
|
)
|
||||||
|
|
||||||
|
const total = next.reduce((acc, r) => {
|
||||||
|
const v = String(r.porcentaje).trim()
|
||||||
|
if (!v) return acc
|
||||||
|
const nn = Number(v)
|
||||||
|
if (!Number.isFinite(nn)) return acc
|
||||||
|
const vv = Math.trunc(nn)
|
||||||
|
if (vv < 1 || vv > 100) return acc
|
||||||
|
return acc + vv
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return total > 100 ? prev : next
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="flex w-[1ch] items-center justify-center text-sm text-slate-600"
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
%
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-red-600 hover:bg-red-50"
|
||||||
|
onClick={() => {
|
||||||
|
setEvalRows((prev) =>
|
||||||
|
prev.filter((r) => r.id !== row.id),
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
aria-label="Quitar renglón"
|
||||||
|
title="Quitar"
|
||||||
|
>
|
||||||
|
<Minus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
'text-sm ' +
|
||||||
|
(evaluationTotal === 100
|
||||||
|
? 'text-muted-foreground'
|
||||||
|
: 'text-destructive font-semibold')
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Total: {evaluationTotal}/100
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-emerald-700 hover:bg-emerald-50"
|
||||||
|
onClick={() => {
|
||||||
|
// Agregar una fila vacía (siempre permitido)
|
||||||
|
setEvalRows((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
criterio: '',
|
||||||
|
porcentaje: '',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="mr-2 h-4 w-4" /> Agregar renglón
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Textarea
|
<Textarea
|
||||||
value={tempText}
|
value={tempText}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={(e) => setTempText(e.target.value)}
|
onChange={(e) => setTempText(e.target.value)}
|
||||||
className="min-h-30 text-sm leading-relaxed"
|
className="min-h-30 text-sm leading-relaxed"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => setIsEditing(false)}
|
onClick={() => {
|
||||||
|
setIsEditing(false)
|
||||||
|
if (type === 'evaluation') {
|
||||||
|
const raw = Array.isArray(data) ? data : []
|
||||||
|
setEvalRows(
|
||||||
|
raw.map((r: CriterioEvaluacionRow) => ({
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
criterio:
|
||||||
|
typeof r.criterio === 'string' ? r.criterio : '',
|
||||||
|
porcentaje:
|
||||||
|
typeof r.porcentaje === 'number'
|
||||||
|
? String(Math.trunc(r.porcentaje))
|
||||||
|
: typeof r.porcentaje === 'string'
|
||||||
|
? String(Math.trunc(Number(r.porcentaje)))
|
||||||
|
: '',
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
@@ -422,6 +719,7 @@ function InfoCard({
|
|||||||
size="sm"
|
size="sm"
|
||||||
className="bg-[#00a878] hover:bg-[#008f66]"
|
className="bg-[#00a878] hover:bg-[#008f66]"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
disabled={type === 'evaluation' && evaluationTotal > 100}
|
||||||
>
|
>
|
||||||
Guardar
|
Guardar
|
||||||
</Button>
|
</Button>
|
||||||
@@ -436,11 +734,14 @@ function InfoCard({
|
|||||||
<p className="text-slate-400 italic">Sin información.</p>
|
<p className="text-slate-400 italic">Sin información.</p>
|
||||||
))}
|
))}
|
||||||
{type === 'requirements' && <RequirementsView items={data} />}
|
{type === 'requirements' && <RequirementsView items={data} />}
|
||||||
{type === 'evaluation' && <EvaluationView items={data} />}
|
{type === 'evaluation' && (
|
||||||
|
<EvaluationView items={data as Array<CriterioEvaluacionRow>} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +767,11 @@ function RequirementsView({ items }: { items: Array<any> }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vista de Evaluación
|
// Vista de Evaluación
|
||||||
function EvaluationView({ items }: { items: Array<any> }) {
|
function EvaluationView({ items }: { items: Array<CriterioEvaluacionRow> }) {
|
||||||
|
const porcentajeTotal = items.reduce(
|
||||||
|
(total, item) => total + Number(item.porcentaje),
|
||||||
|
0,
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
@@ -474,10 +779,92 @@ function EvaluationView({ items }: { items: Array<any> }) {
|
|||||||
key={i}
|
key={i}
|
||||||
className="flex justify-between border-b border-slate-50 pb-1.5 text-sm italic"
|
className="flex justify-between border-b border-slate-50 pb-1.5 text-sm italic"
|
||||||
>
|
>
|
||||||
<span className="text-slate-500">{item.label}</span>
|
<span className="text-slate-500">{item.criterio}</span>
|
||||||
<span className="font-bold text-blue-600">{item.value}</span>
|
<span className="font-bold text-blue-600">{item.porcentaje}%</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{porcentajeTotal < 100 && (
|
||||||
|
<p className="text-destructive text-sm font-medium">
|
||||||
|
El porcentaje total es menor a 100%.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseContenidoTematicoToPlainText(value: unknown): string {
|
||||||
|
if (!Array.isArray(value)) return ''
|
||||||
|
|
||||||
|
const blocks: Array<string> = []
|
||||||
|
|
||||||
|
for (const item of value) {
|
||||||
|
if (!isRecord(item)) continue
|
||||||
|
|
||||||
|
const unidad =
|
||||||
|
typeof item.unidad === 'number' && Number.isFinite(item.unidad)
|
||||||
|
? item.unidad
|
||||||
|
: undefined
|
||||||
|
const titulo = typeof item.titulo === 'string' ? item.titulo : ''
|
||||||
|
|
||||||
|
const header = `${unidad ?? ''}${unidad ? '.' : ''} ${titulo}`.trim()
|
||||||
|
if (!header) continue
|
||||||
|
|
||||||
|
const lines: Array<string> = [header]
|
||||||
|
|
||||||
|
const temas = Array.isArray(item.temas) ? item.temas : []
|
||||||
|
temas.forEach((tema, idx) => {
|
||||||
|
const temaNombre =
|
||||||
|
typeof tema === 'string'
|
||||||
|
? tema
|
||||||
|
: isRecord(tema) && typeof tema.nombre === 'string'
|
||||||
|
? tema.nombre
|
||||||
|
: ''
|
||||||
|
if (!temaNombre) return
|
||||||
|
|
||||||
|
if (unidad != null) {
|
||||||
|
lines.push(`${unidad}.${idx + 1} ${temaNombre}`.trim())
|
||||||
|
} else {
|
||||||
|
lines.push(`${idx + 1}. ${temaNombre}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
blocks.push(lines.join('\n'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks.join('\n\n').trimEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCriteriosEvaluacionToPlainText(value: unknown): string {
|
||||||
|
if (!Array.isArray(value)) return ''
|
||||||
|
|
||||||
|
const lines: Array<string> = []
|
||||||
|
for (const item of value) {
|
||||||
|
if (!isRecord(item)) continue
|
||||||
|
const label = typeof item.criterio === 'string' ? item.criterio.trim() : ''
|
||||||
|
const valueNum =
|
||||||
|
typeof item.porcentaje === 'number'
|
||||||
|
? item.porcentaje
|
||||||
|
: typeof item.porcentaje === 'string'
|
||||||
|
? Number(item.porcentaje)
|
||||||
|
: NaN
|
||||||
|
|
||||||
|
if (!label) continue
|
||||||
|
if (!Number.isFinite(valueNum)) continue
|
||||||
|
|
||||||
|
const v = Math.trunc(valueNum)
|
||||||
|
if (v < 1 || v > 100) continue
|
||||||
|
|
||||||
|
lines.push(`${label}: ${v}%`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnParsers: Partial<Record<string, (value: unknown) => string>> = {
|
||||||
|
contenido_tematico: parseContenidoTematicoToPlainText,
|
||||||
|
criterios_de_evaluacion: parseCriteriosEvaluacionToPlainText,
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const ImprovementCard = ({
|
|||||||
suggestions,
|
suggestions,
|
||||||
onApply,
|
onApply,
|
||||||
planId,
|
planId,
|
||||||
|
dbMessageId,
|
||||||
currentDatos,
|
currentDatos,
|
||||||
activeChatId,
|
activeChatId,
|
||||||
onApplySuccess,
|
onApplySuccess,
|
||||||
@@ -16,6 +17,7 @@ export const ImprovementCard = ({
|
|||||||
onApply?: (key: string, value: string) => void
|
onApply?: (key: string, value: string) => void
|
||||||
planId: string
|
planId: string
|
||||||
currentDatos: any
|
currentDatos: any
|
||||||
|
dbMessageId: string
|
||||||
activeChatId: any
|
activeChatId: any
|
||||||
onApplySuccess?: (key: string) => void
|
onApplySuccess?: (key: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
@@ -53,9 +55,11 @@ export const ImprovementCard = ({
|
|||||||
setLocalApplied((prev) => [...prev, key])
|
setLocalApplied((prev) => [...prev, key])
|
||||||
|
|
||||||
if (onApplySuccess) onApplySuccess(key)
|
if (onApplySuccess) onApplySuccess(key)
|
||||||
if (activeChatId) {
|
|
||||||
|
// --- CAMBIO AQUÍ: Ahora enviamos el ID del mensaje ---
|
||||||
|
if (dbMessageId) {
|
||||||
updateAppliedStatus.mutate({
|
updateAppliedStatus.mutate({
|
||||||
conversacionId: activeChatId,
|
conversacionId: dbMessageId, // Cambiamos el nombre de la propiedad si es necesario
|
||||||
campoAfectado: key,
|
campoAfectado: key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export async function library_search(payload: {
|
|||||||
export async function create_conversation(planId: string) {
|
export async function create_conversation(planId: string) {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const { data, error } = await supabase.functions.invoke(
|
const { data, error } = await supabase.functions.invoke(
|
||||||
'create-chat-conversation/conversations',
|
'create-chat-conversation/plan/conversations',
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
@@ -149,7 +149,7 @@ export async function ai_plan_chat_v2(payload: {
|
|||||||
}): Promise<{ reply: string; meta?: any }> {
|
}): Promise<{ reply: string; meta?: any }> {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
const { data, error } = await supabase.functions.invoke(
|
const { data, error } = await supabase.functions.invoke(
|
||||||
`create-chat-conversation/conversations/${payload.conversacionId}/messages`,
|
`create-chat-conversation/conversations/plan/${payload.conversacionId}/messages`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
@@ -175,6 +175,22 @@ export async function getConversationByPlan(planId: string) {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
return data ?? []
|
return data ?? []
|
||||||
}
|
}
|
||||||
|
export async function getMessagesByConversation(conversationId: string) {
|
||||||
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
|
const { data, error } = await supabase
|
||||||
|
.from('plan_mensajes_ia')
|
||||||
|
.select('*')
|
||||||
|
.eq('conversacion_plan_id', conversationId)
|
||||||
|
.order('fecha_creacion', { ascending: true }) // Ascendente para que el chat fluya en orden cronológico
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('Error al obtener mensajes:', error.message)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ?? []
|
||||||
|
}
|
||||||
|
|
||||||
export async function update_conversation_title(
|
export async function update_conversation_title(
|
||||||
conversacionId: string,
|
conversacionId: string,
|
||||||
@@ -194,45 +210,40 @@ export async function update_conversation_title(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function update_recommendation_applied_status(
|
export async function update_recommendation_applied_status(
|
||||||
conversacionId: string,
|
mensajeId: string, // Ahora es más eficiente usar el ID del mensaje directamente
|
||||||
campoAfectado: string,
|
campoAfectado: string,
|
||||||
) {
|
) {
|
||||||
const supabase = supabaseBrowser()
|
const supabase = supabaseBrowser()
|
||||||
|
|
||||||
// 1. Obtener el estado actual del JSON
|
// 1. Obtener la propuesta actual de ese mensaje específico
|
||||||
const { data: conv, error: fetchError } = await supabase
|
const { data: msgData, error: fetchError } = await supabase
|
||||||
.from('conversaciones_plan')
|
.from('plan_mensajes_ia')
|
||||||
.select('conversacion_json')
|
.select('propuesta')
|
||||||
.eq('id', conversacionId)
|
.eq('id', mensajeId)
|
||||||
.single()
|
.single()
|
||||||
|
|
||||||
if (fetchError) throw fetchError
|
if (fetchError) throw fetchError
|
||||||
if (!conv.conversacion_json) throw new Error('No se encontró la conversación')
|
if (!msgData?.propuesta)
|
||||||
|
throw new Error('No se encontró la propuesta en el mensaje')
|
||||||
|
|
||||||
// 2. Transformar el JSON para marcar como aplicada la recomendación específica
|
const propuestaActual = msgData.propuesta as any
|
||||||
// Usamos una transformación inmutable para evitar efectos secundarios
|
|
||||||
const nuevoJson = (conv.conversacion_json as Array<any>).map((msg) => {
|
// 2. Modificar el array de recommendations dentro de la propuesta
|
||||||
if (msg.user === 'assistant' && Array.isArray(msg.recommendations)) {
|
// Mantenemos el resto de la propuesta (prompt, respuesta, etc.) intacto
|
||||||
return {
|
const nuevaPropuesta = {
|
||||||
...msg,
|
...propuestaActual,
|
||||||
recommendations: msg.recommendations.map((rec: any) =>
|
recommendations: (propuestaActual.recommendations || []).map((rec: any) =>
|
||||||
rec.campo_afectado === campoAfectado
|
rec.campo_afectado === campoAfectado ? { ...rec, aplicada: true } : rec,
|
||||||
? { ...rec, aplicada: true }
|
|
||||||
: rec,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return msg
|
|
||||||
})
|
|
||||||
|
|
||||||
// 3. Actualizar la base de datos con el nuevo JSON
|
// 3. Actualizar la base de datos con el nuevo objeto JSON
|
||||||
const { data, error: updateError } = await supabase
|
const { error: updateError } = await supabase
|
||||||
.from('conversaciones_plan')
|
.from('plan_mensajes_ia')
|
||||||
.update({ conversacion_json: nuevoJson })
|
.update({ propuesta: nuevaPropuesta })
|
||||||
.eq('id', conversacionId)
|
.eq('id', mensajeId)
|
||||||
.select()
|
|
||||||
.single()
|
|
||||||
|
|
||||||
if (updateError) throw updateError
|
if (updateError) throw updateError
|
||||||
return data
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,15 @@
|
|||||||
const DOCUMENT_PDF_URL =
|
const DOCUMENT_PDF_URL =
|
||||||
'https://n8n.app.lci.ulsa.mx/webhook/62ca84ec-0adb-4006-aba1-32282d27d434'
|
'https://n8n.app.lci.ulsa.mx/webhook/62ca84ec-0adb-4006-aba1-32282d27d434'
|
||||||
|
|
||||||
|
const DOCUMENT_PDF_ASIGNATURA_URL =
|
||||||
|
'https://n8n.app.lci.ulsa.mx/webhook/041a68be-7568-46d0-bc08-09ded12d017d'
|
||||||
|
|
||||||
interface GeneratePdfParams {
|
interface GeneratePdfParams {
|
||||||
plan_estudio_id: string
|
plan_estudio_id: string
|
||||||
}
|
}
|
||||||
|
interface GeneratePdfParamsAsignatura {
|
||||||
|
asignatura_id: string
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchPlanPdf({
|
export async function fetchPlanPdf({
|
||||||
plan_estudio_id,
|
plan_estudio_id,
|
||||||
@@ -25,3 +31,22 @@ export async function fetchPlanPdf({
|
|||||||
// n8n devuelve el archivo → lo tratamos como blob
|
// n8n devuelve el archivo → lo tratamos como blob
|
||||||
return await response.blob()
|
return await response.blob()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchAsignaturaPdf({
|
||||||
|
asignatura_id,
|
||||||
|
}: GeneratePdfParamsAsignatura): Promise<Blob> {
|
||||||
|
const response = await fetch(DOCUMENT_PDF_ASIGNATURA_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ asignatura_id }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Error al generar el PDF')
|
||||||
|
}
|
||||||
|
|
||||||
|
// n8n devuelve el archivo → lo tratamos como blob
|
||||||
|
return await response.blob()
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,12 +112,12 @@ export async function subjects_get(subjectId: UUID): Promise<AsignaturaDetail> {
|
|||||||
.from('asignaturas')
|
.from('asignaturas')
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,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,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion,
|
||||||
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))
|
||||||
),
|
),
|
||||||
estructuras_asignatura(id,nombre,version,definicion)
|
estructuras_asignatura(id,nombre,definicion)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.eq('id', subjectId)
|
.eq('id', subjectId)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
update_conversation_status,
|
update_conversation_status,
|
||||||
update_recommendation_applied_status,
|
update_recommendation_applied_status,
|
||||||
update_conversation_title,
|
update_conversation_title,
|
||||||
|
getMessagesByConversation,
|
||||||
} from '../api/ai.api'
|
} from '../api/ai.api'
|
||||||
|
|
||||||
// eslint-disable-next-line node/prefer-node-protocol
|
// eslint-disable-next-line node/prefer-node-protocol
|
||||||
@@ -88,6 +89,25 @@ export function useConversationByPlan(planId: string | null) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useMessagesByChat(conversationId: string | null) {
|
||||||
|
return useQuery({
|
||||||
|
// La queryKey debe ser única; incluimos el ID para que se refresque al cambiar de chat
|
||||||
|
queryKey: ['conversation-messages', conversationId],
|
||||||
|
|
||||||
|
// Solo ejecutamos la función si el ID no es null o undefined
|
||||||
|
queryFn: () => {
|
||||||
|
if (!conversationId) throw new Error('Conversation ID is required')
|
||||||
|
return getMessagesByConversation(conversationId)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Importante: 'enabled' controla que no se dispare la petición si no hay ID
|
||||||
|
enabled: !!conversationId,
|
||||||
|
|
||||||
|
// Opcional: Mantener los datos previos mientras se carga la nueva conversación
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useUpdateRecommendationApplied() {
|
export function useUpdateRecommendationApplied() {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { Textarea } from '@/components/ui/textarea'
|
|||||||
import {
|
import {
|
||||||
useAIPlanChat,
|
useAIPlanChat,
|
||||||
useConversationByPlan,
|
useConversationByPlan,
|
||||||
|
useMessagesByChat,
|
||||||
useUpdateConversationStatus,
|
useUpdateConversationStatus,
|
||||||
useUpdateConversationTitle,
|
useUpdateConversationTitle,
|
||||||
} from '@/data'
|
} from '@/data'
|
||||||
@@ -103,6 +104,8 @@ function RouteComponent() {
|
|||||||
)
|
)
|
||||||
const { data: lastConversation, isLoading: isLoadingConv } =
|
const { data: lastConversation, isLoading: isLoadingConv } =
|
||||||
useConversationByPlan(planId)
|
useConversationByPlan(planId)
|
||||||
|
const { data: mensajesDelChat, isLoading: isLoadingMessages } =
|
||||||
|
useMessagesByChat(activeChatId)
|
||||||
const [selectedArchivoIds, setSelectedArchivoIds] = useState<Array<string>>(
|
const [selectedArchivoIds, setSelectedArchivoIds] = useState<Array<string>>(
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
@@ -154,36 +157,32 @@ function RouteComponent() {
|
|||||||
}, [lastConversation, activeChatId])
|
}, [lastConversation, activeChatId])
|
||||||
|
|
||||||
const chatMessages = useMemo(() => {
|
const chatMessages = useMemo(() => {
|
||||||
// 1. Si no hay ID o no hay data del chat, retornamos vacío
|
if (!activeChatId || !mensajesDelChat) return []
|
||||||
if (!activeChatId || !activeChatData) return []
|
|
||||||
|
|
||||||
const json = (activeChatData.conversacion_json ||
|
// flatMap nos permite devolver 2 elementos (pregunta y respuesta) por cada registro de la BD
|
||||||
[]) as unknown as Array<ChatMessageJSON>
|
return mensajesDelChat.flatMap((msg: any) => {
|
||||||
|
const messages = []
|
||||||
|
|
||||||
// 2. Verificamos que 'json' sea realmente un array antes de mapear
|
// 1. Mensaje del Usuario
|
||||||
if (!Array.isArray(json)) return []
|
messages.push({
|
||||||
|
id: `${msg.id}-user`,
|
||||||
|
role: 'user',
|
||||||
|
content: msg.mensaje,
|
||||||
|
selectedFields: msg.campos || [], // Aquí están tus campos
|
||||||
|
})
|
||||||
|
|
||||||
return json.map((msg, index: number) => {
|
// 2. Mensaje del Asistente (si hay respuesta)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
if (msg.respuesta) {
|
||||||
if (!msg?.user) {
|
// Extraemos las recomendaciones de la nueva estructura: msg.propuesta.recommendations
|
||||||
return {
|
const rawRecommendations = msg.propuesta?.recommendations || []
|
||||||
id: `err-${index}`,
|
|
||||||
|
messages.push({
|
||||||
|
id: `${msg.id}-ai`,
|
||||||
|
dbMessageId: msg.id,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: '',
|
content: msg.respuesta,
|
||||||
suggestions: [],
|
isRefusal: msg.is_refusal,
|
||||||
}
|
suggestions: rawRecommendations.map((rec: any) => {
|
||||||
}
|
|
||||||
|
|
||||||
const isAssistant = msg.user === 'assistant'
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: `${activeChatId}-${index}`,
|
|
||||||
role: isAssistant ? 'assistant' : 'user',
|
|
||||||
content: isAssistant ? msg.message || '' : msg.prompt || '', // Agregamos fallback a string vacío
|
|
||||||
isRefusal: isAssistant && msg.refusal === true,
|
|
||||||
suggestions:
|
|
||||||
isAssistant && msg.recommendations
|
|
||||||
? msg.recommendations.map((rec) => {
|
|
||||||
const fieldConfig = availableFields.find(
|
const fieldConfig = availableFields.find(
|
||||||
(f) => f.key === rec.campo_afectado,
|
(f) => f.key === rec.campo_afectado,
|
||||||
)
|
)
|
||||||
@@ -195,12 +194,13 @@ function RouteComponent() {
|
|||||||
newValue: rec.texto_mejora,
|
newValue: rec.texto_mejora,
|
||||||
applied: rec.aplicada,
|
applied: rec.aplicada,
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
: [],
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}, [activeChatData, activeChatId, availableFields])
|
|
||||||
|
|
||||||
|
return messages
|
||||||
|
})
|
||||||
|
}, [mensajesDelChat, activeChatId, availableFields])
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (scrollRef.current) {
|
if (scrollRef.current) {
|
||||||
// Buscamos el viewport interno del ScrollArea de Radix
|
// Buscamos el viewport interno del ScrollArea de Radix
|
||||||
@@ -226,6 +226,8 @@ function RouteComponent() {
|
|||||||
}, [lastConversation])
|
}, [lastConversation])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log(mensajesDelChat)
|
||||||
|
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, [chatMessages, isLoading])
|
}, [chatMessages, isLoading])
|
||||||
|
|
||||||
@@ -242,30 +244,38 @@ function RouteComponent() {
|
|||||||
}, [input, selectedFields])
|
}, [input, selectedFields])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoadingConv || !lastConversation) return
|
if (isLoadingConv || isSending) return
|
||||||
|
|
||||||
const isChatStillActive = activeChats.some(
|
const currentChatExists = activeChats.some(
|
||||||
(chat) => chat.id === activeChatId,
|
(chat) => chat.id === activeChatId,
|
||||||
)
|
)
|
||||||
const isCreationMode = messages.length === 1 && messages[0].id === 'welcome'
|
const isCreationMode = messages.length === 1 && messages[0].id === 'welcome'
|
||||||
|
|
||||||
// Caso A: El chat actual ya no es válido (fue archivado o borrado)
|
// 1. Si el chat que teníamos seleccionado ya no existe (ej. se archivó)
|
||||||
if (activeChatId && !isChatStillActive && !isCreationMode) {
|
if (activeChatId && !currentChatExists && !isCreationMode) {
|
||||||
setActiveChatId(undefined)
|
setActiveChatId(undefined)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
return // Salimos para evitar ejecuciones extra en este render
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caso B: No hay chat seleccionado y hay chats disponibles (Auto-selección al cargar)
|
// 2. Auto-selección inicial: Solo si no hay ID, no estamos creando y hay chats
|
||||||
if (!activeChatId && activeChats.length > 0 && !isCreationMode) {
|
if (
|
||||||
|
!activeChatId &&
|
||||||
|
activeChats.length > 0 &&
|
||||||
|
!isCreationMode &&
|
||||||
|
chatMessages.length === 0
|
||||||
|
) {
|
||||||
setActiveChatId(activeChats[0].id)
|
setActiveChatId(activeChats[0].id)
|
||||||
}
|
}
|
||||||
|
}, [
|
||||||
|
activeChats,
|
||||||
|
activeChatId,
|
||||||
|
isLoadingConv,
|
||||||
|
isSending,
|
||||||
|
messages.length,
|
||||||
|
chatMessages.length,
|
||||||
|
])
|
||||||
|
|
||||||
// Caso C: Si la lista de chats está vacía y no estamos creando uno, limpiar por si acaso
|
|
||||||
if (activeChats.length === 0 && activeChatId && !isCreationMode) {
|
|
||||||
setActiveChatId(undefined)
|
|
||||||
}
|
|
||||||
}, [activeChats, activeChatId, isLoadingConv, messages.length])
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = routerState.location.state as any
|
const state = routerState.location.state as any
|
||||||
if (!state?.campo_edit || availableFields.length === 0) return
|
if (!state?.campo_edit || availableFields.length === 0) return
|
||||||
@@ -352,13 +362,16 @@ function RouteComponent() {
|
|||||||
input: string,
|
input: string,
|
||||||
fields: Array<SelectedField>,
|
fields: Array<SelectedField>,
|
||||||
) => {
|
) => {
|
||||||
const cleaned = input.replace(/\n?\[Campos:[^\]]*]/g, '').trim()
|
// 1. Limpiamos cualquier rastro anterior de la etiqueta (por si acaso)
|
||||||
|
// Esta regex ahora también limpia si el texto termina de forma natural
|
||||||
|
const cleaned = input.replace(/[:\s]+[^:]*$/, '').trim()
|
||||||
|
|
||||||
if (fields.length === 0) return cleaned
|
if (fields.length === 0) return cleaned
|
||||||
|
|
||||||
const fieldLabels = fields.map((f) => f.label).join(', ')
|
const fieldLabels = fields.map((f) => f.label).join(', ')
|
||||||
|
|
||||||
return `${cleaned}\n[Campos: ${fieldLabels}]`
|
// 2. Devolvemos un formato natural: "Mejora este campo: Nombre del Campo"
|
||||||
|
return `${cleaned}: ${fieldLabels}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleField = (field: SelectedField) => {
|
const toggleField = (field: SelectedField) => {
|
||||||
@@ -388,42 +401,46 @@ function RouteComponent() {
|
|||||||
|
|
||||||
const handleSend = async (promptOverride?: string) => {
|
const handleSend = async (promptOverride?: string) => {
|
||||||
const rawText = promptOverride || input
|
const rawText = promptOverride || input
|
||||||
if (!rawText.trim() && selectedFields.length === 0) return
|
|
||||||
if (isSending || (!rawText.trim() && selectedFields.length === 0)) return
|
if (isSending || (!rawText.trim() && selectedFields.length === 0)) return
|
||||||
|
|
||||||
const currentFields = [...selectedFields]
|
const currentFields = [...selectedFields]
|
||||||
const finalPrompt = buildPrompt(rawText, currentFields)
|
|
||||||
setIsSending(true)
|
setIsSending(true)
|
||||||
setOptimisticMessage(rawText)
|
setOptimisticMessage(rawText)
|
||||||
setInput('')
|
|
||||||
setSelectedArchivoIds([])
|
|
||||||
setSelectedRepositorioIds([])
|
|
||||||
setUploadedFiles([])
|
|
||||||
try {
|
|
||||||
const payload: any = {
|
|
||||||
planId: planId,
|
|
||||||
content: finalPrompt,
|
|
||||||
conversacionId: activeChatId || undefined,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentFields.length > 0) {
|
// Limpiar input inmediatamente para feedback visual
|
||||||
payload.campos = currentFields.map((f) => f.key)
|
setInput('')
|
||||||
|
setSelectedFields([])
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
planId,
|
||||||
|
content: buildPrompt(rawText, currentFields),
|
||||||
|
conversacionId: activeChatId,
|
||||||
|
campos:
|
||||||
|
currentFields.length > 0
|
||||||
|
? currentFields.map((f) => f.key)
|
||||||
|
: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await sendChat(payload)
|
const response = await sendChat(payload)
|
||||||
|
|
||||||
|
// IMPORTANTE: Si es un chat nuevo, actualizar el ID antes de invalidar
|
||||||
if (response.conversacionId && response.conversacionId !== activeChatId) {
|
if (response.conversacionId && response.conversacionId !== activeChatId) {
|
||||||
setActiveChatId(response.conversacionId)
|
setActiveChatId(response.conversacionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
await queryClient.invalidateQueries({
|
// Invalidar ambas para asegurar que la lista de la izquierda y los mensajes se
|
||||||
|
await Promise.all([
|
||||||
|
queryClient.invalidateQueries({
|
||||||
queryKey: ['conversation-by-plan', planId],
|
queryKey: ['conversation-by-plan', planId],
|
||||||
})
|
}),
|
||||||
setOptimisticMessage(null)
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ['conversation-messages', response.conversacionId],
|
||||||
|
}),
|
||||||
|
])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error en el chat:', error)
|
console.error('Error:', error)
|
||||||
// Aquí sí podrías usar un toast o un mensaje de error temporal
|
|
||||||
} finally {
|
} finally {
|
||||||
// 5. CRÍTICO: Detener el estado de carga SIEMPRE
|
|
||||||
setIsSending(false)
|
setIsSending(false)
|
||||||
setOptimisticMessage(null)
|
setOptimisticMessage(null)
|
||||||
}
|
}
|
||||||
@@ -666,6 +683,7 @@ function RouteComponent() {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<ImprovementCard
|
<ImprovementCard
|
||||||
suggestions={msg.suggestions}
|
suggestions={msg.suggestions}
|
||||||
|
dbMessageId={msg.dbMessageId}
|
||||||
planId={planId}
|
planId={planId}
|
||||||
currentDatos={data?.datos}
|
currentDatos={data?.datos}
|
||||||
activeChatId={activeChatId}
|
activeChatId={activeChatId}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createFileRoute, useParams } from '@tanstack/react-router'
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { DocumentoSEPTab } from '@/components/asignaturas/detalle/DocumentoSEPTab'
|
import { DocumentoSEPTab } from '@/components/asignaturas/detalle/DocumentoSEPTab'
|
||||||
import { fetchPlanPdf } from '@/data/api/document.api'
|
import { fetchAsignaturaPdf } from '@/data/api/document.api'
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
'/planes/$planId/asignaturas/$asignaturaId/documento',
|
'/planes/$planId/asignaturas/$asignaturaId/documento',
|
||||||
@@ -11,7 +11,7 @@ export const Route = createFileRoute(
|
|||||||
})
|
})
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { planId } = useParams({
|
const { asignaturaId } = useParams({
|
||||||
from: '/planes/$planId/asignaturas/$asignaturaId/documento',
|
from: '/planes/$planId/asignaturas/$asignaturaId/documento',
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ function RouteComponent() {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
|
|
||||||
const pdfBlob = await fetchPlanPdf({
|
const pdfBlob = await fetchAsignaturaPdf({
|
||||||
plan_estudio_id: planId,
|
asignatura_id: asignaturaId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(pdfBlob)
|
const url = window.URL.createObjectURL(pdfBlob)
|
||||||
@@ -38,7 +38,7 @@ function RouteComponent() {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}, [planId])
|
}, [asignaturaId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadPdfPreview()
|
loadPdfPreview()
|
||||||
@@ -49,8 +49,8 @@ function RouteComponent() {
|
|||||||
}, [loadPdfPreview])
|
}, [loadPdfPreview])
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
const pdfBlob = await fetchPlanPdf({
|
const pdfBlob = await fetchAsignaturaPdf({
|
||||||
plan_estudio_id: planId,
|
asignatura_id: asignaturaId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const url = window.URL.createObjectURL(pdfBlob)
|
const url = window.URL.createObjectURL(pdfBlob)
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ function AsignaturaLayout() {
|
|||||||
{ label: 'Datos', to: '' },
|
{ label: 'Datos', to: '' },
|
||||||
{ label: 'Contenido', to: 'contenido' },
|
{ label: 'Contenido', to: 'contenido' },
|
||||||
{ label: 'Bibliografía', to: 'bibliografia' },
|
{ label: 'Bibliografía', to: 'bibliografia' },
|
||||||
{ label: 'IA', to: 'asignaturaIa' },
|
{ label: 'IA', to: 'iaasignatura' },
|
||||||
{ label: 'Documento SEP', to: 'documento' },
|
{ label: 'Documento SEP', to: 'documento' },
|
||||||
{ label: 'Historial', to: 'historial' },
|
{ label: 'Historial', to: 'historial' },
|
||||||
].map((tab) => {
|
].map((tab) => {
|
||||||
|
|||||||
@@ -81,6 +81,56 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
asignatura_mensajes_ia: {
|
||||||
|
Row: {
|
||||||
|
campos: Array<string>
|
||||||
|
conversacion_asignatura_id: string
|
||||||
|
enviado_por: string
|
||||||
|
estado: Database['public']['Enums']['estado_mensaje_ia']
|
||||||
|
fecha_actualizacion: string
|
||||||
|
fecha_creacion: string
|
||||||
|
id: string
|
||||||
|
is_refusal: boolean
|
||||||
|
mensaje: string
|
||||||
|
propuesta: Json | null
|
||||||
|
respuesta: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
campos?: Array<string>
|
||||||
|
conversacion_asignatura_id: string
|
||||||
|
enviado_por?: string
|
||||||
|
estado?: Database['public']['Enums']['estado_mensaje_ia']
|
||||||
|
fecha_actualizacion?: string
|
||||||
|
fecha_creacion?: string
|
||||||
|
id?: string
|
||||||
|
is_refusal?: boolean
|
||||||
|
mensaje: string
|
||||||
|
propuesta?: Json | null
|
||||||
|
respuesta?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
campos?: Array<string>
|
||||||
|
conversacion_asignatura_id?: string
|
||||||
|
enviado_por?: string
|
||||||
|
estado?: Database['public']['Enums']['estado_mensaje_ia']
|
||||||
|
fecha_actualizacion?: string
|
||||||
|
fecha_creacion?: string
|
||||||
|
id?: string
|
||||||
|
is_refusal?: boolean
|
||||||
|
mensaje?: string
|
||||||
|
propuesta?: Json | null
|
||||||
|
respuesta?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: 'asignatura_mensajes_ia_conversacion_asignatura_id_fkey'
|
||||||
|
columns: ['conversacion_asignatura_id']
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: 'conversaciones_asignatura'
|
||||||
|
referencedColumns: ['id']
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
asignaturas: {
|
asignaturas: {
|
||||||
Row: {
|
Row: {
|
||||||
actualizado_en: string
|
actualizado_en: string
|
||||||
@@ -91,6 +141,7 @@ export type Database = {
|
|||||||
creado_en: string
|
creado_en: string
|
||||||
creado_por: string | null
|
creado_por: string | null
|
||||||
creditos: number
|
creditos: number
|
||||||
|
criterios_de_evaluacion: Json
|
||||||
datos: Json
|
datos: Json
|
||||||
estado: Database['public']['Enums']['estado_asignatura']
|
estado: Database['public']['Enums']['estado_asignatura']
|
||||||
estructura_id: string | null
|
estructura_id: string | null
|
||||||
@@ -115,6 +166,7 @@ export type Database = {
|
|||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
creditos: number
|
creditos: number
|
||||||
|
criterios_de_evaluacion?: Json
|
||||||
datos?: Json
|
datos?: Json
|
||||||
estado?: Database['public']['Enums']['estado_asignatura']
|
estado?: Database['public']['Enums']['estado_asignatura']
|
||||||
estructura_id?: string | null
|
estructura_id?: string | null
|
||||||
@@ -139,6 +191,7 @@ export type Database = {
|
|||||||
creado_en?: string
|
creado_en?: string
|
||||||
creado_por?: string | null
|
creado_por?: string | null
|
||||||
creditos?: number
|
creditos?: number
|
||||||
|
criterios_de_evaluacion?: Json
|
||||||
datos?: Json
|
datos?: Json
|
||||||
estado?: Database['public']['Enums']['estado_asignatura']
|
estado?: Database['public']['Enums']['estado_asignatura']
|
||||||
estructura_id?: string | null
|
estructura_id?: string | null
|
||||||
@@ -176,6 +229,13 @@ export type Database = {
|
|||||||
referencedRelation: 'estructuras_asignatura'
|
referencedRelation: 'estructuras_asignatura'
|
||||||
referencedColumns: ['id']
|
referencedColumns: ['id']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'asignaturas_estructura_id_fkey'
|
||||||
|
columns: ['estructura_id']
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: 'plantilla_asignatura'
|
||||||
|
referencedColumns: ['estructura_id']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
foreignKeyName: 'asignaturas_linea_plan_fk_compuesta'
|
foreignKeyName: 'asignaturas_linea_plan_fk_compuesta'
|
||||||
columns: ['linea_plan_id', 'plan_estudio_id']
|
columns: ['linea_plan_id', 'plan_estudio_id']
|
||||||
@@ -241,6 +301,13 @@ export type Database = {
|
|||||||
referencedRelation: 'asignaturas'
|
referencedRelation: 'asignaturas'
|
||||||
referencedColumns: ['id']
|
referencedColumns: ['id']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'bibliografia_asignatura_asignatura_id_fkey'
|
||||||
|
columns: ['asignatura_id']
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: 'plantilla_asignatura'
|
||||||
|
referencedColumns: ['asignatura_id']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
foreignKeyName: 'bibliografia_asignatura_creado_por_fkey'
|
foreignKeyName: 'bibliografia_asignatura_creado_por_fkey'
|
||||||
columns: ['creado_por']
|
columns: ['creado_por']
|
||||||
@@ -295,6 +362,13 @@ export type Database = {
|
|||||||
referencedRelation: 'asignaturas'
|
referencedRelation: 'asignaturas'
|
||||||
referencedColumns: ['id']
|
referencedColumns: ['id']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'cambios_asignatura_asignatura_id_fkey'
|
||||||
|
columns: ['asignatura_id']
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: 'plantilla_asignatura'
|
||||||
|
referencedColumns: ['asignatura_id']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
foreignKeyName: 'cambios_asignatura_cambiado_por_fkey'
|
foreignKeyName: 'cambios_asignatura_cambiado_por_fkey'
|
||||||
columns: ['cambiado_por']
|
columns: ['cambiado_por']
|
||||||
@@ -441,6 +515,13 @@ export type Database = {
|
|||||||
referencedRelation: 'asignaturas'
|
referencedRelation: 'asignaturas'
|
||||||
referencedColumns: ['id']
|
referencedColumns: ['id']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'conversaciones_asignatura_asignatura_id_fkey'
|
||||||
|
columns: ['asignatura_id']
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: 'plantilla_asignatura'
|
||||||
|
referencedColumns: ['asignatura_id']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
foreignKeyName: 'conversaciones_asignatura_creado_por_fkey'
|
foreignKeyName: 'conversaciones_asignatura_creado_por_fkey'
|
||||||
columns: ['creado_por']
|
columns: ['creado_por']
|
||||||
@@ -552,7 +633,8 @@ export type Database = {
|
|||||||
definicion: Json
|
definicion: Json
|
||||||
id: string
|
id: string
|
||||||
nombre: string
|
nombre: string
|
||||||
version: string | null
|
template_id: string | null
|
||||||
|
tipo: Database['public']['Enums']['tipo_estructura_plan'] | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
actualizado_en?: string
|
actualizado_en?: string
|
||||||
@@ -560,7 +642,8 @@ export type Database = {
|
|||||||
definicion?: Json
|
definicion?: Json
|
||||||
id?: string
|
id?: string
|
||||||
nombre: string
|
nombre: string
|
||||||
version?: string | null
|
template_id?: string | null
|
||||||
|
tipo?: Database['public']['Enums']['tipo_estructura_plan'] | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
actualizado_en?: string
|
actualizado_en?: string
|
||||||
@@ -568,7 +651,8 @@ export type Database = {
|
|||||||
definicion?: Json
|
definicion?: Json
|
||||||
id?: string
|
id?: string
|
||||||
nombre?: string
|
nombre?: string
|
||||||
version?: string | null
|
template_id?: string | null
|
||||||
|
tipo?: Database['public']['Enums']['tipo_estructura_plan'] | null
|
||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
}
|
}
|
||||||
@@ -692,6 +776,13 @@ export type Database = {
|
|||||||
referencedRelation: 'asignaturas'
|
referencedRelation: 'asignaturas'
|
||||||
referencedColumns: ['id']
|
referencedColumns: ['id']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'interacciones_ia_asignatura_id_fkey'
|
||||||
|
columns: ['asignatura_id']
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: 'plantilla_asignatura'
|
||||||
|
referencedColumns: ['asignatura_id']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
foreignKeyName: 'interacciones_ia_plan_estudio_id_fkey'
|
foreignKeyName: 'interacciones_ia_plan_estudio_id_fkey'
|
||||||
columns: ['plan_estudio_id']
|
columns: ['plan_estudio_id']
|
||||||
@@ -798,6 +889,56 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
plan_mensajes_ia: {
|
||||||
|
Row: {
|
||||||
|
campos: Array<string>
|
||||||
|
conversacion_plan_id: string
|
||||||
|
enviado_por: string
|
||||||
|
estado: Database['public']['Enums']['estado_mensaje_ia']
|
||||||
|
fecha_actualizacion: string
|
||||||
|
fecha_creacion: string
|
||||||
|
id: string
|
||||||
|
is_refusal: boolean
|
||||||
|
mensaje: string
|
||||||
|
propuesta: Json | null
|
||||||
|
respuesta: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
campos?: Array<string>
|
||||||
|
conversacion_plan_id: string
|
||||||
|
enviado_por?: string
|
||||||
|
estado?: Database['public']['Enums']['estado_mensaje_ia']
|
||||||
|
fecha_actualizacion?: string
|
||||||
|
fecha_creacion?: string
|
||||||
|
id?: string
|
||||||
|
is_refusal?: boolean
|
||||||
|
mensaje: string
|
||||||
|
propuesta?: Json | null
|
||||||
|
respuesta?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
campos?: Array<string>
|
||||||
|
conversacion_plan_id?: string
|
||||||
|
enviado_por?: string
|
||||||
|
estado?: Database['public']['Enums']['estado_mensaje_ia']
|
||||||
|
fecha_actualizacion?: string
|
||||||
|
fecha_creacion?: string
|
||||||
|
id?: string
|
||||||
|
is_refusal?: boolean
|
||||||
|
mensaje?: string
|
||||||
|
propuesta?: Json | null
|
||||||
|
respuesta?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: 'plan_mensajes_ia_conversacion_plan_id_fkey'
|
||||||
|
columns: ['conversacion_plan_id']
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: 'conversaciones_plan'
|
||||||
|
referencedColumns: ['id']
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
planes_estudio: {
|
planes_estudio: {
|
||||||
Row: {
|
Row: {
|
||||||
activo: boolean
|
activo: boolean
|
||||||
@@ -934,6 +1075,13 @@ export type Database = {
|
|||||||
referencedRelation: 'asignaturas'
|
referencedRelation: 'asignaturas'
|
||||||
referencedColumns: ['id']
|
referencedColumns: ['id']
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: 'responsables_asignatura_asignatura_id_fkey'
|
||||||
|
columns: ['asignatura_id']
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: 'plantilla_asignatura'
|
||||||
|
referencedColumns: ['asignatura_id']
|
||||||
|
},
|
||||||
{
|
{
|
||||||
foreignKeyName: 'responsables_asignatura_usuario_id_fkey'
|
foreignKeyName: 'responsables_asignatura_usuario_id_fkey'
|
||||||
columns: ['usuario_id']
|
columns: ['usuario_id']
|
||||||
@@ -1199,6 +1347,14 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Views: {
|
Views: {
|
||||||
|
plantilla_asignatura: {
|
||||||
|
Row: {
|
||||||
|
asignatura_id: string | null
|
||||||
|
estructura_id: string | null
|
||||||
|
template_id: string | null
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
plantilla_plan: {
|
plantilla_plan: {
|
||||||
Row: {
|
Row: {
|
||||||
estructura_id: string | null
|
estructura_id: string | null
|
||||||
@@ -1221,13 +1377,9 @@ 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_mensaje_ia: 'PROCESANDO' | 'COMPLETADO' | 'ERROR'
|
||||||
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:
|
||||||
@@ -1400,14 +1552,9 @@ 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_mensaje_ia: ['PROCESANDO', 'COMPLETADO', 'ERROR'],
|
||||||
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: [
|
||||||
|
|||||||
Reference in New Issue
Block a user