style: improve code formatting and consistency in AgentAudit.tsx
This commit is contained in:
parent
b392e049e8
commit
e13218a33e
|
|
@ -49,21 +49,21 @@ interface LogItem {
|
||||||
*/
|
*/
|
||||||
function buildAgentTree(flatNodes: AgentTreeNode[]): AgentTreeNode[] {
|
function buildAgentTree(flatNodes: AgentTreeNode[]): AgentTreeNode[] {
|
||||||
if (!flatNodes || flatNodes.length === 0) return [];
|
if (!flatNodes || flatNodes.length === 0) return [];
|
||||||
|
|
||||||
// 创建节点映射(使用 agent_id 作为 key)
|
// 创建节点映射(使用 agent_id 作为 key)
|
||||||
const nodeMap = new Map<string, AgentTreeNode>();
|
const nodeMap = new Map<string, AgentTreeNode>();
|
||||||
|
|
||||||
// 首先克隆所有节点并重置 children
|
// 首先克隆所有节点并重置 children
|
||||||
flatNodes.forEach(node => {
|
flatNodes.forEach(node => {
|
||||||
nodeMap.set(node.agent_id, { ...node, children: [] });
|
nodeMap.set(node.agent_id, { ...node, children: [] });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 构建树结构
|
// 构建树结构
|
||||||
const rootNodes: AgentTreeNode[] = [];
|
const rootNodes: AgentTreeNode[] = [];
|
||||||
|
|
||||||
flatNodes.forEach(node => {
|
flatNodes.forEach(node => {
|
||||||
const currentNode = nodeMap.get(node.agent_id)!;
|
const currentNode = nodeMap.get(node.agent_id)!;
|
||||||
|
|
||||||
if (node.parent_agent_id && nodeMap.has(node.parent_agent_id)) {
|
if (node.parent_agent_id && nodeMap.has(node.parent_agent_id)) {
|
||||||
// 有父节点,添加到父节点的 children
|
// 有父节点,添加到父节点的 children
|
||||||
const parentNode = nodeMap.get(node.parent_agent_id)!;
|
const parentNode = nodeMap.get(node.parent_agent_id)!;
|
||||||
|
|
@ -73,7 +73,7 @@ function buildAgentTree(flatNodes: AgentTreeNode[]): AgentTreeNode[] {
|
||||||
rootNodes.push(currentNode);
|
rootNodes.push(currentNode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return rootNodes;
|
return rootNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,7 +118,7 @@ function SplashScreen({ onComplete }: { onComplete: () => void }) {
|
||||||
<div className="h-screen bg-[#0a0a0f] flex items-center justify-center">
|
<div className="h-screen bg-[#0a0a0f] flex items-center justify-center">
|
||||||
<div className="text-center space-y-6">
|
<div className="text-center space-y-6">
|
||||||
<pre className="text-primary font-mono text-xs sm:text-sm leading-tight select-none">
|
<pre className="text-primary font-mono text-xs sm:text-sm leading-tight select-none">
|
||||||
{`
|
{`
|
||||||
██████╗ ███████╗███████╗██████╗ █████╗ ██╗ ██╗██████╗ ██╗████████╗
|
██████╗ ███████╗███████╗██████╗ █████╗ ██╗ ██╗██████╗ ██╗████████╗
|
||||||
██╔══██╗██╔════╝██╔════╝██╔══██╗██╔══██╗██║ ██║██╔══██╗██║╚══██╔══╝
|
██╔══██╗██╔════╝██╔════╝██╔══██╗██╔══██╗██║ ██║██╔══██╗██║╚══██╔══╝
|
||||||
██║ ██║█████╗ █████╗ ██████╔╝███████║██║ ██║██║ ██║██║ ██║
|
██║ ██║█████╗ █████╗ ██████╔╝███████║██║ ██║██║ ██║██║ ██║
|
||||||
|
|
@ -145,43 +145,43 @@ function SplashScreen({ onComplete }: { onComplete: () => void }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Agent 树节点 - 增强版
|
// Agent 树节点 - 增强版
|
||||||
function AgentTreeNodeItem({
|
function AgentTreeNodeItem({
|
||||||
node,
|
node,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
selectedId,
|
selectedId,
|
||||||
onSelect
|
onSelect
|
||||||
}: {
|
}: {
|
||||||
node: AgentTreeNode;
|
node: AgentTreeNode;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
selectedId: string | null;
|
selectedId: string | null;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(true);
|
||||||
const hasChildren = node.children && node.children.length > 0;
|
const hasChildren = node.children && node.children.length > 0;
|
||||||
const isSelected = selectedId === node.agent_id;
|
const isSelected = selectedId === node.agent_id;
|
||||||
|
|
||||||
// 状态图标和颜色
|
// 状态图标和颜色
|
||||||
const statusConfig: Record<string, { icon: React.ReactNode; color: string; animate?: boolean }> = {
|
const statusConfig: Record<string, { icon: React.ReactNode; color: string; animate?: boolean }> = {
|
||||||
running: {
|
running: {
|
||||||
icon: <div className="w-2 h-2 rounded-full bg-green-400" />,
|
icon: <div className="w-2 h-2 rounded-full bg-green-400" />,
|
||||||
color: "text-green-400",
|
color: "text-green-400",
|
||||||
animate: true
|
animate: true
|
||||||
},
|
},
|
||||||
completed: {
|
completed: {
|
||||||
icon: <CheckCircle2 className="w-3 h-3" />,
|
icon: <CheckCircle2 className="w-3 h-3" />,
|
||||||
color: "text-green-400"
|
color: "text-green-400"
|
||||||
},
|
},
|
||||||
failed: {
|
failed: {
|
||||||
icon: <XCircle className="w-3 h-3" />,
|
icon: <XCircle className="w-3 h-3" />,
|
||||||
color: "text-red-400"
|
color: "text-red-400"
|
||||||
},
|
},
|
||||||
waiting: {
|
waiting: {
|
||||||
icon: <Clock className="w-3 h-3" />,
|
icon: <Clock className="w-3 h-3" />,
|
||||||
color: "text-yellow-400"
|
color: "text-yellow-400"
|
||||||
},
|
},
|
||||||
created: {
|
created: {
|
||||||
icon: <div className="w-2 h-2 rounded-full bg-gray-500" />,
|
icon: <div className="w-2 h-2 rounded-full bg-gray-500" />,
|
||||||
color: "text-gray-400"
|
color: "text-gray-400"
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -207,30 +207,30 @@ function AgentTreeNodeItem({
|
||||||
onClick={() => onSelect(node.agent_id)}
|
onClick={() => onSelect(node.agent_id)}
|
||||||
>
|
>
|
||||||
{hasChildren ? (
|
{hasChildren ? (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded); }}
|
onClick={(e) => { e.stopPropagation(); setExpanded(!expanded); }}
|
||||||
className="hover:bg-white/10 rounded p-0.5"
|
className="hover:bg-white/10 rounded p-0.5"
|
||||||
>
|
>
|
||||||
{expanded ?
|
{expanded ?
|
||||||
<ChevronDown className="w-3 h-3 text-gray-500" /> :
|
<ChevronDown className="w-3 h-3 text-gray-500" /> :
|
||||||
<ChevronRight className="w-3 h-3 text-gray-500" />
|
<ChevronRight className="w-3 h-3 text-gray-500" />
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
) : <span className="w-4" />}
|
) : <span className="w-4" />}
|
||||||
|
|
||||||
{/* 状态指示器 */}
|
{/* 状态指示器 */}
|
||||||
<span className={`${config.color} ${config.animate ? 'animate-pulse' : ''}`}>
|
<span className={`${config.color} ${config.animate ? 'animate-pulse' : ''}`}>
|
||||||
{config.icon}
|
{config.icon}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Agent 类型图标 */}
|
{/* Agent 类型图标 */}
|
||||||
{typeIcons[node.agent_type] || <Bot className="w-3 h-3 text-gray-400" />}
|
{typeIcons[node.agent_type] || <Bot className="w-3 h-3 text-gray-400" />}
|
||||||
|
|
||||||
{/* Agent 名称 */}
|
{/* Agent 名称 */}
|
||||||
<span className={`text-xs font-mono truncate flex-1 ${isSelected ? 'text-white font-semibold' : 'text-gray-300'}`}>
|
<span className={`text-xs font-mono truncate flex-1 ${isSelected ? 'text-white font-semibold' : 'text-gray-300'}`}>
|
||||||
{node.agent_name}
|
{node.agent_name}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* 发现数量 */}
|
{/* 发现数量 */}
|
||||||
{node.findings_count > 0 && (
|
{node.findings_count > 0 && (
|
||||||
<Badge className="h-4 px-1 text-[10px] bg-red-500/20 text-red-400 border-0">
|
<Badge className="h-4 px-1 text-[10px] bg-red-500/20 text-red-400 border-0">
|
||||||
|
|
@ -238,13 +238,13 @@ function AgentTreeNodeItem({
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{expanded && hasChildren && (
|
{expanded && hasChildren && (
|
||||||
<div className="border-l border-gray-800 ml-4">
|
<div className="border-l border-gray-800 ml-4">
|
||||||
{node.children.map(child => (
|
{node.children.map(child => (
|
||||||
<AgentTreeNodeItem
|
<AgentTreeNodeItem
|
||||||
key={child.agent_id}
|
key={child.agent_id}
|
||||||
node={child}
|
node={child}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
selectedId={selectedId}
|
selectedId={selectedId}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
|
|
@ -262,48 +262,48 @@ function LogEntry({ item, isExpanded, onToggle }: {
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
}) {
|
}) {
|
||||||
const config: Record<string, {
|
const config: Record<string, {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
borderColor: string;
|
borderColor: string;
|
||||||
bgColor: string;
|
bgColor: string;
|
||||||
}> = {
|
}> = {
|
||||||
thinking: {
|
thinking: {
|
||||||
icon: <Brain className="w-4 h-4 text-purple-400" />,
|
icon: <Brain className="w-4 h-4 text-purple-400" />,
|
||||||
borderColor: "border-l-purple-500",
|
borderColor: "border-l-purple-500",
|
||||||
bgColor: "bg-purple-950/20"
|
bgColor: "bg-purple-950/20"
|
||||||
},
|
},
|
||||||
tool: {
|
tool: {
|
||||||
icon: <Wrench className="w-4 h-4 text-amber-400" />,
|
icon: <Wrench className="w-4 h-4 text-amber-400" />,
|
||||||
borderColor: "border-l-amber-500",
|
borderColor: "border-l-amber-500",
|
||||||
bgColor: "bg-amber-950/20"
|
bgColor: "bg-amber-950/20"
|
||||||
},
|
},
|
||||||
phase: {
|
phase: {
|
||||||
icon: <Target className="w-4 h-4 text-cyan-400" />,
|
icon: <Target className="w-4 h-4 text-cyan-400" />,
|
||||||
borderColor: "border-l-cyan-500",
|
borderColor: "border-l-cyan-500",
|
||||||
bgColor: "bg-cyan-950/20"
|
bgColor: "bg-cyan-950/20"
|
||||||
},
|
},
|
||||||
finding: {
|
finding: {
|
||||||
icon: <Bug className="w-4 h-4 text-red-400" />,
|
icon: <Bug className="w-4 h-4 text-red-400" />,
|
||||||
borderColor: "border-l-red-500",
|
borderColor: "border-l-red-500",
|
||||||
bgColor: "bg-red-950/20"
|
bgColor: "bg-red-950/20"
|
||||||
},
|
},
|
||||||
dispatch: {
|
dispatch: {
|
||||||
icon: <Zap className="w-4 h-4 text-blue-400" />,
|
icon: <Zap className="w-4 h-4 text-blue-400" />,
|
||||||
borderColor: "border-l-blue-500",
|
borderColor: "border-l-blue-500",
|
||||||
bgColor: "bg-blue-950/20"
|
bgColor: "bg-blue-950/20"
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
icon: <Terminal className="w-4 h-4 text-gray-400" />,
|
icon: <Terminal className="w-4 h-4 text-gray-400" />,
|
||||||
borderColor: "border-l-gray-600",
|
borderColor: "border-l-gray-600",
|
||||||
bgColor: "bg-gray-900/30"
|
bgColor: "bg-gray-900/30"
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
icon: <AlertTriangle className="w-4 h-4 text-red-500" />,
|
icon: <AlertTriangle className="w-4 h-4 text-red-500" />,
|
||||||
borderColor: "border-l-red-600",
|
borderColor: "border-l-red-600",
|
||||||
bgColor: "bg-red-950/30"
|
bgColor: "bg-red-950/30"
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
icon: <Shield className="w-4 h-4 text-blue-400" />,
|
icon: <Shield className="w-4 h-4 text-blue-400" />,
|
||||||
borderColor: "border-l-blue-500",
|
borderColor: "border-l-blue-500",
|
||||||
bgColor: "bg-blue-950/20"
|
bgColor: "bg-blue-950/20"
|
||||||
},
|
},
|
||||||
|
|
@ -327,26 +327,26 @@ function LogEntry({ item, isExpanded, onToggle }: {
|
||||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
{c.icon}
|
{c.icon}
|
||||||
<span className="text-xs text-gray-500 font-mono flex-shrink-0">{item.time}</span>
|
<span className="text-xs text-gray-500 font-mono flex-shrink-0">{item.time}</span>
|
||||||
|
|
||||||
{!isThinking && (
|
{!isThinking && (
|
||||||
<span className="text-sm text-gray-200 truncate">{item.title}</span>
|
<span className="text-sm text-gray-200 truncate">{item.title}</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.isStreaming && (
|
{item.isStreaming && (
|
||||||
<span className="w-2 h-4 bg-purple-400 animate-pulse rounded-sm" />
|
<span className="w-2 h-4 bg-purple-400 animate-pulse rounded-sm" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.tool?.status === 'running' && (
|
{item.tool?.status === 'running' && (
|
||||||
<Loader2 className="w-3 h-3 animate-spin text-amber-400 flex-shrink-0" />
|
<Loader2 className="w-3 h-3 animate-spin text-amber-400 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.agentName && (
|
{item.agentName && (
|
||||||
<Badge variant="outline" className="h-5 px-1.5 text-[10px] uppercase tracking-wider border-gray-700 text-gray-400 flex-shrink-0">
|
<Badge variant="outline" className="h-5 px-1.5 text-[10px] uppercase tracking-wider border-gray-700 text-gray-400 flex-shrink-0">
|
||||||
{item.agentName}
|
{item.agentName}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{item.tool?.duration !== undefined && (
|
{item.tool?.duration !== undefined && (
|
||||||
<span className="text-xs text-gray-500 font-mono">{item.tool.duration}ms</span>
|
<span className="text-xs text-gray-500 font-mono">{item.tool.duration}ms</span>
|
||||||
|
|
@ -357,13 +357,13 @@ function LogEntry({ item, isExpanded, onToggle }: {
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{isCollapsible && (
|
{isCollapsible && (
|
||||||
isExpanded ?
|
isExpanded ?
|
||||||
<ChevronUp className="w-4 h-4 text-gray-500" /> :
|
<ChevronUp className="w-4 h-4 text-gray-500" /> :
|
||||||
<ChevronDown className="w-4 h-4 text-gray-500" />
|
<ChevronDown className="w-4 h-4 text-gray-500" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showContent && item.content && (
|
{showContent && item.content && (
|
||||||
<div className={`
|
<div className={`
|
||||||
mt-2 text-sm whitespace-pre-wrap break-words
|
mt-2 text-sm whitespace-pre-wrap break-words
|
||||||
|
|
@ -377,12 +377,12 @@ function LogEntry({ item, isExpanded, onToggle }: {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选中 Agent 详情面板
|
// 选中 Agent 详情面板
|
||||||
function AgentDetailPanel({
|
function AgentDetailPanel({
|
||||||
agentId,
|
agentId,
|
||||||
treeNodes,
|
treeNodes,
|
||||||
onClose
|
onClose
|
||||||
}: {
|
}: {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
treeNodes: AgentTreeNode[];
|
treeNodes: AgentTreeNode[];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -424,14 +424,14 @@ function AgentDetailPanel({
|
||||||
{typeInfo.icon}
|
{typeInfo.icon}
|
||||||
<span className="text-sm font-bold text-white">{agent.agent_name}</span>
|
<span className="text-sm font-bold text-white">{agent.agent_name}</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="text-gray-500 hover:text-white text-xs"
|
className="text-gray-500 hover:text-white text-xs"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-gray-500">Type</span>
|
<span className="text-gray-500">Type</span>
|
||||||
|
|
@ -480,7 +480,7 @@ function AgentDetailPanel({
|
||||||
// 实时统计面板 - 增强版
|
// 实时统计面板 - 增强版
|
||||||
function StatsPanel({ task, findings }: { task: AgentTask | null; findings: AgentFinding[] }) {
|
function StatsPanel({ task, findings }: { task: AgentTask | null; findings: AgentFinding[] }) {
|
||||||
if (!task) return null;
|
if (!task) return null;
|
||||||
|
|
||||||
const severityCounts = {
|
const severityCounts = {
|
||||||
critical: findings.filter(f => f.severity === 'critical').length,
|
critical: findings.filter(f => f.severity === 'critical').length,
|
||||||
high: findings.filter(f => f.severity === 'high').length,
|
high: findings.filter(f => f.severity === 'high').length,
|
||||||
|
|
@ -496,7 +496,7 @@ function StatsPanel({ task, findings }: { task: AgentTask | null; findings: Agen
|
||||||
<Activity className="w-4 h-4 text-primary" />
|
<Activity className="w-4 h-4 text-primary" />
|
||||||
<span className="font-bold uppercase tracking-wider">Live Stats</span>
|
<span className="font-bold uppercase tracking-wider">Live Stats</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 进度条 */}
|
{/* 进度条 */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
|
|
@ -504,13 +504,13 @@ function StatsPanel({ task, findings }: { task: AgentTask | null; findings: Agen
|
||||||
<span className="text-white font-mono">{task.progress_percentage?.toFixed(0) || 0}%</span>
|
<span className="text-white font-mono">{task.progress_percentage?.toFixed(0) || 0}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
<div className="h-1.5 bg-gray-800 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-primary transition-all duration-500 rounded-full"
|
className="h-full bg-primary transition-all duration-500 rounded-full"
|
||||||
style={{ width: `${task.progress_percentage || 0}%` }}
|
style={{ width: `${task.progress_percentage || 0}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 统计数据 */}
|
{/* 统计数据 */}
|
||||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
|
@ -568,11 +568,10 @@ function StatsPanel({ task, findings }: { task: AgentTask | null; findings: Agen
|
||||||
<div className="pt-2 border-t border-gray-800">
|
<div className="pt-2 border-t border-gray-800">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-xs text-gray-500">Security Score</span>
|
<span className="text-xs text-gray-500">Security Score</span>
|
||||||
<span className={`text-lg font-bold font-mono ${
|
<span className={`text-lg font-bold font-mono ${task.security_score >= 80 ? 'text-green-400' :
|
||||||
task.security_score >= 80 ? 'text-green-400' :
|
|
||||||
task.security_score >= 60 ? 'text-yellow-400' :
|
task.security_score >= 60 ? 'text-yellow-400' :
|
||||||
'text-red-400'
|
'text-red-400'
|
||||||
}`}>
|
}`}>
|
||||||
{task.security_score.toFixed(0)}
|
{task.security_score.toFixed(0)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -627,7 +626,8 @@ export default function AgentAuditPage() {
|
||||||
const currentThinkingId = useRef<string | null>(null);
|
const currentThinkingId = useRef<string | null>(null);
|
||||||
const currentAgentName = useRef<string | null>(null);
|
const currentAgentName = useRef<string | null>(null);
|
||||||
|
|
||||||
const isRunning = task?.status === "running";
|
// 任务是否可取消(包括 pending 和 running 状态)
|
||||||
|
const isRunning = task?.status === "running" || task?.status === "pending";
|
||||||
const isComplete = task?.status === "completed" || task?.status === "failed" || task?.status === "cancelled";
|
const isComplete = task?.status === "completed" || task?.status === "failed" || task?.status === "cancelled";
|
||||||
|
|
||||||
// 构建 Agent 树结构(将扁平列表转换为树)
|
// 构建 Agent 树结构(将扁平列表转换为树)
|
||||||
|
|
@ -653,8 +653,8 @@ export default function AgentAuditPage() {
|
||||||
};
|
};
|
||||||
const selectedAgentName = findAgentName(treeNodes, selectedAgentId);
|
const selectedAgentName = findAgentName(treeNodes, selectedAgentId);
|
||||||
if (!selectedAgentName) return logs;
|
if (!selectedAgentName) return logs;
|
||||||
|
|
||||||
return logs.filter(log =>
|
return logs.filter(log =>
|
||||||
log.agentName?.toLowerCase() === selectedAgentName.toLowerCase() ||
|
log.agentName?.toLowerCase() === selectedAgentName.toLowerCase() ||
|
||||||
log.agentName?.toLowerCase().includes(selectedAgentName.toLowerCase().split('_')[0])
|
log.agentName?.toLowerCase().includes(selectedAgentName.toLowerCase().split('_')[0])
|
||||||
);
|
);
|
||||||
|
|
@ -727,6 +727,36 @@ export default function AgentAuditPage() {
|
||||||
}
|
}
|
||||||
}, [taskId]);
|
}, [taskId]);
|
||||||
|
|
||||||
|
// 🔥 Agent 树刷新防抖 ref
|
||||||
|
const agentTreeRefreshTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const lastAgentTreeRefreshTime = useRef<number>(0);
|
||||||
|
|
||||||
|
// 🔥 防抖刷新 Agent 树
|
||||||
|
const debouncedLoadAgentTree = useCallback(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const minInterval = 500; // 最小刷新间隔 500ms
|
||||||
|
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (agentTreeRefreshTimer.current) {
|
||||||
|
clearTimeout(agentTreeRefreshTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果距离上次刷新不到 minInterval,延迟刷新
|
||||||
|
const timeSinceLastRefresh = now - lastAgentTreeRefreshTime.current;
|
||||||
|
if (timeSinceLastRefresh < minInterval) {
|
||||||
|
agentTreeRefreshTimer.current = setTimeout(() => {
|
||||||
|
lastAgentTreeRefreshTime.current = Date.now();
|
||||||
|
loadAgentTree();
|
||||||
|
}, minInterval - timeSinceLastRefresh);
|
||||||
|
} else {
|
||||||
|
// 立即刷新,但添加一个小延迟确保后端数据已更新
|
||||||
|
agentTreeRefreshTimer.current = setTimeout(() => {
|
||||||
|
lastAgentTreeRefreshTime.current = Date.now();
|
||||||
|
loadAgentTree();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [loadAgentTree]);
|
||||||
|
|
||||||
// 流式事件处理
|
// 流式事件处理
|
||||||
const streamOptions = useMemo(() => ({
|
const streamOptions = useMemo(() => ({
|
||||||
includeThinking: true,
|
includeThinking: true,
|
||||||
|
|
@ -736,16 +766,19 @@ export default function AgentAuditPage() {
|
||||||
if (event.metadata?.agent_name) {
|
if (event.metadata?.agent_name) {
|
||||||
currentAgentName.current = event.metadata.agent_name;
|
currentAgentName.current = event.metadata.agent_name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理 dispatch 事件
|
// 🔥 处理 dispatch 相关事件 - 增加更多事件类型
|
||||||
if (event.type === 'dispatch' || event.type === 'dispatch_complete') {
|
const dispatchEvents = ['dispatch', 'dispatch_complete', 'node_start', 'phase_start'];
|
||||||
addLog({
|
if (dispatchEvents.includes(event.type)) {
|
||||||
type: 'dispatch',
|
if (event.type === 'dispatch' || event.type === 'dispatch_complete') {
|
||||||
title: event.message || `Agent dispatch: ${event.metadata?.agent || 'unknown'}`,
|
addLog({
|
||||||
agentName: currentAgentName.current || undefined,
|
type: 'dispatch',
|
||||||
});
|
title: event.message || `Agent dispatch: ${event.metadata?.agent || 'unknown'}`,
|
||||||
// 🔥 刷新 Agent 树,显示新创建的子 Agent
|
agentName: currentAgentName.current || undefined,
|
||||||
loadAgentTree();
|
});
|
||||||
|
}
|
||||||
|
// 🔥 使用防抖刷新 Agent 树,显示新创建的子 Agent
|
||||||
|
debouncedLoadAgentTree();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onThinkingStart: () => {
|
onThinkingStart: () => {
|
||||||
|
|
@ -789,12 +822,12 @@ export default function AgentAuditPage() {
|
||||||
if (currentThinkingId.current) {
|
if (currentThinkingId.current) {
|
||||||
setLogs(prev => prev.map(log =>
|
setLogs(prev => prev.map(log =>
|
||||||
log.id === currentThinkingId.current
|
log.id === currentThinkingId.current
|
||||||
? {
|
? {
|
||||||
...log,
|
...log,
|
||||||
title: cleanResponse.slice(0, 100) + (cleanResponse.length > 100 ? '...' : ''),
|
title: cleanResponse.slice(0, 100) + (cleanResponse.length > 100 ? '...' : ''),
|
||||||
content: cleanResponse,
|
content: cleanResponse,
|
||||||
isStreaming: false
|
isStreaming: false
|
||||||
}
|
}
|
||||||
: log
|
: log
|
||||||
));
|
));
|
||||||
currentThinkingId.current = null;
|
currentThinkingId.current = null;
|
||||||
|
|
@ -819,9 +852,9 @@ export default function AgentAuditPage() {
|
||||||
setLogs(prev => {
|
setLogs(prev => {
|
||||||
let idx = -1;
|
let idx = -1;
|
||||||
for (let i = prev.length - 1; i >= 0; i--) {
|
for (let i = prev.length - 1; i >= 0; i--) {
|
||||||
if (prev[i].type === 'tool' && prev[i].tool?.name === name && prev[i].tool?.status === 'running') {
|
if (prev[i].type === 'tool' && prev[i].tool?.name === name && prev[i].tool?.status === 'running') {
|
||||||
idx = i;
|
idx = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
|
|
@ -859,7 +892,7 @@ export default function AgentAuditPage() {
|
||||||
onError: (err: string) => {
|
onError: (err: string) => {
|
||||||
addLog({ type: 'error', title: `Error: ${err}` });
|
addLog({ type: 'error', title: `Error: ${err}` });
|
||||||
},
|
},
|
||||||
}), [addLog, loadTask, loadFindings, loadAgentTree]);
|
}), [addLog, loadTask, loadFindings, loadAgentTree, debouncedLoadAgentTree]);
|
||||||
|
|
||||||
const { connect: connectStream, disconnect: disconnectStream, isConnected } = useAgentStream(taskId || null, streamOptions);
|
const { connect: connectStream, disconnect: disconnectStream, isConnected } = useAgentStream(taskId || null, streamOptions);
|
||||||
|
|
||||||
|
|
@ -871,7 +904,7 @@ export default function AgentAuditPage() {
|
||||||
}
|
}
|
||||||
setShowSplash(false);
|
setShowSplash(false);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
Promise.all([loadTask(), loadFindings(), loadAgentTree()])
|
Promise.all([loadTask(), loadFindings(), loadAgentTree()])
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, [taskId, loadTask, loadFindings, loadAgentTree]);
|
}, [taskId, loadTask, loadFindings, loadAgentTree]);
|
||||||
|
|
@ -885,10 +918,10 @@ export default function AgentAuditPage() {
|
||||||
return () => disconnectStream();
|
return () => disconnectStream();
|
||||||
}, [taskId, task?.status, connectStream, disconnectStream, addLog]);
|
}, [taskId, task?.status, connectStream, disconnectStream, addLog]);
|
||||||
|
|
||||||
// 定期刷新 Agent 树
|
// 定期刷新 Agent 树 - 每 2 秒刷新一次
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!taskId || !isRunning) return;
|
if (!taskId || !isRunning) return;
|
||||||
const interval = setInterval(loadAgentTree, 3000);
|
const interval = setInterval(loadAgentTree, 2000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [taskId, isRunning, loadAgentTree]);
|
}, [taskId, isRunning, loadAgentTree]);
|
||||||
|
|
||||||
|
|
@ -906,15 +939,30 @@ export default function AgentAuditPage() {
|
||||||
}
|
}
|
||||||
}, [logs, isAutoScroll]);
|
}, [logs, isAutoScroll]);
|
||||||
|
|
||||||
|
// 取消状态
|
||||||
|
const [isCancelling, setIsCancelling] = useState(false);
|
||||||
|
|
||||||
// 取消任务
|
// 取消任务
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
if (!taskId) return;
|
if (!taskId || isCancelling) return;
|
||||||
|
|
||||||
|
setIsCancelling(true);
|
||||||
|
addLog({ type: 'info', title: '🛑 Requesting task cancellation...' });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cancelAgentTask(taskId);
|
await cancelAgentTask(taskId);
|
||||||
toast.success("Task cancelled");
|
toast.success("Task cancellation requested");
|
||||||
loadTask();
|
addLog({ type: 'info', title: '🛑 Task cancellation confirmed' });
|
||||||
} catch {
|
// 立即刷新任务状态
|
||||||
toast.error("Failed to cancel task");
|
await loadTask();
|
||||||
|
// 断开流连接
|
||||||
|
disconnectStream();
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
toast.error(`Failed to cancel task: ${errorMessage}`);
|
||||||
|
addLog({ type: 'error', title: `Failed to cancel: ${errorMessage}` });
|
||||||
|
} finally {
|
||||||
|
setIsCancelling(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -978,10 +1026,15 @@ export default function AgentAuditPage() {
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
className="text-red-400 hover:text-red-300 hover:bg-red-950/30"
|
disabled={isCancelling}
|
||||||
|
className="text-red-400 hover:text-red-300 hover:bg-red-950/30 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<Square className="w-4 h-4 mr-1" />
|
{isCancelling ? (
|
||||||
Stop
|
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Square className="w-4 h-4 mr-1" />
|
||||||
|
)}
|
||||||
|
{isCancelling ? 'Stopping...' : 'Stop'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -1018,9 +1071,8 @@ export default function AgentAuditPage() {
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsAutoScroll(!isAutoScroll)}
|
onClick={() => setIsAutoScroll(!isAutoScroll)}
|
||||||
className={`text-xs px-2 py-1 rounded transition-colors ${
|
className={`text-xs px-2 py-1 rounded transition-colors ${isAutoScroll ? 'bg-primary/20 text-primary' : 'text-gray-500 hover:text-gray-300'
|
||||||
isAutoScroll ? 'bg-primary/20 text-primary' : 'text-gray-500 hover:text-gray-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
Auto-scroll {isAutoScroll ? 'ON' : 'OFF'}
|
Auto-scroll {isAutoScroll ? 'ON' : 'OFF'}
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1047,7 +1099,7 @@ export default function AgentAuditPage() {
|
||||||
{isRunning ? (
|
{isRunning ? (
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
{selectedAgentId && !showAllLogs
|
{selectedAgentId && !showAllLogs
|
||||||
? 'Waiting for activity from selected agent...'
|
? 'Waiting for activity from selected agent...'
|
||||||
: 'Waiting for agent activity...'}
|
: 'Waiting for agent activity...'}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -1152,8 +1204,8 @@ export default function AgentAuditPage() {
|
||||||
<div className="flex-shrink-0 p-3 space-y-3">
|
<div className="flex-shrink-0 p-3 space-y-3">
|
||||||
{/* 选中 Agent 详情 */}
|
{/* 选中 Agent 详情 */}
|
||||||
{selectedAgentId && !showAllLogs && (
|
{selectedAgentId && !showAllLogs && (
|
||||||
<AgentDetailPanel
|
<AgentDetailPanel
|
||||||
agentId={selectedAgentId}
|
agentId={selectedAgentId}
|
||||||
treeNodes={treeNodes}
|
treeNodes={treeNodes}
|
||||||
onClose={() => { setShowAllLogs(true); setSelectedAgentId(null); }}
|
onClose={() => { setShowAllLogs(true); setSelectedAgentId(null); }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue