openfree commited on
Commit
da35051
ยท
verified ยท
1 Parent(s): 88806b1

Update app.py

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