import { useQueryClient } from '@tanstack/react-query' import { useParams } from '@tanstack/react-router' import { Sparkles, Send, Target, UserCheck, Lightbulb, FileText, GraduationCap, BookOpen, Check, X, MessageSquarePlus, Archive, History, Edit2, // Agregado } from 'lucide-react' import { useState, useEffect, useRef, useMemo } from 'react' import { ImprovementCard } from './SaveAsignatura/ImprovementCardProps' import type { IASugerencia } from '@/types/asignatura' import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' import { Drawer, DrawerContent } from '@/components/ui/drawer' import { ScrollArea } from '@/components/ui/scroll-area' import { Textarea } from '@/components/ui/textarea' import { useAISubjectChat, useConversationBySubject, useMessagesBySubjectChat, useSubject, useUpdateSubjectConversationName, useUpdateSubjectConversationStatus, } from '@/data' import { cn } from '@/lib/utils' interface SelectedField { key: string label: string value: string } interface IAAsignaturaTabProps { asignatura?: Record onAcceptSuggestion: (sugerencia: IASugerencia) => void onRejectSuggestion: (messageId: string) => void } export function IAAsignaturaTab({ onAcceptSuggestion, onRejectSuggestion, }: IAAsignaturaTabProps) { const queryClient = useQueryClient() const { asignaturaId } = useParams({ from: '/planes/$planId/asignaturas/$asignaturaId', }) // --- ESTADOS --- const [activeChatId, setActiveChatId] = useState( undefined, ) const [showArchived, setShowArchived] = useState(false) const [input, setInput] = useState('') const [selectedFields, setSelectedFields] = useState>([]) const [showSuggestions, setShowSuggestions] = useState(false) const [isSending, setIsSending] = useState(false) const scrollRef = useRef(null) // --- DATA QUERIES --- const { data: datosGenerales } = useSubject(asignaturaId) const { data: todasConversaciones, isLoading: loadingConv } = useConversationBySubject(asignaturaId) const { data: rawMessages } = useMessagesBySubjectChat(activeChatId, { enabled: !!activeChatId, }) const { mutateAsync: sendMessage } = useAISubjectChat() const { mutate: updateStatus } = useUpdateSubjectConversationStatus() const [isCreatingNewChat, setIsCreatingNewChat] = useState(false) const hasInitialSelected = useRef(false) const { mutate: updateName } = useUpdateSubjectConversationName() const [editingId, setEditingId] = useState(null) const [tempName, setTempName] = useState('') const [openIA, setOpenIA] = useState(false) const [selectedArchivoIds, setSelectedArchivoIds] = useState>( [], ) const [selectedRepositorioIds, setSelectedRepositorioIds] = useState< Array >([]) const [uploadedFiles, setUploadedFiles] = useState>([]) // Cálculo del total para el Badge del botón const totalReferencias = selectedArchivoIds.length + selectedRepositorioIds.length + uploadedFiles.length const isAiThinking = useMemo(() => { if (isSending) return true if (!rawMessages || rawMessages.length === 0) return false // Verificamos si el último mensaje está en estado de procesamiento const lastMessage = rawMessages[rawMessages.length - 1] return ( lastMessage.estado === 'PROCESANDO' || lastMessage.estado === 'PENDIENTE' ) }, [isSending, rawMessages]) // --- AUTO-SCROLL --- useEffect(() => { const viewport = scrollRef.current?.querySelector( '[data-radix-scroll-area-viewport]', ) if (viewport) { viewport.scrollTop = viewport.scrollHeight } }, [rawMessages, isSending]) // --- FILTRADO DE CHATS --- const { activeChats, archivedChats } = useMemo(() => { const chats = todasConversaciones || [] return { activeChats: chats.filter((c: any) => c.estado === 'ACTIVA'), archivedChats: chats.filter((c: any) => c.estado === 'ARCHIVADA'), } }, [todasConversaciones]) const availableFields = useMemo(() => { if (!datosGenerales?.datos) return [] const estructuraProps = datosGenerales?.estructuras_asignatura?.definicion?.properties || {} return Object.keys(datosGenerales.datos).map((key) => ({ key, label: estructuraProps[key]?.title || key.replace(/_/g, ' ').toUpperCase(), value: String(datosGenerales.datos[key] || ''), })) }, [datosGenerales]) // --- PROCESAMIENTO DE MENSAJES --- // --- PROCESAMIENTO DE MENSAJES --- const messages = useMemo(() => { const msgs: Array = [] // 1. Mensajes existentes de la DB if (rawMessages) { rawMessages.forEach((m) => { // Mensaje del usuario msgs.push({ id: `${m.id}-user`, role: 'user', content: m.mensaje }) // Respuesta de la IA (si existe) if (m.respuesta) { const sugerencias = m.propuesta?.recommendations?.map((rec: any, index: number) => ({ id: `${m.id}-sug-${index}`, messageId: m.id, campoKey: rec.campo_afectado, campoNombre: rec.campo_afectado.replace(/_/g, ' '), valorSugerido: rec.texto_mejora, aceptada: rec.aplicada, })) || [] msgs.push({ id: `${m.id}-ai`, role: 'assistant', content: m.respuesta, sugerencias: sugerencias, }) } }) } // 2. INYECCIÓN OPTIMISTA: Si estamos enviando, mostramos el texto actual del input como mensaje de usuario if (isSending && input.trim()) { msgs.push({ id: 'optimistic-user-msg', role: 'user', content: input, }) } return msgs }, [rawMessages, isSending, input]) // Auto-selección inicial useEffect(() => { // Si ya hay un chat, o si el usuario ya interactuó (hasInitialSelected), abortamos. if (activeChatId || hasInitialSelected.current) return if (activeChats.length > 0 && !loadingConv) { setActiveChatId(activeChats[0].id) hasInitialSelected.current = true } }, [activeChats, loadingConv]) const filteredFields = useMemo(() => { if (!showSuggestions) return availableFields // Extraemos lo que hay después del último ':' para filtrar const lastColonIndex = input.lastIndexOf(':') const query = input.slice(lastColonIndex + 1).toLowerCase() return availableFields.filter( (f) => f.label.toLowerCase().includes(query) || f.key.toLowerCase().includes(query), ) }, [availableFields, input, showSuggestions]) // 2. Efecto para cerrar con ESC useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') setShowSuggestions(false) } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, []) // 3. Función para insertar el campo y limpiar el prompt const handleSelectField = (field: SelectedField) => { // 1. Agregamos al array de objetos (para tu lógica de API) if (!selectedFields.find((f) => f.key === field.key)) { setSelectedFields((prev) => [...prev, field]) } // 2. Lógica de autocompletado en el texto const lastColonIndex = input.lastIndexOf(':') if (lastColonIndex !== -1) { // Tomamos lo que había antes del ":" + el Nombre del Campo + un espacio const nuevoTexto = input.slice(0, lastColonIndex) + `${field.label} ` setInput(nuevoTexto) } // 3. Cerramos el buscador y devolvemos el foco al textarea setShowSuggestions(false) // Opcional: Si tienes una ref del textarea, puedes hacer: // textareaRef.current?.focus() } const handleSaveName = (id: string) => { if (tempName.trim()) { updateName({ id, nombre: tempName }) } setEditingId(null) } const handleSend = async (promptOverride?: string) => { const text = promptOverride || input if (!text.trim() && selectedFields.length === 0) return setIsSending(true) try { const response = await sendMessage({ subjectId: asignaturaId as any, // Importante: se usa para crear la conv si activeChatId es undefined content: text, campos: selectedFields.map((f) => f.key), conversacionId: activeChatId, // Si es undefined, la mutación crea el chat automáticamente }) // IMPORTANTE: Después de la respuesta, actualizamos el ID activo con el que creó el backend if (response.conversacionId) { setActiveChatId(response.conversacionId) } setInput('') setSelectedFields([]) // Invalidamos la lista de conversaciones para que el nuevo chat aparezca en el historial (panel izquierdo) queryClient.invalidateQueries({ queryKey: ['conversation-by-subject', asignaturaId], }) } catch (error) { console.error('Error al enviar mensaje:', error) } finally { setIsSending(false) } } const toggleField = (field: SelectedField) => { setSelectedFields((prev) => prev.find((f) => f.key === field.key) ? prev.filter((f) => f.key !== field.key) : [...prev, field], ) } const createNewChat = () => { setActiveChatId(undefined) // Al ser undefined, el próximo mensaje creará la charla en el backend setInput('') setSelectedFields([]) // Opcional: podrías forzar el foco al textarea aquí con una ref } const PRESETS = [ { id: 'mejorar-obj', label: 'Mejorar objetivo', icon: Target, prompt: 'Mejora la redacción del objetivo...', }, { id: 'sugerir-cont', label: 'Sugerir contenido', icon: BookOpen, prompt: 'Genera un desglose de temas...', }, { id: 'actividades', label: 'Actividades', icon: GraduationCap, prompt: 'Sugiere actividades prácticas...', }, ] return (
{/* PANEL IZQUIERDO */}

Historial

{/* CORRECCIÓN: Mapear ambos casos */} {(showArchived ? archivedChats : activeChats).map((chat: any) => (
{editingId === chat.id ? (
setTempName(e.target.value)} onBlur={() => handleSaveName(chat.id)} // Guardar al hacer clic fuera onKeyDown={(e) => { if (e.key === 'Enter') handleSaveName(chat.id) if (e.key === 'Escape') setEditingId(null) }} />
) : ( <> setActiveChatId(chat.id)} className="flex-1 cursor-pointer truncate" > {/* CORRECCIÓN: Usar 'nombre' si así se llama en tu DB */} {chat.nombre || chat.titulo || 'Conversación'}
{/* Botón para Archivar/Desarchivar dinámico */}
)}
))}
{/* PANEL CENTRAL */}
Asistente IA
{messages.map((msg) => (
{msg.role === 'assistant' ? ( ) : ( )}
{/* Texto del mensaje principal */}
{msg.content}
{/* CONTENEDOR DE SUGERENCIAS INTEGRADO */} {msg.role === 'assistant' && msg.sugerencias && msg.sugerencias.length > 0 && (

Mejoras disponibles:

{msg.sugerencias.map((sug: any) => ( ))}
)}
))} {isAiThinking && (
La IA está analizando tu solicitud...
)} {/* Espacio extra al final para que el scroll no tape el último mensaje */}
{/* INPUT */}
{showSuggestions && (
Filtrando campos... ESC para cerrar
{filteredFields.length > 0 ? ( filteredFields.map((field) => ( )) ) : (
No se encontraron coincidencias
)}
)}
{selectedFields.length > 0 && (
{selectedFields.map((field) => (
{field.label}
))}
)}