Spaces:
Sleeping
Sleeping
import requests | |
import gradio as gr | |
from datetime import datetime | |
import random | |
from selenium import webdriver | |
from selenium.webdriver.support.ui import WebDriverWait | |
from selenium.webdriver.support import expected_conditions as EC | |
from selenium.webdriver.common.by import By | |
from selenium.common.exceptions import WebDriverException, TimeoutException | |
from PIL import Image | |
from io import BytesIO | |
import base64 | |
import time | |
def take_screenshot(url): | |
"""웹사이트 스크린샷 촬영 함수 (로딩 대기 시간 추가)""" | |
if not url.startswith('http'): | |
url = f"https://{url}" | |
options = webdriver.ChromeOptions() | |
options.add_argument('--headless') | |
options.add_argument('--no-sandbox') | |
options.add_argument('--disable-dev-shm-usage') | |
options.add_argument('--window-size=1080,720') | |
try: | |
driver = webdriver.Chrome(options=options) | |
driver.get(url) | |
# 명시적 대기: body 요소가 로드될 때까지 대기 (최대 10초) | |
try: | |
WebDriverWait(driver, 10).until( | |
EC.presence_of_element_located((By.TAG_NAME, "body")) | |
) | |
except TimeoutException: | |
print(f"페이지 로딩 타임아웃: {url}") | |
# 추가 대기 시간 (1초) | |
time.sleep(1) | |
# JavaScript 실행 완료 대기 | |
driver.execute_script("return document.readyState") == "complete" | |
# 스크린샷 촬영 | |
screenshot = driver.get_screenshot_as_png() | |
img = Image.open(BytesIO(screenshot)) | |
buffered = BytesIO() | |
img.save(buffered, format="PNG") | |
return base64.b64encode(buffered.getvalue()).decode() | |
except WebDriverException as e: | |
print(f"스크린샷 촬영 실패: {str(e)} for URL: {url}") | |
return None | |
except Exception as e: | |
print(f"예상치 못한 오류: {str(e)} for URL: {url}") | |
return None | |
finally: | |
if 'driver' in locals(): | |
driver.quit() | |
USERNAME = "openfree" | |
def format_timestamp(timestamp): | |
if not timestamp: | |
return 'N/A' | |
try: | |
# 문자열인 경우 | |
if isinstance(timestamp, str): | |
dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00')) | |
# 정수(밀리초)인 경우 | |
elif isinstance(timestamp, (int, float)): | |
dt = datetime.fromtimestamp(timestamp / 1000) # 밀리초를 초로 변환 | |
else: | |
return 'N/A' | |
return dt.strftime('%Y-%m-%d %H:%M') | |
except Exception as e: | |
print(f"Timestamp conversion error: {str(e)} for timestamp: {timestamp}") | |
return 'N/A' | |
def should_exclude_space(space_name): | |
"""특정 스페이스를 제외하는 필터 함수""" | |
exclude_keywords = [ | |
'mixgen3', 'ginid', 'mouse', 'flxtrainlora', | |
'vidslicegpu', 'stickimg', 'ultpixgen', 'SORA', | |
'badassgi', 'newsplus', 'chargen', 'news', | |
'testhtml' | |
] | |
return any(keyword.lower() in space_name.lower() for keyword in exclude_keywords) | |
def get_pastel_color(index): | |
"""Generate unique pastel colors based on index""" | |
pastel_colors = [ | |
'#FFE6E6', # 연한 분홍 | |
'#FFE6FF', # 연한 보라 | |
'#E6E6FF', # 연한 파랑 | |
'#E6FFFF', # 연한 하늘 | |
'#E6FFE6', # 연한 초록 | |
'#FFFFE6', # 연한 노랑 | |
'#FFF0E6', # 연한 주황 | |
'#F0E6FF', # 연한 라벤더 | |
'#FFE6F0', # 연한 로즈 | |
'#E6FFF0', # 연한 민트 | |
'#F0FFE6', # 연한 라임 | |
'#FFE6EB', # 연한 코랄 | |
'#E6EBFF', # 연한 퍼플블루 | |
'#FFE6F5', # 연한 핑크 | |
'#E6FFF5', # 연한 터코이즈 | |
'#F5E6FF', # 연한 모브 | |
'#FFE6EC', # 연한 살몬 | |
'#E6FFEC', # 연한 스프링그린 | |
'#ECE6FF', # 연한 페리윙클 | |
'#FFE6F7', # 연한 매그놀리아 | |
] | |
return pastel_colors[index % len(pastel_colors)] | |
def get_space_card(space, index): | |
"""Generate HTML card for a space with colorful design and lots of emojis""" | |
space_id = space.get('id', '') | |
space_name = space_id.split('/')[-1] | |
likes = space.get('likes', 0) | |
created_at = format_timestamp(space.get('createdAt')) | |
sdk = space.get('sdk', 'N/A') | |
# SDK별 이모지 및 관련 이모지 세트 | |
sdk_emoji_sets = { | |
'gradio': { | |
'main': '🎨', | |
'related': ['🖼️', '🎭', '🎪', '🎠', '🎡', '🎢', '🎯', '🎲', '🎰', '🎳'] | |
}, | |
'streamlit': { | |
'main': '⚡', | |
'related': ['💫', '✨', '⭐', '🌟', '💥', '⚡', '🔥', '🌈', '🎆', '🎇'] | |
}, | |
'docker': { | |
'main': '🐳', | |
'related': ['🐋', '🌊', '🌍', '🚢', '⛴️', '🛥️', '🐠', '🐡', '🦈', '🐬'] | |
}, | |
'static': { | |
'main': '📄', | |
'related': ['📝', '📰', '📑', '🗂️', '📁', '📂', '📚', '📖', '📒', '📔'] | |
}, | |
'panel': { | |
'main': '📊', | |
'related': ['📈', '📉', '💹', '📋', '📌', '📍', '🗺️', '🎯', '📐', '📏'] | |
}, | |
'N/A': { | |
'main': '🔧', | |
'related': ['🔨', '⚒️', '🛠️', '⚙️', '🔩', '⛏️', '⚡', '🔌', '💡', '🔋'] | |
} | |
} | |
# SDK에 따른 이모지 선택 | |
sdk_lower = sdk.lower() | |
bg_color = get_pastel_color(index) # 인덱스 기반 색상 선택 | |
emoji_set = sdk_emoji_sets.get(sdk_lower, sdk_emoji_sets['N/A']) | |
main_emoji = emoji_set['main'] | |
# 랜덤하게 3개의 관련 이모지 선택 | |
decorative_emojis = random.sample(emoji_set['related'], 3) | |
# 추가 장식용 이모지 | |
general_emojis = ['🚀', '💫', '⭐', '🌟', '✨', '💥', '🔥', '🌈', '🎯', '🎨', | |
'🎭', '🎪', '🎢', '🎡', '🎠', '🎪', '🎭', '🎨', '🎯', '🎲'] | |
random_emojis = random.sample(general_emojis, 3) | |
# 좋아요 수에 따른 하트 이모지 | |
heart_emoji = '❤️' if likes > 100 else '💖' if likes > 50 else '💝' if likes > 10 else '🤍' | |
return f""" | |
<div style='border: none; | |
padding: 25px; | |
margin: 15px; | |
border-radius: 20px; | |
background-color: {bg_color}; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
transition: all 0.3s ease-in-out; | |
position: relative; | |
overflow: hidden;' | |
onmouseover='this.style.transform="translateY(-5px) scale(1.02)"; this.style.boxShadow="0 8px 25px rgba(0,0,0,0.15)"' | |
onmouseout='this.style.transform="translateY(0) scale(1)"; this.style.boxShadow="0 4px 15px rgba(0,0,0,0.1)"'> | |
<div style='position: absolute; top: -15px; right: -15px; font-size: 100px; opacity: 0.1;'> | |
{main_emoji} | |
</div> | |
<div style='position: absolute; top: 10px; right: 10px; font-size: 20px;'> | |
{decorative_emojis[0]} | |
</div> | |
<div style='position: absolute; bottom: 10px; left: 10px; font-size: 20px;'> | |
{decorative_emojis[1]} | |
</div> | |
<div style='position: absolute; top: 50%; right: 10px; font-size: 20px;'> | |
{decorative_emojis[2]} | |
</div> | |
<h3 style='color: #2d2d2d; | |
margin: 0 0 20px 0; | |
font-size: 1.4em; | |
display: flex; | |
align-items: center; | |
gap: 10px;'> | |
<span style='font-size: 1.3em'>{random_emojis[0]}</span> | |
<a href='https://huggingface.co/spaces/{space_id}' target='_blank' | |
style='text-decoration: none; color: #2d2d2d;'> | |
{space_name} | |
</a> | |
<span style='font-size: 1.3em'>{random_emojis[1]}</span> | |
</h3> | |
<div style='margin: 15px 0; color: #444; background: rgba(255,255,255,0.5); | |
padding: 15px; border-radius: 12px;'> | |
<p style='margin: 8px 0;'> | |
<strong>SDK:</strong> {main_emoji} {sdk} {decorative_emojis[0]} | |
</p> | |
<p style='margin: 8px 0;'> | |
<strong>Created:</strong> 📅 {created_at} ⏰ | |
</p> | |
<p style='margin: 8px 0;'> | |
<strong>Likes:</strong> {heart_emoji} {likes} {random_emojis[2]} | |
</p> | |
</div> | |
<div style='margin-top: 20px; | |
display: flex; | |
justify-content: space-between; | |
align-items: center;'> | |
<a href='https://huggingface.co/spaces/{space_id}' target='_blank' | |
style='background: linear-gradient(45deg, #0084ff, #00a3ff); | |
color: white; | |
padding: 10px 20px; | |
border-radius: 15px; | |
text-decoration: none; | |
display: inline-flex; | |
align-items: center; | |
gap: 8px; | |
font-weight: 500; | |
transition: all 0.3s; | |
box-shadow: 0 2px 8px rgba(0,132,255,0.3);' | |
onmouseover='this.style.transform="scale(1.05)"; this.style.boxShadow="0 4px 12px rgba(0,132,255,0.4)"' | |
onmouseout='this.style.transform="scale(1)"; this.style.boxShadow="0 2px 8px rgba(0,132,255,0.3)"'> | |
<span>View Space</span> 🚀 {random_emojis[0]} | |
</a> | |
<span style='color: #666; font-size: 0.9em; opacity: 0.7;'> | |
🆔 {space_id} {decorative_emojis[2]} | |
</span> | |
</div> | |
</div> | |
""" | |
def get_vercel_deployments(): | |
"""Vercel API를 통해 모든 배포된 서비스 정보 가져오기 (페이지네이션 적용)""" | |
token = "A8IFZmgW2cqA4yUNlLPnci0N" | |
base_url = "https://api.vercel.com/v6/deployments" | |
all_deployments = [] | |
has_next = True | |
page = 1 | |
until = None # 첫 요청에서는 until 파라미터 없음 | |
headers = { | |
"Authorization": f"Bearer {token}", | |
"Content-Type": "application/json" | |
} | |
try: | |
while has_next: | |
# URL 구성 (페이지네이션 파라미터 포함) | |
url = f"{base_url}?limit=100" | |
if until: | |
url += f"&until={until}" | |
print(f"Fetching page {page}... URL: {url}") # 디버깅용 | |
response = requests.get(url, headers=headers) | |
if response.status_code != 200: | |
print(f"Vercel API Error: {response.text}") | |
break | |
data = response.json() | |
current_deployments = data.get('deployments', []) | |
if not current_deployments: # 더 이상 데이터가 없으면 종료 | |
break | |
all_deployments.extend(current_deployments) | |
# 다음 페이지를 위한 until 값 설정 | |
pagination = data.get('pagination', {}) | |
until = pagination.get('next') | |
has_next = bool(until) # until 값이 있으면 다음 페이지 존재 | |
print(f"Page {page} fetched. Got {len(current_deployments)} deployments") # 디버깅용 | |
page += 1 | |
print(f"Total deployments fetched: {len(all_deployments)}") # 디버깅용 | |
# 상태가 'READY'이고 'url'이 있는 배포만 필터링하고 'javis1' 제외 | |
active_deployments = [ | |
dep for dep in all_deployments | |
if dep.get('state') == 'READY' and | |
dep.get('url') and | |
'javis1' not in dep.get('name', '').lower() | |
] | |
print(f"Active deployments after filtering: {len(active_deployments)}") # 디버깅용 | |
return active_deployments | |
except Exception as e: | |
print(f"Error fetching Vercel deployments: {str(e)}") | |
return [] | |
def get_vercel_card(deployment, index, is_top_best=False): | |
"""Vercel 배포 카드 HTML 생성 함수""" | |
raw_url = deployment.get('url', '') | |
# URL 처리 | |
if raw_url.startswith('http'): | |
url = raw_url | |
else: | |
url = f"https://{raw_url}" | |
name = deployment.get('name', '이름 없는 프로젝트') | |
# 카드 ID 생성 | |
card_id = f"vercel-card-{url.replace('.', '-').replace('/', '-')}" | |
# Top Best 항목일 경우의 스크린샷 처리 | |
screenshot_html = "" | |
if is_top_best: | |
try: | |
print(f"스크린샷 캡처 시도: {url}") # 디버깅용 로그 | |
screenshot_base64 = take_screenshot(raw_url) | |
if screenshot_base64: | |
screenshot_html = f""" | |
<div style="width: 100%; height: 200px; overflow: hidden; border-radius: 10px; margin-bottom: 15px;"> | |
<img src="data:image/png;base64,{screenshot_base64}" | |
style="width: 100%; height: 100%; object-fit: cover;" | |
alt="{name} 스크린샷"/> | |
</div> | |
""" | |
else: | |
print(f"스크린샷 캡처 실패: {url}") # 디버깅용 로그 | |
except Exception as e: | |
print(f"스크린샷 처리 오류: {str(e)} for URL: {url}") # 디버깅용 로그 | |
bg_color = get_pastel_color(index + (20 if not is_top_best else 0)) | |
tech_emojis = ['⚡', '🚀', '🌟', '✨', '💫', '🔥', '🌈', '🎯', '🎨', '🔮'] | |
random_emojis = random.sample(tech_emojis, 3) | |
# Top Best 카드의 간소화된 정보 섹션 | |
if is_top_best: | |
info_section = f""" | |
<div style='margin: 15px 0; color: #444; background: rgba(255,255,255,0.5); | |
padding: 15px; border-radius: 12px;'> | |
<p style='margin: 8px 0;'> | |
<strong>URL:</strong> 🔗 {url} | |
</p> | |
</div> | |
""" | |
else: | |
info_section = f""" | |
<div style='margin: 15px 0; color: #444; background: rgba(255,255,255,0.5); | |
padding: 15px; border-radius: 12px;'> | |
<p style='margin: 8px 0;'> | |
<strong>Status:</strong> ✅ {deployment.get('state', 'N/A')} | |
</p> | |
<p style='margin: 8px 0;'> | |
<strong>Created:</strong> 📅 {format_timestamp(deployment.get('created'))} | |
</p> | |
<p style='margin: 8px 0;'> | |
<strong>URL:</strong> 🔗 {url} | |
</p> | |
</div> | |
""" | |
return f""" | |
<div id="{card_id}" class="vercel-card" | |
data-likes="0" | |
style='border: none; | |
padding: 25px; | |
margin: 15px; | |
border-radius: 20px; | |
background-color: {bg_color}; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.1); | |
transition: all 0.3s ease-in-out; | |
position: relative; | |
overflow: hidden;' | |
onmouseover='this.style.transform="translateY(-5px) scale(1.02)"; this.style.boxShadow="0 8px 25px rgba(0,0,0,0.15)"' | |
onmouseout='this.style.transform="translateY(0) scale(1)"; this.style.boxShadow="0 4px 15px rgba(0,0,0,0.1)"'> | |
{screenshot_html} | |
<h3 style='color: #2d2d2d; | |
margin: 0 0 20px 0; | |
font-size: 1.4em; | |
display: flex; | |
align-items: center; | |
gap: 10px;'> | |
<span style='font-size: 1.3em'>{random_emojis[0]}</span> | |
<a href='{url}' target='_blank' | |
style='text-decoration: none; color: #2d2d2d;'> | |
{name} | |
</a> | |
<span style='font-size: 1.3em'>{random_emojis[1]}</span> | |
</h3> | |
{info_section} | |
<div style='margin-top: 20px; display: flex; justify-content: space-between; align-items: center;'> | |
<div class="like-section" style="display: flex; align-items: center; gap: 10px;"> | |
<button onclick="toggleLike('{card_id}')" class="like-button" | |
style="background: none; border: none; cursor: pointer; font-size: 1.5em; padding: 5px 10px;"> | |
🤍 | |
</button> | |
<span class="like-count" style="font-size: 1.2em; color: #666;">0</span> | |
</div> | |
<a href='{url}' target='_blank' | |
style='background: linear-gradient(45deg, #0084ff, #00a3ff); | |
color: white; | |
padding: 10px 20px; | |
border-radius: 15px; | |
text-decoration: none; | |
display: inline-flex; | |
align-items: center; | |
gap: 8px; | |
font-weight: 500; | |
transition: all 0.3s; | |
box-shadow: 0 2px 8px rgba(0,132,255,0.3);' | |
onmouseover='this.style.transform="scale(1.05)"; this.style.boxShadow="0 4px 12px rgba(0,132,255,0.4)"' | |
onmouseout='this.style.transform="scale(1)"; this.style.boxShadow="0 2px 8px rgba(0,132,255,0.3)"'> | |
<span>View Deployment</span> 🚀 {random_emojis[0]} | |
</a> | |
</div> | |
</div> | |
""" | |
# Top Best URLs 정의 | |
TOP_BEST_URLS = [ | |
{ | |
"url": "dekvxz.vercel.app", | |
"name": "[게임] 다이어트 헌터", | |
"created": "2024-11-20 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "jtufui.vercel.app", | |
"name": "[게임] 테러리스트", | |
"created": "2024-11-20 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "https://huggingface.co/spaces/openfree/ggumim", | |
"name": "[MOUSE-II] 이미지에 한글 출력", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "xabtnc.vercel.app", | |
"name": "[ChatGPT] 나만의 LLM", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "https://huggingface.co/spaces/openfree/ifbhdc", | |
"name": "[게임] 보석 팡팡", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "nxhquk.vercel.app", | |
"name": "[게임] 테트리스", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "bydcnd.vercel.app", | |
"name": "[모델] 3D 분자 모형", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "ijhama.vercel.app", | |
"name": "투자 포트폴리오 분석", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "oschnl.vercel.app", | |
"name": "로또 번호 분석/추천", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "rzwzrq.vercel.app", | |
"name": "엑셀/CSV 데이터 분석", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "twkqre.vercel.app", | |
"name": "[운세] 타로카드", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "htwymz.vercel.app", | |
"name": "[게임] 소방헬기", | |
"created": "2024-11-20 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "mktmbn.vercel.app", | |
"name": "[게임] 우주전쟁", | |
"created": "2024-11-19 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "euguwt.vercel.app", | |
"name": "[게임] 포세이돈", | |
"created": "2024-11-19 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "qmdzoh.vercel.app", | |
"name": "[게임] 하늘을 지켜라", | |
"created": "2024-11-19 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "kofaqo.vercel.app", | |
"name": "[게임] 운석 충돌!", | |
"created": "2024-11-19 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "qoqqkq.vercel.app", | |
"name": "[게임] 두더쥐 잡기", | |
"created": "2024-11-19 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "nmznel.vercel.app", | |
"name": "[게임] 고양이 전용", | |
"created": "2024-11-19 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "psrrtp.vercel.app", | |
"name": "[대시보드] 세계 인구", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "xxloav.vercel.app", | |
"name": "[게임] 벽돌 깨기", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "https://huggingface.co/spaces/openfree/edpaje", | |
"name": "[게임] 기억력 카드", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "https://huggingface.co/spaces/openfree/ixtidb", | |
"name": "AI 요리사", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "cnlzji.vercel.app", | |
"name": "국가 정보 비교", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "fazely.vercel.app", | |
"name": "Wikipedia 지식 분석", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "pkzhbo.vercel.app", | |
"name": "세계 국가별 시간대", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "pammgl.vercel.app", | |
"name": "보도자료 배포 서비스", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "https://ktduhm.vercel.app/", | |
"name": "수학을 그래프로 이해", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "vjmfoy.vercel.app", | |
"name": "[게임] 3D 벽돌쌓기", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "aodakf.vercel.app", | |
"name": "[버추얼] 3D 가상현실", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
}, | |
{ | |
"url": "mxoeue.vercel.app", | |
"name": "음성 생성(TTS),조정", | |
"created": "2024-11-18 00:00", | |
"state": "READY" | |
} | |
] | |
def get_user_spaces(): | |
# 기존 Hugging Face 스페이스 가져오기 | |
url = f"https://huggingface.co/api/spaces?author={USERNAME}&limit=500" | |
headers = { | |
"Accept": "application/json", | |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" | |
} | |
try: | |
# Hugging Face 스페이스 가져오기 | |
response = requests.get(url, headers=headers) | |
spaces_data = response.json() if response.status_code == 200 else [] | |
# 제외할 스페이스 필터링 | |
user_spaces = [ | |
space for space in spaces_data | |
if not should_exclude_space(space.get('id', '').split('/')[-1]) | |
] | |
# TOP_BEST_URLS 항목 수 | |
top_best_count = len(TOP_BEST_URLS) | |
# Vercel API를 통한 실제 배포 수 | |
vercel_deployments = get_vercel_deployments() | |
actual_vercel_count = len(vercel_deployments) if vercel_deployments else 0 | |
html_content = f""" | |
<div style=' | |
min-height: 100vh; | |
background: linear-gradient(135deg, #f6f8ff 0%, #f0f4ff 100%); | |
background-image: url("data:image/svg+xml,%3Csvg width='100' height='20' viewBox='0 0 100 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M21.184 20c.357-.13.72-.264 1.088-.402l1.768-.661C33.64 15.347 39.647 14 50 14c10.271 0 15.362 1.222 24.629 4.928.955.383 1.869.74 2.75 1.072h6.225c-2.51-.73-5.139-1.691-8.233-2.928C65.888 13.278 60.562 12 50 12c-10.626 0-16.855 1.397-26.66 5.063l-1.767.662c-2.475.923-4.66 1.674-6.724 2.275h6.335zm0-20C13.258 2.892 8.077 4 0 4V2c5.744 0 9.951-.574 14.85-2h6.334zM77.38 0C85.239 2.966 90.502 4 100 4V2c-6.842 0-11.386-.542-16.396-2h-6.225zM0 14c8.44 0 13.718-1.21 22.272-4.402l1.768-.661C33.64 5.347 39.647 4 50 4c10.271 0 15.362 1.222 24.629 4.928C84.112 12.722 89.438 14 100 14v-2c-10.271 0-15.362-1.222-24.629-4.928C65.888 3.278 60.562 2 50 2 39.374 2 33.145 3.397 23.34 7.063l-1.767.662C13.223 10.84 8.163 12 0 12v2z' fill='%23f0f0f0' fill-opacity='0.2' fill-rule='evenodd'/%3E%3C/svg%3E"); | |
padding: 40px; | |
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;'> | |
<!-- 메인 헤더 --> | |
<div style=' | |
background: rgba(255, 255, 255, 0.8); | |
border-radius: 20px; | |
padding: 30px; | |
margin-bottom: 40px; | |
box-shadow: 0 4px 20px rgba(0,0,0,0.05); | |
backdrop-filter: blur(10px); | |
border: 1px solid rgba(255,255,255,0.8);'> | |
<h2 style=' | |
color: #2d2d2d; | |
margin: 0 0 15px 0; | |
font-size: 2em; | |
background: linear-gradient(45deg, #2d2d2d, #0084ff); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent;'> | |
공개 갤러리(생성 Web/App) by MOUSE | |
</h2> | |
<div style=' | |
background: linear-gradient(45deg, #0084ff, #00a3ff); | |
border-radius: 10px; | |
padding: 15px; | |
margin: 20px 0;'> | |
<a href='https://openfree-mouse.hf.space' | |
target='_blank' | |
style=' | |
color: white; | |
text-decoration: none; | |
font-size: 1.1em; | |
display: block; | |
text-align: center;'> | |
🚀 프롬프트만으로 나만의 웹서비스를 즉시 생성하는 MOUSE | |
</a> | |
</div> | |
<p style=' | |
color: #666; | |
margin: 0; | |
font-size: 0.9em; | |
text-align: center; | |
background: rgba(255,255,255,0.5); | |
padding: 10px; | |
border-radius: 10px;'> | |
Found {actual_vercel_count} Vercel deployments and {len(user_spaces)} Hugging Face spaces<br> | |
(Plus {top_best_count} featured items in Top Best section) | |
</p> | |
</div> | |
<!-- Top Best 섹션 --> | |
<div class="section-container" style=' | |
background: rgba(255, 255, 255, 0.4); | |
border-radius: 20px; | |
padding: 30px; | |
margin: 20px 0; | |
backdrop-filter: blur(10px);'> | |
<h3 style=' | |
color: #2d2d2d; | |
margin: 0 0 20px 0; | |
padding: 15px 25px; | |
background: rgba(255,255,255,0.7); | |
border-radius: 15px; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.05); | |
border-left: 5px solid #0084ff; | |
display: flex; | |
align-items: center; | |
gap: 10px;'> | |
<span style='font-size: 1.5em;'>🏆</span> | |
Top Best | |
</h3> | |
<div style=' | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
gap: 20px;'> | |
{"".join(get_vercel_card( | |
{"url": url["url"], "created": url["created"], "name": url["name"], "state": url["state"]}, | |
idx, | |
is_top_best=True | |
) for idx, url in enumerate(TOP_BEST_URLS))} | |
</div> | |
</div> | |
<!-- Vercel Deployments 섹션 --> | |
{f''' | |
<div class="section-container" style=' | |
background: rgba(255, 255, 255, 0.4); | |
border-radius: 20px; | |
padding: 30px; | |
margin: 20px 0; | |
backdrop-filter: blur(10px);'> | |
<h3 style=' | |
color: #2d2d2d; | |
margin: 0 0 20px 0; | |
padding: 15px 25px; | |
background: rgba(255,255,255,0.7); | |
border-radius: 15px; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.05); | |
border-left: 5px solid #00a3ff; | |
display: flex; | |
align-items: center; | |
gap: 10px;'> | |
<span style='font-size: 1.5em;'>⚡</span> | |
Vercel Deployments | |
</h3> | |
<div id="vercel-container" style=' | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
gap: 20px;'> | |
{"".join(get_vercel_card(dep, idx) for idx, dep in enumerate(vercel_deployments))} | |
</div> | |
</div> | |
''' if vercel_deployments else ''} | |
<!-- Hugging Face Spaces 섹션 --> | |
<div class="section-container" style=' | |
background: rgba(255, 255, 255, 0.4); | |
border-radius: 20px; | |
padding: 30px; | |
margin: 20px 0; | |
backdrop-filter: blur(10px);'> | |
<h3 style=' | |
color: #2d2d2d; | |
margin: 0 0 20px 0; | |
padding: 15px 25px; | |
background: rgba(255,255,255,0.7); | |
border-radius: 15px; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.05); | |
border-left: 5px solid #ff6b6b; | |
display: flex; | |
align-items: center; | |
gap: 10px;'> | |
<span style='font-size: 1.5em;'>🤗</span> | |
Hugging Face Spaces | |
</h3> | |
<div style=' | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
gap: 20px;'> | |
{"".join(get_space_card(space, idx) for idx, space in enumerate(user_spaces))} | |
</div> | |
</div> | |
</div> | |
<!-- 기존 JavaScript 코드는 그대로 유지 --> | |
<script> | |
// ... (기존 JavaScript 코드) | |
</script> | |
""" | |
return html_content | |
except Exception as e: | |
print(f"Error: {str(e)}") | |
return f""" | |
<div style='padding: 20px; text-align: center; color: #666;'> | |
<h2>Error occurred while fetching spaces</h2> | |
<p>Error details: {str(e)}</p> | |
<p>Please try again later.</p> | |
</div> | |
""" | |
# Creating the Gradio interface | |
demo = gr.Blocks() | |
with demo: | |
html_output = gr.HTML(value=get_user_spaces()) | |
if __name__ == "__main__": | |
demo.launch() | |