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, RefreshCw, HeartPulse } 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 { a2aApi, type A2ARemoteAgent, type A2ATask } from "@/api/a2a"; import { useProjectStore } from "@/store/projectStore"; import { useMcpHealthStore } from "@/store/mcpHealthStore"; 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; } interface A2ARemoteAgentForm { name: string; base_url: string; auth_scheme: "none" | "bearer"; auth_token: string; } const SOURCE_LOCAL_IMPORT = "local_import"; const SOURCE_SYSTEM_BUILTIN = "system_builtin"; const SOURCE_BACKEND_GENERATED = "backend_generated"; const SOURCE_UPLOADED_FILE = "uploaded_file"; const STATUS_SAFE = "safe"; const STATUS_LOW_RISK = "low_risk"; const normalizeSkillSource = (value?: string): string => { if (!value) return SOURCE_LOCAL_IMPORT; if (value === SOURCE_LOCAL_IMPORT || value === "本地导入" || value === "Local Import") return SOURCE_LOCAL_IMPORT; if (value === SOURCE_SYSTEM_BUILTIN || value === "系统内置" || value === "System Built-in") return SOURCE_SYSTEM_BUILTIN; if (value === SOURCE_BACKEND_GENERATED || value === "后台生成" || value === "Backend Generated") return SOURCE_BACKEND_GENERATED; if (value === SOURCE_UPLOADED_FILE || value === "文件上传" || value === "File Upload") return SOURCE_UPLOADED_FILE; return value; }; const normalizeSkillStatus = (value?: string): string => { if (!value) return STATUS_SAFE; if (value === STATUS_SAFE || value === "安全" || value === "Safe") return STATUS_SAFE; if (value === STATUS_LOW_RISK || value === "低风险" || value === "Low Risk") return STATUS_LOW_RISK; return value; }; const dedupeSkillsById = (skills: Skill[]): Skill[] => { const map = new Map(); for (const skill of skills) { const id = (skill.id || "").trim(); if (!id || map.has(id)) continue; map.set(id, skill); } return Array.from(map.values()); }; export function Skills() { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState<'skills' | 'mcp' | 'a2a'>('skills'); const [sourceFilter, setSourceFilter] = useState('all'); // 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: SOURCE_LOCAL_IMPORT, status: STATUS_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 [isRefreshingMcpHealth, setIsRefreshingMcpHealth] = useState(false); const [a2aAgents, setA2aAgents] = useState([]); const [a2aTasks, setA2aTasks] = useState([]); const [a2aTaskStateFilter, setA2aTaskStateFilter] = useState('all'); const [isA2aLoading, setIsA2aLoading] = useState(false); const [isA2aDialogOpen, setIsA2aDialogOpen] = useState(false); const [editingA2aAgent, setEditingA2aAgent] = useState(null); const [a2aForm, setA2aForm] = useState({ name: '', base_url: '', auth_scheme: 'none', auth_token: '', }); const [isA2aRefreshingHealth, setIsA2aRefreshingHealth] = useState(false); const { currentProject } = useProjectStore(); const { hasMcpError, refresh: refreshMcpHealth } = useMcpHealthStore(); const fileInputRef = useRef(null); const getSourceLabel = (source: string): string => { if (source === 'all') return t('allSources'); if (source === SOURCE_SYSTEM_BUILTIN) return t('systemBuiltin'); if (source === SOURCE_BACKEND_GENERATED) return t('backendGenerated'); if (source === SOURCE_UPLOADED_FILE) return t('uploadedFile'); if (source === SOURCE_LOCAL_IMPORT) return t('localImport'); return source; }; useEffect(() => { const fetchSkills = async () => { if (!currentProject) return; setIsLoading(true); try { const data = await api.get(`/api/v1/skills?project_id=${currentProject.id}`); setSkills(dedupeSkillsById(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 fetchA2aData = async () => { if (!currentProject) return; setIsA2aLoading(true); try { const [agents, tasks] = await Promise.all([ a2aApi.listRemoteAgents(currentProject.id), a2aApi.listTasks(currentProject.id, a2aTaskStateFilter), ]); setA2aAgents(agents || []); setA2aTasks(tasks || []); } catch (error) { console.error("Failed to fetch A2A data", error); } finally { setIsA2aLoading(false); } }; if (currentProject) { void refreshMcpHealth(currentProject.id); if (activeTab === 'skills') { void fetchSkills(); } else if (activeTab === 'mcp') { void fetchMcpServers(); } else { void fetchA2aData(); } } }, [currentProject, currentProject?.id, activeTab, refreshMcpHealth, a2aTaskStateFilter]); const fetchSkills = async () => { if (!currentProject) return; setIsLoading(true); try { const data = await api.get(`/api/v1/skills?project_id=${currentProject.id}`); setSkills(dedupeSkillsById(data || [])); } catch (error) { console.error("Failed to fetch skills", error); } finally { setIsLoading(false); } }; // Get unique sources for the filter dropdown const uniqueSources = Array.from(new Set(skills.map(s => normalizeSkillSource(s.source)))).filter(Boolean); // Filtered skills const filteredSkills = sourceFilter === 'all' ? skills : skills.filter(skill => normalizeSkillSource(skill.source) === sourceFilter); const fetchMcpServers = async () => { if (!currentProject) return; setIsMcpLoading(true); try { const data = await api.get(`/api/v1/mcp?project_id=${currentProject.id}`); setMcpServers(data); void refreshMcpHealth(currentProject.id); } catch (error) { console.error("Failed to fetch MCP servers", error); } finally { setIsMcpLoading(false); } }; const handleRefreshMcpHealth = async () => { if (!currentProject) return; setIsRefreshingMcpHealth(true); try { await refreshMcpHealth(currentProject.id); if (activeTab === 'mcp') { await fetchMcpServers(); } } finally { setIsRefreshingMcpHealth(false); } }; const fetchA2aData = async () => { if (!currentProject) return; setIsA2aLoading(true); try { const [agents, tasks] = await Promise.all([ a2aApi.listRemoteAgents(currentProject.id), a2aApi.listTasks(currentProject.id, a2aTaskStateFilter), ]); setA2aAgents(agents || []); setA2aTasks(tasks || []); } catch (error) { console.error("Failed to fetch A2A data", error); } finally { setIsA2aLoading(false); } }; const handleRefreshA2aHealth = async () => { if (!currentProject || a2aAgents.length === 0) return; setIsA2aRefreshingHealth(true); try { await Promise.all(a2aAgents.map((agent) => a2aApi.healthCheckRemoteAgent(agent.id))); await fetchA2aData(); } finally { setIsA2aRefreshingHealth(false); } }; const handleOpenCreateA2a = () => { setEditingA2aAgent(null); setA2aForm({ name: '', base_url: '', auth_scheme: 'none', auth_token: '', }); setIsA2aDialogOpen(true); }; const handleOpenEditA2a = (agent: A2ARemoteAgent) => { setEditingA2aAgent(agent); setA2aForm({ name: agent.name, base_url: agent.base_url, auth_scheme: agent.auth_scheme, auth_token: '', }); setIsA2aDialogOpen(true); }; const handleSaveA2aAgent = async () => { if (!currentProject) return; if (!a2aForm.name.trim() || !a2aForm.base_url.trim()) return; const payload = { name: a2aForm.name.trim(), base_url: a2aForm.base_url.trim(), auth_scheme: a2aForm.auth_scheme, ...(a2aForm.auth_scheme === 'bearer' && a2aForm.auth_token.trim() ? { auth_token: a2aForm.auth_token.trim() } : {}), }; try { if (editingA2aAgent) { await a2aApi.updateRemoteAgent(editingA2aAgent.id, payload); } else { await a2aApi.createRemoteAgent({ project_id: currentProject.id, ...payload, }); } setIsA2aDialogOpen(false); await fetchA2aData(); } catch (error) { console.error("Failed to save A2A agent", error); } }; const handleDeleteA2aAgent = async (agentId: number) => { if (!window.confirm(t('confirmDeleteA2aAgent'))) return; try { await a2aApi.deleteRemoteAgent(agentId); await fetchA2aData(); } catch (error) { console.error("Failed to delete A2A agent", error); } }; const handleRefreshA2aCard = async (agentId: number) => { try { await a2aApi.refreshRemoteAgentCard(agentId); await fetchA2aData(); } catch (error) { console.error("Failed to refresh A2A card", error); } }; 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/${encodeURIComponent(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: SOURCE_LOCAL_IMPORT, status: STATUS_SAFE }); setEditingSkill(null); setIsDialogOpen(false); } catch (error) { console.error("Failed to save skill", error); } } }; const handleEditSkill = (skill: Skill) => { setEditingSkill(skill); setNewSkill({ ...skill, source: normalizeSkillSource(skill.source), status: normalizeSkillStatus(skill.status), }); setIsDialogOpen(true); }; const handleDeleteSkill = async (id: string) => { if (!currentProject) return; if (!window.confirm(t('confirmDeleteSkill'))) return; try { await api.delete(`/api/v1/skills/${encodeURIComponent(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 (
{t('skillsRepository')}
{activeTab === 'skills' ? ( <> {uniqueSources.length > 0 && ( )} ) : activeTab === 'mcp' ? ( <> ) : ( <> )}
{activeTab === 'skills' ? (
{t('name')} {t('source')} {t('installationTime')} {t('status')} {t('actions')} {isLoading ? (
) : ( <> {filteredSkills.map((skill, index) => (

{skill.name}

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

{skill.description}

{normalizeSkillSource(skill.source) === SOURCE_SYSTEM_BUILTIN ? t('systemBuiltin') : normalizeSkillSource(skill.source) === SOURCE_BACKEND_GENERATED ? t('backendGenerated') : normalizeSkillSource(skill.source) === SOURCE_UPLOADED_FILE ? t('uploadedFile') : normalizeSkillSource(skill.source) === SOURCE_LOCAL_IMPORT ? t('localImport') : skill.source}
{skill.installation_time}
{normalizeSkillStatus(skill.status) === STATUS_SAFE ? ( ) : ( )} {normalizeSkillStatus(skill.status) === STATUS_SAFE ? t('safe') : normalizeSkillStatus(skill.status) === STATUS_LOW_RISK ? t('lowRisk') : skill.status}
{!skill.is_builtin ? ( ) : (
)}
))} {filteredSkills.length === 0 && (

{t('noSkillsInProjectClickImport')}

)} )}
) : activeTab === 'mcp' ? (
{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')}

)} )}
) : (
{t('a2aAgentManagement')}
{t('name')} {t('url')} {t('protocol')} {t('capabilities')} {t('healthStatus')} {t('actions')} {isA2aLoading ? ( ) : a2aAgents.length === 0 ? ( {t('noA2aAgents')} ) : ( a2aAgents.map((agent) => ( {agent.name} {agent.base_url} {agent.protocol_version || '-'} {agent.capabilities.join(', ') || '-'}
{agent.healthy ? : } {agent.healthy ? t('healthy') : t('unhealthy')} #{agent.failure_count}
)) )}
{t('a2aTaskObservability')}
{t('taskId')} {t('taskSource')} {t('status')} {t('content')} {t('time')} {isA2aLoading ? ( ) : a2aTasks.length === 0 ? ( {t('noA2aTasks')} ) : ( a2aTasks.map((task) => ( {task.id} {task.source} {task.state}
{task.error_message || task.output_text || task.input_text}
{task.updated_at}
)) )}
)}
{ setIsDialogOpen(open); if (!open) { setEditingSkill(null); setNewSkill({ type: 'python', content: '', source: SOURCE_LOCAL_IMPORT, status: STATUS_SAFE }); } }}> {editingSkill ? t('viewOrEditSkill') : t('addNewSkill')}
setNewSkill({...newSkill, name: e.target.value})} className="rounded-lg border-border h-10" disabled={editingSkill?.is_builtin} />