nova implementacao modo chat e agente no Parecer
This commit is contained in:
@@ -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
@@ -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 />} />
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user