aura-web/src/pages/chat/components/messages/MessageItem.tsx

131 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { Button, Dropdown, Space, Tag, Tooltip, Avatar } from 'antd';
import { CopyOutlined, SyncOutlined } from '@ant-design/icons';
import type { BranchInfo, ChatMessage } from '../../../../api';
import type { Agent } from '../../../../api/agents';
import Markdown from '../../../../components/Markdown';
import type { CopyMode } from '../../utils/copy';
import { ReasoningView, RetrievedView, ToolCallView } from './MetaViews';
import './MessageItem.css';
export default function MessageItem(props: {
message: ChatMessage;
agentList: Agent[];
currentAgentId: string;
highlighted?: boolean;
branch?: BranchInfo;
busy?: boolean;
onRegenerate?: (id: string) => void;
onSwitchBranch?: (userMsgId: string, branchId: string) => void;
onCopy?: (text: string, mode: CopyMode) => void;
isMobile?: boolean;
}) {
const { message, agentList, currentAgentId, highlighted, branch, busy, onRegenerate, onSwitchBranch, onCopy, isMobile } = props;
const speakerType = (message as any)?.speaker?.type as ('user' | 'agent' | undefined);
const speakerId = (message as any)?.speaker?.id as string | undefined;
const isUser = speakerType ? speakerType === 'user' : message.role === 'user';
const bubbleRole = isUser ? 'user' : 'assistant';
// 获取回答者 Agent 信息
const answerAgentId = !isUser ? (speakerType === 'agent' ? speakerId : undefined) || message.agent_id || currentAgentId : undefined;
const answerAgent = answerAgentId ? agentList.find(a => a.id === answerAgentId) : undefined;
const hasBranches = !!branch && branch.total > 1;
const activeIdx = branch?.activeIndex ?? 0;
const total = branch?.total ?? 1;
const goPrev = () => {
if (!branch || !message.parentId) return;
const i = Math.max(0, activeIdx - 1);
onSwitchBranch?.(message.parentId, branch.ids[i]);
};
const goNext = () => {
if (!branch || !message.parentId) return;
const i = Math.min(total - 1, activeIdx + 1);
onSwitchBranch?.(message.parentId, branch.ids[i]);
};
return (
<div
id={'msg-' + message.id}
className={`message-item-container ${highlighted ? 'highlighted' : ''}`}
>
{!isUser ? (
<div className="message-item-assistant">
<Avatar
src={answerAgent?.avatar}
size={36}
className="message-item-assistant-avatar"
>
{answerAgent?.name?.charAt(0)?.toUpperCase() || 'A'}
</Avatar>
<div className="message-item-assistant-content">
<div className="message-item-assistant-header">
<span className="message-item-assistant-name">
{answerAgent?.name || 'AI'}
</span>
</div>
<div className={`bubble ${bubbleRole}`}>
<Markdown>{message.content}</Markdown>
<div className="message-item-actions">
{hasBranches && (
<Space size={2}>
<Button size="small" type="text" disabled={activeIdx === 0} onClick={goPrev}>
</Button>
<span>
{activeIdx + 1} / {total}
</span>
<Button size="small" type="text" disabled={activeIdx === total - 1} onClick={goNext}>
</Button>
</Space>
)}
{message.meta?.aborted && <Tag color="orange"></Tag>}
<Dropdown
trigger={['click']}
menu={{
items: [
{ key: 'plain', label: '复制纯文本', onClick: () => onCopy?.(message.content, 'plain') },
{ key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(message.content, 'markdown') }
]
}}
>
<Tooltip title="复制">
<Button size="small" className='actions-btn' type="text" icon={<CopyOutlined />} />
</Tooltip>
</Dropdown>
<Tooltip title="重新生成">
<Button size="small" className='actions-btn' type="text" icon={<SyncOutlined />} disabled={busy} onClick={() => onRegenerate?.(message.id)} />
</Tooltip>
</div>
</div>
{!isMobile && message.meta && (
<div>
{!!message.meta.reasoning && <ReasoningView reasoning={message.meta.reasoning} />}
{!!message.meta.retrieved?.length && <RetrievedView retrieved={message.meta.retrieved} />}
{!!message.meta.toolCalls?.length && <ToolCallView calls={message.meta.toolCalls} />}
</div>
)}
</div>
</div>
) : (
<div className="message-item-user">
<div className="message-item-user-content-wrapper">
<div className={`bubble ${bubbleRole}`}>
{message.content.includes('![image](') ? (
<Markdown>{message.content}</Markdown>
) : (
<span dangerouslySetInnerHTML={{
__html: message.content.replace(/@([^\s]+)/g, '<span class="mention">@$1</span>')
}} />
)}
</div>
</div>
<Avatar className="message-item-user-avatar" size={36}></Avatar>
</div>
)}
</div>
);
}