openfree commited on
Commit
f5b807d
ยท
verified ยท
1 Parent(s): 331da74

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +840 -300
app.py CHANGED
@@ -7,67 +7,104 @@ from collections import Counter
7
 
8
  app = Flask(__name__)
9
 
10
-
11
- ##########################################
12
- # 1) ์ฒซ ๋ฒˆ์งธ ํƒญ์šฉ: VIDraft/SanaSprint 6๊ฐœ
13
- ##########################################
14
- def fetch_trending_spaces(offset=0, limit=50):
15
- """
16
- ์›๋ž˜ Hugging Face /api/spaces๋ฅผ ํ˜ธ์ถœํ•˜๋˜ ๋ถ€๋ถ„์„
17
- ์ง์ ‘ ๋”๋ฏธ ๋ฐ์ดํ„ฐ(๋™์ผํ•œ URL 6๊ฐœ)๋งŒ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •.
18
- """
19
  try:
20
- # ์—ฌ๊ธฐ์„œ๋Š” 6๊ฐœ๋งŒ ๋งŒ๋“ค์–ด ๋ฐ˜ํ™˜
21
- # (์‹ค์ œ๋กœ offset/limit ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ , 6๊ฐœ๋งŒ ๋ฐ˜ํ™˜)
22
- my_spaces = []
23
- for i in range(6):
24
- my_spaces.append({
25
- 'id': 'VIDraft/SanaSprint',
26
- 'owner': 'VIDraft',
27
- 'title': f'SanaSprint #{i+1}',
28
- 'likes': 100 + i,
29
- 'createdAt': '2023-01-01T00:00:00.000Z'
30
- })
31
 
