import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import React, { useState, useMemo } from 'react'; import { cn } from "@/lib/utils"; import { Message } from "@/hooks/use-chat"; import { Bot, User } from "lucide-react"; import { markdownRenderers } from "./markdown-renderers"; import { MessageActions } from "./message-actions"; import { MessageBackground } from "./message-background"; import { MessageStepThinking } from "./message-step-thinking"; import { DebugConsole, type DebugData } from "./debug-console"; import type { ThinkingMessageMetadata } from "@/hooks/use-chat"; import { useSettingsStore } from "@/store/settings-store"; interface ChatMessageProps { message: Message; isStreaming?: boolean; highlightTerm?: string; highlightIndex?: number; messageIndex?: number; debugData?: DebugData; } /** * Componente de mensagem do chat otimizado com React.memo * * Renderiza markdown em tempo real durante streaming para melhor UX. * Usa useMemo para otimizar re-renders e manter performance. */ function ChatMessageComponent({ message, isStreaming = false, highlightTerm, highlightIndex = 0, messageIndex, debugData }: ChatMessageProps) { const debugMode = useSettingsStore(state => state.debugMode); // Verificar se é mensagem de processo de pensamento const isThinkingMessage = message.metadata && typeof message.metadata === 'object' && 'type' in message.metadata && message.metadata.type === 'thinking'; if (isThinkingMessage) { return ; } const isUser = message.role === 'user'; const [selected, setSelected] = useState(false); const isEmpty = !message.content || message.content.trim() === ''; // Memoizar processamento de conteúdo const processedContent = useMemo(() => { let content = message.content || ''; // Remove metadata tags content = content.replace(/[\s\S]*?<\/metadata>/gi, '').trim(); // Also remove common prefixes const prefixes = [ 'Bloco oculto de metadados:', 'Metadata:', 'Metadados:', 'Hidden metadata block:', 'JSON metadata:', '---' ]; for (const prefix of prefixes) { const regex = new RegExp(`${prefix}\\s*$`, 'i'); if (regex.test(content)) { content = content.replace(regex, '').trim(); } } return content || message.content; }, [message.content]); // Memoizar conteúdo markdown para otimizar re-renders durante streaming const markdownContent = useMemo(() => processedContent, [processedContent]); // 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 } const term = highlightTerm.trim(); const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const regex = new RegExp(`(${escapedTerm})`, 'gi'); const content = processedContent; // Encontrar todos os matches const matches: Array<{ start: number; end: number; text: string }> = []; let match; while ((match = regex.exec(content)) !== null) { matches.push({ start: match.index, end: match.index + match[0].length, text: match[0] }); } if (matches.length === 0) return null; // Construir conteúdo com highlights let highlighted = ''; let lastIndex = 0; matches.forEach((match, idx) => { // Adicionar texto antes do match highlighted += content.substring(lastIndex, match.start); // Adicionar match com highlight (laranja para atual, amarelo para outros) const isCurrent = highlightIndex !== undefined && idx === highlightIndex; const className = isCurrent ? 'bg-orange-400/60 dark:bg-orange-500/60 px-0.5 rounded' : 'bg-yellow-200/50 dark:bg-yellow-800/50 px-0.5 rounded'; highlighted += `${match.text}`; lastIndex = match.end; }); // Adicionar texto restante highlighted += content.substring(lastIndex); return highlighted; }, [processedContent, highlightTerm, highlightIndex, message.role, isStreaming]); return ( <> setSelected((v) => !v)} tabIndex={0} >
{isUser ? : }
{isUser ? ( // Estilo específico para mensagem do usuário (mais destaque, fonte maior)
{!highlightedContent && processedContent}
) : ( // Estilo Assistant isEmpty && isStreaming ? (
Gerando resposta...
) : isStreaming ? ( // Renderização de markdown em tempo real durante streaming
{markdownContent}
) : ( // Renderização COMPLETA quando mensagem finalizada: ReactMarkdown com syntax highlight
{highlightedContent ? (
) : ( {processedContent} )}
) )}
{!isUser && !isStreaming && (
)} {!isUser && !isStreaming && debugMode && debugData && ( )} ); } /** * 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 && prevProps.debugData === nextProps.debugData ); }); // Adicionar displayName para debugging ChatMessage.displayName = 'ChatMessage';