prithivMLmods commited on
Commit
bb8cd20
Β·
verified Β·
1 Parent(s): 93efa82

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +429 -1530
index.html CHANGED
@@ -3,101 +3,85 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>HuggingFace Repo Structure Visualizer : HFTree</title>
7
- <meta name="description" content="Effortlessly explore and visualize the file structure of any HuggingFace model, dataset, or space online without cloning.">
 
 
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
 
10
  <style>
11
  /* =========================================
12
- 1. VARIABLES & THEME CONFIGURATION
13
  ========================================= */
14
  :root {
15
  --bg: #ffffff;
16
- --text: #24292f;
17
- --header-bg: #f6f8fa;
18
- --border: #d0d7de;
19
- --accent: #ff9d00;
20
- --accent-hover: #e68a00;
21
- --accent-light: rgba(255, 157, 0, 0.1);
22
- --accent-border: rgba(255, 157, 0, 0.3);
23
  --panel-bg: #ffffff;
24
- --panel-border: #d0d7de;
25
- --folder-color: #ff9d00;
26
- --file-color: #7d8590;
27
- --line-num: #8c959f;
28
- --tree-line: #8c959f;
29
- --btn-bg: #f6f8fa;
30
- --btn-border: rgba(27, 31, 36, 0.15);
31
- --btn-hover: #f3f4f6;
32
- --overlay-bg: rgba(255, 255, 255, 0.8);
33
- --shadow: rgba(140, 149, 159, 0.2);
34
- --model-color: #ff9d00;
35
- --dataset-color: #10b981;
36
- --space-color: #6366f1;
37
- --tag-model: rgba(255, 157, 0, 0.1);
38
- --tag-dataset: rgba(16, 185, 129, 0.1);
39
- --tag-space: rgba(99, 102, 241, 0.1);
40
  }
41
 
42
  [data-theme="dark"] {
43
- --bg: #0d1117;
44
- --text: #c9d1d9;
45
- --header-bg: #161b22;
46
- --border: #30363d;
47
- --accent: #ff9d00;
48
- --accent-hover: #ffb340;
49
- --accent-light: rgba(255, 157, 0, 0.08);
50
- --accent-border: rgba(255, 157, 0, 0.25);
51
- --panel-bg: #0d1117;
52
- --panel-border: #30363d;
53
- --folder-color: #ffb340;
54
- --file-color: #8b949e;
55
- --line-num: #484f58;
56
- --tree-line: #6e7681;
57
- --btn-bg: #21262d;
58
- --btn-border: #363b42;
59
- --btn-hover: #30363d;
60
- --overlay-bg: rgba(13, 17, 23, 0.8);
61
- --shadow: rgba(0, 0, 0, 0.4);
62
- --tag-model: rgba(255, 157, 0, 0.08);
63
- --tag-dataset: rgba(16, 185, 129, 0.08);
64
- --tag-space: rgba(99, 102, 241, 0.08);
65
  }
66
 
67
  /* =========================================
68
- 2. GLOBAL RESET & BASE
69
  ========================================= */
70
  * { box-sizing: border-box; }
71
-
72
  body {
73
  margin: 0;
74
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
75
  background: var(--bg);
76
  color: var(--text);
77
  transition: background 0.2s, color 0.2s;
 
 
 
78
  }
79
 
80
- button, input, select { font-family: inherit; }
81
- a { text-decoration: none; color: inherit; }
82
-
83
  .app-container {
84
  max-width: 1000px;
85
  margin: 0 auto;
86
- padding: 20px 20px 65px;
87
- min-height: 100vh;
88
- display: flex;
89
- flex-direction: column;
90
  }
91
 
92
  /* =========================================
93
- 3. HEADER
94
  ========================================= */
95
  header {
96
  display: flex;
97
  justify-content: space-between;
98
  align-items: center;
99
  margin-bottom: 30px;
100
- padding-top: 10px;
 
101
  }
102
 
103
  .brand {
@@ -108,178 +92,30 @@
108
  gap: 10px;
109
  }
110
 
