Ctrl K
ComunidadGuías…

components/render-message.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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<UIMessage<unknown, UIDataTypes, UITools>>['status']
  addToolResult?: (params: { toolCallId: string; result: any }) => void
  onUpdateMessage?: (messageId: string, newContent: string) => Promise<void>
  reload?: (messageId: string) => Promise<void | string | null | undefined>
  isLatestMessage?: boolean
  citationMaps?: Record<string, Record<number, SearchResultItem>>
  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) => (
          <UserFileSection
            key={`${messageId}-user-file-${index}`}
            file={{
              name: part.filename || 'Unknown file',
              url: part.url,
              contentType: part.mediaType
            }}
          />
        ))}
        {(textPart ||
          pastedTexts.length > 0 ||
          quotedContexts.length > 0 ||
          noteContexts.length > 0 ||
          urls.length > 0) && (
          <UserTextSection
            content={textPart?.text ?? ''}
            pastedTexts={pastedTexts}
            quotedContexts={quotedContexts}
            noteContexts={noteContexts}
            urls={urls}
            messageId={messageId}
            onUpdateMessage={onUpdateMessage}
          />
        )}
      </>
    )
  }

  // 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(
      <ResearchProcessSection
        key={`${messageId}-proc-${keySuffix}`}
        message={message}
        messageId={messageId}
        parts={buffer}
        getIsOpen={getIsOpen}
        onOpenChange={onOpenChange}
        status={status}
        addToolResult={addToolResult}
      />
    )
    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(
            <ResearchProcessSection
              key={`${messageId}-proc-seg-${index}`}
              message={message}
              messageId={messageId}
              parts={buffer}
              getIsOpen={getIsOpen}
              onOpenChange={onOpenChange}
              status={status}
              addToolResult={addToolResult}
              hasSubsequentText={true}
            />
          )
          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(
        <AnswerSection
          key={`${messageId}-text-${index}`}
          content={part.text}
          isOpen={getIsOpen(
            messageId,
            part.type,
            index < (message.parts?.length ?? 0) - 1
          )}
          onOpenChange={open => 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(
        <DraftEditNotice
          key={`${messageId}-draft-edit-${index}`}
          part={part as ToolPart}
        />
      )
    } else if (part.type === 'reasoning' || part.type?.startsWith?.('tool-')) {
      buffer.push(part)
    } else if (part.type === 'dynamic-tool') {
      flushBuffer(`seg-${index}`)
      elements.push(
        <DynamicToolDisplay
          key={`${messageId}-dynamic-tool-${index}`}
          part={part as DynamicToolPart}
        />
      )
    }
  })
  // Flush tail (no subsequent text)
  flushBuffer('tail')

  return <>{elements}</>
}