fantaxy's picture
Update app.py
eabd91e verified
from flask import Flask, render_template, request, jsonify
import requests
import os
from collections import Counter
app = Flask(__name__)
# 1) ํŠธ๋ Œ๋”ฉ ๋ชจ๋ธ์„ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜
def fetch_trending_models(offset=0, limit=72):
try:
url = "https://huggingface.co/api/models"
params = {"limit": 10000} # ๋งŽ์€ ๋ชจ๋ธ์„ ๊ฐ€์ ธ์˜ด
response = requests.get(url, params=params, timeout=30)
if response.status_code == 200:
models = response.json()
# ํ•„ํ„ฐ๋ง
filtered = [
m for m in models
if m.get('owner') != 'None' and m.get('id', '').split('/', 1)[0] != 'None'
]
start = min(offset, len(filtered))
end = min(offset + limit, len(filtered))
print(f"Fetched {len(filtered)} models, returning {end - start} from {start} to {end}.")
return {
'models': filtered[start:end],
'total': len(filtered),
'offset': offset,
'limit': limit,
'all_models': filtered
}
else:
# ์˜ค๋ฅ˜ ์‘๋‹ต ์‹œ ๋”๋ฏธ ๋ชจ๋ธ
print(f"Error fetching models: {response.status_code}")
return {
'models': generate_dummy_models(limit),
'total': 200,
'offset': offset,
'limit': limit,
'all_models': generate_dummy_models(500)
}
except Exception as e:
print("Exception when fetching models:", e)
return {
'models': generate_dummy_models(limit),
'total': 200,
'offset': offset,
'limit': limit,
'all_models': generate_dummy_models(500)
}
# 2) ๋”๋ฏธ ๋ชจ๋ธ ์ƒ์„ฑ ํ•จ์ˆ˜(์˜ค๋ฅ˜์‹œ ์‚ฌ์šฉ)
def generate_dummy_models(count):
dummy_list = []
for i in range(count):
dummy_list.append({
'id': f'dummy/model-{i}',
'owner': 'dummy',
'title': f'Example Model {i+1}',
'likes': 100 - i,
'downloads': 9999 - i, # ์ž„์˜์˜ ๋‹ค์šด๋กœ๋“œ ์ˆ˜
'createdAt': '2023-01-01T00:00:00.000Z',
'tags': ['dummy', 'fallback']
})
return dummy_list
# 3) ๋ชจ๋ธ URL ์ƒ์„ฑ
def transform_url(owner, name):
name = name.replace('.', '-').replace('_', '-').lower()
owner = owner.lower()
return f"https://huggingface.co/{owner}/{name}"
# 4) ๋ชจ๋ธ ์ƒ์„ธ์ •๋ณด ๊ฐ€๊ณต
def get_model_details(model_data, index, offset):
try:
if '/' in model_data.get('id', ''):
owner, name = model_data['id'].split('/', 1)
else:
owner = model_data.get('owner', '')
name = model_data.get('id', '')
if owner == 'None' or name == 'None':
return None
original_url = f"https://huggingface.co/{owner}/{name}"
embed_url = transform_url(owner, name)
# ๋‹ค์šด๋กœ๋“œ, ์ข‹์•„์š”
likes_count = model_data.get('likes', 0)
downloads_count = model_data.get('downloads', 0) # ๋‹ค์šด๋กœ๋“œ ์ˆ˜ ์ถ”๊ฐ€
title = model_data.get('title', name)
tags = model_data.get('tags', [])
return {
'url': original_url,
'embedUrl': embed_url,
'title': title,
'owner': owner,
'name': name,
'likes_count': likes_count,
'downloads_count': downloads_count, # ๋‹ค์šด๋กœ๋“œ ์ˆ˜
'tags': tags,
'rank': offset + index + 1
}
except Exception as e:
print("Error processing model data:", e)
return {
'url': 'https://huggingface.co',
'embedUrl': 'https://huggingface.co',
'title': 'Error Loading Model',
'owner': 'huggingface',
'name': 'error',
'likes_count': 0,
'downloads_count': 0,
'tags': [],
'rank': offset + index + 1
}
# 5) ์˜ค๋„ˆ ํ†ต๊ณ„ (Top 30)
def get_owner_stats(all_models):
owners = []
for m in all_models:
if '/' in m.get('id', ''):
o, _ = m['id'].split('/', 1)
else:
o = m.get('owner', '')
if o != 'None':
owners.append(o)
c = Counter(owners)
return c.most_common(30)
@app.route('/')
def home():
return render_template('index.html')
# 6) ํŠธ๋ Œ๋”ฉ ๋ชจ๋ธ API
@app.route('/api/trending-models', methods=['GET'])
def trending_models():
search_query = request.args.get('search', '').lower()
offset = int(request.args.get('offset', 0))
limit = int(request.args.get('limit', 72))
data = fetch_trending_models(offset, limit)
results = []
for index, md in enumerate(data['models']):
info = get_model_details(md, index, offset)
if not info:
continue
if search_query:
title_l = info['title'].lower()
owner_l = info['owner'].lower()
url_l = info['url'].lower()
tags_l = ' '.join(t.lower() for t in info['tags'])
# ๊ฒ€์ƒ‰ ์กฐ๊ฑด์— ์—†์œผ๋ฉด pass
if (search_query not in title_l and
search_query not in owner_l and
search_query not in url_l and
search_query not in tags_l):
continue
results.append(info)
top_owners = get_owner_stats(data['all_models'])
return jsonify({
'models': results,
'total': data['total'],
'offset': offset,
'limit': limit,
'top_owners': top_owners
})
if __name__ == '__main__':
# ํ…œํ”Œ๋ฆฟ ํด๋” ์ƒ์„ฑ
os.makedirs('templates', exist_ok=True)
# index.html ์ƒ์„ฑ
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 Models 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;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', sans-serif;
color: var(--text-primary);
min-height: 100vh;
background: linear-gradient(135deg, var(--pastel-blue) 0%, var(--pastel-purple) 100%);
padding: 2rem;
}
.container {
max-width: 1600px;
margin: 0 auto;
}
.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 { text-align: center; margin-bottom: 1.5rem; }
.header h1 {
font-size: 2.2rem; font-weight: 700; color: #2d3748; letter-spacing: -0.5px;
}
.header p {
color: var(--text-secondary); margin-top: 0.5rem; font-size: 1.1rem;
}
.search-bar {
display: flex; align-items: center; background: #fff; border-radius: 30px;
padding: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.05);
max-width: 600px; margin: 0.5rem auto 1.5rem 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 {
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-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: var(--pastel-pink); }
.grid-item:nth-child(6n+2) { background: var(--pastel-blue); }
.grid-item:nth-child(6n+3) { background: var(--pastel-purple); }
.grid-item:nth-child(6n+4) { background: var(--pastel-yellow); }
.grid-item:nth-child(6n+5) { background: var(--pastel-green); }
.grid-item:nth-child(6n+6) { background: 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: 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: #1a202c; color: #fff; 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; flex-wrap: wrap; align-items: center; font-size: 0.9rem;
gap: 12px;
}
.owner-info {
color: var(--text-secondary); font-weight: 500;
}
.likes-counter {
color: #e53e3e; font-weight: 600;
display: flex; align-items: center;
}
.likes-counter span { margin-left: 4px; }
.downloads-counter {
color: #2f855a; font-weight: 600;
}
.grid-actions {
padding: 10px 15px; text-align: right; background: 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: rgba(237,242,247,0.8);
}
.open-link:hover { background: #e2e8f0; }
.grid-content {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
padding-top: 85px; padding-bottom: 45px;
}
.iframe-container {
width: 100%; height: 100%; position: relative; overflow: hidden;
}
.grid-content iframe {
transform: scale(0.7); transform-origin: top left;
width: 142.857%; height: 142.857%; border: none; border-radius: 0;
}
/* ์ž„๋ฒ ๋“œ ์‹คํŒจ ์‹œ ํƒœ๊ทธ ์ถœ๋ ฅ */
.tags-fallback {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(255,255,255,0.8); backdrop-filter: blur(5px);
display: flex; flex-direction: column; justify-content: center; align-items: center;
text-align: center; padding: 1rem;
}
.tags-list {
display: flex; flex-wrap: wrap; gap: 10px; max-width: 300px; justify-content: center;
}
.tag-chip {
padding: 6px 12px; border-radius: 15px; font-size: 0.85rem; font-weight: 600;
color: #333; background-color: var(--pastel-yellow);
}
.pagination {
display: flex; justify-content: center; align-items: center;
gap: 10px; margin: 2rem 0;
}
.pagination-button {
background: #fff; 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: #f8f9fa; box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.pagination-button.active {
background: var(--pastel-purple); color: #4a5568;
}
.pagination-button:disabled {
background: #edf2f7; color: #a0aec0; cursor: default; box-shadow: none;
}
.loading {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: 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 */
.stats-window { margin: 2rem 0; }
.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: var(--pastel-blue); border: none; padding: 8px 16px; border-radius: 20px;
font-weight: 600; cursor: pointer; transition: all 0.2s;
}
.stats-toggle:hover {
background: var(--pastel-purple);
}
.stats-content {
background: #fff; 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; }
@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; }
}
@keyframes spin {
0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); }
}
</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 Model Leaderboard</h1>
<p>Discover the top trending models from Huggingface</p>
</div>
<!-- ํ†ต๊ณ„ ์„น์…˜ -->
<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">Owner Statistics</div>
</div>
<div class="mac-content">
<div class="stats-header">
<div class="stats-title">Top 30 Creators by Number of Models</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"></div>
</div>
</div>
</div>
<!-- ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ -->
<div id="loadingIndicator" class="loading">
<div class="loading-content">
<div class="loading-spinner"></div>
<div class="loading-text">Loading amazing models...</div>
<div id="loadingError" class="loading-error">
If this takes too long, try refreshing the page.
</div>
</div>
</div>
<script>
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')
};
const state = {
isLoading: false,
models: [],
currentPage: 0,
itemsPerPage: 72,
totalItems: 0,
loadingTimeout: null,
statsVisible: false,
chartInstance: null,
topOwners: [],
iframeStatuses: {}
};
// ์•„์ดํ”„๋ ˆ์ž„ ๋กœ๋”ฉ ์ฒดํฌ
const iframeLoader = {
checkQueue: {},
maxAttempts: 3,
checkInterval: 3000,
startChecking(iframe, modelKey) {
this.checkQueue[modelKey] = {
iframe,
attempts: 0,
status: 'loading'
};
this.checkIframeStatus(modelKey);
},
checkIframeStatus(modelKey) {
if(!this.checkQueue[modelKey]) return;
const item = this.checkQueue[modelKey];
const { iframe } = item;
if(item.status !== 'loading') {
delete this.checkQueue[modelKey];
return;
}
item.attempts++;
try {
if(!iframe || !iframe.parentNode) {
delete this.checkQueue[modelKey];
return;
}
// ์‹ค์ œ ๋กœ๋”ฉ ์—ฌ๋ถ€ ์ฒดํฌ
try {
const hasBody = iframe.contentWindow
&& iframe.contentWindow.document
&& iframe.contentWindow.document.body;
if(hasBody && iframe.contentWindow.document.body.innerHTML.length > 100) {
// ์ผ๋ถ€ ํ…์ŠคํŠธ
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);
} else {
item.status = 'success';
}
delete this.checkQueue[modelKey];
return;
}
} catch(e) {
// ๋ณด์•ˆ ๋“ฑ์œผ๋กœ ์ ‘๊ทผ ๋ถˆ๊ฐ€
}
// ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ ์‹œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
if(item.attempts >= this.maxAttempts) {
item.status = 'error';
handleIframeError(iframe);
delete this.checkQueue[modelKey];
return;
}
setTimeout(() => this.checkIframeStatus(modelKey), this.checkInterval);
} catch(err) {
console.error('checkIframeStatus error:', err);
if(item.attempts >= this.maxAttempts) {
item.status = 'error';
handleIframeError(iframe);
delete this.checkQueue[modelKey];
} else {
setTimeout(() => this.checkIframeStatus(modelKey), this.checkInterval);
}
}
}
};
// ์•„์ดํ”„๋ ˆ์ž„ ์˜ค๋ฅ˜ => ํƒœ๊ทธ๋กœ ๋Œ€์ฒด (1) "Unable to embed model." ๋ฌธ๊ตฌ ์‚ญ์ œ
function handleIframeError(iframe) {
const container = iframe.parentNode;
if(!container) return;
// ์•„์ดํ”„๋ ˆ์ž„ ์ˆจ๊ธฐ๊ธฐ
iframe.style.display = 'none';
// ํƒœ๊ทธ ๊ฐ€์ ธ์˜ค๊ธฐ
const tagsRaw = container.dataset.tags || '[]';
let tags = [];
try {
tags = JSON.parse(tagsRaw);
} catch(e) {
tags = [];
}
// ๋Œ€์ฒด UI
const fallbackDiv = document.createElement('div');
fallbackDiv.className = 'tags-fallback';
// ๋ณ„๋„ ๋ฌธ๊ตฌ ์—†์ด ํƒœ๊ทธ๋งŒ ํ‘œ์‹œ
const tagsList = document.createElement('div');
tagsList.className = 'tags-list';
const pastelColors = [
'var(--pastel-pink)', 'var(--pastel-blue)', 'var(--pastel-purple)',
'var(--pastel-yellow)', 'var(--pastel-green)', 'var(--pastel-orange)'
];
tags.forEach((tag, idx) => {
const chip = document.createElement('span');
chip.className = 'tag-chip';
chip.textContent = tag;
// ํŒŒ์Šคํ…” ์ƒ‰์ƒ ๋ฐ˜๋ณต
const color = pastelColors[idx % pastelColors.length];
chip.style.backgroundColor = color;
tagsList.appendChild(chip);
});
fallbackDiv.appendChild(tagsList);
container.appendChild(fallbackDiv);
}
// ํ†ต๊ณ„ ํ‘œ์‹œ ํ† ๊ธ€
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();
}
}
function renderCreatorStats() {
if(state.chartInstance) {
state.chartInstance.destroy();
}
const ctx = elements.creatorStatsChart.getContext('2d');
const labels = state.topOwners.map(o => o[0]);
const data = state.topOwners.map(o => o[1]);
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)`);
}
state.chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [{
label: 'Number of Models',
data,
backgroundColor: colors,
borderColor: colors.map(c => c.replace('0.7','1')),
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
title: (items) => items[0].label,
label: (ctx) => 'Models: ' + ctx.raw
}
}
},
scales: {
x: {
beginAtZero: true,
title: { display: true, text: 'Number of Models' }
},
y: {
title: { display: true, text: 'Creator ID' },
ticks: { autoSkip: false }
}
}
}
});
}
// ๋ชจ๋ธ ๋กœ๋“œ
async function loadModels(page=0) {
setLoading(true);
try {
const search = elements.searchInput.value;
const offset = page * state.itemsPerPage;
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), 30000);
});
const fetchPromise = fetch(`/api/trending-models?search=${encodeURIComponent(search)}&offset=${offset}&limit=${state.itemsPerPage}`);
const response = await Promise.race([fetchPromise, timeoutPromise]);
const data = await response.json();
state.models = data.models;
state.totalItems = data.total;
state.currentPage = page;
state.topOwners = data.top_owners || [];
renderGrid(data.models);
renderPagination();
if(state.statsVisible && state.topOwners.length > 0){
renderCreatorStats();
}
} catch(error) {
console.error('Error loading models:', error);
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 models</h3>
<p style="color:#666;">Please try refreshing. If it persists, check 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>`;
document.getElementById('retryButton')?.addEventListener('click', () => loadModels(0));
renderPagination();
} finally {
setLoading(false);
}
}
function renderGrid(models) {
elements.gridContainer.innerHTML = '';
if(!models || models.length===0) {
const msg = document.createElement('p');
msg.textContent = 'No models found matching your search.';
msg.style.padding = '2rem';
msg.style.textAlign = 'center';
msg.style.fontStyle = 'italic';
msg.style.color = '#718096';
elements.gridContainer.appendChild(msg);
return;
}
models.forEach((item) => {
try {
const {
url, title, likes_count, downloads_count,
owner, name, rank, tags
} = item;
if(owner==='None') return;
const gridItem = document.createElement('div');
gridItem.className = 'grid-item';
// ํ—ค๋”
const header = document.createElement('div');
header.className = 'grid-header';
const headerTop = document.createElement('div');
headerTop.className = 'grid-header-top';
const titleEl = document.createElement('h3');
titleEl.textContent = title;
titleEl.title = title;
headerTop.appendChild(titleEl);
const rankBadge = document.createElement('div');
rankBadge.className = 'rank-badge';
rankBadge.textContent = `#${rank}`;
headerTop.appendChild(rankBadge);
header.appendChild(headerTop);
// ๋ฉ”ํƒ€
const metaInfo = document.createElement('div');
metaInfo.className = 'grid-meta';
const ownerEl = document.createElement('div');
ownerEl.className = 'owner-info';
ownerEl.textContent = `by ${owner}`;
metaInfo.appendChild(ownerEl);
const likesEl = document.createElement('div');
likesEl.className = 'likes-counter';
likesEl.innerHTML = 'โ™ฅ <span>' + likes_count + '</span>';
metaInfo.appendChild(likesEl);
// ๋‹ค์šด๋กœ๋“œ ์ˆ˜ ์ถ”๊ฐ€
const downloadsEl = document.createElement('div');
downloadsEl.className = 'downloads-counter';
downloadsEl.textContent = 'Downloads: ' + downloads_count;
metaInfo.appendChild(downloadsEl);
header.appendChild(metaInfo);
gridItem.appendChild(header);
// ์ฝ˜ํ…์ธ  (iframe ์‹œ๋„)
const content = document.createElement('div');
content.className = 'grid-content';
const iframeContainer = document.createElement('div');
iframeContainer.className = 'iframe-container';
iframeContainer.dataset.tags = JSON.stringify(tags);
const iframe = document.createElement('iframe');
// direct url
iframe.src = createDirectUrl(owner, name);
iframe.title = title;
iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
iframe.setAttribute('allowfullscreen', '');
iframe.setAttribute('frameborder', '0');
iframe.loading = 'lazy';
const modelKey = `${owner}/${name}`;
state.iframeStatuses[modelKey] = 'loading';
iframe.onload = () => {
iframeLoader.startChecking(iframe, modelKey);
};
iframe.onerror = () => {
state.iframeStatuses[modelKey] = 'error';
handleIframeError(iframe);
};
// ์ผ์ • ์‹œ๊ฐ„ ํ›„์—๋„ ๋กœ๋”ฉ์ด๋ฉด ํƒœ๊ทธ๋กœ ๋Œ€์ฒด
setTimeout(() => {
if(state.iframeStatuses[modelKey]==='loading'){
state.iframeStatuses[modelKey] = 'error';
handleIframeError(iframe);
}
}, 8000);
iframeContainer.appendChild(iframe);
content.appendChild(iframeContainer);
// ํ•˜๋‹จ ๋งํฌ
const actions = document.createElement('div');
actions.className = 'grid-actions';
const linkEl = document.createElement('a');
linkEl.href = url;
linkEl.target = '_blank';
linkEl.className = 'open-link';
linkEl.textContent = 'Open in new window';
actions.appendChild(linkEl);
gridItem.appendChild(content);
gridItem.appendChild(actions);
elements.gridContainer.appendChild(gridItem);
} catch(err) {
console.error('Item rendering error:', err);
}
});
}
// ํŽ˜์ด์ง€๋„ค์ด์…˜
function renderPagination() {
elements.pagination.innerHTML = '';
const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
const prevButton = document.createElement('button');
prevButton.className = 'pagination-button';
prevButton.textContent = 'Previous';
prevButton.disabled = (state.currentPage===0);
prevButton.addEventListener('click', ()=>{
if(state.currentPage>0) loadModels(state.currentPage-1);
});
elements.pagination.appendChild(prevButton);
const maxButtons = 7;
let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons/2));
let endPage = Math.min(totalPages-1, startPage + maxButtons-1);
if(endPage - startPage + 1 < maxButtons) {
startPage = Math.max(0, endPage - maxButtons + 1);
}
for(let i=startPage; i<=endPage; i++){
const pageBtn = document.createElement('button');
pageBtn.className = 'pagination-button';
if(i===state.currentPage) pageBtn.classList.add('active');
pageBtn.textContent = i+1;
pageBtn.addEventListener('click', () => {
if(i!==state.currentPage) loadModels(i);
});
elements.pagination.appendChild(pageBtn);
}
const nextButton = document.createElement('button');
nextButton.className = 'pagination-button';
nextButton.textContent = 'Next';
nextButton.disabled = (state.currentPage >= totalPages-1);
nextButton.addEventListener('click', ()=>{
if(state.currentPage < totalPages-1) loadModels(state.currentPage+1);
});
elements.pagination.appendChild(nextButton);
}
// ๋กœ๋”ฉ ํ‘œ์‹œ
function setLoading(isLoading) {
state.isLoading = isLoading;
elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
if(isLoading) {
elements.refreshButton.classList.add('refreshing');
clearTimeout(state.loadingTimeout);
state.loadingTimeout = setTimeout(()=>{
elements.loadingError.style.display = 'block';
},10000);
} else {
elements.refreshButton.classList.remove('refreshing');
clearTimeout(state.loadingTimeout);
elements.loadingError.style.display = 'none';
}
}
// HF ๋ชจ๋ธ URL ์ƒ์„ฑ
function createDirectUrl(owner, name){
try {
name = name.replace(/\./g,'-').replace(/_/g,'-').toLowerCase();
owner = owner.toLowerCase();
return `https://huggingface.co/${owner}/${name}`;
} catch(e){
console.error(e);
return 'https://huggingface.co';
}
}
// ๊ฒ€์ƒ‰๋ฐ•์Šค ์ด๋ฒคํŠธ
elements.searchInput.addEventListener('input', ()=>{
clearTimeout(state.searchTimeout);
state.searchTimeout = setTimeout(()=>loadModels(0), 300);
});
elements.searchInput.addEventListener('keyup', (e)=>{
if(e.key==='Enter') loadModels(0);
});
elements.refreshButton.addEventListener('click', ()=>loadModels(0));
elements.statsToggle.addEventListener('click', toggleStats);
// Mac ๋ฒ„ํŠผ (์—ฐ์ถœ์šฉ)
document.querySelectorAll('.mac-button').forEach(btn=>{
btn.addEventListener('click', (e)=> e.preventDefault());
});
// ํŽ˜์ด์ง€ ๋กœ๋“œ์‹œ
window.addEventListener('load', () => {
setTimeout(()=>loadModels(0), 500);
});
// 20์ดˆ๊ฐ€ ์ง€๋‚˜๋„ ๋กœ๋”ฉ ์ค‘์ด๋ฉด...
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);
// ์ฆ‰์‹œ ๋กœ๋“œ ํ˜ธ์ถœ
loadModels(0);
</script>
</body>
</html>
""")
app.run(host='0.0.0.0', port=7860)