seawolf2357 commited on
Commit
84739ea
ยท
verified ยท
1 Parent(s): 4318a7e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +179 -834
app.py CHANGED
@@ -1,21 +1,13 @@
1
- from flask import Flask, render_template, request, jsonify, session, Response
2
  import requests
3
- from bs4 import BeautifulSoup
4
  import os
5
  from datetime import timedelta
6
- import logging
7
- import time
8
-
9
- # ๋กœ๊น… ์„ค์ •
10
- logging.basicConfig(level=logging.INFO,
11
- format='%(asctime)s - %(levelname)s - %(message)s')
12
- logger = logging.getLogger(__name__)
13
 
14
  app = Flask(__name__)
15
- app.secret_key = os.urandom(24)
16
- app.permanent_session_lifetime = timedelta(days=7)
17
 
18
- # Hugging Face URL ๋ชฉ๋ก
19
  HUGGINGFACE_URLS = [
20
  "https://huggingface.co/spaces/ginipick/Tech_Hangman_Game",
21
  "https://huggingface.co/spaces/openfree/deepseek_r1_API",
@@ -38,7 +30,7 @@ HUGGINGFACE_URLS = [
38
  "https://huggingface.co/spaces/openfree/Article-Generator",
39
  ]
40
 
41
- # URL์—์„œ ๋ชจ๋ธ/์ŠคํŽ˜์ด์Šค ์ •๋ณด ์ถ”์ถœ
42
  def extract_model_info(url):
43
  parts = url.split('/')
44
  if len(parts) < 6:
@@ -52,8 +44,9 @@ def extract_model_info(url):
52
  'full_id': f"{parts[4]}/{parts[5]}"
53
  }
54
  elif len(parts) >= 5:
 
55
  return {
56
- 'type': 'models',
57
  'owner': parts[3],
58
  'repo': parts[4],
59
  'full_id': f"{parts[3]}/{parts[4]}"
@@ -61,162 +54,53 @@ def extract_model_info(url):
61
 
62
  return None
63
 
64
- # URL์˜ ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„์„ ์ œ๋ชฉ์œผ๋กœ ์ถ”์ถœ
65
  def extract_title(url):
66
  parts = url.split("/")
67
  title = parts[-1] if parts else ""
68
  return title.replace("_", " ").replace("-", " ")
69
 
70
- # ํ—ˆ๊น…ํŽ˜์ด์Šค ํ† ํฐ ๊ฒ€์ฆ
71
  def validate_token(token):
72
  headers = {"Authorization": f"Bearer {token}"}
73
 
 
74
  try:
75
  response = requests.get("https://huggingface.co/api/whoami-v2", headers=headers)
76
  if response.ok:
77
  return True, response.json()
78
  except Exception as e:
79
- logger.error(f"ํ† ํฐ ๊ฒ€์ฆ ์˜ค๋ฅ˜: {e}")
80
-
81
- return False, None
82
-
83
- # ์›น ์Šคํฌ๋ž˜ํ•‘์œผ๋กœ ์ข‹์•„์š” ์ƒํƒœ ํ™•์ธ
84
- def check_like_status_by_scraping(url, token):
85
- headers = {
86
- "Authorization": f"Bearer {token}",
87
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
88
- }
89
 
 
90
  try:
91
- # ํŽ˜์ด์ง€ ์š”์ฒญ
92
- response = requests.get(url, headers=headers)
93
- if not response.ok:
94
- logger.warning(f"ํŽ˜์ด์ง€ ์š”์ฒญ ์‹คํŒจ: {url}, ์ƒํƒœ ์ฝ”๋“œ: {response.status_code}")
95
- return False
96
-
97
- # HTML ํŒŒ์‹ฑ
98
- soup = BeautifulSoup(response.text, 'html.parser')
99
-
100
- # ์ข‹์•„์š” ๋ฒ„ํŠผ ์ฐพ๊ธฐ (๋‹ค์–‘ํ•œ ์„ ํƒ์ž ์‹œ๋„)
101
- like_button = None
102
- selectors = [
103
- '.like-button-container button',
104
- 'button[aria-label="Like"]',
105
- 'button.like-button',
106
- 'button[data-testid="like-button"]',
107
- 'button.heart-button'
108
- ]
109
-
110
- for selector in selectors:
111
- like_button = soup.select_one(selector)
112
- if like_button:
113
- break
114
-
115
- if not like_button:
116
- logger.warning(f"์ข‹์•„์š” ๋ฒ„ํŠผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ: {url}")
117
- return False
118
-
119
- # ์ข‹์•„์š” ์ƒํƒœ ํ™•์ธ (ํด๋ž˜์Šค, aria-pressed ์†์„ฑ ๋“ฑ์œผ๋กœ ํ™•์ธ)
120
- is_liked = False
121
-
122
- # ํด๋ž˜์Šค๋กœ ํ™•์ธ
123
- if 'liked' in like_button.get('class', []) or 'active' in like_button.get('class', []):
124
- is_liked = True
125
-
126
- # aria-pressed ์†์„ฑ์œผ๋กœ ํ™•์ธ
127
- elif like_button.get('aria-pressed') == 'true':
128
- is_liked = True
129
-
130
- # ๋‚ด๋ถ€ ํ…์ŠคํŠธ ๋˜๋Š” ์•„์ด์ฝ˜์œผ๋กœ ํ™•์ธ
131
- elif like_button.find('span', class_='liked') or like_button.find('svg', class_='liked'):
132
- is_liked = True
133
-
134
- logger.info(f"์Šคํฌ๋ž˜ํ•‘ ๊ฒฐ๊ณผ: {url} - ์ข‹์•„์š” {is_liked}")
135
- return is_liked
136
-
137
  except Exception as e:
138
- logger.error(f"์Šคํฌ๋ž˜ํ•‘ ์˜ค๋ฅ˜ ({url}): {e}")
139
- return False
140
-
141
- # ์ „์ฒด URL ๋ชฉ๋ก์˜ ์ข‹์•„์š” ์ƒํƒœ ์Šคํฌ๋ž˜ํ•‘
142
- def scrape_all_like_status(token):
143
- like_status = {}
144
-
145
- for url in HUGGINGFACE_URLS:
146
- try:
147
- # ๊ณผ๋„ํ•œ ์š”์ฒญ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์ง€์—ฐ
148
- time.sleep(1)
149
-
150
- is_liked = check_like_status_by_scraping(url, token)
151
- like_status[url] = is_liked
152
-
153
- logger.info(f"์ข‹์•„์š” ์ƒํƒœ ํ™•์ธ: {url} - {is_liked}")
154
- except Exception as e:
155
- logger.error(f"URL ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {url} - {e}")
156
- like_status[url] = False
157
 
158
- return like_status
159
 
 
160
  @app.route('/')
161
  def home():
162
  return render_template('index.html')
163
 
164
- @app.route('/proxy/<path:url>')
165
- def proxy(url):
166
- try:
167
- # URL ๋””์ฝ”๋”ฉ ๋ฐ ํ”„๋กœํ† ์ฝœ ์ถ”๊ฐ€ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ)
168
- if not url.startswith(('http://', 'https://')):
169
- url = 'https://' + url
170
-
171
- # ์š”์ฒญ ํ—ค๋” ์„ค์ •
172
- headers = {
173
- 'User-Agent': request.headers.get('User-Agent', 'Mozilla/5.0'),
174
- 'Accept': 'text/html,application/xhtml+xml,application/xml',
175
- 'Accept-Language': 'ko,en-US;q=0.9,en;q=0.8',
176
- }
177
-
178
- # ํ† ํฐ์ด ์žˆ์œผ๋ฉด ์ถ”๊ฐ€
179
- if 'token' in session:
180
- headers['Authorization'] = f"Bearer {session['token']}"
181
-
182
- # ์›๊ฒฉ ์‚ฌ์ดํŠธ์— ์š”์ฒญ
183
- response = requests.get(url, headers=headers, stream=True)
184
-
185
- # ์›๊ฒฉ ์‚ฌ์ดํŠธ์˜ ์‘๋‹ต ํ—ค๋”๋ฅผ ๊ฐ€์ ธ์˜ด
186
- response_headers = dict(response.headers)
187
-
188
- # ํ”„๋ก์‹œ ์‘๋‹ต์—์„œ ์ œ์™ธํ•  ํ—ค๋”
189
- excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection',
190
- 'host', 'x-frame-options', 'content-security-policy']
191
-
192
- # ํ•„์š”ํ•œ ํ—ค๋”๋งŒ ์œ ์ง€
193
- headers = [(name, value) for name, value in response_headers.items()
194
- if name.lower() not in excluded_headers]
195
-
196
- # X-Frame-Options ๋ฐ CSP ํ—ค๋” ์ œ๊ฑฐ/๋ณ€๊ฒฝ
197
- headers.append(('X-Frame-Options', 'ALLOWALL'))
198
- headers.append(('Content-Security-Policy', "frame-ancestors 'self' *"))
199
-
200
- # ์‘๋‹ต ์ƒ์„ฑ ๋ฐ ์ „์†ก
201
- return Response(response.iter_content(chunk_size=1024), headers=headers,
202
- status=response.status_code, content_type=response.headers.get('content-type'))
203
- except Exception as e:
204
- logger.error(f"ํ”„๋ก์‹œ ์˜ค๋ฅ˜: {str(e)}")
205
- return jsonify({"error": "ํ”„๋ก์‹œ ์š”์ฒญ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."}), 500
206
-
207
  @app.route('/api/login', methods=['POST'])
208
  def login():
209
  token = request.form.get('token', '')
210
 
211
  if not token:
212
- return jsonify({'success': False, 'message': 'ํ† ํฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'})
213
 
214
  is_valid, user_info = validate_token(token)
215
 
216
  if not is_valid or not user_info:
217
- return jsonify({'success': False, 'message': '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.'})
218
 
219
- # ์‚ฌ์šฉ์ž ์ด๋ฆ„ ์ฐพ๊ธฐ
220
  username = None
221
  if 'name' in user_info:
222
  username = user_info['name']
@@ -225,37 +109,28 @@ def login():
225
  elif 'username' in user_info:
226
  username = user_info['username']
227
  else:
228
- username = '์ธ์ฆ๋œ ์‚ฌ์šฉ์ž'
229
 
230
- # ์„ธ์…˜์— ์ €์žฅ
231
  session['token'] = token
232
  session['username'] = username
233
 
234
- # ์›น ์Šคํฌ๋ž˜ํ•‘์œผ๋กœ ์ข‹์•„์š” ์ƒํƒœ ํ™•์ธ
235
- # ์ฐธ๊ณ : ์ด ์ž‘์—…์ด ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค
236
- # ํ˜„์žฌ๋Š” ์˜ˆ์‹œ๋กœ ๋™๊ธฐ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค
237
- try:
238
- like_status = scrape_all_like_status(token)
239
- session['like_status'] = like_status
240
- except Exception as e:
241
- logger.error(f"์ข‹์•„์š” ์ƒํƒœ ์Šคํฌ๋ž˜ํ•‘ ์ค‘ ์˜ค๋ฅ˜: {e}")
242
- session['like_status'] = {}
243
-
244
  return jsonify({
245
  'success': True,
246
  'username': username
247
  })
248
 
 
249
  @app.route('/api/logout', methods=['POST'])
250
  def logout():
251
  session.pop('token', None)
252
  session.pop('username', None)
253
- session.pop('like_status', None)
254
  return jsonify({'success': True})
255
 
 
256
  @app.route('/api/urls', methods=['GET'])
257
  def get_urls():
258
- like_status = session.get('like_status', {})
259
 
260
  results = []
261
  for url in HUGGINGFACE_URLS:
@@ -265,156 +140,60 @@ def get_urls():
265
  if not model_info:
266
  continue
267
 
268
- # ์ข‹์•„์š” ์ƒํƒœ ํ™•์ธ
269
- is_liked = like_status.get(url, False)
270
 
271
  results.append({
272
  'url': url,
273
  'title': title,
274
- 'model_info': model_info,
275
- 'is_liked': is_liked
276
  })
277
 
278
  return jsonify(results)
279
 
280
- @app.route('/api/toggle-like', methods=['POST'])
281
- def toggle_like():
282
- if 'token' not in session:
283
- return jsonify({'success': False, 'message': '๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'})
284
-
285
- data = request.json
286
- url = data.get('url')
287
-
288
- if not url:
289
- return jsonify({'success': False, 'message': 'URL์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'})
290
-
291
- token = session['token']
292
-
293
- # URL์—์„œ ๋ชจ๋ธ ์ •๋ณด ์ถ”์ถœ
294
- model_info = extract_model_info(url)
295
- if not model_info:
296
- return jsonify({'success': False, 'message': '์ž˜๋ชป๋œ URL ํ˜•์‹์ž…๋‹ˆ๋‹ค.'})
297
-
298
- # ํ˜„์žฌ ์ข‹์•„์š” ์ƒํƒœ ํ™•์ธ
299
- like_status = session.get('like_status', {})
300
- current_status = like_status.get(url, False)
301
-
302
- # API ์š”์ฒญ์„ ์œ„ํ•œ ํ—ค๋” ๋ฐ ๋ฐ์ดํ„ฐ ์„ค์ •
303
- headers = {
304
- "Authorization": f"Bearer {token}",
305
- "Content-Type": "application/json"
306
- }
307
-
308
- # API ์—”๋“œํฌ์ธํŠธ ๋ฐ ๋ฉ”์„œ๋“œ ๊ฒฐ์ •
309
- # Hugging Face API์—์„œ ๋ชจ๋ธ/์ŠคํŽ˜์ด์Šค๋ฅผ ์ข‹์•„์š”/์ทจ์†Œํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ
310
- if model_info['type'] == 'spaces':
311
- api_url = f"https://huggingface.co/api/spaces/{model_info['full_id']}/like"
312
- else:
313
- api_url = f"https://huggingface.co/api/models/{model_info['full_id']}/like"
314
-
315
- # ํ˜„์žฌ ์ƒํƒœ์˜ ๋ฐ˜๋Œ€๋กœ ๋ณ€๊ฒฝ
316
- try:
317
- if current_status:
318
- # ์ข‹์•„์š” ์ทจ์†Œ (DELETE ์š”์ฒญ)
319
- response = requests.delete(api_url, headers=headers)
320
- else:
321
- # ์ข‹์•„์š” ์ถ”๊ฐ€ (POST ์š”์ฒญ)
322
- response = requests.post(api_url, headers=headers, json={})
323
-
324
- # ์‘๋‹ต ํ™•์ธ
325
- if response.status_code in [200, 201, 204]:
326
- # ์„ฑ๊ณต์ ์œผ๋กœ ๋ณ€๊ฒฝ๋˜๋ฉด ์„ธ์…˜ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
327
- new_status = not current_status
328
- like_status[url] = new_status
329
- session['like_status'] = like_status
330
-
331
- return jsonify({
332
- 'success': True,
333
- 'is_liked': new_status,
334
- 'message': '์ข‹์•„์š”๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.' if new_status else '์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ–ˆ์Šต๋‹ˆ๋‹ค.'
335
- })
336
- else:
337
- # API ์‘๋‹ต ์˜ค๋ฅ˜
338
- error_message = f"Hugging Face API ์˜ค๋ฅ˜ (์ƒํƒœ ์ฝ”๋“œ: {response.status_code})"
339
- try:
340
- error_data = response.json()
341
- if 'error' in error_data:
342
- error_message += f": {error_data['error']}"
343
- except:
344
- pass
345
-
346
- logger.error(f"{error_message}, ์‘๋‹ต: {response.text}")
347
- return jsonify({'success': False, 'message': error_message})
348
-
349
- except Exception as e:
350
- logger.error(f"์ข‹์•„์š” ์ƒํƒœ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜: {e}")
351
- return jsonify({
352
- 'success': False,
353
- 'message': f'์ข‹์•„์š” ์ƒํƒœ ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜: {str(e)}'
354
- })
355
-
356
- @app.route('/api/refresh-likes', methods=['POST'])
357
- def refresh_likes():
358
- if 'token' not in session:
359
- return jsonify({'success': False, 'message': '๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'})
360
-
361
- try:
362
- # ์›น ์Šคํฌ๋ž˜ํ•‘์œผ๋กœ ์ข‹์•„์š” ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ
363
- like_status = scrape_all_like_status(session['token'])
364
- session['like_status'] = like_status
365
-
366
- return jsonify({
367
- 'success': True,
368
- 'message': '์ข‹์•„์š” ์ƒํƒœ๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.',
369
- 'like_status': like_status
370
- })
371
- except Exception as e:
372
- logger.error(f"์ข‹์•„์š” ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์ค‘ ์˜ค๋ฅ˜: {e}")
373
- return jsonify({
374
- 'success': False,
375
- 'message': f'์ข‹์•„์š” ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์ค‘ ์˜ค๋ฅ˜: {str(e)}'
376
- })
377
-
378
  @app.route('/api/session-status', methods=['GET'])
