fantaxy commited on
Commit
eabd91e
ยท
verified ยท
1 Parent(s): c330410

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -184
app.py CHANGED
@@ -1,40 +1,37 @@
1
  from flask import Flask, render_template, request, jsonify
2
  import requests
3
  import os
4
- import time
5
- import random
6
  from collections import Counter
7
 
8
  app = Flask(__name__)
9
 
10
- # Function to fetch trending 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),
@@ -44,7 +41,7 @@ def fetch_trending_models(offset=0, limit=72):
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,
@@ -53,31 +50,32 @@ def fetch_trending_models(offset=0, limit=72):
53
  'all_models': generate_dummy_models(500)
54
  }
55
 
56
- # Generate dummy models in case of error
57
  def generate_dummy_models(count):
58
- models = []
59
  for i in range(count):
60
- models.append({
61
  'id': f'dummy/model-{i}',
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', '')
@@ -88,9 +86,12 @@ def get_model_details(model_data, index, offset):
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,
@@ -99,11 +100,12 @@ def get_model_details(model_data, index, offset):
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',
@@ -111,74 +113,71 @@ def get_model_details(model_data, index, offset):
111
  'owner': 'huggingface',
112
  'name': 'error',
113
  'likes_count': 0,
 
114
  'tags': [],
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:
122
- if '/' in model.get('id', ''):
123
- owner, _ = model.get('id', '').split('/', 1)
124
  else:
125
- owner = model.get('owner', '')
126
-
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({
171
  'models': results,
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">
@@ -188,7 +187,7 @@ if __name__ == '__main__':
188
  <title>Huggingface Models Gallery</title>
189
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
190
  <style>
191
- @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
192
 
193
  :root {
194
  --pastel-pink: #FFD6E0;
@@ -213,7 +212,6 @@ if __name__ == '__main__':
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);
@@ -221,12 +219,10 @@ if __name__ == '__main__':
221
  background: linear-gradient(135deg, var(--pastel-blue) 0%, var(--pastel-purple) 100%);
222
  padding: 2rem;
223
  }
224
-
225
  .container {
226
  max-width: 1600px;
227
  margin: 0 auto;
228
  }
229
-
230
  .mac-window {
231
  background-color: var(--mac-window-bg);
232
  border-radius: 10px;
@@ -237,10 +233,8 @@ if __name__ == '__main__':
237
  border: 1px solid var(--mac-border);
238
  }
239
  .mac-toolbar {
240
- display: flex;
241
- align-items: center;
242
- padding: 10px 15px;
243
- background-color: var(--mac-toolbar);
244
  border-bottom: 1px solid var(--mac-border);
245
  }
246
  .mac-buttons {
@@ -257,9 +251,7 @@ if __name__ == '__main__':
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 {
@@ -270,19 +262,18 @@ if __name__ == '__main__':
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);
@@ -295,7 +286,8 @@ if __name__ == '__main__':
295
  animation: spin 1s linear infinite;
296
  }
297
  @keyframes spin {
298
- 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); }
 
299
  }
300
 
301
  .grid-container {
@@ -329,19 +321,24 @@ if __name__ == '__main__':
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);
@@ -353,33 +350,27 @@ if __name__ == '__main__':
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
  }
@@ -387,11 +378,10 @@ if __name__ == '__main__':
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;
@@ -408,10 +398,10 @@ if __name__ == '__main__':
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 {
@@ -426,10 +416,8 @@ if __name__ == '__main__':
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
  }
@@ -460,6 +448,9 @@ if __name__ == '__main__':
460
  .chart-container { height: 300px; }
461
  }
462
 
 
 
 
463
  </style>
464
  </head>
465
  <body>
@@ -473,14 +464,14 @@ if __name__ == '__main__':
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>
481
  </div>
482
 
483
- <!-- Stats Section -->
484
  <div class="stats-window mac-window">
485
  <div class="mac-toolbar">
486
  <div class="mac-buttons">
@@ -512,11 +503,11 @@ if __name__ == '__main__':
512
 
513
  <div id="gridContainer" class="grid-container"></div>
514
  <div id="pagination" class="pagination"></div>
515
-
516
  </div>
517
  </div>
518
  </div>
519
 
 
520
  <div id="loadingIndicator" class="loading">
521
  <div class="loading-content">
522
  <div class="loading-spinner"></div>
@@ -553,14 +544,16 @@ if __name__ == '__main__':
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
  },
