|
|
|
|
|
|
|
|
class AssetManager { |
|
|
constructor() { |
|
|
this.assets = []; |
|
|
this.currentId = 0; |
|
|
this.token = null; |
|
|
this.spaceName = null; |
|
|
this.username = null; |
|
|
} |
|
|
|
|
|
async authenticate(token) { |
|
|
this.token = token; |
|
|
try { |
|
|
const response = await fetch('https://huggingface.co/api/whoami-v2', { |
|
|
headers: { |
|
|
'Authorization': `Bearer ${token}` |
|
|
} |
|
|
}); |
|
|
const userData = await response.json(); |
|
|
this.username = userData.name; |
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error('Authentication failed:', error); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
async loadAssets(spaceName) { |
|
|
this.spaceName = spaceName; |
|
|
try { |
|
|
const response = await fetch(`https://huggingface.co/api/spaces/${this.username}/${spaceName}/files`, { |
|
|
headers: { |
|
|
'Authorization': `Bearer ${this.token}` |
|
|
} |
|
|
}); |
|
|
const files = await response.json(); |
|
|
|
|
|
this.assets = files.map(file => ({ |
|
|
id: ++this.currentId, |
|
|
name: file.path.split('/').pop(), |
|
|
type: this.getFileType(file.path), |
|
|
url: `https://huggingface.co/spaces/${this.username}/${spaceName}/resolve/main/${file.path}`, |
|
|
preview: this.getPreviewUrl(file.path), |
|
|
path: file.path, |
|
|
lastModified: file.lastModified |
|
|
})); |
|
|
|
|
|
return this.assets; |
|
|
} catch (error) { |
|
|
console.error('Failed to load assets:', error); |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
getFileType(filename) { |
|
|
const extension = filename.split('.').pop().toLowerCase(); |
|
|
const types = { |
|
|
'jpg': 'image/jpeg', |
|
|
'jpeg': 'image/jpeg', |
|
|
'png': 'image/png', |
|
|
'gif': 'image/gif', |
|
|
'pdf': 'application/pdf', |
|
|
'txt': 'text/plain', |
|
|
'csv': 'text/csv', |
|
|
'json': 'application/json' |
|
|
}; |
|
|
return types[extension] || 'application/octet-stream'; |
|
|
} |
|
|
|
|
|
getPreviewUrl(filename) { |
|
|
const extension = filename.split('.').pop().toLowerCase(); |
|
|
if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) { |
|
|
return `https://huggingface.co/spaces/${this.username}/${this.spaceName}/preview/${filename}`; |
|
|
} |
|
|
return `http://static.photos/office/320x240/${Math.floor(Math.random() * 100)}`; |
|
|
} |
|
|
async addAsset(file) { |
|
|
const formData = new FormData(); |
|
|
formData.append('file', file); |
|
|
|
|
|
try { |
|
|
const response = await fetch(`https://huggingface.co/api/spaces/${this.username}/${this.spaceName}/upload`, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Authorization': `Bearer ${this.token}` |
|
|
}, |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
const newAsset = { |
|
|
id: ++this.currentId, |
|
|
name: file.name, |
|
|
type: file.type || this.getFileType(file.name), |
|
|
url: `https://huggingface.co/spaces/${this.username}/${this.spaceName}/resolve/main/${file.name}`, |
|
|
preview: this.getPreviewUrl(file.name), |
|
|
path: file.name, |
|
|
lastModified: new Date().toISOString() |
|
|
}; |
|
|
this.assets.push(newAsset); |
|
|
return newAsset; |
|
|
} |
|
|
return null; |
|
|
} catch (error) { |
|
|
console.error('Failed to upload file:', error); |
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
async updateAsset(id, updates) { |
|
|
const asset = this.assets.find(a => a.id === id); |
|
|
if (!asset) return null; |
|
|
|
|
|
|
|
|
if (updates.file) { |
|
|
await this.deleteAsset(id); |
|
|
return await this.addAsset(updates.file); |
|
|
} else { |
|
|
|
|
|
const index = this.assets.findIndex(a => a.id === id); |
|
|
this.assets[index] = { ...asset, ...updates }; |
|
|
return this.assets[index]; |
|
|
} |
|
|
} |
|
|
|
|
|
async deleteAsset(id) { |
|
|
const asset = this.assets.find(a => a.id === id); |
|
|
if (!asset) return false; |
|
|
|
|
|
try { |
|
|
const response = await fetch(`https://huggingface.co/api/spaces/${this.username}/${this.spaceName}/delete/${asset.path}`, { |
|
|
method: 'DELETE', |
|
|
headers: { |
|
|
'Authorization': `Bearer ${this.token}` |
|
|
} |
|
|
}); |
|
|
|
|
|
if (response.ok) { |
|
|
this.assets = this.assets.filter(a => a.id !== id); |
|
|
return true; |
|
|
} |
|
|
return false; |
|
|
} catch (error) { |
|
|
console.error('Failed to delete file:', error); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
getAssets() { |
|
|
return [...this.assets]; |
|
|
} |
|
|
|
|
|
exportAsJSON() { |
|
|
return JSON.stringify(this.assets, null, 2); |
|
|
} |
|
|
} |
|
|
|
|
|
class HuggingSpaceApp { |
|
|
constructor() { |
|
|
this.assetManager = new AssetManager(); |
|
|
this.initElements(); |
|
|
this.initEventListeners(); |
|
|
this.checkAuth(); |
|
|
} |
|
|
initElements() { |
|
|
this.elements = { |
|
|
assetsTable: document.getElementById('assetsTable'), |
|
|
uploadBtn: document.getElementById('uploadBtn'), |
|
|
uploadModal: document.getElementById('uploadModal'), |
|
|
closeModal: document.getElementById('closeModal'), |
|
|
fileInput: document.getElementById('fileInput'), |
|
|
confirmUpload: document.getElementById('confirmUpload'), |
|
|
exportBtn: document.getElementById('exportBtn'), |
|
|
jsonData: document.getElementById('jsonData'), |
|
|
authModal: document.getElementById('authModal'), |
|
|
tokenInput: document.getElementById('tokenInput'), |
|
|
spaceInput: document.getElementById('spaceInput'), |
|
|
authSubmit: document.getElementById('authSubmit'), |
|
|
authError: document.getElementById('authError'), |
|
|
userInfo: document.getElementById('userInfo') |
|
|
}; |
|
|
} |
|
|
initEventListeners() { |
|
|
this.elements.uploadBtn.addEventListener('click', () => this.toggleModal(true)); |
|
|
this.elements.closeModal.addEventListener('click', () => this.toggleModal(false)); |
|
|
this.elements.confirmUpload.addEventListener('click', () => this.handleFileUpload()); |
|
|
this.elements.exportBtn.addEventListener('click', () => this.exportData()); |
|
|
this.elements.authSubmit.addEventListener('click', () => this.handleAuth()); |
|
|
} |
|
|
async checkAuth() { |
|
|
const token = localStorage.getItem('hfToken'); |
|
|
const space = localStorage.getItem('hfSpace'); |
|
|
|
|
|
if (token && space) { |
|
|
const authenticated = await this.assetManager.authenticate(token); |
|
|
if (authenticated) { |
|
|
await this.assetManager.loadAssets(space); |
|
|
this.render(); |
|
|
this.elements.authModal.classList.add('hidden'); |
|
|
this.updateUserInfo(); |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
this.elements.authModal.classList.remove('hidden'); |
|
|
} |
|
|
|
|
|
async handleAuth() { |
|
|
const token = this.elements.tokenInput.value.trim(); |
|
|
const space = this.elements.spaceInput.value.trim(); |
|
|
|
|
|
if (!token || !space) { |
|
|
this.elements.authError.textContent = 'Please enter both token and space name'; |
|
|
return; |
|
|
} |
|
|
|
|
|
const authenticated = await this.assetManager.authenticate(token); |
|
|
if (!authenticated) { |
|
|
this.elements.authError.textContent = 'Invalid token. Please check and try again.'; |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
await this.assetManager.loadAssets(space); |
|
|
localStorage.setItem('hfToken', token); |
|
|
localStorage.setItem('hfSpace', space); |
|
|
this.elements.authModal.classList.add('hidden'); |
|
|
this.render(); |
|
|
this.updateUserInfo(); |
|
|
} catch (error) { |
|
|
this.elements.authError.textContent = 'Failed to load space. Please check space name and try again.'; |
|
|
} |
|
|
} |
|
|
|
|
|
updateUserInfo() { |
|
|
if (this.assetManager.username && this.assetManager.spaceName) { |
|
|
this.elements.userInfo.innerHTML = ` |
|
|
<div class="flex items-center gap-2"> |
|
|
<span class="font-medium">${this.assetManager.username}</span> |
|
|
<span class="text-gray-500">/</span> |
|
|
<span class="font-medium">${this.assetManager.spaceName}</span> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
} |
|
|
|
|
|
async render() { |
|
|
await this.renderAssetsTable(); |
|
|
this.updateJsonPreview(); |
|
|
} |
|
|
async renderAssetsTable() { |
|
|
const { assetsTable } = this.elements; |
|
|
assetsTable.innerHTML = ''; |
|
|
|
|
|
const assets = this.assetManager.getAssets(); |
|
|
if (assets.length === 0) { |
|
|
assetsTable.innerHTML = ` |
|
|
<tr> |
|
|
<td colspan="5" class="px-6 py-4 text-center text-gray-500"> |
|
|
No assets found. Upload files to get started. |
|
|
</td> |
|
|
</tr> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
assets.forEach(asset => { |
|
|
const row = document.createElement('tr'); |
|
|
row.className = 'hover:bg-gray-50'; |
|
|
row.innerHTML = this.createAssetRowHTML(asset); |
|
|
assetsTable.appendChild(row); |
|
|
|
|
|
this.addRowEventListeners(row, asset.id); |
|
|
}); |
|
|
|
|
|
feather.replace(); |
|
|
} |
|
|
|
|
|
createAssetRowHTML(asset) { |
|
|
return ` |
|
|
<td class="px-6 py-4 whitespace-nowrap"> |
|
|
<img src="${asset.preview}" alt="${asset.name}" class="file-preview"> |
|
|
</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap editable-cell" data-id="${asset.id}" data-field="name">${asset.name}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap editable-cell" data-id="${asset.id}" data-field="type">${asset.type}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap url-cell editable-cell" data-id="${asset.id}" data-field="url">${asset.url}</td> |
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium table-actions"> |
|
|
<button class="text-primary-500 hover:text-primary-600" data-action="copy" data-url="${asset.url}"> |
|
|
<i data-feather="copy"></i> |
|
|
</button> |
|
|
<button class="text-red-500 hover:text-red-600" data-action="delete" data-id="${asset.id}"> |
|
|
<i data-feather="trash-2"></i> |
|
|
</button> |
|
|
</td> |
|
|
`; |
|
|
} |
|
|
|
|
|
addRowEventListeners(row, assetId) { |
|
|
|
|
|
row.querySelectorAll('.editable-cell').forEach(cell => { |
|
|
cell.addEventListener('dblclick', () => this.makeCellEditable(cell)); |
|
|
}); |
|
|
|
|
|
|
|
|
row.querySelector('[data-action="copy"]')?.addEventListener('click', (e) => this.copyUrl(e)); |
|
|
row.querySelector('[data-action="delete"]')?.addEventListener('click', (e) => this.deleteAsset(e)); |
|
|
} |
|
|
|
|
|
makeCellEditable(cell) { |
|
|
const originalValue = cell.textContent; |
|
|
const id = parseInt(cell.dataset.id); |
|
|
const field = cell.dataset.field; |
|
|
|
|
|
cell.innerHTML = `<input type="text" value="${originalValue}" class="w-full p-1 border border-gray-300 rounded">`; |
|
|
const input = cell.querySelector('input'); |
|
|
input.focus(); |
|
|
|
|
|
const handleBlur = () => { |
|
|
const newValue = input.value; |
|
|
cell.textContent = newValue; |
|
|
this.assetManager.updateAsset(id, { [field]: newValue }); |
|
|
this.updateJsonPreview(); |
|
|
|
|
|
|
|
|
cell.addEventListener('dblclick', () => this.makeCellEditable(cell)); |
|
|
}; |
|
|
|
|
|
input.addEventListener('blur', handleBlur); |
|
|
input.addEventListener('keypress', (e) => { |
|
|
if (e.key === 'Enter') { |
|
|
handleBlur(); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
copyUrl(e) { |
|
|
const url = e.target.closest('button').dataset.url; |
|
|
navigator.clipboard.writeText(url).then(() => { |
|
|
const originalHTML = e.target.closest('button').innerHTML; |
|
|
e.target.closest('button').innerHTML = '<i data-feather="check"></i>'; |
|
|
feather.replace(); |
|
|
|
|
|
setTimeout(() => { |
|
|
e.target.closest('button').innerHTML = originalHTML; |
|
|
feather.replace(); |
|
|
}, 2000); |
|
|
}); |
|
|
} |
|
|
|
|
|
deleteAsset(e) { |
|
|
const id = parseInt(e.target.closest('button').dataset.id); |
|
|
if (confirm('Are you sure you want to delete this asset?')) { |
|
|
this.assetManager.deleteAsset(id); |
|
|
this.render(); |
|
|
} |
|
|
} |
|
|
|
|
|
toggleModal(show) { |
|
|
this.elements.uploadModal.classList.toggle('hidden', !show); |
|
|
if (!show) { |
|
|
this.elements.fileInput.value = ''; |
|
|
} |
|
|
} |
|
|
async handleFileUpload() { |
|
|
const files = this.elements.fileInput.files; |
|
|
|
|
|
if (files.length === 0) { |
|
|
alert('Please select files to upload'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
for (const file of files) { |
|
|
await this.assetManager.addAsset(file); |
|
|
} |
|
|
await this.render(); |
|
|
this.toggleModal(false); |
|
|
} catch (error) { |
|
|
alert('Failed to upload files. Please try again.'); |
|
|
console.error(error); |
|
|
} |
|
|
} |
|
|
|
|
|
exportData() { |
|
|
const dataStr = this.assetManager.exportAsJSON(); |
|
|
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); |
|
|
|
|
|
const linkElement = document.createElement('a'); |
|
|
linkElement.setAttribute('href', dataUri); |
|
|
linkElement.setAttribute('download', 'hugging-space-assets.json'); |
|
|
linkElement.click(); |
|
|
} |
|
|
|
|
|
updateJsonPreview() { |
|
|
this.elements.jsonData.value = this.assetManager.exportAsJSON(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
new HuggingSpaceApp(); |
|
|
}); |