feat(chat): implement infinite scroll for message loading and optimize chat message rendering

- Added infinite scroll functionality to load older messages when the user scrolls near the top of the chat.
- Implemented pagination state management to track loading status and available messages.
- Enhanced ChatMessage component with React.memo for performance optimization during streaming.
- Updated database interactions to support paginated message retrieval, improving efficiency in loading chat history.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-27 21:53:27 -03:00
1 parent 5fb1489eff
commit 337f7b3056
5 files changed
+761 -18

No files matched your search

+59 -9
View File
@@ -1,6 +1,6 @@
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { useState, useMemo } from 'react';
import React, { useState, useMemo } from 'react';
import { cn } from "@/lib/utils";
import { Message } from "@/hooks/use-chat";
import { Bot, User } from "lucide-react";
@@ -18,7 +18,14 @@ interface ChatMessageProps {
messageIndex?: number;
}
export function ChatMessage({ message, isStreaming = false, highlightTerm, highlightIndex = 0, messageIndex }: ChatMessageProps) {
/**
* Componente de mensagem do chat otimizado com React.memo
*
* Durante streaming, usa renderização simplificada (texto puro) para evitar
* recálculo custoso da árvore DOM e syntax highlight a cada token.
* Markdown completo é aplicado apenas quando a mensagem está finalizada.
*/
function ChatMessageComponent({ message, isStreaming = false, highlightTerm, highlightIndex = 0, messageIndex }: ChatMessageProps) {
// Verificar se é mensagem de processo de pensamento
const isThinkingMessage = message.metadata &&
typeof message.metadata === 'object' &&
@@ -56,7 +63,11 @@ export function ChatMessage({ message, isStreaming = false, highlightTerm, highl
}, [message.content]);
// Processar highlight separadamente (após limpeza de conteúdo)
// Desabilitar durante streaming para performance
const highlightedContent = useMemo(() => {
// Não processar highlight durante streaming
if (isStreaming) return null;
if (!highlightTerm || highlightTerm.trim().length < 2 || (message.role !== 'user' && message.role !== 'assistant')) {
return null; // Não há highlight
}
@@ -100,7 +111,7 @@ export function ChatMessage({ message, isStreaming = false, highlightTerm, highl
// Adicionar texto restante
highlighted += content.substring(lastIndex);
return highlighted;
}, [processedContent, highlightTerm, highlightIndex, message.role]);
}, [processedContent, highlightTerm, highlightIndex, message.role, isStreaming]);
return (
<MessageBackground
@@ -141,14 +152,27 @@ export function ChatMessage({ message, isStreaming = false, highlightTerm, highl
<div className="w-2 h-2 bg-primary rounded-full animate-pulse" />
<span className="text-sm font-medium">Gerando resposta...</span>
</div>
) : isStreaming ? (
// Renderização LEVE durante streaming: texto simples com formatação básica
// Evita recálculo de ReactMarkdown e syntax highlight a cada token
<div className={cn(
"prose prose-neutral dark:prose-invert max-w-none break-words leading-7 text-base text-foreground/90",
"prose-headings:font-semibold prose-headings:tracking-tight",
"relative"
)}>
<div className="whitespace-pre-wrap font-sans">
{processedContent}
</div>
<span className="inline-block w-2 h-4 bg-primary ml-1 animate-pulse rounded-full align-middle" />
</div>
) : (
// Renderização COMPLETA quando mensagem finalizada: ReactMarkdown com syntax highlight
<div className={cn(
"prose prose-neutral dark:prose-invert max-w-none break-words leading-7 text-base text-foreground/90",
"prose-headings:font-semibold prose-headings:tracking-tight prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg",
"prose-p:leading-7 prose-li:leading-7",
"prose-pre:bg-muted/50 prose-pre:border prose-pre:border-border/50 prose-pre:rounded-xl",
"prose-mark:bg-yellow-200/50 prose-mark:dark:bg-yellow-800/50 prose-mark:px-0.5 prose-mark:rounded",
isStreaming && "relative"
"prose-mark:bg-yellow-200/50 prose-mark:dark:bg-yellow-800/50 prose-mark:px-0.5 prose-mark:rounded"
)}>
{highlightedContent ? (
<div dangerouslySetInnerHTML={{ __html: highlightedContent }} />
@@ -160,9 +184,6 @@ export function ChatMessage({ message, isStreaming = false, highlightTerm, highl
{processedContent}
</ReactMarkdown>
)}
{isStreaming && !isEmpty && (
<span className="inline-block w-2 h-4 bg-primary ml-1 animate-pulse rounded-full align-middle" />
)}
</div>
)
)}
@@ -181,5 +202,34 @@ export function ChatMessage({ message, isStreaming = false, highlightTerm, highl
);
}
/**
* ChatMessage com React.memo para evitar re-renders desnecessários
*
* Comparação customizada:
* - Compara conteúdo da mensagem
* - Compara estado de streaming
* - Compara termo de highlight e índice
*/
export const ChatMessage = React.memo(ChatMessageComponent, (prevProps, nextProps) => {
// Se streaming mudou, sempre re-renderizar
if (prevProps.isStreaming !== nextProps.isStreaming) {
return false;
}
// Durante streaming, só re-renderizar se conteúdo mudou
if (nextProps.isStreaming) {
return prevProps.message.content === nextProps.message.content;
}
// Quando não está em streaming, comparar todas as props relevantes
return (
prevProps.message.content === nextProps.message.content &&
prevProps.message.role === nextProps.message.role &&
prevProps.highlightTerm === nextProps.highlightTerm &&
prevProps.highlightIndex === nextProps.highlightIndex &&
prevProps.messageIndex === nextProps.messageIndex
);
});
// Adicionar displayName para debugging
ChatMessage.displayName = 'ChatMessage';