[Parecer Juridico]
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Search, FileText } from "lucide-react";
|
import { Search, FileText } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -12,77 +12,70 @@ import {
|
|||||||
PaginationNext,
|
PaginationNext,
|
||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from "@/components/ui/pagination";
|
} from "@/components/ui/pagination";
|
||||||
import { LegalOpinion } from "./AgentView";
|
import { agentService, OpinionRecord } from "@/services/agent";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
interface AgentSearchProps {
|
interface AgentSearchProps {
|
||||||
onSelectOpinion: (opinion: LegalOpinion) => void;
|
onSelectOpinion: (opinion: OpinionRecord) => 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) => {
|
export const AgentSearch = ({ onSelectOpinion }: AgentSearchProps) => {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [searchResults, setSearchResults] = useState<LegalOpinion[]>([]);
|
const [searchResults, setSearchResults] = useState<OpinionRecord[]>([]);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(5);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
const handleSearch = () => {
|
// Busca pareceres da API
|
||||||
|
const handleSearch = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await agentService.getOpinions({
|
||||||
|
page: currentPage,
|
||||||
|
per_page: itemsPerPage,
|
||||||
|
search: searchTerm,
|
||||||
|
});
|
||||||
|
setSearchResults(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Erro ao buscar pareceres:', error);
|
||||||
|
toast({
|
||||||
|
title: "Erro ao buscar pareceres",
|
||||||
|
description: error.message || "Não foi possível buscar os pareceres.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setSearchResults([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Busca automaticamente quando a página ou itemsPerPage mudam
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchTerm.trim()) {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
}, [currentPage, itemsPerPage]);
|
||||||
|
|
||||||
|
// Debounce para busca automática
|
||||||
|
useEffect(() => {
|
||||||
if (!searchTerm.trim()) {
|
if (!searchTerm.trim()) {
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = mockOpinionsDatabase.filter(
|
const timer = setTimeout(() => {
|
||||||
(opinion) =>
|
if (currentPage === 1) {
|
||||||
opinion.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
handleSearch();
|
||||||
opinion.content.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
} else {
|
||||||
opinion.category?.toLowerCase().includes(searchTerm.toLowerCase())
|
setCurrentPage(1); // Volta para primeira página ao buscar
|
||||||
);
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
setSearchResults(results);
|
return () => clearTimeout(timer);
|
||||||
setCurrentPage(1);
|
}, [searchTerm]);
|
||||||
};
|
|
||||||
|
|
||||||
const totalPages = Math.ceil(searchResults.length / itemsPerPage);
|
const totalPages = Math.ceil(searchResults.length / itemsPerPage);
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
||||||
const paginatedResults = searchResults.slice(startIndex, startIndex + itemsPerPage);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-background">
|
<div className="flex flex-col h-full bg-background">
|
||||||
@@ -92,24 +85,30 @@ export const AgentSearch = ({ onSelectOpinion }: AgentSearchProps) => {
|
|||||||
<Input
|
<Input
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
placeholder="Digite título, frase ou categoria..."
|
placeholder="Digite título ou categoria..."
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleSearch} className="gap-2">
|
<Button onClick={handleSearch} className="gap-2" disabled={isLoading}>
|
||||||
<Search className="w-4 h-4" />
|
<Search className="w-4 h-4" />
|
||||||
Buscar
|
{isLoading ? 'Buscando...' : 'Buscar'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground">
|
{searchResults.length > 0 && (
|
||||||
Base com {mockOpinionsDatabase.length} pareceres disponíveis
|
<p className="text-sm text-muted-foreground">
|
||||||
</p>
|
{searchResults.length} {searchResults.length === 1 ? 'parecer encontrado' : 'pareceres encontrados'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<ScrollArea className="flex-1 p-6">
|
<ScrollArea className="flex-1 p-6">
|
||||||
{searchResults.length > 0 ? (
|
{isLoading ? (
|
||||||
|
<div className="text-center text-muted-foreground py-12">
|
||||||
|
<p>Buscando pareceres...</p>
|
||||||
|
</div>
|
||||||
|
) : searchResults.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{paginatedResults.map((opinion) => (
|
{searchResults.map((opinion) => (
|
||||||
<div
|
<div
|
||||||
key={opinion.id}
|
key={opinion.id}
|
||||||
className="p-4 border border-border rounded-lg hover:bg-accent/50 cursor-pointer transition-colors"
|
className="p-4 border border-border rounded-lg hover:bg-accent/50 cursor-pointer transition-colors"
|
||||||
@@ -118,13 +117,13 @@ export const AgentSearch = ({ onSelectOpinion }: AgentSearchProps) => {
|
|||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<FileText className="w-5 h-5 text-primary mt-1" />
|
<FileText className="w-5 h-5 text-primary mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-semibold text-foreground">{opinion.title}</h3>
|
<h3 className="font-semibold text-foreground">{opinion.titulo}</h3>
|
||||||
<div className="flex gap-4 mt-2 text-sm text-muted-foreground">
|
<div className="flex gap-4 mt-2 text-sm text-muted-foreground">
|
||||||
<span>{opinion.category}</span>
|
<span>{opinion.categoria || '-'}</span>
|
||||||
<span>{new Date(opinion.createdAt).toLocaleDateString('pt-BR')}</span>
|
<span>{new Date(opinion.created_at).toLocaleDateString('pt-BR')}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
|
<p className="text-sm text-muted-foreground mt-2 line-clamp-2">
|
||||||
{opinion.content}
|
{opinion.instrucoes}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Plus, Search, Eye, Trash2, ArrowUpDown } from "lucide-react";
|
import { Plus, Search, Eye, Trash2, ArrowUpDown, Download } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -17,30 +17,84 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
import { OpinionDialog } from "./OpinionDialog";
|
import { OpinionDialog } from "./OpinionDialog";
|
||||||
import { AgentSearch } from "./AgentSearch";
|
import { AgentSearch } from "./AgentSearch";
|
||||||
|
import { agentService, OpinionRecord } from "@/services/agent";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
export interface LegalOpinion {
|
type SortField = "titulo" | "created_at" | "categoria";
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: Date;
|
|
||||||
category?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SortField = "title" | "createdAt" | "category";
|
|
||||||
type SortOrder = "asc" | "desc";
|
type SortOrder = "asc" | "desc";
|
||||||
|
|
||||||
export const AgentView = () => {
|
export const AgentView = () => {
|
||||||
const [opinions, setOpinions] = useState<LegalOpinion[]>([]);
|
const [opinions, setOpinions] = useState<OpinionRecord[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
const [sortField, setSortField] = useState<SortField>("createdAt");
|
const [sortField, setSortField] = useState<SortField>("created_at");
|
||||||
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
|
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [selectedOpinion, setSelectedOpinion] = useState<LegalOpinion | null>(null);
|
const [selectedOpinion, setSelectedOpinion] = useState<OpinionRecord | null>(null);
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [opinionToDelete, setOpinionToDelete] = useState<OpinionRecord | null>(null);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
// Carrega pareceres da API
|
||||||
|
const loadOpinions = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await agentService.getOpinions({
|
||||||
|
page: currentPage,
|
||||||
|
per_page: itemsPerPage,
|
||||||
|
search: searchTerm,
|
||||||
|
});
|
||||||
|
setOpinions(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Erro ao carregar pareceres:', error);
|
||||||
|
toast({
|
||||||
|
title: "Erro ao carregar pareceres",
|
||||||
|
description: error.message || "Não foi possível carregar a lista de pareceres.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Carrega pareceres ao montar o componente e quando mudar página/busca
|
||||||
|
useEffect(() => {
|
||||||
|
loadOpinions();
|
||||||
|
}, [currentPage, itemsPerPage]);
|
||||||
|
|
||||||
|
// Debounce para busca
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (currentPage === 1) {
|
||||||
|
loadOpinions();
|
||||||
|
} else {
|
||||||
|
setCurrentPage(1); // Volta para a primeira página ao buscar
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
const handleSort = (field: SortField) => {
|
const handleSort = (field: SortField) => {
|
||||||
if (sortField === field) {
|
if (sortField === field) {
|
||||||
@@ -51,39 +105,26 @@ export const AgentView = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredOpinions = opinions.filter(
|
const sortedOpinions = [...opinions].sort((a, b) => {
|
||||||
(op) =>
|
|
||||||
op.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
||||||
op.category?.toLowerCase().includes(searchTerm.toLowerCase())
|
|
||||||
);
|
|
||||||
|
|
||||||
const sortedOpinions = [...filteredOpinions].sort((a, b) => {
|
|
||||||
const multiplier = sortOrder === "asc" ? 1 : -1;
|
const multiplier = sortOrder === "asc" ? 1 : -1;
|
||||||
|
|
||||||
if (sortField === "createdAt") {
|
if (sortField === "created_at") {
|
||||||
return multiplier * (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
return multiplier * (new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
const aValue = a[sortField] || "";
|
const aValue = a[sortField] || "";
|
||||||
const bValue = b[sortField] || "";
|
const bValue = b[sortField] || "";
|
||||||
return multiplier * aValue.toString().localeCompare(bValue.toString());
|
return multiplier * aValue.toString().localeCompare(bValue.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalPages = Math.ceil(sortedOpinions.length / itemsPerPage);
|
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 = () => {
|
||||||
setOpinions([newOpinion, ...opinions]);
|
// Recarrega a lista após criar um novo parecer
|
||||||
setIsDialogOpen(false);
|
loadOpinions();
|
||||||
setSelectedOpinion(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteOpinion = (id: string) => {
|
const handleViewOpinion = (opinion: OpinionRecord) => {
|
||||||
setOpinions(opinions.filter(op => op.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewOpinion = (opinion: LegalOpinion) => {
|
|
||||||
setSelectedOpinion(opinion);
|
setSelectedOpinion(opinion);
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
@@ -93,12 +134,69 @@ export const AgentView = () => {
|
|||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectFromSearch = (opinion: LegalOpinion) => {
|
const handleSelectFromSearch = (opinion: OpinionRecord) => {
|
||||||
setSelectedOpinion(opinion);
|
setSelectedOpinion(opinion);
|
||||||
setShowSearch(false);
|
setShowSearch(false);
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadVersion = async (opinion: OpinionRecord, version: 'v1' | 'v2') => {
|
||||||
|
const fileUrl = version === 'v1' ? opinion.file_url : opinion.file_url_melhoria;
|
||||||
|
|
||||||
|
if (!fileUrl) {
|
||||||
|
toast({
|
||||||
|
title: "Arquivo não disponível",
|
||||||
|
description: `A ${version === 'v1' ? 'versão 1' : 'versão melhorada'} ainda não está disponível.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileName = `${opinion.titulo}_${version === 'v1' ? 'v1' : 'melhorada'}.docx`;
|
||||||
|
await agentService.downloadOpinion(fileUrl, fileName);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Download iniciado",
|
||||||
|
description: "O parecer está sendo baixado.",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Erro ao fazer download",
|
||||||
|
description: error.message || "Não foi possível baixar o arquivo.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteOpinion = async () => {
|
||||||
|
if (!opinionToDelete) return;
|
||||||
|
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await agentService.deleteOpinion(opinionToDelete.id);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Parecer excluído",
|
||||||
|
description: "O parecer foi excluído com sucesso.",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recarrega a lista de pareceres
|
||||||
|
await loadOpinions();
|
||||||
|
|
||||||
|
// Fecha o diálogo de confirmação
|
||||||
|
setOpinionToDelete(null);
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Erro ao excluir parecer",
|
||||||
|
description: error.message || "Não foi possível excluir o parecer.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (showSearch) {
|
if (showSearch) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
@@ -116,7 +214,7 @@ export const AgentView = () => {
|
|||||||
<div className="flex flex-col h-full bg-background pb-16 md:pb-0">
|
<div className="flex flex-col h-full bg-background pb-16 md:pb-0">
|
||||||
<div className="p-3 md:p-6 border-b border-border">
|
<div className="p-3 md:p-6 border-b border-border">
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 mb-4">
|
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 mb-4">
|
||||||
<h1 className="text-xl md:text-3xl font-bold text-foreground">Agente de Parecer Jurídico</h1>
|
<h1 className="text-xl md:text-3xl font-bold text-foreground">Gerador de Modelos de Parecer Jurídico</h1>
|
||||||
<div className="flex gap-2 w-full md:w-auto">
|
<div className="flex gap-2 w-full md:w-auto">
|
||||||
<Button onClick={() => setShowSearch(true)} variant="outline" className="gap-1 md:gap-2 flex-1 md:flex-none text-xs md:text-sm">
|
<Button onClick={() => setShowSearch(true)} variant="outline" className="gap-1 md:gap-2 flex-1 md:flex-none text-xs md:text-sm">
|
||||||
<Search className="w-3 h-3 md:w-4 md:h-4" />
|
<Search className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
@@ -169,7 +267,7 @@ export const AgentView = () => {
|
|||||||
<TableHead className="min-w-[200px]">
|
<TableHead className="min-w-[200px]">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => handleSort("title")}
|
onClick={() => handleSort("titulo")}
|
||||||
className="flex items-center gap-1 font-semibold text-xs md:text-sm p-1 md:p-2"
|
className="flex items-center gap-1 font-semibold text-xs md:text-sm p-1 md:p-2"
|
||||||
>
|
>
|
||||||
Título
|
Título
|
||||||
@@ -179,7 +277,7 @@ export const AgentView = () => {
|
|||||||
<TableHead className="hidden md:table-cell">
|
<TableHead className="hidden md:table-cell">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => handleSort("category")}
|
onClick={() => handleSort("categoria")}
|
||||||
className="flex items-center gap-1 font-semibold text-sm"
|
className="flex items-center gap-1 font-semibold text-sm"
|
||||||
>
|
>
|
||||||
Categoria
|
Categoria
|
||||||
@@ -189,7 +287,7 @@ export const AgentView = () => {
|
|||||||
<TableHead className="hidden sm:table-cell">
|
<TableHead className="hidden sm:table-cell">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => handleSort("createdAt")}
|
onClick={() => handleSort("created_at")}
|
||||||
className="flex items-center gap-1 font-semibold text-xs md:text-sm p-1 md:p-2"
|
className="flex items-center gap-1 font-semibold text-xs md:text-sm p-1 md:p-2"
|
||||||
>
|
>
|
||||||
Data
|
Data
|
||||||
@@ -200,7 +298,13 @@ export const AgentView = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{paginatedOpinions.length === 0 ? (
|
{isLoading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center py-12 text-muted-foreground">
|
||||||
|
Carregando pareceres...
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : sortedOpinions.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={4} className="text-center py-12 text-muted-foreground">
|
<TableCell colSpan={4} className="text-center py-12 text-muted-foreground">
|
||||||
{searchTerm
|
{searchTerm
|
||||||
@@ -209,12 +313,12 @@ export const AgentView = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
paginatedOpinions.map((opinion) => (
|
sortedOpinions.map((opinion) => (
|
||||||
<TableRow key={opinion.id}>
|
<TableRow key={opinion.id}>
|
||||||
<TableCell className="font-medium text-xs md:text-sm">{opinion.title}</TableCell>
|
<TableCell className="font-medium text-xs md:text-sm">{opinion.titulo}</TableCell>
|
||||||
<TableCell className="hidden md:table-cell text-sm">{opinion.category || "-"}</TableCell>
|
<TableCell className="hidden md:table-cell text-sm">{opinion.categoria || "-"}</TableCell>
|
||||||
<TableCell className="hidden sm:table-cell text-xs md:text-sm">
|
<TableCell className="hidden sm:table-cell text-xs md:text-sm">
|
||||||
{new Date(opinion.createdAt).toLocaleDateString("pt-BR")}
|
{new Date(opinion.created_at).toLocaleDateString("pt-BR")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-center justify-center gap-1">
|
||||||
@@ -227,12 +331,40 @@ export const AgentView = () => {
|
|||||||
>
|
>
|
||||||
<Eye className="w-3 h-3 md:w-4 md:h-4" />
|
<Eye className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title="Baixar"
|
||||||
|
className="h-7 w-7 md:h-9 md:w-9"
|
||||||
|
>
|
||||||
|
<Download className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDownloadVersion(opinion, 'v1')}
|
||||||
|
disabled={!opinion.file_url}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
Versão 1
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleDownloadVersion(opinion, 'v2')}
|
||||||
|
disabled={!opinion.file_url_melhoria}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
Versão Melhorada
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => handleDeleteOpinion(opinion.id)}
|
onClick={() => setOpinionToDelete(opinion)}
|
||||||
className="text-destructive hover:text-destructive h-7 w-7 md:h-9 md:w-9"
|
|
||||||
title="Excluir"
|
title="Excluir"
|
||||||
|
className="h-7 w-7 md:h-9 md:w-9 text-destructive hover:text-destructive"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3 md:w-4 md:h-4" />
|
<Trash2 className="w-3 h-3 md:w-4 md:h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -245,10 +377,10 @@ export const AgentView = () => {
|
|||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{totalPages > 1 && (
|
{sortedOpinions.length > 0 && (
|
||||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-2 mt-4">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-2 mt-4">
|
||||||
<p className="text-xs md:text-sm text-muted-foreground text-center sm:text-left">
|
<p className="text-xs md:text-sm text-muted-foreground text-center sm:text-left">
|
||||||
{startIndex + 1}-{Math.min(startIndex + itemsPerPage, sortedOpinions.length)} de {sortedOpinions.length}
|
Mostrando {sortedOpinions.length} {sortedOpinions.length === 1 ? 'parecer' : 'pareceres'}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-1 md:gap-2 justify-center">
|
<div className="flex gap-1 md:gap-2 justify-center">
|
||||||
<Button
|
<Button
|
||||||
@@ -307,6 +439,27 @@ export const AgentView = () => {
|
|||||||
selectedOpinion={selectedOpinion}
|
selectedOpinion={selectedOpinion}
|
||||||
onOpinionCreated={handleOpinionCreated}
|
onOpinionCreated={handleOpinionCreated}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AlertDialog open={!!opinionToDelete} onOpenChange={(open) => !open && setOpinionToDelete(null)}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Tem certeza que deseja excluir o parecer "{opinionToDelete?.titulo}"? Esta ação não pode ser desfeita.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel disabled={isDeleting}>Cancelar</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleDeleteOpinion}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
>
|
||||||
|
{isDeleting ? "Excluindo..." : "Excluir"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import axios from "axios";
|
|
||||||
import { Download, Sparkles } from "lucide-react";
|
import { Download, Sparkles } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -14,13 +13,13 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { LegalOpinion } from "./AgentView";
|
import { agentService, OpinionRecord } from "@/services/agent";
|
||||||
|
|
||||||
interface OpinionDialogProps {
|
interface OpinionDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
selectedOpinion: LegalOpinion | null;
|
selectedOpinion: OpinionRecord | null;
|
||||||
onOpinionCreated: (opinion: LegalOpinion) => void;
|
onOpinionCreated: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OpinionDialog = ({
|
export const OpinionDialog = ({
|
||||||
@@ -32,23 +31,23 @@ export const OpinionDialog = ({
|
|||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [category, setCategory] = useState("");
|
const [category, setCategory] = useState("");
|
||||||
const [instructions, setInstructions] = useState("");
|
const [instructions, setInstructions] = useState("");
|
||||||
const [generatedContent, setGeneratedContent] = useState("");
|
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
const [isGenerating, setIsGenerating] = useState(false);
|
||||||
|
const [createdOpinion, setCreatedOpinion] = useState<OpinionRecord | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedOpinion) {
|
if (selectedOpinion) {
|
||||||
setTitle(selectedOpinion.title);
|
setTitle(selectedOpinion.titulo);
|
||||||
setCategory(selectedOpinion.category || "");
|
setCategory(selectedOpinion.categoria || "");
|
||||||
setGeneratedContent(selectedOpinion.content);
|
setInstructions(selectedOpinion.instrucoes || "");
|
||||||
setInstructions("");
|
setCreatedOpinion(selectedOpinion);
|
||||||
} else {
|
} else {
|
||||||
setTitle("");
|
setTitle("");
|
||||||
setCategory("");
|
setCategory("");
|
||||||
setInstructions("");
|
setInstructions("");
|
||||||
setGeneratedContent("");
|
setCreatedOpinion(null);
|
||||||
}
|
}
|
||||||
}, [selectedOpinion]);
|
}, [selectedOpinion, open]);
|
||||||
|
|
||||||
const handleGenerate = async () => {
|
const handleGenerate = async () => {
|
||||||
if (!instructions.trim()) {
|
if (!instructions.trim()) {
|
||||||
@@ -72,53 +71,39 @@ export const OpinionDialog = ({
|
|||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await agentService.createOpinion({
|
||||||
"https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/gepam/parecer-tecnico",
|
titulo: title,
|
||||||
{
|
categoria: category,
|
||||||
titulo: title,
|
instrucoes: instructions,
|
||||||
categoria: category,
|
|
||||||
instrucoes: instructions,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = response.data.parecer || response.data.content || JSON.stringify(response.data, null, 2);
|
|
||||||
|
|
||||||
setGeneratedContent(content);
|
|
||||||
setIsGenerating(false);
|
|
||||||
|
|
||||||
const newOpinion: LegalOpinion = {
|
|
||||||
id: selectedOpinion?.id || Date.now().toString(),
|
|
||||||
title: title,
|
|
||||||
content: content,
|
|
||||||
createdAt: new Date(),
|
|
||||||
category: category || undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
onOpinionCreated(newOpinion);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Parecer gerado com sucesso!",
|
|
||||||
description: "O parecer está pronto para download.",
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
|
||||||
|
if (response.success) {
|
||||||
|
toast({
|
||||||
|
title: "Parecer gerado com sucesso!",
|
||||||
|
description: "O parecer foi criado e está disponível para download.",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chama o callback para atualizar a lista
|
||||||
|
onOpinionCreated();
|
||||||
|
|
||||||
|
// Fecha o dialog após sucesso
|
||||||
|
setIsGenerating(false);
|
||||||
|
onOpenChange(false);
|
||||||
|
|
||||||
|
// Limpa os campos
|
||||||
|
setTitle("");
|
||||||
|
setCategory("");
|
||||||
|
setInstructions("");
|
||||||
|
setCreatedOpinion(null);
|
||||||
|
} else {
|
||||||
|
throw new Error('Falha ao criar parecer');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
console.error("Erro ao gerar parecer:", error);
|
console.error("Erro ao gerar parecer:", error);
|
||||||
setIsGenerating(false);
|
setIsGenerating(false);
|
||||||
|
|
||||||
let errorMessage = "Não foi possível gerar o parecer. Tente novamente.";
|
const errorMessage = error.message || "Não foi possível gerar o parecer. Tente novamente.";
|
||||||
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
if (error.response) {
|
|
||||||
errorMessage = error.response.data?.message || `Erro do servidor: ${error.response.status}`;
|
|
||||||
} else if (error.request) {
|
|
||||||
errorMessage = "Sem resposta do servidor. Verifique sua conexão.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Erro ao gerar parecer",
|
title: "Erro ao gerar parecer",
|
||||||
description: errorMessage,
|
description: errorMessage,
|
||||||
@@ -127,22 +112,45 @@ export const OpinionDialog = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadDocx = () => {
|
const handleDownloadVersion = async (version: 'v1' | 'v2') => {
|
||||||
const content = generatedContent;
|
if (!createdOpinion && !selectedOpinion) {
|
||||||
const blob = new Blob([content], { type: "text/plain" });
|
toast({
|
||||||
const url = URL.createObjectURL(blob);
|
title: "Nenhum parecer disponível",
|
||||||
const a = document.createElement("a");
|
description: "Por favor, gere um parecer primeiro.",
|
||||||
a.href = url;
|
variant: "destructive",
|
||||||
a.download = `${title || "parecer"}.txt`;
|
});
|
||||||
document.body.appendChild(a);
|
return;
|
||||||
a.click();
|
}
|
||||||
document.body.removeChild(a);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
toast({
|
const opinion = createdOpinion || selectedOpinion;
|
||||||
title: "Download iniciado",
|
if (!opinion) return;
|
||||||
description: "O parecer está sendo baixado.",
|
|
||||||
});
|
const fileUrl = version === 'v1' ? opinion.file_url : opinion.file_url_melhoria;
|
||||||
|
|
||||||
|
if (!fileUrl) {
|
||||||
|
toast({
|
||||||
|
title: "Arquivo não disponível",
|
||||||
|
description: `A ${version === 'v1' ? 'versão 1' : 'versão melhorada'} ainda não está disponível.`,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileName = `${opinion.titulo}_${version === 'v1' ? 'v1' : 'melhorada'}.docx`;
|
||||||
|
await agentService.downloadOpinion(fileUrl, fileName);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Download iniciado",
|
||||||
|
description: "O parecer está sendo baixado.",
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
toast({
|
||||||
|
title: "Erro ao fazer download",
|
||||||
|
description: error.message || "Não foi possível baixar o arquivo.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -150,11 +158,11 @@ export const OpinionDialog = ({
|
|||||||
<DialogContent className="max-w-[95vw] md:max-w-4xl max-h-[90vh]">
|
<DialogContent className="max-w-[95vw] md:max-w-4xl max-h-[90vh]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{selectedOpinion ? "Gerar Novo Modelo do Parecer" : "Novo Parecer Jurídico"}
|
{selectedOpinion ? "Visualizar Parecer" : "Novo Parecer Jurídico"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{selectedOpinion
|
{selectedOpinion
|
||||||
? "Forneça instruções para gerar um novo modelo baseado neste parecer"
|
? "Visualize os detalhes do parecer e faça o download das versões disponíveis"
|
||||||
: "Preencha os dados e instruções para gerar um novo parecer com IA"}
|
: "Preencha os dados e instruções para gerar um novo parecer com IA"}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
@@ -196,40 +204,46 @@ export const OpinionDialog = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
{!selectedOpinion && (
|
||||||
onClick={handleGenerate}
|
<Button
|
||||||
disabled={isGenerating}
|
onClick={handleGenerate}
|
||||||
className="w-full gap-2"
|
disabled={isGenerating}
|
||||||
>
|
className="w-full gap-2"
|
||||||
{isGenerating ? (
|
>
|
||||||
<>Gerando parecer...</>
|
{isGenerating ? (
|
||||||
) : (
|
<>Gerando parecer...</>
|
||||||
<>
|
) : (
|
||||||
<Sparkles className="w-4 h-4" />
|
<>
|
||||||
Gerar Parecer com IA
|
<Sparkles className="w-4 h-4" />
|
||||||
</>
|
Gerar Parecer com IA
|
||||||
)}
|
</>
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{generatedContent && (
|
{selectedOpinion && (
|
||||||
<div className="space-y-2 pt-4 border-t">
|
<div className="space-y-3 pt-4 border-t">
|
||||||
<div className="flex items-center justify-between">
|
<Label>Downloads Disponíveis</Label>
|
||||||
<Label>Parecer Gerado</Label>
|
<div className="flex flex-col gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDownloadDocx}
|
onClick={() => handleDownloadVersion('v1')}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
className="w-full gap-2 justify-start"
|
||||||
className="gap-2"
|
disabled={!selectedOpinion.file_url}
|
||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
Baixar DOCX
|
Baixar Versão 1
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleDownloadVersion('v2')}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full gap-2 justify-start"
|
||||||
|
disabled={!selectedOpinion.file_url_melhoria}
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
Baixar Versão Melhorada
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="h-[300px] rounded-md border p-4">
|
|
||||||
<pre className="whitespace-pre-wrap font-sans text-sm">
|
|
||||||
{generatedContent}
|
|
||||||
</pre>
|
|
||||||
</ScrollArea>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect
|
|||||||
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
|
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
|
||||||
const [isDeleteFolderOpen, setIsDeleteFolderOpen] = useState(false);
|
const [isDeleteFolderOpen, setIsDeleteFolderOpen] = useState(false);
|
||||||
const [isDeleteChatOpen, setIsDeleteChatOpen] = useState(false);
|
const [isDeleteChatOpen, setIsDeleteChatOpen] = useState(false);
|
||||||
|
const [isRenameFolderOpen, setIsRenameFolderOpen] = useState(false);
|
||||||
const [newFolderName, setNewFolderName] = useState("");
|
const [newFolderName, setNewFolderName] = useState("");
|
||||||
|
const [renamingFolder, setRenamingFolder] = useState<FolderRecord | null>(null);
|
||||||
|
const [renamedFolderName, setRenamedFolderName] = useState("");
|
||||||
const [deletingFolder, setDeletingFolder] = useState<FolderRecord | null>(null);
|
const [deletingFolder, setDeletingFolder] = useState<FolderRecord | null>(null);
|
||||||
const [deletingChat, setDeletingChat] = useState<ChatRecord | null>(null);
|
const [deletingChat, setDeletingChat] = useState<ChatRecord | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -154,6 +157,36 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect
|
|||||||
setIsDeleteFolderOpen(true);
|
setIsDeleteFolderOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openRenameFolder = (folder: FolderRecord) => {
|
||||||
|
setRenamingFolder(folder);
|
||||||
|
setRenamedFolderName(folder.name);
|
||||||
|
setIsRenameFolderOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenameFolder = async () => {
|
||||||
|
if (renamingFolder && renamedFolderName.trim()) {
|
||||||
|
try {
|
||||||
|
await chatService.renameFolder(renamingFolder.id, renamedFolderName);
|
||||||
|
await loadChatsAndFolders();
|
||||||
|
setRenamingFolder(null);
|
||||||
|
setRenamedFolderName("");
|
||||||
|
setIsRenameFolderOpen(false);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Pasta renomeada",
|
||||||
|
description: `Pasta renomeada para "${renamedFolderName}" com sucesso.`,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Erro ao renomear pasta:', error);
|
||||||
|
toast({
|
||||||
|
title: "Erro ao renomear pasta",
|
||||||
|
description: error.message || "Não foi possível renomear a pasta. Tente novamente.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleFolder = (folderId: string) => {
|
const toggleFolder = (folderId: string) => {
|
||||||
const newExpanded = new Set(expandedFolders);
|
const newExpanded = new Set(expandedFolders);
|
||||||
if (newExpanded.has(folderId)) {
|
if (newExpanded.has(folderId)) {
|
||||||
@@ -318,6 +351,39 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Rename Folder Dialog */}
|
||||||
|
<Dialog open={isRenameFolderOpen} onOpenChange={setIsRenameFolderOpen}>
|
||||||
|
<DialogContent className="glass-effect bg-card border-border z-50">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Renomear Pasta</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Digite o novo nome para a pasta "{renamingFolder?.name}".
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="rename-folder-name">Novo Nome</Label>
|
||||||
|
<Input
|
||||||
|
id="rename-folder-name"
|
||||||
|
value={renamedFolderName}
|
||||||
|
onChange={(e) => setRenamedFolderName(e.target.value)}
|
||||||
|
placeholder="Ex: Projetos, Estudos..."
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleRenameFolder()}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setIsRenameFolderOpen(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleRenameFolder} disabled={!renamedFolderName.trim()}>
|
||||||
|
Renomear
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
{/* Delete Folder Alert */}
|
{/* Delete Folder Alert */}
|
||||||
<AlertDialog open={isDeleteFolderOpen} onOpenChange={setIsDeleteFolderOpen}>
|
<AlertDialog open={isDeleteFolderOpen} onOpenChange={setIsDeleteFolderOpen}>
|
||||||
<AlertDialogContent className="glass-effect bg-card border-border z-50">
|
<AlertDialogContent className="glass-effect bg-card border-border z-50">
|
||||||
@@ -376,6 +442,13 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="glass-effect bg-popover border-border z-50">
|
<DropdownMenuContent align="end" className="glass-effect bg-popover border-border z-50">
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="gap-2"
|
||||||
|
onClick={() => openRenameFolder(folder)}
|
||||||
|
>
|
||||||
|
<Edit className="w-3 h-3" />
|
||||||
|
Renomear Pasta
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="gap-2 text-destructive"
|
className="gap-2 text-destructive"
|
||||||
onClick={() => openDeleteFolder(folder)}
|
onClick={() => openDeleteFolder(folder)}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const ImageView = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verifica se a geração foi bem-sucedida
|
// Verifica se a geração foi bem-sucedida
|
||||||
if (response.success) {
|
if (response.success && response.image_url && response.image_generation_id) {
|
||||||
toast({
|
toast({
|
||||||
title: "Imagem gerada com sucesso",
|
title: "Imagem gerada com sucesso",
|
||||||
description: `Tamanho: ${IMAGE_SIZE_OPTIONS[selectedSize].label}`,
|
description: `Tamanho: ${IMAGE_SIZE_OPTIONS[selectedSize].label}`,
|
||||||
@@ -124,14 +124,22 @@ export const ImageView = () => {
|
|||||||
loadImages(1, perPage);
|
loadImages(1, perPage);
|
||||||
setCurrentPage(1);
|
setCurrentPage(1);
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Erro ao gerar imagem');
|
// Se success for false ou faltarem dados, trata como erro
|
||||||
|
throw {
|
||||||
|
message: response.message || 'Erro ao gerar imagem',
|
||||||
|
code: response.code,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Erro na geração de imagem:', error);
|
console.error('Erro na geração de imagem:', error);
|
||||||
|
|
||||||
|
// Exibe a mensagem de erro exata retornada pela API
|
||||||
|
const errorMessage = error.message || "Não foi possível gerar a imagem. Tente novamente.";
|
||||||
|
const errorCode = error.code ? ` (${error.code})` : '';
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Erro na geração",
|
title: "Erro na geração",
|
||||||
description: error.message || "Não foi possível gerar a imagem. Tente novamente.",
|
description: errorMessage + errorCode,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -0,0 +1,330 @@
|
|||||||
|
import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions';
|
||||||
|
import { apiService } from './api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface para resposta do POST de parecer técnico
|
||||||
|
*/
|
||||||
|
export interface CreateOpinionResponse {
|
||||||
|
success: boolean;
|
||||||
|
id?: string;
|
||||||
|
file_url?: string;
|
||||||
|
file_url_melhoria?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface para requisição de criação de parecer
|
||||||
|
*/
|
||||||
|
export interface CreateOpinionRequest {
|
||||||
|
titulo: string;
|
||||||
|
categoria: string;
|
||||||
|
instrucoes: string;
|
||||||
|
userEmail?: string;
|
||||||
|
estabelecimentoId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface para um parecer retornado pela API
|
||||||
|
*/
|
||||||
|
export interface OpinionRecord {
|
||||||
|
id: string;
|
||||||
|
estabelecimento_id: number;
|
||||||
|
user_email: string;
|
||||||
|
titulo: string;
|
||||||
|
categoria: string;
|
||||||
|
instrucoes: string;
|
||||||
|
file_url: string;
|
||||||
|
created_at: string;
|
||||||
|
file_url_melhoria: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface para parâmetros de paginação e busca
|
||||||
|
*/
|
||||||
|
export interface GetOpinionsParams {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
userEmail?: string;
|
||||||
|
estabelecimentoId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface para resposta do GET de pareceres
|
||||||
|
*/
|
||||||
|
export interface GetOpinionsResponse {
|
||||||
|
data: OpinionRecord[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serviço para gerenciamento de pareceres jurídicos
|
||||||
|
*/
|
||||||
|
class AgentService {
|
||||||
|
private readonly CREATE_OPINION_ENDPOINT = '/webhook/codex/gepam/parecer-tecnico';
|
||||||
|
private readonly GET_OPINIONS_ENDPOINT = '/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/get_parecer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria um novo parecer técnico
|
||||||
|
*
|
||||||
|
* @param request - Dados do parecer
|
||||||
|
* @returns Promise com a resposta da API
|
||||||
|
*/
|
||||||
|
async createOpinion(request: CreateOpinionRequest): Promise<CreateOpinionResponse> {
|
||||||
|
const {
|
||||||
|
titulo,
|
||||||
|
categoria,
|
||||||
|
instrucoes,
|
||||||
|
userEmail,
|
||||||
|
estabelecimentoId
|
||||||
|
} = request;
|
||||||
|
|
||||||
|
// Usa valores do GlobalFunctions se não forem fornecidos
|
||||||
|
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
|
||||||
|
const estabId = estabelecimentoId || GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);
|
||||||
|
|
||||||
|
// Validações
|
||||||
|
if (!titulo || titulo.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Título do parecer é obrigatório',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instrucoes || instrucoes.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Instruções são obrigatórias',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Email do usuário não fornecido',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!estabId) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'ID do estabelecimento não fornecido',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log para debug
|
||||||
|
console.log('Criando parecer:', {
|
||||||
|
titulo,
|
||||||
|
categoria,
|
||||||
|
instrucoesLength: instrucoes.length,
|
||||||
|
userEmail: email,
|
||||||
|
estabelecimentoId: estabId,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Faz a requisição usando o serviço de API
|
||||||
|
const response = await apiService.post<CreateOpinionResponse>(
|
||||||
|
this.CREATE_OPINION_ENDPOINT,
|
||||||
|
{
|
||||||
|
user_email: email,
|
||||||
|
estabelecimento_id: estabId,
|
||||||
|
titulo: titulo.trim(),
|
||||||
|
categoria: categoria.trim(),
|
||||||
|
instrucoes: instrucoes.trim(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Resposta da API (criar parecer):', response.data);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
// Trata erros específicos
|
||||||
|
console.error('Erro ao criar parecer:', error);
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Erro ao criar parecer',
|
||||||
|
status: error.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Busca todos os pareceres do usuário com paginação e busca
|
||||||
|
*
|
||||||
|
* @param params - Parâmetros de paginação e busca
|
||||||
|
* @returns Promise com array de pareceres
|
||||||
|
*/
|
||||||
|
async getOpinions(params?: GetOpinionsParams): Promise<OpinionRecord[]> {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
per_page = 10,
|
||||||
|
search = '',
|
||||||
|
userEmail,
|
||||||
|
estabelecimentoId
|
||||||
|
} = params || {};
|
||||||
|
|
||||||
|
// Usa valores do GlobalFunctions se não forem fornecidos
|
||||||
|
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
|
||||||
|
const estabId = estabelecimentoId || GlobalFunctions.getTransferProperty(TransferAreaProperties.EstabelecimentoCodigo);
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Email do usuário não fornecido',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!estabId) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'ID do estabelecimento não fornecido',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Buscando pareceres:', {
|
||||||
|
userEmail: email,
|
||||||
|
estabelecimentoId: estabId,
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
search,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Constrói a URL com parâmetros de query
|
||||||
|
const url = `${this.GET_OPINIONS_ENDPOINT}/${email}/${estabId}`;
|
||||||
|
|
||||||
|
const response = await apiService.get<OpinionRecord[]>(url, {
|
||||||
|
params: {
|
||||||
|
page,
|
||||||
|
per_page,
|
||||||
|
search,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Resposta da API (buscar pareceres):', response.data);
|
||||||
|
|
||||||
|
// A API retorna diretamente o array de pareceres
|
||||||
|
// Garante que sempre retorna um array
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
return response.data;
|
||||||
|
} else if (response.data && typeof response.data === 'object') {
|
||||||
|
// Se a resposta for um objeto, tenta encontrar o array dentro dele
|
||||||
|
console.warn('API retornou objeto em vez de array:', response.data);
|
||||||
|
|
||||||
|
if (Array.isArray((response.data as any).data)) {
|
||||||
|
return (response.data as any).data;
|
||||||
|
} else if (Array.isArray((response.data as any).opinions)) {
|
||||||
|
return (response.data as any).opinions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se não conseguir extrair array, retorna vazio
|
||||||
|
console.warn('Não foi possível extrair array de pareceres da resposta');
|
||||||
|
return [];
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Erro ao buscar pareceres:', error);
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Erro ao buscar pareceres',
|
||||||
|
status: error.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Faz download de um arquivo de parecer
|
||||||
|
*
|
||||||
|
* @param fileUrl - URL do arquivo a ser baixado
|
||||||
|
* @param fileName - Nome do arquivo para download
|
||||||
|
*/
|
||||||
|
async downloadOpinion(fileUrl: string, fileName: string): Promise<void> {
|
||||||
|
if (!fileUrl) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'URL do arquivo não fornecida',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cria um elemento <a> temporário para forçar o download
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = fileUrl;
|
||||||
|
link.download = fileName;
|
||||||
|
link.target = '_blank';
|
||||||
|
link.rel = 'noopener noreferrer';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
|
||||||
|
console.log('Download iniciado:', { fileUrl, fileName });
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Erro ao fazer download:', error);
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Erro ao fazer download do arquivo',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclui um parecer técnico
|
||||||
|
*
|
||||||
|
* @param opinionId - ID do parecer a ser excluído
|
||||||
|
* @param userEmail - Email do usuário (opcional, usa GlobalFunctions se não fornecido)
|
||||||
|
* @returns Promise com a resposta da API
|
||||||
|
*/
|
||||||
|
async deleteOpinion(opinionId: string, userEmail?: string): Promise<{ success: boolean }> {
|
||||||
|
// Usa valores do GlobalFunctions se não forem fornecidos
|
||||||
|
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
|
||||||
|
|
||||||
|
if (!opinionId) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'ID do parecer não fornecido',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Email do usuário não fornecido',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Excluindo parecer:', {
|
||||||
|
opinionId,
|
||||||
|
userEmail: email,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Constrói a URL com o user_email e id
|
||||||
|
const url = `/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/delete_parecer/${email}/${opinionId}`;
|
||||||
|
|
||||||
|
const response = await apiService.delete<{ success: boolean }[]>(url);
|
||||||
|
|
||||||
|
console.log('Resposta da API (excluir parecer):', response.data);
|
||||||
|
|
||||||
|
// A API retorna um array com { success: true }
|
||||||
|
if (Array.isArray(response.data) && response.data.length > 0) {
|
||||||
|
return response.data[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Erro ao excluir parecer:', error);
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Erro ao excluir parecer',
|
||||||
|
status: error.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporta instância única (Singleton)
|
||||||
|
export const agentService = new AgentService();
|
||||||
@@ -603,6 +603,82 @@ class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renomeia uma pasta existente
|
||||||
|
*
|
||||||
|
* @param folderId - ID da pasta a ser renomeada
|
||||||
|
* @param newName - Novo nome da pasta
|
||||||
|
* @param userEmail - Email do usuário (opcional)
|
||||||
|
* @returns Promise com o resultado da operação
|
||||||
|
*/
|
||||||
|
async renameFolder(folderId: string, newName: string, userEmail?: string): Promise<{ success: boolean; folder?: FolderRecord }> {
|
||||||
|
const email = userEmail || GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Email do usuário não fornecido',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!folderId || folderId.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'ID da pasta não pode estar vazio',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newName || newName.trim().length === 0) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: 'Nome da pasta não pode estar vazio',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Renomeando pasta:', {
|
||||||
|
folderId,
|
||||||
|
newName,
|
||||||
|
userEmail: email,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiService.put<Array<{ success: boolean; user_email?: string; name?: string; id?: string }>>(
|
||||||
|
this.POST_FOLDER_ENDPOINT,
|
||||||
|
{
|
||||||
|
id: folderId,
|
||||||
|
user_email: email,
|
||||||
|
name: newName.trim(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Resposta completa do PUT (renomear pasta):', response);
|
||||||
|
console.log('response.data:', response.data);
|
||||||
|
|
||||||
|
// A API retorna um array com um objeto: [{"success":true, "user_email": "...", "name": "...", "id": "..."}]
|
||||||
|
let result: { success: boolean; user_email?: string; name?: string; id?: string };
|
||||||
|
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
result = response.data[0];
|
||||||
|
console.log('API retornou array, usando primeiro elemento:', result);
|
||||||
|
} else {
|
||||||
|
result = response.data as any;
|
||||||
|
console.log('API retornou objeto direto:', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: result.success ?? true,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Erro ao renomear pasta:', error);
|
||||||
|
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: error.message || 'Erro ao renomear pasta',
|
||||||
|
status: error.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Busca todos os chats e pastas do usuário
|
* Busca todos os chats e pastas do usuário
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ export type ImageSize = '1024x1024' | '1024x1792' | '1792x1024';
|
|||||||
*/
|
*/
|
||||||
export interface ImageGenerationResponse {
|
export interface ImageGenerationResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
image_url: string; // URL da imagem gerada
|
image_url?: string; // URL da imagem gerada (opcional quando há erro)
|
||||||
image_generation_id: string;
|
image_generation_id?: string; // ID da geração (opcional quando há erro)
|
||||||
message: string; // Descrição original
|
message: string; // Descrição original ou mensagem de erro
|
||||||
|
code?: string; // Código de erro (ex: "server_error", "invalid_request")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,14 +138,39 @@ class ImageGenerationService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Verifica se a resposta indica erro
|
||||||
|
if (!response.data.success) {
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: response.data.message || 'Erro ao gerar imagem',
|
||||||
|
code: response.data.code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Trata erros específicos
|
// Trata erros específicos da API
|
||||||
console.error('Erro na geração de imagem:', error);
|
console.error('Erro na geração de imagem:', error);
|
||||||
|
|
||||||
|
// Se o erro já tem a estrutura esperada (veio da validação acima), repassa
|
||||||
|
if (error.success === false && error.message) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se o erro veio da requisição HTTP, tenta extrair a resposta da API
|
||||||
|
if (error.response?.data) {
|
||||||
|
const apiError = error.response.data;
|
||||||
|
throw {
|
||||||
|
success: false,
|
||||||
|
message: apiError.message || 'Erro ao gerar imagem',
|
||||||
|
code: apiError.code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erro genérico
|
||||||
throw {
|
throw {
|
||||||
success: false,
|
success: false,
|
||||||
message: error.message || 'Erro ao gerar imagem',
|
message: error.message || 'Erro ao gerar imagem. Tente novamente.',
|
||||||
status: error.status,
|
status: error.status,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user