Spaces:
Running
Running
| from flask import Flask, render_template, request, jsonify | |
| import requests | |
| import os | |
| from collections import Counter | |
| app = Flask(__name__) | |
| # 1) ํธ๋ ๋ฉ ๋ชจ๋ธ์ ๊ฐ์ ธ์ค๋ ํจ์ | |
| def fetch_trending_models(offset=0, limit=72): | |
| try: | |
| url = "https://huggingface.co/api/models" | |
| params = {"limit": 10000} # ๋ง์ ๋ชจ๋ธ์ ๊ฐ์ ธ์ด | |
| response = requests.get(url, params=params, timeout=30) | |
| if response.status_code == 200: | |
| models = response.json() | |
| # ํํฐ๋ง | |
| filtered = [ | |
| m for m in models | |
| if m.get('owner') != 'None' and m.get('id', '').split('/', 1)[0] != 'None' | |
| ] | |
| start = min(offset, len(filtered)) | |
| end = min(offset + limit, len(filtered)) | |
| print(f"Fetched {len(filtered)} models, returning {end - start} from {start} to {end}.") | |
| return { | |
| 'models': filtered[start:end], | |
| 'total': len(filtered), | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'all_models': filtered | |
| } | |
| else: | |
| # ์ค๋ฅ ์๋ต ์ ๋๋ฏธ ๋ชจ๋ธ | |
| print(f"Error fetching models: {response.status_code}") | |
| return { | |
| 'models': generate_dummy_models(limit), | |
| 'total': 200, | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'all_models': generate_dummy_models(500) | |
| } | |
| except Exception as e: | |
| print("Exception when fetching models:", e) | |
| return { | |
| 'models': generate_dummy_models(limit), | |
| 'total': 200, | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'all_models': generate_dummy_models(500) | |
| } | |
| # 2) ๋๋ฏธ ๋ชจ๋ธ ์์ฑ ํจ์(์ค๋ฅ์ ์ฌ์ฉ) | |
| def generate_dummy_models(count): | |
| dummy_list = [] | |
| for i in range(count): | |
| dummy_list.append({ | |
| 'id': f'dummy/model-{i}', | |
| 'owner': 'dummy', | |
| 'title': f'Example Model {i+1}', | |
| 'likes': 100 - i, | |
| 'downloads': 9999 - i, # ์์์ ๋ค์ด๋ก๋ ์ | |
| 'createdAt': '2023-01-01T00:00:00.000Z', | |
| 'tags': ['dummy', 'fallback'] | |
| }) | |
| return dummy_list | |
| # 3) ๋ชจ๋ธ URL ์์ฑ | |
| def transform_url(owner, name): | |
| name = name.replace('.', '-').replace('_', '-').lower() | |
| owner = owner.lower() | |
| return f"https://huggingface.co/{owner}/{name}" | |
| # 4) ๋ชจ๋ธ ์์ธ์ ๋ณด ๊ฐ๊ณต | |
| def get_model_details(model_data, index, offset): | |
| try: | |
| if '/' in model_data.get('id', ''): | |
| owner, name = model_data['id'].split('/', 1) | |
| else: | |
| owner = model_data.get('owner', '') | |
| name = model_data.get('id', '') | |
| if owner == 'None' or name == 'None': | |
| return None | |
| original_url = f"https://huggingface.co/{owner}/{name}" | |
| embed_url = transform_url(owner, name) | |
| # ๋ค์ด๋ก๋, ์ข์์ | |
| likes_count = model_data.get('likes', 0) | |
| downloads_count = model_data.get('downloads', 0) # ๋ค์ด๋ก๋ ์ ์ถ๊ฐ | |
| title = model_data.get('title', name) | |
| tags = model_data.get('tags', []) | |
| return { | |
| 'url': original_url, | |
| 'embedUrl': embed_url, | |
| 'title': title, | |
| 'owner': owner, | |
| 'name': name, | |
| 'likes_count': likes_count, | |
| 'downloads_count': downloads_count, # ๋ค์ด๋ก๋ ์ | |
| 'tags': tags, | |
| 'rank': offset + index + 1 | |
| } | |
| except Exception as e: | |
| print("Error processing model data:", e) | |
| return { | |
| 'url': 'https://huggingface.co', | |
| 'embedUrl': 'https://huggingface.co', | |
| 'title': 'Error Loading Model', | |
| 'owner': 'huggingface', | |
| 'name': 'error', | |
| 'likes_count': 0, | |
| 'downloads_count': 0, | |
| 'tags': [], | |
| 'rank': offset + index + 1 | |
| } | |
| # 5) ์ค๋ ํต๊ณ (Top 30) | |
| def get_owner_stats(all_models): | |
| owners = [] | |
| for m in all_models: | |
| if '/' in m.get('id', ''): | |
| o, _ = m['id'].split('/', 1) | |
| else: | |
| o = m.get('owner', '') | |
| if o != 'None': | |
| owners.append(o) | |
| c = Counter(owners) | |
| return c.most_common(30) | |
| def home(): | |
| return render_template('index.html') | |
| # 6) ํธ๋ ๋ฉ ๋ชจ๋ธ API | |
| def trending_models(): | |
| search_query = request.args.get('search', '').lower() | |
| offset = int(request.args.get('offset', 0)) | |
| limit = int(request.args.get('limit', 72)) | |
| data = fetch_trending_models(offset, limit) | |
| results = [] | |
| for index, md in enumerate(data['models']): | |
| info = get_model_details(md, index, offset) | |
| if not info: | |
| continue | |
| if search_query: | |
| title_l = info['title'].lower() | |
| owner_l = info['owner'].lower() | |
| url_l = info['url'].lower() | |
| tags_l = ' '.join(t.lower() for t in info['tags']) | |
| # ๊ฒ์ ์กฐ๊ฑด์ ์์ผ๋ฉด pass | |
| if (search_query not in title_l and | |
| search_query not in owner_l and | |
| search_query not in url_l and | |
| search_query not in tags_l): | |
| continue | |
| results.append(info) | |
| top_owners = get_owner_stats(data['all_models']) | |
| return jsonify({ | |
| 'models': results, | |
| 'total': data['total'], | |
| 'offset': offset, | |
| 'limit': limit, | |
| 'top_owners': top_owners | |
| }) | |
| if __name__ == '__main__': | |
| # ํ ํ๋ฆฟ ํด๋ ์์ฑ | |
| os.makedirs('templates', exist_ok=True) | |
| # index.html ์์ฑ | |
| with open('templates/index.html', 'w', encoding='utf-8') as f: | |
| f.write("""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Huggingface Models Gallery</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;600;700&display=swap'); | |
| :root { | |
| --pastel-pink: #FFD6E0; | |
| --pastel-blue: #C5E8FF; | |
| --pastel-purple: #E0C3FC; | |
| --pastel-yellow: #FFF2CC; | |
| --pastel-green: #C7F5D9; | |
| --pastel-orange: #FFE0C3; | |
| --mac-window-bg: rgba(250, 250, 250, 0.85); | |
| --mac-toolbar: #F5F5F7; | |
| --mac-border: #E2E2E2; | |
| --mac-button-red: #FF5F56; | |
| --mac-button-yellow: #FFBD2E; | |
| --mac-button-green: #27C93F; | |
| --text-primary: #333; | |
| --text-secondary: #666; | |
| --box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | |
| } | |
| * { | |
| margin: 0; padding: 0; box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Nunito', sans-serif; | |
| color: var(--text-primary); | |
| min-height: 100vh; | |
| background: linear-gradient(135deg, var(--pastel-blue) 0%, var(--pastel-purple) 100%); | |
| padding: 2rem; | |
| } | |
| .container { | |
| max-width: 1600px; | |
| margin: 0 auto; | |
| } | |
| .mac-window { | |
| background-color: var(--mac-window-bg); | |
| border-radius: 10px; | |
| box-shadow: var(--box-shadow); | |
| backdrop-filter: blur(10px); | |
| overflow: hidden; | |
| margin-bottom: 2rem; | |
| border: 1px solid var(--mac-border); | |
| } | |
| .mac-toolbar { | |
| display: flex; align-items: center; | |
| padding: 10px 15px; background-color: var(--mac-toolbar); | |
| border-bottom: 1px solid var(--mac-border); | |
| } | |
| .mac-buttons { | |
| display: flex; gap: 8px; margin-right: 15px; | |
| } | |
| .mac-button { | |
| width: 12px; height: 12px; border-radius: 50%; | |
| cursor: default; | |
| } | |
| .mac-close { background-color: var(--mac-button-red); } | |
| .mac-minimize { background-color: var(--mac-button-yellow); } | |
| .mac-maximize { background-color: var(--mac-button-green); } | |
| .mac-title { | |
| flex-grow: 1; text-align: center; | |
| font-size: 0.9rem; color: var(--text-secondary); | |
| } | |
| .mac-content { padding: 20px; } | |
| .header { text-align: center; margin-bottom: 1.5rem; } | |
| .header h1 { | |
| font-size: 2.2rem; font-weight: 700; color: #2d3748; letter-spacing: -0.5px; | |
| } | |
| .header p { | |
| color: var(--text-secondary); margin-top: 0.5rem; font-size: 1.1rem; | |
| } | |
| .search-bar { | |
| display: flex; align-items: center; background: #fff; border-radius: 30px; | |
| padding: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); | |
| max-width: 600px; margin: 0.5rem auto 1.5rem auto; | |
| } | |
| .search-bar input { | |
| flex-grow: 1; border: none; padding: 12px 20px; font-size: 1rem; outline: none; | |
| background: transparent; border-radius: 30px; | |
| } | |
| .search-bar .refresh-btn { | |
| background-color: var(--pastel-green); color: #1a202c; border: none; border-radius: 30px; | |
| padding: 10px 20px; font-size: 1rem; font-weight: 600; cursor: pointer; | |
| transition: all 0.2s; display: flex; align-items: center; gap: 8px; | |
| } | |
| .search-bar .refresh-btn:hover { | |
| background-color: #9ee7c0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| } | |
| .refresh-icon { | |
| width: 16px; height: 16px; border: 2px solid #1a202c; | |
| border-top-color: transparent; border-radius: 50%; animation: none; | |
| } | |
| .refreshing .refresh-icon { | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .grid-container { | |
| display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); | |
| gap: 1.5rem; margin-bottom: 2rem; | |
| } | |
| .grid-item { | |
| height: 500px; position: relative; overflow: hidden; | |
| transition: all 0.3s ease; border-radius: 15px; | |
| } | |
| .grid-item:nth-child(6n+1) { background: var(--pastel-pink); } | |
| .grid-item:nth-child(6n+2) { background: var(--pastel-blue); } | |
| .grid-item:nth-child(6n+3) { background: var(--pastel-purple); } | |
| .grid-item:nth-child(6n+4) { background: var(--pastel-yellow); } | |
| .grid-item:nth-child(6n+5) { background: var(--pastel-green); } | |
| .grid-item:nth-child(6n+6) { background: var(--pastel-orange); } | |
| .grid-item:hover { | |
| transform: translateY(-5px); box-shadow: 0 15px 30px rgba(0,0,0,0.15); | |
| } | |
| .grid-header { | |
| padding: 15px; display: flex; flex-direction: column; | |
| background: rgba(255,255,255,0.7); backdrop-filter: blur(5px); | |
| border-bottom: 1px solid rgba(0,0,0,0.05); | |
| } | |
| .grid-header-top { | |
| display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; | |
| } | |
| .rank-badge { | |
| background: #1a202c; color: #fff; font-size: 0.8rem; font-weight: 600; | |
| padding: 4px 8px; border-radius: 50px; | |
| } | |
| .grid-header h3 { | |
| margin: 0; font-size: 1.2rem; font-weight: 700; | |
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | |
| } | |
| .grid-meta { | |
| display: flex; flex-wrap: wrap; align-items: center; font-size: 0.9rem; | |
| gap: 12px; | |
| } | |
| .owner-info { | |
| color: var(--text-secondary); font-weight: 500; | |
| } | |
| .likes-counter { | |
| color: #e53e3e; font-weight: 600; | |
| display: flex; align-items: center; | |
| } | |
| .likes-counter span { margin-left: 4px; } | |
| .downloads-counter { | |
| color: #2f855a; font-weight: 600; | |
| } | |
| .grid-actions { | |
| padding: 10px 15px; text-align: right; background: rgba(255,255,255,0.7); | |
| backdrop-filter: blur(5px); position: absolute; bottom: 0; left: 0; right: 0; | |
| z-index: 10; display: flex; justify-content: flex-end; | |
| } | |
| .open-link { | |
| text-decoration: none; color: #2c5282; font-weight: 600; | |
| padding: 5px 10px; border-radius: 5px; transition: all 0.2s; | |
| background: rgba(237,242,247,0.8); | |
| } | |
| .open-link:hover { background: #e2e8f0; } | |
| .grid-content { | |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; | |
| padding-top: 85px; padding-bottom: 45px; | |
| } | |
| .iframe-container { | |
| width: 100%; height: 100%; position: relative; overflow: hidden; | |
| } | |
| .grid-content iframe { | |
| transform: scale(0.7); transform-origin: top left; | |
| width: 142.857%; height: 142.857%; border: none; border-radius: 0; | |
| } | |
| /* ์๋ฒ ๋ ์คํจ ์ ํ๊ทธ ์ถ๋ ฅ */ | |
| .tags-fallback { | |
| position: absolute; top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(255,255,255,0.8); backdrop-filter: blur(5px); | |
| display: flex; flex-direction: column; justify-content: center; align-items: center; | |
| text-align: center; padding: 1rem; | |
| } | |
| .tags-list { | |
| display: flex; flex-wrap: wrap; gap: 10px; max-width: 300px; justify-content: center; | |
| } | |
| .tag-chip { | |
| padding: 6px 12px; border-radius: 15px; font-size: 0.85rem; font-weight: 600; | |
| color: #333; background-color: var(--pastel-yellow); | |
| } | |
| .pagination { | |
| display: flex; justify-content: center; align-items: center; | |
| gap: 10px; margin: 2rem 0; | |
| } | |
| .pagination-button { | |
| background: #fff; border: none; padding: 10px 20px; border-radius: 10px; | |
| font-size: 1rem; font-weight: 600; cursor: pointer; transition: all 0.2s; | |
| color: var(--text-primary); box-shadow: 0 2px 5px rgba(0,0,0,0.05); | |
| } | |
| .pagination-button:hover { | |
| background: #f8f9fa; box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| } | |
| .pagination-button.active { | |
| background: var(--pastel-purple); color: #4a5568; | |
| } | |
| .pagination-button:disabled { | |
| background: #edf2f7; color: #a0aec0; cursor: default; box-shadow: none; | |
| } | |
| .loading { | |
| position: fixed; top: 0; left: 0; right: 0; bottom: 0; | |
| background: rgba(255,255,255,0.8); backdrop-filter: blur(5px); | |
| display: flex; justify-content: center; align-items: center; z-index: 1000; | |
| } | |
| .loading-content { text-align: center; } | |
| .loading-spinner { | |
| width: 60px; height: 60px; border: 5px solid #e2e8f0; | |
| border-top-color: var(--pastel-purple); border-radius: 50%; | |
| animation: spin 1s linear infinite; margin: 0 auto 15px; | |
| } | |
| .loading-text { | |
| font-size: 1.2rem; font-weight: 600; color: #4a5568; | |
| } | |
| .loading-error { | |
| display: none; margin-top: 10px; color: #e53e3e; font-size: 0.9rem; | |
| } | |
| /* Stats */ | |
| .stats-window { margin: 2rem 0; } | |
| .stats-header { | |
| display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; | |
| } | |
| .stats-title { | |
| font-size: 1.5rem; font-weight: 700; color: #2d3748; | |
| } | |
| .stats-toggle { | |
| background: var(--pastel-blue); border: none; padding: 8px 16px; border-radius: 20px; | |
| font-weight: 600; cursor: pointer; transition: all 0.2s; | |
| } | |
| .stats-toggle:hover { | |
| background: var(--pastel-purple); | |
| } | |
| .stats-content { | |
| background: #fff; border-radius: 10px; padding: 20px; box-shadow: var(--box-shadow); | |
| max-height: 0; overflow: hidden; transition: max-height 0.5s ease-out; | |
| } | |
| .stats-content.open { max-height: 600px; } | |
| .chart-container { width: 100%; height: 500px; } | |
| @media(max-width: 768px) { | |
| body { padding: 1rem; } | |
| .grid-container { grid-template-columns: 1fr; } | |
| .search-bar { flex-direction: column; padding: 10px; } | |
| .search-bar input { width: 100%; margin-bottom: 10px; } | |
| .search-bar .refresh-btn { width: 100%; justify-content: center; } | |
| .pagination { flex-wrap: wrap; } | |
| .chart-container { height: 300px; } | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="mac-window"> | |
| <div class="mac-toolbar"> | |
| <div class="mac-buttons"> | |
| <div class="mac-button mac-close"></div> | |
| <div class="mac-button mac-minimize"></div> | |
| <div class="mac-button mac-maximize"></div> | |
| </div> | |
| <div class="mac-title">Huggingface Explorer</div> | |
| </div> | |
| <div class="mac-content"> | |
| <div class="header"> | |
| <h1>HF Model Leaderboard</h1> | |
| <p>Discover the top trending models from Huggingface</p> | |
| </div> | |
| <!-- ํต๊ณ ์น์ --> | |
| <div class="stats-window mac-window"> | |
| <div class="mac-toolbar"> | |
| <div class="mac-buttons"> | |
| <div class="mac-button mac-close"></div> | |
| <div class="mac-button mac-minimize"></div> | |
| <div class="mac-button mac-maximize"></div> | |
| </div> | |
| <div class="mac-title">Owner Statistics</div> | |
| </div> | |
| <div class="mac-content"> | |
| <div class="stats-header"> | |
| <div class="stats-title">Top 30 Creators by Number of Models</div> | |
| <button id="statsToggle" class="stats-toggle">Show Stats</button> | |
| </div> | |
| <div id="statsContent" class="stats-content"> | |
| <div class="chart-container"> | |
| <canvas id="creatorStatsChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="search-bar"> | |
| <input type="text" id="searchInput" placeholder="Search by name, owner, or tags..."/> | |
| <button id="refreshButton" class="refresh-btn"> | |
| <span class="refresh-icon"></span> Refresh | |
| </button> | |
| </div> | |
| <div id="gridContainer" class="grid-container"></div> | |
| <div id="pagination" class="pagination"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ๋ก๋ฉ ์ธ๋์ผ์ดํฐ --> | |
| <div id="loadingIndicator" class="loading"> | |
| <div class="loading-content"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-text">Loading amazing models...</div> | |
| <div id="loadingError" class="loading-error"> | |
| If this takes too long, try refreshing the page. | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const elements = { | |
| gridContainer: document.getElementById('gridContainer'), | |
| loadingIndicator: document.getElementById('loadingIndicator'), | |
| loadingError: document.getElementById('loadingError'), | |
| searchInput: document.getElementById('searchInput'), | |
| refreshButton: document.getElementById('refreshButton'), | |
| pagination: document.getElementById('pagination'), | |
| statsToggle: document.getElementById('statsToggle'), | |
| statsContent: document.getElementById('statsContent'), | |
| creatorStatsChart: document.getElementById('creatorStatsChart') | |
| }; | |
| const state = { | |
| isLoading: false, | |
| models: [], | |
| currentPage: 0, | |
| itemsPerPage: 72, | |
| totalItems: 0, | |
| loadingTimeout: null, | |
| statsVisible: false, | |
| chartInstance: null, | |
| topOwners: [], | |
| iframeStatuses: {} | |
| }; | |
| // ์์ดํ๋ ์ ๋ก๋ฉ ์ฒดํฌ | |
| const iframeLoader = { | |
| checkQueue: {}, | |
| maxAttempts: 3, | |
| checkInterval: 3000, | |
| startChecking(iframe, modelKey) { | |
| this.checkQueue[modelKey] = { | |
| iframe, | |
| attempts: 0, | |
| status: 'loading' | |
| }; | |
| this.checkIframeStatus(modelKey); | |
| }, | |
| checkIframeStatus(modelKey) { | |
| if(!this.checkQueue[modelKey]) return; | |
| const item = this.checkQueue[modelKey]; | |
| const { iframe } = item; | |
| if(item.status !== 'loading') { | |
| delete this.checkQueue[modelKey]; | |
| return; | |
| } | |
| item.attempts++; | |
| try { | |
| if(!iframe || !iframe.parentNode) { | |
| delete this.checkQueue[modelKey]; | |
| return; | |
| } | |
| // ์ค์ ๋ก๋ฉ ์ฌ๋ถ ์ฒดํฌ | |
| try { | |
| const hasBody = iframe.contentWindow | |
| && iframe.contentWindow.document | |
| && iframe.contentWindow.document.body; | |
| if(hasBody && iframe.contentWindow.document.body.innerHTML.length > 100) { | |
| // ์ผ๋ถ ํ ์คํธ | |
| const bodyText = iframe.contentWindow.document.body.textContent.toLowerCase(); | |
| if( | |
| bodyText.includes('forbidden') || | |
| bodyText.includes('404') || | |
| bodyText.includes('not found') || | |
| bodyText.includes('error') | |
| ) { | |
| item.status = 'error'; | |
| handleIframeError(iframe); | |
| } else { | |
| item.status = 'success'; | |
| } | |
| delete this.checkQueue[modelKey]; | |
| return; | |
| } | |
| } catch(e) { | |
| // ๋ณด์ ๋ฑ์ผ๋ก ์ ๊ทผ ๋ถ๊ฐ | |
| } | |
| // ์๋ ํ์ ์ด๊ณผ ์ ์ค๋ฅ ์ฒ๋ฆฌ | |
| if(item.attempts >= this.maxAttempts) { | |
| item.status = 'error'; | |
| handleIframeError(iframe); | |
| delete this.checkQueue[modelKey]; | |
| return; | |
| } | |
| setTimeout(() => this.checkIframeStatus(modelKey), this.checkInterval); | |
| } catch(err) { | |
| console.error('checkIframeStatus error:', err); | |
| if(item.attempts >= this.maxAttempts) { | |
| item.status = 'error'; | |
| handleIframeError(iframe); | |
| delete this.checkQueue[modelKey]; | |
| } else { | |
| setTimeout(() => this.checkIframeStatus(modelKey), this.checkInterval); | |
| } | |
| } | |
| } | |
| }; | |
| // ์์ดํ๋ ์ ์ค๋ฅ => ํ๊ทธ๋ก ๋์ฒด (1) "Unable to embed model." ๋ฌธ๊ตฌ ์ญ์ | |
| function handleIframeError(iframe) { | |
| const container = iframe.parentNode; | |
| if(!container) return; | |
| // ์์ดํ๋ ์ ์จ๊ธฐ๊ธฐ | |
| iframe.style.display = 'none'; | |
| // ํ๊ทธ ๊ฐ์ ธ์ค๊ธฐ | |
| const tagsRaw = container.dataset.tags || '[]'; | |
| let tags = []; | |
| try { | |
| tags = JSON.parse(tagsRaw); | |
| } catch(e) { | |
| tags = []; | |
| } | |
| // ๋์ฒด UI | |
| const fallbackDiv = document.createElement('div'); | |
| fallbackDiv.className = 'tags-fallback'; | |
| // ๋ณ๋ ๋ฌธ๊ตฌ ์์ด ํ๊ทธ๋ง ํ์ | |
| const tagsList = document.createElement('div'); | |
| tagsList.className = 'tags-list'; | |
| const pastelColors = [ | |
| 'var(--pastel-pink)', 'var(--pastel-blue)', 'var(--pastel-purple)', | |
| 'var(--pastel-yellow)', 'var(--pastel-green)', 'var(--pastel-orange)' | |
| ]; | |
| tags.forEach((tag, idx) => { | |
| const chip = document.createElement('span'); | |
| chip.className = 'tag-chip'; | |
| chip.textContent = tag; | |
| // ํ์คํ ์์ ๋ฐ๋ณต | |
| const color = pastelColors[idx % pastelColors.length]; | |
| chip.style.backgroundColor = color; | |
| tagsList.appendChild(chip); | |
| }); | |
| fallbackDiv.appendChild(tagsList); | |
| container.appendChild(fallbackDiv); | |
| } | |
| // ํต๊ณ ํ์ ํ ๊ธ | |
| function toggleStats() { | |
| state.statsVisible = !state.statsVisible; | |
| elements.statsContent.classList.toggle('open', state.statsVisible); | |
| elements.statsToggle.textContent = state.statsVisible ? 'Hide Stats' : 'Show Stats'; | |
| if(state.statsVisible && state.topOwners.length > 0) { | |
| renderCreatorStats(); | |
| } | |
| } | |
| function renderCreatorStats() { | |
| if(state.chartInstance) { | |
| state.chartInstance.destroy(); | |
| } | |
| const ctx = elements.creatorStatsChart.getContext('2d'); | |
| const labels = state.topOwners.map(o => o[0]); | |
| const data = state.topOwners.map(o => o[1]); | |
| const colors = []; | |
| for(let i=0; i<labels.length; i++){ | |
| const hue = (i * 360 / labels.length) % 360; | |
| colors.push(`hsla(${hue},70%,80%,0.7)`); | |
| } | |
| state.chartInstance = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels, | |
| datasets: [{ | |
| label: 'Number of Models', | |
| data, | |
| backgroundColor: colors, | |
| borderColor: colors.map(c => c.replace('0.7','1')), | |
| borderWidth: 1 | |
| }] | |
| }, | |
| options: { | |
| indexAxis: 'y', | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| callbacks: { | |
| title: (items) => items[0].label, | |
| label: (ctx) => 'Models: ' + ctx.raw | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| beginAtZero: true, | |
| title: { display: true, text: 'Number of Models' } | |
| }, | |
| y: { | |
| title: { display: true, text: 'Creator ID' }, | |
| ticks: { autoSkip: false } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // ๋ชจ๋ธ ๋ก๋ | |
| async function loadModels(page=0) { | |
| setLoading(true); | |
| try { | |
| const search = elements.searchInput.value; | |
| const offset = page * state.itemsPerPage; | |
| const timeoutPromise = new Promise((_, reject) => { | |
| setTimeout(() => reject(new Error('Request timeout')), 30000); | |
| }); | |
| const fetchPromise = fetch(`/api/trending-models?search=${encodeURIComponent(search)}&offset=${offset}&limit=${state.itemsPerPage}`); | |
| const response = await Promise.race([fetchPromise, timeoutPromise]); | |
| const data = await response.json(); | |
| state.models = data.models; | |
| state.totalItems = data.total; | |
| state.currentPage = page; | |
| state.topOwners = data.top_owners || []; | |
| renderGrid(data.models); | |
| renderPagination(); | |
| if(state.statsVisible && state.topOwners.length > 0){ | |
| renderCreatorStats(); | |
| } | |
| } catch(error) { | |
| console.error('Error loading models:', error); | |
| elements.gridContainer.innerHTML = ` | |
| <div style="grid-column:1/-1; text-align:center; padding:40px;"> | |
| <div style="font-size:3rem; margin-bottom:20px;">โ ๏ธ</div> | |
| <h3 style="margin-bottom:10px;">Unable to load models</h3> | |
| <p style="color:#666;">Please try refreshing. If it persists, check later.</p> | |
| <button id="retryButton" | |
| style="margin-top:20px; padding:10px 20px; background:var(--pastel-purple); | |
| border:none; border-radius:5px; cursor:pointer;"> | |
| Try Again | |
| </button> | |
| </div>`; | |
| document.getElementById('retryButton')?.addEventListener('click', () => loadModels(0)); | |
| renderPagination(); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| function renderGrid(models) { | |
| elements.gridContainer.innerHTML = ''; | |
| if(!models || models.length===0) { | |
| const msg = document.createElement('p'); | |
| msg.textContent = 'No models found matching your search.'; | |
| msg.style.padding = '2rem'; | |
| msg.style.textAlign = 'center'; | |
| msg.style.fontStyle = 'italic'; | |
| msg.style.color = '#718096'; | |
| elements.gridContainer.appendChild(msg); | |
| return; | |
| } | |
| models.forEach((item) => { | |
| try { | |
| const { | |
| url, title, likes_count, downloads_count, | |
| owner, name, rank, tags | |
| } = item; | |
| if(owner==='None') return; | |
| const gridItem = document.createElement('div'); | |
| gridItem.className = 'grid-item'; | |
| // ํค๋ | |
| const header = document.createElement('div'); | |
| header.className = 'grid-header'; | |
| const headerTop = document.createElement('div'); | |
| headerTop.className = 'grid-header-top'; | |
| const titleEl = document.createElement('h3'); | |
| titleEl.textContent = title; | |
| titleEl.title = title; | |
| headerTop.appendChild(titleEl); | |
| const rankBadge = document.createElement('div'); | |
| rankBadge.className = 'rank-badge'; | |
| rankBadge.textContent = `#${rank}`; | |
| headerTop.appendChild(rankBadge); | |
| header.appendChild(headerTop); | |
| // ๋ฉํ | |
| const metaInfo = document.createElement('div'); | |
| metaInfo.className = 'grid-meta'; | |
| const ownerEl = document.createElement('div'); | |
| ownerEl.className = 'owner-info'; | |
| ownerEl.textContent = `by ${owner}`; | |
| metaInfo.appendChild(ownerEl); | |
| const likesEl = document.createElement('div'); | |
| likesEl.className = 'likes-counter'; | |
| likesEl.innerHTML = 'โฅ <span>' + likes_count + '</span>'; | |
| metaInfo.appendChild(likesEl); | |
| // ๋ค์ด๋ก๋ ์ ์ถ๊ฐ | |
| const downloadsEl = document.createElement('div'); | |
| downloadsEl.className = 'downloads-counter'; | |
| downloadsEl.textContent = 'Downloads: ' + downloads_count; | |
| metaInfo.appendChild(downloadsEl); | |
| header.appendChild(metaInfo); | |
| gridItem.appendChild(header); | |
| // ์ฝํ ์ธ (iframe ์๋) | |
| const content = document.createElement('div'); | |
| content.className = 'grid-content'; | |
| const iframeContainer = document.createElement('div'); | |
| iframeContainer.className = 'iframe-container'; | |
| iframeContainer.dataset.tags = JSON.stringify(tags); | |
| const iframe = document.createElement('iframe'); | |
| // direct url | |
| iframe.src = createDirectUrl(owner, name); | |
| iframe.title = title; | |
| iframe.allow = 'accelerometer; camera; encrypted-media; geolocation; gyroscope;'; | |
| iframe.setAttribute('allowfullscreen', ''); | |
| iframe.setAttribute('frameborder', '0'); | |
| iframe.loading = 'lazy'; | |
| const modelKey = `${owner}/${name}`; | |
| state.iframeStatuses[modelKey] = 'loading'; | |
| iframe.onload = () => { | |
| iframeLoader.startChecking(iframe, modelKey); | |
| }; | |
| iframe.onerror = () => { | |
| state.iframeStatuses[modelKey] = 'error'; | |
| handleIframeError(iframe); | |
| }; | |
| // ์ผ์ ์๊ฐ ํ์๋ ๋ก๋ฉ์ด๋ฉด ํ๊ทธ๋ก ๋์ฒด | |
| setTimeout(() => { | |
| if(state.iframeStatuses[modelKey]==='loading'){ | |
| state.iframeStatuses[modelKey] = 'error'; | |
| handleIframeError(iframe); | |
| } | |
| }, 8000); | |
| iframeContainer.appendChild(iframe); | |
| content.appendChild(iframeContainer); | |
| // ํ๋จ ๋งํฌ | |
| const actions = document.createElement('div'); | |
| actions.className = 'grid-actions'; | |
| const linkEl = document.createElement('a'); | |
| linkEl.href = url; | |
| linkEl.target = '_blank'; | |
| linkEl.className = 'open-link'; | |
| linkEl.textContent = 'Open in new window'; | |
| actions.appendChild(linkEl); | |
| gridItem.appendChild(content); | |
| gridItem.appendChild(actions); | |
| elements.gridContainer.appendChild(gridItem); | |
| } catch(err) { | |
| console.error('Item rendering error:', err); | |
| } | |
| }); | |
| } | |
| // ํ์ด์ง๋ค์ด์ | |
| function renderPagination() { | |
| elements.pagination.innerHTML = ''; | |
| const totalPages = Math.ceil(state.totalItems / state.itemsPerPage); | |
| const prevButton = document.createElement('button'); | |
| prevButton.className = 'pagination-button'; | |
| prevButton.textContent = 'Previous'; | |
| prevButton.disabled = (state.currentPage===0); | |
| prevButton.addEventListener('click', ()=>{ | |
| if(state.currentPage>0) loadModels(state.currentPage-1); | |
| }); | |
| elements.pagination.appendChild(prevButton); | |
| const maxButtons = 7; | |
| let startPage = Math.max(0, state.currentPage - Math.floor(maxButtons/2)); | |
| let endPage = Math.min(totalPages-1, startPage + maxButtons-1); | |
| if(endPage - startPage + 1 < maxButtons) { | |
| startPage = Math.max(0, endPage - maxButtons + 1); | |
| } | |
| for(let i=startPage; i<=endPage; i++){ | |
| const pageBtn = document.createElement('button'); | |
| pageBtn.className = 'pagination-button'; | |
| if(i===state.currentPage) pageBtn.classList.add('active'); | |
| pageBtn.textContent = i+1; | |
| pageBtn.addEventListener('click', () => { | |
| if(i!==state.currentPage) loadModels(i); | |
| }); | |
| elements.pagination.appendChild(pageBtn); | |
| } | |
| const nextButton = document.createElement('button'); | |
| nextButton.className = 'pagination-button'; | |
| nextButton.textContent = 'Next'; | |
| nextButton.disabled = (state.currentPage >= totalPages-1); | |
| nextButton.addEventListener('click', ()=>{ | |
| if(state.currentPage < totalPages-1) loadModels(state.currentPage+1); | |
| }); | |
| elements.pagination.appendChild(nextButton); | |
| } | |
| // ๋ก๋ฉ ํ์ | |
| function setLoading(isLoading) { | |
| state.isLoading = isLoading; | |
| elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none'; | |
| if(isLoading) { | |
| elements.refreshButton.classList.add('refreshing'); | |
| clearTimeout(state.loadingTimeout); | |
| state.loadingTimeout = setTimeout(()=>{ | |
| elements.loadingError.style.display = 'block'; | |
| },10000); | |
| } else { | |
| elements.refreshButton.classList.remove('refreshing'); | |
| clearTimeout(state.loadingTimeout); | |
| elements.loadingError.style.display = 'none'; | |
| } | |
| } | |
| // HF ๋ชจ๋ธ URL ์์ฑ | |
| function createDirectUrl(owner, name){ | |
| try { | |
| name = name.replace(/\./g,'-').replace(/_/g,'-').toLowerCase(); | |
| owner = owner.toLowerCase(); | |
| return `https://huggingface.co/${owner}/${name}`; | |
| } catch(e){ | |
| console.error(e); | |
| return 'https://huggingface.co'; | |
| } | |
| } | |
| // ๊ฒ์๋ฐ์ค ์ด๋ฒคํธ | |
| elements.searchInput.addEventListener('input', ()=>{ | |
| clearTimeout(state.searchTimeout); | |
| state.searchTimeout = setTimeout(()=>loadModels(0), 300); | |
| }); | |
| elements.searchInput.addEventListener('keyup', (e)=>{ | |
| if(e.key==='Enter') loadModels(0); | |
| }); | |
| elements.refreshButton.addEventListener('click', ()=>loadModels(0)); | |
| elements.statsToggle.addEventListener('click', toggleStats); | |
| // Mac ๋ฒํผ (์ฐ์ถ์ฉ) | |
| document.querySelectorAll('.mac-button').forEach(btn=>{ | |
| btn.addEventListener('click', (e)=> e.preventDefault()); | |
| }); | |
| // ํ์ด์ง ๋ก๋์ | |
| window.addEventListener('load', () => { | |
| setTimeout(()=>loadModels(0), 500); | |
| }); | |
| // 20์ด๊ฐ ์ง๋๋ ๋ก๋ฉ ์ค์ด๋ฉด... | |
| setTimeout(() => { | |
| if(state.isLoading){ | |
| setLoading(false); | |
| elements.gridContainer.innerHTML = ` | |
| <div style="grid-column:1/-1; text-align:center; padding:40px;"> | |
| <div style="font-size:3rem; margin-bottom:20px;">โฑ๏ธ</div> | |
| <h3 style="margin-bottom:10px;">Loading is taking longer than expected</h3> | |
| <p style="color:#666;">Please try refreshing the page.</p> | |
| <button onClick="window.location.reload()" | |
| style="margin-top:20px; padding:10px 20px; background:var(--pastel-purple); | |
| border:none; border-radius:5px; cursor:pointer;"> | |
| Reload Page | |
| </button> | |
| </div>`; | |
| } | |
| },20000); | |
| // ์ฆ์ ๋ก๋ ํธ์ถ | |
| loadModels(0); | |
| </script> | |
| </body> | |
| </html> | |
| """) | |
| app.run(host='0.0.0.0', port=7860) | |