File size: 20,203 Bytes
e51b73e
 
 
d8c3c98
 
aa7bfea
d8c3c98
 
b85d771
 
29951da
 
b85d771
37ca248
b85d771
 
37ca248
 
b85d771
 
37ca248
 
71afbc9
37ca248
e82bb3d
 
 
 
71afbc9
37ca248
 
 
 
b85d771
 
2f2f999
37ca248
 
 
71afbc9
 
e82bb3d
 
8e6714f
 
b85d771
 
37ca248
 
 
71afbc9
 
e82bb3d
 
8e6714f
 
b85d771
 
37ca248
 
b85d771
71afbc9
 
e82bb3d
 
8e6714f
 
b85d771
 
37ca248
 
 
71afbc9
 
e82bb3d
 
8e6714f
 
b85d771
 
e51b73e
928928a
37ca248
 
 
 
 
e51b73e
2f2f999
 
9e08bd2
854b889
68372fd
854b889
29951da
 
928928a
29951da
35d3095
d8c3c98
68372fd
 
 
35d3095
6c8c671
68372fd
37ca248
 
 
 
 
 
 
 
68372fd
 
 
 
35d3095
 
68372fd
37ca248
 
 
 
 
 
 
 
68372fd
 
2f2f999
ad0fb22
35d3095
37c1f3d
ad0fb22
 
 
 
aa7bfea
ad0fb22
 
 
 
 
37ca248
 
 
a8b8f4b
37ca248
 
 
35d3095
ad0fb22
 
 
 
d8c3c98
30e1f68
c02e691
35d3095
3c234dc
c02e691
37ca248
 
 
 
 
 
c02e691
034e7e4
c02e691
 
 
68372fd
35d3095
 
aa7bfea
68372fd
 
 
37ca248
 
 
a8b8f4b
37ca248
 
 
68372fd
 
 
d8c3c98
 
854b889
29951da
 
928928a
29951da
68372fd
35d3095
 
37ca248
 
a8b8f4b
37ca248
a8b8f4b
37ca248
 
68372fd
 
35d3095
3c234dc
68372fd
 
35d3095
854b889
aa7bfea
68372fd
 
d8c3c98
 
68372fd
d8c3c98
 
928928a
37ca248
 
928928a
37ca248
2f2f999
 
37ca248
 
 
 
 
 
 
 
 
 
 
29951da
 
 
 
 
 
37ca248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d8c3c98
c02e691
30e1f68
d8c3c98
aa7bfea
3c234dc
30e1f68
 
aa7bfea
30e1f68
 
 
 
 
 
aa7bfea
ad0fb22
 
 
35d3095
aa7bfea
 
 
 
 
 
35d3095
aa7bfea
35d3095
aa7bfea
 
 
 
35d3095
aa7bfea
 
 
ad0fb22
 
35d3095
ad0fb22
 
 
30e1f68
 
d8c3c98
 
 
aa7bfea
30e1f68
aa7bfea
 
d8c3c98
 
30e1f68
d8c3c98
3c234dc
30e1f68
d8c3c98
aa7bfea
c02e691
e3aa6ee
30e1f68
c02e691
 
e3aa6ee
c02e691
aa7bfea
 
e3aa6ee
c02e691
30e1f68
aa7bfea
 
30e1f68
68372fd
d8c3c98
c02e691
30e1f68
c02e691
0118e4c
30e1f68
7393362
30e1f68
aa7bfea
3c234dc
aa7bfea
35d3095
d8c3c98
c02e691
aa7bfea
30e1f68
 
 
 
 
 
 
 
aa7bfea
68372fd
 
30e1f68
68372fd
44671b5
68372fd
 
30e1f68
37c1f3d
68372fd
30e1f68
68372fd
aa7bfea
 
68372fd
 
c02e691
68372fd
 
aa7bfea
 
68372fd
 
30e1f68
aa7bfea
3c234dc
68372fd
 
30e1f68
aa7bfea
30e1f68
aa7bfea
68372fd
928928a
b85d771
 
aa7bfea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35d3095
aa7bfea
35d3095
aa7bfea
 
 
35d3095
aa7bfea
35d3095
aa7bfea
 
 
68372fd
35d3095
aa7bfea
68372fd
35d3095
 
b85d771
68372fd
 
30e1f68
c02e691
 
30e1f68
aa7bfea
 
 
 
 
 
 
 
 
 
35d3095
aa7bfea
 
 
30e1f68
c02e691
ab0e80d
c7e14cc
c02e691
37c1f3d
c7e14cc
 
30e1f68
aa7bfea
30e1f68
d8c3c98
 
3c234dc
9e08bd2
30e1f68
d8c3c98
35d3095
d8c3c98
 
35d3095
d8c3c98
35d3095
d8c3c98
35d3095
d8c3c98
aa7bfea
d8c3c98
0ea32df
d8c3c98
e51b73e
f17d6e0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
<!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>