openfree commited on
Commit
32c75f5
ยท
verified ยท
1 Parent(s): f7ae2b0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +223 -568
app.py CHANGED
@@ -7,6 +7,19 @@ from collections import Counter
7
 
8
  app = Flask(__name__)
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  # Generate dummy spaces in case of error
11
  def generate_dummy_spaces(count):
12
  """
@@ -29,27 +42,31 @@ def generate_dummy_spaces(count):
29
  })
30
  return spaces
31
 
32
- # Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
33
- def fetch_trending_spaces(offset=0, limit=72):
 
 
34
  """
35
- Trending์šฉ CPU ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ (์ •๋ ฌ์€ Hugging Face ๊ธฐ๋ณธ ์ •๋ ฌ)
 
36
  """
37
  try:
38
  url = "https://huggingface.co/api/spaces"
39
  params = {
40
  "limit": 10000, # ๋” ๋งŽ์ด ๊ฐ€์ ธ์˜ค๊ธฐ
41
- "hardware": "cpu" # <-- Zero GPU(=CPU) ํ•„ํ„ฐ ์ ์šฉ
42
  }
43
  response = requests.get(url, params=params, timeout=30)
44
 
45
  if response.status_code == 200:
46
  spaces = response.json()
47
 
48
- # owner๋‚˜ id๊ฐ€ 'None'์ธ ๊ฒฝ์šฐ ์ œ์™ธ
49
  filtered_spaces = [
50
  space for space in spaces
51
  if space.get('owner') != 'None'
52
  and space.get('id', '').split('/', 1)[0] != 'None'
 
53
  ]
54
 
55
  # ์ „์ฒด ๋ชฉ๋ก์— ๋Œ€ํ•ด "๊ธ€๋กœ๋ฒŒ ๋žญํฌ"๋ฅผ ๋งค๊ธด๋‹ค (1๋ถ€ํ„ฐ ์‹œ์ž‘)
@@ -60,7 +77,7 @@ def fetch_trending_spaces(offset=0, limit=72):
60
  start = min(offset, len(filtered_spaces))
61
  end = min(offset + limit, len(filtered_spaces))
62
 
63
- print(f"[fetch_trending_spaces] CPU๊ธฐ๋ฐ˜ ์ŠคํŽ˜์ด์Šค ์ด {len(filtered_spaces)}๊ฐœ, "
64
  f"์š”์ฒญ ๊ตฌ๊ฐ„ {start}~{end-1} ๋ฐ˜ํ™˜")
65
 
66
  return {
@@ -92,50 +109,51 @@ def fetch_trending_spaces(offset=0, limit=72):
92
  'all_spaces': generate_dummy_spaces(500)
93
  }
94
 
95
- def fetch_latest_spaces(offset=0, limit=72):
 
 
 
96
  """
97
- 'createdAt' ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ตœ๊ทผ ์ŠคํŽ˜์ด์Šค 500๊ฐœ๋ฅผ ์ถ”๋ฆฐ ๋’ค,
98
- offset ~ offset+limit ๊ฐœ๋งŒ ๋ฐ˜ํ™˜
99
  """
100
  try:
101
  url = "https://huggingface.co/api/spaces"
102
  params = {
103
- "limit": 10000, # ์ถฉ๋ถ„ํžˆ ๋งŽ์ด ๊ฐ€์ ธ์˜ด
104
  "hardware": "cpu"
105
  }
106
  response = requests.get(url, params=params, timeout=30)
107
 
108
  if response.status_code == 200:
109
  spaces = response.json()
110
-
111
- # owner๋‚˜ id๊ฐ€ 'None'์ธ ๊ฒฝ์šฐ ์ œ์™ธ
112
  filtered_spaces = [
113
  space for space in spaces
114
  if space.get('owner') != 'None'
115
  and space.get('id', '').split('/', 1)[0] != 'None'
 
116
  ]
117
-
118
- # createdAt ๋‚ด๋ฆผ์ฐจ์ˆœ ์ •๋ ฌ
119
- # createdAt ์˜ˆ: "2023-01-01T00:00:00.000Z"
120
- # ๋ฌธ์ž์—ด ๋น„๊ต๋„ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์•ˆ์ •์„ฑ์„ ์œ„ํ•ด time ํŒŒ์‹ฑ ํ›„ ๋น„๊ตํ•  ์ˆ˜๋„ ์žˆ์Œ
121
  def parse_time(sp):
122
  return sp.get('createdAt') or ''
123
-
124
- # ๋‚ด๋ฆผ์ฐจ์ˆœ
125
  filtered_spaces.sort(key=parse_time, reverse=True)
126
-
127
  # ์ƒ์œ„ 500๊ฐœ๋งŒ ์ถ”๋ฆฌ๊ธฐ
128
  truncated = filtered_spaces[:500]
129
-
130
  # ํ•„์š”ํ•œ ๊ตฌ๊ฐ„ ์Šฌ๋ผ์ด์‹ฑ
131
  start = min(offset, len(truncated))
132
  end = min(offset + limit, len(truncated))
133
-
134
- print(f"[fetch_latest_spaces] CPU๊ธฐ๋ฐ˜ ์ŠคํŽ˜์ด์Šค ์ด {len(spaces)}๊ฐœ ์ค‘ ํ•„ํ„ฐ ํ›„ {len(filtered_spaces)}๊ฐœ, ์ƒ์œ„ 500๊ฐœ ์ค‘ {start}~{end-1} ๋ฐ˜ํ™˜")
 
135
 
136
  return {
137
  'spaces': truncated[start:end],
138
- 'total': len(truncated), # 500 ์ดํ•˜
139
  'offset': offset,
140
  'limit': limit
141
  }
@@ -238,12 +256,10 @@ def get_space_details(space_data, index, offset):
238
  # Get owner statistics from all spaces (for the "Trending" tab's top owners)
239
  def get_owner_stats(all_spaces):
240
  """
241
- ์ƒ์œ„ 500์œ„(global_rank <= 500) ์ด๋‚ด์— ๋ฐฐ์น˜๋œ ์ŠคํŽ˜์ด์Šค๋“ค์˜ owner๋ฅผ ์ถ”์ถœํ•ด,
242
- ๊ฐ owner๊ฐ€ ๋ช‡ ๋ฒˆ ๋“ฑ์žฅํ–ˆ๋Š”์ง€ ์„ผ ๋’ค ์ƒ์œ„ 30๋ช…๋งŒ ๋ฐ˜ํ™˜
243
  """
244
- # Top 500
245
  top_500 = [s for s in all_spaces if s.get('global_rank', 999999) <= 500]
246
-
247
  owners = []
248
  for space in top_500:
249
  if '/' in space.get('id', ''):
@@ -253,10 +269,7 @@ def get_owner_stats(all_spaces):
253
  if owner and owner != 'None':
254
  owners.append(owner)
255
 
256
- # Count occurrences of each owner in top 500
257
  owner_counts = Counter(owners)
258
-
259
- # Get top 30 owners by count
260
  top_owners = owner_counts.most_common(30)
261
  return top_owners
262
 
@@ -272,16 +285,15 @@ def home():
272
  @app.route('/api/trending-spaces', methods=['GET'])
273
  def trending_spaces():
274
  """
275
- hardware=cpu ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์™€ ๊ฒ€์ƒ‰, ํŽ˜์ด์ง•, ํ†ต๊ณ„ ๋“ฑ์„ ์ ์šฉ (๊ธฐ์กด 'Trending')
 
276
  """
277
  search_query = request.args.get('search', '').lower()
278
  offset = int(request.args.get('offset', 0))
279
- limit = int(request.args.get('limit', 72))
280
 
281
- # Fetch zero-gpu (cpu) spaces
282
  spaces_data = fetch_trending_spaces(offset, limit)
283
 
284
- # Process and filter spaces
285
  results = []
286
  for index, space_data in enumerate(spaces_data['spaces']):
287
  space_info = get_space_details(space_data, index, offset)
@@ -298,7 +310,6 @@ def trending_spaces():
298
 
299
  results.append(space_info)
300
 
301
- # ์˜ค๋„ˆ ํ†ต๊ณ„ (Top 500 โ†’ Top 30)
302
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
303
 
304
  return jsonify({
@@ -313,11 +324,11 @@ def trending_spaces():
313
  @app.route('/api/latest-spaces', methods=['GET'])
314
  def latest_spaces():
315
  """
