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' } from 'lucide-react'
import { useState, useEffect, useRef, useMemo } from '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 { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area' import { ScrollArea } from '@/components/ui/scroll-area'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { useSubject } from '@/data' import {
useAISubjectChat,
useConversationBySubject,
useMessagesBySubjectChat,
useSubject,
useUpdateSubjectRecommendation,
} from '@/data'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
// Tipos importados de tu archivo de asignatura // Tipos importados de tu archivo de asignatura
@@ -60,14 +66,13 @@ interface SelectedField {
interface IAAsignaturaTabProps { interface IAAsignaturaTabProps {
asignatura: Record<string, any> asignatura: Record<string, any>
messages: Array<IAMessage>
onSendMessage: (message: string, campoId?: string) => void onSendMessage: (message: string, campoId?: string) => void
onAcceptSuggestion: (sugerencia: IASugerencia) => void onAcceptSuggestion: (sugerencia: IASugerencia) => void
onRejectSuggestion: (messageId: string) => void onRejectSuggestion: (messageId: string) => void
} }
export function IAAsignaturaTab({ export function IAAsignaturaTab({
messages,
onSendMessage, onSendMessage,
onAcceptSuggestion, onAcceptSuggestion,
onRejectSuggestion, onRejectSuggestion,
@@ -85,6 +90,49 @@ export function IAAsignaturaTab({
const [showSuggestions, setShowSuggestions] = useState(false) const [showSuggestions, setShowSuggestions] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const scrollRef = useRef<HTMLDivElement>(null) 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ú // 1. Transformar datos de la asignatura para el menú
const availableFields = useMemo(() => { const availableFields = useMemo(() => {
@@ -187,17 +235,19 @@ export function IAAsignaturaTab({
const rawText = promptOverride || input const rawText = promptOverride || input
if (!rawText.trim() && selectedFields.length === 0) return if (!rawText.trim() && selectedFields.length === 0) return
const finalPrompt = buildPrompt(rawText) setIsSending(true)
try {
setIsLoading(true) await sendMessage({
// Llamamos a la función que viene por props subjectId: asignaturaId as any,
onSendMessage(finalPrompt, selectedFields[0]?.key) content: rawText,
campos: selectedFields.map((f) => f.key),
setInput('') conversacionId: activeConversationId,
setSelectedFields([]) })
setInput('')
// Simular carga local para el feedback visual setSelectedFields([])
setTimeout(() => setIsLoading(false), 1200) } finally {
setIsSending(false)
}
} }
return ( return (

View File

@@ -247,3 +247,115 @@ export async function update_recommendation_applied_status(
return true 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 { import {
ai_plan_chat_v2, ai_plan_chat_v2,
ai_plan_improve, ai_plan_improve,
ai_subject_chat,
ai_subject_improve, ai_subject_improve,
create_conversation, create_conversation,
get_chat_history, get_chat_history,
@@ -13,6 +12,12 @@ import {
update_recommendation_applied_status, update_recommendation_applied_status,
update_conversation_title, update_conversation_title,
getMessagesByConversation, getMessagesByConversation,
update_subject_conversation_status,
update_subject_recommendation_applied,
getMessagesBySubjectConversation,
getConversationBySubject,
ai_subject_chat_v2,
create_subject_conversation,
} from '../api/ai.api' } from '../api/ai.api'
// eslint-disable-next-line node/prefer-node-protocol // eslint-disable-next-line node/prefer-node-protocol
@@ -137,10 +142,6 @@ export function useAISubjectImprove() {
return useMutation({ mutationFn: ai_subject_improve }) return useMutation({ mutationFn: ai_subject_improve })
} }
export function useAISubjectChat() {
return useMutation({ mutationFn: ai_subject_chat })
}
export function useLibrarySearch() { export function useLibrarySearch() {
return useMutation({ mutationFn: library_search }) 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'] })
},
})
}