openfree commited on
Commit
93d180e
ยท
verified ยท
1 Parent(s): 18ae9ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +147 -1411
app.py CHANGED
@@ -1,1434 +1,170 @@
1
  from flask import Flask, render_template, request, jsonify
2
- import requests
3
- import os
4
- import time
5
- import random
6
- 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):
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 {
111
- 'url': original_url,
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',
126
- 'title': 'Error Loading Space',
127
- 'owner': 'huggingface',
128
- 'name': 'error',
129
- 'likes_count': 0,
130
- 'tags': [],
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:
138
- if '/' in space.get('id', ''):
139
- owner, _ = space.get('id', '').split('/', 1)
140
- else:
141
- owner = space.get('owner', '')
142
-
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({
196
- 'spaces': results,
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 {
220
- --pastel-pink: #FFD6E0;
221
- --pastel-blue: #C5E8FF;
222
- --pastel-purple: #E0C3FC;
223
- --pastel-yellow: #FFF2CC;
224
- --pastel-green: #C7F5D9;
225
- --pastel-orange: #FFE0C3;
226
-
227
- --mac-window-bg: rgba(250, 250, 250, 0.85);
228
- --mac-toolbar: #F5F5F7;
229
- --mac-border: #E2E2E2;
230
- --mac-button-red: #FF5F56;
231
- --mac-button-yellow: #FFBD2E;
232
- --mac-button-green: #27C93F;
233
-
234
- --text-primary: #333;
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;
250
- background-image: linear-gradient(135deg, var(--pastel-blue) 0%, var(--pastel-purple) 100%);
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;
264
- box-shadow: var(--box-shadow);
265
- backdrop-filter: blur(10px);
266
- overflow: hidden;
267
- margin-bottom: 2rem;
268
- border: 1px solid var(--mac-border);
269
- }
270
-
271
- .mac-toolbar {
272
- display: flex;
273
- align-items: center;
274
- padding: 10px 15px;
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>
747
- <div class="container">
748
- <div class="mac-window">
749
- <div class="mac-toolbar">
750
- <div class="mac-buttons">
751
- <div class="mac-button mac-close"></div>
752
- <div class="mac-button mac-minimize"></div>
753
- <div class="mac-button mac-maximize"></div>
754
- </div>
755
- <div class="mac-title">Huggingface Explorer</div>
756
- </div>
757
-
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>
808
- <div id="loadingError" class="loading-error">
809
- If this takes too long, try refreshing the page.
810
- </div>
811
- </div>
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'),
821
- refreshButton: document.getElementById('refreshButton'),
822
- pagination: document.getElementById('pagination'),
823
- statsToggle: document.getElementById('statsToggle'),
824
- statsContent: document.getElementById('statsContent'),
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';
903
- handleIframeError(iframe, item.owner, item.name, item.title);
904
- } else {
905
- item.status = 'success';
906
- }
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
- }
934
- delete this.checkQueue[spaceKey];
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: {
992
- labels: labels,
993
- datasets: [{
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
- },
1001
- options: {
1002
- indexAxis: 'y',
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
1041
- };
1042
- }
1043
- }
1044
- }
1045
- }
1046
- }
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' : ''}`;
1138
- pageButton.textContent = i + 1;
1139
- pageButton.addEventListener('click', () => {
1140
- if (i !== state.currentPage) {
1141
- loadSpaces(i);
1142
- }
1143
- });
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';
1151
- nextButton.disabled = state.currentPage >= totalPages - 1;
1152
- nextButton.addEventListener('click', () => {
1153
- if (state.currentPage < totalPages - 1) {
1154
- loadSpaces(state.currentPage + 1);
1155
- }
1156
  });
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';
1177
- directLink.textContent = 'Visit HF Space';
1178
- directLink.style.color = '#3182ce';
1179
- directLink.style.marginTop = '10px';
1180
- directLink.style.display = 'inline-block';
1181
- directLink.style.padding = '8px 16px';
1182
- directLink.style.background = '#ebf8ff';
1183
- directLink.style.borderRadius = '5px';
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);
1305
- state.iframeStatuses[spaceKey] = 'error';
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';
1321
- linkEl.className = 'open-link';
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);
1407
- elements.loadingError.style.display = 'none';
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)
 
1
  from flask import Flask, render_template, request, jsonify
2
+ import os, re, json
 
 
 
 
3
 
4
  app = Flask(__name__)
5
 