316
- hardware=cpu ์ŠคํŽ˜์ด์Šค ์ค‘์—์„œ createdAt ๊ธฐ์ค€์œผ๋กœ ์ตœ์‹ ์ˆœ 500๊ฐœ๋ฅผ ํŽ˜์ด์ง•, ๊ฒ€์ƒ‰
317
  """
318
  search_query = request.args.get('search', '').lower()
319
  offset = int(request.args.get('offset', 0))
320
- limit = int(request.args.get('limit', 72))
321
 
322
  spaces_data = fetch_latest_spaces(offset, limit)
323
 
@@ -353,6 +364,7 @@ if __name__ == '__main__':
353
 
354
  # index.html ์ „์ฒด๋ฅผ ์ƒˆ๋กœ ์ž‘์„ฑ
355
  with open('templates/index.html', 'w', encoding='utf-8') as f:
 
356
  f.write('''<!DOCTYPE html>
357
  <html lang="en">
358
  <head>
@@ -360,9 +372,8 @@ if __name__ == '__main__':
360
  <title>Huggingface Zero-GPU Spaces</title>
361
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
362
  <style>
363
- /* Google Fonts & Base Styling */
364
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
365
-
366
  :root {
367
  --pastel-pink: #FFD6E0;
368
  --pastel-blue: #C5E8FF;
@@ -370,27 +381,24 @@ if __name__ == '__main__':
370
  --pastel-yellow: #FFF2CC;
371
  --pastel-green: #C7F5D9;
372
  --pastel-orange: #FFE0C3;
373
-
374
  --mac-window-bg: rgba(250, 250, 250, 0.85);
375
  --mac-toolbar: #F5F5F7;
376
  --mac-border: #E2E2E2;
377
  --mac-button-red: #FF5F56;
378
  --mac-button-yellow: #FFBD2E;
379
  --mac-button-green: #27C93F;
380
-
381
  --text-primary: #333;
382
  --text-secondary: #666;
383
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
384
  }
385
-
386
  * {
387
- margin: 0;
388
- padding: 0;
389
- box-sizing: border-box;
390
  }
391
-
392
  body {
393
- font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
 
394
  line-height: 1.6;
395
  color: var(--text-primary);
396
  background-color: #f8f9fa;
@@ -398,13 +406,10 @@ if __name__ == '__main__':
398
  min-height: 100vh;
399
  padding: 2rem;
400
  }
401
-
402
  .container {
403
  max-width: 1600px;
404
  margin: 0 auto;
405
  }
406
-
407
- /* Mac OS Window Styling */
408
  .mac-window {
409
  background-color: var(--mac-window-bg);
410
  border-radius: 10px;
@@ -414,587 +419,237 @@ if __name__ == '__main__':
414
  margin-bottom: 2rem;
415
  border: 1px solid var(--mac-border);
416
  }
417
-
418
  .mac-toolbar {
419
- display: flex;
420
- align-items: center;
421
  padding: 10px 15px;
422
  background-color: var(--mac-toolbar);
423
  border-bottom: 1px solid var(--mac-border);
424
  }
425
-
426
  .mac-buttons {
427
- display: flex;
428
- gap: 8px;
429
- margin-right: 15px;
430
  }
431
-
432
  .mac-button {
433
- width: 12px;
434
- height: 12px;
435
- border-radius: 50%;
436
- cursor: default;
437
  }
438
-
439
- .mac-close {
440
- background-color: var(--mac-button-red);
441
- }
442
-
443
- .mac-minimize {
444
- background-color: var(--mac-button-yellow);
445
- }
446
-
447
- .mac-maximize {
448
- background-color: var(--mac-button-green);
449
- }
450
-
451
  .mac-title {
452
- flex-grow: 1;
453
- text-align: center;
454
- font-size: 0.9rem;
455
- color: var(--text-secondary);
456
  }
457
-
458
  .mac-content {
459
  padding: 20px;
460
  }
461
-
462
- /* Header Styling */
463
- .header {
464
- text-align: center;
465
- margin-bottom: 1.5rem;
466
- position: relative;
467
- }
468
-
469
  .header h1 {
470
- font-size: 2.2rem;
471
- font-weight: 700;
472
- margin: 0;
473
- color: #2d3748;
474
- letter-spacing: -0.5px;
475
- }
476
-
477
- .header p {
478
- color: var(--text-secondary);
479
- margin-top: 0.5rem;
480
- font-size: 1.1rem;
481
- }
482
-
483
- /* Tabs Styling */
484
- .tab-nav {
485
- display: flex;
486
- justify-content: center;
487
- margin-bottom: 1.5rem;
488
- }
489
-
490
  .tab-button {
491
- border: none;
492
- background-color: #edf2f7;
493
- color: var(--text-primary);
494
- padding: 10px 20px;
495
- margin: 0 5px;
496
- cursor: pointer;
497
- border-radius: 5px;
498
- font-size: 1rem;
499
- font-weight: 600;
500
- }
501
-
502
- .tab-button.active {
503
- background-color: var(--pastel-purple);
504
- color: #fff;
505
- }
506
-
507
- .tab-content {
508
- display: none;
509
- }
510
-
511
- .tab-content.active {
512
- display: block;
513
- }
514
-
515
- /* Controls Styling */
516
  .search-bar {
517
- display: flex;
518
- align-items: center;
519
- margin-bottom: 1.5rem;
520
- background-color: white;
521
- border-radius: 30px;
522
- padding: 5px;
523
- box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
524
- max-width: 600px;
525
- margin-left: auto;
526
- margin-right: auto;
527
- }
528
-
529
  .search-bar input {
530
- flex-grow: 1;
531
- border: none;
532
- padding: 12px 20px;
533
- font-size: 1rem;
534
- outline: none;
535
- background: transparent;
536
- border-radius: 30px;
537
- }
538
-
539
  .search-bar .refresh-btn {
540
- background-color: var(--pastel-green);
541
- color: #1a202c;
542
- border: none;
543
- border-radius: 30px;
544
- padding: 10px 20px;
545
- font-size: 1rem;
546
- font-weight: 600;
547
- cursor: pointer;
548
- transition: all 0.2s;
549
- display: flex;
550
- align-items: center;
551
- gap: 8px;
552
- }
553
-
554
  .search-bar .refresh-btn:hover {
555
- background-color: #9ee7c0;
556
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
557
  }
558
-
559
  .refresh-icon {
560
- display: inline-block;
561
- width: 16px;
562
- height: 16px;
563
- border: 2px solid #1a202c;
564
- border-top-color: transparent;
565
- border-radius: 50%;
566
  animation: none;
567
  }
568
-
569
- .refreshing .refresh-icon {
570
- animation: spin 1s linear infinite;
571
- }
572
-
573
  @keyframes spin {
574
  0% { transform: rotate(0deg); }
575
  100% { transform: rotate(360deg); }
576
  }
577
-
578
- /* Grid Styling */
579
  .grid-container {
580
- display: grid;
581
- grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
582
- gap: 1.5rem;
583
- margin-bottom: 2rem;
584
  }
585
-
586
  .grid-item {
587
- height: 500px;
588
- position: relative;
589
- overflow: hidden;
590
- transition: all 0.3s ease;
591
- border-radius: 15px;
592
  }
593
-
594
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
595
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
596
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
597
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
598
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
599
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
600
-
601
  .grid-item:hover {
602
- transform: translateY(-5px);
603
- box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
604
  }
605
-
606
  .grid-header {
607
- padding: 15px;
608
- display: flex;
609
- flex-direction: column;
610
- background-color: rgba(255, 255, 255, 0.7);
611
- backdrop-filter: blur(5px);
612
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
613
  }
614
-
615
  .grid-header-top {
616
- display: flex;
617
- justify-content: space-between;
618
- align-items: center;
619
- margin-bottom: 8px;
620
  }
621
-
622
  .rank-badge {
623
- background-color: #1a202c;
624
- color: white;
625
- font-size: 0.8rem;
626
- font-weight: 600;
627
- padding: 4px 8px;
628
- border-radius: 50px;
629
- display: inline-block;
630
- }
631
-
632
  .grid-header h3 {
633
- margin: 0;
634
- font-size: 1.2rem;
635
- font-weight: 700;
636
- white-space: nowrap;
637
- overflow: hidden;
638
- text-overflow: ellipsis;
639
  }
640
-
641
  .grid-meta {
642
- display: flex;
643
- justify-content: space-between;
644
- align-items: center;
645
- font-size: 0.9rem;
646
- }
647
-
648
- .owner-info {
649
- color: var(--text-secondary);
650
- font-weight: 500;
651
  }
652
-
653
  .likes-counter {
654
- display: flex;
655
- align-items: center;
656
- color: #e53e3e;
657
- font-weight: 600;
658
  }
659
-
660
- .likes-counter span {
661
- margin-left: 4px;
662
- }
663
-
664
  .grid-actions {
665
- padding: 10px 15px;
666
- text-align: right;
667
  background-color: rgba(255, 255, 255, 0.7);
668
  backdrop-filter: blur(5px);
669
- position: absolute;
670
- bottom: 0;
671
- left: 0;
672
- right: 0;
673
- z-index: 10;
674
- display: flex;
675
- justify-content: flex-end;
676
- }
677
-
678
  .open-link {
679
- text-decoration: none;
680
- color: #2c5282;
681
- font-weight: 600;
682
- padding: 5px 10px;
683
- border-radius: 5px;
684
- transition: all 0.2s;
685
  background-color: rgba(237, 242, 247, 0.8);
686
  }
687
-
688
- .open-link:hover {
689
- background-color: #e2e8f0;
690
- }
691
-
692
  .grid-content {
693
- position: absolute;
694
- top: 0;
695
- left: 0;
696
- width: 100%;
697
- height: 100%;
698
  padding-top: 85px; /* Header height */
699
  padding-bottom: 45px; /* Actions height */
700
  }
701
-
702
  .iframe-container {
703
- width: 100%;
704
- height: 100%;
705
- overflow: hidden;
706
- position: relative;
707
  }
708
-
709
- /* Apply 70% scaling to iframes */
710
  .grid-content iframe {
711
- transform: scale(0.7);
712
- transform-origin: top left;
713
- width: 142.857%;
714
- height: 142.857%;
715
- border: none;
716
- border-radius: 0;
717
- }
718
-
719
  .error-placeholder {
720
- position: absolute;
721
- top: 0;
722
- left: 0;
723
- width: 100%;
724
- height: 100%;
725
- display: flex;
726
- flex-direction: column;
727
- justify-content: center;
728
- align-items: center;
729
- padding: 20px;
730
- background-color: rgba(255, 255, 255, 0.9);
731
- text-align: center;
732
  }
733
-
734
  .error-emoji {
735
- font-size: 6rem;
736
- margin-bottom: 1.5rem;
737
- animation: bounce 1s infinite alternate;
738
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
739
  }
740
-
741
  @keyframes bounce {
742
- from {
743
- transform: translateY(0px) scale(1);
744
- }
745
- to {
746
- transform: translateY(-15px) scale(1.1);
747
- }
748
  }
749
-
750
- /* Pagination Styling */
751
  .pagination {
752
- display: flex;
753
- justify-content: center;
754
- align-items: center;
755
- gap: 10px;
756
- margin: 2rem 0;
757
  }
758
-
759
  .pagination-button {
760
- background-color: white;
761
- border: none;
762
- padding: 10px 20px;
763
- border-radius: 10px;
764
- font-size: 1rem;
765
- font-weight: 600;
766
- cursor: pointer;
767
- transition: all 0.2s;
768
- color: var(--text-primary);
769
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
770
  }
771
-
772
  .pagination-button:hover {
773
- background-color: #f8f9fa;
774
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
775
  }
776
-
777
  .pagination-button.active {
778
- background-color: var(--pastel-purple);
779
- color: #4a5568;
780
  }
781
-
782
  .pagination-button:disabled {
783
- background-color: #edf2f7;
784
- color: #a0aec0;
785
- cursor: default;
786
- box-shadow: none;
787
  }
788
-
789
- /* Loading Indicator */
790
  .loading {
791
- position: fixed;
792
- top: 0;
793
- left: 0;
794
- right: 0;
795
- bottom: 0;
796
- background-color: rgba(255, 255, 255, 0.8);
797
- backdrop-filter: blur(5px);
798
- display: flex;
799
- justify-content: center;
800
- align-items: center;
801
- z-index: 1000;
802
- }
803
-
804
- .loading-content {
805
- text-align: center;
806
  }
807
-
808
  .loading-spinner {
809
- width: 60px;
810
- height: 60px;
811
- border: 5px solid #e2e8f0;
812
- border-top-color: var(--pastel-purple);
813
- border-radius: 50%;
814
- animation: spin 1s linear infinite;
815
- margin: 0 auto 15px;
816
- }
817
-
818
  .loading-text {
819
- font-size: 1.2rem;
820
- font-weight: 600;
821
- color: #4a5568;
822
  }
823
-
824
  .loading-error {
825
- display: none;
826
- margin-top: 10px;
827
- color: #e53e3e;
828
- font-size: 0.9rem;
829
- }
830
-
831
- /* Stats window styling */
832
- .stats-window {
833
- margin-top: 2rem;
834
- margin-bottom: 2rem;
835
  }
836
-
837
  .stats-header {
838
- display: flex;
839
- justify-content: space-between;
840
- align-items: center;
841
- margin-bottom: 1rem;
842
  }
843
-
844
  .stats-title {
845
- font-size: 1.5rem;
846
- font-weight: 700;
847
- color: #2d3748;
848
  }
849
-
850
  .stats-toggle {
851
- background-color: var(--pastel-blue);
852
- border: none;
853
- padding: 8px 16px;
854
- border-radius: 20px;
855
- font-weight: 600;
856
- cursor: pointer;
857
- transition: all 0.2s;
858
- }
859
-
860
- .stats-toggle:hover {
861
- background-color: var(--pastel-purple);
862
- }
863
-
864
  .stats-content {
865
- background-color: white;
866
- border-radius: 10px;
867
- padding: 20px;
868
- box-shadow: var(--box-shadow);
869
- max-height: 0;
870
- overflow: hidden;
871
  transition: max-height 0.5s ease-out;
872
  }
873
-
874
- .stats-content.open {
875
- max-height: 600px;
876
- }
877
-
878
- .chart-container {
879
- width: 100%;
880
- height: 500px;
881
- }
882
-
883
- /* Responsive Design */
884
  @media (max-width: 768px) {
885
- body {
886
- padding: 1rem;
887
- }
888
-
889
- .grid-container {
890
- grid-template-columns: 1fr;
891
- }
892
-
893
- .search-bar {
894
- flex-direction: column;
895
- padding: 10px;
896
- }
897
-
898
- .search-bar input {
899
- width: 100%;
900
- margin-bottom: 10px;
901
- }
902
-
903
- .search-bar .refresh-btn {
904
- width: 100%;
905
- justify-content: center;
906
- }
907
-
908
- .pagination {
909
- flex-wrap: wrap;
910
- }
911
-
912
- .chart-container {
913
- height: 300px;
914
- }
915
- }
916
-
917
- .error-emoji-detector {
918
- position: fixed;
919
- top: -9999px;
920
- left: -9999px;
921
- z-index: -1;
922
- opacity: 0;
923
  }
924
-
925
- /* ์ถ”๊ฐ€ ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ •(์•„๋ฐ”ํƒ€, ZERO GPU ๋ฑƒ์ง€ ๋“ฑ)์„ ์œ„ํ•ด
926
- ์•„๋ž˜ ํด๋ž˜์Šค๋“ค์„ ์ผ๋ถ€ ์ถ”๊ฐ€/์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์œผ๋‚˜ ์—ฌ๊ธฐ์„œ๋Š” ์ƒ๋žต */
927
-
928
- /* ๋‹ค์Œ ๋ถ€๋ถ„์€ Zero GPU Spaces์šฉ ์นด๋“œ ๊ตฌ์กฐ์—์„œ ํ™œ์šฉ */
929
  .space-header {
930
- display: flex;
931
- align-items: center;
932
- gap: 10px;
933
- margin-bottom: 4px;
934
  }
935
  .avatar-img {
936
- width: 32px;
937
- height: 32px;
938
- border-radius: 50%;
939
- object-fit: cover;
940
- border: 1px solid #ccc;
941
  }
942
  .space-title {
943
- font-size: 1rem;
944
- font-weight: 600;
945
- margin: 0;
946
- overflow: hidden;
947
- text-overflow: ellipsis;
948
- white-space: nowrap;
949
- max-width: 200px;
950
  }
951
  .zero-gpu-badge {
952
- font-size: 0.7rem;
953
- background-color: #e6fffa;
954
- color: #319795;
955
- border: 1px solid #81e6d9;
956
- border-radius: 6px;
957
- padding: 2px 6px;
958
- font-weight: 600;
959
- margin-left: 8px;
960
  }
961
  .desc-text {
962
- font-size: 0.85rem;
963
- color: #444;
964
- margin: 4px 0;
965
- line-clamp: 2;
966
- display: -webkit-box;
967
- -webkit-box-orient: vertical;
968
- overflow: hidden;
969
- }
970
- .author-name {
971
- font-size: 0.8rem;
972
- color: #666;
973
  }
 
974
  .likes-wrapper {
975
- display: flex;
976
- align-items: center;
977
- gap: 4px;
978
- color: #e53e3e;
979
- font-weight: bold;
980
- font-size: 0.85rem;
981
- }
982
- .likes-heart {
983
- font-size: 1rem;
984
- line-height: 1rem;
985
- color: #f56565;
986
- }
987
- /* ์ด๋ชจ์ง€ ์ „์šฉ ์Šคํƒ€์ผ (์„ ํƒ์‚ฌํ•ญ) */
988
- .emoji-avatar {
989
- font-size: 1.2rem;
990
- width: 32px;
991
- height: 32px;
992
- border-radius: 50%;
993
- border: 1px solid #ccc;
994
- display: flex;
995
- align-items: center;
996
- justify-content: center;
997
  }
 
998
  </style>
999
  </head>
1000
  <body>
@@ -1008,20 +663,20 @@ if __name__ == '__main__':
1008
  </div>
1009
  <div class="mac-title">Huggingface Explorer</div>
1010
  </div>
1011
-
1012
  <div class="mac-content">
1013
  <div class="header">
1014
  <h1>ZeroGPU Spaces Leaderboard</h1>
1015
  <p>Discover Zero GPU(Shared A100) spaces from Hugging Face</p>
1016
  </div>
1017
-
1018
  <!-- Tab Navigation -->
1019
  <div class="tab-nav">
1020
  <button id="tabTrendingButton" class="tab-button active">Trending</button>
1021
  <button id="tabLatestButton" class="tab-button">Latest Releases</button>
1022
  <button id="tabFixedButton" class="tab-button">Picks</button>
1023
  </div>
1024
-
1025
  <!-- Trending(Zero GPU) Tab Content -->
1026
  <div id="trendingTab" class="tab-content active">
1027
  <div class="stats-window mac-window">
@@ -1045,7 +700,7 @@ if __name__ == '__main__':
1045
  </div>
1046
  </div>
1047
  </div>
1048
-
1049
  <div class="search-bar">
1050
  <input type="text" id="searchInputTrending" placeholder="Search by name, owner, or description..." />
1051
  <button id="refreshButtonTrending" class="refresh-btn">
@@ -1053,12 +708,11 @@ if __name__ == '__main__':
1053
  Refresh
1054
  </button>
1055
  </div>
1056
-
1057
  <div id="gridContainerTrending" class="grid-container"></div>
1058
-
1059
  <div id="paginationTrending" class="pagination"></div>
1060
  </div>
1061
-
1062
  <!-- Latest Releases Tab Content -->
1063
  <div id="latestTab" class="tab-content">
1064
  <div class="search-bar">
@@ -1068,20 +722,19 @@ if __name__ == '__main__':
1068
  Refresh
1069
  </button>
1070
  </div>
1071
-
1072
  <div id="gridContainerLatest" class="grid-container"></div>
1073
-
1074
  <div id="paginationLatest" class="pagination"></div>
1075
  </div>
1076
-
1077
- <!-- Fixed Tab Content (๊ธฐ์กด ์˜ˆ์‹œ ์œ ์ง€) -->
1078
  <div id="fixedTab" class="tab-content">
1079
  <div id="fixedGrid" class="grid-container"></div>
1080
  </div>
1081
  </div>
1082
  </div>
1083
  </div>
1084
-
1085
  <div id="loadingIndicator" class="loading">
1086
  <div class="loading-content">
1087
  <div class="loading-spinner"></div>
@@ -1091,7 +744,7 @@ if __name__ == '__main__':
1091
  </div>
1092
  </div>
1093
  </div>
1094
-
1095
  <script>
1096
  // ------------------------------------
1097
  // GLOBAL STATE & COMMON FUNCTIONS
@@ -1103,7 +756,7 @@ if __name__ == '__main__':
1103
  function setLoading(isLoading) {
1104
  globalState.isLoading = isLoading;
1105
  document.getElementById('loadingIndicator').style.display = isLoading ? 'flex' : 'none';
1106
-
1107
  const refreshButtons = document.querySelectorAll('.refresh-btn');
1108
  refreshButtons.forEach(btn => {
1109
  if (isLoading) {
@@ -1112,7 +765,7 @@ if __name__ == '__main__':
1112
  btn.classList.remove('refreshing');
1113
  }
1114
  });
1115
-
1116
  if (isLoading) {
1117
  clearTimeout(globalState.loadingTimeout);
1118
  globalState.loadingTimeout = setTimeout(() => {
@@ -1123,6 +776,8 @@ if __name__ == '__main__':
1123
  document.getElementById('loadingError').style.display = 'none';
1124
  }
1125
  }
 
 
1126
  function handleIframeError(iframe, owner, name, title) {
1127
  const container = iframe.parentNode;
1128
  const errorPlaceholder = document.createElement('div');
@@ -1150,13 +805,13 @@ if __name__ == '__main__':
1150
  }
1151
 
1152
  // ------------------------------------
1153
- // IFRAME LOADER (๊ณตํ†ต)
1154
  // ------------------------------------
1155
  const iframeLoader = {
1156
  checkQueue: {},
1157
  maxAttempts: 5,
1158
  checkInterval: 5000,
1159
-
1160
  startChecking: function(iframe, owner, name, title, spaceKey) {
1161
  this.checkQueue[spaceKey] = {
1162
  iframe: iframe,
@@ -1168,45 +823,59 @@ if __name__ == '__main__':
1168
  };
1169
  this.checkIframeStatus(spaceKey);
1170
  },
1171
-
1172
  checkIframeStatus: function(spaceKey) {
1173
  if (!this.checkQueue[spaceKey]) return;
1174
-
1175
  const item = this.checkQueue[spaceKey];
1176
  if (item.status !== 'loading') {
1177
  delete this.checkQueue[spaceKey];
1178
  return;
1179
  }
1180
  item.attempts++;
1181
-
1182
  try {
1183
  if (!item.iframe || !item.iframe.parentNode) {
1184
  delete this.checkQueue[spaceKey];
1185
  return;
1186
  }
1187
 
1188
- // Check if content loaded
1189
  try {
1190
- const hasContent = item.iframe.contentWindow &&
1191
- item.iframe.contentWindow.document &&
1192
  item.iframe.contentWindow.document.body;
1193
- if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
1194
  const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
 
 
 
 
 
 
 
 
 
1195
  if (bodyText.includes('forbidden') || bodyText.includes('404') ||
1196
  bodyText.includes('not found') || bodyText.includes('error')) {
1197
  item.status = 'error';
1198
  handleIframeError(item.iframe, item.owner, item.name, item.title);
1199
- } else {
 
 
 
 
 
1200
  item.status = 'success';
 
 
1201
  }
1202
- delete this.checkQueue[spaceKey];
1203
- return;
1204
  }
1205
  } catch(e) {
1206
- // Cross-origin issues can happen; not always an error
 
1207
  }
1208
 
1209
- // Check if iframe is visible
1210
  const rect = item.iframe.getBoundingClientRect();
1211
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
1212
  item.status = 'success';
@@ -1214,7 +883,6 @@ if __name__ == '__main__':
1214
  return;
1215
  }
1216
 
1217
- // If max attempts reached
1218
  if (item.attempts >= this.maxAttempts) {
1219
  if (item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) {
1220
  item.status = 'success';
@@ -1226,7 +894,6 @@ if __name__ == '__main__':
1226
  return;
1227
  }
1228
 
1229
- // Re-check after some delay
1230
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
1231
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
1232
 
@@ -1249,7 +916,7 @@ if __name__ == '__main__':
1249
  const trendingState = {
1250
  spaces: [],
1251
  currentPage: 0,
1252
- itemsPerPage: 72,
1253
  totalItems: 0,
1254
  topOwners: [],
1255
  iframeStatuses: {}
@@ -1265,7 +932,6 @@ if __name__ == '__main__':
1265
  };
1266
 
1267
  let chartInstance = null;
1268
-
1269
  trendingElements.statsToggle.addEventListener('click', () => {
1270
  const isOpen = trendingElements.statsContent.classList.toggle('open');
1271
  trendingElements.statsToggle.textContent = isOpen ? 'Hide Stats' : 'Show Stats';
@@ -1368,7 +1034,6 @@ if __name__ == '__main__':
1368
  renderTrendingGrid(trendingState.spaces);
1369
  renderTrendingPagination();
1370
 
1371
- // ํ†ต๊ณ„์ฐฝ ์—ด๋ ค์žˆ๋‹ค๋ฉด ์ƒˆ ๋ฐ์ดํ„ฐ๋กœ ๊ฐฑ์‹ 
1372
  if (trendingElements.statsContent.classList.contains('open') && trendingState.topOwners.length > 0) {
1373
  renderCreatorStats(trendingState.topOwners);
1374
  }
@@ -1415,7 +1080,6 @@ if __name__ == '__main__':
1415
  const gridItem = document.createElement('div');
1416
  gridItem.className = 'grid-item';
1417
 
1418
- // ์ƒ๋‹จ ํ—ค๋”
1419
  const headerDiv = document.createElement('div');
1420
  headerDiv.className = 'grid-header';
1421
 
@@ -1587,7 +1251,7 @@ if __name__ == '__main__':
1587
  const latestState = {
1588
  spaces: [],
1589
  currentPage: 0,
1590
- itemsPerPage: 72,
1591
  totalItems: 0,
1592
  iframeStatuses: {}
1593
  };
@@ -1659,13 +1323,11 @@ if __name__ == '__main__':
1659
  description, avatar_url, author_name, embedUrl
1660
  } = item;
1661
 
1662
- // rank๊ฐ€ ์—†์œผ๋ฏ€๋กœ Latest ํƒญ์—์„œ๋Š” offset+index+1 ํ˜•ํƒœ๋กœ ํ‘œ์‹œ
1663
  const computedRank = latestState.currentPage * latestState.itemsPerPage + (index + 1);
1664
 
1665
  const gridItem = document.createElement('div');
1666
  gridItem.className = 'grid-item';
1667
 
1668
- // ์ƒ๋‹จ ํ—ค๋”
1669
  const headerDiv = document.createElement('div');
1670
  headerDiv.className = 'grid-header';
1671
 
@@ -1844,7 +1506,7 @@ if __name__ == '__main__':
1844
  title: "Spaces Research Analysis",
1845
  likes_count: 0,
1846
  owner: "ginipick",
1847
- name: "3D-LLAMA",
1848
  rank: 1
1849
  },
1850
  {
@@ -1852,7 +1514,7 @@ if __name__ == '__main__':
1852
  title: "Spaces Research ",
1853
  likes_count: 0,
1854
  owner: "ginipick",
1855
- name: "3D-LLAMA",
1856
  rank: 2
1857
  },
1858
  {
@@ -1888,7 +1550,6 @@ if __name__ == '__main__':
1888
  const headerTop = document.createElement('div');
1889
  headerTop.className = 'grid-header-top';
1890
 
1891
- // ๋กœ๋ด‡ ์ด๋ชจ์ง€ + ํƒ€์ดํ‹€ ํ•จ๊ป˜ ํ‘œ์‹œ
1892
  const leftWrapper = document.createElement('div');
1893
  leftWrapper.style.display = 'flex';
1894
  leftWrapper.style.alignItems = 'center';
@@ -2019,9 +1680,6 @@ if __name__ == '__main__':
2019
  renderFixedGrid();
2020
  });
2021
 
2022
- // ------------------------------------
2023
- // EVENT LISTENERS
2024
- // ------------------------------------
2025
  trendingElements.searchInput.addEventListener('input', () => {
2026
  clearTimeout(trendingState.searchTimeout);
2027
  trendingState.searchTimeout = setTimeout(() => loadTrending(0), 300);
@@ -2052,7 +1710,6 @@ if __name__ == '__main__':
2052
  setTimeout(() => {
2053
  if (globalState.isLoading) {
2054
  setLoading(false);
2055
- // ํƒ€์ž„์•„์›ƒ ์‹œ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ
2056
  trendingElements.gridContainer.innerHTML = `
2057
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
2058
  <div style="font-size: 3rem; margin-bottom: 20px;">โฑ๏ธ</div>
@@ -2070,8 +1727,6 @@ if __name__ == '__main__':
2070
  </html>
2071
  ''')
2072
 
2073
-
2074
-
2075
- if __name__ == '__main__':
2076
  port = int(os.environ.get("PORT", 7860))
 
2077
  app.run(host='0.0.0.0', port=port)
 
7
 
8
  app = Flask(__name__)
9
 
10
+ # -------------------------------------------------------
11
+ # (1) RUNNING ์ƒํƒœ ํ•„ํ„ฐ๋ง์„ ์œ„ํ•œ ํ—ฌํผ ํ•จ์ˆ˜ ์ถ”๊ฐ€
12
+ # -------------------------------------------------------
13
+ def is_running(space):
14
+ """
15
+ Hugging Face Space์˜ runtime.stage ํ˜น์€ deploymentStatus.stage ๊ฐ€
16
+ 'RUNNING' ์ธ์ง€ ํŒ๋ณ„ํ•œ๋‹ค.
17
+ """
18
+ stage = (space.get("runtime", {}) or {}).get("stage", "")
19
+ if not stage:
20
+ stage = (space.get("deploymentStatus", {}) or {}).get("stage", "")
21
+ return stage.upper() == "RUNNING"
22
+
23
  # Generate dummy spaces in case of error
24
  def generate_dummy_spaces(count):
25
  """
 
