Newer
Older
cortex-hub / frontend / src / features / skills / pages / SkillsPage.js
import React, { useState, useEffect, useMemo } from 'react';
import SkillsPageContent from '../components/SkillsPageContent';
import { getSkills, createSkill, updateSkill, deleteSkill, getSkillFiles, getSkillFileContent, saveSkillFile, deleteSkillFile } from '../../../services/apiService';

export default function SkillsPage({ user, Icon }) {
    const [skills, setSkills] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [searchQuery, setSearchQuery] = useState('');
    const [activeFilter, setActiveFilter] = useState('all'); // all, system, mine, group
    const [showSystemSkills, setShowSystemSkills] = useState(false);
    const [errorModalMessage, setErrorModalMessage] = useState(null);
    const [confirmDeleteId, setConfirmDeleteId] = useState(null);

    const [isModalOpen, setIsModalOpen] = useState(false);
    const [editingSkill, setEditingSkill] = useState(null);

    const [formData, setFormData] = useState({
        name: '',
        description: '',
        skill_type: 'local',
        is_system: false,
        is_enabled: true,
        features: ['swarm_control'],
        extra_metadata: { emoji: '⚙️' }
    });
    // VFS State
    const [skillFiles, setSkillFiles] = useState([]);
    const [activeFile, setActiveFile] = useState(null);
    const [activeFileContent, setActiveFileContent] = useState('');
    const [isSavingFile, setIsSavingFile] = useState(false);

    const loadFiles = async (skillId) => {
        try {
            const files = await getSkillFiles(skillId);
            setSkillFiles(files);
            if (files.some(f => f.path === 'SKILL.md')) {
                loadContent(skillId, 'SKILL.md');
            } else if (files.length > 0) {
                loadContent(skillId, files[0].path);
            } else {
                setActiveFile(null);
                setActiveFileContent('');
            }
        } catch (e) {
            console.error("Failed to load files", e);
        }
    };

    const loadContent = async (skillId, path) => {
        try {
            const file = await getSkillFileContent(skillId, path);
            setActiveFile(path);
            setActiveFileContent(file.content || '');
        } catch (e) {
            console.error("Failed to load content", e);
        }
    };

    const handleSelectFile = (path) => {
        if (!editingSkill) return;
        loadContent(editingSkill.id, path);
    };

    const handleAddFile = async (path) => {
        if (!editingSkill) return;
        try {
            await saveSkillFile(editingSkill.id, path, '');
            await loadFiles(editingSkill.id);
            loadContent(editingSkill.id, path);
        } catch (e) {
            setErrorModalMessage("Failed to create file.");
        }
    };

    const handleSaveActiveFile = async (contentOverride = null) => {
        if (!activeFile || !editingSkill) return;
        setIsSavingFile(true);
        try {
            const contentToSave = typeof contentOverride === 'string' ? contentOverride : activeFileContent;
            await saveSkillFile(editingSkill.id, activeFile, contentToSave);
            // Also explicitly ensure local state is updated just in case it was called directly via override
            if (typeof contentOverride === 'string') {
                setActiveFileContent(contentOverride);
            }
        } catch (err) {
            setErrorModalMessage(`Failed to save ${activeFile}. Check permissions.`);
        } finally {
            setIsSavingFile(false);
        }
    };

    const handleDeleteFile = async (path) => {
        if (!editingSkill) return;
        try {
            await deleteSkillFile(editingSkill.id, path);
            if (activeFile === path) {
                setActiveFile(null);
                setActiveFileContent('');
            }
            await loadFiles(editingSkill.id);
        } catch (e) {
            setErrorModalMessage("Failed to delete file.");
        }
    };

    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 filteredSkills = useMemo(() => {
        return skills.filter(skill => {
            const matchesSearch = skill.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
                (skill.description || '').toLowerCase().includes(searchQuery.toLowerCase());

            if (!matchesSearch) return false;
            if (!showSystemSkills && skill.is_system && activeFilter !== 'system') return false;

            if (activeFilter === 'all') return true;
            if (activeFilter === 'system') return skill.is_system;
            if (activeFilter === 'mine') return !skill.is_system && skill.owner_id === user?.id;
            if (activeFilter === 'group') return skill.group_id && !skill.is_system;
            return true;
        });
    }, [skills, searchQuery, activeFilter, user, showSystemSkills]);

    const stats = useMemo(() => ({
        total: skills.filter(s => showSystemSkills || activeFilter === 'system' || !s.is_system).length,
        system: skills.filter(s => s.is_system).length,
        mine: skills.filter(s => !s.is_system && s.owner_id === user?.id).length,
        group: skills.filter(s => s.group_id).length,
        enabled: skills.filter(s => s.is_enabled).length
    }), [skills, user, showSystemSkills, activeFilter]);

    const openModal = (skill = null) => {
        if (skill) {
            setEditingSkill(skill);
            if (skill.id) loadFiles(skill.id);
            setFormData({
                name: skill.name,
                description: skill.description || '',
                skill_type: skill.skill_type,
                is_system: skill.is_system,
                is_enabled: skill.is_enabled ?? true,
                features: skill.features || ['swarm_control'],
                extra_metadata: skill.extra_metadata || { emoji: '⚙️' }
            });
        } else {
            setEditingSkill(null);
            setSkillFiles([]);
            setActiveFile(null);
            setActiveFileContent('');
            setFormData({
                name: '',
                description: '',
                skill_type: 'local',
                is_system: false,
                is_enabled: true,
                features: ['swarm_control'],
                extra_metadata: { emoji: '⚙️' }
            });
        }
        setIsModalOpen(true);
    };

    const handleClone = (skill) => {
        setEditingSkill(null);
        setFormData({
            name: `${skill.name}_clone`,
            description: skill.description || '',
            skill_type: skill.skill_type,
            is_system: false,
            is_enabled: true,
            features: skill.features || ['swarm_control'],
            extra_metadata: skill.extra_metadata || { emoji: '⚙️' }
        });
        setIsModalOpen(true);
    };

    const closeModal = () => {
        setIsModalOpen(false);
        setEditingSkill(null);
    };

    const handleSave = async () => {
        try {
            const payload = { ...formData };

            if (editingSkill) {
                await updateSkill(editingSkill.id, payload);
                fetchSkills(); // refresh the list softly
            } else {
                const newSkill = await createSkill(payload);
                await fetchSkills();
                // Close the creation modal overlay, but immediately open the IDE on the new folder
                openModal(newSkill); 
            }
        } catch (err) {
            setErrorModalMessage("Error saving folder. Please check your permissions and try again.");
        }
    };

    const handleDelete = (id) => {
        setConfirmDeleteId(id);
    };

    const confirmDelete = async () => {
        if (!confirmDeleteId) return;
        try {
            await deleteSkill(confirmDeleteId);
            setConfirmDeleteId(null);
            fetchSkills();
        } catch (err) {
            setErrorModalMessage("Error deleting skill. It might be in use or you may lack permissions.");
            setConfirmDeleteId(null);
        }
    };

    const isAdmin = user?.role === 'admin';

    const context = {
        user,
        Icon,
        loading,
        error,
        searchQuery,
        setSearchQuery,
        activeFilter,
        setActiveFilter,
        showSystemSkills,
        setShowSystemSkills,
        isModalOpen,
        openModal,
        closeModal,
        handleClone,
        handleSave,
        handleDelete,
        isAdmin,
        filteredSkills,
        stats,
        editingSkill,
        formData,
        setFormData,
        errorModalMessage,
        setErrorModalMessage,
        confirmDeleteId,
        setConfirmDeleteId,
        confirmDelete,
        skillFiles,
        activeFile,
        activeFileContent,
        setActiveFileContent,
        isSavingFile,
        handleSelectFile,
        handleAddFile,
        handleSaveActiveFile,
        handleDeleteFile
    };

    return <SkillsPageContent context={context} />;
}