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; reasoning?: string | null;
parentId?: string | null; parentId?: string | null;
createdAt: number; createdAt: number;
agent_id?: string;
meta?: { meta?: {
retrieved?: RetrievedSnippet[]; retrieved?: RetrievedSnippet[];
toolCalls?: ToolCallTrace[]; toolCalls?: ToolCallTrace[];

View File

@ -158,6 +158,8 @@ export default function ChatPage() {
<ChatBody <ChatBody
bodyRef={bodyRef} bodyRef={bodyRef}
agent={agent} agent={agent}
agentList={agentList}
currentAgentId={id!}
messages={messages} messages={messages}
branches={branches} branches={branches}
highlightId={highlightId} 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 type { Agent, BranchInfo, ChatMessage } from '../../../api';
import Markdown from '../../../components/Markdown'; import Markdown from '../../../components/Markdown';
import type { StreamingState } from '../hooks/useChatSender'; import type { StreamingState } from '../hooks/useChatSender';
@ -11,6 +11,8 @@ const isImageUrl = (url: string) => url?.startsWith('http') || url?.startsWith('
export default function ChatBody(props: { export default function ChatBody(props: {
bodyRef: React.RefObject<HTMLDivElement>; bodyRef: React.RefObject<HTMLDivElement>;
agent: Agent; agent: Agent;
agentList: Agent[];
currentAgentId: string;
messages: ChatMessage[]; messages: ChatMessage[];
branches: Record<string, BranchInfo>; branches: Record<string, BranchInfo>;
highlightId: string | null; highlightId: string | null;
@ -20,7 +22,7 @@ export default function ChatBody(props: {
onSwitchBranch: (userMsgId: string, branchId: string) => void; onSwitchBranch: (userMsgId: string, branchId: string) => void;
onCopy: (text: string, mode: CopyMode) => 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 ( return (
<div ref={bodyRef} className="chat-body"> <div ref={bodyRef} className="chat-body">
@ -55,6 +57,8 @@ export default function ChatBody(props: {
<MessageItem <MessageItem
key={m.id} key={m.id}
message={m} message={m}
agentList={agentList}
currentAgentId={currentAgentId}
highlighted={highlightId === m.id} highlighted={highlightId === m.id}
branch={m.role === 'assistant' && m.parentId ? branches[m.parentId] : undefined} branch={m.role === 'assistant' && m.parentId ? branches[m.parentId] : undefined}
busy={sending} busy={sending}
@ -66,6 +70,31 @@ export default function ChatBody(props: {
{streaming.active && ( {streaming.active && (
<div style={{ marginBottom: 24 }}> <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 className="bubble assistant">
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{!!streaming.retryInfo?.message && ( {!!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 { BranchInfo, ChatMessage } from '../../../../api';
import type { Agent } from '../../../../api/agents';
import Markdown from '../../../../components/Markdown'; import Markdown from '../../../../components/Markdown';
import { ProductCopyIcon } from '../../../../components/icons'; import { ProductCopyIcon } from '../../../../components/icons';
import type { CopyMode } from '../../utils/copy'; import type { CopyMode } from '../../utils/copy';
@ -7,6 +8,8 @@ import { ReasoningView, RetrievedView, ToolCallView } from './MetaViews';
export default function MessageItem(props: { export default function MessageItem(props: {
message: ChatMessage; message: ChatMessage;
agentList: Agent[];
currentAgentId: string;
highlighted?: boolean; highlighted?: boolean;
branch?: BranchInfo; branch?: BranchInfo;
busy?: boolean; busy?: boolean;
@ -14,7 +17,12 @@ export default function MessageItem(props: {
onSwitchBranch?: (userMsgId: string, branchId: string) => void; onSwitchBranch?: (userMsgId: string, branchId: string) => void;
onCopy?: (text: string, mode: CopyMode) => 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 hasBranches = !!branch && branch.total > 1;
const activeIdx = branch?.activeIndex ?? 0; const activeIdx = branch?.activeIndex ?? 0;
const total = branch?.total ?? 1; const total = branch?.total ?? 1;
@ -42,6 +50,24 @@ export default function MessageItem(props: {
transition: 'background 0.4s, padding 0.4s' 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}`}> <div className={`bubble ${message.role}`}>
{message.role === 'assistant' ? ( {message.role === 'assistant' ? (
<Markdown>{message.content}</Markdown> <Markdown>{message.content}</Markdown>

View File

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