42
  })
43
  return spaces
44
 
45
+ # -------------------------------------------------------
46
+ # (2) Fetch Trending Spaces (RUNNING ์ƒํƒœ๋งŒ)
47
+ # -------------------------------------------------------
48
+ def fetch_trending_spaces(offset=0, limit=24):
49
  """
50
+ Trending์šฉ CPU ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ (์ •๋ ฌ์€ Hugging Face ๊ธฐ๋ณธ ์ •๋ ฌ).
51
+ 'runtime.stage'๊ฐ€ RUNNING ์ธ ์ŠคํŽ˜์ด์Šค๋งŒ ๋ฐ˜ํ™˜.
52
  """
53
  try:
54
  url = "https://huggingface.co/api/spaces"
55
  params = {
56
  "limit": 10000, # ๋” ๋งŽ์ด ๊ฐ€์ ธ์˜ค๊ธฐ
57
+ "hardware": "cpu"
58
  }
59
  response = requests.get(url, params=params, timeout=30)
60
 
61
  if response.status_code == 200:
62
  spaces = response.json()
63
 
64
+ # owner/id๊ฐ€ None์ด๊ฑฐ๋‚˜, BUILDING/STOPPED ์ƒํƒœ์ธ ๊ฒฝ์šฐ ์ œ์™ธ
65
  filtered_spaces = [
66
  space for space in spaces
67
  if space.get('owner') != 'None'
68
  and space.get('id', '').split('/', 1)[0] != 'None'
69
+ and is_running(space) # RUNNING ์ƒํƒœ๋งŒ!
70
  ]
