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> |