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:
+2
-2
@@ -6,7 +6,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import { AppLayout } from "./components/layout/AppLayout";
|
||||
import Agents from "./pages/Agents";
|
||||
import Prompts from "./pages/Prompts";
|
||||
import AgentDetails from "./pages/AgentDetails";
|
||||
import TestPrompt from "./pages/TestPrompt";
|
||||
import History from "./pages/History";
|
||||
import NotFound from "./pages/NotFound";
|
||||
@@ -23,7 +23,7 @@ const App = () => (
|
||||
<Routes>
|
||||
<Route element={<AppLayout />}>
|
||||
<Route path="/" element={<Agents />} />
|
||||
<Route path="/prompts" element={<Prompts />} />
|
||||
<Route path="/agents/:id" element={<AgentDetails />} />
|
||||
<Route path="/test" element={<TestPrompt />} />
|
||||
<Route path="/history" element={<History />} />
|
||||
</Route>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import {
|
||||
Bot,
|
||||
FileText,
|
||||
TestTube,
|
||||
ArrowLeft,
|
||||
ChevronLeft,
|
||||
@@ -14,7 +13,6 @@ import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
const menuItems = [
|
||||
{ title: "Agentes", url: "/", icon: Bot },
|
||||
{ title: "Prompts", url: "/prompts", icon: FileText },
|
||||
{ title: "Testar Prompt", url: "/test", icon: TestTube },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { Bot, FileText, TestTube, Menu, X } from "lucide-react";
|
||||
import { Bot, TestTube, Menu, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
const menuItems = [
|
||||
{ title: "Agentes", url: "/", icon: Bot },
|
||||
{ title: "Prompts", url: "/prompts", icon: FileText },
|
||||
{ title: "Testar Prompt", url: "/test", icon: TestTube },
|
||||
];
|
||||
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Wand2 } from "lucide-react";
|
||||
|
||||
interface CreatePromptDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function CreatePromptDialog({ open, onOpenChange }: CreatePromptDialogProps) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [systemPrompt, setSystemPrompt] = useState("");
|
||||
|
||||
// Assistente fields
|
||||
const [agentName, setAgentName] = useState("");
|
||||
const [agentRole, setAgentRole] = useState("");
|
||||
const [targetAudience, setTargetAudience] = useState("");
|
||||
const [mainObjective, setMainObjective] = useState("");
|
||||
const [toneOfVoice, setToneOfVoice] = useState("");
|
||||
const [forbiddenPatterns, setForbiddenPatterns] = useState("");
|
||||
const [refinementInstructions, setRefinementInstructions] = useState("");
|
||||
const [isGenerated, setIsGenerated] = useState(false);
|
||||
|
||||
const handleGeneratePrompt = () => {
|
||||
let generatedPrompt = `Você é ${agentName}, ${agentRole}.
|
||||
|
||||
Público-alvo: ${targetAudience}
|
||||
|
||||
Objetivo principal: ${mainObjective}
|
||||
|
||||
Tom de voz: ${toneOfVoice}
|
||||
|
||||
${forbiddenPatterns ? `Padrões proibidos:\n${forbiddenPatterns}` : ''}
|
||||
|
||||
Siga estas diretrizes em todas as suas interações.`;
|
||||
|
||||
if (refinementInstructions && isGenerated) {
|
||||
generatedPrompt += `\n\nInstruções adicionais:\n${refinementInstructions}`;
|
||||
}
|
||||
|
||||
setSystemPrompt(generatedPrompt);
|
||||
setIsGenerated(true);
|
||||
};
|
||||
|
||||
const handleStartOver = () => {
|
||||
setAgentName("");
|
||||
setAgentRole("");
|
||||
setTargetAudience("");
|
||||
setMainObjective("");
|
||||
setToneOfVoice("");
|
||||
setForbiddenPatterns("");
|
||||
setRefinementInstructions("");
|
||||
setSystemPrompt("");
|
||||
setIsGenerated(false);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log("Salvando prompt:", { title, systemPrompt });
|
||||
// Aqui você implementaria a lógica de salvar no backend
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto w-[95vw] sm:w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Criar Novo Prompt</DialogTitle>
|
||||
<DialogDescription>
|
||||
Crie um prompt manualmente ou use o assistente para gerar automaticamente.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Título</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="Nome do prompt"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="manual" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="manual">Manual</TabsTrigger>
|
||||
<TabsTrigger value="assistant">
|
||||
<Wand2 className="w-4 h-4 mr-2" />
|
||||
Assistente
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="manual" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="systemPrompt">System Prompt</Label>
|
||||
<Textarea
|
||||
id="systemPrompt"
|
||||
placeholder="Digite o system prompt..."
|
||||
value={systemPrompt}
|
||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||
className="min-h-[400px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="assistant" className="space-y-4 mt-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agentName">Nome do Agente</Label>
|
||||
<Input
|
||||
id="agentName"
|
||||
placeholder="Ex: Clara"
|
||||
value={agentName}
|
||||
onChange={(e) => setAgentName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="agentRole">Função / Papel do Agente</Label>
|
||||
<Input
|
||||
id="agentRole"
|
||||
placeholder="Ex: assistente de atendimento ao cliente"
|
||||
value={agentRole}
|
||||
onChange={(e) => setAgentRole(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="targetAudience">Público-alvo</Label>
|
||||
<Input
|
||||
id="targetAudience"
|
||||
placeholder="Ex: Clientes da loja online de eletrônicos"
|
||||
value={targetAudience}
|
||||
onChange={(e) => setTargetAudience(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mainObjective">Objetivo Principal</Label>
|
||||
<Textarea
|
||||
id="mainObjective"
|
||||
placeholder="Ex: Ajudar clientes com dúvidas sobre produtos e pedidos"
|
||||
value={mainObjective}
|
||||
onChange={(e) => setMainObjective(e.target.value)}
|
||||
className="min-h-[80px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="toneOfVoice">Tom de Voz</Label>
|
||||
<Select value={toneOfVoice} onValueChange={setToneOfVoice}>
|
||||
<SelectTrigger id="toneOfVoice">
|
||||
<SelectValue placeholder="Selecione o tom de voz" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="amigavel">Amigável</SelectItem>
|
||||
<SelectItem value="formal">Formal</SelectItem>
|
||||
<SelectItem value="tecnico">Técnico</SelectItem>
|
||||
<SelectItem value="educado">Educado</SelectItem>
|
||||
<SelectItem value="institucional">Institucional</SelectItem>
|
||||
<SelectItem value="neutro">Neutro</SelectItem>
|
||||
<SelectItem value="engracado">Engraçado</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="forbiddenPatterns">Padrões Proibidos</Label>
|
||||
<Textarea
|
||||
id="forbiddenPatterns"
|
||||
placeholder="Ex: Não usar gírias, não dar opiniões pessoais, evitar emojis"
|
||||
value={forbiddenPatterns}
|
||||
onChange={(e) => setForbiddenPatterns(e.target.value)}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleGeneratePrompt}
|
||||
className="flex-1"
|
||||
variant="secondary"
|
||||
disabled={!agentName || !agentRole}
|
||||
>
|
||||
<Wand2 className="w-4 h-4 mr-2" />
|
||||
{isGenerated ? "Regenerar Prompt" : "Gerar System Prompt"}
|
||||
</Button>
|
||||
{isGenerated && (
|
||||
<Button
|
||||
onClick={handleStartOver}
|
||||
variant="outline"
|
||||
>
|
||||
Começar do Zero
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{systemPrompt && (
|
||||
<div className="space-y-4 p-4 border rounded-lg bg-muted/50">
|
||||
<div className="space-y-2">
|
||||
<Label>Prompt Gerado</Label>
|
||||
<Textarea
|
||||
value={systemPrompt}
|
||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||
className="min-h-[400px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="refinement">
|
||||
Instruções Adicionais (opcional)
|
||||
</Label>
|
||||
<Textarea
|
||||
id="refinement"
|
||||
placeholder="Ex: Adicione mais empatia nas respostas, seja mais direto, inclua exemplos práticos..."
|
||||
value={refinementInstructions}
|
||||
onChange={(e) => setRefinementInstructions(e.target.value)}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleGeneratePrompt}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={!refinementInstructions}
|
||||
>
|
||||
<Wand2 className="w-4 h-4 mr-2" />
|
||||
Aplicar Refinamento
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={!title || !systemPrompt}>
|
||||
Criar Prompt
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} 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 { Wand2, GitBranch } from "lucide-react";
|
||||
|
||||
interface EditPromptDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
prompt: {
|
||||
id: number;
|
||||
title: string;
|
||||
systemPrompt?: string;
|
||||
version?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function EditPromptDialog({ open, onOpenChange, prompt }: EditPromptDialogProps) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [systemPrompt, setSystemPrompt] = useState("");
|
||||
const [versionType, setVersionType] = useState<"major" | "minor" | "patch">("minor");
|
||||
const [changeDescription, setChangeDescription] = useState("");
|
||||
const [assistantInstructions, setAssistantInstructions] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (prompt && open) {
|
||||
setTitle(prompt.title);
|
||||
setSystemPrompt(prompt.systemPrompt || "");
|
||||
setChangeDescription("");
|
||||
setVersionType("minor");
|
||||
setAssistantInstructions("");
|
||||
}
|
||||
}, [prompt, open]);
|
||||
|
||||
const handleApplyAssistant = () => {
|
||||
// Aqui você implementaria a lógica de IA para modificar o prompt
|
||||
// Por enquanto, vamos apenas adicionar as instruções ao final do prompt
|
||||
const updatedPrompt = `${systemPrompt}\n\n[Aplicar mudanças: ${assistantInstructions}]`;
|
||||
setSystemPrompt(updatedPrompt);
|
||||
setAssistantInstructions("");
|
||||
console.log("Aplicando instruções do assistente:", assistantInstructions);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
console.log("Salvando nova versão:", {
|
||||
promptId: prompt?.id,
|
||||
title,
|
||||
systemPrompt,
|
||||
versionType,
|
||||
changeDescription
|
||||
});
|
||||
// Aqui você implementaria a lógica de salvar nova versão no backend
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const getVersionDescription = () => {
|
||||
switch (versionType) {
|
||||
case "major":
|
||||
return "Mudanças significativas que alteram a funcionalidade principal";
|
||||
case "minor":
|
||||
return "Novas funcionalidades ou melhorias sem quebrar compatibilidade";
|
||||
case "patch":
|
||||
return "Correções e ajustes menores";
|
||||
}
|
||||
};
|
||||
|
||||
if (!prompt) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl max-h-[90vh] overflow-y-auto w-[95vw] sm:w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar Prompt</DialogTitle>
|
||||
<DialogDescription>
|
||||
Edite o prompt e descreva as mudanças. Uma nova versão será criada preservando o histórico.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 p-3 bg-muted/50 rounded-lg">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Versão atual: <span className="font-medium text-foreground">{prompt.version || "1.0.0"}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">Título</Label>
|
||||
<Input
|
||||
id="title"
|
||||
placeholder="Nome do prompt"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="systemPrompt">System Prompt</Label>
|
||||
<Textarea
|
||||
id="systemPrompt"
|
||||
placeholder="Digite o system prompt..."
|
||||
value={systemPrompt}
|
||||
onChange={(e) => setSystemPrompt(e.target.value)}
|
||||
className="min-h-[300px] font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wand2 className="h-4 w-4 text-primary" />
|
||||
<Label htmlFor="assistantInstructions" className="text-base font-medium">
|
||||
Assistente de Prompt
|
||||
</Label>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Descreva o que deseja incluir ou modificar no prompt acima
|
||||
</p>
|
||||
<Textarea
|
||||
id="assistantInstructions"
|
||||
placeholder="Ex: Adicione mais empatia nas respostas, seja mais técnico ao explicar produtos, inclua exemplos práticos..."
|
||||
value={assistantInstructions}
|
||||
onChange={(e) => setAssistantInstructions(e.target.value)}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleApplyAssistant}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={!assistantInstructions}
|
||||
className="w-full"
|
||||
>
|
||||
<Wand2 className="w-4 h-4 mr-2" />
|
||||
Aplicar Mudanças com Assistente
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 p-4 border rounded-lg bg-muted/30">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="versionType">Tipo de Versão</Label>
|
||||
<Select value={versionType} onValueChange={(v) => setVersionType(v as "major" | "minor" | "patch")}>
|
||||
<SelectTrigger id="versionType">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="major">
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="font-medium">Major (X.0.0)</span>
|
||||
<span className="text-xs text-muted-foreground">Mudanças significativas</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="minor">
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="font-medium">Minor (0.X.0)</span>
|
||||
<span className="text-xs text-muted-foreground">Novas funcionalidades</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="patch">
|
||||
<div className="flex flex-col items-start">
|
||||
<span className="font-medium">Patch (0.0.X)</span>
|
||||
<span className="text-xs text-muted-foreground">Correções menores</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">{getVersionDescription()}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="changeDescription">Descrição das Mudanças</Label>
|
||||
<Textarea
|
||||
id="changeDescription"
|
||||
placeholder="Descreva o que foi alterado nesta versão..."
|
||||
value={changeDescription}
|
||||
onChange={(e) => setChangeDescription(e.target.value)}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!title || !systemPrompt || !changeDescription}
|
||||
>
|
||||
Salvar Nova Versão
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { FileText, Edit, History, Copy, TestTube } from "lucide-react";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface PromptCardProps {
|
||||
title: string;
|
||||
creator: string;
|
||||
lastModified: string;
|
||||
status: "draft" | "published";
|
||||
onEdit: () => void;
|
||||
onHistory: () => void;
|
||||
onTest: () => void;
|
||||
onDuplicate: () => void;
|
||||
}
|
||||
|
||||
export function PromptCard({
|
||||
title,
|
||||
creator,
|
||||
lastModified,
|
||||
status,
|
||||
onEdit,
|
||||
onHistory,
|
||||
onTest,
|
||||
onDuplicate,
|
||||
}: PromptCardProps) {
|
||||
return (
|
||||
<Card className="p-5 hover:shadow-hover transition-all duration-200 animate-fade-in">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-accent/10 flex items-center justify-center">
|
||||
<FileText className="h-5 w-5 text-accent" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground">{title}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
por {creator} • {lastModified}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={status === "published" ? "default" : "secondary"}>
|
||||
{status === "published" ? "Publicado" : "Rascunho"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={onEdit} variant="outline" size="sm" className="flex-1">
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Editar
|
||||
</Button>
|
||||
<Button onClick={onHistory} variant="outline" size="sm">
|
||||
<History className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button onClick={onTest} variant="outline" size="sm">
|
||||
<TestTube className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button onClick={onDuplicate} variant="outline" size="sm">
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { GitBranch, Clock, User, FileText, Copy, Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface PromptVersion {
|
||||
version: string;
|
||||
type: "major" | "minor" | "patch";
|
||||
changeDescription: string;
|
||||
systemPrompt: string;
|
||||
createdAt: string;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
interface PromptHistoryDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
prompt: {
|
||||
id: number;
|
||||
title: string;
|
||||
currentVersion: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export function PromptHistoryDialog({ open, onOpenChange, prompt }: PromptHistoryDialogProps) {
|
||||
const { toast } = useToast();
|
||||
const [copiedVersion, setCopiedVersion] = useState<string | null>(null);
|
||||
|
||||
const handleCopyPrompt = async (content: string, version: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content);
|
||||
setCopiedVersion(version);
|
||||
toast({
|
||||
title: "Conteúdo copiado!",
|
||||
description: `Prompt da versão ${version} copiado para a área de transferência.`,
|
||||
});
|
||||
setTimeout(() => setCopiedVersion(null), 2000);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Erro ao copiar",
|
||||
description: "Não foi possível copiar o conteúdo.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Mock data - substituir por dados reais do backend
|
||||
const versions: PromptVersion[] = [
|
||||
{
|
||||
version: "2.1.0",
|
||||
type: "minor",
|
||||
changeDescription: "Adicionado mais empatia nas respostas e incluídos exemplos práticos",
|
||||
systemPrompt: "Você é Clara, assistente de atendimento ao cliente. Seja empática...",
|
||||
createdAt: "2 dias atrás",
|
||||
createdBy: "João Silva",
|
||||
},
|
||||
{
|
||||
version: "2.0.0",
|
||||
type: "major",
|
||||
changeDescription: "Reformulação completa do tom de voz e objetivos principais",
|
||||
systemPrompt: "Você é Clara, assistente de atendimento ao cliente...",
|
||||
createdAt: "1 semana atrás",
|
||||
createdBy: "Maria Santos",
|
||||
},
|
||||
{
|
||||
version: "1.5.0",
|
||||
type: "minor",
|
||||
changeDescription: "Adicionadas instruções sobre política de devolução",
|
||||
systemPrompt: "Você é um assistente virtual de atendimento...",
|
||||
createdAt: "2 semanas atrás",
|
||||
createdBy: "João Silva",
|
||||
},
|
||||
{
|
||||
version: "1.0.0",
|
||||
type: "major",
|
||||
changeDescription: "Versão inicial do prompt",
|
||||
systemPrompt: "Você é um assistente virtual...",
|
||||
createdAt: "1 mês atrás",
|
||||
createdBy: "João Silva",
|
||||
},
|
||||
];
|
||||
|
||||
const getVersionTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case "major":
|
||||
return "destructive";
|
||||
case "minor":
|
||||
return "default";
|
||||
case "patch":
|
||||
return "secondary";
|
||||
default:
|
||||
return "secondary";
|
||||
}
|
||||
};
|
||||
|
||||
const getVersionTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case "major":
|
||||
return "Major";
|
||||
case "minor":
|
||||
return "Minor";
|
||||
case "patch":
|
||||
return "Patch";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
if (!prompt) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] w-[95vw] sm:w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<GitBranch className="h-5 w-5" />
|
||||
Histórico de Versões
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Histórico completo de versões do prompt "{prompt.title}"
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="h-[600px] pr-4">
|
||||
<div className="space-y-4">
|
||||
{versions.map((version, index) => (
|
||||
<div key={version.version}>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
version.version === prompt.currentVersion
|
||||
? "bg-primary ring-4 ring-primary/20"
|
||||
: "bg-muted-foreground"
|
||||
}`} />
|
||||
{index < versions.length - 1 && (
|
||||
<div className="w-0.5 h-full min-h-[80px] bg-border mt-2" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-3 pb-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-mono font-semibold">
|
||||
v{version.version}
|
||||
</code>
|
||||
{version.version === prompt.currentVersion && (
|
||||
<Badge variant="outline" className="bg-primary/10">
|
||||
Atual
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{version.createdAt}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="h-3 w-3" />
|
||||
{version.createdBy}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<FileText className="h-4 w-4 mt-0.5 text-muted-foreground flex-shrink-0" />
|
||||
<p className="text-sm">{version.changeDescription}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase">
|
||||
Conteúdo do Prompt
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleCopyPrompt(version.systemPrompt, version.version)}
|
||||
className="h-7"
|
||||
>
|
||||
{copiedVersion === version.version ? (
|
||||
<>
|
||||
<Check className="h-3 w-3 mr-1" />
|
||||
Copiado
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-3 w-3 mr-1" />
|
||||
Copiar
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea className="h-[200px] w-full rounded-md border bg-muted/30">
|
||||
<div className="p-4">
|
||||
<p className="text-sm whitespace-pre-wrap leading-relaxed">
|
||||
{version.systemPrompt}
|
||||
</p>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Fechar
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user