@@ -578,20 +571,24 @@ if __name__ == '__main__':
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 {
@@ -601,10 +598,10 @@ if __name__ == '__main__':
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);
@@ -614,7 +611,7 @@ if __name__ == '__main__':
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);
@@ -626,15 +623,15 @@ if __name__ == '__main__':
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 {
@@ -643,14 +640,11 @@ if __name__ == '__main__':
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
 
@@ -659,20 +653,21 @@ if __name__ == '__main__':
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);
@@ -717,8 +712,8 @@ if __name__ == '__main__':
717
  legend: { display: false },
718
  tooltip: {
719
  callbacks: {
720
- title: (tooltipItems) => tooltipItems[0].label,
721
- label: (context) => `Models: ${context.raw}`
722
  }
723
  }
724
  },
@@ -736,16 +731,17 @@ if __name__ == '__main__':
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();
@@ -764,15 +760,16 @@ if __name__ == '__main__':
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>
771
- <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
 
 
772
  Try Again
773
  </button>
774
- </div>
775
- `;
776
  document.getElementById('retryButton')?.addEventListener('click', () => loadModels(0));
777
  renderPagination();
778
  } finally {
@@ -780,9 +777,8 @@ if __name__ == '__main__':
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.';
@@ -794,14 +790,19 @@ if __name__ == '__main__':
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
 
@@ -820,6 +821,7 @@ if __name__ == '__main__':
820
 
821
  header.appendChild(headerTop);
822
 
 
823
  const metaInfo = document.createElement('div');
824
  metaInfo.className = 'grid-meta';
825
 
@@ -828,52 +830,59 @@ if __name__ == '__main__':
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
 
@@ -894,7 +903,8 @@ if __name__ == '__main__':
894
  });
895
  }
896
 
897
- function renderPagination(){
 
898
  elements.pagination.innerHTML = '';
899
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
900
 
@@ -910,19 +920,19 @@ if __name__ == '__main__':
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');
@@ -935,23 +945,24 @@ if __name__ == '__main__':
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();
@@ -963,6 +974,7 @@ if __name__ == '__main__':
963
  }
964
  }
965
 
 
966
  elements.searchInput.addEventListener('input', ()=>{
967
  clearTimeout(state.searchTimeout);
968
  state.searchTimeout = setTimeout(()=>loadModels(0), 300);
@@ -973,16 +985,18 @@ if __name__ == '__main__':
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 = `
@@ -990,19 +1004,20 @@ if __name__ == '__main__':
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)
 
1
  from flask import Flask, render_template, request, jsonify
2
  import requests
3
  import os
 
 
4
  from collections import Counter
5
 
6
  app = Flask(__name__)
7
 
8
+ # 1) ํŠธ๋ Œ๋”ฉ ๋ชจ๋ธ์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜
9
  def fetch_trending_models(offset=0, limit=72):
10
  try:
11
  url = "https://huggingface.co/api/models"
12
+ params = {"limit": 10000} # ๋งŽ์€ ๋ชจ๋ธ์„ ๊ฐ€์ ธ์˜ด
13
  response = requests.get(url, params=params, timeout=30)
14
 
15
  if response.status_code == 200:
16
  models = response.json()
17
+ # ํ•„ํ„ฐ๋ง
18
+ filtered = [
19
+ m for m in models
20
+ if m.get('owner') != 'None' and m.get('id', '').split('/', 1)[0] != 'None'
21
  ]
22
+ start = min(offset, len(filtered))
23
+ end = min(offset + limit, len(filtered))
24
+ print(f"Fetched {len(filtered)} models, returning {end - start} from {start} to {end}.")
 
 
25
 
26
  return {
27
+ 'models': filtered[start:end],
28
+ 'total': len(filtered),
29
  'offset': offset,
30
  'limit': limit,
31
+ 'all_models': filtered
32
  }
33
  else:
34
+ # ์˜ค๋ฅ˜ ์‘๋‹ต ์‹œ ๋”๋ฏธ ๋ชจ๋ธ
35
  print(f"Error fetching models: {response.status_code}")
36
  return {
37
  'models': generate_dummy_models(limit),
 
41
  'all_models': generate_dummy_models(500)
42
  }
43
  except Exception as e:
44
+ print("Exception when fetching models:", e)
45
  return {
46
  'models': generate_dummy_models(limit),
47
  'total': 200,
 
50
  'all_models': generate_dummy_models(500)
51
  }
52
 
