openfree commited on
Commit
85dd6d7
·
verified ·
1 Parent(s): 18ae9ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +328 -664
app.py CHANGED
@@ -7,55 +7,55 @@ from collections import Counter
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):
@@ -68,43 +68,28 @@ def generate_dummy_spaces(count):
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,14 +97,13 @@ def get_space_details(space_data, index, offset):
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,7 +115,6 @@ def get_space_details(space_data, index, offset):
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,53 +126,100 @@ def get_owner_stats(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,23 +227,22 @@ def trending_spaces():
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
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
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
218
 
219
  :root {
@@ -235,15 +264,11 @@ if __name__ == '__main__':
235
  --text-secondary: #666;
236
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
237
  }
238
-
239
  * {
240
- margin: 0;
241
- padding: 0;
242
- box-sizing: border-box;
243
  }
244
-
245
  body {
246
- font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
247
  line-height: 1.6;
248
  color: var(--text-primary);
249
  background-color: #f8f9fa;
@@ -251,13 +276,10 @@ if __name__ == '__main__':
251
  min-height: 100vh;
252
  padding: 2rem;
253
  }
254
-
255
  .container {
256
  max-width: 1600px;
257
  margin: 0 auto;
258
  }
259
-
260
- /* Mac OS Window Styling */
261
  .mac-window {
262
  background-color: var(--mac-window-bg);
263
  border-radius: 10px;
@@ -267,7 +289,6 @@ if __name__ == '__main__':
267
  margin-bottom: 2rem;
268
  border: 1px solid var(--mac-border);
269
  }
270
-
271
  .mac-toolbar {
272
  display: flex;
273
  align-items: center;
@@ -275,472 +296,224 @@ if __name__ == '__main__':
275
  background-color: var(--mac-toolbar);
276
  border-bottom: 1px solid var(--mac-border);
277
  }
278
-
279
  .mac-buttons {
280
- display: flex;
281
- gap: 8px;
282
- margin-right: 15px;
283
  }
284
-
285
  .mac-button {
286
- width: 12px;
287
- height: 12px;
288
- border-radius: 50%;
289
- cursor: default;
290
  }
291
-
292
- .mac-close {
293
- background-color: var(--mac-button-red);
294
- }
295
-
296
- .mac-minimize {
297
- background-color: var(--mac-button-yellow);
298
- }
299
-
300
- .mac-maximize {
301
- background-color: var(--mac-button-green);
302
- }
303
-
304
  .mac-title {
305
- flex-grow: 1;
306
- text-align: center;
307
- font-size: 0.9rem;
308
- color: var(--text-secondary);
309
  }
310
-
311
- .mac-content {
312
- padding: 20px;
313
- }
314
-
315
- /* Header Styling */
316
  .header {
317
  text-align: center;
318
- margin-bottom: 1.5rem;
319
- position: relative;
320
  }
321
-
322
  .header h1 {
323
- font-size: 2.2rem;
324
- font-weight: 700;
325
- margin: 0;
326
- color: #2d3748;
327
- letter-spacing: -0.5px;
328
  }
329
-
330
  .header p {
331
  color: var(--text-secondary);
332
  margin-top: 0.5rem;
333
  font-size: 1.1rem;
334
  }
335
-
336
- /* Controls Styling */
337
  .search-bar {
338
- display: flex;
339
- align-items: center;
340
- margin-bottom: 1.5rem;
341
- background-color: white;
342
- border-radius: 30px;
343
- padding: 5px;
344
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
345
- max-width: 600px;
346
- margin-left: auto;
347
- margin-right: auto;
348
  }
349
-
350
  .search-bar input {
351
- flex-grow: 1;
352
- border: none;
353
- padding: 12px 20px;
354
- font-size: 1rem;
355
- outline: none;
356
- background: transparent;
357
- border-radius: 30px;
358
  }
359
-
360
  .search-bar .refresh-btn {
361
- background-color: var(--pastel-green);
362
- color: #1a202c;
363
- border: none;
364
- border-radius: 30px;
365
- padding: 10px 20px;
366
- font-size: 1rem;
367
- font-weight: 600;
368
- cursor: pointer;
369
- transition: all 0.2s;
370
- display: flex;
371
- align-items: center;
372
- gap: 8px;
373
  }
374
-
375
  .search-bar .refresh-btn:hover {
376
  background-color: #9ee7c0;
377
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
378
  }
379
-
380
  .refresh-icon {
381
- display: inline-block;
382
- width: 16px;
383
- height: 16px;
384
- border: 2px solid #1a202c;
385
- border-top-color: transparent;
386
- border-radius: 50%;
387
- animation: none;
388
  }
389
-
390
  .refreshing .refresh-icon {
391
  animation: spin 1s linear infinite;
392
  }
393
-
394
  @keyframes spin {
395
  0% { transform: rotate(0deg); }
396
  100% { transform: rotate(360deg); }
397
  }
398
-
399
- /* Grid Styling */
400
  .grid-container {
401
  display: grid;
402
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
403
  gap: 1.5rem;
404
  margin-bottom: 2rem;
405
  }
406
-
407
  .grid-item {
408
- height: 500px;
409
- position: relative;
410
- overflow: hidden;
411
- transition: all 0.3s ease;
412
- border-radius: 15px;
413
  }
414
-
415
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
416
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
417
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
418
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
419
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
420
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
421
-
422
  .grid-item:hover {
423
  transform: translateY(-5px);
424
  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
425
  }
426
-
427
  .grid-header {
428
  padding: 15px;
429
- display: flex;
430
- flex-direction: column;
431
  background-color: rgba(255, 255, 255, 0.7);
432
  backdrop-filter: blur(5px);
433
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
434
  }
435
-
436
  .grid-header-top {
437
- display: flex;
438
- justify-content: space-between;
439
- align-items: center;
440
- margin-bottom: 8px;
441
  }
442
-
443
  .rank-badge {
444
- background-color: #1a202c;
445
- color: white;
446
- font-size: 0.8rem;
447
- font-weight: 600;
448
- padding: 4px 8px;
449
- border-radius: 50px;
450
  }
451
-
452
  .grid-header h3 {
453
- margin: 0;
454
- font-size: 1.2rem;
455
- font-weight: 700;
456
- white-space: nowrap;
457
- overflow: hidden;
458
- text-overflow: ellipsis;
459
  }
460
-
461
  .grid-meta {
462
- display: flex;
463
- justify-content: space-between;
464
- align-items: center;
465
- font-size: 0.9rem;
466
  }
467
-
468
- .owner-info {
469
- color: var(--text-secondary);
470
- font-weight: 500;
471
- }
472
-
473
  .likes-counter {
474
- display: flex;
475
- align-items: center;
476
- color: #e53e3e;
477
- font-weight: 600;
478
- }
479
-
480
- .likes-counter span {
481
- margin-left: 4px;
482
  }
483
-
484
  .grid-actions {
485
- padding: 10px 15px;
486
- text-align: right;
487
- background-color: rgba(255, 255, 255, 0.7);
488
- backdrop-filter: blur(5px);
489
- position: absolute;
490
- bottom: 0;
491
- left: 0;
492
- right: 0;
493
- z-index: 10;
494
- display: flex;
495
- justify-content: flex-end;
496
  }
497
-
498
  .open-link {
499
- text-decoration: none;
500
- color: #2c5282;
501
- font-weight: 600;
502
- padding: 5px 10px;
503
- border-radius: 5px;
504
- transition: all 0.2s;
505
  background-color: rgba(237, 242, 247, 0.8);
506
  }
507
-
508
- .open-link:hover {
509
- background-color: #e2e8f0;
510
- }
511
-
512
  .grid-content {
513
- position: absolute;
514
- top: 0;
515
- left: 0;
516
- width: 100%;
517
- height: 100%;
518
- padding-top: 85px; /* Header height */
519
- padding-bottom: 45px; /* Actions height */
520
  }
521
-
522
  .iframe-container {
523
- width: 100%;
524
- height: 100%;
525
- overflow: hidden;
526
- position: relative;
527
  }
528
-
529
- /* Apply 70% scaling to iframes */
530
  .grid-content iframe {
531
- transform: scale(0.7);
532
- transform-origin: top left;
533
- width: 142.857%;
534
- height: 142.857%;
535
- border: none;
536
- border-radius: 0;
537
  }
538
-
539
  .error-placeholder {
540
- position: absolute;
541
- top: 0;
542
- left: 0;
543
- width: 100%;
544
- height: 100%;
545
- display: flex;
546
- flex-direction: column;
547
- justify-content: center;
548
- align-items: center;
549
- padding: 20px;
550
- background-color: rgba(255, 255, 255, 0.9);
551
  text-align: center;
552
  }
553
-
554
  .error-emoji {
555
- font-size: 6rem;
556
- margin-bottom: 1.5rem;
557
  animation: bounce 1s infinite alternate;
558
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
559
  }
560
-
561
  @keyframes bounce {
562
- from {
563
- transform: translateY(0px) scale(1);
564
- }
565
- to {
566
- transform: translateY(-15px) scale(1.1);
567
- }
568
  }
569
-
570
- /* Pagination Styling */
571
  .pagination {
572
- display: flex;
573
- justify-content: center;
574
- align-items: center;
575
- gap: 10px;
576
  margin: 2rem 0;
577
  }
578
-
579
  .pagination-button {
580
- background-color: white;
581
- border: none;
582
- padding: 10px 20px;
583
- border-radius: 10px;
584
- font-size: 1rem;
585
- font-weight: 600;
586
- cursor: pointer;
587
- transition: all 0.2s;
588
- color: var(--text-primary);
589
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
590
  }
591
-
592
  .pagination-button:hover {
593
- background-color: #f8f9fa;
594
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
595
  }
596
-
597
  .pagination-button.active {
598
- background-color: var(--pastel-purple);
599
- color: #4a5568;
600
  }
601
-
602
  .pagination-button:disabled {
603
- background-color: #edf2f7;
604
- color: #a0aec0;
605
- cursor: default;
606
- box-shadow: none;
607
  }
608
-
609
- /* Loading Indicator */
610
  .loading {
611
- position: fixed;
612
- top: 0;
613
- left: 0;
614
- right: 0;
615
- bottom: 0;
616
- background-color: rgba(255, 255, 255, 0.8);
617
- backdrop-filter: blur(5px);
618
- display: flex;
619
- justify-content: center;
620
- align-items: center;
621
- z-index: 1000;
622
  }
623
-
624
- .loading-content {
625
- text-align: center;
626
- }
627
-
628
  .loading-spinner {
629
- width: 60px;
630
- height: 60px;
631
- border: 5px solid #e2e8f0;
632
- border-top-color: var(--pastel-purple);
633
- border-radius: 50%;
634
- animation: spin 1s linear infinite;
635
- margin: 0 auto 15px;
636
  }
637
-
638
  .loading-text {
639
- font-size: 1.2rem;
640
- font-weight: 600;
641
- color: #4a5568;
642
  }
643
-
644
  .loading-error {
645
- display: none;
646
- margin-top: 10px;
647
- color: #e53e3e;
648
- font-size: 0.9rem;
649
  }
650
-
651
- /* Stats window styling */
652
- .stats-window {
653
- margin-top: 2rem;
654
- margin-bottom: 2rem;
655
- }
656
-
657
  .stats-header {
658
- display: flex;
659
- justify-content: space-between;
660
- align-items: center;
661
- margin-bottom: 1rem;
662
- }
663
-
664
- .stats-title {
665
- font-size: 1.5rem;
666
- font-weight: 700;
667
- color: #2d3748;
668
  }
669
-
670
  .stats-toggle {
671
- background-color: var(--pastel-blue);
672
- border: none;
673
- padding: 8px 16px;
674
- border-radius: 20px;
675
- font-weight: 600;
676
- cursor: pointer;
677
- transition: all 0.2s;
678
  }
679
-
680
- .stats-toggle:hover {
681
- background-color: var(--pastel-purple);
682
- }
683
-
684
  .stats-content {
685
- background-color: white;
686
- border-radius: 10px;
687
- padding: 20px;
688
- box-shadow: var(--box-shadow);
689
- max-height: 0;
690
- overflow: hidden;
691
  transition: max-height 0.5s ease-out;
692
  }
693
-
694
- .stats-content.open {
695
- max-height: 600px;
696
- }
697
-
698
- .chart-container {
699
- width: 100%;
700
- height: 500px;
701
- }
702
-
703
- /* Responsive Design */
704
  @media (max-width: 768px) {
705
- body {
706
- padding: 1rem;
707
- }
708
-
709
- .grid-container {
710
- grid-template-columns: 1fr;
711
- }
712
-
713
- .search-bar {
714
- flex-direction: column;
715
- padding: 10px;
716
- }
717
-
718
- .search-bar input {
719
- width: 100%;
720
- margin-bottom: 10px;
721
- }
722
-
723
- .search-bar .refresh-btn {
724
- width: 100%;
725
- justify-content: center;
726
- }
727
-
728
- .pagination {
729
- flex-wrap: wrap;
730
- }
731
-
732
- .chart-container {
733
- height: 300px;
734
- }
735
  }
736
-
737
- .error-emoji-detector {
738
- position: fixed;
739
- top: -9999px;
740
- left: -9999px;
741
- z-index: -1;
742
- opacity: 0;
743
  }
 
 
744
  </style>
745
  </head>
746
  <body>
@@ -758,50 +531,62 @@ if __name__ == '__main__':
758
  <div class="mac-content">
759
  <div class="header">
760
  <h1>HF Space Leaderboard</h1>
761
- <p>Discover the top 500 trending spaces from the Huggingface</p>
762
  </div>
763
 
764
- <!-- Stats Section -->
765
- <div class="stats-window mac-window">
766
- <div class="mac-toolbar">
767
- <div class="mac-buttons">
768
- <div class="mac-button mac-close"></div>
769
- <div class="mac-button mac-minimize"></div>
770
- <div class="mac-button mac-maximize"></div>
771
- </div>
772
- <div class="mac-title">Creator Statistics</div>
773
- </div>
774
- <div class="mac-content">
775
- <div class="stats-header">
776
- <div class="stats-title">Top 30 Creators by Number of Spaces</div>
777
- <button id="statsToggle" class="stats-toggle">Show Stats</button>
 
 
 
778
  </div>
779
- <div id="statsContent" class="stats-content">
780
- <div class="chart-container">
781
- <canvas id="creatorStatsChart"></canvas>
 
 
 
 
 
 
782
  </div>
783
  </div>
784
  </div>
 
 
 
 
 
 
 
 
 
 
785
  </div>
786
 
787
- <div class="search-bar">
788
- <input type="text" id="searchInput" placeholder="Search by name, owner, or tags..." />
789
- <button id="refreshButton" class="refresh-btn">
790
- <span class="refresh-icon"></span>
791
- Refresh
792
- </button>
793
  </div>
794
 
795
- <div id="gridContainer" class="grid-container"></div>
796
-
797
- <div id="pagination" class="pagination">
798
- <!-- Pagination buttons will be dynamically created by JavaScript -->
799
- </div>
800
  </div>
801
  </div>
802
  </div>
803
 
804
- <div id="loadingIndicator" class="loading">
805
  <div class="loading-content">
806
  <div class="loading-spinner"></div>
807
  <div class="loading-text">Loading amazing spaces...</div>
@@ -812,9 +597,9 @@ if __name__ == '__main__':
812
  </div>
813
 
814
  <script>
815
- // DOM element references
816
  const elements = {
817
  gridContainer: document.getElementById('gridContainer'),
 
818
  loadingIndicator: document.getElementById('loadingIndicator'),
819
  loadingError: document.getElementById('loadingError'),
820
  searchInput: document.getElementById('searchInput'),
@@ -825,78 +610,58 @@ if __name__ == '__main__':
825
  creatorStatsChart: document.getElementById('creatorStatsChart')
826
  };
827
 
828
- // Application state
829
  const state = {
830
  isLoading: false,
831
  spaces: [],
832
  currentPage: 0,
833
- itemsPerPage: 72, // 72 items per page
834
  totalItems: 0,
835
  loadingTimeout: null,
836
- staticModeAttempted: {}, // Track which spaces have attempted static mode
837
  statsVisible: false,
838
  chartInstance: null,
839
  topOwners: [],
840
- iframeStatuses: {} // Track iframe loading status
 
 
841
  };
842
 
843
- // Advanced iframe loader for better error detection
844
  const iframeLoader = {
845
  checkQueue: {},
846
- maxAttempts: 5, // Try multiple times
847
- checkInterval: 5000, // Check every 5 seconds
848
 
849
- // Start checking iframe loading status
850
  startChecking: function(iframe, owner, name, title, spaceKey) {
851
- // Initialize tracking
852
  this.checkQueue[spaceKey] = {
853
- iframe: iframe,
854
- owner: owner,
855
- name: name,
856
- title: title,
857
  attempts: 0,
858
  status: 'loading'
859
  };
860
-
861
- // Start recursive checking
862
  this.checkIframeStatus(spaceKey);
863
  },
864
-
865
- // Check iframe loading status
866
  checkIframeStatus: function(spaceKey) {
867
  if (!this.checkQueue[spaceKey]) return;
868
-
869
  const item = this.checkQueue[spaceKey];
870
  const iframe = item.iframe;
871
 
872
- // If already processed, stop checking
873
  if (item.status !== 'loading') {
874
  delete this.checkQueue[spaceKey];
875
  return;
876
  }
877
 
878
- // Increment attempt counter
879
  item.attempts++;
880
-
881
  try {
882
- // 1. Check if iframe was removed from DOM
883
  if (!iframe || !iframe.parentNode) {
884
  delete this.checkQueue[spaceKey];
885
  return;
886
  }
887
 
888
- // 2. Check if content has loaded
889
  try {
890
- const hasContent = iframe.contentWindow &&
891
- iframe.contentWindow.document &&
892
- iframe.contentWindow.document.body;
893
-
894
- // 2.1 If content exists and has actual content loaded
895
  if (hasContent && iframe.contentWindow.document.body.innerHTML.length > 100) {
896
- // Check if it contains error text
897
  const bodyText = iframe.contentWindow.document.body.textContent.toLowerCase();
898
- if (bodyText.includes('forbidden') ||
899
- bodyText.includes('404') ||
900
  bodyText.includes('not found') ||
901
  bodyText.includes('error')) {
902
  item.status = 'error';
@@ -907,27 +672,19 @@ if __name__ == '__main__':
907
  delete this.checkQueue[spaceKey];
908
  return;
909
  }
910
- } catch(e) {
911
- // Cross-origin access errors are expected - might be normal loading
912
- }
913
 
914
- // 3. Check iframe's visible size
915
  const rect = iframe.getBoundingClientRect();
916
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
917
- // If it has sufficient size, mark as success
918
  item.status = 'success';
919
  delete this.checkQueue[spaceKey];
920
  return;
921
  }
922
 
923
- // 4. If we've reached max attempts
924
  if (item.attempts >= this.maxAttempts) {
925
- // Final check: is iframe visible?
926
  if (iframe.offsetWidth > 0 && iframe.offsetHeight > 0) {
927
- // If visible, mark as success
928
  item.status = 'success';
929
  } else {
930
- // If still not visible, mark as error
931
  item.status = 'error';
932
  handleIframeError(iframe, item.owner, item.name, item.title);
933
  }
@@ -935,57 +692,42 @@ if __name__ == '__main__':
935
  return;
936
  }
937
 
938
- // Schedule next check with exponential backoff
939
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
940
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
941
-
942
- } catch (e) {
943
  console.error('Error checking iframe status:', e);
944
-
945
- // If error occurs, try a few more times
946
  if (item.attempts >= this.maxAttempts) {
947
  item.status = 'error';
948
  handleIframeError(iframe, item.owner, item.name, item.title);
949
  delete this.checkQueue[spaceKey];
950
  } else {
951
- // Try again
952
  setTimeout(() => this.checkIframeStatus(spaceKey), this.checkInterval);
953
  }
954
  }
955
  }
956
  };
957
 
958
- // Toggle stats display
959
  function toggleStats() {
960
  state.statsVisible = !state.statsVisible;
961
  elements.statsContent.classList.toggle('open', state.statsVisible);
962
  elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
963
-
964
  if (state.statsVisible && state.topOwners.length > 0) {
965
  renderCreatorStats();
966
  }
967
  }
968
 
969
- // Render creator stats chart
970
  function renderCreatorStats() {
971
  if (state.chartInstance) {
972
  state.chartInstance.destroy();
973
  }
974
-
975
  const ctx = elements.creatorStatsChart.getContext('2d');
976
-
977
- // Prepare data
978
  const labels = state.topOwners.map(item => item[0]);
979
  const data = state.topOwners.map(item => item[1]);
980
-
981
- // Generate colors for bars
982
  const colors = [];
983
  for (let i = 0; i < labels.length; i++) {
984
  const hue = (i * 360 / labels.length) % 360;
985
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
986
  }
987
-
988
- // Create chart
989
  state.chartInstance = new Chart(ctx, {
990
  type: 'bar',
991
  data: {
@@ -994,7 +736,7 @@ if __name__ == '__main__':
994
  label: 'Number of Spaces',
995
  data: data,
996
  backgroundColor: colors,
997
- borderColor: colors.map(color => color.replace('0.7', '1')),
998
  borderWidth: 1
999
  }]
1000
  },
@@ -1003,38 +745,24 @@ if __name__ == '__main__':
1003
  responsive: true,
1004
  maintainAspectRatio: false,
1005
  plugins: {
1006
- legend: {
1007
- display: false
1008
- },
1009
  tooltip: {
1010
  callbacks: {
1011
- title: function(tooltipItems) {
1012
- return tooltipItems[0].label;
1013
- },
1014
- label: function(context) {
1015
- return `Spaces: ${context.raw}`;
1016
- }
1017
  }
1018
  }
1019
  },
1020
  scales: {
1021
  x: {
1022
  beginAtZero: true,
1023
- title: {
1024
- display: true,
1025
- text: 'Number of Spaces'
1026
- }
1027
  },
1028
  y: {
1029
- title: {
1030
- display: true,
1031
- text: 'Creator ID'
1032
- },
1033
- // Ensure all labels are shown without gaps
1034
  ticks: {
1035
  autoSkip: false,
1036
  font: function(context) {
1037
- // Adjust font size to fit all labels if needed
1038
  const defaultSize = 11;
1039
  return {
1040
  size: labels.length > 20 ? defaultSize - 1 : defaultSize
@@ -1047,91 +775,66 @@ if __name__ == '__main__':
1047
  });
1048
  }
1049
 
1050
- // Load spaces with timeout
1051
- async function loadSpaces(page = 0) {
1052
  setLoading(true);
1053
-
1054
  try {
1055
  const searchText = elements.searchInput.value;
1056
  const offset = page * state.itemsPerPage;
1057
-
1058
- // Set timeout (30 seconds)
1059
  const timeoutPromise = new Promise((_, reject) =>
1060
  setTimeout(() => reject(new Error('Request timeout')), 30000)
1061
  );
1062
-
1063
  const fetchPromise = fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`);
1064
-
1065
- // Use the first Promise that completes
1066
  const response = await Promise.race([fetchPromise, timeoutPromise]);
1067
  const data = await response.json();
1068
 
1069
- // Update state on successful load
1070
  state.spaces = data.spaces;
1071
  state.totalItems = data.total;
1072
  state.currentPage = page;
1073
  state.topOwners = data.top_owners || [];
1074
 
1075
- renderGrid(data.spaces);
1076
  renderPagination();
1077
 
1078
- // If stats are visible, update chart
1079
  if (state.statsVisible && state.topOwners.length > 0) {
1080
  renderCreatorStats();
1081
  }
1082
- } catch (error) {
1083
- console.error('Error loading spaces:', error);
1084
-
1085
- // Show empty grid with error message
1086
  elements.gridContainer.innerHTML = `
1087
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1088
  <div style="font-size: 3rem; margin-bottom: 20px;">⚠️</div>
1089
  <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
1090
- <p style="color: #666;">Please try refreshing the page. If the problem persists, try again later.</p>
1091
  <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1092
  Try Again
1093
  </button>
1094
  </div>
1095
  `;
1096
-
1097
- // Add event listener to retry button
1098
  document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
1099
-
1100
- // Render simple pagination
1101
  renderPagination();
1102
  } finally {
1103
  setLoading(false);
1104
  }
1105
  }
1106
 
1107
- // Render pagination
1108
  function renderPagination() {
1109
  elements.pagination.innerHTML = '';
1110
-
1111
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
1112
-
1113
- // Previous page button
1114
  const prevButton = document.createElement('button');
1115
  prevButton.className = `pagination-button ${state.currentPage === 0 ? 'disabled' : ''}`;
1116
  prevButton.textContent = 'Previous';
1117
  prevButton.disabled = state.currentPage === 0;
1118
  prevButton.addEventListener('click', () => {
1119
- if (state.currentPage > 0) {
1120
- loadSpaces(state.currentPage - 1);
1121
- }
1122
  });
1123
  elements.pagination.appendChild(prevButton);
1124
 
1125
- // Page buttons (maximum of 7)
1126
  const maxButtons = 7;
1127
  let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
1128
  let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
1129
-
1130
- // Adjust start page if the end page is less than maximum buttons
1131
  if (endPage - startPage + 1 < maxButtons) {
1132
  startPage = Math.max(0, endPage - maxButtons + 1);
1133
  }
1134
-
1135
  for (let i = startPage; i <= endPage; i++) {
1136
  const pageButton = document.createElement('button');
1137
  pageButton.className = `pagination-button ${i === state.currentPage ? 'active' : ''}`;
@@ -1144,7 +847,6 @@ if __name__ == '__main__':
1144
  elements.pagination.appendChild(pageButton);
1145
  }
1146
 
1147
- // Next page button
1148
  const nextButton = document.createElement('button');
1149
  nextButton.className = `pagination-button ${state.currentPage >= totalPages - 1 ? 'disabled' : ''}`;
1150
  nextButton.textContent = 'Next';
@@ -1157,20 +859,15 @@ if __name__ == '__main__':
1157
  elements.pagination.appendChild(nextButton);
1158
  }
1159
 
1160
- // Handle iframe error and provide fallback error message
1161
  function handleIframeError(iframe, owner, name, title) {
1162
  const container = iframe.parentNode;
1163
-
1164
- // Error message container
1165
  const errorPlaceholder = document.createElement('div');
1166
  errorPlaceholder.className = 'error-placeholder';
1167
 
1168
- // Error message
1169
  const errorMessage = document.createElement('p');
1170
  errorMessage.textContent = `"${title}" space couldn't be loaded`;
1171
  errorPlaceholder.appendChild(errorMessage);
1172
 
1173
- // Direct HF link
1174
  const directLink = document.createElement('a');
1175
  directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
1176
  directLink.target = '_blank';
@@ -1184,121 +881,89 @@ if __name__ == '__main__':
1184
  directLink.style.fontWeight = '600';
1185
  errorPlaceholder.appendChild(directLink);
1186
 
1187
- // Hide iframe and show error message
1188
  iframe.style.display = 'none';
1189
  container.appendChild(errorPlaceholder);
1190
  }
1191
 
1192
- // Render grid
1193
- function renderGrid(spaces) {
1194
- elements.gridContainer.innerHTML = '';
1195
-
1196
  if (!spaces || spaces.length === 0) {
1197
  const noResultsMsg = document.createElement('p');
1198
- noResultsMsg.textContent = 'No spaces found matching your search.';
1199
  noResultsMsg.style.padding = '2rem';
1200
  noResultsMsg.style.textAlign = 'center';
1201
  noResultsMsg.style.fontStyle = 'italic';
1202
  noResultsMsg.style.color = '#718096';
1203
- elements.gridContainer.appendChild(noResultsMsg);
1204
  return;
1205
  }
1206
 
1207
- spaces.forEach((item) => {
1208
  try {
1209
  const { url, title, likes_count, owner, name, rank } = item;
 
1210
 
1211
- // Skip if owner is 'None'
1212
- if (owner === 'None') {
1213
- return;
1214
- }
1215
-
1216
- // Create grid item - Apply rotating pastel colors
1217
  const gridItem = document.createElement('div');
1218
  gridItem.className = 'grid-item';
1219
 
1220
- // Header
1221
  const header = document.createElement('div');
1222
  header.className = 'grid-header';
1223
 
1224
- // Header top part with rank
1225
  const headerTop = document.createElement('div');
1226
  headerTop.className = 'grid-header-top';
1227
 
1228
- // Title
1229
  const titleEl = document.createElement('h3');
1230
  titleEl.textContent = title;
1231
- titleEl.title = title; // For tooltip on hover
1232
  headerTop.appendChild(titleEl);
1233
 
1234
- // Rank badge
1235
  const rankBadge = document.createElement('div');
1236
  rankBadge.className = 'rank-badge';
1237
  rankBadge.textContent = `#${rank}`;
1238
  headerTop.appendChild(rankBadge);
1239
-
1240
  header.appendChild(headerTop);
1241
 
1242
- // Grid meta info
1243
  const metaInfo = document.createElement('div');
1244
  metaInfo.className = 'grid-meta';
1245
 
1246
- // Owner info
1247
  const ownerEl = document.createElement('div');
1248
  ownerEl.className = 'owner-info';
1249
  ownerEl.textContent = `by ${owner}`;
1250
  metaInfo.appendChild(ownerEl);
1251
 
1252
- // Likes counter
1253
  const likesCounter = document.createElement('div');
1254
  likesCounter.className = 'likes-counter';
1255
  likesCounter.innerHTML = '♥ <span>' + likes_count + '</span>';
1256
  metaInfo.appendChild(likesCounter);
1257
 
1258
  header.appendChild(metaInfo);
1259
-
1260
- // Add header to grid item
1261
  gridItem.appendChild(header);
1262
 
1263
- // Content area
1264
  const content = document.createElement('div');
1265
  content.className = 'grid-content';
1266
 
1267
- // iframe container
1268
  const iframeContainer = document.createElement('div');
1269
  iframeContainer.className = 'iframe-container';
1270
 
1271
- // Create iframe to display the content
1272
  const iframe = document.createElement('iframe');
1273
  const directUrl = createDirectUrl(owner, name);
1274
  iframe.src = directUrl;
1275
  iframe.title = title;
1276
- // Remove microphone permission
1277
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1278
  iframe.setAttribute('allowfullscreen', '');
1279
  iframe.setAttribute('frameborder', '0');
1280
- iframe.loading = 'lazy'; // Lazy load iframes for better performance
1281
-
1282
- // Unique ID for this iframe
1283
- const iframeId = `iframe-${owner}-${name}`;
1284
- iframe.id = iframeId;
1285
 
1286
- // Track this space
1287
  const spaceKey = `${owner}/${name}`;
1288
  state.iframeStatuses[spaceKey] = 'loading';
1289
 
1290
- // Use the advanced loader for better error detection
1291
  iframe.onload = function() {
1292
  iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1293
  };
1294
-
1295
- // Direct error handling
1296
  iframe.onerror = function() {
1297
  handleIframeError(iframe, owner, name, title);
1298
  state.iframeStatuses[spaceKey] = 'error';
1299
  };
1300
-
1301
- // Final fallback - if nothing has happened after 30 seconds, show error
1302
  setTimeout(() => {
1303
  if (state.iframeStatuses[spaceKey] === 'loading') {
1304
  handleIframeError(iframe, owner, name, title);
@@ -1306,15 +971,12 @@ if __name__ == '__main__':
1306
  }
1307
  }, 30000);
1308
 
1309
- // Add iframe to container
1310
  iframeContainer.appendChild(iframe);
1311
  content.appendChild(iframeContainer);
1312
 
1313
- // Actions section at bottom
1314
  const actions = document.createElement('div');
1315
  actions.className = 'grid-actions';
1316
 
1317
- // Open link
1318
  const linkEl = document.createElement('a');
1319
  linkEl.href = url;
1320
  linkEl.target = '_blank';
@@ -1322,85 +984,54 @@ if __name__ == '__main__':
1322
  linkEl.textContent = 'Open in new window';
1323
  actions.appendChild(linkEl);
1324
 
1325
- // Add content and actions to grid item
1326
  gridItem.appendChild(content);
1327
  gridItem.appendChild(actions);
1328
 
1329
- // Add grid item to container
1330
- elements.gridContainer.appendChild(gridItem);
1331
- } catch (error) {
1332
- console.error('Item rendering error:', error);
1333
- // Continue rendering other items even if one fails
1334
  }
1335
  });
1336
  }
1337
 
1338
- // Filter event listeners
 
 
 
 
 
 
 
 
 
 
1339
  elements.searchInput.addEventListener('input', () => {
1340
- // Debounce input to prevent API calls on every keystroke
1341
  clearTimeout(state.searchTimeout);
1342
  state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
1343
  });
1344
-
1345
- // Enter key in search box
1346
  elements.searchInput.addEventListener('keyup', (event) => {
1347
  if (event.key === 'Enter') {
1348
  loadSpaces(0);
1349
  }
1350
  });
1351
-
1352
- // Refresh button event listener
1353
  elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1354
-
1355
- // Stats toggle button event listener
1356
  elements.statsToggle.addEventListener('click', toggleStats);
1357
 
1358
- // Mac buttons functionality (just for show)
1359
  document.querySelectorAll('.mac-button').forEach(button => {
1360
- button.addEventListener('click', function(e) {
1361
  e.preventDefault();
1362
- // Mac buttons don't do anything, just for style
1363
  });
1364
  });
1365
 
1366
- // Page load complete event detection
1367
- window.addEventListener('load', function() {
1368
- // Start loading data when page is fully loaded
1369
- setTimeout(() => loadSpaces(0), 500);
1370
- });
1371
-
1372
- // Safety mechanism to prevent infinite loading
1373
- setTimeout(() => {
1374
- if (state.isLoading) {
1375
- setLoading(false);
1376
- elements.gridContainer.innerHTML = `
1377
- <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1378
- <div style="font-size: 3rem; margin-bottom: 20px;">⏱️</div>
1379
- <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3>
1380
- <p style="color: #666;">Please try refreshing the page.</p>
1381
- <button onClick="window.location.reload()" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1382
- Reload Page
1383
- </button>
1384
- </div>
1385
- `;
1386
- }
1387
- }, 20000); // Force end loading state after 20 seconds
1388
-
1389
- // Start loading immediately - dual call with window.load for reliability
1390
- loadSpaces(0);
1391
-
1392
- // Display loading indicator control
1393
  function setLoading(isLoading) {
1394
  state.isLoading = isLoading;
1395
  elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
1396
-
1397
  if (isLoading) {
1398
  elements.refreshButton.classList.add('refreshing');
1399
- // Show error message if loading takes too long
1400
  clearTimeout(state.loadingTimeout);
1401
  state.loadingTimeout = setTimeout(() => {
1402
  elements.loadingError.style.display = 'block';
1403
- }, 10000); // Show error message after 10 seconds
1404
  } else {
1405
  elements.refreshButton.classList.remove('refreshing');
1406
  clearTimeout(state.loadingTimeout);
@@ -1408,27 +1039,60 @@ if __name__ == '__main__':
1408
  }
1409
  }
1410
 
1411
- // Create direct URL function with fixes for static sites
1412
- function createDirectUrl(owner, name) {
 
 
 
 
 
 
 
 
 
 
 
 
 
1413
  try {
1414
- // 1. Replace '.' characters with '-'
1415
- name = name.replace(/\./g, '-');
1416
- // 2. Replace '_' characters with '-'
1417
- name = name.replace(/_/g, '-');
1418
- // 3. Convert everything to lowercase
1419
- owner = owner.toLowerCase();
1420
- name = name.toLowerCase();
1421
-
1422
- return `https://${owner}-${name}.hf.space`;
1423
- } catch (error) {
1424
- console.error('URL creation error:', error);
1425
- return 'https://huggingface.co';
1426
  }
1427
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1428
  </script>
1429
  </body>
1430
  </html>
1431
  ''')
1432
 
1433
- # Use port 7860 for Huggingface Spaces
1434
  app.run(host='0.0.0.0', port=7860)
 
7
 
8
  app = Flask(__name__)
9
 
10
+ ###################
11
+ # 1) 최대 50개만 가져오기 위해 limit=50으로 수정
12
+ ###################
13
+ def fetch_trending_spaces(offset=0, limit=50):
14
  try:
 
15
  url = "https://huggingface.co/api/spaces"
16
+ # 여기서는 최대 10000개를 가져오지만, slicing 시에 우리가 limit=50 적용
17
+ params = {"limit": 10000}
18
 
 
19
  response = requests.get(url, params=params, timeout=30)
20
 
21
  if response.status_code == 200:
22
  spaces = response.json()
23
+ filtered_spaces = [space for space in spaces
24
+ if space.get('owner') != 'None'
25
+ and space.get('id', '').split('/', 1)[0] != 'None']
26
 
27
+ # offset limit 적용
28
  start = min(offset, len(filtered_spaces))
29
  end = min(offset + limit, len(filtered_spaces))
30
 
31
+ print(f"Fetched {len(filtered_spaces)} spaces, returning {end - start} items from {start} to {end}")
32
 
33
  return {
34
  'spaces': filtered_spaces[start:end],
35
+ 'total': min(len(filtered_spaces), 50), # 최대 50까지만 총합으로 반영
36
  'offset': offset,
37
  'limit': limit,
38
+ 'all_spaces': filtered_spaces[:50] # 전체 통계 계산도 50개만
39
  }
40
  else:
41
  print(f"Error fetching spaces: {response.status_code}")
 
42
  return {
43
  'spaces': generate_dummy_spaces(limit),
44
+ 'total': 50,
45
  'offset': offset,
46
  'limit': limit,
47
+ 'all_spaces': generate_dummy_spaces(50)
48
  }
49
  except Exception as e:
50
  print(f"Exception when fetching spaces: {e}")
 
51
  return {
52
  'spaces': generate_dummy_spaces(limit),
53
+ 'total': 50,
54
  'offset': offset,
55
  'limit': limit,
56
+ 'all_spaces': generate_dummy_spaces(50)
57
  }
58
 
 
59
  def generate_dummy_spaces(count):
60
  spaces = []
61
  for i in range(count):
 
68
  })
69
  return spaces
70
 
 
71
  def transform_url(owner, name):
 
72
  name = name.replace('.', '-')
 
73
  name = name.replace('_', '-')
 
74
  owner = owner.lower()
75
  name = name.lower()
 
76
  return f"https://{owner}-{name}.hf.space"
77
 
 
78
  def get_space_details(space_data, index, offset):
79
  try:
 
80
  if '/' in space_data.get('id', ''):
81
  owner, name = space_data.get('id', '').split('/', 1)
82
  else:
83
  owner = space_data.get('owner', '')
84
  name = space_data.get('id', '')
85
 
 
86
  if owner == 'None' or name == 'None':
87
  return None
88
 
 
89
  original_url = f"https://huggingface.co/spaces/{owner}/{name}"
90
  embed_url = transform_url(owner, name)
 
 
91
  likes_count = space_data.get('likes', 0)
 
 
92
  title = space_data.get('title', name)
 
 
93
  tags = space_data.get('tags', [])
94
 
95
  return {
 
97
  'embedUrl': embed_url,
98
  'title': title,
99
  'owner': owner,
100
+ 'name': name,
101
  'likes_count': likes_count,
102
  'tags': tags,
103
  'rank': offset + index + 1
104
  }
105
  except Exception as e:
106
  print(f"Error processing space data: {e}")
 
107
  return {
108
  'url': 'https://huggingface.co/spaces',
109
  'embedUrl': 'https://huggingface.co/spaces',
 
115
  'rank': offset + index + 1
116
  }
117
 
 
118
  def get_owner_stats(all_spaces):
119
  owners = []
120
  for space in all_spaces:
 
126
  if owner != 'None':
127
  owners.append(owner)
128
 
 
129
  owner_counts = Counter(owners)
 
 
130
  top_owners = owner_counts.most_common(30)
 
131
  return top_owners
132
 
133
+ ###########################
134
+ # 2) 두 번째 탭을 위한 "고정된" Huggingface URL 목록 예시
135
+ # (원하는 스페이스들을 추가하세요)
136
+ ###########################
137
+ CUSTOM_SPACES = [
138
+ {
139
+ 'owner': 'huggingface',
140
+ 'name': 'diffuse-the-rest',
141
+ 'title': 'Diffuse The Rest Demo',
142
+ 'likes': 999,
143
+ },
144
+ {
145
+ 'owner': 'openai',
146
+ 'name': 'whisper',
147
+ 'title': 'Whisper Demo',
148
+ 'likes': 777,
149
+ },
150
+ {
151
+ 'owner': 'HuggingFaceH4',
152
+ 'name': 'Chat-UI',
153
+ 'title': 'Chat UI (HuggingFaceH4)',
154
+ 'likes': 450,
155
+ },
156
+ ]
157
+
158
+ ###########################
159
+ # 2) 두 번째 탭 API 라우트
160
+ ###########################
161
+ @app.route('/api/custom-spaces', methods=['GET'])
162
+ def custom_spaces():
163
+ # 필요하다면 검색 기능 등 추가 가능. 여기서는 단순히 전체 반환
164
+ results = []
165
+ for index, item in enumerate(CUSTOM_SPACES):
166
+ owner = item['owner']
167
+ name = item['name']
168
+ title = item.get('title', name)
169
+ likes_count = item.get('likes', 0)
170
+
171
+ # rank는 임의로 index+1
172
+ results.append({
173
+ 'url': f"https://huggingface.co/spaces/{owner}/{name}",
174
+ 'embedUrl': transform_url(owner, name),
175
+ 'title': title,
176
+ 'owner': owner,
177
+ 'name': name,
178
+ 'likes_count': likes_count,
179
+ 'tags': [],
180
+ 'rank': index + 1
181
+ })
182
+
183
+ return jsonify({
184
+ 'spaces': results,
185
+ 'total': len(results),
186
+ })
187
+
188
+
189
  @app.route('/')
190
  def home():
191
  return render_template('index.html')
192
 
 
193
  @app.route('/api/trending-spaces', methods=['GET'])
194
  def trending_spaces():
195
  search_query = request.args.get('search', '').lower()
196
  offset = int(request.args.get('offset', 0))
197
+ # 여기서도 limit=50으로 고정
198
+ limit = int(request.args.get('limit', 50))
199
+
200
  spaces_data = fetch_trending_spaces(offset, limit)
201
 
 
202
  results = []
203
  for index, space_data in enumerate(spaces_data['spaces']):
204
  space_info = get_space_details(space_data, index, offset)
 
205
  if not space_info:
206
  continue
207
+
208
+ # 검색어 필터
209
  if search_query:
210
  title = space_info['title'].lower()
211
  owner = space_info['owner'].lower()
212
  url = space_info['url'].lower()
213
  tags = ' '.join([str(tag) for tag in space_info.get('tags', [])]).lower()
214
 
215
+ if (search_query not in title
216
+ and search_query not in owner
217
+ and search_query not in url
218
+ and search_query not in tags):
219
  continue
220
 
221
  results.append(space_info)
222
 
 
223
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
224
 
225
  return jsonify({
 
227
  'total': spaces_data['total'],
228
  'offset': offset,
229
  'limit': limit,
230
+ 'top_owners': top_owners
231
  })
232
 
233
  if __name__ == '__main__':
 
234
  os.makedirs('templates', exist_ok=True)
235
 
236
+ # index.html 생성 (원본에서 탭 기능만 추가)
237
  with open('templates/index.html', 'w', encoding='utf-8') as f:
238
  f.write('''<!DOCTYPE html>
239
  <html lang="en">
240
  <head>
241
  <meta charset="UTF-8">
 
242
  <title>Huggingface Spaces Gallery</title>
243
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
244
  <style>
245
+ /* (중략) — 원본 CSS 그대로 유지 */
246
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
247
 
248
  :root {
 
264
  --text-secondary: #666;
265
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
266
  }
 
267
  * {
268
+ margin: 0; padding: 0; box-sizing: border-box;
 
 
269
  }
 
270
  body {
271
+ font-family: 'Nunito', sans-serif;
272
  line-height: 1.6;
273
  color: var(--text-primary);
274
  background-color: #f8f9fa;
 
276
  min-height: 100vh;
277
  padding: 2rem;
278
  }
 
279
  .container {
280
  max-width: 1600px;
281
  margin: 0 auto;
282
  }
 
 
283
  .mac-window {
284
  background-color: var(--mac-window-bg);
285
  border-radius: 10px;
 
289
  margin-bottom: 2rem;
290
  border: 1px solid var(--mac-border);
291
  }
 
292
  .mac-toolbar {
293
  display: flex;
294
  align-items: center;
 
296
  background-color: var(--mac-toolbar);
297
  border-bottom: 1px solid var(--mac-border);
298
  }
 
299
  .mac-buttons {
300
+ display: flex; gap: 8px; margin-right: 15px;
 
 
301
  }
 
302
  .mac-button {
303
+ width: 12px; height: 12px; border-radius: 50%; cursor: default;
 
 
 
304
  }
305
+ .mac-close { background-color: var(--mac-button-red); }
306
+ .mac-minimize { background-color: var(--mac-button-yellow); }
307
+ .mac-maximize { background-color: var(--mac-button-green); }
 
 
 
 
 
 
 
 
 
 
308
  .mac-title {
309
+ flex-grow: 1; text-align: center; font-size: 0.9rem; color: var(--text-secondary);
 
 
 
310
  }
311
+ .mac-content { padding: 20px; }
 
 
 
 
 
312
  .header {
313
  text-align: center;
314
+ margin-bottom: 1.5rem; position: relative;
 
315
  }
 
316
  .header h1 {
317
+ font-size: 2.2rem; font-weight: 700; margin: 0; color: #2d3748; letter-spacing: -0.5px;
 
 
 
 
318
  }
 
319
  .header p {
320
  color: var(--text-secondary);
321
  margin-top: 0.5rem;
322
  font-size: 1.1rem;
323
  }
 
 
324
  .search-bar {
325
+ display: flex; align-items: center; margin-bottom: 1.5rem;
326
+ background-color: white; border-radius: 30px; padding: 5px;
327
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); max-width: 600px;
328
+ margin-left: auto; margin-right: auto;
 
 
 
 
 
 
329
  }
 
330
  .search-bar input {
331
+ flex-grow: 1; border: none; padding: 12px 20px; font-size: 1rem;
332
+ outline: none; background: transparent; border-radius: 30px;
 
 
 
 
 
333
  }
 
334
  .search-bar .refresh-btn {
335
+ background-color: var(--pastel-green); color: #1a202c; border: none;
336
+ border-radius: 30px; padding: 10px 20px; font-size: 1rem; font-weight: 600;
337
+ cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 8px;
 
 
 
 
 
 
 
 
 
338
  }
 
339
  .search-bar .refresh-btn:hover {
340
  background-color: #9ee7c0;
341
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
342
  }
 
343
  .refresh-icon {
344
+ display: inline-block; width: 16px; height: 16px; border: 2px solid #1a202c;
345
+ border-top-color: transparent; border-radius: 50%; animation: none;
 
 
 
 
 
346
  }
 
347
  .refreshing .refresh-icon {
348
  animation: spin 1s linear infinite;
349
  }
 
350
  @keyframes spin {
351
  0% { transform: rotate(0deg); }
352
  100% { transform: rotate(360deg); }
353
  }
 
 
354
  .grid-container {
355
  display: grid;
356
  grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
357
  gap: 1.5rem;
358
  margin-bottom: 2rem;
359
  }
 
360
  .grid-item {
361
+ height: 500px; position: relative; overflow: hidden;
362
+ transition: all 0.3s ease; border-radius: 15px;
 
 
 
363
  }
 
364
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
365
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
366
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
367
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
368
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
369
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
 
370
  .grid-item:hover {
371
  transform: translateY(-5px);
372
  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
373
  }
 
374
  .grid-header {
375
  padding: 15px;
376
+ display: flex; flex-direction: column;
 
377
  background-color: rgba(255, 255, 255, 0.7);
378
  backdrop-filter: blur(5px);
379
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
380
  }
 
381
  .grid-header-top {
382
+ display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;
 
 
 
383
  }
 
384
  .rank-badge {
385
+ background-color: #1a202c; color: white; font-size: 0.8rem; font-weight: 600;
386
+ padding: 4px 8px; border-radius: 50px;
 
 
 
 
387
  }
 
388
  .grid-header h3 {
389
+ margin: 0; font-size: 1.2rem; font-weight: 700;
390
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
 
 
 
 
391
  }
 
392
  .grid-meta {
393
+ display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem;
 
 
 
394
  }
395
+ .owner-info { color: var(--text-secondary); font-weight: 500; }
 
 
 
 
 
396
  .likes-counter {
397
+ display: flex; align-items: center; color: #e53e3e; font-weight: 600;
 
 
 
 
 
 
 
398
  }
399
+ .likes-counter span { margin-left: 4px; }
400
  .grid-actions {
401
+ padding: 10px 15px; text-align: right; background-color: rgba(255, 255, 255, 0.7);
402
+ backdrop-filter: blur(5px); position: absolute; bottom: 0; left: 0; right: 0;
403
+ z-index: 10; display: flex; justify-content: flex-end;
 
 
 
 
 
 
 
 
404
  }
 
405
  .open-link {
406
+ text-decoration: none; color: #2c5282; font-weight: 600;
407
+ padding: 5px 10px; border-radius: 5px; transition: all 0.2s;
 
 
 
 
408
  background-color: rgba(237, 242, 247, 0.8);
409
  }
410
+ .open-link:hover { background-color: #e2e8f0; }
 
 
 
 
411
  .grid-content {
412
+ position: absolute; top: 0; left: 0; width: 100%; height: 100%;
413
+ padding-top: 85px; padding-bottom: 45px;
 
 
 
 
 
414
  }
 
415
  .iframe-container {
416
+ width: 100%; height: 100%; overflow: hidden; position: relative;
 
 
 
417
  }
 
 
418
  .grid-content iframe {
419
+ transform: scale(0.7); transform-origin: top left;
420
+ width: 142.857%; height: 142.857%; border: none; border-radius: 0;
 
 
 
 
421
  }
 
422
  .error-placeholder {
423
+ position: absolute; top: 0; left: 0; width: 100%; height: 100%;
424
+ display: flex; flex-direction: column; justify-content: center; align-items: center;
425
+ padding: 20px; background-color: rgba(255, 255, 255, 0.9);
 
 
 
 
 
 
 
 
426
  text-align: center;
427
  }
 
428
  .error-emoji {
429
+ font-size: 6rem; margin-bottom: 1.5rem;
 
430
  animation: bounce 1s infinite alternate;
431
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
432
  }
 
433
  @keyframes bounce {
434
+ from { transform: translateY(0px) scale(1); }
435
+ to { transform: translateY(-15px) scale(1.1); }
 
 
 
 
436
  }
 
 
437
  .pagination {
438
+ display: flex; justify-content: center; align-items: center; gap: 10px;
 
 
 
439
  margin: 2rem 0;
440
  }
 
441
  .pagination-button {
442
+ background-color: white; border: none; padding: 10px 20px; border-radius: 10px;
443
+ font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s;
444
+ color: var(--text-primary); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
 
 
 
 
 
 
 
445
  }
 
446
  .pagination-button:hover {
447
+ background-color: #f8f9fa; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
 
448
  }
 
449
  .pagination-button.active {
450
+ background-color: var(--pastel-purple); color: #4a5568;
 
451
  }
 
452
  .pagination-button:disabled {
453
+ background-color: #edf2f7; color: #a0aec0;
454
+ cursor: default; box-shadow: none;
 
 
455
  }
 
 
456
  .loading {
457
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
458
+ background-color: rgba(255, 255, 255, 0.8); backdrop-filter: blur(5px);
459
+ display: flex; justify-content: center; align-items: center; z-index: 1000;
 
 
 
 
 
 
 
 
460
  }
461
+ .loading-content { text-align: center; }
 
 
 
 
462
  .loading-spinner {
463
+ width: 60px; height: 60px; border: 5px solid #e2e8f0;
464
+ border-top-color: var(--pastel-purple); border-radius: 50%;
465
+ animation: spin 1s linear infinite; margin: 0 auto 15px;
 
 
 
 
466
  }
 
467
  .loading-text {
468
+ font-size: 1.2rem; font-weight: 600; color: #4a5568;
 
 
469
  }
 
470
  .loading-error {
471
+ display: none; margin-top: 10px; color: #e53e3e; font-size: 0.9rem;
 
 
 
472
  }
473
+ .stats-window { margin-top: 2rem; margin-bottom: 2rem; }
 
 
 
 
 
 
474
  .stats-header {
475
+ display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;
 
 
 
 
 
 
 
 
 
476
  }
477
+ .stats-title { font-size: 1.5rem; font-weight: 700; color: #2d3748; }
478
  .stats-toggle {
479
+ background-color: var(--pastel-blue); border: none; padding: 8px 16px;
480
+ border-radius: 20px; font-weight: 600; cursor: pointer; transition: all 0.2s;
 
 
 
 
 
481
  }
482
+ .stats-toggle:hover { background-color: var(--pastel-purple); }
 
 
 
 
483
  .stats-content {
484
+ background-color: white; border-radius: 10px; padding: 20px;
485
+ box-shadow: var(--box-shadow); max-height: 0; overflow: hidden;
 
 
 
 
486
  transition: max-height 0.5s ease-out;
487
  }
488
+ .stats-content.open { max-height: 600px; }
489
+ .chart-container { width: 100%; height: 500px; }
 
 
 
 
 
 
 
 
 
490
  @media (max-width: 768px) {
491
+ body { padding: 1rem; }
492
+ .grid-container { grid-template-columns: 1fr; }
493
+ .search-bar { flex-direction: column; padding: 10px; }
494
+ .search-bar input { width: 100%; margin-bottom: 10px; }
495
+ .search-bar .refresh-btn { width: 100%; justify-content: center; }
496
+ .pagination { flex-wrap: wrap; }
497
+ .chart-container { height: 300px; }
498
+ }
499
+ /* 탭 스타일 추가 */
500
+ .tab-nav {
501
+ display: flex; gap: 20px; justify-content: center; margin-bottom: 20px;
502
+ }
503
+ .tab-button {
504
+ padding: 10px 20px; border-radius: 20px; border: none; cursor: pointer;
505
+ background-color: #ffffffcc; font-weight: 600; transition: 0.2s;
506
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
507
+ }
508
+ .tab-button:hover {
509
+ background-color: var(--pastel-green);
 
 
 
 
 
 
 
 
 
 
 
510
  }
511
+ .tab-button.active {
512
+ background-color: var(--pastel-purple);
513
+ color: #4a5568;
 
 
 
 
514
  }
515
+ .tab-content { display: none; }
516
+ .tab-content.active { display: block; }
517
  </style>
518
  </head>
519
  <body>
 
531
  <div class="mac-content">
532
  <div class="header">
533
  <h1>HF Space Leaderboard</h1>
534
+ <p>Discover the top 50 trending spaces from Huggingface</p>
535
  </div>
536
 
537
+ <!-- 네비게이션 -->
538
+ <div class="tab-nav">
539
+ <button class="tab-button active" data-tab="leaderboardTab">리더보드</button>
540
+ <button class="tab-button" data-tab="customTab">두 번째 탭</button>
541
+ </div>
542
+
543
+ <!-- 리더보드 탭 -->
544
+ <div id="leaderboardTab" class="tab-content active">
545
+ <!-- Stats Section (원본) -->
546
+ <div class="stats-window mac-window">
547
+ <div class="mac-toolbar">
548
+ <div class="mac-buttons">
549
+ <div class="mac-button mac-close"></div>
550
+ <div class="mac-button mac-minimize"></div>
551
+ <div class="mac-button mac-maximize"></div>
552
+ </div>
553
+ <div class="mac-title">Creator Statistics</div>
554
  </div>
555
+ <div class="mac-content">
556
+ <div class="stats-header">
557
+ <div class="stats-title">Top 30 Creators by Number of Spaces</div>
558
+ <button id="statsToggle" class="stats-toggle">Show Stats</button>
559
+ </div>
560
+ <div id="statsContent" class="stats-content">
561
+ <div class="chart-container">
562
+ <canvas id="creatorStatsChart"></canvas>
563
+ </div>
564
  </div>
565
  </div>
566
  </div>
567
+
568
+ <div class="search-bar">
569
+ <input type="text" id="searchInput" placeholder="Search by name, owner, or tags..." />
570
+ <button id="refreshButton" class="refresh-btn">
571
+ <span class="refresh-icon"></span>
572
+ Refresh
573
+ </button>
574
+ </div>
575
+ <div id="gridContainer" class="grid-container"></div>
576
+ <div id="pagination" class="pagination"></div>
577
  </div>
578
 
579
+ <!-- 두 번째 탭(커스텀 스페이스) -->
580
+ <div id="customTab" class="tab-content">
581
+ <h2 style="text-align:center; margin-bottom:1rem;">지정된 Huggingface URL 리스트</h2>
582
+ <div id="customGridContainer" class="grid-container"></div>
 
 
583
  </div>
584
 
 
 
 
 
 
585
  </div>
586
  </div>
587
  </div>
588
 
589
+ <div id="loadingIndicator" class="loading" style="display:none;">
590
  <div class="loading-content">
591
  <div class="loading-spinner"></div>
592
  <div class="loading-text">Loading amazing spaces...</div>
 
597
  </div>
598
 
599
  <script>
 
600
  const elements = {
601
  gridContainer: document.getElementById('gridContainer'),
602
+ customGridContainer: document.getElementById('customGridContainer'),
603
  loadingIndicator: document.getElementById('loadingIndicator'),
604
  loadingError: document.getElementById('loadingError'),
605
  searchInput: document.getElementById('searchInput'),
 
610
  creatorStatsChart: document.getElementById('creatorStatsChart')
611
  };
612
 
 
613
  const state = {
614
  isLoading: false,
615
  spaces: [],
616
  currentPage: 0,
617
+ itemsPerPage: 50, // 50개
618
  totalItems: 0,
619
  loadingTimeout: null,
620
+ staticModeAttempted: {},
621
  statsVisible: false,
622
  chartInstance: null,
623
  topOwners: [],
624
+ iframeStatuses: {},
625
+ // 두 번째 탭용
626
+ customSpaces: []
627
  };
628
 
 
629
  const iframeLoader = {
630
  checkQueue: {},
631
+ maxAttempts: 5,
632
+ checkInterval: 5000,
633
 
 
634
  startChecking: function(iframe, owner, name, title, spaceKey) {
 
635
  this.checkQueue[spaceKey] = {
636
+ iframe, owner, name, title,
 
 
 
637
  attempts: 0,
638
  status: 'loading'
639
  };
 
 
640
  this.checkIframeStatus(spaceKey);
641
  },
 
 
642
  checkIframeStatus: function(spaceKey) {
643
  if (!this.checkQueue[spaceKey]) return;
 
644
  const item = this.checkQueue[spaceKey];
645
  const iframe = item.iframe;
646
 
 
647
  if (item.status !== 'loading') {
648
  delete this.checkQueue[spaceKey];
649
  return;
650
  }
651
 
 
652
  item.attempts++;
 
653
  try {
 
654
  if (!iframe || !iframe.parentNode) {
655
  delete this.checkQueue[spaceKey];
656
  return;
657
  }
658
 
 
659
  try {
660
+ const hasContent = iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.body;
 
 
 
 
661
  if (hasContent && iframe.contentWindow.document.body.innerHTML.length > 100) {
 
662
  const bodyText = iframe.contentWindow.document.body.textContent.toLowerCase();
663
+ if (bodyText.includes('forbidden') ||
664
+ bodyText.includes('404') ||
665
  bodyText.includes('not found') ||
666
  bodyText.includes('error')) {
667
  item.status = 'error';
 
672
  delete this.checkQueue[spaceKey];
673
  return;
674
  }
675
+ } catch(e) {}
 
 
676
 
 
677
  const rect = iframe.getBoundingClientRect();
678
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
 
679
  item.status = 'success';
680
  delete this.checkQueue[spaceKey];
681
  return;
682
  }
683
 
 
684
  if (item.attempts >= this.maxAttempts) {
 
685
  if (iframe.offsetWidth > 0 && iframe.offsetHeight > 0) {
 
686
  item.status = 'success';
687
  } else {
 
688
  item.status = 'error';
689
  handleIframeError(iframe, item.owner, item.name, item.title);
690
  }
 
692
  return;
693
  }
694
 
 
695
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
696
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
697
+ } catch(e) {
 
698
  console.error('Error checking iframe status:', e);
 
 
699
  if (item.attempts >= this.maxAttempts) {
700
  item.status = 'error';
701
  handleIframeError(iframe, item.owner, item.name, item.title);
702
  delete this.checkQueue[spaceKey];
703
  } else {
 
704
  setTimeout(() => this.checkIframeStatus(spaceKey), this.checkInterval);
705
  }
706
  }
707
  }
708
  };
709
 
 
710
  function toggleStats() {
711
  state.statsVisible = !state.statsVisible;
712
  elements.statsContent.classList.toggle('open', state.statsVisible);
713
  elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
 
714
  if (state.statsVisible && state.topOwners.length > 0) {
715
  renderCreatorStats();
716
  }
717
  }
718
 
 
719
  function renderCreatorStats() {
720
  if (state.chartInstance) {
721
  state.chartInstance.destroy();
722
  }
 
723
  const ctx = elements.creatorStatsChart.getContext('2d');
 
 
724
  const labels = state.topOwners.map(item => item[0]);
725
  const data = state.topOwners.map(item => item[1]);
 
 
726
  const colors = [];
727
  for (let i = 0; i < labels.length; i++) {
728
  const hue = (i * 360 / labels.length) % 360;
729
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
730
  }
 
 
731
  state.chartInstance = new Chart(ctx, {
732
  type: 'bar',
733
  data: {
 
736
  label: 'Number of Spaces',
737
  data: data,
738
  backgroundColor: colors,
739
+ borderColor: colors.map(c => c.replace('0.7','1')),
740
  borderWidth: 1
741
  }]
742
  },
 
745
  responsive: true,
746
  maintainAspectRatio: false,
747
  plugins: {
748
+ legend: { display: false },
 
 
749
  tooltip: {
750
  callbacks: {
751
+ title: function(tooltipItems) { return tooltipItems[0].label; },
752
+ label: function(context) { return `Spaces: ${context.raw}`; }
 
 
 
 
753
  }
754
  }
755
  },
756
  scales: {
757
  x: {
758
  beginAtZero: true,
759
+ title: { display: true, text: 'Number of Spaces' }
 
 
 
760
  },
761
  y: {
762
+ title: { display: true, text: 'Creator ID' },
 
 
 
 
763
  ticks: {
764
  autoSkip: false,
765
  font: function(context) {
 
766
  const defaultSize = 11;
767
  return {
768
  size: labels.length > 20 ? defaultSize - 1 : defaultSize
 
775
  });
776
  }
777
 
778
+ async function loadSpaces(page=0) {
 
779
  setLoading(true);
 
780
  try {
781
  const searchText = elements.searchInput.value;
782
  const offset = page * state.itemsPerPage;
 
 
783
  const timeoutPromise = new Promise((_, reject) =>
784
  setTimeout(() => reject(new Error('Request timeout')), 30000)
785
  );
 
786
  const fetchPromise = fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`);
 
 
787
  const response = await Promise.race([fetchPromise, timeoutPromise]);
788
  const data = await response.json();
789
 
 
790
  state.spaces = data.spaces;
791
  state.totalItems = data.total;
792
  state.currentPage = page;
793
  state.topOwners = data.top_owners || [];
794
 
795
+ renderGrid(data.spaces, elements.gridContainer);
796
  renderPagination();
797
 
 
798
  if (state.statsVisible && state.topOwners.length > 0) {
799
  renderCreatorStats();
800
  }
801
+ } catch(e) {
802
+ console.error(e);
 
 
803
  elements.gridContainer.innerHTML = `
804
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
805
  <div style="font-size: 3rem; margin-bottom: 20px;">⚠️</div>
806
  <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
807
+ <p style="color: #666;">Please try refreshing the page.</p>
808
  <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
809
  Try Again
810
  </button>
811
  </div>
812
  `;
 
 
813
  document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
 
 
814
  renderPagination();
815
  } finally {
816
  setLoading(false);
817
  }
818
  }
819
 
 
820
  function renderPagination() {
821
  elements.pagination.innerHTML = '';
 
822
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
 
 
823
  const prevButton = document.createElement('button');
824
  prevButton.className = `pagination-button ${state.currentPage === 0 ? 'disabled' : ''}`;
825
  prevButton.textContent = 'Previous';
826
  prevButton.disabled = state.currentPage === 0;
827
  prevButton.addEventListener('click', () => {
828
+ if (state.currentPage > 0) loadSpaces(state.currentPage - 1);
 
 
829
  });
830
  elements.pagination.appendChild(prevButton);
831
 
 
832
  const maxButtons = 7;
833
  let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
834
  let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
 
 
835
  if (endPage - startPage + 1 < maxButtons) {
836
  startPage = Math.max(0, endPage - maxButtons + 1);
837
  }
 
838
  for (let i = startPage; i <= endPage; i++) {
839
  const pageButton = document.createElement('button');
840
  pageButton.className = `pagination-button ${i === state.currentPage ? 'active' : ''}`;
 
847
  elements.pagination.appendChild(pageButton);
848
  }
849
 
 
850
  const nextButton = document.createElement('button');
851
  nextButton.className = `pagination-button ${state.currentPage >= totalPages - 1 ? 'disabled' : ''}`;
852
  nextButton.textContent = 'Next';
 
859
  elements.pagination.appendChild(nextButton);
860
  }
861
 
 
862
  function handleIframeError(iframe, owner, name, title) {
863
  const container = iframe.parentNode;
 
 
864
  const errorPlaceholder = document.createElement('div');
865
  errorPlaceholder.className = 'error-placeholder';
866
 
 
867
  const errorMessage = document.createElement('p');
868
  errorMessage.textContent = `"${title}" space couldn't be loaded`;
869
  errorPlaceholder.appendChild(errorMessage);
870
 
 
871
  const directLink = document.createElement('a');
872
  directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
873
  directLink.target = '_blank';
 
881
  directLink.style.fontWeight = '600';
882
  errorPlaceholder.appendChild(directLink);
883
 
 
884
  iframe.style.display = 'none';
885
  container.appendChild(errorPlaceholder);
886
  }
887
 
888
+ function renderGrid(spaces, containerEl) {
889
+ containerEl.innerHTML = '';
 
 
890
  if (!spaces || spaces.length === 0) {
891
  const noResultsMsg = document.createElement('p');
892
+ noResultsMsg.textContent = 'No spaces found.';
893
  noResultsMsg.style.padding = '2rem';
894
  noResultsMsg.style.textAlign = 'center';
895
  noResultsMsg.style.fontStyle = 'italic';
896
  noResultsMsg.style.color = '#718096';
897
+ containerEl.appendChild(noResultsMsg);
898
  return;
899
  }
900
 
901
+ spaces.forEach(item => {
902
  try {
903
  const { url, title, likes_count, owner, name, rank } = item;
904
+ if (owner === 'None') return;
905
 
 
 
 
 
 
 
906
  const gridItem = document.createElement('div');
907
  gridItem.className = 'grid-item';
908
 
 
909
  const header = document.createElement('div');
910
  header.className = 'grid-header';
911
 
 
912
  const headerTop = document.createElement('div');
913
  headerTop.className = 'grid-header-top';
914
 
 
915
  const titleEl = document.createElement('h3');
916
  titleEl.textContent = title;
917
+ titleEl.title = title;
918
  headerTop.appendChild(titleEl);
919
 
 
920
  const rankBadge = document.createElement('div');
921
  rankBadge.className = 'rank-badge';
922
  rankBadge.textContent = `#${rank}`;
923
  headerTop.appendChild(rankBadge);
 
924
  header.appendChild(headerTop);
925
 
 
926
  const metaInfo = document.createElement('div');
927
  metaInfo.className = 'grid-meta';
928
 
 
929
  const ownerEl = document.createElement('div');
930
  ownerEl.className = 'owner-info';
931
  ownerEl.textContent = `by ${owner}`;
932
  metaInfo.appendChild(ownerEl);
933
 
 
934
  const likesCounter = document.createElement('div');
935
  likesCounter.className = 'likes-counter';
936
  likesCounter.innerHTML = '♥ <span>' + likes_count + '</span>';
937
  metaInfo.appendChild(likesCounter);
938
 
939
  header.appendChild(metaInfo);
 
 
940
  gridItem.appendChild(header);
941
 
 
942
  const content = document.createElement('div');
943
  content.className = 'grid-content';
944
 
 
945
  const iframeContainer = document.createElement('div');
946
  iframeContainer.className = 'iframe-container';
947
 
 
948
  const iframe = document.createElement('iframe');
949
  const directUrl = createDirectUrl(owner, name);
950
  iframe.src = directUrl;
951
  iframe.title = title;
 
952
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
953
  iframe.setAttribute('allowfullscreen', '');
954
  iframe.setAttribute('frameborder', '0');
955
+ iframe.loading = 'lazy';
 
 
 
 
956
 
 
957
  const spaceKey = `${owner}/${name}`;
958
  state.iframeStatuses[spaceKey] = 'loading';
959
 
 
960
  iframe.onload = function() {
961
  iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
962
  };
 
 
963
  iframe.onerror = function() {
964
  handleIframeError(iframe, owner, name, title);
965
  state.iframeStatuses[spaceKey] = 'error';
966
  };
 
 
967
  setTimeout(() => {
968
  if (state.iframeStatuses[spaceKey] === 'loading') {
969
  handleIframeError(iframe, owner, name, title);
 
971
  }
972
  }, 30000);
973
 
 
974
  iframeContainer.appendChild(iframe);
975
  content.appendChild(iframeContainer);
976
 
 
977
  const actions = document.createElement('div');
978
  actions.className = 'grid-actions';
979
 
 
980
  const linkEl = document.createElement('a');
981
  linkEl.href = url;
982
  linkEl.target = '_blank';
 
984
  linkEl.textContent = 'Open in new window';
985
  actions.appendChild(linkEl);
986
 
 
987
  gridItem.appendChild(content);
988
  gridItem.appendChild(actions);
989
 
990
+ containerEl.appendChild(gridItem);
991
+ } catch(e) {
992
+ console.error('Item rendering error:', e);
 
 
993
  }
994
  });
995
  }
996
 
997
+ function createDirectUrl(owner, name) {
998
+ try {
999
+ name = name.replace(/\\./g, '-').replace(/_/g, '-').toLowerCase();
1000
+ owner = owner.toLowerCase();
1001
+ return \`https://\${owner}-\${name}.hf.space\`;
1002
+ } catch(e) {
1003
+ console.error(e);
1004
+ return 'https://huggingface.co';
1005
+ }
1006
+ }
1007
+
1008
  elements.searchInput.addEventListener('input', () => {
 
1009
  clearTimeout(state.searchTimeout);
1010
  state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
1011
  });
 
 
1012
  elements.searchInput.addEventListener('keyup', (event) => {
1013
  if (event.key === 'Enter') {
1014
  loadSpaces(0);
1015
  }
1016
  });
 
 
1017
  elements.refreshButton.addEventListener('click', () => loadSpaces(0));
 
 
1018
  elements.statsToggle.addEventListener('click', toggleStats);
1019
 
 
1020
  document.querySelectorAll('.mac-button').forEach(button => {
1021
+ button.addEventListener('click', (e) => {
1022
  e.preventDefault();
 
1023
  });
1024
  });
1025
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1026
  function setLoading(isLoading) {
1027
  state.isLoading = isLoading;
1028
  elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
 
1029
  if (isLoading) {
1030
  elements.refreshButton.classList.add('refreshing');
 
1031
  clearTimeout(state.loadingTimeout);
1032
  state.loadingTimeout = setTimeout(() => {
1033
  elements.loadingError.style.display = 'block';
1034
+ }, 10000);
1035
  } else {
1036
  elements.refreshButton.classList.remove('refreshing');
1037
  clearTimeout(state.loadingTimeout);
 
1039
  }
1040
  }
1041
 
1042
+ // 전환 기능
1043
+ const tabButtons = document.querySelectorAll('.tab-button');
1044
+ const tabContents = document.querySelectorAll('.tab-content');
1045
+ tabButtons.forEach(btn => {
1046
+ btn.addEventListener('click', () => {
1047
+ tabButtons.forEach(b => b.classList.remove('active'));
1048
+ tabContents.forEach(tc => tc.classList.remove('active'));
1049
+ btn.classList.add('active');
1050
+ const tabId = btn.getAttribute('data-tab');
1051
+ document.getElementById(tabId).classList.add('active');
1052
+ });
1053
+ });
1054
+
1055
+ // 두 번째 탭(커스텀 스페이스) 불러오기
1056
+ async function loadCustomSpaces() {
1057
  try {
1058
+ const resp = await fetch('/api/custom-spaces');
1059
+ const data = await resp.json();
1060
+ state.customSpaces = data.spaces || [];
1061
+ renderGrid(state.customSpaces, elements.customGridContainer);
1062
+ } catch(e) {
1063
+ console.error(e);
1064
+ elements.customGridContainer.innerHTML = '<p style="text-align:center; color:red;">Error loading custom spaces.</p>';
 
 
 
 
 
1065
  }
1066
  }
1067
+
1068
+ // 초기 실행
1069
+ window.addEventListener('load', function() {
1070
+ // 리더보드 로드
1071
+ loadSpaces(0);
1072
+ // 두 번째 탭(커스텀)도 같이 로드
1073
+ loadCustomSpaces();
1074
+ });
1075
+
1076
+ // 안전장치
1077
+ setTimeout(() => {
1078
+ if (state.isLoading) {
1079
+ setLoading(false);
1080
+ elements.gridContainer.innerHTML = \`
1081
+ <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1082
+ <div style="font-size: 3rem; margin-bottom: 20px;">⏱️</div>
1083
+ <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3>
1084
+ <p style="color: #666;">Please try refreshing the page.</p>
1085
+ <button onClick="window.location.reload()" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1086
+ Reload Page
1087
+ </button>
1088
+ </div>
1089
+ \`;
1090
+ }
1091
+ }, 20000);
1092
  </script>
1093
  </body>
1094
  </html>
1095
  ''')
1096
 
1097
+ # Huggingface Spaces에서 보통 port=7860 많이 씁니다.
1098
  app.run(host='0.0.0.0', port=7860)