// ======================================== // Skills Manager Page // ======================================== // Browse and manage skills library with search/filter import { useState, useMemo, useCallback } from 'react'; import { useIntl } from 'react-intl'; import { Sparkles, Search, Plus, RefreshCw, Power, PowerOff, Tag, ChevronDown, ChevronRight, EyeOff, List, Grid3x3, } from 'lucide-react'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/Select'; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel, } from '@/components/ui'; import { SkillCard, SkillDetailPanel } from '@/components/shared'; import { LocationSwitcher } from '@/components/commands/LocationSwitcher'; import { useSkills, useSkillMutations } from '@/hooks'; import { fetchSkillDetail } from '@/lib/api'; import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore'; import type { Skill } from '@/lib/api'; import { cn } from '@/lib/utils'; // ========== Skill Grid Component ========== interface SkillGridProps { skills: Skill[]; isLoading: boolean; onToggle: (skill: Skill, enabled: boolean) => void; onClick: (skill: Skill) => void; isToggling: boolean; compact?: boolean; } function SkillGrid({ skills, isLoading, onToggle, onClick, isToggling, compact }: SkillGridProps) { const { formatMessage } = useIntl(); if (isLoading) { return (
{[1, 2, 3, 4, 5, 6].map((i) => (
))}
); } if (skills.length === 0) { return (

{formatMessage({ id: 'skills.emptyState.title' })}

{formatMessage({ id: 'skills.emptyState.message' })}

); } return (
{skills.map((skill) => ( ))}
); } // ========== Main Page Component ========== export function SkillsManagerPage() { const { formatMessage } = useIntl(); const projectPath = useWorkflowStore(selectProjectPath); const [searchQuery, setSearchQuery] = useState(''); const [categoryFilter, setCategoryFilter] = useState('all'); const [sourceFilter, setSourceFilter] = useState('all'); const [enabledFilter, setEnabledFilter] = useState<'all' | 'enabled' | 'disabled'>('all'); const [viewMode, setViewMode] = useState<'grid' | 'compact'>('grid'); const [showDisabledSection, setShowDisabledSection] = useState(false); const [confirmDisable, setConfirmDisable] = useState<{ skill: Skill; enable: boolean } | null>(null); const [locationFilter, setLocationFilter] = useState<'project' | 'user'>('project'); // Skill detail panel state const [selectedSkill, setSelectedSkill] = useState(null); const [isDetailLoading, setIsDetailLoading] = useState(false); const [isDetailPanelOpen, setIsDetailPanelOpen] = useState(false); const { skills, categories, projectSkills, userSkills, isLoading, isFetching, refetch, } = useSkills({ filter: { search: searchQuery || undefined, category: categoryFilter !== 'all' ? categoryFilter : undefined, source: sourceFilter !== 'all' ? sourceFilter as Skill['source'] : undefined, enabledOnly: enabledFilter === 'enabled', location: locationFilter, }, }); const { toggleSkill, isToggling } = useSkillMutations(); // Filter skills based on enabled filter const filteredSkills = useMemo(() => { if (enabledFilter === 'disabled') { return skills.filter((s) => !s.enabled); } return skills; }, [skills, enabledFilter]); // Calculate counts based on current location filter (from skills, not allSkills) const currentLocationEnabledCount = useMemo(() => skills.filter(s => s.enabled).length, [skills]); const currentLocationTotalCount = skills.length; const currentLocationDisabledCount = currentLocationTotalCount - currentLocationEnabledCount; const handleToggle = async (skill: Skill, enabled: boolean) => { // Use the skill's location property const location = skill.location || 'project'; // Use folderName for API calls (actual folder name), fallback to name if not available const skillIdentifier = skill.folderName || skill.name; // Debug logging console.log('[SkillToggle] Toggling skill:', { name: skill.name, folderName: skill.folderName, location, enabled, skillIdentifier }); try { await toggleSkill(skillIdentifier, enabled, location); } catch (error) { console.error('[SkillToggle] Toggle failed:', error); throw error; } }; const handleToggleWithConfirm = (skill: Skill, enabled: boolean) => { if (!enabled) { // Show confirmation dialog when disabling setConfirmDisable({ skill, enable: false }); } else { // Enable directly without confirmation handleToggle(skill, true); } }; const handleConfirmDisable = async () => { if (confirmDisable) { await handleToggle(confirmDisable.skill, false); setConfirmDisable(null); } }; // Skill detail panel handlers const handleSkillClick = useCallback(async (skill: Skill) => { setIsDetailLoading(true); setIsDetailPanelOpen(true); setSelectedSkill(skill); try { // Fetch full skill details from API const data = await fetchSkillDetail( skill.name, skill.location || 'project', projectPath ); setSelectedSkill(data.skill); } catch (error) { console.error('Failed to fetch skill details:', error); // Keep the basic skill info if fetch fails } finally { setIsDetailLoading(false); } }, [projectPath]); const handleCloseDetailPanel = useCallback(() => { setIsDetailPanelOpen(false); setSelectedSkill(null); }, []); return (
{/* Page Header */}

{formatMessage({ id: 'skills.title' })}

{formatMessage({ id: 'skills.description' })}

{/* Location Switcher */}
{/* Stats Cards */}
{currentLocationTotalCount}

{formatMessage({ id: 'common.stats.totalSkills' })}

{currentLocationEnabledCount}

{formatMessage({ id: 'skills.state.enabled' })}

{currentLocationDisabledCount}

{formatMessage({ id: 'skills.state.disabled' })}

{categories.length}

{formatMessage({ id: 'skills.card.category' })}

{/* Filters and Search */}
setSearchQuery(e.target.value)} className="pl-9" />
{/* Quick Filters */}
{/* Skills Grid */} {/* Disabled Skills Section */} {enabledFilter === 'all' && currentLocationDisabledCount > 0 && (
{showDisabledSection && ( !s.enabled)} isLoading={false} onToggle={handleToggleWithConfirm} onClick={handleSkillClick} isToggling={isToggling || !!confirmDisable} compact={true} /> )}
)} {/* Disable Confirmation Dialog */} !open && setConfirmDisable(null)}> {formatMessage({ id: 'skills.disableConfirm.title' })} {formatMessage( { id: 'skills.disableConfirm.message' }, { name: confirmDisable?.skill.name || '' } )} {formatMessage({ id: 'skills.actions.cancel' })} {formatMessage({ id: 'skills.actions.confirmDisable' })} {/* Skill Detail Panel */}
); } export default SkillsManagerPage;