openfree commited on
Commit
7962292
ยท
verified ยท
1 Parent(s): 32c75f5

Delete app.py

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