71
 
72
  # ์ „์ฒด ๋ชฉ๋ก์— ๋Œ€ํ•ด "๊ธ€๋กœ๋ฒŒ ๋žญํฌ"๋ฅผ ๋งค๊ธด๋‹ค (1๋ถ€ํ„ฐ ์‹œ์ž‘)
 
77
  start = min(offset, len(filtered_spaces))
78
  end = min(offset + limit, len(filtered_spaces))
79
 
80
+ print(f"[fetch_trending_spaces] CPU๊ธฐ๋ฐ˜(RUNNING) ์ŠคํŽ˜์ด์Šค ์ด {len(filtered_spaces)}๊ฐœ, "
81
  f"์š”์ฒญ ๊ตฌ๊ฐ„ {start}~{end-1} ๋ฐ˜ํ™˜")
82
 
83
  return {
 
109
  'all_spaces': generate_dummy_spaces(500)
110
  }
111
 
112
+ # -------------------------------------------------------
113
+ # (3) Fetch Latest Spaces (RUNNING ์ƒํƒœ๋งŒ)
114
+ # -------------------------------------------------------
115
+ def fetch_latest_spaces(offset=0, limit=24):
116
  """
117
+ 'createdAt' ๊ธฐ์ค€ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ตœ๊ทผ ์ŠคํŽ˜์ด์Šค๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ ,
118
+ ๊ทธ ์ค‘ RUNNING ์ƒํƒœ์ธ ๊ฒƒ๋งŒ ํ•„ํ„ฐ๋ง ํ›„ ์ƒ์œ„ 500๊ฐœ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํŽ˜์ด์ง•.
119
  """
120
  try:
121
  url = "https://huggingface.co/api/spaces"
122
  params = {
123
+ "limit": 10000,
124
  "hardware": "cpu"
125
  }
126
  response = requests.get(url, params=params, timeout=30)
127
 
128
  if response.status_code == 200:
129
  spaces = response.json()
130
+
131
+ # owner/id๊ฐ€ None์ด๊ฑฐ๋‚˜ RUNNING์ด ์•„๋‹Œ ๊ฒฝ์šฐ ์ œ์™ธ
132
  filtered_spaces = [
133
  space for space in spaces
134
  if space.get('owner') != 'None'
135
  and space.get('id', '').split('/', 1)[0] != 'None'
136
+ and is_running(space)
137
  ]
138
+
139
+ # createdAt ๋‚ด๋ฆผ์ฐจ์ˆœ
 
 
140
  def parse_time(sp):
141
  return sp.get('createdAt') or ''
 
 
142
  filtered_spaces.sort(key=parse_time, reverse=True)
143
+
144
  # ์ƒ์œ„ 500๊ฐœ๋งŒ ์ถ”๋ฆฌ๊ธฐ
145
  truncated = filtered_spaces[:500]
146
+
147
  # ํ•„์š”ํ•œ ๊ตฌ๊ฐ„ ์Šฌ๋ผ์ด์‹ฑ
148
  start = min(offset, len(truncated))
149
  end = min(offset + limit, len(truncated))
150
+
151
+ print(f"[fetch_latest_spaces] RUNNING ์ŠคํŽ˜์ด์Šค ํ•„ํ„ฐ ํ›„ {len(filtered_spaces)}๊ฐœ, "
152
+ f"์ƒ์œ„ 500๊ฐœ ์ค‘ {start}~{end-1} ๋ฐ˜ํ™˜")
153
 
154
  return {
155
  'spaces': truncated[start:end],
156
+ 'total': len(truncated),
157
  'offset': offset,
158
  'limit': limit
159
  }
 
256
  # Get owner statistics from all spaces (for the "Trending" tab's top owners)
257
  def get_owner_stats(all_spaces):
