true message

This commit is contained in:
Manus
2026-03-20 16:39:29 -04:00
parent b18e6e244f
commit 159a89a156
17 changed files with 3054 additions and 191 deletions

View File

@@ -0,0 +1,91 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AgentCreateModal } from "./AgentCreateModal";
// Mock trpc
vi.mock("@/lib/trpc", () => ({
trpc: {
ollama: {
models: {
useQuery: () => ({
data: {
models: [
{ id: "deepseek-v3.2", provider: "Ollama" },
{ id: "gpt-4", provider: "OpenAI" },
],
},
isLoading: false,
}),
},
},
agents: {
create: {
useMutation: () => ({
mutateAsync: vi.fn().mockResolvedValue({}),
isPending: false,
}),
},
},
},
}));
describe("AgentCreateModal", () => {
it("should render create modal when open is true", () => {
render(
<AgentCreateModal
open={true}
onOpenChange={() => {}}
/>
);
expect(screen.getByText(/Deploy New Agent/)).toBeInTheDocument();
});
it("should have required form fields", () => {
render(
<AgentCreateModal
open={true}
onOpenChange={() => {}}
/>
);
expect(screen.getByLabelText(/Agent Name/)).toBeInTheDocument();
expect(screen.getByLabelText(/Description/)).toBeInTheDocument();
expect(screen.getByLabelText(/Role/)).toBeInTheDocument();
expect(screen.getByLabelText(/Provider/)).toBeInTheDocument();
expect(screen.getByLabelText(/Model/)).toBeInTheDocument();
});
it("should have LLM parameter controls", () => {
render(
<AgentCreateModal
open={true}
onOpenChange={() => {}}
/>
);
expect(screen.getByLabelText(/Temperature/)).toBeInTheDocument();
expect(screen.getByLabelText(/Max Tokens/)).toBeInTheDocument();
expect(screen.getByLabelText(/System Prompt/)).toBeInTheDocument();
});
it("should disable create button when name is empty", () => {
render(
<AgentCreateModal
open={true}
onOpenChange={() => {}}
/>
);
const createButton = screen.getByRole("button", { name: /Create Agent/i });
expect(createButton).toBeDisabled();
});
it("should have cancel and create buttons", () => {
render(
<AgentCreateModal
open={true}
onOpenChange={() => {}}
/>
);
expect(screen.getByRole("button", { name: /Cancel/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Create Agent/i })).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,257 @@
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { trpc } from "@/lib/trpc";
import { toast } from "sonner";
import { Loader2, Plus } from "lucide-react";
interface AgentCreateModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess?: () => void;
}
const AGENT_ROLES = [
{ value: "developer", label: "Developer - Code generation & testing" },
{ value: "researcher", label: "Researcher - Data analysis & research" },
{ value: "executor", label: "Executor - Task automation" },
{ value: "monitor", label: "Monitor - System monitoring" },
];
const PROVIDERS = [
{ value: "Ollama", label: "Ollama (Local/Cloud)" },
{ value: "OpenAI", label: "OpenAI (GPT)" },
{ value: "Anthropic", label: "Anthropic (Claude)" },
];
export function AgentCreateModal({ open, onOpenChange, onSuccess }: AgentCreateModalProps) {
const [formData, setFormData] = useState({
name: "",
description: "",
role: "developer",
provider: "Ollama",
model: "deepseek-v3.2",
temperature: 0.7,
maxTokens: 2048,
systemPrompt: "",
});
const [isLoading, setIsLoading] = useState(false);
const { data: models } = trpc.ollama.models.useQuery();
const createMutation = trpc.agents.create.useMutation({
onSuccess: () => {
toast.success("Agent created successfully");
setFormData({
name: "",
description: "",
role: "developer",
provider: "Ollama",
model: "deepseek-v3.2",
temperature: 0.7,
maxTokens: 2048,
systemPrompt: "",
});
onOpenChange(false);
onSuccess?.();
},
onError: (error) => {
toast.error(`Failed to create agent: ${error.message}`);
},
});
const handleCreate = async () => {
if (!formData.name.trim()) {
toast.error("Agent name is required");
return;
}
setIsLoading(true);
try {
await createMutation.mutateAsync({
name: formData.name,
description: formData.description,
role: formData.role,
provider: formData.provider,
model: formData.model,
temperature: formData.temperature,
maxTokens: formData.maxTokens,
systemPrompt: formData.systemPrompt,
allowedTools: [],
});
} finally {
setIsLoading(false);
}
};
const availableModels = models?.models
?.filter((m: any) => m.provider === formData.provider || formData.provider === "Ollama")
.map((m: any) => m.id) || [];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Deploy New Agent</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* Basic Info */}
<div className="space-y-2">
<Label htmlFor="name">Agent Name *</Label>
<Input
id="name"
placeholder="e.g., Code Reviewer Agent"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="font-mono"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="What is this agent responsible for?"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={2}
className="font-mono text-sm"
/>
</div>
{/* Role & Provider */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="role">Role *</Label>
<Select value={formData.role} onValueChange={(value) => setFormData({ ...formData, role: value })}>
<SelectTrigger id="role" className="font-mono">
<SelectValue />
</SelectTrigger>
<SelectContent>
{AGENT_ROLES.map((role) => (
<SelectItem key={role.value} value={role.value}>
{role.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="provider">Provider *</Label>
<Select value={formData.provider} onValueChange={(value) => setFormData({ ...formData, provider: value, model: "" })}>
<SelectTrigger id="provider" className="font-mono">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PROVIDERS.map((provider) => (
<SelectItem key={provider.value} value={provider.value}>
{provider.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Model Selection */}
<div className="space-y-2">
<Label htmlFor="model">Model *</Label>
<Select value={formData.model} onValueChange={(value) => setFormData({ ...formData, model: value })}>
<SelectTrigger id="model" className="font-mono">
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableModels.length > 0 ? (
availableModels.map((model: string) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))
) : (
<SelectItem value="default" disabled>
No models available
</SelectItem>
)}
</SelectContent>
</Select>
</div>
{/* LLM Parameters */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="temperature">
Temperature: {formData.temperature.toFixed(2)}
</Label>
<Input
id="temperature"
type="range"
min="0"
max="2"
step="0.01"
value={formData.temperature}
onChange={(e) => setFormData({ ...formData, temperature: parseFloat(e.target.value) })}
className="cursor-pointer"
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxTokens">Max Tokens</Label>
<Input
id="maxTokens"
type="number"
value={formData.maxTokens}
onChange={(e) => setFormData({ ...formData, maxTokens: parseInt(e.target.value) })}
className="font-mono"
/>
</div>
</div>
{/* System Prompt */}
<div className="space-y-2">
<Label htmlFor="systemPrompt">System Prompt</Label>
<Textarea
id="systemPrompt"
placeholder="Define the agent's behavior and instructions..."
value={formData.systemPrompt}
onChange={(e) => setFormData({ ...formData, systemPrompt: e.target.value })}
rows={4}
className="font-mono text-sm"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleCreate} disabled={isLoading || createMutation.isPending || !formData.name.trim()}>
{isLoading || createMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Creating...
</>
) : (
<>
<Plus className="w-4 h-4 mr-2" />
Create Agent
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,89 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { AgentDetailModal } from "./AgentDetailModal";
// Mock trpc
vi.mock("@/lib/trpc", () => ({
trpc: {
agents: {
update: {
useMutation: () => ({
mutateAsync: vi.fn().mockResolvedValue({}),
isPending: false,
}),
},
},
},
}));
describe("AgentDetailModal", () => {
const mockAgent = {
id: 1,
name: "Test Agent",
description: "Test Description",
role: "developer",
model: "gpt-4",
provider: "OpenAI",
temperature: "0.7",
maxTokens: 2048,
topP: "1.0",
frequencyPenalty: "0.0",
presencePenalty: "0.0",
systemPrompt: "You are a helpful assistant",
allowedTools: ["browser", "shell"],
allowedDomains: ["example.com"],
maxRequestsPerHour: 100,
isActive: true,
tags: ["test"],
createdAt: new Date(),
updatedAt: new Date(),
};
it("should not render when agent is null", () => {
const { container } = render(
<AgentDetailModal
agent={null}
open={true}
onOpenChange={() => {}}
/>
);
expect(container.firstChild).toBeNull();
});
it("should render modal when agent is provided and open is true", () => {
render(
<AgentDetailModal
agent={mockAgent}
open={true}
onOpenChange={() => {}}
/>
);
expect(screen.getByText(/Agent Configuration:/)).toBeInTheDocument();
});
it("should display agent name in title", () => {
render(
<AgentDetailModal
agent={mockAgent}
open={true}
onOpenChange={() => {}}
/>
);
expect(screen.getByText(/Test Agent/)).toBeInTheDocument();
});
it("should have tabs for different sections", () => {
render(
<AgentDetailModal
agent={mockAgent}
open={true}
onOpenChange={() => {}}
/>
);
expect(screen.getByRole("tab", { name: /General/i })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: /LLM/i })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: /Tools/i })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: /Info/i })).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,383 @@
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { trpc } from "@/lib/trpc";
import { toast } from "sonner";
import { Loader2, Save, X } from "lucide-react";
interface Agent {
id: number;
name: string;
description: string | null;
role: string;
model: string;
provider: string;
temperature: string | number;
maxTokens: number;
topP: string | number;
frequencyPenalty: string | number;
presencePenalty: string | number;
systemPrompt: string | null;
allowedTools: string[];
allowedDomains: string[];
maxRequestsPerHour: number;
isActive: boolean;
tags: string[];
createdAt: Date;
updatedAt: Date;
}
interface AgentDetailModalProps {
agent: Agent | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onSave?: () => void;
}
export function AgentDetailModal({ agent, open, onOpenChange, onSave }: AgentDetailModalProps) {
const [formData, setFormData] = useState<Partial<Agent>>(agent ? { ...agent } : {});
const [isLoading, setIsLoading] = useState(false);
const updateMutation = trpc.agents.update.useMutation({
onSuccess: () => {
toast.success("Agent configuration updated");
onOpenChange(false);
onSave?.();
},
onError: (error) => {
toast.error(`Failed to update agent: ${error.message}`);
},
});
const handleSave = async () => {
if (!agent) return;
setIsLoading(true);
try {
await updateMutation.mutateAsync({
id: agent.id,
name: formData.name || agent.name,
description: formData.description || undefined,
temperature: typeof formData.temperature === "string" ? parseFloat(formData.temperature) : (formData.temperature || 0.7),
maxTokens: formData.maxTokens || 2048,
systemPrompt: formData.systemPrompt || undefined,
isActive: formData.isActive !== undefined ? formData.isActive : true,
});
} finally {
setIsLoading(false);
}
};
if (!agent) return null;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
<span>Agent Configuration: {agent.name}</span>
<Button
variant="ghost"
size="sm"
onClick={() => onOpenChange(false)}
className="h-6 w-6 p-0"
>
<X className="w-4 h-4" />
</Button>
</DialogTitle>
</DialogHeader>
<Tabs defaultValue="general" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="llm">LLM</TabsTrigger>
<TabsTrigger value="tools">Tools</TabsTrigger>
<TabsTrigger value="info">Info</TabsTrigger>
</TabsList>
{/* General Tab */}
<TabsContent value="general" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name">Agent Name</Label>
<Input
id="name"
value={formData.name ?? ""}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="font-mono"
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description ?? ""}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
rows={3}
className="font-mono text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="role">Role</Label>
<Input
id="role"
value={formData.role || ""}
disabled
className="font-mono bg-muted"
/>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<div className="flex items-center gap-2">
<Badge variant={formData.isActive ? "default" : "secondary"}>
{formData.isActive ? "Active" : "Inactive"}
</Badge>
<Button
size="sm"
variant="outline"
onClick={() => setFormData({ ...formData, isActive: !formData.isActive })}
>
Toggle
</Button>
</div>
</div>
</div>
</TabsContent>
{/* LLM Tab */}
<TabsContent value="llm" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="model">Model</Label>
<Input
id="model"
value={formData.model || ""}
disabled
className="font-mono bg-muted"
/>
</div>
<div className="space-y-2">
<Label htmlFor="provider">Provider</Label>
<Input
id="provider"
value={formData.provider || ""}
disabled
className="font-mono bg-muted"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="temperature">
Temperature: {typeof formData.temperature === "string" ? parseFloat(formData.temperature).toFixed(2) : formData.temperature?.toFixed(2)}
</Label>
<Input
id="temperature"
type="range"
min="0"
max="2"
step="0.01"
value={typeof formData.temperature === "string" ? parseFloat(formData.temperature) : formData.temperature || 0.7}
onChange={(e) => setFormData({ ...formData, temperature: parseFloat(e.target.value) })}
className="cursor-pointer"
/>
</div>
<div className="space-y-2">
<Label htmlFor="maxTokens">Max Tokens</Label>
<Input
id="maxTokens"
type="number"
value={formData.maxTokens || 2048}
onChange={(e) => setFormData({ ...formData, maxTokens: parseInt(e.target.value) })}
className="font-mono"
/>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="topP">
Top P: {typeof formData.topP === "string" ? parseFloat(formData.topP).toFixed(2) : formData.topP?.toFixed(2)}
</Label>
<Input
id="topP"
type="range"
min="0"
max="1"
step="0.01"
value={typeof formData.topP === "string" ? parseFloat(formData.topP) : formData.topP || 1.0}
onChange={(e) => setFormData({ ...formData, topP: parseFloat(e.target.value) })}
className="cursor-pointer"
/>
</div>
<div className="space-y-2">
<Label htmlFor="frequencyPenalty">
Freq Penalty: {typeof formData.frequencyPenalty === "string" ? parseFloat(formData.frequencyPenalty).toFixed(2) : formData.frequencyPenalty?.toFixed(2)}
</Label>
<Input
id="frequencyPenalty"
type="range"
min="-2"
max="2"
step="0.01"
value={typeof formData.frequencyPenalty === "string" ? parseFloat(formData.frequencyPenalty) : formData.frequencyPenalty || 0}
onChange={(e) => setFormData({ ...formData, frequencyPenalty: parseFloat(e.target.value) })}
className="cursor-pointer"
/>
</div>
<div className="space-y-2">
<Label htmlFor="presencePenalty">
Pres Penalty: {typeof formData.presencePenalty === "string" ? parseFloat(formData.presencePenalty).toFixed(2) : formData.presencePenalty?.toFixed(2)}
</Label>
<Input
id="presencePenalty"
type="range"
min="-2"
max="2"
step="0.01"
value={typeof formData.presencePenalty === "string" ? parseFloat(formData.presencePenalty) : formData.presencePenalty || 0}
onChange={(e) => setFormData({ ...formData, presencePenalty: parseFloat(e.target.value) })}
className="cursor-pointer"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="systemPrompt">System Prompt</Label>
<Textarea
id="systemPrompt"
value={formData.systemPrompt || ""}
onChange={(e) => setFormData({ ...formData, systemPrompt: e.target.value })}
rows={5}
className="font-mono text-sm"
placeholder="Enter the system prompt that defines agent behavior..."
/>
</div>
</TabsContent>
{/* Tools Tab */}
<TabsContent value="tools" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-sm">Allowed Tools</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{formData.allowedTools && formData.allowedTools.length > 0 ? (
formData.allowedTools.map((tool) => (
<Badge key={tool} variant="secondary">
{tool}
</Badge>
))
) : (
<p className="text-sm text-muted-foreground">No tools assigned</p>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm">Allowed Domains</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{formData.allowedDomains && formData.allowedDomains.length > 0 ? (
formData.allowedDomains.map((domain) => (
<Badge key={domain} variant="outline">
{domain}
</Badge>
))
) : (
<p className="text-sm text-muted-foreground">No domain restrictions</p>
)}
</div>
</CardContent>
</Card>
<div className="space-y-2">
<Label htmlFor="maxRequestsPerHour">Max Requests Per Hour</Label>
<Input
id="maxRequestsPerHour"
type="number"
value={formData.maxRequestsPerHour || 100}
disabled
className="font-mono bg-muted"
/>
</div>
</TabsContent>
{/* Info Tab */}
<TabsContent value="info" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-sm">Metadata</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm font-mono">
<div className="flex justify-between">
<span className="text-muted-foreground">Created:</span>
<span>{new Date(agent.createdAt).toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Updated:</span>
<span>{new Date(agent.updatedAt).toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Agent ID:</span>
<span>{agent.id}</span>
</div>
</div>
</CardContent>
</Card>
{formData.tags && formData.tags.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-sm">Tags</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{formData.tags.map((tag) => (
<Badge key={tag} variant="outline">
{tag}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isLoading || updateMutation.isPending}>
{isLoading || updateMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-4 h-4 mr-2" />
Save Changes
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -8,6 +8,15 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Bot,
Play,
@@ -24,95 +33,22 @@ import {
Mail,
FileText,
Eye,
Loader2,
AlertCircle,
} from "lucide-react";
import { motion } from "framer-motion";
import { useState } from "react";
import { toast } from "sonner";
import { trpc } from "@/lib/trpc";
import { AgentDetailModal } from "@/components/AgentDetailModal";
import { AgentCreateModal } from "@/components/AgentCreateModal";
const AGENT_CARD_BG = "https://d2xsxph8kpxj0f.cloudfront.net/97147719/ZEGAT83geRq9CNvryykaQv/agent-card-bg-4pCQxardRUWZ77WDF6yvS3.webp";
const AGENTS_DATA = [
{
id: "agent-coder",
name: "Coder Agent",
description: "Пишет и тестирует Go-модули, рефакторит код, создаёт новые скиллы",
model: "claude-3.5-sonnet",
provider: "Anthropic",
status: "running",
icon: Terminal,
tasks_completed: 147,
tasks_active: 3,
cpu: 45,
mem: 512,
uptime: "2d 14h 23m",
skills: ["go_compiler", "git_ops", "test_runner", "code_review"],
node: "goclaw-worker-01",
},
{
id: "agent-browser",
name: "Browser Agent",
description: "Управляет Chromium, выполняет веб-скрапинг и автоматизацию",
model: "gpt-4o",
provider: "OpenAI",
status: "running",
icon: Globe,
tasks_completed: 89,
tasks_active: 1,
cpu: 62,
mem: 1024,
uptime: "2d 14h 23m",
skills: ["chromedp", "screenshot", "form_fill", "web_scrape"],
node: "goclaw-worker-01",
},
{
id: "agent-mail",
name: "Mail Agent",
description: "Мониторит почту, классифицирует письма, отправляет ответы",
model: "gpt-4o-mini",
provider: "OpenAI",
status: "idle",
icon: Mail,
tasks_completed: 234,
tasks_active: 0,
cpu: 2,
mem: 128,
uptime: "2d 14h 23m",
skills: ["imap_reader", "smtp_sender", "email_classifier"],
node: "goclaw-worker-02",
},
{
id: "agent-monitor",
name: "Monitor Agent",
description: "Следит за состоянием кластера, логами и метриками",
model: "llama-3.1-8b",
provider: "Ollama (local)",
status: "running",
icon: Eye,
tasks_completed: 1205,
tasks_active: 5,
cpu: 8,
mem: 256,
uptime: "2d 14h 23m",
skills: ["log_parser", "metric_collector", "alert_sender"],
node: "goclaw-manager-01",
},
{
id: "agent-docs",
name: "Docs Agent",
description: "Работает с документами: PDF, DOCX, создаёт отчёты",
model: "claude-3-haiku",
provider: "Anthropic",
status: "error",
icon: FileText,
tasks_completed: 56,
tasks_active: 0,
cpu: 0,
mem: 0,
uptime: "0h 0m",
skills: ["pdf_parser", "docx_writer", "summarizer"],
node: "goclaw-worker-02",
},
];
const ROLE_ICONS: Record<string, any> = {
developer: Terminal,
researcher: Brain,
executor: Zap,
monitor: Eye,
};
function getStatusConfig(status: string) {
switch (status) {
@@ -128,7 +64,61 @@ function getStatusConfig(status: string) {
}
export default function Agents() {
const [agents] = useState(AGENTS_DATA);
const [selectedAgent, setSelectedAgent] = useState<any>(null);
const [detailModalOpen, setDetailModalOpen] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [agentToDelete, setAgentToDelete] = useState<number | null>(null);
// Fetch agents from database
const { data: agents = [], isLoading, refetch } = trpc.agents.list.useQuery();
const deleteMutation = trpc.agents.delete.useMutation({
onSuccess: () => {
toast.success("Agent deleted successfully");
setDeleteConfirmOpen(false);
setAgentToDelete(null);
refetch();
},
onError: (error) => {
toast.error(`Failed to delete agent: ${error.message}`);
},
});
const handleDeleteClick = (agentId: number) => {
setAgentToDelete(agentId);
setDeleteConfirmOpen(true);
};
const handleConfirmDelete = async () => {
if (agentToDelete) {
await deleteMutation.mutateAsync({ id: agentToDelete });
}
};
const handleEditAgent = (agent: any) => {
setSelectedAgent(agent);
setDetailModalOpen(true);
};
const handleCreateSuccess = () => {
refetch();
};
const handleDetailSave = () => {
refetch();
};
if (isLoading) {
return (
<div className="flex items-center justify-center h-96">
<div className="text-center">
<Loader2 className="w-8 h-8 animate-spin mx-auto mb-2 text-primary" />
<p className="text-muted-foreground">Loading agents...</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
@@ -137,130 +127,197 @@ export default function Agents() {
<div>
<h2 className="text-xl font-bold text-foreground">Agent Fleet</h2>
<p className="text-sm text-muted-foreground font-mono mt-1">
{agents.filter(a => a.status === "running").length} running &middot; {agents.filter(a => a.status === "idle").length} idle &middot; {agents.filter(a => a.status === "error").length} error
{agents.length} total agents
</p>
</div>
<Button
size="sm"
className="bg-primary/15 text-primary border border-primary/30 hover:bg-primary/25"
onClick={() => toast("Feature coming soon")}
onClick={() => setCreateModalOpen(true)}
>
<Plus className="w-4 h-4 mr-2" />
Deploy Agent
</Button>
</div>
{/* Agent cards grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{agents.map((agent, i) => {
const sc = getStatusConfig(agent.status);
const Icon = agent.icon;
return (
<motion.div
key={agent.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.08 }}
{/* Empty State */}
{agents.length === 0 ? (
<Card className="bg-card border-border/50">
<CardContent className="p-12 text-center">
<AlertCircle className="w-12 h-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<h3 className="text-lg font-semibold text-foreground mb-2">No Agents Deployed</h3>
<p className="text-sm text-muted-foreground mb-6">
Start by deploying your first AI agent to the cluster.
</p>
<Button
onClick={() => setCreateModalOpen(true)}
className="bg-primary/15 text-primary border border-primary/30 hover:bg-primary/25"
>
<Card className={`bg-card border-border/50 hover:border-primary/30 transition-all ${sc.glow}`}>
<CardContent className="p-5">
{/* Top row */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-secondary/50 border border-border/50 flex items-center justify-center">
<Icon className={`w-5 h-5 ${sc.color}`} />
</div>
<div>
<h3 className="text-sm font-semibold text-foreground">{agent.name}</h3>
<p className="text-[11px] text-muted-foreground mt-0.5">{agent.description}</p>
</div>
</div>
<Badge variant="outline" className={`text-[10px] font-mono ${sc.badge}`}>
<span className={`w-1.5 h-1.5 rounded-full ${sc.bg} mr-1.5 ${agent.status === "running" ? "pulse-indicator" : ""}`} />
{agent.status.toUpperCase()}
</Badge>
</div>
<Plus className="w-4 h-4 mr-2" />
Deploy First Agent
</Button>
</CardContent>
</Card>
) : (
/* Agent cards grid */
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{agents.map((agent: any, i: number) => {
const sc = getStatusConfig(agent.status || "idle");
const Icon = ROLE_ICONS[agent.role] || Bot;
const temperature = typeof agent.temperature === "string" ? parseFloat(agent.temperature) : agent.temperature;
{/* Model & Node info */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
<div className="flex items-center gap-1.5 mb-1">
<Brain className="w-3 h-3 text-primary" />
<span className="text-[10px] text-muted-foreground font-mono">MODEL</span>
return (
<motion.div
key={agent.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.08 }}
>
<Card className={`bg-card border-border/50 hover:border-primary/30 transition-all cursor-pointer ${sc.glow}`} onClick={() => handleEditAgent(agent)}>
<CardContent className="p-5">
{/* Top row */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-secondary/50 border border-border/50 flex items-center justify-center">
<Icon className={`w-5 h-5 ${sc.color}`} />
</div>
<div>
<h3 className="text-sm font-semibold text-foreground">{agent.name}</h3>
<p className="text-[11px] text-muted-foreground mt-0.5">{agent.description || "No description"}</p>
</div>
</div>
<div className="text-xs font-mono font-medium text-primary">{agent.model}</div>
<div className="text-[10px] font-mono text-muted-foreground">{agent.provider}</div>
<Badge variant="outline" className={`text-[10px] font-mono ${sc.badge}`}>
<span className={`w-1.5 h-1.5 rounded-full ${sc.bg} mr-1.5 ${agent.isActive ? "pulse-indicator" : ""}`} />
{agent.isActive ? "ACTIVE" : "INACTIVE"}
</Badge>
</div>
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
<div className="flex items-center gap-1.5 mb-1">
<Cpu className="w-3 h-3 text-primary" />
<span className="text-[10px] text-muted-foreground font-mono">NODE</span>
{/* Model & Node info */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
<div className="flex items-center gap-1.5 mb-1">
<Brain className="w-3 h-3 text-primary" />
<span className="text-[10px] text-muted-foreground font-mono">MODEL</span>
</div>
<div className="text-xs font-mono font-medium text-primary">{agent.model}</div>
<div className="text-[10px] font-mono text-muted-foreground">{agent.provider}</div>
</div>
<div className="text-xs font-mono font-medium text-foreground">{agent.node}</div>
<div className="text-[10px] font-mono text-muted-foreground">
CPU: {agent.cpu}% &middot; MEM: {agent.mem}MB
<div className="p-2.5 rounded-md bg-secondary/30 border border-border/20">
<div className="flex items-center gap-1.5 mb-1">
<Cpu className="w-3 h-3 text-primary" />
<span className="text-[10px] text-muted-foreground font-mono">CONFIG</span>
</div>
<div className="text-xs font-mono font-medium text-foreground">T: {temperature.toFixed(2)}</div>
<div className="text-[10px] font-mono text-muted-foreground">Tokens: {agent.maxTokens}</div>
</div>
</div>
</div>
{/* Metrics row */}
<div className="flex items-center gap-4 mb-4 text-[10px] font-mono">
<div className="flex items-center gap-1">
<Zap className="w-3 h-3 text-neon-amber" />
<span className="text-muted-foreground">Tasks:</span>
<span className="text-foreground font-medium">{agent.tasks_completed}</span>
{/* Metrics row */}
<div className="flex items-center gap-4 mb-4 text-[10px] font-mono">
<div className="flex items-center gap-1">
<Zap className="w-3 h-3 text-neon-amber" />
<span className="text-muted-foreground">Role:</span>
<span className="text-foreground font-medium capitalize">{agent.role}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3 text-primary" />
<span className="text-muted-foreground">Created:</span>
<span className="text-foreground font-medium">{new Date(agent.createdAt).toLocaleDateString()}</span>
</div>
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3 text-primary" />
<span className="text-muted-foreground">Uptime:</span>
<span className="text-foreground font-medium">{agent.uptime}</span>
</div>
<div className="flex items-center gap-1">
<Play className="w-3 h-3 text-neon-green" />
<span className="text-muted-foreground">Active:</span>
<span className="text-neon-green font-medium">{agent.tasks_active}</span>
</div>
</div>
{/* Skills */}
<div className="mb-4">
<span className="text-[10px] text-muted-foreground font-mono block mb-1.5">SKILLS</span>
<div className="flex flex-wrap gap-1.5">
{agent.skills.map((skill) => (
<span
key={skill}
className="px-2 py-0.5 rounded text-[10px] font-mono bg-primary/10 text-primary border border-primary/20"
>
{skill}
</span>
))}
</div>
</div>
{/* Actions */}
<div className="flex items-center gap-2 pt-3 border-t border-border/30">
{agent.status === "running" ? (
<Button size="sm" variant="outline" className="h-7 text-[11px] text-neon-amber border-neon-amber/30 hover:bg-neon-amber/10" onClick={() => toast("Feature coming soon")}>
<Pause className="w-3 h-3 mr-1" /> Pause
</Button>
) : (
<Button size="sm" variant="outline" className="h-7 text-[11px] text-neon-green border-neon-green/30 hover:bg-neon-green/10" onClick={() => toast("Feature coming soon")}>
<Play className="w-3 h-3 mr-1" /> Start
</Button>
{/* Tools */}
{agent.allowedTools && agent.allowedTools.length > 0 && (
<div className="mb-4">
<span className="text-[10px] text-muted-foreground font-mono block mb-1.5">TOOLS</span>
<div className="flex flex-wrap gap-1.5">
{agent.allowedTools.map((tool: string) => (
<span
key={tool}
className="px-2 py-0.5 rounded text-[10px] font-mono bg-primary/10 text-primary border border-primary/20"
>
{tool}
</span>
))}
</div>
</div>
)}
<Button size="sm" variant="outline" className="h-7 text-[11px] text-primary border-primary/30 hover:bg-primary/10" onClick={() => toast("Feature coming soon")}>
<RotateCcw className="w-3 h-3 mr-1" /> Restart
</Button>
<Button size="sm" variant="outline" className="h-7 text-[11px] text-neon-red border-neon-red/30 hover:bg-neon-red/10 ml-auto" onClick={() => toast("Feature coming soon")}>
<Trash2 className="w-3 h-3 mr-1" /> Remove
</Button>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</div>
{/* Actions */}
<div className="flex items-center gap-2 pt-3 border-t border-border/30">
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] text-primary border-primary/30 hover:bg-primary/10"
onClick={(e) => {
e.stopPropagation();
handleEditAgent(agent);
}}
>
Edit
</Button>
<Button
size="sm"
variant="outline"
className="h-7 text-[11px] text-neon-red border-neon-red/30 hover:bg-neon-red/10 ml-auto"
onClick={(e) => {
e.stopPropagation();
handleDeleteClick(agent.id);
}}
>
<Trash2 className="w-3 h-3 mr-1" /> Delete
</Button>
</div>
</CardContent>
</Card>
</motion.div>
);
})}
</div>
)}
{/* Modals */}
<AgentDetailModal
agent={selectedAgent}
open={detailModalOpen}
onOpenChange={setDetailModalOpen}
onSave={handleDetailSave}
/>
<AgentCreateModal
open={createModalOpen}
onOpenChange={setCreateModalOpen}
onSuccess={handleCreateSuccess}
/>
{/* Delete Confirmation Dialog */}
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Agent</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this agent? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="flex gap-3">
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmDelete}
disabled={deleteMutation.isPending}
className="bg-neon-red hover:bg-neon-red/90"
>
{deleteMutation.isPending ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Deleting...
</>
) : (
"Delete"
)}
</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialog>
</div>
);
}