import { Edit, FileCode, Plus, Puzzle, RefreshCw, Search, Trash2, UserCheck } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { AgentIdenticon } from "../components/AgentIdenticon"; import { Button } from "../components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../components/ui/dialog"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { Textarea } from "../components/ui/textarea"; import { useAgents, useUpdateAgent } from "../hooks/useAgents"; import { useCreateSkill, useDeleteSkill, useImportSkill, useSkill, useSkills, useUpdateSkill } from "../hooks/useSkills"; export function SkillsPage() { const { skills, loading: skillsLoading, refresh: refreshSkills } = useSkills(); const { agents, refresh: refreshAgents } = useAgents(); const updateAgent = useUpdateAgent(); const createSkillMutation = useCreateSkill(); const updateSkillMutation = useUpdateSkill(); const deleteSkillMutation = useDeleteSkill(); const importSkillMutation = useImportSkill(); const [selectedSkillId, setSelectedSkillId] = useState(null); const { skill: selectedSkill, loading: skillLoading } = useSkill(selectedSkillId ?? undefined); const [searchQuery, setSearchQuery] = useState(""); const [activeTab, setActiveTab] = useState<"content" | "assign">("content"); // Dialog states const [importOpen, setImportOpen] = useState(false); const [importRef, setImportRef] = useState(""); const [createOpen, setCreateOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); // Form states const [formId, setFormId] = useState(""); const [formName, setFormName] = useState(""); const [formDesc, setFormDesc] = useState(""); const [formContent, setFormContent] = useState(""); // Agent updating tracking by agent ID const [updatingAgentId, setUpdatingAgentId] = useState(null); // Default selection useEffect(() => { if (skills.length > 0 && !selectedSkillId) { setSelectedSkillId(skills[0].id); } }, [skills, selectedSkillId]); const filteredSkills = skills.filter( (s) => s.id.toLowerCase().includes(searchQuery.toLowerCase()) || s.name.toLowerCase().includes(searchQuery.toLowerCase()) || (s.description || "").toLowerCase().includes(searchQuery.toLowerCase()), ); const handleImport = async (e: React.FormEvent) => { e.preventDefault(); if (!importRef.trim()) return; try { await importSkillMutation.mutateAsync(importRef.trim()); toast.success("Skill imported successfully!"); setImportOpen(false); setImportRef(""); refreshSkills(); } catch (err: any) { toast.error(err.message || "Failed to import skill"); } }; const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); if (!formId.trim() || !formName.trim() || !formContent.trim()) { toast.error("Please fill in ID, Name, and Content"); return; } try { const newSkill = await createSkillMutation.mutateAsync({ id: formId.trim(), name: formName.trim(), description: formDesc.trim() || undefined, content: formContent.trim(), }); toast.success("Skill created successfully!"); setCreateOpen(false); clearForm(); setSelectedSkillId(newSkill.id); refreshSkills(); } catch (err: any) { toast.error(err.message || "Failed to create skill"); } }; const handleEdit = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedSkillId) return; try { await updateSkillMutation.mutateAsync({ id: selectedSkillId, body: { name: formName.trim(), description: formDesc.trim() || null, content: formContent.trim(), }, }); toast.success("Skill updated successfully!"); setEditOpen(false); clearForm(); refreshSkills(); } catch (err: any) { toast.error(err.message || "Failed to update skill"); } }; const handleDelete = async (id: string) => { if (!confirm("Are you sure you want to delete this skill? This will not remove it from agents automatically but they won't be able to fetch it.")) return; try { await deleteSkillMutation.mutateAsync(id); toast.success("Skill deleted successfully!"); setSelectedSkillId(null); refreshSkills(); } catch (err: any) { toast.error(err.message || "Failed to delete skill"); } }; const handleToggleAssign = async (agent: any, isChecked: boolean) => { if (!selectedSkillId) return; setUpdatingAgentId(agent.id); try { const currentSkills = agent.skills || []; const nextSkills = isChecked ? [...currentSkills.filter((s: string) => s !== selectedSkillId), selectedSkillId] : currentSkills.filter((s: string) => s !== selectedSkillId); await updateAgent.mutateAsync({ id: agent.id, body: { skills: nextSkills }, }); toast.success(isChecked ? `Assigned to agent ${agent.name}` : `Removed from agent ${agent.name}`); refreshAgents(); } catch (err: any) { toast.error(err.message || "Failed to update agent skills"); } finally { setUpdatingAgentId(null); } }; const clearForm = () => { setFormId(""); setFormName(""); setFormDesc(""); setFormContent(""); }; const openEdit = () => { if (!selectedSkill) return; setFormName(selectedSkill.name); setFormDesc(selectedSkill.description || ""); setFormContent(selectedSkill.content); setEditOpen(true); }; const latestAgents = agents.filter((a) => a.version === "latest"); return (
{/* Left panel: List */}

Skills

setSearchQuery(e.target.value)} className="pl-8 h-9 text-xs" />
{skillsLoading ? (
{[0, 1, 2, 3].map((i) => (
))}
) : filteredSkills.length === 0 ? (
No skills found.
) : ( filteredSkills.map((s) => ( )) )}
{/* Right panel: Details */}
{selectedSkillId ? ( skillLoading ? (

Loading skill details...

) : selectedSkill ? (
{/* Detail Header */}

{selectedSkill.name}

{selectedSkill.id.includes("@") && ( Imported )}
ID: {selectedSkill.id}
{selectedSkill.description &&

{selectedSkill.description}

}
{/* Tabs list */}
{/* Tab Contents */}
{activeTab === "content" && (
{selectedSkill.content}
)} {activeTab === "assign" && (

Select agents to assign this skill

{latestAgents.length === 0 ? (

No active agents found.

) : ( latestAgents.map((agent) => { const isAssigned = (agent.skills || []).includes(selectedSkill.id); const isUpdating = updatingAgentId === agent.id; return (
{agent.name}
@{agent.username}
); }) )}
)}
) : (

Skill not found.

) ) : (

No skill selected

Select a skill from the list, or create/import a new one to begin.

)}
{/* Dialog: Import Skill */} Import Skill from GitHub
setImportRef(e.target.value)} className="font-mono text-xs" />

Format: owner/repo[#branch]@skill-name.
The source will fetch from raw.githubusercontent.com at the specified path.

{/* Dialog: Create Skill */} Create Custom Skill
setFormId(e.target.value.toLowerCase().replace(/[^a-z0-9-_]/g, "-"))} className="font-mono text-xs" />
setFormName(e.target.value)} className="text-xs" />
setFormDesc(e.target.value)} className="text-xs" />