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) // Returns same format as server (desktop-compatible) 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 in desktop-compatible format 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'); const author = spaceId.split('/')[0]; const name = spaceId.split('/').pop(); return { // Core fields id: spaceId, name, description: space.cardData?.short_description || '', url: `https://huggingface.co/spaces/${spaceId}`, source_kind: 'hf_space', isOfficial, // Extra metadata (desktop-compatible structure) extra: { id: spaceId, author, likes: space.likes || 0, downloads: space.downloads || 0, lastModified: space.lastModified, runtime: space.runtime || null, tags, isPythonApp, cardData: { emoji: space.cardData?.emoji || (isPythonApp ? '📦' : '🌐'), short_description: space.cardData?.short_description || '', sdk: space.cardData?.sdk || null, tags: space.cardData?.tags || [], ...space.cardData, }, }, }; }); // Sort: official first, then by likes allApps.sort((a, b) => { if (a.isOfficial !== b.isOfficial) { return a.isOfficial ? -1 : 1; } return (b.extra.likes || 0) - (a.extra.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; }