Spaces:
Running
Running
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": 10000} # Get max 10000 to fetch more spaces | |
# 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; | |
} | |
/* 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; | |
} | |
</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 Leaderboard</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: [], | |
iframeStatuses: {} // Track iframe loading status | |
}; | |
// Advanced iframe loader for better error detection | |
const iframeLoader = { | |
checkQueue: {}, | |
maxAttempts: 5, // Try multiple times | |
checkInterval: 5000, // Check every 5 seconds | |
// Start checking iframe loading status | |
startChecking: function(iframe, owner, name, title, spaceKey) { | |
// Initialize tracking | |
this.checkQueue[spaceKey] = { | |
iframe: iframe, | |
owner: owner, | |
name: name, | |
title: title, | |
attempts: 0, | |
status: 'loading' | |
}; | |
// Start recursive checking | |
this.checkIframeStatus(spaceKey); | |
}, | |
// Check iframe loading status | |
checkIframeStatus: function(spaceKey) { | |
if (!this.checkQueue[spaceKey]) return; | |
const item = this.checkQueue[spaceKey]; | |
const iframe = item.iframe; | |
// If already processed, stop checking | |
if (item.status !== 'loading') { | |
delete this.checkQueue[spaceKey]; | |
return; | |
} | |
// Increment attempt counter | |
item.attempts++; | |
try { | |
// 1. Check if iframe was removed from DOM | |
if (!iframe || !iframe.parentNode) { | |
delete this.checkQueue[spaceKey]; | |
return; | |
} | |
// 2. Check if content has loaded | |
try { | |
const hasContent = iframe.contentWindow && | |
iframe.contentWindow.document && | |
iframe.contentWindow.document.body; | |
// 2.1 If content exists and has actual content loaded | |
if (hasContent && iframe.contentWindow.document.body.innerHTML.length > 100) { | |
// Check if it contains error text | |
const bodyText = iframe.contentWindow.document.body.textContent.toLowerCase(); | |
if (bodyText.includes('forbidden') || | |
bodyText.includes('404') || | |
bodyText.includes('not found') || | |
bodyText.includes('error')) { | |
item.status = 'error'; | |
handleIframeError(iframe, item.owner, item.name, item.title); | |
} else { | |
item.status = 'success'; | |
} | |
delete this.checkQueue[spaceKey]; | |
return; | |
} | |
} catch(e) { | |
// Cross-origin access errors are expected - might be normal loading | |
} | |
// 3. Check iframe's visible size | |
const rect = iframe.getBoundingClientRect(); | |
if (rect.width > 50 && rect.height > 50 && item.attempts > 2) { | |
// If it has sufficient size, mark as success | |
item.status = 'success'; | |
delete this.checkQueue[spaceKey]; | |
return; | |
} | |
// 4. If we've reached max attempts | |
if (item.attempts >= this.maxAttempts) { | |
// Final check: is iframe visible? | |
if (iframe.offsetWidth > 0 && iframe.offsetHeight > 0) { | |
// If visible, mark as success | |
item.status = 'success'; | |
} else { | |
// If still not visible, mark as error | |
item.status = 'error'; | |
handleIframeError(iframe, item.owner, item.name, item.title); | |
} | |
delete this.checkQueue[spaceKey]; | |
return; | |
} | |
// Schedule next check with exponential backoff | |
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 error occurs, try a few more times | |
if (item.attempts >= this.maxAttempts) { | |
item.status = 'error'; | |
handleIframeError(iframe, item.owner, item.name, item.title); | |
delete this.checkQueue[spaceKey]; | |
} else { | |
// Try again | |
setTimeout(() => this.checkIframeStatus(spaceKey), this.checkInterval); | |
} | |
} | |
} | |
}; | |
// 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' | |
}, | |
// Ensure all labels are shown without gaps | |
ticks: { | |
autoSkip: false, | |
font: function(context) { | |
// Adjust font size to fit all labels if needed | |
const defaultSize = 11; | |
return { | |
size: labels.length > 20 ? defaultSize - 1 : defaultSize | |
}; | |
} | |
} | |
} | |
} | |
} | |
}); | |
} | |
// 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 response.json(); | |
// 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 fallback error message | |
function handleIframeError(iframe, owner, name, title) { | |
const container = iframe.parentNode; | |
// Error message container | |
const errorPlaceholder = document.createElement('div'); | |
errorPlaceholder.className = 'error-placeholder'; | |
// Error message | |
const errorMessage = document.createElement('p'); | |
errorMessage.textContent = `"${title}" space couldn't be loaded`; | |
errorPlaceholder.appendChild(errorMessage); | |
// 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 | |
// Unique ID for this iframe | |
const iframeId = `iframe-${owner}-${name}`; | |
iframe.id = iframeId; | |
// Track this space | |
const spaceKey = `${owner}/${name}`; | |
state.iframeStatuses[spaceKey] = 'loading'; | |
// Use the advanced loader for better error detection | |
iframe.onload = function() { | |
iframeLoader.startChecking(iframe, owner, name, title, spaceKey); | |
}; | |
// Direct error handling | |
iframe.onerror = function() { | |
handleIframeError(iframe, owner, name, title); | |
state.iframeStatuses[spaceKey] = 'error'; | |
}; | |
// Final fallback - if nothing has happened after 30 seconds, show error | |
setTimeout(() => { | |
if (state.iframeStatuses[spaceKey] === 'loading') { | |
handleIframeError(iframe, owner, name, title); | |
state.iframeStatuses[spaceKey] = 'error'; | |
} | |
}, 30000); | |
// Add iframe to container | |
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); | |
// Display loading indicator control | |
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'; | |
} | |
} | |
// 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'; | |
} | |
} | |
</script> | |
</body> | |
</html> | |
''') | |
# Use port 7860 for Huggingface Spaces | |
app.run(host='0.0.0.0', port=7860) | |