Chats de ia en segundo plano para asignaturas #160 #161

Merged
roberto.silva merged 1 commits from issue/160-chats-de-ia-en-segundo-plano-para-asignaturas into main 2026-03-09 22:31:47 +00:00
3 changed files with 70 additions and 8 deletions

View File

@@ -76,6 +76,17 @@ export function IAAsignaturaTab({
const [isCreatingNewChat, setIsCreatingNewChat] = useState(false) const [isCreatingNewChat, setIsCreatingNewChat] = useState(false)
const hasInitialSelected = useRef(false) const hasInitialSelected = useRef(false)
const isAiThinking = useMemo(() => {
if (isSending) return true
if (!rawMessages || rawMessages.length === 0) return false
// Verificamos si el último mensaje está en estado de procesamiento
const lastMessage = rawMessages[rawMessages.length - 1]
return (
lastMessage.estado === 'PROCESANDO' || lastMessage.estado === 'PENDIENTE'
)
}, [isSending, rawMessages])
// --- AUTO-SCROLL --- // --- AUTO-SCROLL ---
useEffect(() => { useEffect(() => {
const viewport = scrollRef.current?.querySelector( const viewport = scrollRef.current?.querySelector(
@@ -392,11 +403,23 @@ export function IAAsignaturaTab({
</div> </div>
</div> </div>
))} ))}
{isSending && ( {isAiThinking && (
<div className="flex animate-pulse gap-2 p-4"> <div className="animate-in fade-in flex flex-row items-start gap-3 duration-300">
<div className="h-2 w-2 rounded-full bg-teal-400" /> <Avatar className="h-8 w-8 shrink-0 border bg-teal-50">
<div className="h-2 w-2 rounded-full bg-teal-400" /> <AvatarFallback>
<div className="h-2 w-2 rounded-full bg-teal-400" /> <Sparkles
size={14}
className="animate-pulse text-teal-600"
/>
</AvatarFallback>
</Avatar>
<div className="rounded-2xl rounded-tl-none border bg-white p-4 shadow-sm">
<div className="flex gap-1.5">
<div className="h-2 w-2 animate-bounce rounded-full bg-teal-400 [animation-delay:-0.3s]" />
<div className="h-2 w-2 animate-bounce rounded-full bg-teal-400 [animation-delay:-0.15s]" />
<div className="h-2 w-2 animate-bounce rounded-full bg-teal-400" />
</div>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -301,7 +301,7 @@ export async function getConversationBySubject(subjectId: string) {
export async function getMessagesBySubjectConversation(conversationId: string) { export async function getMessagesBySubjectConversation(conversationId: string) {
const supabase = supabaseBrowser() const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from('asignatura_mensajes_ia') // Tabla corregida .from('asignatura_mensajes_ia' as any)
.select('*') .select('*')
.eq('conversacion_asignatura_id', conversationId) .eq('conversacion_asignatura_id', conversationId)
.order('fecha_creacion', { ascending: true }) .order('fecha_creacion', { ascending: true })

View File

@@ -243,15 +243,54 @@ export function useConversationBySubject(subjectId: string | null) {
} }
export function useMessagesBySubjectChat(conversationId: string | null) { export function useMessagesBySubjectChat(conversationId: string | null) {
return useQuery({ const queryClient = useQueryClient()
const query = useQuery({
queryKey: ['subject-messages', conversationId], queryKey: ['subject-messages', conversationId],
queryFn: () => { queryFn: async () => {
if (!conversationId) throw new Error('Conversation ID is required') if (!conversationId) throw new Error('Conversation ID is required')
return getMessagesBySubjectConversation(conversationId) return getMessagesBySubjectConversation(conversationId)
}, },
enabled: !!conversationId, enabled: !!conversationId,
placeholderData: (previousData) => previousData, placeholderData: (previousData) => previousData,
}) })
useEffect(() => {
if (!conversationId) return
const supabase = supabaseBrowser()
// Suscripción a cambios en la tabla específica para esta conversación
const channel = supabase
.channel(`subject_messages_${conversationId}`)
.on(
'postgres_changes',
{
event: 'UPDATE', // Solo nos interesan las actualizaciones (cuando pasa de PROCESANDO a COMPLETADO)
schema: 'public',
table: 'asignatura_mensajes_ia',
filter: `conversacion_asignatura_id=eq.${conversationId}`,
},
(payload) => {
// Si el mensaje se completó o dio error, invalidamos la caché para traer los datos nuevos
if (
payload.new.estado === 'COMPLETADO' ||
payload.new.estado === 'ERROR'
) {
queryClient.invalidateQueries({
queryKey: ['subject-messages', conversationId],
})
}
},
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [conversationId, queryClient])
return query
} }
export function useUpdateSubjectRecommendation() { export function useUpdateSubjectRecommendation() {