Refactor agent and prompt management

Restructures the application to integrate prompt management directly into the agent details view. Removes the separate "Prompts" page and its associated components. Agents are now the root route, and clicking an agent navigates to a detail page where its system prompt, AI model, and version history can be managed.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-07 19:14:00 +00:00
parent 954b09f836
commit 64b4c39c92
10 changed files with 279 additions and 1131 deletions
+256
View File
@@ -0,0 +1,256 @@
import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
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 { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area";
import { ArrowLeft, Bot, Save, Clock, Copy } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
export default function AgentDetails() {
const { id } = useParams();
const navigate = useNavigate();
const { toast } = useToast();
// Mock data - substituir por dados reais do backend
const [agent, setAgent] = useState({
id: id,
name: "Agente de Atendimento",
status: "active" as const,
model: "google/gemini-2.5-flash",
systemPrompt: "Você é um assistente de atendimento ao cliente. Seja educado, prestativo e objetivo nas suas respostas. Sempre mantenha um tom profissional e cordial.",
});
const [versions] = useState([
{
id: 1,
version: "2.1",
type: "major" as const,
description: "Melhorias na clareza das respostas",
prompt: "Você é um assistente de atendimento ao cliente. Seja educado, prestativo e objetivo nas suas respostas. Sempre mantenha um tom profissional e cordial.",
createdAt: "2024-01-15 14:30",
author: "João Silva",
},
{
id: 2,
version: "2.0",
type: "major" as const,
description: "Reformulação completa do prompt",
prompt: "Você é um assistente de atendimento. Seja cordial e ajude o cliente.",
createdAt: "2024-01-10 09:15",
author: "Maria Santos",
},
{
id: 3,
version: "1.5",
type: "minor" as const,
description: "Ajustes de tom",
prompt: "Você é um assistente. Seja educado.",
createdAt: "2024-01-05 16:45",
author: "João Silva",
},
]);
const aiModels = [
{ value: "google/gemini-2.5-pro", label: "Google Gemini 2.5 Pro" },
{ value: "google/gemini-2.5-flash", label: "Google Gemini 2.5 Flash" },
{ value: "google/gemini-2.5-flash-lite", label: "Google Gemini 2.5 Flash Lite" },
{ value: "openai/gpt-5", label: "OpenAI GPT-5" },
{ value: "openai/gpt-5-mini", label: "OpenAI GPT-5 Mini" },
{ value: "openai/gpt-5-nano", label: "OpenAI GPT-5 Nano" },
];
const handleSave = () => {
// TODO: Implementar salvamento no backend
console.log("Saving agent:", agent);
toast({
title: "Agente atualizado",
description: "As alterações foram salvas com sucesso.",
});
};
const handleCopyPrompt = async (prompt: string) => {
try {
await navigator.clipboard.writeText(prompt);
toast({
title: "Copiado!",
description: "Prompt copiado para a área de transferência.",
});
} catch (error) {
toast({
title: "Erro",
description: "Não foi possível copiar o prompt.",
variant: "destructive",
});
}
};
const getVersionTypeColor = (type: string) => {
switch (type) {
case "major":
return "bg-primary text-primary-foreground";
case "minor":
return "bg-secondary text-secondary-foreground";
case "patch":
return "bg-muted text-muted-foreground";
default:
return "bg-muted text-muted-foreground";
}
};
return (
<div className="animate-fade-in pb-8">
{/* Header */}
<div className="mb-6">
<Button
variant="ghost"
className="mb-4 gap-2"
onClick={() => navigate("/")}
>
<ArrowLeft className="h-4 w-4" />
Voltar para Agentes
</Button>
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4">
<div className="flex items-start gap-4">
<div className="h-16 w-16 rounded-xl bg-primary/10 flex items-center justify-center">
<Bot className="h-8 w-8 text-primary" />
</div>
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground">{agent.name}</h1>
<div className="flex items-center gap-2 mt-2">
<Badge
variant={agent.status === "active" ? "default" : "secondary"}
className={
agent.status === "active"
? "bg-green-600 hover:bg-green-700"
: "bg-muted text-muted-foreground"
}
>
{agent.status === "active" ? "● Ativo" : "○ Inativo"}
</Badge>
</div>
</div>
</div>
<Button className="gap-2 w-full md:w-auto" onClick={handleSave}>
<Save className="h-4 w-4" />
Salvar Alterações
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Prompt & Model */}
<div className="lg:col-span-2 space-y-6">
{/* System Prompt */}
<Card>
<CardHeader>
<CardTitle>System Prompt</CardTitle>
<CardDescription>
Define o comportamento e personalidade do agente
</CardDescription>
</CardHeader>
<CardContent>
<Textarea
value={agent.systemPrompt}
onChange={(e) => setAgent({ ...agent, systemPrompt: e.target.value })}
className="min-h-[200px] font-mono text-sm"
placeholder="Digite o prompt do sistema..."
/>
</CardContent>
</Card>
{/* AI Model */}
<Card>
<CardHeader>
<CardTitle>Modelo de IA</CardTitle>
<CardDescription>
Selecione o modelo de inteligência artificial
</CardDescription>
</CardHeader>
<CardContent>
<Select
value={agent.model}
onValueChange={(value) => setAgent({ ...agent, model: value })}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{aiModels.map((model) => (
<SelectItem key={model.value} value={model.value}>
{model.label}
</SelectItem>
))}
</SelectContent>
</Select>
</CardContent>
</Card>
</div>
{/* Right Column - Version History */}
<div className="lg:col-span-1">
<Card className="h-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Histórico de Versões
</CardTitle>
<CardDescription>
Todas as alterações salvas do prompt
</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[600px] pr-4">
<div className="space-y-4">
{versions.map((version) => (
<div
key={version.id}
className="p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<div>
<div className="flex items-center gap-2">
<span className="font-semibold">v{version.version}</span>
<Badge className={getVersionTypeColor(version.type)}>
{version.type}
</Badge>
</div>
<p className="text-sm text-muted-foreground mt-1">
{version.description}
</p>
</div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => handleCopyPrompt(version.prompt)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
<div className="mt-3 p-3 rounded bg-muted/50">
<p className="text-xs font-mono text-muted-foreground line-clamp-3">
{version.prompt}
</p>
</div>
<div className="mt-3 text-xs text-muted-foreground">
<p>{version.author} {version.createdAt}</p>
</div>
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
</div>
</div>
);
}
+20 -4
View File
@@ -1,4 +1,5 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { CreateAgentDialog } from "@/components/agents/CreateAgentDialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -13,6 +14,7 @@ import {
import { Plus, Search, MoreVertical, Edit, Trash2, Bot, Zap } from "lucide-react";
export default function Agents() {
const navigate = useNavigate();
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
@@ -86,7 +88,8 @@ export default function Agents() {
{filteredAgents.map((agent) => (
<Card
key={agent.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"
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(`/agents/${agent.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" />
@@ -104,17 +107,30 @@ export default function Agents() {
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={(e) => e.stopPropagation()}
>
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => console.log("Edit", agent.id)}>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
navigate(`/agents/${agent.id}`);
}}
>
<Edit className="h-4 w-4 mr-2" />
Editar
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => console.log("Delete", agent.id)}
onClick={(e) => {
e.stopPropagation();
console.log("Delete", agent.id);
}}
className="text-destructive"
>
<Trash2 className="h-4 w-4 mr-2" />
-353
View File
@@ -1,353 +0,0 @@
import { useState, useMemo } from "react";
import { CreatePromptDialog } from "@/components/prompts/CreatePromptDialog";
import { EditPromptDialog } from "@/components/prompts/EditPromptDialog";
import { PromptHistoryDialog } from "@/components/prompts/PromptHistoryDialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { Plus, Search, MoreVertical, Edit, History } from "lucide-react";
const ITEMS_PER_PAGE = 10;
export default function Prompts() {
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isHistoryDialogOpen, setIsHistoryDialogOpen] = useState(false);
const [selectedPrompt, setSelectedPrompt] = useState<any>(null);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [prompts] = useState([
{
id: 1,
title: "Prompt de Atendimento v2.1",
creator: "João Silva",
lastModified: "2 dias atrás",
status: "published" as const,
version: "2.1.0",
systemPrompt: "Você é Clara, assistente de atendimento ao cliente...",
},
{
id: 2,
title: "Prompt de Vendas v1.5",
creator: "Maria Santos",
lastModified: "5 dias atrás",
status: "published" as const,
version: "1.5.0",
systemPrompt: "Você é um assistente de vendas...",
},
{
id: 3,
title: "Prompt de Análise",
creator: "Pedro Costa",
lastModified: "1 semana atrás",
status: "draft" as const,
version: "1.0.0",
systemPrompt: "Você é um analista especializado...",
},
{
id: 4,
title: "Prompt de Marketing",
creator: "Ana Lima",
lastModified: "3 dias atrás",
status: "published" as const,
version: "1.2.1",
systemPrompt: "Você é um especialista em marketing...",
},
{
id: 5,
title: "Prompt de Suporte Técnico",
creator: "Carlos Mendes",
lastModified: "4 dias atrás",
status: "published" as const,
version: "2.0.0",
systemPrompt: "Você é um técnico de suporte...",
},
{
id: 6,
title: "Prompt de Onboarding",
creator: "Fernanda Silva",
lastModified: "6 dias atrás",
status: "draft" as const,
version: "1.0.0",
systemPrompt: "Você é um guia de onboarding...",
},
{
id: 7,
title: "Prompt de Follow-up",
creator: "Rafael Santos",
lastModified: "1 dia atrás",
status: "published" as const,
version: "1.1.0",
systemPrompt: "Você é responsável por follow-ups...",
},
]);
// Filtra prompts baseado na pesquisa
const filteredPrompts = useMemo(() => {
if (!searchQuery.trim()) return prompts;
const query = searchQuery.toLowerCase();
return prompts.filter(prompt =>
prompt.title.toLowerCase().includes(query) ||
prompt.creator.toLowerCase().includes(query)
);
}, [prompts, searchQuery]);
// Calcula paginação
const totalPages = Math.ceil(filteredPrompts.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const paginatedPrompts = filteredPrompts.slice(startIndex, startIndex + ITEMS_PER_PAGE);
// Reset para primeira página quando busca muda
const handleSearchChange = (value: string) => {
setSearchQuery(value);
setCurrentPage(1);
};
const handleEditPrompt = (prompt: any) => {
setSelectedPrompt(prompt);
setIsEditDialogOpen(true);
};
const handleViewHistory = (prompt: any) => {
setSelectedPrompt({
id: prompt.id,
title: prompt.title,
currentVersion: prompt.version,
});
setIsHistoryDialogOpen(true);
};
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 Prompts</h1>
<p className="text-sm md:text-base text-muted-foreground">
Crie, edite e gerencie os prompts dos seus agentes de IA
</p>
</div>
<Button className="gap-2 w-full md:w-auto" onClick={() => setIsCreateDialogOpen(true)}>
<Plus className="h-4 w-4" />
Criar Prompt
</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 prompts por título ou criador..."
value={searchQuery}
onChange={(e) => handleSearchChange(e.target.value)}
className="pl-10"
/>
</div>
<p className="text-sm text-muted-foreground mt-2">
{filteredPrompts.length} {filteredPrompts.length === 1 ? 'prompt encontrado' : 'prompts encontrados'}
</p>
</div>
{paginatedPrompts.length === 0 ? (
<div className="text-center py-12">
<p className="text-muted-foreground">Nenhum prompt encontrado com os critérios de busca.</p>
</div>
) : (
<>
{/* Mobile Cards */}
<div className="md:hidden space-y-4 mb-6">
{paginatedPrompts.map((prompt) => (
<div key={prompt.id} className="bg-card border rounded-lg p-4 space-y-3">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="font-medium text-lg">{prompt.title}</h3>
<p className="text-sm text-muted-foreground mt-1">
por {prompt.creator} {prompt.lastModified}
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleEditPrompt(prompt)}>
<Edit className="h-4 w-4 mr-2" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleViewHistory(prompt)}>
<History className="h-4 w-4 mr-2" />
Histórico
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex items-center justify-between">
<code className="text-xs bg-muted px-2 py-1 rounded">{prompt.version}</code>
<Badge
variant={prompt.status === "published" ? "default" : "secondary"}
className={prompt.status === "published" ? "bg-green-600 hover:bg-green-700" : "bg-muted text-muted-foreground hover:bg-muted"}
>
{prompt.status === "published" ? "Publicado" : "Rascunho"}
</Badge>
</div>
</div>
))}
</div>
{/* Desktop Table */}
<div className="hidden md:block rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Título</TableHead>
<TableHead>Versão</TableHead>
<TableHead>Criador</TableHead>
<TableHead>Última Modificação</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedPrompts.map((prompt) => (
<TableRow key={prompt.id}>
<TableCell className="font-medium">{prompt.title}</TableCell>
<TableCell>
<code className="text-xs bg-muted px-2 py-1 rounded">
{prompt.version}
</code>
</TableCell>
<TableCell>{prompt.creator}</TableCell>
<TableCell>{prompt.lastModified}</TableCell>
<TableCell>
<Badge
variant={prompt.status === "published" ? "default" : "secondary"}
className={prompt.status === "published" ? "bg-green-600 hover:bg-green-700" : "bg-muted text-muted-foreground hover:bg-muted"}
>
{prompt.status === "published" ? "Publicado" : "Rascunho"}
</Badge>
</TableCell>
<TableCell className="text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleEditPrompt(prompt)}>
<Edit className="h-4 w-4 mr-2" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleViewHistory(prompt)}>
<History className="h-4 w-4 mr-2" />
Histórico
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{totalPages > 1 && (
<div className="mt-8">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/>
</PaginationItem>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => {
// Mostrar primeira página, última página e páginas próximas à atual
if (
page === 1 ||
page === totalPages ||
(page >= currentPage - 1 && page <= currentPage + 1)
) {
return (
<PaginationItem key={page}>
<PaginationLink
onClick={() => setCurrentPage(page)}
isActive={currentPage === page}
className="cursor-pointer"
>
{page}
</PaginationLink>
</PaginationItem>
);
} else if (
page === currentPage - 2 ||
page === currentPage + 2
) {
return (
<PaginationItem key={page}>
<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>
)}
</>
)}
<CreatePromptDialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
/>
<EditPromptDialog
open={isEditDialogOpen}
onOpenChange={setIsEditDialogOpen}
prompt={selectedPrompt}
/>
<PromptHistoryDialog
open={isHistoryDialogOpen}
onOpenChange={setIsHistoryDialogOpen}
prompt={selectedPrompt}
/>
</div>
);
}