feat(swarm): implement Swarm View remake with Operations, Archetypes, and Templates
This commit includes the new SwarmWorkspace with its 3 sub-tabs, the LeftPanel mission picker, and the comprehensive Operations Command Dashboard featuring the live interactive DAG telemetry and task assignment prep flow.
This commit is contained in:
parent
409a7e7256
commit
dfaf523029
74 changed files with 11066 additions and 2046 deletions
148
src/components/mission/mission-card.tsx
Normal file
148
src/components/mission/mission-card.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Users, AlertTriangle, Activity, CheckCircle2, Circle } from 'lucide-react';
|
||||
import { AgentAvatar } from '../shared/agent-avatar';
|
||||
import type { AgentRecord } from '../../lib/agent-registry';
|
||||
import { SwarmGraph } from './swarm-graph';
|
||||
import { useSwarmTopology } from '../../hooks/use-swarm-topology';
|
||||
|
||||
export interface MissionCardProps {
|
||||
id: string;
|
||||
projectRoot: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status: 'planning' | 'active' | 'blocked' | 'completed';
|
||||
stats: {
|
||||
total: number;
|
||||
done: number;
|
||||
blocked: number;
|
||||
};
|
||||
agents: AgentRecord[];
|
||||
onDeploy: () => void;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const STATUS_CONFIG = {
|
||||
planning: {
|
||||
color: 'text-blue-400',
|
||||
border: 'border-blue-500/30',
|
||||
bg: 'bg-blue-500/5',
|
||||
label: 'PLANNING',
|
||||
icon: Circle
|
||||
},
|
||||
active: {
|
||||
color: 'text-emerald-400',
|
||||
border: 'border-emerald-500/30',
|
||||
bg: 'bg-emerald-500/5',
|
||||
label: 'ACTIVE',
|
||||
icon: Activity
|
||||
},
|
||||
blocked: {
|
||||
color: 'text-rose-400',
|
||||
border: 'border-rose-500/30',
|
||||
bg: 'bg-rose-500/5',
|
||||
label: 'BLOCKED',
|
||||
icon: AlertTriangle
|
||||
},
|
||||
completed: {
|
||||
color: 'text-slate-400',
|
||||
border: 'border-slate-500/30',
|
||||
bg: 'bg-slate-500/5',
|
||||
label: 'COMPLETE',
|
||||
icon: CheckCircle2
|
||||
},
|
||||
};
|
||||
|
||||
export function MissionCard({ id, projectRoot, title, description, status, stats, agents, onDeploy, onClick }: MissionCardProps) {
|
||||
const config = STATUS_CONFIG[status] || STATUS_CONFIG.planning;
|
||||
const StatusIcon = config.icon;
|
||||
const { topology, isLoading } = useSwarmTopology(projectRoot, id);
|
||||
|
||||
const isUnstaffed = agents.length === 0;
|
||||
const isWorking = agents.some(a => a.status === 'working');
|
||||
const showPulse = status === 'active' || isWorking;
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={onClick}
|
||||
className="group relative flex flex-col h-[320px] cursor-pointer overflow-hidden rounded-2xl border border-[var(--ui-border-soft)] bg-[var(--ui-bg-card)] hover:border-[var(--ui-accent-info)] hover:shadow-xl hover:shadow-black/20 transition-all duration-300"
|
||||
>
|
||||
{/* Decorative Top Glow */}
|
||||
<div className={cn("absolute top-0 left-0 right-0 h-1 opacity-50 group-hover:opacity-100 transition-opacity", config.bg.replace('/5', '/40'))} />
|
||||
|
||||
{/* HEADER */}
|
||||
<div className="p-5 flex flex-col gap-3 min-h-0">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-[10px] tracking-wider text-slate-500">{id}</span>
|
||||
<Badge variant="outline" className={cn("text-[9px] px-2 py-0.5 border h-5 flex items-center gap-1.5", config.color, config.border, config.bg)}>
|
||||
<StatusIcon className="h-3 w-3" />
|
||||
{config.label}
|
||||
</Badge>
|
||||
</div>
|
||||
{showPulse && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-bold text-lg text-slate-100 leading-tight group-hover:text-white transition-colors line-clamp-2">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 line-clamp-2 leading-relaxed">
|
||||
{description || "No mission brief available."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GRAPH VISUALIZATION */}
|
||||
<div className="px-5 py-2 flex-1 flex flex-col justify-end">
|
||||
<SwarmGraph topology={topology} isLoading={isLoading} />
|
||||
</div>
|
||||
|
||||
{/* FOOTER: SQUAD */}
|
||||
<div className="px-5 py-3 border-t border-white/5 flex items-center justify-between bg-[var(--ui-bg-shell)] mt-auto">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex -space-x-2">
|
||||
{agents.slice(0, 4).map(agent => (
|
||||
<div key={agent.agent_id} className="ring-2 ring-[var(--ui-bg-shell)] rounded-full transition-transform hover:scale-110 z-0 hover:z-10 relative" title={`${agent.display_name} (${agent.role})`}>
|
||||
<AgentAvatar name={agent.display_name} status={agent.status as any} size="sm" />
|
||||
</div>
|
||||
))}
|
||||
{agents.length === 0 && (
|
||||
<div className="h-7 w-7 rounded-full bg-slate-800 border border-slate-700 border-dashed flex items-center justify-center text-slate-600">
|
||||
<Users className="h-3 w-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{agents.length === 0 && (
|
||||
<span className="text-[10px] font-medium text-slate-500 uppercase tracking-wide">Unstaffed</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => { e.stopPropagation(); onDeploy(); }}
|
||||
className={cn(
|
||||
"h-7 px-3 text-[10px] font-bold uppercase tracking-wider border transition-all",
|
||||
isUnstaffed
|
||||
? "border-blue-500/20 text-blue-400 bg-blue-500/5 hover:bg-blue-500/10 hover:border-blue-500/40"
|
||||
: "border-slate-700 text-slate-400 hover:text-white hover:bg-white/5 hover:border-slate-500"
|
||||
)}
|
||||
>
|
||||
{isUnstaffed ? 'Deploy' : 'Manage'}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
141
src/components/mission/mission-inspector.tsx
Normal file
141
src/components/mission/mission-inspector.tsx
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Loader2, Map, MessageSquare, Users, X, Activity } from 'lucide-react';
|
||||
import { WorkflowGraph } from '../shared/workflow-graph';
|
||||
import { AgentAvatar } from '../shared/agent-avatar';
|
||||
import { useMissionGraph } from '../../hooks/use-mission-graph';
|
||||
import type { AgentRecord } from '../../lib/agent-registry';
|
||||
|
||||
interface MissionInspectorProps {
|
||||
missionId: string;
|
||||
missionTitle: string; // Passed in or fetched? Better to pass in for instant header
|
||||
projectRoot: string;
|
||||
assignedAgents: AgentRecord[];
|
||||
onClose: () => void;
|
||||
onAssign: (agentId: string, action: 'join' | 'leave') => void;
|
||||
}
|
||||
|
||||
export function MissionInspector({
|
||||
missionId,
|
||||
missionTitle,
|
||||
projectRoot,
|
||||
assignedAgents,
|
||||
onClose,
|
||||
onAssign
|
||||
}: MissionInspectorProps) {
|
||||
const { nodes, isLoading: isGraphLoading } = useMissionGraph(projectRoot, missionId);
|
||||
const [activeTab, setActiveTab] = useState('map');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-[#08111d] border-l border-slate-800 text-slate-200">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between p-4 border-b border-slate-800 bg-[#0d1621]">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 font-mono text-[10px]">
|
||||
{missionId}
|
||||
</Badge>
|
||||
<span className="text-[10px] uppercase tracking-wider text-slate-500 font-bold">Active Operation</span>
|
||||
</div>
|
||||
<h2 className="text-sm font-semibold text-white line-clamp-2">{missionTitle}</h2>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="h-6 w-6 text-slate-500 hover:text-white" onClick={onClose}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col min-h-0">
|
||||
<div className="px-4 pt-2 bg-[#0d1621] border-b border-slate-800">
|
||||
<TabsList className="bg-transparent p-0 h-auto gap-4">
|
||||
<TabsTrigger
|
||||
value="map"
|
||||
className="rounded-none border-b-2 border-transparent px-2 py-2 text-[10px] uppercase tracking-wider data-[state=active]:border-emerald-500 data-[state=active]:bg-transparent data-[state=active]:text-emerald-400"
|
||||
>
|
||||
<Map className="h-3 w-3 mr-1.5" />
|
||||
Map
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="comms"
|
||||
className="rounded-none border-b-2 border-transparent px-2 py-2 text-[10px] uppercase tracking-wider data-[state=active]:border-emerald-500 data-[state=active]:bg-transparent data-[state=active]:text-emerald-400"
|
||||
>
|
||||
<MessageSquare className="h-3 w-3 mr-1.5" />
|
||||
Comms
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="squad"
|
||||
className="rounded-none border-b-2 border-transparent px-2 py-2 text-[10px] uppercase tracking-wider data-[state=active]:border-emerald-500 data-[state=active]:bg-transparent data-[state=active]:text-emerald-400"
|
||||
>
|
||||
<Users className="h-3 w-3 mr-1.5" />
|
||||
Squad <span className="ml-1 text-slate-500">{assignedAgents.length}</span>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-hidden relative">
|
||||
|
||||
<TabsContent value="map" className="h-full m-0 p-0 data-[state=active]:flex flex-col">
|
||||
{isGraphLoading ? (
|
||||
<div className="flex h-full items-center justify-center"><Loader2 className="h-6 w-6 animate-spin text-slate-600" /></div>
|
||||
) : (
|
||||
<div className="flex-1 relative bg-slate-950">
|
||||
<WorkflowGraph beads={nodes} selectedId={undefined} hideClosed={false} className="h-full w-full border-0 rounded-none" />
|
||||
<div className="absolute bottom-4 right-4 pointer-events-none">
|
||||
<Badge variant="outline" className="bg-black/50 border-white/10 backdrop-blur text-xs">
|
||||
{nodes.length} Nodes
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="comms" className="h-full m-0 p-4 overflow-y-auto">
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-500 space-y-2 opacity-60">
|
||||
<Activity className="h-8 w-8" />
|
||||
<p className="text-xs">Secure Uplink Offline</p>
|
||||
<p className="text-[10px] italic">Inter-agent communication feed coming in Phase 3.2</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="squad" className="h-full m-0">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4 space-y-3">
|
||||
{assignedAgents.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-500 text-xs">
|
||||
No agents deployed.
|
||||
</div>
|
||||
) : (
|
||||
assignedAgents.map(agent => (
|
||||
<div key={agent.agent_id} className="flex items-center justify-between p-3 rounded-lg bg-slate-800/40 border border-slate-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<AgentAvatar name={agent.display_name} status={agent.status as any} />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-200">{agent.display_name}</p>
|
||||
<div className="flex items-center gap-2 text-[10px] text-slate-500">
|
||||
<span className="uppercase font-bold tracking-wider">{agent.role}</span>
|
||||
<span>•</span>
|
||||
<span className="font-mono">{agent.status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-7 text-xs text-rose-400 hover:text-rose-300 hover:bg-rose-950/30" onClick={() => onAssign(agent.agent_id, 'leave')}>
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
107
src/components/mission/swarm-graph.tsx
Normal file
107
src/components/mission/swarm-graph.tsx
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { SwarmTopologyData } from '../../hooks/use-swarm-topology';
|
||||
|
||||
interface SwarmGraphProps {
|
||||
topology: SwarmTopologyData | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export function SwarmGraph({ topology, isLoading }: SwarmGraphProps) {
|
||||
const nodes = useMemo(() => {
|
||||
if (!topology) return [];
|
||||
|
||||
// Simple layout strategy: Clusters
|
||||
// Done: Left side
|
||||
// Active: Center
|
||||
// Ready: Right
|
||||
// Blocked: Bottom Right
|
||||
|
||||
const output: React.ReactNode[] = [];
|
||||
const scale = 0.5;
|
||||
|
||||
// 1. Completed (Green Cluster)
|
||||
topology.completed.forEach((item, i) => {
|
||||
const col = i % 5;
|
||||
const row = Math.floor(i / 5);
|
||||
output.push(
|
||||
<circle
|
||||
key={`done-${item.id}`}
|
||||
cx={20 + (col * 8)}
|
||||
cy={20 + (row * 8)}
|
||||
r={2.5}
|
||||
fill="#34d399"
|
||||
opacity={0.5}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
// 2. Active (Pulsing Center)
|
||||
topology.active.forEach((item, i) => {
|
||||
const cx = 140 + (i * 20);
|
||||
const cy = 30 + (i % 2) * 10;
|
||||
output.push(
|
||||
<g key={`active-${item.id}`}>
|
||||
<circle cx={cx} cy={cy} r={6} fill="#10b981" className="animate-pulse" />
|
||||
<circle cx={cx} cy={cy} r={3} fill="#ecfdf5" />
|
||||
</g>
|
||||
);
|
||||
});
|
||||
|
||||
// 3. Ready (White Pipeline)
|
||||
topology.ready.forEach((item, i) => {
|
||||
const cx = 220 + (i * 10);
|
||||
const cy = 30;
|
||||
output.push(
|
||||
<circle key={`ready-${item.id}`} cx={cx} cy={cy} r={3} fill="#94a3b8" />
|
||||
);
|
||||
});
|
||||
|
||||
// 4. Blocked (Red Hazard)
|
||||
topology.blocked.forEach((item, i) => {
|
||||
const cx = 220 + (i * 10);
|
||||
const cy = 50;
|
||||
output.push(
|
||||
<circle key={`blocked-${item.id}`} cx={cx} cy={cy} r={3} fill="#f43f5e" />
|
||||
);
|
||||
});
|
||||
|
||||
return output;
|
||||
}, [topology]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-16 w-full flex items-center justify-center bg-black/20 rounded-lg animate-pulse">
|
||||
<span className="text-[10px] text-slate-600 font-mono">SCANNING TOPOLOGY...</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!topology || (topology.completed.length === 0 && topology.active.length === 0 && topology.ready.length === 0)) {
|
||||
return (
|
||||
<div className="h-16 w-full flex items-center justify-center bg-black/20 rounded-lg border border-dashed border-slate-800">
|
||||
<span className="text-[10px] text-slate-600 font-mono">EMPTY SIGNAL</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-16 w-full bg-black/40 rounded-lg border border-white/5 overflow-hidden relative">
|
||||
<svg width="100%" height="100%" viewBox="0 0 300 64" preserveAspectRatio="xMidYMid meet">
|
||||
{/* Connection Lines (Abstract) */}
|
||||
<path d="M 60 30 L 130 30" stroke="#334155" strokeWidth="1" strokeDasharray="4 4" />
|
||||
<path d="M 180 30 L 210 30" stroke="#334155" strokeWidth="1" strokeDasharray="4 4" />
|
||||
|
||||
{nodes}
|
||||
|
||||
{/* Labels */}
|
||||
<text x="30" y="55" fontSize="8" fill="#475569" textAnchor="middle" fontFamily="monospace">DONE</text>
|
||||
<text x="150" y="55" fontSize="8" fill="#10b981" textAnchor="middle" fontFamily="monospace" fontWeight="bold">ACTIVE</text>
|
||||
<text x="240" y="15" fontSize="8" fill="#94a3b8" textAnchor="middle" fontFamily="monospace">READY</text>
|
||||
<text x="240" y="60" fontSize="8" fill="#f43f5e" textAnchor="middle" fontFamily="monospace">BLOCKED</text>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
255
src/components/mission/team-manager-dialog.tsx
Normal file
255
src/components/mission/team-manager-dialog.tsx
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { AgentAvatar } from '../shared/agent-avatar';
|
||||
import { useAgentPool } from '../../hooks/use-agent-pool';
|
||||
import { Loader2, Plus, Minus, ShieldCheck, Search, Users, ChevronLeft, Save } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import type { AgentRecord } from '../../lib/agent-registry';
|
||||
|
||||
interface TeamManagerDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
missionId: string;
|
||||
missionTitle: string;
|
||||
projectRoot: string;
|
||||
assignedAgents: AgentRecord[];
|
||||
onAssign: (agentId: string, action: 'join' | 'leave') => Promise<void>;
|
||||
}
|
||||
|
||||
export function TeamManagerDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
missionId,
|
||||
missionTitle,
|
||||
projectRoot,
|
||||
assignedAgents,
|
||||
onAssign
|
||||
}: TeamManagerDialogProps) {
|
||||
const { agents, isLoading, refresh } = useAgentPool(projectRoot);
|
||||
const [search, setSearch] = useState('');
|
||||
const [pendingAction, setPendingAction] = useState<string | null>(null);
|
||||
|
||||
// Creation Mode State
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [newName, setNewName] = useState('');
|
||||
const [newRole, setNewRole] = useState('');
|
||||
const [newInstructions, setNewInstructions] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const assignedIds = new Set(assignedAgents.map(a => a.agent_id));
|
||||
|
||||
const availableAgents = agents.filter(a =>
|
||||
!assignedIds.has(a.agent_id) &&
|
||||
(a.display_name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
a.role.toLowerCase().includes(search.toLowerCase()))
|
||||
);
|
||||
|
||||
const handleAction = async (agentId: string, action: 'join' | 'leave') => {
|
||||
setPendingAction(agentId);
|
||||
try {
|
||||
await onAssign(agentId, action);
|
||||
} finally {
|
||||
setPendingAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateAgent = async () => {
|
||||
if (!newName || !newRole) return;
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const res = await fetch('/api/agent/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projectRoot, name: newName, role: newRole, instructions: newInstructions })
|
||||
});
|
||||
if (res.ok) {
|
||||
await refresh();
|
||||
setIsCreating(false);
|
||||
setNewName('');
|
||||
setNewRole('');
|
||||
setNewInstructions('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<DialogContent className="max-w-2xl bg-[#08111d] border-slate-800 text-slate-200">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-5 w-5 text-emerald-500" />
|
||||
Manage Mission Squad
|
||||
<Badge variant="outline" className="ml-2 border-slate-700 text-slate-400 font-mono font-normal">
|
||||
{missionId}
|
||||
</Badge>
|
||||
</DialogTitle>
|
||||
<p className="text-sm text-slate-400">{missionTitle}</p>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 h-[450px] mt-4">
|
||||
{/* Left: Available Pool / Creation Form */}
|
||||
<div className="flex flex-col gap-2 rounded-lg border border-slate-800 bg-[#0d1621] p-3 transition-all relative overflow-hidden">
|
||||
{isCreating ? (
|
||||
// CREATION FORM
|
||||
<div className="flex flex-col h-full animate-in slide-in-from-left-4 fade-in duration-200">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Button variant="ghost" size="sm" onClick={() => setIsCreating(false)} className="-ml-2 text-slate-400 hover:text-white">
|
||||
<ChevronLeft className="h-4 w-4 mr-1" /> Back
|
||||
</Button>
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-emerald-500">Draft New Agent</h4>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 flex-1">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] uppercase text-slate-500 font-semibold">Codename</label>
|
||||
<Input
|
||||
placeholder="e.g. Data Miner"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-[10px] uppercase text-slate-500 font-semibold">Role</label>
|
||||
<Input
|
||||
placeholder="e.g. data-engineer"
|
||||
className="bg-slate-900 border-slate-700 font-mono text-xs"
|
||||
value={newRole}
|
||||
onChange={e => setNewRole(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 flex-1 flex flex-col">
|
||||
<label className="text-[10px] uppercase text-slate-500 font-semibold">Directives / Instructions</label>
|
||||
<Textarea
|
||||
placeholder="Primary directive: Extract data from..."
|
||||
className="bg-slate-900 border-slate-700 flex-1 resize-none text-xs leading-relaxed"
|
||||
value={newInstructions}
|
||||
onChange={e => setNewInstructions(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleCreateAgent}
|
||||
disabled={!newName || !newRole || isSaving}
|
||||
className="mt-4 bg-emerald-600 hover:bg-emerald-500 text-white w-full"
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <><Save className="h-4 w-4 mr-2" /> Recruit Agent</>}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
// LIST VIEW
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-slate-500">Available Resources</h4>
|
||||
<Badge variant="secondary" className="bg-slate-800 text-slate-400">{availableAgents.length}</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2 top-2.5 h-3 w-3 text-slate-500" />
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
className="h-8 pl-7 bg-slate-900 border-slate-700 text-xs"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="h-8 w-8 border-slate-700 border-dashed text-slate-400 hover:text-emerald-400 hover:border-emerald-500/50"
|
||||
onClick={() => setIsCreating(true)}
|
||||
title="Draft New Agent"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 pr-3">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center p-4"><Loader2 className="h-5 w-5 animate-spin text-slate-500" /></div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{availableAgents.map(agent => (
|
||||
<div key={agent.agent_id} className="flex items-center justify-between p-2 rounded hover:bg-white/5 transition-colors group">
|
||||
<div className="flex items-center gap-2">
|
||||
<AgentAvatar name={agent.display_name} status={agent.status as any} size="sm" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-slate-200">{agent.display_name}</p>
|
||||
<p className="text-[10px] text-slate-500 uppercase">{agent.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 opacity-0 group-hover:opacity-100 hover:bg-emerald-500/20 hover:text-emerald-400"
|
||||
onClick={() => handleAction(agent.agent_id, 'join')}
|
||||
disabled={!!pendingAction}
|
||||
>
|
||||
{pendingAction === agent.agent_id ? <Loader2 className="h-3 w-3 animate-spin" /> : <Plus className="h-3 w-3" />}
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: Assigned Squad */}
|
||||
<div className="flex flex-col gap-2 rounded-lg border border-emerald-900/30 bg-emerald-950/10 p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="text-xs font-bold uppercase tracking-wider text-emerald-500">Deployed Squad</h4>
|
||||
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400">{assignedAgents.length}</Badge>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 pr-3">
|
||||
<div className="space-y-2">
|
||||
{assignedAgents.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full text-slate-500 text-xs italic border border-dashed border-emerald-900/30 rounded bg-emerald-950/20">
|
||||
<Users className="h-8 w-8 mb-2 opacity-20" />
|
||||
No agents assigned
|
||||
</div>
|
||||
) : (
|
||||
assignedAgents.map(agent => (
|
||||
<div key={agent.agent_id} className="flex items-center justify-between p-2 rounded bg-emerald-500/5 border border-emerald-500/10">
|
||||
<div className="flex items-center gap-2">
|
||||
<AgentAvatar name={agent.display_name} status={agent.status as any} size="sm" />
|
||||
<div>
|
||||
<p className="text-xs font-medium text-emerald-100">{agent.display_name}</p>
|
||||
<p className="text-[10px] text-emerald-500/70 uppercase">{agent.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-6 w-6 hover:bg-rose-500/20 hover:text-rose-400"
|
||||
onClick={() => handleAction(agent.agent_id, 'leave')}
|
||||
disabled={!!pendingAction}
|
||||
>
|
||||
{pendingAction === agent.agent_id ? <Loader2 className="h-3 w-3 animate-spin" /> : <Minus className="h-3 w-3" />}
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose} className="border-slate-700 text-slate-300">Done</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue