|
|
| |
| 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(); |
| }); |