Add "Agente de Parecer" feature
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import { useState } from "react";
|
||||
import { MessageSquare, Image, Mic, ArrowLeft, Plus, ChevronLeft, ChevronRight, Bot } from "lucide-react";
|
||||
import { MessageSquare, Image, Mic, ArrowLeft, Plus, ChevronLeft, ChevronRight, Bot, FileText } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
activeTab: "chat" | "images" | "audio" | "bots";
|
||||
onTabChange: (tab: "chat" | "images" | "audio" | "bots") => void;
|
||||
activeTab: "chat" | "images" | "audio" | "bots" | "agent";
|
||||
onTabChange: (tab: "chat" | "images" | "audio" | "bots" | "agent") => void;
|
||||
}
|
||||
|
||||
type TabType = "chat" | "images" | "audio" | "bots";
|
||||
type TabType = "chat" | "images" | "audio" | "bots" | "agent";
|
||||
|
||||
export const Layout = ({ children, activeTab, onTabChange }: LayoutProps) => {
|
||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||
@@ -20,6 +20,7 @@ export const Layout = ({ children, activeTab, onTabChange }: LayoutProps) => {
|
||||
{ id: "images" as TabType, label: "Imagens", icon: Image },
|
||||
{ id: "audio" as TabType, label: "Áudio", icon: Mic },
|
||||
{ id: "bots" as TabType, label: "Bots", icon: Bot },
|
||||
{ id: "agent" as TabType, label: "Agente de Parecer", icon: FileText },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user