feature: add agent avatar header for messages & improve @mention logging
parent
332464bd35
commit
b6d1c9e08d
|
|
@ -20,6 +20,7 @@ export interface ChatMessage {
|
|||
reasoning?: string | null;
|
||||
parentId?: string | null;
|
||||
createdAt: number;
|
||||
agent_id?: string;
|
||||
meta?: {
|
||||
retrieved?: RetrievedSnippet[];
|
||||
toolCalls?: ToolCallTrace[];
|
||||
|
|
|
|||
|
|
@ -158,6 +158,8 @@ export default function ChatPage() {
|
|||
<ChatBody
|
||||
bodyRef={bodyRef}
|
||||
agent={agent}
|
||||
agentList={agentList}
|
||||
currentAgentId={id!}
|
||||
messages={messages}
|
||||
branches={branches}
|
||||
highlightId={highlightId}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue