true message
This commit is contained in:
91
client/src/components/AgentCreateModal.test.ts
Normal file
91
client/src/components/AgentCreateModal.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
257
client/src/components/AgentCreateModal.tsx
Normal file
257
client/src/components/AgentCreateModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
89
client/src/components/AgentDetailModal.test.ts
Normal file
89
client/src/components/AgentDetailModal.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
383
client/src/components/AgentDetailModal.tsx
Normal file
383
client/src/components/AgentDetailModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 · {agents.filter(a => a.status === "idle").length} idle · {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}% · 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user