openfree commited on
Commit
dcdc31d
ยท
verified ยท
1 Parent(s): e878e76

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1071 -311
app.py CHANGED
@@ -7,38 +7,60 @@ from collections import Counter
7
 
8
  app = Flask(__name__)
9
 
10
- # Function to fetch zero-gpu (cpu-based) spaces from Huggingface with pagination
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def fetch_trending_spaces(offset=0, limit=72):
12
  """
13
- Zero GPU(=CPU)๋งŒ ํ•„ํ„ฐ๋งํ•˜๊ธฐ ์œ„ํ•ด, Hugging Face API ํ˜ธ์ถœ ์‹œ hardware=cpu ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ํฌํ•จ์‹œํ‚ต๋‹ˆ๋‹ค.
 
14
  """
15
  try:
16
  url = "https://huggingface.co/api/spaces"
17
  params = {
18
- "limit": 10000,
19
- "hardware": "cpu", # <-- Zero GPU ์ŠคํŽ˜์ด์Šค(=CPU) ํ•„ํ„ฐ
20
- # "sort": "trending", # (ํ•„์š”์‹œ ์ •๋ ฌ ์ง€์ • ๊ฐ€๋Šฅ: ์‹ค์ œ ์ง€์› ์—ฌ๋ถ€๋Š” HF API์— ๋”ฐ๋ผ ๋‹ค๋ฆ„)
21
  }
22
-
23
  response = requests.get(url, params=params, timeout=30)
24
-
25
  if response.status_code == 200:
26
  spaces = response.json()
27
 
28
- # ๋งŒ์•ฝ, ๋ฐ์ดํ„ฐ ์ค‘ owner๋‚˜ id๊ฐ€ 'None'์ธ ๊ฒฝ์šฐ ํ•„ํ„ฐ๋ง
29
  filtered_spaces = [
30
  space for space in spaces
31
  if space.get('owner') != 'None'
32
  and space.get('id', '').split('/', 1)[0] != 'None'
33
  ]
34
 
35
- # ํŽ˜์ด์ง•
36
  start = min(offset, len(filtered_spaces))
37
  end = min(offset + limit, len(filtered_spaces))
38
-
39
  print(f"[fetch_trending_spaces] CPU๊ธฐ๋ฐ˜ ์ŠคํŽ˜์ด์Šค ์ด {len(filtered_spaces)}๊ฐœ, "
40
  f"์š”์ฒญ ๊ตฌ๊ฐ„ {start}~{end-1} ๋ฐ˜ํ™˜")
41
-
42
  return {
43
  'spaces': filtered_spaces[start:end],
44
  'total': len(filtered_spaces),
@@ -48,6 +70,7 @@ def fetch_trending_spaces(offset=0, limit=72):
48
  }
49
  else:
50
  print(f"Error fetching spaces: {response.status_code}")
 
51
  return {
52
  'spaces': generate_dummy_spaces(limit),
53
  'total': 200,
@@ -55,8 +78,10 @@ def fetch_trending_spaces(offset=0, limit=72):
55
  'limit': limit,
56
  'all_spaces': generate_dummy_spaces(500)
57
  }
 
58
  except Exception as e:
59
  print(f"Exception when fetching spaces: {e}")
 
60
  return {
61
  'spaces': generate_dummy_spaces(limit),
62
  'total': 200,
@@ -65,40 +90,27 @@ def fetch_trending_spaces(offset=0, limit=72):
65
  'all_spaces': generate_dummy_spaces(500)
66
  }
67
 
68
- def generate_dummy_spaces(count):
69
- """
70
- API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ์˜ˆ์‹œ์šฉ ๋”๋ฏธ ์ŠคํŽ˜์ด์Šค ์ƒ์„ฑ
71
- """
72
- spaces = []
73
- for i in range(count):
74
- spaces.append({
75
- 'id': f'dummy/space-{i}',
76
- 'owner': 'dummy',
77
- 'title': f'Example Space {i+1}',
78
- 'description': 'Dummy space for fallback',
79
- 'likes': 100 - i,
80
- 'createdAt': '2023-01-01T00:00:00.000Z',
81
- 'hardware': 'cpu',
82
- 'user': {
83
- 'avatar_url': 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg',
84
- 'name': 'dummyUser'
85
- }
86
- })
87
- return spaces
88
-
89
  def transform_url(owner, name):
90
  """
91
- Hugging Face Space -> ์ง์ ‘ ์ ‘๊ทผ๊ฐ€๋Šฅํ•œ ์„œ๋ธŒ๋„๋ฉ”์ธ URL
92
- ์˜ˆ: huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space
93
  """
94
- name = name.replace('.', '-').replace('_', '-')
 
 
 
 
95
  owner = owner.lower()
96
  name = name.lower()
 
97
  return f"https://{owner}-{name}.hf.space"
98
 
 
99
  def get_space_details(space_data, index, offset):
100
  """
101
- ๊ฐœ๋ณ„ ์ŠคํŽ˜์ด์Šค์—์„œ ํ•„์š”ํ•œ ์ •๋ณด๋“ค(ZeroGPU ๋ฑƒ์ง€, ์•„๋ฐ”ํƒ€ URL, short description ๋“ฑ) ์ถ”๊ฐ€ ์ถ”์ถœ
 
102
  """
103
  try:
104
  if '/' in space_data.get('id', ''):
@@ -107,25 +119,27 @@ def get_space_details(space_data, index, offset):
107
  owner = space_data.get('owner', '')
108
  name = space_data.get('id', '')
109
 
 
110
  if owner == 'None' or name == 'None':
111
  return None
112
-
113
- # Construct original and embed URL
114
  original_url = f"https://huggingface.co/spaces/{owner}/{name}"
115
  embed_url = transform_url(owner, name)
116
 
117
- # Title / Description
 
 
 
118
  title = space_data.get('title') or name
119
- short_desc = space_data.get('description', '') # ์ƒˆ๋กœ ์ถ”๊ฐ€
120
 
121
- # Likes
122
- likes_count = space_data.get('likes', 0)
123
 
124
- # User / avatar
125
- # (Hugging Face API ์ƒ ์‹ค์ œ ์œ„์น˜๊ฐ€ space_data['user'] ๋‚ด๋ถ€์ผ ์ˆ˜ ์žˆ์Œ)
126
  user_info = space_data.get('user', {})
127
  avatar_url = user_info.get('avatar_url', '')
128
- author_name = user_info.get('name') or owner # ์—†์œผ๋ฉด owner๋ช…์œผ๋กœ ๋Œ€์ฒด
129
 
130
  return {
131
  'url': original_url,
@@ -141,6 +155,7 @@ def get_space_details(space_data, index, offset):
141
  }
142
  except Exception as e:
143
  print(f"Error processing space data: {e}")
 
144
  return {
145
  'url': 'https://huggingface.co/spaces',
146
  'embedUrl': 'https://huggingface.co/spaces',
@@ -154,9 +169,10 @@ def get_space_details(space_data, index, offset):
154
  'rank': offset + index + 1
155
  }
156
 
 
157
  def get_owner_stats(all_spaces):
158
  """
159
- ๋ชจ๋“  ์ŠคํŽ˜์ด์Šค ์ค‘ owner(=ํฌ๋ฆฌ์—์ดํ„ฐ) ๋“ฑ์žฅ ๋นˆ๋„ ์ƒ์œ„ 30๋ช…
160
  """
161
  owners = []
162
  for space in all_spaces:
@@ -164,37 +180,47 @@ def get_owner_stats(all_spaces):
164
  owner, _ = space.get('id', '').split('/', 1)
165
  else:
166
  owner = space.get('owner', '')
167
-
168
  if owner != 'None':
169
  owners.append(owner)
170
-
 
171
  owner_counts = Counter(owners)
 
 
172
  top_owners = owner_counts.most_common(30)
 
173
  return top_owners
174
 
 
175
  @app.route('/')
176
  def home():
 
 
 
177
  return render_template('index.html')
178
 
 
179
  @app.route('/api/trending-spaces', methods=['GET'])
180
  def trending_spaces():
181
  """
182
- ์ฒซ ๋ฒˆ์งธ ํƒญ(Zero GPU Spaces)์—์„œ ์‚ฌ์šฉํ•  API.
183
- ๊ธฐ์กด์—๋Š” 'ํŠธ๋ Œ๋”ฉ ์ „๋ถ€'๋ฅผ ๊ฐ€์ ธ์™”์ง€๋งŒ, ์œ„์—์„œ hardware=cpu๋กœ ํ•„ํ„ฐ๋ง๋œ ๋ฆฌ์ŠคํŠธ๋งŒ ๋‚ด๋ ค์คŒ.
184
  """
185
  search_query = request.args.get('search', '').lower()
186
  offset = int(request.args.get('offset', 0))
187
- limit = int(request.args.get('limit', 72))
188
 
 
189
  spaces_data = fetch_trending_spaces(offset, limit)
190
 
 
191
  results = []
192
  for index, space_data in enumerate(spaces_data['spaces']):
193
  space_info = get_space_details(space_data, index, offset)
194
  if not space_info:
195
  continue
196
 
197
- # ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ
198
  if search_query:
