From e84e0abe8d9e46c9d8d487a4c34caf55c062206a Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Thu, 5 Mar 2026 09:09:39 -0600 Subject: [PATCH 1/3] Se agregan hooks para manejo de la ia en asignaturas --- .../asignaturas/detalle/IAAsignaturaTab.tsx | 80 ++++++++++--- src/data/api/ai.api.ts | 112 ++++++++++++++++++ src/data/hooks/useAI.ts | 97 ++++++++++++++- 3 files changed, 269 insertions(+), 20 deletions(-) diff --git a/src/components/asignaturas/detalle/IAAsignaturaTab.tsx b/src/components/asignaturas/detalle/IAAsignaturaTab.tsx index bdb7bd9..24a78c5 100644 --- a/src/components/asignaturas/detalle/IAAsignaturaTab.tsx +++ b/src/components/asignaturas/detalle/IAAsignaturaTab.tsx @@ -13,14 +13,20 @@ import { } from 'lucide-react' import { useState, useEffect, useRef, useMemo } from 'react' -import type { IAMessage, IASugerencia } from '@/types/asignatura' +import type { IASugerencia } from '@/types/asignatura' import { Avatar, AvatarFallback } from '@/components/ui/avatar' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' import { Textarea } from '@/components/ui/textarea' -import { useSubject } from '@/data' +import { + useAISubjectChat, + useConversationBySubject, + useMessagesBySubjectChat, + useSubject, + useUpdateSubjectRecommendation, +} from '@/data' import { cn } from '@/lib/utils' // Tipos importados de tu archivo de asignatura @@ -60,14 +66,13 @@ interface SelectedField { interface IAAsignaturaTabProps { asignatura: Record - messages: Array + onSendMessage: (message: string, campoId?: string) => void onAcceptSuggestion: (sugerencia: IASugerencia) => void onRejectSuggestion: (messageId: string) => void } export function IAAsignaturaTab({ - messages, onSendMessage, onAcceptSuggestion, onRejectSuggestion, @@ -85,6 +90,49 @@ export function IAAsignaturaTab({ const [showSuggestions, setShowSuggestions] = useState(false) const [isLoading, setIsLoading] = useState(false) const scrollRef = useRef(null) + const [isSending, setIsSending] = useState(false) + + const { data: conversaciones } = useConversationBySubject(asignaturaId) + const activeConversationId = conversaciones?.[0]?.id + + const { data: rawMessages } = useMessagesBySubjectChat(activeConversationId) + const { mutateAsync: sendMessage } = useAISubjectChat() + const { mutate: applySuggestion } = useUpdateSubjectRecommendation() + + const messages = useMemo(() => { + return rawMessages + ?.map((m) => ({ + id: m.id, + role: 'user', // El mensaje del usuario + content: m.mensaje, + sugerencia: null, + })) + .concat( + rawMessages?.map((m) => ({ + id: `${m.id}-ai`, + role: 'assistant', + content: m.respuesta, + sugerencia: + m.propuesta?.recommendations?.length > 0 + ? { + id: m.id, + campoKey: m.propuesta.recommendations[0].campo_afectado, + campoNombre: + m.propuesta.recommendations[0].campo_afectado.replace( + /_/g, + ' ', + ), + valorSugerido: m.propuesta.recommendations[0].texto_mejora, + aceptada: m.propuesta.recommendations[0].aplicada, + } + : null, + })), + ) + .sort((a, b) => (a.id > b.id ? 1 : -1)) // Unir y ordenar (simplificado) + + // NOTA: En producción, es mejor que tu query devuelva los mensajes ya intercalados + // o usar el campo 'conversacion_json' de la tabla padre para mayor fidelidad. + }, [rawMessages]) // 1. Transformar datos de la asignatura para el menú const availableFields = useMemo(() => { @@ -187,17 +235,19 @@ export function IAAsignaturaTab({ const rawText = promptOverride || input if (!rawText.trim() && selectedFields.length === 0) return - const finalPrompt = buildPrompt(rawText) - - setIsLoading(true) - // Llamamos a la función que viene por props - onSendMessage(finalPrompt, selectedFields[0]?.key) - - setInput('') - setSelectedFields([]) - - // Simular carga local para el feedback visual - setTimeout(() => setIsLoading(false), 1200) + setIsSending(true) + try { + await sendMessage({ + subjectId: asignaturaId as any, + content: rawText, + campos: selectedFields.map((f) => f.key), + conversacionId: activeConversationId, + }) + setInput('') + setSelectedFields([]) + } finally { + setIsSending(false) + } } return ( diff --git a/src/data/api/ai.api.ts b/src/data/api/ai.api.ts index d69669e..caa9d10 100644 --- a/src/data/api/ai.api.ts +++ b/src/data/api/ai.api.ts @@ -247,3 +247,115 @@ export async function update_recommendation_applied_status( return true } + +// --- FUNCIONES DE ASIGNATURA --- + +export async function create_subject_conversation(subjectId: string) { + const supabase = supabaseBrowser() + const { data, error } = await supabase.functions.invoke( + 'create-chat-conversation/asignatura/conversations', // Ruta corregida + { + method: 'POST', + body: { + asignatura_id: subjectId, + instanciador: 'alex', + }, + }, + ) + if (error) throw error + return data // Retorna { conversation_asignatura: { id, ... } } +} + +export async function ai_subject_chat_v2(payload: { + conversacionId: string + content: string + campos?: Array +}) { + const supabase = supabaseBrowser() + const { data, error } = await supabase.functions.invoke( + `create-chat-conversation/conversations/asignatura/${payload.conversacionId}/messages`, // Ruta corregida + { + method: 'POST', + body: { + content: payload.content, + campos: payload.campos || [], + }, + }, + ) + if (error) throw error + return data +} + +export async function getConversationBySubject(subjectId: string) { + const supabase = supabaseBrowser() + const { data, error } = await supabase + .from('conversaciones_asignatura') // Tabla corregida + .select('*') + .eq('asignatura_id', subjectId) + .order('creado_en', { ascending: false }) + + if (error) throw error + return data ?? [] +} + +export async function getMessagesBySubjectConversation(conversationId: string) { + const supabase = supabaseBrowser() + const { data, error } = await supabase + .from('asignatura_mensajes_ia') // Tabla corregida + .select('*') + .eq('conversacion_asignatura_id', conversationId) + .order('fecha_creacion', { ascending: true }) + + if (error) throw error + return data ?? [] +} + +export async function update_subject_recommendation_applied( + mensajeId: string, + campoAfectado: string, +) { + const supabase = supabaseBrowser() + + // 1. Obtener propuesta actual + const { data: msgData, error: fetchError } = await supabase + .from('asignatura_mensajes_ia') + .select('propuesta') + .eq('id', mensajeId) + .single() + + if (fetchError) throw fetchError + const propuestaActual = msgData?.propuesta as any + + // 2. Marcar como aplicada + const nuevaPropuesta = { + ...propuestaActual, + recommendations: (propuestaActual.recommendations || []).map((rec: any) => + rec.campo_afectado === campoAfectado ? { ...rec, aplicada: true } : rec, + ), + } + + // 3. Update + const { error: updateError } = await supabase + .from('asignatura_mensajes_ia') + .update({ propuesta: nuevaPropuesta }) + .eq('id', mensajeId) + + if (updateError) throw updateError + return true +} + +export async function update_subject_conversation_status( + conversacionId: string, + nuevoEstado: 'ARCHIVADA' | 'ACTIVA', +) { + const supabase = supabaseBrowser() + const { data, error } = await supabase + .from('conversaciones_asignatura') + .update({ estado: nuevoEstado }) + .eq('id', conversacionId) + .select() + .single() + + if (error) throw error + return data +} diff --git a/src/data/hooks/useAI.ts b/src/data/hooks/useAI.ts index 1e23617..1f95e4a 100644 --- a/src/data/hooks/useAI.ts +++ b/src/data/hooks/useAI.ts @@ -3,7 +3,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { ai_plan_chat_v2, ai_plan_improve, - ai_subject_chat, ai_subject_improve, create_conversation, get_chat_history, @@ -13,6 +12,12 @@ import { update_recommendation_applied_status, update_conversation_title, getMessagesByConversation, + update_subject_conversation_status, + update_subject_recommendation_applied, + getMessagesBySubjectConversation, + getConversationBySubject, + ai_subject_chat_v2, + create_subject_conversation, } from '../api/ai.api' // eslint-disable-next-line node/prefer-node-protocol @@ -137,10 +142,6 @@ export function useAISubjectImprove() { return useMutation({ mutationFn: ai_subject_improve }) } -export function useAISubjectChat() { - return useMutation({ mutationFn: ai_subject_chat }) -} - export function useLibrarySearch() { return useMutation({ mutationFn: library_search }) } @@ -157,3 +158,89 @@ export function useUpdateConversationTitle() { }, }) } + +// Asignaturas + +export function useAISubjectChat() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: async (payload: { + subjectId: UUID + content: string + campos?: Array + conversacionId?: string + }) => { + let currentId = payload.conversacionId + + // 1. Si no hay ID, creamos la conversación de asignatura + if (!currentId) { + const response = await create_subject_conversation(payload.subjectId) + currentId = response.conversation_asignatura.id + } + + // 2. Enviamos mensaje al endpoint de asignatura + const result = await ai_subject_chat_v2({ + conversacionId: currentId!, + content: payload.content, + campos: payload.campos, + }) + + return { ...result, conversacionId: currentId } + }, + onSuccess: (data) => { + // Invalidamos mensajes para que se refresque el chat + qc.invalidateQueries({ + queryKey: ['subject-messages', data.conversacionId], + }) + }, + }) +} + +export function useConversationBySubject(subjectId: string | null) { + return useQuery({ + queryKey: ['conversation-by-subject', subjectId], + queryFn: () => getConversationBySubject(subjectId!), + enabled: !!subjectId, + }) +} + +export function useMessagesBySubjectChat(conversationId: string | null) { + return useQuery({ + queryKey: ['subject-messages', conversationId], + queryFn: () => { + if (!conversationId) throw new Error('Conversation ID is required') + return getMessagesBySubjectConversation(conversationId) + }, + enabled: !!conversationId, + placeholderData: (previousData) => previousData, + }) +} + +export function useUpdateSubjectRecommendation() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: (payload: { mensajeId: string; campoAfectado: string }) => + update_subject_recommendation_applied( + payload.mensajeId, + payload.campoAfectado, + ), + onSuccess: () => { + // Refrescamos los mensajes para ver el check de "aplicado" + qc.invalidateQueries({ queryKey: ['subject-messages'] }) + }, + }) +} + +export function useUpdateSubjectConversationStatus() { + const qc = useQueryClient() + + return useMutation({ + mutationFn: (payload: { id: string; estado: 'ARCHIVADA' | 'ACTIVA' }) => + update_subject_conversation_status(payload.id, payload.estado), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['conversation-by-subject'] }) + }, + }) +} From 772f3b6750c08eb0d498f1024a493cb126035193 Mon Sep 17 00:00:00 2001 From: "Roberto.silva" Date: Fri, 6 Mar 2026 13:33:38 -0600 Subject: [PATCH 2/3] Se prepara chat asignaturas para ia --- .../asignaturas/detalle/IAAsignaturaTab.tsx | 515 ++++++++++-------- 1 file changed, 282 insertions(+), 233 deletions(-) diff --git a/src/components/asignaturas/detalle/IAAsignaturaTab.tsx b/src/components/asignaturas/detalle/IAAsignaturaTab.tsx index 24a78c5..a56cad0 100644 --- a/src/components/asignaturas/detalle/IAAsignaturaTab.tsx +++ b/src/components/asignaturas/detalle/IAAsignaturaTab.tsx @@ -1,4 +1,5 @@ -import { useParams, useRouterState } from '@tanstack/react-router' +import { useQueryClient } from '@tanstack/react-query' +import { useParams } from '@tanstack/react-router' import { Sparkles, Send, @@ -10,13 +11,15 @@ import { BookOpen, Check, X, + MessageSquarePlus, + Archive, + History, // Agregado } from 'lucide-react' import { useState, useEffect, useRef, useMemo } from 'react' import type { IASugerencia } from '@/types/asignatura' import { Avatar, AvatarFallback } from '@/components/ui/avatar' -import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { ScrollArea } from '@/components/ui/scroll-area' import { Textarea } from '@/components/ui/textarea' @@ -25,39 +28,10 @@ import { useConversationBySubject, useMessagesBySubjectChat, useSubject, - useUpdateSubjectRecommendation, + useUpdateSubjectConversationStatus, } from '@/data' import { cn } from '@/lib/utils' -// Tipos importados de tu archivo de asignatura - -const PRESETS = [ - { - id: 'mejorar-objetivo', - label: 'Mejorar objetivo', - icon: Target, - prompt: 'Mejora la redacción del objetivo de esta asignatura...', - }, - { - id: 'contenido-tematico', - label: 'Sugerir contenido', - icon: BookOpen, - prompt: 'Genera un desglose de temas para esta asignatura...', - }, - { - id: 'actividades', - label: 'Actividades de aprendizaje', - icon: GraduationCap, - prompt: 'Sugiere actividades prácticas para los temas seleccionados...', - }, - { - id: 'bibliografia', - label: 'Actualizar bibliografía', - icon: FileText, - prompt: 'Recomienda bibliografía reciente para esta asignatura...', - }, -] - interface SelectedField { key: string label: string @@ -65,209 +39,298 @@ interface SelectedField { } interface IAAsignaturaTabProps { - asignatura: Record - - onSendMessage: (message: string, campoId?: string) => void + asignatura?: Record onAcceptSuggestion: (sugerencia: IASugerencia) => void onRejectSuggestion: (messageId: string) => void } export function IAAsignaturaTab({ - onSendMessage, onAcceptSuggestion, onRejectSuggestion, }: IAAsignaturaTabProps) { - const routerState = useRouterState() + const queryClient = useQueryClient() const { asignaturaId } = useParams({ from: '/planes/$planId/asignaturas/$asignaturaId', }) - const { data: datosGenerales, isLoading: loadingAsig } = - useSubject(asignaturaId) - // ESTADOS PRINCIPALES (Igual que en Planes) + // --- 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 [isLoading, setIsLoading] = useState(false) - const scrollRef = useRef(null) const [isSending, setIsSending] = useState(false) + const scrollRef = useRef(null) - const { data: conversaciones } = useConversationBySubject(asignaturaId) - const activeConversationId = conversaciones?.[0]?.id - - const { data: rawMessages } = useMessagesBySubjectChat(activeConversationId) + // --- 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: applySuggestion } = useUpdateSubjectRecommendation() + const { mutate: updateStatus } = useUpdateSubjectConversationStatus() + const [isCreatingNewChat, setIsCreatingNewChat] = useState(false) + const hasInitialSelected = useRef(false) + // --- 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]) + + // --- PROCESAMIENTO DE MENSAJES --- const messages = useMemo(() => { - return rawMessages - ?.map((m) => ({ - id: m.id, - role: 'user', // El mensaje del usuario - content: m.mensaje, - sugerencia: null, - })) - .concat( - rawMessages?.map((m) => ({ + if (!rawMessages) return [] + return rawMessages.flatMap((m) => { + const msgs = [] + msgs.push({ id: `${m.id}-user`, role: 'user', content: m.mensaje }) + if (m.respuesta) { + msgs.push({ id: `${m.id}-ai`, role: 'assistant', content: m.respuesta, - sugerencia: - m.propuesta?.recommendations?.length > 0 - ? { - id: m.id, - campoKey: m.propuesta.recommendations[0].campo_afectado, - campoNombre: - m.propuesta.recommendations[0].campo_afectado.replace( - /_/g, - ' ', - ), - valorSugerido: m.propuesta.recommendations[0].texto_mejora, - aceptada: m.propuesta.recommendations[0].aplicada, - } - : null, - })), - ) - .sort((a, b) => (a.id > b.id ? 1 : -1)) // Unir y ordenar (simplificado) - - // NOTA: En producción, es mejor que tu query devuelva los mensajes ya intercalados - // o usar el campo 'conversacion_json' de la tabla padre para mayor fidelidad. + sugerencia: m.propuesta?.recommendations?.[0] + ? { + id: m.id, + campoKey: m.propuesta.recommendations[0].campo_afectado, + campoNombre: + m.propuesta.recommendations[0].campo_afectado.replace( + /_/g, + ' ', + ), + valorSugerido: m.propuesta.recommendations[0].texto_mejora, + aceptada: m.propuesta.recommendations[0].aplicada, + } + : null, + }) + } + return msgs + }) }, [rawMessages]) - // 1. Transformar datos de la asignatura para el menú - const availableFields = useMemo(() => { - if (!datosGenerales?.datos) return [] - - const estructuraProps = - datosGenerales?.estructuras_asignatura?.definicion?.properties || {} - - return Object.keys(datosGenerales.datos).map((key) => { - const estructuraCampo = estructuraProps[key] - - const labelAmigable = - estructuraCampo?.title || - key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()) - - return { - key, - label: labelAmigable, - value: String(datosGenerales.datos[key] || ''), - } - }) - }, [datosGenerales]) - - // 2. Manejar el estado inicial si viene de "Datos de Asignatura" (Prefill) - + // Auto-selección inicial useEffect(() => { - const state = routerState.location.state as any + // Si ya hay un chat, o si el usuario ya interactuó (hasInitialSelected), abortamos. + if (activeChatId || hasInitialSelected.current) return - if (state?.prefillCampo && availableFields.length > 0) { - console.log(state?.prefillCampo) - console.log(availableFields) - - const field = availableFields.find((f) => f.key === state.prefillCampo) - - if (field && !selectedFields.find((sf) => sf.key === field.key)) { - setSelectedFields([field]) - // Sincronizamos el texto inicial con el campo pre-seleccionado - setInput(`Mejora el campo ${field.label}: `) - } + if (activeChats.length > 0 && !loadingConv) { + setActiveChatId(activeChats[0].id) + hasInitialSelected.current = true } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [availableFields]) - - // Scroll automático - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight - } - }, [messages, isLoading]) - - // 3. Lógica para el disparador ":" - const handleInputChange = (e: React.ChangeEvent) => { - const val = e.target.value - setInput(val) - setShowSuggestions(val.endsWith(':')) - } - - const toggleField = (field: SelectedField) => { - setSelectedFields((prev) => { - const isSelected = prev.find((f) => f.key === field.key) - - // 1. Si ya está seleccionado, lo quitamos (Toggle OFF) - if (isSelected) { - return prev.filter((f) => f.key !== field.key) - } - - // 2. Si no está, lo agregamos a la lista (Toggle ON) - const newSelected = [...prev, field] - - // 3. Actualizamos el texto del input para reflejar los títulos (labels) - setInput((prevText) => { - // Separamos lo que el usuario escribió antes del disparador ":" - // y lo que viene después (posibles keys/labels previos) - const parts = prevText.split(':') - const beforeColon = parts[0] - - // Creamos un string con los labels de todos los campos seleccionados - const labelsPath = newSelected.map((f) => f.label).join(', ') - - return `${beforeColon.trim()}: ${labelsPath} ` - }) - - return newSelected - }) - - // Opcional: mantener abierto si quieres que el usuario elija varios seguidos - // setShowSuggestions(false) - } - - const buildPrompt = (userInput: string) => { - if (selectedFields.length === 0) return userInput - const fieldsText = selectedFields - .map((f) => `- ${f.label}: ${f.value || '(vacio)'}`) - .join('\n') - - return `${userInput}\n\nCampos a analizar:\n${fieldsText}`.trim() - } + }, [activeChats, loadingConv]) const handleSend = async (promptOverride?: string) => { - const rawText = promptOverride || input - if (!rawText.trim() && selectedFields.length === 0) return + const text = promptOverride || input + if (!text.trim() && selectedFields.length === 0) return setIsSending(true) try { - await sendMessage({ - subjectId: asignaturaId as any, - content: rawText, + 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: activeConversationId, + 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 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]) + + 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 DE CHAT PRINCIPAL */} -
- {/* Barra superior */} -
-
- - IA de Asignatura - -
+
+ {/* PANEL IZQUIERDO */} +
+
+

