import React, { useState, useEffect } from 'react';
import { getSkills, createSkill, updateSkill, deleteSkill } from '../services/apiService';
export default function SkillsPage({ user, Icon }) {
const [skills, setSkills] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingSkill, setEditingSkill] = useState(null);
const [formData, setFormData] = useState({
name: '',
description: '',
skill_type: 'local',
config: '{}',
is_system: false,
group_id: ''
});
const fetchSkills = async () => {
try {
setLoading(true);
const data = await getSkills();
setSkills(data);
setError(null);
} catch (err) {
setError("Failed to load skills.");
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSkills();
}, []);
const openModal = (skill = null) => {
if (skill) {
setEditingSkill(skill);
setFormData({
name: skill.name,
description: skill.description || '',
skill_type: skill.skill_type,
config: JSON.stringify(skill.config, null, 2),
is_system: skill.is_system,
group_id: skill.group_id || ''
});
} else {
setEditingSkill(null);
setFormData({
name: '',
description: '',
skill_type: 'local',
config: '{}',
is_system: false,
group_id: ''
});
}
setIsModalOpen(true);
};
const handleClone = (skill) => {
setEditingSkill(null); // Force it to act like a 'Create'
setFormData({
name: `${skill.name}_clone`,
description: skill.description || '',
skill_type: skill.skill_type,
config: JSON.stringify(skill.config, null, 2),
is_system: false, // cloned skills are never system by default
group_id: skill.group_id || ''
});
setIsModalOpen(true);
};
const closeModal = () => {
setIsModalOpen(false);
setEditingSkill(null);
};
const handleSave = async () => {
try {
let configObj = {};
try {
configObj = JSON.parse(formData.config);
} catch (e) {
alert("Invalid JSON in config");
return;
}
const payload = {
name: formData.name,
description: formData.description,
skill_type: formData.skill_type,
config: configObj,
group_id: formData.group_id || null,
is_system: formData.is_system
};
if (editingSkill) {
await updateSkill(editingSkill.id, payload);
} else {
await createSkill(payload);
}
closeModal();
fetchSkills();
} catch (err) {
alert("Error saving skill");
}
};
const handleDelete = async (id) => {
if (!window.confirm("Are you sure you want to delete this skill?")) return;
try {
await deleteSkill(id);
fetchSkills();
} catch (err) {
alert("Error deleting skill");
}
};
const isAdmin = user?.role === 'admin';
return (
<div className="flex flex-col h-full bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 overflow-hidden">
<div className="flex-none p-6 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 shadow-sm flex justify-between items-center z-10">
<div>
<h1 className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-indigo-500 to-purple-600">
Skills & Workflows
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Create, manage, and share AI capabilities and workflows.
</p>
</div>
<button
onClick={() => openModal()}
className="flex items-center space-x-2 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg font-medium transition-colors shadow-sm"
>
<Icon path="M12 5v14m-7-7h14" className="w-5 h-5" />
<span>Create Skill</span>
</button>
</div>
<div className="flex-grow p-6 overflow-y-auto">
{loading ? (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-500"></div>
</div>
) : error ? (
<div className="p-4 bg-red-100 text-red-700 rounded-lg">{error}</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{skills.map((skill) => (
<div key={skill.id} className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-5 shadow-sm hover:shadow-md transition-shadow flex flex-col">
<div className="flex justify-between items-start mb-2">
<h3 className="text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2">
{skill.name}
{skill.is_system && (
<span className="bg-yellow-100 text-yellow-800 text-xs px-2 py-0.5 rounded border border-yellow-200">System</span>
)}
{skill.group_id && !skill.is_system && (
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-0.5 rounded border border-blue-200">Group</span>
)}
</h3>
<div className="flex items-center space-x-2 text-gray-400">
<button onClick={() => handleClone(skill)} className="hover:text-green-500 transition-colors" title="Clone Skill">
<Icon path="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2" className="w-5 h-5" />
</button>
{(isAdmin || skill.owner_id === user?.id) && (
<>
<button onClick={() => openModal(skill)} className="hover:text-indigo-500 transition-colors" title="Edit Skill">
<Icon path="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" className="w-5 h-5" />
</button>
<button onClick={() => handleDelete(skill.id)} className="hover:text-red-500 transition-colors" title="Delete Skill">
<Icon path="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" className="w-5 h-5" />
</button>
</>
)}
</div>
</div>
<p className="text-gray-600 dark:text-gray-400 text-sm flex-grow mb-4">
{skill.description || "No description provided."}
</p>
<div className="mt-auto pt-4 border-t border-gray-100 dark:border-gray-700 flex justify-between items-center text-xs text-gray-500">
<span className="uppercase tracking-wider font-semibold bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
TYPE: {skill.skill_type}
</span>
<span>Created: {new Date(skill.created_at).toLocaleDateString()}</span>
</div>
</div>
))}
{skills.length === 0 && (
<div className="col-span-full p-8 text-center text-gray-500 bg-white dark:bg-gray-800 rounded-xl border border-dashed border-gray-300 dark:border-gray-700">
<Icon path="M12 2l-1 4h-4l3 3-1 4 3-2 3 2-1-4 3-3h-4z" className="w-12 h-12 mx-auto mb-3 text-gray-400" />
<p className="text-lg">No skills found.</p>
<p className="text-sm mt-1">Create your first skill to extend the AI's capabilities.</p>
</div>
)}
</div>
)}
</div>
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<div className="bg-white dark:bg-gray-800 rounded-2xl w-full max-w-2xl max-h-[90vh] flex flex-col shadow-2xl overflow-hidden border border-gray-200 dark:border-gray-700">
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center bg-gray-50 dark:bg-gray-800/50">
<h2 className="text-xl font-bold flex items-center gap-2">
<Icon path="M12 2l-1 4h-4l3 3-1 4 3-2 3 2-1-4 3-3h-4z" className="w-6 h-6 text-indigo-500" />
{editingSkill ? 'Edit Skill Configuration' : 'Create New Skill'}
</h2>
<button onClick={closeModal} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<Icon path="M6 18L18 6M6 6l12 12" className="w-6 h-6" />
</button>
</div>
<div className="p-6 overflow-y-auto space-y-6 flex-grow custom-scrollbar">
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 md:col-span-1">
<label className="block text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Skill Name</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all placeholder-gray-400"
placeholder="e.g. github_search"
/>
</div>
<div className="col-span-2 md:col-span-1">
<label className="block text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Skill Type</label>
<select
value={formData.skill_type}
onChange={(e) => setFormData({ ...formData, skill_type: e.target.value })}
className="w-full bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 outline-none transition-all"
>
<option value="local">Local Native</option>
<option value="mcp">MCP Protocol</option>
<option value="remote_grpc">Remote Node (gRPC)</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Description / Goal</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-3 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all min-h-[100px] resize-y placeholder-gray-400"
placeholder="Describe what this capability does..."
/>
</div>
<div>
<label className="block text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Configuration Payload (JSON)</label>
<textarea
value={formData.config}
onChange={(e) => setFormData({ ...formData, config: e.target.value })}
className="w-full bg-gray-900 text-green-400 font-mono text-sm border border-gray-700 rounded-lg px-4 py-3 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all min-h-[150px] resize-y"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Share with Group (Optional)</label>
<input
type="text"
value={formData.group_id}
onChange={(e) => setFormData({ ...formData, group_id: e.target.value })}
className="w-full bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg px-4 py-2 focus:ring-2 focus:ring-indigo-500 focus:border-transparent outline-none transition-all placeholder-gray-400"
placeholder="Enter Group ID"
/>
</div>
{isAdmin && (
<div className="flex items-center mt-8">
<input
type="checkbox"
id="isSystem"
checked={formData.is_system}
onChange={(e) => setFormData({ ...formData, is_system: e.target.checked })}
className="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"
/>
<label htmlFor="isSystem" className="ml-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
Mark as Core System Skill
</label>
</div>
)}
</div>
</div>
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 flex justify-end space-x-3">
<button
onClick={closeModal}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
className="px-6 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg shadow transition-colors flex items-center gap-2"
>
<Icon path="M5 13l4 4L19 7" className="w-4 h-4" />
{editingSkill ? 'Save Changes' : 'Create Skill'}
</button>
</div>
</div>
</div>
)}
</div>
);
}