53
+ # 2) ๋”๋ฏธ ๋ชจ๋ธ ์ƒ์„ฑ ํ•จ์ˆ˜(์˜ค๋ฅ˜์‹œ ์‚ฌ์šฉ)
54
  def generate_dummy_models(count):
55
+ dummy_list = []
56
  for i in range(count):
57
+ dummy_list.append({
58
  'id': f'dummy/model-{i}',
59
  'owner': 'dummy',
60
  'title': f'Example Model {i+1}',
61
  'likes': 100 - i,
62
+ 'downloads': 9999 - i, # ์ž„์˜์˜ ๋‹ค์šด๋กœ๋“œ ์ˆ˜
63
  'createdAt': '2023-01-01T00:00:00.000Z',
64
  'tags': ['dummy', 'fallback']
65
  })
66
+ return dummy_list
67
 
68
+ # 3) ๋ชจ๋ธ URL ์ƒ์„ฑ
69
  def transform_url(owner, name):
70
  name = name.replace('.', '-').replace('_', '-').lower()
71
  owner = owner.lower()
72
  return f"https://huggingface.co/{owner}/{name}"
73
 
74
+ # 4) ๋ชจ๋ธ ์ƒ์„ธ์ •๋ณด ๊ฐ€๊ณต
75
  def get_model_details(model_data, index, offset):
76
  try:
77
  if '/' in model_data.get('id', ''):
78
+ owner, name = model_data['id'].split('/', 1)
79
  else:
80
  owner = model_data.get('owner', '')
81
  name = model_data.get('id', '')
 
86
  original_url = f"https://huggingface.co/{owner}/{name}"
87
  embed_url = transform_url(owner, name)
88
 
89
+ # ๋‹ค์šด๋กœ๋“œ, ์ข‹์•„์š”
90
  likes_count = model_data.get('likes', 0)
91
+ downloads_count = model_data.get('downloads', 0) # ๋‹ค์šด๋กœ๋“œ ์ˆ˜ ์ถ”๊ฐ€
92
+
93
  title = model_data.get('title', name)
94
+ tags = model_data.get('tags', [])
95
 
96
  return {
97
  'url': original_url,
 
100
  'owner': owner,
101
  'name': name,
102
  'likes_count': likes_count,
103
+ 'downloads_count': downloads_count, # ๋‹ค์šด๋กœ๋“œ ์ˆ˜
104
  'tags': tags,
105
  'rank': offset + index + 1
106
  }
107
  except Exception as e:
108
+ print("Error processing model data:", e)
109
  return {
110
  'url': 'https://huggingface.co',
111
  'embedUrl': 'https://huggingface.co',
 
113
  'owner': 'huggingface',
114
  'name': 'error',
115
  'likes_count': 0,
116
+ 'downloads_count': 0,
117
  'tags': [],
118
  'rank': offset + index + 1
119
  }
120
 
121
+ # 5) ์˜ค๋„ˆ ํ†ต๊ณ„ (Top 30)
122
  def get_owner_stats(all_models):
123
  owners = []
124
+ for m in all_models:
125
+ if '/' in m.get('id', ''):
126
+ o, _ = m['id'].split('/', 1)
127
  else:
128
+ o = m.get('owner', '')
129
+ if o != 'None':
130
+ owners.append(o)
 
131
 
132
+ c = Counter(owners)
133
+ return c.most_common(30)
 
134
 
135
  @app.route('/')
136
  def home():
137
  return render_template('index.html')
138
 
139
+ # 6) ํŠธ๋ Œ๋”ฉ ๋ชจ๋ธ API
140
  @app.route('/api/trending-models', methods=['GET'])
141
  def trending_models():
142
  search_query = request.args.get('search', '').lower()
143
  offset = int(request.args.get('offset', 0))
144
  limit = int(request.args.get('limit', 72))
145
 
146
+ data = fetch_trending_models(offset, limit)
 
147
  results = []
148
+ for index, md in enumerate(data['models']):
149
+ info = get_model_details(md, index, offset)
150
  if not info:
151
  continue
152
 
153
  if search_query:
154
+ title_l = info['title'].lower()
155
+ owner_l = info['owner'].lower()
156
+ url_l = info['url'].lower()
157
+ tags_l = ' '.join(t.lower() for t in info['tags'])
158
+ # ๊ฒ€์ƒ‰ ์กฐ๊ฑด์— ์—†์œผ๋ฉด pass
159
+ if (search_query not in title_l and
160
+ search_query not in owner_l and
161
+ search_query not in url_l and
162
+ search_query not in tags_l):
 
