/* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-static-element-interactions */ import { useQueryClient } from '@tanstack/react-query' import { createFileRoute, useRouterState } from '@tanstack/react-router' import { Send, Target, Lightbulb, FileText, GraduationCap, BookOpen, Check, X, MessageSquarePlus, Archive, RotateCcw, } from 'lucide-react' import { useState, useEffect, useRef, useMemo } from 'react' import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone' import { ImprovementCard } from '@/components/planes/detalle/Ia/ImprovementCard' import ReferenciasParaIA from '@/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA' 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 { useAIPlanChat, useConversationByPlan, useUpdateConversationStatus, useUpdateConversationTitle, } from '@/data' import { usePlan } from '@/data/hooks/usePlans' const PRESETS = [ { id: 'objetivo', label: 'Mejorar objetivo general', icon: Target, prompt: 'Mejora la redacción del objetivo general...', }, { id: 'perfil-egreso', label: 'Redactar perfil de egreso', icon: GraduationCap, prompt: 'Genera un perfil de egreso detallado...', }, { id: 'competencias', label: 'Sugerir competencias', icon: BookOpen, prompt: 'Genera una lista de competencias...', }, { id: 'pertinencia', label: 'Justificar pertinencia', icon: FileText, prompt: 'Redacta una justificación de pertinencia...', }, ] // --- Tipado y Helpers --- interface SelectedField { key: string label: string value: string } export const Route = createFileRoute('/planes/$planId/_detalle/iaplan')({ component: RouteComponent, }) function RouteComponent() { const { planId } = Route.useParams() const { data } = usePlan(planId) const routerState = useRouterState() const [openIA, setOpenIA] = useState(false) const [conversacionId, setConversacionId] = useState(null) const { mutateAsync: sendChat, isLoading } = useAIPlanChat() const { mutate: updateStatusMutation } = useUpdateConversationStatus() const [activeChatId, setActiveChatId] = useState( undefined, ) /* const { data: historyMessages, isLoading: isLoadingHistory } = useChatHistory(activeChatId) */ const { data: lastConversation, isLoading: isLoadingConv } = useConversationByPlan(planId) // archivos const [selectedArchivoIds, setSelectedArchivoIds] = useState>( [], ) const [selectedRepositorioIds, setSelectedRepositorioIds] = useState< Array >([]) const [uploadedFiles, setUploadedFiles] = useState>([]) const [messages, setMessages] = useState>([]) const [input, setInput] = useState('') const [selectedFields, setSelectedFields] = useState>([]) const [showSuggestions, setShowSuggestions] = useState(false) const [pendingSuggestion, setPendingSuggestion] = useState(null) const queryClient = useQueryClient() const scrollRef = useRef(null) const [showArchived, setShowArchived] = useState(false) const [editingChatId, setEditingChatId] = useState(null) const editableRef = useRef(null) const { mutate: updateTitleMutation } = useUpdateConversationTitle() const availableFields = useMemo(() => { if (!data?.estructuras_plan?.definicion?.properties) return [] return Object.entries(data.estructuras_plan.definicion.properties).map( ([key, value]) => ({ key, label: value.title, value: String(value.description || ''), }), ) }, [data]) const activeChatData = useMemo(() => { return lastConversation?.find((chat: any) => chat.id === activeChatId) }, [lastConversation, activeChatId]) const conversacionJson = activeChatData?.conversacion_json || [] const chatMessages = useMemo(() => { const json = activeChatData?.conversacion_json || [] return json.map((msg: any, index: number) => { const isAssistant = msg.user === 'assistant' return { id: `${activeChatId}-${index}`, role: isAssistant ? 'assistant' : 'user', content: isAssistant ? msg.message : msg.prompt, // EXTRAEMOS EL CAMPO REFUSAL isRefusal: isAssistant && msg.refusal === true, suggestions: isAssistant && msg.recommendations ? msg.recommendations.map((rec: any) => ({ key: rec.campo_afectado, label: rec.campo_afectado.replace(/_/g, ' '), newValue: rec.texto_mejora, applied: rec.aplicada, })) : [], } }) }, [activeChatData, activeChatId]) useEffect(() => { // Si no hay un chat seleccionado manualmente y la API nos devuelve chats existentes const isCreationMode = messages.length === 1 && messages[0].id === 'welcome' if ( !activeChatId && lastConversation && lastConversation.length > 0 && !isCreationMode ) { setActiveChatId(lastConversation[0].id) } }, [lastConversation, activeChatId]) useEffect(() => { const state = routerState.location.state as any if (!state?.campo_edit || availableFields.length === 0) return const field = availableFields.find( (f) => f.value === state.campo_edit.label || f.key === state.campo_edit.clave, ) if (!field) return setSelectedFields([field]) setInput((prev) => injectFieldsIntoInput(prev || 'Mejora este campo:', [field]), ) }, [availableFields]) const createNewChat = () => { setActiveChatId(undefined) // Al ser undefined, el próximo handleSend creará uno nuevo setMessages([ { id: 'welcome', role: 'assistant', content: 'Iniciando una nueva conversación. ¿En qué puedo ayudarte?', }, ]) setInput('') setSelectedFields([]) } const archiveChat = (e: React.MouseEvent, id: string) => { e.stopPropagation() updateStatusMutation( { id, estado: 'ARCHIVADA' }, { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['conversation-by-plan', planId], }) if (activeChatId === id) { setActiveChatId(undefined) setMessages([]) } }, }, ) } const unarchiveChat = (e: React.MouseEvent, id: string) => { e.stopPropagation() updateStatusMutation( { id, estado: 'ACTIVA' }, { onSuccess: () => { // Al invalidar la query, React Query traerá la lista fresca // y el chat se moverá solo de "archivados" a "activos" queryClient.invalidateQueries({ queryKey: ['conversation-by-plan', planId], }) }, }, ) } const handleInputChange = (e: React.ChangeEvent) => { const val = e.target.value setInput(val) // Solo abrir si termina en ":" setShowSuggestions(val.endsWith(':')) } const injectFieldsIntoInput = ( input: string, fields: Array, ) => { // Quita cualquier bloque previo de campos const cleaned = input.replace(/\n?\[Campos:[^\]]*]/g, '').trim() if (fields.length === 0) return cleaned const fieldLabels = fields.map((f) => f.label).join(', ') return `${cleaned}\n[Campos: ${fieldLabels}]` } const toggleField = (field: SelectedField) => { let isAdding = false setSelectedFields((prev) => { const isSelected = prev.find((f) => f.key === field.key) if (isSelected) { return prev.filter((f) => f.key !== field.key) } else { isAdding = true return [...prev, field] } }) setInput((prev) => { const cleanPrev = prev.replace(/:/g, '').trim() if (cleanPrev === '') { return `${field.label} ` } return `${cleanPrev} ${field.label} ` }) setShowSuggestions(false) } const buildPrompt = (userInput: string, fields: Array) => { // Si no hay campos, enviamos el texto tal cual if (fields.length === 0) return userInput return ` ${userInput}` } const handleSend = async (promptOverride?: string) => { const rawText = promptOverride || input if (!rawText.trim() && selectedFields.length === 0) return const currentFields = [...selectedFields] const finalPrompt = buildPrompt(rawText, currentFields) setInput('') try { const payload: any = { planId: planId, content: finalPrompt, conversacionId: activeChatId || undefined, } if (currentFields.length > 0) { payload.campos = currentFields.map((f) => f.key) } const response = await sendChat(payload) if (response.conversacionId && response.conversacionId !== activeChatId) { setActiveChatId(response.conversacionId) } await queryClient.invalidateQueries({ queryKey: ['conversation-by-plan', planId], }) } catch (error) { console.error('Error en el chat:', error) // Aquí sí podrías usar un toast o un mensaje de error temporal } } const totalReferencias = useMemo(() => { return ( selectedArchivoIds.length + selectedRepositorioIds.length + uploadedFiles.length ) }, [selectedArchivoIds, selectedRepositorioIds, uploadedFiles]) const { activeChats, archivedChats } = useMemo(() => { const allChats = lastConversation || [] return { activeChats: allChats.filter((chat: any) => chat.estado === 'ACTIVA'), archivedChats: allChats.filter( (chat: any) => chat.estado === 'ARCHIVADA', ), } }, [lastConversation]) return (
{/* --- PANEL IZQUIERDO: HISTORIAL --- */}

