Nova versao codex Parecer, atualizacao completa

This commit is contained in:
Vitex Tecnologia
2026-03-18 01:07:27 -03:00
parent 39032ea621
commit 6bf7e7d42e
49 changed files with 8003 additions and 2486 deletions
+368
View File
@@ -0,0 +1,368 @@
import { useState, useMemo, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { FileText, Plus, Eye, Edit, Copy } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { usePrompts } from "@/contexts/PromptsContext";
import type { Prompt } from "@/contexts/PromptsContext";
import { promptsService } from "@/services/promptsApi";
import { toast } from "sonner";
function apiItemToPrompt(item: { id: string; titulo: string; area_nome: string; area_id: string; descricao?: string; conteudo?: string }): Prompt {
return {
id: item.id,
titulo: item.titulo,
area: item.area_nome,
area_id: item.area_id,
descricao: item.descricao ?? undefined,
conteudo: item.conteudo ?? "",
};
}
export const PromptsView = () => {
const navigate = useNavigate();
const { prompts, setPrompts, areaItems } = usePrompts();
const [isDetailsDialogOpen, setIsDetailsDialogOpen] = useState(false);
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const [filterNome, setFilterNome] = useState("");
const [filterAreaId, setFilterAreaId] = useState<string>("");
const [promptsList, setPromptsList] = useState<Prompt[]>([]);
const [totalRegistros, setTotalRegistros] = useState(0);
const [totalPaginas, setTotalPaginas] = useState(1);
const [loadingList, setLoadingList] = useState(true);
const totalPages = Math.max(1, totalPaginas);
const selectedAreaItem = useMemo(
() => (selectedPrompt ? areaItems.find((a) => a.nome === selectedPrompt.area || a.id === selectedPrompt.area_id) ?? null : null),
[selectedPrompt, areaItems]
);
useEffect(() => {
let cancelled = false;
setLoadingList(true);
promptsService.listar({
titulo: filterNome.trim() || undefined,
area_id: filterAreaId.trim() || undefined,
page: currentPage,
per_page: itemsPerPage,
})
.then((res) => {
if (!cancelled) {
const list = (res.data ?? []).map(apiItemToPrompt);
setPromptsList(list);
setTotalRegistros(res.total_registros ?? 0);
setTotalPaginas(res.total_paginas ?? 1);
setPrompts((prev) => {
const byId = new Map(prev.map((p) => [p.id, p]));
list.forEach((p) => byId.set(p.id, p));
return Array.from(byId.values());
});
}
})
.catch((e: unknown) => {
if (!cancelled) {
const msg = e && typeof e === "object" && "message" in e ? (e as { message: string }).message : "Erro ao carregar prompts.";
toast.error(msg);
setPromptsList([]);
setTotalRegistros(0);
setTotalPaginas(1);
}
})
.finally(() => {
if (!cancelled) setLoadingList(false);
});
return () => { cancelled = true; };
}, [filterNome, filterAreaId, currentPage, itemsPerPage]);
useEffect(() => {
setCurrentPage(1);
}, [filterNome, filterAreaId]);
const clearFilters = () => {
setFilterNome("");
setFilterAreaId("");
setCurrentPage(1);
};
const hasActiveFilters = filterNome.trim() !== "" || filterAreaId !== "";
const handleItemsPerPageChange = (value: string) => {
setItemsPerPage(Number(value));
setCurrentPage(1);
};
const openDetailsDialog = (prompt: Prompt) => {
setSelectedPrompt(prompt);
setIsDetailsDialogOpen(true);
};
const openEditFromDetails = () => {
if (!selectedPrompt) return;
setIsDetailsDialogOpen(false);
navigate(`/codex/prompts/${selectedPrompt.id}`);
};
return (
<div className="flex flex-col h-full bg-background pb-16 md:pb-0">
<div className="p-3 md:p-6 border-b border-border">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-3 mb-4">
<h1 className="text-xl md:text-3xl font-bold text-foreground flex items-center gap-2">
<FileText className="w-5 h-5 md:w-6 md:h-6" />
Prompts
</h1>
<div className="flex gap-2 w-full md:w-auto">
<Button onClick={() => navigate("/codex/prompts/novo")} className="gap-1 md:gap-2 flex-1 md:flex-none text-xs md:text-sm">
<Plus className="w-3 h-3 md:w-4 md:h-4" />
<span className="hidden sm:inline">Novo prompt</span>
<span className="sm:hidden">Novo</span>
</Button>
</div>
</div>
<div className="flex flex-col md:flex-row items-stretch md:items-center gap-2 md:gap-4">
<Input
placeholder="Buscar prompts..."
value={filterNome}
onChange={(e) => setFilterNome(e.target.value)}
className="w-full md:max-w-sm text-sm"
/>
<Select value={filterAreaId || "all"} onValueChange={(v) => setFilterAreaId(v === "all" ? "" : v)}>
<SelectTrigger className="w-full md:w-[180px] text-sm">
<SelectValue placeholder="Todas as áreas" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas as áreas</SelectItem>
{areaItems.map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.nome}
</SelectItem>
))}
</SelectContent>
</Select>
{hasActiveFilters && (
<Button variant="ghost" size="sm" onClick={clearFilters}>
Limpar
</Button>
)}
<div className="flex items-center gap-2 justify-between md:justify-start">
<span className="text-xs md:text-sm text-muted-foreground whitespace-nowrap">Itens:</span>
<Select value={itemsPerPage.toString()} onValueChange={handleItemsPerPageChange}>
<SelectTrigger className="w-16 md:w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="20">20</SelectItem>
<SelectItem value="50">50</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="flex-1 overflow-auto p-3 md:p-6">
{!loadingList && totalRegistros === 0 && !hasActiveFilters ? (
<Card className="max-w-2xl mx-auto mt-12">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
Nenhum prompt cadastrado
</CardTitle>
<CardDescription>
Crie seu primeiro prompt para organizar por título e área.
</CardDescription>
</CardHeader>
<CardContent>
<Button onClick={() => navigate("/codex/prompts/novo")}>
<Plus className="w-4 h-4 mr-2" />
Novo prompt
</Button>
</CardContent>
</Card>
) : (
<>
<div className="border rounded-lg overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[200px] font-semibold text-xs md:text-sm">Título</TableHead>
<TableHead className="min-w-[120px] font-semibold text-xs md:text-sm">Área</TableHead>
<TableHead className="text-center text-xs md:text-sm">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loadingList ? (
<TableRow>
<TableCell colSpan={3} className="text-center text-muted-foreground py-8">
Carregando...
</TableCell>
</TableRow>
) : promptsList.length === 0 ? (
<TableRow>
<TableCell colSpan={3} className="text-center text-muted-foreground py-10 text-base">
Não resultados.
</TableCell>
</TableRow>
) : (
promptsList.map((prompt) => (
<TableRow key={prompt.id}>
<TableCell className="font-medium text-xs md:text-sm">{prompt.titulo}</TableCell>
<TableCell className="text-xs md:text-sm">{prompt.area}</TableCell>
<TableCell>
<div className="flex items-center justify-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => openDetailsDialog(prompt)}
title="Detalhes"
className="h-7 w-7 md:h-9 md:w-9 hover:bg-slate-300 dark:hover:bg-slate-700 hover:text-foreground"
>
<Eye className="w-3 h-3 md:w-4 md:h-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => navigate(`/codex/prompts/${prompt.id}`)}
title="Editar"
className="h-7 w-7 md:h-9 md:w-9 hover:bg-slate-300 dark:hover:bg-slate-700 hover:text-foreground"
>
<Edit className="w-3 h-3 md:w-4 md:h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{!loadingList && totalRegistros > 0 && (
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-2 mt-4">
<p className="text-xs md:text-sm text-muted-foreground text-center sm:text-left">
Mostrando {totalRegistros} {totalRegistros === 1 ? "prompt" : "prompts"}
</p>
<div className="flex gap-1 md:gap-2 justify-center">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="text-xs md:text-sm px-2 md:px-4"
>
<span className="hidden sm:inline">Anterior</span>
<span className="sm:hidden">Ant</span>
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let page: number;
if (totalPages <= 5) {
page = i + 1;
} else if (currentPage <= 3) {
page = i + 1;
} else if (currentPage >= totalPages - 2) {
page = totalPages - 4 + i;
} else {
page = currentPage - 2 + i;
}
return (
<Button
key={page}
variant={currentPage === page ? "default" : "outline"}
size="sm"
onClick={() => setCurrentPage(page)}
className="w-8 h-8 md:w-10 md:h-9 p-0 text-xs md:text-sm"
>
{page}
</Button>
);
})}
</div>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
className="text-xs md:text-sm px-2 md:px-4"
>
<span className="hidden sm:inline">Próxima</span>
<span className="sm:hidden">Prox</span>
</Button>
</div>
</div>
)}
</>
)}
</div>
{/* Modal Detalhes */}
<Dialog open={isDetailsDialogOpen} onOpenChange={setIsDetailsDialogOpen}>
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle>Detalhes do Prompt</DialogTitle>
<DialogDescription>
Informações do prompt selecionado
</DialogDescription>
</DialogHeader>
{selectedPrompt && (
<div className="space-y-4 overflow-y-auto flex-1 min-h-0 p-3">
<div>
<Label className="text-muted-foreground">Título</Label>
<p className="font-medium mt-1">{selectedPrompt.titulo}</p>
</div>
<div>
<Label className="text-muted-foreground">Área</Label>
<p className="mt-1 font-medium">{selectedPrompt.area}</p>
{selectedAreaItem?.descricao?.trim() && (
<p className="mt-1 text-sm text-muted-foreground">{selectedAreaItem.descricao}</p>
)}
</div>
{selectedPrompt.conteudo != null && selectedPrompt.conteudo !== "" && (
<div>
<Label className="text-muted-foreground">Conteúdo</Label>
<div className="mt-1 rounded-md border bg-muted/30 p-4 max-h-[50vh] overflow-y-auto">
<p className="whitespace-pre-wrap text-sm leading-relaxed">{selectedPrompt.conteudo}</p>
</div>
</div>
)}
</div>
)}
<DialogFooter>
<Button
variant="outline"
onClick={async () => {
if (selectedPrompt?.conteudo != null) {
await navigator.clipboard.writeText(selectedPrompt.conteudo);
toast.success("Prompt copiado para a área de transferência.");
} else {
toast.error("Nenhum conteúdo para copiar.");
}
}}
disabled={!selectedPrompt?.conteudo?.trim()}
>
<Copy className="w-4 h-4 mr-2" />
Copiar prompt
</Button>
<Button variant="outline" onClick={() => setIsDetailsDialogOpen(false)}>
Fechar
</Button>
<Button onClick={openEditFromDetails}>
<Edit className="w-4 h-4 mr-2" />
Editar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};