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