Chats

{/* Botón de toggle archivados movido aquí arriba */}
{!showArchived ? ( activeChats.map((chat) => (
setActiveChatId(chat.id)} className={`group relative flex w-full cursor-pointer items-center gap-3 rounded-lg px-3 py-3 text-sm transition-colors ${ activeChatId === chat.id ? 'bg-slate-100 font-medium text-slate-900' : 'text-slate-600 hover:bg-slate-50' }`} > { e.stopPropagation() setEditingChatId(chat.id) }} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault() const newTitle = e.currentTarget.textContent || '' updateTitleMutation( { id: chat.id, titulo: newTitle }, { onSuccess: () => setEditingChatId(null), }, ) } if (e.key === 'Escape') { setEditingChatId(null) e.currentTarget.textContent = chat.nombre || '' } }} onBlur={(e) => { if (editingChatId === chat.id) { const newTitle = e.currentTarget.textContent || '' if (newTitle !== chat.nombre) { updateTitleMutation({ id: chat.id, nombre: newTitle }) } setEditingChatId(null) } }} onClick={(e) => { if (editingChatId === chat.id) e.stopPropagation() }} > {chat.nombre || `Chat ${chat.creado_en.split('T')[0]}`} {/* ACCIONES */}
)) ) : ( /* ... Resto del código de archivados (sin cambios) ... */

Archivados

{archivedChats.map((chat) => (
{chat.nombre || `Archivado ${chat.creado_en.split('T')[0]}`}
))}
)}
{/* PANEL DE CHAT PRINCIPAL */}
{/* NUEVO: Barra superior de campos seleccionados */}
Mejorar con IA
{/* CONTENIDO DEL CHAT */}
{chatMessages.map((msg) => (
{/* Icono opcional de advertencia si es refusal */} {msg.isRefusal && (
Aviso del Asistente
)} {msg.content} {/* Renderizado de sugerencias (ImprovementCard) */} {!msg.isRefusal && msg.suggestions && msg.suggestions.length > 0 && (
)}
))} {isLoading && (
)}
{/* Botones flotantes de aplicación */} {pendingSuggestion && !isLoading && (
)}
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
{/* MENÚ DE SUGERENCIAS FLOTANTE */} {showSuggestions && (
Seleccionar campo para IA
{availableFields.map((field) => ( ))}
)} {/* CONTENEDOR DEL INPUT TRANSFORMADO */}
{/* 1. Visualización de campos dentro del input ) */} {selectedFields.length > 0 && (
{selectedFields.map((field) => (
Campo: {field.label}
))}
)} {/* 2. Área de escritura */}