163
  continue
164
 
165
  results.append(info)
166
 
167
+ top_owners = get_owner_stats(data['all_models'])
 
168
  return jsonify({
169
  'models': results,
170
+ 'total': data['total'],
171
  'offset': offset,
172
  'limit': limit,
173
  'top_owners': top_owners
174
  })
175
 
176
  if __name__ == '__main__':
177
+ # ํ…œํ”Œ๋ฆฟ ํด๋” ์ƒ์„ฑ
178
  os.makedirs('templates', exist_ok=True)
179
 
180
+ # index.html ์ƒ์„ฑ
181
  with open('templates/index.html', 'w', encoding='utf-8') as f:
182
  f.write("""<!DOCTYPE html>
183
  <html lang="en">
 
187
  <title>Huggingface Models Gallery</title>
188
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
189
  <style>
190
+ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700&display=swap');
191
 
192
  :root {
193
  --pastel-pink: #FFD6E0;
 
212
  * {
213
  margin: 0; padding: 0; box-sizing: border-box;
214
  }
 
215
  body {
216
  font-family: 'Nunito', sans-serif;
217
  color: var(--text-primary);
 
219
  background: linear-gradient(135deg, var(--pastel-blue) 0%, var(--pastel-purple) 100%);
220
  padding: 2rem;
221
  }
 
222
  .container {
223
  max-width: 1600px;
224
  margin: 0 auto;
225
  }
 
226
  .mac-window {
227
  background-color: var(--mac-window-bg);
228
  border-radius: 10px;
 
233
  border: 1px solid var(--mac-border);
234
  }
235
  .mac-toolbar {
236
+ display: flex; align-items: center;
237
+ padding: 10px 15px; background-color: var(--mac-toolbar);
 
 
238
  border-bottom: 1px solid var(--mac-border);
239
  }
240
  .mac-buttons {
 
251
  flex-grow: 1; text-align: center;
252
  font-size: 0.9rem; color: var(--text-secondary);
253
  }
254
+ .mac-content { padding: 20px; }
 
 
255
 
256
  .header { text-align: center; margin-bottom: 1.5rem; }
257
  .header h1 {
 
262
  }
263
 
264
  .search-bar {
265
+ display: flex; align-items: center; background: #fff; border-radius: 30px;
266
+ padding: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);
 
267
  max-width: 600px; margin: 0.5rem auto 1.5rem auto;
268
  }
269
  .search-bar input {
270
+ flex-grow: 1; border: none; padding: 12px 20px; font-size: 1rem; outline: none;
271
+ background: transparent; border-radius: 30px;
272
  }
273
  .search-bar .refresh-btn {
274
  background-color: var(--pastel-green); color: #1a202c; border: none; border-radius: 30px;
275
+ padding: 10px 20px; font-size: 1rem; font-weight: 600; cursor: pointer;
276
+ transition: all 0.2s; display: flex; align-items: center; gap: 8px;
277
  }
278
  .search-bar .refresh-btn:hover {
279
  background-color: #9ee7c0; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
 
286
  animation: spin 1s linear infinite;
287
  }
288
  @keyframes spin {
289
+ 0% { transform: rotate(0deg); }
290
+ 100% { transform: rotate(360deg); }
291
  }
292
 
293
  .grid-container {
 
321
  padding: 4px 8px; border-radius: 50px;
322
  }
323
  .grid-header h3 {
324
+ margin: 0; font-size: 1.2rem; font-weight: 700;
325
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
326
  }
327
  .grid-meta {
328
+ display: flex; flex-wrap: wrap; align-items: center; font-size: 0.9rem;
329
+ gap: 12px;
330
  }
331
  .owner-info {
332
  color: var(--text-secondary); font-weight: 500;
333
  }
334
  .likes-counter {
335
+ color: #e53e3e; font-weight: 600;
336
+ display: flex; align-items: center;
337
  }
338
  .likes-counter span { margin-left: 4px; }
339
+ .downloads-counter {
340
+ color: #2f855a; font-weight: 600;
341
+ }
342
 
343
  .grid-actions {
344
  padding: 10px 15px; text-align: right; background: rgba(255,255,255,0.7);
 
350
  padding: 5px 10px; border-radius: 5px; transition: all 0.2s;
351
  background: rgba(237,242,247,0.8);
352
  }
353
+ .open-link:hover { background: #e2e8f0; }
 
 
354
 
355
  .grid-content {
356
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
357
  padding-top: 85px; padding-bottom: 45px;
358
  }
359
  .iframe-container {
360
+ width: 100%; height: 100%; position: relative; overflow: hidden;
361
  }
 
362
  .grid-content iframe {
363
  transform: scale(0.7); transform-origin: top left;
364
  width: 142.857%; height: 142.857%; border: none; border-radius: 0;
365
  }
366
 
367
+ /* ์ž„๋ฒ ๋“œ ์‹คํŒจ ์‹œ ํƒœ๊ทธ ์ถœ๋ ฅ */
368
  .tags-fallback {
369
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
370
  background: rgba(255,255,255,0.8); backdrop-filter: blur(5px);
371
  display: flex; flex-direction: column; justify-content: center; align-items: center;
372
  text-align: center; padding: 1rem;
373
  }
 
 
 
374
  .tags-list {
375
  display: flex; flex-wrap: wrap; gap: 10px; max-width: 300px; justify-content: center;
376
  }
 
378
  padding: 6px 12px; border-radius: 15px; font-size: 0.85rem; font-weight: 600;
379
  color: #333; background-color: var(--pastel-yellow);
380
  }
 
381
 
 
382
  .pagination {
383
+ display: flex; justify-content: center; align-items: center;
384
+ gap: 10px; margin: 2rem 0;
385
  }
386
  .pagination-button {
387
  background: #fff; border: none; padding: 10px 20px; border-radius: 10px;
 
398
  background: #edf2f7; color: #a0aec0; cursor: default; box-shadow: none;
399
  }
400
 
 
401
  .loading {
402
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
403
+ background: rgba(255,255,255,0.8); backdrop-filter: blur(5px);
404
+ display: flex; justify-content: center; align-items: center; z-index: 1000;
405
  }
406
  .loading-content { text-align: center; }
407
  .loading-spinner {
 
416
  display: none; margin-top: 10px; color: #e53e3e; font-size: 0.9rem;
417
  }
418
 
419
+ /* Stats */
420
+ .stats-window { margin: 2rem 0; }
 
 
421
  .stats-header {
422
  display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;
423
  }
 
448
  .chart-container { height: 300px; }
449
  }
450
 
451
+ @keyframes spin {
452
+ 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); }
453
+ }
454
  </style>
455
  </head>
456
  <body>
 
464
  </div>
465
  <div class="mac-title">Huggingface Explorer</div>
466
  </div>
467
+
468
  <div class="mac-content">
 
469
  <div class="header">
470
  <h1>HF Model Leaderboard</h1>
471
+ <p>Discover the top trending models from Huggingface</p>
472
  </div>
473
 
474
+ <!-- ํ†ต๊ณ„ ์„น์…˜ -->
475
  <div class="stats-window mac-window">
476
  <div class="mac-toolbar">
477
  <div class="mac-buttons">
 
503
 
504
  <div id="gridContainer" class="grid-container"></div>
505
  <div id="pagination" class="pagination"></div>
 
506
  </div>
507
  </div>
508
  </div>
509
 
510
+ <!-- ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ -->
511
  <div id="loadingIndicator" class="loading">
512
  <div class="loading-content">
513
  <div class="loading-spinner"></div>
 
544
  iframeStatuses: {}
545
  };
546
 
547
+ // ์•„์ดํ”„๋ ˆ์ž„ ๋กœ๋”ฉ ์ฒดํฌ
548
  const iframeLoader = {
549
  checkQueue: {},
550
  maxAttempts: 3,
551
  checkInterval: 3000,
552
+ startChecking(iframe, modelKey) {
553
  this.checkQueue[modelKey] = {
554
+ iframe,
555
+ attempts: 0,
556
+ status: 'loading'
557
  };
558
  this.checkIframeStatus(modelKey);
559
  },
 
571
  item.attempts++;
572
 
573
  try {
 
574
  if(!iframe || !iframe.parentNode) {
575
  delete this.checkQueue[modelKey];
576
  return;
577
  }
578
+ // ์‹ค์ œ ๋กœ๋”ฉ ์—ฌ๋ถ€ ์ฒดํฌ
 
579
  try {
580
+ const hasBody = iframe.contentWindow
581
+ && iframe.contentWindow.document
582
+ && iframe.contentWindow.document.body;
583
  if(hasBody && iframe.contentWindow.document.body.innerHTML.length > 100) {
584
+ // ์ผ๋ถ€ ํ…์ŠคํŠธ
585
  const bodyText = iframe.contentWindow.document.body.textContent.toLowerCase();
586
+ if(
587
+ bodyText.includes('forbidden') ||
588
+ bodyText.includes('404') ||
589
+ bodyText.includes('not found') ||
590
+ bodyText.includes('error')
591
+ ) {
592
  item.status = 'error';
593
  handleIframeError(iframe);
594
  } else {
 
598
  return;
599
  }
600
  } catch(e) {
601
+ // ๋ณด์•ˆ ๋“ฑ์œผ๋กœ ์ ‘๊ทผ ๋ถˆ๊ฐ€
602
  }
603
 
604
+ // ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ ์‹œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
605
  if(item.attempts >= this.maxAttempts) {
606
  item.status = 'error';
607
  handleIframeError(iframe);
 
611
 
612
  setTimeout(() => this.checkIframeStatus(modelKey), this.checkInterval);
613
  } catch(err) {
614
+ console.error('checkIframeStatus error:', err);
615
  if(item.attempts >= this.maxAttempts) {
616
  item.status = 'error';
617
  handleIframeError(iframe);
 
623
  }
624
  };
625
 
626
+ // ์•„์ดํ”„๋ ˆ์ž„ ์˜ค๋ฅ˜ => ํƒœ๊ทธ๋กœ ๋Œ€์ฒด (1) "Unable to embed model." ๋ฌธ๊ตฌ ์‚ญ์ œ
627
  function handleIframeError(iframe) {
628
  const container = iframe.parentNode;
629
  if(!container) return;
630
 
631
+ // ์•„์ดํ”„๋ ˆ์ž„ ์ˆจ๊ธฐ๊ธฐ
632
  iframe.style.display = 'none';
633
 
634
+ // ํƒœ๊ทธ ๊ฐ€์ ธ์˜ค๊ธฐ
635
  const tagsRaw = container.dataset.tags || '[]';
636
  let tags = [];
637
  try {
 
640
  tags = [];
641
  }
642
 
643
+ // ๋Œ€์ฒด UI
644
  const fallbackDiv = document.createElement('div');
645
  fallbackDiv.className = 'tags-fallback';
646
 
647
+ // ๋ณ„๋„ ๋ฌธ๊ตฌ ์—†์ด ํƒœ๊ทธ๋งŒ ํ‘œ์‹œ
 
 
 
648
  const tagsList = document.createElement('div');
649
  tagsList.className = 'tags-list';
650
 
 
653
  'var(--pastel-yellow)', 'var(--pastel-green)', 'var(--pastel-orange)'
654
  ];
655
 
656
+ tags.forEach((tag, idx) => {
657
+ const chip = document.createElement('span');
658
+ chip.className = 'tag-chip';
659
+ chip.textContent = tag;
660
+ // ํŒŒ์Šคํ…” ์ƒ‰์ƒ ๋ฐ˜๋ณต
661
+ const color = pastelColors[idx % pastelColors.length];
662
+ chip.style.backgroundColor = color;
663
+ tagsList.appendChild(chip);
664
  });
665
 
666
  fallbackDiv.appendChild(tagsList);
667
  container.appendChild(fallbackDiv);
668
  }
669
 
670
+ // ํ†ต๊ณ„ ํ‘œ์‹œ ํ† ๊ธ€
671
  function toggleStats() {
672
  state.statsVisible = !state.statsVisible;
673
  elements.statsContent.classList.toggle('open', state.statsVisible);
 
712
  legend: { display: false },
713
  tooltip: {
714
  callbacks: {
715
+ title: (items) => items[0].label,
716
+ label: (ctx) => 'Models: ' + ctx.raw
717
  }
718
  }
719
  },
 
731
  });
732
  }
733
 
734
+ // ๋ชจ๋ธ ๋กœ๋“œ
735
+ async function loadModels(page=0) {
736
  setLoading(true);
737
  try {
738
+ const search = elements.searchInput.value;
739
  const offset = page * state.itemsPerPage;
740
 
741
  const timeoutPromise = new Promise((_, reject) => {
742
  setTimeout(() => reject(new Error('Request timeout')), 30000);
743
  });
744
+ const fetchPromise = fetch(`/api/trending-models?search=${encodeURIComponent(search)}&offset=${offset}&limit=${state.itemsPerPage}`);
745
 
746
  const response = await Promise.race([fetchPromise, timeoutPromise]);
747
  const data = await response.json();
 
760
  } catch(error) {
761
  console.error('Error loading models:', error);
762
  elements.gridContainer.innerHTML = `
763
+ <div style="grid-column:1/-1; text-align:center; padding:40px;">
764
+ <div style="font-size:3rem; margin-bottom:20px;">โš ๏ธ</div>
765
+ <h3 style="margin-bottom:10px;">Unable to load models</h3>
766
+ <p style="color:#666;">Please try refreshing. If it persists, check later.</p>
767
+ <button id="retryButton"
768
+ style="margin-top:20px; padding:10px 20px; background:var(--pastel-purple);
769
+ border:none; border-radius:5px; cursor:pointer;">
770
  Try Again
771
  </button>
772
+ </div>`;
 
773
  document.getElementById('retryButton')?.addEventListener('click', () => loadModels(0));
774
  renderPagination();
775
  } finally {
 
777
  }
778
  }
779
 
780
+ function renderGrid(models) {
781
  elements.gridContainer.innerHTML = '';
 
782
  if(!models || models.length===0) {
783
  const msg = document.createElement('p');
784
  msg.textContent = 'No models found matching your search.';
 
790
  return;
791
  }
792
 
793
+ models.forEach((item) => {
794
  try {
795
+ const {
796
+ url, title, likes_count, downloads_count,
797
+ owner, name, rank, tags
798
+ } = item;
799
+
800
  if(owner==='None') return;
801
 
802
  const gridItem = document.createElement('div');
803
  gridItem.className = 'grid-item';
804
 
805
+ // ํ—ค๋”
806
  const header = document.createElement('div');
807
  header.className = 'grid-header';
808
 
 
821
 
822
  header.appendChild(headerTop);
823
 
824
+ // ๋ฉ”ํƒ€
825
  const metaInfo = document.createElement('div');
826
  metaInfo.className = 'grid-meta';
827
 
 
830
  ownerEl.textContent = `by ${owner}`;
831
  metaInfo.appendChild(ownerEl);
832
 
833
+ const likesEl = document.createElement('div');
834
+ likesEl.className = 'likes-counter';
835
+ likesEl.innerHTML = 'โ™ฅ <span>' + likes_count + '</span>';
836
+ metaInfo.appendChild(likesEl);
837
+
838
+ // ๋‹ค์šด๋กœ๋“œ ์ˆ˜ ์ถ”๊ฐ€
839
+ const downloadsEl = document.createElement('div');
840
+ downloadsEl.className = 'downloads-counter';
841
+ downloadsEl.textContent = 'Downloads: ' + downloads_count;
842
+ metaInfo.appendChild(downloadsEl);
843
 
844
  header.appendChild(metaInfo);
845
  gridItem.appendChild(header);
846
 
847
+ // ์ฝ˜ํ…์ธ  (iframe ์‹œ๋„)
848
  const content = document.createElement('div');
849
  content.className = 'grid-content';
850
 
851
  const iframeContainer = document.createElement('div');
852
  iframeContainer.className = 'iframe-container';
 
 
853
  iframeContainer.dataset.tags = JSON.stringify(tags);
854
 
855
  const iframe = document.createElement('iframe');
856
+ // direct url
857
+ iframe.src = createDirectUrl(owner, name);
858
  iframe.title = title;
859
  iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
860
+ iframe.setAttribute('allowfullscreen', '');
861
+ iframe.setAttribute('frameborder', '0');
862
  iframe.loading = 'lazy';
863
 
864
  const modelKey = `${owner}/${name}`;
865
  state.iframeStatuses[modelKey] = 'loading';
866
 
867
+ iframe.onload = () => {
868
+ iframeLoader.startChecking(iframe, modelKey);
869
  };
870
+ iframe.onerror = () => {
871
  state.iframeStatuses[modelKey] = 'error';
872
  handleIframeError(iframe);
873
  };
874
+ // ์ผ์ • ์‹œ๊ฐ„ ํ›„์—๋„ ๋กœ๋”ฉ์ด๋ฉด ํƒœ๊ทธ๋กœ ๋Œ€์ฒด
875
+ setTimeout(() => {
876
  if(state.iframeStatuses[modelKey]==='loading'){
877
  state.iframeStatuses[modelKey] = 'error';
878
  handleIframeError(iframe);
879
  }
880
+ }, 8000);
881
 
882
  iframeContainer.appendChild(iframe);
883
  content.appendChild(iframeContainer);
884
 
885
+ // ํ•˜๋‹จ ๋งํฌ
886
  const actions = document.createElement('div');
887
  actions.className = 'grid-actions';
888
 
 
903
  });
904
  }
905
 
906
+ // ํŽ˜์ด์ง€๋„ค์ด์…˜
907
+ function renderPagination() {
908
  elements.pagination.innerHTML = '';
909
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
910
 
 
920
  const maxButtons = 7;
921
  let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons/2));
922
  let endPage = Math.min(totalPages-1, startPage + maxButtons-1);
923
+ if(endPage - startPage + 1 < maxButtons) {
924
  startPage = Math.max(0, endPage - maxButtons + 1);
925
  }
926
 
927
  for(let i=startPage; i<=endPage; i++){
928
+ const pageBtn = document.createElement('button');
929
+ pageBtn.className = 'pagination-button';
930
+ if(i===state.currentPage) pageBtn.classList.add('active');
931
+ pageBtn.textContent = i+1;
932
+ pageBtn.addEventListener('click', () => {
933
  if(i!==state.currentPage) loadModels(i);
934
  });
935
+ elements.pagination.appendChild(pageBtn);
936
  }
937
 
938
  const nextButton = document.createElement('button');
 
945
  elements.pagination.appendChild(nextButton);
946
  }
947
 
948
+ // ๋กœ๋”ฉ ํ‘œ์‹œ
949
+ function setLoading(isLoading) {
950
  state.isLoading = isLoading;
951
+ elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
952
  if(isLoading) {
953
  elements.refreshButton.classList.add('refreshing');
954
  clearTimeout(state.loadingTimeout);
955
  state.loadingTimeout = setTimeout(()=>{
956
+ elements.loadingError.style.display = 'block';
957
  },10000);
958
  } else {
959
  elements.refreshButton.classList.remove('refreshing');
960
  clearTimeout(state.loadingTimeout);
961
+ elements.loadingError.style.display = 'none';
962
  }
963
  }
964
 
965
+ // HF ๋ชจ๋ธ URL ์ƒ์„ฑ
966
  function createDirectUrl(owner, name){
967
  try {
968
  name = name.replace(/\./g,'-').replace(/_/g,'-').toLowerCase();
 
974
  }
975
  }
976
 
977
+ // ๊ฒ€์ƒ‰๋ฐ•์Šค ์ด๋ฒคํŠธ
978
  elements.searchInput.addEventListener('input', ()=>{
979
  clearTimeout(state.searchTimeout);
980
  state.searchTimeout = setTimeout(()=>loadModels(0), 300);
 
985
  elements.refreshButton.addEventListener('click', ()=>loadModels(0));
986
  elements.statsToggle.addEventListener('click', toggleStats);
987
 
988
+ // Mac ๋ฒ„ํŠผ (์—ฐ์ถœ์šฉ)
989
  document.querySelectorAll('.mac-button').forEach(btn=>{
990
+ btn.addEventListener('click', (e)=> e.preventDefault());
 
 
991
  });
992
 
993
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ์‹œ
994
+ window.addEventListener('load', () => {
995
  setTimeout(()=>loadModels(0), 500);
996
  });
997
+
998
+ // 20์ดˆ๊ฐ€ ์ง€๋‚˜๋„ ๋กœ๋”ฉ ์ค‘์ด๋ฉด...
999
+ setTimeout(() => {
1000
  if(state.isLoading){
1001
  setLoading(false);
1002
  elements.gridContainer.innerHTML = `
 
1004
  <div style="font-size:3rem; margin-bottom:20px;">โฑ๏ธ</div>
1005
  <h3 style="margin-bottom:10px;">Loading is taking longer than expected</h3>
1006
  <p style="color:#666;">Please try refreshing the page.</p>
1007
+ <button onClick="window.location.reload()"
1008
+ style="margin-top:20px; padding:10px 20px; background:var(--pastel-purple);
1009
+ border:none; border-radius:5px; cursor:pointer;">
1010
  Reload Page
1011
  </button>
1012
+ </div>`;
 
1013
  }
1014
  },20000);
1015
 
1016
+ // ์ฆ‰์‹œ ๋กœ๋“œ ํ˜ธ์ถœ
1017
  loadModels(0);
1018
  </script>
1019
  </body>
1020
  </html>
1021
  """)
1022
 
 
1023
  app.run(host='0.0.0.0', port=7860)