Spaces:
Running
Running
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Hugging Face URL μΉ΄λ 리μ€νΈ</title> | |
| <style> | |
| /* μΉ΄λλ€μ λ΄μ 컨ν μ΄λ */ | |
| .container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| padding: 1rem; | |
| } | |
| /* κ° μΉ΄λ μ€νμΌ */ | |
| .card { | |
| border: 1px solid #ccc; | |
| border-radius: 5px; | |
| padding: 1rem; | |
| width: 300px; | |
| box-shadow: 2px 2px 8px rgba(0,0,0,0.1); | |
| position: relative; | |
| background-color: #f9f9f9; | |
| } | |
| /* μΉ΄λ λ΄λΆ λ§ν¬ μ€νμΌ */ | |
| .card a { | |
| text-decoration: none; | |
| color: #333; | |
| word-break: break-all; | |
| } | |
| /* μ’μμ λ²νΌ κΈ°λ³Έ μ€νμΌ */ | |
| .like-button { | |
| position: absolute; | |
| top: 1rem; | |
| right: 1rem; | |
| border: none; | |
| background: transparent; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| transition: color 0.2s; | |
| } | |
| /* μ’μμ ν μν: λΉ¨κ°μ */ | |
| .like-button.liked { | |
| color: red; | |
| } | |
| /* μ’μμ μν μν: ν°μ (ν λ리 ν¨κ³Ό) */ | |
| .like-button.not-liked { | |
| color: white; | |
| -webkit-text-stroke: 1px #333; | |
| } | |
| /* ν ν° μ λ ₯ λ° μν νμ μμ */ | |
| .auth-controls { | |
| padding: 1rem; | |
| margin-bottom: 1rem; | |
| background-color: #f0f0f0; | |
| border-radius: 5px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .auth-controls input { | |
| padding: 0.5rem; | |
| width: 300px; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| } | |
| .auth-controls button { | |
| padding: 0.5rem 1rem; | |
| margin-left: 0.5rem; | |
| background-color: #4CAF50; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| .auth-controls button:hover { | |
| background-color: #45a049; | |
| } | |
| /* λ‘λ© νμ */ | |
| .loading { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(255, 255, 255, 0.7); | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| font-size: 1.5rem; | |
| } | |
| /* μν λ©μμ§ */ | |
| .status-message { | |
| margin: 1rem; | |
| padding: 1rem; | |
| border-radius: 4px; | |
| } | |
| .success { | |
| background-color: #dff0d8; | |
| color: #3c763d; | |
| } | |
| .error { | |
| background-color: #f2dede; | |
| color: #a94442; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- ν ν° μ λ ₯ λ° μΈμ¦ 컨νΈλ‘€ --> | |
| <div class="auth-controls" id="authControls"> | |
| <div> | |
| <span>Hugging Face μΈμ¦: </span> | |
| <span id="authStatus">μΈμ¦λμ§ μμ</span> | |
| </div> | |
| <div> | |
| <input type="password" id="tokenInput" placeholder="Hugging Face API ν ν° μ λ ₯" /> | |
| <button id="authButton">μΈμ¦νκΈ°</button> | |
| <button id="logoutButton" style="display: none; background-color: #f44336;">λ‘κ·Έμμ</button> | |
| </div> | |
| </div> | |
| <!-- μν λ©μμ§ --> | |
| <div id="statusMessage" class="status-message" style="display: none;"></div> | |
| <!-- λ‘λ© νμ --> | |
| <div class="loading" id="loadingIndicator" style="display: none;"> | |
| λ°μ΄ν°λ₯Ό λΆλ¬μ€λ μ€... | |
| </div> | |
| <!-- μΉ΄λ 컨ν μ΄λ --> | |
| <div class="container" id="cardsContainer"></div> | |
| <script> | |
| // API μλν¬μΈνΈ | |
| const HF_API_BASE = 'https://huggingface.co'; | |
| // μν κ΄λ¦¬ | |
| const state = { | |
| token: null, | |
| username: null, | |
| likedModels: {}, | |
| isLoading: false | |
| }; | |
| // DOM μμ μ°Έμ‘° | |
| const elements = { | |
| tokenInput: document.getElementById('tokenInput'), | |
| authButton: document.getElementById('authButton'), | |
| logoutButton: document.getElementById('logoutButton'), | |
| authStatus: document.getElementById('authStatus'), | |
| cardsContainer: document.getElementById('cardsContainer'), | |
| loadingIndicator: document.getElementById('loadingIndicator'), | |
| statusMessage: document.getElementById('statusMessage') | |
| }; | |
| // Hugging Faceμ spaces/models URL λͺ©λ‘ | |
| const urls = [ | |
| "https://huggingface.co/spaces/ginipick/Tech_Hangman_Game", | |
| "https://huggingface.co/spaces/openfree/deepseek_r1_API", | |
| "https://huggingface.co/spaces/ginipick/open_Deep-Research", | |
| "https://huggingface.co/spaces/aiqmaster/open-deep-research", | |
| "https://huggingface.co/spaces/seawolf2357/DeepSeek-R1-32b-search", | |
| "https://huggingface.co/spaces/ginigen/LLaDA", | |
| "https://huggingface.co/spaces/VIDraft/PHI4-Multimodal", | |
| "https://huggingface.co/spaces/ginigen/Ovis2-8B", | |
| "https://huggingface.co/spaces/ginigen/Graph-Mind", | |
| // λλ¨Έμ§ URL λͺ©λ‘... μ€μ λ‘λ λͺ¨λ URLμ ν¬ν¨ν΄μΌ ν¨ | |
| ]; | |
| // URLμμ λͺ¨λΈ/μ€νμ΄μ€ ID μΆμΆ | |
| function extractModelInfo(url) { | |
| try { | |
| const parts = url.split('/'); | |
| let type, owner, repo; | |
| // URL νμμ λ°λΌ λ€λ₯Έ μ²λ¦¬ | |
| if (parts[3] === 'spaces' || parts[3] === 'models') { | |
| type = parts[3]; | |
| owner = parts[4]; | |
| repo = parts[5]; | |
| } else { | |
| // νμμ΄ λ€λ₯Έ κ²½μ° (μ: https://huggingface.co/deepseek/deepseek-ai) | |
| type = 'models'; // κΈ°λ³Έκ°μΌλ‘ models μ¬μ© | |
| owner = parts[3]; | |
| repo = parts[4]; | |
| } | |
| // μλ κ°μ΄ μμΌλ©΄ κΈ°λ³Έκ° μ€μ | |
| type = type || 'models'; | |
| owner = owner || ''; | |
| repo = repo || ''; | |
| return { type, owner, repo, fullId: `${owner}/${repo}` }; | |
| } catch (e) { | |
| console.error('URL νμ± μ€λ₯:', e, url); | |
| return { type: 'models', owner: '', repo: '', fullId: '' }; | |
| } | |
| } | |
| // URLμ λ§μ§λ§ λΆλΆμ μ λͺ©μΌλ‘ μΆμΆ (μΈλλ°, νμ΄νμ 곡백μΌλ‘ λ³ν) | |
| function extractTitle(url) { | |
| const parts = url.split("/"); | |
| let title = parts[parts.length - 1]; | |
| return title.replace(/[_-]/g, " "); | |
| } | |
| // λ‘λ© μν νμ ν¨μ | |
| function setLoading(isLoading) { | |
| state.isLoading = isLoading; | |
| elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none'; | |
| } | |
| // μν λ©μμ§ νμ ν¨μ | |
| function showMessage(message, isError = false) { | |
| elements.statusMessage.textContent = message; | |
| elements.statusMessage.className = `status-message ${isError ? 'error' : 'success'}`; | |
| elements.statusMessage.style.display = 'block'; | |
| // 3μ΄ ν λ©μμ§ μ¬λΌμ§ | |
| setTimeout(() => { | |
| elements.statusMessage.style.display = 'none'; | |
| }, 3000); | |
| } | |
| // Hugging Face API νΈμΆ ν¨μ | |
| async function fetchWithToken(endpoint, options = {}) { | |
| if (!state.token) { | |
| throw new Error('μΈμ¦ ν ν°μ΄ μμ΅λλ€.'); | |
| } | |
| const url = `${HF_API_BASE}${endpoint}`; | |
| console.log('API νΈμΆ:', url, options.method || 'GET'); | |
| const headers = { | |
| 'Authorization': `Bearer ${state.token}`, | |
| 'Content-Type': 'application/json', | |
| ...options.headers | |
| }; | |
| try { | |
| const response = await fetch(url, { | |
| ...options, | |
| headers | |
| }); | |
| // λλ²κΉ μ μν μλ΅ μ 보 μΆλ ₯ | |
| console.log('API μλ΅ μν:', response.status, response.statusText); | |
| return response; | |
| } catch (error) { | |
| console.error('API νΈμΆ μ€λ₯:', error); | |
| throw error; | |
| } | |
| } | |
| // μ¬μ©μ μ 보 κ°μ Έμ€κΈ° | |
| async function fetchUserInfo() { | |
| const response = await fetchWithToken('/api/whoami-v2'); | |
| if (!response.ok) { | |
| throw new Error('μ¬μ©μ μ 보λ₯Ό κ°μ Έμ€λλ° μ€ν¨νμ΅λλ€. μν μ½λ: ' + response.status); | |
| } | |
| return response.json(); | |
| } | |
| // μ¬μ©μκ° μ’μμν λͺ¨λΈ/μ€νμ΄μ€ λͺ©λ‘ κ°μ Έμ€κΈ° | |
| async function fetchLikedRepos() { | |
| try { | |
| // μ’μμν λͺ¨λΈ κ°μ Έμ€κΈ° | |
| const modelsResponse = await fetchWithToken('/api/me/likes'); | |
| if (!modelsResponse.ok) { | |
| console.error('μ’μμ λͺ©λ‘ κ°μ Έμ€κΈ° μλ΅:', modelsResponse.status, modelsResponse.statusText); | |
| throw new Error('μ’μμ λͺ©λ‘μ κ°μ Έμ€λλ° μ€ν¨νμ΅λλ€. μν μ½λ: ' + modelsResponse.status); | |
| } | |
| const likedModels = await modelsResponse.json(); | |
| console.log('κ°μ Έμ¨ μ’μμ λͺ©λ‘:', likedModels); | |
| // κ²°κ³Όλ₯Ό κ°μ²΄λ‘ λ³ν (λΉ λ₯Έ κ²μμ μν΄) | |
| const likedMap = {}; | |
| likedModels.forEach(model => { | |
| likedMap[`${model.owner}/${model.name}`] = true; | |
| }); | |
| return likedMap; | |
| } catch (error) { | |
| console.error('μ’μμ λͺ©λ‘ κ°μ Έμ€κΈ° μ€λ₯:', error); | |
| throw error; | |
| } | |
| } | |
| // μ’μμ ν κΈ API νΈμΆ | |
| async function toggleLikeAPI(type, owner, repo, isLiked) { | |
| try { | |
| const method = isLiked ? 'DELETE' : 'POST'; | |
| const response = await fetchWithToken(`/api/${type}/${owner}/${repo}/like`, { | |
| method | |
| }); | |
| if (!response.ok) { | |
| console.error('μ’μμ ν κΈ μλ΅:', response.status, response.statusText); | |
| throw new Error(`μ’μμ ${isLiked ? 'μ·¨μ' : 'μΆκ°'} μ€ν¨. μν μ½λ: ${response.status}`); | |
| } | |
| return response.ok; | |
| } catch (error) { | |
| console.error('μ’μμ ν κΈ API μ€λ₯:', error); | |
| throw error; | |
| } | |
| } | |
| // μΈμ¦ μ²λ¦¬ | |
| async function authenticate(token) { | |
| if (!token.trim()) { | |
| showMessage('ν ν°μ μ λ ₯ν΄μ£ΌμΈμ.', true); | |
| return; | |
| } | |
| setLoading(true); | |
| try { | |
| // ν ν° μ μ₯ | |
| state.token = token; | |
| console.log('μΈμ¦ μλ (ν ν° μΌλΆ):', token.substring(0, 4) + '...'); | |
| // μ¬μ©μ μ 보 κ°μ Έμ€κΈ° | |
| const userInfo = await fetchUserInfo(); | |
| console.log('μ¬μ©μ μ 보:', userInfo); | |
| // μ¬μ©μ μ΄λ¦ μΆμΆ (API μλ΅ κ΅¬μ‘°μ λ°λΌ λ€λ₯Ό μ μμ) | |
| if (userInfo.name) { | |
| state.username = userInfo.name; | |
| } else if (userInfo.user && userInfo.user.username) { | |
| state.username = userInfo.user.username; | |
| } else if (userInfo.username) { | |
| state.username = userInfo.username; | |
| } else { | |
| state.username = 'μΈμ¦λ μ¬μ©μ'; | |
| } | |
| // μ€μ μ’μμ λͺ©λ‘ κ°μ Έμ€κΈ° | |
| state.likedModels = await fetchLikedRepos(); | |
| // UI μ λ°μ΄νΈ | |
| elements.authStatus.textContent = `${state.username}λμΌλ‘ μΈμ¦λ¨`; | |
| elements.tokenInput.style.display = 'none'; | |
| elements.authButton.style.display = 'none'; | |
| elements.logoutButton.style.display = 'inline-block'; | |
| showMessage('μΈμ¦ μ±κ³΅! μ’μμ μ 보λ₯Ό λΆλ¬μμ΅λλ€.'); | |
| // μΉ΄λ λ€μ λ λλ§ | |
| renderCards(); | |
| } catch (error) { | |
| console.error('μΈμ¦ μ€λ₯:', error); | |
| showMessage(`μΈμ¦μ μ€ν¨νμ΅λλ€: ${error.message}`, true); | |
| state.token = null; | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // λ‘κ·Έμμ μ²λ¦¬ | |
| function logout() { | |
| // μν μ΄κΈ°ν | |
| state.token = null; | |
| state.username = null; | |
| state.likedModels = {}; | |
| // UI μ λ°μ΄νΈ | |
| elements.authStatus.textContent = 'μΈμ¦λμ§ μμ'; | |
| elements.tokenInput.style.display = 'inline-block'; | |
| elements.tokenInput.value = ''; | |
| elements.authButton.style.display = 'inline-block'; | |
| elements.logoutButton.style.display = 'none'; | |
| showMessage('λ‘κ·Έμμλμμ΅λλ€.'); | |
| // μΉ΄λ λ€μ λ λλ§ | |
| renderCards(); | |
| } | |
| // μ’μμ ν κΈ μ²λ¦¬ | |
| async function toggleLike(url, button) { | |
| if (!state.token) { | |
| showMessage('μ’μμλ₯Ό νλ €λ©΄ HF ν ν°μΌλ‘ μΈμ¦μ΄ νμν©λλ€.', true); | |
| return; | |
| } | |
| const modelInfo = extractModelInfo(url); | |
| const modelId = modelInfo.fullId; | |
| const isCurrentlyLiked = state.likedModels[modelId] || false; | |
| setLoading(true); | |
| try { | |
| // μ€μ API νΈμΆλ‘ μ’μμ μν ν κΈ | |
| await toggleLikeAPI(modelInfo.type, modelInfo.owner, modelInfo.repo, isCurrentlyLiked); | |
| // μν μ λ°μ΄νΈ | |
| if (isCurrentlyLiked) { | |
| delete state.likedModels[modelId]; | |
| button.classList.remove("liked"); | |
| button.classList.add("not-liked"); | |
| showMessage(`${modelInfo.repo}μ λν μ’μμλ₯Ό μ·¨μνμ΅λλ€.`); | |
| } else { | |
| state.likedModels[modelId] = true; | |
| button.classList.add("liked"); | |
| button.classList.remove("not-liked"); | |
| showMessage(`${modelInfo.repo}λ₯Ό μ’μμ νμ΅λλ€.`); | |
| } | |
| } catch (error) { | |
| showMessage('μ’μμ μ²λ¦¬ μ€ μ€λ₯κ° λ°μνμ΅λλ€.', true); | |
| console.error('μ’μμ μ€λ₯:', error); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // μΉ΄λ λ λλ§ | |
| function renderCards() { | |
| // μΉ΄λ 컨ν μ΄λ μ΄κΈ°ν | |
| elements.cardsContainer.innerHTML = ''; | |
| // κ° URLμ λν΄ μΉ΄λ μμ± | |
| urls.forEach(url => { | |
| const card = document.createElement("div"); | |
| card.className = "card"; | |
| // μ λͺ© | |
| const titleEl = document.createElement("h3"); | |
| titleEl.textContent = extractTitle(url); | |
| card.appendChild(titleEl); | |
| // URL λ§ν¬ | |
| const linkEl = document.createElement("a"); | |
| linkEl.href = url; | |
| linkEl.textContent = url; | |
| linkEl.target = "_blank"; | |
| card.appendChild(linkEl); | |
| // μ’μμ λ²νΌ (β₯ μμ΄μ½) | |
| const likeBtn = document.createElement("button"); | |
| likeBtn.className = "like-button"; | |
| likeBtn.textContent = "β₯"; | |
| // μ’μμ μν μ€μ | |
| try { | |
| const modelInfo = extractModelInfo(url); | |
| const isLiked = state.token && state.likedModels[modelInfo.fullId]; | |
| if (isLiked) { | |
| likeBtn.classList.add("liked"); | |
| } else { | |
| likeBtn.classList.add("not-liked"); | |
| } | |
| } catch (e) { | |
| // URL νμ± μ€λ₯ λ± μμΈ μ²λ¦¬ | |
| likeBtn.classList.add("not-liked"); | |
| } | |
| likeBtn.addEventListener("click", function(e) { | |
| e.preventDefault(); // λ§ν¬ ν΄λ¦ λ°©μ§ | |
| toggleLike(url, likeBtn); | |
| }); | |
| card.appendChild(likeBtn); | |
| elements.cardsContainer.appendChild(card); | |
| }); | |
| } | |
| // μ΄λ²€νΈ 리μ€λ μ€μ | |
| elements.authButton.addEventListener('click', () => { | |
| authenticate(elements.tokenInput.value); | |
| }); | |
| elements.logoutButton.addEventListener('click', logout); | |
| // μν° ν€λ‘ μΈμ¦ κ°λ₯νκ² | |
| elements.tokenInput.addEventListener('keypress', (event) => { | |
| if (event.key === 'Enter') { | |
| authenticate(elements.tokenInput.value); | |
| } | |
| }); | |
| // μ΄κΈ° λ λλ§ | |
| renderCards(); | |
| </script> | |
| </body> | |
| </html> |