import React from 'react'
import { cn } from '@/utils/cn'
import type { ExtendedEvent, Attachment, HandleImageAttachmentClick } from '../../types'
import type { ApiEvent } from '../../hooks/useNovaEvents'
import { MessageHeader } from './MessageHeader'
import { MessageFooter } from './MessageFooter'
import { ToolCallAction } from './ToolCallAction'
import { BrowserUseAction } from './BrowserUseAction'
import { ContentMessage } from './ContentMessage'
import { FactCheck } from './FactCheck'
import { UserMessage } from './UserMessage'
import { SystemMessage } from './SystemMessage'
import { getMessageType, SILENT_ACTION_TYPES } from './utils'
export interface MessageItemProps {
event: ExtendedEvent
className?: string
shouldShowTimestamp?: boolean
shouldShowCopyButton?: boolean
onCopyMessage?: () => void
onAttachmentClick?: (attachment: Attachment) => void
onImageAttachmentClick?: HandleImageAttachmentClick
onToolCallClick?: (event: ApiEvent) => void
onSendMessage?: (content: string) => void
}
/**
* 消息项组件 - 结构严格对齐 next-agent message-body.tsx
*
* 渲染顺序:
* MessageHeader → ActionMessage → ContentMessage → FactCheck
* → UserMessage → SystemMessage → MessageFooter
*/
function InnerMessageItem({
event,
className,
shouldShowTimestamp,
shouldShowCopyButton = false,
onCopyMessage,
onAttachmentClick,
onImageAttachmentClick,
onToolCallClick,
onSendMessage,
}: MessageItemProps) {
// task_end 不渲染
if (event.event_type === 'task_end') return null
const messageType = getMessageType(event)
const {
action,
content,
userInteraction,
attachment,
imageAttachment,
operation,
mcpContent,
planConfig,
taskTodoConfig,
base,
} = event.renderProps || {}
// plan_step_state 为 canceled 时显示"用户取消"
const planStepState = (event as ExtendedEvent & { plan_step_state?: string })
.plan_step_state
if (planStepState === 'canceled') {
return (
)
}
// 对应 message-body.tsx showContentMessage 逻辑(完全对齐 Remix)
const showContentMessage =
base &&
(messageType.showContent || messageType.showUserInteraction) &&
!messageType.isFactCheck &&
(!!content || !!userInteraction)
// 对应 Remix useMessageActions.shouldShowTimestamp:
// 只有 isUserInput 或 isSummary 且有内容时才显示时间戳
const timestamp = base?.timestamp ?? event.timestamp
const showTimestamp =
shouldShowTimestamp !== undefined
? shouldShowTimestamp
: !!(messageType.showTimestamp && content?.content && (messageType.isSummary || messageType.isUserInput))
// 对应 Remix useMessageActions.shouldShowCopyButton
const showCopyButton =
shouldShowCopyButton !== undefined
? shouldShowCopyButton
: !!(content?.content && (messageType.isUserInput || messageType.isSummary))
// 判断 action 是否实际会渲染(排除 silent 类型和空 action)
// 对应 Remix Action 组件: if (!name || !action_type) return null
const hasRenderableAction =
messageType.showAction &&
!!action &&
!!(action.action_type || action.name) &&
!SILENT_ACTION_TYPES.has(action.action_type || '')
// 完全空的事件不渲染(Remix 靠 show/notEmpty 提前过滤,SDK 在此兜底)
if (
!hasRenderableAction &&
!showContentMessage &&
!messageType.isFactCheck &&
!messageType.showUserFile &&
!messageType.showTemplate &&
!messageType.showSystemAttachments &&
!messageType.showMcpContent &&
!messageType.showOperation &&
!messageType.showPlanConfig &&
!messageType.showTaskTodoList
) {
return null
}
return (
{/* MessageHeader:对应 */}
{/* ActionMessage:对应 messageType.showAction && action */}
{messageType.showAction && action && (
action.action_type === 'browser_use' ? (
) : (
)
)}
{/* ContentMessage:对应 showContentMessage */}
{showContentMessage && (
)}
{/* FactCheck:对应 messageType.isFactCheck */}
{messageType.isFactCheck && (
)}
{/* UserMessage:对应 messageType.showUserFile || messageType.showTemplate */}
{(messageType.showUserFile || messageType.showTemplate) && (
)}
{/* SystemMessage */}
{/* MessageFooter:对应 */}
{})}
/>
)
}
export const MessageItem = React.memo(InnerMessageItem)
export default MessageItem