import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Trash2, Terminal, Loader2, FolderOpen, Eye, ShieldCheck, AlertCircle, Wand2, Upload, Plus } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { api } from "@/lib/api"; import { useProjectStore } from "@/store/projectStore"; import { useRef } from 'react'; interface Skill { id: string; name: string; description: string; content: string; type: string; project_id?: number; source: string; installation_time: string; status: string; file_path?: string; is_builtin?: boolean; } interface MCPServer { id?: string; project_id?: number; name: string; type: 'stdio' | 'sse' | 'streamableHttp'; command?: string; args?: string[]; env?: Record; url?: string; headers?: Record; status?: string; } export function Skills() { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState<'skills' | 'mcp'>('skills'); // Skills state const [skills, setSkills] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingSkill, setEditingSkill] = useState(null); const [newSkill, setNewSkill] = useState>({ type: 'python', content: '', source: t('localImport'), status: t('safe') }); // MCP state const [mcpServers, setMcpServers] = useState([]); const [isMcpLoading, setIsMcpLoading] = useState(false); const [isMcpDialogOpen, setIsMcpDialogOpen] = useState(false); const [editingMcp, setEditingMcp] = useState(null); const [newMcp, setNewMcp] = useState>({ type: 'stdio' }); const [mcpArgsStr, setMcpArgsStr] = useState(''); const [mcpEnvStr, setMcpEnvStr] = useState(''); const [mcpHeadersStr, setMcpHeadersStr] = useState(''); const { currentProject } = useProjectStore(); const fileInputRef = useRef(null); useEffect(() => { const fetchSkills = async () => { if (!currentProject) return; setIsLoading(true); try { const data = await api.get(`/api/v1/skills?project_id=${currentProject.id}`); setSkills(data); } catch (error) { console.error("Failed to fetch skills", error); } finally { setIsLoading(false); } }; const fetchMcpServers = async () => { if (!currentProject) return; setIsMcpLoading(true); try { const data = await api.get(`/api/v1/mcp?project_id=${currentProject.id}`); setMcpServers(data); } catch (error) { console.error("Failed to fetch MCP servers", error); } finally { setIsMcpLoading(false); } }; if (currentProject) { if (activeTab === 'skills') { fetchSkills(); } else { fetchMcpServers(); } } }, [currentProject, activeTab]); const fetchSkills = async () => { if (!currentProject) return; setIsLoading(true); try { const data = await api.get(`/api/v1/skills?project_id=${currentProject.id}`); setSkills(data); } catch (error) { console.error("Failed to fetch skills", error); } finally { setIsLoading(false); } }; const fetchMcpServers = async () => { if (!currentProject) return; setIsMcpLoading(true); try { const data = await api.get(`/api/v1/mcp?project_id=${currentProject.id}`); setMcpServers(data); } catch (error) { console.error("Failed to fetch MCP servers", error); } finally { setIsMcpLoading(false); } }; const handleFileUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file || !currentProject) return; const formData = new FormData(); formData.append('file', file); formData.append('project_id', currentProject.id.toString()); setIsLoading(true); try { await api.post('/api/v1/skills/upload', formData); await fetchSkills(); } catch (error: unknown) { console.error("Failed to upload skill", error); const err = error as { response?: { data?: { detail?: string } }, message?: string }; const errorMessage = err.response?.data?.detail || err.message || t('unknownError'); alert(t('uploadFailed') + ': ' + errorMessage); } finally { setIsLoading(false); if (fileInputRef.current) fileInputRef.current.value = ''; } }; const handleAddSkill = async () => { if (!currentProject) return; if (newSkill.name && newSkill.description && newSkill.content) { try { if (editingSkill) { await api.put(`/api/v1/skills/${editingSkill.id}?project_id=${currentProject.id}`, { ...newSkill, project_id: currentProject.id }); } else { const skillToCreate = { ...newSkill, id: Date.now().toString(), project_id: currentProject.id }; await api.post('/api/v1/skills', skillToCreate); } await fetchSkills(); setNewSkill({ type: 'python', content: '', source: t('localImport'), status: t('safe') }); setEditingSkill(null); setIsDialogOpen(false); } catch (error) { console.error("Failed to save skill", error); } } }; const handleEditSkill = (skill: Skill) => { setEditingSkill(skill); setNewSkill(skill); setIsDialogOpen(true); }; const handleDeleteSkill = async (id: string) => { if (!currentProject) return; if (!window.confirm(t('confirmDeleteSkill'))) return; try { await api.delete(`/api/v1/skills/${id}?project_id=${currentProject.id}`); setSkills(skills.filter(s => s.id !== id)); } catch (error) { console.error("Failed to delete skill", error); } }; const handleAddMcpServer = async () => { if (!currentProject) return; try { const payload: Partial = { name: newMcp.name, type: newMcp.type, project_id: currentProject.id }; if (newMcp.type === 'stdio') { payload.command = newMcp.command; try { payload.args = mcpArgsStr ? JSON.parse(mcpArgsStr) : []; } catch { alert("Args must be a valid JSON array"); return; } try { payload.env = mcpEnvStr ? JSON.parse(mcpEnvStr) : {}; } catch { alert("Env must be a valid JSON object"); return; } } else { payload.url = newMcp.url; try { payload.headers = mcpHeadersStr ? JSON.parse(mcpHeadersStr) : {}; } catch { alert("Headers must be a valid JSON object"); return; } } if (editingMcp && editingMcp.id) { await api.put(`/api/v1/mcp/${editingMcp.id}?project_id=${currentProject.id}`, payload); } else { await api.post(`/api/v1/mcp`, payload); } await fetchMcpServers(); setIsMcpDialogOpen(false); setEditingMcp(null); setNewMcp({ type: 'stdio' }); setMcpArgsStr(''); setMcpEnvStr(''); setMcpHeadersStr(''); } catch (error: unknown) { console.error("Failed to save MCP server", error); const err = error as { response?: { data?: { detail?: string } }, message?: string }; alert(t('saveFailed') + (err.response?.data?.detail || err.message)); } }; const handleEditMcpServer = (mcp: MCPServer) => { setEditingMcp(mcp); setNewMcp(mcp); setMcpArgsStr(mcp.args ? JSON.stringify(mcp.args, null, 2) : ''); setMcpEnvStr(mcp.env ? JSON.stringify(mcp.env, null, 2) : ''); setMcpHeadersStr(mcp.headers ? JSON.stringify(mcp.headers, null, 2) : ''); setIsMcpDialogOpen(true); }; const handleDeleteMcpServer = async (id: string) => { if (!currentProject) return; if (!window.confirm(t('confirmDeleteMcpServer'))) return; try { await api.delete(`/api/v1/mcp/${id}?project_id=${currentProject.id}`); setMcpServers(mcpServers.filter(s => s.id !== id)); } catch (error) { console.error("Failed to delete MCP server", error); } }; if (!currentProject) { return (

{t('selectProjectToManageSkills')}

); } return (

< Wand2 className="h-6 w-6 text-indigo-500" />{t('skillsRepository')}

{t('manageAiSkillsDesc')}

{activeTab === 'skills' ? ( <> ) : ( )}
{activeTab === 'skills' ? (
{t('name')} {t('source')} {t('installationTime')} {t('status')} {t('actions')} {isLoading ? (
) : ( <> {skills.map((skill) => (

{skill.name}

{skill.type === 'agentskill' && ( Agent )}

{skill.description}

{skill.source}
{skill.installation_time}
{skill.status === t('safe') ? ( ) : ( )} {skill.status}
{!skill.is_builtin ? ( ) : (
)}
))} {skills.length === 0 && (

{t('noSkillsInProjectClickImport')}

)} )}
) : (
{t('mcpServerName')} {t('transport')} {t('content')} {t('status')} {t('actions')} {isMcpLoading ? (
) : ( <> {mcpServers.map((mcp) => (

{mcp.name}

{mcp.type} {mcp.type === 'stdio' ? mcp.command : mcp.url}
{mcp.status === 'connected' ? ( ) : ( )} {mcp.status}
))} {mcpServers.length === 0 && (

{t('noMcpServers')}

)} )}
)}
{ setIsDialogOpen(open); if (!open) { setEditingSkill(null); setNewSkill({ type: 'python', content: '', source: t('localImport'), status: t('safe') }); } }}> {editingSkill ? t('viewOrEditSkill') : t('addNewSkill')}
setNewSkill({...newSkill, name: e.target.value})} className="rounded-lg border-zinc-200 h-10" disabled={editingSkill?.is_builtin} />