379
  def session_status():
380
  return jsonify({
381
- 'logged_in': 'username' in session,
382
  'username': session.get('username')
383
  })
384
 
385
  if __name__ == '__main__':
 
386
  os.makedirs('templates', exist_ok=True)
387
 
 
388
  with open('templates/index.html', 'w', encoding='utf-8') as f:
389
  f.write('''
390
  <!DOCTYPE html>
391
- <html lang="ko">
392
  <head>
393
  <meta charset="UTF-8">
394
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
395
- <title>Hugging Face URL ๊ทธ๋ฆฌ๋“œ</title>
396
  <style>
397
  body {
398
- font-family: Arial, sans-serif;
399
  line-height: 1.6;
400
  margin: 0;
401
  padding: 0;
402
  color: #333;
403
- background-color: #f4f5f7;
404
  }
405
 
406
  .container {
407
- max-width: 1600px;
408
  margin: 0 auto;
409
  padding: 1rem;
410
  }
411
 
412
  .header {
413
- background-color: #fff;
414
  padding: 1rem;
415
  border-radius: 8px;
416
  margin-bottom: 1rem;
417
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
418
  }
419
 
420
  .user-controls {
@@ -425,31 +204,31 @@ if __name__ == '__main__':
425
  }
426
 
427
  .filter-controls {
428
- background-color: #fff;
429
  padding: 1rem;
430
  border-radius: 8px;
431
  margin-bottom: 1rem;
432
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
433
- display: flex;
434
- justify-content: space-between;
435
- align-items: center;
436
  }
437
 
438
  input[type="password"],
439
  input[type="text"] {
440
- padding: 0.5rem;
441
  border: 1px solid #ddd;
442
  border-radius: 4px;
443
  margin-right: 5px;
 
 
444
  }
445
 
446
  button {
447
- padding: 0.5rem 1rem;
448
  background-color: #4CAF50;
449
  color: white;
450
  border: none;
451
  border-radius: 4px;
452
  cursor: pointer;
 
453
  transition: background-color 0.2s;
454
  }
455
 
@@ -457,14 +236,6 @@ if __name__ == '__main__':
457
  background-color: #45a049;
458
  }
459
 
460
- button.refresh {
461
- background-color: #2196F3;
462
- }
463
-
464
- button.refresh:hover {
465
- background-color: #0b7dda;
466
- }
467
-
468
  button.logout {
469
  background-color: #f44336;
470
  }
@@ -475,7 +246,7 @@ if __name__ == '__main__':
475
 
476
  .token-help {
477
  margin-top: 0.5rem;
478
- font-size: 0.8rem;
479
  color: #666;
480
  }
481
 
@@ -488,143 +259,66 @@ if __name__ == '__main__':
488
  text-decoration: underline;
489
  }
490
 
491
- /* ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ ์Šคํƒ€์ผ */
492
  .grid-container {
493
  display: grid;
494
- grid-template-columns: repeat(4, 1fr);
495
- gap: 1rem;
496
  }
497
 
498
  .grid-item {
499
- border: 1px solid #ddd;
500
  border-radius: 8px;
501
- background-color: #fff;
502
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
503
- transition: all 0.3s ease;
504
- position: relative;
505
  display: flex;
506
  flex-direction: column;
507
- overflow: hidden;
508
- }
509
-
510
- .grid-item:hover {
511
- transform: translateY(-5px);
512
- box-shadow: 0 5px 15px rgba(0,0,0,0.1);
513
- }
514
-
515
- .grid-item.liked {
516
- border-color: #ff4757;
517
- background-color: #ffebee;
518
  }
519
 
520
  .grid-header {
521
- padding: 0.5rem 1rem;
522
  border-bottom: 1px solid #eee;
523
- position: relative;
524
  }
525
 
526
- .grid-title {
527
- font-size: 1rem;
528
  margin: 0;
529
- padding-right: 30px;
530
- white-space: nowrap;
531
- overflow: hidden;
532
- text-overflow: ellipsis;
533
  }
534
 
535
  .grid-content {
536
  flex: 1;
537
  position: relative;
538
- height: 300px;
 
539
  }
540
 
541
- .iframe-container {
542
  position: absolute;
543
  top: 0;
544
  left: 0;
545
  width: 100%;
546
  height: 100%;
547
- }
548
-
549
- .iframe-container iframe {
550
- width: 100%;
551
- height: 100%;
552
  border: none;
553
  }
554
 
555
- .like-button {
556
- position: absolute;
557
- top: 0.5rem;
558
- right: 0.5rem;
559
- width: 24px;
560
- height: 24px;
561
- display: flex;
562
- align-items: center;
563
- justify-content: center;
564
- border-radius: 50%;
565
- border: none;
566
- background: transparent;
567
- font-size: 1.2rem;
568
- cursor: pointer;
569
- transition: all 0.3s ease;
570
- color: #ddd;
571
- padding: 0;
572
- }
573
-
574
- .like-button:hover {
575
- transform: scale(1.2);
576
- }
577
-
578
- .like-button.liked {
579
- color: #ff4757;
580
- }
581
-
582
- .like-badge {
583
- position: absolute;
584
- top: -5px;
585
- left: -5px;
586
- background-color: #ff4757;
587
- color: white;
588
- padding: 0.2rem 0.5rem;
589
- border-radius: 4px;
590
- font-size: 0.7rem;
591
- font-weight: bold;
592
- z-index: 10;
593
- }
594
-
595
- .like-status {
596
- background-color: #fff;
597
- padding: 1rem;
598
- border-radius: 8px;
599
- margin-bottom: 1rem;
600
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
601
- display: none;
602
- }
603
-
604
- .like-status strong {
605
- color: #ff4757;
606
- }
607
-
608
  .status-message {
609
- position: fixed;
610
- bottom: 20px;
611
- right: 20px;
612
  padding: 1rem;
613
  border-radius: 8px;
 
614
  display: none;
615
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
616
- z-index: 1000;
617
- max-width: 300px;
618
  }
619
 
620
  .success {
621
- background-color: #4CAF50;
622
- color: white;
623
  }
624
 
625
  .error {
626
- background-color: #f44336;
627
- color: white;
628
  }
629
 
630
  .loading {
@@ -638,14 +332,15 @@ if __name__ == '__main__':
638
  justify-content: center;
639
  align-items: center;
640
  z-index: 1000;
 
641
  }
642
 
643
- .spinner {
644
- width: 40px;
645
- height: 40px;
646
- border: 4px solid #f3f3f3;
647
- border-top: 4px solid #3498db;
648
  border-radius: 50%;
 
 
649
  animation: spin 1s linear infinite;
650
  }
651
 
@@ -654,21 +349,6 @@ if __name__ == '__main__':
654
  100% { transform: rotate(360deg); }
655
  }
656
 
657
- .filter-toggle {
658
- display: flex;
659
- }
660
-
661
- .filter-toggle button {
662
- margin-right: 0.5rem;
663
- background-color: #f0f0f0;
664
- color: #333;
665
- }
666
-
667
- .filter-toggle button.active {
668
- background-color: #4CAF50;
669
- color: white;
670
- }
671
-
672
  .login-section {
673
  margin-top: 1rem;
674
  }
@@ -678,38 +358,6 @@ if __name__ == '__main__':
678
  margin-top: 1rem;
679
  }
680
 
681
- .note {
682
- padding: 0.5rem;
683
- background-color: #fffde7;
684
- border-left: 3px solid #ffd600;
685
- margin-bottom: 1rem;
686
- font-size: 0.9rem;
687
- }
688
-
689
- .view-toggle {
690
- margin-top: 1rem;
691
- display: flex;
692
- justify-content: space-between;
693
- align-items: center;
694
- }
695
-
696
- .view-toggle button {
697
- margin-left: 0.5rem;
698
- }
699
-
700
- /* ๋ฐ˜์‘ํ˜• ์„ค์ • */
701
- @media (max-width: 1400px) {
702
- .grid-container {
703
- grid-template-columns: repeat(3, 1fr);
704
- }
705
- }
706
-
707
- @media (max-width: 1024px) {
708
- .grid-container {
709
- grid-template-columns: repeat(2, 1fr);
710
- }
711
- }
712
-
713
  @media (max-width: 768px) {
714
  .user-controls {
715
  flex-direction: column;
@@ -720,14 +368,6 @@ if __name__ == '__main__':
720
  margin-bottom: 1rem;
721
  }
722
 
723
- .filter-controls {
724
- flex-direction: column;
725
- }
726
-
727
- .filter-controls > div {
728
- margin-bottom: 0.5rem;
729
- }
730
-
731
  .grid-container {
732
  grid-template-columns: 1fr;
733
  }
@@ -739,137 +379,86 @@ if __name__ == '__main__':
739
  <div class="header">
740
  <div class="user-controls">
741
  <div>
742
- <span>ํ—ˆ๊น…ํŽ˜์ด์Šค ๊ณ„์ •: </span>
743
- <span id="currentUser">๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์Œ</span>
744
  </div>
745
 
746
  <div id="loginSection" class="login-section">
747
- <input type="password" id="tokenInput" placeholder="ํ—ˆ๊น…ํŽ˜์ด์Šค API ํ† ํฐ ์ž…๋ ฅ" />
748
- <button id="loginButton">์ธ์ฆํ•˜๊ธฐ</button>
749
  <div class="token-help">
750
- API ํ† ํฐ์€ <a href="https://huggingface.co/settings/tokens" target="_blank">ํ—ˆ๊น…ํŽ˜์ด์Šค ํ† ํฐ ํŽ˜์ด์ง€</a>์—์„œ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
751
  </div>
752
  </div>
753
 
754
  <div id="loggedInSection" class="logged-in-section">
755
- <button id="refreshButton" class="refresh">์ƒˆ๋กœ๊ณ ์นจ</button>
756
- <button id="logoutButton" class="logout">๋กœ๊ทธ์•„์›ƒ</button>
757
  </div>
758
  </div>
759
  </div>
760
 
761
- <div class="note">
762
- <p><strong>์ฐธ๊ณ :</strong> ์ด ํŽ˜์ด์ง€๋Š” ์›น ์Šคํฌ๋ž˜ํ•‘ ๋ฐฉ์‹์œผ๋กœ ์ข‹์•„์š” ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์ข‹์•„์š” ์ƒํƒœ๊ฐ€ ์ •ํ™•ํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ์ง€์—ฐ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. '์ƒˆ๋กœ๊ณ ์นจ' ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ์ตœ์‹  ์ƒํƒœ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.</p>
763
- </div>
764
-
765
- <div id="likeStatus" class="like-status">
766
- <div id="likeStatsText">์ด <span id="totalUrlCount">0</span>๊ฐœ ์ค‘ <strong><span id="likedUrlCount">0</span>๊ฐœ</strong>์˜ URL์„ ์ข‹์•„์š” ํ–ˆ์Šต๋‹ˆ๋‹ค.</div>
767
- </div>
768
-
769
  <div class="filter-controls">
770
- <div>
771
- <input type="text" id="searchInput" placeholder="URL ๋˜๋Š” ์ œ๋ชฉ์œผ๋กœ ๊ฒ€์ƒ‰" style="width: 300px;" />
772
- </div>
773
- <div class="filter-toggle">
774
- <button id="allUrlsBtn" class="active">์ „์ฒด ๋ณด๊ธฐ</button>
775
- <button id="likedUrlsBtn">์ข‹์•„์š”๋งŒ ๋ณด๊ธฐ</button>
776
- </div>
777
- </div>
778
-
779
- <div class="view-toggle">
780
- <div>
781
- <input type="checkbox" id="embedToggle" checked />
782
- <label for="embedToggle">URL ์ž„๋ฒ ๋”ฉ ๋ณด๊ธฐ</label>
783
- </div>
784
  </div>
785
 
786
  <div id="statusMessage" class="status-message"></div>
787
 
788
- <div id="loadingIndicator" class="loading">
789
- <div class="spinner"></div>
790
- </div>
791
-
792
  <div id="gridContainer" class="grid-container"></div>
793
  </div>
794
 
 
 
 
 
795
  <script>
796
- // DOM ์š”์†Œ ์ฐธ์กฐ
797
- // DOM ์š”์†Œ ์ฐธ์กฐ
798
- const elements = {
799
- tokenInput: document.getElementById('tokenInput'),
800
- loginButton: document.getElementById('loginButton'),
801
- logoutButton: document.getElementById('logoutButton'),
802
- refreshButton: document.getElementById('refreshButton'),
803
- currentUser: document.getElementById('currentUser'),
804
- gridContainer: document.getElementById('gridContainer'),
805
- loadingIndicator: document.getElementById('loadingIndicator'),
806
- statusMessage: document.getElementById('statusMessage'),
807
- searchInput: document.getElementById('searchInput'),
808
- loginSection: document.getElementById('loginSection'),
809
- loggedInSection: document.getElementById('loggedInSection'),
810
- likeStatus: document.getElementById('likeStatus'),
811
- totalUrlCount: document.getElementById('totalUrlCount'),
812
- likedUrlCount: document.getElementById('likedUrlCount'),
813
- allUrlsBtn: document.getElementById('allUrlsBtn'),
814
- likedUrlsBtn: document.getElementById('likedUrlsBtn'),
815
- embedToggle: document.getElementById('embedToggle')
816
- };
817
-
818
-
819
- likeStatus: document.getElementById('likeStatus'),
820
- totalUrlCount: document.getElementById('totalUrlCount'),
821
- likedUrlCount: document.getElementById('likedUrlCount'),
822
- allUrlsBtn: document.getElementById('allUrlsBtn'),
823
- likedUrlsBtn: document.getElementById('likedUrlsBtn'),
824
- embedToggle: document.getElementById('embedToggle')
825
  };
826
 
827
- // ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒํƒœ
828
  const state = {
829
  username: null,
830
- allURLs: [],
831
- isLoading: false,
832
- viewMode: 'all', // 'all' ๋˜๋Š” 'liked'
833
- embedView: true // iframe ์ž„๋ฒ ๋”ฉ ์—ฌ๋ถ€
834
  };
835
 
836
- // ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ ํ•จ์ˆ˜
837
  function setLoading(isLoading) {
838
  state.isLoading = isLoading;
839
  elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
840
  }
841
 
842
- // ์ƒํƒœ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ํ•จ์ˆ˜
843
  function showMessage(message, isError = false) {
844
  elements.statusMessage.textContent = message;
845
  elements.statusMessage.className = `status-message ${isError ? 'error' : 'success'}`;
846
  elements.statusMessage.style.display = 'block';
847
 
848
- // 3์ดˆ ํ›„ ๋ฉ”์‹œ์ง€ ์‚ฌ๋ผ์ง
849
  setTimeout(() => {
850
  elements.statusMessage.style.display = 'none';
851
  }, 3000);
852
  }
853
 
854
- // API ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
855
  async function handleApiResponse(response) {
856
  if (!response.ok) {
857
  const errorText = await response.text();
858
- throw new Error(`API ์˜ค๋ฅ˜ (${response.status}): ${errorText}`);
859
  }
860
  return response.json();
861
  }
862
 
863
- // ์ข‹์•„์š” ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
864
- function updateLikeStats() {
865
- const totalCount = state.allURLs.length;
866
- const likedCount = state.allURLs.filter(item => item.is_liked).length;
867
-
868
- elements.totalUrlCount.textContent = totalCount;
869
- elements.likedUrlCount.textContent = likedCount;
870
- }
871
-
872
- // ์„ธ์…˜ ์ƒํƒœ ํ™•์ธ
873
  async function checkSessionStatus() {
874
  try {
875
  const response = await fetch('/api/session-status');
@@ -880,20 +469,19 @@ likeStatus: document.getElementById('likeStatus'),
880
  elements.currentUser.textContent = data.username;
881
  elements.loginSection.style.display = 'none';
882
  elements.loggedInSection.style.display = 'block';
883
- elements.likeStatus.style.display = 'block';
884
 
885
- // URL ๋ชฉ๋ก ๋กœ๋“œ
886
  loadUrls();
887
  }
888
  } catch (error) {
889
- console.error('์„ธ์…˜ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
890
  }
891
  }
892
 
893
- // ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
894
  async function login(token) {
895
  if (!token.trim()) {
896
- showMessage('ํ† ํฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.', true);
897
  return;
898
  }
899
 
@@ -916,24 +504,23 @@ likeStatus: document.getElementById('likeStatus'),
916
  elements.currentUser.textContent = state.username;
917
  elements.loginSection.style.display = 'none';
918
  elements.loggedInSection.style.display = 'block';
919
- elements.likeStatus.style.display = 'block';
920
 
921
- showMessage(`${state.username}๋‹˜์œผ๋กœ ๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`);
922
 
923
- // URL ๋ชฉ๋ก ๋กœ๋“œ
924
  loadUrls();
925
  } else {
926
- showMessage(data.message || '๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', true);
927
  }
928
  } catch (error) {
929
- console.error('๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:', error);
930
- showMessage(`๋กœ๊ทธ์ธ ์˜ค๋ฅ˜: ${error.message}`, true);
931
  } finally {
932
  setLoading(false);
933
  }
934
  }
935
 
936
- // ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ
937
  async function logout() {
938
  setLoading(true);
939
 
@@ -946,363 +533,121 @@ likeStatus: document.getElementById('likeStatus'),
946
 
947
  if (data.success) {
948
  state.username = null;
949
- state.allURLs = [];
950
 
951
- elements.currentUser.textContent = '๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์Œ';
952
  elements.tokenInput.value = '';
953
  elements.loginSection.style.display = 'block';
954
  elements.loggedInSection.style.display = 'none';
955
- elements.likeStatus.style.display = 'none';
956
 
957
- showMessage('๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
958
 
959
- // ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™”
960
  elements.gridContainer.innerHTML = '';
961
  }
962
  } catch (error) {
963
- console.error('๋กœ๊ทธ์•„์›ƒ ์˜ค๋ฅ˜:', error);
964
- showMessage(`๋กœ๊ทธ์•„์›ƒ ์˜ค๋ฅ˜: ${error.message}`, true);
965
- } finally {
966
- setLoading(false);
967
- }
968
- }
969
-
970
- // ์ข‹์•„์š” ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ
971
- async function refreshLikes() {
972
- setLoading(true);
973
-
974
- try {
975
- const response = await fetch('/api/refresh-likes', {
976
- method: 'POST'
977
- });
978
-
979
- const data = await handleApiResponse(response);
980
-
981
- if (data.success) {
982
- // URL ๋ชฉ๋ก ๋‹ค์‹œ ๋กœ๋“œ
983
- loadUrls();
984
- showMessage('์ข‹์•„์š” ์ƒํƒœ๊ฐ€ ์ƒˆ๋กœ๊ณ ์นจ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
985
- } else {
986
- showMessage(data.message || '์ข‹์•„์š” ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', true);
987
- }
988
- } catch (error) {
989
- console.error('์ข‹์•„์š” ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์˜ค๋ฅ˜:', error);
990
- showMessage(`์ข‹์•„์š” ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์˜ค๋ฅ˜: ${error.message}`, true);
991
  } finally {
992
  setLoading(false);
993
  }
994
  }
995
 
996
- // URL ๋ชฉ๋ก ๋กœ๋“œ
997
  async function loadUrls() {
998
  setLoading(true);
999
 
1000
  try {
1001
- const response = await fetch('/api/urls');
1002
- const data = await handleApiResponse(response);
1003
 
1004
- state.allURLs = data;
1005
- updateLikeStats();
1006
- renderGrid();
 
1007
  } catch (error) {
1008
- console.error('URL ๋ชฉ๋ก ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
1009
- showMessage(`URL ๋ชฉ๋ก ๋กœ๋“œ ์˜ค๋ฅ˜: ${error.message}`, true);
1010
  } finally {
1011
  setLoading(false);
1012
  }
1013
  }
1014
 
1015
- // ์ข‹์•„์š” ํ† ๊ธ€
1016
- async function toggleLike(url) {
1017
- try {
1018
- const response = await fetch('/api/toggle-like', {
1019
- method: 'POST',
1020
- headers: {
1021
- 'Content-Type': 'application/json'
1022
- },
1023
- body: JSON.stringify({ url })
1024
- });
1025
-
1026
- const data = await handleApiResponse(response);
1027
-
1028
- if (data.success) {
1029
- // URL ๊ฐ์ฒด ์ฐพ๊ธฐ
1030
- const urlObj = state.allURLs.find(item => item.url === url);
1031
- if (urlObj) {
1032
- urlObj.is_liked = data.is_liked;
1033
- updateLikeStats();
1034
- renderGrid();
1035
- }
1036
-
1037
- showMessage(data.message);
1038
- } else {
1039
- showMessage(data.message || '์ข‹์•„์š” ์ƒํƒœ ๋ณ€๊ฒฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', true);
1040
- }
1041
- } catch (error) {
1042
- console.error('์ข‹์•„์š” ํ† ๊ธ€ ์˜ค๋ฅ˜:', error);
1043
- showMessage(`์ข‹์•„์š” ํ† ๊ธ€ ์˜ค๋ฅ˜: ${error.message}`, true);
1044
- }
1045
- }
1046
-
1047
- // ๊ทธ๋ฆฌ๋“œ ๋ Œ๋”๋ง
1048
- function renderGrid() {
1049
  elements.gridContainer.innerHTML = '';
1050
 
1051
- let urlsToShow = state.allURLs;
1052
-
1053
- // ๊ฒ€์ƒ‰์–ด๋กœ ํ•„ํ„ฐ๋ง
1054
- const searchTerm = elements.searchInput.value.trim().toLowerCase();
1055
- if (searchTerm) {
1056
- urlsToShow = urlsToShow.filter(item =>
1057
- item.url.toLowerCase().includes(searchTerm) ||
1058
- item.title.toLowerCase().includes(searchTerm)
1059
- );
1060
- }
1061
-
1062
- // ๋ณด๊ธฐ ๋ชจ๋“œ๋กœ ํ•„ํ„ฐ๋ง (์ „์ฒด ๋˜๋Š” ์ข‹์•„์š”๋งŒ)
1063
- if (state.viewMode === 'liked') {
1064
- urlsToShow = urlsToShow.filter(item => item.is_liked);
1065
- }
1066
-
1067
- if (urlsToShow.length === 0) {
1068
- const emptyMessage = document.createElement('div');
1069
- emptyMessage.textContent = 'ํ‘œ์‹œํ•  URL์ด ์—†์Šต๋‹ˆ๋‹ค.';
1070
- emptyMessage.style.padding = '1rem';
1071
- emptyMessage.style.width = '100%';
1072
- emptyMessage.style.textAlign = 'center';
1073
- elements.gridContainer.appendChild(emptyMessage);
1074
  return;
1075
  }
1076
 
1077
- // ๊ทธ๋ฆฌ๋“œ ์•„์ดํ…œ ์ƒ์„ฑ
1078
- urlsToShow.forEach(item => {
1079
- const gridItem = document.createElement('div');
1080
- gridItem.className = `grid-item ${item.is_liked ? 'liked' : ''}`;
1081
 
1082
- if (item.is_liked) {
1083
- const badge = document.createElement('div');
1084
- badge.className = 'like-badge';
1085
- badge.textContent = '์ข‹์•„์š”';
1086
- gridItem.appendChild(badge);
1087
- }
1088
 
1089
- // ํ—ค๋” ๋ถ€๋ถ„
1090
  const header = document.createElement('div');
1091
  header.className = 'grid-header';
1092
 
1093
- const title = document.createElement('h3');
1094
- title.className = 'grid-title';
1095
- title.textContent = item.title;
1096
-
1097
- const likeButton = document.createElement('button');
1098
- likeButton.className = `like-button ${item.is_liked ? 'liked' : ''}`;
1099
- likeButton.innerHTML = 'โค';
1100
- likeButton.dataset.url = item.url;
1101
- likeButton.addEventListener('click', (e) => {
1102
- e.preventDefault();
1103
- toggleLike(item.url);
1104
- });
1105
 
1106
- header.appendChild(title);
1107
- header.appendChild(likeButton);
1108
 
1109
- // ์ปจํ…์ธ  ๋ถ€๋ถ„ (iframe ์ž„๋ฒ ๋”ฉ)
1110
  const content = document.createElement('div');
1111
  content.className = 'grid-content';
1112
 
1113
- if (state.embedView) {
1114
- const iframeContainer = document.createElement('div');
1115
- iframeContainer.className = 'iframe-container';
1116
-
1117
- // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ
1118
- const loadingIndicator = document.createElement('div');
1119
- loadingIndicator.className = 'loading-indicator';
1120
- loadingIndicator.style.position = 'absolute';
1121
- loadingIndicator.style.top = '0';
1122
- loadingIndicator.style.left = '0';
1123
- loadingIndicator.style.width = '100%';
1124
- loadingIndicator.style.height = '100%';
1125
- loadingIndicator.style.backgroundColor = '#f8f9fa';
1126
- loadingIndicator.style.display = 'flex';
1127
- loadingIndicator.style.alignItems = 'center';
1128
- loadingIndicator.style.justifyContent = 'center';
1129
- loadingIndicator.style.flexDirection = 'column';
1130
- loadingIndicator.style.zIndex = '10';
1131
-
1132
- const spinner = document.createElement('div');
1133
- spinner.className = 'spinner';
1134
- spinner.style.width = '40px';
1135
- spinner.style.height = '40px';
1136
- spinner.style.border = '4px solid #f3f3f3';
1137
- spinner.style.borderTop = '4px solid #3498db';
1138
- spinner.style.borderRadius = '50%';
1139
- spinner.style.animation = 'spin 1s linear infinite';
1140
-
1141
- const loadingText = document.createElement('div');
1142
- loadingText.textContent = '์ฝ˜ํ…์ธ  ๋กœ๋”ฉ ์ค‘...';
1143
- loadingText.style.marginTop = '10px';
1144
-
1145
- loadingIndicator.appendChild(spinner);
1146
- loadingIndicator.appendChild(loadingText);
1147
-
1148
- // ํ”„๋ก์‹œ๋ฅผ ํ†ตํ•œ URL ์ธ์ฝ”๋”ฉ
1149
- const originalUrl = item.url;
1150
- const proxyUrl = `/proxy/${encodeURIComponent(originalUrl.replace(/^https?:\/\//, ''))}`;
1151
-
1152
- // iframe ์ƒ์„ฑ
1153
- const iframe = document.createElement('iframe');
1154
- iframe.src = proxyUrl;
1155
- iframe.title = item.title;
1156
- iframe.width = '100%';
1157
- iframe.height = '100%';
1158
- iframe.style.border = 'none';
1159
- iframe.style.backgroundColor = 'white';
1160
-
1161
- // iframe ๋กœ๋“œ ์ด๋ฒคํŠธ
1162
- iframe.onload = () => {
1163
- loadingIndicator.style.display = 'none';
1164
- };
1165
-
1166
- // ๋กœ๋“œ ํƒ€์ž„์•„์›ƒ - 10์ดˆ ํ›„์—๋„ ๋กœ๋“œ๋˜์ง€ ์•Š์œผ๋ฉด ๋Œ€์ฒด UI ํ‘œ์‹œ
1167
- let loadTimeout = setTimeout(() => {
1168
- if (loadingIndicator.style.display !== 'none') {
1169
- loadingIndicator.innerHTML = '';
1170
-
1171
- const errorIcon = document.createElement('div');
1172
- errorIcon.style.fontSize = '32px';
1173
- errorIcon.textContent = 'โš ๏ธ';
1174
-
1175
- const errorText = document.createElement('div');
1176
- errorText.textContent = '์ฝ˜ํ…์ธ ๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค';
1177
- errorText.style.margin = '10px 0';
1178
- errorText.style.fontWeight = 'bold';
1179
-
1180
- const errorDesc = document.createElement('div');
1181
- errorDesc.textContent = '์ด ์ฝ˜ํ…์ธ ๋Š” ์™ธ๋ถ€ ์ž„๋ฒ ๋”ฉ์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.';
1182
- errorDesc.style.fontSize = '0.9rem';
1183
- errorDesc.style.marginBottom = '15px';
1184
- errorDesc.style.textAlign = 'center';
1185
-
1186
- const visitLink = document.createElement('a');
1187
- visitLink.href = originalUrl;
1188
- visitLink.target = '_blank';
1189
- visitLink.textContent = '์ƒˆ ์ฐฝ์—์„œ ์ง์ ‘ ๋ฐฉ๋ฌธํ•˜๊ธฐ';
1190
- visitLink.style.color = 'white';
1191
- visitLink.style.backgroundColor = '#3498db';
1192
- visitLink.style.padding = '8px 16px';
1193
- visitLink.style.borderRadius = '4px';
1194
- visitLink.style.textDecoration = 'none';
1195
-
1196
- loadingIndicator.appendChild(errorIcon);
1197
- loadingIndicator.appendChild(errorText);
1198
- loadingIndicator.appendChild(errorDesc);
1199
- loadingIndicator.appendChild(visitLink);
1200
- }
1201
- }, 10000);
1202
-
1203
- // iframe ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
1204
- iframe.onerror = () => {
1205
- clearTimeout(loadTimeout);
1206
- loadingIndicator.innerHTML = '์ฝ˜ํ…์ธ ๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. <a href="' + originalUrl + '" target="_blank">์ง์ ‘ ๋ฐฉ๋ฌธํ•˜๊ธฐ</a>';
1207
- };
1208
-
1209
- iframeContainer.appendChild(iframe);
1210
- iframeContainer.appendChild(loadingIndicator);
1211
-
1212
- content.appendChild(iframeContainer);
1213
- } else {
1214
- // ์ž„๋ฒ ๋”ฉ ์—†๋Š” ๊ฒฝ์šฐ ๋งํฌ๋งŒ ํ‘œ์‹œ
1215
- const linkContainer = document.createElement('div');
1216
- linkContainer.style.padding = '1rem';
1217
-
1218
- const link = document.createElement('a');
1219
- link.href = item.url;
1220
- link.textContent = item.url;
1221
- link.target = '_blank';
1222
-
1223
- const owner = document.createElement('div');
1224
- owner.textContent = `์†Œ์œ ์ž: ${item.model_info.owner}`;
1225
- owner.style.marginTop = '0.5rem';
1226
-
1227
- const repo = document.createElement('div');
1228
- repo.textContent = `์ €์žฅ์†Œ: ${item.model_info.repo}`;
1229
-
1230
- const type = document.createElement('div');
1231
- type.textContent = `์œ ํ˜•: ${item.model_info.type}`;
1232
-
1233
- linkContainer.appendChild(link);
1234
- linkContainer.appendChild(owner);
1235
- linkContainer.appendChild(repo);
1236
- linkContainer.appendChild(type);
1237
-
1238
- content.appendChild(linkContainer);
1239
- }
1240
 
1241
- // ๊ทธ๋ฆฌ๋“œ ์•„์ดํ…œ์— ์ถ”๊ฐ€
1242
- gridItem.appendChild(header);
 
1243
  gridItem.appendChild(content);
1244
 
 
1245
  elements.gridContainer.appendChild(gridItem);
1246
  });
1247
  }
1248
 
1249
- // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
1250
- function registerEventListeners() {
1251
- // ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ
1252
- elements.loginButton.addEventListener('click', () => {
 
 
 
 
 
 
1253
  login(elements.tokenInput.value);
1254
- });
1255
-
1256
- // ์—”ํ„ฐ ํ‚ค๋กœ ๋กœ๊ทธ์ธ
1257
- elements.tokenInput.addEventListener('keydown', (e) => {
1258
- if (e.key === 'Enter') {
1259
- login(elements.tokenInput.value);
1260
- }
1261
- });
1262
-
1263
- // ๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ
1264
- elements.logoutButton.addEventListener('click', logout);
1265
-
1266
- // ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ
1267
- elements.refreshButton.addEventListener('click', refreshLikes);
1268
-
1269
- // ๊ฒ€์ƒ‰ ์ž…๋ ฅ ํ•„๋“œ
1270
- elements.searchInput.addEventListener('input', renderGrid);
1271
-
1272
- // ํ•„ํ„ฐ ๋ฒ„ํŠผ - ์ „์ฒด ๋ณด๊ธฐ
1273
- elements.allUrlsBtn.addEventListener('click', () => {
1274
- elements.allUrlsBtn.classList.add('active');
1275
- elements.likedUrlsBtn.classList.remove('active');
1276
- state.viewMode = 'all';
1277
- renderGrid();
1278
- });
1279
-
1280
- // ํ•„ํ„ฐ ๋ฒ„ํŠผ - ์ข‹์•„์š”๋งŒ ๋ณด๊ธฐ
1281
- elements.likedUrlsBtn.addEventListener('click', () => {
1282
- elements.likedUrlsBtn.classList.add('active');
1283
- elements.allUrlsBtn.classList.remove('active');
1284
- state.viewMode = 'liked';
1285
- renderGrid();
1286
- });
1287
-
1288
- // ์ž„๋ฒ ๋”ฉ ํ† ๊ธ€
1289
- elements.embedToggle.addEventListener('change', () => {
1290
- state.embedView = elements.embedToggle.checked;
1291
- renderGrid();
1292
- });
1293
- }
1294
 
1295
- // ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
1296
- function init() {
1297
- registerEventListeners();
1298
- checkSessionStatus();
1299
- }
 
1300
 
1301
- // ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ดˆ๊ธฐํ™”
1302
- init();
1303
  </script>
1304
  </body>
1305
  </html>
1306
  ''')