111
- .brand a {
112
- display: flex;
113
- align-items: center;
114
- gap: 10px;
115
- }
116
-
117
- .brand-icon {
118
- width: 36px;
119
- height: 36px;
120
- background: linear-gradient(135deg, #ff9d00, #ffb340);
121
- border-radius: 8px;
122
- display: flex;
123
- align-items: center;
124
- justify-content: center;
125
- color: white;
126
- font-size: 1.2rem;
127
- }
128
-
129
- .header-actions {
130
- display: flex;
131
- gap: 15px;
132
- align-items: center;
133
- }
134
 
135
- .icon-btn {
136
  background: none;
137
  border: none;
138
- font-size: 1.25rem;
139
  color: var(--text);
140
- width: 40px;
141
- height: 40px;
142
- text-align: center;
143
  cursor: pointer;
144
  padding: 8px;
145
  border-radius: 50%;
146
- transition: background 0.2s;
147
- }
148
-
149
- .icon-btn:hover { background: var(--btn-bg); }
150
-
151
- .text-btn {
152
- background: none;
153
- border: none;
154
- font-size: 0.9rem;
155
- color: var(--text);
156
- cursor: pointer;
157
- font-weight: 500;
158
- display: flex;
159
- align-items: center;
160
- gap: 6px;
161
- }
162
-
163
- .text-btn:hover { color: var(--accent); }
164
-
165
- .sun-icon { display: none !important; }
166
- [data-theme="dark"] .sun-icon { display: inline-block !important; color: #e3b341; }
167
- [data-theme="dark"] .moon-icon { display: none !important; }
168
- [data-theme="light"] .moon-icon { display: inline-block !important; color: #586069; }
169
-
170
- /* =========================================
171
- 4. TOKEN PANEL
172
- ========================================= */
173
- .token-panel {
174
- background: var(--header-bg);
175
- border: 1px solid var(--border);
176
- padding: 15px;
177
- border-radius: 8px;
178
- margin-bottom: 25px;
179
- animation: slideDown 0.2s ease-out;
180
- }
181
-
182
- @keyframes slideDown {
183
- from { opacity: 0; transform: translateY(-10px); }
184
- to { opacity: 1; transform: translateY(0); }
185
- }
186
-
187
- .token-inner {
188
- display: flex;
189
- gap: 10px;
190
- align-items: center;
191
- }
192
-
193
- .token-inner input {
194
- background: var(--bg);
195
- border: 1px solid var(--border);
196
- padding: 8px 12px;
197
- border-radius: 6px;
198
- flex: 1;
199
- color: var(--text);
200
- font-family: 'Fira Code', monospace;
201
- font-size: 0.85rem;
202
  }
203
-
204
- .token-inner button {
205
- background: var(--accent);
206
- color: white;
207
- border: none;
208
- padding: 8px 16px;
209
- border-radius: 6px;
210
- cursor: pointer;
211
- font-weight: 600;
212
- font-size: 0.85rem;
213
- }
214
-
215
- .token-warning {
216
- font-size: 0.8rem;
217
- color: var(--line-num);
218
- margin: 8px 0 0 0;
219
- display: flex;
220
- align-items: center;
221
- gap: 6px;
222
- }
223
-
224
- .token-warning i { color: #d29922; }
225
 
226
  /* =========================================
227
- 5. SEARCH SECTION
228
  ========================================= */
229
- .search-section {
230
  display: flex;
231
- gap: 10px;
232
  flex-wrap: wrap;
 
233
  margin-bottom: 20px;
234
  }
235
 
236
- .type-selector {
237
- display: flex;
238
- gap: 0;
239
- border: 1px solid var(--border);
240
- border-radius: 6px;
241
- overflow: hidden;
242
- height: 42px;
243
- }
244
-
245
- .type-btn {
246
- background: var(--btn-bg);
247
- border: none;
248
- border-right: 1px solid var(--border);
249
- padding: 0 14px;
250
- cursor: pointer;
251
- font-size: 0.85rem;
252
- font-weight: 500;
253
- color: var(--text);
254
- display: flex;
255
- align-items: center;
256
- gap: 6px;
257
- transition: all 0.2s;
258
- white-space: nowrap;
259
- }
260
-
261
- .type-btn:last-child { border-right: none; }
262
-
263
- .type-btn:hover { background: var(--btn-hover); }
264
-
265
- .type-btn.active-model {
266
- background: var(--tag-model);
267
- color: var(--model-color);
268
- font-weight: 600;
269
- }
270
-
271
- .type-btn.active-dataset {
272
- background: var(--tag-dataset);
273
- color: var(--dataset-color);
274
- font-weight: 600;
275
- }
276
-
277
- .type-btn.active-space {
278
- background: var(--tag-space);
279
- color: var(--space-color);
280
- font-weight: 600;
281
- }
282
-
283
  .input-group {
284
  background: var(--header-bg);
285
  border: 1px solid var(--border);
@@ -288,21 +124,23 @@
288
  align-items: center;
289
  padding: 0 12px;
290
  height: 42px;
291
- flex: 1;
292
- transition: border-color 0.2s, box-shadow 0.2s;
293
  }
294
 
295
  .input-group:focus-within {
296
  border-color: var(--accent);
297
- box-shadow: 0 0 0 2px var(--accent-border);
298
  }
299
 
300
- .prefix {
301
- color: var(--line-num);
302
- font-size: 0.85rem;
303
- margin-right: 2px;
304
- white-space: nowrap;
305
- font-family: 'Fira Code', monospace;
 
 
 
306
  }
307
 
308
  input {
@@ -311,15 +149,17 @@
311
  color: var(--text);
312
  width: 100%;
313
  outline: none;
314
- font-size: 0.95rem;
315
- font-family: 'Fira Code', monospace;
316
  }
317
 
318
- .branch-input { max-width: 160px; }
 
 
319
 
320
  .primary-btn {
321
  background: var(--accent);
322
- color: white;
323
  border: none;
324
  padding: 0 25px;
325
  border-radius: 6px;
@@ -328,1490 +168,549 @@
328
  height: 42px;
329
  display: flex;
330
  align-items: center;
331
- gap: 10px;
332
- font-size: .85em;
333
- transition: background 0.2s;
334
- }
335
-
336
- .primary-btn:hover { background: var(--accent-hover); }
337
- .primary-btn:disabled { opacity: 0.7; cursor: wait; }
338
-
339
- /* =========================================
340
- 6. STATES
341
- ========================================= */
342
- #statusMsg {
343
- padding: 10px;
344
- margin-bottom: 20px;
345
- font-size: 0.9rem;
346
- text-align: center;
347
- font-weight: 500;
348
- border-radius: 6px;
349
- }
350
-
351
- .error {
352
- color: #cf222e;
353
- background: rgba(207, 34, 46, 0.1);
354
- border: 1px solid rgba(207, 34, 46, 0.2);
355
- }
356
-
357
- .loading {
358
- color: var(--accent);
359
- background: var(--accent-light);
360
- border: 1px solid var(--accent-border);
361
- }
362
-
363
- .empty-state { text-align: center; }
364
-
365
- .empty-state h3 {
366
- font-weight: 500;
367
- margin-top: 20px;
368
- }
369
-
370
- .tag-cloud {
371
- display: flex;
372
- justify-content: center;
373
- gap: 10px;
374
- flex-wrap: wrap;
375
- max-width: 800px;
376
- margin: 0 auto;
377
- }
378
-
379
- .repo-tag {
380
- background: var(--btn-bg);
381
- border: 1px solid var(--border);
382
- color: var(--text);
383
- padding: 8px 16px;
384
- border-radius: 20px;
385
- cursor: pointer;
386
- font-size: 0.85rem;
387
- font-family: 'Fira Code', monospace;
388
- transition: all 0.2s;
389
- display: flex;
390
- align-items: center;
391
  gap: 8px;
392
  }
393
-
394
- .repo-tag:hover {
395
- border-color: var(--accent);
396
- background: var(--bg);
397
- transform: translateY(-2px);
398
- }
399
-
400
- .repo-tag .tag-type {
401
- font-size: 0.7rem;
402
- padding: 2px 6px;
403
- border-radius: 10px;
404
- font-weight: 600;
405
- text-transform: uppercase;
406
- letter-spacing: 0.5px;
407
- }
408
-
409
- .tag-type.model { background: var(--tag-model); color: var(--model-color); }
410
- .tag-type.dataset { background: var(--tag-dataset); color: var(--dataset-color); }
411
- .tag-type.space { background: var(--tag-space); color: var(--space-color); }
412
-
413
- .homepage-section {
414
- margin-bottom: 40px;
415
- }
416
-
417
- .homepage-section h3 {
418
- font-size: 1.1rem;
419
- color: var(--file-color);
420
- text-transform: uppercase;
421
- letter-spacing: 1px;
422
- }
423
-
424
- /* =========================================
425
- 7. REPO INFO CARD
426
- ========================================= */
427
- .repo-info-card {
428
- background: var(--header-bg);
429
- border: 1px solid var(--border);
430
- border-radius: 8px;
431
- padding: 16px 20px;
432
- margin-bottom: 15px;
433
- display: none;
434
- }
435
-
436
- .repo-info-card.visible { display: block; }
437
-
438
- .repo-info-header {
439
- display: flex;
440
- justify-content: space-between;
441
- align-items: center;
442
- flex-wrap: wrap;
443
- gap: 10px;
444
- }
445
-
446
- .repo-info-left {
447
- display: flex;
448
- align-items: center;
449
- gap: 12px;
450
- }
451
-
452
- .repo-type-badge {
453
- font-size: 0.75rem;
454
- padding: 4px 10px;
455
- border-radius: 12px;
456
- font-weight: 600;
457
- text-transform: uppercase;
458
- letter-spacing: 0.5px;
459
- }
460
-
461
- .repo-type-badge.model { background: var(--tag-model); color: var(--model-color); }
462
- .repo-type-badge.dataset { background: var(--tag-dataset); color: var(--dataset-color); }
463
- .repo-type-badge.space { background: var(--tag-space); color: var(--space-color); }
464
-
465
- .repo-info-name {
466
- font-size: 1.1rem;
467
- font-weight: 600;
468
- font-family: 'Fira Code', monospace;
469
- }
470
-
471
- .repo-info-name a {
472
- color: var(--accent);
473
- text-decoration: none;
474
- }
475
-
476
- .repo-info-name a:hover { text-decoration: underline; }
477
-
478
- .repo-info-stats {
479
- display: flex;
480
- gap: 16px;
481
- font-size: 0.85rem;
482
- color: var(--file-color);
483
- }
484
-
485
- .repo-info-stats span {
486
- display: flex;
487
- align-items: center;
488
- gap: 5px;
489
- }
490
 
491
  /* =========================================
492
- 8. TREE WRAPPER & TOOLBAR
493
  ========================================= */
494
- .tree-wrapper {
495
  border: 1px solid var(--border);
496
  border-radius: 8px;
497
- display: flex;
498
- flex-direction: column;
499
  background: var(--panel-bg);
500
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
501
- overflow: visible;
502
- position: relative;
503
- z-index: 10;
504
  }
505
 
506
  .toolbar {
507
  background: var(--header-bg);
508
- padding: 10px 15px;
509
  border-bottom: 1px solid var(--border);
510
  display: flex;
511
  justify-content: space-between;
512
- align-items: center;
513
- border-radius: 8px 8px 0 0;
514
- }
515
-
516
- .tools-group {
517
- display: flex;
518
- gap: 10px;
519
  }
520
 
521
  .tool-btn {
522
  background: var(--btn-bg);
523
  border: 1px solid var(--btn-border);
524
  color: var(--text);
525
- padding: 6px 14px;
526
- border-radius: 6px;
527
- font-size: 0.85rem;
528
- font-weight: 500;
529
  cursor: pointer;
530
  display: flex;
531
  align-items: center;
532
- gap: 8px;
533
- transition: all 0.2s;
534
- }
535
-
536
- .tool-btn:hover, .tool-btn.active {
537
- background: var(--btn-hover);
538
- border-color: var(--line-num);
539
- }
540
-
541
- .action-btn {
542
- color: #ffffff;
543
- background-color: var(--accent);
544
- }
545
-
546
- .action-btn:hover {
547
- background-color: var(--accent-hover);
548
- border-color: var(--accent-hover);
549
- }
550
-
551
- /* =========================================
552
- 9. DROPDOWNS
553
- ========================================= */
554
- .dropdown { position: relative; }
555
-
556
- .dropdown-content {
557
- display: none;
558
- position: absolute;
559
- top: 100%;
560
- left: 0;
561
- margin-top: 6px;
562
- background: var(--panel-bg);
563
- border: 1px solid var(--border);
564
- border-radius: 6px;
565
- box-shadow: 0 10px 30px var(--shadow);
566
- z-index: 100;
567
- min-width: 200px;
568
- padding: 6px 0;
569
- }
570
-
571
- .dropdown.active .dropdown-content { display: block; }
572
-
573
- .dropdown-content a {
574
- display: block;
575
- padding: 10px 16px;
576
- color: var(--text);
577
- text-decoration: none;
578
- font-size: 0.9rem;
579
- }
580
-
581
- .dropdown-content a:hover {
582
- background: var(--accent);
583
- color: white;
584
  }
 
585
 
586
- /* =========================================
587
- 10. TERMINAL / TREE VIEW
588
- ========================================= */
589
  .terminal-window {
590
- background: var(--panel-bg);
591
  display: flex;
592
  overflow: auto;
593
- padding: 0;
594
- font-family: 'Fira Code', monospace;
595
  font-size: 14px;
596
- line-height: 1.6;
597
- border-radius: 0 0 8px 8px;
598
  }
599
 
600
- .line-col {
601
- padding: 15px;
602
  text-align: right;
603
  color: var(--line-num);
604
  border-right: 1px solid var(--border);
605
- min-width: 50px;
606
- height: fit-content;
 
607
  }
608
 
609
- .code-col {
610
  padding: 15px;
611
  flex: 1;
612
  white-space: pre;
613
  }
614
 
615
- .tree-line {
616
  display: flex;
617
  align-items: center;
618
- height: 24px;
619
- border-radius: 4px;
620
- }
621
-
622
- .tree-line:hover { background: var(--header-bg); }
623
-
624
- .t-prefix {
625
- color: var(--tree-line);
626
- white-space: pre;
627
- margin-right: 4px;
628
  }
 
629
 
630
- .t-icon {
631
- width: 24px;
632
- text-align: center;
633
- display: inline-block;
634
- margin-right: 6px;
 
635
  }
636
-
637
  .fa-folder { color: var(--folder-color); }
638
  .fa-file { color: var(--file-color); }
639
 
640
  .t-name { color: var(--text); }
641
  .t-name.folder { font-weight: 600; }
642
 
643
- .t-size {
644
- color: var(--line-num);
645
- font-size: 0.8rem;
646
- margin-left: 12px;
647
- }
648
-
649
- .sub-copy {
650
  opacity: 0;
651
- margin-left: 10px;
652
- background: none;
653
- border: none;
654
- color: var(--line-num);
655
- cursor: pointer;
656
- padding: 4px;
657
- font-size: 0.85rem;
658
- transition: all 0.2s;
659
  display: flex;
660
- align-items: center;
661
  }
 
662
 
663
- .tree-line:hover .sub-copy { opacity: 1; }
664
- .sub-copy:hover { color: var(--accent); transform: scale(1.1); }
665
-
666
- .fa-check { color: #2da44e; }
667
-
668
- #lineNumbers div { height: 24px; }
669
 
670
  /* =========================================
671
- 11. SHARE OVERLAY
672
  ========================================= */
673
- .overlay {
674
- position: fixed;
675
- top: 0; left: 0;
676
- width: 100%; height: 100%;
677
- background: var(--shadow);
678
- backdrop-filter: blur(4px);
679
  display: none;
680
- justify-content: center;
681
- align-items: center;
682
- z-index: 1000;
683
- }
684
-
685
- .overlay.visible { display: flex; }
686
-
687
- .overlay-content {
688
- background: var(--panel-bg);
689
- padding: 30px;
690
- border-radius: 12px;
691
- width: 90%;
692
- max-width: 500px;
693
- border: 1px solid var(--border);
694
- box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3);
695
- position: relative;
696
- animation: slideUp 0.2s ease-out;
697
- }
698
-
699
- @keyframes slideUp {
700
- from { transform: translateY(10px); opacity: 0; }
701
- to { transform: translateY(0); opacity: 1; }
702
  }
 
 
703
 
704
- .overlay-content h3 {
705
- margin: 0 0 20px 0;
706
- font-size: 1.2rem;
 
707
  }
 
708
 
709
- .share-box {
 
710
  display: flex;
 
711
  gap: 10px;
712
- margin-top: 15px;
713
- }
714
-
715
- .share-box input {
716
- background: var(--header-bg);
717
- border: 1px solid var(--border);
718
- padding: 12px;
719
- border-radius: 6px;
720
- color: var(--text);
721
- font-family: 'Fira Code', monospace;
722
- font-size: 0.9rem;
723
- flex: 1;
724
- }
725
-
726
- .share-box button {
727
- background: var(--accent);
728
- color: white;
729
- border: none;
730
- border-radius: 6px;
731
- padding: 0 20px;
732
- cursor: pointer;
733
- font-size: 1.1rem;
734
  }
735
-
736
- .share-box button:hover { background: var(--accent-hover); }
737
-
738
- .close-btn {
739
- position: absolute;
740
- top: 15px; right: 15px;
741
- background: none;
742
- border: none;
743
- font-size: 1.5rem;
744
- color: var(--line-num);
745
  cursor: pointer;
746
- line-height: 1;
747
- }
748
-
749
- .close-btn:hover { color: var(--text); }
750
-
751
- /* =========================================
752
- 12. SEARCH FILTER
753
- ========================================= */
754
- .tree-search {
755
- padding: 10px 15px;
756
- border-bottom: 1px solid var(--border);
757
- background: var(--header-bg);
758
- }
759
-
760
- .tree-search input {
761
- width: 100%;
762
- padding: 8px 12px;
763
- background: var(--bg);
764
  border: 1px solid var(--border);
765
- border-radius: 6px;
766
- color: var(--text);
767
- font-family: 'Fira Code', monospace;
768
- font-size: 0.85rem;
769
  }
 
770
 
771
- .tree-search input:focus {
772
- border-color: var(--accent);
773
- outline: none;
774
- box-shadow: 0 0 0 2px var(--accent-border);
775
- }
776
-
777
- /* =========================================
778
- 13. FOOTER
779
- ========================================= */
780
  .footer {
781
- position: fixed;
782
- bottom: 10px;
783
- font-size: .75rem;
784
- color: var(--text);
785
  text-align: center;
786
- opacity: .85;
787
- width: fit-content;
788
- font-family: monospace;
789
- left: 50%;
790
- transform: translateX(-50%);
791
- padding: 5px 20px 10px;
792
- background-color: var(--bg);
793
- border-radius: 20px;
794
  }
795
 
796
- .footer svg { margin: 5px 2px; }
797
- .footer b { font-weight: 500; }
798
- .footer a { color: inherit; text-decoration: none; }
799
- .footer a:hover { text-decoration: underline; }
800
-
801
- @keyframes fadeIn { to { opacity: 1; } }
802
-
803
- /* =========================================
804
- 14. RESPONSIVE
805
- ========================================= */
806
  @media (max-width: 600px) {
807
- .search-section {
808
- flex-direction: column;
809
- display: grid;
810
- }
811
-
812
- .primary-btn {
813
- align-content: center;
814
- display: inline-block;
815
- text-align: center;
816
- }
817
-
818
- .primary-btn .fa-search { margin-right: .5em; }
819
- .branch-input { max-width: 100%; }
820
-
821
- .toolbar {
822
- flex-direction: column;
823
- align-items: flex-start;
824
- gap: 15px;
825
- padding: 15px;
826
- }
827
-
828
- .tools-group {
829
- width: 100%;
830
- justify-content: space-between;
831
- flex-wrap: wrap;
832
- }
833
-
834
- .type-selector {
835
- width: 100%;
836
- justify-content: center;
837
- }
838
-
839
- .repo-info-header { flex-direction: column; align-items: flex-start; }
840
- }
841
-
842
- /* Scrollbar */
843
- ::-webkit-scrollbar { width: 10px; height: 10px; }
844
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; border: 2px solid var(--bg); }
845
- ::-webkit-scrollbar-track { background: transparent; }
846
-
847
- /* Loading spinner */
848
- .spinner {
849
- display: inline-block;
850
- width: 16px;
851
- height: 16px;
852
- border: 2px solid var(--accent-border);
853
- border-top-color: var(--accent);
854
- border-radius: 50%;
855
- animation: spin 0.6s linear infinite;
856
- margin-right: 8px;
857
- vertical-align: middle;
858
- }
859
-
860
- @keyframes spin { to { transform: rotate(360deg); } }
861
-
862
- /* Expand/Collapse indicators */
863
- .tree-line.clickable { cursor: pointer; }
864
- .tree-line.clickable:hover { background: var(--accent-light); }
865
-
866
- .lfs-badge {
867
- font-size: 0.65rem;
868
- padding: 1px 5px;
869
- border-radius: 4px;
870
- background: var(--tag-dataset);
871
- color: var(--dataset-color);
872
- margin-left: 8px;
873
- font-weight: 600;
874
  }
875
  </style>
876
  </head>
877
  <body>
878
- <div class="app-container">
879
 
 
 
880
  <header>
881
  <div class="brand">
882
- <a href="#">
883
- <div class="brand-icon">
884
- <i class="fas fa-tree"></i>
885
- </div>
886
- <span>HFTree</span>
887
- </a>
888
  </div>
889
  <div class="header-actions">
890
- <button id="privateRepoBtn" class="text-btn" title="Access Private Repos">
891
- <i class="fas fa-lock"></i> Token
892
- </button>
893
- <button id="themeToggle" class="icon-btn" aria-label="Toggle Theme">
894
- <i class="fas fa-sun sun-icon"></i>
895
- <i class="fas fa-moon moon-icon"></i>
896
  </button>
 
 
 
897
  </div>
898
  </header>
899
 
900
- <div id="tokenSection" class="token-panel" style="display: none;">
901
- <div class="token-inner">
902
- <i class="fas fa-key"></i>
903
- <input type="password" id="hfToken" placeholder="Paste HuggingFace Access Token (hf_...)">
904
- <button id="saveTokenBtn">Save</button>
905
- <button id="clearTokenBtn" style="display:none;">Clear</button>
906
- </div>
907
- <p class="token-warning">
908
- <i class="fas fa-exclamation-triangle"></i> Token is saved to your browser's <b>LocalStorage</b>. Do not use on public computers.
909
- </p>
910
- </div>
911
-
912
- <div class="search-section">
913
- <div class="type-selector">
914
- <button class="type-btn active-model" data-type="models" id="typeModel">
915
- <i class="fas fa-cube"></i> Model
916
- </button>
917
- <button class="type-btn" data-type="datasets" id="typeDataset">
918
- <i class="fas fa-database"></i> Dataset
919
- </button>
920
- <button class="type-btn" data-type="spaces" id="typeSpace">
921
- <i class="fas fa-rocket"></i> Space
922
- </button>
923
  </div>
924
  <div class="input-group main-input">
925
- <span class="prefix" id="urlPrefix">huggingface.co/</span>
926
- <input type="text" id="repoInput" placeholder="username/repo-name" autocomplete="off">
927
  </div>
928
  <div class="input-group branch-input">
929
- <span class="prefix">rev:</span>
930
- <input type="text" id="branchInput" placeholder="main" autocomplete="off">
931
  </div>
932
  <button id="fetchBtn" class="primary-btn">
933
- <i class="fas fa-search"></i> Fetch
934
  </button>
935
  </div>
936
 
937
- <div id="statusMsg" style="display: none;"></div>
938
-
939
- <div id="repoInfoCard" class="repo-info-card">
940
- <div class="repo-info-header">
941
- <div class="repo-info-left">
942
- <span class="repo-type-badge" id="repoTypeBadge">MODEL</span>
943
- <span class="repo-info-name" id="repoInfoName"></span>
944
- </div>
945
- <div class="repo-info-stats" id="repoInfoStats"></div>
946
- </div>
947
- </div>
948
 
949
  <div id="emptyState" class="empty-state">
950
- <div class="homepage-section">
951
- <h3>πŸ€— Explore HuggingFace Repositories</h3>
952
- <p style="color: var(--file-color); margin-bottom: 20px;">
953
- Visualize the file tree of any Model, Dataset, or Space
954
- </p>
955
- </div>
956
-
957
- <div class="homepage-section">
958
- <h3>Featured Repositories</h3>
959
- <div id="featuredCloud" class="tag-cloud"></div>
960
- </div>
961
-
962
- <div class="homepage-section">
963
- <h3>Popular Models</h3>
964
- <div id="modelCloud" class="tag-cloud"></div>
965
- </div>
966
-
967
- <div class="homepage-section">
968
- <h3>Popular Datasets</h3>
969
- <div id="datasetCloud" class="tag-cloud"></div>
970
  </div>
971
  </div>
972
 
973
- <div id="treeWrapper" class="tree-wrapper" style="display: none;">
974
  <div class="toolbar">
975
- <div class="tools-group">
976
- <div class="dropdown">
977
- <button class="tool-btn" id="sortBtnLabel">
978
- <i class="fas fa-sort-amount-down"></i> Sort
979
- </button>
980
- <div class="dropdown-content">
981
- <a href="#" data-sort="folder-az">Folders First (A-Z)</a>
982
- <a href="#" data-sort="folder-za">Folders First (Z-A)</a>
983
- <a href="#" data-sort="alpha-az">Name (A-Z)</a>
984
- <a href="#" data-sort="alpha-za">Name (Z-A)</a>
985
- <a href="#" data-sort="size-desc">Size (Largest)</a>
986
- <a href="#" data-sort="size-asc">Size (Smallest)</a>
987
- </div>
988
- </div>
989
-
990
- <div class="dropdown">
991
- <button class="tool-btn" id="styleBtnLabel">
992
- <i class="fas fa-code-branch"></i> Style
993
- </button>
994
- <div class="dropdown-content">
995
- <a href="#" data-style="classic">Classic (└──)</a>
996
- <a href="#" data-style="slashed">Slashed (/src)</a>
997
- <a href="#" data-style="plus">ASCII (+--)</a>
998
- <a href="#" data-style="minimal">Minimal (Indent)</a>
999
- </div>
1000
- </div>
1001
  </div>
1002
-
1003
- <div class="tools-group right">
1004
- <button id="shareBtn" class="tool-btn">
1005
- <i class="fas fa-share-alt"></i> Share
1006
- </button>
1007
- <button id="copyAllBtn" class="tool-btn action-btn">
1008
  <i class="far fa-copy"></i> Copy Tree
1009
  </button>
1010
  </div>
1011
  </div>
1012
-
1013
- <div class="tree-search">
1014
- <input type="text" id="treeFilter" placeholder="Filter files... (e.g. .safetensors, config, README)">
1015
- </div>
1016
-
1017
  <div class="terminal-window">
1018
- <div id="lineNumbers" class="line-col"></div>
1019
- <div id="treeContent" class="code-col"></div>
1020
  </div>
1021
  </div>
1022
- </div>
1023
-
1024
- <div class="footer">
1025
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="1.0166668891906738 2.9749999046325684 21.966665267944336 20.024999618530273">
1026
- <path d="M7 3c-1.535 0-3.078.5-4.25 1.7-2.343 2.4-2.279 6.1 0 8.5L12 23l9.25-9.8c2.279-2.4 2.343-6.1 0-8.5-2.343-2.3-6.157-2.3-8.5 0l-.75.8-.75-.8C10.078 3.5 8.536 3 7 3" fill="#e74c3c"></path>
1027
- </svg>
1028
- <b>{ HFTree - <a href="https://huggingface.co">HuggingFace</a> Repo Visualizer }</b>
1029
- </div>
1030
 
1031
- <div id="shareOverlay" class="overlay">
1032
- <div class="overlay-content">
1033
- <h3>Share View</h3>
1034
- <div class="share-box">
1035
- <input type="text" id="shareInput" readonly>
1036
- <button id="copyShareBtn"><i class="far fa-copy"></i></button>
1037
- </div>
1038
- <button id="closeOverlay" class="close-btn">&times;</button>
1039
  </div>
1040
  </div>
1041
 
1042
- <script>
1043
- // ============================================================
1044
- // HFTree - HuggingFace Repository Tree Visualizer
1045
- // ============================================================
1046
-
1047
- // --- State ---
1048
- let currentData = [];
1049
- let currentSort = 'folder-az';
1050
- let currentStyle = 'classic';
1051
- let currentType = 'models'; // models | datasets | spaces
1052
- let currentRepo = '';
1053
- let currentBranch = 'main';
1054
- let filterText = '';
1055
-
1056
- // --- Elements ---
1057
  const els = {
 
1058
  repo: document.getElementById('repoInput'),
1059
  branch: document.getElementById('branchInput'),
1060
- fetchBtn: document.getElementById('fetchBtn'),
1061
- wrapper: document.getElementById('treeWrapper'),
1062
- empty: document.getElementById('emptyState'),
1063
  status: document.getElementById('statusMsg'),
1064
- lineNums: document.getElementById('lineNumbers'),
1065
- treeContent: document.getElementById('treeContent'),
1066
- privateBtn: document.getElementById('privateRepoBtn'),
1067
- tokenPanel: document.getElementById('tokenSection'),
1068
- hfToken: document.getElementById('hfToken'),
1069
- saveToken: document.getElementById('saveTokenBtn'),
1070
- clearToken: document.getElementById('clearTokenBtn'),
1071
- copyAll: document.getElementById('copyAllBtn'),
1072
- shareBtn: document.getElementById('shareBtn'),
1073
- overlay: document.getElementById('shareOverlay'),
1074
- shareInput: document.getElementById('shareInput'),
1075
- copyShareBtn: document.getElementById('copyShareBtn'),
1076
- closeOverlay: document.getElementById('closeOverlay'),
1077
- sortBtnLabel: document.getElementById('sortBtnLabel'),
1078
- styleBtnLabel: document.getElementById('styleBtnLabel'),
1079
- dropdowns: document.querySelectorAll('.dropdown'),
1080
- urlPrefix: document.getElementById('urlPrefix'),
1081
- typeModel: document.getElementById('typeModel'),
1082
- typeDataset: document.getElementById('typeDataset'),
1083
- typeSpace: document.getElementById('typeSpace'),
1084
- repoInfoCard: document.getElementById('repoInfoCard'),
1085
- repoTypeBadge: document.getElementById('repoTypeBadge'),
1086
- repoInfoName: document.getElementById('repoInfoName'),
1087
- repoInfoStats: document.getElementById('repoInfoStats'),
1088
- treeFilter: document.getElementById('treeFilter'),
1089
  };
1090
 
1091
- // --- Featured Repos ---
1092
- const FEATURED = [
1093
- { repo: 'prithivMLmods/Qwen3-VL-4B-Instruct-Unredacted-MAX', type: 'models', label: 'Qwen3-VL-4B' },
1094
- { repo: 'prithivMLmods/Qwen3-VL-abliterated-MAX-Fast', type: 'spaces', label: 'Qwen3-VL Space' },
1095
- { repo: 'prithivMLmods/LAP2-K-Think-v1.b', type: 'datasets', label: 'LAP2-K-Think' },
1096
- ];
1097
-
1098
- const POPULAR_MODELS = [
1099
- { repo: 'meta-llama/Llama-3.1-8B-Instruct', type: 'models' },
1100
- { repo: 'mistralai/Mistral-7B-Instruct-v0.3', type: 'models' },
1101
- { repo: 'google/gemma-2-9b-it', type: 'models' },
1102
- { repo: 'Qwen/Qwen2.5-7B-Instruct', type: 'models' },
1103
- { repo: 'stabilityai/stable-diffusion-xl-base-1.0', type: 'models' },
1104
- { repo: 'openai/whisper-large-v3', type: 'models' },
1105
- { repo: 'microsoft/Phi-3-mini-4k-instruct', type: 'models' },
1106
- { repo: 'black-forest-labs/FLUX.1-dev', type: 'models' },
1107
- ];
1108
-
1109
- const POPULAR_DATASETS = [
1110
- { repo: 'tatsu-lab/alpaca', type: 'datasets' },
1111
- { repo: 'databricks/databricks-dolly-15k', type: 'datasets' },
1112
- { repo: 'Open-Orca/OpenOrca', type: 'datasets' },
1113
- { repo: 'HuggingFaceFW/fineweb', type: 'datasets' },
1114
- { repo: 'allenai/c4', type: 'datasets' },
1115
- ];
1116
-
1117
- // --- HuggingFace API ---
1118
- const HF_API = 'https://huggingface.co/api';
1119
-
1120
- function getApiUrl(type, repo, branch) {
1121
- // type: models, datasets, spaces
1122
- return `${HF_API}/${type}/${repo}/tree/${branch}`;
1123
- }
1124
 
1125
- function getRepoPageUrl(type, repo) {
1126
- if (type === 'models') return `https://huggingface.co/${repo}`;
1127
- if (type === 'datasets') return `https://huggingface.co/datasets/${repo}`;
1128
- if (type === 'spaces') return `https://huggingface.co/spaces/${repo}`;
1129
- return `https://huggingface.co/${repo}`;
1130
- }
1131
-
1132
- function getHeaders() {
1133
- const token = localStorage.getItem('hft_token');
1134
- const h = { 'Accept': 'application/json' };
1135
- if (token) h['Authorization'] = `Bearer ${token}`;
1136
- return h;
1137
- }
1138
-
1139
- // Recursive fetch: HF API returns per-directory, so we must recurse
1140
- async function fetchTree(type, repo, branch, path = '') {
1141
- const url = path
1142
- ? `${HF_API}/${type}/${repo}/tree/${branch}/${path}`
1143
- : `${HF_API}/${type}/${repo}/tree/${branch}`;
1144
-
1145
- const resp = await fetch(url, { headers: getHeaders() });
1146
-
1147
- if (!resp.ok) {
1148
- if (resp.status === 401) throw new Error('401: Unauthorized. Check your token.');
1149
- if (resp.status === 403) throw new Error('403: Forbidden / Rate Limited.');
1150
- if (resp.status === 404) {
1151
- // Try fallback branch
1152
- if (branch === 'main' && !path) {
1153
- const resp2 = await fetch(`${HF_API}/${type}/${repo}/tree/master`, { headers: getHeaders() });
1154
- if (resp2.ok) {
1155
- const data = await resp2.json();
1156
- return { items: data, switchedBranch: 'master' };
1157
- }
1158
- }
1159
- throw new Error('404: Repository not found (or is private).');
1160
- }
1161
- throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
1162
- }
1163
-
1164
- const data = await resp.json();
1165
- return { items: data, switchedBranch: null };
1166
- }
1167
-
1168
- async function fetchFullTree(type, repo, branch) {
1169
- let allItems = [];
1170
- let switchedBranch = null;
1171
-
1172
- async function recurse(path) {
1173
- const result = await fetchTree(type, repo, switchedBranch || branch, path);
1174
-
1175
- if (result.switchedBranch && !switchedBranch) {
1176
- switchedBranch = result.switchedBranch;
1177
- }
1178
-
1179
- for (const item of result.items) {
1180
- allItems.push({
1181
- path: item.path,
1182
- type: item.type === 'directory' ? 'tree' : 'blob',
1183
- size: item.size || 0,
1184
- lfs: item.lfs || null,
1185
- });
1186
-
1187
- if (item.type === 'directory') {
1188
- try {
1189
- await recurse(item.path);
1190
- } catch (e) {
1191
- // Some dirs may fail, continue
1192
- console.warn(`Skipping ${item.path}: ${e.message}`);
1193
- }
1194
- }
1195
- }
1196
- }
1197
-
1198
- await recurse('');
1199
- return { tree: allItems, branch: switchedBranch || branch };
1200
- }
1201
-
1202
- // --- Initialization ---
1203
  document.addEventListener('DOMContentLoaded', () => {
1204
- initTheme();
1205
- checkSavedToken();
1206
- setupDropdowns();
1207
- setupShareOverlay();
1208
- buildHomepageTags();
1209
-
1210
- els.fetchBtn.addEventListener('click', loadTree);
1211
- els.copyAll.addEventListener('click', copyFullTree);
1212
-
1213
- // Token panel
1214
- els.privateBtn.addEventListener('click', () => {
1215
- const hidden = els.tokenPanel.style.display === 'none';
1216
- els.tokenPanel.style.display = hidden ? 'block' : 'none';
 
1217
  });
1218
- els.saveToken.addEventListener('click', saveToken);
1219
- els.clearToken.addEventListener('click', clearToken);
1220
 
1221
- // Type buttons
1222
- [els.typeModel, els.typeDataset, els.typeSpace].forEach(btn => {
1223
- btn.addEventListener('click', () => setType(btn.dataset.type));
 
 
 
1224
  });
1225
 
1226
- // Filter
1227
- els.treeFilter.addEventListener('input', (e) => {
1228
- filterText = e.target.value.trim().toLowerCase();
1229
- render();
 
1230
  });
1231
-
1232
- // Enter key
1233
- [els.repo, els.branch, els.hfToken].forEach(el =>
1234
- el.addEventListener('keypress', e => {
1235
- if (e.key === 'Enter') {
1236
- if (el === els.hfToken) saveToken();
1237
- else loadTree();
1238
- }
1239
- })
1240
- );
1241
-
1242
- // Check URL hash
1243
- parseUrlHash();
1244
  });
1245
 
1246
- function setType(type) {
1247
- currentType = type;
1248
- [els.typeModel, els.typeDataset, els.typeSpace].forEach(b => {
1249
- b.className = 'type-btn';
1250
- });
1251
- if (type === 'models') {
1252
- els.typeModel.classList.add('active-model');
1253
- els.urlPrefix.textContent = 'huggingface.co/';
1254
- } else if (type === 'datasets') {
1255
- els.typeDataset.classList.add('active-dataset');
1256
- els.urlPrefix.textContent = 'huggingface.co/datasets/';
1257
- } else if (type === 'spaces') {
1258
- els.typeSpace.classList.add('active-space');
1259
- els.urlPrefix.textContent = 'huggingface.co/spaces/';
1260
- }
1261
  }
1262
 
1263
- function buildHomepageTags() {
1264
- const featuredEl = document.getElementById('featuredCloud');
1265
- FEATURED.forEach(item => {
1266
- const tag = createRepoTag(item.label || item.repo, item.type, item.repo);
1267
- featuredEl.appendChild(tag);
1268
- });
1269
-
1270
- const modelEl = document.getElementById('modelCloud');
1271
- POPULAR_MODELS.forEach(item => {
1272
- const tag = createRepoTag(item.repo, item.type, item.repo);
1273
- modelEl.appendChild(tag);
1274
- });
1275
-
1276
- const datasetEl = document.getElementById('datasetCloud');
1277
- POPULAR_DATASETS.forEach(item => {
1278
- const tag = createRepoTag(item.repo, item.type, item.repo);
1279
- datasetEl.appendChild(tag);
1280
- });
1281
  }
1282
 
1283
- function createRepoTag(label, type, repo) {
1284
- const btn = document.createElement('button');
1285
- btn.className = 'repo-tag';
1286
- const typeClass = type === 'models' ? 'model' : type === 'datasets' ? 'dataset' : 'space';
1287
- const typeLabel = type === 'models' ? 'MODEL' : type === 'datasets' ? 'DATASET' : 'SPACE';
1288
- btn.innerHTML = `<span class="tag-type ${typeClass}">${typeLabel}</span> ${label}`;
1289
- btn.addEventListener('click', () => {
1290
- setType(type);
1291
- els.repo.value = repo;
1292
- els.branch.value = 'main';
1293
- loadTree();
1294
- });
1295
- return btn;
1296
- }
 
 
1297
 
1298
- function parseUrlHash() {
1299
- const hash = window.location.hash;
1300
- if (hash && hash.startsWith('#')) {
1301
- // Format: #type/user/repo/branch
1302
- const parts = hash.substring(1).split('/');
1303
- if (parts.length >= 3) {
1304
- const type = parts[0];
1305
- const repo = parts[1] + '/' + parts[2];
1306
- const branch = parts[3] || 'main';
1307
-
1308
- if (['models', 'datasets', 'spaces'].includes(type)) {
1309
- setType(type);
1310
- els.repo.value = repo;
1311
- els.branch.value = branch;
1312
- loadTree();
1313
- }
1314
  }
 
 
 
 
 
 
 
 
1315
  }
1316
  }
1317
 
1318
- // --- Fetch Logic ---
1319
- async function loadTree() {
1320
  const repo = els.repo.value.trim();
 
1321
  const branch = els.branch.value.trim() || 'main';
1322
 
1323
- if (!repo.includes('/')) return showMsg("Invalid format. Use 'username/repo-name'", "error");
1324
-
1325
- currentRepo = repo;
1326
- currentBranch = branch;
1327
- filterText = '';
1328
- els.treeFilter.value = '';
1329
 
1330
- if (els.empty) els.empty.style.display = 'none';
1331
- els.wrapper.style.display = 'none';
1332
- els.repoInfoCard.classList.remove('visible');
1333
- showMsg(`<span class="spinner"></span> Fetching ${currentType}/${repo} (${branch})...`, "loading");
1334
- els.fetchBtn.disabled = true;
 
 
1335
 
1336
  try {
1337
- const cacheKey = `hft_${currentType}_${repo}_${branch}`;
1338
- const tokenActive = !!localStorage.getItem('hft_token');
1339
- const cached = !tokenActive ? sessionStorage.getItem(cacheKey) : null;
1340
-
1341
- if (cached) {
1342
- const parsed = JSON.parse(cached);
1343
- currentData = parsed.tree;
1344
- currentBranch = parsed.branch;
1345
- showMsg("", "");
1346
- } else {
1347
- const result = await fetchFullTree(currentType, repo, branch);
1348
- currentData = result.tree;
1349
- currentBranch = result.branch;
1350
-
1351
- if (result.branch !== branch) {
1352
- showMsg(`Branch '${branch}' not found. Switched to '${result.branch}'.`, "loading");
1353
- els.branch.value = result.branch;
1354
- } else {
1355
- showMsg("", "");
1356
- }
1357
-
1358
- if (!tokenActive) {
1359
- try {
1360
- sessionStorage.setItem(cacheKey, JSON.stringify({ tree: currentData, branch: currentBranch }));
1361
- } catch (e) { /* quota */ }
1362
- }
1363
  }
1364
 
1365
- // Update hash
1366
- window.location.hash = `${currentType}/${repo}/${currentBranch}`;
1367
-
1368
- // Update page title
1369
- document.title = `${repo} - HFTree`;
1370
-
1371
- // Show repo info
1372
- showRepoInfo(repo, currentBranch);
1373
-
1374
- render();
1375
- els.wrapper.style.display = 'flex';
 
1376
 
1377
  } catch (err) {
1378
- console.error(err);
1379
- let msg = err.message;
1380
-
1381
- if (msg.includes('401')) {
1382
- showMsg("Unauthorized. Check your HuggingFace token.", "error");
1383
- } else if (msg.includes('403')) {
1384
- showMsg("Rate Limited or Forbidden. Try adding a token.", "error");
1385
- } else if (msg.includes('404')) {
1386
- showMsg("Repository not found. Check the name and type.", "error");
1387
- } else {
1388
- showMsg(msg, "error");
1389
- }
1390
-
1391
  els.empty.style.display = 'block';
1392
  } finally {
1393
- els.fetchBtn.disabled = false;
1394
  }
1395
  }
1396
 
1397
- function showRepoInfo(repo, branch) {
1398
- const typeClass = currentType === 'models' ? 'model' : currentType === 'datasets' ? 'dataset' : 'space';
1399
- const typeLabel = currentType === 'models' ? 'MODEL' : currentType === 'datasets' ? 'DATASET' : 'SPACE';
1400
-
1401
- els.repoTypeBadge.className = `repo-type-badge ${typeClass}`;
1402
- els.repoTypeBadge.textContent = typeLabel;
1403
-
1404
- const pageUrl = getRepoPageUrl(currentType, repo);
1405
- els.repoInfoName.innerHTML = `<a href="${pageUrl}" target="_blank" rel="noopener">${repo}</a>`;
1406
-
1407
- // Stats
1408
- const fileCount = currentData.filter(i => i.type === 'blob').length;
1409
- const folderCount = currentData.filter(i => i.type === 'tree').length;
1410
- const totalSize = currentData.reduce((acc, i) => acc + (i.size || 0), 0);
1411
-
1412
- els.repoInfoStats.innerHTML = `
1413
- <span><i class="far fa-file"></i> ${fileCount} files</span>
1414
- <span><i class="far fa-folder"></i> ${folderCount} folders</span>
1415
- <span><i class="fas fa-weight-hanging"></i> ${formatSize(totalSize)}</span>
1416
- <span><i class="fas fa-code-branch"></i> ${branch}</span>
1417
- `;
1418
-
1419
- els.repoInfoCard.classList.add('visible');
1420
- }
1421
-
1422
- // --- Sort ---
1423
- function sortTree(data, mode) {
1424
- const sorted = [...data];
1425
- sorted.sort((a, b) => {
1426
- const aIsDir = a.type === 'tree' ? 1 : 0;
1427
- const bIsDir = b.type === 'tree' ? 1 : 0;
1428
- const aName = a.path.toLowerCase();
1429
- const bName = b.path.toLowerCase();
1430
-
1431
- switch (mode) {
1432
- case 'folder-az':
1433
- if (aIsDir !== bIsDir) return bIsDir - aIsDir;
1434
- return aName.localeCompare(bName);
1435
- case 'folder-za':
1436
- if (aIsDir !== bIsDir) return bIsDir - aIsDir;
1437
- return bName.localeCompare(aName);
1438
- case 'alpha-az':
1439
- return aName.localeCompare(bName);
1440
- case 'alpha-za':
1441
- return bName.localeCompare(aName);
1442
- case 'size-desc':
1443
- return (b.size || 0) - (a.size || 0);
1444
- case 'size-asc':
1445
- return (a.size || 0) - (b.size || 0);
1446
- default:
1447
- return 0;
1448
- }
1449
- });
1450
- return sorted;
1451
- }
1452
-
1453
- // --- Render ---
1454
- function render() {
1455
- els.lineNums.innerHTML = '';
1456
- els.treeContent.innerHTML = '';
1457
-
1458
- let dataToRender = currentData;
1459
-
1460
- // Apply filter
1461
- if (filterText) {
1462
- const matchedPaths = new Set();
1463
- currentData.forEach(item => {
1464
- if (item.path.toLowerCase().includes(filterText)) {
1465
- matchedPaths.add(item.path);
1466
- // Add parent dirs
1467
- const parts = item.path.split('/');
1468
- for (let i = 1; i < parts.length; i++) {
1469
- matchedPaths.add(parts.slice(0, i).join('/'));
1470
- }
1471
- }
1472
- });
1473
- dataToRender = currentData.filter(item => matchedPaths.has(item.path));
1474
- }
1475
-
1476
- const sortedFlat = sortTree(dataToRender, currentSort);
1477
- const hierarchy = buildVisualHierarchy(sortedFlat);
1478
-
1479
- const fragNums = document.createDocumentFragment();
1480
- const fragLines = document.createDocumentFragment();
1481
-
1482
- hierarchy.forEach((item, idx) => {
1483
- const n = document.createElement('div');
1484
- n.textContent = idx + 1;
1485
- fragNums.appendChild(n);
1486
-
1487
- const row = document.createElement('div');
1488
- row.className = 'tree-line';
1489
- row.style.animation = `fadeIn 0.1s forwards ${Math.min(idx * 2, 500)}ms`;
1490
- row.style.opacity = '0';
1491
-
1492
- const iconClass = item.type === 'tree' ? 'fa-solid fa-folder' : getFileIcon(item.name);
1493
- const typeClass = item.type === 'tree' ? 'folder' : 'file';
1494
-
1495
- let prefixDisplay = item.prefix;
1496
- let nameDisplay = item.name;
1497
-
1498
- if (currentStyle === 'minimal') prefixDisplay = item.indent;
1499
- if (currentStyle === 'plus') prefixDisplay = prefixDisplay.replace(/└──/g, '+--').replace(/β”œβ”€β”€/g, '+--').replace(/β”‚/g, '|');
1500
- if (currentStyle === 'slashed') {
1501
- if (item.type === 'tree') nameDisplay = `/${item.name}`;
1502
- prefixDisplay = item.prefix.replace(/β”‚/g, ' ').replace(/β”œβ”€β”€/g, ' ').replace(/└──/g, ' ');
1503
- }
1504
-
1505
- const sizeStr = item.type === 'blob' && item.size ? `<span class="t-size">${formatSize(item.size)}</span>` : '';
1506
- const lfsStr = item.lfs ? `<span class="lfs-badge">LFS</span>` : '';
1507
-
1508
- // Highlight matching text in filter
1509
- let nameHtml = nameDisplay;
1510
- if (filterText && nameDisplay.toLowerCase().includes(filterText)) {
1511
- const regex = new RegExp(`(${escapeRegex(filterText)})`, 'gi');
1512
- nameHtml = nameDisplay.replace(regex, '<mark style="background: var(--accent-light); color: var(--accent); padding: 0 2px; border-radius: 2px;">$1</mark>');
1513
- }
1514
-
1515
- row.innerHTML = `
1516
- <span class="t-prefix">${prefixDisplay}</span>
1517
- <span class="t-icon"><i class="${iconClass}"></i></span>
1518
- <span class="t-name ${typeClass}">${nameHtml}</span>
1519
- ${sizeStr}${lfsStr}
1520
- <button class="sub-copy" data-path="${item.path}" title="Copy Path">
1521
- <i class="far fa-copy"></i>
1522
- </button>
1523
- `;
1524
- fragLines.appendChild(row);
1525
  });
1526
 
1527
- els.lineNums.appendChild(fragNums);
1528
- els.treeContent.appendChild(fragLines);
1529
-
1530
- // Event Delegation
1531
- els.treeContent.onclick = (e) => {
1532
- const btn = e.target.closest('.sub-copy');
1533
- if (btn) handleCopy(btn, btn.dataset.path, true);
1534
- };
1535
- }
1536
-
1537
- function escapeRegex(string) {
1538
- return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1539
- }
1540
-
1541
- // --- File Icon Helper ---
1542
- function getFileIcon(name) {
1543
- const ext = name.split('.').pop().toLowerCase();
1544
- const iconMap = {
1545
- 'py': 'fa-brands fa-python',
1546
- 'js': 'fa-brands fa-js',
1547
- 'ts': 'fa-brands fa-js',
1548
- 'jsx': 'fa-brands fa-react',
1549
- 'tsx': 'fa-brands fa-react',
1550
- 'html': 'fa-brands fa-html5',
1551
- 'css': 'fa-brands fa-css3-alt',
1552
- 'md': 'fa-solid fa-file-lines',
1553
- 'json': 'fa-solid fa-file-code',
1554
- 'yaml': 'fa-solid fa-file-code',
1555
- 'yml': 'fa-solid fa-file-code',
1556
- 'toml': 'fa-solid fa-file-code',
1557
- 'txt': 'fa-solid fa-file-lines',
1558
- 'csv': 'fa-solid fa-file-csv',
1559
- 'png': 'fa-solid fa-file-image',
1560
- 'jpg': 'fa-solid fa-file-image',
1561
- 'jpeg': 'fa-solid fa-file-image',
1562
- 'gif': 'fa-solid fa-file-image',
1563
- 'svg': 'fa-solid fa-file-image',
1564
- 'webp': 'fa-solid fa-file-image',
1565
- 'mp4': 'fa-solid fa-file-video',
1566
- 'mp3': 'fa-solid fa-file-audio',
1567
- 'wav': 'fa-solid fa-file-audio',
1568
- 'pdf': 'fa-solid fa-file-pdf',
1569
- 'zip': 'fa-solid fa-file-zipper',
1570
- 'tar': 'fa-solid fa-file-zipper',
1571
- 'gz': 'fa-solid fa-file-zipper',
1572
- 'safetensors': 'fa-solid fa-cube',
1573
- 'bin': 'fa-solid fa-cube',
1574
- 'gguf': 'fa-solid fa-cube',
1575
- 'pt': 'fa-solid fa-cube',
1576
- 'pth': 'fa-solid fa-cube',
1577
- 'onnx': 'fa-solid fa-cube',
1578
- 'h5': 'fa-solid fa-cube',
1579
- 'parquet': 'fa-solid fa-database',
1580
- 'arrow': 'fa-solid fa-database',
1581
- 'rs': 'fa-brands fa-rust',
1582
- 'go': 'fa-brands fa-golang',
1583
- 'java': 'fa-brands fa-java',
1584
- 'dockerfile': 'fa-brands fa-docker',
1585
- 'sh': 'fa-solid fa-terminal',
1586
- 'bash': 'fa-solid fa-terminal',
1587
- 'gitignore': 'fa-brands fa-git-alt',
1588
- 'gitattributes': 'fa-brands fa-git-alt',
1589
- 'license': 'fa-solid fa-scale-balanced',
1590
- };
1591
-
1592
- // Check full filename for special cases
1593
- const nameLower = name.toLowerCase();
1594
- if (nameLower === 'dockerfile') return 'fa-brands fa-docker';
1595
- if (nameLower === 'license' || nameLower === 'licence') return 'fa-solid fa-scale-balanced';
1596
- if (nameLower === 'readme.md' || nameLower === 'readme') return 'fa-solid fa-book';
1597
- if (nameLower === 'requirements.txt') return 'fa-solid fa-list-check';
1598
- if (nameLower === '.gitignore' || nameLower === '.gitattributes') return 'fa-brands fa-git-alt';
1599
-
1600
- return iconMap[ext] || 'fa-regular fa-file';
1601
- }
1602
-
1603
- // --- Build Visual Hierarchy ---
1604
- function buildVisualHierarchy(sortedFlatList) {
1605
- const root = { children: [] };
1606
  const map = { '': root };
1607
 
1608
- sortedFlatList.forEach(item => {
1609
- map[item.path] = { ...item, name: item.path.split('/').pop(), children: [] };
1610
- });
1611
-
1612
- sortedFlatList.forEach(item => {
1613
  const parts = item.path.split('/');
1614
- parts.pop();
1615
- const parentPath = parts.join('/');
1616
- const parent = map[parentPath] || root;
1617
- parent.children.push(map[item.path]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1618
  });
1619
 
1620
- const result = [];
1621
- const traverse = (node, prefix, isLast) => {
1622
- const connector = isLast ? "└── " : "β”œβ”€β”€ ";
1623
- result.push({
1624
- name: node.name,
1625
- path: node.path,
1626
- type: node.type,
1627
- size: node.size,
1628
- lfs: node.lfs,
1629
- prefix: prefix + connector,
1630
- indent: prefix.replace(/β”‚ /g, ' ').replace(/β”œβ”€β”€ /g, ' ').replace(/└── /g, ' ') + ' '
1631
- });
1632
-
1633
- if (node.children && node.children.length > 0) {
1634
- const childPrefix = prefix + (isLast ? " " : "β”‚ ");
1635
- node.children.forEach((child, i) => {
1636
- traverse(child, childPrefix, i === node.children.length - 1);
1637
  });
 
1638
  }
1639
  };
 
1640
 
1641
- root.children.forEach((child, i) => {
1642
- traverse(child, "", i === root.children.length - 1);
1643
- });
1644
- return result;
1645
  }
1646
 
1647
- // --- Generate ASCII Tree Text ---
1648
- function generateAsciiTree(data) {
1649
- const sorted = sortTree(data, currentSort);
1650
- const hierarchy = buildVisualHierarchy(sorted);
1651
- const lines = [`${currentRepo} (${currentBranch})`];
1652
-
1653
- hierarchy.forEach(item => {
1654
- let prefix = item.prefix;
1655
- let name = item.name;
1656
-
1657
- if (currentStyle === 'minimal') prefix = item.indent;
1658
- if (currentStyle === 'plus') prefix = prefix.replace(/└──/g, '+--').replace(/β”œβ”€β”€/g, '+--').replace(/β”‚/g, '|');
1659
- if (currentStyle === 'slashed') {
1660
- if (item.type === 'tree') name = `/${name}`;
1661
- prefix = item.prefix.replace(/β”‚/g, ' ').replace(/β”œβ”€β”€/g, ' ').replace(/└──/g, ' ');
1662
- }
1663
 
1664
- const size = item.type === 'blob' && item.size ? ` (${formatSize(item.size)})` : '';
1665
- lines.push(`${prefix}${name}${size}`);
1666
- });
1667
-
1668
- return lines.join('\n');
1669
- }
1670
-
1671
- // --- Copy ---
1672
- function handleCopy(btn, text, isSub) {
1673
- navigator.clipboard.writeText(text).then(() => {
1674
- if (!isSub) {
1675
- const originalHTML = btn.innerHTML;
1676
- btn.innerHTML = `<i class="fas fa-check"></i> Copied`;
1677
- setTimeout(() => { btn.innerHTML = originalHTML; }, 1500);
1678
- } else {
1679
- const icon = btn.querySelector('i');
1680
- const originalClass = icon.className;
1681
- icon.className = "fas fa-check";
1682
- setTimeout(() => { icon.className = originalClass; }, 1500);
1683
- }
1684
- }).catch(err => console.error("Copy failed", err));
1685
- }
1686
-
1687
- function copyFullTree() {
1688
- const text = generateAsciiTree(currentData);
1689
- handleCopy(els.copyAll, text, false);
1690
- }
 
 
 
 
 
 
1691
 
1692
- // --- Token ---
1693
- function saveToken() {
1694
- const token = els.hfToken.value.trim();
1695
- if (!token) return;
1696
- localStorage.setItem('hft_token', token);
1697
- checkSavedToken();
1698
- showMsg("Token saved.", "loading");
1699
- setTimeout(() => showMsg("", ""), 1500);
1700
- }
1701
 
1702
- function clearToken() {
1703
- localStorage.removeItem('hft_token');
1704
- els.hfToken.value = '';
1705
- checkSavedToken();
1706
- showMsg("Token cleared.", "error");
1707
- setTimeout(() => showMsg("", ""), 1500);
1708
- }
1709
 
1710
- function checkSavedToken() {
1711
- const token = localStorage.getItem('hft_token');
1712
- if (token) {
1713
- els.hfToken.value = token;
1714
- els.privateBtn.innerHTML = `<i class="fas fa-lock-open"></i> Token Active`;
1715
- els.privateBtn.style.color = 'var(--folder-color)';
1716
- els.saveToken.style.display = 'none';
1717
- els.clearToken.style.display = 'inline-block';
1718
- } else {
1719
- els.privateBtn.innerHTML = `<i class="fas fa-lock"></i> Token`;
1720
- els.privateBtn.style.color = '';
1721
- els.saveToken.style.display = 'inline-block';
1722
- els.clearToken.style.display = 'none';
1723
- }
1724
- }
1725
-
1726
- // --- Size Formatter ---
1727
- function formatSize(bytes) {
1728
- if (!bytes || bytes === 0) return '0 B';
1729
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
1730
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
1731
- const size = (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1);
1732
- return `${size} ${units[i]}`;
1733
- }
1734
-
1735
- // --- UI Helpers ---
1736
- function setupDropdowns() {
1737
- [els.sortBtnLabel, els.styleBtnLabel].forEach(btn => {
1738
- btn.addEventListener('click', (e) => {
1739
- e.stopPropagation();
1740
- const parent = btn.parentElement;
1741
- closeDropdowns(parent);
1742
- parent.classList.toggle('active');
1743
- });
1744
- });
1745
 
1746
- document.querySelectorAll('[data-sort]').forEach(el => {
1747
- el.addEventListener('click', (e) => {
1748
- e.preventDefault();
1749
- currentSort = e.target.dataset.sort;
1750
- els.sortBtnLabel.innerHTML = `<i class="fas fa-sort-amount-down"></i> ${e.target.innerText}`;
1751
- closeDropdowns();
1752
- render();
1753
- });
1754
- });
 
1755
 
1756
- document.querySelectorAll('[data-style]').forEach(el => {
1757
- el.addEventListener('click', (e) => {
1758
- e.preventDefault();
1759
- currentStyle = e.target.dataset.style;
1760
- els.styleBtnLabel.innerHTML = `<i class="fas fa-code-branch"></i> ${e.target.innerText}`;
1761
- closeDropdowns();
1762
- render();
1763
  });
1764
- });
1765
-
1766
- document.addEventListener('click', () => closeDropdowns());
1767
- }
1768
 
1769
- function closeDropdowns(except = null) {
1770
- els.dropdowns.forEach(d => {
1771
- if (d !== except) d.classList.remove('active');
1772
- });
1773
  }
1774
 
1775
- function setupShareOverlay() {
1776
- els.shareBtn.addEventListener('click', (e) => {
1777
- e.stopPropagation();
1778
- els.shareInput.value = window.location.href;
1779
- els.overlay.classList.add('visible');
1780
- });
1781
-
1782
- els.closeOverlay.addEventListener('click', () => els.overlay.classList.remove('visible'));
1783
-
1784
- els.overlay.addEventListener('click', (e) => {
1785
- if (e.target === els.overlay) els.overlay.classList.remove('visible');
1786
- });
1787
-
1788
- els.shareInput.addEventListener('click', () => els.shareInput.select());
1789
- els.copyShareBtn.addEventListener('click', () => {
1790
- handleCopy(els.copyShareBtn, els.shareInput.value, true);
1791
- });
1792
  }
1793
 
1794
- function showMsg(text, type) {
1795
- els.status.style.display = text ? 'block' : 'none';
1796
- els.status.innerHTML = text;
1797
- els.status.className = type;
1798
  }
1799
 
1800
- function initTheme() {
1801
- const saved = localStorage.getItem('hft_theme') || 'light';
1802
- document.documentElement.setAttribute('data-theme', saved);
1803
-
1804
- document.getElementById('themeToggle').addEventListener('click', () => {
1805
- const next = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
1806
- document.documentElement.setAttribute('data-theme', next);
1807
- localStorage.setItem('hft_theme', next);
1808
  });
1809
  }
1810
-
1811
- // Inject fadeIn keyframes
1812
- const styleEl = document.createElement('style');
1813
- styleEl.innerHTML = `@keyframes fadeIn { to { opacity: 1; } }`;
1814
- document.head.appendChild(styleEl);
1815
- </script>
1816
  </body>
1817
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>HuggingFace Tree : File Visualizer</title>
7
+ <meta name="description" content="Explore Hugging Face Models, Datasets, and Spaces file structures without cloning.">
8
+
9
+ <!-- Icons and Fonts -->
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
12
+
13
  <style>
14
  /* =========================================
15
+ VARIABLES & THEME
16
  ========================================= */
17
  :root {
18
  --bg: #ffffff;
19
+ --text: #1f2937;
20
+ --header-bg: #f9fafb;
21
+ --border: #e5e7eb;
 
 
 
 
22
  --panel-bg: #ffffff;
23
+ --accent: #FFD21E; /* HF Yellow */
24
+ --accent-hover: #e5bc18;
25
+ --accent-text: #6b4f00; /* Darker for contrast on yellow */
26
+ --folder-color: #FCD34D;
27
+ --file-color: #9CA3AF;
28
+ --line-num: #9CA3AF;
29
+ --tree-line: #D1D5DB;
30
+ --btn-bg: #f3f4f6;
31
+ --btn-border: #d1d5db;
32
+ --code-font: 'Fira Code', monospace;
 
 
 
 
 
 
33
  }
34
 
35
  [data-theme="dark"] {
36
+ --bg: #0b0f19;
37
+ --text: #e5e7eb;
38
+ --header-bg: #111827;
39
+ --border: #374151;
40
+ --panel-bg: #1f2937;
41
+ --accent: #FFD21E;
42
+ --accent-hover: #e5bc18;
43
+ --accent-text: #000000;
44
+ --folder-color: #FBBF24;
45
+ --file-color: #9CA3AF;
46
+ --line-num: #6B7280;
47
+ --tree-line: #4B5563;
48
+ --btn-bg: #374151;
49
+ --btn-border: #4B5563;
 
 
 
 
 
 
 
 
50
  }
51
 
52
  /* =========================================
53
+ RESET & LAYOUT
54
  ========================================= */
55
  * { box-sizing: border-box; }
 
56
  body {
57
  margin: 0;
58
+ font-family: 'Inter', sans-serif;
59
  background: var(--bg);
60
  color: var(--text);
61
  transition: background 0.2s, color 0.2s;
62
+ display: flex;
63
+ flex-direction: column;
64
+ min-height: 100vh;
65
  }
66
 
 
 
 
67
  .app-container {
68
  max-width: 1000px;
69
  margin: 0 auto;
70
+ padding: 20px;
71
+ width: 100%;
72
+ flex: 1;
 
73
  }
74
 
75
  /* =========================================
76
+ HEADER
77
  ========================================= */
78
  header {
79
  display: flex;
80
  justify-content: space-between;
81
  align-items: center;
82
  margin-bottom: 30px;
83
+ padding-bottom: 20px;
84
+ border-bottom: 1px solid var(--border);
85
  }
86
 
87
  .brand {
 
92
  gap: 10px;
93
  }
94
 
95
+ .brand span { color: var(--text); }
96
+ .hf-emoji { font-size: 1.8rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ .header-actions button {
99
  background: none;
100
  border: none;
 
101
  color: var(--text);
102
+ font-size: 1.2rem;
 
 
103
  cursor: pointer;
104
  padding: 8px;
105
  border-radius: 50%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
107
+ .header-actions button:hover { background: var(--btn-bg); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  /* =========================================
110
+ SEARCH SECTION
111
  ========================================= */
112
+ .search-wrapper {
113
  display: flex;
 
114
  flex-wrap: wrap;
115
+ gap: 10px;
116
  margin-bottom: 20px;
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  .input-group {
120
  background: var(--header-bg);
121
  border: 1px solid var(--border);
 
124
  align-items: center;
125
  padding: 0 12px;
126
  height: 42px;
127
+ transition: border-color 0.2s;
 
128
  }
129
 
130
  .input-group:focus-within {
131
  border-color: var(--accent);
132
+ box-shadow: 0 0 0 2px rgba(255, 210, 30, 0.3);
133
  }
134
 
135
+ select {
136
+ background: transparent;
137
+ border: none;
138
+ color: var(--text);
139
+ font-family: inherit;
140
+ outline: none;
141
+ cursor: pointer;
142
+ font-weight: 600;
143
+ margin-right: 5px;
144
  }
145
 
146
  input {
 
149
  color: var(--text);
150
  width: 100%;
151
  outline: none;
152
+ font-family: var(--code-font);
153
+ font-size: 0.9rem;
154
  }
155
 
156
+ .type-select { width: 110px; }
157
+ .main-input { flex: 2; min-width: 250px; }
158
+ .branch-input { width: 120px; }
159
 
160
  .primary-btn {
161
  background: var(--accent);
162
+ color: var(--accent-text);
163
  border: none;
164
  padding: 0 25px;
165
  border-radius: 6px;
 
168
  height: 42px;
169
  display: flex;
170
  align-items: center;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  gap: 8px;
172
  }
173
+ .primary-btn:hover { background: var(--accent-hover); }
174
+ .primary-btn:disabled { opacity: 0.6; cursor: not-allowed; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  /* =========================================
177
+ TREE VIEW
178
  ========================================= */
179
+ .tree-container {
180
  border: 1px solid var(--border);
181
  border-radius: 8px;
 
 
182
  background: var(--panel-bg);
183
+ display: none; /* Hidden by default */
184
+ flex-direction: column;
185
+ overflow: hidden;
 
186
  }
187
 
188
  .toolbar {
189
  background: var(--header-bg);
190
+ padding: 8px 15px;
191
  border-bottom: 1px solid var(--border);
192
  display: flex;
193
  justify-content: space-between;
 
 
 
 
 
 
 
194
  }
195
 
196
  .tool-btn {
197
  background: var(--btn-bg);
198
  border: 1px solid var(--btn-border);
199
  color: var(--text);
200
+ padding: 5px 12px;
201
+ border-radius: 4px;
202
+ font-size: 0.8rem;
 
203
  cursor: pointer;
204
  display: flex;
205
  align-items: center;
206
+ gap: 6px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  }
208
+ .tool-btn:hover { background: calc(var(--btn-bg) - 10%); }
209
 
 
 
 
210
  .terminal-window {
 
211
  display: flex;
212
  overflow: auto;
213
+ font-family: var(--code-font);
 
214
  font-size: 14px;
215
+ line-height: 1.5;
216
+ max-height: 70vh;
217
  }
218
 
219
+ .line-numbers {
220
+ padding: 15px 10px;
221
  text-align: right;
222
  color: var(--line-num);
223
  border-right: 1px solid var(--border);
224
+ background: var(--header-bg);
225
+ min-width: 45px;
226
+ user-select: none;
227
  }
228
 
229
+ .code-content {
230
  padding: 15px;
231
  flex: 1;
232
  white-space: pre;
233
  }
234
 
235
+ .tree-row {
236
  display: flex;
237
  align-items: center;
238
+ height: 22px;
 
 
 
 
 
 
 
 
 
239
  }
240
+ .tree-row:hover { background: var(--header-bg); }
241
 
242
+ .t-prefix { color: var(--tree-line); }
243
+
244
+ .t-icon {
245
+ width: 20px;
246
+ text-align: center;
247
+ margin-right: 5px;
248
  }
249
+
250
  .fa-folder { color: var(--folder-color); }
251
  .fa-file { color: var(--file-color); }
252
 
253
  .t-name { color: var(--text); }
254
  .t-name.folder { font-weight: 600; }
255
 
256
+ .actions {
257
+ margin-left: auto;
 
 
 
 
 
258
  opacity: 0;
 
 
 
 
 
 
 
 
259
  display: flex;
260
+ gap: 8px;
261
  }
262
+ .tree-row:hover .actions { opacity: 1; }
263
 
264
+ .action-icon {
265
+ color: var(--line-num);
266
+ cursor: pointer;
267
+ font-size: 12px;
268
+ }
269
+ .action-icon:hover { color: var(--accent); }
270
 
271
  /* =========================================
272
+ UTILITIES
273
  ========================================= */
274
+ .status-msg {
275
+ text-align: center;
276
+ margin: 20px;
277
+ padding: 10px;
278
+ border-radius: 6px;
 
279
  display: none;
280
+ font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  }
282
+ .status-msg.error { background: rgba(220, 38, 38, 0.1); color: #dc2626; }
283
+ .status-msg.loading { background: rgba(255, 210, 30, 0.1); color: #b45309; }
284
 
285
+ .empty-state {
286
+ text-align: center;
287
+ margin-top: 40px;
288
+ color: var(--line-num);
289
  }
290
+ .empty-state i { font-size: 3rem; margin-bottom: 15px; opacity: 0.5; }
291
 
292
+ .examples {
293
+ margin-top: 20px;
294
  display: flex;
295
+ justify-content: center;
296
  gap: 10px;
297
+ flex-wrap: wrap;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  }
299
+ .chip {
300
+ background: var(--btn-bg);
301
+ padding: 5px 12px;
302
+ border-radius: 15px;
303
+ font-size: 0.8rem;
 
 
 
 
 
304
  cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  border: 1px solid var(--border);
 
 
 
 
306
  }
307
+ .chip:hover { border-color: var(--accent); }
308
 
 
 
 
 
 
 
 
 
 
309
  .footer {
 
 
 
 
310
  text-align: center;
311
+ font-size: 0.8rem;
312
+ color: var(--line-num);
313
+ padding: 20px;
314
+ margin-top: auto;
 
 
 
 
315
  }
316
 
317
+ /* Mobile */
 
 
 
 
 
 
 
 
 
318
  @media (max-width: 600px) {
319
+ .input-group { width: 100%; flex: auto; }
320
+ .search-wrapper { flex-direction: column; }
321
+ .primary-btn { width: 100%; justify-content: center; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
  </style>
324
  </head>
325
  <body>
 
326
 
327
+ <div class="app-container">
328
+
329
  <header>
330
  <div class="brand">
331
+ <span class="hf-emoji">πŸ€—</span>
332
+ <span>HuggingFace Tree</span>
 
 
 
 
333
  </div>
334
  <div class="header-actions">
335
+ <button id="themeToggle" title="Toggle Theme">
336
+ <i class="fas fa-moon"></i>
 
 
 
 
337
  </button>
338
+ <a href="https://huggingface.co" target="_blank" style="color:var(--text); font-size:1.2rem; padding:8px;">
339
+ <i class="fa-solid fa-arrow-up-right-from-square"></i>
340
+ </a>
341
  </div>
342
  </header>
343
 
344
+ <div class="search-wrapper">
345
+ <div class="input-group type-select">
346
+ <select id="repoType">
347
+ <option value="models">Model</option>
348
+ <option value="datasets">Dataset</option>
349
+ <option value="spaces">Space</option>
350
+ </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  </div>
352
  <div class="input-group main-input">
353
+ <input type="text" id="repoInput" placeholder="username/repo_name" autocomplete="off">
 
354
  </div>
355
  <div class="input-group branch-input">
356
+ <span style="color:var(--line-num); margin-right:5px; font-size:0.8rem;">Branch:</span>
357
+ <input type="text" id="branchInput" value="main">
358
  </div>
359
  <button id="fetchBtn" class="primary-btn">
360
+ <i class="fas fa-search"></i> Fetch Tree
361
  </button>
362
  </div>
363
 
364
+ <div id="statusMsg" class="status-msg"></div>
 
 
 
 
 
 
 
 
 
 
365
 
366
  <div id="emptyState" class="empty-state">
367
+ <i class="fas fa-network-wired"></i>
368
+ <h3>Visualize Hugging Face Structures</h3>
369
+ <p>Paste a repository URL or ID to see the file tree without cloning.</p>
370
+
371
+ <div class="examples">
372
+ <div class="chip" onclick="quickLoad('models', 'prithivMLmods/Qwen3-VL-4B-Instruct-Unredacted-MAX')">Model Example</div>
373
+ <div class="chip" onclick="quickLoad('spaces', 'prithivMLmods/Qwen3-VL-abliterated-MAX-Fast')">Space Example</div>
374
+ <div class="chip" onclick="quickLoad('datasets', 'prithivMLmods/LAP2-K-Think-v1.b')">Dataset Example</div>
 
 
 
 
 
 
 
 
 
 
 
 
375
  </div>
376
  </div>
377
 
378
+ <div id="treeContainer" class="tree-container">
379
  <div class="toolbar">
380
+ <div style="font-weight:600; font-size:0.9rem; display:flex; align-items:center; gap:5px;">
381
+ <i class="far fa-folder-open"></i> <span id="currentRepoLabel"></span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
  </div>
383
+ <div>
384
+ <button class="tool-btn" id="copyBtn">
 
 
 
 
385
  <i class="far fa-copy"></i> Copy Tree
386
  </button>
387
  </div>
388
  </div>
 
 
 
 
 
389
  <div class="terminal-window">
390
+ <div id="lineNumbers" class="line-numbers"></div>
391
+ <div id="treeContent" class="code-content"></div>
392
  </div>
393
  </div>
 
 
 
 
 
 
 
 
394
 
395
+ <div class="footer">
396
+ Generated client-side. Not affiliated with Hugging Face.
 
 
 
 
 
 
397
  </div>
398
  </div>
399
 
400
+ <script>
401
+ // --- State & Elements ---
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  const els = {
403
+ type: document.getElementById('repoType'),
404
  repo: document.getElementById('repoInput'),
405
  branch: document.getElementById('branchInput'),
406
+ btn: document.getElementById('fetchBtn'),
 
 
407
  status: document.getElementById('statusMsg'),
408
+ empty: document.getElementById('emptyState'),
409
+ container: document.getElementById('treeContainer'),
410
+ lines: document.getElementById('lineNumbers'),
411
+ content: document.getElementById('treeContent'),
412
+ label: document.getElementById('currentRepoLabel'),
413
+ copy: document.getElementById('copyBtn'),
414
+ theme: document.getElementById('themeToggle')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  };
416
 
417
+ let currentTreeData = null;
418
+ let rawTreeText = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
+ // --- Event Listeners ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  document.addEventListener('DOMContentLoaded', () => {
422
+ // Theme Init
423
+ const savedTheme = localStorage.getItem('hf_theme') || 'light';
424
+ document.documentElement.setAttribute('data-theme', savedTheme);
425
+ updateThemeIcon(savedTheme);
426
+
427
+ els.btn.addEventListener('click', fetchTree);
428
+ els.copy.addEventListener('click', copyFullTree);
429
+
430
+ els.theme.addEventListener('click', () => {
431
+ const current = document.documentElement.getAttribute('data-theme');
432
+ const next = current === 'dark' ? 'light' : 'dark';
433
+ document.documentElement.setAttribute('data-theme', next);
434
+ localStorage.setItem('hf_theme', next);
435
+ updateThemeIcon(next);
436
  });
 
 
437
 
438
+ // Input paste handler to auto-detect type
439
+ els.repo.addEventListener('input', (e) => {
440
+ const val = e.target.value;
441
+ if (val.includes('huggingface.co/')) {
442
+ parseUrl(val);
443
+ }
444
  });
445
 
446
+ // Enter key support
447
+ [els.repo, els.branch].forEach(el => {
448
+ el.addEventListener('keypress', (e) => {
449
+ if(e.key === 'Enter') fetchTree();
450
+ });
451
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  });
453
 
454
+ function updateThemeIcon(theme) {
455
+ const icon = els.theme.querySelector('i');
456
+ icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
 
 
 
 
 
 
 
 
 
 
 
 
457
  }
458
 
459
+ function quickLoad(type, repo) {
460
+ els.type.value = type;
461
+ els.repo.value = repo;
462
+ els.branch.value = 'main';
463
+ fetchTree();
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  }
465
 
466
+ // --- URL Parsing ---
467
+ function parseUrl(url) {
468
+ try {
469
+ // Remove https://huggingface.co/
470
+ let clean = url.replace(/https?:\/\/huggingface\.co\//, '');
471
+
472
+ if (clean.startsWith('datasets/')) {
473
+ els.type.value = 'datasets';
474
+ clean = clean.replace('datasets/', '');
475
+ } else if (clean.startsWith('spaces/')) {
476
+ els.type.value = 'spaces';
477
+ clean = clean.replace('spaces/', '');
478
+ } else {
479
+ els.type.value = 'models';
480
+ // Models often don't have a prefix, or sometimes 'models/' is implied
481
+ }
482
 
483
+ // Extract repoId (user/repo) and potentially tree/revision
484
+ // Logic: Take the first two segments as user/repo
485
+ const parts = clean.split('/');
486
+ if (parts.length >= 2) {
487
+ els.repo.value = `${parts[0]}/${parts[1]}`;
 
 
 
 
 
 
 
 
 
 
 
488
  }
489
+
490
+ // Simple check for branch in URL (very basic)
491
+ if (parts.includes('tree')) {
492
+ const idx = parts.indexOf('tree');
493
+ if (parts[idx + 1]) els.branch.value = parts[idx + 1];
494
+ }
495
+ } catch (e) {
496
+ console.error("URL Parse error", e);
497
  }
498
  }
499
 
500
+ // --- API Fetch Logic ---
501
+ async function fetchTree() {
502
  const repo = els.repo.value.trim();
503
+ const type = els.type.value; // models, datasets, spaces
504
  const branch = els.branch.value.trim() || 'main';
505
 
506
+ if (!repo || repo.split('/').length < 2) {
507
+ showStatus("Please enter a valid 'username/repo'", "error");
508
+ return;
509
+ }
 
 
510
 
511
+ // Reset UI
512
+ els.empty.style.display = 'none';
513
+ els.container.style.display = 'none';
514
+ showStatus(`Fetching ${type} file tree...`, "loading");
515
+ els.btn.disabled = true;
516
+ els.content.innerHTML = '';
517
+ els.lines.innerHTML = '';
518
 
519
  try {
520
+ // HF API Endpoint: /api/models|datasets|spaces/{repo}/tree/{revision}?recursive=true
521
+ const endpoint = `https://huggingface.co/api/${type}/${repo}/tree/${branch}?recursive=true`;
522
+
523
+ const response = await fetch(endpoint);
524
+
525
+ if (!response.ok) {
526
+ if (response.status === 404) throw new Error("Repository or Branch not found.");
527
+ if (response.status === 401) throw new Error("Repository is private or requires authentication.");
528
+ throw new Error(`API Error: ${response.status}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
  }
530
 
531
+ const data = await response.json();
532
+
533
+ // Build Tree Object
534
+ const hierarchy = buildHierarchy(data);
535
+
536
+ // Render
537
+ currentTreeData = hierarchy;
538
+ els.label.innerText = `${repo} (${branch})`;
539
+ renderTree(hierarchy, repo, type, branch);
540
+
541
+ els.container.style.display = 'flex';
542
+ showStatus("", ""); // Hide status
543
 
544
  } catch (err) {
545
+ showStatus(err.message, "error");
 
 
 
 
 
 
 
 
 
 
 
 
546
  els.empty.style.display = 'block';
547
  } finally {
548
+ els.btn.disabled = false;
549
  }
550
  }
551
 
552
+ // --- Tree Logic ---
553
+ function buildHierarchy(flatList) {
554
+ // flatList is array of { path, type, size, lfs, ... }
555
+ // Sort: Folders first, then Name
556
+ flatList.sort((a, b) => {
557
+ if (a.type === 'directory' && b.type !== 'directory') return -1;
558
+ if (a.type !== 'directory' && b.type === 'directory') return 1;
559
+ return a.path.localeCompare(b.path);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  });
561
 
562
+ // Convert to nested object
563
+ const root = { name: 'root', type: 'directory', children: [] };
564
+
565
+ // Map paths to nodes
566
+ // Since HF returns recursive list, we can just parse paths
567
+ // However, HF output usually contains ALL files.
568
+ // We need to reconstruct the tree structure.
569
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  const map = { '': root };
571
 
572
+ flatList.forEach(item => {
 
 
 
 
573
  const parts = item.path.split('/');
574
+ const fileName = parts.pop();
575
+ const dirPath = parts.join('/');
576
+
577
+ // Ensure parent directory exists (API usually guarantees this order but let's be safe)
578
+ if (!map[dirPath]) {
579
+ // If we hit a file whose parent isn't in map yet, we have to create parents (rare in recursive return)
580
+ // For simplicity, assume recursive=true returns parents or we attach to closest known.
581
+ // Better approach: Create node
582
+ }
583
+
584
+ const node = {
585
+ ...item,
586
+ name: fileName,
587
+ children: item.type === 'directory' ? [] : null
588
+ };
589
+
590
+ map[item.path] = node;
591
+
592
+ // Find parent
593
+ const parent = map[dirPath] || root;
594
+ if(parent.children) parent.children.push(node);
595
  });
596
 
597
+ // Re-sort children of every node: Directories first, then alphabetical
598
+ const sortNodes = (node) => {
599
+ if (node.children) {
600
+ node.children.sort((a, b) => {
601
+ if (a.type === 'directory' && b.type !== 'directory') return -1;
602
+ if (a.type !== 'directory' && b.type === 'directory') return 1;
603
+ return a.name.localeCompare(b.name);
 
 
 
 
 
 
 
 
 
 
604
  });
605
+ node.children.forEach(sortNodes);
606
  }
607
  };
608
+ sortNodes(root);
609
 
610
+ return root;
 
 
 
611
  }
612
 
613
+ // --- Rendering ---
614
+ function renderTree(root, repo, type, revision) {
615
+ const linesFragment = document.createDocumentFragment();
616
+ const numsFragment = document.createDocumentFragment();
617
+ let lineCount = 0;
618
+ let textBuffer = ""; // For copy functionality
 
 
 
 
 
 
 
 
 
 
619
 
620
+ // Recursive render function
621
+ const traverse = (node, prefix, isLast) => {
622
+ // Don't print root itself, only children
623
+ if (node.name !== 'root') {
624
+ lineCount++;
625
+
626
+ // 1. Visual Line Construction
627
+ const row = document.createElement('div');
628
+ row.className = 'tree-row';
629
+
630
+ // 2. Prefix (β”‚ β”œβ”€β”€ )
631
+ const connector = isLast ? "└── " : "β”œβ”€β”€ ";
632
+ const displayPrefix = prefix + connector;
633
+
634
+ // 3. Icon
635
+ const iconClass = node.type === 'directory' ? 'fas fa-folder' : getFileIcon(node.name);
636
+
637
+ // 4. Download Link Construction
638
+ // HF Resolve URL: https://huggingface.co/{repo}/resolve/{revision}/{path}
639
+ // Note: type 'spaces' usually works same way for file resolution
640
+ const fileUrl = `https://huggingface.co/${type === 'models' ? '' : type === 'spaces' ? 'spaces/' : 'datasets/'}${repo}/resolve/${revision}/${node.path}`;
641
+ const viewUrl = `https://huggingface.co/${type === 'models' ? '' : type === 'spaces' ? 'spaces/' : 'datasets/'}${repo}/blob/${revision}/${node.path}`;
642
+
643
+ row.innerHTML = `
644
+ <span class="t-prefix">${displayPrefix}</span>
645
+ <span class="t-icon"><i class="${iconClass}"></i></span>
646
+ <a href="${node.type === 'directory' ? '#' : viewUrl}" target="_blank" class="t-name ${node.type === 'directory' ? 'folder' : ''}">${node.name}</a>
647
+ ${node.type !== 'directory' ? `
648
+ <div class="actions">
649
+ <a href="${fileUrl}" title="Download Raw" download class="action-icon"><i class="fas fa-download"></i></a>
650
+ </div>
651
+ ` : ''}
652
+ `;
653
 
654
+ linesFragment.appendChild(row);
 
 
 
 
 
 
 
 
655
 
656
+ // 5. Line Number
657
+ const num = document.createElement('div');
658
+ num.textContent = lineCount;
659
+ numsFragment.appendChild(num);
 
 
 
660
 
661
+ // 6. Add to Text Buffer (ASCII Tree)
662
+ textBuffer += `${displayPrefix}${node.name}\n`;
663
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
 
665
+ // Recurse
666
+ if (node.children) {
667
+ // Calculate new prefix for children
668
+ const childPrefix = node.name === 'root' ? "" : prefix + (isLast ? " " : "β”‚ ");
669
+
670
+ node.children.forEach((child, index) => {
671
+ traverse(child, childPrefix, index === node.children.length - 1);
672
+ });
673
+ }
674
+ };
675
 
676
+ // Start Traversal
677
+ // We iterate root.children so we don't print a "." root node
678
+ if(root.children) {
679
+ root.children.forEach((child, i) => {
680
+ traverse(child, "", i === root.children.length - 1);
 
 
681
  });
682
+ }
 
 
 
683
 
684
+ els.lines.appendChild(numsFragment);
685
+ els.content.appendChild(linesFragment);
686
+ rawTreeText = textBuffer;
 
687
  }
688
 
689
+ // --- Helpers ---
690
+ function getFileIcon(filename) {
691
+ const ext = filename.split('.').pop().toLowerCase();
692
+ if (filename === 'README.md') return 'fas fa-book';
693
+ if (['jpg','png','jpeg','webp'].includes(ext)) return 'far fa-image';
694
+ if (['json','yaml','yml'].includes(ext)) return 'fas fa-code';
695
+ if (['py','js','html','css'].includes(ext)) return 'fab fa-python'; // Generic code
696
+ if (['bin','safetensors','pt','h5','gguf'].includes(ext)) return 'fas fa-box'; // Model weights
697
+ return 'far fa-file';
 
 
 
 
 
 
 
 
698
  }
699
 
700
+ function showStatus(msg, type) {
701
+ els.status.style.display = msg ? 'block' : 'none';
702
+ els.status.className = `status-msg ${type}`;
703
+ els.status.innerText = msg;
704
  }
705
 
706
+ function copyFullTree() {
707
+ if (!rawTreeText) return;
708
+ navigator.clipboard.writeText(rawTreeText).then(() => {
709
+ const originalHtml = els.copy.innerHTML;
710
+ els.copy.innerHTML = `<i class="fas fa-check"></i> Copied`;
711
+ setTimeout(() => els.copy.innerHTML = originalHtml, 2000);
 
 
712
  });
713
  }
714
+ </script>
 
 
 
 
 
715
  </body>
716
  </html>