6
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 1. CURATED CATEGORIES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
7
+ CATEGORIES = {
8
+ "Productivity": [
9
+ "https://huggingface.co/spaces/ginigen/perflexity-clone",
10
+ "https://huggingface.co/spaces/ginipick/IDEA-DESIGN",
11
+ "https://huggingface.co/spaces/VIDraft/mouse-webgen",
12
+ "https://huggingface.co/spaces/openfree/Vibe-Game",
13
+ "https://huggingface.co/spaces/openfree/Game-Gallery",
14
+ "https://huggingface.co/spaces/aiqtech/Contributors-Leaderboard",
15
+ "https://huggingface.co/spaces/fantaxy/Model-Leaderboard",
16
+ "https://huggingface.co/spaces/fantaxy/Space-Leaderboard",
17
+ "https://huggingface.co/spaces/openfree/Korean-Leaderboard",
18
+ ],
19
+ "Multimodal": [
20
+ "https://huggingface.co/spaces/openfree/DreamO-video",
21
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored-photo",
22
+ "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored",
23
+ "https://huggingface.co/spaces/fantaxy/Sound-AI-SFX",
24
+ "https://huggingface.co/spaces/ginigen/SFX-Sound-magic",
25
+ "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
26
+ "https://huggingface.co/spaces/aiqcamp/MCP-kokoro",
27
+ "https://huggingface.co/spaces/aiqcamp/ENGLISH-Speaking-Scoring",
28
+ ],
29
+ "Professional": [
30
+ "https://huggingface.co/spaces/ginigen/blogger",
31
+ "https://huggingface.co/spaces/VIDraft/money-radar",
32
+ "https://huggingface.co/spaces/immunobiotech/drug-discovery",
33
+ "https://huggingface.co/spaces/immunobiotech/Gemini-MICHELIN",
34
+ "https://huggingface.co/spaces/Heartsync/Papers-Leaderboard",
35
+ "https://huggingface.co/spaces/VIDraft/PapersImpact",
36
+ "https://huggingface.co/spaces/ginipick/AgentX-Papers",
37
+ "https://huggingface.co/spaces/openfree/Cycle-Navigator",
38
+ ],
39
+ "Image": [
40
+ "https://huggingface.co/spaces/ginigen/interior-design",
41
+ "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
42
+ "https://huggingface.co/spaces/ginigen/Multi-LoRAgen",
43
+ "https://huggingface.co/spaces/ginigen/Every-Text",
44
+ "https://huggingface.co/spaces/ginigen/text3d-r1",
45
+ "https://huggingface.co/spaces/ginipick/FLUXllama",
46
+ "https://huggingface.co/spaces/Heartsync/FLUX-Vision",
47
+ "https://huggingface.co/spaces/ginigen/VisualCloze",
48
+ "https://huggingface.co/spaces/seawolf2357/Ghibli-Multilingual-Text-rendering",
49
+ "https://huggingface.co/spaces/ginigen/Ghibli-Meme-Studio",
50
+ "https://huggingface.co/spaces/VIDraft/Open-Meme-Studio",
51
+ "https://huggingface.co/spaces/ginigen/3D-LLAMA",
52
+ ],
53
+ "LLM / VLM": [
54
+ "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-4B",
55
+ "https://huggingface.co/spaces/VIDraft/Gemma-3-R1984-12B",
56
+ "https://huggingface.co/spaces/ginigen/Mistral-Perflexity",
57
+ "https://huggingface.co/spaces/aiqcamp/gemini-2.5-flash-preview",
58
+ "https://huggingface.co/spaces/openfree/qwen3-30b-a3b-research",
59
+ "https://huggingface.co/spaces/openfree/qwen3-235b-a22b-research",
60
+ "https://huggingface.co/spaces/openfree/Llama-4-Maverick-17B-Research",
61
+ ],
62
+ }
63
 
64
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 2. HELPERS: build embed & direct URLs from HF link โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
65
+ def space_embed_url(hf_url: str) -> str:
66
+ m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
67
+ if not m:
68
+ return hf_url
69
+ owner, name = m.groups()
70
+ return f"https://huggingface.co/spaces/{owner}/{name}/embed"
 
 
 
 
 
71
 
72
+ def space_direct_url(hf_url: str) -> str:
73
+ m = re.match(r"https?://huggingface\.co/spaces/([^/]+)/([^/?#]+)", hf_url)
74
+ if not m:
75
+ return hf_url
76
+ owner, name = m.groups()
77
+ # dots/underscores โ†’ dashes, lowercase
 
