Reachy_Mini / src /context /AppsContext.jsx
tfrere's picture
tfrere HF Staff
Add Express server with 24h cache + differentiate web/python apps
e8700c6
raw
history blame
4.05 kB
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;
}