258
  """
259
+ ์ƒ์œ„ 500์œ„(global_rank <= 500) ์ด๋‚ด์˜ RUNNING ์ŠคํŽ˜์ด์Šค๋“ค ์ค‘,
260
+ ๊ฐ owner๊ฐ€ ๋ช‡ ๋ฒˆ ๋“ฑ์žฅํ•˜๋Š”์ง€ ์„ธ๊ณ , ๊ทธ ์ค‘ ์ƒ์œ„ 30๋ช…๋งŒ ๋ฐ˜ํ™˜
261
  """
 
262
  top_500 = [s for s in all_spaces if s.get('global_rank', 999999) <= 500]
 
263
  owners = []
264
  for space in top_500:
265
  if '/' in space.get('id', ''):
 
269
  if owner and owner != 'None':
270
  owners.append(owner)
271
 
 
272
  owner_counts = Counter(owners)
 
 
273
  top_owners = owner_counts.most_common(30)
274
  return top_owners
275
 
 
285
  @app.route('/api/trending-spaces', methods=['GET'])
286
  def trending_spaces():
287
  """
288
+ hardware=cpu ์ŠคํŽ˜์ด์Šค ์ค‘ RUNNING ์ƒํƒœ๋งŒ ๊ฒ€์ƒ‰ + ํŽ˜์ด์ง• + ํ†ต๊ณ„
289
+ (๊ธฐ์กด 'Trending')
290
  """
291
  search_query = request.args.get('search', '').lower()
292
  offset = int(request.args.get('offset', 0))
293
+ limit = int(request.args.get('limit', 24)) # 24๊ฐœ์”ฉ ๋กœ๋“œ๋กœ ๋ณ€๊ฒฝ
294
 
 
295
  spaces_data = fetch_trending_spaces(offset, limit)
296
 
 
297
  results = []
298
  for index, space_data in enumerate(spaces_data['spaces']):
299
  space_info = get_space_details(space_data, index, offset)
 
310
 
311
  results.append(space_info)
312
 
 
313
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
314
 
