openfree commited on
Commit
331da74
ยท
verified ยท
1 Parent(s): 85dd6d7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -157
app.py CHANGED
@@ -7,72 +7,50 @@ from collections import Counter
7
 
8
  app = Flask(__name__)
9
 
10
- ###################
11
- # 1) ์ตœ๋Œ€ 50๊ฐœ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด limit=50์œผ๋กœ ์ˆ˜์ •
12
- ###################
 
13
  def fetch_trending_spaces(offset=0, limit=50):
 
 
 
 
14
  try:
15
- url = "https://huggingface.co/api/spaces"
16
- # ์—ฌ๊ธฐ์„œ๋Š” ์ตœ๋Œ€ 10000๊ฐœ๋ฅผ ๊ฐ€์ ธ์˜ค์ง€๋งŒ, slicing ์‹œ์— ์šฐ๋ฆฌ๊ฐ€ limit=50 ์„ ์ ์šฉ
17
- params = {"limit": 10000}
18
-
19
- response = requests.get(url, params=params, timeout=30)
 
 
 
 
 
 
20
 
21
- if response.status_code == 200:
22
- spaces = response.json()
23
- filtered_spaces = [space for space in spaces
24
- if space.get('owner') != 'None'
25
- and space.get('id', '').split('/', 1)[0] != 'None']
26
-
27
- # offset ๋ฐ limit ์ ์šฉ
28
- start = min(offset, len(filtered_spaces))
29
- end = min(offset + limit, len(filtered_spaces))
30
-
31
- print(f"Fetched {len(filtered_spaces)} spaces, returning {end - start} items from {start} to {end}")
32
-
33
- return {
34
- 'spaces': filtered_spaces[start:end],
35
- 'total': min(len(filtered_spaces), 50), # ์ตœ๋Œ€ 50๊นŒ์ง€๋งŒ ์ดํ•ฉ์œผ๋กœ ๋ฐ˜์˜
36
- 'offset': offset,
37
- 'limit': limit,
38
- 'all_spaces': filtered_spaces[:50] # ์ „์ฒด ํ†ต๊ณ„ ๊ณ„์‚ฐ๋„ 50๊ฐœ๋งŒ
39
- }
40
- else:
41
- print(f"Error fetching spaces: {response.status_code}")
42
- return {
43
- 'spaces': generate_dummy_spaces(limit),
44
- 'total': 50,
45
- 'offset': offset,
46
- 'limit': limit,
47
- 'all_spaces': generate_dummy_spaces(50)
48
- }
49
  except Exception as e:
50
  print(f"Exception when fetching spaces: {e}")
 
51
  return {
52
- 'spaces': generate_dummy_spaces(limit),
53
- 'total': 50,
54
- 'offset': offset,
55
- 'limit': limit,
56
- 'all_spaces': generate_dummy_spaces(50)
57
  }
58
 
59
- def generate_dummy_spaces(count):
60
- spaces = []
61
- for i in range(count):
62
- spaces.append({
63
- 'id': f'dummy/space-{i}',
64
- 'owner': 'dummy',
65
- 'title': f'Example Space {i+1}',
66
- 'likes': 100 - i,
67
- 'createdAt': '2023-01-01T00:00:00.000Z'
68
- })
69
- return spaces
70
-
71
  def transform_url(owner, name):
72
- name = name.replace('.', '-')
73
- name = name.replace('_', '-')
74
  owner = owner.lower()
75
- name = name.lower()
76
  return f"https://{owner}-{name}.hf.space"
77
 
78
  def get_space_details(space_data, index, offset):
@@ -130,37 +108,24 @@ def get_owner_stats(all_spaces):
130
  top_owners = owner_counts.most_common(30)
131
  return top_owners
132
 
133
- ###########################
134
- # 2) ๋‘ ๋ฒˆ์งธ ํƒญ์„ ์œ„ํ•œ "๊ณ ์ •๋œ" Huggingface URL ๋ชฉ๋ก ์˜ˆ์‹œ
135
- # (์›ํ•˜๋Š” ์ŠคํŽ˜์ด์Šค๋“ค์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”)
136
- ###########################
137
- CUSTOM_SPACES = [
138
- {
139
- 'owner': 'huggingface',
140
- 'name': 'diffuse-the-rest',
141
- 'title': 'Diffuse The Rest Demo',
142
- 'likes': 999,
143
- },
144
- {
145
- 'owner': 'openai',
146
- 'name': 'whisper',
147
- 'title': 'Whisper Demo',
148
- 'likes': 777,
149
- },
150
- {
151
- 'owner': 'HuggingFaceH4',
152
- 'name': 'Chat-UI',
153
- 'title': 'Chat UI (HuggingFaceH4)',
154
- 'likes': 450,
155
- },
156
- ]
157
 
