Spaces:
Running
Running
| 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 ( | |
| <AppsContext.Provider value={{ apps, loading, error }}> | |
| {children} | |
| </AppsContext.Provider> | |
| ); | |
| } | |
| // 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; | |
| } | |