78
  owner = owner.lower()
79
+ name = name.replace('.', '-').replace('_', '-').lower()
 
80
  return f"https://{owner}-{name}.hf.space"
81
 
82
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 3. API: return spaces for a given category โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
83
+ @app.route('/api/category')
84
+ def api_category():
85
+ cat = request.args.get('name', '')
86
+ urls = CATEGORIES.get(cat, [])
87
+ spaces = [{
88
+ "title": url.split('/')[-1],
89
+ "embedUrl": space_embed_url(url),
90
+ "directUrl": space_direct_url(url)
91
+ } for url in urls]
92
+ return jsonify({"spaces": spaces})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 4. ROUTES โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  @app.route('/')
96
  def home():
97
+ return render_template('index.html', categories=list(CATEGORIES.keys()))
98
 
99
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 5. CREATE index.html with tabbed UI (once) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
100
+ if not os.path.exists('templates'):
101
+ os.makedirs('templates')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ with open('templates/index.html', 'w', encoding='utf-8') as f:
104
+ f.write(r'''<!DOCTYPE html>
 
 
 
 
 
105
  <html lang="en">
106
  <head>
107
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
108
+ <title>Curated HF Spaces</title>
109
+ <style>
110
+ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;500;700&display=swap');
111
+ body{margin:0;font-family:Nunito,sans-serif;background:#f0f2f8;}
112
+ .tabs{display:flex;gap:6px;flex-wrap:wrap;padding:16px;}
113
+ .tab-btn{padding:8px 14px;border:none;border-radius:20px;background:#e2e8f0;cursor:pointer;font-weight:600}
114
+ .tab-btn.active{background:#c4b5fd;color:#1a202c}
115
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;padding:0 16px 40px}
116
+ .card{background:#fff;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.06);overflow:hidden;display:flex;flex-direction:column;height:420px;position:relative}
117
+ .tag{position:absolute;top:6px;left:6px;background:#10b981;color:#fff;font-size:.65rem;font-weight:700;padding:2px 6px;border-radius:4px;z-index:2}
118
+ .frame{flex:1;overflow:hidden}
119
+ .frame iframe{position:absolute;width:166.667%;height:166.667%;transform:scale(.6);transform-origin:top left;border:0}
120
+ .foot{height:40px;background:#fafafa;display:flex;align-items:center;justify-content:center;border-top:1px solid #eee}
121
+ .foot a{font-size:.82rem;font-weight:600;color:#4a6dd8;text-decoration:none}
122
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  </head>
124
  <body>
125
+ <div class="tabs" id="tabs"></div>
126
+ <div id="grid" class="grid"></div>
127
+
128
+ <script>
129
+ const cats={{ categories|tojson }};
130
+ const tabsEl=document.getElementById('tabs');
131
+ const gridEl=document.getElementById('grid');
132
+ let active='';
133
+
134
+ function loadTab(cat){
135
+ if(cat===active) return;
136
+ active=cat;
137
+ [...tabsEl.children].forEach(b=>b.classList.toggle('active',b.dataset.cat===cat));
138
+ gridEl.innerHTML='<p style="grid-column:1/-1;text-align:center;padding:40px">Loadingโ€ฆ</p>';
139
+ fetch(`/api/category?name=${encodeURIComponent(cat)}`)
140
+ .then(r=>r.json()).then(data=>{
141
+ gridEl.innerHTML='';
142
+ data.spaces.forEach(sp=>{
143
+ const card=document.createElement('div');
144
+ card.className='card';
145
+ card.innerHTML=`
146
+ <span class="tag">LIVE</span>
147
+ <div class="frame"><iframe src="${sp.embedUrl}" loading="lazy" allow="accelerometer; gyroscope;"></iframe></div>
148
+ <div class="foot"><a href="${sp.directUrl}" target="_blank">Open โ†—</a></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  `;
150
+ gridEl.appendChild(card);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  });
153
+ }
154
+
155
+ cats.forEach((c,i)=>{
156
+ const b=document.createElement('button');
157
+ b.textContent=c;
158
+ b.className='tab-btn';
159
+ b.dataset.cat=c;
160
+ b.onclick=()=>loadTab(c);
161
+ tabsEl.appendChild(b);
162
+ if(i===0) loadTab(c);
163
+ });
164
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  </body>
166
+ </html>''')
167
+
168
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ 6. RUN โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
169
+ if __name__ == '__main__':
170
  app.run(host='0.0.0.0', port=7860)