| <!DOCTYPE html> |
| <html lang="id"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Media Uploader & Viewer</title> |
| |
| <script src="https://cdn.tailwindcss.com"></script> |
| |
| <style> |
| |
| input:focus, button:focus, label:focus, [tabindex]:focus, div:focus { |
| outline: none !important; |
| box-shadow: none !important; |
| } |
| |
| |
| #sap-flying-focus { |
| position: absolute; |
| pointer-events: none; |
| z-index: 9999; |
| opacity: 0; |
| box-sizing: content-box; |
| transition: |
| top 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), |
| left 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), |
| width 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), |
| height 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), |
| opacity 0.15s ease; |
| } |
| |
| #sap-flying-focus.active { |
| opacity: 1; |
| } |
| |
| |
| #sap-flying-focus::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| width: 6px; |
| height: 6px; |
| border-top: 2px solid #bb0000; |
| border-left: 2px solid #bb0000; |
| } |
| |
| #sap-flying-focus::after { |
| content: ''; |
| position: absolute; |
| bottom: 0; |
| right: 0; |
| width: 6px; |
| height: 6px; |
| border-bottom: 2px solid #bb0000; |
| border-right: 2px solid #bb0000; |
| } |
| |
| .sap-focus-corners-extra::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| right: 0; |
| width: 6px; |
| height: 6px; |
| border-top: 2px solid #bb0000; |
| border-right: 2px solid #bb0000; |
| } |
| |
| .sap-focus-corners-extra::after { |
| content: ''; |
| position: absolute; |
| bottom: 0; |
| left: 0; |
| width: 6px; |
| height: 6px; |
| border-bottom: 2px solid #bb0000; |
| border-left: 2px solid #bb0000; |
| } |
| </style> |
| </head> |
| <body class="bg-slate-100 text-slate-800 min-h-screen p-4 sm:p-6 relative"> |
|
|
| |
| <div id="sap-flying-focus"> |
| <div class="sap-focus-corners-extra w-full h-full"></div> |
| </div> |
|
|
| |
| <div class="w-full relative z-10"> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start"> |
| |
| |
| <div |
| tabindex="0" |
| class="sap-focusable sap-panel lg:col-span-5 bg-white rounded-xl shadow-sm border border-slate-200 p-6 space-y-5 h-fit transition duration-200" |
| > |
| <h2 class="text-lg font-semibold text-slate-900 border-b border-slate-100 pb-3">Configuration & Upload</h2> |
|
|
| <form id="uploadForm" class="space-y-4"> |
| |
| <div> |
| <label for="hfToken" class="block text-xs font-semibold text-slate-600 mb-1"> |
| Your Access Token |
| </label> |
| <input |
| type="password" |
| id="hfToken" |
| required |
| oninvalid="this.setCustomValidity('Please make sure you type required entries !')" |
| oninput="this.setCustomValidity('')" |
| class="sap-focusable w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-slate-800 placeholder-slate-400 focus:outline-none text-sm transition" |
| /> |
| </div> |
|
|
| |
| <div> |
| <label for="repoId" class="block text-xs font-semibold text-slate-600 mb-1"> |
| Your Repo ID (Example: <code class="text-blue-600 bg-blue-50 px-1 rounded">username/repo</code>) |
| </label> |
| <input |
| type="text" |
| id="repoId" |
| required |
| oninvalid="this.setCustomValidity('Please make sure you type required entries !')" |
| oninput="this.setCustomValidity('')" |
| class="sap-focusable w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-slate-800 placeholder-slate-400 focus:outline-none text-sm transition" |
| /> |
| </div> |
|
|
| |
| <div> |
| <label class="block text-xs font-semibold text-slate-600 mb-1"> |
| Choose Files |
| </label> |
| |
| <input |
| type="file" |
| id="fileInput" |
| required |
| class="hidden" |
| /> |
| |
| <div class="flex items-center gap-3"> |
| <label |
| for="fileInput" |
| tabindex="0" |
| class="sap-focusable inline-block px-3 py-1.5 bg-gradient-to-b from-white to-slate-200 hover:from-slate-50 hover:to-slate-300 text-slate-800 text-xs font-semibold rounded-lg cursor-pointer transition shadow-sm border border-slate-300 focus:outline-none" |
| > |
| Choose Files |
| </label> |
| <span id="fileNameDisplay" class="text-xs text-slate-500 truncate"> |
| Nothing files chosen |
| </span> |
| </div> |
| </div> |
|
|
| |
| <div> |
| <label for="customFileName" class="block text-xs font-semibold text-slate-600 mb-1"> |
| New Custom Name |
| </label> |
| <input |
| type="text" |
| id="customFileName" |
| placeholder="Empty for defaults" |
| class="sap-focusable w-full px-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-slate-800 placeholder-slate-400 focus:outline-none text-sm transition" |
| /> |
| <p class="text-[11px] text-slate-400 mt-1"> |
| Type as you want |
| </p> |
| </div> |
|
|
| |
| <div id="previewContainer" class="hidden text-center bg-slate-50 p-3 rounded-lg border border-slate-200"> |
| <p class="text-xs text-slate-500 mb-2">Preview:</p> |
| <div id="previewContent"></div> |
| </div> |
|
|
| |
| <button |
| type="submit" |
| id="submitBtn" |
| class="sap-focusable w-full py-2.5 px-4 bg-gradient-to-b from-white to-slate-200 hover:from-slate-50 hover:to-slate-300 text-slate-800 font-semibold rounded-lg transition duration-200 focus:outline-none text-sm shadow-sm border border-slate-300 active:to-slate-400 disabled:opacity-50 disabled:cursor-not-allowed" |
| > |
| UPLOAD |
| </button> |
| </form> |
|
|
| <div id="statusMessage" class="hidden p-3 rounded-lg text-xs break-words"></div> |
| </div> |
|
|
| |
| <div |
| tabindex="0" |
| class="sap-focusable sap-panel lg:col-span-7 bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex flex-col justify-between transition duration-200" |
| > |
| <div> |
| <div class="flex items-center justify-between border-b border-slate-100 pb-3 mb-4"> |
| <h2 class="text-lg font-semibold text-slate-900">Datasets Viewer</h2> |
| <button |
| id="fetchGalleryBtn" |
| class="sap-focusable px-3 py-1.5 bg-gradient-to-b from-white to-slate-200 hover:from-slate-50 hover:to-slate-300 text-slate-800 text-xs font-semibold rounded-lg border border-slate-300 transition flex items-center gap-1.5 shadow-sm focus:outline-none" |
| > |
| <svg class="w-3.5 h-3.5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg> |
| Load |
| </button> |
| </div> |
|
|
| <div id="galleryStatus" class="text-xs text-slate-500 mb-4 text-center py-2 bg-slate-50 border border-slate-100 rounded-lg"> |
| Enter Repo ID to perform next step. |
| </div> |
|
|
| |
| <div id="galleryGrid" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-3 xl:grid-cols-4 gap-3 max-h-[580px] overflow-y-auto pr-1"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| </div> |
| </div> |
|
|
| |
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| const flyingFocus = document.getElementById('sap-flying-focus'); |
| const OFFSET = 3; |
| const PANEL_OFFSET = 5; |
| |
| function moveFocusTo(target) { |
| if (!target || !target.classList.contains('sap-focusable')) { |
| flyingFocus.classList.remove('active'); |
| return; |
| } |
| |
| const rect = target.getBoundingClientRect(); |
| const scrollTop = window.pageYOffset || document.documentElement.scrollTop; |
| const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; |
| |
| const currentOffset = target.classList.contains('sap-panel') ? PANEL_OFFSET : OFFSET; |
| |
| flyingFocus.style.top = `${rect.top + scrollTop - currentOffset}px`; |
| flyingFocus.style.left = `${rect.left + scrollLeft - currentOffset}px`; |
| flyingFocus.style.width = `${rect.width + (currentOffset * 2)}px`; |
| flyingFocus.style.height = `${rect.height + (currentOffset * 2)}px`; |
| |
| flyingFocus.classList.add('active'); |
| } |
| |
| document.addEventListener('focusin', (e) => { |
| moveFocusTo(e.target); |
| }); |
| |
| document.addEventListener('focusout', () => { |
| setTimeout(() => { |
| if (!document.activeElement || !document.activeElement.classList.contains('sap-focusable')) { |
| flyingFocus.classList.remove('active'); |
| } |
| }, 10); |
| }); |
| |
| window.addEventListener('resize', () => { |
| if (document.activeElement) moveFocusTo(document.activeElement); |
| }); |
| |
| document.addEventListener('scroll', () => { |
| if (document.activeElement) moveFocusTo(document.activeElement); |
| }, true); |
| }); |
| </script> |
|
|
| |
| <script type="module"> |
| import { uploadFile, listFiles } from 'https://cdn.jsdelivr.net/npm/@huggingface/hub@latest/+esm'; |
| |
| const uploadForm = document.getElementById('uploadForm'); |
| const fileInput = document.getElementById('fileInput'); |
| const fileNameDisplay = document.getElementById('fileNameDisplay'); |
| const customFileNameInput = document.getElementById('customFileName'); |
| const previewContainer = document.getElementById('previewContainer'); |
| const previewContent = document.getElementById('previewContent'); |
| const submitBtn = document.getElementById('submitBtn'); |
| const statusMessage = document.getElementById('statusMessage'); |
| const fetchGalleryBtn = document.getElementById('fetchGalleryBtn'); |
| const galleryGrid = document.getElementById('galleryGrid'); |
| const galleryStatus = document.getElementById('galleryStatus'); |
| |
| fileInput.addEventListener('change', (e) => { |
| const file = e.target.files[0]; |
| if (file) { |
| fileNameDisplay.innerText = file.name; |
| fileNameDisplay.classList.replace('text-slate-500', 'text-slate-800'); |
| previewContainer.classList.remove('hidden'); |
| |
| const ext = file.name.split('.').pop().toLowerCase(); |
| const fileUrl = URL.createObjectURL(file); |
| |
| if (['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg', 'bmp'].includes(ext)) { |
| previewContent.innerHTML = `<img src="${fileUrl}" class="max-h-36 mx-auto rounded-lg border border-slate-200 object-contain" />`; |
| } else if (['mp4', 'webm', 'ogg'].includes(ext)) { |
| previewContent.innerHTML = `<video src="${fileUrl}" controls class="max-h-36 mx-auto rounded-lg border border-slate-200"></video>`; |
| } else if (['mp3', 'wav', 'flac'].includes(ext)) { |
| previewContent.innerHTML = `<audio src="${fileUrl}" controls class="w-full my-2"></audio>`; |
| } else { |
| previewContent.innerHTML = ` |
| <div class="p-3 bg-white rounded border border-slate-200 text-slate-700 text-xs"> |
| ๐ File: <b>${file.name}</b> (${(file.size / 1024).toFixed(1)} KB) |
| </div>`; |
| } |
| } else { |
| fileNameDisplay.innerText = 'Nothing files chosen'; |
| fileNameDisplay.classList.replace('text-slate-800', 'text-slate-500'); |
| previewContainer.classList.add('hidden'); |
| } |
| }); |
| |
| uploadForm.addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const token = document.getElementById('hfToken').value.trim(); |
| const repoId = document.getElementById('repoId').value.trim(); |
| const file = fileInput.files[0]; |
| |
| if (!token || !repoId || !file) { |
| showStatus('Please make sure you type all required entries.', 'error'); |
| return; |
| } |
| |
| setLoading(true); |
| showStatus('Uploading...', 'info'); |
| |
| try { |
| const fileExtension = file.name.includes('.') ? file.name.substring(file.name.lastIndexOf('.')) : ''; |
| const customName = customFileNameInput.value.trim(); |
| let finalFileName = ''; |
| |
| if (customName !== '') { |
| const sanitizedCustomName = customName.replace(/[^a-zA-Z0-9_\-]/g, '_'); |
| finalFileName = `${sanitizedCustomName}${fileExtension}`; |
| } else { |
| const rawName = file.name.includes('.') ? file.name.substring(0, file.name.lastIndexOf('.')) : file.name; |
| const sanitizedOriginalName = rawName.replace(/\s+/g, '_'); |
| finalFileName = `${Date.now()}_${sanitizedOriginalName}${fileExtension}`; |
| } |
| |
| const renamedFile = new File([file], finalFileName, { type: file.type }); |
| const filePathInRepo = `files/${finalFileName}`; |
| |
| await uploadFile({ |
| repo: { type: 'dataset', name: repoId }, |
| credentials: { accessToken: token }, |
| file: renamedFile, |
| path: filePathInRepo, |
| }); |
| |
| showStatus(`Has been uploaded as: <b>${finalFileName}</b>`, 'success'); |
| |
| fileInput.value = ''; |
| customFileNameInput.value = ''; |
| fileNameDisplay.innerText = 'Nothing files chosen'; |
| fileNameDisplay.classList.replace('text-slate-800', 'text-slate-500'); |
| previewContainer.classList.add('hidden'); |
| |
| loadDatasetFiles(); |
| } catch (err) { |
| console.error(err); |
| showStatus(`Failed uploading: Check access token & Repo ID.`, 'error'); |
| } finally { |
| setLoading(false); |
| } |
| }); |
| |
| async function loadDatasetFiles() { |
| const token = document.getElementById('hfToken').value.trim(); |
| const repoId = document.getElementById('repoId').value.trim(); |
| |
| if (!repoId) { |
| galleryStatus.innerText = 'Please type Repo ID as Dataset address name.'; |
| return; |
| } |
| |
| galleryStatus.innerText = 'Loading Files...'; |
| galleryGrid.innerHTML = ''; |
| |
| try { |
| const allFiles = []; |
| |
| for await (const entry of listFiles({ |
| repo: { type: 'dataset', name: repoId }, |
| credentials: token ? { accessToken: token } : undefined, |
| recursive: true |
| })) { |
| if (entry.type === 'file') { |
| allFiles.push(entry); |
| } |
| } |
| |
| if (allFiles.length === 0) { |
| galleryStatus.innerText = 'Nothing else.'; |
| return; |
| } |
| |
| galleryStatus.innerText = `Found ${allFiles.length} files:`; |
| |
| for (const fileEntry of allFiles.reverse()) { |
| const card = document.createElement('div'); |
| card.className = 'sap-focusable bg-white rounded-lg overflow-hidden border border-slate-200 flex flex-col justify-between group shadow-sm transition duration-200 cursor-pointer'; |
| card.setAttribute('tabindex', '0'); |
| |
| const fullFileUrl = `https://huggingface.co/datasets/${repoId}/resolve/main/${fileEntry.path.split('/').map(encodeURIComponent).join('/')}`; |
| const ext = fileEntry.path.split('.').pop().toLowerCase(); |
| const fileName = fileEntry.path.split('/').pop(); |
| |
| let previewMediaHtml = ''; |
| |
| if (['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg', 'bmp'].includes(ext)) { |
| previewMediaHtml = ` |
| <span class="text-[10px] text-slate-400 animate-pulse img-loader">Loading...</span> |
| <img src="${fullFileUrl}" class="object-cover h-full w-full rounded group-hover:scale-105 transition duration-200 hidden" /> |
| `; |
| } else if (['mp4', 'webm', 'ogg'].includes(ext)) { |
| previewMediaHtml = `<video src="${fullFileUrl}" controls class="h-full w-full object-cover"></video>`; |
| } else if (['mp3', 'wav', 'flac'].includes(ext)) { |
| previewMediaHtml = ` |
| <div class="flex flex-col items-center justify-center p-2 text-center"> |
| <span class="text-xl">๐ต</span> |
| <audio src="${fullFileUrl}" controls class="w-full max-w-[120px] mt-1 h-6"></audio> |
| </div>`; |
| } else if (['txt', 'json', 'csv', 'parquet', 'md'].includes(ext)) { |
| previewMediaHtml = ` |
| <div class="flex flex-col items-center justify-center text-slate-600"> |
| <span class="text-2xl">๐</span> |
| <span class="text-[10px] text-slate-400 mt-1 uppercase font-semibold">${ext} Data</span> |
| </div>`; |
| } else { |
| previewMediaHtml = ` |
| <div class="flex flex-col items-center justify-center text-slate-600"> |
| <span class="text-2xl">๐ฆ</span> |
| <span class="text-[10px] text-slate-400 mt-1 uppercase font-semibold">${ext} File</span> |
| </div>`; |
| } |
| |
| card.innerHTML = ` |
| <div class="h-28 bg-slate-50 overflow-hidden flex items-center justify-center p-1 relative border-b border-slate-100"> |
| ${previewMediaHtml} |
| </div> |
| <div class="p-2 text-[10px] text-slate-600 truncate flex justify-between items-center bg-white"> |
| <span class="truncate max-w-[100px] font-medium" title="${fileEntry.path}">${fileName}</span> |
| <a href="${fullFileUrl}" target="_blank" class="text-blue-600 hover:text-blue-700 underline font-semibold focus:outline-none">OPEN</a> |
| </div> |
| `; |
| |
| const imgElement = card.querySelector('img'); |
| const loaderElement = card.querySelector('.img-loader'); |
| |
| if (imgElement) { |
| imgElement.onload = () => { |
| imgElement.classList.remove('hidden'); |
| loaderElement?.classList.add('hidden'); |
| }; |
| |
| imgElement.onerror = () => { |
| if (loaderElement) { |
| loaderElement.innerText = 'Failed load file'; |
| loaderElement.classList.remove('animate-pulse'); |
| loaderElement.classList.add('text-red-500'); |
| } |
| }; |
| } |
| |
| galleryGrid.appendChild(card); |
| } |
| } catch (err) { |
| console.error(err); |
| galleryStatus.innerText = `Failed load managers !`; |
| } |
| } |
| |
| fetchGalleryBtn.addEventListener('click', loadDatasetFiles); |
| |
| function setLoading(isLoading) { |
| submitBtn.disabled = isLoading; |
| submitBtn.innerText = isLoading ? 'Uploading...' : 'UPLOAD'; |
| } |
| |
| function showStatus(msg, type) { |
| statusMessage.classList.remove('hidden', 'bg-red-50', 'text-red-700', 'border-red-200', 'bg-emerald-50', 'text-emerald-700', 'border-emerald-200', 'bg-blue-50', 'text-blue-700', 'border-blue-200'); |
| |
| if (type === 'error') { |
| statusMessage.classList.add('bg-red-50', 'text-red-700', 'border', 'border-red-200'); |
| } else if (type === 'success') { |
| statusMessage.classList.add('bg-emerald-50', 'text-emerald-700', 'border', 'border-emerald-200'); |
| } else { |
| statusMessage.classList.add('bg-blue-50', 'text-blue-700', 'border', 'border-blue-200'); |
| } |
| |
| statusMessage.innerHTML = msg; |
| } |
| </script> |
| </body> |
| </html> |