From 45ea53b364c2a818f09601d2c63d5a22d0d0a6ae Mon Sep 17 00:00:00 2001 From: luisfepsale Date: Tue, 28 Oct 2025 09:56:35 -0300 Subject: [PATCH] [Parecer Juridico] --- src/components/agent/AgentSearch.tsx | 135 +++++----- src/components/agent/AgentView.tsx | 253 +++++++++++++++---- src/components/agent/OpinionDialog.tsx | 212 ++++++++-------- src/components/chat/ChatSidebar.tsx | 73 ++++++ src/components/images/ImageView.tsx | 14 +- src/services/agent.ts | 330 +++++++++++++++++++++++++ src/services/chat.ts | 76 ++++++ src/services/imageGeneration.ts | 36 ++- 8 files changed, 904 insertions(+), 225 deletions(-) create mode 100644 src/services/agent.ts diff --git a/src/components/agent/AgentSearch.tsx b/src/components/agent/AgentSearch.tsx index b9ad228..abde091 100644 --- a/src/components/agent/AgentSearch.tsx +++ b/src/components/agent/AgentSearch.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Search, FileText } from "lucide-react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -12,77 +12,70 @@ import { PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; -import { LegalOpinion } from "./AgentView"; +import { agentService, OpinionRecord } from "@/services/agent"; +import { useToast } from "@/hooks/use-toast"; 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) => { const [searchTerm, setSearchTerm] = useState(""); - const [searchResults, setSearchResults] = useState([]); + const [searchResults, setSearchResults] = useState([]); 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()) { setSearchResults([]); return; } - const results = mockOpinionsDatabase.filter( - (opinion) => - opinion.title.toLowerCase().includes(searchTerm.toLowerCase()) || - opinion.content.toLowerCase().includes(searchTerm.toLowerCase()) || - opinion.category?.toLowerCase().includes(searchTerm.toLowerCase()) - ); + const timer = setTimeout(() => { + if (currentPage === 1) { + handleSearch(); + } else { + setCurrentPage(1); // Volta para primeira página ao buscar + } + }, 500); - setSearchResults(results); - setCurrentPage(1); - }; + return () => clearTimeout(timer); + }, [searchTerm]); const totalPages = Math.ceil(searchResults.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const paginatedResults = searchResults.slice(startIndex, startIndex + itemsPerPage); return (
@@ -92,24 +85,30 @@ export const AgentSearch = ({ onSelectOpinion }: AgentSearchProps) => { setSearchTerm(e.target.value)} - placeholder="Digite título, frase ou categoria..." + placeholder="Digite título ou categoria..." onKeyDown={(e) => e.key === "Enter" && handleSearch()} /> -
-

- Base com {mockOpinionsDatabase.length} pareceres disponíveis -

+ {searchResults.length > 0 && ( +

+ {searchResults.length} {searchResults.length === 1 ? 'parecer encontrado' : 'pareceres encontrados'} +

+ )}
- {searchResults.length > 0 ? ( + {isLoading ? ( +
+

Buscando pareceres...

+
+ ) : searchResults.length > 0 ? (
- {paginatedResults.map((opinion) => ( + {searchResults.map((opinion) => (
{
-

{opinion.title}

+

{opinion.titulo}

- {opinion.category} - {new Date(opinion.createdAt).toLocaleDateString('pt-BR')} + {opinion.categoria || '-'} + {new Date(opinion.created_at).toLocaleDateString('pt-BR')}

- {opinion.content} + {opinion.instrucoes}

diff --git a/src/components/agent/AgentView.tsx b/src/components/agent/AgentView.tsx index c947db7..7634aa8 100644 --- a/src/components/agent/AgentView.tsx +++ b/src/components/agent/AgentView.tsx @@ -1,5 +1,5 @@ -import { useState } from "react"; -import { Plus, Search, Eye, Trash2, ArrowUpDown } from "lucide-react"; +import { useState, useEffect } from "react"; +import { Plus, Search, Eye, Trash2, ArrowUpDown, Download } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -17,30 +17,84 @@ import { SelectTrigger, SelectValue, } 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 { AgentSearch } from "./AgentSearch"; +import { agentService, OpinionRecord } from "@/services/agent"; +import { useToast } from "@/hooks/use-toast"; -export interface LegalOpinion { - id: string; - title: string; - content: string; - createdAt: Date; - category?: string; -} - -type SortField = "title" | "createdAt" | "category"; +type SortField = "titulo" | "created_at" | "categoria"; type SortOrder = "asc" | "desc"; export const AgentView = () => { - const [opinions, setOpinions] = useState([]); + const [opinions, setOpinions] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(10); - const [sortField, setSortField] = useState("createdAt"); + const [sortField, setSortField] = useState("created_at"); const [sortOrder, setSortOrder] = useState("desc"); const [isDialogOpen, setIsDialogOpen] = useState(false); - const [selectedOpinion, setSelectedOpinion] = useState(null); + const [selectedOpinion, setSelectedOpinion] = useState(null); const [showSearch, setShowSearch] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [opinionToDelete, setOpinionToDelete] = useState(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) => { if (sortField === field) { @@ -51,39 +105,26 @@ export const AgentView = () => { } }; - const filteredOpinions = opinions.filter( - (op) => - op.title.toLowerCase().includes(searchTerm.toLowerCase()) || - op.category?.toLowerCase().includes(searchTerm.toLowerCase()) - ); - - const sortedOpinions = [...filteredOpinions].sort((a, b) => { + const sortedOpinions = [...opinions].sort((a, b) => { const multiplier = sortOrder === "asc" ? 1 : -1; - - if (sortField === "createdAt") { - return multiplier * (new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); + + if (sortField === "created_at") { + return multiplier * (new Date(a.created_at).getTime() - new Date(b.created_at).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) => { - setOpinions([newOpinion, ...opinions]); - setIsDialogOpen(false); - setSelectedOpinion(null); + const handleOpinionCreated = () => { + // Recarrega a lista após criar um novo parecer + loadOpinions(); }; - const handleDeleteOpinion = (id: string) => { - setOpinions(opinions.filter(op => op.id !== id)); - }; - - const handleViewOpinion = (opinion: LegalOpinion) => { + const handleViewOpinion = (opinion: OpinionRecord) => { setSelectedOpinion(opinion); setIsDialogOpen(true); }; @@ -93,12 +134,69 @@ export const AgentView = () => { setIsDialogOpen(true); }; - const handleSelectFromSearch = (opinion: LegalOpinion) => { + const handleSelectFromSearch = (opinion: OpinionRecord) => { setSelectedOpinion(opinion); setShowSearch(false); 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) { return (
@@ -116,7 +214,7 @@ export const AgentView = () => {
-

Agente de Parecer Jurídico

+

Gerador de Modelos de Parecer Jurídico

+ + + + + + handleDownloadVersion(opinion, 'v1')} + disabled={!opinion.file_url} + > + + Versão 1 + + handleDownloadVersion(opinion, 'v2')} + disabled={!opinion.file_url_melhoria} + > + + Versão Melhorada + + + @@ -245,10 +377,10 @@ export const AgentView = () => {
- {totalPages > 1 && ( + {sortedOpinions.length > 0 && (

- {startIndex + 1}-{Math.min(startIndex + itemsPerPage, sortedOpinions.length)} de {sortedOpinions.length} + Mostrando {sortedOpinions.length} {sortedOpinions.length === 1 ? 'parecer' : 'pareceres'}

); }; diff --git a/src/components/agent/OpinionDialog.tsx b/src/components/agent/OpinionDialog.tsx index 3a3a2ca..9c13608 100644 --- a/src/components/agent/OpinionDialog.tsx +++ b/src/components/agent/OpinionDialog.tsx @@ -1,5 +1,4 @@ import { useState, useEffect } from "react"; -import axios from "axios"; import { Download, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; import { @@ -14,13 +13,13 @@ 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"; +import { agentService, OpinionRecord } from "@/services/agent"; interface OpinionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; - selectedOpinion: LegalOpinion | null; - onOpinionCreated: (opinion: LegalOpinion) => void; + selectedOpinion: OpinionRecord | null; + onOpinionCreated: () => void; } export const OpinionDialog = ({ @@ -32,23 +31,23 @@ export const OpinionDialog = ({ const [title, setTitle] = useState(""); const [category, setCategory] = useState(""); const [instructions, setInstructions] = useState(""); - const [generatedContent, setGeneratedContent] = useState(""); const [isGenerating, setIsGenerating] = useState(false); + const [createdOpinion, setCreatedOpinion] = useState(null); const { toast } = useToast(); useEffect(() => { if (selectedOpinion) { - setTitle(selectedOpinion.title); - setCategory(selectedOpinion.category || ""); - setGeneratedContent(selectedOpinion.content); - setInstructions(""); + setTitle(selectedOpinion.titulo); + setCategory(selectedOpinion.categoria || ""); + setInstructions(selectedOpinion.instrucoes || ""); + setCreatedOpinion(selectedOpinion); } else { setTitle(""); setCategory(""); setInstructions(""); - setGeneratedContent(""); + setCreatedOpinion(null); } - }, [selectedOpinion]); + }, [selectedOpinion, open]); const handleGenerate = async () => { if (!instructions.trim()) { @@ -72,53 +71,39 @@ export const OpinionDialog = ({ setIsGenerating(true); try { - const response = await axios.post( - "https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/codex/gepam/parecer-tecnico", - { - titulo: title, - 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.", + const response = await agentService.createOpinion({ + titulo: title, + categoria: category, + instrucoes: instructions, }); - } 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); setIsGenerating(false); - - let errorMessage = "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."; - } - } - + + const errorMessage = error.message || "Não foi possível gerar o parecer. Tente novamente."; + toast({ title: "Erro ao gerar parecer", description: errorMessage, @@ -127,22 +112,45 @@ export const OpinionDialog = ({ } }; - 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); + const handleDownloadVersion = async (version: 'v1' | 'v2') => { + if (!createdOpinion && !selectedOpinion) { + toast({ + title: "Nenhum parecer disponível", + description: "Por favor, gere um parecer primeiro.", + variant: "destructive", + }); + return; + } - toast({ - title: "Download iniciado", - description: "O parecer está sendo baixado.", - }); + const opinion = createdOpinion || selectedOpinion; + if (!opinion) return; + + 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 ( @@ -150,11 +158,11 @@ export const OpinionDialog = ({ - {selectedOpinion ? "Gerar Novo Modelo do Parecer" : "Novo Parecer Jurídico"} + {selectedOpinion ? "Visualizar Parecer" : "Novo Parecer Jurídico"} {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"} @@ -196,40 +204,46 @@ export const OpinionDialog = ({ />
- + {!selectedOpinion && ( + + )} - {generatedContent && ( -
-
- + {selectedOpinion && ( +
+ +
+
- -
-                    {generatedContent}
-                  
-
)}
diff --git a/src/components/chat/ChatSidebar.tsx b/src/components/chat/ChatSidebar.tsx index 198746f..781822e 100644 --- a/src/components/chat/ChatSidebar.tsx +++ b/src/components/chat/ChatSidebar.tsx @@ -48,7 +48,10 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false); const [isDeleteFolderOpen, setIsDeleteFolderOpen] = useState(false); const [isDeleteChatOpen, setIsDeleteChatOpen] = useState(false); + const [isRenameFolderOpen, setIsRenameFolderOpen] = useState(false); const [newFolderName, setNewFolderName] = useState(""); + const [renamingFolder, setRenamingFolder] = useState(null); + const [renamedFolderName, setRenamedFolderName] = useState(""); const [deletingFolder, setDeletingFolder] = useState(null); const [deletingChat, setDeletingChat] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -154,6 +157,36 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect 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 newExpanded = new Set(expandedFolders); if (newExpanded.has(folderId)) { @@ -318,6 +351,39 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect + {/* Rename Folder Dialog */} + + + + Renomear Pasta + + Digite o novo nome para a pasta "{renamingFolder?.name}". + + +
+
+ + setRenamedFolderName(e.target.value)} + placeholder="Ex: Projetos, Estudos..." + onKeyDown={(e) => e.key === "Enter" && handleRenameFolder()} + autoFocus + /> +
+
+ + + + +
+
+ {/* Delete Folder Alert */} @@ -376,6 +442,13 @@ export const ChatSidebar = ({ isCollapsed, onToggleCollapse, onNewChat, onSelect + openRenameFolder(folder)} + > + + Renomear Pasta + openDeleteFolder(folder)} diff --git a/src/components/images/ImageView.tsx b/src/components/images/ImageView.tsx index f0a64e6..830eb38 100644 --- a/src/components/images/ImageView.tsx +++ b/src/components/images/ImageView.tsx @@ -92,7 +92,7 @@ export const ImageView = () => { }); // Verifica se a geração foi bem-sucedida - if (response.success) { + if (response.success && response.image_url && response.image_generation_id) { toast({ title: "Imagem gerada com sucesso", description: `Tamanho: ${IMAGE_SIZE_OPTIONS[selectedSize].label}`, @@ -124,14 +124,22 @@ export const ImageView = () => { loadImages(1, perPage); setCurrentPage(1); } 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) { 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({ title: "Erro na geração", - description: error.message || "Não foi possível gerar a imagem. Tente novamente.", + description: errorMessage + errorCode, variant: "destructive", }); } finally { diff --git a/src/services/agent.ts b/src/services/agent.ts new file mode 100644 index 0000000..45759e4 --- /dev/null +++ b/src/services/agent.ts @@ -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 { + 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( + 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 { + 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(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 { + if (!fileUrl) { + throw { + success: false, + message: 'URL do arquivo não fornecida', + }; + } + + try { + // Cria um elemento 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(); diff --git a/src/services/chat.ts b/src/services/chat.ts index 5d91e81..7e62eed 100644 --- a/src/services/chat.ts +++ b/src/services/chat.ts @@ -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>( + 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 * diff --git a/src/services/imageGeneration.ts b/src/services/imageGeneration.ts index 87af73c..eac2650 100644 --- a/src/services/imageGeneration.ts +++ b/src/services/imageGeneration.ts @@ -11,9 +11,10 @@ export type ImageSize = '1024x1024' | '1024x1792' | '1792x1024'; */ export interface ImageGenerationResponse { success: boolean; - image_url: string; // URL da imagem gerada - image_generation_id: string; - message: string; // Descrição original + image_url?: string; // URL da imagem gerada (opcional quando há erro) + image_generation_id?: string; // ID da geração (opcional quando há erro) + 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; } catch (error: any) { - // Trata erros específicos + // Trata erros específicos da API 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 { success: false, - message: error.message || 'Erro ao gerar imagem', + message: error.message || 'Erro ao gerar imagem. Tente novamente.', status: error.status, }; }