Files
2026-03-20 07:33:46 +00:00

226 lines
7.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (
<div
className={cn('flex flex-col text-sm relative w-full items-start', className)}
data-event-id={event.event_id}
>
<div className="text-sm text-muted-foreground italic px-1"></div>
</div>
)
}
// 对应 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 (
<div
className={cn(
'group flex flex-col text-sm relative w-full',
// 进入时轻微淡入 + 左右滑入,靠近样例中的对话动画
'animate-in fade-in-0 duration-200',
messageType.isUserInput ? 'slide-in-from-right-2' : 'slide-in-from-left-2',
messageType.containerAlignment,
className,
)}
data-event-id={event.event_id}
>
{/* MessageHeader对应 <MessageHeader avatarId={base?.metadata?.agent_id} /> */}
<MessageHeader avatarId={base?.metadata?.agent_id} />
{/* ActionMessage对应 messageType.showAction && action */}
{messageType.showAction && action && (
action.action_type === 'browser_use' ? (
<BrowserUseAction
name={action.name}
arguments={action.arguments}
toolOutput={action.tool_output}
/>
) : (
<ToolCallAction
name={action.name}
arguments={action.arguments}
action_type={action.action_type}
event={event as ApiEvent}
onClick={onToolCallClick}
/>
)
)}
{/* ContentMessage对应 showContentMessage */}
{showContentMessage && (
<ContentMessage
content={content}
userInteraction={userInteraction}
base={base}
isUserInput={messageType.isUserInput}
showUserFile={messageType.showUserFile || messageType.showTemplate}
onSendMessage={onSendMessage}
/>
)}
{/* FactCheck对应 messageType.isFactCheck */}
{messageType.isFactCheck && (
<FactCheck
content={content}
userInteraction={userInteraction}
base={base}
isUserInput={messageType.isUserInput}
showUserFile={messageType.showUserFile}
/>
)}
{/* UserMessage对应 messageType.showUserFile || messageType.showTemplate */}
{(messageType.showUserFile || messageType.showTemplate) && (
<UserMessage
base={base}
loading={base?.metadata?.isTemp}
attachment={attachment}
isUserInput={messageType.isUserInput}
showTemplate={messageType.showTemplate}
onAttachmentClick={onAttachmentClick}
onImageAttachmentClick={onImageAttachmentClick}
/>
)}
{/* SystemMessage */}
<SystemMessage
attachment={attachment}
imageAttachment={imageAttachment}
operation={operation}
mcpContent={mcpContent}
planConfig={planConfig}
taskTodoConfig={taskTodoConfig}
base={base}
showSystemAttachments={messageType.showSystemAttachments}
showOperation={messageType.showOperation}
showMcpContent={messageType.showMcpContent}
showPlanConfig={messageType.showPlanConfig}
showTaskTodoList={messageType.showTaskTodoList}
onAttachmentClick={onAttachmentClick}
onImageAttachmentClick={onImageAttachmentClick}
/>
{/* MessageFooter对应 <MessageFooter ... /> */}
<MessageFooter
shouldShowTimestamp={showTimestamp}
shouldShowCopyButton={showCopyButton}
showUserFile={messageType.showUserFile}
timestamp={timestamp}
isUserInput={messageType.isUserInput}
isSummary={messageType.isSummary}
showSystemAttachments={messageType.showSystemAttachments}
onCopyMessage={onCopyMessage ?? (() => {})}
/>
</div>
)
}
export const MessageItem = React.memo(InnerMessageItem)
export default MessageItem