import { createContext, useContext, useState, useEffect } from 'react'; // Context const AppsContext = createContext(null); // API endpoint (uses server cache in production, falls back to direct HF API in dev) const API_ENDPOINT = '/api/apps'; const FALLBACK_HF_API = 'https://huggingface.co/api/spaces'; const FALLBACK_OFFICIAL_URL = 'https://huggingface.co/datasets/pollen-robotics/reachy-mini-official-app-store/raw/main/app-list.json'; // Note: HF API doesn't support pagination with filter=, so we use a high limit const FALLBACK_LIMIT = 1000; // Fallback: fetch directly from HuggingFace API (for dev mode) async function fetchAppsDirectFromHF() { console.log('[AppsContext] Fallback: fetching directly from HuggingFace API'); // Fetch official app IDs const officialResponse = await fetch(FALLBACK_OFFICIAL_URL); let officialIdList = []; if (officialResponse.ok) { officialIdList = await officialResponse.json(); } const officialSet = new Set(officialIdList.map(id => id.toLowerCase())); // Fetch all spaces // Note: HF API doesn't support pagination with filter=, so we use a high limit const spacesResponse = await fetch(`${FALLBACK_HF_API}?filter=reachy_mini&full=true&limit=${FALLBACK_LIMIT}`); if (!spacesResponse.ok) { throw new Error(`HF API returned ${spacesResponse.status}`); } const allSpaces = await spacesResponse.json(); // Build apps list const allApps = allSpaces.map(space => { const spaceId = space.id || ''; const tags = space.tags || []; const isOfficial = officialSet.has(spaceId.toLowerCase()); const isPythonApp = tags.includes('reachy_mini_python_app'); return { id: spaceId, name: spaceId.split('/').pop(), description: space.cardData?.short_description || '', cardData: space.cardData || {}, likes: space.likes || 0, lastModified: space.lastModified, author: spaceId.split('/')[0], isOfficial, isPythonApp, tags, }; }); // Sort: official first, then by likes allApps.sort((a, b) => { if (a.isOfficial !== b.isOfficial) { return a.isOfficial ? -1 : 1; } return (b.likes || 0) - (a.likes || 0); }); return allApps; } // Provider component export function AppsProvider({ children }) { const [apps, setApps] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [hasFetched, setHasFetched] = useState(false); useEffect(() => { // Only fetch once if (hasFetched) return; async function fetchApps() { setLoading(true); setError(null); try { let allApps; // Try server cache first try { const response = await fetch(API_ENDPOINT); if (response.ok) { const data = await response.json(); allApps = data.apps; console.log(`[AppsContext] Fetched ${allApps.length} apps from server cache (age: ${data.cacheAge}s)`); } else { throw new Error('Server API not available'); } } catch (serverErr) { // Fallback to direct HF API (for dev mode or if server fails) console.log('[AppsContext] Server cache unavailable, using direct API'); allApps = await fetchAppsDirectFromHF(); console.log(`[AppsContext] Fetched ${allApps.length} apps directly from HuggingFace`); } setApps(allApps); setHasFetched(true); } catch (err) { console.error('Failed to fetch apps:', err); setError('Failed to load apps. Please try again later.'); } finally { setLoading(false); } } fetchApps(); }, [hasFetched]); return ( {children} ); } // Hook to use the apps context export function useApps() { const context = useContext(AppsContext); if (!context) { throw new Error('useApps must be used within an AppsProvider'); } return context; }