Files
PromptLab-Front-end/src/pages/Agents.tsx
T
2025-11-14 17:17:30 -03:00

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>
);
}