Add "Agente de Parecer" feature

This commit is contained in:
gpt-engineer-app[bot]
2025-10-17 16:24:42 +00:00
parent 530e829680
commit 4ca7b2ff64
6 changed files with 498 additions and 5 deletions
+195
View File
@@ -0,0 +1,195 @@
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Input } from "@/components/ui/input";
import { Download, Send, Sparkles } from "lucide-react";
import { LegalOpinion } from "./AgentView";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useToast } from "@/hooks/use-toast";
interface AgentChatProps {
selectedOpinion: LegalOpinion | null;
isCreatingNew: boolean;
onOpinionCreated: (opinion: LegalOpinion) => void;
}
export const AgentChat = ({
selectedOpinion,
isCreatingNew,
onOpinionCreated,
}: AgentChatProps) => {
const [title, setTitle] = useState("");
const [instructions, setInstructions] = useState("");
const [category, setCategory] = useState("");
const [generatedContent, setGeneratedContent] = useState("");
const [isGenerating, setIsGenerating] = useState(false);
const { toast } = useToast();
const handleGenerate = async () => {
if (!instructions.trim()) {
toast({
title: "Instruções necessárias",
description: "Por favor, forneça instruções para gerar o parecer.",
variant: "destructive",
});
return;
}
setIsGenerating(true);
// Simulação de chamada à IA - aqui você integraria com a API real
setTimeout(() => {
const mockContent = `PARECER JURÍDICO
TÍTULO: ${title || "Parecer Jurídico"}
${instructions}
ANÁLISE:
Este parecer foi gerado com base nas instruções fornecidas. A análise considera os seguintes aspectos legais:
1. Fundamentação Legal
2. Precedentes Judiciais
3. Doutrina Aplicável
4. Conclusão e Recomendações
CONCLUSÃO:
Com base na análise realizada, conclui-se que...
___________________________
Parecer gerado em ${new Date().toLocaleDateString('pt-BR')}`;
setGeneratedContent(mockContent);
setIsGenerating(false);
if (isCreatingNew) {
const newOpinion: LegalOpinion = {
id: Date.now().toString(),
title: title || "Novo Parecer",
content: mockContent,
createdAt: new Date(),
category: category || undefined,
};
onOpinionCreated(newOpinion);
}
toast({
title: "Parecer gerado com sucesso!",
description: "O parecer está pronto para download.",
});
}, 2000);
};
const handleDownloadDocx = () => {
// Aqui você implementaria a geração real do DOCX
// Por enquanto, vamos criar um arquivo de texto
const content = generatedContent || selectedOpinion?.content || "";
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${title || selectedOpinion?.title || 'parecer'}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast({
title: "Download iniciado",
description: "O parecer está sendo baixado.",
});
};
const displayContent = generatedContent || selectedOpinion?.content;
return (
<div className="flex flex-col h-full bg-background">
<div className="p-6 border-b border-border">
<h2 className="text-2xl font-bold text-foreground">
{selectedOpinion ? selectedOpinion.title : "Novo Parecer Jurídico"}
</h2>
{selectedOpinion && (
<p className="text-sm text-muted-foreground mt-1">
Criado em {new Date(selectedOpinion.createdAt).toLocaleDateString('pt-BR')}
</p>
)}
</div>
<div className="flex-1 overflow-hidden">
<div className="h-full p-6 space-y-4">
{(isCreatingNew || !selectedOpinion) && (
<>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Título do Parecer</label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Ex: Análise sobre Contrato de Prestação de Serviços"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">Categoria</label>
<Input
value={category}
onChange={(e) => setCategory(e.target.value)}
placeholder="Ex: Direito Civil, Trabalhista, etc."
/>
</div>
</>
)}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{selectedOpinion ? "Instruções para novo modelo" : "Instruções para gerar o parecer"}
</label>
<Textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
placeholder="Descreva os detalhes, contexto e aspectos legais que devem ser considerados no parecer..."
className="min-h-[120px]"
/>
</div>
<Button
onClick={handleGenerate}
disabled={isGenerating}
className="w-full gap-2"
>
{isGenerating ? (
<>Gerando parecer...</>
) : (
<>
<Sparkles className="w-4 h-4" />
{selectedOpinion ? "Gerar Novo Modelo com IA" : "Gerar Parecer com IA"}
</>
)}
</Button>
{displayContent && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium text-foreground">Parecer Gerado</label>
<Button
onClick={handleDownloadDocx}
variant="outline"
size="sm"
className="gap-2"
>
<Download className="w-4 h-4" />
Baixar DOCX
</Button>
</div>
<ScrollArea className="h-[300px] rounded-md border border-border p-4">
<pre className="whitespace-pre-wrap font-sans text-sm text-foreground">
{displayContent}
</pre>
</ScrollArea>
</div>
)}
</div>
</div>
</div>
);
};
+129
View File
@@ -0,0 +1,129 @@
import { useState } from "react";
import { Search, FileText } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { LegalOpinion } from "./AgentView";
interface AgentSearchProps {
onSelectOpinion: (opinion: LegalOpinion) => void;
}
// Simulação de base de pareceres
const mockOpinionsDatabase: LegalOpinion[] = [
{
id: "base-1",
title: "Análise Contratual - Prestação de Serviços Continuados",
content: "Parecer completo sobre prestação de serviços...",
createdAt: new Date("2024-01-15"),
category: "Direito Civil",
},
{
id: "base-2",
title: "Rescisão de Contrato de Trabalho - Justa Causa",
content: "Análise jurídica sobre rescisão contratual...",
createdAt: new Date("2024-02-20"),
category: "Direito Trabalhista",
},
{
id: "base-3",
title: "Responsabilidade Civil - Acidente de Trânsito",
content: "Parecer sobre responsabilidade civil em acidentes...",
createdAt: new Date("2024-03-10"),
category: "Direito Civil",
},
{
id: "base-4",
title: "Dissolução de Sociedade - Procedimentos e Requisitos",
content: "Análise completa sobre dissolução societária...",
createdAt: new Date("2024-01-25"),
category: "Direito Empresarial",
},
{
id: "base-5",
title: "Direitos do Consumidor - Vícios em Produtos",
content: "Parecer sobre direitos do consumidor e garantias...",
createdAt: new Date("2024-02-05"),
category: "Direito do Consumidor",
},
];
export const AgentSearch = ({ onSelectOpinion }: AgentSearchProps) => {
const [searchTerm, setSearchTerm] = useState("");
const [searchResults, setSearchResults] = useState<LegalOpinion[]>([]);
const handleSearch = () => {
if (!searchTerm.trim()) {
setSearchResults([]);
return;
}
const results = mockOpinionsDatabase.filter(
(opinion) =>
opinion.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
opinion.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
opinion.category?.toLowerCase().includes(searchTerm.toLowerCase())
);
setSearchResults(results);
};
return (
<div className="flex flex-col h-full bg-background">
<div className="p-6 border-b border-border space-y-4">
<h2 className="text-2xl font-bold text-foreground">Pesquisar Base de Pareceres</h2>
<div className="flex gap-2">
<Input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Digite título, frase ou categoria..."
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
/>
<Button onClick={handleSearch} className="gap-2">
<Search className="w-4 h-4" />
Buscar
</Button>
</div>
<p className="text-sm text-muted-foreground">
Base com {mockOpinionsDatabase.length} pareceres disponíveis
</p>
</div>
<ScrollArea className="flex-1 p-6">
{searchResults.length > 0 ? (
<div className="space-y-3">
{searchResults.map((opinion) => (
<div
key={opinion.id}
className="p-4 border border-border rounded-lg hover:bg-accent/50 cursor-pointer transition-colors"
onClick={() => onSelectOpinion(opinion)}
>
<div className="flex items-start gap-3">
<FileText className="w-5 h-5 text-primary mt-1" />
<div className="flex-1">
<h3 className="font-semibold text-foreground">{opinion.title}</h3>
<div className="flex gap-4 mt-2 text-sm text-muted-foreground">
<span>{opinion.category}</span>
<span>{new Date(opinion.createdAt).toLocaleDateString('pt-BR')}</span>
</div>
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
{opinion.content}
</p>
</div>
</div>
</div>
))}
</div>
) : searchTerm ? (
<div className="text-center text-muted-foreground py-12">
<p>Nenhum parecer encontrado para "{searchTerm}"</p>
</div>
) : (
<div className="text-center text-muted-foreground py-12">
<p>Digite um termo para pesquisar na base de pareceres</p>
</div>
)}
</ScrollArea>
</div>
);
};
+92
View File
@@ -0,0 +1,92 @@
import { Plus, Search, Trash2, FileText } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { LegalOpinion } from "./AgentView";
import { Input } from "@/components/ui/input";
import { useState } from "react";
interface AgentSidebarProps {
opinions: LegalOpinion[];
onCreateNew: () => void;
onSelectOpinion: (opinion: LegalOpinion) => void;
onDeleteOpinion: (id: string) => void;
onSearch: () => void;
selectedOpinionId?: string;
}
export const AgentSidebar = ({
opinions,
onCreateNew,
onSelectOpinion,
onDeleteOpinion,
onSearch,
selectedOpinionId,
}: AgentSidebarProps) => {
const [searchTerm, setSearchTerm] = useState("");
const filteredOpinions = opinions.filter(op =>
op.title.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="w-80 border-r border-border bg-sidebar flex flex-col h-full">
<div className="p-4 border-b border-border space-y-3">
<h2 className="text-xl font-bold text-sidebar-foreground">Agente de Parecer</h2>
<Button onClick={onCreateNew} className="w-full gap-2">
<Plus className="w-4 h-4" />
Novo Parecer
</Button>
<Button onClick={onSearch} variant="outline" className="w-full gap-2">
<Search className="w-4 h-4" />
Pesquisar Base de Pareceres
</Button>
<Input
placeholder="Buscar pareceres..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full"
/>
</div>
<ScrollArea className="flex-1">
<div className="p-2 space-y-1">
{filteredOpinions.map((opinion) => (
<div
key={opinion.id}
className={`group relative p-3 rounded-lg cursor-pointer transition-colors ${
selectedOpinionId === opinion.id
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "hover:bg-sidebar-accent/50 text-sidebar-foreground"
}`}
onClick={() => onSelectOpinion(opinion)}
>
<div className="flex items-start gap-2">
<FileText className="w-4 h-4 mt-1 flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{opinion.title}</p>
<p className="text-xs text-muted-foreground">
{new Date(opinion.createdAt).toLocaleDateString('pt-BR')}
</p>
{opinion.category && (
<p className="text-xs text-muted-foreground mt-1">{opinion.category}</p>
)}
</div>
<Button
variant="ghost"
size="icon"
className="opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6"
onClick={(e) => {
e.stopPropagation();
onDeleteOpinion(opinion.id);
}}
>
<Trash2 className="w-3 h-3" />
</Button>
</div>
</div>
))}
</div>
</ScrollArea>
</div>
);
};
+74
View File
@@ -0,0 +1,74 @@
import { useState } from "react";
import { AgentSidebar } from "./AgentSidebar";
import { AgentChat } from "./AgentChat";
import { AgentSearch } from "./AgentSearch";
export interface LegalOpinion {
id: string;
title: string;
content: string;
createdAt: Date;
category?: string;
}
export const AgentView = () => {
const [opinions, setOpinions] = useState<LegalOpinion[]>([]);
const [selectedOpinion, setSelectedOpinion] = useState<LegalOpinion | null>(null);
const [isCreatingNew, setIsCreatingNew] = useState(false);
const [showSearch, setShowSearch] = useState(false);
const handleCreateNew = () => {
setIsCreatingNew(true);
setSelectedOpinion(null);
setShowSearch(false);
};
const handleSelectOpinion = (opinion: LegalOpinion) => {
setSelectedOpinion(opinion);
setIsCreatingNew(false);
setShowSearch(false);
};
const handleSearch = () => {
setShowSearch(true);
setIsCreatingNew(false);
setSelectedOpinion(null);
};
const handleOpinionCreated = (newOpinion: LegalOpinion) => {
setOpinions([newOpinion, ...opinions]);
setSelectedOpinion(newOpinion);
setIsCreatingNew(false);
};
const handleDeleteOpinion = (id: string) => {
setOpinions(opinions.filter(op => op.id !== id));
if (selectedOpinion?.id === id) {
setSelectedOpinion(null);
}
};
return (
<div className="flex h-full">
<AgentSidebar
opinions={opinions}
onCreateNew={handleCreateNew}
onSelectOpinion={handleSelectOpinion}
onDeleteOpinion={handleDeleteOpinion}
onSearch={handleSearch}
selectedOpinionId={selectedOpinion?.id}
/>
<div className="flex-1">
{showSearch ? (
<AgentSearch onSelectOpinion={handleSelectOpinion} />
) : (
<AgentChat
selectedOpinion={selectedOpinion}
isCreatingNew={isCreatingNew}
onOpinionCreated={handleOpinionCreated}
/>
)}
</div>
</div>
);
};