openfree's picture
Update app.py
b873a08 verified
raw
history blame
31.6 kB
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()