AgentEvalTool/frontend/web/src/components/ChatBubble.tsx
sinohqb b26c432f3a refactor(frontend): reuse ChatBubble for exploration drill-down
The drawer hand-rolled chat bubbles duplicating ChatBubble's shape.
Add a mirrored layout prop (virtual user left, target right) and reuse
the shared component.
2026-08-04 02:50:26 +08:00

69 lines
2.3 KiB
TypeScript

import type { ReactNode } from 'react'
import { RobotOutlined, UserOutlined, WarningOutlined } from '@ant-design/icons'
import { colors } from '../tokens'
interface ChatBubbleProps {
role: 'user' | 'agent' | 'system'
content: ReactNode
meta?: ReactNode
pending?: boolean
/** 镜像布局:探索下钻里虚拟用户居左、被评对象居右(与常规聊天相反) */
mirrored?: boolean
}
export default function ChatBubble({ role, content, meta, pending, mirrored }: ChatBubbleProps) {
const isUser = role === 'user'
const isAgent = role === 'agent'
const isSystem = role === 'system'
const bg = isUser ? colors.chatUser : isAgent ? colors.chatAgent : colors.chatSystemError
const border = isSystem ? '#ffccc7' : 'transparent'
const textColor = isSystem ? '#cf1322' : colors.text
const alignRight = isSystem ? false : isUser ? !mirrored : !!mirrored
const align = alignRight ? 'flex-end' : 'flex-start'
const radius = alignRight ? '10px 10px 2px 10px' : '10px 10px 10px 2px'
const icon = isUser
? <UserOutlined style={{ color: colors.primary }} />
: isAgent
? <RobotOutlined style={{ color: '#52c41a' }} />
: <WarningOutlined style={{ color: '#cf1322' }} />
const label = isUser ? '用户' : isAgent ? '智能体' : '系统'
return (
<div style={{ display: 'flex', justifyContent: align, marginBottom: 6 }}>
<div style={{ maxWidth: '82%', display: 'flex', flexDirection: 'column', alignItems: alignRight ? 'flex-end' : 'flex-start', gap: 2 }}>
<div style={{ display: 'flex', gap: 4, alignItems: 'center', fontSize: 11, color: colors.textMuted }}>
{!isUser && icon}
<span>{label}</span>
{isUser && icon}
{meta}
</div>
<div
style={{
background: bg,
border: `1px solid ${border}`,
borderRadius: radius,
padding: '6px 10px',
fontSize: 13,
lineHeight: 1.55,
color: textColor,
wordBreak: 'break-word',
whiteSpace: 'pre-wrap',
}}
>
{pending ? <TypingDots /> : content}
</div>
</div>
</div>
)
}
function TypingDots() {
return (
<span className="typing-dots" aria-label="等待回复中">
<span /><span /><span />
</span>
)
}