158
- ###########################
159
- # 2) ๋‘ ๋ฒˆ์งธ ํƒญ API ๋ผ์šฐํŠธ
160
- ###########################
161
  @app.route('/api/custom-spaces', methods=['GET'])
162
  def custom_spaces():
163
- # ํ•„์š”ํ•˜๋‹ค๋ฉด ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๋“ฑ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ. ์—ฌ๊ธฐ์„œ๋Š” ๋‹จ์ˆœํžˆ ์ „์ฒด ๋ฐ˜ํ™˜
 
 
164
  results = []
165
  for index, item in enumerate(CUSTOM_SPACES):
166
  owner = item['owner']
@@ -168,7 +133,6 @@ def custom_spaces():
168
  title = item.get('title', name)
169
  likes_count = item.get('likes', 0)
170
 
171
- # rank๋Š” ์ž„์˜๋กœ index+1
172
  results.append({
173
  'url': f"https://huggingface.co/spaces/{owner}/{name}",
174
  'embedUrl': transform_url(owner, name),
@@ -192,9 +156,12 @@ def home():
192
 
193
  @app.route('/api/trending-spaces', methods=['GET'])
194
  def trending_spaces():
 
 
 
 
195
  search_query = request.args.get('search', '').lower()
196
  offset = int(request.args.get('offset', 0))
197
- # ์—ฌ๊ธฐ์„œ๋„ limit=50์œผ๋กœ ๊ณ ์ •
198
  limit = int(request.args.get('limit', 50))
199
 
200
  spaces_data = fetch_trending_spaces(offset, limit)
@@ -205,7 +172,7 @@ def trending_spaces():
205
  if not space_info:
206
  continue
207
 
208
- # ๊ฒ€์ƒ‰์–ด ํ•„ํ„ฐ
209
  if search_query:
210
  title = space_info['title'].lower()
211
  owner = space_info['owner'].lower()
@@ -233,7 +200,6 @@ def trending_spaces():
233
  if __name__ == '__main__':
234
  os.makedirs('templates', exist_ok=True)
235
 
236
- # index.html ์ƒ์„ฑ (์›๋ณธ์—์„œ ํƒญ ๊ธฐ๋Šฅ๋งŒ ์ถ”๊ฐ€)
237
  with open('templates/index.html', 'w', encoding='utf-8') as f:
238
  f.write('''<!DOCTYPE html>
239
  <html lang="en">
@@ -242,7 +208,7 @@ if __name__ == '__main__':
242
  <title>Huggingface Spaces Gallery</title>
243
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
244
  <style>
245
- /* (์ค‘๋žต) โ€” ์›๋ณธ CSS ๊ทธ๋Œ€๋กœ ์œ ์ง€ */
246
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
247
 
248
  :root {
@@ -321,6 +287,23 @@ if __name__ == '__main__':
321
  margin-top: 0.5rem;
322
  font-size: 1.1rem;
323
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  .search-bar {
325
  display: flex; align-items: center; margin-bottom: 1.5rem;
326
  background-color: white; border-radius: 30px; padding: 5px;
@@ -496,24 +479,6 @@ if __name__ == '__main__':
496
  .pagination { flex-wrap: wrap; }
497
  .chart-container { height: 300px; }
498
  }
499
- /* ํƒญ ์Šคํƒ€์ผ ์ถ”๊ฐ€ */
500
- .tab-nav {
501
- display: flex; gap: 20px; justify-content: center; margin-bottom: 20px;
502
- }
503
- .tab-button {
504
- padding: 10px 20px; border-radius: 20px; border: none; cursor: pointer;
505
- background-color: #ffffffcc; font-weight: 600; transition: 0.2s;
506
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
507
- }
508
- .tab-button:hover {
509
- background-color: var(--pastel-green);
510
- }
511
- .tab-button.active {
512
- background-color: var(--pastel-purple);
513
- color: #4a5568;
514
- }
515
- .tab-content { display: none; }
516
- .tab-content.active { display: block; }
517
  </style>
518
  </head>
519
  <body>
@@ -531,18 +496,17 @@ if __name__ == '__main__':
531
  <div class="mac-content">
532
  <div class="header">
533
  <h1>HF Space Leaderboard</h1>
534
- <p>Discover the top 50 trending spaces from Huggingface</p>
535
  </div>
536
 
537
  <!-- ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ -->
538
  <div class="tab-nav">
539
- <button class="tab-button active" data-tab="leaderboardTab">๋ฆฌ๋”๋ณด๋“œ</button>
540
  <button class="tab-button" data-tab="customTab">๋‘ ๋ฒˆ์งธ ํƒญ</button>
541
  </div>
542
 
543
- <!-- ๋ฆฌ๋”๋ณด๋“œ ํƒญ -->
544
  <div id="leaderboardTab" class="tab-content active">
545
- <!-- Stats Section (์›๋ณธ) -->
546
  <div class="stats-window mac-window">
547
  <div class="mac-toolbar">
548
  <div class="mac-buttons">
@@ -576,12 +540,11 @@ if __name__ == '__main__':
576
  <div id="pagination" class="pagination"></div>
577
  </div>
578
 
579
- <!-- ๋‘ ๋ฒˆ์งธ ํƒญ(์ปค์Šคํ…€ ์ŠคํŽ˜์ด์Šค) -->
580
  <div id="customTab" class="tab-content">
581
- <h2 style="text-align:center; margin-bottom:1rem;">์ง€์ •๋œ Huggingface URL ๋ฆฌ์ŠคํŠธ</h2>
582
  <div id="customGridContainer" class="grid-container"></div>
583
  </div>
584
-
585
  </div>
586
  </div>
587
  </div>
@@ -614,7 +577,7 @@ if __name__ == '__main__':
614
  isLoading: false,
615
  spaces: [],
616
  currentPage: 0,
617
- itemsPerPage: 50, // 50๊ฐœ
618
  totalItems: 0,
619
  loadingTimeout: null,
620
  staticModeAttempted: {},
@@ -622,7 +585,6 @@ if __name__ == '__main__':
622
  chartInstance: null,
623
  topOwners: [],
624
  iframeStatuses: {},
625
- // ๋‘ ๋ฒˆ์งธ ํƒญ์šฉ
626
  customSpaces: []
627
  };
628
 
@@ -655,7 +617,6 @@ if __name__ == '__main__':
655
  delete this.checkQueue[spaceKey];
656
  return;
657
  }
658
-
659
  try {
660
  const hasContent = iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.body;
661
  if (hasContent && iframe.contentWindow.document.body.innerHTML.length > 100) {
@@ -749,7 +710,7 @@ if __name__ == '__main__':
749
  tooltip: {
750
  callbacks: {
751
  title: function(tooltipItems) { return tooltipItems[0].label; },
752
- label: function(context) { return `Spaces: ${context.raw}`; }
753
  }
754
  }
755
  },
@@ -783,7 +744,7 @@ if __name__ == '__main__':
783
  const timeoutPromise = new Promise((_, reject) =>
784
  setTimeout(() => reject(new Error('Request timeout')), 30000)
785
  );
786
- const fetchPromise = fetch(`/api/trending-spaces?search=${encodeURIComponent(searchText)}&offset=${offset}&limit=${state.itemsPerPage}`);
787
  const response = await Promise.race([fetchPromise, timeoutPromise]);
788
  const data = await response.json();
789
 
@@ -800,7 +761,7 @@ if __name__ == '__main__':
800
  }
801
  } catch(e) {
802
  console.error(e);
803
- elements.gridContainer.innerHTML = `
804
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
805
  <div style="font-size: 3rem; margin-bottom: 20px;">โš ๏ธ</div>
806
  <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
@@ -809,7 +770,7 @@ if __name__ == '__main__':
809
  Try Again
810
  </button>
811
  </div>
812
- `;
813
  document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
814
  renderPagination();
815
  } finally {
@@ -820,8 +781,9 @@ if __name__ == '__main__':
820
  function renderPagination() {
821
  elements.pagination.innerHTML = '';
822
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
 
823
  const prevButton = document.createElement('button');
824
- prevButton.className = `pagination-button ${state.currentPage === 0 ? 'disabled' : ''}`;
825
  prevButton.textContent = 'Previous';
826
  prevButton.disabled = state.currentPage === 0;
827
  prevButton.addEventListener('click', () => {
@@ -837,7 +799,7 @@ if __name__ == '__main__':
837
  }
838
  for (let i = startPage; i <= endPage; i++) {
839
  const pageButton = document.createElement('button');
840
- pageButton.className = `pagination-button ${i === state.currentPage ? 'active' : ''}`;
841
  pageButton.textContent = i + 1;
842
  pageButton.addEventListener('click', () => {
843
  if (i !== state.currentPage) {
@@ -848,7 +810,7 @@ if __name__ == '__main__':
848
  }
849
 
850
  const nextButton = document.createElement('button');
851
- nextButton.className = `pagination-button ${state.currentPage >= totalPages - 1 ? 'disabled' : ''}`;
852
  nextButton.textContent = 'Next';
853
  nextButton.disabled = state.currentPage >= totalPages - 1;
854
  nextButton.addEventListener('click', () => {
@@ -865,11 +827,11 @@ if __name__ == '__main__':
865
  errorPlaceholder.className = 'error-placeholder';
866
 
867
  const errorMessage = document.createElement('p');
868
- errorMessage.textContent = `"${title}" space couldn't be loaded`;
869
  errorPlaceholder.appendChild(errorMessage);
870
 
871
  const directLink = document.createElement('a');
872
- directLink.href = `https://huggingface.co/spaces/${owner}/${name}`;
873
  directLink.target = '_blank';
874
  directLink.textContent = 'Visit HF Space';
875
  directLink.style.color = '#3182ce';
@@ -919,7 +881,7 @@ if __name__ == '__main__':
919
 
920
  const rankBadge = document.createElement('div');
921
  rankBadge.className = 'rank-badge';
922
- rankBadge.textContent = `#${rank}`;
923
  headerTop.appendChild(rankBadge);
924
  header.appendChild(headerTop);
925
 
@@ -928,7 +890,7 @@ if __name__ == '__main__':
928
 
929
  const ownerEl = document.createElement('div');
930
  ownerEl.className = 'owner-info';
931
- ownerEl.textContent = `by ${owner}`;
932
  metaInfo.appendChild(ownerEl);
933
 
934
  const likesCounter = document.createElement('div');
@@ -954,7 +916,7 @@ if __name__ == '__main__':
954
  iframe.setAttribute('frameborder', '0');
955
  iframe.loading = 'lazy';
956
 
957
- const spaceKey = `${owner}/${name}`;
958
  state.iframeStatuses[spaceKey] = 'loading';
959
 
960
  iframe.onload = function() {
@@ -1005,17 +967,17 @@ if __name__ == '__main__':
1005
  }
1006
  }
1007
 
1008
- elements.searchInput.addEventListener('input', () => {
1009
- clearTimeout(state.searchTimeout);
1010
- state.searchTimeout = setTimeout(() => loadSpaces(0), 300);
1011
- });
1012
- elements.searchInput.addEventListener('keyup', (event) => {
1013
- if (event.key === 'Enter') {
1014
- loadSpaces(0);
 
 
1015
  }
1016
- });
1017
- elements.refreshButton.addEventListener('click', () => loadSpaces(0));
1018
- elements.statsToggle.addEventListener('click', toggleStats);
1019
 
1020
  document.querySelectorAll('.mac-button').forEach(button => {
1021
  button.addEventListener('click', (e) => {
@@ -1039,7 +1001,7 @@ if __name__ == '__main__':
1039
  }
1040
  }
1041
 
1042
- // ํƒญ ์ „ํ™˜ ๊ธฐ๋Šฅ
1043
  const tabButtons = document.querySelectorAll('.tab-button');
1044
  const tabContents = document.querySelectorAll('.tab-content');
1045
  tabButtons.forEach(btn => {
@@ -1052,28 +1014,15 @@ if __name__ == '__main__':
1052
  });
1053
  });
1054
 
1055
- // ๋‘ ๋ฒˆ์งธ ํƒญ(์ปค์Šคํ…€ ์ŠคํŽ˜์ด์Šค) ๋ถˆ๋Ÿฌ์˜ค๊ธฐ
1056
- async function loadCustomSpaces() {
1057
- try {
1058
- const resp = await fetch('/api/custom-spaces');
1059
- const data = await resp.json();
1060
- state.customSpaces = data.spaces || [];
1061
- renderGrid(state.customSpaces, elements.customGridContainer);
1062
- } catch(e) {
1063
- console.error(e);
1064
- elements.customGridContainer.innerHTML = '<p style="text-align:center; color:red;">Error loading custom spaces.</p>';
1065
- }
1066
- }
1067
-
1068
  // ์ดˆ๊ธฐ ์‹คํ–‰
1069
  window.addEventListener('load', function() {
1070
- // ๋ฆฌ๋”๋ณด๋“œ ๋กœ๋“œ
1071
  loadSpaces(0);
1072
- // ๋‘ ๋ฒˆ์งธ ํƒญ(์ปค์Šคํ…€)๋„ ๊ฐ™์ด ๋กœ๋“œ
1073
  loadCustomSpaces();
1074
  });
1075
 
1076
- // ์•ˆ์ „์žฅ์น˜
1077
  setTimeout(() => {
1078
  if (state.isLoading) {
1079
  setLoading(false);
@@ -1094,5 +1043,5 @@ if __name__ == '__main__':
1094
  </html>
1095
  ''')
1096
 
1097
- # Huggingface Spaces์—์„œ ๋ณดํ†ต port=7860 ์„ ๋งŽ์ด ์”๋‹ˆ๋‹ค.
1098
  app.run(host='0.0.0.0', port=7860)
 
7
 
8
  app = Flask(__name__)
9
 
10
+
11
+ ##########################################
12
+ # 1) ์ฒซ ๋ฒˆ์งธ ํƒญ์šฉ: VIDraft/SanaSprint 6๊ฐœ
13
+ ##########################################
14
  def fetch_trending_spaces(offset=0, limit=50):
15
+ """
16
+ ์›๋ž˜ Hugging Face /api/spaces๋ฅผ ํ˜ธ์ถœํ•˜๋˜ ๋ถ€๋ถ„์„
17
+ ์ง์ ‘ ๋”๋ฏธ ๋ฐ์ดํ„ฐ(๋™์ผํ•œ URL 6๊ฐœ)๋งŒ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •.
18
+ """
19
  try:
20
+ # ์—ฌ๊ธฐ์„œ๋Š” 6๊ฐœ๋งŒ ๋งŒ๋“ค์–ด ๋ฐ˜ํ™˜
21
+ # (์‹ค์ œ๋กœ offset/limit ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๊ณ , 6๊ฐœ๋งŒ ๋ฐ˜ํ™˜)
22
+ my_spaces = []
23
+ for i in range(6):
24
+ my_spaces.append({
25
+ 'id': 'VIDraft/SanaSprint',
26
+ 'owner': 'VIDraft',
27
+ 'title': f'SanaSprint #{i+1}',
28
+ 'likes': 100 + i,
29
+ 'createdAt': '2023-01-01T00:00:00.000Z'
30
+ })
31
 
32
+ return {
33
+ 'spaces': my_spaces,
34
+ 'total': 6, # ์ด 6๊ฐœ
35
+ 'offset': 0,
36
+ 'limit': 6,
37
+ 'all_spaces': my_spaces # ํ†ต๊ณ„ ๊ณ„์‚ฐ์šฉ(์—ฌ๊ธฐ์„œ๋Š” 6๊ฐœ๋งŒ ๊ทธ๋Œ€๋กœ)
38
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  except Exception as e:
40
  print(f"Exception when fetching spaces: {e}")
41
+ # ๋ฌธ์ œ ๋ฐœ์ƒ์‹œ, ํ˜น์€ ๋นˆ ๋ฐฐ์—ด ์ฃผ์–ด๋„ ๋จ
42
  return {
43
+ 'spaces': [],
44
+ 'total': 0,
45
+ 'offset': 0,
46
+ 'limit': 6,
47
+ 'all_spaces': []
48
  }
49
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  def transform_url(owner, name):
51
+ # ์ (.) -> ๋Œ€์‰ฌ(-), ์–ธ๋”๋ฐ”(_) -> ๋Œ€์‰ฌ(-), ์†Œ๋ฌธ์žํ™”
52
+ name = name.replace('.', '-').replace('_', '-').lower()
53
  owner = owner.lower()
 
54
  return f"https://{owner}-{name}.hf.space"
55
 
56
  def get_space_details(space_data, index, offset):
 
108
  top_owners = owner_counts.most_common(30)
109
  return top_owners
110
 
111
+ ##########################################
112
+ # 2) ๋‘ ๋ฒˆ์งธ ํƒญ์šฉ: VIDraft/SanaSprint
113
+ # (์›ํ•˜๋Š” ๊ฐœ์ˆ˜๋งŒํผ - ์—ฌ๊ธฐ์„  ์˜ˆ์‹œ๋กœ 6๊ฐœ)
114
+ ##########################################
115
+ CUSTOM_SPACES = []
116
+ for i in range(6):
117
+ CUSTOM_SPACES.append({
118
+ 'owner': 'VIDraft',
119
+ 'name': 'SanaSprint',
120
+ 'title': f'SanaSprint Demo #{i+1}',
121
+ 'likes': 500 + i,
122
+ })
 
 
 
 
 
 
 
 
 
 
 
 
123
 
 
 
 
124
  @app.route('/api/custom-spaces', methods=['GET'])
125
  def custom_spaces():
126
+ """
127
+ ๋‘ ๋ฒˆ์งธ ํƒญ์—์„œ ํ‘œ์‹œํ•  Space๋“ค์„ ๋ฐ˜ํ™˜ (์—ฌ๊ธฐ์„œ๋„ 6๊ฐœ).
128
+ """
129
  results = []
130
  for index, item in enumerate(CUSTOM_SPACES):
131
  owner = item['owner']
 
133
  title = item.get('title', name)
134
  likes_count = item.get('likes', 0)
135
 
 
136
  results.append({
137
  'url': f"https://huggingface.co/spaces/{owner}/{name}",
138
  'embedUrl': transform_url(owner, name),
 
156
 
157
  @app.route('/api/trending-spaces', methods=['GET'])
158
  def trending_spaces():
159
+ """
160
+ ์ฒซ ๋ฒˆ์งธ ํƒญ: ์œ„์—์„œ ๋งŒ๋“  fetch_trending_spaces()์˜ ๊ฒฐ๊ณผ๋ฅผ
161
+ ๊ทธ๋Œ€๋กœ ๋ Œ๋”๋ง (์‹ค์ œ๋กœ๋Š” 6๊ฐœ ์งœ๋ฆฌ ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜).
162
+ """
163
  search_query = request.args.get('search', '').lower()
164
  offset = int(request.args.get('offset', 0))
 
165
  limit = int(request.args.get('limit', 50))
166
 
167
  spaces_data = fetch_trending_spaces(offset, limit)
 
172
  if not space_info:
173
  continue
174
 
175
+ # ๊ฒ€์ƒ‰์–ด๊ฐ€ ์žˆ์œผ๋ฉด ํ•„ํ„ฐ๋ง(์‚ฌ์šฉ ์•ˆ ํ•˜์…”๋„ ๋จ)
176
  if search_query:
177
  title = space_info['title'].lower()
178
  owner = space_info['owner'].lower()
 
200
  if __name__ == '__main__':
201
  os.makedirs('templates', exist_ok=True)
202
 
 
203
  with open('templates/index.html', 'w', encoding='utf-8') as f:
204
  f.write('''<!DOCTYPE html>
205
  <html lang="en">
 
208
  <title>Huggingface Spaces Gallery</title>
209
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
210
  <style>
211
+ /* ๊ธฐ์กด ์Šคํƒ€์ผ ๋™์ผ (์ƒ๋žต ๊ฐ€๋Šฅ) */
212
  @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap');
213
 
214
  :root {
 
287
  margin-top: 0.5rem;
288
  font-size: 1.1rem;
289
  }
290
+ .tab-nav {
291
+ display: flex; gap: 20px; justify-content: center; margin-bottom: 20px;
292
+ }
293
+ .tab-button {
294
+ padding: 10px 20px; border-radius: 20px; border: none; cursor: pointer;
295
+ background-color: #ffffffcc; font-weight: 600; transition: 0.2s;
296
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
297
+ }
298
+ .tab-button:hover {
299
+ background-color: var(--pastel-green);
300
+ }
301
+ .tab-button.active {
302
+ background-color: var(--pastel-purple);
303
+ color: #4a5568;
304
+ }
305
+ .tab-content { display: none; }
306
+ .tab-content.active { display: block; }
307
  .search-bar {
308
  display: flex; align-items: center; margin-bottom: 1.5rem;
309
  background-color: white; border-radius: 30px; padding: 5px;
 
479
  .pagination { flex-wrap: wrap; }
480
  .chart-container { height: 300px; }
481
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  </style>
483
  </head>
484
  <body>
 
496
  <div class="mac-content">
497
  <div class="header">
498
  <h1>HF Space Leaderboard</h1>
499
+ <p>์ฒซ ๋ฒˆ์งธ ํƒญ: ๋™์ผํ•œ URL 6๊ฐœ / ๋‘ ๋ฒˆ์งธ ํƒญ: ๋™์ผํ•œ URL 6๊ฐœ</p>
500
  </div>
501
 
502
  <!-- ํƒญ ๋„ค๋น„๊ฒŒ์ด์…˜ -->
503
  <div class="tab-nav">
504
+ <button class="tab-button active" data-tab="leaderboardTab">์ฒซ ๋ฒˆ์งธ ํƒญ</button>
505
  <button class="tab-button" data-tab="customTab">๋‘ ๋ฒˆ์งธ ํƒญ</button>
506
  </div>
507
 
508
+ <!-- ์ฒซ ๋ฒˆ์งธ ํƒญ (๋ฆฌ๋”๋ณด๋“œ) -->
509
  <div id="leaderboardTab" class="tab-content active">
 
510
  <div class="stats-window mac-window">
511
  <div class="mac-toolbar">
512
  <div class="mac-buttons">
 
540
  <div id="pagination" class="pagination"></div>
541
  </div>
542
 
543
+ <!-- ๋‘ ๋ฒˆ์งธ ํƒญ (์ปค์Šคํ…€) -->
544
  <div id="customTab" class="tab-content">
545
+ <h2 style="text-align:center; margin-bottom:1rem;">๋‘ ๋ฒˆ์งธ ํƒญ: ๋™์ผํ•œ URL 6๊ฐœ</h2>
546
  <div id="customGridContainer" class="grid-container"></div>
547
  </div>
 
548
  </div>
549
  </div>
550
  </div>
 
577
  isLoading: false,
578
  spaces: [],
579
  currentPage: 0,
580
+ itemsPerPage: 50,
581
  totalItems: 0,
582
  loadingTimeout: null,
583
  staticModeAttempted: {},
 
585
  chartInstance: null,
586
  topOwners: [],
587
  iframeStatuses: {},
 
588
  customSpaces: []
589
  };
590
 
 
617
  delete this.checkQueue[spaceKey];
618
  return;
619
  }
 
620
  try {
621
  const hasContent = iframe.contentWindow && iframe.contentWindow.document && iframe.contentWindow.document.body;
622
  if (hasContent && iframe.contentWindow.document.body.innerHTML.length > 100) {
 
710
  tooltip: {
711
  callbacks: {
712
  title: function(tooltipItems) { return tooltipItems[0].label; },
713
+ label: function(context) { return \`Spaces: \${context.raw}\`; }
714
  }
715
  }
716
  },
 
744
  const timeoutPromise = new Promise((_, reject) =>
745
  setTimeout(() => reject(new Error('Request timeout')), 30000)
746
  );
747
+ const fetchPromise = fetch(\`/api/trending-spaces?search=\${encodeURIComponent(searchText)}&offset=\${offset}&limit=\${state.itemsPerPage}\`);
748
  const response = await Promise.race([fetchPromise, timeoutPromise]);
749
  const data = await response.json();
750
 
 
761
  }
762
  } catch(e) {
763
  console.error(e);
764
+ elements.gridContainer.innerHTML = \`
765
  <div style="grid-column: 1/-1; text-align: center; padding: 40px;">
766
  <div style="font-size: 3rem; margin-bottom: 20px;">โš ๏ธ</div>
767
  <h3 style="margin-bottom: 10px;">Unable to load spaces</h3>
 
770
  Try Again
771
  </button>
772
  </div>
773
+ \`;
774
  document.getElementById('retryButton')?.addEventListener('click', () => loadSpaces(0));
775
  renderPagination();
776
  } finally {
 
781
  function renderPagination() {
782
  elements.pagination.innerHTML = '';
783
  const totalPages = Math.ceil(state.totalItems / state.itemsPerPage);
784
+
785
  const prevButton = document.createElement('button');
786
+ prevButton.className = \`pagination-button \${state.currentPage === 0 ? 'disabled' : ''}\`;
787
  prevButton.textContent = 'Previous';
788
  prevButton.disabled = state.currentPage === 0;
789
  prevButton.addEventListener('click', () => {
 
799
  }
800
  for (let i = startPage; i <= endPage; i++) {
801
  const pageButton = document.createElement('button');
802
+ pageButton.className = \`pagination-button \${i === state.currentPage ? 'active' : ''}\`;
803
  pageButton.textContent = i + 1;
804
  pageButton.addEventListener('click', () => {
805
  if (i !== state.currentPage) {
 
810
  }
811
 
812
  const nextButton = document.createElement('button');
813
+ nextButton.className = \`pagination-button \${state.currentPage >= totalPages - 1 ? 'disabled' : ''}\`;
814
  nextButton.textContent = 'Next';
815
  nextButton.disabled = state.currentPage >= totalPages - 1;
816
  nextButton.addEventListener('click', () => {
 
827
  errorPlaceholder.className = 'error-placeholder';
828
 
829
  const errorMessage = document.createElement('p');
830
+ errorMessage.textContent = \`"\${title}" space couldn't be loaded\`;
831
  errorPlaceholder.appendChild(errorMessage);
832
 
833
  const directLink = document.createElement('a');
834
+ directLink.href = \`https://huggingface.co/spaces/\${owner}/\${name}\`;
835
  directLink.target = '_blank';
836
  directLink.textContent = 'Visit HF Space';
837
  directLink.style.color = '#3182ce';
 
881
 
882
  const rankBadge = document.createElement('div');
883
  rankBadge.className = 'rank-badge';
884
+ rankBadge.textContent = \`#\${rank}\`;
885
  headerTop.appendChild(rankBadge);
886
  header.appendChild(headerTop);
887
 
 
890
 
891
  const ownerEl = document.createElement('div');
892
  ownerEl.className = 'owner-info';
893
+ ownerEl.textContent = \`by \${owner}\`;
894
  metaInfo.appendChild(ownerEl);
895
 
896
  const likesCounter = document.createElement('div');
 
916
  iframe.setAttribute('frameborder', '0');
917
  iframe.loading = 'lazy';
918
 
919
+ const spaceKey = \`\${owner}/\${name}\`;
920
  state.iframeStatuses[spaceKey] = 'loading';
921
 
922
  iframe.onload = function() {
 
967
  }
968
  }
969
 
970
+ async function loadCustomSpaces() {
971
+ try {
972
+ const resp = await fetch('/api/custom-spaces');
973
+ const data = await resp.json();
974
+ state.customSpaces = data.spaces || [];
975
+ renderGrid(state.customSpaces, elements.customGridContainer);
976
+ } catch(e) {
977
+ console.error(e);
978
+ elements.customGridContainer.innerHTML = '<p style="text-align:center; color:red;">Error loading custom spaces.</p>';
979
  }
980
+ }
 
 
981
 
982
  document.querySelectorAll('.mac-button').forEach(button => {
983
  button.addEventListener('click', (e) => {
 
1001
  }
1002
  }
1003
 
1004
+ // ํƒญ ์ „ํ™˜
1005
  const tabButtons = document.querySelectorAll('.tab-button');
1006
  const tabContents = document.querySelectorAll('.tab-content');
1007
  tabButtons.forEach(btn => {
 
1014
  });
1015
  });
1016
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1017
  // ์ดˆ๊ธฐ ์‹คํ–‰
1018
  window.addEventListener('load', function() {
1019
+ // ์ฒซ ๋ฒˆ์งธ ํƒญ ๋กœ๋“œ
1020
  loadSpaces(0);
1021
+ // ๋‘ ๋ฒˆ์งธ ํƒญ๋„ ๋กœ๋“œ
1022
  loadCustomSpaces();
1023
  });
1024
 
1025
+ // ํ˜น์‹œ๋‚˜ ๋กœ๋”ฉ์ด ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ฆด ๋•Œ ์•ˆ์ „์žฅ์น˜
1026
  setTimeout(() => {
1027
  if (state.isLoading) {
1028
  setLoading(false);
 
1043
  </html>
1044
  ''')
1045
 
1046
+ # Huggingface Spaces์—์„œ ์ผ๋ฐ˜์ ์œผ๋กœ 7860 ํฌํŠธ ์‚ฌ์šฉ
1047
  app.run(host='0.0.0.0', port=7860)