265 lines
10 KiB
TypeScript
265 lines
10 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { CreateAgentDialog } from "@/components/agents/CreateAgentDialog";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
Pagination,
|
|
PaginationContent,
|
|
PaginationEllipsis,
|
|
PaginationItem,
|
|
PaginationLink,
|
|
PaginationNext,
|
|
PaginationPrevious,
|
|
} from "@/components/ui/pagination";
|
|
import { Plus, Search, Bot, ChevronRight, Loader2 } from "lucide-react";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { listAgents } from "@/lib/api/agents";
|
|
|
|
export default function Agents() {
|
|
const navigate = useNavigate();
|
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
const [itemsPerPage, setItemsPerPage] = useState(6);
|
|
|
|
// Debounce search query
|
|
useEffect(() => {
|
|
const handler = setTimeout(() => {
|
|
setDebouncedSearch(searchQuery);
|
|
setCurrentPage(1); // Reset to first page on search
|
|
}, 500);
|
|
|
|
return () => {
|
|
clearTimeout(handler);
|
|
};
|
|
}, [searchQuery]);
|
|
|
|
// Fetch agents from API
|
|
const {
|
|
data: agentsData,
|
|
isLoading,
|
|
isError,
|
|
refetch,
|
|
} = useQuery({
|
|
queryKey: ["agents", debouncedSearch, currentPage, itemsPerPage],
|
|
queryFn: () =>
|
|
listAgents(
|
|
debouncedSearch || undefined,
|
|
currentPage,
|
|
itemsPerPage
|
|
),
|
|
});
|
|
|
|
const agents = agentsData?.data || [];
|
|
const totalPages = agentsData?.total_pages || 0;
|
|
const totalAgents = agentsData?.total || 0;
|
|
|
|
return (
|
|
<div className="animate-fade-in">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
|
|
<div>
|
|
<h1 className="text-2xl md:text-3xl font-bold text-foreground mb-2">Gestão de Agentes</h1>
|
|
<p className="text-sm md:text-base text-muted-foreground">
|
|
Crie e gerencie agentes de IA com prompts e modelos específicos
|
|
</p>
|
|
</div>
|
|
<Button className="gap-2 w-full md:w-auto" onClick={() => setIsCreateDialogOpen(true)}>
|
|
<Plus className="h-4 w-4" />
|
|
Criar Agente
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="mb-6">
|
|
<div className="relative max-w-md">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
|
|
<Input
|
|
placeholder="Pesquisar agentes..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="pl-10"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mt-2">
|
|
<p className="text-sm text-muted-foreground">
|
|
{totalAgents} {totalAgents === 1 ? 'agente encontrado' : 'agentes encontrados'}
|
|
</p>
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-sm text-muted-foreground whitespace-nowrap">Itens por página:</span>
|
|
<Select
|
|
value={itemsPerPage.toString()}
|
|
onValueChange={(value) => {
|
|
setItemsPerPage(Number(value));
|
|
setCurrentPage(1);
|
|
}}
|
|
>
|
|
<SelectTrigger className="w-[70px]">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="6">6</SelectItem>
|
|
<SelectItem value="9">9</SelectItem>
|
|
<SelectItem value="12">12</SelectItem>
|
|
<SelectItem value="18">18</SelectItem>
|
|
<SelectItem value="24">24</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
{totalPages > 1 && (
|
|
<p className="text-sm text-muted-foreground">
|
|
Página {currentPage} de {totalPages}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex items-center justify-center py-12">
|
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
|
</div>
|
|
) : isError ? (
|
|
<div className="text-center py-12">
|
|
<p className="text-destructive mb-4">Erro ao carregar agentes.</p>
|
|
<Button onClick={() => refetch()} variant="outline">
|
|
Tentar novamente
|
|
</Button>
|
|
</div>
|
|
) : agents.length === 0 ? (
|
|
<div className="text-center py-12">
|
|
<p className="text-muted-foreground">Nenhum agente encontrado.</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{agents.map((agent) => (
|
|
<Card
|
|
key={agent.agente_id}
|
|
className="group relative overflow-hidden border-2 transition-all duration-300 hover:shadow-lg hover:scale-[1.02] bg-gradient-to-br from-card to-card/50 cursor-pointer"
|
|
onClick={() => navigate(`/prompt-labs/agents/${agent.agente_id}`)}
|
|
>
|
|
{/* Glow effect on hover */}
|
|
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-primary/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
|
|
|
<CardHeader className="relative pb-3">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
|
<div className="h-14 w-14 rounded-xl bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 transition-colors flex-shrink-0">
|
|
<Bot className="h-7 w-7 text-primary" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<CardTitle className="text-xl truncate">{agent.nome_agente}</CardTitle>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<Badge variant="outline" className="text-xs">
|
|
v{agent.versao_agente}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex-shrink-0 h-10 w-10 rounded-lg bg-primary/10 flex items-center justify-center group-hover:bg-primary/20 group-hover:scale-110 transition-all">
|
|
<ChevronRight className="h-5 w-5 text-primary" />
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="relative space-y-3">
|
|
<div className="p-3 rounded-lg bg-muted/50">
|
|
<p className="text-xs text-muted-foreground mb-2">Modelo de IA</p>
|
|
<p className="text-sm font-medium">{agent.modelo_ia}</p>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-2 border-t">
|
|
<span className="text-xs text-muted-foreground">Status</span>
|
|
<Badge
|
|
variant={agent.status === "ativo" ? "default" : "secondary"}
|
|
className={
|
|
agent.status === "ativo"
|
|
? "bg-green-600 hover:bg-green-700 shadow-sm"
|
|
: "bg-muted text-muted-foreground hover:bg-muted"
|
|
}
|
|
>
|
|
{agent.status === "ativo" ? "● Ativo" : "○ Inativo"}
|
|
</Badge>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
|
|
{totalPages > 1 && (
|
|
<div className="mt-8 flex justify-center">
|
|
<Pagination>
|
|
<PaginationContent>
|
|
<PaginationItem>
|
|
<PaginationPrevious
|
|
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
|
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
/>
|
|
</PaginationItem>
|
|
|
|
{[...Array(totalPages)].map((_, index) => {
|
|
const pageNumber = index + 1;
|
|
|
|
// Mostrar primeira página, última página e páginas próximas à atual
|
|
if (
|
|
pageNumber === 1 ||
|
|
pageNumber === totalPages ||
|
|
(pageNumber >= currentPage - 1 && pageNumber <= currentPage + 1)
|
|
) {
|
|
return (
|
|
<PaginationItem key={pageNumber}>
|
|
<PaginationLink
|
|
onClick={() => setCurrentPage(pageNumber)}
|
|
isActive={currentPage === pageNumber}
|
|
className="cursor-pointer"
|
|
>
|
|
{pageNumber}
|
|
</PaginationLink>
|
|
</PaginationItem>
|
|
);
|
|
}
|
|
|
|
// Mostrar ellipsis
|
|
if (pageNumber === currentPage - 2 || pageNumber === currentPage + 2) {
|
|
return (
|
|
<PaginationItem key={pageNumber}>
|
|
<PaginationEllipsis />
|
|
</PaginationItem>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
})}
|
|
|
|
<PaginationItem>
|
|
<PaginationNext
|
|
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
|
className={currentPage === totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
|
|
/>
|
|
</PaginationItem>
|
|
</PaginationContent>
|
|
</Pagination>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<CreateAgentDialog
|
|
open={isCreateDialogOpen}
|
|
onOpenChange={setIsCreateDialogOpen}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|