File size: 4,048 Bytes
5c85958
 
 
 
 
e8700c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c85958
e8700c6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c85958
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8700c6
 
 
 
 
 
 
 
 
 
 
5c85958
e8700c6
 
 
 
 
 
5c85958
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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;
}