feature: add agent avatar header for messages & improve @mention logging

main
sp mac bookpro 2605 2026-06-07 20:48:22 +08:00
parent 332464bd35
commit b6d1c9e08d
5 changed files with 91 additions and 12 deletions

View File

@ -20,6 +20,7 @@ export interface ChatMessage {
reasoning?: string | null;
parentId?: string | null;
createdAt: number;
agent_id?: string;
meta?: {
retrieved?: RetrievedSnippet[];
toolCalls?: ToolCallTrace[];

View File

@ -158,6 +158,8 @@ export default function ChatPage() {
<ChatBody
bodyRef={bodyRef}
agent={agent}
agentList={agentList}
currentAgentId={id!}
messages={messages}
branches={branches}
highlightId={highlightId}

View File

@ -1,4 +1,4 @@
import { Divider, Tag } from 'antd';
import { Divider, Tag, Avatar } from 'antd';
import type { Agent, BranchInfo, ChatMessage } from '../../../api';
import Markdown from '../../../components/Markdown';
import type { StreamingState } from '../hooks/useChatSender';
@ -11,6 +11,8 @@ const isImageUrl = (url: string) => url?.startsWith('http') || url?.startsWith('
export default function ChatBody(props: {
bodyRef: React.RefObject<HTMLDivElement>;
agent: Agent;
agentList: Agent[];
currentAgentId: string;
messages: ChatMessage[];
branches: Record<string, BranchInfo>;
highlightId: string | null;
@ -20,7 +22,7 @@ export default function ChatBody(props: {
onSwitchBranch: (userMsgId: string, branchId: string) => void;
onCopy: (text: string, mode: CopyMode) => void;
}) {
const { bodyRef, agent, messages, branches, highlightId, sending, streaming, onRegenerate, onSwitchBranch, onCopy } = props;
const { bodyRef, agent, agentList, currentAgentId, messages, branches, highlightId, sending, streaming, onRegenerate, onSwitchBranch, onCopy } = props;
return (
<div ref={bodyRef} className="chat-body">
@ -55,6 +57,8 @@ export default function ChatBody(props: {
<MessageItem
key={m.id}
message={m}
agentList={agentList}
currentAgentId={currentAgentId}
highlighted={highlightId === m.id}
branch={m.role === 'assistant' && m.parentId ? branches[m.parentId] : undefined}
busy={sending}
@ -66,6 +70,31 @@ export default function ChatBody(props: {
{streaming.active && (
<div style={{ marginBottom: 24 }}>
{(() => {
const streamingAgentId = streaming.targetAgentId || currentAgentId;
const streamingAgent = agentList.find(a => a.id === streamingAgentId);
if (streamingAgent) {
return (
<div className="message-agent-header" style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 6,
marginLeft: 2
}}>
<Avatar src={streamingAgent.avatar} size={24} />
<span style={{
fontSize: 13,
fontWeight: 500,
color: 'var(--color-text)'
}}>
{streamingAgent.name}
</span>
</div>
);
}
return null;
})()}
<div className="bubble assistant">
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{!!streaming.retryInfo?.message && (

View File

@ -1,5 +1,6 @@
import { Button, Dropdown, Space, Tag, Tooltip } from 'antd';
import { Button, Dropdown, Space, Tag, Tooltip, Avatar } from 'antd';
import type { BranchInfo, ChatMessage } from '../../../../api';
import type { Agent } from '../../../../api/agents';
import Markdown from '../../../../components/Markdown';
import { ProductCopyIcon } from '../../../../components/icons';
import type { CopyMode } from '../../utils/copy';
@ -7,6 +8,8 @@ import { ReasoningView, RetrievedView, ToolCallView } from './MetaViews';
export default function MessageItem(props: {
message: ChatMessage;
agentList: Agent[];
currentAgentId: string;
highlighted?: boolean;
branch?: BranchInfo;
busy?: boolean;
@ -14,7 +17,12 @@ export default function MessageItem(props: {
onSwitchBranch?: (userMsgId: string, branchId: string) => void;
onCopy?: (text: string, mode: CopyMode) => void;
}) {
const { message, highlighted, branch, busy, onRegenerate, onSwitchBranch, onCopy } = props;
const { message, agentList, currentAgentId, highlighted, branch, busy, onRegenerate, onSwitchBranch, onCopy } = props;
// 获取回答者 Agent 信息
const answerAgentId = message.agent_id || (message.role === 'assistant' ? currentAgentId : undefined);
const answerAgent = answerAgentId ? agentList.find(a => a.id === answerAgentId) : undefined;
const hasAnswerAgent = !!answerAgent && message.role === 'assistant';
const hasBranches = !!branch && branch.total > 1;
const activeIdx = branch?.activeIndex ?? 0;
const total = branch?.total ?? 1;
@ -42,6 +50,24 @@ export default function MessageItem(props: {
transition: 'background 0.4s, padding 0.4s'
}}
>
{hasAnswerAgent && (
<div className="message-agent-header" style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 6,
marginLeft: 2
}}>
<Avatar src={answerAgent!.avatar} size={24} />
<span style={{
fontSize: 13,
fontWeight: 500,
color: 'var(--color-text)'
}}>
{answerAgent!.name}
</span>
</div>
)}
<div className={`bubble ${message.role}`}>
{message.role === 'assistant' ? (
<Markdown>{message.content}</Markdown>

View File

@ -12,6 +12,7 @@ export interface StreamingState {
retryInfo: any | null;
retrieved: RetrievedSnippet[];
toolCalls: ToolCallTrace[];
targetAgentId?: string;
}
// 解析文本中的 @AgentName返回找到的目标 Agent
@ -30,7 +31,20 @@ export function parseMentionedAgent(text: string, agentList: Agent[]): Agent | n
const startMatch = firstLine.match(/^@([^\s]+)/);
const candidateName = startMatch ? startMatch[1] : lastMention;
if (!candidateName) return null;
console.log('[parseMentionedAgent]', {
text,
lastMention,
firstLine,
startMatch,
candidateName,
agentListCount: agentList.length,
agentList: agentList.map(a => ({ id: a.id, name: a.name }))
});
if (!candidateName) {
console.log('[parseMentionedAgent] no candidateName, return null');
return null;
}
// 模糊匹配(前缀匹配,不区分大小写)
const lowerCandidate = candidateName.toLowerCase();
@ -39,17 +53,21 @@ export function parseMentionedAgent(text: string, agentList: Agent[]): Agent | n
for (const a of agentList) {
const lowerName = a.name.toLowerCase();
console.log('[parseMentionedAgent] checking', { candidate: lowerCandidate, agentName: lowerName, id: a.id });
if (lowerName === lowerCandidate) {
console.log('[parseMentionedAgent] exact match found', a);
return a; // 精确匹配直接返回
}
if (lowerName.startsWith(lowerCandidate)) {
const score = candidateName.length / lowerName.length;
console.log('[parseMentionedAgent] prefix match', { score, agent: a });
if (score > bestScore) {
bestScore = score;
bestMatch = a;
}
} else if (lowerName.includes(lowerCandidate)) {
const score = candidateName.length / lowerName.length * 0.8; // 包含匹配权重稍低
console.log('[parseMentionedAgent] contains match', { score, agent: a });
if (score > bestScore) {
bestScore = score;
bestMatch = a;
@ -57,7 +75,9 @@ export function parseMentionedAgent(text: string, agentList: Agent[]): Agent | n
}
}
return bestScore > 0.3 ? bestMatch : null;
const result = bestScore > 0.3 ? bestMatch : null;
console.log('[parseMentionedAgent] final result', { bestScore, result });
return result;
}
export function useChatSender(args: {
@ -89,7 +109,8 @@ export function useChatSender(args: {
errorMessage: null,
retryInfo: null,
retrieved: [],
toolCalls: []
toolCalls: [],
targetAgentId: undefined
});
const [sessionRefresh, setSessionRefresh] = useState(0);
@ -103,19 +124,19 @@ export function useChatSender(args: {
notify.error('会话未初始化,请稍后重试');
return;
}
// 解析 @提及的目标 Agent
const targetAgent = parseMentionedAgent(text, agentList);
const targetAgentId = targetAgent?.id || agentId;
const tempUser: ChatMessage = { id: 'tmp-' + Date.now(), role: 'user', content: text, createdAt: Date.now() };
args.setMessages((m) => [...(m || []), tempUser]);
setStreaming({ active: true, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] });
setStreaming({ active: true, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [], targetAgentId });
scrollBottom(true);
abortRef.current?.abort();
const ctrl = new AbortController();
abortRef.current = ctrl;
// 解析 @提及的目标 Agent
const targetAgent = parseMentionedAgent(text, agentList);
const targetAgentId = targetAgent?.id || agentId;
// 如果提及了其他 Agent使用该 Agent 的第一个模型
let targetModel: string;
let targetModelId: string;