Refactor Agent View
This commit is contained in:
@@ -1,195 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import { Plus, Search, Trash2, Eye } 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";
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow,
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
|
|
||||||
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">
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
<TableRow>
|
|
||||||
<TableHead>Título</TableHead>
|
|
||||||
<TableHead className="w-[120px]">Data</TableHead>
|
|
||||||
<TableHead className="w-[100px] text-center">Ações</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{filteredOpinions.map((opinion) => (
|
|
||||||
<TableRow
|
|
||||||
key={opinion.id}
|
|
||||||
className={selectedOpinionId === opinion.id ? "bg-sidebar-accent" : ""}
|
|
||||||
>
|
|
||||||
<TableCell className="font-medium">
|
|
||||||
<div>
|
|
||||||
<p className="truncate">{opinion.title}</p>
|
|
||||||
{opinion.category && (
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">{opinion.category}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-sm text-muted-foreground">
|
|
||||||
{new Date(opinion.createdAt).toLocaleDateString('pt-BR')}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center justify-center gap-1">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => onSelectOpinion(opinion)}
|
|
||||||
title="Visualizar"
|
|
||||||
>
|
|
||||||
<Eye className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
||||||
onClick={() => onDeleteOpinion(opinion.id)}
|
|
||||||
title="Excluir"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
{filteredOpinions.length === 0 && (
|
|
||||||
<div className="text-center text-muted-foreground py-8">
|
|
||||||
<p className="text-sm">Nenhum parecer encontrado</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,23 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { AgentSidebar } from "./AgentSidebar";
|
import { Plus, Search, Eye, Trash2, ArrowUpDown } from "lucide-react";
|
||||||
import { AgentChat } from "./AgentChat";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { OpinionDialog } from "./OpinionDialog";
|
||||||
import { AgentSearch } from "./AgentSearch";
|
import { AgentSearch } from "./AgentSearch";
|
||||||
|
|
||||||
export interface LegalOpinion {
|
export interface LegalOpinion {
|
||||||
@@ -11,64 +28,267 @@ export interface LegalOpinion {
|
|||||||
category?: string;
|
category?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SortField = "title" | "createdAt" | "category";
|
||||||
|
type SortOrder = "asc" | "desc";
|
||||||
|
|
||||||
export const AgentView = () => {
|
export const AgentView = () => {
|
||||||
const [opinions, setOpinions] = useState<LegalOpinion[]>([]);
|
const [opinions, setOpinions] = useState<LegalOpinion[]>([]);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
const [sortField, setSortField] = useState<SortField>("createdAt");
|
||||||
|
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [selectedOpinion, setSelectedOpinion] = useState<LegalOpinion | null>(null);
|
const [selectedOpinion, setSelectedOpinion] = useState<LegalOpinion | null>(null);
|
||||||
const [isCreatingNew, setIsCreatingNew] = useState(false);
|
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
|
|
||||||
const handleCreateNew = () => {
|
const handleSort = (field: SortField) => {
|
||||||
setIsCreatingNew(true);
|
if (sortField === field) {
|
||||||
setSelectedOpinion(null);
|
setSortOrder(sortOrder === "asc" ? "desc" : "asc");
|
||||||
setShowSearch(false);
|
} else {
|
||||||
|
setSortField(field);
|
||||||
|
setSortOrder("asc");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectOpinion = (opinion: LegalOpinion) => {
|
const filteredOpinions = opinions.filter(
|
||||||
setSelectedOpinion(opinion);
|
(op) =>
|
||||||
setIsCreatingNew(false);
|
op.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
setShowSearch(false);
|
op.category?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
};
|
);
|
||||||
|
|
||||||
const handleSearch = () => {
|
const sortedOpinions = [...filteredOpinions].sort((a, b) => {
|
||||||
setShowSearch(true);
|
const multiplier = sortOrder === "asc" ? 1 : -1;
|
||||||
setIsCreatingNew(false);
|
|
||||||
setSelectedOpinion(null);
|
if (sortField === "createdAt") {
|
||||||
};
|
return multiplier * (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
const aValue = a[sortField] || "";
|
||||||
|
const bValue = b[sortField] || "";
|
||||||
|
return multiplier * aValue.toString().localeCompare(bValue.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(sortedOpinions.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const paginatedOpinions = sortedOpinions.slice(startIndex, startIndex + itemsPerPage);
|
||||||
|
|
||||||
const handleOpinionCreated = (newOpinion: LegalOpinion) => {
|
const handleOpinionCreated = (newOpinion: LegalOpinion) => {
|
||||||
setOpinions([newOpinion, ...opinions]);
|
setOpinions([newOpinion, ...opinions]);
|
||||||
setSelectedOpinion(newOpinion);
|
setIsDialogOpen(false);
|
||||||
setIsCreatingNew(false);
|
setSelectedOpinion(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteOpinion = (id: string) => {
|
const handleDeleteOpinion = (id: string) => {
|
||||||
setOpinions(opinions.filter(op => op.id !== id));
|
setOpinions(opinions.filter(op => op.id !== id));
|
||||||
if (selectedOpinion?.id === id) {
|
|
||||||
setSelectedOpinion(null);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleViewOpinion = (opinion: LegalOpinion) => {
|
||||||
|
setSelectedOpinion(opinion);
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewOpinion = () => {
|
||||||
|
setSelectedOpinion(null);
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectFromSearch = (opinion: LegalOpinion) => {
|
||||||
|
setSelectedOpinion(opinion);
|
||||||
|
setShowSearch(false);
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showSearch) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="p-6 border-b border-border">
|
||||||
|
<Button variant="outline" onClick={() => setShowSearch(false)}>
|
||||||
|
Voltar para Pareceres
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<AgentSearch onSelectOpinion={handleSelectFromSearch} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full">
|
<div className="flex flex-col h-full bg-background">
|
||||||
<AgentSidebar
|
<div className="p-6 border-b border-border">
|
||||||
opinions={opinions}
|
<div className="flex items-center justify-between mb-4">
|
||||||
onCreateNew={handleCreateNew}
|
<h1 className="text-3xl font-bold text-foreground">Agente de Parecer Jurídico</h1>
|
||||||
onSelectOpinion={handleSelectOpinion}
|
<div className="flex gap-2">
|
||||||
onDeleteOpinion={handleDeleteOpinion}
|
<Button onClick={() => setShowSearch(true)} variant="outline" className="gap-2">
|
||||||
onSearch={handleSearch}
|
<Search className="w-4 h-4" />
|
||||||
selectedOpinionId={selectedOpinion?.id}
|
Pesquisar Base de Pareceres
|
||||||
/>
|
</Button>
|
||||||
<div className="flex-1">
|
<Button onClick={handleNewOpinion} className="gap-2">
|
||||||
{showSearch ? (
|
<Plus className="w-4 h-4" />
|
||||||
<AgentSearch onSelectOpinion={handleSelectOpinion} />
|
Novo Parecer
|
||||||
) : (
|
</Button>
|
||||||
<AgentChat
|
</div>
|
||||||
selectedOpinion={selectedOpinion}
|
</div>
|
||||||
isCreatingNew={isCreatingNew}
|
|
||||||
onOpinionCreated={handleOpinionCreated}
|
<div className="flex items-center gap-4">
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar pareceres..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">Itens por página:</span>
|
||||||
|
<Select
|
||||||
|
value={itemsPerPage.toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setItemsPerPage(Number(value));
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-20">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="5">5</SelectItem>
|
||||||
|
<SelectItem value="10">10</SelectItem>
|
||||||
|
<SelectItem value="20">20</SelectItem>
|
||||||
|
<SelectItem value="50">50</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-auto p-6">
|
||||||
|
<div className="border rounded-lg">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleSort("title")}
|
||||||
|
className="flex items-center gap-1 font-semibold"
|
||||||
|
>
|
||||||
|
Título
|
||||||
|
<ArrowUpDown className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleSort("category")}
|
||||||
|
className="flex items-center gap-1 font-semibold"
|
||||||
|
>
|
||||||
|
Categoria
|
||||||
|
<ArrowUpDown className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => handleSort("createdAt")}
|
||||||
|
className="flex items-center gap-1 font-semibold"
|
||||||
|
>
|
||||||
|
Data de Criação
|
||||||
|
<ArrowUpDown className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center">Ações</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{paginatedOpinions.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center py-12 text-muted-foreground">
|
||||||
|
{searchTerm
|
||||||
|
? "Nenhum parecer encontrado"
|
||||||
|
: "Nenhum parecer criado ainda. Clique em 'Novo Parecer' para começar."}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
paginatedOpinions.map((opinion) => (
|
||||||
|
<TableRow key={opinion.id}>
|
||||||
|
<TableCell className="font-medium">{opinion.title}</TableCell>
|
||||||
|
<TableCell>{opinion.category || "-"}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{new Date(opinion.createdAt).toLocaleDateString("pt-BR")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleViewOpinion(opinion)}
|
||||||
|
title="Visualizar"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDeleteOpinion(opinion.id)}
|
||||||
|
className="text-destructive hover:text-destructive"
|
||||||
|
title="Excluir"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Mostrando {startIndex + 1} a {Math.min(startIndex + itemsPerPage, sortedOpinions.length)} de{" "}
|
||||||
|
{sortedOpinions.length} pareceres
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
Anterior
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
|
||||||
|
<Button
|
||||||
|
key={page}
|
||||||
|
variant={currentPage === page ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
className="w-10"
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
Próxima
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<OpinionDialog
|
||||||
|
open={isDialogOpen}
|
||||||
|
onOpenChange={setIsDialogOpen}
|
||||||
|
selectedOpinion={selectedOpinion}
|
||||||
|
onOpinionCreated={handleOpinionCreated}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Download, Sparkles } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { LegalOpinion } from "./AgentView";
|
||||||
|
|
||||||
|
interface OpinionDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
selectedOpinion: LegalOpinion | null;
|
||||||
|
onOpinionCreated: (opinion: LegalOpinion) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OpinionDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
selectedOpinion,
|
||||||
|
onOpinionCreated,
|
||||||
|
}: OpinionDialogProps) => {
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [category, setCategory] = useState("");
|
||||||
|
const [instructions, setInstructions] = useState("");
|
||||||
|
const [generatedContent, setGeneratedContent] = useState("");
|
||||||
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedOpinion) {
|
||||||
|
setTitle(selectedOpinion.title);
|
||||||
|
setCategory(selectedOpinion.category || "");
|
||||||
|
setGeneratedContent(selectedOpinion.content);
|
||||||
|
setInstructions("");
|
||||||
|
} else {
|
||||||
|
setTitle("");
|
||||||
|
setCategory("");
|
||||||
|
setInstructions("");
|
||||||
|
setGeneratedContent("");
|
||||||
|
}
|
||||||
|
}, [selectedOpinion]);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title.trim()) {
|
||||||
|
toast({
|
||||||
|
title: "Título necessário",
|
||||||
|
description: "Por favor, forneça um título para 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}
|
||||||
|
CATEGORIA: ${category || "Não especificada"}
|
||||||
|
|
||||||
|
${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
|
||||||
|
- Análise da legislação aplicável ao caso
|
||||||
|
- Interpretação dos dispositivos legais pertinentes
|
||||||
|
|
||||||
|
2. Precedentes Judiciais
|
||||||
|
- Jurisprudência dos tribunais superiores
|
||||||
|
- Decisões relevantes em casos semelhantes
|
||||||
|
|
||||||
|
3. Doutrina Aplicável
|
||||||
|
- Posicionamento dos principais autores
|
||||||
|
- Análise crítica da literatura jurídica
|
||||||
|
|
||||||
|
4. Conclusão e Recomendações
|
||||||
|
- Síntese dos pontos fundamentais
|
||||||
|
- Orientações práticas para o caso
|
||||||
|
|
||||||
|
CONCLUSÃO:
|
||||||
|
|
||||||
|
Com base na análise realizada, considerando a legislação vigente, a jurisprudência consolidada e a doutrina majoritária, conclui-se que os fundamentos apresentados nas instruções são juridicamente sustentáveis.
|
||||||
|
|
||||||
|
Recomenda-se:
|
||||||
|
- Acompanhamento da evolução legislativa
|
||||||
|
- Monitoramento de novas decisões judiciais
|
||||||
|
- Revisão periódica do presente parecer
|
||||||
|
|
||||||
|
___________________________
|
||||||
|
Parecer gerado em ${new Date().toLocaleDateString("pt-BR")}
|
||||||
|
Profissional responsável: IA Jurídica HGTX Codex`;
|
||||||
|
|
||||||
|
setGeneratedContent(mockContent);
|
||||||
|
setIsGenerating(false);
|
||||||
|
|
||||||
|
const newOpinion: LegalOpinion = {
|
||||||
|
id: selectedOpinion?.id || Date.now().toString(),
|
||||||
|
title: title,
|
||||||
|
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 = () => {
|
||||||
|
const content = generatedContent;
|
||||||
|
const blob = new Blob([content], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${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.",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{selectedOpinion ? "Gerar Novo Modelo do Parecer" : "Novo Parecer Jurídico"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{selectedOpinion
|
||||||
|
? "Forneça instruções para gerar um novo modelo baseado neste parecer"
|
||||||
|
: "Preencha os dados e instruções para gerar um novo parecer com IA"}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<ScrollArea className="max-h-[calc(90vh-120px)] pr-4">
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="title">Título do Parecer *</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="Ex: Análise sobre Contrato de Prestação de Serviços"
|
||||||
|
disabled={!!selectedOpinion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="category">Categoria</Label>
|
||||||
|
<Input
|
||||||
|
id="category"
|
||||||
|
value={category}
|
||||||
|
onChange={(e) => setCategory(e.target.value)}
|
||||||
|
placeholder="Ex: Direito Civil, Trabalhista, etc."
|
||||||
|
disabled={!!selectedOpinion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="instructions">
|
||||||
|
Instruções para gerar o parecer *
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="instructions"
|
||||||
|
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-[150px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleGenerate}
|
||||||
|
disabled={isGenerating}
|
||||||
|
className="w-full gap-2"
|
||||||
|
>
|
||||||
|
{isGenerating ? (
|
||||||
|
<>Gerando parecer...</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
Gerar Parecer com IA
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{generatedContent && (
|
||||||
|
<div className="space-y-2 pt-4 border-t">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>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 p-4">
|
||||||
|
<pre className="whitespace-pre-wrap font-sans text-sm">
|
||||||
|
{generatedContent}
|
||||||
|
</pre>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user