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'] }) + }, + }) +}