Spaces:
Running
Running
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 +29 -9
- src/components/InstallModal.jsx +5 -5
- src/context/AppsContext.jsx +28 -9
- src/pages/Apps.jsx +7 -7
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
|
| 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
|
| 56 |
description: space.cardData?.short_description || '',
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
lastModified: space.lastModified,
|
| 60 |
-
author: spaceId.split('/')[0],
|
| 61 |
isOfficial,
|
| 62 |
-
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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] ||
|
| 39 |
const isOfficial = app.isOfficial;
|
| 40 |
-
const likes = app.likes || 0;
|
| 41 |
-
const lastModified = app.lastModified ||
|
| 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
|
| 43 |
description: space.cardData?.short_description || '',
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
lastModified: space.lastModified,
|
| 47 |
-
author: spaceId.split('/')[0],
|
| 48 |
isOfficial,
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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] ||
|
| 34 |
-
const likes = app.likes || 0;
|
| 35 |
-
const lastModified = app.lastModified ||
|
| 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 |
|