nova implementacao modo chat e agente no Parecer

This commit is contained in:
Vitex Tecnologia
2026-03-20 13:12:17 -03:00
parent 6537e2dd53
commit 8d8818c32e
4 changed files with 296 additions and 29 deletions
+4 -5
View File
@@ -1,5 +1,5 @@
import { useState } from "react"; 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 { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { ThemeToggle } from "@/components/ThemeToggle"; import { ThemeToggle } from "@/components/ThemeToggle";
@@ -7,11 +7,11 @@ import { GlobalFunctions } from "@/GlobalFunctions";
interface LayoutProps { interface LayoutProps {
children: React.ReactNode; children: React.ReactNode;
activeTab: "chat" | "images" | "transcription" | "generation" | "bots" | "agent" | "prompts" | "parecerJuridico" | "areas"; activeTab: "chat" | "images" | "transcription" | "generation" | "bots" | "prompts" | "parecerJuridico" | "areas";
onTabChange: (tab: "chat" | "images" | "transcription" | "generation" | "bots" | "agent" | "prompts" | "parecerJuridico" | "areas") => void; 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) => { export const Layout = ({ children, activeTab, onTabChange }: LayoutProps) => {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); 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: "transcription" as TabType, label: "Transcrição de Áudio", icon: AudioLines },
{ id: "generation" as TabType, label: "Geração de Áudio", icon: Mic }, { id: "generation" as TabType, label: "Geração de Áudio", icon: Mic },
//{ id: "bots" as TabType, label: "Bots", icon: Bot }, //{ 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: "prompts" as TabType, label: "Prompts", icon: FileText },
{ id: "parecerJuridico" as TabType, label: "Parecer Jurídico", icon: Bot }, { id: "parecerJuridico" as TabType, label: "Parecer Jurídico", icon: Bot },
{ id: "areas" as TabType, label: "Áreas", icon: Layers }, { id: "areas" as TabType, label: "Áreas", icon: Layers },
@@ -10,21 +10,26 @@ import {
Info, Info,
Loader2, Loader2,
MessageSquare, MessageSquare,
Pencil,
Send, Send,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { Button } from "@/components/ui/button"; 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 { ScrollArea } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogFooter,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { import {
AlertDialog, AlertDialog,
@@ -37,7 +42,7 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { usePrompts } from "@/contexts/PromptsContext"; 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 { ParecerJuridicoGeneratingScreen } from "@/components/parecer-juridico/ParecerJuridicoGeneratingScreen";
import { playSuccessSound } from "@/utils/sound"; import { playSuccessSound } from "@/utils/sound";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -62,6 +67,7 @@ interface ChatMessage {
content: string; content: string;
created_at?: string; created_at?: string;
user_email?: string; user_email?: string;
tipo_resposta?: "chat" | "agente" | null;
} }
export function ParecerJuridicoDetailView() { export function ParecerJuridicoDetailView() {
@@ -79,9 +85,16 @@ export function ParecerJuridicoDetailView() {
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const { areaItems } = usePrompts(); 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<ChatMessage[]>([]); const [messages, setMessages] = useState<ChatMessage[]>([]);
const [chatLoading, setChatLoading] = useState(true); const [chatLoading, setChatLoading] = useState(true);
const [chatProcessing, setChatProcessing] = useState(false); const [chatProcessing, setChatProcessing] = useState(false);
const [chatInlineLoading, setChatInlineLoading] = useState(false);
const [modo, setModo] = useState<ParecerChatModo>("agente");
const [conteudoAtualizadoChat, setConteudoAtualizadoChat] = useState<string | null>(null); const [conteudoAtualizadoChat, setConteudoAtualizadoChat] = useState<string | null>(null);
const [historicoOpen, setHistoricoOpen] = useState(false); const [historicoOpen, setHistoricoOpen] = useState(false);
const [historicoLoading, setHistoricoLoading] = useState(false); const [historicoLoading, setHistoricoLoading] = useState(false);
@@ -141,6 +154,7 @@ export function ParecerJuridicoDetailView() {
content: m.content, content: m.content,
created_at: m.created_at, created_at: m.created_at,
user_email: m.user_email, user_email: m.user_email,
tipo_resposta: m.tipo_resposta ?? null,
})) }))
); );
} catch { } 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 = () => { const handleExcluirParecer = () => {
if (!id?.trim()) return; if (!id?.trim()) return;
setDeleting(true); setDeleting(true);
@@ -283,25 +332,51 @@ export function ParecerJuridicoDetailView() {
const handleSendMessage = () => { const handleSendMessage = () => {
const text = chatInput.trim(); const text = chatInput.trim();
if (!text || !id?.trim() || chatProcessing) return; if (!text || !id?.trim() || chatProcessing) return;
const isChatMode = modo === "chat";
const tinhaParecer = parecer != null; const tinhaParecer = parecer != null;
setChatInput(""); 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); setChatProcessing(true);
parecerService parecerService
.enviarMensagemChat(id, text) .enviarMensagemChat(id, text, modo)
.then(async (res) => { .then(async (res) => {
if (tinhaParecer) { if (!isChatMode && "conteudo_gerado" in res) {
setParecer((p) => (p ? { ...p, conteudo_gerado: res.conteudo_gerado } : p)); if (tinhaParecer) {
} else { setParecer((p) => (p ? { ...p, conteudo_gerado: res.conteudo_gerado } : p));
setConteudoAtualizadoChat(res.conteudo_gerado); } else {
setConteudoAtualizadoChat(res.conteudo_gerado);
}
} }
await carregarChat(true); 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(); playSuccessSound();
toast.success("Parecer atualizado conforme sua solicitação."); toast.success(isChatMode ? "Resposta enviada ao chat." : "Parecer atualizado conforme sua solicitação.");
}) })
.catch((e) => { .catch((e) => {
toast.error(e?.message ?? "Erro ao enviar mensagem."); toast.error(e?.message ?? "Erro ao enviar mensagem.");
}) })
.finally(() => setChatProcessing(false)); .finally(() => {
setChatProcessing(false);
setChatInlineLoading(false);
});
}; };
if (!id?.trim()) { if (!id?.trim()) {
@@ -350,16 +425,27 @@ export function ParecerJuridicoDetailView() {
return ( return (
<> <>
{chatProcessing && ( {chatProcessing && modo === "agente" && (
<ParecerJuridicoGeneratingScreen message="Processando sua solicitação no chat. Isso pode levar vários minutos. Mantenha esta tela aberta até finalizar." /> <ParecerJuridicoGeneratingScreen message="Processando sua solicitação no chat. Isso pode levar vários minutos. Mantenha esta tela aberta até finalizar." />
)} )}
<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">
<header className="shrink-0 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between p-3 md:p-6 border-b border-border"> <header className="shrink-0 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between p-3 md:p-6 border-b border-border">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0 flex-1">
<Button variant="ghost" size="icon" onClick={() => navigate("/codex/parecer-juridico")} className="shrink-0"> <Button variant="ghost" size="icon" onClick={() => navigate("/codex/parecer-juridico")} className="shrink-0">
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
</Button> </Button>
<h1 className="text-lg md:text-xl font-bold truncate">Parecer: {titulo}</h1> <h1 className="text-lg md:text-xl font-bold truncate flex-1">{titulo}</h1>
<Button
type="button"
variant="outline"
size="sm"
className="gap-2"
disabled={!parecer || !id || editSaving}
onClick={abrirModalEdicao}
>
<Pencil className="w-4 h-4" />
Editar
</Button>
{loading && hasPreview && ( {loading && hasPreview && (
<Loader2 className="w-4 h-4 shrink-0 animate-spin text-muted-foreground" aria-label="Sincronizando" /> <Loader2 className="w-4 h-4 shrink-0 animate-spin text-muted-foreground" aria-label="Sincronizando" />
)} )}
@@ -465,6 +551,67 @@ export function ParecerJuridicoDetailView() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={editOpen} onOpenChange={setEditOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Editar título e área</DialogTitle>
<DialogDescription>Atualize os campos obrigatórios do parecer.</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-titulo" className="flex flex-wrap items-baseline gap-1">
<span>Título</span>
<span className="text-destructive font-semibold" aria-hidden>
*
</span>
<span className="sr-only">obrigatório</span>
</Label>
<Input
id="edit-titulo"
value={editTitulo}
onChange={(e) => setEditTitulo(e.target.value)}
placeholder="Título do parecer"
required
aria-required="true"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-area" className="flex flex-wrap items-baseline gap-1">
<span>Área</span>
<span className="text-destructive font-semibold" aria-hidden>
*
</span>
<span className="sr-only">obrigatório</span>
</Label>
<Select value={editAreaId || "none"} onValueChange={(v) => setEditAreaId(v === "none" ? "" : v)}>
<SelectTrigger id="edit-area">
<SelectValue placeholder="Selecione a área" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Selecione a área</SelectItem>
{areaItems.map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.nome}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter className="gap-2">
<Button type="button" variant="outline" onClick={() => setEditOpen(false)} disabled={editSaving}>
Cancelar
</Button>
<Button type="button" onClick={() => void handleSalvarEdicao()} disabled={editSaving || !editTitulo.trim() || !editAreaId.trim()}>
{editSaving ? "Salvando..." : "Salvar"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<AlertDialog open={deleteOpen} onOpenChange={(open) => !deleting && setDeleteOpen(open)}> <AlertDialog open={deleteOpen} onOpenChange={(open) => !deleting && setDeleteOpen(open)}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
@@ -605,13 +752,19 @@ export function ParecerJuridicoDetailView() {
return ( return (
<div key={msg.id} className={`flex flex-col gap-1 ${msg.role === "user" ? "items-end" : "items-start"}`}> <div key={msg.id} className={`flex flex-col gap-1 ${msg.role === "user" ? "items-end" : "items-start"}`}>
<div <div
className={`max-w-[85%] rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-sm whitespace-pre-wrap break-words ${ className={`max-w-[85%] rounded-2xl px-4 py-3 text-sm leading-relaxed shadow-sm break-words ${
msg.role === "user" msg.role === "user"
? "bg-primary text-primary-foreground rounded-br-md" ? "bg-primary text-primary-foreground rounded-br-md whitespace-pre-wrap"
: "bg-card border border-border text-card-foreground rounded-bl-md" : "bg-card border border-border text-card-foreground rounded-bl-md"
}`} }`}
> >
{msg.content} {msg.role === "assistant" ? (
<div className="prose prose-sm prose-neutral dark:prose-invert max-w-none prose-headings:font-semibold prose-p:leading-relaxed">
<ReactMarkdown>{msg.content}</ReactMarkdown>
</div>
) : (
msg.content
)}
</div> </div>
<div <div
className={`flex flex-wrap items-center gap-1 ${msg.role === "user" ? "flex-row-reverse justify-end" : "flex-row"}`} className={`flex flex-wrap items-center gap-1 ${msg.role === "user" ? "flex-row-reverse justify-end" : "flex-row"}`}
@@ -639,7 +792,19 @@ export function ParecerJuridicoDetailView() {
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
{msg.role === "assistant" && ( {msg.role === "assistant" && msg.tipo_resposta && (
<span
className={[
"inline-flex items-center gap-0.5 rounded-md border px-1.5 py-0.5 text-[10px] font-medium",
msg.tipo_resposta === "chat"
? "bg-primary/10 border-primary/30 text-primary"
: "bg-emerald-500/10 border-emerald-500/30 text-emerald-700 dark:text-emerald-400",
].join(" ")}
>
{msg.tipo_resposta}
</span>
)}
{msg.role === "assistant" && msg.tipo_resposta === "agente" && (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
@@ -658,10 +823,35 @@ export function ParecerJuridicoDetailView() {
</div> </div>
</div> </div>
); })} ); })}
{!chatLoading && modo === "chat" && chatInlineLoading && (
<div className="flex items-center gap-2 py-4 text-muted-foreground text-sm">
<Loader2 className="w-4 h-4 animate-spin" />
Gerando resposta no chat...
</div>
)}
<div ref={chatEndRef} aria-hidden className="h-0 w-full shrink-0" /> <div ref={chatEndRef} aria-hidden className="h-0 w-full shrink-0" />
</div> </div>
</ScrollArea> </ScrollArea>
<div className="p-3 border-t border-border space-y-2"> <div className="p-3 border-t border-border space-y-2">
<div className="flex items-center justify-between gap-3">
<div className="space-y-0.5">
<Label className="text-sm font-medium">Modo</Label>
<p className="text-xs text-muted-foreground">
{modo === "chat" ? "Apenas responde no chat" : "Agente (atualiza o parecer)"}
</p>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Chat</span>
<Switch
checked={modo === "agente"}
onCheckedChange={(checked) => setModo(checked ? "agente" : "chat")}
disabled={chatProcessing}
aria-label="Alternar modo de resposta"
/>
<span className="text-xs text-muted-foreground">Agente</span>
</div>
</div>
<div className="flex gap-1 md:gap-2 items-end"> <div className="flex gap-1 md:gap-2 items-end">
<div className="flex-1 glass-effect rounded-xl overflow-hidden"> <div className="flex-1 glass-effect rounded-xl overflow-hidden">
<div <div
+1 -5
View File
@@ -3,7 +3,6 @@ import { ChatView } from "@/components/chat/ChatView";
import { ImageView } from "@/components/images/ImageView"; import { ImageView } from "@/components/images/ImageView";
import { TranscriptionView } from "@/components/audio/TranscriptionView"; import { TranscriptionView } from "@/components/audio/TranscriptionView";
import { GenerationView } from "@/components/audio/GenerationView"; import { GenerationView } from "@/components/audio/GenerationView";
import { AgentView } from "@/components/agent/AgentView";
import { PromptsView } from "@/components/prompts/PromptsView"; import { PromptsView } from "@/components/prompts/PromptsView";
import { PromptsFormView } from "../components/prompts/PromptsFormView"; import { PromptsFormView } from "../components/prompts/PromptsFormView";
import { AreasView } from "@/components/areas/AreasView"; import { AreasView } from "@/components/areas/AreasView";
@@ -15,14 +14,13 @@ import { Navigate, Route, Routes, useLocation, useNavigate, Outlet } from "react
import React from "react"; import React from "react";
import { GlobalFunctions } from "@/GlobalFunctions"; import { GlobalFunctions } from "@/GlobalFunctions";
type TabType = "chat" | "images" | "transcription" | "generation" | "agent" | "prompts" | "parecerJuridico" | "areas"; type TabType = "chat" | "images" | "transcription" | "generation" | "prompts" | "parecerJuridico" | "areas";
const PATH_TO_TAB: Record<string, TabType> = { const PATH_TO_TAB: Record<string, TabType> = {
"/codex/bate-papo": "chat", "/codex/bate-papo": "chat",
"/codex/imagens": "images", "/codex/imagens": "images",
"/codex/transcricao-audio": "transcription", "/codex/transcricao-audio": "transcription",
"/codex/geracao-audio": "generation", "/codex/geracao-audio": "generation",
"/codex/parecer": "agent",
"/codex/parecer-juridico": "parecerJuridico", "/codex/parecer-juridico": "parecerJuridico",
"/codex/areas": "areas", "/codex/areas": "areas",
}; };
@@ -40,7 +38,6 @@ const TAB_TO_PATH: Record<TabType, string> = {
images: "/codex/imagens", images: "/codex/imagens",
transcription: "/codex/transcricao-audio", transcription: "/codex/transcricao-audio",
generation: "/codex/geracao-audio", generation: "/codex/geracao-audio",
agent: "/codex/parecer",
prompts: "/codex/prompts", prompts: "/codex/prompts",
parecerJuridico: "/codex/parecer-juridico", parecerJuridico: "/codex/parecer-juridico",
areas: "/codex/areas", areas: "/codex/areas",
@@ -76,7 +73,6 @@ const Index = () => {
<Route path="imagens" element={<ImageView />} /> <Route path="imagens" element={<ImageView />} />
<Route path="transcricao-audio" element={<TranscriptionView />} /> <Route path="transcricao-audio" element={<TranscriptionView />} />
<Route path="geracao-audio" element={<GenerationView />} /> <Route path="geracao-audio" element={<GenerationView />} />
<Route path="parecer" element={<AgentView />} />
<Route path="parecer-juridico" element={<ParecerJuridicoView />} /> <Route path="parecer-juridico" element={<ParecerJuridicoView />} />
<Route path="parecer-juridico/novo" element={<ParecerJuridicoFormView />} /> <Route path="parecer-juridico/novo" element={<ParecerJuridicoFormView />} />
<Route path="parecer-juridico/:id" element={<ParecerJuridicoDetailView />} /> <Route path="parecer-juridico/:id" element={<ParecerJuridicoDetailView />} />
+86 -4
View File
@@ -56,8 +56,15 @@ export interface ParecerChatMessage {
role: "user" | "assistant"; role: "user" | "assistant";
content: string; content: string;
created_at: 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 { export interface ParecerDetalhe {
id: string; id: string;
estabelecimento_id: number; estabelecimento_id: number;
@@ -77,6 +84,23 @@ export interface ParecerDetalhe {
atualizado_por: string | null; 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 type ParecerStatus = "processando" | "concluido" | "erro";
export interface ParecerListItem { export interface ParecerListItem {
@@ -216,7 +240,11 @@ class ParecerService {
return item as ParecerDetalhe; return item as ParecerDetalhe;
} }
async enviarMensagemChat(parecerId: string, message: string): Promise<{ conteudo_gerado: string }> { async enviarMensagemChat(
parecerId: string,
messageText: string,
modo: ParecerChatModo = "agente"
): Promise<EnviarMensagemChatResposta> {
const userEmail = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail); const userEmail = GlobalFunctions.getTransferProperty(TransferAreaProperties.UsuarioEmail);
if (!userEmail) { if (!userEmail) {
throw new Error("Usuário não identificado (user_email)"); throw new Error("Usuário não identificado (user_email)");
@@ -224,20 +252,33 @@ class ParecerService {
if (!parecerId?.trim()) { if (!parecerId?.trim()) {
throw new Error("ID do parecer é obrigatório"); throw new Error("ID do parecer é obrigatório");
} }
if (!message?.trim()) { if (!messageText?.trim()) {
throw new Error("Digite uma mensagem"); throw new Error("Digite uma mensagem");
} }
type ChatPostPayload = { success: boolean; conteudo_gerado?: string; message?: string }; type ChatPostPayload = { success: boolean; conteudo_gerado?: string; message?: string };
const response = await apiService.post<ChatPostPayload[] | ChatPostPayload>( const response = await apiService.post<ChatPostPayload[] | ChatPostPayload>(
PARECER_CHAT_ENVIAR_URL(parecerId), 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" } } { timeout: 0, headers: { "Content-Type": "application/json" } }
); );
const raw = response.data; const raw = response.data;
const payload = Array.isArray(raw) ? raw[0] : raw; 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"); 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 }; return { conteudo_gerado: payload.conteudo_gerado };
} }
@@ -272,6 +313,47 @@ class ParecerService {
return item; return item;
} }
async editarTituloArea(
parecerId: string,
body: EditarParecerTituloAreaBody
): Promise<EditarParecerTituloAreaSuccessResponse> {
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 }> { async excluir(id: string): Promise<{ message: string }> {
if (!id?.trim()) { if (!id?.trim()) {
throw new Error("ID do parecer é obrigatório"); throw new Error("ID do parecer é obrigatório");