import { BrowserFrame } from "@/components/Browser/BrowserFrame"; import { useState, useRef, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Plus, Play, Trash2, Pencil, Loader2, CheckCircle, XCircle, Clock, Search, Zap, Code2, ListChecks, ChevronRight, History, Copy, Store, Download, CheckCheck, Tag, Sparkles, } from "lucide-react"; import { useToast } from "@/hooks/use-toast"; import Editor from "@monaco-editor/react"; // ─── Types ──────────────────────────────────────────────────────────────────── interface Skill { id: string; name: string; slug: string; description: string | null; namespace: string; status: string; triggerType: string | null; version: string; body: string | null; parametersSchema: Record | null; tags: string[] | null; extends: string[] | null; createdAt: string; updatedAt: string; } interface SkillExecution { id: string; skillId: string; status: string; triggeredBy: string; inputParams: Record | null; outputResult: Record | null; errorMessage: string | null; durationMs: number | null; startedAt: string; completedAt: string | null; } interface MarketplaceSkill extends Skill { imported: boolean; } const EMPTY_SKILL = { name: "", slug: "", description: "", namespace: "tenant", status: "draft", triggerType: "manual", body: "# Skill\n\nDescreva o comportamento desta skill aqui.\n\n## Entradas\n\n/var/input\n\n## Processamento\n\n...\n\n## Saída\n\n...", parametersSchema: "{}", tags: "", extends: "", }; // ─── Helpers ────────────────────────────────────────────────────────────────── function statusBadge(status: string) { const map: Record = { active: "bg-green-500/20 text-green-400 border-green-500/30", draft: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30", archived: "bg-gray-500/20 text-gray-400 border-gray-500/30", }; return map[status] ?? map.draft; } function execStatusIcon(status: string) { if (status === "success") return ; if (status === "error") return ; return ; } function toSlug(name: string) { return name .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]+/g, "_") .replace(/^_|_$/g, ""); } // ─── Main Component ─────────────────────────────────────────────────────────── export default function Skills() { const qc = useQueryClient(); const { toast } = useToast(); const [search, setSearch] = useState(""); const [nsFilter, setNsFilter] = useState("all"); const [statusFilter, setStatusFilter] = useState("all"); const [editOpen, setEditOpen] = useState(false); const [editingSkill, setEditingSkill] = useState(null); const [form, setForm] = useState(EMPTY_SKILL); const [execOpen, setExecOpen] = useState(false); const [execSkill, setExecSkill] = useState(null); const [execParams, setExecParams] = useState("{}"); const [execResult, setExecResult] = useState(null); const [historyOpen, setHistoryOpen] = useState(false); const [historySkill, setHistorySkill] = useState(null); const [view, setView] = useState<"minhas" | "marketplace">("minhas"); const [mktSearch, setMktSearch] = useState(""); const [mktTag, setMktTag] = useState("all"); const [importedId, setImportedId] = useState(null); // Ref para skills — usado no completion provider sem closure stale const skillsRef = useRef([]); const completionDisposable = useRef<{ dispose(): void } | null>(null); // ── Queries ──────────────────────────────────────────────────────────────── const { data: skillsData, isLoading } = useQuery<{ skills: Skill[] }>({ queryKey: ["/api/skills", search, nsFilter, statusFilter], queryFn: async () => { const params = new URLSearchParams(); if (search) params.set("search", search); if (nsFilter !== "all") params.set("namespace", nsFilter); if (statusFilter !== "all") params.set("status", statusFilter); const res = await fetch(`/api/skills?${params}`); return res.json(); }, }); const { data: historyData } = useQuery<{ executions: SkillExecution[] }>({ queryKey: ["/api/skills", historySkill?.id, "executions"], queryFn: async () => { const res = await fetch(`/api/skills/${historySkill!.id}/executions?limit=20`); return res.json(); }, enabled: !!historySkill && historyOpen, }); const { data: mktData, isLoading: mktLoading } = useQuery<{ skills: MarketplaceSkill[] }>({ queryKey: ["/api/skills/marketplace", mktSearch, mktTag], queryFn: async () => { const params = new URLSearchParams(); if (mktSearch) params.set("search", mktSearch); if (mktTag !== "all") params.set("tag", mktTag); const res = await fetch(`/api/skills/marketplace?${params}`); return res.json(); }, enabled: view === "marketplace", }); // ── Mutations ────────────────────────────────────────────────────────────── const saveMutation = useMutation({ mutationFn: async (data: typeof form) => { let paramsSchema: Record = {}; try { paramsSchema = JSON.parse(data.parametersSchema); } catch {} const payload = { name: data.name, slug: data.slug || toSlug(data.name), description: data.description || null, namespace: data.namespace, status: data.status, triggerType: data.triggerType, body: data.body, parametersSchema: paramsSchema, tags: data.tags ? data.tags.split(",").map(t => t.trim()).filter(Boolean) : [], extends: data.extends ? data.extends.split(",").map(t => t.trim()).filter(Boolean) : [], }; const url = editingSkill ? `/api/skills/${editingSkill.id}` : "/api/skills"; const method = editingSkill ? "PUT" : "POST"; const res = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(await res.text()); return res.json(); }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["/api/skills"] }); setEditOpen(false); toast({ title: editingSkill ? "Skill atualizada" : "Skill criada" }); }, onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), }); const deleteMutation = useMutation({ mutationFn: async (id: string) => { const res = await fetch(`/api/skills/${id}`, { method: "DELETE" }); if (!res.ok) throw new Error("Erro ao deletar"); }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["/api/skills"] }); toast({ title: "Skill removida" }); }, onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), }); const importMutation = useMutation({ mutationFn: async (skillId: string) => { const res = await fetch(`/api/skills/marketplace/${skillId}/import`, { method: "POST" }); if (!res.ok) throw new Error(await res.text()); return res.json() as Promise<{ skill: Skill }>; }, onSuccess: (data) => { setImportedId(data.skill.id); qc.invalidateQueries({ queryKey: ["/api/skills/marketplace"] }); qc.invalidateQueries({ queryKey: ["/api/skills"] }); toast({ title: `"${data.skill.name}" importada para suas skills` }); }, onError: (e: any) => toast({ title: "Erro ao importar", description: e.message, variant: "destructive" }), }); const executeMutation = useMutation({ mutationFn: async ({ skillId, params }: { skillId: string; params: string }) => { let inputParams: Record = {}; try { inputParams = JSON.parse(params); } catch {} const res = await fetch(`/api/skills/${skillId}/execute`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ inputParams, triggeredBy: "manual" }), }); return res.json() as Promise; }, onSuccess: (data) => { setExecResult(data); qc.invalidateQueries({ queryKey: ["/api/skills"] }); }, onError: (e: any) => toast({ title: "Erro", description: e.message, variant: "destructive" }), }); // ── Handlers ─────────────────────────────────────────────────────────────── function openNew() { setEditingSkill(null); setForm(EMPTY_SKILL); setEditOpen(true); } function openEdit(skill: Skill) { setEditingSkill(skill); setForm({ name: skill.name, slug: skill.slug, description: skill.description ?? "", namespace: skill.namespace, status: skill.status, triggerType: skill.triggerType ?? "manual", body: skill.body ?? "", parametersSchema: JSON.stringify(skill.parametersSchema ?? {}, null, 2), tags: (skill.tags ?? []).join(", "), extends: (skill.extends ?? []).join(", "), }); setEditOpen(true); } function openExec(skill: Skill) { setExecSkill(skill); setExecParams("{}"); setExecResult(null); setExecOpen(true); } function openHistory(skill: Skill) { setHistorySkill(skill); setHistoryOpen(true); } const skills = skillsData?.skills ?? []; // Mantém ref atualizado para o completion provider useEffect(() => { skillsRef.current = skills; }, [skills]); // Cleanup do completion provider ao desmontar useEffect(() => () => { completionDisposable.current?.dispose(); }, []); // Handler do Monaco Body — registra completion provider de referências / function handleBodyEditorMount(editor: unknown, monaco: any) { completionDisposable.current?.dispose(); const PREFIXES = [ { label: "/skill/", detail: "Skill do tenant atual" }, { label: "/skill:system/", detail: "Skill global do sistema" }, { label: "/skill:company/", detail: "Skill da empresa" }, { label: "/var/", detail: "Variável de contexto ou parâmetro de entrada" }, { label: "/data/", detail: "Dado dinâmico do banco" }, { label: "/kg/", detail: "Nó do Knowledge Graph (Neo4j)" }, { label: "/file/", detail: "Arquivo no storage" }, { label: "/tool/", detail: "Tool disponível no Manus" }, { label: "/agent/", detail: "Agente especialista" }, ]; completionDisposable.current = monaco.languages.registerCompletionItemProvider("markdown", { triggerCharacters: ["/"], provideCompletionItems(model: any, position: any) { const line = model.getValueInRange({ startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: position.column, }); const match = line.match(/\/([\w:\/\-.]*)$/); if (!match) return { suggestions: [] }; const typed = match[1]; const startCol = position.column - typed.length - 1; const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: startCol, endColumn: position.column, }; const CIK = monaco.languages.CompletionItemKind; // Sugestões de prefixos const prefixSuggestions = PREFIXES.map(p => ({ label: p.label, kind: CIK.Reference, detail: p.detail, insertText: p.label, sortText: "0_" + p.label, range, })); // Sugestões de skills existentes quando digita /skill/... const skillSuggestions: any[] = []; if (typed.startsWith("skill/") || typed.startsWith("skill:")) { const afterSkill = typed.replace(/^skill(:[^/]+)?\//, ""); skillsRef.current.forEach(s => { const ref = `/skill/${s.namespace}/${s.slug}`; if (!afterSkill || s.slug.includes(afterSkill) || s.name.toLowerCase().includes(afterSkill)) { skillSuggestions.push({ label: ref, kind: CIK.Variable, detail: s.description ?? s.name, documentation: `namespace: ${s.namespace} | status: ${s.status}`, insertText: ref, sortText: "1_" + ref, range, }); } }); } return { suggestions: [...prefixSuggestions, ...skillSuggestions] }; }, }); } // ── Render ───────────────────────────────────────────────────────────────── return (
{/* Header */}

