fantaxy commited on
Commit
c330410
·
verified ·
1 Parent(s): 214ced4

Update app.py

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