'use client'; import { useState, useEffect, useCallback } from 'react'; import { playUISound } from '@/lib/ui-audio'; interface PackInfo { id: string; publisher: string; name: string; description?: string; version?: string; } interface RegistryPack { id: string; name: string; publisher: string; description: string; stars: number; updatedAt: string; } type PackAction = 'idle' | 'installing' | 'updating' | 'removing'; export function PacksPanel() { const [installed, setInstalled] = useState([]); const [registry, setRegistry] = useState([]); const [actions, setActions] = useState>({}); const [registryError, setRegistryError] = useState(false); const fetchInstalled = useCallback(() => { fetch('/api/packs').then(r => r.json()).then(setInstalled).catch(console.error); }, []); useEffect(() => { fetchInstalled(); // Fetch registry index fetch('https://rohenaz.github.io/agentcraft-registry/index.json') .then(r => r.json()) .then(setRegistry) .catch(() => setRegistryError(true)); }, [fetchInstalled]); const setAction = (id: string, action: PackAction) => setActions(prev => ({ ...prev, [id]: action })); const handleInstall = async (id: string) => { setAction(id, 'installing'); try { const r = await fetch('/api/packs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ repo: id }) }); if (!r.ok) throw new Error(); playUISound('confirm', 0.5); fetchInstalled(); } catch { playUISound('error', 0.5); } setAction(id, 'idle'); }; const handleUpdate = async (id: string) => { setAction(id, 'updating'); try { const r = await fetch('/api/packs', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ repo: id }) }); if (!r.ok) throw new Error(); playUISound('confirm', 0.4); } catch { playUISound('error', 0.5); } setAction(id, 'idle'); }; const handleRemove = async (id: string) => { setAction(id, 'removing'); try { const r = await fetch('/api/packs', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ repo: id }) }); if (!r.ok) throw new Error(); fetchInstalled(); } catch { playUISound('error', 0.5); } setAction(id, 'idle'); }; const installedIds = new Set(installed.map(p => p.id)); const browsePacks = registry.filter(p => !installedIds.has(p.id)); const btnStyle = (active: boolean, danger = false) => ({ border: `1px solid ${danger ? 'rgba(255,80,80,0.4)' : active ? 'var(--sf-cyan)' : 'var(--sf-border)'}`, color: danger ? 'rgba(255,80,80,0.7)' : active ? 'var(--sf-cyan)' : 'rgba(255,255,255,0.4)', backgroundColor: 'transparent', padding: '2px 8px', fontSize: '9px', fontFamily: 'inherit', letterSpacing: '0.08em', textTransform: 'uppercase' as const, cursor: 'pointer', transition: 'all 0.15s', }); return (
{/* Installed */}
INSTALLED PACKS
{installed.length === 0 && (
NO PACKS INSTALLED
)} {installed.map(pack => { const action = actions[pack.id] ?? 'idle'; return (
{pack.name}
{pack.publisher}{pack.version ? ` · v${pack.version}` : ''}
{pack.description && (
{pack.description}
)}
); })} {/* Browse */}
BROWSE PACKS
{registryError && (
REGISTRY UNAVAILABLE
)} {!registryError && browsePacks.length === 0 && registry.length > 0 && (
ALL AVAILABLE PACKS INSTALLED
)} {browsePacks.map(pack => { const action = actions[pack.id] ?? 'idle'; return (
{pack.name} {pack.stars > 0 && ( ★ {pack.stars} )}
{pack.publisher}
{pack.description}
); })}
); }