tfrere HF Staff Cursor commited on
Commit
4d77ab0
Β·
1 Parent(s): 6d63e8c

Refactor API to desktop-compatible format

Browse files

- Add url, source_kind, downloads, runtime fields
- Move metadata into extra object structure
- Update frontend to use new extra.* accessors
- Enables desktop app to use website API directly

Co-authored-by: Cursor <cursoragent@cursor.com>

server/index.js CHANGED
@@ -22,6 +22,7 @@ let appsCache = {
22
  };
23
 
24
  // Fetch apps from HuggingFace API
 
25
  async function fetchAppsFromHF() {
26
  console.log('[Cache] Fetching apps from HuggingFace API...');
27
 
@@ -43,24 +44,43 @@ async function fetchAppsFromHF() {
43
  const allSpaces = await spacesResponse.json();
44
  console.log(`[Cache] Fetched ${allSpaces.length} spaces from HuggingFace`);
45
 
46
- // 3. Build apps list with isOfficial and isPythonApp flags
47
  const allApps = allSpaces.map(space => {
48
  const spaceId = space.id || '';
49
  const tags = space.tags || [];
50
  const isOfficial = officialSet.has(spaceId.toLowerCase());
51
  const isPythonApp = tags.includes('reachy_mini_python_app');
 
 
52
 
53
  return {
 
54
  id: spaceId,
55
- name: spaceId.split('/').pop(),
56
  description: space.cardData?.short_description || '',
57
- cardData: space.cardData || {},
58
- likes: space.likes || 0,
59
- lastModified: space.lastModified,
60
- author: spaceId.split('/')[0],
61
  isOfficial,
62
- isPythonApp,
63
- tags,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  };
65
  });
66
 
@@ -69,7 +89,7 @@ async function fetchAppsFromHF() {
69
  if (a.isOfficial !== b.isOfficial) {
70
  return a.isOfficial ? -1 : 1;
71
  }
72
- return (b.likes || 0) - (a.likes || 0);
73
  });
74
 
75
  return allApps;
 
22
  };
23
 
24
  // Fetch apps from HuggingFace API
25
+ // Returns format compatible with desktop app (with url, source_kind, extra)
26
  async function fetchAppsFromHF() {
27
  console.log('[Cache] Fetching apps from HuggingFace API...');
28
 
 
44
  const allSpaces = await spacesResponse.json();
45
  console.log(`[Cache] Fetched ${allSpaces.length} spaces from HuggingFace`);
46
 
47
+ // 3. Build apps list in desktop-compatible format
48
  const allApps = allSpaces.map(space => {
49
  const spaceId = space.id || '';
50
  const tags = space.tags || [];
51
  const isOfficial = officialSet.has(spaceId.toLowerCase());
52
  const isPythonApp = tags.includes('reachy_mini_python_app');
53
+ const author = spaceId.split('/')[0];
54
+ const name = spaceId.split('/').pop();
55
 
56
  return {
57
+ // Core fields (used by both website and desktop)
58
  id: spaceId,
59
+ name,
60
  description: space.cardData?.short_description || '',
61
+ url: `https://huggingface.co/spaces/${spaceId}`,
62
+ source_kind: 'hf_space',
 
 
63
  isOfficial,
64
+
65
+ // Extra metadata (desktop-compatible structure)
66
+ extra: {
67
+ id: spaceId,
68
+ author,
69
+ likes: space.likes || 0,
70
+ downloads: space.downloads || 0,
71
+ lastModified: space.lastModified,
72
+ runtime: space.runtime || null,
73
+ tags,
74
+ isPythonApp,
75
+ cardData: {
76
+ emoji: space.cardData?.emoji || (isPythonApp ? 'πŸ“¦' : '🌐'),
77
+ short_description: space.cardData?.short_description || '',
78
+ sdk: space.cardData?.sdk || null,
79
+ tags: space.cardData?.tags || [],
80
+ // Preserve other cardData fields
81
+ ...space.cardData,
82
+ },
83
+ },
84
  };
85
  });
86
 
 
89
  if (a.isOfficial !== b.isOfficial) {
90
  return a.isOfficial ? -1 : 1;
91
  }
92
+ return (b.extra.likes || 0) - (a.extra.likes || 0);
93
  });
