Se agregan hooks para manejo de la ia en asignaturas

This commit is contained in:
2026-03-05 09:09:39 -06:00
parent 896c694a85
commit e84e0abe8d
3 changed files with 269 additions and 20 deletions

View File

@@ -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<string, any>
messages: Array<IAMessage>
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<HTMLDivElement>(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)
setIsSending(true)
try {
await sendMessage({
subjectId: asignaturaId as any,
content: rawText,
campos: selectedFields.map((f) => f.key),
conversacionId: activeConversationId,
})
setInput('')
setSelectedFields([])
// Simular carga local para el feedback visual
setTimeout(() => setIsLoading(false), 1200)
} finally {
setIsSending(false)
}
}
return (

View File

@@ -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<string>
}) {
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
}

View File

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