import React, { useState } from 'react'; import { Plus, RefreshCw, Trash2, Edit2, Zap, X, FileText, Save } from 'lucide-react'; import { motion, AnimatePresence } from 'motion/react'; import { useTranslation } from 'react-i18next'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; import { Skill } from '../types'; const initialSkillForm: Omit = { name: '', description: '', tools: [], system_prompt: '' }; const Skills: React.FC = () => { const { t } = useTranslation(); const { skills, refreshSkills, q } = useAppContext(); const ui = useUI(); const [installName, setInstallName] = useState(''); const qp = (k: string, v: string) => `${q}${q ? '&' : '?'}${k}=${encodeURIComponent(v)}`; const [isModalOpen, setIsModalOpen] = useState(false); const [editingSkill, setEditingSkill] = useState(null); const [form, setForm] = useState>(initialSkillForm); const [isFileModalOpen, setIsFileModalOpen] = useState(false); const [activeSkill, setActiveSkill] = useState(''); const [skillFiles, setSkillFiles] = useState([]); const [activeFile, setActiveFile] = useState(''); const [fileContent, setFileContent] = useState(''); async function deleteSkill(id: string) { if (!await ui.confirmDialog({ title: 'Delete Skill', message: 'Are you sure you want to delete this skill?', danger: true, confirmText: 'Delete' })) return; try { await fetch(`/webui/api/skills${qp('id', id)}`, { method: 'DELETE' }); await refreshSkills(); } catch (e) { console.error(e); } } async function installSkill() { const name = installName.trim(); if (!name) return; const r = await fetch(`/webui/api/skills${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'install', name }), }); if (!r.ok) { await ui.notify({ title: 'Request Failed', message: await r.text() }); return; } setInstallName(''); await refreshSkills(); } async function handleSubmit() { try { const action = editingSkill ? 'update' : 'create'; const r = await fetch(`/webui/api/skills${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, ...(editingSkill && { id: editingSkill.id }), ...form }) }); if (r.ok) { setIsModalOpen(false); await refreshSkills(); } else { await ui.notify({ title: 'Request Failed', message: await r.text() }); } } catch (e) { await ui.notify({ title: 'Error', message: String(e) }); } } async function openFileManager(skillId: string) { setActiveSkill(skillId); setIsFileModalOpen(true); const r = await fetch(`/webui/api/skills${q ? `${q}&id=${encodeURIComponent(skillId)}&files=1` : `?id=${encodeURIComponent(skillId)}&files=1`}`); if (!r.ok) { await ui.notify({ title: 'Request Failed', message: await r.text() }); return; } const j = await r.json(); const files = Array.isArray(j.files) ? j.files : []; setSkillFiles(files); if (files.length > 0) { await openFile(skillId, files[0]); } else { setActiveFile(''); setFileContent(''); } } async function openFile(skillId: string, file: string) { const url = `/webui/api/skills${q ? `${q}&id=${encodeURIComponent(skillId)}&file=${encodeURIComponent(file)}` : `?id=${encodeURIComponent(skillId)}&file=${encodeURIComponent(file)}`}`; const r = await fetch(url); if (!r.ok) { await ui.notify({ title: 'Request Failed', message: await r.text() }); return; } const j = await r.json(); setActiveFile(file); setFileContent(String(j.content || '')); } async function saveFile() { if (!activeSkill || !activeFile) return; const r = await fetch(`/webui/api/skills${q}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'write_file', id: activeSkill, file: activeFile, content: fileContent }), }); if (!r.ok) { await ui.notify({ title: 'Request Failed', message: await r.text() }); return; } await ui.notify({ title: 'Saved', message: 'Skill file saved successfully.' }); } return (

{t('skills')}

setInstallName(e.target.value)} placeholder="skill name" className="px-3 py-2 bg-zinc-950 border border-zinc-800 rounded-lg text-sm" />
{skills.map(s => (

{s.name}

ID: {s.id.slice(-6)}

{s.description || 'No description provided.'}

Tools
{(Array.isArray(s.tools) ? s.tools : []).map(tool => ( {tool} ))} {(!Array.isArray(s.tools) || s.tools.length === 0) && No tools defined}
))}
{isModalOpen && (
setIsModalOpen(false)} className="absolute inset-0 bg-black/60 backdrop-blur-sm" />

{editingSkill ? 'Edit Skill' : 'Add Skill'}