94
 
95
  return allApps;
src/components/InstallModal.jsx CHANGED
@@ -29,16 +29,16 @@ function InstallModal({ open, onClose, app }) {
29
  if (!app) return null;
30
 
31
  const appName = app.name || app.id?.split('/').pop();
32
- const cardData = app.cardData || {};
33
  const emoji = cardData.emoji || 'πŸ“¦';
34
  const description = cardData.short_description || app.description || 'No description';
35
  const deepLinkUrl = `reachymini://install/${appName}`;
36
- const spaceUrl = `https://huggingface.co/spaces/${app.id}`;
37
 
38
- const author = app.id?.split('/')?.[0] || app.author || null;
39
  const isOfficial = app.isOfficial;
40
- const likes = app.likes || 0;
41
- const lastModified = app.lastModified || app.createdAt || null;
42
  const formattedDate = lastModified
43
  ? new Date(lastModified).toLocaleDateString('en-US', {
44
  month: 'short',
 
29
  if (!app) return null;
30
 
31
  const appName = app.name || app.id?.split('/').pop();
32
+ const cardData = app.extra?.cardData || {};
33
  const emoji = cardData.emoji || 'πŸ“¦';
34
  const description = cardData.short_description || app.description || 'No description';
35
  const deepLinkUrl = `reachymini://install/${appName}`;
36
+ const spaceUrl = app.url || `https://huggingface.co/spaces/${app.id}`;
37
 
38
+ const author = app.extra?.author || app.id?.split('/')?.[0] || null;
39
  const isOfficial = app.isOfficial;
40
+ const likes = app.extra?.likes || 0;
41
+ const lastModified = app.extra?.lastModified || null;
42
  const formattedDate = lastModified
43
  ? new Date(lastModified).toLocaleDateString('en-US', {
44
  month: 'short',
src/context/AppsContext.jsx CHANGED
@@ -11,6 +11,7 @@ const FALLBACK_OFFICIAL_URL = 'https://huggingface.co/datasets/pollen-robotics/r
11
  const FALLBACK_LIMIT = 1000;
12
 
13
  // Fallback: fetch directly from HuggingFace API (for dev mode)
 
14
  async function fetchAppsDirectFromHF() {
15
  console.log('[AppsContext] Fallback: fetching directly from HuggingFace API');
16
 
@@ -30,24 +31,42 @@ async function fetchAppsDirectFromHF() {
30
  }
31
  const allSpaces = await spacesResponse.json();
32
 
33
- // Build apps list
34
  const allApps = allSpaces.map(space => {
35
  const spaceId = space.id || '';
36
  const tags = space.tags || [];
37
  const isOfficial = officialSet.has(spaceId.toLowerCase());
38
  const isPythonApp = tags.includes('reachy_mini_python_app');
 
 
39
 
40
  return {
 
41
  id: spaceId,
42
- name: spaceId.split('/').pop(),
43
  description: space.cardData?.short_description || '',
44
- cardData: space.cardData || {},
45
- likes: space.likes || 0,
46
- lastModified: space.lastModified,
47
- author: spaceId.split('/')[0],
48
  isOfficial,
49
- isPythonApp,
50
- tags,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  };
52
  });
53
 
@@ -56,7 +75,7 @@ async function fetchAppsDirectFromHF() {
56
  if (a.isOfficial !== b.isOfficial) {
57
  return a.isOfficial ? -1 : 1;
58
  }
59
- return (b.likes || 0) - (a.likes || 0);
60
  });
61
 
62
  return allApps;
 
11
  const FALLBACK_LIMIT = 1000;
12
 
13
  // Fallback: fetch directly from HuggingFace API (for dev mode)
14
+ // Returns same format as server (desktop-compatible)
15
  async function fetchAppsDirectFromHF() {
16
  console.log('[AppsContext] Fallback: fetching directly from HuggingFace API');
17
 
 
31
  }
32
  const allSpaces = await spacesResponse.json();
33
 
34
+ // Build apps list in desktop-compatible format
35
  const allApps = allSpaces.map(space => {
36
  const spaceId = space.id || '';
37
  const tags = space.tags || [];
38
  const isOfficial = officialSet.has(spaceId.toLowerCase());
39
  const isPythonApp = tags.includes('reachy_mini_python_app');
40
+ const author = spaceId.split('/')[0];
41
+ const name = spaceId.split('/').pop();
42
 
43
  return {
44
+ // Core fields
45
  id: spaceId,
46
+ name,
47
  description: space.cardData?.short_description || '',
48
+ url: `https://huggingface.co/spaces/${spaceId}`,
49
+ source_kind: 'hf_space',
 
 
50
  isOfficial,
51
+
52
+ // Extra metadata (desktop-compatible structure)
53
+ extra: {
54
+ id: spaceId,
55
+ author,
56
+ likes: space.likes || 0,
57
+ downloads: space.downloads || 0,
58
+ lastModified: space.lastModified,
59
+ runtime: space.runtime || null,
60
+ tags,
61
+ isPythonApp,
62
+ cardData: {
63
+ emoji: space.cardData?.emoji || (isPythonApp ? 'πŸ“¦' : '🌐'),
64
+ short_description: space.cardData?.short_description || '',
65
+ sdk: space.cardData?.sdk || null,
66
+ tags: space.cardData?.tags || [],
67
+ ...space.cardData,
68
+ },
69
+ },
70
  };
71
  });
72
 
 
75
  if (a.isOfficial !== b.isOfficial) {
76
  return a.isOfficial ? -1 : 1;
77
  }
78
+ return (b.extra.likes || 0) - (a.extra.likes || 0);
79
  });
80
 
81
  return allApps;
src/pages/Apps.jsx CHANGED
@@ -28,13 +28,13 @@ import InstallModal from '../components/InstallModal';
28
  // App Card Component
29
  function AppCard({ app, onInstallClick }) {
30
  const isOfficial = app.isOfficial;
31
- const isPythonApp = app.isPythonApp !== false; // Default to true for backwards compatibility
32
- const cardData = app.cardData || {};
33
- const author = app.id?.split('/')?.[0] || app.author || null;
34
- const likes = app.likes || 0;
35
- const lastModified = app.lastModified || app.createdAt || null;
36
  const emoji = cardData.emoji || (isPythonApp ? 'πŸ“¦' : '🌐');
37
- const spaceUrl = `https://huggingface.co/spaces/${app.id}`;
38
 
39
  const formattedDate = lastModified
40
  ? new Date(lastModified).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
@@ -327,7 +327,7 @@ export default function Apps() {
327
  app.name?.toLowerCase().includes(query) ||
328
  app.id?.toLowerCase().includes(query) ||
329
  app.description?.toLowerCase().includes(query) ||
330
- app.cardData?.short_description?.toLowerCase().includes(query)
331
  );
332
  }
333
 
 
28
  // App Card Component
29
  function AppCard({ app, onInstallClick }) {
30
  const isOfficial = app.isOfficial;
31
+ const isPythonApp = app.extra?.isPythonApp !== false; // Default to true for backwards compatibility
32
+ const cardData = app.extra?.cardData || {};
33
+ const author = app.extra?.author || app.id?.split('/')?.[0] || null;
34
+ const likes = app.extra?.likes || 0;
35
+ const lastModified = app.extra?.lastModified || null;
36
  const emoji = cardData.emoji || (isPythonApp ? 'πŸ“¦' : '🌐');
37
+ const spaceUrl = app.url || `https://huggingface.co/spaces/${app.id}`;
38
 
39
  const formattedDate = lastModified
40
  ? new Date(lastModified).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
 
327
  app.name?.toLowerCase().includes(query) ||
328
  app.id?.toLowerCase().includes(query) ||
329
  app.description?.toLowerCase().includes(query) ||
330
+ app.extra?.cardData?.short_description?.toLowerCase().includes(query)
331
  );
332
  }
333