199
  if (search_query not in space_info['title'].lower()
200
  and search_query not in space_info['owner'].lower()
@@ -203,7 +229,8 @@ def trending_spaces():
203
  continue
204
 
205
  results.append(space_info)
206
-
 
207
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
208
 
209
  return jsonify({
@@ -214,25 +241,589 @@ def trending_spaces():
214
  'top_owners': top_owners
215
  })
216
 
217
- # ---------------------------
218
- # ์—ฌ๊ธฐ๋ถ€ํ„ฐ๋Š” Flask ์•ฑ ์‹คํ–‰ ๋ฐ index.html ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ ๋ถ€๋ถ„
219
- # ---------------------------
220
-
221
  if __name__ == '__main__':
 
 
 
 
222
  os.makedirs('templates', exist_ok=True)
 
 
223
  with open('templates/index.html', 'w', encoding='utf-8') as f:
224
- f.write("""<!DOCTYPE html>
225
  <html lang="en">
226
  <head>
227
  <meta charset="UTF-8">
228
  <title>Huggingface Zero-GPU Spaces</title>
229
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
230
  <style>
231
- /* (์ƒ๋žต: ๊ธฐ๋ณธ ์Šคํƒ€์ผ + MacWindow UI + ... ๊ธฐ์กด ์ฝ”๋“œ ๋™์ผ) */
232
- /* ์•„๋ž˜ ์นด๋“œ ๋ถ€๋ถ„(.grid-item) ์•ˆ์ชฝ ๊ตฌ์กฐ๋งŒ ์ผ๋ถ€ ์ˆ˜์ •ํ•ด์„œ,
233
- ์ฝ”๋“œ #2์ฒ˜๋Ÿผ Zero GPU ๋ฐฐ์ง€, ์•„๋ฐ”ํƒ€, description ํ‘œ์‹œ ๋“ฑ ๋ฐ˜์˜. */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
- /* ์ƒˆ๋กœ์šด ๋‚ด์šฉ */
236
  .space-header {
237
  display: flex;
238
  align-items: center;
@@ -304,20 +895,21 @@ if __name__ == '__main__':
304
  </div>
305
  <div class="mac-title">Huggingface Explorer</div>
306
  </div>
 
307
  <div class="mac-content">
308
  <div class="header">
309
- <!-- ํƒญ ์ด๋ฆ„์„ Zero GPU Spaces๋กœ ๋ณ€๊ฒฝ -->
310
  <h1>Zero GPU Spaces</h1>
311
  <p>Discover CPU-based (No GPU) spaces from Hugging Face</p>
312
  </div>
313
 
314
- <!-- ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ (๊ธฐ์กด) -->
315
  <div class="tab-nav">
316
  <button id="tabTrendingButton" class="tab-button active">Zero GPU Spaces</button>
317
  <button id="tabFixedButton" class="tab-button">Fixed Tab</button>
318
  </div>
319
-
320
- <!-- ์ฒซ ๋ฒˆ์งธ ํƒญ(์ด๋ฆ„๋งŒ ๋ฐ”๋€œ) -->
321
  <div id="trendingTab" class="tab-content active">
322
  <div class="stats-window mac-window">
323
  <div class="mac-toolbar">
@@ -340,7 +932,7 @@ if __name__ == '__main__':
340
  </div>
341
  </div>
342
  </div>
343
-
344
  <div class="search-bar">
345
  <input type="text" id="searchInput" placeholder="Search by name, owner, or description..." />
346
  <button id="refreshButton" class="refresh-btn">
@@ -348,13 +940,13 @@ if __name__ == '__main__':
348
  Refresh
349
  </button>
350
  </div>
351
-
352
  <div id="gridContainer" class="grid-container"></div>
353
-
354
  <div id="pagination" class="pagination"></div>
355
  </div>
356
-
357
- <!-- ๋‘ ๋ฒˆ์งธ ํƒญ(๊ณ ์ •) -->
358
  <div id="fixedTab" class="tab-content">
359
  <div id="fixedGrid" class="grid-container"></div>
360
  </div>
@@ -373,7 +965,7 @@ if __name__ == '__main__':
373
  </div>
374
 
375
  <script>
376
- // JS ๋กœ์ง์€ ๊ธฐ์กด ๊ตฌ์กฐ ์œ ์ง€, renderGrid()์—์„œ ์นด๋“œ ๋ ˆ์ด์•„์›ƒ๋งŒ ์ˆ˜์ •
377
  const elements = {
378
  gridContainer: document.getElementById('gridContainer'),
379
  loadingIndicator: document.getElementById('loadingIndicator'),
@@ -385,7 +977,7 @@ if __name__ == '__main__':
385
  statsContent: document.getElementById('statsContent'),
386
  creatorStatsChart: document.getElementById('creatorStatsChart')
387
  };
388
-
389
  const tabTrendingButton = document.getElementById('tabTrendingButton');
390
  const tabFixedButton = document.getElementById('tabFixedButton');
391
  const trendingTab = document.getElementById('trendingTab');
@@ -403,41 +995,52 @@ if __name__ == '__main__':
403
  statsVisible: false,
404
  chartInstance: null,
405
  topOwners: [],
406
- iframeStatuses: {},
407
  };
408
 
 
409
  const iframeLoader = {
410
  checkQueue: {},
411
  maxAttempts: 5,
412
  checkInterval: 5000,
 
413
  startChecking: function(iframe, owner, name, title, spaceKey) {
414
  this.checkQueue[spaceKey] = {
415
- iframe, owner, name, title,
416
- attempts: 0, status: 'loading'
 
 
 
 
417
  };
418
  this.checkIframeStatus(spaceKey);
419
  },
 
420
  checkIframeStatus: function(spaceKey) {
421
- if(!this.checkQueue[spaceKey]) return;
 
422
  const item = this.checkQueue[spaceKey];
423
-
424
- if(item.status !== 'loading') {
425
  delete this.checkQueue[spaceKey];
426
  return;
427
  }
428
  item.attempts++;
429
-
430
  try {
431
- if(!item.iframe || !item.iframe.parentNode) {
432
  delete this.checkQueue[spaceKey];
433
  return;
434
  }
 
 
435
  try {
436
- const hasContent = item.iframe.contentWindow && item.iframe.contentWindow.document.body;
437
- if(hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
 
 
438
  const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
439
- if(bodyText.includes('forbidden') || bodyText.includes('404')
440
- || bodyText.includes('not found') || bodyText.includes('error')) {
441
  item.status = 'error';
442
  handleIframeError(item.iframe, item.owner, item.name, item.title);
443
  } else {
@@ -446,17 +1049,21 @@ if __name__ == '__main__':
446
  delete this.checkQueue[spaceKey];
447
  return;
448
  }
449
- } catch(e) {}
 
 
450
 
 
451
  const rect = item.iframe.getBoundingClientRect();
452
- if(rect.width > 50 && rect.height > 50 && item.attempts > 2) {
453
  item.status = 'success';
454
  delete this.checkQueue[spaceKey];
455
  return;
456
  }
457
 
458
- if(item.attempts >= this.maxAttempts) {
459
- if(item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) {
 
460
  item.status = 'success';
461
  } else {
462
  item.status = 'error';
@@ -465,12 +1072,14 @@ if __name__ == '__main__':
465
  delete this.checkQueue[spaceKey];
466
  return;
467
  }
 
 
468
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
469
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
470
 
471
- } catch(e) {
472
  console.error('Error checking iframe status:', e);
473
- if(item.attempts >= this.maxAttempts) {
474
  item.status = 'error';
475
  handleIframeError(item.iframe, item.owner, item.name, item.title);
476
  delete this.checkQueue[spaceKey];
@@ -485,22 +1094,26 @@ if __name__ == '__main__':
485
  state.statsVisible = !state.statsVisible;
486
  elements.statsContent.classList.toggle('open', state.statsVisible);
487
  elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
488
- if(state.statsVisible && state.topOwners.length > 0) {
 
489
  renderCreatorStats();
490
  }
491
  }
 
492
  function renderCreatorStats() {
493
- if(state.chartInstance) {
494
  state.chartInstance.destroy();
495
  }
496
  const ctx = elements.creatorStatsChart.getContext('2d');
497
  const labels = state.topOwners.map(item => item[0]);
498
  const data = state.topOwners.map(item => item[1]);
 
499
  const colors = [];
500
- for(let i=0;i<labels.length;i++){
501
  const hue = (i * 360 / labels.length) % 360;
502
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
503
  }
 
504
  state.chartInstance = new Chart(ctx, {
505
  type: 'bar',
506
  data: {
@@ -509,32 +1122,47 @@ if __name__ == '__main__':
509
  label: 'Number of Spaces',
510
  data: data,
511
  backgroundColor: colors,
512
- borderColor: colors.map(c => c.replace('0.7','1')),
513
- borderWidth:1
514
  }]
515
  },
516
  options: {
517
  indexAxis: 'y',
518
  responsive: true,
519
- maintainAspectRatio:false,
520
- plugins:{
521
- legend:{display:false},
522
- tooltip:{
523
- callbacks:{
524
- title:function(t){return t[0].label;},
525
- label:function(c){return `Spaces: ${c.raw}`;}
 
 
 
 
526
  }
527
  }
528
  },
529
- scales:{
530
- x:{beginAtZero:true,title:{display:true,text:'Number of Spaces'}},
531
- y:{
532
- title:{display:true,text:'Creator ID'},
533
- ticks:{
534
- autoSkip:false,
535
- font:function(ctx){
536
- const defaultSize=11;
537
- return {size: labels.length>20?defaultSize-1:defaultSize};
 
 
 
 
 
 
 
 
 
 
 
538
  }
539
  }
540
  }
@@ -543,14 +1171,15 @@ if __name__ == '__main__':
543
  });
544
  }
545
 
546
- async function loadSpaces(page=0) {
547
  setLoading(true);
548
- try{
549
  const searchText = elements.searchInput.value;
550
  const offset = page * state.itemsPerPage;
551
 
552
- const timeoutPromise = new Promise((_,reject)=>
553
- setTimeout(()=>reject(new Error('Request timeout')),30000));
 
554
  const fetchPromise = fetch(
555
  `/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`
556
  );
@@ -564,279 +1193,407 @@ if __name__ == '__main__':
564
 
565
  renderGrid(state.spaces);
566
  renderPagination();
567
- if(state.statsVisible && state.topOwners.length>0){
 
568
  renderCreatorStats();
569
  }
570
- } catch(e) {
571
- console.error('Error loading spaces:', e);
572
- elements.gridContainer.innerHTML=`
573
- <div style="grid-column: 1/-1; text-align:center; padding:40px;">
574
- <div style="font-size:3rem; margin-bottom:20px;">โš ๏ธ</div>
575
- <h3 style="margin-bottom:10px;">Unable to load spaces</h3>
576
- <p style="color:#666;">Please try refreshing the page. If problem persists, try again later.</p>
577
- <button id="retryButton" style="margin-top:20px; padding:10px 20px; background: var(--pastel-purple); border:none; border-radius:5px; cursor:pointer;">
578
  Try Again
579
  </button>
580
- </div>`;
581
- document.getElementById('retryButton')?.addEventListener('click',()=>loadSpaces(0));
 
582
  renderPagination();
583
  } finally {
584
  setLoading(false);
585
  }
586
  }
587
 
588
- function renderPagination(){
589
- elements.pagination.innerHTML='';
590
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
591
 
 
592
  const prevButton = document.createElement('button');
593
  prevButton.className = 'pagination-button';
594
  prevButton.textContent = 'Previous';
595
- prevButton.disabled = (state.currentPage===0);
596
- prevButton.addEventListener('click', ()=>{
597
- if(state.currentPage>0) loadSpaces(state.currentPage-1);
 
 
598
  });
599
  elements.pagination.appendChild(prevButton);
600
 
601
- const maxButtons=7;
602
- let startPage = Math.max(0,state.currentPage-Math.floor(maxButtons/2));
603
- let endPage = Math.min(totalPages-1, startPage+maxButtons-1);
604
- if(endPage-startPage+1<maxButtons){
605
- startPage = Math.max(0,endPage-maxButtons+1);
 
606
  }
607
- for(let i=startPage;i<=endPage;i++){
 
608
  const pageButton = document.createElement('button');
609
- pageButton.className='pagination-button'+(i===state.currentPage?' active':'');
610
- pageButton.textContent=(i+1);
611
- pageButton.addEventListener('click',()=>{if(i!==state.currentPage)loadSpaces(i)});
 
 
 
 
612
  elements.pagination.appendChild(pageButton);
613
  }
614
 
 
615
  const nextButton = document.createElement('button');
616
- nextButton.className='pagination-button';
617
- nextButton.textContent='Next';
618
- nextButton.disabled=(state.currentPage>=totalPages-1);
619
- nextButton.addEventListener('click',()=>{
620
- if(state.currentPage<totalPages-1)loadSpaces(state.currentPage+1);
 
 
621
  });
622
  elements.pagination.appendChild(nextButton);
623
  }
624
 
625
- function handleIframeError(iframe, owner, name, title){
626
- const container=iframe.parentNode;
627
- const errorPlaceholder=document.createElement('div');
628
- errorPlaceholder.className='error-placeholder';
629
-
630
- const errorMessage=document.createElement('p');
631
- errorMessage.textContent=`"${title}" space couldn't be loaded`;
 
632
  errorPlaceholder.appendChild(errorMessage);
633
-
634
- const directLink=document.createElement('a');
635
- directLink.href=`https://huggingface.co/spaces/${owner}/${name}`;
636
- directLink.target='_blank';
637
- directLink.textContent='Visit HF Space';
638
- directLink.style.color='#3182ce';
639
- directLink.style.marginTop='10px';
640
- directLink.style.display='inline-block';
641
- directLink.style.padding='8px 16px';
642
- directLink.style.background='#ebf8ff';
643
- directLink.style.borderRadius='5px';
644
- directLink.style.fontWeight='600';
645
  errorPlaceholder.appendChild(directLink);
646
-
647
- iframe.style.display='none';
648
  container.appendChild(errorPlaceholder);
649
  }
650
 
651
- // โ–ผโ–ผโ–ผ ์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์นด๋“œ HTML ๊ตฌ์กฐ๋ฅผ Next.js ์˜ˆ์‹œ(์ฝ”๋“œ #2) ์ฐธ๊ณ ํ•˜์—ฌ ๋ณ€๊ฒฝ โ–ผโ–ผโ–ผ
652
- function renderGrid(spaces){
653
- elements.gridContainer.innerHTML='';
654
- if(!spaces||spaces.length===0){
655
- const noResultsMsg=document.createElement('p');
656
- noResultsMsg.textContent='No zero-gpu spaces found matching your search.';
657
- noResultsMsg.style.padding='2rem';
658
- noResultsMsg.style.textAlign='center';
659
- noResultsMsg.style.fontStyle='italic';
660
- noResultsMsg.style.color='#718096';
 
661
  elements.gridContainer.appendChild(noResultsMsg);
662
  return;
663
  }
664
 
665
- spaces.forEach((item)=>{
666
- try{
667
  const {
668
  url, title, likes_count, owner, name, rank,
669
  description, avatar_url, author_name, embedUrl
670
- }=item;
671
 
672
- const gridItem=document.createElement('div');
673
- gridItem.className='grid-item';
674
 
675
  // ์ƒ๋‹จ ํ—ค๋”
676
- const headerDiv=document.createElement('div');
677
- headerDiv.className='grid-header';
678
-
679
- // ์ข€ ๋” ์œ ์—ฐํ•œ layout์„ ์œ„ํ•ด ๋‚ด๋ถ€ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์„ฑ
680
  // space-header (์•„๋ฐ”ํƒ€ + ์ œ๋ชฉ + Zero GPU ๋ฐฐ์ง€)
681
- const spaceHeader=document.createElement('div');
682
- spaceHeader.className='space-header';
683
-
684
- // ์•„๋ฐ”ํƒ€ ์ด๋ฏธ์ง€
685
- const avatarImg=document.createElement('img');
686
- avatarImg.className='avatar-img';
687
- if(avatar_url) {
688
- avatarImg.src=avatar_url;
689
  } else {
690
- avatarImg.src='https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg';
691
  }
692
  spaceHeader.appendChild(avatarImg);
693
 
694
- // ์ œ๋ชฉ+๋ฐฐ์ง€ ๋ฌถ์Œ
695
- const titleWrapper=document.createElement('div');
696
- titleWrapper.style.display='flex';
697
- titleWrapper.style.alignItems='center';
698
 
699
- const titleEl=document.createElement('h3');
700
- titleEl.className='space-title';
701
- titleEl.textContent=title;
702
- titleEl.title=title;
703
  titleWrapper.appendChild(titleEl);
704
 
705
- // Zero-GPU ๋ฐฐ์ง€
706
- const zeroGpuBadge=document.createElement('span');
707
- zeroGpuBadge.className='zero-gpu-badge';
708
- zeroGpuBadge.textContent='ZERO GPU';
709
  titleWrapper.appendChild(zeroGpuBadge);
710
 
711
  spaceHeader.appendChild(titleWrapper);
712
  headerDiv.appendChild(spaceHeader);
713
 
714
- // ๋‘ ๋ฒˆ์งธ ์ค„ (owner + rank + likes)
715
- const metaInfo=document.createElement('div');
716
- metaInfo.className='grid-meta';
717
- metaInfo.style.display='flex';
718
- metaInfo.style.justifyContent='space-between';
719
- metaInfo.style.alignItems='center';
720
- metaInfo.style.marginTop='6px';
721
 
722
  // ์™ผ์ชฝ: rank + author
723
- const leftMeta=document.createElement('div');
724
- const rankBadge=document.createElement('div');
725
- rankBadge.className='rank-badge';
726
- rankBadge.textContent=`#${rank}`;
 
727
  leftMeta.appendChild(rankBadge);
728
 
729
- const authorSpan=document.createElement('span');
730
- authorSpan.className='author-name';
731
- authorSpan.style.marginLeft='8px';
732
- authorSpan.textContent=`by ${author_name}`;
733
  leftMeta.appendChild(authorSpan);
734
 
735
  metaInfo.appendChild(leftMeta);
736
 
737
  // ์˜ค๋ฅธ์ชฝ: likes
738
- const likesDiv=document.createElement('div');
739
- likesDiv.className='likes-wrapper';
740
- likesDiv.innerHTML=`<span class="likes-heart">โ™ฅ</span><span>${likes_count}</span>`;
741
  metaInfo.appendChild(likesDiv);
742
 
743
  headerDiv.appendChild(metaInfo);
744
  gridItem.appendChild(headerDiv);
745
 
746
- // description ํ‘œ์‹œ
747
- if(description) {
748
- const descP=document.createElement('p');
749
- descP.className='desc-text';
750
- descP.textContent=description;
751
  gridItem.appendChild(descP);
752
  }
753
 
754
- // iframe
755
- const content=document.createElement('div');
756
- content.className='grid-content';
757
 
758
- const iframeContainer=document.createElement('div');
759
- iframeContainer.className='iframe-container';
760
 
761
- const iframe=document.createElement('iframe');
762
- iframe.src=embedUrl;
763
- iframe.title=title;
764
- iframe.allow='accelerometer; camera; encrypted-media; geolocation; gyroscope;';
765
- iframe.setAttribute('allowfullscreen','');
766
- iframe.setAttribute('frameborder','0');
767
- iframe.loading='lazy';
768
 
769
- const spaceKey=`${owner}/${name}`;
770
- state.iframeStatuses[spaceKey]='loading';
771
 
772
- iframe.onload=function(){
773
  iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
774
  };
775
- iframe.onerror=function(){
776
  handleIframeError(iframe, owner, name, title);
777
- state.iframeStatuses[spaceKey]='error';
778
  };
779
- setTimeout(()=>{
780
- if(state.iframeStatuses[spaceKey]==='loading'){
781
  handleIframeError(iframe, owner, name, title);
782
- state.iframeStatuses[spaceKey]='error';
783
  }
784
- },30000);
785
 
786
  iframeContainer.appendChild(iframe);
787
  content.appendChild(iframeContainer);
788
 
789
- // actions (์—ด๊ธฐ๋ฒ„ํŠผ)
790
- const actions=document.createElement('div');
791
- actions.className='grid-actions';
792
- const linkEl=document.createElement('a');
793
- linkEl.href=url;
794
- linkEl.target='_blank';
795
- linkEl.className='open-link';
796
- linkEl.textContent='Open in new window';
797
- actions.appendChild(linkEl);
798
 
 
 
 
 
 
 
 
799
  gridItem.appendChild(content);
800
  gridItem.appendChild(actions);
801
 
802
  elements.gridContainer.appendChild(gridItem);
803
 
804
- }catch(e){
805
- console.error('Item rendering error:', e);
806
  }
807
  });
808
  }
809
- // โ–ฒโ–ฒโ–ฒ ์—ฌ๊ธฐ๊นŒ์ง€ ์นด๋“œ HTML ๊ตฌ์กฐ ๋ณ€๊ฒฝ ์™„๋ฃŒ โ–ฒโ–ฒโ–ฒ
810
-
811
- function renderFixedGrid(){
812
- // (๊ธฐ์กด fixedTab ์— ๋“ค์–ด๊ฐˆ ์นด๋“œ๋“ค ๊ณ ์ •: ์˜ˆ์‹œ ๊ทธ๋Œ€๋กœ)
813
- fixedGridContainer.innerHTML='';
814
- const staticSpaces=[ /* ... */ ];
815
 
816
- if(!staticSpaces||staticSpaces.length===0){
817
- const noResultsMsg=document.createElement('p');
818
- noResultsMsg.textContent='No spaces to display.';
819
- noResultsMsg.style.padding='2rem';
820
- noResultsMsg.style.textAlign='center';
821
- noResultsMsg.style.fontStyle='italic';
822
- noResultsMsg.style.color='#718096';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
823
  fixedGridContainer.appendChild(noResultsMsg);
824
  return;
825
  }
826
- // (์ƒ๋žต: ๊ธฐ์กด ์˜ˆ์‹œ ์ฝ”๋“œ ์œ ์ง€)
827
- staticSpaces.forEach((item)=>{
828
- /* ... */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
829
  });
830
  }
831
 
832
- tabTrendingButton.addEventListener('click', ()=>{
 
833
  tabTrendingButton.classList.add('active');
834
  tabFixedButton.classList.remove('active');
835
  trendingTab.classList.add('active');
836
  fixedTab.classList.remove('active');
837
  loadSpaces(state.currentPage);
838
  });
839
- tabFixedButton.addEventListener('click', ()=>{
840
  tabFixedButton.classList.add('active');
841
  tabTrendingButton.classList.remove('active');
842
  fixedTab.classList.add('active');
@@ -844,57 +1601,60 @@ if __name__ == '__main__':
844
  renderFixedGrid();
845
  });
846
 
847
- elements.searchInput.addEventListener('input', ()=>{
848
  clearTimeout(state.searchTimeout);
849
- state.searchTimeout=setTimeout(()=>loadSpaces(0),300);
850
  });
851
- elements.searchInput.addEventListener('keyup',(e)=>{
852
- if(e.key==='Enter')loadSpaces(0);
 
 
853
  });
854
- elements.refreshButton.addEventListener('click',()=>loadSpaces(0));
855
- elements.statsToggle.addEventListener('click',toggleStats);
856
 
857
- window.addEventListener('load', function(){
858
- setTimeout(()=>loadSpaces(0),500);
859
  });
860
- setTimeout(()=>{
861
- if(state.isLoading){
 
862
  setLoading(false);
863
- elements.gridContainer.innerHTML=`
864
- <div style="grid-column: 1/-1; text-align:center; padding:40px;">
865
- <div style="font-size:3rem; margin-bottom:20px;">โฑ๏ธ</div>
866
- <h3 style="margin-bottom:10px;">Loading is taking longer than expected</h3>
867
- <p style="color:#666;">Please try refreshing the page.</p>
868
- <button onClick="window.location.reload()" style="margin-top:20px; padding:10px 20px; background: var(--pastel-purple); border:none; border-radius:5px; cursor:pointer;">
869
  Reload Page
870
  </button>
871
- </div>`;
 
872
  }
873
- },20000);
874
 
875
  loadSpaces(0);
876
 
877
- function setLoading(isLoading){
878
- state.isLoading=isLoading;
879
- elements.loadingIndicator.style.display=isLoading?'flex':'none';
880
- if(isLoading){
 
881
  elements.refreshButton.classList.add('refreshing');
882
  clearTimeout(state.loadingTimeout);
883
- state.loadingTimeout=setTimeout(()=>{
884
- elements.loadingError.style.display='block';
885
- },10000);
886
- }else{
887
  elements.refreshButton.classList.remove('refreshing');
888
  clearTimeout(state.loadingTimeout);
889
- elements.loadingError.style.display='none';
890
  }
891
  }
892
-
893
  </script>
894
  </body>
895
  </html>
896
- """)
897
 
898
- # Flask run
899
- # ํฌํŠธ 7860 (Spaces์—์„œ default), ํ•„์š”์‹œ ๋ณ€๊ฒฝ
900
  app.run(host='0.0.0.0', port=7860)
 
7
 
8
  app = Flask(__name__)
9
 
10
+ # Generate dummy spaces in case of error
11
+ def generate_dummy_spaces(count):
12
+ """
13
+ API ํ˜ธ์ถœ ์‹คํŒจ ์‹œ ์˜ˆ์‹œ์šฉ ๋”๋ฏธ ์ŠคํŽ˜์ด์Šค ์ƒ์„ฑ
14
+ """
15
+ spaces = []
16
+ for i in range(count):
17
+ spaces.append({
18
+ 'id': f'dummy/space-{i}',
19
+ 'owner': 'dummy',
20
+ 'title': f'Example Space {i+1}',
21
+ 'description': 'Dummy space for fallback',
22
+ 'likes': 100 - i,
23
+ 'createdAt': '2023-01-01T00:00:00.000Z',
24
+ 'hardware': 'cpu',
25
+ 'user': {
26
+ 'avatar_url': 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg',
27
+ 'name': 'dummyUser'
28
+ }
29
+ })
30
+ return spaces
31
+
32
+ # Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
33
  def fetch_trending_spaces(offset=0, limit=72):
34
  """
35
+ ์›๋ณธ ์ฝ”๋“œ์—์„œ ์ˆ˜์ •: hardware=cpu ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ
36
+ GPU๊ฐ€ ์—†๋Š”(=CPU ์ „์šฉ) ์ŠคํŽ˜์ด์Šค๋งŒ ํ•„ํ„ฐ๋ง
37
  """
38
  try:
39
  url = "https://huggingface.co/api/spaces"
40
  params = {
41
+ "limit": 10000, # ๋” ๋งŽ์ด ๊ฐ€์ ธ์˜ค๊ธฐ
42
+ "hardware": "cpu" # <-- Zero GPU(=CPU) ํ•„ํ„ฐ ์ ์šฉ
43
+ # "sort": "trending", # ํ•„์š”์‹œ ์ถ”๊ฐ€ (HF API ์ง€์› ์—ฌ๋ถ€์— ๋”ฐ๋ผ)
44
  }
 
45
  response = requests.get(url, params=params, timeout=30)
46
+
47
  if response.status_code == 200:
48
  spaces = response.json()
49
 
50
+ # owner๋‚˜ id๊ฐ€ 'None'์ธ ๊ฒฝ์šฐ ์ œ์™ธ
51
  filtered_spaces = [
52
  space for space in spaces
53
  if space.get('owner') != 'None'
54
  and space.get('id', '').split('/', 1)[0] != 'None'
55
  ]
56
 
57
+ # Slice according to requested offset and limit
58
  start = min(offset, len(filtered_spaces))
59
  end = min(offset + limit, len(filtered_spaces))
60
+
61
  print(f"[fetch_trending_spaces] CPU๊ธฐ๋ฐ˜ ์ŠคํŽ˜์ด์Šค ์ด {len(filtered_spaces)}๊ฐœ, "
62
  f"์š”์ฒญ ๊ตฌ๊ฐ„ {start}~{end-1} ๋ฐ˜ํ™˜")
63
+
64
  return {
65
  'spaces': filtered_spaces[start:end],
66
  'total': len(filtered_spaces),
 
70
  }
71
  else:
72
  print(f"Error fetching spaces: {response.status_code}")
73
+ # ์‹คํŒจ ์‹œ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
74
  return {
75
  'spaces': generate_dummy_spaces(limit),
76
  'total': 200,
 
78
  'limit': limit,
79
  'all_spaces': generate_dummy_spaces(500)
80
  }
81
+
82
  except Exception as e:
83
  print(f"Exception when fetching spaces: {e}")
84
+ # ์‹คํŒจ ์‹œ ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
85
  return {
86
  'spaces': generate_dummy_spaces(limit),
87
  'total': 200,
 
90
  'all_spaces': generate_dummy_spaces(500)
91
  }
92
 
93
+ # Transform Huggingface URL to direct space URL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def transform_url(owner, name):
95
  """
96
+ Hugging Face Space -> ์„œ๋ธŒ๋„๋ฉ”์ธ ์ ‘๊ทผ URL
97
+ ์˜ˆ) huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space
98
  """
99
+ # 1. Replace '.' with '-'
100
+ name = name.replace('.', '-')
101
+ # 2. Replace '_' with '-'
102
+ name = name.replace('_', '-')
103
+ # 3. Convert to lowercase
104
  owner = owner.lower()
105
  name = name.lower()
106
+
107
  return f"https://{owner}-{name}.hf.space"
108
 
109
+ # Get space details
110
  def get_space_details(space_data, index, offset):
111
  """
112
+ ์›๋ณธ ์ฝ”๋“œ์—์„œ ์ˆ˜์ •:
113
+ - description, avatar_url, author_name ๋“ฑ ์ถ”๊ฐ€ ํ•„๋“œ๋ฅผ ์ถ”์ถœ
114
  """
115
  try:
116
  if '/' in space_data.get('id', ''):
 
119
  owner = space_data.get('owner', '')
120
  name = space_data.get('id', '')
121
 
122
+ # Ignore if contains None
123
  if owner == 'None' or name == 'None':
124
  return None
125
+
126
+ # Construct URLs
127
  original_url = f"https://huggingface.co/spaces/{owner}/{name}"
128
  embed_url = transform_url(owner, name)
129
 
130
+ # Likes count
131
+ likes_count = space_data.get('likes', 0)
132
+
133
+ # Title
134
  title = space_data.get('title') or name
 
135
 
136
+ # Description
137
+ short_desc = space_data.get('description', '')
138
 
139
+ # User info (avatar, name)
 
140
  user_info = space_data.get('user', {})
141
  avatar_url = user_info.get('avatar_url', '')
142
+ author_name = user_info.get('name') or owner
143
 
144
  return {
145
  'url': original_url,
 
155
  }
156
  except Exception as e:
157
  print(f"Error processing space data: {e}")
158
+ # Return basic object even if error occurs
159
  return {
160
  'url': 'https://huggingface.co/spaces',
161
  'embedUrl': 'https://huggingface.co/spaces',
 
169
  'rank': offset + index + 1
170
  }
171
 
172
+ # Get owner statistics from all spaces
173
  def get_owner_stats(all_spaces):
174
  """
175
+ ๋ชจ๋“  ์ŠคํŽ˜์ด์Šค์—์„œ owner ๋“ฑ์žฅ ๋นˆ๋„ ์ƒ์œ„ 30๋ช… ์ถ”์ถœ
176
  """
177
  owners = []
178
  for space in all_spaces:
 
180
  owner, _ = space.get('id', '').split('/', 1)
181
  else:
182
  owner = space.get('owner', '')
183
+
184
  if owner != 'None':
185
  owners.append(owner)
186
+
187
+ # Count occurrences of each owner
188
  owner_counts = Counter(owners)
189
+
190
+ # Get top 30 owners by count
191
  top_owners = owner_counts.most_common(30)
192
+
193
  return top_owners
194
 
195
+ # Homepage route
196
  @app.route('/')
197
  def home():
198
+ """
199
+ index.html ํ…œํ”Œ๋ฆฟ ๋ Œ๋”๋ง (๋ฉ”์ธ ํŽ˜์ด์ง€)
200
+ """
201
  return render_template('index.html')
202
 
203
+ # Zero-GPU spaces API (์›๋ณธ: 'trending-spaces' -> ์ด์ œ CPU only)
204
  @app.route('/api/trending-spaces', methods=['GET'])
205
  def trending_spaces():
206
  """
207
+ hardware=cpu ์ŠคํŽ˜์ด์Šค ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์™€ ๊ฒ€์ƒ‰, ํŽ˜์ด์ง•, ํ†ต๊ณ„ ๋“ฑ์„ ์ ์šฉ
 
208
  """
209
  search_query = request.args.get('search', '').lower()
210
  offset = int(request.args.get('offset', 0))
211
+ limit = int(request.args.get('limit', 72)) # Default 72
212
 
213
+ # Fetch zero-gpu (cpu) spaces
214
  spaces_data = fetch_trending_spaces(offset, limit)
215
 
216
+ # Process and filter spaces
217
  results = []
218
  for index, space_data in enumerate(spaces_data['spaces']):
219
  space_info = get_space_details(space_data, index, offset)
220
  if not space_info:
221
  continue
222
 
223
+ # Apply search filter if needed
224
  if search_query:
225
  if (search_query not in space_info['title'].lower()
226
  and search_query not in space_info['owner'].lower()
 
229
  continue
230
 
231
  results.append(space_info)
232
+
233
+ # Get owner statistics for all spaces
234
  top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
235
 
236
  return jsonify({
 
241
  'top_owners': top_owners
242
  })
243
 
 
 
 
 
244
  if __name__ == '__main__':
245
+ """
246
+ ์„œ๋ฒ„ ๊ตฌ๋™ ์‹œ, templates/index.html ํŒŒ์ผ์„ ์ƒ์„ฑ ํ›„ Flask ์‹คํ–‰
247
+ """
248
+ # Create templates folder if not exists
249
  os.makedirs('templates', exist_ok=True)
250
+
251
+ # index.html ์ „์ฒด๋ฅผ ์ƒˆ๋กœ ์ž‘์„ฑ
252
  with open('templates/index.html', 'w', encoding='utf-8') as f:
253
+ f.write('''<!DOCTYPE html>
254
  <html lang="en">
255
  <head>
256
  <meta charset="UTF-8">
257
  <title>Huggingface Zero-GPU Spaces</title>
258
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
259
  <style>
260
+ /* Google Fonts & Base Styling */
261
+ @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
262
+
263
+ :root {
264
+ --pastel-pink: #FFD6E0;
265
+ --pastel-blue: #C5E8FF;
266
+ --pastel-purple: #E0C3FC;
267
+ --pastel-yellow: #FFF2CC;
268
+ --pastel-green: #C7F5D9;
269
+ --pastel-orange: #FFE0C3;
270
+
271
+ --mac-window-bg: rgba(250, 250, 250, 0.85);
272
+ --mac-toolbar: #F5F5F7;
273
+ --mac-border: #E2E2E2;
274
+ --mac-button-red: #FF5F56;
275
+ --mac-button-yellow: #FFBD2E;
276
+ --mac-button-green: #27C93F;
277
+
278
+ --text-primary: #333;
279
+ --text-secondary: #666;
280
+ --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
281
+ }
282
+
283
+ * {
284
+ margin: 0;
285
+ padding: 0;
286
+ box-sizing: border-box;
287
+ }
288
+
289
+ body {
290
+ font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
291
+ line-height: 1.6;
292
+ color: var(--text-primary);
293
+ background-color: #f8f9fa;
294
+ background-image: linear-gradient(135deg, var(--pastel-blue) 0%, var(--pastel-purple) 100%);
295
+ min-height: 100vh;
296
+ padding: 2rem;
297
+ }
298
+
299
+ .container {
300
+ max-width: 1600px;
301
+ margin: 0 auto;
302
+ }
303
+
304
+ /* Mac OS Window Styling */
305
+ .mac-window {
306
+ background-color: var(--mac-window-bg);
307
+ border-radius: 10px;
308
+ box-shadow: var(--box-shadow);
309
+ backdrop-filter: blur(10px);
310
+ overflow: hidden;
311
+ margin-bottom: 2rem;
312
+ border: 1px solid var(--mac-border);
313
+ }
314
+
315
+ .mac-toolbar {
316
+ display: flex;
317
+ align-items: center;
318
+ padding: 10px 15px;
319
+ background-color: var(--mac-toolbar);
320
+ border-bottom: 1px solid var(--mac-border);
321
+ }
322
+
323
+ .mac-buttons {
324
+ display: flex;
325
+ gap: 8px;
326
+ margin-right: 15px;
327
+ }
328
+
329
+ .mac-button {
330
+ width: 12px;
331
+ height: 12px;
332
+ border-radius: 50%;
333
+ cursor: default;
334
+ }
335
+
336
+ .mac-close {
337
+ background-color: var(--mac-button-red);
338
+ }
339
+
340
+ .mac-minimize {
341
+ background-color: var(--mac-button-yellow);
342
+ }
343
+
344
+ .mac-maximize {
345
+ background-color: var(--mac-button-green);
346
+ }
347
+
348
+ .mac-title {
349
+ flex-grow: 1;
350
+ text-align: center;
351
+ font-size: 0.9rem;
352
+ color: var(--text-secondary);
353
+ }
354
+
355
+ .mac-content {
356
+ padding: 20px;
357
+ }
358
+
359
+ /* Header Styling */
360
+ .header {
361
+ text-align: center;
362
+ margin-bottom: 1.5rem;
363
+ position: relative;
364
+ }
365
+
366
+ .header h1 {
367
+ font-size: 2.2rem;
368
+ font-weight: 700;
369
+ margin: 0;
370
+ color: #2d3748;
371
+ letter-spacing: -0.5px;
372
+ }
373
+
374
+ .header p {
375
+ color: var(--text-secondary);
376
+ margin-top: 0.5rem;
377
+ font-size: 1.1rem;
378
+ }
379
+
380
+ /* Tabs Styling */
381
+ .tab-nav {
382
+ display: flex;
383
+ justify-content: center;
384
+ margin-bottom: 1.5rem;
385
+ }
386
+
387
+ .tab-button {
388
+ border: none;
389
+ background-color: #edf2f7;
390
+ color: var(--text-primary);
391
+ padding: 10px 20px;
392
+ margin: 0 5px;
393
+ cursor: pointer;
394
+ border-radius: 5px;
395
+ font-size: 1rem;
396
+ font-weight: 600;
397
+ }
398
+
399
+ .tab-button.active {
400
+ background-color: var(--pastel-purple);
401
+ color: #fff;
402
+ }
403
+
404
+ .tab-content {
405
+ display: none;
406
+ }
407
+
408
+ .tab-content.active {
409
+ display: block;
410
+ }
411
+
412
+ /* Controls Styling */
413
+ .search-bar {
414
+ display: flex;
415
+ align-items: center;
416
+ margin-bottom: 1.5rem;
417
+ background-color: white;
418
+ border-radius: 30px;
419
+ padding: 5px;
420
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
421
+ max-width: 600px;
422
+ margin-left: auto;
423
+ margin-right: auto;
424
+ }
425
+
426
+ .search-bar input {
427
+ flex-grow: 1;
428
+ border: none;
429
+ padding: 12px 20px;
430
+ font-size: 1rem;
431
+ outline: none;
432
+ background: transparent;
433
+ border-radius: 30px;
434
+ }
435
+
436
+ .search-bar .refresh-btn {
437
+ background-color: var(--pastel-green);
438
+ color: #1a202c;
439
+ border: none;
440
+ border-radius: 30px;
441
+ padding: 10px 20px;
442
+ font-size: 1rem;
443
+ font-weight: 600;
444
+ cursor: pointer;
445
+ transition: all 0.2s;
446
+ display: flex;
447
+ align-items: center;
448
+ gap: 8px;
449
+ }
450
+
451
+ .search-bar .refresh-btn:hover {
452
+ background-color: #9ee7c0;
453
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
454
+ }
455
+
456
+ .refresh-icon {
457
+ display: inline-block;
458
+ width: 16px;
459
+ height: 16px;
460
+ border: 2px solid #1a202c;
461
+ border-top-color: transparent;
462
+ border-radius: 50%;
463
+ animation: none;
464
+ }
465
+
466
+ .refreshing .refresh-icon {
467
+ animation: spin 1s linear infinite;
468
+ }
469
+
470
+ @keyframes spin {
471
+ 0% { transform: rotate(0deg); }
472
+ 100% { transform: rotate(360deg); }
473
+ }
474
+
475
+ /* Grid Styling */
476
+ .grid-container {
477
+ display: grid;
478
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
479
+ gap: 1.5rem;
480
+ margin-bottom: 2rem;
481
+ }
482
+
483
+ .grid-item {
484
+ height: 500px;
485
+ position: relative;
486
+ overflow: hidden;
487
+ transition: all 0.3s ease;
488
+ border-radius: 15px;
489
+ }
490
+
491
+ .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); }
492
+ .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); }
493
+ .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); }
494
+ .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); }
495
+ .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); }
496
+ .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); }
497
+
498
+ .grid-item:hover {
499
+ transform: translateY(-5px);
500
+ box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
501
+ }
502
+
503
+ .grid-header {
504
+ padding: 15px;
505
+ display: flex;
506
+ flex-direction: column;
507
+ background-color: rgba(255, 255, 255, 0.7);
508
+ backdrop-filter: blur(5px);
509
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
510
+ }
511
+
512
+ .grid-header-top {
513
+ display: flex;
514
+ justify-content: space-between;
515
+ align-items: center;
516
+ margin-bottom: 8px;
517
+ }
518
+
519
+ .rank-badge {
520
+ background-color: #1a202c;
521
+ color: white;
522
+ font-size: 0.8rem;
523
+ font-weight: 600;
524
+ padding: 4px 8px;
525
+ border-radius: 50px;
526
+ display: inline-block;
527
+ }
528
+
529
+ .grid-header h3 {
530
+ margin: 0;
531
+ font-size: 1.2rem;
532
+ font-weight: 700;
533
+ white-space: nowrap;
534
+ overflow: hidden;
535
+ text-overflow: ellipsis;
536
+ }
537
+
538
+ .grid-meta {
539
+ display: flex;
540
+ justify-content: space-between;
541
+ align-items: center;
542
+ font-size: 0.9rem;
543
+ }
544
+
545
+ .owner-info {
546
+ color: var(--text-secondary);
547
+ font-weight: 500;
548
+ }
549
+
550
+ .likes-counter {
551
+ display: flex;
552
+ align-items: center;
553
+ color: #e53e3e;
554
+ font-weight: 600;
555
+ }
556
+
557
+ .likes-counter span {
558
+ margin-left: 4px;
559
+ }
560
+
561
+ .grid-actions {
562
+ padding: 10px 15px;
563
+ text-align: right;
564
+ background-color: rgba(255, 255, 255, 0.7);
565
+ backdrop-filter: blur(5px);
566
+ position: absolute;
567
+ bottom: 0;
568
+ left: 0;
569
+ right: 0;
570
+ z-index: 10;
571
+ display: flex;
572
+ justify-content: flex-end;
573
+ }
574
+
575
+ .open-link {
576
+ text-decoration: none;
577
+ color: #2c5282;
578
+ font-weight: 600;
579
+ padding: 5px 10px;
580
+ border-radius: 5px;
581
+ transition: all 0.2s;
582
+ background-color: rgba(237, 242, 247, 0.8);
583
+ }
584
+
585
+ .open-link:hover {
586
+ background-color: #e2e8f0;
587
+ }
588
+
589
+ .grid-content {
590
+ position: absolute;
591
+ top: 0;
592
+ left: 0;
593
+ width: 100%;
594
+ height: 100%;
595
+ padding-top: 85px; /* Header height */
596
+ padding-bottom: 45px; /* Actions height */
597
+ }
598
+
599
+ .iframe-container {
600
+ width: 100%;
601
+ height: 100%;
602
+ overflow: hidden;
603
+ position: relative;
604
+ }
605
+
606
+ /* Apply 70% scaling to iframes */
607
+ .grid-content iframe {
608
+ transform: scale(0.7);
609
+ transform-origin: top left;
610
+ width: 142.857%;
611
+ height: 142.857%;
612
+ border: none;
613
+ border-radius: 0;
614
+ }
615
+
616
+ .error-placeholder {
617
+ position: absolute;
618
+ top: 0;
619
+ left: 0;
620
+ width: 100%;
621
+ height: 100%;
622
+ display: flex;
623
+ flex-direction: column;
624
+ justify-content: center;
625
+ align-items: center;
626
+ padding: 20px;
627
+ background-color: rgba(255, 255, 255, 0.9);
628
+ text-align: center;
629
+ }
630
+
631
+ .error-emoji {
632
+ font-size: 6rem;
633
+ margin-bottom: 1.5rem;
634
+ animation: bounce 1s infinite alternate;
635
+ text-shadow: 0 10px 20px rgba(0,0,0,0.1);
636
+ }
637
+
638
+ @keyframes bounce {
639
+ from {
640
+ transform: translateY(0px) scale(1);
641
+ }
642
+ to {
643
+ transform: translateY(-15px) scale(1.1);
644
+ }
645
+ }
646
+
647
+ /* Pagination Styling */
648
+ .pagination {
649
+ display: flex;
650
+ justify-content: center;
651
+ align-items: center;
652
+ gap: 10px;
653
+ margin: 2rem 0;
654
+ }
655
+
656
+ .pagination-button {
657
+ background-color: white;
658
+ border: none;
659
+ padding: 10px 20px;
660
+ border-radius: 10px;
661
+ font-size: 1rem;
662
+ font-weight: 600;
663
+ cursor: pointer;
664
+ transition: all 0.2s;
665
+ color: var(--text-primary);
666
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
667
+ }
668
+
669
+ .pagination-button:hover {
670
+ background-color: #f8f9fa;
671
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
672
+ }
673
+
674
+ .pagination-button.active {
675
+ background-color: var(--pastel-purple);
676
+ color: #4a5568;
677
+ }
678
+
679
+ .pagination-button:disabled {
680
+ background-color: #edf2f7;
681
+ color: #a0aec0;
682
+ cursor: default;
683
+ box-shadow: none;
684
+ }
685
+
686
+ /* Loading Indicator */
687
+ .loading {
688
+ position: fixed;
689
+ top: 0;
690
+ left: 0;
691
+ right: 0;
692
+ bottom: 0;
693
+ background-color: rgba(255, 255, 255, 0.8);
694
+ backdrop-filter: blur(5px);
695
+ display: flex;
696
+ justify-content: center;
697
+ align-items: center;
698
+ z-index: 1000;
699
+ }
700
+
701
+ .loading-content {
702
+ text-align: center;
703
+ }
704
+
705
+ .loading-spinner {
706
+ width: 60px;
707
+ height: 60px;
708
+ border: 5px solid #e2e8f0;
709
+ border-top-color: var(--pastel-purple);
710
+ border-radius: 50%;
711
+ animation: spin 1s linear infinite;
712
+ margin: 0 auto 15px;
713
+ }
714
+
715
+ .loading-text {
716
+ font-size: 1.2rem;
717
+ font-weight: 600;
718
+ color: #4a5568;
719
+ }
720
+
721
+ .loading-error {
722
+ display: none;
723
+ margin-top: 10px;
724
+ color: #e53e3e;
725
+ font-size: 0.9rem;
726
+ }
727
+
728
+ /* Stats window styling */
729
+ .stats-window {
730
+ margin-top: 2rem;
731
+ margin-bottom: 2rem;
732
+ }
733
+
734
+ .stats-header {
735
+ display: flex;
736
+ justify-content: space-between;
737
+ align-items: center;
738
+ margin-bottom: 1rem;
739
+ }
740
+
741
+ .stats-title {
742
+ font-size: 1.5rem;
743
+ font-weight: 700;
744
+ color: #2d3748;
745
+ }
746
+
747
+ .stats-toggle {
748
+ background-color: var(--pastel-blue);
749
+ border: none;
750
+ padding: 8px 16px;
751
+ border-radius: 20px;
752
+ font-weight: 600;
753
+ cursor: pointer;
754
+ transition: all 0.2s;
755
+ }
756
+
757
+ .stats-toggle:hover {
758
+ background-color: var(--pastel-purple);
759
+ }
760
+
761
+ .stats-content {
762
+ background-color: white;
763
+ border-radius: 10px;
764
+ padding: 20px;
765
+ box-shadow: var(--box-shadow);
766
+ max-height: 0;
767
+ overflow: hidden;
768
+ transition: max-height 0.5s ease-out;
769
+ }
770
+
771
+ .stats-content.open {
772
+ max-height: 600px;
773
+ }
774
+
775
+ .chart-container {
776
+ width: 100%;
777
+ height: 500px;
778
+ }
779
+
780
+ /* Responsive Design */
781
+ @media (max-width: 768px) {
782
+ body {
783
+ padding: 1rem;
784
+ }
785
+
786
+ .grid-container {
787
+ grid-template-columns: 1fr;
788
+ }
789
+
790
+ .search-bar {
791
+ flex-direction: column;
792
+ padding: 10px;
793
+ }
794
+
795
+ .search-bar input {
796
+ width: 100%;
797
+ margin-bottom: 10px;
798
+ }
799
+
800
+ .search-bar .refresh-btn {
801
+ width: 100%;
802
+ justify-content: center;
803
+ }
804
+
805
+ .pagination {
806
+ flex-wrap: wrap;
807
+ }
808
+
809
+ .chart-container {
810
+ height: 300px;
811
+ }
812
+ }
813
+
814
+ .error-emoji-detector {
815
+ position: fixed;
816
+ top: -9999px;
817
+ left: -9999px;
818
+ z-index: -1;
819
+ opacity: 0;
820
+ }
821
+
822
+ /* ์ถ”๊ฐ€ ๋ ˆ์ด์•„์›ƒ ์ˆ˜์ •(์•„๋ฐ”ํƒ€, ZERO GPU ๋ฑƒ์ง€ ๋“ฑ)์„ ์œ„ํ•ด
823
+ ์•„๋ž˜ ํด๋ž˜์Šค๋“ค์„ ์ผ๋ถ€ ์ถ”๊ฐ€/์ˆ˜์ •ํ•ด๋„ ์ข‹์ง€๋งŒ
824
+ ์˜ˆ์‹œ๋Š” ์œ„ ์˜์—ญ์—์„œ ์ด๋ฏธ ์ถฉ๋ถ„ํžˆ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์ƒ๋žต */
825
 
826
+ /* ๋‹ค์Œ ๋ถ€๋ถ„์€ Zero GPU Spaces์šฉ ์นด๋“œ ๊ตฌ์กฐ์—์„œ ํ™œ์šฉ */
827
  .space-header {
828
  display: flex;
829
  align-items: center;
 
895
  </div>
896
  <div class="mac-title">Huggingface Explorer</div>
897
  </div>
898
+
899
  <div class="mac-content">
900
  <div class="header">
901
+ <!-- ์ฒซ ๋ฒˆ์งธ ํƒญ ์ œ๋ชฉ์„ Zero GPU Spaces๋กœ ๋ณ€๊ฒฝ -->
902
  <h1>Zero GPU Spaces</h1>
903
  <p>Discover CPU-based (No GPU) spaces from Hugging Face</p>
904
  </div>
905
 
906
+ <!-- Tab Navigation -->
907
  <div class="tab-nav">
908
  <button id="tabTrendingButton" class="tab-button active">Zero GPU Spaces</button>
909
  <button id="tabFixedButton" class="tab-button">Fixed Tab</button>
910
  </div>
911
+
912
+ <!-- Trending(Zero GPU) Tab Content -->
913
  <div id="trendingTab" class="tab-content active">
914
  <div class="stats-window mac-window">
915
  <div class="mac-toolbar">
 
932
  </div>
933
  </div>
934
  </div>
935
+
936
  <div class="search-bar">
937
  <input type="text" id="searchInput" placeholder="Search by name, owner, or description..." />
938
  <button id="refreshButton" class="refresh-btn">
 
940
  Refresh
941
  </button>
942
  </div>
943
+
944
  <div id="gridContainer" class="grid-container"></div>
945
+
946
  <div id="pagination" class="pagination"></div>
947
  </div>
948
+
949
+ <!-- Fixed Tab Content (๊ธฐ์กด ์˜ˆ์‹œ ์œ ์ง€) -->
950
  <div id="fixedTab" class="tab-content">
951
  <div id="fixedGrid" class="grid-container"></div>
952
  </div>
 
965
  </div>
966
 
967
  <script>
968
+ // DOM Elements
969
  const elements = {
970
  gridContainer: document.getElementById('gridContainer'),
971
  loadingIndicator: document.getElementById('loadingIndicator'),
 
977
  statsContent: document.getElementById('statsContent'),
978
  creatorStatsChart: document.getElementById('creatorStatsChart')
979
  };
980
+
981
  const tabTrendingButton = document.getElementById('tabTrendingButton');
982
  const tabFixedButton = document.getElementById('tabFixedButton');
983
  const trendingTab = document.getElementById('trendingTab');
 
995
  statsVisible: false,
996
  chartInstance: null,
997
  topOwners: [],
998
+ iframeStatuses: {}
999
  };
1000
 
1001
+ // Advanced iframe loader for better error detection
1002
  const iframeLoader = {
1003
  checkQueue: {},
1004
  maxAttempts: 5,
1005
  checkInterval: 5000,
1006
+
1007
  startChecking: function(iframe, owner, name, title, spaceKey) {
1008
  this.checkQueue[spaceKey] = {
1009
+ iframe: iframe,
1010
+ owner: owner,
1011
+ name: name,
1012
+ title: title,
1013
+ attempts: 0,
1014
+ status: 'loading'
1015
  };
1016
  this.checkIframeStatus(spaceKey);
1017
  },
1018
+
1019
  checkIframeStatus: function(spaceKey) {
1020
+ if (!this.checkQueue[spaceKey]) return;
1021
+
1022
  const item = this.checkQueue[spaceKey];
1023
+ if (item.status !== 'loading') {
 
1024
  delete this.checkQueue[spaceKey];
1025
  return;
1026
  }
1027
  item.attempts++;
1028
+
1029
  try {
1030
+ if (!item.iframe || !item.iframe.parentNode) {
1031
  delete this.checkQueue[spaceKey];
1032
  return;
1033
  }
1034
+
1035
+ // Check if content loaded
1036
  try {
1037
+ const hasContent = item.iframe.contentWindow &&
1038
+ item.iframe.contentWindow.document &&
1039
+ item.iframe.contentWindow.document.body;
1040
+ if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) {
1041
  const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase();
1042
+ if (bodyText.includes('forbidden') || bodyText.includes('404') ||
1043
+ bodyText.includes('not found') || bodyText.includes('error')) {
1044
  item.status = 'error';
1045
  handleIframeError(item.iframe, item.owner, item.name, item.title);
1046
  } else {
 
1049
  delete this.checkQueue[spaceKey];
1050
  return;
1051
  }
1052
+ } catch(e) {
1053
+ // Cross-origin issues can happen; not always an error
1054
+ }
1055
 
1056
+ // Check if iframe is visible
1057
  const rect = item.iframe.getBoundingClientRect();
1058
+ if (rect.width > 50 && rect.height > 50 && item.attempts > 2) {
1059
  item.status = 'success';
1060
  delete this.checkQueue[spaceKey];
1061
  return;
1062
  }
1063
 
1064
+ // If max attempts reached
1065
+ if (item.attempts >= this.maxAttempts) {
1066
+ if (item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) {
1067
  item.status = 'success';
1068
  } else {
1069
  item.status = 'error';
 
1072
  delete this.checkQueue[spaceKey];
1073
  return;
1074
  }
1075
+
1076
+ // Re-check after some delay
1077
  const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1);
1078
  setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay);
1079
 
1080
+ } catch (e) {
1081
  console.error('Error checking iframe status:', e);
1082
+ if (item.attempts >= this.maxAttempts) {
1083
  item.status = 'error';
1084
  handleIframeError(item.iframe, item.owner, item.name, item.title);
1085
  delete this.checkQueue[spaceKey];
 
1094
  state.statsVisible = !state.statsVisible;
1095
  elements.statsContent.classList.toggle('open', state.statsVisible);
1096
  elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats';
1097
+
1098
+ if (state.statsVisible && state.topOwners.length > 0) {
1099
  renderCreatorStats();
1100
  }
1101
  }
1102
+
1103
  function renderCreatorStats() {
1104
+ if (state.chartInstance) {
1105
  state.chartInstance.destroy();
1106
  }
1107
  const ctx = elements.creatorStatsChart.getContext('2d');
1108
  const labels = state.topOwners.map(item => item[0]);
1109
  const data = state.topOwners.map(item => item[1]);
1110
+
1111
  const colors = [];
1112
+ for (let i = 0; i < labels.length; i++) {
1113
  const hue = (i * 360 / labels.length) % 360;
1114
  colors.push(`hsla(${hue}, 70%, 80%, 0.7)`);
1115
  }
1116
+
1117
  state.chartInstance = new Chart(ctx, {
1118
  type: 'bar',
1119
  data: {
 
1122
  label: 'Number of Spaces',
1123
  data: data,
1124
  backgroundColor: colors,
1125
+ borderColor: colors.map(color => color.replace('0.7', '1')),
1126
+ borderWidth: 1
1127
  }]
1128
  },
1129
  options: {
1130
  indexAxis: 'y',
1131
  responsive: true,
1132
+ maintainAspectRatio: false,
1133
+ plugins: {
1134
+ legend: { display: false },
1135
+ tooltip: {
1136
+ callbacks: {
1137
+ title: function(tooltipItems) {
1138
+ return tooltipItems[0].label;
1139
+ },
1140
+ label: function(context) {
1141
+ return `Spaces: ${context.raw}`;
1142
+ }
1143
  }
1144
  }
1145
  },
1146
+ scales: {
1147
+ x: {
1148
+ beginAtZero: true,
1149
+ title: {
1150
+ display: true,
1151
+ text: 'Number of Spaces'
1152
+ }
1153
+ },
1154
+ y: {
1155
+ title: {
1156
+ display: true,
1157
+ text: 'Creator ID'
1158
+ },
1159
+ ticks: {
1160
+ autoSkip: false,
1161
+ font: function(context) {
1162
+ const defaultSize = 11;
1163
+ return {
1164
+ size: labels.length > 20 ? defaultSize - 1 : defaultSize
1165
+ };
1166
  }
1167
  }
1168
  }
 
1171
  });
1172
  }
1173
 
1174
+ async function loadSpaces(page = 0) {
1175
  setLoading(true);
1176
+ try {
1177
  const searchText = elements.searchInput.value;
1178
  const offset = page * state.itemsPerPage;
1179
 
1180
+ const timeoutPromise = new Promise((_, reject) =>
1181
+ setTimeout(() => reject(new Error('Request timeout')), 30000)
1182
+ );
1183
  const fetchPromise = fetch(
1184
  `/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`
1185
  );
 
1193
 
1194
  renderGrid(state.spaces);
1195
  renderPagination();
1196
+
1197
+ if (state.statsVisible && state.topOwners.length > 0) {
1198
  renderCreatorStats();
1199
  }
1200
+ } catch (error) {
1201
+ console.error('Error loading spaces:', error);
1202
+ elements.gridContainer.innerHTML = `
1203
+ <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1204
+ <div style="font-size: 3rem; margin-bottom: 20px;">โš ๏ธ</div>
1205
+ <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
1206
+ <p style="color: #666;">Please try refreshing the page. If the problem persists, try again later.</p>
1207
+ <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1208
  Try Again
1209
  </button>
1210
+ </div>
1211
+ `;
1212
+ document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
1213
  renderPagination();
1214
  } finally {
1215
  setLoading(false);
1216
  }
1217
  }
1218
 
1219
+ function renderPagination() {
1220
+ elements.pagination.innerHTML = '';
1221
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
1222
 
1223
+ // Previous page
1224
  const prevButton = document.createElement('button');
1225
  prevButton.className = 'pagination-button';
1226
  prevButton.textContent = 'Previous';
1227
+ prevButton.disabled = (state.currentPage === 0);
1228
+ prevButton.addEventListener('click', () => {
1229
+ if (state.currentPage > 0) {
1230
+ loadSpaces(state.currentPage - 1);
1231
+ }
1232
  });
1233
  elements.pagination.appendChild(prevButton);
1234
 
1235
+ const maxButtons = 7;
1236
+ let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2));
1237
+ let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1);
1238
+
1239
+ if (endPage - startPage + 1 < maxButtons) {
1240
+ startPage = Math.max(0, endPage - maxButtons + 1);
1241
  }
1242
+
1243
+ for (let i = startPage; i <= endPage; i++) {
1244
  const pageButton = document.createElement('button');
1245
+ pageButton.className = 'pagination-button' + (i === state.currentPage ? ' active' : '');
1246
+ pageButton.textContent = (i + 1);
1247
+ pageButton.addEventListener('click', () => {
1248
+ if (i !== state.currentPage) {
1249
+ loadSpaces(i);
1250
+ }
1251
+ });
1252
  elements.pagination.appendChild(pageButton);
1253
  }
1254
 
1255
+ // Next page
1256
  const nextButton = document.createElement('button');
1257
+ nextButton.className = 'pagination-button';
1258
+ nextButton.textContent = 'Next';
1259
+ nextButton.disabled = (state.currentPage >= totalPages - 1);
1260
+ nextButton.addEventListener('click', () => {
1261
+ if (state.currentPage < totalPages - 1) {
1262
+ loadSpaces(state.currentPage + 1);
1263
+ }
1264
  });
1265
  elements.pagination.appendChild(nextButton);
1266
  }
1267
 
1268
+ function handleIframeError(iframe, owner, name, title) {
1269
+ const container = iframe.parentNode;
1270
+
1271
+ const errorPlaceholder = document.createElement('div');
1272
+ errorPlaceholder.className = 'error-placeholder';
1273
+
1274
+ const errorMessage = document.createElement('p');
1275
+ errorMessage.textContent = `"${title}" space couldn't be loaded`;
1276
  errorPlaceholder.appendChild(errorMessage);
1277
+
1278
+ const directLink = document.createElement('a');
1279
+ directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
1280
+ directLink.target = '_blank';
1281
+ directLink.textContent = 'Visit HF Space';
1282
+ directLink.style.color = '#3182ce';
1283
+ directLink.style.marginTop = '10px';
1284
+ directLink.style.display = 'inline-block';
1285
+ directLink.style.padding = '8px 16px';
1286
+ directLink.style.background = '#ebf8ff';
1287
+ directLink.style.borderRadius = '5px';
1288
+ directLink.style.fontWeight = '600';
1289
  errorPlaceholder.appendChild(directLink);
1290
+
1291
+ iframe.style.display = 'none';
1292
  container.appendChild(errorPlaceholder);
1293
  }
1294
 
1295
+ // ์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ์นด๋“œ HTML ๊ตฌ์กฐ (Zero-GPU ์ „์šฉ) ๋ Œ๋”๋ง
1296
+ function renderGrid(spaces) {
1297
+ elements.gridContainer.innerHTML = '';
1298
+
1299
+ if (!spaces || spaces.length === 0) {
1300
+ const noResultsMsg = document.createElement('p');
1301
+ noResultsMsg.textContent = 'No zero-gpu spaces found matching your search.';
1302
+ noResultsMsg.style.padding = '2rem';
1303
+ noResultsMsg.style.textAlign = 'center';
1304
+ noResultsMsg.style.fontStyle = 'italic';
1305
+ noResultsMsg.style.color = '#718096';
1306
  elements.gridContainer.appendChild(noResultsMsg);
1307
  return;
1308
  }
1309
 
1310
+ spaces.forEach((item) => {
1311
+ try {
1312
  const {
1313
  url, title, likes_count, owner, name, rank,
1314
  description, avatar_url, author_name, embedUrl
1315
+ } = item;
1316
 
1317
+ const gridItem = document.createElement('div');
1318
+ gridItem.className = 'grid-item';
1319
 
1320
  // ์ƒ๋‹จ ํ—ค๋”
1321
+ const headerDiv = document.createElement('div');
1322
+ headerDiv.className = 'grid-header';
1323
+
 
1324
  // space-header (์•„๋ฐ”ํƒ€ + ์ œ๋ชฉ + Zero GPU ๋ฐฐ์ง€)
1325
+ const spaceHeader = document.createElement('div');
1326
+ spaceHeader.className = 'space-header';
1327
+
1328
+ // ์•„๋ฐ”ํƒ€
1329
+ const avatarImg = document.createElement('img');
1330
+ avatarImg.className = 'avatar-img';
1331
+ if (avatar_url) {
1332
+ avatarImg.src = avatar_url;
1333
  } else {
1334
+ avatarImg.src = 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg';
1335
  }
1336
  spaceHeader.appendChild(avatarImg);
1337
 
1338
+ // ์ œ๋ชฉ+๋ฐฐ์ง€
1339
+ const titleWrapper = document.createElement('div');
1340
+ titleWrapper.style.display = 'flex';
1341
+ titleWrapper.style.alignItems = 'center';
1342
 
1343
+ const titleEl = document.createElement('h3');
1344
+ titleEl.className = 'space-title';
1345
+ titleEl.textContent = title;
1346
+ titleEl.title = title;
1347
  titleWrapper.appendChild(titleEl);
1348
 
1349
+ const zeroGpuBadge = document.createElement('span');
1350
+ zeroGpuBadge.className = 'zero-gpu-badge';
1351
+ zeroGpuBadge.textContent = 'ZERO GPU';
 
1352
  titleWrapper.appendChild(zeroGpuBadge);
1353
 
1354
  spaceHeader.appendChild(titleWrapper);
1355
  headerDiv.appendChild(spaceHeader);
1356
 
1357
+ // rank + author + likes
1358
+ const metaInfo = document.createElement('div');
1359
+ metaInfo.className = 'grid-meta';
1360
+ metaInfo.style.display = 'flex';
1361
+ metaInfo.style.justifyContent = 'space-between';
1362
+ metaInfo.style.alignItems = 'center';
1363
+ metaInfo.style.marginTop = '6px';
1364
 
1365
  // ์™ผ์ชฝ: rank + author
1366
+ const leftMeta = document.createElement('div');
1367
+
1368
+ const rankBadge = document.createElement('div');
1369
+ rankBadge.className = 'rank-badge';
1370
+ rankBadge.textContent = `#${rank}`;
1371
  leftMeta.appendChild(rankBadge);
1372
 
1373
+ const authorSpan = document.createElement('span');
1374
+ authorSpan.className = 'author-name';
1375
+ authorSpan.style.marginLeft = '8px';
1376
+ authorSpan.textContent = `by ${author_name}`;
1377
  leftMeta.appendChild(authorSpan);
1378
 
1379
  metaInfo.appendChild(leftMeta);
1380
 
1381
  // ์˜ค๋ฅธ์ชฝ: likes
1382
+ const likesDiv = document.createElement('div');
1383
+ likesDiv.className = 'likes-wrapper';
1384
+ likesDiv.innerHTML = `<span class="likes-heart">โ™ฅ</span><span>${likes_count}</span>`;
1385
  metaInfo.appendChild(likesDiv);
1386
 
1387
  headerDiv.appendChild(metaInfo);
1388
  gridItem.appendChild(headerDiv);
1389
 
1390
+ // description
1391
+ if (description) {
1392
+ const descP = document.createElement('p');
1393
+ descP.className = 'desc-text';
1394
+ descP.textContent = description;
1395
  gridItem.appendChild(descP);
1396
  }
1397
 
1398
+ // iframe container
1399
+ const content = document.createElement('div');
1400
+ content.className = 'grid-content';
1401
 
1402
+ const iframeContainer = document.createElement('div');
1403
+ iframeContainer.className = 'iframe-container';
1404
 
1405
+ const iframe = document.createElement('iframe');
1406
+ iframe.src = embedUrl; // transformed direct URL
1407
+ iframe.title = title;
1408
+ iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1409
+ iframe.setAttribute('allowfullscreen', '');
1410
+ iframe.setAttribute('frameborder', '0');
1411
+ iframe.loading = 'lazy';
1412
 
1413
+ const spaceKey = `${owner}/${name}`;
1414
+ state.iframeStatuses[spaceKey] = 'loading';
1415
 
1416
+ iframe.onload = function() {
1417
  iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1418
  };
1419
+ iframe.onerror = function() {
1420
  handleIframeError(iframe, owner, name, title);
1421
+ state.iframeStatuses[spaceKey] = 'error';
1422
  };
1423
+ setTimeout(() => {
1424
+ if (state.iframeStatuses[spaceKey] === 'loading') {
1425
  handleIframeError(iframe, owner, name, title);
1426
+ state.iframeStatuses[spaceKey] = 'error';
1427
  }
1428
+ }, 30000);
1429
 
1430
  iframeContainer.appendChild(iframe);
1431
  content.appendChild(iframeContainer);
1432
 
1433
+ // actions
1434
+ const actions = document.createElement('div');
1435
+ actions.className = 'grid-actions';
 
 
 
 
 
 
1436
 
1437
+ const linkEl = document.createElement('a');
1438
+ linkEl.href = url;
1439
+ linkEl.target = '_blank';
1440
+ linkEl.className = 'open-link';
1441
+ linkEl.textContent = 'Open in new window';
1442
+
1443
+ actions.appendChild(linkEl);
1444
  gridItem.appendChild(content);
1445
  gridItem.appendChild(actions);
1446
 
1447
  elements.gridContainer.appendChild(gridItem);
1448
 
1449
+ } catch (err) {
1450
+ console.error('Item rendering error:', err);
1451
  }
1452
  });
1453
  }
 
 
 
 
 
 
1454
 
1455
+ function renderFixedGrid() {
1456
+ // Fixed Tab ์˜ˆ์‹œ์šฉ (์›๋ณธ ์ฝ”๋“œ ์˜ˆ์‹œ ์œ ์ง€)
1457
+ fixedGridContainer.innerHTML = '';
1458
+
1459
+ const staticSpaces = [
1460
+ {
1461
+ url: "https://huggingface.co/spaces/VIDraft/SanaSprint",
1462
+ title: "SanaSprint",
1463
+ likes_count: 0,
1464
+ owner: "VIDraft",
1465
+ name: "SanaSprint",
1466
+ rank: 1
1467
+ },
1468
+ {
1469
+ url: "https://huggingface.co/spaces/VIDraft/SanaSprint",
1470
+ title: "SanaSprint",
1471
+ likes_count: 0,
1472
+ owner: "VIDraft",
1473
+ name: "SanaSprint",
1474
+ rank: 2
1475
+ },
1476
+ {
1477
+ url: "https://huggingface.co/spaces/VIDraft/SanaSprint",
1478
+ title: "SanaSprint",
1479
+ likes_count: 0,
1480
+ owner: "VIDraft",
1481
+ name: "SanaSprint",
1482
+ rank: 3
1483
+ }
1484
+ ];
1485
+
1486
+ if (!staticSpaces || staticSpaces.length === 0) {
1487
+ const noResultsMsg = document.createElement('p');
1488
+ noResultsMsg.textContent = 'No spaces to display.';
1489
+ noResultsMsg.style.padding = '2rem';
1490
+ noResultsMsg.style.textAlign = 'center';
1491
+ noResultsMsg.style.fontStyle = 'italic';
1492
+ noResultsMsg.style.color = '#718096';
1493
  fixedGridContainer.appendChild(noResultsMsg);
1494
  return;
1495
  }
1496
+
1497
+ staticSpaces.forEach((item) => {
1498
+ try {
1499
+ const { url, title, likes_count, owner, name, rank } = item;
1500
+ const gridItem = document.createElement('div');
1501
+ gridItem.className = 'grid-item';
1502
+
1503
+ const header = document.createElement('div');
1504
+ header.className = 'grid-header';
1505
+
1506
+ const headerTop = document.createElement('div');
1507
+ headerTop.className = 'grid-header-top';
1508
+
1509
+ const titleEl = document.createElement('h3');
1510
+ titleEl.textContent = title;
1511
+ titleEl.title = title;
1512
+ headerTop.appendChild(titleEl);
1513
+
1514
+ const rankBadge = document.createElement('div');
1515
+ rankBadge.className = 'rank-badge';
1516
+ rankBadge.textContent = `#${rank}`;
1517
+ headerTop.appendChild(rankBadge);
1518
+
1519
+ header.appendChild(headerTop);
1520
+
1521
+ const metaInfo = document.createElement('div');
1522
+ metaInfo.className = 'grid-meta';
1523
+
1524
+ const ownerEl = document.createElement('div');
1525
+ ownerEl.className = 'owner-info';
1526
+ ownerEl.textContent = `by ${owner}`;
1527
+ metaInfo.appendChild(ownerEl);
1528
+
1529
+ const likesCounter = document.createElement('div');
1530
+ likesCounter.className = 'likes-counter';
1531
+ likesCounter.innerHTML = 'โ™ฅ <span>' + likes_count + '</span>';
1532
+ metaInfo.appendChild(likesCounter);
1533
+
1534
+ header.appendChild(metaInfo);
1535
+ gridItem.appendChild(header);
1536
+
1537
+ const content = document.createElement('div');
1538
+ content.className = 'grid-content';
1539
+
1540
+ const iframeContainer = document.createElement('div');
1541
+ iframeContainer.className = 'iframe-container';
1542
+
1543
+ const iframe = document.createElement('iframe');
1544
+ iframe.src = "https://" + owner.toLowerCase() + "-" + name.toLowerCase() + ".hf.space";
1545
+ iframe.title = title;
1546
+ iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
1547
+ iframe.setAttribute('allowfullscreen', '');
1548
+ iframe.setAttribute('frameborder', '0');
1549
+ iframe.loading = 'lazy';
1550
+
1551
+ const spaceKey = `${owner}/${name}`;
1552
+ iframe.onload = function() {
1553
+ iframeLoader.startChecking(iframe, owner, name, title, spaceKey);
1554
+ };
1555
+ iframe.onerror = function() {
1556
+ handleIframeError(iframe, owner, name, title);
1557
+ };
1558
+ setTimeout(() => {
1559
+ if (iframe.offsetWidth === 0 || iframe.offsetHeight === 0) {
1560
+ handleIframeError(iframe, owner, name, title);
1561
+ }
1562
+ }, 30000);
1563
+
1564
+ iframeContainer.appendChild(iframe);
1565
+ content.appendChild(iframeContainer);
1566
+
1567
+ const actions = document.createElement('div');
1568
+ actions.className = 'grid-actions';
1569
+
1570
+ const linkEl = document.createElement('a');
1571
+ linkEl.href = url;
1572
+ linkEl.target = '_blank';
1573
+ linkEl.className = 'open-link';
1574
+ linkEl.textContent = 'Open in new window';
1575
+ actions.appendChild(linkEl);
1576
+
1577
+ gridItem.appendChild(content);
1578
+ gridItem.appendChild(actions);
1579
+
1580
+ fixedGridContainer.appendChild(gridItem);
1581
+
1582
+ } catch (error) {
1583
+ console.error('Fixed tab rendering error:', error);
1584
+ }
1585
  });
1586
  }
1587
 
1588
+ // Tab switching
1589
+ tabTrendingButton.addEventListener('click', () => {
1590
  tabTrendingButton.classList.add('active');
1591
  tabFixedButton.classList.remove('active');
1592
  trendingTab.classList.add('active');
1593
  fixedTab.classList.remove('active');
1594
  loadSpaces(state.currentPage);
1595
  });
1596
+ tabFixedButton.addEventListener('click', () => {
1597
  tabFixedButton.classList.add('active');
1598
  tabTrendingButton.classList.remove('active');
1599
  fixedTab.classList.add('active');
 
1601
  renderFixedGrid();
1602
  });
1603
 
1604
+ elements.searchInput.addEventListener('input', () => {
1605
  clearTimeout(state.searchTimeout);
1606
+ state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
1607
  });
1608
+ elements.searchInput.addEventListener('keyup', (event) => {
1609
+ if (event.key === 'Enter') {
1610
+ loadSpaces(0);
1611
+ }
1612
  });
1613
+ elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1614
+ elements.statsToggle.addEventListener('click', toggleStats);
1615
 
1616
+ window.addEventListener('load', function() {
1617
+ setTimeout(() => loadSpaces(0), 500);
1618
  });
1619
+
1620
+ setTimeout(() => {
1621
+ if (state.isLoading) {
1622
  setLoading(false);
1623
+ elements.gridContainer.innerHTML = `
1624
+ <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
1625
+ <div style="font-size: 3rem; margin-bottom: 20px;">โฑ๏ธ</div>
1626
+ <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3>
1627
+ <p style="color: #666;">Please try refreshing the page.</p>
1628
+ <button onClick="window.location.reload()" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;">
1629
  Reload Page
1630
  </button>
1631
+ </div>
1632
+ `;
1633
  }
1634
+ }, 20000);
1635
 
1636
  loadSpaces(0);
1637
 
1638
+ function setLoading(isLoading) {
1639
+ state.isLoading = isLoading;
1640
+ elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
1641
+
1642
+ if (isLoading) {
1643
  elements.refreshButton.classList.add('refreshing');
1644
  clearTimeout(state.loadingTimeout);
1645
+ state.loadingTimeout = setTimeout(() => {
1646
+ elements.loadingError.style.display = 'block';
1647
+ }, 10000);
1648
+ } else {
1649
  elements.refreshButton.classList.remove('refreshing');
1650
  clearTimeout(state.loadingTimeout);
1651
+ elements.loadingError.style.display = 'none';
1652
  }
1653
  }
 
1654
  </script>
1655
  </body>
1656
  </html>
1657
+ ''')
1658
 
1659
+ # Flask ์•ฑ ์‹คํ–‰ (ํฌํŠธ 7860)
 
1660
  app.run(host='0.0.0.0', port=7860)