1307
 
1308
- app.run(debug=True, host='0.0.0.0', port=7860)
 
 
1
+ from flask import Flask, render_template, request, redirect, url_for, jsonify, session
2
  import requests
 
3
  import os
4
  from datetime import timedelta
 
 
 
 
 
 
 
5
 
6
  app = Flask(__name__)
7
+ app.secret_key = os.urandom(24) # Session encryption key
8
+ app.permanent_session_lifetime = timedelta(days=7) # Session duration
9
 
10
+ # Huggingface URL list
11
  HUGGINGFACE_URLS = [
12
  "https://huggingface.co/spaces/ginipick/Tech_Hangman_Game",
13
  "https://huggingface.co/spaces/openfree/deepseek_r1_API",
 
30
  "https://huggingface.co/spaces/openfree/Article-Generator",
31
  ]
32
 
33
+ # Extract model/space info from URL
34
  def extract_model_info(url):
35
  parts = url.split('/')
36
  if len(parts) < 6:
 
44
  'full_id': f"{parts[4]}/{parts[5]}"
45
  }
46
  elif len(parts) >= 5:
47
+ # Other URL format
48
  return {
49
+ 'type': 'models', # Default
50
  'owner': parts[3],
51
  'repo': parts[4],
52
  'full_id': f"{parts[3]}/{parts[4]}"
 
54
 
55
  return None
56
 
57
+ # Extract title from the last part of URL
58
  def extract_title(url):
59
  parts = url.split("/")
60
  title = parts[-1] if parts else ""
61
  return title.replace("_", " ").replace("-", " ")
62
 
63
+ # Huggingface token validation
64
  def validate_token(token):
65
  headers = {"Authorization": f"Bearer {token}"}
66
 
67
+ # Try whoami-v2 endpoint first
68
  try:
69
  response = requests.get("https://huggingface.co/api/whoami-v2", headers=headers)
70
  if response.ok:
71
  return True, response.json()
72
  except Exception as e:
73
+ print(f"whoami-v2 token validation error: {e}")
 
 
 
 
 
 
 
 
 
74
 
75
+ # Try the original whoami endpoint
76
  try:
77
+ response = requests.get("https://huggingface.co/api/whoami", headers=headers)
78
+ if response.ok:
79
+ return True, response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  except Exception as e:
81
+ print(f"whoami token validation error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ return False, None
84
 
85
+ # Homepage route
86
  @app.route('/')
87
  def home():
88
  return render_template('index.html')
89
 
90
+ # Login API
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  @app.route('/api/login', methods=['POST'])
92
  def login():
93
  token = request.form.get('token', '')
94
 
95
  if not token:
96
+ return jsonify({'success': False, 'message': 'Please enter a token.'})
97
 
98
  is_valid, user_info = validate_token(token)
99
 
100
  if not is_valid or not user_info:
101
+ return jsonify({'success': False, 'message': 'Invalid token.'})
102
 
103
+ # Find username
104
  username = None
105
  if 'name' in user_info:
106
  username = user_info['name']
 
109
  elif 'username' in user_info:
110
  username = user_info['username']
111
  else:
112
+ username = 'Authenticated User'
113
 
114
+ # Save to session
115
  session['token'] = token
116
  session['username'] = username
117
 
 
 
 
 
 
 
 
 
 
 
118
  return jsonify({
119
  'success': True,
120
  'username': username
121
  })
122
 
123
+ # Logout API
124
  @app.route('/api/logout', methods=['POST'])
125
  def logout():
126
  session.pop('token', None)
127
  session.pop('username', None)
 
128
  return jsonify({'success': True})
129
 
130
+ # URL list API
131
  @app.route('/api/urls', methods=['GET'])
132
  def get_urls():
133
+ search_query = request.args.get('search', '').lower()
134
 
135
  results = []
136
  for url in HUGGINGFACE_URLS:
 
140
  if not model_info:
141
  continue
142
 
143
+ if search_query and search_query not in url.lower() and search_query not in title.lower():
144
+ continue
145
 
146
  results.append({
147
  'url': url,
148
  'title': title,
149
+ 'model_info': model_info
 
150
  })
151
 
152
  return jsonify(results)
153
 
154
+ # Session status API
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  @app.route('/api/session-status', methods=['GET'])
156
  def session_status():
157
  return jsonify({
158
+ 'logged_in': 'token' in session,
159
  'username': session.get('username')
160
  })
161
 
162
  if __name__ == '__main__':
163
+ # Create templates folder
164
  os.makedirs('templates', exist_ok=True)
165
 
166
+ # Create index.html file
167
  with open('templates/index.html', 'w', encoding='utf-8') as f:
168
  f.write('''
169
  <!DOCTYPE html>
170
+ <html lang="en">
171
  <head>
172
  <meta charset="UTF-8">
173
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
174
+ <title>Huggingface Spaces Grid</title>
175
  <style>
176
  body {
177
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
178
  line-height: 1.6;
179
  margin: 0;
180
  padding: 0;
181
  color: #333;
182
+ background-color: #f5f8fa;
183
  }
184
 
185
  .container {
186
+ max-width: 1400px;
187
  margin: 0 auto;
188
  padding: 1rem;
189
  }
190
 
191
  .header {
192
+ background-color: #ffffff;
193
  padding: 1rem;
194
  border-radius: 8px;
195
  margin-bottom: 1rem;
196
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
197
  }
198
 
199
  .user-controls {
 
204
  }
205
 
206
  .filter-controls {
207
+ background-color: #ffffff;
208
  padding: 1rem;
209
  border-radius: 8px;
210
  margin-bottom: 1rem;
211
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
 
 
 
212
  }
213
 
214
  input[type="password"],
215
  input[type="text"] {
216
+ padding: 0.7rem;
217
  border: 1px solid #ddd;
218
  border-radius: 4px;
219
  margin-right: 5px;
220
+ font-size: 1rem;
221
+ width: 250px;
222
  }
223
 
224
  button {
225
+ padding: 0.7rem 1.2rem;
226
  background-color: #4CAF50;
227
  color: white;
228
  border: none;
229
  border-radius: 4px;
230
  cursor: pointer;
231
+ font-size: 1rem;
232
  transition: background-color 0.2s;
233
  }
234
 
 
236
  background-color: #45a049;
237
  }
238
 
 
 
 
 
 
 
 
 
239
  button.logout {
240
  background-color: #f44336;
241
  }
 
246
 
247
  .token-help {
248
  margin-top: 0.5rem;
249
+ font-size: 0.9rem;
250
  color: #666;
251
  }
252
 
 
259
  text-decoration: underline;
260
  }
261
 
 
262
  .grid-container {
263
  display: grid;
264
+ grid-template-columns: repeat(auto-fill, minmax(600px, 1fr));
265
+ gap: 1.5rem;
266
  }
267
 
268
  .grid-item {
269
+ background-color: #ffffff;
270
  border-radius: 8px;
271
+ overflow: hidden;
272
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
 
 
273
  display: flex;
274
  flex-direction: column;
 
 
 
 
 
 
 
 
 
 
 
275
  }
276
 
277
  .grid-header {
278
+ padding: 1rem;
279
  border-bottom: 1px solid #eee;
280
+ background-color: #f9f9f9;
281
  }
282
 
283
+ .grid-header h3 {
 
284
  margin: 0;
285
+ padding: 0;
286
+ font-size: 1.2rem;
287
+ color: #333;
 
288
  }
289
 
290
  .grid-content {
291
  flex: 1;
292
  position: relative;
293
+ height: 500px;
294
+ overflow: hidden;
295
  }
296
 
297
+ .grid-content iframe {
298
  position: absolute;
299
  top: 0;
300
  left: 0;
301
  width: 100%;
302
  height: 100%;
 
 
 
 
 
303
  border: none;
304
  }
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  .status-message {
 
 
 
307
  padding: 1rem;
308
  border-radius: 8px;
309
+ margin-bottom: 1rem;
310
  display: none;
311
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
 
 
312
  }
313
 
314
  .success {
315
+ background-color: #dff0d8;
316
+ color: #3c763d;
317
  }
318
 
319
  .error {
320
+ background-color: #f2dede;
321
+ color: #a94442;
322
  }
323
 
324
  .loading {
 
332
  justify-content: center;
333
  align-items: center;
334
  z-index: 1000;
335
+ font-size: 1.5rem;
336
  }
337
 
338
+ .loading-spinner {
339
+ border: 5px solid #f3f3f3;
340
+ border-top: 5px solid #4CAF50;
 
 
341
  border-radius: 50%;
342
+ width: 50px;
343
+ height: 50px;
344
  animation: spin 1s linear infinite;
345
  }
346
 
 
349
  100% { transform: rotate(360deg); }
350
  }
351
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  .login-section {
353
  margin-top: 1rem;
354
  }
 
358
  margin-top: 1rem;
359
  }
360
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  @media (max-width: 768px) {
362
  .user-controls {
363
  flex-direction: column;
 
368
  margin-bottom: 1rem;
369
  }
370
 
 
 
 
 
 
 
 
 
371
  .grid-container {
372
  grid-template-columns: 1fr;
373
  }
 
379
  <div class="header">
380
  <div class="user-controls">
381
  <div>
382
+ <span>Huggingface Account: </span>
383
+ <span id="currentUser">Not logged in</span>
384
  </div>
385
 
386
  <div id="loginSection" class="login-section">
387
+ <input type="password" id="tokenInput" placeholder="Enter Huggingface API Token" />
388
+ <button id="loginButton">Authenticate</button>
389
  <div class="token-help">
390
+ You can generate an API token on the <a href="https://huggingface.co/settings/tokens" target="_blank">Huggingface tokens page</a>.
391
  </div>
392
  </div>
393
 
394
  <div id="loggedInSection" class="logged-in-section">
395
+ <button id="logoutButton" class="logout">Logout</button>
 
396
  </div>
397
  </div>
398
  </div>
399
 
 
 
 
 
 
 
 
 
400
  <div class="filter-controls">
401
+ <input type="text" id="searchInput" placeholder="Search by URL or title" />
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  </div>
403
 
404
  <div id="statusMessage" class="status-message"></div>
405
 
 
 
 
 
406
  <div id="gridContainer" class="grid-container"></div>
407
  </div>
408
 
409
+ <div id="loadingIndicator" class="loading">
410
+ <div class="loading-spinner"></div>
411
+ </div>
412
+
413
  <script>
414
+ // DOM element references
415
+ const elements = {
416
+ tokenInput: document.getElementById('tokenInput'),
417
+ loginButton: document.getElementById('loginButton'),
418
+ logoutButton: document.getElementById('logoutButton'),
419
+ currentUser: document.getElementById('currentUser'),
420
+ gridContainer: document.getElementById('gridContainer'),
421
+ loadingIndicator: document.getElementById('loadingIndicator'),
422
+ statusMessage: document.getElementById('statusMessage'),
423
+ searchInput: document.getElementById('searchInput'),
424
+ loginSection: document.getElementById('loginSection'),
425
+ loggedInSection: document.getElementById('loggedInSection')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  };
427
 
428
+ // Application state
429
  const state = {
430
  username: null,
431
+ isLoading: false
 
 
 
432
  };
433
 
434
+ // Display loading indicator
435
  function setLoading(isLoading) {
436
  state.isLoading = isLoading;
437
  elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
438
  }
439
 
440
+ // Display status message
441
  function showMessage(message, isError = false) {
442
  elements.statusMessage.textContent = message;
443
  elements.statusMessage.className = `status-message ${isError ? 'error' : 'success'}`;
444
  elements.statusMessage.style.display = 'block';
445
 
446
+ // Hide message after 3 seconds
447
  setTimeout(() => {
448
  elements.statusMessage.style.display = 'none';
449
  }, 3000);
450
  }
451
 
452
+ // API error handling
453
  async function handleApiResponse(response) {
454
  if (!response.ok) {
455
  const errorText = await response.text();
456
+ throw new Error(`API error (${response.status}): ${errorText}`);
457
  }
458
  return response.json();
459
  }
460
 
461
+ // Check session status
 
 
 
 
 
 
 
 
 
462
  async function checkSessionStatus() {
463
  try {
464
  const response = await fetch('/api/session-status');
 
469
  elements.currentUser.textContent = data.username;
470
  elements.loginSection.style.display = 'none';
471
  elements.loggedInSection.style.display = 'block';
 
472
 
473
+ // Load URL list
474
  loadUrls();
475
  }
476
  } catch (error) {
477
+ console.error('Session status check error:', error);
478
  }
479
  }
480
 
481
+ // Login process
482
  async function login(token) {
483
  if (!token.trim()) {
484
+ showMessage('Please enter a token.', true);
485
  return;
486
  }
487
 
 
504
  elements.currentUser.textContent = state.username;
505
  elements.loginSection.style.display = 'none';
506
  elements.loggedInSection.style.display = 'block';
 
507
 
508
+ showMessage(`Logged in as ${state.username}.`);
509
 
510
+ // Load URL list
511
  loadUrls();
512
  } else {
513
+ showMessage(data.message || 'Login failed.', true);
514
  }
515
  } catch (error) {
516
+ console.error('Login error:', error);
517
+ showMessage(`Login error: ${error.message}`, true);
518
  } finally {
519
  setLoading(false);
520
  }
521
  }
522
 
523
+ // Logout process
524
  async function logout() {
525
  setLoading(true);
526
 
 
533
 
534
  if (data.success) {
535
  state.username = null;
 
536
 
537
+ elements.currentUser.textContent = 'Not logged in';
538
  elements.tokenInput.value = '';
539
  elements.loginSection.style.display = 'block';
540
  elements.loggedInSection.style.display = 'none';
 
541
 
542
+ showMessage('Logged out successfully.');
543
 
544
+ // Clear grid
545
  elements.gridContainer.innerHTML = '';
546
  }
547
  } catch (error) {
548
+ console.error('Logout error:', error);
549
+ showMessage(`Logout error: ${error.message}`, true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  } finally {
551
  setLoading(false);
552
  }
553
  }
554
 
555
+ // Load URL list
556
  async function loadUrls() {
557
  setLoading(true);
558
 
559
  try {
560
+ const searchText = elements.searchInput.value;
 
561
 
562
+ const response = await fetch(`/api/urls?search=${encodeURIComponent(searchText)}`);
563
+ const urls = await handleApiResponse(response);
564
+
565
+ renderGrid(urls);
566
  } catch (error) {
567
+ console.error('URL list loading error:', error);
568
+ showMessage(`URL loading error: ${error.message}`, true);
569
  } finally {
570
  setLoading(false);
571
  }
572
  }
573
 
574
+ // Render grid
575
+ function renderGrid(urls) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
  elements.gridContainer.innerHTML = '';
577
 
578
+ if (!urls || urls.length === 0) {
579
+ const noResultsMsg = document.createElement('p');
580
+ noResultsMsg.textContent = 'No URLs to display.';
581
+ noResultsMsg.style.padding = '1rem';
582
+ noResultsMsg.style.fontStyle = 'italic';
583
+ elements.gridContainer.appendChild(noResultsMsg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  return;
585
  }
586
 
587
+ urls.forEach(item => {
588
+ const { url, title } = item;
 
 
589
 
590
+ // Create grid item
591
+ const gridItem = document.createElement('div');
592
+ gridItem.className = 'grid-item';
 
 
 
593
 
594
+ // Header with title
595
  const header = document.createElement('div');
596
  header.className = 'grid-header';
597
 
598
+ const titleEl = document.createElement('h3');
599
+ titleEl.textContent = title;
600
+ header.appendChild(titleEl);
 
 
 
 
 
 
 
 
 
601
 
602
+ // Add header to grid item
603
+ gridItem.appendChild(header);
604
 
605
+ // Content with iframe
606
  const content = document.createElement('div');
607
  content.className = 'grid-content';
608
 
609
+ const iframe = document.createElement('iframe');
610
+ iframe.src = url;
611
+ iframe.title = title;
612
+ iframe.loading = 'lazy'; // Lazy load iframes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
613
 
614
+ content.appendChild(iframe);
615
+
616
+ // Add content to grid item
617
  gridItem.appendChild(content);
618
 
619
+ // Add grid item to container
620
  elements.gridContainer.appendChild(gridItem);
621
  });
622
  }
623
 
624
+ // Event listeners
625
+ elements.loginButton.addEventListener('click', () => {
626
+ login(elements.tokenInput.value);
627
+ });
628
+
629
+ elements.logoutButton.addEventListener('click', logout);
630
+
631
+ // Login with Enter key
632
+ elements.tokenInput.addEventListener('keypress', (event) => {
633
+ if (event.key === 'Enter') {
634
  login(elements.tokenInput.value);
635
+ }
636
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
 
638
+ // Filter event listener
639
+ elements.searchInput.addEventListener('input', () => {
640
+ // Debounce input to prevent API calls on every keystroke
641
+ clearTimeout(state.searchTimeout);
642
+ state.searchTimeout = setTimeout(loadUrls, 300);
643
+ });
644
 
645
+ // Initialize
646
+ checkSessionStatus();
647
  </script>
648
  </body>
649
  </html>
650
  ''')
651
 
652
+ # Use port 7860 for Huggingface Spaces
653
+ app.run(host='0.0.0.0', port=7860)