import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { 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 type { ThinkingMessageMetadata } from "@/hooks/use-chat"; interface ChatMessageProps { message: Message; isStreaming?: boolean; highlightTerm?: string; highlightIndex?: number; messageIndex?: number; } export function ChatMessage({ message, isStreaming = false, highlightTerm, highlightIndex = 0, messageIndex }: ChatMessageProps) { // 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]); // Processar highlight separadamente (após limpeza de conteúdo) const highlightedContent = useMemo(() => { 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]); 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...
) : (
{highlightedContent ? (
) : ( {processedContent} )} {isStreaming && !isEmpty && ( )}
) )}
{!isUser && !isStreaming && (
)} ); }