Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -23,7 +23,7 @@ def generate_dummy_spaces(count):
|
|
23 |
'createdAt': '2023-01-01T00:00:00.000Z',
|
24 |
'hardware': 'cpu',
|
25 |
'user': {
|
26 |
-
'avatar_url':
|
27 |
'name': 'dummyUser'
|
28 |
}
|
29 |
})
|
@@ -32,15 +32,15 @@ def generate_dummy_spaces(count):
|
|
32 |
# Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
|
33 |
def fetch_trending_spaces(offset=0, limit=72):
|
34 |
"""
|
35 |
-
hardware=cpu
|
36 |
-
|
37 |
"""
|
38 |
try:
|
39 |
url = "https://huggingface.co/api/spaces"
|
40 |
params = {
|
41 |
-
"limit": 10000,
|
42 |
-
"hardware": "cpu"
|
43 |
-
"sort": "trending" #
|
44 |
}
|
45 |
response = requests.get(url, params=params, timeout=30)
|
46 |
|
@@ -54,10 +54,11 @@ def fetch_trending_spaces(offset=0, limit=72):
|
|
54 |
and space.get('id', '').split('/', 1)[0] != 'None'
|
55 |
]
|
56 |
|
|
|
57 |
start = min(offset, len(filtered_spaces))
|
58 |
end = min(offset + limit, len(filtered_spaces))
|
59 |
|
60 |
-
print(f"[fetch_trending_spaces] CPU기반 스페이스 총 {len(filtered_spaces)}
|
61 |
f"요청 구간 {start}~{end-1} 반환")
|
62 |
|
63 |
return {
|
@@ -65,7 +66,7 @@ def fetch_trending_spaces(offset=0, limit=72):
|
|
65 |
'total': len(filtered_spaces),
|
66 |
'offset': offset,
|
67 |
'limit': limit,
|
68 |
-
'all_spaces': filtered_spaces #
|
69 |
}
|
70 |
else:
|
71 |
print(f"Error fetching spaces: {response.status_code}")
|
@@ -108,8 +109,8 @@ def transform_url(owner, name):
|
|
108 |
# Get space details
|
109 |
def get_space_details(space_data, index, offset):
|
110 |
"""
|
111 |
-
|
112 |
-
|
113 |
"""
|
114 |
try:
|
115 |
if '/' in space_data.get('id', ''):
|
@@ -135,9 +136,9 @@ def get_space_details(space_data, index, offset):
|
|
135 |
# Description
|
136 |
short_desc = space_data.get('description', '')
|
137 |
|
138 |
-
# User info
|
139 |
user_info = space_data.get('user', {})
|
140 |
-
|
141 |
author_name = user_info.get('name') or owner
|
142 |
|
143 |
return {
|
@@ -148,7 +149,7 @@ def get_space_details(space_data, index, offset):
|
|
148 |
'name': name,
|
149 |
'likes_count': likes_count,
|
150 |
'description': short_desc,
|
151 |
-
|
152 |
'author_name': author_name,
|
153 |
'rank': offset + index + 1
|
154 |
}
|
@@ -163,7 +164,7 @@ def get_space_details(space_data, index, offset):
|
|
163 |
'name': 'error',
|
164 |
'likes_count': 0,
|
165 |
'description': '',
|
166 |
-
|
167 |
'author_name': 'huggingface',
|
168 |
'rank': offset + index + 1
|
169 |
}
|
@@ -171,15 +172,10 @@ def get_space_details(space_data, index, offset):
|
|
171 |
# Get owner statistics from all spaces
|
172 |
def get_owner_stats(all_spaces):
|
173 |
"""
|
174 |
-
|
175 |
-
-> 상위 1000개만 골라서(owner) 개수를 세어
|
176 |
-
상위 30명을 산출.
|
177 |
"""
|
178 |
-
# TOP 1000만 추려서 rank<=1000로 간주
|
179 |
-
top_1000_spaces = all_spaces[:1000]
|
180 |
-
|
181 |
owners = []
|
182 |
-
for space in
|
183 |
if '/' in space.get('id', ''):
|
184 |
owner, _ = space.get('id', '').split('/', 1)
|
185 |
else:
|
@@ -188,7 +184,10 @@ def get_owner_stats(all_spaces):
|
|
188 |
if owner != 'None':
|
189 |
owners.append(owner)
|
190 |
|
|
|
191 |
owner_counts = Counter(owners)
|
|
|
|
|
192 |
top_owners = owner_counts.most_common(30)
|
193 |
|
194 |
return top_owners
|
@@ -196,28 +195,32 @@ def get_owner_stats(all_spaces):
|
|
196 |
# Homepage route
|
197 |
@app.route('/')
|
198 |
def home():
|
|
|
|
|
|
|
199 |
return render_template('index.html')
|
200 |
|
201 |
-
# Zero-GPU spaces API
|
202 |
@app.route('/api/trending-spaces', methods=['GET'])
|
203 |
def trending_spaces():
|
204 |
"""
|
205 |
-
hardware=cpu
|
206 |
-
검색, 페이징 적용 + top 1000 집계
|
207 |
"""
|
208 |
search_query = request.args.get('search', '').lower()
|
209 |
offset = int(request.args.get('offset', 0))
|
210 |
limit = int(request.args.get('limit', 72)) # Default 72
|
211 |
|
|
|
212 |
spaces_data = fetch_trending_spaces(offset, limit)
|
213 |
|
|
|
214 |
results = []
|
215 |
for index, space_data in enumerate(spaces_data['spaces']):
|
216 |
space_info = get_space_details(space_data, index, offset)
|
217 |
if not space_info:
|
218 |
continue
|
219 |
|
220 |
-
#
|
221 |
if search_query:
|
222 |
if (search_query not in space_info['title'].lower()
|
223 |
and search_query not in space_info['owner'].lower()
|
@@ -227,7 +230,7 @@ def trending_spaces():
|
|
227 |
|
228 |
results.append(space_info)
|
229 |
|
230 |
-
#
|
231 |
top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
|
232 |
|
233 |
return jsonify({
|
@@ -245,7 +248,7 @@ if __name__ == '__main__':
|
|
245 |
# Create templates folder if not exists
|
246 |
os.makedirs('templates', exist_ok=True)
|
247 |
|
248 |
-
# index.html
|
249 |
with open('templates/index.html', 'w', encoding='utf-8') as f:
|
250 |
f.write('''<!DOCTYPE html>
|
251 |
<html lang="en">
|
@@ -808,20 +811,88 @@ if __name__ == '__main__':
|
|
808 |
}
|
809 |
}
|
810 |
|
811 |
-
|
812 |
-
|
813 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
814 |
border: 1px solid #ccc;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
815 |
width: 32px;
|
816 |
height: 32px;
|
817 |
border-radius: 50%;
|
|
|
818 |
display: flex;
|
819 |
align-items: center;
|
820 |
justify-content: center;
|
821 |
-
margin-right: 8px;
|
822 |
}
|
823 |
-
|
824 |
-
/* 기존 아바타 관련 CSS는 제거/미사용 */
|
825 |
</style>
|
826 |
</head>
|
827 |
<body>
|
@@ -838,14 +909,15 @@ if __name__ == '__main__':
|
|
838 |
|
839 |
<div class="mac-content">
|
840 |
<div class="header">
|
841 |
-
|
842 |
-
<
|
|
|
843 |
</div>
|
844 |
|
845 |
<!-- Tab Navigation -->
|
846 |
<div class="tab-nav">
|
847 |
-
<button id="tabTrendingButton" class="tab-button active">
|
848 |
-
<button id="tabFixedButton" class="tab-button">
|
849 |
</div>
|
850 |
|
851 |
<!-- Trending(Zero GPU) Tab Content -->
|
@@ -861,8 +933,7 @@ if __name__ == '__main__':
|
|
861 |
</div>
|
862 |
<div class="mac-content">
|
863 |
<div class="stats-header">
|
864 |
-
|
865 |
-
<div class="stats-title">Top 30 Creators by Count Number of TOP 1000 Rank</div>
|
866 |
<button id="statsToggle" class="stats-toggle">Show Stats</button>
|
867 |
</div>
|
868 |
<div id="statsContent" class="stats-content">
|
@@ -886,7 +957,7 @@ if __name__ == '__main__':
|
|
886 |
<div id="pagination" class="pagination"></div>
|
887 |
</div>
|
888 |
|
889 |
-
<!-- Fixed Tab Content -->
|
890 |
<div id="fixedTab" class="tab-content">
|
891 |
<div id="fixedGrid" class="grid-container"></div>
|
892 |
</div>
|
@@ -1232,7 +1303,7 @@ if __name__ == '__main__':
|
|
1232 |
container.appendChild(errorPlaceholder);
|
1233 |
}
|
1234 |
|
1235 |
-
//
|
1236 |
function renderGrid(spaces) {
|
1237 |
elements.gridContainer.innerHTML = '';
|
1238 |
|
@@ -1251,7 +1322,7 @@ if __name__ == '__main__':
|
|
1251 |
try {
|
1252 |
const {
|
1253 |
url, title, likes_count, owner, name, rank,
|
1254 |
-
description, author_name, embedUrl
|
1255 |
} = item;
|
1256 |
|
1257 |
const gridItem = document.createElement('div');
|
@@ -1261,49 +1332,64 @@ if __name__ == '__main__':
|
|
1261 |
const headerDiv = document.createElement('div');
|
1262 |
headerDiv.className = 'grid-header';
|
1263 |
|
1264 |
-
//
|
1265 |
-
|
1266 |
-
|
1267 |
-
headerTop.className = 'grid-header-top';
|
1268 |
|
1269 |
-
//
|
1270 |
-
const leftWrapper = document.createElement('div');
|
1271 |
-
leftWrapper.style.display = 'flex';
|
1272 |
-
leftWrapper.style.alignItems = 'center';
|
1273 |
-
|
1274 |
const emojiAvatar = document.createElement('div');
|
1275 |
emojiAvatar.className = 'emoji-avatar';
|
1276 |
-
emojiAvatar.textContent = '🤖';
|
1277 |
-
|
|
|
|
|
|
|
|
|
|
|
1278 |
|
1279 |
const titleEl = document.createElement('h3');
|
|
|
1280 |
titleEl.textContent = title;
|
1281 |
titleEl.title = title;
|
1282 |
-
|
1283 |
|
1284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1285 |
|
1286 |
-
// 오른쪽: rank
|
1287 |
const rankBadge = document.createElement('div');
|
1288 |
rankBadge.className = 'rank-badge';
|
1289 |
rankBadge.textContent = `#${rank}`;
|
1290 |
-
|
1291 |
|
1292 |
-
|
|
|
|
|
|
|
|
|
1293 |
|
1294 |
-
|
1295 |
-
const metaInfo = document.createElement('div');
|
1296 |
-
metaInfo.className = 'grid-meta';
|
1297 |
|
1298 |
-
|
1299 |
-
|
1300 |
-
|
1301 |
-
|
1302 |
-
|
1303 |
-
const likesCounter = document.createElement('div');
|
1304 |
-
likesCounter.className = 'likes-counter';
|
1305 |
-
likesCounter.innerHTML = `♥ <span>${likes_count}</span>`;
|
1306 |
-
metaInfo.appendChild(likesCounter);
|
1307 |
|
1308 |
headerDiv.appendChild(metaInfo);
|
1309 |
gridItem.appendChild(headerDiv);
|
@@ -1311,10 +1397,7 @@ if __name__ == '__main__':
|
|
1311 |
// description
|
1312 |
if (description) {
|
1313 |
const descP = document.createElement('p');
|
1314 |
-
descP.
|
1315 |
-
descP.style.fontSize = '0.85rem';
|
1316 |
-
descP.style.color = '#444';
|
1317 |
-
descP.style.marginTop = '6px';
|
1318 |
descP.textContent = description;
|
1319 |
gridItem.appendChild(descP);
|
1320 |
}
|
@@ -1327,7 +1410,7 @@ if __name__ == '__main__':
|
|
1327 |
iframeContainer.className = 'iframe-container';
|
1328 |
|
1329 |
const iframe = document.createElement('iframe');
|
1330 |
-
iframe.src = embedUrl;
|
1331 |
iframe.title = title;
|
1332 |
iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
|
1333 |
iframe.setAttribute('allowfullscreen', '');
|
@@ -1376,8 +1459,8 @@ if __name__ == '__main__':
|
|
1376 |
});
|
1377 |
}
|
1378 |
|
1379 |
-
// (2) Fixed Tab - 이모지로 대체
|
1380 |
function renderFixedGrid() {
|
|
|
1381 |
fixedGridContainer.innerHTML = '';
|
1382 |
|
1383 |
const staticSpaces = [
|
@@ -1421,17 +1504,16 @@ if __name__ == '__main__':
|
|
1421 |
staticSpaces.forEach((item) => {
|
1422 |
try {
|
1423 |
const { url, title, likes_count, owner, name, rank } = item;
|
1424 |
-
|
1425 |
const gridItem = document.createElement('div');
|
1426 |
gridItem.className = 'grid-item';
|
1427 |
|
1428 |
const header = document.createElement('div');
|
1429 |
header.className = 'grid-header';
|
1430 |
|
1431 |
-
// 첫줄(top): 이모지 + 제목 + rank
|
1432 |
const headerTop = document.createElement('div');
|
1433 |
headerTop.className = 'grid-header-top';
|
1434 |
|
|
|
1435 |
const leftWrapper = document.createElement('div');
|
1436 |
leftWrapper.style.display = 'flex';
|
1437 |
leftWrapper.style.alignItems = 'center';
|
@@ -1455,7 +1537,6 @@ if __name__ == '__main__':
|
|
1455 |
|
1456 |
header.appendChild(headerTop);
|
1457 |
|
1458 |
-
// 둘째줄(meta): owner + likes
|
1459 |
const metaInfo = document.createElement('div');
|
1460 |
metaInfo.className = 'grid-meta';
|
1461 |
|
@@ -1510,12 +1591,13 @@ if __name__ == '__main__':
|
|
1510 |
linkEl.target = '_blank';
|
1511 |
linkEl.className = 'open-link';
|
1512 |
linkEl.textContent = 'Open in new window';
|
1513 |
-
|
1514 |
actions.appendChild(linkEl);
|
|
|
1515 |
gridItem.appendChild(content);
|
1516 |
gridItem.appendChild(actions);
|
1517 |
|
1518 |
fixedGridContainer.appendChild(gridItem);
|
|
|
1519 |
} catch (error) {
|
1520 |
console.error('Fixed tab rendering error:', error);
|
1521 |
}
|
|
|
23 |
'createdAt': '2023-01-01T00:00:00.000Z',
|
24 |
'hardware': 'cpu',
|
25 |
'user': {
|
26 |
+
'avatar_url': 'https://huggingface.co/front/thumbnails/huggingface/default-avatar.svg',
|
27 |
'name': 'dummyUser'
|
28 |
}
|
29 |
})
|
|
|
32 |
# Function to fetch Zero-GPU (CPU-based) Spaces from Huggingface with pagination
|
33 |
def fetch_trending_spaces(offset=0, limit=72):
|
34 |
"""
|
35 |
+
원본 코드에서 수정: hardware=cpu 파라미터를 추가하여
|
36 |
+
GPU가 없는(=CPU 전용) 스페이스만 필터링
|
37 |
"""
|
38 |
try:
|
39 |
url = "https://huggingface.co/api/spaces"
|
40 |
params = {
|
41 |
+
"limit": 10000, # 더 많이 가져오기
|
42 |
+
"hardware": "cpu" # <-- Zero GPU(=CPU) 필터 적용
|
43 |
+
# "sort": "trending", # 필요시 추가 (HF API 지원 여부에 따라)
|
44 |
}
|
45 |
response = requests.get(url, params=params, timeout=30)
|
46 |
|
|
|
54 |
and space.get('id', '').split('/', 1)[0] != 'None'
|
55 |
]
|
56 |
|
57 |
+
# Slice according to requested offset and limit
|
58 |
start = min(offset, len(filtered_spaces))
|
59 |
end = min(offset + limit, len(filtered_spaces))
|
60 |
|
61 |
+
print(f"[fetch_trending_spaces] CPU기반 스페이스 총 {len(filtered_spaces)}개, "
|
62 |
f"요청 구간 {start}~{end-1} 반환")
|
63 |
|
64 |
return {
|
|
|
66 |
'total': len(filtered_spaces),
|
67 |
'offset': offset,
|
68 |
'limit': limit,
|
69 |
+
'all_spaces': filtered_spaces # 통계 산출용
|
70 |
}
|
71 |
else:
|
72 |
print(f"Error fetching spaces: {response.status_code}")
|
|
|
109 |
# Get space details
|
110 |
def get_space_details(space_data, index, offset):
|
111 |
"""
|
112 |
+
원본 코드에서 수정:
|
113 |
+
- description, avatar_url, author_name 등 추가 필드를 추출
|
114 |
"""
|
115 |
try:
|
116 |
if '/' in space_data.get('id', ''):
|
|
|
136 |
# Description
|
137 |
short_desc = space_data.get('description', '')
|
138 |
|
139 |
+
# User info (avatar, name)
|
140 |
user_info = space_data.get('user', {})
|
141 |
+
avatar_url = user_info.get('avatar_url', '')
|
142 |
author_name = user_info.get('name') or owner
|
143 |
|
144 |
return {
|
|
|
149 |
'name': name,
|
150 |
'likes_count': likes_count,
|
151 |
'description': short_desc,
|
152 |
+
'avatar_url': avatar_url,
|
153 |
'author_name': author_name,
|
154 |
'rank': offset + index + 1
|
155 |
}
|
|
|
164 |
'name': 'error',
|
165 |
'likes_count': 0,
|
166 |
'description': '',
|
167 |
+
'avatar_url': '',
|
168 |
'author_name': 'huggingface',
|
169 |
'rank': offset + index + 1
|
170 |
}
|
|
|
172 |
# Get owner statistics from all spaces
|
173 |
def get_owner_stats(all_spaces):
|
174 |
"""
|
175 |
+
모든 스페이스에서 owner 등장 빈도 상위 30명 추출
|
|
|
|
|
176 |
"""
|
|
|
|
|
|
|
177 |
owners = []
|
178 |
+
for space in all_spaces:
|
179 |
if '/' in space.get('id', ''):
|
180 |
owner, _ = space.get('id', '').split('/', 1)
|
181 |
else:
|
|
|
184 |
if owner != 'None':
|
185 |
owners.append(owner)
|
186 |
|
187 |
+
# Count occurrences of each owner
|
188 |
owner_counts = Counter(owners)
|
189 |
+
|
190 |
+
# Get top 30 owners by count
|
191 |
top_owners = owner_counts.most_common(30)
|
192 |
|
193 |
return top_owners
|
|
|
195 |
# Homepage route
|
196 |
@app.route('/')
|
197 |
def home():
|
198 |
+
"""
|
199 |
+
index.html 템플릿 렌더링 (���인 페이지)
|
200 |
+
"""
|
201 |
return render_template('index.html')
|
202 |
|
203 |
+
# Zero-GPU spaces API (원본: 'trending-spaces' -> 이제 CPU only)
|
204 |
@app.route('/api/trending-spaces', methods=['GET'])
|
205 |
def trending_spaces():
|
206 |
"""
|
207 |
+
hardware=cpu 스페이스 목록을 불러와 검색, 페이징, 통계 등을 적용
|
|
|
208 |
"""
|
209 |
search_query = request.args.get('search', '').lower()
|
210 |
offset = int(request.args.get('offset', 0))
|
211 |
limit = int(request.args.get('limit', 72)) # Default 72
|
212 |
|
213 |
+
# Fetch zero-gpu (cpu) spaces
|
214 |
spaces_data = fetch_trending_spaces(offset, limit)
|
215 |
|
216 |
+
# Process and filter spaces
|
217 |
results = []
|
218 |
for index, space_data in enumerate(spaces_data['spaces']):
|
219 |
space_info = get_space_details(space_data, index, offset)
|
220 |
if not space_info:
|
221 |
continue
|
222 |
|
223 |
+
# Apply search filter if needed
|
224 |
if search_query:
|
225 |
if (search_query not in space_info['title'].lower()
|
226 |
and search_query not in space_info['owner'].lower()
|
|
|
230 |
|
231 |
results.append(space_info)
|
232 |
|
233 |
+
# Get owner statistics for all spaces
|
234 |
top_owners = get_owner_stats(spaces_data.get('all_spaces', []))
|
235 |
|
236 |
return jsonify({
|
|
|
248 |
# Create templates folder if not exists
|
249 |
os.makedirs('templates', exist_ok=True)
|
250 |
|
251 |
+
# index.html 전체를 새로 작성
|
252 |
with open('templates/index.html', 'w', encoding='utf-8') as f:
|
253 |
f.write('''<!DOCTYPE html>
|
254 |
<html lang="en">
|
|
|
811 |
}
|
812 |
}
|
813 |
|
814 |
+
.error-emoji-detector {
|
815 |
+
position: fixed;
|
816 |
+
top: -9999px;
|
817 |
+
left: -9999px;
|
818 |
+
z-index: -1;
|
819 |
+
opacity: 0;
|
820 |
+
}
|
821 |
+
|
822 |
+
/* 추가 레이아웃 수정(아바타, ZERO GPU 뱃지 등)을 위해
|
823 |
+
아래 클래스들을 일부 추가/수정해도 좋지만
|
824 |
+
예시는 위 영역에서 이미 충분히 처리하므로 생략 */
|
825 |
+
|
826 |
+
/* 다음 부분은 Zero GPU Spaces용 카드 구조에서 활용 */
|
827 |
+
.space-header {
|
828 |
+
display: flex;
|
829 |
+
align-items: center;
|
830 |
+
gap: 10px;
|
831 |
+
margin-bottom: 4px;
|
832 |
+
}
|
833 |
+
.avatar-img {
|
834 |
+
width: 32px;
|
835 |
+
height: 32px;
|
836 |
+
border-radius: 50%;
|
837 |
+
object-fit: cover;
|
838 |
border: 1px solid #ccc;
|
839 |
+
}
|
840 |
+
.space-title {
|
841 |
+
font-size: 1rem;
|
842 |
+
font-weight: 600;
|
843 |
+
margin: 0;
|
844 |
+
overflow: hidden;
|
845 |
+
text-overflow: ellipsis;
|
846 |
+
white-space: nowrap;
|
847 |
+
max-width: 200px;
|
848 |
+
}
|
849 |
+
.zero-gpu-badge {
|
850 |
+
font-size: 0.7rem;
|
851 |
+
background-color: #e6fffa;
|
852 |
+
color: #319795;
|
853 |
+
border: 1px solid #81e6d9;
|
854 |
+
border-radius: 6px;
|
855 |
+
padding: 2px 6px;
|
856 |
+
font-weight: 600;
|
857 |
+
margin-left: 8px;
|
858 |
+
}
|
859 |
+
.desc-text {
|
860 |
+
font-size: 0.85rem;
|
861 |
+
color: #444;
|
862 |
+
margin: 4px 0;
|
863 |
+
line-clamp: 2;
|
864 |
+
display: -webkit-box;
|
865 |
+
-webkit-box-orient: vertical;
|
866 |
+
overflow: hidden;
|
867 |
+
}
|
868 |
+
.author-name {
|
869 |
+
font-size: 0.8rem;
|
870 |
+
color: #666;
|
871 |
+
}
|
872 |
+
.likes-wrapper {
|
873 |
+
display: flex;
|
874 |
+
align-items: center;
|
875 |
+
gap: 4px;
|
876 |
+
color: #e53e3e;
|
877 |
+
font-weight: bold;
|
878 |
+
font-size: 0.85rem;
|
879 |
+
}
|
880 |
+
.likes-heart {
|
881 |
+
font-size: 1rem;
|
882 |
+
line-height: 1rem;
|
883 |
+
color: #f56565;
|
884 |
+
}
|
885 |
+
/* 이모지 전용 스타일 (선택사항) */
|
886 |
+
.emoji-avatar {
|
887 |
+
font-size: 1.2rem;
|
888 |
width: 32px;
|
889 |
height: 32px;
|
890 |
border-radius: 50%;
|
891 |
+
border: 1px solid #ccc;
|
892 |
display: flex;
|
893 |
align-items: center;
|
894 |
justify-content: center;
|
|
|
895 |
}
|
|
|
|
|
896 |
</style>
|
897 |
</head>
|
898 |
<body>
|
|
|
909 |
|
910 |
<div class="mac-content">
|
911 |
<div class="header">
|
912 |
+
<!-- 첫 번째 탭 제목을 Zero GPU Spaces로 변경 -->
|
913 |
+
<h1>Zero GPU Spaces</h1>
|
914 |
+
<p>Discover 'TOP 1,000' Zero GPU(Shared A100) spaces from Hugging Face</p>
|
915 |
</div>
|
916 |
|
917 |
<!-- Tab Navigation -->
|
918 |
<div class="tab-nav">
|
919 |
+
<button id="tabTrendingButton" class="tab-button active">Zero GPU Spaces</button>
|
920 |
+
<button id="tabFixedButton" class="tab-button">Fixed Tab</button>
|
921 |
</div>
|
922 |
|
923 |
<!-- Trending(Zero GPU) Tab Content -->
|
|
|
933 |
</div>
|
934 |
<div class="mac-content">
|
935 |
<div class="stats-header">
|
936 |
+
<div class="stats-title">Top 30 Creators by Count Number of TOP 1000 RANK</div>
|
|
|
937 |
<button id="statsToggle" class="stats-toggle">Show Stats</button>
|
938 |
</div>
|
939 |
<div id="statsContent" class="stats-content">
|
|
|
957 |
<div id="pagination" class="pagination"></div>
|
958 |
</div>
|
959 |
|
960 |
+
<!-- Fixed Tab Content (기존 예시 유지) -->
|
961 |
<div id="fixedTab" class="tab-content">
|
962 |
<div id="fixedGrid" class="grid-container"></div>
|
963 |
</div>
|
|
|
1303 |
container.appendChild(errorPlaceholder);
|
1304 |
}
|
1305 |
|
1306 |
+
// 여기서부터 카드 HTML 구조 (Zero-GPU 전용) 렌더링
|
1307 |
function renderGrid(spaces) {
|
1308 |
elements.gridContainer.innerHTML = '';
|
1309 |
|
|
|
1322 |
try {
|
1323 |
const {
|
1324 |
url, title, likes_count, owner, name, rank,
|
1325 |
+
description, avatar_url, author_name, embedUrl
|
1326 |
} = item;
|
1327 |
|
1328 |
const gridItem = document.createElement('div');
|
|
|
1332 |
const headerDiv = document.createElement('div');
|
1333 |
headerDiv.className = 'grid-header';
|
1334 |
|
1335 |
+
// space-header (로봇 이모지 + 제목 + Zero GPU 배지)
|
1336 |
+
const spaceHeader = document.createElement('div');
|
1337 |
+
spaceHeader.className = 'space-header';
|
|
|
1338 |
|
1339 |
+
// 로봇 이모지 대체
|
|
|
|
|
|
|
|
|
1340 |
const emojiAvatar = document.createElement('div');
|
1341 |
emojiAvatar.className = 'emoji-avatar';
|
1342 |
+
emojiAvatar.textContent = '🤖';
|
1343 |
+
spaceHeader.appendChild(emojiAvatar);
|
1344 |
+
|
1345 |
+
// 제목+배지
|
1346 |
+
const titleWrapper = document.createElement('div');
|
1347 |
+
titleWrapper.style.display = 'flex';
|
1348 |
+
titleWrapper.style.alignItems = 'center';
|
1349 |
|
1350 |
const titleEl = document.createElement('h3');
|
1351 |
+
titleEl.className = 'space-title';
|
1352 |
titleEl.textContent = title;
|
1353 |
titleEl.title = title;
|
1354 |
+
titleWrapper.appendChild(titleEl);
|
1355 |
|
1356 |
+
const zeroGpuBadge = document.createElement('span');
|
1357 |
+
zeroGpuBadge.className = 'zero-gpu-badge';
|
1358 |
+
zeroGpuBadge.textContent = 'ZERO GPU';
|
1359 |
+
titleWrapper.appendChild(zeroGpuBadge);
|
1360 |
+
|
1361 |
+
spaceHeader.appendChild(titleWrapper);
|
1362 |
+
headerDiv.appendChild(spaceHeader);
|
1363 |
+
|
1364 |
+
// rank + author + likes
|
1365 |
+
const metaInfo = document.createElement('div');
|
1366 |
+
metaInfo.className = 'grid-meta';
|
1367 |
+
metaInfo.style.display = 'flex';
|
1368 |
+
metaInfo.style.justifyContent = 'space-between';
|
1369 |
+
metaInfo.style.alignItems = 'center';
|
1370 |
+
metaInfo.style.marginTop = '6px';
|
1371 |
+
|
1372 |
+
// 왼쪽: rank + author
|
1373 |
+
const leftMeta = document.createElement('div');
|
1374 |
|
|
|
1375 |
const rankBadge = document.createElement('div');
|
1376 |
rankBadge.className = 'rank-badge';
|
1377 |
rankBadge.textContent = `#${rank}`;
|
1378 |
+
leftMeta.appendChild(rankBadge);
|
1379 |
|
1380 |
+
const authorSpan = document.createElement('span');
|
1381 |
+
authorSpan.className = 'author-name';
|
1382 |
+
authorSpan.style.marginLeft = '8px';
|
1383 |
+
authorSpan.textContent = `by ${author_name}`;
|
1384 |
+
leftMeta.appendChild(authorSpan);
|
1385 |
|
1386 |
+
metaInfo.appendChild(leftMeta);
|
|
|
|
|
1387 |
|
1388 |
+
// 오른쪽: likes
|
1389 |
+
const likesDiv = document.createElement('div');
|
1390 |
+
likesDiv.className = 'likes-wrapper';
|
1391 |
+
likesDiv.innerHTML = `<span class="likes-heart">♥</span><span>${likes_count}</span>`;
|
1392 |
+
metaInfo.appendChild(likesDiv);
|
|
|
|
|
|
|
|
|
1393 |
|
1394 |
headerDiv.appendChild(metaInfo);
|
1395 |
gridItem.appendChild(headerDiv);
|
|
|
1397 |
// description
|
1398 |
if (description) {
|
1399 |
const descP = document.createElement('p');
|
1400 |
+
descP.className = 'desc-text';
|
|
|
|
|
|
|
1401 |
descP.textContent = description;
|
1402 |
gridItem.appendChild(descP);
|
1403 |
}
|
|
|
1410 |
iframeContainer.className = 'iframe-container';
|
1411 |
|
1412 |
const iframe = document.createElement('iframe');
|
1413 |
+
iframe.src = embedUrl; // transformed direct URL
|
1414 |
iframe.title = title;
|
1415 |
iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;';
|
1416 |
iframe.setAttribute('allowfullscreen', '');
|
|
|
1459 |
});
|
1460 |
}
|
1461 |
|
|
|
1462 |
function renderFixedGrid() {
|
1463 |
+
// Fixed Tab 예시용 (원본 코드 예시 유지)
|
1464 |
fixedGridContainer.innerHTML = '';
|
1465 |
|
1466 |
const staticSpaces = [
|
|
|
1504 |
staticSpaces.forEach((item) => {
|
1505 |
try {
|
1506 |
const { url, title, likes_count, owner, name, rank } = item;
|
|
|
1507 |
const gridItem = document.createElement('div');
|
1508 |
gridItem.className = 'grid-item';
|
1509 |
|
1510 |
const header = document.createElement('div');
|
1511 |
header.className = 'grid-header';
|
1512 |
|
|
|
1513 |
const headerTop = document.createElement('div');
|
1514 |
headerTop.className = 'grid-header-top';
|
1515 |
|
1516 |
+
// 로봇 이모지 + 타이틀 함께 표시
|
1517 |
const leftWrapper = document.createElement('div');
|
1518 |
leftWrapper.style.display = 'flex';
|
1519 |
leftWrapper.style.alignItems = 'center';
|
|
|
1537 |
|
1538 |
header.appendChild(headerTop);
|
1539 |
|
|
|
1540 |
const metaInfo = document.createElement('div');
|
1541 |
metaInfo.className = 'grid-meta';
|
1542 |
|
|
|
1591 |
linkEl.target = '_blank';
|
1592 |
linkEl.className = 'open-link';
|
1593 |
linkEl.textContent = 'Open in new window';
|
|
|
1594 |
actions.appendChild(linkEl);
|
1595 |
+
|
1596 |
gridItem.appendChild(content);
|
1597 |
gridItem.appendChild(actions);
|
1598 |
|
1599 |
fixedGridContainer.appendChild(gridItem);
|
1600 |
+
|
1601 |
} catch (error) {
|
1602 |
console.error('Fixed tab rendering error:', error);
|
1603 |
}
|