ZeroGPU-Leader / app.py
seawolf2357's picture
Update app.py
b1a8e9d verified
raw
history blame
35.3 kB
from flask import Flask, render_template, request, jsonify
import requests
import os
import time
app = Flask(__name__)
# Function to fetch trending spaces from Huggingface with pagination
def fetch_trending_spaces(offset=0, limit=72):
try:
# 단순하게 데이터 가져오기
url = "https://huggingface.co/api/spaces"
params = {"limit": 500} # 최대 500개 가져오기
# 타임아웃 늘리기
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']
# 요청된 offset과 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
}
else:
print(f"Error fetching spaces: {response.status_code}")
# 빈 공간 반환하지만 200개 제한의 가짜 데이터
return {
'spaces': generate_dummy_spaces(limit),
'total': 200,
'offset': offset,
'limit': limit
}
except Exception as e:
print(f"Exception when fetching spaces: {e}")
# 가짜 데이터 생성
return {
'spaces': generate_dummy_spaces(limit),
'total': 200,
'offset': offset,
'limit': limit
}
# 오류 시 가짜 데이터 생성
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. '.' 문자를 '-'로 변경
name = name.replace('.', '-')
# 2. '_' 문자를 '-'로 변경
name = name.replace('_', '-')
# 3. 대소문자 구분 없이 모두 소문자로 변경
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:
# 공통 정보 추출
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', '')
# None이 포함된 경우 무시
if owner == 'None' or name == 'None':
return None
# URL 구성
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', name)
# 태그
tags = space_data.get('tags', [])
return {
'url': original_url,
'embedUrl': embed_url,
'title': title,
'owner': owner,
'name': name, # Space 이름 추가 저장
'likes_count': likes_count,
'tags': tags,
'rank': offset + index + 1
}
except Exception as e:
print(f"Error processing space data: {e}")
# 오류 발생 시에도 기본 객체 반환
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
}
# Homepage route
@app.route('/')
def home():
return render_template('index.html')
# Trending spaces API
@app.route('/api/trending-spaces', methods=['GET'])
def trending_spaces():
search_query = request.args.get('search', '').lower()
offset = int(request.args.get('offset', 0))
limit = int(request.args.get('limit', 72)) # 기본값 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)
return jsonify({
'spaces': results,
'total': spaces_data['total'],
'offset': offset,
'limit': limit
})
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>
<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-icon {
font-size: 3rem;
margin-bottom: 1rem;
color: #e53e3e;
}
/* 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;
}
/* 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;
}
}
</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>
<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')
};
// 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
};
// 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';
}
}
// 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;
renderGrid(data.spaces);
renderPagination();
} 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
function handleIframeError(iframe, owner, name, title) {
const container = iframe.parentNode;
// Error message container
const errorPlaceholder = document.createElement('div');
errorPlaceholder.className = 'error-placeholder';
// Error icon
const errorIcon = document.createElement('div');
errorIcon.className = 'error-icon';
errorIcon.textContent = '⚠️';
errorPlaceholder.appendChild(errorIcon);
// 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
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 && !state.staticModeAttempted[spaceKey]) {
// If it's an error page and we haven't tried static mode yet
state.staticModeAttempted[spaceKey] = true;
iframe.src = directUrl + '/index.html';
} else if (isErrorPage) {
// If static mode already attempted and still failing
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));
// 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)