openfree commited on
Commit
aab38e1
ยท
verified ยท
1 Parent(s): b1f7531

Delete app-backup.py

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