Merge pull request 'Que pueda generar el contenido temático Y el sistema de evaluación fix #174 fix #163 fix # 173' (#176) from issue/174-que-pueda-generar-el-contenido-temtico-y-el-sistem into main
Reviewed-on: #176
This commit was merged in pull request #176.
This commit is contained in:
@@ -131,15 +131,45 @@ export function IAAsignaturaTab({
|
|||||||
}, [todasConversaciones])
|
}, [todasConversaciones])
|
||||||
|
|
||||||
const availableFields = useMemo(() => {
|
const availableFields = useMemo(() => {
|
||||||
if (!datosGenerales?.datos) return []
|
// 1. Obtenemos los campos dinámicos de la DB
|
||||||
|
const dynamicFields = datosGenerales?.datos
|
||||||
|
? Object.keys(datosGenerales.datos).map((key) => {
|
||||||
const estructuraProps =
|
const estructuraProps =
|
||||||
datosGenerales?.estructuras_asignatura?.definicion?.properties || {}
|
datosGenerales?.estructuras_asignatura?.definicion?.properties || {}
|
||||||
return Object.keys(datosGenerales.datos).map((key) => ({
|
return {
|
||||||
key,
|
key,
|
||||||
label:
|
label:
|
||||||
estructuraProps[key]?.title || key.replace(/_/g, ' ').toUpperCase(),
|
estructuraProps[key]?.title ||
|
||||||
|
key.replace(/_/g, ' ').toUpperCase(),
|
||||||
value: String(datosGenerales.datos[key] || ''),
|
value: String(datosGenerales.datos[key] || ''),
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
|
||||||
|
// 2. Definimos tus campos manuales (hardcoded)
|
||||||
|
const hardcodedFields = [
|
||||||
|
{
|
||||||
|
key: 'contenido_tematico',
|
||||||
|
label: 'Contenido temático',
|
||||||
|
value: '', // Puedes dejarlo vacío o buscarlo en datosGenerales si existiera
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'criterios_de_evaluacion',
|
||||||
|
label: 'Criterios de evaluación',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 3. Unimos ambos, filtrando duplicados por si acaso el backend ya los envía
|
||||||
|
const combined = [...dynamicFields]
|
||||||
|
|
||||||
|
hardcodedFields.forEach((hf) => {
|
||||||
|
if (!combined.some((f) => f.key === hf.key)) {
|
||||||
|
combined.push(hf)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return combined
|
||||||
}, [datosGenerales])
|
}, [datosGenerales])
|
||||||
|
|
||||||
// --- PROCESAMIENTO DE MENSAJES ---
|
// --- PROCESAMIENTO DE MENSAJES ---
|
||||||
@@ -269,7 +299,7 @@ export function IAAsignaturaTab({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setInput('')
|
setInput('')
|
||||||
setSelectedFields([])
|
// setSelectedFields([])
|
||||||
|
|
||||||
// Invalidamos la lista de conversaciones para que el nuevo chat aparezca en el historial (panel izquierdo)
|
// Invalidamos la lista de conversaciones para que el nuevo chat aparezca en el historial (panel izquierdo)
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
@@ -511,6 +541,7 @@ export function IAAsignaturaTab({
|
|||||||
>
|
>
|
||||||
{/* Texto del mensaje principal */}
|
{/* Texto del mensaje principal */}
|
||||||
<div
|
<div
|
||||||
|
style={{ whiteSpace: 'pre-line' }}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-sm leading-relaxed',
|
'text-sm leading-relaxed',
|
||||||
msg.role === 'assistant' && 'p-4',
|
msg.role === 'assistant' && 'p-4',
|
||||||
@@ -532,6 +563,18 @@ export function IAAsignaturaTab({
|
|||||||
key={sug.id}
|
key={sug.id}
|
||||||
sug={sug}
|
sug={sug}
|
||||||
asignaturaId={asignaturaId}
|
asignaturaId={asignaturaId}
|
||||||
|
onApplied={(campoFinalizado) => {
|
||||||
|
// Filtramos el array para conservar todos MENOS el que se aplicó
|
||||||
|
console.log(campoFinalizado)
|
||||||
|
console.log('campos:', selectedFields)
|
||||||
|
|
||||||
|
setSelectedFields((prev) =>
|
||||||
|
prev.filter((fieldObj) => {
|
||||||
|
// Accedemos a .key porque fieldObj es { key: "...", label: "..." }
|
||||||
|
return fieldObj.key !== campoFinalizado
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Check, Loader2 } from 'lucide-react'
|
import { Check, Loader2, BookOpen, Clock, ListChecks } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import type { IASugerencia } from '@/types/asignatura'
|
import type { IASugerencia } from '@/types/asignatura'
|
||||||
@@ -7,50 +7,65 @@ import { Button } from '@/components/ui/button'
|
|||||||
import {
|
import {
|
||||||
useUpdateAsignatura,
|
useUpdateAsignatura,
|
||||||
useSubject,
|
useSubject,
|
||||||
useUpdateSubjectRecommendation, // Importamos tu nuevo hook
|
useUpdateSubjectRecommendation,
|
||||||
} from '@/data'
|
} from '@/data'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface ImprovementCardProps {
|
interface ImprovementCardProps {
|
||||||
sug: IASugerencia
|
sug: IASugerencia
|
||||||
asignaturaId: string
|
asignaturaId: string
|
||||||
|
onApplied: (campoKey: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImprovementCard({ sug, asignaturaId }: ImprovementCardProps) {
|
export function ImprovementCard({
|
||||||
|
sug,
|
||||||
|
asignaturaId,
|
||||||
|
onApplied,
|
||||||
|
}: ImprovementCardProps) {
|
||||||
const { data: asignatura } = useSubject(asignaturaId)
|
const { data: asignatura } = useSubject(asignaturaId)
|
||||||
const updateAsignatura = useUpdateAsignatura()
|
const updateAsignatura = useUpdateAsignatura()
|
||||||
|
|
||||||
// Hook para marcar en la base de datos que la sugerencia fue aceptada
|
|
||||||
const updateRecommendation = useUpdateSubjectRecommendation()
|
const updateRecommendation = useUpdateSubjectRecommendation()
|
||||||
|
|
||||||
const [isApplying, setIsApplying] = useState(false)
|
const [isApplying, setIsApplying] = useState(false)
|
||||||
|
|
||||||
const handleApply = async () => {
|
const handleApply = async () => {
|
||||||
if (!asignatura?.datos) return
|
if (!asignatura) return
|
||||||
|
|
||||||
setIsApplying(true)
|
setIsApplying(true)
|
||||||
try {
|
try {
|
||||||
// 1. Actualizar el contenido real de la asignatura (JSON datos)
|
// 1. Identificar a qué columna debe ir el guardado
|
||||||
const nuevosDatos = {
|
let patchData = {}
|
||||||
|
|
||||||
|
if (sug.campoKey === 'contenido_tematico') {
|
||||||
|
// Se guarda directamente en la columna contenido_tematico
|
||||||
|
patchData = { contenido_tematico: sug.valorSugerido }
|
||||||
|
} else if (sug.campoKey === 'criterios_de_evaluacion') {
|
||||||
|
// Se guarda directamente en la columna criterios_de_evaluacion
|
||||||
|
patchData = { criterios_de_evaluacion: sug.valorSugerido }
|
||||||
|
} else {
|
||||||
|
// Otros campos (ciclo, fines, etc.) se siguen guardando en el JSON de la columna 'datos'
|
||||||
|
patchData = {
|
||||||
|
datos: {
|
||||||
...asignatura.datos,
|
...asignatura.datos,
|
||||||
[sug.campoKey]: sug.valorSugerido,
|
[sug.campoKey]: sug.valorSugerido,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Ejecutar la actualización con la estructura correcta
|
||||||
await updateAsignatura.mutateAsync({
|
await updateAsignatura.mutateAsync({
|
||||||
asignaturaId: asignaturaId as any,
|
asignaturaId: asignaturaId as any,
|
||||||
patch: {
|
patch: patchData as any,
|
||||||
datos: nuevosDatos,
|
|
||||||
} as any,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2. Marcar la sugerencia como "aplicada: true" en la tabla de mensajes
|
// 3. Marcar la recomendación como aplicada
|
||||||
// Usamos los datos que vienen en el objeto 'sug'
|
|
||||||
await updateRecommendation.mutateAsync({
|
await updateRecommendation.mutateAsync({
|
||||||
mensajeId: sug.messageId,
|
mensajeId: sug.messageId,
|
||||||
campoAfectado: sug.campoKey,
|
campoAfectado: sug.campoKey,
|
||||||
})
|
})
|
||||||
|
console.log(sug.campoKey)
|
||||||
|
|
||||||
// Al terminar, React Query invalidará 'subject-messages'
|
onApplied(sug.campoKey)
|
||||||
// y la card pasará automáticamente al estado "Aplicado" (gris)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error al aplicar mejora:', error)
|
console.error('Error al aplicar mejora:', error)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -58,10 +73,89 @@ export function ImprovementCard({ sug, asignaturaId }: ImprovementCardProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- FUNCIÓN PARA RENDERIZAR EL CONTENIDO DE FORMA SEGURA ---
|
||||||
|
const renderContenido = (valor: any) => {
|
||||||
|
// Si no es un array, es texto simple
|
||||||
|
if (!Array.isArray(valor)) {
|
||||||
|
return <p className="italic">"{String(valor)}"</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CASO 1: CONTENIDO TEMÁTICO (Detectamos si el primer objeto tiene 'unidad') ---
|
||||||
|
if (valor[0]?.hasOwnProperty('unidad')) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{valor.map((u: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="rounded-md border border-teal-100 bg-white p-2 shadow-sm"
|
||||||
|
>
|
||||||
|
<div className="mb-1 flex items-center gap-2 border-b border-slate-50 pb-1 text-[11px] font-bold text-teal-800">
|
||||||
|
<BookOpen size={12} /> Unidad {u.unidad}: {u.titulo}
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{u.temas?.map((t: any, tidx: number) => (
|
||||||
|
<li
|
||||||
|
key={tidx}
|
||||||
|
className="flex items-start justify-between gap-2 text-[10px] text-slate-600"
|
||||||
|
>
|
||||||
|
<span className="leading-tight">• {t.nombre}</span>
|
||||||
|
<span className="flex shrink-0 items-center gap-0.5 font-mono text-slate-400">
|
||||||
|
<Clock size={10} /> {t.horasEstimadas}h
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- CASO 2: CRITERIOS DE EVALUACIÓN (Detectamos si tiene 'criterio') ---
|
||||||
|
if (valor[0]?.hasOwnProperty('criterio')) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="mb-1 flex items-center gap-2 text-[10px] font-bold text-slate-400 uppercase">
|
||||||
|
<ListChecks size={12} /> Desglose de evaluación
|
||||||
|
</div>
|
||||||
|
{valor.map((c: any, idx: number) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex items-center justify-between gap-3 rounded-md border border-slate-100 bg-white p-2 shadow-sm"
|
||||||
|
>
|
||||||
|
<span className="text-[11px] leading-tight text-slate-700">
|
||||||
|
{c.criterio}
|
||||||
|
</span>
|
||||||
|
<div className="flex shrink-0 items-center gap-1 rounded-full border border-orange-100 bg-orange-50 px-2 py-0.5 text-[10px] font-bold text-orange-600">
|
||||||
|
{c.porcentaje}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{/* Opcional: Suma total para verificar que de 100% */}
|
||||||
|
<div className="pt-1 text-right text-[9px] font-medium text-slate-400">
|
||||||
|
Total:{' '}
|
||||||
|
{valor.reduce(
|
||||||
|
(acc: number, curr: any) => acc + (curr.porcentaje || 0),
|
||||||
|
0,
|
||||||
|
)}
|
||||||
|
%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caso por defecto (Array genérico)
|
||||||
|
return (
|
||||||
|
<pre className="text-[10px]">
|
||||||
|
{/* JSON.stringify(valor, null, 2)*/ 'hola'}
|
||||||
|
</pre>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// --- ESTADO APLICADO ---
|
// --- ESTADO APLICADO ---
|
||||||
if (sug.aceptada) {
|
if (sug.aceptada) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col rounded-xl border border-slate-100 bg-white p-3 shadow-sm">
|
<div className="flex flex-col rounded-xl border border-slate-100 bg-white p-3 opacity-80 shadow-sm">
|
||||||
<div className="mb-3 flex items-center justify-between gap-4">
|
<div className="mb-3 flex items-center justify-between gap-4">
|
||||||
<span className="text-sm font-bold text-slate-800">
|
<span className="text-sm font-bold text-slate-800">
|
||||||
{sug.campoNombre}
|
{sug.campoNombre}
|
||||||
@@ -72,7 +166,7 @@ export function ImprovementCard({ sug, asignaturaId }: ImprovementCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-teal-100 bg-teal-50/30 p-3 text-xs leading-relaxed text-slate-500">
|
<div className="rounded-lg border border-teal-100 bg-teal-50/30 p-3 text-xs leading-relaxed text-slate-500">
|
||||||
"{sug.valorSugerido}"
|
{renderContenido(sug.valorSugerido)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -101,8 +195,13 @@ export function ImprovementCard({ sug, asignaturaId }: ImprovementCardProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="line-clamp-4 rounded-lg border border-dashed border-slate-200 bg-slate-50/50 p-3 text-xs leading-relaxed text-slate-600 italic">
|
<div
|
||||||
"{sug.valorSugerido}"
|
className={cn(
|
||||||
|
'rounded-lg border border-dashed border-slate-200 bg-slate-50/50 p-3 text-xs leading-relaxed text-slate-600',
|
||||||
|
!Array.isArray(sug.valorSugerido) && 'line-clamp-4 italic',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{renderContenido(sug.valorSugerido)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ function RouteComponent() {
|
|||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}, [chatMessages, isLoading])
|
}, [chatMessages, isLoading])
|
||||||
|
|
||||||
useEffect(() => {
|
/* useEffect(() => {
|
||||||
// Verificamos cuáles campos de la lista "selectedFields" ya no están presentes en el texto del input
|
// Verificamos cuáles campos de la lista "selectedFields" ya no están presentes en el texto del input
|
||||||
const camposActualizados = selectedFields.filter((field) =>
|
const camposActualizados = selectedFields.filter((field) =>
|
||||||
input.includes(field.label),
|
input.includes(field.label),
|
||||||
@@ -237,7 +237,7 @@ function RouteComponent() {
|
|||||||
if (camposActualizados.length !== selectedFields.length) {
|
if (camposActualizados.length !== selectedFields.length) {
|
||||||
setSelectedFields(camposActualizados)
|
setSelectedFields(camposActualizados)
|
||||||
}
|
}
|
||||||
}, [input, selectedFields])
|
}, [input, selectedFields]) */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoadingConv || isSending) return
|
if (isLoadingConv || isSending) return
|
||||||
@@ -297,7 +297,7 @@ function RouteComponent() {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
setInput('')
|
setInput('')
|
||||||
setSelectedFields([])
|
// setSelectedFields([])
|
||||||
}
|
}
|
||||||
|
|
||||||
const archiveChat = (e: React.MouseEvent, id: string) => {
|
const archiveChat = (e: React.MouseEvent, id: string) => {
|
||||||
@@ -405,7 +405,7 @@ function RouteComponent() {
|
|||||||
setIsSending(true)
|
setIsSending(true)
|
||||||
setOptimisticMessage(finalContent)
|
setOptimisticMessage(finalContent)
|
||||||
setInput('')
|
setInput('')
|
||||||
setSelectedFields([])
|
// setSelectedFields([])
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
Reference in New Issue
Block a user