Add agent role management system for Bees-AgenticWorkers integration
Backend: - Database migration for agent role fields and predefined roles - AgentRole and AgentCollaboration models - Updated Agent model with role-based fields Frontend: - AgentRoleSelector component for role assignment - CollaborationDashboard for monitoring agent interactions - AgentManagement interface with role analytics 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
376
frontend/src/components/agents/AgentManagement.tsx
Normal file
376
frontend/src/components/agents/AgentManagement.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { DataTable } from '../ui/DataTable';
|
||||
import { Alert, AlertDescription } from '../ui/alert';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '../ui/dialog';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
import AgentRoleSelector from './AgentRoleSelector';
|
||||
import CollaborationDashboard from './CollaborationDashboard';
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
endpoint: string;
|
||||
model: string;
|
||||
status: string;
|
||||
role?: string;
|
||||
expertise?: string[];
|
||||
reports_to?: string[];
|
||||
deliverables?: string[];
|
||||
capabilities?: string[];
|
||||
collaboration_settings?: any;
|
||||
last_seen?: string;
|
||||
}
|
||||
|
||||
interface RoleDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
system_prompt: string;
|
||||
reports_to: string[];
|
||||
expertise: string[];
|
||||
deliverables: string[];
|
||||
capabilities: string[];
|
||||
collaboration_defaults: any;
|
||||
}
|
||||
|
||||
export const AgentManagement: React.FC = () => {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(null);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgents();
|
||||
}, []);
|
||||
|
||||
const fetchAgents = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/agents');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch agents');
|
||||
}
|
||||
const data = await response.json();
|
||||
setAgents(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load agents');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleUpdate = async (agent: Agent, role: RoleDefinition) => {
|
||||
try {
|
||||
const response = await fetch(`/api/agents/${agent.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role: role.name,
|
||||
system_prompt: role.system_prompt,
|
||||
reports_to: role.reports_to,
|
||||
expertise: role.expertise,
|
||||
deliverables: role.deliverables,
|
||||
capabilities: role.capabilities,
|
||||
collaboration_settings: role.collaboration_defaults
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update agent role');
|
||||
}
|
||||
|
||||
// Refresh agents list
|
||||
await fetchAgents();
|
||||
setEditDialogOpen(false);
|
||||
setSelectedAgent(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to update agent');
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'online': return 'default';
|
||||
case 'busy': return 'secondary';
|
||||
case 'offline': return 'destructive';
|
||||
default: return 'outline';
|
||||
}
|
||||
};
|
||||
|
||||
const agentColumns = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Agent Name',
|
||||
cell: ({ row }: any) => (
|
||||
<div>
|
||||
<div className="font-medium">{row.original.name}</div>
|
||||
<div className="text-sm text-gray-500">{row.original.id.substring(0, 8)}...</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'role',
|
||||
header: 'Role',
|
||||
cell: ({ row }: any) => (
|
||||
row.original.role ? (
|
||||
<Badge variant="outline">
|
||||
{row.original.role.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
) : (
|
||||
<span className="text-gray-400">No role assigned</span>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }: any) => (
|
||||
<Badge variant={getStatusColor(row.original.status)}>
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'model',
|
||||
header: 'Model',
|
||||
cell: ({ row }: any) => (
|
||||
<span className="font-mono text-sm">{row.original.model || 'N/A'}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'expertise',
|
||||
header: 'Expertise',
|
||||
cell: ({ row }: any) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(row.original.expertise || []).slice(0, 3).map((exp: string) => (
|
||||
<Badge key={exp} variant="secondary" className="text-xs">
|
||||
{exp.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
))}
|
||||
{(row.original.expertise || []).length > 3 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{(row.original.expertise || []).length - 3} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'last_seen',
|
||||
header: 'Last Seen',
|
||||
cell: ({ row }: any) => (
|
||||
<div className="text-sm text-gray-500">
|
||||
{row.original.last_seen ?
|
||||
new Date(row.original.last_seen).toLocaleString() :
|
||||
'Never'
|
||||
}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }: any) => (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSelectedAgent(row.original);
|
||||
setEditDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Edit Role
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const getRoleDistribution = () => {
|
||||
const distribution: Record<string, number> = {};
|
||||
agents.forEach(agent => {
|
||||
const role = agent.role || 'unassigned';
|
||||
distribution[role] = (distribution[role] || 0) + 1;
|
||||
});
|
||||
return distribution;
|
||||
};
|
||||
|
||||
const getStatusDistribution = () => {
|
||||
const distribution: Record<string, number> = {};
|
||||
agents.forEach(agent => {
|
||||
distribution[agent.status] = (distribution[agent.status] || 0) + 1;
|
||||
});
|
||||
return distribution;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="animate-pulse">
|
||||
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
|
||||
<div className="h-64 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
<Button onClick={fetchAgents} className="mt-4">
|
||||
Retry
|
||||
</Button>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const roleDistribution = getRoleDistribution();
|
||||
const statusDistribution = getStatusDistribution();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold">Agent Management</h1>
|
||||
<Button onClick={fetchAgents}>Refresh</Button>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="agents" className="w-full">
|
||||
<TabsList>
|
||||
<TabsTrigger value="agents">Agents</TabsTrigger>
|
||||
<TabsTrigger value="collaborations">Collaborations</TabsTrigger>
|
||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="agents" className="space-y-6">
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-2xl font-bold">{agents.length}</div>
|
||||
<div className="text-sm text-gray-500">Total Agents</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-2xl font-bold">{statusDistribution.online || 0}</div>
|
||||
<div className="text-sm text-gray-500">Online</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-2xl font-bold">{agents.filter(a => a.role).length}</div>
|
||||
<div className="text-sm text-gray-500">Role Assigned</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-2xl font-bold">{Object.keys(roleDistribution).length}</div>
|
||||
<div className="text-sm text-gray-500">Unique Roles</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Agents Table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Registered Agents</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
columns={agentColumns}
|
||||
data={agents}
|
||||
pagination={true}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="collaborations">
|
||||
<CollaborationDashboard />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics" className="space-y-6">
|
||||
{/* Role Distribution */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Role Distribution</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(roleDistribution).map(([role, count]) => (
|
||||
<div key={role} className="flex justify-between items-center">
|
||||
<span className="font-medium">
|
||||
{role === 'unassigned' ? 'Unassigned' : role.replace(/_/g, ' ')}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-32 h-2 bg-gray-200 rounded">
|
||||
<div
|
||||
className="h-full bg-blue-500 rounded"
|
||||
style={{ width: `${(count / agents.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{count}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Status Distribution */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Agent Status</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(statusDistribution).map(([status, count]) => (
|
||||
<div key={status} className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={getStatusColor(status)}>{status}</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-32 h-2 bg-gray-200 rounded">
|
||||
<div
|
||||
className="h-full bg-green-500 rounded"
|
||||
style={{ width: `${(count / agents.length) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-gray-500">{count}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Role Assignment Dialog */}
|
||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Assign Role to {selectedAgent?.name}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedAgent && (
|
||||
<AgentRoleSelector
|
||||
agentId={selectedAgent.id}
|
||||
currentRole={selectedAgent.role}
|
||||
onRoleChange={(role) => handleRoleUpdate(selectedAgent, role)}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentManagement;
|
||||
231
frontend/src/components/agents/AgentRoleSelector.tsx
Normal file
231
frontend/src/components/agents/AgentRoleSelector.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Select } from '../ui/select';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Input } from '../ui/input';
|
||||
import { Textarea } from '../ui/textarea';
|
||||
import { Label } from '../ui/label';
|
||||
import { Alert, AlertDescription } from '../ui/alert';
|
||||
|
||||
interface RoleDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
system_prompt: string;
|
||||
reports_to: string[];
|
||||
expertise: string[];
|
||||
deliverables: string[];
|
||||
capabilities: string[];
|
||||
collaboration_defaults: {
|
||||
preferred_message_types: string[];
|
||||
auto_subscribe_to_roles: string[];
|
||||
auto_subscribe_to_expertise: string[];
|
||||
response_timeout_seconds: number;
|
||||
max_collaboration_depth: number;
|
||||
escalation_threshold: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AgentRoleSelectorProps {
|
||||
agentId?: string;
|
||||
currentRole?: string;
|
||||
onRoleChange: (role: RoleDefinition) => void;
|
||||
onCustomPromptChange?: (prompt: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AgentRoleSelector: React.FC<AgentRoleSelectorProps> = ({
|
||||
agentId,
|
||||
currentRole,
|
||||
onRoleChange,
|
||||
onCustomPromptChange,
|
||||
className = ""
|
||||
}) => {
|
||||
const [roles, setRoles] = useState<RoleDefinition[]>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<RoleDefinition | null>(null);
|
||||
const [customPrompt, setCustomPrompt] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAvailableRoles();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentRole && roles.length > 0) {
|
||||
const role = roles.find(r => r.name === currentRole);
|
||||
if (role) {
|
||||
setSelectedRole(role);
|
||||
}
|
||||
}
|
||||
}, [currentRole, roles]);
|
||||
|
||||
const fetchAvailableRoles = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/agent-roles');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch agent roles');
|
||||
}
|
||||
const data = await response.json();
|
||||
setRoles(data);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load roles');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleSelect = (roleName: string) => {
|
||||
const role = roles.find(r => r.name === roleName);
|
||||
if (role) {
|
||||
setSelectedRole(role);
|
||||
setCustomPrompt(role.system_prompt);
|
||||
onRoleChange(role);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomPromptChange = (prompt: string) => {
|
||||
setCustomPrompt(prompt);
|
||||
if (onCustomPromptChange) {
|
||||
onCustomPromptChange(prompt);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Agent Role Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="animate-pulse">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4 mb-4"></div>
|
||||
<div className="h-32 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Agent Role Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
<Button onClick={fetchAvailableRoles} className="mt-4">
|
||||
Retry
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Agent Role Configuration</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Role Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role-select">Select Role</Label>
|
||||
<Select onValueChange={handleRoleSelect} value={selectedRole?.name || ''}>
|
||||
<option value="">Select a role...</option>
|
||||
{roles.map(role => (
|
||||
<option key={role.id} value={role.name}>
|
||||
{role.display_name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Role Details */}
|
||||
{selectedRole && (
|
||||
<>
|
||||
{/* Expertise Areas */}
|
||||
<div className="space-y-2">
|
||||
<Label>Expertise Areas</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedRole.expertise.map(exp => (
|
||||
<Badge key={exp} variant="secondary">
|
||||
{exp.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reports To */}
|
||||
{selectedRole.reports_to.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label>Reports To</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedRole.reports_to.map(role => (
|
||||
<Badge key={role} variant="outline">
|
||||
{role.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deliverables */}
|
||||
<div className="space-y-2">
|
||||
<Label>Key Deliverables</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedRole.deliverables.map(deliverable => (
|
||||
<Badge key={deliverable} variant="default">
|
||||
{deliverable.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Capabilities */}
|
||||
<div className="space-y-2">
|
||||
<Label>Capabilities</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedRole.capabilities.map(capability => (
|
||||
<Badge key={capability} variant="secondary">
|
||||
{capability}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Collaboration Settings */}
|
||||
<div className="space-y-2">
|
||||
<Label>Collaboration Preferences</Label>
|
||||
<div className="text-sm text-gray-600 space-y-1">
|
||||
<div>Response Timeout: {selectedRole.collaboration_defaults.response_timeout_seconds}s</div>
|
||||
<div>Max Collaboration Depth: {selectedRole.collaboration_defaults.max_collaboration_depth}</div>
|
||||
<div>Escalation Threshold: {selectedRole.collaboration_defaults.escalation_threshold}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Prompt */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="system-prompt">System Prompt</Label>
|
||||
<Textarea
|
||||
id="system-prompt"
|
||||
value={customPrompt}
|
||||
onChange={(e) => handleCustomPromptChange(e.target.value)}
|
||||
placeholder="System prompt for this agent role..."
|
||||
rows={8}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentRoleSelector;
|
||||
271
frontend/src/components/agents/CollaborationDashboard.tsx
Normal file
271
frontend/src/components/agents/CollaborationDashboard.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { DataTable } from '../ui/DataTable';
|
||||
import { Alert, AlertDescription } from '../ui/alert';
|
||||
import { Select } from '../ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
|
||||
|
||||
interface Collaboration {
|
||||
id: string;
|
||||
from_agent_id: string;
|
||||
to_agent_id: string;
|
||||
message_type: string;
|
||||
thread_id: string;
|
||||
project_id: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
created_at: string;
|
||||
responded_at?: string;
|
||||
resolved_at?: string;
|
||||
message_data: any;
|
||||
response_data?: any;
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
role: string;
|
||||
status: string;
|
||||
expertise: string[];
|
||||
}
|
||||
|
||||
interface CollaborationDashboardProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CollaborationDashboard: React.FC<CollaborationDashboardProps> = ({
|
||||
className = ""
|
||||
}) => {
|
||||
const [collaborations, setCollaborations] = useState<Collaboration[]>([]);
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedStatus, setSelectedStatus] = useState<string>('all');
|
||||
const [selectedMessageType, setSelectedMessageType] = useState<string>('all');
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [collaborationsRes, agentsRes] = await Promise.all([
|
||||
fetch('/api/agent-collaborations'),
|
||||
fetch('/api/agents')
|
||||
]);
|
||||
|
||||
if (!collaborationsRes.ok || !agentsRes.ok) {
|
||||
throw new Error('Failed to fetch collaboration data');
|
||||
}
|
||||
|
||||
const [collaborationsData, agentsData] = await Promise.all([
|
||||
collaborationsRes.json(),
|
||||
agentsRes.json()
|
||||
]);
|
||||
|
||||
setCollaborations(collaborationsData);
|
||||
setAgents(agentsData);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getAgentName = (agentId: string) => {
|
||||
const agent = agents.find(a => a.id === agentId);
|
||||
return agent ? agent.name : agentId;
|
||||
};
|
||||
|
||||
const getAgentRole = (agentId: string) => {
|
||||
const agent = agents.find(a => a.id === agentId);
|
||||
return agent ? agent.role : 'Unknown';
|
||||
};
|
||||
|
||||
const filteredCollaborations = collaborations.filter(collab => {
|
||||
const statusMatch = selectedStatus === 'all' || collab.status === selectedStatus;
|
||||
const typeMatch = selectedMessageType === 'all' || collab.message_type === selectedMessageType;
|
||||
return statusMatch && typeMatch;
|
||||
});
|
||||
|
||||
const collaborationColumns = [
|
||||
{
|
||||
accessorKey: 'from_agent_id',
|
||||
header: 'From Agent',
|
||||
cell: ({ row }: any) => (
|
||||
<div>
|
||||
<div className="font-medium">{getAgentName(row.original.from_agent_id)}</div>
|
||||
<div className="text-sm text-gray-500">{getAgentRole(row.original.from_agent_id)}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'to_agent_id',
|
||||
header: 'To Agent',
|
||||
cell: ({ row }: any) => (
|
||||
<div>
|
||||
<div className="font-medium">{getAgentName(row.original.to_agent_id)}</div>
|
||||
<div className="text-sm text-gray-500">{getAgentRole(row.original.to_agent_id)}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'message_type',
|
||||
header: 'Message Type',
|
||||
cell: ({ row }: any) => (
|
||||
<Badge variant="outline">
|
||||
{row.original.message_type.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }: any) => {
|
||||
const status = row.original.status;
|
||||
const variant =
|
||||
status === 'resolved' ? 'default' :
|
||||
status === 'responded' ? 'secondary' :
|
||||
status === 'escalated' ? 'destructive' :
|
||||
'outline';
|
||||
return <Badge variant={variant}>{status}</Badge>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'priority',
|
||||
header: 'Priority',
|
||||
cell: ({ row }: any) => {
|
||||
const priority = row.original.priority;
|
||||
const variant =
|
||||
priority === 'urgent' ? 'destructive' :
|
||||
priority === 'high' ? 'default' :
|
||||
priority === 'medium' ? 'secondary' :
|
||||
'outline';
|
||||
return <Badge variant={variant}>{priority}</Badge>;
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Created',
|
||||
cell: ({ row }: any) => (
|
||||
<div className="text-sm">
|
||||
{new Date(row.original.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const getCollaborationStats = () => {
|
||||
const total = collaborations.length;
|
||||
const pending = collaborations.filter(c => c.status === 'pending').length;
|
||||
const resolved = collaborations.filter(c => c.status === 'resolved').length;
|
||||
const escalated = collaborations.filter(c => c.status === 'escalated').length;
|
||||
|
||||
return { total, pending, resolved, escalated };
|
||||
};
|
||||
|
||||
const stats = getCollaborationStats();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Collaboration Dashboard</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||
<div className="h-32 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader>
|
||||
<CardTitle>Collaboration Dashboard</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
<Button onClick={fetchData} className="mt-4">
|
||||
Retry
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`space-y-6 ${className}`}>
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-2xl font-bold">{stats.total}</div>
|
||||
<div className="text-sm text-gray-500">Total Collaborations</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-2xl font-bold">{stats.pending}</div>
|
||||
<div className="text-sm text-gray-500">Pending</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-2xl font-bold">{stats.resolved}</div>
|
||||
<div className="text-sm text-gray-500">Resolved</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="text-2xl font-bold">{stats.escalated}</div>
|
||||
<div className="text-sm text-gray-500">Escalated</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Main Dashboard */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Agent Collaborations</CardTitle>
|
||||
<div className="flex gap-4">
|
||||
<Select onValueChange={setSelectedStatus} value={selectedStatus}>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="responded">Responded</option>
|
||||
<option value="resolved">Resolved</option>
|
||||
<option value="escalated">Escalated</option>
|
||||
</Select>
|
||||
<Select onValueChange={setSelectedMessageType} value={selectedMessageType}>
|
||||
<option value="all">All Message Types</option>
|
||||
<option value="task_help_request">Help Request</option>
|
||||
<option value="coordination_request">Coordination Request</option>
|
||||
<option value="expertise_request">Expertise Request</option>
|
||||
<option value="mentorship_request">Mentorship Request</option>
|
||||
<option value="dependency_alert">Dependency Alert</option>
|
||||
<option value="escalation_trigger">Escalation</option>
|
||||
</Select>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
columns={collaborationColumns}
|
||||
data={filteredCollaborations}
|
||||
pagination={true}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollaborationDashboard;
|
||||
Reference in New Issue
Block a user