32
- return {
33
- 'spaces': my_spaces,
34
- 'total': 6, # ์ด 6๊ฐœ
35
- 'offset': 0,
36
- 'limit': 6,
37
- 'all_spaces': my_spaces # ํ†ต๊ณ„ ๊ณ„์‚ฐ์šฉ(์—ฌ๊ธฐ์„œ๋Š” 6๊ฐœ๋งŒ ๊ทธ๋Œ€๋กœ)
38
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  except Exception as e:
40
  print(f"Exception when fetching spaces: {e}")
41
- # ๋ฌธ์ œ ๋ฐœ์ƒ์‹œ, ํ˜น์€ ๋นˆ ๋ฐฐ์—ด ์ฃผ์–ด๋„ ๋จ
42
  return {
43
- 'spaces': [],
44
- 'total': 0,
45
- 'offset': 0,
46
- 'limit': 6,
47
- 'all_spaces': []
48
  }
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  def transform_url(owner, name):
51
- # ์ (.) -> ๋Œ€์‰ฌ(-), ์–ธ๋”๋ฐ”(_) -> ๋Œ€์‰ฌ(-), ์†Œ๋ฌธ์žํ™”
52
- name = name.replace('.', '-').replace('_', '-').lower()
 
 
 
53
  owner = owner.lower()
 
 
54
  return f"https://{owner}-{name}.hf.space"
55
 
 
56
  def get_space_details(space_data, index, offset):
57
  try:
 
58
  if '/' in space_data.get('id', ''):
59
  owner, name = space_data.get('id', '').split('/', 1)
60
  else:
61
  owner = space_data.get('owner', '')
62
  name = space_data.get('id', '')
63
 
 
64
  if owner == 'None' or name == 'None':
65
  return None
66
 
 
67
  original_url = f"https://huggingface.co/spaces/{owner}/{name}"
68
  embed_url = transform_url(owner, name)
 
 
69
  likes_count = space_data.get('likes', 0)
 
 
70
  title = space_data.get('title', name)
 
 
71
  tags = space_data.get('tags', [])
72
 
73
  return {
@@ -75,13 +112,14 @@ def get_space_details(space_data, index, offset):
75
  'embedUrl': embed_url,
76
  'title': title,
77
  'owner': owner,
78
- 'name': name,
79
  'likes_count': likes_count,
80
  'tags': tags,
81
  'rank': offset + index + 1
82
  }
83
  except Exception as e:
84
  print(f"Error processing space data: {e}")
 
85
  return {
86
  'url': 'https://huggingface.co/spaces',
87
  'embedUrl': 'https://huggingface.co/spaces',
@@ -93,6 +131,7 @@ def get_space_details(space_data, index, offset):
93
  'rank': offset + index + 1
94
  }
95
 
 
96
  def get_owner_stats(all_spaces):
97
  owners = []
98
  for space in all_spaces:
@@ -104,89 +143,53 @@ def get_owner_stats(all_spaces):
104
  if owner != 'None':
105
  owners.append(owner)
106
 
 
107
  owner_counts = Counter(owners)
 
 
108
  top_owners = owner_counts.most_common(30)
109
- return top_owners
110
-
111
- ##########################################
112
- # 2) ๋‘ ๋ฒˆ์งธ ํƒญ์šฉ: VIDraft/SanaSprint
113
- # (์›ํ•˜๋Š” ๊ฐœ์ˆ˜๋งŒํผ - ์—ฌ๊ธฐ์„  ์˜ˆ์‹œ๋กœ 6๊ฐœ)
114
- ##########################################
115
- CUSTOM_SPACES = []
116
- for i in range(6):
117
- CUSTOM_SPACES.append({
118
- 'owner': 'VIDraft',
119
- 'name': 'SanaSprint',
120
- 'title': f'SanaSprint Demo #{i+1}',
121
- 'likes': 500 + i,
122
- })
123
-
124
- @app.route('/api/custom-spaces', methods=['GET'])
125
- def custom_spaces():
126
- """
127
- ๋‘ ๋ฒˆ์งธ ํƒญ์—์„œ ํ‘œ์‹œํ•  Space๋“ค์„ ๋ฐ˜ํ™˜ (์—ฌ๊ธฐ์„œ๋„ 6๊ฐœ).
128
- """
129
- results = []
130
- for index, item in enumerate(CUSTOM_SPACES):
131
- owner = item['owner']
132
- name = item['name']
133
- title = item.get('title', name)
134
- likes_count = item.get('likes', 0)
135
-
136
- results.append({
137
- 'url': f"https://huggingface.co/spaces/{owner}/{name}",
138
- 'embedUrl': transform_url(owner, name),
139
- 'title': title,
140
- 'owner': owner,
141
- 'name': name,
142
- 'likes_count': likes_count,
143
- 'tags': [],
144
- 'rank': index + 1
145
- })
146
 
147
- return jsonify({
148
- 'spaces': results,
149
- 'total': len(results),
150
- })
151
-
152
 
 
153
  @app.route('/')
154
  def home():
155
  return render_template('index.html')
156
 
 
157
  @app.route('/api/trending-spaces', methods=['GET'])
158
  def trending_spaces():
159
- """
160
- ์ฒซ ๋ฒˆ์งธ ํƒญ: ์œ„์—์„œ ๋งŒ๋“  fetch_trending_spaces()์˜ ๊ฒฐ๊ณผ๋ฅผ
161
- ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋ง (์‹ค์ œ๋กœ๋Š” 6๊ฐœ ์งœ๋ฆฌ ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜).
162
- """
163
  search_query = request.args.get('search', '').lower()
164
  offset = int(request.args.get('offset', 0))
165
- limit = int(request.args.get('limit', 50))
166
-
 
167
  spaces_data = fetch_trending_spaces(offset, limit)
168
 
 
169
  results = []
170
  for index, space_data in enumerate(spaces_data['spaces']):
171
  space_info = get_space_details(space_data, index, offset)
 
172
  if not space_info:
173
  continue
174
-
175
- # ๊ฒ€์ƒ‰์–ด๊ฐ€ ์žˆ์œผ๋ฉด ํ•„ํ„ฐ๋ง(์‚ฌ์šฉ ์•ˆ ํ•˜์…”๋„ ๋จ)
176
  if search_query:
177
  title = space_info['title'].lower()
178
  owner = space_info['owner'].lower()
179
  url = space_info['url'].lower()
180
  tags = ' '.join([str(tag) for tag in space_info.get('tags', [])]).lower()
181
 
182
- if (search_query not in title
183
- and search_query not in owner
184
- and search_query not in url
185
- and search_query not in tags):
186
  continue
187
 
188
  results.append(space_info)
189
 
 
190
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
191
 
192
  return jsonify({
@@ -194,21 +197,24 @@ def trending_spaces():
194
  'total': spaces_data['total'],
195
  'offset': offset,
196
  'limit': limit,
197
- 'top_owners': top_owners
198
  })
199
 
200
  if __name__ == '__main__':
 
201
  os.makedirs('templates', exist_ok=True)
202
 
 
203
  with open('templates/index.html', 'w', encoding='utf-8') as f:
204
  f.write('''<!DOCTYPE html>
205
  <html lang="en">
206
  <head>
207
  <meta charset="UTF-8">
 
208
  <title>Huggingface Spaces Gallery</title>
209
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
210
  <style>
211
- /* ๊ธฐ์กด ์Šคํƒ€์ผ ๋™์ผ (์ƒ๋žต ๊ฐ€๋Šฅ) */
212
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
213
 
214
  :root {
@@ -230,11 +236,15 @@ if __name__ == '__main__':
230
  --text-secondary: #666;
231
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
232
  }
 
233
  * {
234
- margin: 0; padding: 0; box-sizing: border-box;
 
 
235
  }
 
236
  body {
237
- font-family: 'Nunito', sans-serif;
238
  line-height: 1.6;
239
  color: var(--text-primary);
240
  background-color: #f8f9fa;
@@ -242,10 +252,13 @@ if __name__ == '__main__':
242
  min-height: 100vh;
243
  padding: 2rem;
244
  }
 
245
  .container {
246
  max-width: 1600px;
247
  margin: 0 auto;
248
  }
 
 
249
  .mac-window {
250
  background-color: var(--mac-window-bg);
251
  border-radius: 10px;
@@ -255,6 +268,7 @@ if __name__ == '__main__':
255
  margin-bottom: 2rem;
256
  border: 1px solid var(--mac-border);
257
  }
 
258
  .mac-toolbar {
259
  display: flex;
260
  align-items: center;
@@ -262,222 +276,503 @@ if __name__ == '__main__':
262
  background-color: var(--mac-toolbar);
263
  border-bottom: 1px solid var(--mac-border);
264
  }
 
265
  .mac-buttons {
266
- display: flex; gap: 8px; margin-right: 15px;
 
 
267
  }
 
268
  .mac-button {
269
- width: 12px; height: 12px; border-radius: 50%; cursor: default;
 
 
 
 
 
 
 
270
  }
271
- .mac-close { background-color: var(--mac-button-red); }
272
- .mac-minimize { background-color: var(--mac-button-yellow); }
273
- .mac-maximize { background-color: var(--mac-button-green); }
 
 
 
 
 
 
274
  .mac-title {
275
- flex-grow: 1; text-align: center; font-size: 0.9rem; color: var(--text-secondary);
 
 
 
 
 
 
 
276
  }
277
- .mac-content { padding: 20px; }
 
278
  .header {
279
  text-align: center;
280
- margin-bottom: 1.5rem; position: relative;
 
281
  }
 
282
  .header h1 {
283
- font-size: 2.2rem; font-weight: 700; margin: 0; color: #2d3748; letter-spacing: -0.5px;
 
 
 
 
284
  }
 
285
  .header p {
286
  color: var(--text-secondary);
287
  margin-top: 0.5rem;
288
  font-size: 1.1rem;
289
  }
 
 
290
  .tab-nav {
291
- display: flex; gap: 20px; justify-content: center; margin-bottom: 20px;
 
 
292
  }
 
293
  .tab-button {
294
- padding: 10px 20px; border-radius: 20px; border: none; cursor: pointer;
295
- background-color: #ffffffcc; font-weight: 600; transition: 0.2s;
296
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
297
- }
298
- .tab-button:hover {
299
- background-color: var(--pastel-green);
 
 
 
300
  }
 
301
  .tab-button.active {
302
  background-color: var(--pastel-purple);
303
- color: #4a5568;
 
 
 
 
 
 
 
 
304
  }
305
- .tab-content { display: none; }
306
- .tab-content.active { display: block; }
307
  .search-bar {
308
- display: flex; align-items: center; margin-bottom: 1.5rem;
309
- background-color: white; border-radius: 30px; padding: 5px;
310
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); max-width: 600px;
311
- margin-left: auto; margin-right: auto;
 
 
 
 
 
 
312
  }
 
313
  .search-bar input {
314
- flex-grow: 1; border: none; padding: 12px 20px; font-size: 1rem;
315
- outline: none; background: transparent; border-radius: 30px;
 
 
 
 
 
316
  }
 
317
  .search-bar .refresh-btn {
318
- background-color: var(--pastel-green); color: #1a202c; border: none;
319
- border-radius: 30px; padding: 10px 20px; font-size: 1rem; font-weight: 600;
320
- cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 8px;
 
 
 
 
 
 
 
 
 
321
  }
 
322
  .search-bar .refresh-btn:hover {
323
  background-color: #9ee7c0;
324
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
325
  }
 
326
  .refresh-icon {
327
- display: inline-block; width: 16px; height: 16px; border: 2px solid #1a202c;
328
- border-top-color: transparent; border-radius: 50%; animation: none;
 
 
 
 
 
329
  }
 
330
  .refreshing .refresh-icon {
331
  animation: spin 1s linear infinite;
332
  }
 
333
  @keyframes spin {
334
  0% { transform: rotate(0deg); }
335
  100% { transform: rotate(360deg); }
336
  }
 
 
337
  .grid-container {
338
  display: grid;
339
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
340
  gap: 1.5rem;
341
  margin-bottom: 2rem;
342
  }
 
343
  .grid-item {
344
- height: 500px; position: relative; overflow: hidden;
345
- transition: all 0.3s ease; border-radius: 15px;
 
 
 
346
  }
 
347
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
348
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
349
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
350
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
351
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
352
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
 
353
  .grid-item:hover {
354
  transform: translateY(-5px);
355
  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
356
  }
 
357
  .grid-header {
358
  padding: 15px;
359
- display: flex; flex-direction: column;
 
360
  background-color: rgba(255, 255, 255, 0.7);
361
  backdrop-filter: blur(5px);
362
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
363
  }
 
364
  .grid-header-top {
365
- display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;
 
 
 
366
  }
 
367
  .rank-badge {
368
- background-color: #1a202c; color: white; font-size: 0.8rem; font-weight: 600;
369
- padding: 4px 8px; border-radius: 50px;
 
 
 
 
370
  }
 
371
  .grid-header h3 {
372
- margin: 0; font-size: 1.2rem; font-weight: 700;
373
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
 
 
 
 
374
  }
 
375
  .grid-meta {
376
- display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem;
 
 
 
 
 
 
 
 
377
  }
378
- .owner-info { color: var(--text-secondary); font-weight: 500; }
379
  .likes-counter {
380
- display: flex; align-items: center; color: #e53e3e; font-weight: 600;
 
 
 
 
 
 
 
381
  }
382
- .likes-counter span { margin-left: 4px; }
383
  .grid-actions {
384
- padding: 10px 15px; text-align: right; background-color: rgba(255, 255, 255, 0.7);
385
- backdrop-filter: blur(5px); position: absolute; bottom: 0; left: 0; right: 0;
386
- z-index: 10; display: flex; justify-content: flex-end;
 
 
 
 
 
 
 
 
387
  }
 
388
  .open-link {
389
- text-decoration: none; color: #2c5282; font-weight: 600;
390
- padding: 5px 10px; border-radius: 5px; transition: all 0.2s;
 
 
 
 
391
  background-color: rgba(237, 242, 247, 0.8);
392
  }
393
- .open-link:hover { background-color: #e2e8f0; }
 
 
 
 
394
  .grid-content {
395
- position: absolute; top: 0; left: 0; width: 100%; height: 100%;
396
- padding-top: 85px; padding-bottom: 45px;
 
 
 
 
 
397
  }
 
398
  .iframe-container {
399
- width: 100%; height: 100%; overflow: hidden; position: relative;
 
 
 
400
  }
 
 
401
  .grid-content iframe {
402
- transform: scale(0.7); transform-origin: top left;
403
- width: 142.857%; height: 142.857%; border: none; border-radius: 0;
 
 
 
 
404
  }
 
405
  .error-placeholder {
406
- position: absolute; top: 0; left: 0; width: 100%; height: 100%;
407
- display: flex; flex-direction: column; justify-content: center; align-items: center;
408
- padding: 20px; background-color: rgba(255, 255, 255, 0.9);
 
 
 
 
 
 
 
 
409
  text-align: center;
410
  }
 
411
  .error-emoji {
412
- font-size: 6rem; margin-bottom: 1.5rem;
 
413
  animation: bounce 1s infinite alternate;
414
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
415
  }
 
416
  @keyframes bounce {
417
- from { transform: translateY(0px) scale(1); }
418
- to { transform: translateY(-15px) scale(1.1); }
 
 
 
 
419
  }
 
 
420
  .pagination {
421
- display: flex; justify-content: center; align-items: center; gap: 10px;
 
 
 
422
  margin: 2rem 0;
423
  }
 
424
  .pagination-button {
425
- background-color: white; border: none; padding: 10px 20px; border-radius: 10px;
426
- font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s;
427
- color: var(--text-primary); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
 
 
 
 
 
 
 
428
  }
 
429
  .pagination-button:hover {
430
- background-color: #f8f9fa; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
 
431
  }
 
432
  .pagination-button.active {
433
- background-color: var(--pastel-purple); color: #4a5568;
 
434
  }
 
435
  .pagination-button:disabled {
436
- background-color: #edf2f7; color: #a0aec0;
437
- cursor: default; box-shadow: none;
 
 
438
  }
 
 
439
  .loading {
440
- position: fixed; top: 0; left: 0; right: 0; bottom: 0;
441
- background-color: rgba(255, 255, 255, 0.8); backdrop-filter: blur(5px);
442
- display: flex; justify-content: center; align-items: center; z-index: 1000;
 
 
 
 
 
 
 
 
443
  }
444
- .loading-content { text-align: center; }
 
 
 
 
445
  .loading-spinner {
446
- width: 60px; height: 60px; border: 5px solid #e2e8f0;
447
- border-top-color: var(--pastel-purple); border-radius: 50%;
448
- animation: spin 1s linear infinite; margin: 0 auto 15px;
 
 
 
 
449
  }
 
450
  .loading-text {
451
- font-size: 1.2rem; font-weight: 600; color: #4a5568;
 
 
452
  }
 
453
  .loading-error {
454
- display: none; margin-top: 10px; color: #e53e3e; font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
455
  }
456
- .stats-window { margin-top: 2rem; margin-bottom: 2rem; }
457
  .stats-header {
458
- display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;
 
 
 
 
 
 
 
 
 
459
  }
460
- .stats-title { font-size: 1.5rem; font-weight: 700; color: #2d3748; }
461
  .stats-toggle {
462
- background-color: var(--pastel-blue); border: none; padding: 8px 16px;
463
- border-radius: 20px; font-weight: 600; cursor: pointer; transition: all 0.2s;
 
 
 
 
 
 
 
 
 
464
  }
465
- .stats-toggle:hover { background-color: var(--pastel-purple); }
466
  .stats-content {
467
- background-color: white; border-radius: 10px; padding: 20px;
468
- box-shadow: var(--box-shadow); max-height: 0; overflow: hidden;
 
 
 
 
469
  transition: max-height 0.5s ease-out;
470
  }
471
- .stats-content.open { max-height: 600px; }
472
- .chart-container { width: 100%; height: 500px; }
 
 
 
 
 
 
 
 
 
473
  @media (max-width: 768px) {
474
- body { padding: 1rem; }
475
- .grid-container { grid-template-columns: 1fr; }
476
- .search-bar { flex-direction: column; padding: 10px; }
477
- .search-bar input { width: 100%; margin-bottom: 10px; }
478
- .search-bar .refresh-btn { width: 100%; justify-content: center; }
479
- .pagination { flex-wrap: wrap; }
480
- .chart-container { height: 300px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  }
482
  </style>
483
  </head>
@@ -496,17 +791,18 @@ if __name__ == '__main__':
496
  <div class="mac-content">
497
  <div class="header">
498
  <h1>HF Space Leaderboard</h1>
499
- <p>์ฒซ ๋ฒˆ์งธ ํƒญ: ๋™์ผํ•œ URL 6๊ฐœ / ๋‘ ๋ฒˆ์งธ ํƒญ: ๋™์ผํ•œ URL 6๊ฐœ</p>
500
  </div>
501
 
502
- <!-- ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ -->
503
  <div class="tab-nav">
504
- <button class="tab-button active" data-tab="leaderboardTab">์ฒซ ๋ฒˆ์งธ ํƒญ</button>
505
- <button class="tab-button" data-tab="customTab">๋‘ ๋ฒˆ์งธ ํƒญ</button>
506
  </div>
507
 
508
- <!-- ์ฒซ ๋ฒˆ์งธ ํƒญ (๋ฆฌ๋”๋ณด๋“œ) -->
509
- <div id="leaderboardTab" class="tab-content active">
 
510
  <div class="stats-window mac-window">
511
  <div class="mac-toolbar">
512
  <div class="mac-buttons">
@@ -536,20 +832,23 @@ if __name__ == '__main__':
536
  Refresh
537
  </button>
538
  </div>
 
539
  <div id="gridContainer" class="grid-container"></div>
540
- <div id="pagination" class="pagination"></div>
 
 
 
541
  </div>
542
 
543
- <!-- ๋‘ ๋ฒˆ์งธ ํƒญ (์ปค์Šคํ…€) -->
544
- <div id="customTab" class="tab-content">
545
- <h2 style="text-align:center; margin-bottom:1rem;">๋‘ ๋ฒˆ์งธ ํƒญ: ๋™์ผํ•œ URL 6๊ฐœ</h2>
546
- <div id="customGridContainer" class="grid-container"></div>
547
  </div>
548
  </div>
549
  </div>
550
  </div>
551
 
552
- <div id="loadingIndicator" class="loading" style="display:none;">
553
  <div class="loading-content">
554
  <div class="loading-spinner"></div>
555
  <div class="loading-text">Loading amazing spaces...</div>
@@ -560,9 +859,9 @@ if __name__ == '__main__':
560
  </div>
561
 
562
  <script>
 
563
  const elements = {
564
  gridContainer: document.getElementById('gridContainer'),
565
- customGridContainer: document.getElementById('customGridContainer'),
566
  loadingIndicator: document.getElementById('loadingIndicator'),
567
  loadingError: document.getElementById('loadingError'),
568
  searchInput: document.getElementById('searchInput'),
@@ -573,56 +872,85 @@ if __name__ == '__main__':
573
  creatorStatsChart: document.getElementById('creatorStatsChart')
574
  };
575
 
 
 
 
 
 
 
 
 
576
  const state = {
577
  isLoading: false,
578
  spaces: [],
579
  currentPage: 0,
580
- itemsPerPage: 50,
581
  totalItems: 0,
582
  loadingTimeout: null,
583
- staticModeAttempted: {},
584
  statsVisible: false,
585
  chartInstance: null,
586
  topOwners: [],
587
- iframeStatuses: {},
588
- customSpaces: []
589
  };
590
 
 
591
  const iframeLoader = {
592
  checkQueue: {},
593
- maxAttempts: 5,
594
- checkInterval: 5000,
595
 
 
596
  startChecking: function(iframe, owner, name, title, spaceKey) {
 
597
  this.checkQueue[spaceKey] = {
598
- iframe, owner, name, title,
 
 
 
599
  attempts: 0,
600
  status: 'loading'
601
  };
 
 
602
  this.checkIframeStatus(spaceKey);
603
  },
 
 
604
  checkIframeStatus: function(spaceKey) {
605
  if (!this.checkQueue[spaceKey]) return;
 
606
  const item = this.checkQueue[spaceKey];
607
  const iframe = item.iframe;
608
 
 
609
  if (item.status !== 'loading') {
610
  delete this.checkQueue[spaceKey];
611
  return;
612
  }
613
 
 
614
  item.attempts++;
 
615
  try {
 
616
  if (!iframe || !iframe.parentNode) {
617
  delete this.checkQueue[spaceKey];
618
  return;
619
  }
 
 
620
  try {
621
- const hasContent = iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.body;
 
 
 
 
622
  if (hasContent && iframe.contentWindow.document.body.innerHTML.length > 100) {
 
623
  const bodyText = iframe.contentWindow.document.body.textContent.toLowerCase();
624
- if (bodyText.includes('forbidden') ||
625
- bodyText.includes('404') ||
626
  bodyText.includes('not found') ||
627
  bodyText.includes('error')) {
628
  item.status = 'error';
@@ -633,19 +961,27 @@ if __name__ == '__main__':
633
  delete this.checkQueue[spaceKey];
634
  return;
635
  }
636
- } catch(e) {}
 
 
637
 
 
638
  const rect = iframe.getBoundingClientRect();
639
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
 
640
  item.status = 'success';
641
  delete this.checkQueue[spaceKey];
642
  return;
643
  }
644
 
 
645
  if (item.attempts >= this.maxAttempts) {
 
646
  if (iframe.offsetWidth > 0 && iframe.offsetHeight > 0) {
 
647
  item.status = 'success';
648
  } else {
 
649
  item.status = 'error';
650
  handleIframeError(iframe, item.owner, item.name, item.title);
651
  }
@@ -653,42 +989,57 @@ if __name__ == '__main__':
653
  return;
654
  }
655
 
 
656
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
657
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
658
- } catch(e) {
 
659
  console.error('Error checking iframe status:', e);
 
 
660
  if (item.attempts >= this.maxAttempts) {
661
  item.status = 'error';
662
  handleIframeError(iframe, item.owner, item.name, item.title);
663
  delete this.checkQueue[spaceKey];
664
  } else {
 
665
  setTimeout(() => this.checkIframeStatus(spaceKey), this.checkInterval);
666
  }
667
  }
668
  }
669
  };
670
 
 
671
  function toggleStats() {
672
  state.statsVisible = !state.statsVisible;
673
  elements.statsContent.classList.toggle('open', state.statsVisible);
674
  elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
 
675
  if (state.statsVisible && state.topOwners.length > 0) {
676
  renderCreatorStats();
677
  }
678
  }
679
 
 
680
  function renderCreatorStats() {
681
  if (state.chartInstance) {
682
  state.chartInstance.destroy();
683
  }
 
684
  const ctx = elements.creatorStatsChart.getContext('2d');
 
 
685
  const labels = state.topOwners.map(item => item[0]);
686
  const data = state.topOwners.map(item => item[1]);
 
 
687
  const colors = [];
688
  for (let i = 0; i < labels.length; i++) {
689
  const hue = (i * 360 / labels.length) % 360;
690
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
691
  }
 
 
692
  state.chartInstance = new Chart(ctx, {
693
  type: 'bar',
694
  data: {
@@ -697,7 +1048,7 @@ if __name__ == '__main__':
697
  label: 'Number of Spaces',
698
  data: data,
699
  backgroundColor: colors,
700
- borderColor: colors.map(c => c.replace('0.7','1')),
701
  borderWidth: 1
702
  }]
703
  },
@@ -706,21 +1057,33 @@ if __name__ == '__main__':
706
  responsive: true,
707
  maintainAspectRatio: false,
708
  plugins: {
709
- legend: { display: false },
 
 
710
  tooltip: {
711
  callbacks: {
712
- title: function(tooltipItems) { return tooltipItems[0].label; },
713
- label: function(context) { return \`Spaces: \${context.raw}\`; }
 
 
 
 
714
  }
715
  }
716
  },
717
  scales: {
718
  x: {
719
  beginAtZero: true,
720
- title: { display: true, text: 'Number of Spaces' }
 
 
 
721
  },
722
  y: {
723
- title: { display: true, text: 'Creator ID' },
 
 
 
724
  ticks: {
725
  autoSkip: false,
726
  font: function(context) {
@@ -736,70 +1099,92 @@ if __name__ == '__main__':
736
  });
737
  }
738
 
739
- async function loadSpaces(page=0) {
 
740
  setLoading(true);
 
741
  try {
742
  const searchText = elements.searchInput.value;
743
  const offset = page * state.itemsPerPage;
 
 
744
  const timeoutPromise = new Promise((_, reject) =>
745
  setTimeout(() => reject(new Error('Request timeout')), 30000)
746
  );
747
- const fetchPromise = fetch(\`/api/trending-spaces?search=\${encodeURIComponent(searchText)}&offset=\${offset}&limit=\${state.itemsPerPage}\`);
 
 
 
748
  const response = await Promise.race([fetchPromise, timeoutPromise]);
749
  const data = await response.json();
750
 
 
751
  state.spaces = data.spaces;
752
  state.totalItems = data.total;
753
  state.currentPage = page;
754
  state.topOwners = data.top_owners || [];
755
 
756
- renderGrid(data.spaces, elements.gridContainer);
757
  renderPagination();
758
 
 
759
  if (state.statsVisible && state.topOwners.length > 0) {
760
  renderCreatorStats();
761
  }
762
- } catch(e) {
763
- console.error(e);
764
- elements.gridContainer.innerHTML = \`
 
 
765
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
766
  <div style="font-size: 3rem; margin-bottom: 20px;">โš ๏ธ</div>
767
  <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
768
- <p style="color: #666;">Please try refreshing the page.</p>
769
  <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
770
  Try Again
771
  </button>
772
  </div>
773
- \`;
 
 
774
  document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
 
775
  renderPagination();
776
  } finally {
777
  setLoading(false);
778
  }
779
  }
780
 
 
781
  function renderPagination() {
782
  elements.pagination.innerHTML = '';
 
783
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
784
 
 
785
  const prevButton = document.createElement('button');
786
- prevButton.className = \`pagination-button \${state.currentPage === 0 ? 'disabled' : ''}\`;
787
  prevButton.textContent = 'Previous';
788
  prevButton.disabled = state.currentPage === 0;
789
  prevButton.addEventListener('click', () => {
790
- if (state.currentPage > 0) loadSpaces(state.currentPage - 1);
 
 
791
  });
792
  elements.pagination.appendChild(prevButton);
793
 
 
794
  const maxButtons = 7;
795
  let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
796
  let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
 
797
  if (endPage - startPage + 1 < maxButtons) {
798
  startPage = Math.max(0, endPage - maxButtons + 1);
799
  }
 
800
  for (let i = startPage; i <= endPage; i++) {
801
  const pageButton = document.createElement('button');
802
- pageButton.className = \`pagination-button \${i === state.currentPage ? 'active' : ''}\`;
803
  pageButton.textContent = i + 1;
804
  pageButton.addEventListener('click', () => {
805
  if (i !== state.currentPage) {
@@ -809,8 +1194,9 @@ if __name__ == '__main__':
809
  elements.pagination.appendChild(pageButton);
810
  }
811
 
 
812
  const nextButton = document.createElement('button');
813
- nextButton.className = \`pagination-button \${state.currentPage >= totalPages - 1 ? 'disabled' : ''}\`;
814
  nextButton.textContent = 'Next';
815
  nextButton.disabled = state.currentPage >= totalPages - 1;
816
  nextButton.addEventListener('click', () => {
@@ -821,17 +1207,19 @@ if __name__ == '__main__':
821
  elements.pagination.appendChild(nextButton);
822
  }
823
 
 
824
  function handleIframeError(iframe, owner, name, title) {
825
  const container = iframe.parentNode;
 
826
  const errorPlaceholder = document.createElement('div');
827
  errorPlaceholder.className = 'error-placeholder';
828
 
829
  const errorMessage = document.createElement('p');
830
- errorMessage.textContent = \`"\${title}" space couldn't be loaded\`;
831
  errorPlaceholder.appendChild(errorMessage);
832
 
833
  const directLink = document.createElement('a');
834
- directLink.href = \`https://huggingface.co/spaces/\${owner}/\${name}\`;
835
  directLink.target = '_blank';
836
  directLink.textContent = 'Visit HF Space';
837
  directLink.style.color = '#3182ce';
@@ -847,23 +1235,28 @@ if __name__ == '__main__':
847
  container.appendChild(errorPlaceholder);
848
  }
849
 
850
- function renderGrid(spaces, containerEl) {
851
- containerEl.innerHTML = '';
 
 
852
  if (!spaces || spaces.length === 0) {
853
  const noResultsMsg = document.createElement('p');
854
- noResultsMsg.textContent = 'No spaces found.';
855
  noResultsMsg.style.padding = '2rem';
856
  noResultsMsg.style.textAlign = 'center';
857
  noResultsMsg.style.fontStyle = 'italic';
858
  noResultsMsg.style.color = '#718096';
859
- containerEl.appendChild(noResultsMsg);
860
  return;
861
  }
862
 
863
- spaces.forEach(item => {
864
  try {
865
  const { url, title, likes_count, owner, name, rank } = item;
866
- if (owner === 'None') return;
 
 
 
867
 
868
  const gridItem = document.createElement('div');
869
  gridItem.className = 'grid-item';
@@ -881,8 +1274,9 @@ if __name__ == '__main__':
881
 
882
  const rankBadge = document.createElement('div');
883
  rankBadge.className = 'rank-badge';
884
- rankBadge.textContent = \`#\${rank}\`;
885
  headerTop.appendChild(rankBadge);
 
886
  header.appendChild(headerTop);
887
 
888
  const metaInfo = document.createElement('div');
@@ -890,7 +1284,7 @@ if __name__ == '__main__':
890
 
891
  const ownerEl = document.createElement('div');
892
  ownerEl.className = 'owner-info';
893
- ownerEl.textContent = \`by \${owner}\`;
894
  metaInfo.appendChild(ownerEl);
895
 
896
  const likesCounter = document.createElement('div');
@@ -916,16 +1310,18 @@ if __name__ == '__main__':
916
  iframe.setAttribute('frameborder', '0');
917
  iframe.loading = 'lazy';
918
 
919
- const spaceKey = \`\${owner}/\${name}\`;
920
  state.iframeStatuses[spaceKey] = 'loading';
921
 
922
  iframe.onload = function() {
923
  iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
924
  };
 
925
  iframe.onerror = function() {
926
  handleIframeError(iframe, owner, name, title);
927
  state.iframeStatuses[spaceKey] = 'error';
928
  };
 
929
  setTimeout(() => {
930
  if (state.iframeStatuses[spaceKey] === 'loading') {
931
  handleIframeError(iframe, owner, name, title);
@@ -949,84 +1345,195 @@ if __name__ == '__main__':
949
  gridItem.appendChild(content);
950
  gridItem.appendChild(actions);
951
 
952
- containerEl.appendChild(gridItem);
953
- } catch(e) {
954
- console.error('Item rendering error:', e);
955
  }
956
  });
957
  }
958
 
959
- function createDirectUrl(owner, name) {
960
- try {
961
- name = name.replace(/\\./g, '-').replace(/_/g, '-').toLowerCase();
962
- owner = owner.toLowerCase();
963
- return \`https://\${owner}-\${name}.hf.space\`;
964
- } catch(e) {
965
- console.error(e);
966
- return 'https://huggingface.co';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
967
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
968
  }
969
 
970
- async function loadCustomSpaces() {
971
- try {
972
- const resp = await fetch('/api/custom-spaces');
973
- const data = await resp.json();
974
- state.customSpaces = data.spaces || [];
975
- renderGrid(state.customSpaces, elements.customGridContainer);
976
- } catch(e) {
977
- console.error(e);
978
- elements.customGridContainer.innerHTML = '<p style="text-align:center; color:red;">Error loading custom spaces.</p>';
979
- }
980
- }
981
 
982
- document.querySelectorAll('.mac-button').forEach(button => {
983
- button.addEventListener('click', (e) => {
984
- e.preventDefault();
985
- });
 
 
 
986
  });
987
 
988
- function setLoading(isLoading) {
989
- state.isLoading = isLoading;
990
- elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
991
- if (isLoading) {
992
- elements.refreshButton.classList.add('refreshing');
993
- clearTimeout(state.loadingTimeout);
994
- state.loadingTimeout = setTimeout(() => {
995
- elements.loadingError.style.display = 'block';
996
- }, 10000);
997
- } else {
998
- elements.refreshButton.classList.remove('refreshing');
999
- clearTimeout(state.loadingTimeout);
1000
- elements.loadingError.style.display = 'none';
1001
  }
1002
- }
1003
 
1004
- // ํƒญ ์ „ํ™˜
1005
- const tabButtons = document.querySelectorAll('.tab-button');
1006
- const tabContents = document.querySelectorAll('.tab-content');
1007
- tabButtons.forEach(btn => {
1008
- btn.addEventListener('click', () => {
1009
- tabButtons.forEach(b => b.classList.remove('active'));
1010
- tabContents.forEach(tc => tc.classList.remove('active'));
1011
- btn.classList.add('active');
1012
- const tabId = btn.getAttribute('data-tab');
1013
- document.getElementById(tabId).classList.add('active');
1014
  });
1015
  });
1016
 
1017
- // ์ดˆ๊ธฐ ์‹คํ–‰
1018
  window.addEventListener('load', function() {
1019
- // ์ฒซ ๋ฒˆ์งธ ํƒญ ๋กœ๋“œ
1020
- loadSpaces(0);
1021
- // ๋‘ ๋ฒˆ์งธ ํƒญ๋„ ๋กœ๋“œ
1022
- loadCustomSpaces();
1023
  });
1024
 
1025
- // ํ˜น์‹œ๋‚˜ ๋กœ๋”ฉ์ด ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ฆด ๋•Œ ์•ˆ์ „์žฅ์น˜
1026
  setTimeout(() => {
1027
  if (state.isLoading) {
1028
  setLoading(false);
1029
- elements.gridContainer.innerHTML = \`
1030
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1031
  <div style="font-size: 3rem; margin-bottom: 20px;">โฑ๏ธ</div>
1032
  <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3>
@@ -1035,13 +1542,46 @@ if __name__ == '__main__':
1035
  Reload Page
1036
  </button>
1037
  </div>
1038
- \`;
1039
  }
1040
  }, 20000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1041
  </script>
1042
  </body>
1043
  </html>
1044
  ''')
1045
 
1046
- # Huggingface Spaces์—์„œ ์ผ๋ฐ˜์ ์œผ๋กœ 7860 ํฌํŠธ ์‚ฌ์šฉ
1047
  app.run(host='0.0.0.0', port=7860)
 
7
 
8
  app = Flask(__name__)
9
 
10
+ # Function to fetch trending spaces from Huggingface with pagination
11
+ def fetch_trending_spaces(offset=0, limit=72):
 
 
 
 
 
 
 
12
  try:
13
+ # Simple data fetching
14
+ url = "https://huggingface.co/api/spaces"
15
+ params = {"limit": 10000} # Get max 10000 to fetch more spaces
 
 
 
 
 
 
 
 
16
 
17
+ # Increase timeout
18
+ response = requests.get(url, params=params, timeout=30)
19
+
20
+ if response.status_code == 200:
21
+ spaces = response.json()
22
+ filtered_spaces = [space for space in spaces if space.get('owner') != 'None' and space.get('id', '').split('/', 1)[0] != 'None']
23
+
24
+ # Slice according to requested offset and limit
25
+ start = min(offset, len(filtered_spaces))
26
+ end = min(offset + limit, len(filtered_spaces))
27
+
28
+ print(f"Fetched {len(filtered_spaces)} spaces, returning {end-start} items from {start} to {end}")
29
+
30
+ return {
31
+ 'spaces': filtered_spaces[start:end],
32
+ 'total': len(filtered_spaces),
33
+ 'offset': offset,
34
+ 'limit': limit,
35
+ 'all_spaces': filtered_spaces # Return all spaces for stats calculation
36
+ }
37
+ else:
38
+ print(f"Error fetching spaces: {response.status_code}")
39
+ # Return empty spaces with fake 200 limit data
40
+ return {
41
+ 'spaces': generate_dummy_spaces(limit),
42
+ 'total': 200,
43
+ 'offset': offset,
44
+ 'limit': limit,
45
+ 'all_spaces': generate_dummy_spaces(500) # Dummy data for stats
46
+ }
47
  except Exception as e:
48
  print(f"Exception when fetching spaces: {e}")
49
+ # Generate fake data
50
  return {
51
+ 'spaces': generate_dummy_spaces(limit),
52
+ 'total': 200,
53
+ 'offset': offset,
54
+ 'limit': limit,
55
+ 'all_spaces': generate_dummy_spaces(500) # Dummy data for stats
56
  }
57
 
58
+ # Generate dummy spaces in case of error
59
+ def generate_dummy_spaces(count):
60
+ spaces = []
61
+ for i in range(count):
62
+ spaces.append({
63
+ 'id': f'dummy/space-{i}',
64
+ 'owner': 'dummy',
65
+ 'title': f'Example Space {i+1}',
66
+ 'likes': 100 - i,
67
+ 'createdAt': '2023-01-01T00:00:00.000Z'
68
+ })
69
+ return spaces
70
+
71
+ # Transform Huggingface URL to direct space URL
72
  def transform_url(owner, name):
73
+ # 1. Replace '.' with '-'
74
+ name = name.replace('.', '-')
75
+ # 2. Replace '_' with '-'
76
+ name = name.replace('_', '-')
77
+ # 3. Convert to lowercase
78
  owner = owner.lower()
79
+ name = name.lower()
80
+
81
  return f"https://{owner}-{name}.hf.space"
82
 
83
+ # Get space details
84
  def get_space_details(space_data, index, offset):
85
  try:
86
+ # Extract common info
87
  if '/' in space_data.get('id', ''):
88
  owner, name = space_data.get('id', '').split('/', 1)
89
  else:
90
  owner = space_data.get('owner', '')
91
  name = space_data.get('id', '')
92
 
93
+ # Ignore if contains None
94
  if owner == 'None' or name == 'None':
95
  return None
96
 
97
+ # Construct URLs
98
  original_url = f"https://huggingface.co/spaces/{owner}/{name}"
99
  embed_url = transform_url(owner, name)
100
+
101
+ # Likes count
102
  likes_count = space_data.get('likes', 0)
103
+
104
+ # Extract title
105
  title = space_data.get('title', name)
106
+
107
+ # Tags
108
  tags = space_data.get('tags', [])
109
 
110
  return {
 
112
  'embedUrl': embed_url,
113
  'title': title,
114
  'owner': owner,
115
+ 'name': name, # Store Space name
116
  'likes_count': likes_count,
117
  'tags': tags,
118
  'rank': offset + index + 1
119
  }
120
  except Exception as e:
121
  print(f"Error processing space data: {e}")
122
+ # Return basic object even if error occurs
123
  return {
124
  'url': 'https://huggingface.co/spaces',
125
  'embedUrl': 'https://huggingface.co/spaces',
 
131
  'rank': offset + index + 1
132
  }
133
 
134
+ # Get owner statistics from all spaces
135
  def get_owner_stats(all_spaces):
136
  owners = []
137
  for space in all_spaces:
 
143
  if owner != 'None':
144
  owners.append(owner)
145
 
146
+ # Count occurrences of each owner
147
  owner_counts = Counter(owners)
148
+
149
+ # Get top 30 owners by count
150
  top_owners = owner_counts.most_common(30)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
152
+ return top_owners
 
 
 
 
153
 
154
+ # Homepage route
155
  @app.route('/')
156
  def home():
157
  return render_template('index.html')
158
 
159
+ # Trending spaces API
160
  @app.route('/api/trending-spaces', methods=['GET'])
161
  def trending_spaces():
 
 
 
 
162
  search_query = request.args.get('search', '').lower()
163
  offset = int(request.args.get('offset', 0))
164
+ limit = int(request.args.get('limit', 72)) # Default 72
165
+
166
+ # Fetch trending spaces
167
  spaces_data = fetch_trending_spaces(offset, limit)
168
 
169
+ # Process and filter spaces
170
  results = []
171
  for index, space_data in enumerate(spaces_data['spaces']):
172
  space_info = get_space_details(space_data, index, offset)
173
+
174
  if not space_info:
175
  continue
176
+
177
+ # Apply search filter if needed
178
  if search_query:
179
  title = space_info['title'].lower()
180
  owner = space_info['owner'].lower()
181
  url = space_info['url'].lower()
182
  tags = ' '.join([str(tag) for tag in space_info.get('tags', [])]).lower()
183
 
184
+ if (search_query not in title and
185
+ search_query not in owner and
186
+ search_query not in url and
187
+ search_query not in tags):
188
  continue
189
 
190
  results.append(space_info)
191
 
192
+ # Get owner statistics for all spaces
193
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
194
 
195
  return jsonify({
 
197
  'total': spaces_data['total'],
198
  'offset': offset,
199
  'limit': limit,
200
+ 'top_owners': top_owners # Add top owners data
201
  })
202
 
203
  if __name__ == '__main__':
204
+ # Create templates folder
205
  os.makedirs('templates', exist_ok=True)
206
 
207
+ # Create index.html file with the updated tab structure and JavaScript logic
208
  with open('templates/index.html', 'w', encoding='utf-8') as f:
209
  f.write('''<!DOCTYPE html>
210
  <html lang="en">
211
  <head>
212
  <meta charset="UTF-8">
213
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
214
  <title>Huggingface Spaces Gallery</title>
215
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
216
  <style>
217
+ /* Google Fonts & Base Styling */
218
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
219
 
220
  :root {
 
236
  --text-secondary: #666;
237
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
238
  }
239
+
240
  * {
241
+ margin: 0;
242
+ padding: 0;
243
+ box-sizing: border-box;
244
  }
245
+
246
  body {
247
+ font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
248
  line-height: 1.6;
249
  color: var(--text-primary);
250
  background-color: #f8f9fa;
 
252
  min-height: 100vh;
253
  padding: 2rem;
254
  }
255
+
256
  .container {
257
  max-width: 1600px;
258
  margin: 0 auto;
259
  }
260
+
261
+ /* Mac OS Window Styling */
262
  .mac-window {
263
  background-color: var(--mac-window-bg);
264
  border-radius: 10px;
 
268
  margin-bottom: 2rem;
269
  border: 1px solid var(--mac-border);
270
  }
271
+
272
  .mac-toolbar {
273
  display: flex;
274
  align-items: center;
 
276
  background-color: var(--mac-toolbar);
277
  border-bottom: 1px solid var(--mac-border);
278
  }
279
+
280
  .mac-buttons {
281
+ display: flex;
282
+ gap: 8px;
283
+ margin-right: 15px;
284
  }
285
+
286
  .mac-button {
287
+ width: 12px;
288
+ height: 12px;
289
+ border-radius: 50%;
290
+ cursor: default;
291
+ }
292
+
293
+ .mac-close {
294
+ background-color: var(--mac-button-red);
295
  }
296
+
297
+ .mac-minimize {
298
+ background-color: var(--mac-button-yellow);
299
+ }
300
+
301
+ .mac-maximize {
302
+ background-color: var(--mac-button-green);
303
+ }
304
+
305
  .mac-title {
306
+ flex-grow: 1;
307
+ text-align: center;
308
+ font-size: 0.9rem;
309
+ color: var(--text-secondary);
310
+ }
311
+
312
+ .mac-content {
313
+ padding: 20px;
314
  }
315
+
316
+ /* Header Styling */
317
  .header {
318
  text-align: center;
319
+ margin-bottom: 1.5rem;
320
+ position: relative;
321
  }
322
+
323
  .header h1 {
324
+ font-size: 2.2rem;
325
+ font-weight: 700;
326
+ margin: 0;
327
+ color: #2d3748;
328
+ letter-spacing: -0.5px;
329
  }
330
+
331
  .header p {
332
  color: var(--text-secondary);
333
  margin-top: 0.5rem;
334
  font-size: 1.1rem;
335
  }
336
+
337
+ /* Tabs Styling */
338
  .tab-nav {
339
+ display: flex;
340
+ justify-content: center;
341
+ margin-bottom: 1.5rem;
342
  }
343
+
344
  .tab-button {
345
+ border: none;
346
+ background-color: #edf2f7;
347
+ color: var(--text-primary);
348
+ padding: 10px 20px;
349
+ margin: 0 5px;
350
+ cursor: pointer;
351
+ border-radius: 5px;
352
+ font-size: 1rem;
353
+ font-weight: 600;
354
  }
355
+
356
  .tab-button.active {
357
  background-color: var(--pastel-purple);
358
+ color: #fff;
359
+ }
360
+
361
+ .tab-content {
362
+ display: none;
363
+ }
364
+
365
+ .tab-content.active {
366
+ display: block;
367
  }
368
+
369
+ /* Controls Styling */
370
  .search-bar {
371
+ display: flex;
372
+ align-items: center;
373
+ margin-bottom: 1.5rem;
374
+ background-color: white;
375
+ border-radius: 30px;
376
+ padding: 5px;
377
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
378
+ max-width: 600px;
379
+ margin-left: auto;
380
+ margin-right: auto;
381
  }
382
+
383
  .search-bar input {
384
+ flex-grow: 1;
385
+ border: none;
386
+ padding: 12px 20px;
387
+ font-size: 1rem;
388
+ outline: none;
389
+ background: transparent;
390
+ border-radius: 30px;
391
  }
392
+
393
  .search-bar .refresh-btn {
394
+ background-color: var(--pastel-green);
395
+ color: #1a202c;
396
+ border: none;
397
+ border-radius: 30px;
398
+ padding: 10px 20px;
399
+ font-size: 1rem;
400
+ font-weight: 600;
401
+ cursor: pointer;
402
+ transition: all 0.2s;
403
+ display: flex;
404
+ align-items: center;
405
+ gap: 8px;
406
  }
407
+
408
  .search-bar .refresh-btn:hover {
409
  background-color: #9ee7c0;
410
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
411
  }
412
+
413
  .refresh-icon {
414
+ display: inline-block;
415
+ width: 16px;
416
+ height: 16px;
417
+ border: 2px solid #1a202c;
418
+ border-top-color: transparent;
419
+ border-radius: 50%;
420
+ animation: none;
421
  }
422
+
423
  .refreshing .refresh-icon {
424
  animation: spin 1s linear infinite;
425
  }
426
+
427
  @keyframes spin {
428
  0% { transform: rotate(0deg); }
429
  100% { transform: rotate(360deg); }
430
  }
431
+
432
+ /* Grid Styling */
433
  .grid-container {
434
  display: grid;
435
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
436
  gap: 1.5rem;
437
  margin-bottom: 2rem;
438
  }
439
+
440
  .grid-item {
441
+ height: 500px;
442
+ position: relative;
443
+ overflow: hidden;
444
+ transition: all 0.3s ease;
445
+ border-radius: 15px;
446
  }
447
+
448
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
449
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
450
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
451
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
452
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
453
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
454
+
455
  .grid-item:hover {
456
  transform: translateY(-5px);
457
  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
458
  }
459
+
460
  .grid-header {
461
  padding: 15px;
462
+ display: flex;
463
+ flex-direction: column;
464
  background-color: rgba(255, 255, 255, 0.7);
465
  backdrop-filter: blur(5px);
466
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
467
  }
468
+
469
  .grid-header-top {
470
+ display: flex;
471
+ justify-content: space-between;
472
+ align-items: center;
473
+ margin-bottom: 8px;
474
  }
475
+
476
  .rank-badge {
477
+ background-color: #1a202c;
478
+ color: white;
479
+ font-size: 0.8rem;
480
+ font-weight: 600;
481
+ padding: 4px 8px;
482
+ border-radius: 50px;
483
  }
484
+
485
  .grid-header h3 {
486
+ margin: 0;
487
+ font-size: 1.2rem;
488
+ font-weight: 700;
489
+ white-space: nowrap;
490
+ overflow: hidden;
491
+ text-overflow: ellipsis;
492
  }
493
+
494
  .grid-meta {
495
+ display: flex;
496
+ justify-content: space-between;
497
+ align-items: center;
498
+ font-size: 0.9rem;
499
+ }
500
+
501
+ .owner-info {
502
+ color: var(--text-secondary);
503
+ font-weight: 500;
504
  }
505
+
506
  .likes-counter {
507
+ display: flex;
508
+ align-items: center;
509
+ color: #e53e3e;
510
+ font-weight: 600;
511
+ }
512
+
513
+ .likes-counter span {
514
+ margin-left: 4px;
515
  }
516
+
517
  .grid-actions {
518
+ padding: 10px 15px;
519
+ text-align: right;
520
+ background-color: rgba(255, 255, 255, 0.7);
521
+ backdrop-filter: blur(5px);
522
+ position: absolute;
523
+ bottom: 0;
524
+ left: 0;
525
+ right: 0;
526
+ z-index: 10;
527
+ display: flex;
528
+ justify-content: flex-end;
529
  }
530
+
531
  .open-link {
532
+ text-decoration: none;
533
+ color: #2c5282;
534
+ font-weight: 600;
535
+ padding: 5px 10px;
536
+ border-radius: 5px;
537
+ transition: all 0.2s;
538
  background-color: rgba(237, 242, 247, 0.8);
539
  }
540
+
541
+ .open-link:hover {
542
+ background-color: #e2e8f0;
543
+ }
544
+
545
  .grid-content {
546
+ position: absolute;
547
+ top: 0;
548
+ left: 0;
549
+ width: 100%;
550
+ height: 100%;
551
+ padding-top: 85px; /* Header height */
552
+ padding-bottom: 45px; /* Actions height */
553
  }
554
+
555
  .iframe-container {
556
+ width: 100%;
557
+ height: 100%;
558
+ overflow: hidden;
559
+ position: relative;
560
  }
561
+
562
+ /* Apply 70% scaling to iframes */
563
  .grid-content iframe {
564
+ transform: scale(0.7);
565
+ transform-origin: top left;
566
+ width: 142.857%;
567
+ height: 142.857%;
568
+ border: none;
569
+ border-radius: 0;
570
  }
571
+
572
  .error-placeholder {
573
+ position: absolute;
574
+ top: 0;
575
+ left: 0;
576
+ width: 100%;
577
+ height: 100%;
578
+ display: flex;
579
+ flex-direction: column;
580
+ justify-content: center;
581
+ align-items: center;
582
+ padding: 20px;
583
+ background-color: rgba(255, 255, 255, 0.9);
584
  text-align: center;
585
  }
586
+
587
  .error-emoji {
588
+ font-size: 6rem;
589
+ margin-bottom: 1.5rem;
590
  animation: bounce 1s infinite alternate;
591
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
592
  }
593
+
594
  @keyframes bounce {
595
+ from {
596
+ transform: translateY(0px) scale(1);
597
+ }
598
+ to {
599
+ transform: translateY(-15px) scale(1.1);
600
+ }
601
  }
602
+
603
+ /* Pagination Styling */
604
  .pagination {
605
+ display: flex;
606
+ justify-content: center;
607
+ align-items: center;
608
+ gap: 10px;
609
  margin: 2rem 0;
610
  }
611
+
612
  .pagination-button {
613
+ background-color: white;
614
+ border: none;
615
+ padding: 10px 20px;
616
+ border-radius: 10px;
617
+ font-size: 1rem;
618
+ font-weight: 600;
619
+ cursor: pointer;
620
+ transition: all 0.2s;
621
+ color: var(--text-primary);
622
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
623
  }
624
+
625
  .pagination-button:hover {
626
+ background-color: #f8f9fa;
627
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
628
  }
629
+
630
  .pagination-button.active {
631
+ background-color: var(--pastel-purple);
632
+ color: #4a5568;
633
  }
634
+
635
  .pagination-button:disabled {
636
+ background-color: #edf2f7;
637
+ color: #a0aec0;
638
+ cursor: default;
639
+ box-shadow: none;
640
  }
641
+
642
+ /* Loading Indicator */
643
  .loading {
644
+ position: fixed;
645
+ top: 0;
646
+ left: 0;
647
+ right: 0;
648
+ bottom: 0;
649
+ background-color: rgba(255, 255, 255, 0.8);
650
+ backdrop-filter: blur(5px);
651
+ display: flex;
652
+ justify-content: center;
653
+ align-items: center;
654
+ z-index: 1000;
655
  }
656
+
657
+ .loading-content {
658
+ text-align: center;
659
+ }
660
+
661
  .loading-spinner {
662
+ width: 60px;
663
+ height: 60px;
664
+ border: 5px solid #e2e8f0;
665
+ border-top-color: var(--pastel-purple);
666
+ border-radius: 50%;
667
+ animation: spin 1s linear infinite;
668
+ margin: 0 auto 15px;
669
  }
670
+
671
  .loading-text {
672
+ font-size: 1.2rem;
673
+ font-weight: 600;
674
+ color: #4a5568;
675
  }
676
+
677
  .loading-error {
678
+ display: none;
679
+ margin-top: 10px;
680
+ color: #e53e3e;
681
+ font-size: 0.9rem;
682
+ }
683
+
684
+ /* Stats window styling */
685
+ .stats-window {
686
+ margin-top: 2rem;
687
+ margin-bottom: 2rem;
688
  }
689
+
690
  .stats-header {
691
+ display: flex;
692
+ justify-content: space-between;
693
+ align-items: center;
694
+ margin-bottom: 1rem;
695
+ }
696
+
697
+ .stats-title {
698
+ font-size: 1.5rem;
699
+ font-weight: 700;
700
+ color: #2d3748;
701
  }
702
+
703
  .stats-toggle {
704
+ background-color: var(--pastel-blue);
705
+ border: none;
706
+ padding: 8px 16px;
707
+ border-radius: 20px;
708
+ font-weight: 600;
709
+ cursor: pointer;
710
+ transition: all 0.2s;
711
+ }
712
+
713
+ .stats-toggle:hover {
714
+ background-color: var(--pastel-purple);
715
  }
716
+
717
  .stats-content {
718
+ background-color: white;
719
+ border-radius: 10px;
720
+ padding: 20px;
721
+ box-shadow: var(--box-shadow);
722
+ max-height: 0;
723
+ overflow: hidden;
724
  transition: max-height 0.5s ease-out;
725
  }
726
+
727
+ .stats-content.open {
728
+ max-height: 600px;
729
+ }
730
+
731
+ .chart-container {
732
+ width: 100%;
733
+ height: 500px;
734
+ }
735
+
736
+ /* Responsive Design */
737
  @media (max-width: 768px) {
738
+ body {
739
+ padding: 1rem;
740
+ }
741
+
742
+ .grid-container {
743
+ grid-template-columns: 1fr;
744
+ }
745
+
746
+ .search-bar {
747
+ flex-direction: column;
748
+ padding: 10px;
749
+ }
750
+
751
+ .search-bar input {
752
+ width: 100%;
753
+ margin-bottom: 10px;
754
+ }
755
+
756
+ .search-bar .refresh-btn {
757
+ width: 100%;
758
+ justify-content: center;
759
+ }
760
+
761
+ .pagination {
762
+ flex-wrap: wrap;
763
+ }
764
+
765
+ .chart-container {
766
+ height: 300px;
767
+ }
768
+ }
769
+
770
+ .error-emoji-detector {
771
+ position: fixed;
772
+ top: -9999px;
773
+ left: -9999px;
774
+ z-index: -1;
775
+ opacity: 0;
776
  }
777
  </style>
778
  </head>
 
791
  <div class="mac-content">
792
  <div class="header">
793
  <h1>HF Space Leaderboard</h1>
794
+ <p>Discover the top 500 trending spaces from the Huggingface</p>
795
  </div>
796
 
797
+ <!-- Tab Navigation -->
798
  <div class="tab-nav">
799
+ <button id="tabTrendingButton" class="tab-button active">Trending Spaces</button>
800
+ <button id="tabFixedButton" class="tab-button">Fixed Tab</button>
801
  </div>
802
 
803
+ <!-- Trending Tab Content -->
804
+ <div id="trendingTab" class="tab-content active">
805
+ <!-- Stats Section -->
806
  <div class="stats-window mac-window">
807
  <div class="mac-toolbar">
808
  <div class="mac-buttons">
 
832
  Refresh
833
  </button>
834
  </div>
835
+
836
  <div id="gridContainer" class="grid-container"></div>
837
+
838
+ <div id="pagination" class="pagination">
839
+ <!-- Pagination buttons will be dynamically created by JavaScript -->
840
+ </div>
841
  </div>
842
 
843
+ <!-- Fixed Tab Content -->
844
+ <div id="fixedTab" class="tab-content">
845
+ <div id="fixedGrid" class="grid-container"></div>
 
846
  </div>
847
  </div>
848
  </div>
849
  </div>
850
 
851
+ <div id="loadingIndicator" class="loading">
852
  <div class="loading-content">
853
  <div class="loading-spinner"></div>
854
  <div class="loading-text">Loading amazing spaces...</div>
 
859
  </div>
860
 
861
  <script>
862
+ // DOM element references for trending tab
863
  const elements = {
864
  gridContainer: document.getElementById('gridContainer'),
 
865
  loadingIndicator: document.getElementById('loadingIndicator'),
866
  loadingError: document.getElementById('loadingError'),
867
  searchInput: document.getElementById('searchInput'),
 
872
  creatorStatsChart: document.getElementById('creatorStatsChart')
873
  };
874
 
875
+ // DOM element references for tab navigation and fixed tab
876
+ const tabTrendingButton = document.getElementById('tabTrendingButton');
877
+ const tabFixedButton = document.getElementById('tabFixedButton');
878
+ const trendingTab = document.getElementById('trendingTab');
879
+ const fixedTab = document.getElementById('fixedTab');
880
+ const fixedGridContainer = document.getElementById('fixedGrid');
881
+
882
+ // Application state for trending spaces
883
  const state = {
884
  isLoading: false,
885
  spaces: [],
886
  currentPage: 0,
887
+ itemsPerPage: 72, // 72 items per page
888
  totalItems: 0,
889
  loadingTimeout: null,
890
+ staticModeAttempted: {}, // Track which spaces have attempted static mode
891
  statsVisible: false,
892
  chartInstance: null,
893
  topOwners: [],
894
+ iframeStatuses: {} // Track iframe loading status
 
895
  };
896
 
897
+ // Advanced iframe loader for better error detection (unchanged)
898
  const iframeLoader = {
899
  checkQueue: {},
900
+ maxAttempts: 5, // Try multiple times
901
+ checkInterval: 5000, // Check every 5 seconds
902
 
903
+ // Start checking iframe loading status
904
  startChecking: function(iframe, owner, name, title, spaceKey) {
905
+ // Initialize tracking
906
  this.checkQueue[spaceKey] = {
907
+ iframe: iframe,
908
+ owner: owner,
909
+ name: name,
910
+ title: title,
911
  attempts: 0,
912
  status: 'loading'
913
  };
914
+
915
+ // Start recursive checking
916
  this.checkIframeStatus(spaceKey);
917
  },
918
+
919
+ // Check iframe loading status
920
  checkIframeStatus: function(spaceKey) {
921
  if (!this.checkQueue[spaceKey]) return;
922
+
923
  const item = this.checkQueue[spaceKey];
924
  const iframe = item.iframe;
925
 
926
+ // If already processed, stop checking
927
  if (item.status !== 'loading') {
928
  delete this.checkQueue[spaceKey];
929
  return;
930
  }
931
 
932
+ // Increment attempt counter
933
  item.attempts++;
934
+
935
  try {
936
+ // 1. Check if iframe was removed from DOM
937
  if (!iframe || !iframe.parentNode) {
938
  delete this.checkQueue[spaceKey];
939
  return;
940
  }
941
+
942
+ // 2. Check if content has loaded
943
  try {
944
+ const hasContent = iframe.contentWindow &&
945
+ iframe.contentWindow.document &&
946
+ iframe.contentWindow.document.body;
947
+
948
+ // 2.1 If content exists and has actual content loaded
949
  if (hasContent && iframe.contentWindow.document.body.innerHTML.length > 100) {
950
+ // Check if it contains error text
951
  const bodyText = iframe.contentWindow.document.body.textContent.toLowerCase();
952
+ if (bodyText.includes('forbidden') ||
953
+ bodyText.includes('404') ||
954
  bodyText.includes('not found') ||
955
  bodyText.includes('error')) {
956
  item.status = 'error';
 
961
  delete this.checkQueue[spaceKey];
962
  return;
963
  }
964
+ } catch(e) {
965
+ // Cross-origin access errors are expected - might be normal loading
966
+ }
967
 
968
+ // 3. Check iframe's visible size
969
  const rect = iframe.getBoundingClientRect();
970
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
971
+ // If it has sufficient size, mark as success
972
  item.status = 'success';
973
  delete this.checkQueue[spaceKey];
974
  return;
975
  }
976
 
977
+ // 4. If we've reached max attempts
978
  if (item.attempts >= this.maxAttempts) {
979
+ // Final check: is iframe visible?
980
  if (iframe.offsetWidth > 0 && iframe.offsetHeight > 0) {
981
+ // If visible, mark as success
982
  item.status = 'success';
983
  } else {
984
+ // If still not visible, mark as error
985
  item.status = 'error';
986
  handleIframeError(iframe, item.owner, item.name, item.title);
987
  }
 
989
  return;
990
  }
991
 
992
+ // Schedule next check with exponential backoff
993
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
994
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
995
+
996
+ } catch (e) {
997
  console.error('Error checking iframe status:', e);
998
+
999
+ // If error occurs, try a few more times
1000
  if (item.attempts >= this.maxAttempts) {
1001
  item.status = 'error';
1002
  handleIframeError(iframe, item.owner, item.name, item.title);
1003
  delete this.checkQueue[spaceKey];
1004
  } else {
1005
+ // Try again
1006
  setTimeout(() => this.checkIframeStatus(spaceKey), this.checkInterval);
1007
  }
1008
  }
1009
  }
1010
  };
1011
 
1012
+ // Toggle stats display
1013
  function toggleStats() {
1014
  state.statsVisible = !state.statsVisible;
1015
  elements.statsContent.classList.toggle('open', state.statsVisible);
1016
  elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
1017
+
1018
  if (state.statsVisible && state.topOwners.length > 0) {
1019
  renderCreatorStats();
1020
  }
1021
  }
1022
 
1023
+ // Render creator stats chart
1024
  function renderCreatorStats() {
1025
  if (state.chartInstance) {
1026
  state.chartInstance.destroy();
1027
  }
1028
+
1029
  const ctx = elements.creatorStatsChart.getContext('2d');
1030
+
1031
+ // Prepare data
1032
  const labels = state.topOwners.map(item => item[0]);
1033
  const data = state.topOwners.map(item => item[1]);
1034
+
1035
+ // Generate colors for bars
1036
  const colors = [];
1037
  for (let i = 0; i < labels.length; i++) {
1038
  const hue = (i * 360 / labels.length) % 360;
1039
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
1040
  }
1041
+
1042
+ // Create chart
1043
  state.chartInstance = new Chart(ctx, {
1044
  type: 'bar',
1045
  data: {
 
1048
  label: 'Number of Spaces',
1049
  data: data,
1050
  backgroundColor: colors,
1051
+ borderColor: colors.map(color => color.replace('0.7', '1')),
1052
  borderWidth: 1
1053
  }]
1054
  },
 
1057
  responsive: true,
1058
  maintainAspectRatio: false,
1059
  plugins: {
1060
+ legend: {
1061
+ display: false
1062
+ },
1063
  tooltip: {
1064
  callbacks: {
1065
+ title: function(tooltipItems) {
1066
+ return tooltipItems[0].label;
1067
+ },
1068
+ label: function(context) {
1069
+ return `Spaces: ${context.raw}`;
1070
+ }
1071
  }
1072
  }
1073
  },
1074
  scales: {
1075
  x: {
1076
  beginAtZero: true,
1077
+ title: {
1078
+ display: true,
1079
+ text: 'Number of Spaces'
1080
+ }
1081
  },
1082
  y: {
1083
+ title: {
1084
+ display: true,
1085
+ text: 'Creator ID'
1086
+ },
1087
  ticks: {
1088
  autoSkip: false,
1089
  font: function(context) {
 
1099
  });
1100
  }
1101
 
1102
+ // Load spaces with timeout for trending tab
1103
+ async function loadSpaces(page = 0) {
1104
  setLoading(true);
1105
+
1106
  try {
1107
  const searchText = elements.searchInput.value;
1108
  const offset = page * state.itemsPerPage;
1109
+
1110
+ // Set timeout (30 seconds)
1111
  const timeoutPromise = new Promise((_, reject) =>
1112
  setTimeout(() => reject(new Error('Request timeout')), 30000)
1113
  );
1114
+
1115
+ const fetchPromise = fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`);
1116
+
1117
+ // Use the first Promise that completes
1118
  const response = await Promise.race([fetchPromise, timeoutPromise]);
1119
  const data = await response.json();
1120
 
1121
+ // Update state on successful load
1122
  state.spaces = data.spaces;
1123
  state.totalItems = data.total;
1124
  state.currentPage = page;
1125
  state.topOwners = data.top_owners || [];
1126
 
1127
+ renderGrid(data.spaces);
1128
  renderPagination();
1129
 
1130
+ // If stats are visible, update chart
1131
  if (state.statsVisible && state.topOwners.length > 0) {
1132
  renderCreatorStats();
1133
  }
1134
+ } catch (error) {
1135
+ console.error('Error loading spaces:', error);
1136
+
1137
+ // Show empty grid with error message
1138
+ elements.gridContainer.innerHTML = `
1139
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1140
  <div style="font-size: 3rem; margin-bottom: 20px;">โš ๏ธ</div>
1141
  <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
1142
+ <p style="color: #666;">Please try refreshing the page. If the problem persists, try again later.</p>
1143
  <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1144
  Try Again
1145
  </button>
1146
  </div>
1147
+ `;
1148
+
1149
+ // Add event listener to retry button
1150
  document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
1151
+
1152
  renderPagination();
1153
  } finally {
1154
  setLoading(false);
1155
  }
1156
  }
1157
 
1158
+ // Render pagination for trending tab
1159
  function renderPagination() {
1160
  elements.pagination.innerHTML = '';
1161
+
1162
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
1163
 
1164
+ // Previous page button
1165
  const prevButton = document.createElement('button');
1166
+ prevButton.className = `pagination-button ${state.currentPage === 0 ? 'disabled' : ''}`;
1167
  prevButton.textContent = 'Previous';
1168
  prevButton.disabled = state.currentPage === 0;
1169
  prevButton.addEventListener('click', () => {
1170
+ if (state.currentPage > 0) {
1171
+ loadSpaces(state.currentPage - 1);
1172
+ }
1173
  });
1174
  elements.pagination.appendChild(prevButton);
1175
 
1176
+ // Page buttons (maximum of 7)
1177
  const maxButtons = 7;
1178
  let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
1179
  let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
1180
+
1181
  if (endPage - startPage + 1 < maxButtons) {
1182
  startPage = Math.max(0, endPage - maxButtons + 1);
1183
  }
1184
+
1185
  for (let i = startPage; i <= endPage; i++) {
1186
  const pageButton = document.createElement('button');
1187
+ pageButton.className = `pagination-button ${i === state.currentPage ? 'active' : ''}`;
1188
  pageButton.textContent = i + 1;
1189
  pageButton.addEventListener('click', () => {
1190
  if (i !== state.currentPage) {
 
1194
  elements.pagination.appendChild(pageButton);
1195
  }
1196
 
1197
+ // Next page button
1198
  const nextButton = document.createElement('button');
1199
+ nextButton.className = `pagination-button ${state.currentPage >= totalPages - 1 ? 'disabled' : ''}`;
1200
  nextButton.textContent = 'Next';
1201
  nextButton.disabled = state.currentPage >= totalPages - 1;
1202
  nextButton.addEventListener('click', () => {
 
1207
  elements.pagination.appendChild(nextButton);
1208
  }
1209
 
1210
+ // Handle iframe error and provide fallback error message
1211
  function handleIframeError(iframe, owner, name, title) {
1212
  const container = iframe.parentNode;
1213
+
1214
  const errorPlaceholder = document.createElement('div');
1215
  errorPlaceholder.className = 'error-placeholder';
1216
 
1217
  const errorMessage = document.createElement('p');
1218
+ errorMessage.textContent = `"${title}" space couldn't be loaded`;
1219
  errorPlaceholder.appendChild(errorMessage);
1220
 
1221
  const directLink = document.createElement('a');
1222
+ directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
1223
  directLink.target = '_blank';
1224
  directLink.textContent = 'Visit HF Space';
1225
  directLink.style.color = '#3182ce';
 
1235
  container.appendChild(errorPlaceholder);
1236
  }
1237
 
1238
+ // Render grid for trending tab
1239
+ function renderGrid(spaces) {
1240
+ elements.gridContainer.innerHTML = '';
1241
+
1242
  if (!spaces || spaces.length === 0) {
1243
  const noResultsMsg = document.createElement('p');
1244
+ noResultsMsg.textContent = 'No spaces found matching your search.';
1245
  noResultsMsg.style.padding = '2rem';
1246
  noResultsMsg.style.textAlign = 'center';
1247
  noResultsMsg.style.fontStyle = 'italic';
1248
  noResultsMsg.style.color = '#718096';
1249
+ elements.gridContainer.appendChild(noResultsMsg);
1250
  return;
1251
  }
1252
 
1253
+ spaces.forEach((item) => {
1254
  try {
1255
  const { url, title, likes_count, owner, name, rank } = item;
1256
+
1257
+ if (owner === 'None') {
1258
+ return;
1259
+ }
1260
 
1261
  const gridItem = document.createElement('div');
1262
  gridItem.className = 'grid-item';
 
1274
 
1275
  const rankBadge = document.createElement('div');
1276
  rankBadge.className = 'rank-badge';
1277
+ rankBadge.textContent = `#${rank}`;
1278
  headerTop.appendChild(rankBadge);
1279
+
1280
  header.appendChild(headerTop);
1281
 
1282
  const metaInfo = document.createElement('div');
 
1284
 
1285
  const ownerEl = document.createElement('div');
1286
  ownerEl.className = 'owner-info';
1287
+ ownerEl.textContent = `by ${owner}`;
1288
  metaInfo.appendChild(ownerEl);
1289
 
1290
  const likesCounter = document.createElement('div');
 
1310
  iframe.setAttribute('frameborder', '0');
1311
  iframe.loading = 'lazy';
1312
 
1313
+ const spaceKey = `${owner}/${name}`;
1314
  state.iframeStatuses[spaceKey] = 'loading';
1315
 
1316
  iframe.onload = function() {
1317
  iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1318
  };
1319
+
1320
  iframe.onerror = function() {
1321
  handleIframeError(iframe, owner, name, title);
1322
  state.iframeStatuses[spaceKey] = 'error';
1323
  };
1324
+
1325
  setTimeout(() => {
1326
  if (state.iframeStatuses[spaceKey] === 'loading') {
1327
  handleIframeError(iframe, owner, name, title);
 
1345
  gridItem.appendChild(content);
1346
  gridItem.appendChild(actions);
1347
 
1348
+ elements.gridContainer.appendChild(gridItem);
1349
+ } catch (error) {
1350
+ console.error('Item rendering error:', error);
1351
  }
1352
  });
1353
  }
1354
 
1355
+ // Render grid for Fixed Tab (static cards)
1356
+ function renderFixedGrid() {
1357
+ fixedGridContainer.innerHTML = '';
1358
+
1359
+ const staticSpaces = [
1360
+ {
1361
+ url: "https://huggingface.co/spaces/VIDraft/SanaSprint",
1362
+ title: "SanaSprint",
1363
+ likes_count: 0,
1364
+ owner: "VIDraft",
1365
+ name: "SanaSprint",
1366
+ rank: 1
1367
+ },
1368
+ {
1369
+ url: "https://huggingface.co/spaces/VIDraft/SanaSprint",
1370
+ title: "SanaSprint",
1371
+ likes_count: 0,
1372
+ owner: "VIDraft",
1373
+ name: "SanaSprint",
1374
+ rank: 2
1375
+ },
1376
+ {
1377
+ url: "https://huggingface.co/spaces/VIDraft/SanaSprint",
1378
+ title: "SanaSprint",
1379
+ likes_count: 0,
1380
+ owner: "VIDraft",
1381
+ name: "SanaSprint",
1382
+ rank: 3
1383
+ }
1384
+ ];
1385
+
1386
+ if (!staticSpaces || staticSpaces.length === 0) {
1387
+ const noResultsMsg = document.createElement('p');
1388
+ noResultsMsg.textContent = 'No spaces to display.';
1389
+ noResultsMsg.style.padding = '2rem';
1390
+ noResultsMsg.style.textAlign = 'center';
1391
+ noResultsMsg.style.fontStyle = 'italic';
1392
+ noResultsMsg.style.color = '#718096';
1393
+ fixedGridContainer.appendChild(noResultsMsg);
1394
+ return;
1395
  }
1396
+
1397
+ staticSpaces.forEach((item) => {
1398
+ try {
1399
+ const { url, title, likes_count, owner, name, rank } = item;
1400
+
1401
+ const gridItem = document.createElement('div');
1402
+ gridItem.className = 'grid-item';
1403
+
1404
+ const header = document.createElement('div');
1405
+ header.className = 'grid-header';
1406
+
1407
+ const headerTop = document.createElement('div');
1408
+ headerTop.className = 'grid-header-top';
1409
+
1410
+ const titleEl = document.createElement('h3');
1411
+ titleEl.textContent = title;
1412
+ titleEl.title = title;
1413
+ headerTop.appendChild(titleEl);
1414
+
1415
+ const rankBadge = document.createElement('div');
1416
+ rankBadge.className = 'rank-badge';
1417
+ rankBadge.textContent = `#${rank}`;
1418
+ headerTop.appendChild(rankBadge);
1419
+
1420
+ header.appendChild(headerTop);
1421
+
1422
+ const metaInfo = document.createElement('div');
1423
+ metaInfo.className = 'grid-meta';
1424
+
1425
+ const ownerEl = document.createElement('div');
1426
+ ownerEl.className = 'owner-info';
1427
+ ownerEl.textContent = `by ${owner}`;
1428
+ metaInfo.appendChild(ownerEl);
1429
+
1430
+ const likesCounter = document.createElement('div');
1431
+ likesCounter.className = 'likes-counter';
1432
+ likesCounter.innerHTML = 'โ™ฅ <span>' + likes_count + '</span>';
1433
+ metaInfo.appendChild(likesCounter);
1434
+
1435
+ header.appendChild(metaInfo);
1436
+ gridItem.appendChild(header);
1437
+
1438
+ const content = document.createElement('div');
1439
+ content.className = 'grid-content';
1440
+
1441
+ const iframeContainer = document.createElement('div');
1442
+ iframeContainer.className = 'iframe-container';
1443
+
1444
+ const iframe = document.createElement('iframe');
1445
+ const directUrl = createDirectUrl(owner, name);
1446
+ iframe.src = directUrl;
1447
+ iframe.title = title;
1448
+ iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1449
+ iframe.setAttribute('allowfullscreen', '');
1450
+ iframe.setAttribute('frameborder', '0');
1451
+ iframe.loading = 'lazy';
1452
+
1453
+ const spaceKey = `${owner}/${name}`;
1454
+ iframe.onload = function() {
1455
+ iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1456
+ };
1457
+ iframe.onerror = function() {
1458
+ handleIframeError(iframe, owner, name, title);
1459
+ };
1460
+ setTimeout(() => {
1461
+ if (iframe.offsetWidth === 0 || iframe.offsetHeight === 0) {
1462
+ handleIframeError(iframe, owner, name, title);
1463
+ }
1464
+ }, 30000);
1465
+
1466
+ iframeContainer.appendChild(iframe);
1467
+ content.appendChild(iframeContainer);
1468
+
1469
+ const actions = document.createElement('div');
1470
+ actions.className = 'grid-actions';
1471
+
1472
+ const linkEl = document.createElement('a');
1473
+ linkEl.href = url;
1474
+ linkEl.target = '_blank';
1475
+ linkEl.className = 'open-link';
1476
+ linkEl.textContent = 'Open in new window';
1477
+ actions.appendChild(linkEl);
1478
+
1479
+ gridItem.appendChild(content);
1480
+ gridItem.appendChild(actions);
1481
+
1482
+ fixedGridContainer.appendChild(gridItem);
1483
+ } catch (error) {
1484
+ console.error('Fixed tab rendering error:', error);
1485
+ }
1486
+ });
1487
  }
1488
 
1489
+ // Tab switching event listeners
1490
+ tabTrendingButton.addEventListener('click', () => {
1491
+ tabTrendingButton.classList.add('active');
1492
+ tabFixedButton.classList.remove('active');
1493
+ trendingTab.classList.add('active');
1494
+ fixedTab.classList.remove('active');
1495
+ // Reload trending spaces if needed
1496
+ loadSpaces(state.currentPage);
1497
+ });
 
 
1498
 
1499
+ tabFixedButton.addEventListener('click', () => {
1500
+ tabFixedButton.classList.add('active');
1501
+ tabTrendingButton.classList.remove('active');
1502
+ fixedTab.classList.add('active');
1503
+ trendingTab.classList.remove('active');
1504
+ // Render the fixed tab static cards
1505
+ renderFixedGrid();
1506
  });
1507
 
1508
+ // Filter event listeners
1509
+ elements.searchInput.addEventListener('input', () => {
1510
+ clearTimeout(state.searchTimeout);
1511
+ state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
1512
+ });
1513
+
1514
+ elements.searchInput.addEventListener('keyup', (event) => {
1515
+ if (event.key === 'Enter') {
1516
+ loadSpaces(0);
 
 
 
 
1517
  }
1518
+ });
1519
 
1520
+ elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1521
+ elements.statsToggle.addEventListener('click', toggleStats);
1522
+
1523
+ document.querySelectorAll('.mac-button').forEach(button => {
1524
+ button.addEventListener('click', function(e) {
1525
+ e.preventDefault();
 
 
 
 
1526
  });
1527
  });
1528
 
 
1529
  window.addEventListener('load', function() {
1530
+ setTimeout(() => loadSpaces(0), 500);
 
 
 
1531
  });
1532
 
 
1533
  setTimeout(() => {
1534
  if (state.isLoading) {
1535
  setLoading(false);
1536
+ elements.gridContainer.innerHTML = `
1537
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1538
  <div style="font-size: 3rem; margin-bottom: 20px;">โฑ๏ธ</div>
1539
  <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3>
 
1542
  Reload Page
1543
  </button>
1544
  </div>
1545
+ `;
1546
  }
1547
  }, 20000);
1548
+
1549
+ loadSpaces(0);
1550
+
1551
+ function setLoading(isLoading) {
1552
+ state.isLoading = isLoading;
1553
+ elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
1554
+
1555
+ if (isLoading) {
1556
+ elements.refreshButton.classList.add('refreshing');
1557
+ clearTimeout(state.loadingTimeout);
1558
+ state.loadingTimeout = setTimeout(() => {
1559
+ elements.loadingError.style.display = 'block';
1560
+ }, 10000);
1561
+ } else {
1562
+ elements.refreshButton.classList.remove('refreshing');
1563
+ clearTimeout(state.loadingTimeout);
1564
+ elements.loadingError.style.display = 'none';
1565
+ }
1566
+ }
1567
+
1568
+ function createDirectUrl(owner, name) {
1569
+ try {
1570
+ name = name.replace(/\./g, '-');
1571
+ name = name.replace(/_/g, '-');
1572
+ owner = owner.toLowerCase();
1573
+ name = name.toLowerCase();
1574
+
1575
+ return `https://${owner}-${name}.hf.space`;
1576
+ } catch (error) {
1577
+ console.error('URL creation error:', error);
1578
+ return 'https://huggingface.co';
1579
+ }
1580
+ }
1581
  </script>
1582
  </body>
1583
  </html>
1584
  ''')
1585
 
1586
+ # Use port 7860 for Huggingface Spaces
1587
  app.run(host='0.0.0.0', port=7860)