"use client"; import { createFileRoute, useNavigate, Link } from "@tanstack/react-router"; import { useState, useCallback, lazy, Suspense } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { ArrowLeft, Check, Loader2, Play, Rocket, Save, MoreVertical, Copy, Archive, Trash2 } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import { ptBR } from "date-fns/locale"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { toast } from "sonner"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/hooks/use-auth"; import { useSkill } from "@/hooks/use-skills"; import { syncAgentSkills } from "@/lib/sync-agent-skills"; import { SkillStatusBadge } from "@/components/mika/skills/SkillStatusBadge"; import { SkillTestPanel } from "@/components/mika/skills/SkillTestPanel"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; const CodeMirrorEditor = lazy(() => import("@/components/mika/skills/SkillMarkdownEditor")); export const Route = createFileRoute("/painel/skills/$id")({ component: SkillDetailPage, }); interface SkillVersion { id: string; version_number: number; markdown_content: string; form_inputs: Record; is_live: boolean; created_at: string; } function SkillDetailPage() { const { id } = Route.useParams(); const { user } = useAuth(); const navigate = useNavigate(); const qc = useQueryClient(); const skill = useSkill(id); const versions = useQuery({ queryKey: ["skill-versions", id], enabled: !!id, queryFn: async (): Promise => { const { data, error } = await supabase .from("skill_versions") .select("id, version_number, markdown_content, form_inputs, is_live, created_at") .eq("skill_id", id) .order("version_number", { ascending: false }) .limit(12); if (error) throw error; return (data ?? []) as SkillVersion[]; }, }); const [selectedVersionId, setSelectedVersionId] = useState(null); const [markdown, setMarkdown] = useState(""); const [editing, setEditing] = useState(false); const [testOpen, setTestOpen] = useState(false); const [confirmArchive, setConfirmArchive] = useState(false); // Sync markdown when versions load or selection changes const currentVersion = versions.data?.find((v) => selectedVersionId ? v.id === selectedVersionId : v.is_live, ) ?? versions.data?.[0]; if (currentVersion && markdown === "" && !editing) { // initial load setTimeout(() => setMarkdown(currentVersion.markdown_content), 0); } const selectVersion = useCallback( (v: SkillVersion) => { setSelectedVersionId(v.id); setMarkdown(v.markdown_content); setEditing(false); }, [], ); // Save new version const saveVersion = useMutation({ mutationFn: async () => { if (!user || !versions.data) throw new Error("Dados indisponíveis"); const maxVer = Math.max(...versions.data.map((v) => v.version_number), 0); const { data, error } = await supabase .from("skill_versions") .insert([{ skill_id: id, version_number: maxVer + 1, markdown_content: markdown, // eslint-disable-next-line @typescript-eslint/no-explicit-any form_inputs: (currentVersion?.form_inputs ?? {}) as any, is_live: false, created_by: user.id, }]) .select("id, version_number") .single(); if (error) throw error; return data; }, onSuccess: (data) => { toast.success(`Versão ${data.version_number} salva`); setSelectedVersionId(data.id); setEditing(false); qc.invalidateQueries({ queryKey: ["skill-versions", id] }); }, onError: (e: unknown) => toast.error(e instanceof Error ? e.message : "Erro ao salvar"), }); // Publish version const publishVersion = useMutation({ mutationFn: async (versionId: string) => { const { data, error } = await supabase.functions.invoke<{ success?: boolean; no_op?: boolean; version_number?: number; synced?: boolean; sync_error?: string; }>("publish-skill-version", { body: { skill_version_id: versionId }, }); if (error) throw error; return data; }, onSuccess: (data) => { if (data?.no_op) { toast.info("Esta versão já está publicada"); } else if (data?.synced === false) { toast.warning(`Versão ${data.version_number} publicada, mas o sync falhou.`, { description: data.sync_error || "Tente novamente após o próximo deploy.", }); } else { toast.success(`Versão ${data?.version_number ?? ""} publicada!`); } qc.invalidateQueries({ queryKey: ["skill-versions", id] }); qc.invalidateQueries({ queryKey: ["skill", id] }); qc.invalidateQueries({ queryKey: ["skills"] }); }, onError: (e: unknown) => { const msg = e instanceof Error ? e.message : "Erro"; if (msg.includes("409")) { toast.error("Conflito de concorrência. Recarregue e tente novamente."); } else { toast.error(msg); } }, }); // Archive const archiveSkill = useMutation({ mutationFn: async () => { const { error } = await supabase .from("skills") .update({ status: "archived", updated_at: new Date().toISOString() }) .eq("id", id); if (error) throw error; }, onSuccess: () => { toast.success("Skill arquivada"); qc.invalidateQueries({ queryKey: ["skills"] }); qc.invalidateQueries({ queryKey: ["user-limits"] }); if (skill.data?.agent_instance_id) { void syncAgentSkills(skill.data.agent_instance_id).then(({ error }) => { if (error) { toast.warning("Skill arquivada, mas o sync com o container falhou.", { description: error.message, }); } }); } navigate({ to: "/painel/skills" }); }, onError: (e: unknown) => toast.error(e instanceof Error ? e.message : "Erro"), }); const loading = skill.isLoading || versions.isLoading; if (loading) { return (
); } if (!skill.data) { return (

Skill não encontrada.

); } const isCurrentLive = currentVersion?.is_live === true; const hasChanged = editing && markdown !== currentVersion?.markdown_content; return (
{/* Header */}

{skill.data.name}

{currentVersion && ( v{currentVersion.version_number} {currentVersion.is_live && " (live)"} )}
{editing && hasChanged && ( )} {!editing && currentVersion && !isCurrentLive && ( )} {!editing && ( )} Duplicar setConfirmArchive(true)} className="text-destructive focus:text-destructive cursor-pointer" > Arquivar
{/* Main content */}
{/* Editor / Preview (3 cols) */}
{/* Desktop split */}
}> { setMarkdown(v); if (!editing) setEditing(true); }} readOnly={!editing} />
{markdown}
{/* Mobile tabs */}
Editor Preview
}> { setMarkdown(v); if (!editing) setEditing(true); }} readOnly={!editing} />
{markdown}
{/* Timeline sidebar (1 col) */}

Versões

{versions.data?.map((v) => ( )} ))}
{/* Test panel */} {testOpen && currentVersion && ( )} {/* Archive dialog */} Arquivar esta skill? A skill ficará invisível para o agente. Você poderá restaurá-la depois. Cancelar archiveSkill.mutate()}>Arquivar
); }