+ Historial +

+ +
+ + + + +
+ {(showArchived ? archivedChats : activeChats).map((chat: any) => ( +
{ + setActiveChatId(chat.id) + setIsCreatingNewChat(false) // <--- Volvemos al modo normal + }} + className={cn( + 'group relative flex cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-sm transition-all', + activeChatId === chat.id + ? 'bg-teal-50 font-medium text-teal-900' + : 'text-slate-600 hover:bg-slate-100', + )} + > + + + {chat.titulo || 'Conversación'} + + +
+ ))} +
+
+
+ + {/* PANEL CENTRAL */} +
+
+ + Asistente IA +
- {/* CONTENIDO DEL CHAT */}
- {messages?.map((msg) => ( + {messages.length === 0 && !isSending && ( +
+
+ +
+
+

+ Nueva Consultoría IA +

+

+ Selecciona campos con ":" o usa una acción rápida para + comenzar. +

+
+
+ )} + {messages.map((msg) => (
- {/* Renderizado de Sugerencias (Homologado con lógica de Asignatura) */} {msg.sugerencia && !msg.sugerencia.aceptada && (

- Propuesta para: {msg.sugerencia.campoNombre} + Propuesta: {msg.sugerencia.campoNombre}

{msg.sugerencia.valorSugerido} @@ -310,10 +372,8 @@ export function IAAsignaturaTab({
@@ -321,7 +381,7 @@ export function IAAsignaturaTab({ size="sm" variant="outline" onClick={() => onRejectSuggestion(msg.id)} - className="h-8 text-xs" + className="h-8" > Descartar @@ -329,44 +389,36 @@ export function IAAsignaturaTab({
)} - {msg.sugerencia?.aceptada && ( - - Sugerencia aplicada - - )}
))} - {isLoading && ( -
-
-
-
+ {isSending && ( +
+
+
+
)}
- {/* INPUT FIJO AL FONDO */} + {/* INPUT */}
- {/* MENÚ DE SUGERENCIAS FLOTANTE */} {showSuggestions && (
-
- Seleccionar campo de asignatura +
+ Campos de Asignatura
{availableFields.map((field) => (
)} - {/* CONTENEDOR DEL INPUT */}
- {/* Visualización de Tags */} {selectedFields.length > 0 && (
{selectedFields.map((field) => ( @@ -386,10 +436,10 @@ export function IAAsignaturaTab({ key={field.key} className="animate-in zoom-in-95 flex items-center gap-1 rounded-md border border-teal-200 bg-teal-100 px-2 py-0.5 text-[11px] font-semibold text-teal-800" > - Campo: {field.label} + {field.label} @@ -401,27 +451,28 @@ export function IAAsignaturaTab({