openfree commited on
Commit
9db20d1
ยท
verified ยท
1 Parent(s): 71ad306

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +575 -187
app.py CHANGED
@@ -32,8 +32,7 @@ def generate_dummy_spaces(count):
32
  # Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
33
  def fetch_trending_spaces(offset=0, limit=72):
34
  """
35
- hardware=cpu ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ
36
- GPU๊ฐ€ ์—†๋Š”(=CPU ์ „์šฉ) ์ŠคํŽ˜์ด์Šค๋งŒ ํ•„ํ„ฐ๋ง
37
  """
38
  try:
39
  url = "https://huggingface.co/api/spaces"
@@ -93,6 +92,70 @@ def fetch_trending_spaces(offset=0, limit=72):
93
  'all_spaces': generate_dummy_spaces(500)
94
  }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  # Transform Huggingface URL to direct space URL
97
  def transform_url(owner, name):
98
  """
@@ -112,9 +175,8 @@ def transform_url(owner, name):
112
  # Get space details
113
  def get_space_details(space_data, index, offset):
114
  """
115
- - description, avatar_url, author_name ๋“ฑ ์ถ”๊ฐ€ ํ•„๋“œ๋ฅผ ์ถ”์ถœ
116
- - rank๋Š” ํ˜„์žฌ ํŽ˜์ด์ง€ ๋‚ด offset ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณ„์‚ฐ
117
- (๋‹จ, Top 500 ๊ณ„์‚ฐ์€ global_rank ์‚ฌ์šฉ)
118
  """
119
  try:
120
  if '/' in space_data.get('id', ''):
@@ -140,7 +202,7 @@ def get_space_details(space_data, index, offset):
140
  # Description
141
  short_desc = space_data.get('description', '')
142
 
143
- # User info (avatar, name)
144
  user_info = space_data.get('user', {})
145
  avatar_url = user_info.get('avatar_url', '')
146
  author_name = user_info.get('name') or owner
@@ -155,11 +217,11 @@ def get_space_details(space_data, index, offset):
155
  'description': short_desc,
156
  'avatar_url': avatar_url,
157
  'author_name': author_name,
158
- 'rank': offset + index + 1 # ํ˜„์žฌ ํŽ˜์ด์ง€์—์„œ์˜ ํ‘œ์‹œ์šฉ ๋žญํฌ
159
  }
160
  except Exception as e:
161
  print(f"Error processing space data: {e}")
162
- # Return basic object even if error occurs
163
  return {
164
  'url': 'https://huggingface.co/spaces',
165
  'embedUrl': 'https://huggingface.co/spaces',
@@ -173,7 +235,7 @@ def get_space_details(space_data, index, offset):
173
  'rank': offset + index + 1
174
  }
175
 
176
- # Get owner statistics from all spaces
177
  def get_owner_stats(all_spaces):
178
  """
179
  ์ƒ์œ„ 500์œ„(global_rank <= 500) ์ด๋‚ด์— ๋ฐฐ์น˜๋œ ์ŠคํŽ˜์ด์Šค๋“ค์˜ owner๋ฅผ ์ถ”์ถœํ•ด,
@@ -206,15 +268,15 @@ def home():
206
  """
207
  return render_template('index.html')
208
 
209
- # Zero-GPU spaces API
210
  @app.route('/api/trending-spaces', methods=['GET'])
211
  def trending_spaces():
212
  """
213
- hardware=cpu ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์™€ ๊ฒ€์ƒ‰, ํŽ˜์ด์ง•, ํ†ต๊ณ„ ๋“ฑ์„ ์ ์šฉ
214
  """
215
  search_query = request.args.get('search', '').lower()
216
  offset = int(request.args.get('offset', 0))
217
- limit = int(request.args.get('limit', 72)) # Default 72
218
 
219
  # Fetch zero-gpu (cpu) spaces
220
  spaces_data = fetch_trending_spaces(offset, limit)
@@ -226,7 +288,7 @@ def trending_spaces():
226
  if not space_info:
227
  continue
228
 
229
- # Apply search filter if needed
230
  if search_query:
231
  if (search_query not in space_info['title'].lower()
232
  and search_query not in space_info['owner'].lower()
@@ -236,7 +298,7 @@ def trending_spaces():
236
 
237
  results.append(space_info)
238
 
239
- # Get owner statistics (Top 500 โ†’ Top 30)
240
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
241
 
242
  return jsonify({
@@ -247,6 +309,41 @@ def trending_spaces():
247
  'top_owners': top_owners
248
  })
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  if __name__ == '__main__':
251
  """
252
  ์„œ๋ฒ„ ๊ตฌ๋™ ์‹œ, templates/index.html ํŒŒ์ผ์„ ์ƒ์„ฑ ํ›„ Flask ์‹คํ–‰
@@ -826,8 +923,7 @@ if __name__ == '__main__':
826
  }
827
 
828
  /* ์ถ”๊ฐ€ ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ •(์•„๋ฐ”ํƒ€, ZERO GPU ๋ฑƒ์ง€ ๋“ฑ)์„ ์œ„ํ•ด
829
- ์•„๋ž˜ ํด๋ž˜์Šค๋“ค์„ ์ผ๋ถ€ ์ถ”๊ฐ€/์ˆ˜์ •ํ•ด๋„ ์ข‹์ง€๋งŒ
830
- ์˜ˆ์‹œ๋Š” ์œ„ ์˜์—ญ์—์„œ ์ด๋ฏธ ์ถฉ๋ถ„ํžˆ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์ƒ๋žต */
831
 
832
  /* ๋‹ค์Œ ๋ถ€๋ถ„์€ Zero GPU Spaces์šฉ ์นด๋“œ ๊ตฌ์กฐ์—์„œ ํ™œ์šฉ */
