@@ -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:
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase">
|
||||
Mejoras disponibles
|
||||
</p>
|
||||
|
||||
{/* 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
|
||||
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)
|
||||
|
||||
onApplied={(key) =>
|
||||
setSelectedFields((prev) =>
|
||||
prev.filter((fieldObj) => {
|
||||
// Accedemos a .key porque fieldObj es { key: "...", label: "..." }
|
||||
return fieldObj.key !== campoFinalizado
|
||||
}),
|
||||
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({
|
||||
|
||||
@@ -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,9 +851,50 @@ function RouteComponent() {
|
||||
|
||||
{/* Recomendaciones */}
|
||||
{isAI && msg.suggestions?.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<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={msg.suggestions} // Usamos el nombre normalizado en el flatMap
|
||||
suggestions={[sug]} // Pasamos solo una para que la card actúe individualmente
|
||||
dbMessageId={msg.dbMessageId}
|
||||
planId={planId}
|
||||
currentDatos={data?.datos}
|
||||
@@ -764,6 +904,46 @@ function RouteComponent() {
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user