Merge pull request 'Que se puedan aplicar todos los campos y todas las mejoras fix #165' (#226) from issue/165-que-se-puedan-aplicar-todos-los-campos-y-todas-las into main
Deploy to Azure Static Web Apps / build-and-deploy (push) Successful in 1m21s

Reviewed-on: #226
This commit was merged in pull request #226.
This commit is contained in:
2026-03-25 21:14:55 +00:00
3 changed files with 354 additions and 33 deletions
@@ -14,7 +14,8 @@ import {
MessageSquarePlus,
Archive,
History,
Edit2, // Agregado
Edit2,
Loader2, // Agregado
} from 'lucide-react'
import { useState, useEffect, useRef, useMemo } from 'react'
@@ -42,8 +43,10 @@ import {
useConversationBySubject,
useMessagesBySubjectChat,
useSubject,
useUpdateAsignatura,
useUpdateSubjectConversationName,
useUpdateSubjectConversationStatus,
useUpdateSubjectRecommendation,
} from '@/data'
import { cn } from '@/lib/utils'
@@ -71,6 +74,9 @@ export function IAAsignaturaTab() {
const scrollRef = useRef<HTMLDivElement>(null)
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
const updateAsignatura = useUpdateAsignatura()
const updateRecommendation = useUpdateSubjectRecommendation()
// --- DATA QUERIES ---
const { data: datosGenerales } = useSubject(asignaturaId)
const { data: todasConversaciones, isLoading: loadingConv } =
@@ -94,6 +100,81 @@ export function IAAsignaturaTab() {
Array<string>
>([])
const [uploadedFiles, setUploadedFiles] = useState<Array<File>>([])
const [selectedImprovements, setSelectedImprovements] = useState<
Array<string>
>([])
const handleApplyMultiple = async (sugerencias: Array<any>) => {
if (!asignaturaId || !datosGenerales || sugerencias.length === 0) return
setIsSending(true)
try {
// 1. Consolidar el Patch
const patchData: any = {
datos: { ...datosGenerales.datos },
}
for (const sug of sugerencias) {
if (sug.campoKey === 'contenido_tematico') {
patchData.contenido_tematico = sug.valorSugerido
} else if (sug.campoKey === 'criterios_de_evaluacion') {
patchData.criterios_de_evaluacion = sug.valorSugerido
} else {
patchData.datos[sug.campoKey] = sug.valorSugerido
}
}
// 2. Actualización única de la asignatura
await updateAsignatura.mutateAsync({
asignaturaId: asignaturaId as any,
patch: patchData,
})
// 3. Marcar recomendaciones una por una (Secuencialmente para evitar colisiones)
for (const sug of sugerencias) {
await updateRecommendation.mutateAsync({
mensajeId: sug.messageId,
campoAfectado: sug.campoKey,
})
}
// 4. Limpieza de estados
const appliedKeys = sugerencias.map((s) => s.campoKey)
setSelectedFields((prev) =>
prev.filter((f) => !appliedKeys.includes(f.key)),
)
setSelectedImprovements([])
// Forzar actualización visual
queryClient.invalidateQueries({ queryKey: ['subject', asignaturaId] })
} catch (error) {
console.error('Error en aplicación masiva:', error)
} finally {
setIsSending(false)
}
}
const toggleImprovementSelection = (sugId: string) => {
setSelectedImprovements((prev) =>
prev.includes(sugId)
? prev.filter((id) => id !== sugId)
: [...prev, sugId],
)
}
const toggleAllFromMessage = (sugerencias: Array<any>) => {
const allIds = sugerencias.map((s) => s.id)
const allSelected = allIds.every((id) => selectedImprovements.includes(id))
if (allSelected) {
setSelectedImprovements((prev) =>
prev.filter((id) => !allIds.includes(id)),
)
} else {
setSelectedImprovements((prev) =>
Array.from(new Set([...prev, ...allIds])),
)
}
}
// Cálculo del total para el Badge del botón
const totalReferencias =
@@ -678,31 +759,90 @@ export function IAAsignaturaTab() {
{/* CONTENEDOR DE SUGERENCIAS INTEGRADO */}
{msg.role === 'assistant' &&
msg.sugerencias &&
msg.sugerencias.length > 0 && (
msg.sugerencias?.length > 0 && (
<div className="space-y-3 border-t bg-slate-50/50 p-3">
<p className="mb-1 text-[10px] font-bold text-slate-400 uppercase">
Mejoras disponibles:
</p>
{msg.sugerencias.map((sug: any) => (
<ImprovementCard
key={sug.id}
sug={sug}
asignaturaId={asignaturaId}
onApplied={(campoFinalizado) => {
// Filtramos el array para conservar todos MENOS el que se aplicó
console.log(campoFinalizado)
console.log('campos:', selectedFields)
<div className="flex items-center justify-between">
<p className="text-[10px] font-bold text-slate-400 uppercase">
Mejoras disponibles
</p>
setSelectedFields((prev) =>
prev.filter((fieldObj) => {
// Accedemos a .key porque fieldObj es { key: "...", label: "..." }
return fieldObj.key !== campoFinalizado
}),
{/* Solo mostramos "Seleccionar todo" si hay sugerencias pendientes en ESTE mensaje */}
{msg.sugerencias.some((s) => !s.aceptada) && (
<Button
variant="ghost"
className="h-6 text-[10px] text-teal-600 hover:bg-teal-100"
onClick={() =>
toggleAllFromMessage(msg.sugerencias)
}
>
{msg.sugerencias
.filter((s) => !s.aceptada)
.every((s) =>
selectedImprovements.includes(s.id),
)
? 'Desmarcar todas'
: 'Seleccionar todas'}
</Button>
)}
</div>
{msg.sugerencias.map((sug: any) => (
<div key={sug.id} className="flex gap-2">
{!sug.aceptada && (
<input
type="checkbox"
className="mt-4 h-4 w-4 rounded border-slate-300 accent-teal-600"
checked={selectedImprovements.includes(
sug.id,
)}
onChange={() =>
toggleImprovementSelection(sug.id)
}
/>
)}
<div className="flex-1">
<ImprovementCard
sug={sug}
asignaturaId={asignaturaId}
onApplied={(key) =>
setSelectedFields((prev) =>
prev.filter((f) => f.key !== key),
)
}
/>
</div>
</div>
))}
{/* EL BOTÓN DE APLICAR: Lo movemos para que solo aparezca si hay algo seleccionado de ESTE mensaje */}
{msg.sugerencias.some((s) =>
selectedImprovements.includes(s.id),
) && (
<Button
size="sm"
disabled={isSending}
className="w-full bg-teal-600 text-xs font-bold shadow-md hover:bg-teal-700"
onClick={() => {
const seleccionadasDeEsteMensaje =
msg.sugerencias.filter((s) =>
selectedImprovements.includes(s.id),
)
handleApplyMultiple(
seleccionadasDeEsteMensaje,
)
}}
/>
))}
>
{isSending ? (
<Loader2
className="mr-2 animate-spin"
size={14}
/>
) : (
<Check className="mr-2" size={14} />
)}
Aplicar mejoras seleccionadas
</Button>
)}
</div>
)}
</div>
@@ -15,6 +15,7 @@ interface ImprovementCardProps {
sug: IASugerencia
asignaturaId: string
onApplied: (campoKey: string) => void
isSelected?: boolean
}
export function ImprovementCard({
+191 -11
View File
@@ -40,6 +40,8 @@ import {
useMessagesByChat,
useUpdateConversationStatus,
useUpdateConversationTitle,
useUpdatePlanFields,
useUpdateRecommendationApplied,
} from '@/data'
import { usePlan } from '@/data/hooks/usePlans'
@@ -143,6 +145,12 @@ function RouteComponent() {
const [isHistoryOpen, setIsHistoryOpen] = useState(false)
const [isActionsOpen, setIsActionsOpen] = useState(false)
const [selectedImprovements, setSelectedImprovements] = useState<
Array<string>
>([])
const updatePlan = useUpdatePlanFields()
const updateAppliedStatus = useUpdateRecommendationApplied()
const availableFields = useMemo(() => {
const definicion = data?.estructuras_plan
?.definicion as EstructuraDefinicion
@@ -210,6 +218,96 @@ function RouteComponent() {
return messages
})
}, [mensajesDelChat, activeChatId, availableFields])
// En tu componente principal (el padre)
const handleApplyMultiple = async (
sugerencias: Array<any>,
dbMessageId: string,
) => {
if (!planId || !data?.datos || sugerencias.length === 0) return
setIsSending(true)
try {
const datosActualizados = { ...data.datos }
for (const sug of sugerencias) {
const key = sug.key
const newValue = sug.newValue
const currentValue = datosActualizados[key]
if (
typeof currentValue === 'object' &&
currentValue !== null &&
'description' in currentValue
) {
datosActualizados[key] = { ...currentValue, description: newValue }
} else {
datosActualizados[key] = newValue
}
}
await updatePlan.mutateAsync({
planId: planId as any,
patch: { datos: datosActualizados },
})
for (const sug of sugerencias) {
try {
await updateAppliedStatus.mutateAsync({
conversacionId: dbMessageId,
campoAfectado: sug.key,
})
console.log(sug.key)
removeSelectedField(sug.key)
} catch (err) {
console.error(
`Error al marcar aplicada la sugerencia: ${sug.key}`,
err,
)
// Si una falla, el bucle sigue con las demás
}
}
setSelectedImprovements([])
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['plan', planId] }),
queryClient.invalidateQueries({ queryKey: ['conversation-messages'] }),
])
} catch (error) {
console.error('Error crítico en aplicación masiva:', error)
} finally {
setIsSending(false)
}
}
const toggleImprovementSelection = (sugKey: string) => {
setSelectedImprovements((prev) =>
prev.includes(sugKey)
? prev.filter((key) => key !== sugKey)
: [...prev, sugKey],
)
}
const toggleAllFromMessage = (suggestions: Array<any>) => {
const pending = suggestions.filter((s) => !s.applied)
const allKeys = pending.map((s) => s.key)
const allSelected = allKeys.every((key) =>
selectedImprovements.includes(key),
)
if (allSelected) {
setSelectedImprovements((prev) =>
prev.filter((key) => !allKeys.includes(key)),
)
} else {
setSelectedImprovements((prev) =>
Array.from(new Set([...prev, ...allKeys])),
)
}
}
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
if (scrollRef.current) {
const scrollContainer = scrollRef.current.querySelector(
@@ -478,6 +576,7 @@ function RouteComponent() {
}, [selectedArchivoIds, selectedRepositorioIds, uploadedFiles])
const removeSelectedField = (fieldKey: string) => {
console.log(fieldKey)
setSelectedFields((prev) => prev.filter((f) => f.key !== fieldKey))
}
@@ -752,17 +851,98 @@ function RouteComponent() {
{/* Recomendaciones */}
{isAI && msg.suggestions?.length > 0 && (
<div className="mt-4">
<ImprovementCard
suggestions={msg.suggestions} // Usamos el nombre normalizado en el flatMap
dbMessageId={msg.dbMessageId}
planId={planId}
currentDatos={data?.datos}
activeChatId={activeChatId}
onApplySuccess={(key) =>
removeSelectedField(key)
}
/>
<div className="mt-4 w-full space-y-3 rounded-xl border border-slate-100 bg-slate-50/50 p-3">
<div className="flex items-center justify-between px-1">
<span className="text-[10px] font-bold text-slate-400 uppercase">
Sugerencias de mejora
</span>
{/* Botón Seleccionar Todo */}
{msg.suggestions.some(
(s: any) => !s.applied,
) && (
<Button
variant="ghost"
className="h-6 px-2 text-[10px] text-teal-600 hover:bg-teal-100"
onClick={() =>
toggleAllFromMessage(msg.suggestions)
}
>
{msg.suggestions
.filter((s: any) => !s.applied)
.every((s: any) =>
selectedImprovements.includes(s.key),
)
? 'Desmarcar todo'
: 'Seleccionar todo'}
</Button>
)}
</div>
{msg.suggestions.map((sug: any) => (
<div key={sug.key} className="flex gap-2">
{!sug.applied && (
<input
type="checkbox"
className="mt-4 h-4 w-4 shrink-0 rounded border-slate-300 accent-teal-600"
checked={selectedImprovements.includes(
sug.key,
)}
onChange={() =>
toggleImprovementSelection(sug.key)
}
/>
)}
<div className="flex-1">
<ImprovementCard
suggestions={[sug]} // Pasamos solo una para que la card actúe individualmente
dbMessageId={msg.dbMessageId}
planId={planId}
currentDatos={data?.datos}
activeChatId={activeChatId}
onApplySuccess={(key) =>
removeSelectedField(key)
}
/>
</div>
</div>
))}
{/* Botón de Acción Masiva específico para este mensaje */}
{msg.suggestions.some((s: any) =>
selectedImprovements.includes(s.key),
) && (
<Button
size="sm"
disabled={isSending}
className="w-full bg-teal-600 py-1 text-xs font-bold text-white hover:bg-teal-700"
onClick={() => {
const seleccionadas =
msg.suggestions.filter((s: any) =>
selectedImprovements.includes(s.key),
)
handleApplyMultiple(
seleccionadas,
msg.dbMessageId,
)
}}
>
{isSending ? (
<Loader2
className="mr-2 animate-spin"
size={12}
/>
) : (
<Check className="mr-2" size={12} />
)}
Aplicar seleccionadas (
{
msg.suggestions.filter((s: any) =>
selectedImprovements.includes(s.key),
).length
}
)
</Button>
)}
</div>
)}
</div>