初始化模版工程
This commit is contained in:
225
components/nova-sdk/message-list/message-item/index.tsx
Normal file
225
components/nova-sdk/message-list/message-item/index.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
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
|
||||
Reference in New Issue
Block a user