import { UseChatHelpers } from '@ai-sdk/react' import type { SearchResultItem } from '@/lib/types' import type { ToolPart, UIDataTypes, UIMessage, UIMessageMetadata, UITools } from '@/lib/types/ai' import type { DynamicToolPart } from '@/lib/types/dynamic-tools' import { DraftEditNotice } from './draft/draft-card' import { AnswerSection } from './answer-section' import { DynamicToolDisplay } from './dynamic-tool-display' import ResearchProcessSection from './research-process-section' import { UserFileSection } from './user-file-section' import { UserTextSection } from './user-text-section' interface RenderMessageProps { message: UIMessage messageId: string getIsOpen: (id: string, partType?: string, hasNextPart?: boolean) => boolean onOpenChange: (id: string, open: boolean) => void chatId?: string isGuest?: boolean isCloudDeployment?: boolean libraryAvailable?: boolean status?: UseChatHelpers>['status'] addToolResult?: (params: { toolCallId: string; result: any }) => void onUpdateMessage?: (messageId: string, newContent: string) => Promise reload?: (messageId: string) => Promise isLatestMessage?: boolean citationMaps?: Record> onQuoteContext?: (text: string) => void } export function RenderMessage({ message, messageId, getIsOpen, onOpenChange, chatId, isGuest = false, isCloudDeployment = false, libraryAvailable = true, status, addToolResult, onUpdateMessage, reload, isLatestMessage = false, citationMaps = {}, onQuoteContext }: RenderMessageProps) { const isNonEmptyTextPart = (part: any) => part?.type === 'text' && typeof part.text === 'string' && part.text.trim().length > 0 // Use provided citation maps (from all messages) if (message.role === 'user') { const parts = (message.parts ?? []) as any[] const textPart = parts.find((part: any) => part.type === 'text') const files = parts.filter((part: any) => part.type === 'file') const pastedTexts = parts .filter((part: any) => part.type === 'data-pastedContent') .map((part: any) => part.data?.text ?? '') const quotedContexts = parts .filter((part: any) => part.type === 'data-quotedContext') .map((part: any) => part.data?.text ?? '') const noteContexts = parts .filter((part: any) => part.type === 'data-noteContext') .map((part: any) => ({ title: part.data?.title, text: part.data?.text ?? '' })) const urls = parts .filter((part: any) => part.type === 'data-sourceUrl') .map((part: any) => part.data?.url ?? '') return ( <> {files.map((part: any, index: number) => ( ))} {(textPart || pastedTexts.length > 0 || quotedContexts.length > 0 || noteContexts.length > 0 || urls.length > 0) && ( )} ) } // New rendering: interleave text parts with grouped non-text segments const elements: React.ReactNode[] = [] let buffer: any[] = [] const flushBuffer = (keySuffix: string) => { if (buffer.length === 0) return elements.push( ) buffer = [] } message.parts?.forEach((part: any, index: number) => { if (part.type === 'text') { // Ignore empty text chunks (some providers emit them before reasoning/tool parts). if (!isNonEmptyTextPart(part)) { return } // Check if there's buffered content before this text part const hasBufferedContent = buffer.length > 0 // Flush accumulated non-text first, marking that text follows if (hasBufferedContent) { // Create a custom flush that passes hasSubsequentText if (buffer.length > 0) { elements.push( ) buffer = [] } } const remainingParts = message.parts?.slice(index + 1) || [] const hasMoreTextParts = remainingParts.some(isNonEmptyTextPart) const isLastTextPart = !hasMoreTextParts const isStreamingComplete = status !== 'streaming' && status !== 'submitted' const shouldShowActions = isLastTextPart && (isLatestMessage ? isStreamingComplete : true) elements.push( onOpenChange(messageId, open)} chatId={chatId} isGuest={isGuest} isCloudDeployment={isCloudDeployment} libraryAvailable={libraryAvailable} showActions={shouldShowActions} messageId={messageId} metadata={message.metadata as UIMessageMetadata | undefined} reload={reload} status={status} citationMaps={citationMaps} onQuoteContext={onQuoteContext} /> ) } else if (part.type === 'tool-editDraft') { flushBuffer(`seg-${index}`) elements.push( ) } else if (part.type === 'reasoning' || part.type?.startsWith?.('tool-')) { buffer.push(part) } else if (part.type === 'dynamic-tool') { flushBuffer(`seg-${index}`) elements.push( ) } }) // Flush tail (no subsequent text) flushBuffer('tail') return <>{elements} }