Skills

Objetos reutilizáveis — herança, composição, polimorfismo

{view === "minhas" && ( )}
{/* ── VIEW: MINHAS SKILLS ──────────────────────────────────────────────── */} {view === "minhas" && (<> {/* Filters */}
setSearch(e.target.value)} className="pl-8 h-8 text-sm bg-white/5 border-white/10" />
{/* List */} {isLoading ? (
) : skills.length === 0 ? (

Nenhuma skill encontrada

) : (
{skills.map(skill => (
{skill.name} {skill.status} {skill.namespace}

/skill/{skill.namespace}/{skill.slug}

{skill.description && (

{skill.description}

)} {skill.tags && skill.tags.length > 0 && (
{skill.tags.map(tag => ( {tag} ))}
)}
))}
)}
)} {/* ── VIEW: BIBLIOTECA (MARKETPLACE) ──────────────────────────────────── */} {view === "marketplace" && (<> {/* Marketplace filters */}
setMktSearch(e.target.value)} className="pl-8 h-8 text-sm bg-white/5 border-white/10" />
{/* Marketplace grid */} {mktLoading ? (
) : !mktData?.skills?.length ? (

Biblioteca vazia

Ainda não há skills do sistema publicadas. Skills com namespace system e status active aparecerão aqui.

) : (
{mktData.skills.map(skill => ( {/* Top row */}
{skill.name}

/skill:system/{skill.slug}

{/* Import button */} {skill.imported || importedId === skill.id ? ( Importada ) : ( )}
{/* Description */} {skill.description && (

{skill.description}

)} {/* Tags */} {skill.tags && skill.tags.length > 0 && (
{skill.tags.map(tag => ( setMktTag(tag)} className="text-[10px] bg-white/10 hover:bg-white/20 px-2 py-0.5 rounded cursor-pointer transition-colors" > {tag} ))}
)} {/* Footer */}
{skill.triggerType ?? "manual"} v{skill.version}
))}
)}
)}
{/* ── Edit / Create Dialog ────────────────────────────────────────────── */} {editingSkill ? "Editar Skill" : "Nova Skill"} Geral Body Parâmetros {/* Tab: Geral */}
setForm(f => ({ ...f, name: e.target.value, slug: toSlug(e.target.value) }))} placeholder="Relatório de Vendas" className="bg-white/5 border-white/10 text-sm" />
setForm(f => ({ ...f, slug: e.target.value }))} placeholder="relatorio_vendas" className="bg-white/5 border-white/10 text-sm font-mono" />