Spaces:
Sleeping
Sleeping
| from flask import Flask, render_template, request, jsonify | |
| import requests | |
| import os | |
| import time | |
| import random | |
| from collections import Counter | |
| app = Flask(__name__) | |
| # Function to fetch trending spaces from Huggingface with pagination | |
| def fetch_trending_spaces(offset=0, limit=72): | |
| try: | |
| # Simple data fetching | |
| url = "https://huggingface.co/api/spaces" | |
| params = {"limit": 500} # Get max 500 | |
| # Increase timeout | |
| response = requests.get(url, params=params, timeout=30) | |
| if response.status_code == 200: | |
| spaces = response.json() | |
| filtered_spaces = [space for space in spaces if space.get('owner') != 'None' and space.get('id', '').split('/', 1)[0] != 'None'] | |
| # Slice according to requested offset and limit | |
| start = min(offset, len(filtered_spaces)) | |
| end = min(offset + limit, len(filtered_spaces)) | |
| print(f"Fetched {len(filtered_spaces)} spaces, returning {end-start} items from {start} to {end}") | |
| return { | |
| 'spaces': filtered_spaces[start:end], | |
| 'total': len(filtered_spaces), | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'all_spaces': filtered_spaces # Return all spaces for stats calculation | |
| } | |
| else: | |
| print(f"Error fetching spaces: {response.status_code}") | |
| # Return empty spaces with fake 200 limit data | |
| return { | |
| 'spaces': generate_dummy_spaces(limit), | |
| 'total': 200, | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'all_spaces': generate_dummy_spaces(500) # Dummy data for stats | |
| } | |
| except Exception as e: | |
| print(f"Exception when fetching spaces: {e}") | |
| # Generate fake data | |
| return { | |
| 'spaces': generate_dummy_spaces(limit), | |
| 'total': 200, | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'all_spaces': generate_dummy_spaces(500) # Dummy data for stats | |
| } | |
| # Generate dummy spaces in case of error | |
| def generate_dummy_spaces(count): | |
| spaces = [] | |
| for i in range(count): | |
| spaces.append({ | |
| 'id': f'dummy/space-{i}', | |
| 'owner': 'dummy', | |
| 'title': f'Example Space {i+1}', | |
| 'likes': 100 - i, | |
| 'createdAt': '2023-01-01T00:00:00.000Z' | |
| }) | |
| return spaces | |
| # Transform Huggingface URL to direct space URL | |
| def transform_url(owner, name): | |
| # 1. Replace '.' with '-' | |
| name = name.replace('.', '-') | |
| # 2. Replace '_' with '-' | |
| name = name.replace('_', '-') | |
| # 3. Convert to lowercase | |
| owner = owner.lower() | |
| name = name.lower() | |
| return f"https://{owner}-{name}.hf.space" | |
| # Get space details | |
| def get_space_details(space_data, index, offset): | |
| try: | |
| # Extract common info | |
| if '/' in space_data.get('id', ''): | |
| owner, name = space_data.get('id', '').split('/', 1) | |
| else: | |
| owner = space_data.get('owner', '') | |
| name = space_data.get('id', '') | |
| # Ignore if contains None | |
| if owner == 'None' or name == 'None': | |
| return None | |
| # Construct URLs | |
| original_url = f"https://huggingface.co/spaces/{owner}/{name}" | |
| embed_url = transform_url(owner, name) | |
| # Likes count | |
| likes_count = space_data.get('likes', 0) | |
| # Extract title | |
| title = space_data.get('title', name) | |
| # Tags | |
| tags = space_data.get('tags', []) | |
| return { | |
| 'url': original_url, | |
| 'embedUrl': embed_url, | |
| 'title': title, | |
| 'owner': owner, | |
| 'name': name, # Store Space name | |
| 'likes_count': likes_count, | |
| 'tags': tags, | |
| 'rank': offset + index + 1 | |
| } | |
| except Exception as e: | |
| print(f"Error processing space data: {e}") | |
| # Return basic object even if error occurs | |
| return { | |
| 'url': 'https://huggingface.co/spaces', | |
| 'embedUrl': 'https://huggingface.co/spaces', | |
| 'title': 'Error Loading Space', | |
| 'owner': 'huggingface', | |
| 'name': 'error', | |
| 'likes_count': 0, | |
| 'tags': [], | |
| 'rank': offset + index + 1 | |
| } | |
| # Get owner statistics from all spaces | |
| def get_owner_stats(all_spaces): | |
| owners = [] | |
| for space in all_spaces: | |
| if '/' in space.get('id', ''): | |
| owner, _ = space.get('id', '').split('/', 1) | |
| else: | |
| owner = space.get('owner', '') | |
| if owner != 'None': | |
| owners.append(owner) | |
| # Count occurrences of each owner | |
| owner_counts = Counter(owners) | |
| # Get top 30 owners by count | |
| top_owners = owner_counts.most_common(30) | |
| return top_owners | |
| # Homepage route | |
| def home(): | |
| return render_template('index.html') | |
| # Trending spaces API | |
| def trending_spaces(): | |
| search_query = request.args.get('search', '').lower() | |
| offset = int(request.args.get('offset', 0)) | |
| limit = int(request.args.get('limit', 72)) # Default 72 | |
| # Fetch trending spaces | |
| spaces_data = fetch_trending_spaces(offset, limit) | |
| # Process and filter spaces | |
| results = [] | |
| for index, space_data in enumerate(spaces_data['spaces']): | |
| space_info = get_space_details(space_data, index, offset) | |
| if not space_info: | |
| continue | |
| # Apply search filter if needed | |
| if search_query: | |
| title = space_info['title'].lower() | |
| owner = space_info['owner'].lower() | |
| url = space_info['url'].lower() | |
| tags = ' '.join([str(tag) for tag in space_info.get('tags', [])]).lower() | |
| if (search_query not in title and | |
| search_query not in owner and | |
| search_query not in url and | |
| search_query not in tags): | |
| continue | |
| results.append(space_info) | |
| # Get owner statistics for all spaces | |
| top_owners = get_owner_stats(spaces_data.get('all_spaces', [])) | |
| return jsonify({ | |
| 'spaces': results, | |
| 'total': spaces_data['total'], | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'top_owners': top_owners # Add top owners data | |
| }) | |
| if __name__ == '__main__': | |
| # Create templates folder | |
| os.makedirs('templates', exist_ok=True) | |
| # Create index.html file | |
| with open('templates/index.html', 'w', encoding='utf-8') as f: | |
| f.write(''' | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Huggingface Spaces Gallery</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| @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 OS Window Styling */ | |
| .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 Styling */ | |
| .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; | |
| } | |
| /* Controls Styling */ | |
| .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 Styling */ | |
| .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; | |
| } | |
| .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; | |
| } | |
| .grid-content iframe { | |
| width: 100%; | |
| height: 100%; | |
| 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: 5rem; | |
| margin-bottom: 1rem; | |
| animation: bounce 1s infinite alternate; | |
| } | |
| @keyframes bounce { | |
| from { | |
| transform: translateY(0px); | |
| } | |
| to { | |
| transform: translateY(-10px); | |
| } | |
| } | |
| /* 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; | |
| } | |
| } | |
| </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>HF Space 'Top Rank' Gallery</h1> | |
| <p>Discover the top 500 trending spaces from the Huggingface</p> | |
| </div> | |
| <!-- Stats Section --> | |
| <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</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 tags..." /> | |
| <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"> | |
| <!-- Pagination buttons will be dynamically created by JavaScript --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="loadingIndicator" class="loading"> | |
| <div class="loading-content"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-text">Loading amazing spaces...</div> | |
| <div id="loadingError" class="loading-error"> | |
| If this takes too long, try refreshing the page. | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM element references | |
| 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') | |
| }; | |
| // Application state | |
| const state = { | |
| isLoading: false, | |
| spaces: [], | |
| currentPage: 0, | |
| itemsPerPage: 72, // 72 items per page | |
| totalItems: 0, | |
| loadingTimeout: null, | |
| staticModeAttempted: {}, // Track which spaces have attempted static mode | |
| statsVisible: false, | |
| chartInstance: null, | |
| topOwners: [] | |
| }; | |
| // Random emoji list for 404 errors | |
| const randomEmojis = [ | |
| '🙈', '🙉', '🙊', '🐵', '🐒', '🦍', '🦧', '🐶', '🐕', '🦮', | |
| '🐩', '🐺', '🦊', '🦝', '🐱', '🐈', '🦁', '🐯', '🐅', '🐆', | |
| '🐴', '🐎', '🦄', '🦓', '🦌', '🐮', '🐂', '🐃', '🐄', '🐷', | |
| '🐖', '🐗', '🐏', '🐑', '🐐', '🐪', '🐫', '🦙', '🦒', '🐘', | |
| '🦏', '🦛', '🐭', '🐁', '🐀', '🐹', '🐰', '🐇', '🐿️', '🦔', | |
| '🦇', '🐻', '🐨', '🐼', '🦥', '🦦', '🦨', '🦘', '🦡', '🐾', | |
| '🦃', '🐔', '🐓', '🐣', '🐤', '🐥', '🐦', '🐧', '🕊️', '🦅', | |
| '🦆', '🦢', '🦉', '🦩', '🦚', '🦜', '🐸', '🐊', '🐢', '🦎', | |
| '🐍', '🐲', '🐉', '🦕', '🦖', '🐳', '🐋', '🐬', '🐟', '🐠', | |
| '🐡', '🦈', '🐙', '🐚', '🐌', '🦋', '🐛', '🐜', '🐝', '🐞', | |
| '🦗', '🕷️', '🕸️', '🦂', '🦟', '🦠', '💐', '🌸', '💮', '🏵️', | |
| '🌹', '🥀', '🌺', '🌻', '🌼', '🌷', '🌱', '🌲', '🌳', '🌴', | |
| '🌵', '🌾', '🌿', '☘️', '🍀', '🍁', '🍂', '🍃', '🍇', '🍈', | |
| '🍉', '🍊', '🍋', '🍌', '🍍', '🥭', '🍎', '🍏', '🍐', '🍑', | |
| '🍒', '🍓', '🥝', '🍅', '🥥', '🥑', '🍆', '🥔', '🥕', '🌽', | |
| '🌶️', '🥒', '🥬', '🥦', '🧄', '🧅', '🍄', '🥜', '🌰', '🍞', | |
| '🥐', '🥖', '🥨', '🥯', '🥞', '🧇', '🧀', '🍖', '🍗', '🥩', | |
| '🥓', '🍔', '🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🥙', '🧆', | |
| '🥚', '🍳', '🥘', '🍲', '🥣', '🥗', '🍿', '🧈', '🧂', '🥫', | |
| '🍱', '🍘', '🍙', '🍚', '🍛', '🍜', '🍝', '🍠', '🍢', '🍣', | |
| '🍤', '🍥', '🥮', '🍡', '🥟', '🥠', '🥡', '🦀', '🦞', '🦐', | |
| '🦑', '🦪', '🍦', '🍧', '🍨', '🍩', '🍪', '🎂', '🍰', '🧁', | |
| '🥧', '🍫', '🍬', '🍭', '🍮', '🍯', '🍼', '🥛', '☕', '🍵', | |
| '🍶', '🍾', '🍷', '🍸', '🍹', '🍺', '🍻', '🥂', '🥃', '🥤', | |
| '🧃', '🧉', '🧊', '🥢', '🍽️', '🍴', '🥄', '🔪', '🏺', '🌍', | |
| '🌎', '🌏', '🌐', '🗺️', '🗾', '🧭', '🏔️', '⛰️', '🌋', '🗻', | |
| '🏕️', '🏖️', '🏜️', '🏝️', '🏞️', '🏟️', '🏛️', '🏗️', '🧱', '🏘️', | |
| '🏚️', '🏠', '🏡', '🏢', '🏣', '🏤', '🏥', '🏦', '🏨', '🏩', | |
| '🏪', '🏫', '🏬', '🏭', '🏯', '🏰', '💒', '🗼', '🗽', '⛪', | |
| '🕌', '🛕', '🕍', '⛩️', '🕋', '⛲', '⛺', '🌁', '🌃', '🏙️', | |
| '🌄', '🌅', '🌆', '🌇', '🌉', '♨️', '🎠', '🎡', '🎢', '💈', | |
| '🎪', '🚂', '🚃', '🚄', '🚅', '🚆', '🚇', '🚈', '🚉', '🚊', | |
| '🚝', '🚞', '🚋', '🚌', '🚍', '🚎', '🚐', '🚑', '🚒', '🚓', | |
| '🚔', '🚕', '🚖', '🚗', '🚘', '🚙', '🚚', '🚛', '🚜', '🏎️', | |
| '🏍️', '🛵', '🦽', '🦼', '🛺', '🚲', '🛴', '🛹', '🚏', '🛣️', | |
| '🛤️', '🛢️', '⛽', '🚨', '🚥', '🚦', '🛑', '🚧', '⚓', '⛵', | |
| '🛶', '🚤', '🛳️', '⛴️', '🛥️', '🚢', '✈️', '🛩️', '🛫', '🛬', | |
| '🪂', '💺', '🚁', '🚟', '🚠', '🚡', '🛰️', '🚀', '🛸', '🛎️', | |
| '🧳', '⌛', '⏳', '⌚', '⏰', '⏱️', '⏲️', '🕰️', '🕛', '🕧', | |
| '🕐', '🕜', '🕑', '🕝', '🕒', '🕞', '🕓', '🕟', '🕔', '🕠', | |
| '🕕', '🕡', '🕖', '🕢', '🕗', '🕣', '🕘', '🕤', '🕙', '🕥', | |
| '🕚', '🕦', '🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘', | |
| '🌙', '🌚', '🌛', '🌜', '🌡️', '☀️', '🌝', '🌞', '🪐', '⭐', | |
| '🌟', '🌠', '🌌', '☁️', '⛅', '⛈️', '🌤️', '🌥️', '🌦️', '🌧️', | |
| '🌨️', '🌩️', '🌪️', '🌫️', '🌬️', '🌀', '🌈', '🌂', '☂️', '☔', | |
| '⛱️', '⚡', '❄️', '☃️', '⛄', '☄️', '🔥', '💧', '🌊', '🎃', | |
| '🎄', '🎆', '🎇', '🧨', '✨', '🎈', '🎉', '🎊', '🎋', '🎍', | |
| '🎎', '🎏', '🎐', '🎑', '🧧', '🎀', '🎁', '🎗️', '🎟️', '🎫', | |
| '🎖️', '🏆', '🏅', '🥇', '🥈', '🥉', '⚽', '⚾', '🥎', '🏀', | |
| '🏐', '🏈', '🏉', '🎾', '🥏', '🎳', '🏏', '🏑', '🏒', '🥍', | |
| '🏓', '🏸', '🥊', '🥋', '🥅', '⛳', '⛸️', '🎣', '🤿', '🎽', | |
| '🎿', '🛷', '🥌', '🎯', '🪀', '🪁', '🎱', '🔮', '🧿', '🎮', | |
| '🕹️', '🎰', '🎲', '🧩', '🧸', '♠️', '♥️', '♦️', '♣️', '♟️', | |
| '🃏', '🀄', '🎴', '🎭', '🖼️', '🎨', '🧵', '🧶', '👓', '🕶️', | |
| '🥽', '🥼', '🦺', '👔', '👕', '👖', '🧣', '🧤', '🧥', '🧦', | |
| '👗', '👘', '🥻', '🩱', '🩲', '🩳', '👙', '👚', '👛', '👜', | |
| '👝', '🛍️', '🎒', '👞', '👟', '🥾', '🥿', '👠', '👡', '🩰', | |
| '👢', '👑', '👒', '🎩', '🎓', '🧢', '⛑️', '📿', '💄', '💍', | |
| '💎', '🔇', '🔈', '🔉', '🔊', '📢', '📣', '📯', '🔔', '🔕', | |
| '🎼', '🎵', '🎶', '🎙️', '🎚️', '🎛️', '🎤', '🎧', '📻', '🎷', | |
| '🎸', '🎹', '🎺', '🎻', '🪕', '🥁', '📱', '📲', '☎️', '📞', | |
| '📟', '📠', '🔋', '🔌', '💻', '🖥️', '🖨️', '⌨️', '🖱️', '🖲️', | |
| '💽', '💾', '💿', '📀', '🧮', '🎥', '🎞️', '📽️', '🎬', '📺', | |
| '📷', '📸', '📹', '📼', '🔍', '🔎', '🕯️', '💡', '🔦', '🏮', | |
| '🪔', '📔', '📕', '📖', '📗', '📘', '📙', '📚', '📓', '📒', | |
| '📃', '📜', '📄', '📰', '🗞️', '📑', '🔖', '🏷️', '💰', '💴', | |
| '💵', '💶', '💷', '💸', '💳', '🧾', '💹', '✉️', '📧', '📨', | |
| '📩', '📤', '📥', '📦', '📫', '📪', '📬', '📭', '📮', '🗳️', | |
| '✏️', '✒️', '🖋️', '🖊️', '🖌️', '🖍️', '📝', '💼', '📁', '📂', | |
| '🗂️', '📅', '📆', '🗒️', '🗓️', '📇', '📈', '📉', '📊', '📋', | |
| '📌', '📍', '📎', '🖇️', '📏', '📐', '✂️', '🗃️', '🗄️', '🗑️', | |
| '🔒', '🔓', '🔏', '🔐', '🔑', '🗝️', '🔨', '🪓', '⛏️', '⚒️', | |
| '🛠️', '🗡️', '⚔️', '🔫', '🏹', '🛡️', '🔧', '🔩', '⚙️', '🗜️', | |
| '⚖️', '🦯', '🔗', '⛓️', '🧰', '🧲', '⚗️', '🧪', '🧫', '🧬', | |
| '🔬', '🔭', '📡', '💉', '🩸', '💊', '🩹', '🩺', '🚪', '🛏️', | |
| '🛋️', '🪑', '🚽', '🚿', '🛁', '🪒', '🧴', '🧷', '🧹', '🧺', | |
| '🧻', '🧼', '🧽', '🧯', '🛒', '🚬', '⚰️', '⚱️', '🗿', '🏧', | |
| '🚮', '🚰', '♿', '🚹', '🚺', '🚻', '🚼', '🚾', '🛂', '🛃', | |
| '🛄', '🛅', '⚠️', '🚸', '⛔', '🚫', '🚳', '🚭', '🚯', '🚱', | |
| '🚷', '📵', '🔞', '☢️', '☣️', '⬆️', '↗️', '➡️', '↘️', '⬇️', | |
| '↙️', '⬅️', '↖️', '↕️', '↔️', '↩️', '↪️', '⤴️', '⤵️', '🔃', | |
| '🔄', '🔙', '🔚', '🔛', '🔜', '🔝', '🛐', '⚛️', '🕉️', '✡️', | |
| '☸️', '☯️', '✝️', '☦️', '☪️', '☮️', '🕎', '🔯', '♈', '♉', | |
| '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '⛎', | |
| '🔀', '🔁', '🔂', '▶️', '⏩', '⏭️', '⏯️', '◀️', '⏪', '⏮️', | |
| '🔼', '⏫', '🔽', '⏬', '⏸️', '⏹️', '⏺️', '⏏️', '🎦', '🔅', | |
| '🔆', '📶', '📳', '📴', '♀️', '♂️', '⚧️', '✖️', '➕', '➖', | |
| '➗', '♾️', '‼️', '⁉️', '❓', '❔', '❕', '❗', '〰️', '💱', | |
| '💲', '⚕️', '♻️', '⚜️', '🔱', '📛', '🔰', '⭕', '✅', '☑️', | |
| '✔️', '❌', '❎', '➰', '➿', '〽️', '✳️', '✴️', '❇️', '©️', | |
| '®️', '™️', '#️⃣', '*️⃣', '0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', | |
| '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🔠', '🔡', '🔢', '🔣', '🔤', | |
| '🅰️', '🆎', '🅱️', '🆑', '🆒', '🆓', 'ℹ️', '🆔', 'Ⓜ️', '🆕', | |
| '🆖', '🅾️', '🆗', '🅿️', '🆘', '🆙', '🆚', '🈁', '🈂️', '🈷️', | |
| '🈶', '🈯', '🉐', '🈹', '🈚', '🈲', '🉑', '🈸', '🈴', '🈳', | |
| '㊗️', '㊙️', '🈺', '🈵', '🔴', '🟠', '🟡', '🟢', '🔵', '🟣', | |
| '🟤', '⚫', '⚪', '🟥', '🟧', '🟨', '🟩', '🟦', '🟪', '🟫', | |
| '⬛', '⬜', '◼️', '◻️', '◾', '◽', '▪️', '▫️', '🔶', '🔷', | |
| '🔸', '🔹', '🔺', '🔻', '💠', '🔘', '🔳', '🔲', '🏁', '🚩', | |
| '🎌', '🏴', '🏳️', '🏳️🌈', '🏴☠️', '🇦🇨', '🇦🇩', '🇦🇪', '🇦🇫', '🇦🇬', | |
| '🇦🇮', '🇦🇱', '🇦🇲', '🇦🇴', '🇦🇶', '🇦🇷', '🇦🇸', '🇦🇹', '🇦🇺', '🇦🇼', | |
| '🇦🇽', '🇦🇿', '🇧🇦', '🇧🇧', '🇧🇩', '🇧🇪', '🇧🇫', '🇧🇬', '🇧🇭', '🇧🇮', | |
| '🇧🇯', '🇧🇱', '🇧🇲', '🇧🇳', '🇧🇴', '🇧🇶', '🇧🇷', '🇧🇸', '🇧🇹', '🇧🇻', | |
| '🇧🇼', '🇧🇾', '🇧🇿', '🇨🇦', '🇨🇨', '🇨🇩', '🇨🇫', '🇨🇬', '🇨🇭', '🇨🇮', | |
| '🇨🇰', '🇨🇱', '🇨🇲', '🇨🇳', '🇨🇴', '🇨🇵', '🇨🇷', '🇨🇺', '🇨🇻', '🇨🇼', | |
| '🇨🇽', '🇨🇾', '🇨🇿' | |
| ]; | |
| // Display loading indicator | |
| function setLoading(isLoading) { | |
| state.isLoading = isLoading; | |
| elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none'; | |
| if (isLoading) { | |
| elements.refreshButton.classList.add('refreshing'); | |
| // Show error message if loading takes too long | |
| clearTimeout(state.loadingTimeout); | |
| state.loadingTimeout = setTimeout(() => { | |
| elements.loadingError.style.display = 'block'; | |
| }, 10000); // Show error message after 10 seconds | |
| } else { | |
| elements.refreshButton.classList.remove('refreshing'); | |
| clearTimeout(state.loadingTimeout); | |
| elements.loadingError.style.display = 'none'; | |
| } | |
| } | |
| // API error handling | |
| async function handleApiResponse(response) { | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`API Error (${response.status}): ${errorText}`); | |
| } | |
| return response.json(); | |
| } | |
| // Create direct URL function with fixes for static sites | |
| function createDirectUrl(owner, name) { | |
| try { | |
| // 1. Replace '.' characters with '-' | |
| name = name.replace(/\./g, '-'); | |
| // 2. Replace '_' characters with '-' | |
| name = name.replace(/_/g, '-'); | |
| // 3. Convert everything to lowercase | |
| owner = owner.toLowerCase(); | |
| name = name.toLowerCase(); | |
| return `https://${owner}-${name}.hf.space`; | |
| } catch (error) { | |
| console.error('URL creation error:', error); | |
| return 'https://huggingface.co'; | |
| } | |
| } | |
| // Get random emoji for 404 errors | |
| function getRandomEmoji() { | |
| return randomEmojis[Math.floor(Math.random() * randomEmojis.length)]; | |
| } | |
| // Toggle stats display | |
| 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(); | |
| } | |
| } | |
| // Render creator stats chart | |
| function renderCreatorStats() { | |
| if (state.chartInstance) { | |
| state.chartInstance.destroy(); | |
| } | |
| const ctx = elements.creatorStatsChart.getContext('2d'); | |
| // Prepare data | |
| const labels = state.topOwners.map(item => item[0]); | |
| const data = state.topOwners.map(item => item[1]); | |
| // Generate colors for bars | |
| 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)`); | |
| } | |
| // Create chart | |
| state.chartInstance = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'Number of Spaces', | |
| data: 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: function(tooltipItems) { | |
| return tooltipItems[0].label; | |
| }, | |
| label: function(context) { | |
| return `Spaces: ${context.raw}`; | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| beginAtZero: true, | |
| title: { | |
| display: true, | |
| text: 'Number of Spaces' | |
| } | |
| }, | |
| y: { | |
| title: { | |
| display: true, | |
| text: 'Creator ID' | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Load spaces with timeout | |
| async function loadSpaces(page = 0) { | |
| setLoading(true); | |
| try { | |
| const searchText = elements.searchInput.value; | |
| const offset = page * state.itemsPerPage; | |
| // Set timeout (30 seconds) | |
| 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}`); | |
| // Use the first Promise that completes | |
| const response = await Promise.race([fetchPromise, timeoutPromise]); | |
| const data = await handleApiResponse(response); | |
| // Update state on successful load | |
| state.spaces = data.spaces; | |
| state.totalItems = data.total; | |
| state.currentPage = page; | |
| state.topOwners = data.top_owners || []; | |
| renderGrid(data.spaces); | |
| renderPagination(); | |
| // If stats are visible, update chart | |
| if (state.statsVisible && state.topOwners.length > 0) { | |
| renderCreatorStats(); | |
| } | |
| } catch (error) { | |
| console.error('Error loading spaces:', error); | |
| // Show empty grid with error message | |
| 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> | |
| `; | |
| // Add event listener to retry button | |
| document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0)); | |
| // Render simple pagination | |
| renderPagination(); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // Render pagination | |
| function renderPagination() { | |
| elements.pagination.innerHTML = ''; | |
| const totalPages = Math.ceil(state.totalItems / state.itemsPerPage); | |
| // Previous page button | |
| const prevButton = document.createElement('button'); | |
| prevButton.className = `pagination-button ${state.currentPage === 0 ? 'disabled' : ''}`; | |
| prevButton.textContent = 'Previous'; | |
| prevButton.disabled = state.currentPage === 0; | |
| prevButton.addEventListener('click', () => { | |
| if (state.currentPage > 0) { | |
| loadSpaces(state.currentPage - 1); | |
| } | |
| }); | |
| elements.pagination.appendChild(prevButton); | |
| // Page buttons (maximum of 7) | |
| const maxButtons = 7; | |
| let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons / 2)); | |
| let endPage = Math.min(totalPages - 1, startPage + maxButtons - 1); | |
| // Adjust start page if the end page is less than maximum buttons | |
| 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 page button | |
| const nextButton = document.createElement('button'); | |
| nextButton.className = `pagination-button ${state.currentPage >= totalPages - 1 ? 'disabled' : ''}`; | |
| 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); | |
| } | |
| // Handle iframe error and provide static site fallback with random emoji | |
| function handleIframeError(iframe, owner, name, title) { | |
| const container = iframe.parentNode; | |
| // Error message container | |
| const errorPlaceholder = document.createElement('div'); | |
| errorPlaceholder.className = 'error-placeholder'; | |
| // Random emoji instead of error icon | |
| const errorEmoji = document.createElement('div'); | |
| errorEmoji.className = 'error-emoji'; | |
| errorEmoji.textContent = getRandomEmoji(); | |
| errorPlaceholder.appendChild(errorEmoji); | |
| // Error message | |
| const errorMessage = document.createElement('p'); | |
| errorMessage.textContent = `"${title}" space couldn't be loaded`; | |
| errorPlaceholder.appendChild(errorMessage); | |
| // Try static site version button | |
| const directStaticLink = document.createElement('a'); | |
| directStaticLink.href = `https://${owner}-${name}.hf.space/index.html`; | |
| directStaticLink.target = '_blank'; | |
| directStaticLink.textContent = 'Try Static Version'; | |
| directStaticLink.style.color = '#3182ce'; | |
| directStaticLink.style.marginTop = '10px'; | |
| directStaticLink.style.display = 'inline-block'; | |
| directStaticLink.style.padding = '8px 16px'; | |
| directStaticLink.style.background = '#ebf8ff'; | |
| directStaticLink.style.borderRadius = '5px'; | |
| directStaticLink.style.fontWeight = '600'; | |
| directStaticLink.style.marginRight = '10px'; | |
| errorPlaceholder.appendChild(directStaticLink); | |
| // Direct HF link | |
| 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); | |
| // Hide iframe and show error message | |
| iframe.style.display = 'none'; | |
| container.appendChild(errorPlaceholder); | |
| } | |
| // Render grid | |
| function renderGrid(spaces) { | |
| elements.gridContainer.innerHTML = ''; | |
| if (!spaces || spaces.length === 0) { | |
| const noResultsMsg = document.createElement('p'); | |
| noResultsMsg.textContent = 'No 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 } = item; | |
| // Skip if owner is 'None' | |
| if (owner === 'None') { | |
| return; | |
| } | |
| // Create grid item - Apply rotating pastel colors | |
| const gridItem = document.createElement('div'); | |
| gridItem.className = 'grid-item'; | |
| // Header | |
| const header = document.createElement('div'); | |
| header.className = 'grid-header'; | |
| // Header top part with rank | |
| const headerTop = document.createElement('div'); | |
| headerTop.className = 'grid-header-top'; | |
| // Title | |
| const titleEl = document.createElement('h3'); | |
| titleEl.textContent = title; | |
| titleEl.title = title; // For tooltip on hover | |
| headerTop.appendChild(titleEl); | |
| // Rank badge | |
| const rankBadge = document.createElement('div'); | |
| rankBadge.className = 'rank-badge'; | |
| rankBadge.textContent = `#${rank}`; | |
| headerTop.appendChild(rankBadge); | |
| header.appendChild(headerTop); | |
| // Grid meta info | |
| const metaInfo = document.createElement('div'); | |
| metaInfo.className = 'grid-meta'; | |
| // Owner info | |
| const ownerEl = document.createElement('div'); | |
| ownerEl.className = 'owner-info'; | |
| ownerEl.textContent = `by ${owner}`; | |
| metaInfo.appendChild(ownerEl); | |
| // Likes counter | |
| const likesCounter = document.createElement('div'); | |
| likesCounter.className = 'likes-counter'; | |
| likesCounter.innerHTML = '♥ <span>' + likes_count + '</span>'; | |
| metaInfo.appendChild(likesCounter); | |
| header.appendChild(metaInfo); | |
| // Add header to grid item | |
| gridItem.appendChild(header); | |
| // Content area | |
| const content = document.createElement('div'); | |
| content.className = 'grid-content'; | |
| // iframe container | |
| const iframeContainer = document.createElement('div'); | |
| iframeContainer.className = 'iframe-container'; | |
| // Create iframe to display the content | |
| const iframe = document.createElement('iframe'); | |
| const directUrl = createDirectUrl(owner, name); | |
| iframe.src = directUrl; | |
| iframe.title = title; | |
| // Remove microphone permission | |
| iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;'; | |
| iframe.setAttribute('allowfullscreen', ''); | |
| iframe.setAttribute('frameborder', '0'); | |
| iframe.loading = 'lazy'; // Lazy load iframes for better performance | |
| // Track this space | |
| const spaceKey = `${owner}/${name}`; | |
| state.staticModeAttempted[spaceKey] = false; | |
| // Handle iframe loading errors | |
| iframe.onerror = function() { | |
| if (!state.staticModeAttempted[spaceKey]) { | |
| // Try static mode | |
| state.staticModeAttempted[spaceKey] = true; | |
| iframe.src = directUrl + '/index.html'; | |
| } else { | |
| // If static mode also failed, show error with random emoji | |
| handleIframeError(iframe, owner, name, title); | |
| } | |
| }; | |
| // Advanced error handling for iframe load | |
| iframe.onload = function() { | |
| try { | |
| // Try to access iframe content to check if it loaded properly | |
| const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; | |
| // Check if we got a 404 page or other error by looking for certain elements | |
| const isErrorPage = iframeDoc.title.includes('404') || | |
| iframeDoc.body.textContent.includes('404') || | |
| iframeDoc.body.textContent.includes('not found'); | |
| if (isErrorPage) { | |
| // For 404 errors, show random emoji | |
| if (!state.staticModeAttempted[spaceKey]) { | |
| // Try static mode first | |
| state.staticModeAttempted[spaceKey] = true; | |
| iframe.src = directUrl + '/index.html'; | |
| } else { | |
| // If static mode already attempted and still failing, show random emoji | |
| handleIframeError(iframe, owner, name, title); | |
| } | |
| } | |
| } catch (e) { | |
| // Cross-origin errors are expected, this generally means the iframe loaded | |
| // If we need to check for static mode, we do it based on other signals | |
| // We can try detecting failed loads by using a timer and checking if the iframe content is visible | |
| setTimeout(() => { | |
| // This is a basic heuristic - if the iframe still has no visible content after 5s, try static mode | |
| if (!state.staticModeAttempted[spaceKey] && | |
| (iframe.clientHeight < 10 || iframe.clientWidth < 10)) { | |
| state.staticModeAttempted[spaceKey] = true; | |
| iframe.src = directUrl + '/index.html'; | |
| } | |
| }, 5000); | |
| } | |
| }; | |
| iframeContainer.appendChild(iframe); | |
| content.appendChild(iframeContainer); | |
| // Actions section at bottom | |
| const actions = document.createElement('div'); | |
| actions.className = 'grid-actions'; | |
| // Open link | |
| const linkEl = document.createElement('a'); | |
| linkEl.href = url; | |
| linkEl.target = '_blank'; | |
| linkEl.className = 'open-link'; | |
| linkEl.textContent = 'Open in new window'; | |
| actions.appendChild(linkEl); | |
| // Add content and actions to grid item | |
| gridItem.appendChild(content); | |
| gridItem.appendChild(actions); | |
| // Add grid item to container | |
| elements.gridContainer.appendChild(gridItem); | |
| } catch (error) { | |
| console.error('Item rendering error:', error); | |
| // Continue rendering other items even if one fails | |
| } | |
| }); | |
| } | |
| // Filter event listeners | |
| elements.searchInput.addEventListener('input', () => { | |
| // Debounce input to prevent API calls on every keystroke | |
| clearTimeout(state.searchTimeout); | |
| state.searchTimeout = setTimeout(() => loadSpaces(0), 300); | |
| }); | |
| // Enter key in search box | |
| elements.searchInput.addEventListener('keyup', (event) => { | |
| if (event.key === 'Enter') { | |
| loadSpaces(0); | |
| } | |
| }); | |
| // Refresh button event listener | |
| elements.refreshButton.addEventListener('click', () => loadSpaces(0)); | |
| // Stats toggle button event listener | |
| elements.statsToggle.addEventListener('click', toggleStats); | |
| // Mac buttons functionality (just for show) | |
| document.querySelectorAll('.mac-button').forEach(button => { | |
| button.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| // Mac buttons don't do anything, just for style | |
| }); | |
| }); | |
| // Page load complete event detection | |
| window.addEventListener('load', function() { | |
| // Start loading data when page is fully loaded | |
| setTimeout(() => loadSpaces(0), 500); | |
| }); | |
| // Safety mechanism to prevent infinite loading | |
| 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); // Force end loading state after 20 seconds | |
| // Start loading immediately - dual call with window.load for reliability | |
| loadSpaces(0); | |
| </script> | |
| </body> | |
| </html> | |
| ''') | |
| # Use port 7860 for Huggingface Spaces | |
| app.run(host='0.0.0.0', port=7860) |