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