Se agregan hooks para manejo de la ia en asignaturas
This commit is contained in:
@@ -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)
|
||||
|
||||
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 (
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user