Spaces:
Sleeping
Sleeping
| from flask import Flask, render_template, request, jsonify | |
| import requests | |
| import os | |
| from collections import Counter | |
| from datetime import datetime | |
| import dateutil.parser # ํ์ฌ ์ฝ๋์์ dateutil.parser๋ ์ฐ์ง ์์ง๋ง, ๋ค๋ฅธ ๋ถ๋ถ์ ํ์ํ ์๋ ์์ด ๋จ๊น | |
| ############################################################################## | |
| # 1) ์ ์ญ ๋ณ์ & ๋๋ฏธ ๋ฐ์ดํฐ | |
| ############################################################################## | |
| # ์ ์ญ ์บ์: CPU ์ ์ฉ ์คํ์ด์ค ๋ชฉ๋ก | |
| SPACE_CACHE = [] | |
| # ๋ก๋ ์ฌ๋ถ ํ๋๊ทธ (์ต์ด ์์ฒญ ์ ํ ๋ฒ๋ง ๋ก๋) | |
| CACHE_LOADED = False | |
| def generate_dummy_spaces(count): | |
| """ | |
| API ํธ์ถ ์คํจ ์ ์์์ฉ ๋๋ฏธ ์คํ์ด์ค ์์ฑ | |
| """ | |
| spaces = [] | |
| for i in range(count): | |
| spaces.append({ | |
| 'id': f'dummy/space-{i}', | |
| 'owner': 'dummy', | |
| 'title': f'Dummy Space {i+1}', | |
| 'description': 'This is a fallback dummy space.', | |
| 'likes': 100 - i, | |
| 'createdAt': '2023-01-01T00:00:00.000Z', # ์์ | |
| 'hardware': 'cpu', | |
| 'user': { | |
| 'avatar_url': 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg', | |
| 'name': 'dummyUser' | |
| } | |
| }) | |
| return spaces | |
| ############################################################################## | |
| # 2) Hugging Face API์์ CPU ์คํ์ด์ค๋ฅผ ํ ๋ฒ๋ง ๊ฐ์ ธ์ค๋ ๋ก์ง | |
| ############################################################################## | |
| def fetch_zero_gpu_spaces_once(): | |
| """ | |
| Hugging Face API (hardware=cpu) ์คํ์ด์ค ๋ชฉ๋ก์ ๊ฐ์ ธ์ด | |
| """ | |
| try: | |
| url = "https://huggingface.co/api/spaces" | |
| params = { | |
| "limit": 1000, | |
| "hardware": "cpu" | |
| } | |
| resp = requests.get(url, params=params, timeout=30) | |
| if resp.status_code == 200: | |
| raw_spaces = resp.json() | |
| # owner๋ id๊ฐ 'None'์ธ ๊ฒฝ์ฐ ์ ์ธ | |
| filtered = [ | |
| sp for sp in raw_spaces | |
| if sp.get('owner') != 'None' | |
| and sp.get('id', '').split('/', 1)[0] != 'None' | |
| ] | |
| # global_rank ๋ถ์ฌ | |
| for i, sp in enumerate(filtered): | |
| sp['global_rank'] = i + 1 | |
| print(f"[fetch_zero_gpu_spaces_once] ๋ก๋๋ ์คํ์ด์ค: {len(filtered)}๊ฐ") | |
| return filtered | |
| else: | |
| print(f"[fetch_zero_gpu_spaces_once] API ์๋ฌ: {resp.status_code}") | |
| except Exception as e: | |
| print(f"[fetch_zero_gpu_spaces_once] ์์ธ ๋ฐ์: {e}") | |
| # ์คํจ ์ ๋๋ฏธ ๋ฐ์ดํฐ ๋ฐํ | |
| print("[fetch_zero_gpu_spaces_once] ์คํจ โ ๋๋ฏธ ๋ฐ์ดํฐ ์ฌ์ฉ") | |
| return generate_dummy_spaces(100) | |
| def ensure_cache_loaded(): | |
| """ | |
| Lazy Loading: | |
| - ์ต์ด ์์ฒญ์ด ๋ค์ด์์ ๋๋ง ์บ์๋ฅผ ๋ก๋ | |
| - ์ด๋ฏธ ๋ก๋๋์๋ค๋ฉด ์๋ฌด ๊ฒ๋ ํ์ง ์์ | |
| """ | |
| global CACHE_LOADED, SPACE_CACHE | |
| if not CACHE_LOADED: | |
| SPACE_CACHE = fetch_zero_gpu_spaces_once() | |
| CACHE_LOADED = True | |
| print(f"[ensure_cache_loaded] Loaded {len(SPACE_CACHE)} CPU-based spaces into cache.") | |
| ############################################################################## | |
| # 3) Flask ์ฑ ์์ฑ & ์ ํธ ํจ์ | |
| ############################################################################## | |
| app = Flask(__name__) | |
| def transform_url(owner, name): | |
| """ | |
| huggingface.co/spaces/owner/spaceName -> owner-spacename.hf.space ๋ณํ | |
| """ | |
| owner = owner.lower() | |
| # '.'์ '_'๋ฅผ '-'๋ก ์นํ | |
| name = name.replace('.', '-').replace('_', '-').lower() | |
| return f"https://{owner}-{name}.hf.space" | |
| def get_space_details(space_data, index, offset): | |
| """ | |
| ํน์ ์คํ์ด์ค ์ ๋ณด๋ฅผ Python dict๋ก ์ ๋ฆฌ | |
| - rank: (offset + index + 1) | |
| """ | |
| try: | |
| space_id = space_data.get('id', '') | |
| if '/' in space_id: | |
| owner, name = space_id.split('/', 1) | |
| else: | |
| owner = space_data.get('owner', '') | |
| name = space_id | |
| if owner == 'None' or name == 'None': | |
| return None | |
| original_url = f"https://huggingface.co/spaces/{owner}/{name}" | |
| embed_url = transform_url(owner, name) | |
| likes_count = space_data.get('likes', 0) | |
| title = space_data.get('title') or name | |
| short_desc = space_data.get('description', '') | |
| user_info = space_data.get('user', {}) | |
| avatar_url = user_info.get('avatar_url', '') | |
| author_name = user_info.get('name') or owner | |
| return { | |
| 'url': original_url, | |
| 'embedUrl': embed_url, | |
| 'title': title, | |
| 'owner': owner, | |
| 'name': name, | |
| 'likes_count': likes_count, | |
| 'description': short_desc, | |
| 'avatar_url': avatar_url, | |
| 'author_name': author_name, | |
| 'rank': offset + index + 1 | |
| } | |
| except Exception as e: | |
| print(f"[get_space_details] ์์ธ: {e}") | |
| return None | |
| def get_owner_stats(all_spaces): | |
| """ | |
| ์์ 500(global_rank<=500)์ ์ํ๋ ์คํ์ด์ค์ owner ๋น๋์ ์์ 30๋ช | |
| """ | |
| top_500 = [s for s in all_spaces if s.get('global_rank', 999999) <= 500] | |
| owners = [] | |
| for sp in top_500: | |
| sp_id = sp.get('id', '') | |
| if '/' in sp_id: | |
| o, _ = sp_id.split('/', 1) | |
| else: | |
| o = sp.get('owner', '') | |
| if o and o != 'None': | |
| owners.append(o) | |
| counts = Counter(owners) | |
| return counts.most_common(30) | |
| def fetch_trending_spaces(offset=0, limit=24): | |
| """ | |
| ์ด๋ฏธ ์บ์๋ SPACE_CACHE๋ฅผ offset, limit๋ก ์ฌ๋ผ์ด์ฑ | |
| """ | |
| global SPACE_CACHE | |
| total = len(SPACE_CACHE) | |
| start = min(offset, total) | |
| end = min(offset + limit, total) | |
| sliced = SPACE_CACHE[start:end] | |
| return { | |
| 'spaces': sliced, | |
| 'total': total, | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'all_spaces': SPACE_CACHE | |
| } | |
| ############################################################################## | |
| # 4) Flask ๋ผ์ฐํธ | |
| ############################################################################## | |
| def home(): | |
| """ | |
| ๋ฉ์ธ ํ์ด์ง(index.html) ๋ ๋๋ง | |
| """ | |
| return render_template('index.html') | |
| def trending_spaces(): | |
| """ | |
| Zero-GPU ์คํ์ด์ค ๋ชฉ๋ก (ํธ๋ ๋ฉ) | |
| - Lazy Load๋ก ์บ์ ๋ก๋ | |
| - offset, limit ํ๋ผ๋ฏธํฐ๋ก ํ์ด์ง๋ค์ด์ | |
| - search ํ๋ผ๋ฏธํฐ๋ก ๊ฒ์ | |
| - ์์ 500 ๋ด owner ํต๊ณ | |
| """ | |
| ensure_cache_loaded() | |
| search_query = request.args.get('search', '').lower() | |
| offset = int(request.args.get('offset', 0)) | |
| limit = int(request.args.get('limit', 24)) | |
| data = fetch_trending_spaces(offset, limit) | |
| results = [] | |
| for idx, sp in enumerate(data['spaces']): | |
| info = get_space_details(sp, idx, offset) | |
| if not info: | |
| continue | |
| # ๊ฒ์์ด ํํฐ ์ ์ฉ (title, owner, url, description) | |
| if search_query: | |
| text_block = " ".join([ | |
| info['title'].lower(), | |
| info['owner'].lower(), | |
| info['url'].lower(), | |
| info['description'].lower() | |
| ]) | |
| if search_query not in text_block: | |
| continue | |
| results.append(info) | |
| top_owners = get_owner_stats(data['all_spaces']) | |
| return jsonify({ | |
| 'spaces': results, | |
| 'total': data['total'], | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'top_owners': top_owners | |
| }) | |
| ############################################################################## | |
| # 5) ์๋ฒ ์คํ (templates/index.html) | |
| ############################################################################## | |
| if __name__ == '__main__': | |
| os.makedirs('templates', exist_ok=True) | |
| with open('templates/index.html', 'w', encoding='utf-8') as f: | |
| f.write('''<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Huggingface Zero-GPU Spaces</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| /* ==================== (CSS ๊ทธ๋๋ก ์ ์ง) ==================== */ | |
| @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap'); | |
| :root { | |
| --pastel-pink: #FFD6E0; | |
| --pastel-blue: #C5E8FF; | |
| --pastel-purple: #E0C3FC; | |
| --pastel-yellow: #FFF2CC; | |
| --pastel-green: #C7F5D9; | |
| --pastel-orange: #FFE0C3; | |
| --mac-window-bg: rgba(250, 250, 250, 0.85); | |
| --mac-toolbar: #F5F5F7; | |
| --mac-border: #E2E2E2; | |
| --mac-button-red: #FF5F56; | |
| --mac-button-yellow: #FFBD2E; | |
| --mac-button-green: #27C93F; | |
| --text-primary: #333; | |
| --text-secondary: #666; | |
| --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Nunito', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; | |
| line-height: 1.6; | |
| color: var(--text-primary); | |
| background-color: #f8f9fa; | |
| background-image: linear-gradient(135deg, var(--pastel-blue) 0%, var(--pastel-purple) 100%); | |
| min-height: 100vh; | |
| padding: 2rem; | |
| } | |
| .container { | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| } | |
| .mac-window { | |
| background-color: var(--mac-window-bg); | |
| border-radius: 10px; | |
| box-shadow: var(--box-shadow); | |
| backdrop-filter: blur(10px); | |
| overflow: hidden; | |
| margin-bottom: 2rem; | |
| border: 1px solid var(--mac-border); | |
| } | |
| .mac-toolbar { | |
| display: flex; | |
| align-items: center; | |
| padding: 10px 15px; | |
| background-color: var(--mac-toolbar); | |
| border-bottom: 1px solid var(--mac-border); | |
| } | |
| .mac-buttons { | |
| display: flex; | |
| gap: 8px; | |
| margin-right: 15px; | |
| } | |
| .mac-button { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| cursor: default; | |
| } | |
| .mac-close { | |
| background-color: var(--mac-button-red); | |
| } | |
| .mac-minimize { | |
| background-color: var(--mac-button-yellow); | |
| } | |
| .mac-maximize { | |
| background-color: var(--mac-button-green); | |
| } | |
| .mac-title { | |
| flex-grow: 1; | |
| text-align: center; | |
| font-size: 0.9rem; | |
| color: var(--text-secondary); | |
| } | |
| .mac-content { | |
| padding: 20px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 1.5rem; | |
| position: relative; | |
| } | |
| .header h1 { | |
| font-size: 2.2rem; | |
| font-weight: 700; | |
| margin: 0; | |
| color: #2d3748; | |
| letter-spacing: -0.5px; | |
| } | |
| .header p { | |
| color: var(--text-secondary); | |
| margin-top: 0.5rem; | |
| font-size: 1.1rem; | |
| } | |
| .tab-nav { | |
| display: flex; | |
| justify-content: center; | |
| margin-bottom: 1.5rem; | |
| } | |
| .tab-button { | |
| border: none; | |
| background-color: #edf2f7; | |
| color: var(--text-primary); | |
| padding: 10px 20px; | |
| margin: 0 5px; | |
| cursor: pointer; | |
| border-radius: 5px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| } | |
| .tab-button.active { | |
| background-color: var(--pastel-purple); | |
| color: #fff; | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| .search-bar { | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 1.5rem; | |
| background-color: white; | |
| border-radius: 30px; | |
| padding: 5px; | |
| box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); | |
| max-width: 600px; | |
| margin-left: auto; | |
| margin-right: auto; | |
| } | |
| .search-bar input { | |
| flex-grow: 1; | |
| border: none; | |
| padding: 12px 20px; | |
| font-size: 1rem; | |
| outline: none; | |
| background: transparent; | |
| border-radius: 30px; | |
| } | |
| .search-bar .refresh-btn { | |
| background-color: var(--pastel-green); | |
| color: #1a202c; | |
| border: none; | |
| border-radius: 30px; | |
| padding: 10px 20px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .search-bar .refresh-btn:hover { | |
| background-color: #9ee7c0; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
| } | |
| .refresh-icon { | |
| display: inline-block; | |
| width: 16px; | |
| height: 16px; | |
| border: 2px solid #1a202c; | |
| border-top-color: transparent; | |
| border-radius: 50%; | |
| animation: none; | |
| } | |
| .refreshing .refresh-icon { | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .grid-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); | |
| gap: 1.5rem; | |
| margin-bottom: 2rem; | |
| } | |
| .grid-item { | |
| height: 500px; | |
| position: relative; | |
| overflow: hidden; | |
| transition: all 0.3s ease; | |
| border-radius: 15px; | |
| } | |
| .grid-item:nth-child(6n+1) { background-color: var(--pastel-pink); } | |
| .grid-item:nth-child(6n+2) { background-color: var(--pastel-blue); } | |
| .grid-item:nth-child(6n+3) { background-color: var(--pastel-purple); } | |
| .grid-item:nth-child(6n+4) { background-color: var(--pastel-yellow); } | |
| .grid-item:nth-child(6n+5) { background-color: var(--pastel-green); } | |
| .grid-item:nth-child(6n+6) { background-color: var(--pastel-orange); } | |
| .grid-item:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15); | |
| } | |
| .grid-header { | |
| padding: 15px; | |
| display: flex; | |
| flex-direction: column; | |
| background-color: rgba(255, 255, 255, 0.7); | |
| backdrop-filter: blur(5px); | |
| border-bottom: 1px solid rgba(0, 0, 0, 0.05); | |
| } | |
| .grid-header-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .rank-badge { | |
| background-color: #1a202c; | |
| color: white; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| padding: 4px 8px; | |
| border-radius: 50px; | |
| display: inline-block; | |
| } | |
| .grid-header h3 { | |
| margin: 0; | |
| font-size: 1.2rem; | |
| font-weight: 700; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .grid-meta { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: 0.9rem; | |
| } | |
| .owner-info { | |
| color: var(--text-secondary); | |
| font-weight: 500; | |
| } | |
| .likes-counter { | |
| display: flex; | |
| align-items: center; | |
| color: #e53e3e; | |
| font-weight: 600; | |
| } | |
| .likes-counter span { | |
| margin-left: 4px; | |
| } | |
| .grid-actions { | |
| padding: 10px 15px; | |
| text-align: right; | |
| background-color: rgba(255, 255, 255, 0.7); | |
| backdrop-filter: blur(5px); | |
| position: absolute; | |
| bottom: 0; | |
| left: 0; | |
| right: 0; | |
| z-index: 10; | |
| display: flex; | |
| justify-content: flex-end; | |
| } | |
| .open-link { | |
| text-decoration: none; | |
| color: #2c5282; | |
| font-weight: 600; | |
| padding: 5px 10px; | |
| border-radius: 5px; | |
| transition: all 0.2s; | |
| background-color: rgba(237, 242, 247, 0.8); | |
| } | |
| .open-link:hover { | |
| background-color: #e2e8f0; | |
| } | |
| .grid-content { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| padding-top: 85px; /* Header height */ | |
| padding-bottom: 45px; /* Actions height */ | |
| } | |
| .iframe-container { | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| /* Apply 70% scaling to iframes */ | |
| .grid-content iframe { | |
| transform: scale(0.7); | |
| transform-origin: top left; | |
| width: 142.857%; | |
| height: 142.857%; | |
| border: none; | |
| border-radius: 0; | |
| } | |
| .error-placeholder { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| background-color: rgba(255, 255, 255, 0.9); | |
| text-align: center; | |
| } | |
| .error-emoji { | |
| font-size: 6rem; | |
| margin-bottom: 1.5rem; | |
| animation: bounce 1s infinite alternate; | |
| text-shadow: 0 10px 20px rgba(0,0,0,0.1); | |
| } | |
| @keyframes bounce { | |
| from { | |
| transform: translateY(0px) scale(1); | |
| } | |
| to { | |
| transform: translateY(-15px) scale(1.1); | |
| } | |
| } | |
| /* Pagination Styling */ | |
| .pagination { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 10px; | |
| margin: 2rem 0; | |
| } | |
| .pagination-button { | |
| background-color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 10px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| color: var(--text-primary); | |
| box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); | |
| } | |
| .pagination-button:hover { | |
| background-color: #f8f9fa; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); | |
| } | |
| .pagination-button.active { | |
| background-color: var(--pastel-purple); | |
| color: #4a5568; | |
| } | |
| .pagination-button:disabled { | |
| background-color: #edf2f7; | |
| color: #a0aec0; | |
| cursor: default; | |
| box-shadow: none; | |
| } | |
| /* Loading Indicator */ | |
| .loading { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(255, 255, 255, 0.8); | |
| backdrop-filter: blur(5px); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| } | |
| .loading-content { | |
| text-align: center; | |
| } | |
| .loading-spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 5px solid #e2e8f0; | |
| border-top-color: var(--pastel-purple); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto 15px; | |
| } | |
| .loading-text { | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| color: #4a5568; | |
| } | |
| .loading-error { | |
| display: none; | |
| margin-top: 10px; | |
| color: #e53e3e; | |
| font-size: 0.9rem; | |
| } | |
| /* Stats window styling */ | |
| .stats-window { | |
| margin-top: 2rem; | |
| margin-bottom: 2rem; | |
| } | |
| .stats-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| } | |
| .stats-title { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: #2d3748; | |
| } | |
| .stats-toggle { | |
| background-color: var(--pastel-blue); | |
| border: none; | |
| padding: 8px 16px; | |
| border-radius: 20px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .stats-toggle:hover { | |
| background-color: var(--pastel-purple); | |
| } | |
| .stats-content { | |
| background-color: white; | |
| border-radius: 10px; | |
| padding: 20px; | |
| box-shadow: var(--box-shadow); | |
| max-height: 0; | |
| overflow: hidden; | |
| transition: max-height 0.5s ease-out; | |
| } | |
| .stats-content.open { | |
| max-height: 600px; | |
| } | |
| .chart-container { | |
| width: 100%; | |
| height: 500px; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| body { | |
| padding: 1rem; | |
| } | |
| .grid-container { | |
| grid-template-columns: 1fr; | |
| } | |
| .search-bar { | |
| flex-direction: column; | |
| padding: 10px; | |
| } | |
| .search-bar input { | |
| width: 100%; | |
| margin-bottom: 10px; | |
| } | |
| .search-bar .refresh-btn { | |
| width: 100%; | |
| justify-content: center; | |
| } | |
| .pagination { | |
| flex-wrap: wrap; | |
| } | |
| .chart-container { | |
| height: 300px; | |
| } | |
| } | |
| .error-emoji-detector { | |
| position: fixed; | |
| top: -9999px; | |
| left: -9999px; | |
| z-index: -1; | |
| opacity: 0; | |
| } | |
| .space-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-bottom: 4px; | |
| } | |
| .avatar-img { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| object-fit: cover; | |
| border: 1px solid #ccc; | |
| } | |
| .space-title { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| margin: 0; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| max-width: 200px; | |
| } | |
| .zero-gpu-badge { | |
| font-size: 0.7rem; | |
| background-color: #e6fffa; | |
| color: #319795; | |
| border: 1px solid #81e6d9; | |
| border-radius: 6px; | |
| padding: 2px 6px; | |
| font-weight: 600; | |
| margin-left: 8px; | |
| } | |
| .desc-text { | |
| font-size: 0.85rem; | |
| color: #444; | |
| margin: 4px 0; | |
| line-clamp: 2; | |
| display: -webkit-box; | |
| -webkit-box-orient: vertical; | |
| overflow: hidden; | |
| } | |
| .author-name { | |
| font-size: 0.8rem; | |
| color: #666; | |
| } | |
| .likes-wrapper { | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| color: #e53e3e; | |
| font-weight: bold; | |
| font-size: 0.85rem; | |
| } | |
| .likes-heart { | |
| font-size: 1rem; | |
| line-height: 1rem; | |
| color: #f56565; | |
| } | |
| .emoji-avatar { | |
| font-size: 1.2rem; | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 50%; | |
| border: 1px solid #ccc; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="mac-window"> | |
| <div class="mac-toolbar"> | |
| <div class="mac-buttons"> | |
| <div class="mac-button mac-close"></div> | |
| <div class="mac-button mac-minimize"></div> | |
| <div class="mac-button mac-maximize"></div> | |
| </div> | |
| <div class="mac-title">Huggingface Explorer</div> | |
| </div> | |
| <div class="mac-content"> | |
| <div class="header"> | |
| <h1>ZeroGPU Spaces Leaderboard</h1> | |
| <p>Discover Zero GPU(Shared A100) spaces from Hugging Face</p> | |
| </div> | |
| <!-- ํญ ๋ค๋น๊ฒ์ด์ : (1) Trending, (2) Picks) --> | |
| <div class="tab-nav"> | |
| <button id="tabTrendingButton" class="tab-button active">Trending</button> | |
| <button id="tabFixedButton" class="tab-button">Picks</button> | |
| </div> | |
| <!-- Trending Tab Content --> | |
| <div id="trendingTab" class="tab-content active"> | |
| <div class="stats-window mac-window"> | |
| <div class="mac-toolbar"> | |
| <div class="mac-buttons"> | |
| <div class="mac-button mac-close"></div> | |
| <div class="mac-button mac-minimize"></div> | |
| <div class="mac-button mac-maximize"></div> | |
| </div> | |
| <div class="mac-title">Creator Statistics</div> | |
| </div> | |
| <div class="mac-content"> | |
| <div class="stats-header"> | |
| <div class="stats-title">Top 30 Creators by Number of Spaces Ranked within Top 500</div> | |
| <button id="statsToggle" class="stats-toggle">Show Stats</button> | |
| </div> | |
| <div id="statsContent" class="stats-content"> | |
| <div class="chart-container"> | |
| <canvas id="creatorStatsChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="search-bar"> | |
| <input type="text" id="searchInput" placeholder="Search by name, owner, or description..." /> | |
| <button id="refreshButton" class="refresh-btn"> | |
| <span class="refresh-icon"></span> | |
| Refresh | |
| </button> | |
| </div> | |
| <div id="gridContainer" class="grid-container"></div> | |
| <div id="pagination" class="pagination"></div> | |
| </div> | |
| <!-- Picks Tab Content (๊ธฐ์กด) --> | |
| <div id="fixedTab" class="tab-content"> | |
| <div id="fixedGrid" class="grid-container"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="loadingIndicator" class="loading"> | |
| <div class="loading-content"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-text">Loading Zero-GPU spaces...</div> | |
| <div id="loadingError" class="loading-error"> | |
| If this takes too long, try refreshing the page. | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --------------------- ์ ์ญ DOM Elements --------------------- | |
| const elements = { | |
| gridContainer: document.getElementById('gridContainer'), | |
| loadingIndicator: document.getElementById('loadingIndicator'), | |
| loadingError: document.getElementById('loadingError'), | |
| searchInput: document.getElementById('searchInput'), | |
| refreshButton: document.getElementById('refreshButton'), | |
| pagination: document.getElementById('pagination'), | |
| statsToggle: document.getElementById('statsToggle'), | |
| statsContent: document.getElementById('statsContent'), | |
| creatorStatsChart: document.getElementById('creatorStatsChart'), | |
| // second tab ์ ๊ฑฐ๋ก newCreatedGrid๋ ์ ๊ฑฐ | |
| }; | |
| // --------------------- ํญ ๋ฒํผ --------------------- | |
| const tabTrendingButton = document.getElementById('tabTrendingButton'); | |
| const tabFixedButton = document.getElementById('tabFixedButton'); | |
| // ํญ ๋ด์ฉ | |
| const trendingTab = document.getElementById('trendingTab'); | |
| const fixedTab = document.getElementById('fixedTab'); | |
| // --------------------- ์ํ --------------------- | |
| const state = { | |
| isLoading: false, | |
| spaces: [], | |
| currentPage: 0, | |
| itemsPerPage: 24, | |
| totalItems: 0, | |
| loadingTimeout: null, | |
| staticModeAttempted: {}, | |
| statsVisible: false, | |
| chartInstance: null, | |
| topOwners: [], | |
| iframeStatuses: {} | |
| }; | |
| // --------------------- iframe ์๋ฌ ๊ฐ์ง ๋ก์ง --------------------- | |
| const iframeLoader = { | |
| checkQueue: {}, | |
| maxAttempts: 5, | |
| checkInterval: 5000, | |
| startChecking(iframe, owner, name, title, spaceKey) { | |
| this.checkQueue[spaceKey] = { | |
| iframe, owner, name, title, attempts: 0, status: 'loading' | |
| }; | |
| this.checkIframeStatus(spaceKey); | |
| }, | |
| checkIframeStatus(spaceKey) { | |
| if (!this.checkQueue[spaceKey]) return; | |
| const item = this.checkQueue[spaceKey]; | |
| if (item.status !== 'loading') { | |
| delete this.checkQueue[spaceKey]; | |
| return; | |
| } | |
| item.attempts++; | |
| try { | |
| if (!item.iframe || !item.iframe.parentNode) { | |
| delete this.checkQueue[spaceKey]; | |
| return; | |
| } | |
| try { | |
| const hasContent = item.iframe.contentWindow && | |
| item.iframe.contentWindow.document && | |
| item.iframe.contentWindow.document.body; | |
| if (hasContent && item.iframe.contentWindow.document.body.innerHTML.length > 100) { | |
| const bodyText = item.iframe.contentWindow.document.body.textContent.toLowerCase(); | |
| if (bodyText.includes('forbidden') || bodyText.includes('404') || | |
| bodyText.includes('not found') || bodyText.includes('error')) { | |
| item.status = 'error'; | |
| handleIframeError(item.iframe, item.owner, item.name, item.title); | |
| } else { | |
| item.status = 'success'; | |
| } | |
| delete this.checkQueue[spaceKey]; | |
| return; | |
| } | |
| } catch(e) { | |
| // cross-origin ์๋ฌ๋ ๋จ์ ๋ฌด์ | |
| } | |
| const rect = item.iframe.getBoundingClientRect(); | |
| if (rect.width > 50 && rect.height > 50 && item.attempts > 2) { | |
| item.status = 'success'; | |
| delete this.checkQueue[spaceKey]; | |
| return; | |
| } | |
| if (item.attempts >= this.maxAttempts) { | |
| if (item.iframe.offsetWidth > 0 && item.iframe.offsetHeight > 0) { | |
| item.status = 'success'; | |
| } else { | |
| item.status = 'error'; | |
| handleIframeError(item.iframe, item.owner, item.name, item.title); | |
| } | |
| delete this.checkQueue[spaceKey]; | |
| return; | |
| } | |
| const nextDelay = this.checkInterval * Math.pow(1.5, item.attempts - 1); | |
| setTimeout(() => this.checkIframeStatus(spaceKey), nextDelay); | |
| } catch (e) { | |
| console.error('Error checking iframe status:', e); | |
| if (item.attempts >= this.maxAttempts) { | |
| item.status = 'error'; | |
| handleIframeError(item.iframe, item.owner, item.name, item.title); | |
| delete this.checkQueue[spaceKey]; | |
| } else { | |
| setTimeout(() => this.checkIframeStatus(spaceKey), this.checkInterval); | |
| } | |
| } | |
| } | |
| }; | |
| // --------------------- ํต๊ณ ์ฐฝ toggle --------------------- | |
| function toggleStats() { | |
| state.statsVisible = !state.statsVisible; | |
| elements.statsContent.classList.toggle('open', state.statsVisible); | |
| elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats'; | |
| if (state.statsVisible && state.topOwners.length > 0) { | |
| renderCreatorStats(); | |
| } | |
| } | |
| function renderCreatorStats() { | |
| if (state.chartInstance) { | |
| state.chartInstance.destroy(); | |
| } | |
| const ctx = elements.creatorStatsChart.getContext('2d'); | |
| const labels = state.topOwners.map(item => item[0]); | |
| const data = state.topOwners.map(item => item[1]); | |
| const colors = []; | |
| for (let i = 0; i < labels.length; i++) { | |
| const hue = (i * 360 / labels.length) % 360; | |
| colors.push(`hsla(${hue}, 70%, 80%, 0.7)`); | |
| } | |
| state.chartInstance = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels, | |
| datasets: [{ | |
| label: 'Number of Spaces in Top 500', | |
| data, | |
| backgroundColor: colors, | |
| borderColor: colors.map(color => color.replace('0.7', '1')), | |
| borderWidth: 1 | |
| }] | |
| }, | |
| options: { | |
| indexAxis: 'y', | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| callbacks: { | |
| title(tooltipItems) { | |
| return tooltipItems[0].label; | |
| }, | |
| label(context) { | |
| return `Spaces: ${context.raw}`; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| beginAtZero: true, | |
| title: { display: true, text: 'Number of Spaces' } | |
| }, | |
| y: { | |
| title: { display: true, text: 'Creator ID' }, | |
| ticks: { | |
| autoSkip: false, | |
| font(context) { | |
| const defaultSize = 11; | |
| return { size: labels.length > 20 ? defaultSize - 1 : defaultSize }; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // --------------------- TRENDING(๊ธฐ์กด) --------------------- | |
| async function loadSpaces(page = 0) { | |
| setLoading(true); | |
| try { | |
| const searchText = elements.searchInput.value; | |
| const offset = page * state.itemsPerPage; | |
| const timeoutPromise = new Promise((_, reject) => | |
| setTimeout(() => reject(new Error('Request timeout')), 30000) | |
| ); | |
| const fetchPromise = fetch( | |
| `/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}` | |
| ); | |
| const response = await Promise.race([fetchPromise, timeoutPromise]); | |
| const data = await response.json(); | |
| state.spaces = data.spaces; | |
| state.totalItems = data.total; | |
| state.currentPage = page; | |
| state.topOwners = data.top_owners || []; | |
| renderGrid(state.spaces); | |
| renderPagination(); | |
| // ํต๊ณ Visible ์ํ๋ผ๋ฉด ์ ๋ฐ์ดํธ | |
| if (state.statsVisible && state.topOwners.length > 0) { | |
| renderCreatorStats(); | |
| } | |
| } catch (error) { | |
| console.error('Error loading spaces:', error); | |
| elements.gridContainer.innerHTML = ` | |
| <div style="grid-column: 1/-1; text-align: center; padding: 40px;"> | |
| <div style="font-size: 3rem; margin-bottom: 20px;">โ ๏ธ</div> | |
| <h3 style="margin-bottom: 10px;">Unable to load spaces</h3> | |
| <p style="color: #666;">Please try refreshing the page. If the problem persists, try again later.</p> | |
| <button id="retryButton" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;"> | |
| Try Again | |
| </button> | |
| </div> | |
| `; | |
| document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0)); | |
| renderPagination(); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| function renderPagination() { | |
| elements.pagination.innerHTML = ''; | |
| const totalPages = Math.ceil(state.totalItems / state.itemsPerPage); | |
| // Previous | |
| const prevButton = document.createElement('button'); | |
| prevButton.className = 'pagination-button'; | |
| prevButton.textContent = 'Previous'; | |
| prevButton.disabled = (state.currentPage === 0); | |
| prevButton.addEventListener('click', () => { | |
| if (state.currentPage > 0) { | |
| loadSpaces(state.currentPage - 1); | |
| } | |
| }); | |
| elements.pagination.appendChild(prevButton); | |
| // ์ค๊ฐ ํ์ด์ง๋ค | |
| const maxButtons = 7; | |
| let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2)); | |
| let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1); | |
| if (endPage - startPage + 1 < maxButtons) { | |
| startPage = Math.max(0, endPage - maxButtons + 1); | |
| } | |
| for (let i = startPage; i <= endPage; i++) { | |
| const pageButton = document.createElement('button'); | |
| pageButton.className = 'pagination-button' + (i === state.currentPage ? ' active' : ''); | |
| pageButton.textContent = (i + 1); | |
| pageButton.addEventListener('click', () => { | |
| if (i !== state.currentPage) { | |
| loadSpaces(i); | |
| } | |
| }); | |
| elements.pagination.appendChild(pageButton); | |
| } | |
| // Next | |
| const nextButton = document.createElement('button'); | |
| nextButton.className = 'pagination-button'; | |
| nextButton.textContent = 'Next'; | |
| nextButton.disabled = (state.currentPage >= totalPages - 1); | |
| nextButton.addEventListener('click', () => { | |
| if (state.currentPage < totalPages - 1) { | |
| loadSpaces(state.currentPage + 1); | |
| } | |
| }); | |
| elements.pagination.appendChild(nextButton); | |
| } | |
| function handleIframeError(iframe, owner, name, title) { | |
| const container = iframe.parentNode; | |
| const errorPlaceholder = document.createElement('div'); | |
| errorPlaceholder.className = 'error-placeholder'; | |
| const errorMessage = document.createElement('p'); | |
| errorMessage.textContent = `"${title}" space couldn't be loaded`; | |
| errorPlaceholder.appendChild(errorMessage); | |
| const directLink = document.createElement('a'); | |
| directLink.href = `https://huggingface.co/spaces/${owner}/${name}`; | |
| directLink.target = '_blank'; | |
| directLink.textContent = 'Visit HF Space'; | |
| directLink.style.color = '#3182ce'; | |
| directLink.style.marginTop = '10px'; | |
| directLink.style.display = 'inline-block'; | |
| directLink.style.padding = '8px 16px'; | |
| directLink.style.background = '#ebf8ff'; | |
| directLink.style.borderRadius = '5px'; | |
| directLink.style.fontWeight = '600'; | |
| errorPlaceholder.appendChild(directLink); | |
| iframe.style.display = 'none'; | |
| container.appendChild(errorPlaceholder); | |
| } | |
| function renderGrid(spaces) { | |
| elements.gridContainer.innerHTML = ''; | |
| if (!spaces || spaces.length === 0) { | |
| const noResultsMsg = document.createElement('p'); | |
| noResultsMsg.textContent = 'No zero-gpu spaces found matching your search.'; | |
| noResultsMsg.style.padding = '2rem'; | |
| noResultsMsg.style.textAlign = 'center'; | |
| noResultsMsg.style.fontStyle = 'italic'; | |
| noResultsMsg.style.color = '#718096'; | |
| elements.gridContainer.appendChild(noResultsMsg); | |
| return; | |
| } | |
| spaces.forEach((item) => { | |
| try { | |
| const { | |
| url, title, likes_count, owner, name, rank, | |
| description, avatar_url, author_name, embedUrl | |
| } = item; | |
| const gridItem = document.createElement('div'); | |
| gridItem.className = 'grid-item'; | |
| // Header | |
| const headerDiv = document.createElement('div'); | |
| headerDiv.className = 'grid-header'; | |
| const spaceHeader = document.createElement('div'); | |
| spaceHeader.className = 'space-header'; | |
| const rankBadge = document.createElement('div'); | |
| rankBadge.className = 'rank-badge'; | |
| rankBadge.textContent = `#${rank}`; | |
| spaceHeader.appendChild(rankBadge); | |
| const titleWrapper = document.createElement('div'); | |
| titleWrapper.style.display = 'flex'; | |
| titleWrapper.style.alignItems = 'center'; | |
| titleWrapper.style.marginLeft = '8px'; | |
| const titleEl = document.createElement('h3'); | |
| titleEl.className = 'space-title'; | |
| titleEl.textContent = title; | |
| titleEl.title = title; | |
| titleWrapper.appendChild(titleEl); | |
| const zeroGpuBadge = document.createElement('span'); | |
| zeroGpuBadge.className = 'zero-gpu-badge'; | |
| zeroGpuBadge.textContent = 'ZERO GPU'; | |
| titleWrapper.appendChild(zeroGpuBadge); | |
| spaceHeader.appendChild(titleWrapper); | |
| headerDiv.appendChild(spaceHeader); | |
| const metaInfo = document.createElement('div'); | |
| metaInfo.className = 'grid-meta'; | |
| metaInfo.style.display = 'flex'; | |
| metaInfo.style.justifyContent = 'space-between'; | |
| metaInfo.style.alignItems = 'center'; | |
| metaInfo.style.marginTop = '6px'; | |
| const leftMeta = document.createElement('div'); | |
| const authorSpan = document.createElement('span'); | |
| authorSpan.className = 'author-name'; | |
| authorSpan.style.marginLeft = '8px'; | |
| authorSpan.textContent = `by ${author_name}`; | |
| leftMeta.appendChild(authorSpan); | |
| metaInfo.appendChild(leftMeta); | |
| const likesDiv = document.createElement('div'); | |
| likesDiv.className = 'likes-wrapper'; | |
| likesDiv.innerHTML = `<span class="likes-heart">โฅ</span><span>${likes_count}</span>`; | |
| metaInfo.appendChild(likesDiv); | |
| headerDiv.appendChild(metaInfo); | |
| gridItem.appendChild(headerDiv); | |
| if (description) { | |
| const descP = document.createElement('p'); | |
| descP.className = 'desc-text'; | |
| descP.textContent = description; | |
| gridItem.appendChild(descP); | |
| } | |
| const content = document.createElement('div'); | |
| content.className = 'grid-content'; | |
| const iframeContainer = document.createElement('div'); | |
| iframeContainer.className = 'iframe-container'; | |
| const iframe = document.createElement('iframe'); | |
| iframe.src = embedUrl; | |
| iframe.title = title; | |
| iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;'; | |
| iframe.setAttribute('allowfullscreen', ''); | |
| iframe.setAttribute('frameborder', '0'); | |
| iframe.loading = 'lazy'; | |
| const spaceKey = `${owner}/${name}`; | |
| state.iframeStatuses[spaceKey] = 'loading'; | |
| iframe.onload = function() { | |
| iframeLoader.startChecking(iframe, owner, name, title, spaceKey); | |
| }; | |
| iframe.onerror = function() { | |
| handleIframeError(iframe, owner, name, title); | |
| state.iframeStatuses[spaceKey] = 'error'; | |
| }; | |
| setTimeout(() => { | |
| if (state.iframeStatuses[spaceKey] === 'loading') { | |
| handleIframeError(iframe, owner, name, title); | |
| state.iframeStatuses[spaceKey] = 'error'; | |
| } | |
| }, 30000); | |
| iframeContainer.appendChild(iframe); | |
| content.appendChild(iframeContainer); | |
| const actions = document.createElement('div'); | |
| actions.className = 'grid-actions'; | |
| const linkEl = document.createElement('a'); | |
| linkEl.href = url; | |
| linkEl.target = '_blank'; | |
| linkEl.className = 'open-link'; | |
| linkEl.textContent = 'Open in new window'; | |
| actions.appendChild(linkEl); | |
| gridItem.appendChild(content); | |
| gridItem.appendChild(actions); | |
| elements.gridContainer.appendChild(gridItem); | |
| } catch (err) { | |
| console.error('Item rendering error:', err); | |
| } | |
| }); | |
| } | |
| // | |
| // | |
| // | |
| // --------------------- PICKS (์ ์ ) --------------------- | |
| function renderFixedGrid() { | |
| // ์์์ฉ ์ ์ ๋ชฉ๋ก | |
| const fixedGridContainer = document.getElementById('fixedGrid'); | |
| fixedGridContainer.innerHTML = ''; | |
| const staticSpaces = [ | |
| { | |
| url: "https://huggingface.co/spaces/openfree/AI-Podcast", | |
| title: "AI-Podcast", | |
| likes_count: 999, | |
| owner: "openfree", | |
| name: "AI-Podcast", | |
| rank: 1 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/Heartsync/NSFW-Uncensored", | |
| title: "NSFW-Uncensored", | |
| likes_count: 999, | |
| owner: "Heartsync", | |
| name: "NSFW-Uncensored", | |
| rank: 2 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/openfree/DreamO-video", | |
| title: "DreamO-video", | |
| likes_count: 999, | |
| owner: "openfree", | |
| name: "DreamO-video", | |
| rank: 3 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/ginipick/NH-Korea", | |
| title: "NH-Korea", | |
| likes_count: 999, | |
| owner: "ginipick", | |
| name: "NH-Korea", | |
| rank: 4 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/VIDraft/NH-Prediction", | |
| title: "NH-Prediction", | |
| likes_count: 999, | |
| owner: "VIDraft", | |
| name: "NH-Prediction", | |
| rank: 5 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/openfree/Game-Gallery", | |
| title: "Game-Gallery", | |
| likes_count: 999, | |
| owner: "openfree", | |
| name: "Game-Gallery", | |
| rank: 6 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/openfree/Vibe-Game", | |
| title: "Vibe-Game", | |
| likes_count: 999, | |
| owner: "openfree", | |
| name: "Vibe-Game", | |
| rank: 7 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/ginipick/IDEA-DESIGN", | |
| title: "IDEA-DESIGN", | |
| likes_count: 999, | |
| owner: "ginipick", | |
| name: "IDEA-DESIGN", | |
| rank: 8 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/openfree/Cycle-Navigator", | |
| title: "Cycle-Navigator", | |
| likes_count: 999, | |
| owner: "openfree", | |
| name: "Cycle-Navigator", | |
| rank: 9 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/ginipick/AI-BOOK", | |
| title: "AI-BOOK", | |
| likes_count: 999, | |
| owner: "ginipick", | |
| name: "AI-BOOK", | |
| rank: 10 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/VIDraft/Open-Meme-Studio", | |
| title: "Open Meme Studio", | |
| likes_count: 999, | |
| owner: "VIDraft", | |
| name: "Open-Meme-Studio", | |
| rank: 11 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/ginigen/Workflow-Canvas", | |
| title: "Workflow Canvas", | |
| likes_count: 999, | |
| owner: "ginigen", | |
| name: "Workflow-Canvas", | |
| rank: 12 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/openfree/Stock-Trading-Analysis", | |
| title: "Stock-Trading-Analysis", | |
| likes_count: 999, | |
| owner: "openfree", | |
| name: "Stock-Trading-Analysis", | |
| rank: 13 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/aiqtech/FLUX-Ghibli-Studio-LoRA", | |
| title: "FLUX-Ghibli-Studio-LoRA", | |
| likes_count: 999, | |
| owner: "aiqtech", | |
| name: "FLUX-Ghibli-Studio-LoRA", | |
| rank: 14 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/fantos/flxloraexp", | |
| title: "flux lora explorer", | |
| likes_count: 999, | |
| owner: "fantos", | |
| name: "flxloraexp", | |
| rank: 15 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/aiqtech/imaginpaint", | |
| title: "image in-painting", | |
| likes_count: 999, | |
| owner: "aiqtech", | |
| name: "imaginpaint", | |
| rank: 16 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/VIDraft/Polaroid-Style", | |
| title: "Polaroid Style", | |
| likes_count: 999, | |
| owner: "VIDraft", | |
| name: "Polaroid-Style", | |
| rank: 17 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/VIDraft/ReSize-Image-Outpainting", | |
| title: "ReSize-Image-Outpainting", | |
| likes_count: 999, | |
| owner: "VIDraft", | |
| name: "ReSize-Image-Outpainting", | |
| rank: 18 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/fantos/textcutobject", | |
| title: "text cut object", | |
| likes_count: 999, | |
| owner: "fantos", | |
| name: "textcutobject", | |
| rank: 19 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/seawolf2357/Ghibli-Multilingual-Text-rendering", | |
| title: "Ghibli Multilingual Text Rendering", | |
| likes_count: 999, | |
| owner: "seawolf2357", | |
| name: "Ghibli-Multilingual-Text-rendering", | |
| rank: 20 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/seawolf2357/REALVISXL-V5", | |
| title: "REALVISXL-V5", | |
| likes_count: 999, | |
| owner: "seawolf2357", | |
| name: "REALVISXL-V5", | |
| rank: 21 | |
| }, | |
| { | |
| url: "https://huggingface.co/spaces/ginigen/FLUX-Open-Ghibli-Studio", | |
| title: "FLUX Open Ghibli Studio", | |
| likes_count: 999, | |
| owner: "ginigen", | |
| name: "FLUX-Open-Ghibli-Studio", | |
| rank: 22 | |
| } | |
| ]; | |
| if (!staticSpaces || staticSpaces.length === 0) { | |
| const noResultsMsg = document.createElement('p'); | |
| noResultsMsg.textContent = 'No spaces to display.'; | |
| noResultsMsg.style.padding = '2rem'; | |
| noResultsMsg.style.textAlign = 'center'; | |
| noResultsMsg.style.fontStyle = 'italic'; | |
| noResultsMsg.style.color = '#718096'; | |
| fixedGridContainer.appendChild(noResultsMsg); | |
| return; | |
| } | |
| staticSpaces.forEach((item) => { | |
| try { | |
| const { url, title, likes_count, owner, name, rank } = item; | |
| const gridItem = document.createElement('div'); | |
| gridItem.className = 'grid-item'; | |
| const header = document.createElement('div'); | |
| header.className = 'grid-header'; | |
| const headerTop = document.createElement('div'); | |
| headerTop.className = 'grid-header-top'; | |
| const leftWrapper = document.createElement('div'); | |
| leftWrapper.style.display = 'flex'; | |
| leftWrapper.style.alignItems = 'center'; | |
| const emojiAvatar = document.createElement('div'); | |
| emojiAvatar.className = 'emoji-avatar'; | |
| emojiAvatar.textContent = '๐ค'; | |
| leftWrapper.appendChild(emojiAvatar); | |
| const titleEl = document.createElement('h3'); | |
| titleEl.textContent = title; | |
| titleEl.title = title; | |
| leftWrapper.appendChild(titleEl); | |
| headerTop.appendChild(leftWrapper); | |
| const rankBadge = document.createElement('div'); | |
| rankBadge.className = 'rank-badge'; | |
| rankBadge.textContent = `#${rank}`; | |
| headerTop.appendChild(rankBadge); | |
| header.appendChild(headerTop); | |
| const metaInfo = document.createElement('div'); | |
| metaInfo.className = 'grid-meta'; | |
| const ownerEl = document.createElement('div'); | |
| ownerEl.className = 'owner-info'; | |
| ownerEl.textContent = `by ${owner}`; | |
| metaInfo.appendChild(ownerEl); | |
| const likesCounter = document.createElement('div'); | |
| likesCounter.className = 'likes-counter'; | |
| likesCounter.innerHTML = 'โฅ <span>' + likes_count + '</span>'; | |
| metaInfo.appendChild(likesCounter); | |
| header.appendChild(metaInfo); | |
| gridItem.appendChild(header); | |
| const content = document.createElement('div'); | |
| content.className = 'grid-content'; | |
| const iframeContainer = document.createElement('div'); | |
| iframeContainer.className = 'iframe-container'; | |
| const iframe = document.createElement('iframe'); | |
| iframe.src = "https://" + owner.toLowerCase() + "-" + name.toLowerCase() + ".hf.space"; | |
| iframe.title = title; | |
| iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;'; | |
| iframe.setAttribute('allowfullscreen', ''); | |
| iframe.setAttribute('frameborder', '0'); | |
| iframe.loading = 'lazy'; | |
| const spaceKey = `${owner}/${name}`; | |
| iframe.onload = function() { | |
| iframeLoader.startChecking(iframe, owner, name, title, spaceKey); | |
| }; | |
| iframe.onerror = function() { | |
| handleIframeError(iframe, owner, name, title); | |
| }; | |
| setTimeout(() => { | |
| if (iframe.offsetWidth === 0 || iframe.offsetHeight === 0) { | |
| handleIframeError(iframe, owner, name, title); | |
| } | |
| }, 30000); | |
| iframeContainer.appendChild(iframe); | |
| content.appendChild(iframeContainer); | |
| const actions = document.createElement('div'); | |
| actions.className = 'grid-actions'; | |
| const linkEl = document.createElement('a'); | |
| linkEl.href = url; | |
| linkEl.target = '_blank'; | |
| linkEl.className = 'open-link'; | |
| linkEl.textContent = 'Open in new window'; | |
| actions.appendChild(linkEl); | |
| gridItem.appendChild(content); | |
| gridItem.appendChild(actions); | |
| fixedGridContainer.appendChild(gridItem); | |
| } catch (error) { | |
| console.error('Fixed tab rendering error:', error); | |
| } | |
| }); | |
| } | |
| // --------------------- Tab Switching --------------------- | |
| tabTrendingButton.addEventListener('click', () => { | |
| tabTrendingButton.classList.add('active'); | |
| tabFixedButton.classList.remove('active'); | |
| trendingTab.classList.add('active'); | |
| fixedTab.classList.remove('active'); | |
| loadSpaces(state.currentPage); | |
| }); | |
| tabFixedButton.addEventListener('click', () => { | |
| tabTrendingButton.classList.remove('active'); | |
| tabFixedButton.classList.add('active'); | |
| trendingTab.classList.remove('active'); | |
| fixedTab.classList.add('active'); | |
| renderFixedGrid(); | |
| }); | |
| // ๊ฒ์ ์ ๋ ฅ | |
| elements.searchInput.addEventListener('input', () => { | |
| clearTimeout(state.searchTimeout); | |
| state.searchTimeout = setTimeout(() => loadSpaces(0), 300); | |
| }); | |
| elements.searchInput.addEventListener('keyup', (event) => { | |
| if (event.key === 'Enter') { | |
| loadSpaces(0); | |
| } | |
| }); | |
| elements.refreshButton.addEventListener('click', () => loadSpaces(0)); | |
| elements.statsToggle.addEventListener('click', toggleStats); | |
| // ํ์ด์ง ๋ก๋ ์ ์ฒซ ํญ(Trending) ์คํ | |
| window.addEventListener('load', function() { | |
| setTimeout(() => loadSpaces(0), 500); | |
| }); | |
| setTimeout(() => { | |
| if (state.isLoading) { | |
| setLoading(false); | |
| elements.gridContainer.innerHTML = ` | |
| <div style="grid-column: 1/-1; text-align: center; padding: 40px;"> | |
| <div style="font-size: 3rem; margin-bottom: 20px;">โฑ๏ธ</div> | |
| <h3 style="margin-bottom: 10px;">Loading is taking longer than expected</h3> | |
| <p style="color: #666;">Please try refreshing the page.</p> | |
| <button onClick="window.location.reload()" style="margin-top: 20px; padding: 10px 20px; background: var(--pastel-purple); border: none; border-radius: 5px; cursor: pointer;"> | |
| Reload Page | |
| </button> | |
| </div> | |
| `; | |
| } | |
| }, 20000); | |
| function setLoading(isLoading) { | |
| state.isLoading = isLoading; | |
| elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none'; | |
| if (isLoading) { | |
| elements.refreshButton.classList.add('refreshing'); | |
| clearTimeout(state.loadingTimeout); | |
| state.loadingTimeout = setTimeout(() => { | |
| elements.loadingError.style.display = 'block'; | |
| }, 10000); | |
| } else { | |
| elements.refreshButton.classList.remove('refreshing'); | |
| clearTimeout(state.loadingTimeout); | |
| elements.loadingError.style.display = 'none'; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| ''') | |
| app.run(host='0.0.0.0', port=7860) | |