feature: add agent avatar header for messages & improve @mention logging
parent
332464bd35
commit
b6d1c9e08d
|
|
@ -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[];
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue