diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 0458234..d86a4aa 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { MessageSquare, Image, AudioLines, Mic, ArrowLeft, Plus, ChevronLeft, ChevronRight, Bot, Brain, FileText, Layers } from "lucide-react"; +import { MessageSquare, Image, AudioLines, Mic, ArrowLeft, Plus, ChevronLeft, ChevronRight, Bot, FileText, Layers } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import { ThemeToggle } from "@/components/ThemeToggle"; @@ -7,11 +7,11 @@ import { GlobalFunctions } from "@/GlobalFunctions"; interface LayoutProps { children: React.ReactNode; - activeTab: "chat" | "images" | "transcription" | "generation" | "bots" | "agent" | "prompts" | "parecerJuridico" | "areas"; - onTabChange: (tab: "chat" | "images" | "transcription" | "generation" | "bots" | "agent" | "prompts" | "parecerJuridico" | "areas") => void; + activeTab: "chat" | "images" | "transcription" | "generation" | "bots" | "prompts" | "parecerJuridico" | "areas"; + onTabChange: (tab: "chat" | "images" | "transcription" | "generation" | "bots" | "prompts" | "parecerJuridico" | "areas") => void; } -type TabType = "chat" | "images" | "transcription" | "generation" | "bots" | "agent" | "prompts" | "parecerJuridico" | "areas"; +type TabType = "chat" | "images" | "transcription" | "generation" | "bots" | "prompts" | "parecerJuridico" | "areas"; export const Layout = ({ children, activeTab, onTabChange }: LayoutProps) => { const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); @@ -22,7 +22,6 @@ export const Layout = ({ children, activeTab, onTabChange }: LayoutProps) => { { id: "transcription" as TabType, label: "Transcrição de Áudio", icon: AudioLines }, { id: "generation" as TabType, label: "Geração de Áudio", icon: Mic }, //{ id: "bots" as TabType, label: "Bots", icon: Bot }, - { id: "agent" as TabType, label: "Agente de Parecer", icon: Brain }, { id: "prompts" as TabType, label: "Prompts", icon: FileText }, { id: "parecerJuridico" as TabType, label: "Parecer Jurídico", icon: Bot }, { id: "areas" as TabType, label: "Áreas", icon: Layers }, diff --git a/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx b/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx index 8474070..c5cd0c6 100644 --- a/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx +++ b/src/components/parecer-juridico/ParecerJuridicoDetailView.tsx @@ -10,21 +10,26 @@ import { Info, Loader2, MessageSquare, + Pencil, Send, Trash2, } from "lucide-react"; import ReactMarkdown from "react-markdown"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Textarea } from "@/components/ui/textarea"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, + DialogFooter, } from "@/components/ui/dialog"; import { AlertDialog, @@ -37,7 +42,7 @@ import { } from "@/components/ui/alert-dialog"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { usePrompts } from "@/contexts/PromptsContext"; -import { parecerService, type ParecerChatHistoricoItem, type ParecerDetalhe } from "@/services/parecerApi"; +import { parecerService, type ParecerChatHistoricoItem, type ParecerDetalhe, type ParecerChatModo } from "@/services/parecerApi"; import { ParecerJuridicoGeneratingScreen } from "@/components/parecer-juridico/ParecerJuridicoGeneratingScreen"; import { playSuccessSound } from "@/utils/sound"; import { toast } from "sonner"; @@ -62,6 +67,7 @@ interface ChatMessage { content: string; created_at?: string; user_email?: string; + tipo_resposta?: "chat" | "agente" | null; } export function ParecerJuridicoDetailView() { @@ -79,9 +85,16 @@ export function ParecerJuridicoDetailView() { const [deleting, setDeleting] = useState(false); const { areaItems } = usePrompts(); + const [editOpen, setEditOpen] = useState(false); + const [editTitulo, setEditTitulo] = useState(""); + const [editAreaId, setEditAreaId] = useState(""); + const [editSaving, setEditSaving] = useState(false); + const [messages, setMessages] = useState([]); const [chatLoading, setChatLoading] = useState(true); const [chatProcessing, setChatProcessing] = useState(false); + const [chatInlineLoading, setChatInlineLoading] = useState(false); + const [modo, setModo] = useState("agente"); const [conteudoAtualizadoChat, setConteudoAtualizadoChat] = useState(null); const [historicoOpen, setHistoricoOpen] = useState(false); const [historicoLoading, setHistoricoLoading] = useState(false); @@ -141,6 +154,7 @@ export function ParecerJuridicoDetailView() { content: m.content, created_at: m.created_at, user_email: m.user_email, + tipo_resposta: m.tipo_resposta ?? null, })) ); } catch { @@ -241,6 +255,41 @@ export function ParecerJuridicoDetailView() { } }; + const abrirModalEdicao = () => { + if (!parecer) return; + setEditTitulo(parecer.titulo ?? ""); + setEditAreaId(parecer.area_id ?? ""); + setEditOpen(true); + }; + + const handleSalvarEdicao = async () => { + if (!id?.trim()) return; + const tituloTrim = editTitulo.trim(); + const areaTrim = editAreaId.trim(); + + if (!tituloTrim || !areaTrim) { + toast.error("Título e área são obrigatórios."); + return; + } + + setEditSaving(true); + try { + await parecerService.editarTituloArea(id, { titulo: tituloTrim, area_id: areaTrim }); + const updated = await parecerService.buscarPorId(id); + setParecer(updated); + setConteudoAtualizadoChat(null); + setEditOpen(false); + playSuccessSound(); + toast.success("Parecer atualizado."); + } catch (e: unknown) { + const message = + e && typeof e === "object" && "message" in e ? String((e as { message: string }).message) : "Erro ao atualizar parecer."; + toast.error(message); + } finally { + setEditSaving(false); + } + }; + const handleExcluirParecer = () => { if (!id?.trim()) return; setDeleting(true); @@ -283,25 +332,51 @@ export function ParecerJuridicoDetailView() { const handleSendMessage = () => { const text = chatInput.trim(); if (!text || !id?.trim() || chatProcessing) return; + const isChatMode = modo === "chat"; const tinhaParecer = parecer != null; setChatInput(""); + if (isChatMode) { + setChatInlineLoading(true); + // Garante que o usuário veja imediatamente o loading no final do chat. + requestAnimationFrame(() => { + chatEndRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); + }); + } setChatProcessing(true); parecerService - .enviarMensagemChat(id, text) + .enviarMensagemChat(id, text, modo) .then(async (res) => { - if (tinhaParecer) { - setParecer((p) => (p ? { ...p, conteudo_gerado: res.conteudo_gerado } : p)); - } else { - setConteudoAtualizadoChat(res.conteudo_gerado); + if (!isChatMode && "conteudo_gerado" in res) { + if (tinhaParecer) { + setParecer((p) => (p ? { ...p, conteudo_gerado: res.conteudo_gerado } : p)); + } else { + setConteudoAtualizadoChat(res.conteudo_gerado); + } } await carregarChat(true); + + if (!isChatMode) { + // Garante que o modal de detalhes reflita também metadados atualizados + // (ex.: "Atualizado em"), não só o conteúdo retornado no chat. + try { + const updatedParecer = await parecerService.buscarPorId(id); + setParecer(updatedParecer); + setConteudoAtualizadoChat(null); + } catch { + // Não bloqueia o fluxo do chat se a recarga falhar. + } + } + playSuccessSound(); - toast.success("Parecer atualizado conforme sua solicitação."); + toast.success(isChatMode ? "Resposta enviada ao chat." : "Parecer atualizado conforme sua solicitação."); }) .catch((e) => { toast.error(e?.message ?? "Erro ao enviar mensagem."); }) - .finally(() => setChatProcessing(false)); + .finally(() => { + setChatProcessing(false); + setChatInlineLoading(false); + }); }; if (!id?.trim()) { @@ -350,16 +425,27 @@ export function ParecerJuridicoDetailView() { return ( <> - {chatProcessing && ( + {chatProcessing && modo === "agente" && ( )}
-
+
-

Parecer: {titulo}

+

{titulo}

+ {loading && hasPreview && ( )} @@ -465,6 +551,67 @@ export function ParecerJuridicoDetailView() { + + + + Editar título e área + Atualize os campos obrigatórios do parecer. + + +
+
+ + setEditTitulo(e.target.value)} + placeholder="Título do parecer" + required + aria-required="true" + /> +
+ +
+ + +
+
+ + + + + +
+
+ !deleting && setDeleteOpen(open)}> @@ -605,13 +752,19 @@ export function ParecerJuridicoDetailView() { return (
- {msg.content} + {msg.role === "assistant" ? ( +
+ {msg.content} +
+ ) : ( + msg.content + )}
- {msg.role === "assistant" && ( + {msg.role === "assistant" && msg.tipo_resposta && ( + + {msg.tipo_resposta} + + )} + {msg.role === "assistant" && msg.tipo_resposta === "agente" && (
); })} + {!chatLoading && modo === "chat" && chatInlineLoading && ( +
+ + Gerando resposta no chat... +
+ )}
+
+
+ +

+ {modo === "chat" ? "Apenas responde no chat" : "Agente (atualiza o parecer)"} +

+
+
+ Chat + setModo(checked ? "agente" : "chat")} + disabled={chatProcessing} + aria-label="Alternar modo de resposta" + /> + Agente +
+
+
= { "/codex/bate-papo": "chat", "/codex/imagens": "images", "/codex/transcricao-audio": "transcription", "/codex/geracao-audio": "generation", - "/codex/parecer": "agent", "/codex/parecer-juridico": "parecerJuridico", "/codex/areas": "areas", }; @@ -40,7 +38,6 @@ const TAB_TO_PATH: Record = { images: "/codex/imagens", transcription: "/codex/transcricao-audio", generation: "/codex/geracao-audio", - agent: "/codex/parecer", prompts: "/codex/prompts", parecerJuridico: "/codex/parecer-juridico", areas: "/codex/areas", @@ -76,7 +73,6 @@ const Index = () => { } /> } /> } /> - } /> } /> } /> } /> diff --git a/src/services/parecerApi.ts b/src/services/parecerApi.ts index 1d903a6..12fc116 100644 --- a/src/services/parecerApi.ts +++ b/src/services/parecerApi.ts @@ -56,8 +56,15 @@ export interface ParecerChatMessage { role: "user" | "assistant"; content: string; created_at: string; + tipo_resposta?: "chat" | "agente" | null; } +export type ParecerChatModo = "chat" | "agente"; + +export type EnviarMensagemChatResposta = + | { message: string } + | { conteudo_gerado: string }; + export interface ParecerDetalhe { id: string; estabelecimento_id: number; @@ -77,6 +84,23 @@ export interface ParecerDetalhe { atualizado_por: string | null; } +export interface EditarParecerTituloAreaBody { + titulo: string; + area_id: string; +} + +export interface EditarParecerTituloAreaSuccessResponse { + success: true; + id: string; + titulo: string; + area_id: string; +} + +export interface EditarParecerTituloAreaErrorResponse { + success: false; + message?: string; +} + export type ParecerStatus = "processando" | "concluido" | "erro"; export interface ParecerListItem { @@ -216,7 +240,11 @@ class ParecerService { return item as ParecerDetalhe; } - async enviarMensagemChat(parecerId: string, message: string): Promise<{ conteudo_gerado: string }> { + async enviarMensagemChat( + parecerId: string, + messageText: string, + modo: ParecerChatModo = "agente" + ): Promise { const userEmail = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail); if (!userEmail) { throw new Error("Usuário não identificado (user_email)"); @@ -224,20 +252,33 @@ class ParecerService { if (!parecerId?.trim()) { throw new Error("ID do parecer é obrigatório"); } - if (!message?.trim()) { + if (!messageText?.trim()) { throw new Error("Digite uma mensagem"); } type ChatPostPayload = { success: boolean; conteudo_gerado?: string; message?: string }; const response = await apiService.post( PARECER_CHAT_ENVIAR_URL(parecerId), - { user_email: String(userEmail), message: message.trim() }, + { user_email: String(userEmail), message: messageText.trim(), modo }, { timeout: 0, headers: { "Content-Type": "application/json" } } ); const raw = response.data; const payload = Array.isArray(raw) ? raw[0] : raw; - if (!payload?.success || typeof payload.conteudo_gerado !== "string") { + + if (!payload?.success) { throw new Error(payload?.message ?? "Erro ao processar a mensagem do chat"); } + + if (modo === "chat") { + if (typeof payload.message !== "string") { + throw new Error(payload?.message ?? "Erro ao processar resposta do chat"); + } + return { message: payload.message }; + } + + // modo === "agente" + if (typeof payload.conteudo_gerado !== "string") { + throw new Error(payload?.message ?? "Erro ao processar atualização do parecer"); + } return { conteudo_gerado: payload.conteudo_gerado }; } @@ -272,6 +313,47 @@ class ParecerService { return item; } + async editarTituloArea( + parecerId: string, + body: EditarParecerTituloAreaBody + ): Promise { + if (!parecerId?.trim()) { + throw new Error("ID do parecer é obrigatório"); + } + if (!body?.titulo?.trim()) { + throw new Error("Título é obrigatório"); + } + if (!body?.area_id?.trim()) { + throw new Error("Área é obrigatória"); + } + + const url = `https://prod-hgtx-intelligence-n8n.hgtx.com.br/webhook/fd073ec0-81ab-4681-8acd-b0da71f6ad30/codex/parecer/${encodeURIComponent( + parecerId + )}/editar`; + + const response = await apiService.put< + | EditarParecerTituloAreaSuccessResponse + | EditarParecerTituloAreaErrorResponse + | (EditarParecerTituloAreaSuccessResponse | EditarParecerTituloAreaErrorResponse)[] + >(url, { + titulo: body.titulo.trim(), + area_id: body.area_id.trim(), + }); + + const raw = response.data; + const data = Array.isArray(raw) ? raw[0] : raw; + + if (!data || typeof data !== "object" || !("success" in data)) { + throw new Error("Resposta inválida ao editar o parecer"); + } + + if (data.success === false) { + throw new Error((data as EditarParecerTituloAreaErrorResponse).message ?? "Erro ao editar parecer"); + } + + return data as EditarParecerTituloAreaSuccessResponse; + } + async excluir(id: string): Promise<{ message: string }> { if (!id?.trim()) { throw new Error("ID do parecer é obrigatório");