import { useCallback, useEffect, useRef, useState } from "react"; import { Wrench, RefreshCw, X } from "lucide-react"; import { fetchDiscoveredSkills, toggleExecutionSkill, fetchSkillsCatalog, } from "../api"; import type { DiscoveredSkill, CatalogEntry } from "@fusion/dashboard"; import type { ToastType } from "../hooks/useToast"; interface SkillsViewProps { projectId?: string; addToast: (message: string, type?: ToastType) => void; onClose: () => void; } export interface DiscoveredSkillDisplay extends DiscoveredSkill { toggling?: boolean; } export function SkillsView({ projectId, addToast, onClose }: SkillsViewProps) { const [discoveredSkills, setDiscoveredSkills] = useState([]); const [isLoadingDiscovered, setIsLoadingDiscovered] = useState(true); const [isLoadingCatalog, setIsLoadingCatalog] = useState(false); const [catalogError, setCatalogError] = useState(null); const [catalogEntries, setCatalogEntries] = useState([]); const [searchQuery, setSearchQuery] = useState(""); // Debounce timer for catalog search const debounceRef = useRef | null>(null); const [debouncedQuery, setDebouncedQuery] = useState(""); // Fetch discovered skills const loadDiscoveredSkills = useCallback(async () => { setIsLoadingDiscovered(true); try { const skills = await fetchDiscoveredSkills(projectId); setDiscoveredSkills(skills); } catch (err) { const message = err instanceof Error ? err.message : "Failed to load discovered skills"; addToast(message, "error"); } finally { setIsLoadingDiscovered(false); } }, [projectId, addToast]); // Fetch catalog const loadCatalog = useCallback(async (query: string) => { setIsLoadingCatalog(true); setCatalogError(null); try { const result = await fetchSkillsCatalog(query, 20, projectId); setCatalogEntries(result.entries); } catch (err) { // Check for upstream error with code (502 etc.) const error = err as { error?: string; code?: string }; if (error.error && error.code) { setCatalogError("Catalog is temporarily unavailable. Please try again later."); } else { const message = err instanceof Error ? err.message : "Failed to load catalog"; setCatalogError(message); } } finally { setIsLoadingCatalog(false); } }, [projectId]); // Initial load useEffect(() => { void loadDiscoveredSkills(); void loadCatalog(""); }, [loadDiscoveredSkills, loadCatalog]); // Handle search input with debounce const handleSearchChange = useCallback((value: string) => { setSearchQuery(value); if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { setDebouncedQuery(value); }, 300); }, []); // Fetch catalog when debounced query changes useEffect(() => { void loadCatalog(debouncedQuery); }, [debouncedQuery, loadCatalog]); // Handle toggle skill const handleToggleSkill = useCallback(async (skillId: string, currentEnabled: boolean) => { const newEnabled = !currentEnabled; // Optimistic update setDiscoveredSkills((prev) => prev.map((s) => (s.id === skillId ? { ...s, toggling: true } : s)) ); try { await toggleExecutionSkill(skillId, newEnabled, projectId); // Update local state with new enabled value setDiscoveredSkills((prev) => prev.map((s) => (s.id === skillId ? { ...s, enabled: newEnabled, toggling: false } : s)) ); addToast(`Skill ${newEnabled ? "enabled" : "disabled"}`, "success"); } catch (err) { // Revert optimistic update setDiscoveredSkills((prev) => prev.map((s) => (s.id === skillId ? { ...s, toggling: false } : s)) ); const message = err instanceof Error ? err.message : "Failed to toggle skill"; addToast(`Failed to toggle skill: ${message}`, "error"); } }, [projectId, addToast]); return (
{/* Header */}

Skills

{discoveredSkills.length} discovered
{/* Discovered Skills Section */}

Discovered Skills

{isLoadingDiscovered ? (
Loading discovered skills...
) : discoveredSkills.length === 0 ? (

No skills discovered in this project.

) : (
{discoveredSkills.map((skill) => (
{skill.name} {skill.relativePath} {skill.metadata.source}
))}
)}
{/* Catalog Section */}

Skills Catalog

{/* Search */}
handleSearchChange(e.target.value)} aria-label="Search skills catalog" />
{/* Catalog Content */} {catalogError ? (

{catalogError}

) : isLoadingCatalog ? (
Loading catalog...
) : catalogEntries.length === 0 ? (
{searchQuery ? (

No skills match your search.

) : (

No skills available in the catalog.

)}
) : (
{catalogEntries.map((entry) => (

{entry.name}

{entry.description && (

{entry.description}

)} {entry.tags && entry.tags.length > 0 && (
{entry.tags.map((tag) => ( {tag} ))}
)} {entry.installs !== undefined && ( {entry.installs.toLocaleString()} installs )}
))}
)}
); }