mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-11-02 13:07:46 +00:00
1216 lines
37 KiB
TypeScript
1216 lines
37 KiB
TypeScript
"use client"
|
|
|
|
import React, { useCallback, useEffect, useRef, useState } from "react"
|
|
import { Grid } from "@hanzo/ui/grid"
|
|
import {
|
|
Activity,
|
|
AlertCircle,
|
|
ArrowRight,
|
|
Bot,
|
|
Brain,
|
|
CheckCircle,
|
|
CircleStop,
|
|
Clock,
|
|
Code,
|
|
Copy,
|
|
Edit,
|
|
MessageSquare,
|
|
Network,
|
|
Palette,
|
|
Pause,
|
|
Play,
|
|
Plus,
|
|
Search,
|
|
Settings,
|
|
Trash2,
|
|
Zap,
|
|
} from "lucide-react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
import { Badge } from "@/registry/default/ui/badge"
|
|
import { Button } from "@/registry/default/ui/button"
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/registry/default/ui/card"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/registry/default/ui/dialog"
|
|
import { Input } from "@/registry/default/ui/input"
|
|
import { Label } from "@/registry/default/ui/label"
|
|
import { Progress } from "@/registry/default/ui/progress"
|
|
import { ScrollArea } from "@/registry/default/ui/scroll-area"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/registry/default/ui/select"
|
|
import { Separator } from "@/registry/default/ui/separator"
|
|
import {
|
|
Tabs,
|
|
TabsContent,
|
|
TabsList,
|
|
TabsTrigger,
|
|
} from "@/registry/default/ui/tabs"
|
|
import { Textarea } from "@/registry/default/ui/textarea"
|
|
|
|
// Types and Interfaces
|
|
export type AgentType =
|
|
"research" | "code" | "analysis" | "creative" | "automation"
|
|
export type AgentStatus = "idle" | "working" | "completed" | "error"
|
|
export type WorkflowMode = "sequential" | "parallel" | "custom"
|
|
|
|
export interface Task {
|
|
id: string
|
|
title: string
|
|
description: string
|
|
priority: "low" | "medium" | "high"
|
|
status: "pending" | "in-progress" | "completed" | "failed"
|
|
assignedTo?: string
|
|
progress: number
|
|
createdAt: Date
|
|
estimatedTime?: number
|
|
}
|
|
|
|
export interface Agent {
|
|
id: string
|
|
name: string
|
|
type: AgentType
|
|
capabilities: string[]
|
|
status: AgentStatus
|
|
currentTask?: Task
|
|
position: { x: number; y: number }
|
|
connections: string[]
|
|
avatar?: string
|
|
description?: string
|
|
performance: {
|
|
tasksCompleted: number
|
|
successRate: number
|
|
avgCompletionTime: number
|
|
}
|
|
}
|
|
|
|
export interface WorkflowConnection {
|
|
id: string
|
|
source: string
|
|
target: string
|
|
condition?: string
|
|
animated?: boolean
|
|
}
|
|
|
|
export interface AIAgentsProps {
|
|
agents?: Agent[]
|
|
onAgentCreate?: (
|
|
agent: Omit<Agent, "id" | "position" | "performance">
|
|
) => void
|
|
onTaskAssign?: (task: Task, agentId: string) => void
|
|
onWorkflowSave?: (workflow: {
|
|
agents: Agent[]
|
|
connections: WorkflowConnection[]
|
|
}) => void
|
|
workflowMode?: WorkflowMode
|
|
className?: string
|
|
}
|
|
|
|
// Agent type configurations
|
|
const agentTypeConfig = {
|
|
research: {
|
|
icon: Search,
|
|
color: "bg-blue-500",
|
|
capabilities: [
|
|
"web-search",
|
|
"data-analysis",
|
|
"fact-checking",
|
|
"source-validation",
|
|
],
|
|
},
|
|
code: {
|
|
icon: Code,
|
|
color: "bg-green-500",
|
|
capabilities: ["code-generation", "debugging", "testing", "optimization"],
|
|
},
|
|
analysis: {
|
|
icon: Brain,
|
|
color: "bg-purple-500",
|
|
capabilities: [
|
|
"data-processing",
|
|
"pattern-recognition",
|
|
"insights",
|
|
"reporting",
|
|
],
|
|
},
|
|
creative: {
|
|
icon: Palette,
|
|
color: "bg-pink-500",
|
|
capabilities: [
|
|
"content-creation",
|
|
"design",
|
|
"brainstorming",
|
|
"storytelling",
|
|
],
|
|
},
|
|
automation: {
|
|
icon: Zap,
|
|
color: "bg-orange-500",
|
|
capabilities: [
|
|
"workflow-execution",
|
|
"api-integration",
|
|
"scheduling",
|
|
"monitoring",
|
|
],
|
|
},
|
|
}
|
|
|
|
// Communication animation component
|
|
const CommunicationFlow: React.FC<{
|
|
connections: WorkflowConnection[]
|
|
agents: Agent[]
|
|
}> = ({ connections, agents }) => {
|
|
const [activeFlows, setActiveFlows] = useState<string[]>([])
|
|
|
|
useEffect(() => {
|
|
const interval = setInterval(() => {
|
|
if (connections.length > 0) {
|
|
const randomConnection =
|
|
connections[Math.floor(Math.random() * connections.length)]
|
|
setActiveFlows((prev) => [...prev, randomConnection.id])
|
|
|
|
setTimeout(() => {
|
|
setActiveFlows((prev) =>
|
|
prev.filter((id) => id !== randomConnection.id)
|
|
)
|
|
}, 2000)
|
|
}
|
|
}, 3000)
|
|
|
|
return () => clearInterval(interval)
|
|
}, [connections])
|
|
|
|
return (
|
|
<svg
|
|
className="absolute inset-0 w-full h-full pointer-events-none z-10"
|
|
style={{ overflow: "visible" }}
|
|
>
|
|
{connections.map((connection) => {
|
|
const sourceAgent = agents.find((a) => a.id === connection.source)
|
|
const targetAgent = agents.find((a) => a.id === connection.target)
|
|
|
|
if (!sourceAgent || !targetAgent) return null
|
|
|
|
const isActive = activeFlows.includes(connection.id)
|
|
|
|
return (
|
|
<g key={connection.id}>
|
|
<defs>
|
|
<marker
|
|
id={`arrowhead-${connection.id}`}
|
|
markerWidth="10"
|
|
markerHeight="7"
|
|
refX="10"
|
|
refY="3.5"
|
|
orient="auto"
|
|
>
|
|
<polygon
|
|
points="0 0, 10 3.5, 0 7"
|
|
fill={isActive ? "#3b82f6" : "#94a3b8"}
|
|
/>
|
|
</marker>
|
|
</defs>
|
|
<line
|
|
x1={sourceAgent.position.x + 50}
|
|
y1={sourceAgent.position.y + 50}
|
|
x2={targetAgent.position.x + 50}
|
|
y2={targetAgent.position.y + 50}
|
|
stroke={isActive ? "#3b82f6" : "#94a3b8"}
|
|
strokeWidth={isActive ? "3" : "2"}
|
|
strokeDasharray={isActive ? "5,5" : "0"}
|
|
markerEnd={`url(#arrowhead-${connection.id})`}
|
|
className={isActive ? "animate-pulse" : ""}
|
|
/>
|
|
{isActive && (
|
|
<circle r="4" fill="#3b82f6" className="animate-pulse">
|
|
<animateMotion
|
|
dur="2s"
|
|
repeatCount="1"
|
|
path={`M${sourceAgent.position.x + 50},${sourceAgent.position.y + 50} L${targetAgent.position.x + 50},${targetAgent.position.y + 50}`}
|
|
/>
|
|
</circle>
|
|
)}
|
|
</g>
|
|
)
|
|
})}
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
// Agent card component
|
|
const AgentCard: React.FC<{
|
|
agent: Agent
|
|
isSelected?: boolean
|
|
onSelect?: () => void
|
|
onEdit?: () => void
|
|
onDelete?: () => void
|
|
onDrag?: (position: { x: number; y: number }) => void
|
|
isDragging?: boolean
|
|
}> = ({
|
|
agent,
|
|
isSelected,
|
|
onSelect,
|
|
onEdit,
|
|
onDelete,
|
|
onDrag,
|
|
isDragging,
|
|
}) => {
|
|
const dragRef = useRef<HTMLDivElement>(null)
|
|
const [isDragActive, setIsDragActive] = useState(false)
|
|
const config = agentTypeConfig[agent.type]
|
|
const Icon = config.icon
|
|
|
|
const handleMouseDown = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
if (!onDrag) return
|
|
|
|
setIsDragActive(true)
|
|
const startX = e.clientX - agent.position.x
|
|
const startY = e.clientY - agent.position.y
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
const newX = e.clientX - startX
|
|
const newY = e.clientY - startY
|
|
onDrag({ x: Math.max(0, newX), y: Math.max(0, newY) })
|
|
}
|
|
|
|
const handleMouseUp = () => {
|
|
setIsDragActive(false)
|
|
document.removeEventListener("mousemove", handleMouseMove)
|
|
document.removeEventListener("mouseup", handleMouseUp)
|
|
}
|
|
|
|
document.addEventListener("mousemove", handleMouseMove)
|
|
document.addEventListener("mouseup", handleMouseUp)
|
|
},
|
|
[agent.position, onDrag]
|
|
)
|
|
|
|
const getStatusColor = (status: AgentStatus) => {
|
|
switch (status) {
|
|
case "idle":
|
|
return "bg-gray-500"
|
|
case "working":
|
|
return "bg-blue-500"
|
|
case "completed":
|
|
return "bg-green-500"
|
|
case "error":
|
|
return "bg-red-500"
|
|
default:
|
|
return "bg-gray-500"
|
|
}
|
|
}
|
|
|
|
const getStatusIcon = (status: AgentStatus) => {
|
|
switch (status) {
|
|
case "idle":
|
|
return Clock
|
|
case "working":
|
|
return Activity
|
|
case "completed":
|
|
return CheckCircle
|
|
case "error":
|
|
return AlertCircle
|
|
default:
|
|
return Clock
|
|
}
|
|
}
|
|
|
|
const StatusIcon = getStatusIcon(agent.status)
|
|
|
|
return (
|
|
<Card
|
|
ref={dragRef}
|
|
className={cn(
|
|
"absolute w-64 transition-all duration-200 cursor-move select-none",
|
|
isSelected && "ring-2 ring-blue-500",
|
|
isDragActive && "shadow-lg scale-105",
|
|
isDragging && "opacity-50"
|
|
)}
|
|
style={{
|
|
left: agent.position.x,
|
|
top: agent.position.y,
|
|
transform: isDragActive ? "scale(1.05)" : "scale(1)",
|
|
}}
|
|
onMouseDown={handleMouseDown}
|
|
onClick={onSelect}
|
|
>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className={cn("p-2 rounded-lg text-white", config.color)}>
|
|
<Icon className="h-4 w-4" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-sm">{agent.name}</CardTitle>
|
|
<CardDescription className="text-xs capitalize">
|
|
{agent.type} Agent
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onEdit?.()
|
|
}}
|
|
>
|
|
<Edit className="h-3 w-3" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onDelete?.()
|
|
}}
|
|
>
|
|
<Trash2 className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid columns={1} gap={8}>
|
|
<div className="flex items-center gap-2">
|
|
<StatusIcon
|
|
className={cn("h-3 w-3", getStatusColor(agent.status))}
|
|
/>
|
|
<span className="text-xs font-medium capitalize">
|
|
{agent.status}
|
|
</span>
|
|
</div>
|
|
|
|
{agent.currentTask && (
|
|
<div className="bg-muted p-2 rounded text-xs">
|
|
<div className="font-medium">{agent.currentTask.title}</div>
|
|
<Progress
|
|
value={agent.currentTask.progress}
|
|
className="mt-1 h-1"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
{agent.capabilities.slice(0, 3).map((capability) => (
|
|
<Badge key={capability} variant="secondary" className="text-xs">
|
|
{capability}
|
|
</Badge>
|
|
))}
|
|
{agent.capabilities.length > 3 && (
|
|
<Badge variant="outline" className="text-xs">
|
|
+{agent.capabilities.length - 3}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<Grid
|
|
columns="auto auto"
|
|
gap={8}
|
|
className="text-xs text-muted-foreground"
|
|
style={{ justifyContent: "space-between" }}
|
|
>
|
|
<span>Tasks: {agent.performance.tasksCompleted}</span>
|
|
<span>Success: {agent.performance.successRate}%</span>
|
|
</Grid>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
// Agent creation dialog
|
|
const CreateAgentDialog: React.FC<{
|
|
onAgentCreate: (agent: Omit<Agent, "id" | "position" | "performance">) => void
|
|
}> = ({ onAgentCreate }) => {
|
|
const [open, setOpen] = useState(false)
|
|
const [formData, setFormData] = useState({
|
|
name: "",
|
|
type: "research" as AgentType,
|
|
description: "",
|
|
capabilities: [] as string[],
|
|
})
|
|
|
|
const handleSubmit = () => {
|
|
if (!formData.name) return
|
|
|
|
onAgentCreate({
|
|
name: formData.name,
|
|
type: formData.type,
|
|
status: "idle",
|
|
capabilities:
|
|
formData.capabilities.length > 0
|
|
? formData.capabilities
|
|
: agentTypeConfig[formData.type].capabilities,
|
|
connections: [],
|
|
description: formData.description,
|
|
})
|
|
|
|
setFormData({
|
|
name: "",
|
|
type: "research",
|
|
description: "",
|
|
capabilities: [],
|
|
})
|
|
setOpen(false)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Create Agent
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Agent</DialogTitle>
|
|
<DialogDescription>
|
|
Configure a new AI agent for your workflow
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<Grid columns={1} gap={16}>
|
|
<Grid columns={1} gap={8}>
|
|
<Label htmlFor="name">Agent Name</Label>
|
|
<Input
|
|
id="name"
|
|
value={formData.name}
|
|
onChange={(e) =>
|
|
setFormData((prev) => ({ ...prev, name: e.target.value }))
|
|
}
|
|
placeholder="Enter agent name"
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid columns={1} gap={8}>
|
|
<Label htmlFor="type">Agent Type</Label>
|
|
<Select
|
|
value={formData.type}
|
|
onValueChange={(value: AgentType) =>
|
|
setFormData((prev) => ({ ...prev, type: value }))
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.entries(agentTypeConfig).map(([type, config]) => {
|
|
const Icon = config.icon
|
|
return (
|
|
<SelectItem key={type} value={type}>
|
|
<div className="flex items-center gap-2">
|
|
<Icon className="h-4 w-4" />
|
|
<span className="capitalize">{type}</span>
|
|
</div>
|
|
</SelectItem>
|
|
)
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
</Grid>
|
|
|
|
<Grid columns={1} gap={8}>
|
|
<Label htmlFor="description">Description</Label>
|
|
<Textarea
|
|
id="description"
|
|
value={formData.description}
|
|
onChange={(e) =>
|
|
setFormData((prev) => ({
|
|
...prev,
|
|
description: e.target.value,
|
|
}))
|
|
}
|
|
placeholder="Describe the agent's purpose and role"
|
|
/>
|
|
</Grid>
|
|
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleSubmit} disabled={!formData.name}>
|
|
Create Agent
|
|
</Button>
|
|
</div>
|
|
</Grid>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
// Task assignment dialog
|
|
const TaskAssignmentDialog: React.FC<{
|
|
agents: Agent[]
|
|
onTaskAssign: (task: Task, agentId: string) => void
|
|
}> = ({ agents, onTaskAssign }) => {
|
|
const [open, setOpen] = useState(false)
|
|
const [taskData, setTaskData] = useState({
|
|
title: "",
|
|
description: "",
|
|
priority: "medium" as Task["priority"],
|
|
assignedTo: "",
|
|
estimatedTime: 0,
|
|
})
|
|
|
|
const handleSubmit = () => {
|
|
if (!taskData.title || !taskData.assignedTo) return
|
|
|
|
const task: Task = {
|
|
id: crypto.randomUUID(),
|
|
title: taskData.title,
|
|
description: taskData.description,
|
|
priority: taskData.priority,
|
|
status: "pending",
|
|
progress: 0,
|
|
createdAt: new Date(),
|
|
estimatedTime: taskData.estimatedTime || undefined,
|
|
}
|
|
|
|
onTaskAssign(task, taskData.assignedTo)
|
|
|
|
setTaskData({
|
|
title: "",
|
|
description: "",
|
|
priority: "medium",
|
|
assignedTo: "",
|
|
estimatedTime: 0,
|
|
})
|
|
setOpen(false)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
<DialogTrigger asChild>
|
|
<Button variant="outline">
|
|
<MessageSquare className="h-4 w-4 mr-2" />
|
|
Assign Task
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Assign New Task</DialogTitle>
|
|
<DialogDescription>
|
|
Create and assign a task to an agent
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<Grid columns={1} gap={16}>
|
|
<Grid columns={1} gap={8}>
|
|
<Label htmlFor="title">Task Title</Label>
|
|
<Input
|
|
id="title"
|
|
value={taskData.title}
|
|
onChange={(e) =>
|
|
setTaskData((prev) => ({ ...prev, title: e.target.value }))
|
|
}
|
|
placeholder="Enter task title"
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid columns={1} gap={8}>
|
|
<Label htmlFor="description">Description</Label>
|
|
<Textarea
|
|
id="description"
|
|
value={taskData.description}
|
|
onChange={(e) =>
|
|
setTaskData((prev) => ({
|
|
...prev,
|
|
description: e.target.value,
|
|
}))
|
|
}
|
|
placeholder="Describe the task requirements"
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid columns={{ min: 180, max: 2 }} gap={16}>
|
|
<Grid columns={1} gap={8}>
|
|
<Label htmlFor="priority">Priority</Label>
|
|
<Select
|
|
value={taskData.priority}
|
|
onValueChange={(value: Task["priority"]) =>
|
|
setTaskData((prev) => ({ ...prev, priority: value }))
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="low">Low</SelectItem>
|
|
<SelectItem value="medium">Medium</SelectItem>
|
|
<SelectItem value="high">High</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</Grid>
|
|
|
|
<Grid columns={1} gap={8}>
|
|
<Label htmlFor="estimatedTime">Estimated Time (hours)</Label>
|
|
<Input
|
|
id="estimatedTime"
|
|
type="number"
|
|
value={taskData.estimatedTime}
|
|
onChange={(e) =>
|
|
setTaskData((prev) => ({
|
|
...prev,
|
|
estimatedTime: parseInt(e.target.value) || 0,
|
|
}))
|
|
}
|
|
placeholder="0"
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
<Grid columns={1} gap={8}>
|
|
<Label htmlFor="assignedTo">Assign to Agent</Label>
|
|
<Select
|
|
value={taskData.assignedTo}
|
|
onValueChange={(value) =>
|
|
setTaskData((prev) => ({ ...prev, assignedTo: value }))
|
|
}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select an agent" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{agents
|
|
.filter((agent) => agent.status === "idle")
|
|
.map((agent) => (
|
|
<SelectItem key={agent.id} value={agent.id}>
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={cn(
|
|
"w-2 h-2 rounded-full",
|
|
agentTypeConfig[agent.type].color
|
|
)}
|
|
/>
|
|
{agent.name}
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</Grid>
|
|
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={handleSubmit}
|
|
disabled={!taskData.title || !taskData.assignedTo}
|
|
>
|
|
Assign Task
|
|
</Button>
|
|
</div>
|
|
</Grid>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
// Main component
|
|
export const AIAgents: React.FC<AIAgentsProps> = ({
|
|
agents: initialAgents = [],
|
|
onAgentCreate,
|
|
onTaskAssign,
|
|
onWorkflowSave,
|
|
workflowMode = "custom",
|
|
className,
|
|
}) => {
|
|
const [agents, setAgents] = useState<Agent[]>(initialAgents)
|
|
const [connections, setConnections] = useState<WorkflowConnection[]>([])
|
|
const [selectedAgent, setSelectedAgent] = useState<string | null>(null)
|
|
const [isWorkflowRunning, setIsWorkflowRunning] = useState(false)
|
|
const workflowRef = useRef<HTMLDivElement>(null)
|
|
|
|
// Initialize default agents if none provided
|
|
useEffect(() => {
|
|
if (initialAgents.length === 0) {
|
|
const defaultAgents: Agent[] = [
|
|
{
|
|
id: "1",
|
|
name: "Research Assistant",
|
|
type: "research",
|
|
capabilities: ["web-search", "data-analysis", "fact-checking"],
|
|
status: "idle",
|
|
position: { x: 50, y: 50 },
|
|
connections: ["2"],
|
|
performance: {
|
|
tasksCompleted: 12,
|
|
successRate: 95,
|
|
avgCompletionTime: 45,
|
|
},
|
|
},
|
|
{
|
|
id: "2",
|
|
name: "Code Generator",
|
|
type: "code",
|
|
capabilities: ["code-generation", "debugging", "testing"],
|
|
status: "working",
|
|
position: { x: 400, y: 50 },
|
|
connections: ["3"],
|
|
currentTask: {
|
|
id: "t1",
|
|
title: "Generate React Component",
|
|
description: "Create a reusable button component",
|
|
priority: "high",
|
|
status: "in-progress",
|
|
progress: 65,
|
|
createdAt: new Date(),
|
|
},
|
|
performance: {
|
|
tasksCompleted: 8,
|
|
successRate: 90,
|
|
avgCompletionTime: 38,
|
|
},
|
|
},
|
|
{
|
|
id: "3",
|
|
name: "Quality Analyst",
|
|
type: "analysis",
|
|
capabilities: ["code-review", "performance-analysis", "testing"],
|
|
status: "idle",
|
|
position: { x: 750, y: 50 },
|
|
connections: [],
|
|
performance: {
|
|
tasksCompleted: 15,
|
|
successRate: 98,
|
|
avgCompletionTime: 25,
|
|
},
|
|
},
|
|
]
|
|
setAgents(defaultAgents)
|
|
|
|
const defaultConnections: WorkflowConnection[] = [
|
|
{ id: "c1", source: "1", target: "2", animated: true },
|
|
{ id: "c2", source: "2", target: "3", animated: true },
|
|
]
|
|
setConnections(defaultConnections)
|
|
}
|
|
}, [initialAgents])
|
|
|
|
const handleAgentCreate = (
|
|
agentData: Omit<Agent, "id" | "position" | "performance">
|
|
) => {
|
|
const newAgent: Agent = {
|
|
...agentData,
|
|
id: crypto.randomUUID(),
|
|
position: { x: 100 + agents.length * 300, y: 100 },
|
|
performance: {
|
|
tasksCompleted: 0,
|
|
successRate: 100,
|
|
avgCompletionTime: 0,
|
|
},
|
|
}
|
|
|
|
setAgents((prev) => [...prev, newAgent])
|
|
onAgentCreate?.(agentData)
|
|
}
|
|
|
|
const handleTaskAssign = (task: Task, agentId: string) => {
|
|
setAgents((prev) =>
|
|
prev.map((agent) =>
|
|
agent.id === agentId
|
|
? { ...agent, currentTask: task, status: "working" as AgentStatus }
|
|
: agent
|
|
)
|
|
)
|
|
onTaskAssign?.(task, agentId)
|
|
|
|
// Simulate task progress
|
|
const progressInterval = setInterval(() => {
|
|
setAgents((prev) =>
|
|
prev.map((agent) => {
|
|
if (agent.id === agentId && agent.currentTask) {
|
|
const newProgress = Math.min(agent.currentTask.progress + 10, 100)
|
|
const updatedTask = { ...agent.currentTask, progress: newProgress }
|
|
|
|
if (newProgress === 100) {
|
|
clearInterval(progressInterval)
|
|
return {
|
|
...agent,
|
|
currentTask: undefined,
|
|
status: "completed" as AgentStatus,
|
|
performance: {
|
|
...agent.performance,
|
|
tasksCompleted: agent.performance.tasksCompleted + 1,
|
|
},
|
|
}
|
|
}
|
|
|
|
return { ...agent, currentTask: updatedTask }
|
|
}
|
|
return agent
|
|
})
|
|
)
|
|
}, 2000)
|
|
}
|
|
|
|
const handleAgentDrag = (
|
|
agentId: string,
|
|
position: { x: number; y: number }
|
|
) => {
|
|
setAgents((prev) =>
|
|
prev.map((agent) =>
|
|
agent.id === agentId ? { ...agent, position } : agent
|
|
)
|
|
)
|
|
}
|
|
|
|
const handleDeleteAgent = (agentId: string) => {
|
|
setAgents((prev) => prev.filter((agent) => agent.id !== agentId))
|
|
setConnections((prev) =>
|
|
prev.filter((conn) => conn.source !== agentId && conn.target !== agentId)
|
|
)
|
|
if (selectedAgent === agentId) {
|
|
setSelectedAgent(null)
|
|
}
|
|
}
|
|
|
|
const handleWorkflowToggle = () => {
|
|
setIsWorkflowRunning(!isWorkflowRunning)
|
|
|
|
if (!isWorkflowRunning) {
|
|
// Start workflow simulation
|
|
agents.forEach((agent, index) => {
|
|
setTimeout(() => {
|
|
setAgents((prev) =>
|
|
prev.map((a) =>
|
|
a.id === agent.id ? { ...a, status: "working" as AgentStatus } : a
|
|
)
|
|
)
|
|
}, index * 1000)
|
|
})
|
|
} else {
|
|
// Stop workflow
|
|
setAgents((prev) =>
|
|
prev.map((agent) => ({ ...agent, status: "idle" as AgentStatus }))
|
|
)
|
|
}
|
|
}
|
|
|
|
const handleSaveWorkflow = () => {
|
|
const workflow = { agents, connections }
|
|
onWorkflowSave?.(workflow)
|
|
}
|
|
|
|
return (
|
|
<div className={cn("w-full h-full", className)}>
|
|
<Tabs defaultValue="workflow" className="w-full h-full">
|
|
<div className="flex items-center justify-between p-4 border-b">
|
|
<TabsList>
|
|
<TabsTrigger value="workflow">Workflow Designer</TabsTrigger>
|
|
<TabsTrigger value="agents">Agent Manager</TabsTrigger>
|
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<CreateAgentDialog onAgentCreate={handleAgentCreate} />
|
|
<TaskAssignmentDialog
|
|
agents={agents}
|
|
onTaskAssign={handleTaskAssign}
|
|
/>
|
|
<Separator orientation="vertical" className="h-6" />
|
|
<Button
|
|
variant={isWorkflowRunning ? "destructive" : "default"}
|
|
onClick={handleWorkflowToggle}
|
|
>
|
|
{isWorkflowRunning ? (
|
|
<>
|
|
<CircleStop className="h-4 w-4 mr-2" />
|
|
Stop Workflow
|
|
</>
|
|
) : (
|
|
<>
|
|
<Play className="h-4 w-4 mr-2" />
|
|
Run Workflow
|
|
</>
|
|
)}
|
|
</Button>
|
|
<Button variant="outline" onClick={handleSaveWorkflow}>
|
|
Save Workflow
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<TabsContent value="workflow" className="flex-1 p-0">
|
|
<div
|
|
ref={workflowRef}
|
|
className="relative w-full h-[600px] bg-muted/20 overflow-hidden"
|
|
style={{
|
|
backgroundImage: `radial-gradient(circle, #e5e7eb 1px, transparent 1px)`,
|
|
backgroundSize: "20px 20px",
|
|
}}
|
|
>
|
|
<CommunicationFlow connections={connections} agents={agents} />
|
|
|
|
{agents.map((agent) => (
|
|
<AgentCard
|
|
key={agent.id}
|
|
agent={agent}
|
|
isSelected={selectedAgent === agent.id}
|
|
onSelect={() => setSelectedAgent(agent.id)}
|
|
onEdit={() => {
|
|
/* TODO: Implement edit */
|
|
}}
|
|
onDelete={() => handleDeleteAgent(agent.id)}
|
|
onDrag={(position) => handleAgentDrag(agent.id, position)}
|
|
/>
|
|
))}
|
|
|
|
{agents.length === 0 && (
|
|
<Grid
|
|
columns={1}
|
|
gap={8}
|
|
className="absolute inset-0 text-muted-foreground text-center"
|
|
style={{ placeContent: "center", justifyItems: "center" }}
|
|
>
|
|
<Bot className="h-12 w-12" />
|
|
<div>
|
|
<h3 className="text-lg font-medium">No Agents Created</h3>
|
|
<p className="text-sm">
|
|
Create your first AI agent to get started
|
|
</p>
|
|
</div>
|
|
</Grid>
|
|
)}
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="agents" className="flex-1 p-4">
|
|
<Grid columns={{ min: 280, max: 3 }} gap={16}>
|
|
{agents.map((agent) => {
|
|
const config = agentTypeConfig[agent.type]
|
|
const Icon = config.icon
|
|
|
|
return (
|
|
<Card key={agent.id}>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className={cn(
|
|
"p-3 rounded-lg text-white",
|
|
config.color
|
|
)}
|
|
>
|
|
<Icon className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-lg">{agent.name}</CardTitle>
|
|
<CardDescription className="capitalize">
|
|
{agent.type} Agent
|
|
</CardDescription>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid columns={1} gap={16}>
|
|
<Grid
|
|
columns="minmax(0, 1fr) auto"
|
|
gap={8}
|
|
style={{ alignItems: "center" }}
|
|
>
|
|
<span className="text-sm font-medium">Status</span>
|
|
<Badge
|
|
variant={
|
|
agent.status === "working" ? "default" : "secondary"
|
|
}
|
|
className="capitalize"
|
|
>
|
|
{agent.status}
|
|
</Badge>
|
|
</Grid>
|
|
|
|
{agent.currentTask && (
|
|
<Grid columns={1} gap={8}>
|
|
<div className="text-sm font-medium">
|
|
Current Task
|
|
</div>
|
|
<div className="bg-muted p-3 rounded-lg">
|
|
<div className="font-medium text-sm">
|
|
{agent.currentTask.title}
|
|
</div>
|
|
<Progress
|
|
value={agent.currentTask.progress}
|
|
className="mt-2"
|
|
/>
|
|
<div className="text-xs text-muted-foreground mt-1">
|
|
{agent.currentTask.progress}% Complete
|
|
</div>
|
|
</div>
|
|
</Grid>
|
|
)}
|
|
|
|
<Grid columns={1} gap={8}>
|
|
<div className="text-sm font-medium">Capabilities</div>
|
|
<div className="flex flex-wrap gap-1">
|
|
{agent.capabilities.map((capability) => (
|
|
<Badge
|
|
key={capability}
|
|
variant="outline"
|
|
className="text-xs"
|
|
>
|
|
{capability}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
</Grid>
|
|
|
|
<Grid columns={3} gap={8} className="text-center">
|
|
<div>
|
|
<div className="text-lg font-bold">
|
|
{agent.performance.tasksCompleted}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
Tasks
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-lg font-bold">
|
|
{agent.performance.successRate}%
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
Success
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-lg font-bold">
|
|
{agent.performance.avgCompletionTime}m
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
Avg Time
|
|
</div>
|
|
</div>
|
|
</Grid>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})}
|
|
</Grid>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="monitoring" className="flex-1 p-4">
|
|
<Grid columns={{ min: 380, max: 2 }} gap={24}>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Activity className="h-5 w-5" />
|
|
System Overview
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Grid columns={1} gap={16}>
|
|
<Grid columns={{ min: 130, max: 2 }} gap={16}>
|
|
<div className="text-center p-4 bg-muted rounded-lg">
|
|
<div className="text-2xl font-bold">{agents.length}</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
Total Agents
|
|
</div>
|
|
</div>
|
|
<div className="text-center p-4 bg-muted rounded-lg">
|
|
<div className="text-2xl font-bold">
|
|
{agents.filter((a) => a.status === "working").length}
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
Active
|
|
</div>
|
|
</div>
|
|
</Grid>
|
|
|
|
<Grid columns={1} gap={8} className="text-sm">
|
|
<Grid
|
|
columns="minmax(0, 1fr) auto"
|
|
gap={8}
|
|
style={{ alignItems: "center" }}
|
|
>
|
|
<span>Workflow Status</span>
|
|
<Badge
|
|
variant={isWorkflowRunning ? "default" : "secondary"}
|
|
>
|
|
{isWorkflowRunning ? "Running" : "Stopped"}
|
|
</Badge>
|
|
</Grid>
|
|
<Grid columns="minmax(0, 1fr) auto" gap={8}>
|
|
<span>Connected Agents</span>
|
|
<span>{connections.length} connections</span>
|
|
</Grid>
|
|
</Grid>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Network className="h-5 w-5" />
|
|
Agent Performance
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ScrollArea className="h-64">
|
|
<Grid columns={1} gap={12}>
|
|
{agents.map((agent) => (
|
|
<Grid
|
|
key={agent.id}
|
|
columns="minmax(0, 1fr) auto"
|
|
gap={12}
|
|
className="p-3 bg-muted rounded-lg"
|
|
style={{ alignItems: "center" }}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div
|
|
className={cn(
|
|
"w-3 h-3 rounded-full",
|
|
agentTypeConfig[agent.type].color
|
|
)}
|
|
/>
|
|
<div>
|
|
<div className="font-medium text-sm">
|
|
{agent.name}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground capitalize">
|
|
{agent.type}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-right">
|
|
<div className="text-sm font-medium">
|
|
{agent.performance.successRate}%
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
success rate
|
|
</div>
|
|
</div>
|
|
</Grid>
|
|
))}
|
|
</Grid>
|
|
</ScrollArea>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AIAgents
|