315
  return jsonify({
 
324
  @app.route('/api/latest-spaces', methods=['GET'])
325
  def latest_spaces():
326
  """
327
+ hardware=cpu ์ŠคํŽ˜์ด์Šค ์ค‘ RUNNING ์ƒํƒœ๋งŒ, createdAt ๊ธฐ์ค€ ์ตœ์‹ ์ˆœ 500๊ฐœ ๋‚ด์—์„œ ํŽ˜์ด์ง•
328
  """
329
  search_query = request.args.get('search', '').lower()
330
  offset = int(request.args.get('offset', 0))
331
+ limit = int(request.args.get('limit', 24)) # 24๊ฐœ์”ฉ ๋กœ๋“œ๋กœ ๋ณ€๊ฒฝ
332
 
333
  spaces_data = fetch_latest_spaces(offset, limit)
334
 
 
364
 
365
  # index.html ์ „์ฒด๋ฅผ ์ƒˆ๋กœ ์ž‘์„ฑ
366
  with open('templates/index.html', 'w', encoding='utf-8') as f:
367
+ # ์•„๋ž˜ HTML/JS์—๋Š” iframe ๋‚ด๋ถ€ "building" ํ…์ŠคํŠธ๋ฅผ ํƒ์ง€ํ•˜๋„๋ก ์ˆ˜์ •๋œ ๋ถ€๋ถ„์ด ํฌํ•จ๋จ
368
  f.write('''<!DOCTYPE html>
369
  <html lang="en">
370
  <head>
 
372
  <title>Huggingface Zero-GPU Spaces</title>
373
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
374
  <style>
375
+ /* (์ƒ๋žต) ๊ธฐ์กด CSS ๋™์ผํ•˜๋˜ itemsPerPage๋งŒ ์ˆ˜์ • */
376
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
 
377
  :root {
378
  --pastel-pink: #FFD6E0;
379
  --pastel-blue: #C5E8FF;
 
381
  --pastel-yellow: #FFF2CC;
382
  --pastel-green: #C7F5D9;
383
  --pastel-orange: #FFE0C3;
384
+
385
  --mac-window-bg: rgba(250, 250, 250, 0.85);
386
  --mac-toolbar: #F5F5F7;
387
  --mac-border: #E2E2E2;
388
  --mac-button-red: #FF5F56;
389
  --mac-button-yellow: #FFBD2E;
390
  --mac-button-green: #27C93F;
391
+
392
  --text-primary: #333;
393
  --text-secondary: #666;
394
  --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
395
  }
 
396
  * {
397
+ margin: 0; padding: 0; box-sizing: border-box;
 
 
398
  }
 
399
  body {
400
+ font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
401
+ Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
402
  line-height: 1.6;
403
  color: var(--text-primary);
404
  background-color: #f8f9fa;
 
406
  min-height: 100vh;
407
  padding: 2rem;
408
  }
 
409
  .container {
410
  max-width: 1600px;
411
  margin: 0 auto;
412
  }
 
 
413
  .mac-window {
414
  background-color: var(--mac-window-bg);
415
  border-radius: 10px;
 
419
  margin-bottom: 2rem;
420
  border: 1px solid var(--mac-border);
421
  }
 
422
  .mac-toolbar {
423
+ display: flex; align-items: center;
 
424
  padding: 10px 15px;
425
  background-color: var(--mac-toolbar);
426
  border-bottom: 1px solid var(--mac-border);
427
  }
 
428
  .mac-buttons {
429
+ display: flex; gap: 8px; margin-right: 15px;
 
 
430
  }
 
431
  .mac-button {
432
+ width: 12px; height: 12px; border-radius: 50%; cursor: default;
 
 
 
433
  }
434
+ .mac-close { background-color: var(--mac-button-red); }
435
+ .mac-minimize { background-color: var(--mac-button-yellow); }
436
+ .mac-maximize { background-color: var(--mac-button-green); }
 
 
 
 
 
 
 
 
 
 
437
  .mac-title {
438
+ flex-grow: 1; text-align: center; font-size: 0.9rem; color: var(--text-secondary);
 
 
 
439
  }
 
440
  .mac-content {
441
  padding: 20px;
442
  }
443
+ .header { text-align: center; margin-bottom: 1.5rem; position: relative; }
 
 
 
 
 
 
 
444
  .header h1 {
445
+ font-size: 2.2rem; font-weight: 700; margin: 0; color: #2d3748; letter-spacing: -0.5px;
446
+ }
447
+ .header p { color: var(--text-secondary); margin-top: 0.5rem; font-size: 1.1rem; }
448
+ .tab-nav { display: flex; justify-content: center; margin-bottom: 1.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  .tab-button {
450
+ border: none; background-color: #edf2f7; color: var(--text-primary);
451
+ padding: 10px 20px; margin: 0 5px; cursor: pointer; border-radius: 5px;
452
+ font-size: 1rem; font-weight: 600;
453
+ }
454
+ .tab-button.active { background-color: var(--pastel-purple); color: #fff; }
455
+ .tab-content { display: none; }
456
+ .tab-content.active { display: block; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  .search-bar {
458
+ display: flex; align-items: center; margin-bottom: 1.5rem; background-color: white;
459
+ border-radius: 30px; padding: 5px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
460
+ max-width: 600px; margin-left: auto; margin-right: auto;
461
+ }
 
 
 
 
 
 
 
 
462
  .search-bar input {
463
+ flex-grow: 1; border: none; padding: 12px 20px; font-size: 1rem; outline: none;
464
+ background: transparent; border-radius: 30px;
465
+ }
 
 
 
 
 
 
466
  .search-bar .refresh-btn {
467
+ background-color: var(--pastel-green); color: #1a202c; border: none; border-radius: 30px;
468
+ padding: 10px 20px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s;
469
+ display: flex; align-items: center; gap: 8px;
470
+ }
 
 
 
 
 
 
 
 
 
 
471
  .search-bar .refresh-btn:hover {
472
+ background-color: #9ee7c0; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
 
473
  }
 
474
  .refresh-icon {
475
+ display: inline-block; width: 16px; height: 16px;
476
+ border: 2px solid #1a202c; border-top-color: transparent; border-radius: 50%;
 
 
 
 
477
  animation: none;
478
  }
479
+ .refreshing .refresh-icon { animation: spin 1s linear infinite; }
 
 
 
 
480
  @keyframes spin {
481
  0% { transform: rotate(0deg); }
482
  100% { transform: rotate(360deg); }
483
  }
 
 
484
  .grid-container {
485
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
486
+ gap: 1.5rem; margin-bottom: 2rem;
 
 
487
  }
 
488
  .grid-item {
489
+ height: 500px; position: relative; overflow: hidden; transition: all 0.3s ease; border-radius: 15px;
 
 
 
 
490
  }
 
491
  .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
492
  .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
493
  .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
494
  .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
495
  .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
496
  .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
 
497
  .grid-item:hover {
498
+ transform: translateY(-5px); box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
 
499
  }
 
500
  .grid-header {
501
+ padding: 15px; display: flex; flex-direction: column;
502
+ background-color: rgba(255, 255, 255, 0.7); backdrop-filter: blur(5px);
 
 
 
503
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
504
  }
 
505
  .grid-header-top {
506
+ display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;
 
 
 
507
  }
 
508
  .rank-badge {
509
+ background-color: #1a202c; color: white; font-size: 0.8rem; font-weight: 600;
510
+ padding: 4px 8px; border-radius: 50px; display: inline-block;
511
+ }
 
 
 
 
 
 
512
  .grid-header h3 {
513
+ margin: 0; font-size: 1.2rem; font-weight: 700;
514
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
 
 
 
 
515
  }
 
516
  .grid-meta {
517
+ display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem;
 
 
 
 
 
 
 
 
518
  }
519
+ .owner-info { color: var(--text-secondary); font-weight: 500; }
520
  .likes-counter {
521
+ display: flex; align-items: center; color: #e53e3e; font-weight: 600;
 
 
 
522
  }
523
+ .likes-counter span { margin-left: 4px; }
 
 
 
 
524
  .grid-actions {
525
+ padding: 10px 15px; text-align: right;
 
526
  background-color: rgba(255, 255, 255, 0.7);
527
  backdrop-filter: blur(5px);
528
+ position: absolute; bottom: 0; left: 0; right: 0; z-index: 10;
529
+ display: flex; justify-content: flex-end;
530
+ }
 
 
 
 
 
 
531
  .open-link {
532
+ text-decoration: none; color: #2c5282; font-weight: 600;
533
+ padding: 5px 10px; border-radius: 5px; transition: all 0.2s;
 
 
 
 
534
  background-color: rgba(237, 242, 247, 0.8);
535
  }
536
+ .open-link:hover { background-color: #e2e8f0; }
 
 
 
 
537
  .grid-content {
538
+ position: absolute; top: 0; left: 0; width: 100%; height: 100%;
 
 
 
 
539
  padding-top: 85px; /* Header height */
540
  padding-bottom: 45px; /* Actions height */
541
  }
 
542
  .iframe-container {
543
+ width: 100%; height: 100%; overflow: hidden; position: relative;
 
 
 
544
  }
 
 
545
  .grid-content iframe {
546
+ transform: scale(0.7); transform-origin: top left;
547
+ width: 142.857%; height: 142.857%; border: none; border-radius: 0;
548
+ }
 
 
 
 
 
549
  .error-placeholder {
550
+ position: absolute; top: 0; left: 0; width: 100%; height: 100%;
551
+ display: flex; flex-direction: column; justify-content: center; align-items: center;
552
+ padding: 20px; background-color: rgba(255, 255, 255, 0.9); text-align: center;
 
 
 
 
 
 
 
 
 
553
  }
 
554
  .error-emoji {
555
+ font-size: 6rem; margin-bottom: 1.5rem; animation: bounce 1s infinite alternate;
 
 
556
  text-shadow: 0 10px 20px rgba(0,0,0,0.1);
557
  }
 
558
  @keyframes bounce {
559
+ from { transform: translateY(0px) scale(1); }
560
+ to { transform: translateY(-15px) scale(1.1); }
 
 
 
 
561
  }
 
 
562
  .pagination {
563
+ display: flex; justify-content: center; align-items: center; gap: 10px; margin: 2rem 0;
 
 
 
 
564
  }
 
565
  .pagination-button {
566
+ background-color: white; border: none; padding: 10px 20px; border-radius: 10px;
567
+ font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s;
568
+ color: var(--text-primary); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
 
 
 
 
 
 
 
569
  }
 
570
  .pagination-button:hover {
571
+ background-color: #f8f9fa; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
 
572
  }
 
573
  .pagination-button.active {
574
+ background-color: var(--pastel-purple); color: #4a5568;
 
575
  }
 
576
  .pagination-button:disabled {
577
+ background-color: #edf2f7; color: #a0aec0; cursor: default; box-shadow: none;
 
 
 
578
  }
 
 
579
  .loading {
580
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
581
+ background-color: rgba(255, 255, 255, 0.8); backdrop-filter: blur(5px);
582
+ display: flex; justify-content: center; align-items: center; z-index: 1000;
 
 
 
 
 
 
 
 
 
 
 
 
583
  }
584
+ .loading-content { text-align: center; }
585
  .loading-spinner {
586
+ width: 60px; height: 60px;
587
+ border: 5px solid #e2e8f0; border-top-color: var(--pastel-purple);
588
+ border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 15px;
589
+ }
 
 
 
 
 
590
  .loading-text {
591
+ font-size: 1.2rem; font-weight: 600; color: #4a5568;
 
 
592
  }
 
593
  .loading-error {
594
+ display: none; margin-top: 10px; color: #e53e3e; font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
595
  }
596
+ .stats-window { margin-top: 2rem; margin-bottom: 2rem; }
597
  .stats-header {
598
+ display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;
 
 
 
599
  }
 
600
  .stats-title {
601
+ font-size: 1.5rem; font-weight: 700; color: #2d3748;
 
 
602
  }
 
603
  .stats-toggle {
604
+ background-color: var(--pastel-blue); border: none; padding: 8px 16px;
605
+ border-radius: 20px; font-weight: 600; cursor: pointer; transition: all 0.2s;
606
+ }
607
+ .stats-toggle:hover { background-color: var(--pastel-purple); }
 
 
 
 
 
 
 
 
 
608
  .stats-content {
609
+ background-color: white; border-radius: 10px; padding: 20px;
610
+ box-shadow: var(--box-shadow); max-height: 0; overflow: hidden;
 
 
 
 
611
  transition: max-height 0.5s ease-out;
612
  }
613
+ .stats-content.open { max-height: 600px; }
614
+ .chart-container { width: 100%; height: 500px; }
 
 
 
 
 
 
 
 
 
615
  @media (max-width: 768px) {
616
+ body { padding: 1rem; }
617
+ .grid-container { grid-template-columns: 1fr; }
618
+ .search-bar { flex-direction: column; padding: 10px; }
619
+ .search-bar input { width: 100%; margin-bottom: 10px; }
620
+ .search-bar .refresh-btn { width: 100%; justify-content: center; }
621
+ .pagination { flex-wrap: wrap; }
622
+ .chart-container { height: 300px; }
623
+ }
624
+ .error-emoji-detector {
625
+ position: fixed; top: -9999px; left: -9999px;
626
+ z-index: -1; opacity: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
  }
 
 
 
 
 
628
  .space-header {
629
+ display: flex; align-items: center; gap: 10px; margin-bottom: 4px;
 
 
 
630
  }
631
  .avatar-img {
632
+ width: 32px; height: 32px; border-radius: 50%;
633
+ object-fit: cover; border: 1px solid #ccc;
 
 
 
634
  }
635
  .space-title {
636
+ font-size: 1rem; font-weight: 600; margin: 0; overflow: hidden; text-overflow: ellipsis;
637
+ white-space: nowrap; max-width: 200px;
 
 
 
 
 
638
  }
639
  .zero-gpu-badge {
640
+ font-size: 0.7rem; background-color: #e6fffa; color: #319795; border: 1px solid #81e6d9;
641
+ border-radius: 6px; padding: 2px 6px; font-weight: 600; margin-left: 8px;
 
 
 
 
 
 
642
  }
643
  .desc-text {
644
+ font-size: 0.85rem; color: #444; margin: 4px 0;
645
+ line-clamp: 2; display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden;
 
 
 
 
 
 
 
 
 
646
  }
647
+ .author-name { font-size: 0.8rem; color: #666; }
648
  .likes-wrapper {
649
+ display: flex; align-items: center; gap: 4px;
650
+ color: #e53e3e; font-weight: bold; font-size: 0.85rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  }
652
+ .likes-heart { font-size: 1rem; line-height: 1rem; color: #f56565; }
653
  </style>
654
  </head>
655
  <body>
 
663
  </div>
664
  <div class="mac-title">Huggingface Explorer</div>
665
  </div>
666
+
667
  <div class="mac-content">
668
  <div class="header">
669
  <h1>ZeroGPU Spaces Leaderboard</h1>
670
  <p>Discover Zero GPU(Shared A100) spaces from Hugging Face</p>
671
  </div>
672
+
673
  <!-- Tab Navigation -->
674
  <div class="tab-nav">
675
  <button id="tabTrendingButton" class="tab-button active">Trending</button>
676
  <button id="tabLatestButton" class="tab-button">Latest Releases</button>
677
  <button id="tabFixedButton" class="tab-button">Picks</button>
678
  </div>
679
+
680
  <!-- Trending(Zero GPU) Tab Content -->
681
  <div id="trendingTab" class="tab-content active">
682
  <div class="stats-window mac-window">
 
700
  </div>
701
  </div>
702
  </div>
703
+
704
  <div class="search-bar">
705
  <input type="text" id="searchInputTrending" placeholder="Search by name, owner, or description..." />
706
  <button id="refreshButtonTrending" class="refresh-btn">
 
708
  Refresh
709
  </button>
710
  </div>
711
+
712
  <div id="gridContainerTrending" class="grid-container"></div>
 
713
  <div id="paginationTrending" class="pagination"></div>
714
  </div>
715
+
716
  <!-- Latest Releases Tab Content -->
717
  <div id="latestTab" class="tab-content">
718
  <div class="search-bar">
 
722
  Refresh
723
  </button>
724
  </div>
725
+
726
  <div id="gridContainerLatest" class="grid-container"></div>
 
727
  <div id="paginationLatest" class="pagination"></div>
728
  </div>
729
+
730
+ <!-- Fixed Tab Content -->
731
  <div id="fixedTab" class="tab-content">
732
  <div id="fixedGrid" class="grid-container"></div>
733
  </div>
734
  </div>
735
  </div>
736
  </div>
737
+
738
  <div id="loadingIndicator" class="loading">
739
  <div class="loading-content">
740
  <div class="loading-spinner"></div>
 
744
  </div>
745
  </div>
746
  </div>
747
+
748
  <script>
749
  // ------------------------------------
750
  // GLOBAL STATE & COMMON FUNCTIONS
 
756
  function setLoading(isLoading) {
757
  globalState.isLoading = isLoading;
758
  document.getElementById('loadingIndicator').style.display = isLoading ? 'flex' : 'none';
759
+
760
  const refreshButtons = document.querySelectorAll('.refresh-btn');
761
  refreshButtons.forEach(btn => {
762
  if (isLoading) {
 
765
  btn.classList.remove('refreshing');
766
  }
767
  });
768
+
769
  if (isLoading) {
770
  clearTimeout(globalState.loadingTimeout);
771
  globalState.loadingTimeout = setTimeout(() => {
 
776
  document.getElementById('loadingError').style.display = 'none';
777
  }
778
  }
779
+
780
+ // iframe ์—๋Ÿฌ ํ‘œ์‹œ
781
  function handleIframeError(iframe, owner, name, title) {
782
  const container = iframe.parentNode;
783
  const errorPlaceholder = document.createElement('div');
 
805
  }
806
 
807
  // ------------------------------------
808
+ // IFRAME LOADER
809
  // ------------------------------------
810
  const iframeLoader = {
811
  checkQueue: {},
812
  maxAttempts: 5,
813
  checkInterval: 5000,
814
+
815
  startChecking: function(iframe, owner, name, title, spaceKey) {
816
  this.checkQueue[spaceKey] = {
817
  iframe: iframe,
 
823
  };
824
  this.checkIframeStatus(spaceKey);
825
  },
826
+
827
  checkIframeStatus: function(spaceKey) {
828
  if (!this.checkQueue[spaceKey]) return;
829
+
830
  const item = this.checkQueue[spaceKey];
831
  if (item.status !== 'loading') {
832
  delete this.checkQueue[spaceKey];
833
  return;
834
  }
835
  item.attempts++;
836
+
837
  try {
838
  if (!item.iframe || !item.iframe.parentNode) {
839
  delete this.checkQueue[spaceKey];
840
  return;
841
  }
842
 
 
843
  try {
844
+ const hasContent = item.iframe.contentWindow &&
845
+ item.iframe.contentWindow.document &&
846
  item.iframe.contentWindow.document.body;
847
+ if (hasContent) {
848
  const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
849
+
850
+ // (4) "building" ํ…์ŠคํŠธ๊ฐ€ ํฌํ•จ๋˜๋ฉด ์ฆ‰์‹œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ
851
+ if (bodyText.includes('building')) {
852
+ item.status = 'error';
853
+ handleIframeError(item.iframe, item.owner, item.name, item.title);
854
+ delete this.checkQueue[spaceKey];
855
+ return;
856
+ }
857
+
858
  if (bodyText.includes('forbidden') || bodyText.includes('404') ||
859
  bodyText.includes('not found') || bodyText.includes('error')) {
860
  item.status = 'error';
861
  handleIframeError(item.iframe, item.owner, item.name, item.title);
862
+ delete this.checkQueue[spaceKey];
863
+ return;
864
+ }
865
+
866
+ // ๋‚ด์šฉ์ด ์–ด๋А ์ •๋„ ์žˆ์œผ๋ฉด ์„ฑ๊ณต ์ฒ˜๋ฆฌ
867
+ if (item.iframe.contentWindow.document.body.innerHTML.length > 100) {
868
  item.status = 'success';
869
+ delete this.checkQueue[spaceKey];
870
+ return;
871
  }
 
 
872
  }
873
  } catch(e) {
874
+ // Cross-origin ๋“ฑ์˜ ์ด์œ ๋กœ ์ ‘๊ทผ์ด ๋ง‰ํžŒ ๊ฒฝ์šฐ
875
+ // => ์—ฌ๋Ÿฌ ๋ฒˆ ์žฌ์‹œ๋„
876
  }
877
 
878
+ // ์•„์ดํ”„๋ ˆ์ž„์ด ๋กœ๋”ฉ๋œ ๋“ฏ ๋ณด์ด๋ฉด
879
  const rect = item.iframe.getBoundingClientRect();
880
  if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
881
  item.status = 'success';
 
883
  return;
884
  }
885
 
 
886
  if (item.attempts >= this.maxAttempts) {
887
  if (item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) {
888
  item.status = 'success';
 
894
  return;
895
  }
896
 
 
897
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
898
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
899
 
 
916
  const trendingState = {
917
  spaces: [],
918
  currentPage: 0,
919
+ itemsPerPage: 24, // (3) ํ•œ ํŽ˜์ด์ง€ 24๊ฐœ๋กœ ์ถ•์†Œ
920
  totalItems: 0,
921
  topOwners: [],
922
  iframeStatuses: {}
 
932
  };
933
 
934
  let chartInstance = null;
 
935
  trendingElements.statsToggle.addEventListener('click', () => {
936
  const isOpen = trendingElements.statsContent.classList.toggle('open');
937
  trendingElements.statsToggle.textContent = isOpen ? 'Hide Stats' : 'Show Stats';
 
1034
  renderTrendingGrid(trendingState.spaces);
1035
  renderTrendingPagination();
1036
 
 
1037
  if (trendingElements.statsContent.classList.contains('open') && trendingState.topOwners.length > 0) {
1038
  renderCreatorStats(trendingState.topOwners);
1039
  }
 
1080
  const gridItem = document.createElement('div');
1081
  gridItem.className = 'grid-item';
1082
 
 
1083
  const headerDiv = document.createElement('div');
1084
  headerDiv.className = 'grid-header';
1085
 
 
1251
  const latestState = {
1252
  spaces: [],
1253
  currentPage: 0,
1254
+ itemsPerPage: 24, // (3) ํ•œ ํŽ˜์ด์ง€ 24๊ฐœ๋กœ ์ถ•์†Œ
1255
  totalItems: 0,
1256
  iframeStatuses: {}
1257
  };
 
1323
  description, avatar_url, author_name, embedUrl
1324
  } = item;
1325
 
 
1326
  const computedRank = latestState.currentPage * latestState.itemsPerPage + (index + 1);
1327
 
1328
  const gridItem = document.createElement('div');
1329
  gridItem.className = 'grid-item';
1330
 
 
1331
  const headerDiv = document.createElement('div');
1332
  headerDiv.className = 'grid-header';
1333
 
 
1506
  title: "Spaces Research Analysis",
1507
  likes_count: 0,
1508
  owner: "ginipick",
1509
+ name: "3D-LLAMA",
1510
  rank: 1
1511
  },
1512
  {
 
1514
  title: "Spaces Research ",
1515
  likes_count: 0,
1516
  owner: "ginipick",
1517
+ name: "3D-LLAMA",
1518
  rank: 2
1519
  },
1520
  {
 
1550
  const headerTop = document.createElement('div');
1551
  headerTop.className = 'grid-header-top';
1552
 
 
1553
  const leftWrapper = document.createElement('div');
1554
  leftWrapper.style.display = 'flex';
1555
  leftWrapper.style.alignItems = 'center';
 
1680
  renderFixedGrid();
1681
  });
1682
 
 
 
 
1683
  trendingElements.searchInput.addEventListener('input', () => {
1684
  clearTimeout(trendingState.searchTimeout);
1685
  trendingState.searchTimeout = setTimeout(() => loadTrending(0), 300);
 
1710
  setTimeout(() => {
1711
  if (globalState.isLoading) {
1712
  setLoading(false);
 
1713
  trendingElements.gridContainer.innerHTML = `
1714
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1715
  <div style="font-size: 3rem; margin-bottom: 20px;">โฑ๏ธ</div>
 
1727
  </html>
1728
  ''')
1729
 
 
 
 
1730
  port = int(os.environ.get("PORT", 7860))
1731
+ print(f"===== Application Startup at {time.strftime('%Y-%m-%d %H:%M:%S')} =====")
1732
  app.run(host='0.0.0.0', port=port)