833
  .space-header {
@@ -915,7 +1011,6 @@ if __name__ == '__main__':
915
 
916
  <div class="mac-content">
917
  <div class="header">
918
- <!-- ์ฒซ ๋ฒˆ์งธ ํƒญ ์ œ๋ชฉ์„ Zero GPU Spaces๋กœ ๋ณ€๊ฒฝ -->
919
  <h1>ZeroGPU Spaces Leaderboard</h1>
920
  <p>Discover Zero GPU(Shared A100) spaces from Hugging Face</p>
921
  </div>
@@ -923,6 +1018,7 @@ if __name__ == '__main__':
923
  <!-- Tab Navigation -->
924
  <div class="tab-nav">
925
  <button id="tabTrendingButton" class="tab-button active">Trending</button>
 
926
  <button id="tabFixedButton" class="tab-button">Picks</button>
927
  </div>
928
 
@@ -951,16 +1047,31 @@ if __name__ == '__main__':
951
  </div>
952
 
953
  <div class="search-bar">
954
- <input type="text" id="searchInput" placeholder="Search by name, owner, or description..." />
955
- <button id="refreshButton" class="refresh-btn">
956
  <span class="refresh-icon"></span>
957
  Refresh
958
  </button>
959
  </div>
960
 
961
- <div id="gridContainer" class="grid-container"></div>
962
 
963
- <div id="pagination" class="pagination"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
964
  </div>
965
 
966
  <!-- Fixed Tab Content (๊ธฐ์กด ์˜ˆ์‹œ ์œ ์ง€) -->
@@ -982,40 +1093,65 @@ if __name__ == '__main__':
982
  </div>
983
 
984
  <script>
985
- // DOM Elements
986
- const elements = {
987
- gridContainer: document.getElementById('gridContainer'),
988
- loadingIndicator: document.getElementById('loadingIndicator'),
989
- loadingError: document.getElementById('loadingError'),
990
- searchInput: document.getElementById('searchInput'),
991
- refreshButton: document.getElementById('refreshButton'),
992
- pagination: document.getElementById('pagination'),
993
- statsToggle: document.getElementById('statsToggle'),
994
- statsContent: document.getElementById('statsContent'),
995
- creatorStatsChart: document.getElementById('creatorStatsChart')
996
- };
997
-
998
- const tabTrendingButton = document.getElementById('tabTrendingButton');
999
- const tabFixedButton = document.getElementById('tabFixedButton');
1000
- const trendingTab = document.getElementById('trendingTab');
1001
- const fixedTab = document.getElementById('fixedTab');
1002
- const fixedGridContainer = document.getElementById('fixedGrid');
1003
-
1004
- const state = {
1005
  isLoading: false,
1006
- spaces: [],
1007
- currentPage: 0,
1008
- itemsPerPage: 72,
1009
- totalItems: 0,
1010
  loadingTimeout: null,
1011
- staticModeAttempted: {},
1012
- statsVisible: false,
1013
- chartInstance: null,
1014
- topOwners: [],
1015
- iframeStatuses: {}
1016
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1017
 
1018
- // Advanced iframe loader for better error detection
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1019
  const iframeLoader = {
1020
  checkQueue: {},
1021
  maxAttempts: 5,
@@ -1107,23 +1243,44 @@ if __name__ == '__main__':
1107
  }
1108
  };
1109
 
1110
- function toggleStats() {
1111
- state.statsVisible = !state.statsVisible;
1112
- elements.statsContent.classList.toggle('open', state.statsVisible);
1113
- elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
1114
-
1115
- if (state.statsVisible && state.topOwners.length > 0) {
1116
- renderCreatorStats();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1117
  }
1118
- }
1119
 
1120
- function renderCreatorStats() {
1121
- if (state.chartInstance) {
1122
- state.chartInstance.destroy();
1123
  }
1124
- const ctx = elements.creatorStatsChart.getContext('2d');
1125
- const labels = state.topOwners.map(item => item[0]);
1126
- const data = state.topOwners.map(item => item[1]);
1127
 
1128
  const colors = [];
1129
  for (let i = 0; i < labels.length; i++) {
@@ -1131,7 +1288,7 @@ if __name__ == '__main__':
1131
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
1132
  }
1133
 
1134
- state.chartInstance = new Chart(ctx, {
1135
  type: 'bar',
1136
  data: {
1137
  labels: labels,
@@ -1188,69 +1345,211 @@ if __name__ == '__main__':
1188
  });
1189
  }
1190
 
1191
- async function loadSpaces(page = 0) {
1192
  setLoading(true);
1193
  try {
1194
- const searchText = elements.searchInput.value;
1195
- const offset = page * state.itemsPerPage;
1196
 
1197
  const timeoutPromise = new Promise((_, reject) =>
1198
  setTimeout(() => reject(new Error('Request timeout')), 30000)
1199
  );
1200
  const fetchPromise = fetch(
1201
- `/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`
1202
  );
1203
  const response = await Promise.race([fetchPromise, timeoutPromise]);
1204
  const data = await response.json();
1205
 
1206
- state.spaces = data.spaces;
1207
- state.totalItems = data.total;
1208
- state.currentPage = page;
1209
- state.topOwners = data.top_owners || [];
1210
 
1211
- renderGrid(state.spaces);
1212
- renderPagination();
1213
 
1214
- if (state.statsVisible && state.topOwners.length > 0) {
1215
- renderCreatorStats();
 
1216
  }
1217
  } catch (error) {
1218
- console.error('Error loading spaces:', error);
1219
- elements.gridContainer.innerHTML = `
1220
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1221
  <div style="font-size: 3rem; margin-bottom: 20px;">โš ๏ธ</div>
1222
- <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
1223
  <p style="color: #666;">Please try refreshing the page. If the problem persists, try again later.</p>
1224
- <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1225
  Try Again
1226
  </button>
1227
  </div>
1228
  `;
1229
- document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
1230
- renderPagination();
1231
  } finally {
1232
  setLoading(false);
1233
  }
1234
  }
1235
 
1236
- function renderPagination() {
1237
- elements.pagination.innerHTML = '';
1238
- const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1239
 
1240
  // Previous page
1241
  const prevButton = document.createElement('button');
1242
  prevButton.className = 'pagination-button';
1243
  prevButton.textContent = 'Previous';
1244
- prevButton.disabled = (state.currentPage === 0);
1245
  prevButton.addEventListener('click', () => {
1246
- if (state.currentPage > 0) {
1247
- loadSpaces(state.currentPage - 1);
1248
  }
1249
  });
1250
- elements.pagination.appendChild(prevButton);
1251
 
1252
  const maxButtons = 7;
1253
- let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
1254
  let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
1255
 
1256
  if (endPage - startPage + 1 < maxButtons) {
@@ -1259,59 +1558,89 @@ if __name__ == '__main__':
1259
 
1260
  for (let i = startPage; i <= endPage; i++) {
1261
  const pageButton = document.createElement('button');
1262
- pageButton.className = 'pagination-button' + (i === state.currentPage ? ' active' : '');
1263
  pageButton.textContent = (i + 1);
1264
  pageButton.addEventListener('click', () => {
1265
- if (i !== state.currentPage) {
1266
- loadSpaces(i);
1267
  }
1268
  });
1269
- elements.pagination.appendChild(pageButton);
1270
  }
1271
 
1272
  // Next page
1273
  const nextButton = document.createElement('button');
1274
  nextButton.className = 'pagination-button';
1275
  nextButton.textContent = 'Next';
1276
- nextButton.disabled = (state.currentPage >= totalPages - 1);
1277
  nextButton.addEventListener('click', () => {
1278
- if (state.currentPage < totalPages - 1) {
1279
- loadSpaces(state.currentPage + 1);
1280
  }
1281
  });
1282
- elements.pagination.appendChild(nextButton);
1283
  }
1284
 
1285
- function handleIframeError(iframe, owner, name, title) {
1286
- const container = iframe.parentNode;
1287
-
1288
- const errorPlaceholder = document.createElement('div');
1289
- errorPlaceholder.className = 'error-placeholder';
1290
-
1291
- const errorMessage = document.createElement('p');
1292
- errorMessage.textContent = `"${title}" space couldn't be loaded`;
1293
- errorPlaceholder.appendChild(errorMessage);
1294
-
1295
- const directLink = document.createElement('a');
1296
- directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
1297
- directLink.target = '_blank';
1298
- directLink.textContent = 'Visit HF Space';
1299
- directLink.style.color = '#3182ce';
1300
- directLink.style.marginTop = '10px';
1301
- directLink.style.display = 'inline-block';
1302
- directLink.style.padding = '8px 16px';
1303
- directLink.style.background = '#ebf8ff';
1304
- directLink.style.borderRadius = '5px';
1305
- directLink.style.fontWeight = '600';
1306
- errorPlaceholder.appendChild(directLink);
1307
-
1308
- iframe.style.display = 'none';
1309
- container.appendChild(errorPlaceholder);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1310
  }
1311
 
1312
- function renderGrid(spaces) {
1313
- elements.gridContainer.innerHTML = '';
1314
-
1315
  if (!spaces || spaces.length === 0) {
1316
  const noResultsMsg = document.createElement('p');
1317
  noResultsMsg.textContent = 'No zero-gpu spaces found matching your search.';
@@ -1319,17 +1648,20 @@ if __name__ == '__main__':
1319
  noResultsMsg.style.textAlign = 'center';
1320
  noResultsMsg.style.fontStyle = 'italic';
1321
  noResultsMsg.style.color = '#718096';
1322
- elements.gridContainer.appendChild(noResultsMsg);
1323
  return;
1324
  }
1325
 
1326
- spaces.forEach((item) => {
1327
  try {
1328
  const {
1329
  url, title, likes_count, owner, name, rank,
1330
  description, avatar_url, author_name, embedUrl
1331
  } = item;
1332
 
 
 
 
1333
  const gridItem = document.createElement('div');
1334
  gridItem.className = 'grid-item';
1335
 
@@ -1337,17 +1669,14 @@ if __name__ == '__main__':
1337
  const headerDiv = document.createElement('div');
1338
  headerDiv.className = 'grid-header';
1339
 
1340
- // space-header (์ˆœ์œ„ + ์ œ๋ชฉ + Zero GPU ๋ฐฐ์ง€)
1341
  const spaceHeader = document.createElement('div');
1342
  spaceHeader.className = 'space-header';
1343
 
1344
- // 1) ์ƒ๋‹จ ์ˆœ์œ„ ๋ฑƒ์ง€
1345
  const rankBadge = document.createElement('div');
1346
  rankBadge.className = 'rank-badge';
1347
- rankBadge.textContent = `#${rank}`;
1348
  spaceHeader.appendChild(rankBadge);
1349
 
1350
- // ์ œ๋ชฉ+๋ฐฐ์ง€
1351
  const titleWrapper = document.createElement('div');
1352
  titleWrapper.style.display = 'flex';
1353
  titleWrapper.style.alignItems = 'center';
@@ -1367,7 +1696,6 @@ if __name__ == '__main__':
1367
  spaceHeader.appendChild(titleWrapper);
1368
  headerDiv.appendChild(spaceHeader);
1369
 
1370
- // ํ•˜๋‹จ ๋ฉ”ํƒ€ ์ •๋ณด
1371
  const metaInfo = document.createElement('div');
1372
  metaInfo.className = 'grid-meta';
1373
  metaInfo.style.display = 'flex';
@@ -1375,11 +1703,7 @@ if __name__ == '__main__':
1375
  metaInfo.style.alignItems = 'center';
1376
  metaInfo.style.marginTop = '6px';
1377
 
1378
- // ์™ผ์ชฝ: (์ค‘๋ณต rank ์ œ๊ฑฐ)
1379
  const leftMeta = document.createElement('div');
1380
-
1381
- // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ† rankBadge2 ๋ถ€๋ถ„ ์ œ๊ฑฐ๋จ
1382
-
1383
  const authorSpan = document.createElement('span');
1384
  authorSpan.className = 'author-name';
1385
  authorSpan.style.marginLeft = '8px';
@@ -1388,7 +1712,6 @@ if __name__ == '__main__':
1388
 
1389
  metaInfo.appendChild(leftMeta);
1390
 
1391
- // ์˜ค๋ฅธ์ชฝ: likes
1392
  const likesDiv = document.createElement('div');
1393
  likesDiv.className = 'likes-wrapper';
1394
  likesDiv.innerHTML = `<span class="likes-heart">โ™ฅ</span><span>${likes_count}</span>`;
@@ -1397,7 +1720,6 @@ if __name__ == '__main__':
1397
  headerDiv.appendChild(metaInfo);
1398
  gridItem.appendChild(headerDiv);
1399
 
1400
- // description
1401
  if (description) {
1402
  const descP = document.createElement('p');
1403
  descP.className = 'desc-text';
@@ -1405,7 +1727,6 @@ if __name__ == '__main__':
1405
  gridItem.appendChild(descP);
1406
  }
1407
 
1408
- // iframe container
1409
  const content = document.createElement('div');
1410
  content.className = 'grid-content';
1411
 
@@ -1413,7 +1734,7 @@ if __name__ == '__main__':
1413
  iframeContainer.className = 'iframe-container';
1414
 
1415
  const iframe = document.createElement('iframe');
1416
- iframe.src = embedUrl; // transformed direct URL
1417
  iframe.title = title;
1418
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1419
  iframe.setAttribute('allowfullscreen', '');
@@ -1421,26 +1742,25 @@ if __name__ == '__main__':
1421
  iframe.loading = 'lazy';
1422
 
1423
  const spaceKey = `${owner}/${name}`;
1424
- state.iframeStatuses[spaceKey] = 'loading';
1425
 
1426
  iframe.onload = function() {
1427
  iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1428
  };
1429
  iframe.onerror = function() {
1430
  handleIframeError(iframe, owner, name, title);
1431
- state.iframeStatuses[spaceKey] = 'error';
1432
  };
1433
  setTimeout(() => {
1434
- if (state.iframeStatuses[spaceKey] === 'loading') {
1435
  handleIframeError(iframe, owner, name, title);
1436
- state.iframeStatuses[spaceKey] = 'error';
1437
  }
1438
  }, 30000);
1439
 
1440
  iframeContainer.appendChild(iframe);
1441
  content.appendChild(iframeContainer);
1442
 
1443
- // actions
1444
  const actions = document.createElement('div');
1445
  actions.className = 'grid-actions';
1446
 
@@ -1449,24 +1769,73 @@ if __name__ == '__main__':
1449
  linkEl.target = '_blank';
1450
  linkEl.className = 'open-link';
1451
  linkEl.textContent = 'Open in new window';
1452
-
1453
  actions.appendChild(linkEl);
 
1454
  gridItem.appendChild(content);
1455
  gridItem.appendChild(actions);
1456
 
1457
- elements.gridContainer.appendChild(gridItem);
1458
 
1459
  } catch (err) {
1460
- console.error('Item rendering error:', err);
1461
  }
1462
  });
1463
  }
1464
 
1465
- ############# Picks URL ์ž…๋ ฅ
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1466
 
 
 
 
 
 
 
 
 
 
 
 
1467
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1468
  function renderFixedGrid() {
1469
- // Fixed Tab ์˜ˆ์‹œ์šฉ (์›๋ณธ ์ฝ”๋“œ ์˜ˆ์‹œ ์œ ์ง€)
1470
  fixedGridContainer.innerHTML = '';
1471
 
1472
  const staticSpaces = [
@@ -1573,9 +1942,8 @@ if __name__ == '__main__':
1573
  iframe.setAttribute('frameborder', '0');
1574
  iframe.loading = 'lazy';
1575
 
1576
- const spaceKey = `${owner}/${name}`;
1577
  iframe.onload = function() {
1578
- iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1579
  };
1580
  iframe.onerror = function() {
1581
  handleIframeError(iframe, owner, name, title);
@@ -1610,42 +1978,82 @@ if __name__ == '__main__':
1610
  });
1611
  }
1612
 
1613
- // Tab switching
 
 
 
 
 
 
 
 
 
 
1614
  tabTrendingButton.addEventListener('click', () => {
1615
  tabTrendingButton.classList.add('active');
 
1616
  tabFixedButton.classList.remove('active');
1617
  trendingTab.classList.add('active');
 
 
 
 
 
 
 
 
 
 
 
1618
  fixedTab.classList.remove('active');
1619
- loadSpaces(state.currentPage);
1620
  });
 
1621
  tabFixedButton.addEventListener('click', () => {
1622
  tabFixedButton.classList.add('active');
1623
  tabTrendingButton.classList.remove('active');
 
1624
  fixedTab.classList.add('active');
1625
  trendingTab.classList.remove('active');
 
1626
  renderFixedGrid();
1627
  });
1628
 
1629
- elements.searchInput.addEventListener('input', () => {
1630
- clearTimeout(state.searchTimeout);
1631
- state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1632
  });
1633
- elements.searchInput.addEventListener('keyup', (event) => {
1634
  if (event.key === 'Enter') {
1635
- loadSpaces(0);
1636
  }
1637
  });
1638
- elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1639
- elements.statsToggle.addEventListener('click', toggleStats);
1640
 
1641
  window.addEventListener('load', function() {
1642
- setTimeout(() => loadSpaces(0), 500);
 
1643
  });
1644
 
1645
  setTimeout(() => {
1646
- if (state.isLoading) {
1647
  setLoading(false);
1648
- elements.gridContainer.innerHTML = `
 
1649
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1650
  <div style="font-size: 3rem; margin-bottom: 20px;">โฑ๏ธ</div>
1651
  <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3>
@@ -1657,29 +2065,9 @@ if __name__ == '__main__':
1657
  `;
1658
  }
1659
  }, 20000);
1660
-
1661
- loadSpaces(0);
1662
-
1663
- function setLoading(isLoading) {
1664
- state.isLoading = isLoading;
1665
- elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
1666
-
1667
- if (isLoading) {
1668
- elements.refreshButton.classList.add('refreshing');
1669
- clearTimeout(state.loadingTimeout);
1670
- state.loadingTimeout = setTimeout(() => {
1671
- elements.loadingError.style.display = 'block';
1672
- }, 10000);
1673
- } else {
1674
- elements.refreshButton.classList.remove('refreshing');
1675
- clearTimeout(state.loadingTimeout);
1676
- elements.loadingError.style.display = 'none';
1677
- }
1678
- }
1679
  </script>
1680
  </body>
1681
  </html>
1682
  ''')
1683
 
1684
- # Flask ์•ฑ ์‹คํ–‰ (ํฌํŠธ 7860)
1685
  app.run(host='0.0.0.0', port=7860)
 
32
  # Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
33
  def fetch_trending_spaces(offset=0, limit=72):
34
  """
35
+ Trending์šฉ CPU ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ (์ •๋ ฌ์€ Hugging Face ๊ธฐ๋ณธ ์ •๋ ฌ)
 
36
  """
37
  try:
38
  url = "https://huggingface.co/api/spaces"
 
92
  'all_spaces': generate_dummy_spaces(500)
93
  }
94
 
95
+ def fetch_latest_spaces(offset=0, limit=72):
96
+ """
97
+ 'createdAt' ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ตœ๊ทผ ์ŠคํŽ˜์ด์Šค 500๊ฐœ๋ฅผ ์ถ”๋ฆฐ ๋’ค,
98
+ offset ~ offset+limit ๊ฐœ๋งŒ ๋ฐ˜ํ™˜
99
+ """
100
+ try:
101
+ url = "https://huggingface.co/api/spaces"
102
+ params = {
103
+ "limit": 10000, # ์ถฉ๋ถ„ํžˆ ๋งŽ์ด ๊ฐ€์ ธ์˜ด
104
+ "hardware": "cpu"
105
+ }
106
+ response = requests.get(url, params=params, timeout=30)
107
+
108
+ if response.status_code == 200:
109
+ spaces = response.json()
110
+
111
+ # owner๋‚˜ id๊ฐ€ 'None'์ธ ๊ฒฝ์šฐ ์ œ์™ธ
112
+ filtered_spaces = [
113
+ space for space in spaces
114
+ if space.get('owner') != 'None'
115
+ and space.get('id', '').split('/', 1)[0] != 'None'
116
+ ]
117
+
118
+ # createdAt ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ
119
+ # createdAt ์˜ˆ: "2023-01-01T00:00:00.000Z"
120
+ # ๋ฌธ์ž์—ด ๋น„๊ต๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด time ํŒŒ์‹ฑ ํ›„ ๋น„๊ตํ•  ์ˆ˜๋„ ์žˆ์Œ
121
+ def parse_time(sp):
122
+ return sp.get('createdAt') or ''
123
+
124
+ # ๋‚ด๋ฆผ์ฐจ์ˆœ
125
+ filtered_spaces.sort(key=parse_time, reverse=True)
126
+
127
+ # ์ƒ์œ„ 500๊ฐœ๋งŒ ์ถ”๋ฆฌ๊ธฐ
128
+ truncated = filtered_spaces[:500]
129
+
130
+ # ํ•„์š”ํ•œ ๊ตฌ๊ฐ„ ์Šฌ๋ผ์ด์‹ฑ
131
+ start = min(offset, len(truncated))
132
+ end = min(offset + limit, len(truncated))
133
+
134
+ print(f"[fetch_latest_spaces] CPU๊ธฐ๋ฐ˜ ์ŠคํŽ˜์ด์Šค ์ด {len(spaces)}๊ฐœ ์ค‘ ํ•„ํ„ฐ ํ›„ {len(filtered_spaces)}๊ฐœ, ์ƒ์œ„ 500๊ฐœ ์ค‘ {start}~{end-1} ๋ฐ˜ํ™˜")
135
+
136
+ return {
137
+ 'spaces': truncated[start:end],
138
+ 'total': len(truncated), # 500 ์ดํ•˜
139
+ 'offset': offset,
140
+ 'limit': limit
141
+ }
142
+ else:
143
+ print(f"Error fetching spaces: {response.status_code}")
144
+ return {
145
+ 'spaces': generate_dummy_spaces(limit),
146
+ 'total': 500,
147
+ 'offset': offset,
148
+ 'limit': limit
149
+ }
150
+ except Exception as e:
151
+ print(f"Exception when fetching spaces: {e}")
152
+ return {
153
+ 'spaces': generate_dummy_spaces(limit),
154
+ 'total': 500,
155
+ 'offset': offset,
156
+ 'limit': limit
157
+ }
158
+
159
  # Transform Huggingface URL to direct space URL
160
  def transform_url(owner, name):
161
  """
 
175
  # Get space details
176
  def get_space_details(space_data, index, offset):
177
  """
178
+ ์ŠคํŽ˜์ด์Šค ์ƒ์„ธ ํ•„๋“œ ์ถ”์ถœ
179
+ - rank๋Š” offset ๊ธฐ๋ฐ˜ (ํ˜„์žฌ ํŽ˜์ด์ง€)
 
180
  """
181
  try:
182
  if '/' in space_data.get('id', ''):
 
202
  # Description
203
  short_desc = space_data.get('description', '')
204
 
205
+ # User info
206
  user_info = space_data.get('user', {})
207
  avatar_url = user_info.get('avatar_url', '')
208
  author_name = user_info.get('name') or owner
 
217
  'description': short_desc,
218
  'avatar_url': avatar_url,
219
  'author_name': author_name,
220
+ 'rank': offset + index + 1 # ํ˜„์žฌ ํŽ˜์ด์ง€ ํ‘œ์‹œ์šฉ ๋žญํฌ
221
  }
222
  except Exception as e:
223
  print(f"Error processing space data: {e}")
224
+ # ์—๋Ÿฌ ์‹œ ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ๋กœ ๋Œ€์ฒด
225
  return {
226
  'url': 'https://huggingface.co/spaces',
227
  'embedUrl': 'https://huggingface.co/spaces',
 
235
  'rank': offset + index + 1
236
  }
237
 
238
+ # Get owner statistics from all spaces (for the "Trending" tab's top owners)
239
  def get_owner_stats(all_spaces):
240
  """
241
  ์ƒ์œ„ 500์œ„(global_rank <= 500) ์ด๋‚ด์— ๋ฐฐ์น˜๋œ ์ŠคํŽ˜์ด์Šค๋“ค์˜ owner๋ฅผ ์ถ”์ถœํ•ด,
 
268
  """
269
  return render_template('index.html')
270
 
271
+ # Zero-GPU spaces API (Trending)
272
  @app.route('/api/trending-spaces', methods=['GET'])
273
  def trending_spaces():
274
  """
275
+ hardware=cpu ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์™€ ๊ฒ€์ƒ‰, ํŽ˜์ด์ง•, ํ†ต๊ณ„ ๋“ฑ์„ ์ ์šฉ (๊ธฐ์กด 'Trending')
276
  """
277
  search_query = request.args.get('search', '').lower()
278
  offset = int(request.args.get('offset', 0))
279
+ limit = int(request.args.get('limit', 72))
280
 
281
  # Fetch zero-gpu (cpu) spaces
282
  spaces_data = fetch_trending_spaces(offset, limit)
 
288
  if not space_info:
289
  continue
290
 
291
+ # ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ
292
  if search_query:
293
  if (search_query not in space_info['title'].lower()
294
  and search_query not in space_info['owner'].lower()
 
298
 
299
  results.append(space_info)
300
 
301
+ # ์˜ค๋„ˆ ํ†ต๊ณ„ (Top 500 โ†’ Top 30)
302
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
303
 
304
  return jsonify({
 
309
  'top_owners': top_owners
310
  })
311
 
312
+ # Zero-GPU spaces API (Latest Releases)
313
+ @app.route('/api/latest-spaces', methods=['GET'])
314
+ def latest_spaces():
315
+ """
316
+ hardware=cpu ์ŠคํŽ˜์ด์Šค ์ค‘์—์„œ createdAt ๊ธฐ์ค€์œผ๋กœ ์ตœ์‹ ์ˆœ 500๊ฐœ๋ฅผ ํŽ˜์ด์ง•, ๊ฒ€์ƒ‰
317
+ """
318
+ search_query = request.args.get('search', '').lower()
319
+ offset = int(request.args.get('offset', 0))
320
+ limit = int(request.args.get('limit', 72))
321
+
322
+ spaces_data = fetch_latest_spaces(offset, limit)
323
+
324
+ results = []
325
+ for index, space_data in enumerate(spaces_data['spaces']):
326
+ space_info = get_space_details(space_data, index, offset)
327
+ if not space_info:
328
+ continue
329
+
330
+ # ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ
331
+ if search_query:
332
+ if (search_query not in space_info['title'].lower()
333
+ and search_query not in space_info['owner'].lower()
334
+ and search_query not in space_info['url'].lower()
335
+ and search_query not in space_info['description'].lower()):
336
+ continue
337
+
338
+ results.append(space_info)
339
+
340
+ return jsonify({
341
+ 'spaces': results,
342
+ 'total': spaces_data['total'],
343
+ 'offset': offset,
344
+ 'limit': limit
345
+ })
346
+
347
  if __name__ == '__main__':
348
  """
349
  ์„œ๋ฒ„ ๊ตฌ๋™ ์‹œ, templates/index.html ํŒŒ์ผ์„ ์ƒ์„ฑ ํ›„ Flask ์‹คํ–‰
 
923
  }
924
 
925
  /* ์ถ”๊ฐ€ ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ •(์•„๋ฐ”ํƒ€, ZERO GPU ๋ฑƒ์ง€ ๋“ฑ)์„ ์œ„ํ•ด
926
+ ์•„๋ž˜ ํด๋ž˜์Šค๋“ค์„ ์ผ๋ถ€ ์ถ”๊ฐ€/์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ์—ฌ๊ธฐ์„œ๋Š” ์ƒ๋žต */
 
927
 
928
  /* ๋‹ค์Œ ๋ถ€๋ถ„์€ Zero GPU Spaces์šฉ ์นด๋“œ ๊ตฌ์กฐ์—์„œ ํ™œ์šฉ */
929
  .space-header {
 
1011
 
1012
  <div class="mac-content">
1013
  <div class="header">
 
1014
  <h1>ZeroGPU Spaces Leaderboard</h1>
1015
  <p>Discover Zero GPU(Shared A100) spaces from Hugging Face</p>
1016
  </div>
 
1018
  <!-- Tab Navigation -->
1019
  <div class="tab-nav">
1020
  <button id="tabTrendingButton" class="tab-button active">Trending</button>
1021
+ <button id="tabLatestButton" class="tab-button">Latest Releases</button>
1022
  <button id="tabFixedButton" class="tab-button">Picks</button>
1023
  </div>
1024
 
 
1047
  </div>
1048
 
1049
  <div class="search-bar">
1050
+ <input type="text" id="searchInputTrending" placeholder="Search by name, owner, or description..." />
1051
+ <button id="refreshButtonTrending" class="refresh-btn">
1052
  <span class="refresh-icon"></span>
1053
  Refresh
1054
  </button>
1055
  </div>
1056
 
1057
+ <div id="gridContainerTrending" class="grid-container"></div>
1058
 
1059
+ <div id="paginationTrending" class="pagination"></div>
1060
+ </div>
1061
+
1062
+ <!-- Latest Releases Tab Content -->
1063
+ <div id="latestTab" class="tab-content">
1064
+ <div class="search-bar">
1065
+ <input type="text" id="searchInputLatest" placeholder="Search by name, owner, or description..." />
1066
+ <button id="refreshButtonLatest" class="refresh-btn">
1067
+ <span class="refresh-icon"></span>
1068
+ Refresh
1069
+ </button>
1070
+ </div>
1071
+
1072
+ <div id="gridContainerLatest" class="grid-container"></div>
1073
+
1074
+ <div id="paginationLatest" class="pagination"></div>
1075
  </div>
1076
 
1077
  <!-- Fixed Tab Content (๊ธฐ์กด ์˜ˆ์‹œ ์œ ์ง€) -->
 
1093
  </div>
1094
 
1095
  <script>
1096
+ // ------------------------------------
1097
+ // GLOBAL STATE & COMMON FUNCTIONS
1098
+ // ------------------------------------
1099
+ const globalState = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1100
  isLoading: false,
 
 
 
 
1101
  loadingTimeout: null,
 
 
 
 
 
1102
  };
1103
+ function setLoading(isLoading) {
1104
+ globalState.isLoading = isLoading;
1105
+ document.getElementById('loadingIndicator').style.display = isLoading ? 'flex' : 'none';
1106
+
1107
+ const refreshButtons = document.querySelectorAll('.refresh-btn');
1108
+ refreshButtons.forEach(btn => {
1109
+ if (isLoading) {
1110
+ btn.classList.add('refreshing');
1111
+ } else {
1112
+ btn.classList.remove('refreshing');
1113
+ }
1114
+ });
1115
+
1116
+ if (isLoading) {
1117
+ clearTimeout(globalState.loadingTimeout);
1118
+ globalState.loadingTimeout = setTimeout(() => {
1119
+ document.getElementById('loadingError').style.display = 'block';
1120
+ }, 10000);
1121
+ } else {
1122
+ clearTimeout(globalState.loadingTimeout);
1123
+ document.getElementById('loadingError').style.display = 'none';
1124
+ }
1125
+ }
1126
+ function handleIframeError(iframe, owner, name, title) {
1127
+ const container = iframe.parentNode;
1128
+ const errorPlaceholder = document.createElement('div');
1129
+ errorPlaceholder.className = 'error-placeholder';
1130
 
1131
+ const errorMessage = document.createElement('p');
1132
+ errorMessage.textContent = `"${title}" space couldn't be loaded`;
1133
+ errorPlaceholder.appendChild(errorMessage);
1134
+
1135
+ const directLink = document.createElement('a');
1136
+ directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
1137
+ directLink.target = '_blank';
1138
+ directLink.textContent = 'Visit HF Space';
1139
+ directLink.style.color = '#3182ce';
1140
+ directLink.style.marginTop = '10px';
1141
+ directLink.style.display = 'inline-block';
1142
+ directLink.style.padding = '8px 16px';
1143
+ directLink.style.background = '#ebf8ff';
1144
+ directLink.style.borderRadius = '5px';
1145
+ directLink.style.fontWeight = '600';
1146
+ errorPlaceholder.appendChild(directLink);
1147
+
1148
+ iframe.style.display = 'none';
1149
+ container.appendChild(errorPlaceholder);
1150
+ }
1151
+
1152
+ // ------------------------------------
1153
+ // IFRAME LOADER (๊ณตํ†ต)
1154
+ // ------------------------------------
1155
  const iframeLoader = {
1156
  checkQueue: {},
1157
  maxAttempts: 5,
 
1243
  }
1244
  };
1245
 
1246
+ // ------------------------------------
1247
+ // TRENDING TAB
1248
+ // ------------------------------------
1249
+ const trendingState = {
1250
+ spaces: [],
1251
+ currentPage: 0,
1252
+ itemsPerPage: 72,
1253
+ totalItems: 0,
1254
+ topOwners: [],
1255
+ iframeStatuses: {}
1256
+ };
1257
+ const trendingElements = {
1258
+ searchInput: document.getElementById('searchInputTrending'),
1259
+ refreshButton: document.getElementById('refreshButtonTrending'),
1260
+ gridContainer: document.getElementById('gridContainerTrending'),
1261
+ pagination: document.getElementById('paginationTrending'),
1262
+ statsToggle: document.getElementById('statsToggle'),
1263
+ statsContent: document.getElementById('statsContent'),
1264
+ creatorStatsChart: document.getElementById('creatorStatsChart')
1265
+ };
1266
+
1267
+ let chartInstance = null;
1268
+
1269
+ trendingElements.statsToggle.addEventListener('click', () => {
1270
+ const isOpen = trendingElements.statsContent.classList.toggle('open');
1271
+ trendingElements.statsToggle.textContent = isOpen ? 'Hide Stats' : 'Show Stats';
1272
+ if (isOpen && trendingState.topOwners.length > 0) {
1273
+ renderCreatorStats(trendingState.topOwners);
1274
  }
1275
+ });
1276
 
1277
+ function renderCreatorStats(topOwners) {
1278
+ if (chartInstance) {
1279
+ chartInstance.destroy();
1280
  }
1281
+ const ctx = trendingElements.creatorStatsChart.getContext('2d');
1282
+ const labels = topOwners.map(item => item[0]);
1283
+ const data = topOwners.map(item => item[1]);
1284
 
1285
  const colors = [];
1286
  for (let i = 0; i < labels.length; i++) {
 
1288
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
1289
  }
1290
 
1291
+ chartInstance = new Chart(ctx, {
1292
  type: 'bar',
1293
  data: {
1294
  labels: labels,
 
1345
  });
1346
  }
1347
 
1348
+ async function loadTrending(page=0) {
1349
  setLoading(true);
1350
  try {
1351
+ const searchText = trendingElements.searchInput.value;
1352
+ const offset = page * trendingState.itemsPerPage;
1353
 
1354
  const timeoutPromise = new Promise((_, reject) =>
1355
  setTimeout(() => reject(new Error('Request timeout')), 30000)
1356
  );
1357
  const fetchPromise = fetch(
1358
+ `/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${trendingState.itemsPerPage}`
1359
  );
1360
  const response = await Promise.race([fetchPromise, timeoutPromise]);
1361
  const data = await response.json();
1362
 
1363
+ trendingState.spaces = data.spaces || [];
1364
+ trendingState.totalItems = data.total || 0;
1365
+ trendingState.currentPage = page;
1366
+ trendingState.topOwners = data.top_owners || [];
1367
 
1368
+ renderTrendingGrid(trendingState.spaces);
1369
+ renderTrendingPagination();
1370
 
1371
+ // ํ†ต๊ณ„์ฐฝ ์—ด๋ ค์žˆ๋‹ค๋ฉด ์ƒˆ ๋ฐ์ดํ„ฐ๋กœ ๊ฐฑ์‹ 
1372
+ if (trendingElements.statsContent.classList.contains('open') && trendingState.topOwners.length > 0) {
1373
+ renderCreatorStats(trendingState.topOwners);
1374
  }
1375
  } catch (error) {
1376
+ console.error('Error loading trending spaces:', error);
1377
+ trendingElements.gridContainer.innerHTML = `
1378
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1379
  <div style="font-size: 3rem; margin-bottom: 20px;">โš ๏ธ</div>
1380
+ <h3 style="margin-bottom: 10px;">Unable to load spaces (Trending)</h3>
1381
  <p style="color: #666;">Please try refreshing the page. If the problem persists, try again later.</p>
1382
+ <button id="retryTrendingButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1383
  Try Again
1384
  </button>
1385
  </div>
1386
  `;
1387
+ document.getElementById('retryTrendingButton')?.addEventListener('click', () => loadTrending(0));
1388
+ renderTrendingPagination();
1389
  } finally {
1390
  setLoading(false);
1391
  }
1392
  }
1393
 
1394
+ function renderTrendingGrid(spaces) {
1395
+ trendingElements.gridContainer.innerHTML = '';
1396
+
1397
+ if (!spaces || spaces.length === 0) {
1398
+ const noResultsMsg = document.createElement('p');
1399
+ noResultsMsg.textContent = 'No zero-gpu spaces found matching your search.';
1400
+ noResultsMsg.style.padding = '2rem';
1401
+ noResultsMsg.style.textAlign = 'center';
1402
+ noResultsMsg.style.fontStyle = 'italic';
1403
+ noResultsMsg.style.color = '#718096';
1404
+ trendingElements.gridContainer.appendChild(noResultsMsg);
1405
+ return;
1406
+ }
1407
+
1408
+ spaces.forEach((item) => {
1409
+ try {
1410
+ const {
1411
+ url, title, likes_count, owner, name, rank,
1412
+ description, avatar_url, author_name, embedUrl
1413
+ } = item;
1414
+
1415
+ const gridItem = document.createElement('div');
1416
+ gridItem.className = 'grid-item';
1417
+
1418
+ // ์ƒ๋‹จ ํ—ค๋”
1419
+ const headerDiv = document.createElement('div');
1420
+ headerDiv.className = 'grid-header';
1421
+
1422
+ const spaceHeader = document.createElement('div');
1423
+ spaceHeader.className = 'space-header';
1424
+
1425
+ const rankBadge = document.createElement('div');
1426
+ rankBadge.className = 'rank-badge';
1427
+ rankBadge.textContent = `#${rank}`;
1428
+ spaceHeader.appendChild(rankBadge);
1429
+
1430
+ const titleWrapper = document.createElement('div');
1431
+ titleWrapper.style.display = 'flex';
1432
+ titleWrapper.style.alignItems = 'center';
1433
+ titleWrapper.style.marginLeft = '8px';
1434
+
1435
+ const titleEl = document.createElement('h3');
1436
+ titleEl.className = 'space-title';
1437
+ titleEl.textContent = title;
1438
+ titleEl.title = title;
1439
+ titleWrapper.appendChild(titleEl);
1440
+
1441
+ const zeroGpuBadge = document.createElement('span');
1442
+ zeroGpuBadge.className = 'zero-gpu-badge';
1443
+ zeroGpuBadge.textContent = 'ZERO GPU';
1444
+ titleWrapper.appendChild(zeroGpuBadge);
1445
+
1446
+ spaceHeader.appendChild(titleWrapper);
1447
+ headerDiv.appendChild(spaceHeader);
1448
+
1449
+ const metaInfo = document.createElement('div');
1450
+ metaInfo.className = 'grid-meta';
1451
+ metaInfo.style.display = 'flex';
1452
+ metaInfo.style.justifyContent = 'space-between';
1453
+ metaInfo.style.alignItems = 'center';
1454
+ metaInfo.style.marginTop = '6px';
1455
+
1456
+ const leftMeta = document.createElement('div');
1457
+ const authorSpan = document.createElement('span');
1458
+ authorSpan.className = 'author-name';
1459
+ authorSpan.style.marginLeft = '8px';
1460
+ authorSpan.textContent = `by ${author_name}`;
1461
+ leftMeta.appendChild(authorSpan);
1462
+
1463
+ metaInfo.appendChild(leftMeta);
1464
+
1465
+ const likesDiv = document.createElement('div');
1466
+ likesDiv.className = 'likes-wrapper';
1467
+ likesDiv.innerHTML = `<span class="likes-heart">โ™ฅ</span><span>${likes_count}</span>`;
1468
+ metaInfo.appendChild(likesDiv);
1469
+
1470
+ headerDiv.appendChild(metaInfo);
1471
+ gridItem.appendChild(headerDiv);
1472
+
1473
+ if (description) {
1474
+ const descP = document.createElement('p');
1475
+ descP.className = 'desc-text';
1476
+ descP.textContent = description;
1477
+ gridItem.appendChild(descP);
1478
+ }
1479
+
1480
+ const content = document.createElement('div');
1481
+ content.className = 'grid-content';
1482
+
1483
+ const iframeContainer = document.createElement('div');
1484
+ iframeContainer.className = 'iframe-container';
1485
+
1486
+ const iframe = document.createElement('iframe');
1487
+ iframe.src = embedUrl;
1488
+ iframe.title = title;
1489
+ iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1490
+ iframe.setAttribute('allowfullscreen', '');
1491
+ iframe.setAttribute('frameborder', '0');
1492
+ iframe.loading = 'lazy';
1493
+
1494
+ const spaceKey = `${owner}/${name}`;
1495
+ trendingState.iframeStatuses[spaceKey] = 'loading';
1496
+
1497
+ iframe.onload = function() {
1498
+ iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1499
+ };
1500
+ iframe.onerror = function() {
1501
+ handleIframeError(iframe, owner, name, title);
1502
+ trendingState.iframeStatuses[spaceKey] = 'error';
1503
+ };
1504
+ setTimeout(() => {
1505
+ if (trendingState.iframeStatuses[spaceKey] === 'loading') {
1506
+ handleIframeError(iframe, owner, name, title);
1507
+ trendingState.iframeStatuses[spaceKey] = 'error';
1508
+ }
1509
+ }, 30000);
1510
+
1511
+ iframeContainer.appendChild(iframe);
1512
+ content.appendChild(iframeContainer);
1513
+
1514
+ const actions = document.createElement('div');
1515
+ actions.className = 'grid-actions';
1516
+
1517
+ const linkEl = document.createElement('a');
1518
+ linkEl.href = url;
1519
+ linkEl.target = '_blank';
1520
+ linkEl.className = 'open-link';
1521
+ linkEl.textContent = 'Open in new window';
1522
+ actions.appendChild(linkEl);
1523
+
1524
+ gridItem.appendChild(content);
1525
+ gridItem.appendChild(actions);
1526
+
1527
+ trendingElements.gridContainer.appendChild(gridItem);
1528
+
1529
+ } catch (err) {
1530
+ console.error('Item rendering error:', err);
1531
+ }
1532
+ });
1533
+ }
1534
+
1535
+ function renderTrendingPagination() {
1536
+ trendingElements.pagination.innerHTML = '';
1537
+ const totalPages = Math.ceil(trendingState.totalItems / trendingState.itemsPerPage);
1538
 
1539
  // Previous page
1540
  const prevButton = document.createElement('button');
1541
  prevButton.className = 'pagination-button';
1542
  prevButton.textContent = 'Previous';
1543
+ prevButton.disabled = (trendingState.currentPage === 0);
1544
  prevButton.addEventListener('click', () => {
1545
+ if (trendingState.currentPage > 0) {
1546
+ loadTrending(trendingState.currentPage - 1);
1547
  }
1548
  });
1549
+ trendingElements.pagination.appendChild(prevButton);
1550
 
1551
  const maxButtons = 7;
1552
+ let startPage = Math.max(0, trendingState.currentPage - Math.floor(maxButtons / 2));
1553
  let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
1554
 
1555
  if (endPage - startPage + 1 < maxButtons) {
 
1558
 
1559
  for (let i = startPage; i <= endPage; i++) {
1560
  const pageButton = document.createElement('button');
1561
+ pageButton.className = 'pagination-button' + (i === trendingState.currentPage ? ' active' : '');
1562
  pageButton.textContent = (i + 1);
1563
  pageButton.addEventListener('click', () => {
1564
+ if (i !== trendingState.currentPage) {
1565
+ loadTrending(i);
1566
  }
1567
  });
1568
+ trendingElements.pagination.appendChild(pageButton);
1569
  }
1570
 
1571
  // Next page
1572
  const nextButton = document.createElement('button');
1573
  nextButton.className = 'pagination-button';
1574
  nextButton.textContent = 'Next';
1575
+ nextButton.disabled = (trendingState.currentPage >= totalPages - 1);
1576
  nextButton.addEventListener('click', () => {
1577
+ if (trendingState.currentPage < totalPages - 1) {
1578
+ loadTrending(trendingState.currentPage + 1);
1579
  }
1580
  });
1581
+ trendingElements.pagination.appendChild(nextButton);
1582
  }
1583
 
1584
+ // ------------------------------------
1585
+ // LATEST RELEASES TAB
1586
+ // ------------------------------------
1587
+ const latestState = {
1588
+ spaces: [],
1589
+ currentPage: 0,
1590
+ itemsPerPage: 72,
1591
+ totalItems: 0,
1592
+ iframeStatuses: {}
1593
+ };
1594
+ const latestElements = {
1595
+ searchInput: document.getElementById('searchInputLatest'),
1596
+ refreshButton: document.getElementById('refreshButtonLatest'),
1597
+ gridContainer: document.getElementById('gridContainerLatest'),
1598
+ pagination: document.getElementById('paginationLatest')
1599
+ };
1600
+
1601
+ async function loadLatest(page=0) {
1602
+ setLoading(true);
1603
+ try {
1604
+ const searchText = latestElements.searchInput.value;
1605
+ const offset = page * latestState.itemsPerPage;
1606
+
1607
+ const timeoutPromise = new Promise((_, reject) =>
1608
+ setTimeout(() => reject(new Error('Request timeout')), 30000)
1609
+ );
1610
+ const fetchPromise = fetch(
1611
+ `/api/latest-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${latestState.itemsPerPage}`
1612
+ );
1613
+ const response = await Promise.race([fetchPromise, timeoutPromise]);
1614
+ const data = await response.json();
1615
+
1616
+ latestState.spaces = data.spaces || [];
1617
+ latestState.totalItems = data.total || 0;
1618
+ latestState.currentPage = page;
1619
+
1620
+ renderLatestGrid(latestState.spaces);
1621
+ renderLatestPagination();
1622
+ } catch (error) {
1623
+ console.error('Error loading latest spaces:', error);
1624
+ latestElements.gridContainer.innerHTML = `
1625
+ <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1626
+ <div style="font-size: 3rem; margin-bottom: 20px;">โš ๏ธ</div>
1627
+ <h3 style="margin-bottom: 10px;">Unable to load spaces (Latest)</h3>
1628
+ <p style="color: #666;">Please try refreshing the page. If the problem persists, try again later.</p>
1629
+ <button id="retryLatestButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1630
+ Try Again
1631
+ </button>
1632
+ </div>
1633
+ `;
1634
+ document.getElementById('retryLatestButton')?.addEventListener('click', () => loadLatest(0));
1635
+ renderLatestPagination();
1636
+ } finally {
1637
+ setLoading(false);
1638
+ }
1639
  }
1640
 
1641
+ function renderLatestGrid(spaces) {
1642
+ latestElements.gridContainer.innerHTML = '';
1643
+
1644
  if (!spaces || spaces.length === 0) {
1645
  const noResultsMsg = document.createElement('p');
1646
  noResultsMsg.textContent = 'No zero-gpu spaces found matching your search.';
 
1648
  noResultsMsg.style.textAlign = 'center';
1649
  noResultsMsg.style.fontStyle = 'italic';
1650
  noResultsMsg.style.color = '#718096';
1651
+ latestElements.gridContainer.appendChild(noResultsMsg);
1652
  return;
1653
  }
1654
 
1655
+ spaces.forEach((item, index) => {
1656
  try {
1657
  const {
1658
  url, title, likes_count, owner, name, rank,
1659
  description, avatar_url, author_name, embedUrl
1660
  } = item;
1661
 
1662
+ // rank๊ฐ€ ์—†์œผ๋ฏ€๋กœ Latest ํƒญ์—์„œ๋Š” offset+index+1 ํ˜•ํƒœ๋กœ ํ‘œ์‹œ
1663
+ const computedRank = latestState.currentPage * latestState.itemsPerPage + (index + 1);
1664
+
1665
  const gridItem = document.createElement('div');
1666
  gridItem.className = 'grid-item';
1667
 
 
1669
  const headerDiv = document.createElement('div');
1670
  headerDiv.className = 'grid-header';
1671
 
 
1672
  const spaceHeader = document.createElement('div');
1673
  spaceHeader.className = 'space-header';
1674
 
 
1675
  const rankBadge = document.createElement('div');
1676
  rankBadge.className = 'rank-badge';
1677
+ rankBadge.textContent = `#${computedRank}`;
1678
  spaceHeader.appendChild(rankBadge);
1679
 
 
1680
  const titleWrapper = document.createElement('div');
1681
  titleWrapper.style.display = 'flex';
1682
  titleWrapper.style.alignItems = 'center';
 
1696
  spaceHeader.appendChild(titleWrapper);
1697
  headerDiv.appendChild(spaceHeader);
1698
 
 
1699
  const metaInfo = document.createElement('div');
1700
  metaInfo.className = 'grid-meta';
1701
  metaInfo.style.display = 'flex';
 
1703
  metaInfo.style.alignItems = 'center';
1704
  metaInfo.style.marginTop = '6px';
1705
 
 
1706
  const leftMeta = document.createElement('div');
 
 
 
1707
  const authorSpan = document.createElement('span');
1708
  authorSpan.className = 'author-name';
1709
  authorSpan.style.marginLeft = '8px';
 
1712
 
1713
  metaInfo.appendChild(leftMeta);
1714
 
 
1715
  const likesDiv = document.createElement('div');
1716
  likesDiv.className = 'likes-wrapper';
1717
  likesDiv.innerHTML = `<span class="likes-heart">โ™ฅ</span><span>${likes_count}</span>`;
 
1720
  headerDiv.appendChild(metaInfo);
1721
  gridItem.appendChild(headerDiv);
1722
 
 
1723
  if (description) {
1724
  const descP = document.createElement('p');
1725
  descP.className = 'desc-text';
 
1727
  gridItem.appendChild(descP);
1728
  }
1729
 
 
1730
  const content = document.createElement('div');
1731
  content.className = 'grid-content';
1732
 
 
1734
  iframeContainer.className = 'iframe-container';
1735
 
1736
  const iframe = document.createElement('iframe');
1737
+ iframe.src = embedUrl;
1738
  iframe.title = title;
1739
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1740
  iframe.setAttribute('allowfullscreen', '');
 
1742
  iframe.loading = 'lazy';
1743
 
1744
  const spaceKey = `${owner}/${name}`;
1745
+ latestState.iframeStatuses[spaceKey] = 'loading';
1746
 
1747
  iframe.onload = function() {
1748
  iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1749
  };
1750
  iframe.onerror = function() {
1751
  handleIframeError(iframe, owner, name, title);
1752
+ latestState.iframeStatuses[spaceKey] = 'error';
1753
  };
1754
  setTimeout(() => {
1755
+ if (latestState.iframeStatuses[spaceKey] === 'loading') {
1756
  handleIframeError(iframe, owner, name, title);
1757
+ latestState.iframeStatuses[spaceKey] = 'error';
1758
  }
1759
  }, 30000);
1760
 
1761
  iframeContainer.appendChild(iframe);
1762
  content.appendChild(iframeContainer);
1763
 
 
1764
  const actions = document.createElement('div');
1765
  actions.className = 'grid-actions';
1766
 
 
1769
  linkEl.target = '_blank';
1770
  linkEl.className = 'open-link';
1771
  linkEl.textContent = 'Open in new window';
 
1772
  actions.appendChild(linkEl);
1773
+
1774
  gridItem.appendChild(content);
1775
  gridItem.appendChild(actions);
1776
 
1777
+ latestElements.gridContainer.appendChild(gridItem);
1778
 
1779
  } catch (err) {
1780
+ console.error('Item rendering error (Latest):', err);
1781
  }
1782
  });
1783
  }
1784
 
1785
+ function renderLatestPagination() {
1786
+ latestElements.pagination.innerHTML = '';
1787
+ const totalPages = Math.ceil(latestState.totalItems / latestState.itemsPerPage);
1788
+
1789
+ // Previous page
1790
+ const prevButton = document.createElement('button');
1791
+ prevButton.className = 'pagination-button';
1792
+ prevButton.textContent = 'Previous';
1793
+ prevButton.disabled = (latestState.currentPage === 0);
1794
+ prevButton.addEventListener('click', () => {
1795
+ if (latestState.currentPage > 0) {
1796
+ loadLatest(latestState.currentPage - 1);
1797
+ }
1798
+ });
1799
+ latestElements.pagination.appendChild(prevButton);
1800
+
1801
+ const maxButtons = 7;
1802
+ let startPage = Math.max(0, latestState.currentPage - Math.floor(maxButtons / 2));
1803
+ let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
1804
+
1805
+ if (endPage - startPage + 1 < maxButtons) {
1806
+ startPage = Math.max(0, endPage - maxButtons + 1);
1807
+ }
1808
 
1809
+ for (let i = startPage; i <= endPage; i++) {
1810
+ const pageButton = document.createElement('button');
1811
+ pageButton.className = 'pagination-button' + (i === latestState.currentPage ? ' active' : '');
1812
+ pageButton.textContent = (i + 1);
1813
+ pageButton.addEventListener('click', () => {
1814
+ if (i !== latestState.currentPage) {
1815
+ loadLatest(i);
1816
+ }
1817
+ });
1818
+ latestElements.pagination.appendChild(pageButton);
1819
+ }
1820
 
1821
+ // Next page
1822
+ const nextButton = document.createElement('button');
1823
+ nextButton.className = 'pagination-button';
1824
+ nextButton.textContent = 'Next';
1825
+ nextButton.disabled = (latestState.currentPage >= totalPages - 1);
1826
+ nextButton.addEventListener('click', () => {
1827
+ if (latestState.currentPage < totalPages - 1) {
1828
+ loadLatest(latestState.currentPage + 1);
1829
+ }
1830
+ });
1831
+ latestElements.pagination.appendChild(nextButton);
1832
+ }
1833
+
1834
+ // ------------------------------------
1835
+ // FIXED TAB
1836
+ // ------------------------------------
1837
+ const fixedGridContainer = document.getElementById('fixedGrid');
1838
  function renderFixedGrid() {
 
1839
  fixedGridContainer.innerHTML = '';
1840
 
1841
  const staticSpaces = [
 
1942
  iframe.setAttribute('frameborder', '0');
1943
  iframe.loading = 'lazy';
1944
 
 
1945
  iframe.onload = function() {
1946
+ iframeLoader.startChecking(iframe, owner, name, title, `${owner}/${name}`);
1947
  };
1948
  iframe.onerror = function() {
1949
  handleIframeError(iframe, owner, name, title);
 
1978
  });
1979
  }
1980
 
1981
+ // ------------------------------------
1982
+ // TAB HANDLERS
1983
+ // ------------------------------------
1984
+ const tabTrendingButton = document.getElementById('tabTrendingButton');
1985
+ const tabLatestButton = document.getElementById('tabLatestButton');
1986
+ const tabFixedButton = document.getElementById('tabFixedButton');
1987
+
1988
+ const trendingTab = document.getElementById('trendingTab');
1989
+ const latestTab = document.getElementById('latestTab');
1990
+ const fixedTab = document.getElementById('fixedTab');
1991
+
1992
  tabTrendingButton.addEventListener('click', () => {
1993
  tabTrendingButton.classList.add('active');
1994
+ tabLatestButton.classList.remove('active');
1995
  tabFixedButton.classList.remove('active');
1996
  trendingTab.classList.add('active');
1997
+ latestTab.classList.remove('active');
1998
+ fixedTab.classList.remove('active');
1999
+ loadTrending(trendingState.currentPage);
2000
+ });
2001
+
2002
+ tabLatestButton.addEventListener('click', () => {
2003
+ tabLatestButton.classList.add('active');
2004
+ tabTrendingButton.classList.remove('active');
2005
+ tabFixedButton.classList.remove('active');
2006
+ latestTab.classList.add('active');
2007
+ trendingTab.classList.remove('active');
2008
  fixedTab.classList.remove('active');
2009
+ loadLatest(latestState.currentPage);
2010
  });
2011
+
2012
  tabFixedButton.addEventListener('click', () => {
2013
  tabFixedButton.classList.add('active');
2014
  tabTrendingButton.classList.remove('active');
2015
+ tabLatestButton.classList.remove('active');
2016
  fixedTab.classList.add('active');
2017
  trendingTab.classList.remove('active');
2018
+ latestTab.classList.remove('active');
2019
  renderFixedGrid();
2020
  });
2021
 
2022
+ // ------------------------------------
2023
+ // EVENT LISTENERS
2024
+ // ------------------------------------
2025
+ trendingElements.searchInput.addEventListener('input', () => {
2026
+ clearTimeout(trendingState.searchTimeout);
2027
+ trendingState.searchTimeout = setTimeout(() => loadTrending(0), 300);
2028
+ });
2029
+ trendingElements.searchInput.addEventListener('keyup', (event) => {
2030
+ if (event.key === 'Enter') {
2031
+ loadTrending(0);
2032
+ }
2033
+ });
2034
+ trendingElements.refreshButton.addEventListener('click', () => loadTrending(0));
2035
+
2036
+ latestElements.searchInput.addEventListener('input', () => {
2037
+ clearTimeout(latestState.searchTimeout);
2038
+ latestState.searchTimeout = setTimeout(() => loadLatest(0), 300);
2039
  });
2040
+ latestElements.searchInput.addEventListener('keyup', (event) => {
2041
  if (event.key === 'Enter') {
2042
+ loadLatest(0);
2043
  }
2044
  });
2045
+ latestElements.refreshButton.addEventListener('click', () => loadLatest(0));
 
2046
 
2047
  window.addEventListener('load', function() {
2048
+ // ์ฒซ ์ง„์ž…์‹œ Trending ํƒญ ๋จผ์ € ๋กœ๋“œ
2049
+ setTimeout(() => loadTrending(0), 500);
2050
  });
2051
 
2052
  setTimeout(() => {
2053
+ if (globalState.isLoading) {
2054
  setLoading(false);
2055
+ // ํƒ€์ž„์•„์›ƒ ์‹œ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
2056
+ trendingElements.gridContainer.innerHTML = `
2057
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
2058
  <div style="font-size: 3rem; margin-bottom: 20px;">โฑ๏ธ</div>
2059
  <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3>
 
2065
  `;
2066
  }
2067
  }, 20000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2068
  </script>
2069
  </body>
2070
  </html>
2071
  ''')
2072
 
 
2073
  app.run(host='0.0.0.0', port=7860)