AnantaXLIllustrious's picture
Update index.html
a8b8f4b verified
Raw
History Blame Contribute Delete
20.2 kB
<!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>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Hilangkan outline & ring standar browser pada elemen interaktif & panel */
input:focus, button:focus, label:focus, [tabindex]:focus, div:focus {
outline: none !important;
box-shadow: none !important;
}
/* Target Animasi Flying Focus Ring (SAP Corner Style) */
#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;
}
/* 4 Sudut Siku Merah SAP */
#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">
<!-- ELEMEN TERBANG (FLYING FOCUS OVERLAY) -->
<div id="sap-flying-focus">
<div class="sap-focus-corners-extra w-full h-full"></div>
</div>
<!-- Container Utama Responsif -->
<div class="w-full relative z-10">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 items-start">
<!-- Panel Pengaturan & Upload (Kiri - Lebar 5/12 Kolom) -->
<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">
<!-- Input Token -->
<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>
<!-- Input Repositori -->
<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>
<!-- Input File -->
<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>
<!-- Textbox Custom Nama File -->
<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>
<!-- Preview Sebelum Upload -->
<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>
<!-- Tombol Submit -->
<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>
<!-- Panel Galeri Viewer (Kanan - Lebar 7/12 Kolom) -->
<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>
<!-- Grid Dataset All Files -->
<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">
<!-- Tempat semua jenis file akan dirender -->
</div>
</div>
</div>
</div>
</div>
<!-- JAVASCRIPT: FLYING FOCUS LOGIC -->
<script>
document.addEventListener('DOMContentLoaded', () => {
// LOGIC FLYING FOCUS (SAP CORNERS)
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>
<!-- APP MAIN MODULE 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>