Files
OPEN_CODEX_API/src/components/prompts/PromptsFormView.tsx
T
2026-05-16 19:29:25 -03:00

394 lines
15 KiB
TypeScript

import React, { useState, useEffect, useRef, forwardRef } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, ArrowRight, Sparkles, User } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { InteractiveHoverButton } from "@/components/ui/interactive-hover-button";
import { PromptAssistantButton } from "@/components/prompts/PromptAssistantButton";
import { Confetti, type ConfettiRef } from "@/components/ui/confetti";
import { cn } from "@/lib/utils";
import { usePrompts } from "@/contexts/PromptsContext";
import { areasService, type AreaItem } from "@/services/areas";
import { PromptGeneratingScreen } from "@/components/prompts/PromptGeneratingScreen";
import { assistentePromptsService } from "@/services/assistentePrompts";
import { promptsService } from "@/services/promptsApi";
import { toast } from "sonner";
import { playSuccessSound } from "@/utils/sound";
export function PromptsFormView() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { prompts, setPrompts } = usePrompts();
const isEdit = Boolean(id);
const prompt = id ? prompts.find((p) => p.id === id) : null;
const [areaItems, setAreaItems] = useState<AreaItem[]>([]);
const [areasLoading, setAreasLoading] = useState(true);
const [titulo, setTitulo] = useState("");
const [descricao, setDescricao] = useState("");
const [areaId, setAreaId] = useState("");
const [conteudo, setConteudo] = useState("");
const [isSaving, setIsSaving] = useState(false);
const [isAssistantOpen, setIsAssistantOpen] = useState(false);
const [assistantMode, setAssistantMode] = useState<"create" | "refine">("create");
const [assistantInstructions, setAssistantInstructions] = useState("");
const [isAssistantLoading, setIsAssistantLoading] = useState(false);
const [isAssistantWorkingInBackground, setIsAssistantWorkingInBackground] = useState(false);
const [showGeneratingModal, setShowGeneratingModal] = useState(false);
const confettiRef = useRef<ConfettiRef>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const hasContent = conteudo.trim().length > 0;
const effectiveAreaId = areaId && areaItems.some((a) => a.id === areaId) ? areaId : (areaItems[0]?.id ?? "");
const effectiveAreaName = areaItems.find((a) => a.id === effectiveAreaId)?.nome ?? "";
useEffect(() => {
setAreasLoading(true);
areasService.listarTotal()
.then((data) => setAreaItems(data))
.catch(() => setAreaItems([]))
.finally(() => setAreasLoading(false));
}, []);
useEffect(() => {
if (prompt) {
setTitulo(prompt.titulo);
setDescricao(prompt.descricao ?? "");
setAreaId(prompt.area_id ?? "");
setConteudo(prompt.conteudo ?? "");
} else if (!id) {
setTitulo("");
setDescricao("");
setAreaId("");
setConteudo("");
}
}, [prompt, id]);
useEffect(() => {
if (!id && areaItems.length > 0 && !areaId) {
setAreaId(areaItems[0].id);
}
}, [id, areaItems, areaId]);
useEffect(() => {
if (id && !prompt && prompts.length > 0) {
navigate("/commander/prompts", { replace: true });
}
}, [id, prompt, prompts.length, navigate]);
useEffect(() => {
if (!conteudo.trim() && assistantMode === "refine") {
setAssistantMode("create");
}
}, [conteudo, assistantMode]);
useEffect(() => {
return () => {
abortControllerRef.current?.abort();
};
}, []);
const handleSave = async () => {
const finalTitulo = titulo.trim();
const finalAreaId = effectiveAreaId;
const finalConteudo = conteudo.trim();
if (!finalTitulo || !finalAreaId) {
toast.error("Preencha o título e a área.");
return;
}
if (!finalConteudo) {
toast.error("Preencha o conteúdo do prompt.");
return;
}
const finalAreaName = areaItems.find((a) => a.id === finalAreaId)?.nome ?? "";
const finalDescricao = descricao.trim();
setIsSaving(true);
try {
if (isEdit && prompt) {
const res = await promptsService.editar(prompt.id, {
titulo: finalTitulo,
descricao: finalDescricao,
area_id: finalAreaId,
conteudo: finalConteudo,
});
setPrompts((prev) =>
prev.map((p) =>
p.id === prompt.id
? { ...p, id: res.id, titulo: res.titulo, area: finalAreaName, area_id: res.area_id, descricao: finalDescricao || undefined, conteudo: res.conteudo ?? "" }
: p
)
);
toast.success("Prompt atualizado.");
} else {
const res = await promptsService.criar({
titulo: finalTitulo,
descricao: finalDescricao,
area_id: finalAreaId,
conteudo: finalConteudo,
});
setPrompts((prev) => [...prev, { id: res.id, titulo: res.titulo, area: finalAreaName, area_id: finalAreaId, descricao: finalDescricao || undefined, conteudo: res.conteudo ?? "" }]);
toast.success("Prompt criado.");
}
navigate("/commander/prompts");
} catch (e: unknown) {
const message = e && typeof e === "object" && "message" in e ? (e as { message: string }).message : "Erro ao salvar.";
toast.error(message);
} finally {
setIsSaving(false);
}
};
const handleAssistantSubmit = () => {
if (!assistantInstructions.trim()) {
toast.error("Preencha as instruções.");
return;
}
if (assistantMode === "refine" && !conteudo.trim()) {
toast.error("Não há conteúdo para melhorar.");
return;
}
setIsAssistantOpen(false);
setShowGeneratingModal(true);
setIsAssistantWorkingInBackground(true);
setIsAssistantLoading(true);
const ac = new AbortController();
abortControllerRef.current = ac;
const signal = ac.signal;
const onSuccess = (text: string) => {
setConteudo(text);
confettiRef.current?.fire({});
playSuccessSound();
const msg =
assistantMode === "create" ? "Modelo inicial gerado!" : "Conteúdo atualizado!";
toast.success(msg, {
className: "bg-green-600 border-green-700 text-white",
});
};
const onFinish = () => {
setShowGeneratingModal(false);
setIsAssistantLoading(false);
setIsAssistantWorkingInBackground(false);
abortControllerRef.current = null;
};
const onError = (e: unknown) => {
const isAborted =
(e && typeof e === "object" && "code" in e && (e as { code: string }).code === "ERR_CANCELED") ||
(e && typeof e === "object" && "name" in e && (e as { name: string }).name === "AbortError");
if (!isAborted) {
const message =
e && typeof e === "object" && "message" in e
? (e as { message: string }).message
: "Erro ao processar.";
toast.error(message);
}
};
if (assistantMode === "create") {
assistentePromptsService.gerar(assistantInstructions, signal)
.then(onSuccess)
.catch(onError)
.finally(onFinish);
} else {
assistentePromptsService.refinar(conteudo, assistantInstructions, signal)
.then(onSuccess)
.catch(onError)
.finally(onFinish);
}
};
return (
<div className="flex-1 flex flex-col h-full overflow-hidden pb-16 md:pb-0 relative">
<Confetti
ref={confettiRef}
manualstart
className="absolute inset-0 size-full pointer-events-none z-50"
/>
<div className="border-b border-border p-3 md:p-6 flex-shrink-0">
<div className="flex flex-col gap-3">
<Button variant="ghost" size="sm" className="w-fit gap-2" onClick={() => navigate("/commander/prompts")}>
<ArrowLeft className="w-4 h-4" />
Voltar
</Button>
<div className="flex flex-wrap items-end gap-4">
<div className="flex-1 min-w-[200px] space-y-2">
<Label htmlFor="form-titulo">Título</Label>
<Input
id="form-titulo"
value={titulo}
onChange={(e) => setTitulo(e.target.value)}
placeholder="Ex: Prompt para análise de documentos"
/>
</div>
<div className="w-full sm:w-[220px] space-y-2">
<Label htmlFor="form-area">Área</Label>
<Select value={effectiveAreaId || "none"} onValueChange={(v) => setAreaId(v === "none" ? "" : v)} disabled={areasLoading}>
<SelectTrigger id="form-area">
<SelectValue placeholder={areasLoading ? "Carregando áreas..." : "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>
<div className="space-y-2">
<Label htmlFor="form-descricao">Descrição <span className="text-muted-foreground font-normal">(opcional)</span></Label>
<Input
id="form-descricao"
value={descricao}
onChange={(e) => setDescricao(e.target.value)}
placeholder="Ex: Breve descrição do prompt"
/>
</div>
</div>
</div>
<div className="flex-1 flex flex-col min-h-0 p-3 md:p-6 gap-2">
<div className="flex items-center justify-between gap-2 flex-wrap">
<Label htmlFor="form-conteudo">Conteúdo do Prompt</Label>
<div className="flex items-center gap-2">
{isAssistantWorkingInBackground && (
<div
className="w-2 h-2 rounded-full bg-primary animate-pulse-glow shrink-0"
title="Gerando em andamento"
/>
)}
<PromptAssistantButton
onClick={() => setIsAssistantOpen(true)}
disabled={isAssistantWorkingInBackground}
loading={isAssistantWorkingInBackground}
/>
</div>
</div>
<Textarea
id="form-conteudo"
value={conteudo}
onChange={(e) => setConteudo(e.target.value)}
placeholder="Digite o conteúdo do prompt..."
className="flex-1 min-h-[200px] resize-none p-3"
/>
<div className="flex justify-end pt-2">
<InteractiveHoverButton
type="button"
onClick={handleSave}
disabled={!titulo.trim() || !effectiveAreaId || !conteudo.trim() || isSaving}
className="disabled:opacity-50 disabled:pointer-events-none"
>
{isSaving ? "Salvando..." : isEdit ? "Salvar alterações" : "Criar prompt"}
</InteractiveHoverButton>
</div>
</div>
{showGeneratingModal && (
<PromptGeneratingScreen
mode={assistantMode}
message="Está em processamento. Aguarde que em breve o prompt ficará pronto. O modal será fechado assim que finalizar."
/>
)}
<Dialog open={isAssistantOpen} onOpenChange={setIsAssistantOpen}>
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col gap-6 overflow-hidden rounded-xl border border-border/80 bg-card p-8 shadow-modal">
<DialogHeader className="space-y-4">
<div className="flex justify-center">
<PromptAssistantButton asBadge />
</div>
<DialogTitle className="sr-only">Assistente de Criação de Prompt</DialogTitle>
<DialogDescription className="text-base text-muted-foreground">
Escolha criar um novo prompt do zero ou melhorar o conteúdo atual. Preencha as instruções e o assistente gerará o texto.
</DialogDescription>
</DialogHeader>
<div className="space-y-8 overflow-y-auto flex-1 min-h-0 px-1 pt-1 pb-2 pr-3 relative z-10">
<div className="space-y-4">
<Label className="text-base font-medium">O que deseja fazer?</Label>
<RadioGroup
value={assistantMode}
onValueChange={(v) => setAssistantMode(v as "create" | "refine")}
className="grid gap-4"
>
<div className="flex items-center space-x-3">
<RadioGroupItem value="create" id="assistant-create" />
<Label htmlFor="assistant-create" className="font-normal cursor-pointer">
Criar novo
</Label>
</div>
<div className="flex items-center space-x-3">
<RadioGroupItem
value="refine"
id="assistant-refine"
disabled={!hasContent}
/>
<Label
htmlFor="assistant-refine"
className={`font-normal ${!hasContent ? "cursor-not-allowed text-muted-foreground" : "cursor-pointer"}`}
>
Melhorar atual
{!hasContent && " (preencha o conteúdo do prompt antes)"}
</Label>
</div>
</RadioGroup>
</div>
<div className="space-y-4">
<Label htmlFor="assistant-instructions" className="text-base font-medium">
Instruções
</Label>
<Textarea
id="assistant-instructions"
value={assistantInstructions}
onChange={(e) => setAssistantInstructions(e.target.value)}
placeholder={
assistantMode === "create"
? "Ex: Um prompt que resuma reuniões em tópicos e ações..."
: "Ex: Tornar mais conciso, adicionar seção de exemplos..."
}
rows={8}
className="resize-none text-base py-4 px-4 min-h-[180px]"
/>
</div>
</div>
<DialogFooter className="gap-3 pt-4 relative z-10">
<Button variant="outline" onClick={() => setIsAssistantOpen(false)} disabled={isAssistantLoading}>
Fechar
</Button>
<Button
onClick={handleAssistantSubmit}
disabled={
!assistantInstructions.trim() ||
isAssistantLoading ||
(assistantMode === "refine" && !hasContent)
}
className="gap-2"
>
<Sparkles className="w-4 h-4 shrink-0" />
{assistantMode === "create" ? "Gerar" : "Melhorar"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}