prithivMLmods commited on
Commit
3858f23
Β·
verified Β·
1 Parent(s): 23a47bc

update app

Browse files
Files changed (1) hide show
  1. index.html +1343 -135
index.html CHANGED
@@ -3,175 +3,1383 @@
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 rel="stylesheet" href="index.css">
10
- <script type="importmap">
11
- {
12
- "imports": {
13
- "react": "https://esm.sh/react@19.0.0",
14
- "react-dom": "https://esm.sh/react-dom@19.0.0",
15
- "lucide-react": "https://esm.sh/lucide-react@0.474.0",
16
- "@google/genai": "https://esm.sh/@google/genai@0.2.1"
17
- }
18
- }
19
- </script>
20
- <link rel="stylesheet" href="/index.css">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  </head>
22
  <body>
23
- <div class="app-container">
24
- <header>
25
- <div class="brand">
26
- <a href="#">
27
- <div class="brand-icon"><i class="fas fa-tree"></i></div>
28
- <span>hf-treeπŸ€—</span>
29
- </a>
30
- </div>
31
- <div class="header-actions">
32
- <button id="privateRepoBtn" class="text-btn" title="Access Private Repos">
33
- <i class="fas fa-lock"></i> Token
34
- </button>
35
- <button id="themeToggle" class="icon-btn" aria-label="Toggle Theme">
36
- <i class="fas fa-sun sun-icon"></i>
37
- <i class="fas fa-moon moon-icon"></i>
38
  </button>
39
  </div>
40
- </header>
41
-
42
- <div id="tokenSection" class="token-panel" style="display: none;">
43
- <div class="token-inner">
44
- <i class="fas fa-key"></i>
45
- <input type="password" id="hfToken" placeholder="Paste HuggingFace Access Token (hf_...)">
46
- <button id="saveTokenBtn">Save</button>
47
- <button id="clearTokenBtn" style="display:none;">Clear</button>
 
48
  </div>
49
- <p class="token-warning">
50
- <i class="fas fa-exclamation-triangle"></i> Token is saved to your browser's <b>LocalStorage</b>. Do not use on public computers.
51
- </p>
52
  </div>
53
 
54
- <div class="search-section">
55
- <div class="type-selector">
56
- <button class="type-btn active-model" data-type="models" id="typeModel">
57
- <i class="fas fa-cube"></i> Model
58
- </button>
59
- <button class="type-btn" data-type="datasets" id="typeDataset">
60
- <i class="fas fa-database"></i> Dataset
61
- </button>
62
- <button class="type-btn" data-type="spaces" id="typeSpace">
63
- <i class="fas fa-rocket"></i> Space
 
 
 
 
 
 
 
 
 
64
  </button>
65
  </div>
66
- <div class="input-group main-input">
67
- <span class="prefix" id="urlPrefix">hf.co/</span>
68
- <input type="text" id="repoInput" placeholder="username/repo-name" autocomplete="off">
69
- </div>
70
- <div class="input-group branch-input">
71
- <span class="prefix">rev:</span>
72
- <input type="text" id="branchInput" placeholder="main" autocomplete="off">
73
- </div>
74
- <button id="fetchBtn" class="primary-btn">
75
- <i class="fas fa-search"></i> Fetch
76
- </button>
77
- </div>
78
 
79
- <div id="statusMsg" style="display: none;"></div>
80
 
81
- <div id="repoInfoCard" class="repo-info-card">
82
- <div class="repo-info-header">
83
- <div class="repo-info-left">
84
- <span class="repo-type-badge" id="repoTypeBadge">MODEL</span>
85
- <span class="repo-info-name" id="repoInfoName"></span>
 
 
 
 
 
 
 
 
 
 
 
 
86
  </div>
87
- <div class="repo-info-stats" id="repoInfoStats"></div>
88
  </div>
89
- </div>
90
 
91
- <div id="emptyState" class="empty-state">
92
- <div class="homepage-section">
93
- <h3 style="font-size: 1.3rem; text-transform: none; letter-spacing: -0.02em; color: var(--text);">
94
- Explore Hugging-face Repositories
95
- </h3>
96
- <p>Visualize the file tree of any Model, Dataset, or Space</p>
97
- </div>
98
- <div class="homepage-section">
99
- <h3>Featured</h3>
100
- <div id="featuredCloud" class="tag-cloud"></div>
101
- </div>
102
- <div class="homepage-section">
103
- <h3>Popular Models</h3>
104
- <div id="modelCloud" class="tag-cloud"></div>
105
- </div>
106
- <div class="homepage-section">
107
- <h3>Popular Datasets</h3>
108
- <div id="datasetCloud" class="tag-cloud"></div>
109
- </div>
110
- </div>
111
 
112
- <div id="treeWrapper" class="tree-wrapper" style="display: none;">
113
- <div class="toolbar">
114
- <div class="tools-group">
115
- <div class="dropdown">
116
- <button class="tool-btn" id="sortBtnLabel">
117
- <i class="fas fa-sort-amount-down"></i> Sort
118
- </button>
119
- <div class="dropdown-content">
120
- <a href="#" data-sort="folder-az">Folders First (A-Z)</a>
121
- <a href="#" data-sort="folder-za">Folders First (Z-A)</a>
122
- <a href="#" data-sort="alpha-az">Name (A-Z)</a>
123
- <a href="#" data-sort="alpha-za">Name (Z-A)</a>
124
- <a href="#" data-sort="size-desc">Size (Largest)</a>
125
- <a href="#" data-sort="size-asc">Size (Smallest)</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  </div>
127
  </div>
128
- <div class="dropdown">
129
- <button class="tool-btn" id="styleBtnLabel">
130
- <i class="fas fa-code-branch"></i> Style
131
- </button>
132
- <div class="dropdown-content">
133
- <a href="#" data-style="classic">Classic (└──)</a>
134
- <a href="#" data-style="slashed">Slashed (/src)</a>
135
- <a href="#" data-style="plus">ASCII (+--)</a>
136
- <a href="#" data-style="minimal">Minimal (Indent)</a>
137
  </div>
138
  </div>
139
- </div>
140
- <div class="tools-group right">
141
- <button id="shareBtn" class="tool-btn">
142
- <i class="fas fa-share-alt"></i> Share
143
- </button>
144
- <button id="copyAllBtn" class="tool-btn action-btn">
145
- <i class="far fa-copy"></i> Copy Tree
146
- </button>
147
  </div>
148
  </div>
149
- <div class="tree-search">
150
- <input type="text" id="treeFilter" placeholder="Filter files… (e.g. .safetensors, config, README)">
151
- </div>
152
- <div class="terminal-window">
153
- <div id="lineNumbers" class="line-col"></div>
154
- <div id="treeContent" class="code-col"></div>
155
- </div>
156
- </div>
157
 
158
- <div class="footer">
159
- Built by <a href="https://hf.co/prithivMLmods" target="_blank" rel="noopener">prithivMLmods</a>
 
160
  </div>
161
  </div>
162
 
163
- <div id="shareOverlay" class="overlay">
 
164
  <div class="overlay-content">
165
- <h3>Share View</h3>
 
166
  <div class="share-box">
167
  <input type="text" id="shareInput" readonly>
168
- <button id="copyShareBtn"><i class="far fa-copy"></i></button>
169
  </div>
170
  <button id="closeOverlay" class="close-btn">&times;</button>
171
  </div>
172
  </div>
173
 
174
- <script type="module" src="index.tsx"></script>
175
- <script type="module" src="/index.tsx"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  </body>
177
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>hftree-visualizer</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
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap');
11
+
12
+ * { box-sizing: border-box; margin: 0; padding: 0; }
13
+
14
+ body {
15
+ font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', 'Courier New', monospace;
16
+ background: #000000;
17
+ color: #ffffff;
18
+ -webkit-font-smoothing: antialiased;
19
+ min-height: 100vh;
20
+ }
21
+
22
+ button, input, select { font-family: inherit; }
23
+ a { text-decoration: none; color: inherit; }
24
+
25
+ ::selection { background: #ffffff; color: #000000; }
26
+
27
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
28
+ ::-webkit-scrollbar-thumb { background: #333333; border-radius: 3px; }
29
+ ::-webkit-scrollbar-thumb:hover { background: #555555; }
30
+ ::-webkit-scrollbar-track { background: #000000; }
31
+
32
+ .terminal-shell {
33
+ max-width: 1100px;
34
+ margin: 0 auto;
35
+ padding: 0;
36
+ min-height: 100vh;
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ /* ── Title Bar ── */
42
+ .title-bar {
43
+ display: flex;
44
+ align-items: center;
45
+ padding: 14px 20px;
46
+ border-bottom: 1px solid #222222;
47
+ gap: 12px;
48
+ user-select: none;
49
+ position: sticky;
50
+ top: 0;
51
+ background: #000000;
52
+ z-index: 100;
53
+ }
54
+
55
+ .title-dot {
56
+ width: 10px;
57
+ height: 10px;
58
+ border-radius: 50%;
59
+ background: #ffffff;
60
+ flex-shrink: 0;
61
+ box-shadow: 0 0 6px rgba(255,255,255,0.4);
62
+ }
63
+
64
+ .title-text {
65
+ font-size: 0.85rem;
66
+ font-weight: 600;
67
+ color: #ffffff;
68
+ letter-spacing: 0.02em;
69
+ }
70
+
71
+ .title-actions {
72
+ margin-left: auto;
73
+ display: flex;
74
+ gap: 8px;
75
+ align-items: center;
76
+ }
77
+
78
+ .title-btn {
79
+ background: transparent;
80
+ border: 1px solid #333333;
81
+ color: #888888;
82
+ padding: 5px 12px;
83
+ border-radius: 3px;
84
+ cursor: pointer;
85
+ font-size: 0.72rem;
86
+ font-weight: 500;
87
+ transition: all 0.15s;
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 6px;
91
+ }
92
+ .title-btn:hover { border-color: #ffffff; color: #ffffff; }
93
+ .title-btn.active-token { color: #ffffff; border-color: #555555; }
94
+
95
+ /* ── Token Section ── */
96
+ .token-section {
97
+ border-bottom: 1px solid #222222;
98
+ padding: 14px 20px;
99
+ animation: termSlide 0.2s ease-out;
100
+ }
101
+
102
+ @keyframes termSlide {
103
+ from { opacity: 0; max-height: 0; }
104
+ to { opacity: 1; max-height: 200px; }
105
+ }
106
+
107
+ .token-row {
108
+ display: flex;
109
+ gap: 10px;
110
+ align-items: center;
111
+ }
112
+
113
+ .token-prompt {
114
+ color: #888888;
115
+ font-size: 0.78rem;
116
+ flex-shrink: 0;
117
+ }
118
+
119
+ .token-row input {
120
+ background: #111111;
121
+ border: 1px solid #333333;
122
+ padding: 7px 12px;
123
+ border-radius: 3px;
124
+ flex: 1;
125
+ color: #ffffff;
126
+ font-size: 0.78rem;
127
+ }
128
+ .token-row input:focus { outline: none; border-color: #ffffff; }
129
+
130
+ .token-row button {
131
+ background: #ffffff;
132
+ color: #000000;
133
+ border: none;
134
+ padding: 7px 16px;
135
+ border-radius: 3px;
136
+ cursor: pointer;
137
+ font-weight: 600;
138
+ font-size: 0.78rem;
139
+ transition: opacity 0.15s;
140
+ }
141
+ .token-row button:hover { opacity: 0.85; }
142
+
143
+ .token-note {
144
+ font-size: 0.68rem;
145
+ color: #555555;
146
+ margin-top: 8px;
147
+ }
148
+
149
+ /* ── Main Content ── */
150
+ .term-body {
151
+ padding: 20px;
152
+ flex: 1;
153
+ display: flex;
154
+ flex-direction: column;
155
+ }
156
+
157
+ /* ── Input Line ── */
158
+ .input-line {
159
+ display: flex;
160
+ gap: 10px;
161
+ margin-bottom: 20px;
162
+ flex-wrap: wrap;
163
+ }
164
+
165
+ .type-selector {
166
+ display: flex;
167
+ border: 1px solid #222222;
168
+ border-radius: 3px;
169
+ overflow: hidden;
170
+ height: 40px;
171
+ }
172
+
173
+ .type-btn {
174
+ background: #0a0a0a;
175
+ border: none;
176
+ border-right: 1px solid #222222;
177
+ padding: 0 16px;
178
+ cursor: pointer;
179
+ color: #555555;
180
+ font-size: 0.72rem;
181
+ font-weight: 600;
182
+ transition: all 0.15s;
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 6px;
186
+ text-transform: capitalize;
187
+ }
188
+ .type-btn:last-child { border-right: none; }
189
+ .type-btn:hover { background: #111111; color: #aaaaaa; }
190
+ .type-btn.active {
191
+ background: #1a1a1a;
192
+ color: #ffffff;
193
+ }
194
+
195
+ .prompt-group {
196
+ display: flex;
197
+ align-items: center;
198
+ background: #0a0a0a;
199
+ border: 1px solid #222222;
200
+ border-radius: 3px;
201
+ padding: 0 14px;
202
+ height: 40px;
203
+ flex: 1;
204
+ transition: border-color 0.15s;
205
+ }
206
+ .prompt-group:focus-within { border-color: #ffffff; }
207
+
208
+ .prompt-symbol {
209
+ color: #555555;
210
+ font-size: 0.82rem;
211
+ margin-right: 8px;
212
+ white-space: nowrap;
213
+ font-weight: 500;
214
+ }
215
+
216
+ .prompt-group input {
217
+ background: transparent;
218
+ border: none;
219
+ color: #ffffff;
220
+ width: 100%;
221
+ outline: none;
222
+ font-size: 0.82rem;
223
+ font-weight: 400;
224
+ }
225
+ .prompt-group input::placeholder { color: #444444; }
226
+
227
+ .exec-btn {
228
+ background: #ffffff;
229
+ color: #000000;
230
+ border: none;
231
+ padding: 0 24px;
232
+ border-radius: 3px;
233
+ font-weight: 700;
234
+ cursor: pointer;
235
+ height: 40px;
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 8px;
239
+ font-size: 0.82rem;
240
+ transition: opacity 0.15s;
241
+ white-space: nowrap;
242
+ }
243
+ .exec-btn:hover { opacity: 0.85; }
244
+ .exec-btn:disabled { opacity: 0.4; cursor: wait; }
245
+
246
+ /* ── Status ── */
247
+ #statusMsg {
248
+ padding: 10px 14px;
249
+ margin-bottom: 14px;
250
+ font-size: 0.78rem;
251
+ border-radius: 3px;
252
+ display: none;
253
+ border: 1px solid #222222;
254
+ }
255
+ .error { color: #ffffff; background: #1a0000; border-color: #440000 !important; }
256
+ .loading { color: #ffffff; background: #0a0a0a; border-color: #333333 !important; }
257
+
258
+ /* ── Empty / Home State ── */
259
+ .empty-state {
260
+ text-align: center;
261
+ padding-top: 40px;
262
+ }
263
+
264
+ .empty-title {
265
+ font-size: 1.1rem;
266
+ font-weight: 600;
267
+ color: #ffffff;
268
+ margin-bottom: 8px;
269
+ }
270
+
271
+ .empty-desc {
272
+ color: #666666;
273
+ font-size: 0.8rem;
274
+ margin-bottom: 32px;
275
+ line-height: 1.6;
276
+ }
277
+
278
+ .section-label {
279
+ font-size: 0.7rem;
280
+ color: #555555;
281
+ text-transform: uppercase;
282
+ letter-spacing: 2px;
283
+ font-weight: 600;
284
+ margin-bottom: 14px;
285
+ }
286
+
287
+ .org-cloud {
288
+ display: flex;
289
+ justify-content: center;
290
+ gap: 8px;
291
+ flex-wrap: wrap;
292
+ max-width: 850px;
293
+ margin: 0 auto;
294
+ }
295
+
296
+ .homepage-section { margin-bottom: 32px; }
297
+
298
+ .org-chip {
299
+ background: transparent;
300
+ border: 1px solid #222222;
301
+ color: #888888;
302
+ padding: 6px 14px;
303
+ border-radius: 3px;
304
+ cursor: pointer;
305
+ font-size: 0.75rem;
306
+ font-weight: 500;
307
+ transition: all 0.15s;
308
+ }
309
+ .org-chip:hover {
310
+ border-color: #ffffff;
311
+ color: #ffffff;
312
+ }
313
+
314
+ /* ── Repo Info Card ── */
315
+ .profile-block {
316
+ border: 1px solid #222222;
317
+ border-radius: 3px;
318
+ padding: 20px;
319
+ margin-bottom: 16px;
320
+ animation: termSlide 0.25s ease-out;
321
+ }
322
+
323
+ .profile-top {
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 16px;
327
+ }
328
+
329
+ .profile-avatar {
330
+ width: 56px;
331
+ height: 56px;
332
+ border-radius: 4px;
333
+ border: 1px solid #333333;
334
+ display: flex;
335
+ align-items: center;
336
+ justify-content: center;
337
+ overflow: hidden;
338
+ flex-shrink: 0;
339
+ background: #111111;
340
+ }
341
+ .profile-avatar i { font-size: 1.4rem; color: #666666; }
342
+
343
+ .profile-info { flex: 1; }
344
+
345
+ .profile-name {
346
+ font-size: 1rem;
347
+ font-weight: 700;
348
+ color: #ffffff;
349
+ display: flex;
350
+ align-items: center;
351
+ gap: 10px;
352
+ flex-wrap: wrap;
353
+ }
354
+ .profile-name a { color: #ffffff; }
355
+ .profile-name a:hover { text-decoration: underline; }
356
+
357
+ .org-badge {
358
+ font-size: 0.6rem;
359
+ padding: 2px 8px;
360
+ border-radius: 2px;
361
+ font-weight: 700;
362
+ text-transform: uppercase;
363
+ letter-spacing: 0.5px;
364
+ background: #1a1a1a;
365
+ color: #888888;
366
+ border: 1px solid #333333;
367
+ }
368
+
369
+ .profile-fullname {
370
+ font-size: 0.78rem;
371
+ color: #666666;
372
+ margin-top: 2px;
373
+ }
374
+
375
+ .profile-members {
376
+ font-size: 0.72rem;
377
+ color: #555555;
378
+ display: flex;
379
+ align-items: center;
380
+ gap: 16px;
381
+ margin-top: 8px;
382
+ flex-wrap: wrap;
383
+ }
384
+
385
+ .social-stat {
386
+ display: flex;
387
+ align-items: center;
388
+ gap: 6px;
389
+ font-size: 0.75rem;
390
+ color: #666666;
391
+ font-weight: 500;
392
+ }
393
+ .social-stat i { font-size: 0.72rem; color: #888888; }
394
+ .social-stat .social-num { font-weight: 700; color: #ffffff; }
395
+
396
+ /* ── Tree Wrapper / Toolbar ── */
397
+ .tree-wrapper {
398
+ border: 1px solid #222222;
399
+ border-radius: 3px;
400
+ display: flex;
401
+ flex-direction: column;
402
+ background: #000000;
403
+ overflow: hidden;
404
+ }
405
+
406
+ .tree-header {
407
+ background: #0a0a0a;
408
+ padding: 10px 16px;
409
+ border-bottom: 1px solid #222222;
410
+ display: flex;
411
+ justify-content: space-between;
412
+ align-items: center;
413
+ flex-wrap: wrap;
414
+ gap: 8px;
415
+ }
416
+
417
+ .tools-group {
418
+ display: flex;
419
+ gap: 8px;
420
+ align-items: center;
421
+ }
422
+
423
+ .filter-pill {
424
+ background: transparent;
425
+ border: 1px solid #222222;
426
+ color: #666666;
427
+ padding: 5px 12px;
428
+ border-radius: 3px;
429
+ cursor: pointer;
430
+ font-size: 0.72rem;
431
+ font-weight: 600;
432
+ transition: all 0.15s;
433
+ white-space: nowrap;
434
+ display: flex;
435
+ align-items: center;
436
+ gap: 6px;
437
+ }
438
+ .filter-pill:hover { border-color: #555555; color: #aaaaaa; }
439
+
440
+ .dropdown { position: relative; display: inline-block; }
441
+ .dropdown-content {
442
+ display: none;
443
+ position: absolute;
444
+ background: #111111;
445
+ min-width: 180px;
446
+ box-shadow: 0 4px 12px rgba(0,0,0,0.5);
447
+ border: 1px solid #333333;
448
+ border-radius: 3px;
449
+ z-index: 100;
450
+ top: 100%;
451
+ left: 0;
452
+ margin-top: 4px;
453
+ padding: 4px 0;
454
+ }
455
+ .dropdown:hover .dropdown-content, .dropdown:focus-within .dropdown-content { display: block; }
456
+ .dropdown-content a {
457
+ color: #aaaaaa;
458
+ padding: 8px 14px;
459
+ text-decoration: none;
460
+ display: block;
461
+ font-size: 0.72rem;
462
+ font-weight: 500;
463
+ cursor: pointer;
464
+ }
465
+ .dropdown-content a:hover { background: #1a1a1a; color: #ffffff; }
466
+
467
+ .copy-btn {
468
+ background: #ffffff;
469
+ color: #000000;
470
+ border: none;
471
+ padding: 6px 14px;
472
+ border-radius: 3px;
473
+ font-size: 0.72rem;
474
+ font-weight: 700;
475
+ cursor: pointer;
476
+ transition: opacity 0.15s;
477
+ display: flex;
478
+ align-items: center;
479
+ gap: 6px;
480
+ }
481
+ .copy-btn:hover { opacity: 0.85; }
482
+
483
+ .filter-bar {
484
+ display: flex;
485
+ gap: 8px;
486
+ margin-bottom: 0;
487
+ border-bottom: 1px solid #1a1a1a;
488
+ padding: 10px 16px;
489
+ background: #050505;
490
+ }
491
+
492
+ .filter-search {
493
+ margin-left: 0;
494
+ background: #0a0a0a;
495
+ border: 1px solid #222222;
496
+ border-radius: 3px;
497
+ padding: 5px 12px;
498
+ display: flex;
499
+ align-items: center;
500
+ gap: 6px;
501
+ transition: border-color 0.15s;
502
+ width: 100%;
503
+ }
504
+ .filter-search:focus-within { border-color: #ffffff; }
505
+ .filter-search-icon { color: #555555; font-size: 0.72rem; }
506
+ .filter-search input {
507
+ font-size: 0.72rem;
508
+ width: 100%;
509
+ background: transparent;
510
+ border: none;
511
+ color: #ffffff;
512
+ outline: none;
513
+ }
514
+ .filter-search input::placeholder { color: #444444; }
515
+
516
+ /* ── Terminal Window ── */
517
+ .terminal-window {
518
+ background: #000000;
519
+ display: flex;
520
+ overflow: auto;
521
+ font-size: 12.5px;
522
+ line-height: 1.7;
523
+ max-height: 65vh;
524
+ }
525
+
526
+ .line-col {
527
+ padding: 14px 10px;
528
+ text-align: right;
529
+ color: #333333;
530
+ border-right: 1px solid #1a1a1a;
531
+ min-width: 44px;
532
+ user-select: none;
533
+ font-size: 0.7rem;
534
+ font-weight: 500;
535
+ }
536
+ .line-col div {
537
+ height: 30px;
538
+ display: flex;
539
+ align-items: center;
540
+ justify-content: flex-end;
541
+ }
542
+
543
+ .code-col {
544
+ padding: 14px;
545
+ flex: 1;
546
+ white-space: pre;
547
+ min-width: 0;
548
+ }
549
+
550
+ .tree-line {
551
+ display: flex;
552
+ align-items: center;
553
+ height: 30px;
554
+ border-radius: 2px;
555
+ padding: 0 6px;
556
+ gap: 2px;
557
+ }
558
+ .tree-line:hover { background: #0d0d0d; }
559
+
560
+ .t-prefix {
561
+ color: #444444;
562
+ white-space: pre;
563
+ margin-right: 4px;
564
+ flex-shrink: 0;
565
+ }
566
+
567
+ .t-name {
568
+ color: #ffffff;
569
+ font-weight: 600;
570
+ white-space: nowrap;
571
+ overflow: hidden;
572
+ text-overflow: ellipsis;
573
+ min-width: 0;
574
+ display: flex;
575
+ align-items: center;
576
+ gap: 8px;
577
+ }
578
+ .t-name i { color: #888888; font-size: 0.85rem; }
579
+ .t-name a { color: #ffffff; transition: color 0.1s; }
580
+ .t-name a:hover { color: #cccccc; text-decoration: underline; }
581
+
582
+ .t-meta {
583
+ margin-left: auto;
584
+ display: flex;
585
+ align-items: center;
586
+ gap: 12px;
587
+ flex-shrink: 0;
588
+ padding-left: 12px;
589
+ }
590
+
591
+ .t-stat {
592
+ font-size: 0.65rem;
593
+ color: #555555;
594
+ display: flex;
595
+ align-items: center;
596
+ gap: 4px;
597
+ font-weight: 500;
598
+ white-space: nowrap;
599
+ }
600
+
601
+ .no-data {
602
+ padding: 40px 20px;
603
+ text-align: center;
604
+ color: #555555;
605
+ font-size: 0.82rem;
606
+ }
607
+
608
+ /* ── Footer ── */
609
+ .footer {
610
+ padding: 30px 20px;
611
+ text-align: center;
612
+ font-size: 0.68rem;
613
+ color: #333333;
614
+ margin-top: auto;
615
+ border-top: 1px solid #111111;
616
+ }
617
+ .footer a { color: #888888; font-weight: 600; }
618
+ .footer a:hover { color: #ffffff; }
619
+
620
+ /* ── Spinner ── */
621
+ .spinner {
622
+ display: inline-block;
623
+ width: 12px;
624
+ height: 12px;
625
+ border: 2px solid #333333;
626
+ border-top-color: #ffffff;
627
+ border-radius: 50%;
628
+ animation: spin 0.6s linear infinite;
629
+ margin-right: 8px;
630
+ vertical-align: middle;
631
+ }
632
+
633
+ @keyframes spin { to { transform: rotate(360deg); } }
634
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
635
+
636
+ .hidden { display: none !important; }
637
+
638
+ /* ── Overlay ── */
639
+ .overlay {
640
+ position: fixed;
641
+ top: 0; left: 0; right: 0; bottom: 0;
642
+ background: rgba(0,0,0,0.85);
643
+ display: flex;
644
+ align-items: center;
645
+ justify-content: center;
646
+ z-index: 1000;
647
+ }
648
+ .overlay-content {
649
+ background: #000000;
650
+ border: 1px solid #333333;
651
+ border-radius: 4px;
652
+ padding: 24px;
653
+ width: 100%;
654
+ max-width: 500px;
655
+ position: relative;
656
+ margin: 20px;
657
+ }
658
+ .overlay-content h3 {
659
+ font-size: 1.1rem;
660
+ font-weight: 600;
661
+ color: #ffffff;
662
+ margin-bottom: 8px;
663
+ }
664
+ .overlay-desc {
665
+ font-size: 0.8rem;
666
+ color: #666;
667
+ margin-bottom: 16px;
668
+ }
669
+ .share-box {
670
+ display: flex;
671
+ gap: 10px;
672
+ }
673
+ .share-box input {
674
+ flex: 1;
675
+ background: #111111;
676
+ border: 1px solid #333333;
677
+ padding: 8px 12px;
678
+ border-radius: 3px;
679
+ color: #ffffff;
680
+ font-family: inherit;
681
+ font-size: 0.8rem;
682
+ outline: none;
683
+ }
684
+ .share-box input:focus { border-color: #ffffff; }
685
+ .close-btn {
686
+ position: absolute;
687
+ top: 12px; right: 12px;
688
+ background: transparent;
689
+ border: none;
690
+ color: #888;
691
+ font-size: 1.5rem;
692
+ cursor: pointer;
693
+ line-height: 1;
694
+ }
695
+ .close-btn:hover { color: #fff; }
696
+
697
+ /* ── Responsive ── */
698
+ @media (max-width: 640px) {
699
+ .input-line { flex-direction: column; }
700
+ .type-selector { width: 100%; }
701
+ .type-btn { flex: 1; justify-content: center; }
702
+ .exec-btn { width: 100%; justify-content: center; }
703
+ .profile-top { flex-direction: column; text-align: center; }
704
+ .profile-name { justify-content: center; }
705
+ .profile-members { justify-content: center; }
706
+ .tree-header { flex-direction: column; align-items: stretch; }
707
+ .tools-group { justify-content: space-between; }
708
+ .terminal-window { max-height: 50vh; }
709
+ }
710
+ </style>
711
  </head>
712
  <body>
713
+ <div class="terminal-shell">
714
+ <!-- Title Bar -->
715
+ <div class="title-bar">
716
+ <div class="title-dot"></div>
717
+ <span class="title-text">[hftree-visualizer]</span>
718
+ <div class="title-actions">
719
+ <button id="privateRepoBtn" class="title-btn" title="Access Private Repos">
720
+ <i class="fas fa-lock"></i> token
 
 
 
 
 
 
 
721
  </button>
722
  </div>
723
+ </div>
724
+
725
+ <!-- Token Panel -->
726
+ <div id="tokenSection" class="token-section hidden">
727
+ <div class="token-row">
728
+ <span class="token-prompt">token ~$</span>
729
+ <input type="password" id="hfToken" placeholder="paste hf access token (hf_...)">
730
+ <button id="saveTokenBtn">save</button>
731
+ <button id="clearTokenBtn" class="hidden">clear</button>
732
  </div>
733
+ <p class="token-note">token stored in localStorage. required for private repos.</p>
 
 
734
  </div>
735
 
736
+ <!-- Main Body -->
737
+ <div class="term-body">
738
+ <!-- Input -->
739
+ <div class="input-line">
740
+ <div class="type-selector">
741
+ <button class="type-btn active" data-type="models"><i class="fas fa-cube"></i> model</button>
742
+ <button class="type-btn" data-type="datasets"><i class="fas fa-database"></i> dataset</button>
743
+ <button class="type-btn" data-type="spaces"><i class="fas fa-rocket"></i> space</button>
744
+ </div>
745
+ <div class="prompt-group" style="flex:2;">
746
+ <span class="prompt-symbol">~$&nbsp;fetch&nbsp;</span>
747
+ <input type="text" id="repoInput" placeholder="username/repo-name" autocomplete="off">
748
+ </div>
749
+ <div class="prompt-group" style="flex:1; max-width: 200px;">
750
+ <span class="prompt-symbol">rev:</span>
751
+ <input type="text" id="branchInput" placeholder="main" autocomplete="off">
752
+ </div>
753
+ <button id="fetchBtn" class="exec-btn">
754
+ <i class="fas fa-terminal"></i> execute
755
  </button>
756
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
757
 
758
+ <div id="statusMsg"></div>
759
 
760
+ <!-- Empty State -->
761
+ <div id="emptyState" class="empty-state">
762
+ <div class="homepage-section">
763
+ <div class="empty-title">hugging-face repo structure visualizer</div>
764
+ <div class="empty-desc">effortlessly explore and visualize the file structure of any model, dataset, or space online without cloning.</div>
765
+ </div>
766
+ <div class="homepage-section">
767
+ <div class="section-label">featured</div>
768
+ <div id="featuredCloud" class="org-cloud"></div>
769
+ </div>
770
+ <div class="homepage-section">
771
+ <div class="section-label">popular models</div>
772
+ <div id="modelCloud" class="org-cloud"></div>
773
+ </div>
774
+ <div class="homepage-section">
775
+ <div class="section-label">popular datasets</div>
776
+ <div id="datasetCloud" class="org-cloud"></div>
777
  </div>
 
778
  </div>
 
779
 
780
+ <!-- Results -->
781
+ <div id="resultsSection" class="hidden">
782
+ <!-- Repo Info -->
783
+ <div id="repoInfoCard" class="profile-block">
784
+ <div class="profile-top">
785
+ <div class="profile-avatar">
786
+ <i class="fas fa-folder-tree"></i>
787
+ </div>
788
+ <div class="profile-info">
789
+ <div class="profile-name">
790
+ <a id="repoLink" href="#" target="_blank" rel="noopener">
791
+ <span id="repoName"></span>
792
+ </a>
793
+ <span class="org-badge" id="repoTypeBadge">MODEL</span>
794
+ </div>
795
+ <div class="profile-fullname" id="repoBranch"></div>
796
+ <div class="profile-members" id="repoStats"></div>
797
+ </div>
798
+ </div>
799
+ </div>
800
 
801
+ <!-- Tree -->
802
+ <div class="tree-wrapper" id="treeWrapper">
803
+ <div class="tree-header">
804
+ <div class="tools-group">
805
+ <div class="dropdown">
806
+ <button class="filter-pill" id="sortBtnLabel">
807
+ <i class="fas fa-sort-amount-down"></i> sort: <span id="sortLabel">default</span>
808
+ </button>
809
+ <div class="dropdown-content" id="sortDropdown">
810
+ <a href="#" data-sort="default">Default</a>
811
+ <a href="#" data-sort="folder-az">Folders First (A-Z)</a>
812
+ <a href="#" data-sort="folder-za">Folders First (Z-A)</a>
813
+ <a href="#" data-sort="alpha-az">Name (A-Z)</a>
814
+ <a href="#" data-sort="alpha-za">Name (Z-A)</a>
815
+ <a href="#" data-sort="size-desc">Size (Largest)</a>
816
+ <a href="#" data-sort="size-asc">Size (Smallest)</a>
817
+ </div>
818
+ </div>
819
+ <div class="dropdown">
820
+ <button class="filter-pill" id="styleBtnLabel">
821
+ <i class="fas fa-code-branch"></i> style: <span id="styleLabel">classic</span>
822
+ </button>
823
+ <div class="dropdown-content" id="styleDropdown">
824
+ <a href="#" data-style="classic">Classic (└──)</a>
825
+ <a href="#" data-style="slashed">Slashed (/src)</a>
826
+ <a href="#" data-style="plus">ASCII (+--)</a>
827
+ <a href="#" data-style="minimal">Minimal (Indent)</a>
828
+ </div>
829
+ </div>
830
+ </div>
831
+ <div class="tools-group" style="gap: 8px;">
832
+ <button id="shareBtn" class="filter-pill">
833
+ <i class="fas fa-share-alt"></i> share
834
+ </button>
835
+ <button id="copyAllBtn" class="copy-btn">
836
+ <i class="far fa-copy"></i> copy tree
837
+ </button>
838
  </div>
839
  </div>
840
+ <div class="filter-bar">
841
+ <div class="filter-search">
842
+ <span class="filter-search-icon"><i class="fas fa-search"></i></span>
843
+ <input type="text" id="treeFilter" placeholder="filter files... (e.g. .safetensors, config)">
 
 
 
 
 
844
  </div>
845
  </div>
846
+ <div class="terminal-window">
847
+ <div class="line-col" id="lineNumbers"></div>
848
+ <div class="code-col" id="treeContent"></div>
849
+ </div>
 
 
 
 
850
  </div>
851
  </div>
 
 
 
 
 
 
 
 
852
 
853
+ <div class="footer">
854
+ built by <a href="https://hf.co/prithivMLmods" target="_blank" rel="noopener">prithivMLmods</a>
855
+ </div>
856
  </div>
857
  </div>
858
 
859
+ <!-- Share Overlay -->
860
+ <div id="shareOverlay" class="overlay hidden">
861
  <div class="overlay-content">
862
+ <h3>share view</h3>
863
+ <p class="overlay-desc">copy the link below to share this exact repository view.</p>
864
  <div class="share-box">
865
  <input type="text" id="shareInput" readonly>
866
+ <button id="copyShareBtn" class="copy-btn"><i class="far fa-copy"></i></button>
867
  </div>
868
  <button id="closeOverlay" class="close-btn">&times;</button>
869
  </div>
870
  </div>
871
 
872
+ <script>
873
+ const HF_API = 'https://huggingface.co/api';
874
+ const HF_BASE = 'https://huggingface.co';
875
+
876
+ let state = {
877
+ type: 'models',
878
+ repoId: '',
879
+ branch: 'main',
880
+ treeData: [],
881
+ info: null,
882
+ sort: 'default',
883
+ style: 'classic',
884
+ filterText: ''
885
+ };
886
+
887
+ const $ = id => document.getElementById(id);
888
+
889
+ const els = {
890
+ repoInput: $('repoInput'),
891
+ branchInput: $('branchInput'),
892
+ fetchBtn: $('fetchBtn'),
893
+ statusMsg: $('statusMsg'),
894
+ emptyState: $('emptyState'),
895
+ resultsSection: $('resultsSection'),
896
+ repoLink: $('repoLink'),
897
+ repoName: $('repoName'),
898
+ repoTypeBadge: $('repoTypeBadge'),
899
+ repoBranch: $('repoBranch'),
900
+ repoStats: $('repoStats'),
901
+ lineNumbers: $('lineNumbers'),
902
+ treeContent: $('treeContent'),
903
+ treeFilter: $('treeFilter'),
904
+ sortLabel: $('sortLabel'),
905
+ styleLabel: $('styleLabel'),
906
+ copyAllBtn: $('copyAllBtn'),
907
+ shareBtn: $('shareBtn'),
908
+ tokenSection: $('tokenSection'),
909
+ hfToken: $('hfToken'),
910
+ saveTokenBtn: $('saveTokenBtn'),
911
+ clearTokenBtn: $('clearTokenBtn'),
912
+ privateRepoBtn: $('privateRepoBtn'),
913
+ shareOverlay: $('shareOverlay'),
914
+ shareInput: $('shareInput'),
915
+ copyShareBtn: $('copyShareBtn'),
916
+ closeOverlay: $('closeOverlay')
917
+ };
918
+
919
+ const FEATURED = ['prithivMLmods/Pegadoc', 'openai/whisper-large-v3', 'meta-llama/Llama-3.2-1B'];
920
+ const POPULAR_MODELS = ['black-forest-labs/FLUX.1-dev', 'stabilityai/stable-diffusion-3.5-large', 'deepseek-ai/DeepSeek-R1', 'Qwen/Qwen2.5-72B-Instruct', 'google-bert/bert-base-uncased', 'sentence-transformers/all-MiniLM-L6-v2'];
921
+ const POPULAR_DATASETS = ['HuggingFaceFW/fineweb', 'wikimedia/wikipedia', 'allenai/dolma', 'open-web-math/open-web-math', 'databricks/databricks-dolly-15k'];
922
+
923
+ function getHeaders() {
924
+ const t = localStorage.getItem('hf_token');
925
+ const h = { Accept: 'application/json' };
926
+ if (t) h['Authorization'] = `Bearer ${t}`;
927
+ return h;
928
+ }
929
+
930
+ function formatNum(n) {
931
+ if (n == null) return 'β€”';
932
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
933
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
934
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
935
+ return n.toLocaleString();
936
+ }
937
+
938
+ function formatBytes(bytes) {
939
+ if (bytes == null || bytes === 0) return '';
940
+ const k = 1024;
941
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
942
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
943
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
944
+ }
945
+
946
+ function relativeTime(d) {
947
+ if (!d) return '';
948
+ const diff = Date.now() - new Date(d).getTime();
949
+ const m = Math.floor(diff / 60000);
950
+ if (m < 1) return 'just now';
951
+ if (m < 60) return m + 'm ago';
952
+ const h = Math.floor(m / 60);
953
+ if (h < 24) return h + 'h ago';
954
+ const dy = Math.floor(h / 24);
955
+ if (dy < 7) return dy + 'd ago';
956
+ const w = Math.floor(dy / 7);
957
+ if (w < 5) return w + 'w ago';
958
+ const mo = Math.floor(dy / 30);
959
+ if (mo < 12) return mo + 'mo ago';
960
+ return Math.floor(dy / 365) + 'y ago';
961
+ }
962
+
963
+ function escapeHtml(s) {
964
+ const d = document.createElement('div');
965
+ d.textContent = s;
966
+ return d.innerHTML;
967
+ }
968
+
969
+ function showMsg(text, type) {
970
+ els.statusMsg.style.display = text ? 'block' : 'none';
971
+ els.statusMsg.innerHTML = text;
972
+ els.statusMsg.className = type || '';
973
+ }
974
+
975
+ function checkToken() {
976
+ const t = localStorage.getItem('hf_token');
977
+ if (t) {
978
+ els.hfToken.value = t;
979
+ els.privateRepoBtn.innerHTML = '<i class="fas fa-lock-open"></i> active';
980
+ els.privateRepoBtn.classList.add('active-token');
981
+ els.saveTokenBtn.classList.add('hidden');
982
+ els.clearTokenBtn.classList.remove('hidden');
983
+ } else {
984
+ els.privateRepoBtn.innerHTML = '<i class="fas fa-lock"></i> token';
985
+ els.privateRepoBtn.classList.remove('active-token');
986
+ els.saveTokenBtn.classList.remove('hidden');
987
+ els.clearTokenBtn.classList.add('hidden');
988
+ }
989
+ }
990
+
991
+ async function fetchRepoInfo() {
992
+ const headers = getHeaders();
993
+ try {
994
+ const resp = await fetch(`${HF_API}/${state.type}/${encodeURIComponent(state.repoId)}`, { headers });
995
+ if (resp.ok) return await resp.json();
996
+ } catch (e) {}
997
+ return null;
998
+ }
999
+
1000
+ async function fetchTree() {
1001
+ state.repoId = els.repoInput.value.trim().replace(/^@/, '').replace(/\/$/, '');
1002
+ state.branch = els.branchInput.value.trim() || 'main';
1003
+
1004
+ if (!state.repoId) return showMsg("error: please enter a repository name.", "error");
1005
+
1006
+ els.emptyState.classList.add('hidden');
1007
+ els.resultsSection.classList.add('hidden');
1008
+ els.fetchBtn.disabled = true;
1009
+ showMsg(`<span class="spinner"></span> fetching tree for ${escapeHtml(state.repoId)}...`, 'loading');
1010
+
1011
+ try {
1012
+ const headers = getHeaders();
1013
+ const uEnc = encodeURIComponent(state.repoId);
1014
+ const bEnc = encodeURIComponent(state.branch);
1015
+
1016
+ const [treeResp, infoData] = await Promise.allSettled([
1017
+ fetch(`${HF_API}/${state.type}/${uEnc}/tree/${bEnc}?recursive=true`, { headers }),
1018
+ fetchRepoInfo()
1019
+ ]);
1020
+
1021
+ if (treeResp.status !== 'fulfilled' || !treeResp.value.ok) {
1022
+ throw new Error(`HTTP ${treeResp.value?.status || 'Network Error'}`);
1023
+ }
1024
+
1025
+ const rawTree = await treeResp.value.json();
1026
+ state.treeData = rawTree;
1027
+ state.info = infoData.status === 'fulfilled' ? infoData.value : null;
1028
+
1029
+ if (state.treeData.length === 0) {
1030
+ showMsg(`error: tree is empty or not found for "${escapeHtml(state.repoId)}".`, 'error');
1031
+ els.emptyState.classList.remove('hidden');
1032
+ return;
1033
+ }
1034
+
1035
+ showMsg('', '');
1036
+ window.location.hash = `type=${state.type}&repo=${state.repoId}&branch=${state.branch}`;
1037
+ document.title = `${state.repoId} β€” hftree-visualizer`;
1038
+
1039
+ renderRepoInfo();
1040
+ renderTree();
1041
+ els.resultsSection.classList.remove('hidden');
1042
+
1043
+ } catch (err) {
1044
+ console.error(err);
1045
+ showMsg(`error: ${err.message}`, 'error');
1046
+ els.emptyState.classList.remove('hidden');
1047
+ } finally {
1048
+ els.fetchBtn.disabled = false;
1049
+ }
1050
+ }
1051
+
1052
+ function renderRepoInfo() {
1053
+ els.repoName.textContent = state.repoId;
1054
+ els.repoLink.href = `${HF_BASE}/${state.type}/${state.repoId}`;
1055
+ els.repoTypeBadge.textContent = state.type.slice(0, -1).toUpperCase(); // MODEL, DATASET, SPACE
1056
+ els.repoBranch.textContent = `branch: ${state.branch}`;
1057
+
1058
+ const info = state.info;
1059
+ let statsHtml = '';
1060
+ if (info) {
1061
+ if (info.downloads != null) statsHtml += `<span class="social-stat"><i class="fas fa-download"></i> <span class="social-num">${formatNum(info.downloads)}</span> downloads</span>`;
1062
+ if (info.likes != null) statsHtml += `<span class="social-stat"><i class="fas fa-heart"></i> <span class="social-num">${formatNum(info.likes)}</span> likes</span>`;
1063
+ if (info.lastModified) statsHtml += `<span class="social-stat"><i class="far fa-clock"></i> updated ${relativeTime(info.lastModified)}</span>`;
1064
+ }
1065
+ els.repoStats.innerHTML = statsHtml;
1066
+ }
1067
+
1068
+ function buildTreeStructure(paths) {
1069
+ const root = { name: '', children: {}, type: 'dir', path: '' };
1070
+ paths.forEach(item => {
1071
+ const parts = item.path.split('/');
1072
+ let curr = root;
1073
+ parts.forEach((part, i) => {
1074
+ if (!curr.children[part]) {
1075
+ const isLast = i === parts.length - 1;
1076
+ curr.children[part] = {
1077
+ name: part,
1078
+ children: {},
1079
+ type: isLast ? item.type : 'dir',
1080
+ size: isLast ? item.size : 0,
1081
+ path: parts.slice(0, i + 1).join('/')
1082
+ };
1083
+ }
1084
+ curr = curr.children[part];
1085
+ });
1086
+ });
1087
+ return root;
1088
+ }
1089
+
1090
+ function sortTree(node) {
1091
+ const children = Object.values(node.children);
1092
+ if (children.length === 0) return;
1093
+
1094
+ children.forEach(sortTree);
1095
+
1096
+ switch (state.sort) {
1097
+ case 'folder-az':
1098
+ children.sort((a, b) => (a.type === 'dir' ? 0 : 1) - (b.type === 'dir' ? 0 : 1) || a.name.localeCompare(b.name));
1099
+ break;
1100
+ case 'folder-za':
1101
+ children.sort((a, b) => (a.type === 'dir' ? 0 : 1) - (b.type === 'dir' ? 0 : 1) || b.name.localeCompare(a.name));
1102
+ break;
1103
+ case 'alpha-az':
1104
+ children.sort((a, b) => a.name.localeCompare(b.name));
1105
+ break;
1106
+ case 'alpha-za':
1107
+ children.sort((a, b) => b.name.localeCompare(a.name));
1108
+ break;
1109
+ case 'size-desc':
1110
+ children.sort((a, b) => (b.size || 0) - (a.size || 0));
1111
+ break;
1112
+ case 'size-asc':
1113
+ children.sort((a, b) => (a.size || 0) - (b.size || 0));
1114
+ break;
1115
+ default:
1116
+ children.sort((a, b) => a.name.localeCompare(b.name));
1117
+ break;
1118
+ }
1119
+ node.children = children;
1120
+ }
1121
+
1122
+ function filterTree(node) {
1123
+ if (!state.filterText) return true;
1124
+
1125
+ let matchFound = false;
1126
+ const newChildren = {};
1127
+
1128
+ Object.values(node.children).forEach(child => {
1129
+ const childMatch = child.name.toLowerCase().includes(state.filterText);
1130
+ const deepMatch = filterTree(child);
1131
+ if (childMatch || deepMatch) {
1132
+ newChildren[child.name] = child;
1133
+ matchFound = true;
1134
+ }
1135
+ });
1136
+
1137
+ node.children = newChildren;
1138
+ return matchFound;
1139
+ }
1140
+
1141
+ function renderTree() {
1142
+ const treeRoot = buildTreeStructure(state.treeData);
1143
+ filterTree(treeRoot);
1144
+ sortTree(treeRoot);
1145
+
1146
+ els.lineNumbers.innerHTML = '';
1147
+ els.treeContent.innerHTML = '';
1148
+
1149
+ const fragL = document.createDocumentFragment();
1150
+ const fragT = document.createDocumentFragment();
1151
+ let lineCount = 0;
1152
+
1153
+ function traverse(node, prefix, isLast, depth) {
1154
+ if (depth > 0) {
1155
+ lineCount++;
1156
+ const lN = document.createElement('div');
1157
+ lN.textContent = lineCount;
1158
+ fragL.appendChild(lN);
1159
+
1160
+ const row = document.createElement('div');
1161
+ row.className = 'tree-line';
1162
+ row.style.animation = `fadeIn 0.12s forwards ${Math.min(lineCount * 4, 600)}ms`;
1163
+ row.style.opacity = '0';
1164
+
1165
+ let connector = '';
1166
+ let linePrefix = '';
1167
+
1168
+ if (state.style === 'classic') {
1169
+ connector = isLast ? '└── ' : 'β”œβ”€β”€ ';
1170
+ linePrefix = prefix;
1171
+ } else if (state.style === 'plus') {
1172
+ connector = isLast ? '`-- ' : '+-- ';
1173
+ linePrefix = prefix;
1174
+ } else if (state.style === 'slashed') {
1175
+ connector = '/' + node.path;
1176
+ linePrefix = '';
1177
+ } else if (state.style === 'minimal') {
1178
+ connector = '';
1179
+ linePrefix = ' '.repeat(depth - 1);
1180
+ }
1181
+
1182
+ const name = node.name;
1183
+ let nameHtml = escapeHtml(name);
1184
+ if (state.filterText && name.toLowerCase().includes(state.filterText)) {
1185
+ const rx = new RegExp(`(${state.filterText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
1186
+ nameHtml = name.replace(rx, '<mark style="background:#333333;color:#ffffff;padding:0 2px;border-radius:2px;">$1</mark>');
1187
+ }
1188
+
1189
+ const isDir = node.type === 'directory' || node.type === 'dir';
1190
+ const icon = isDir ? '<i class="far fa-folder"></i>' : '<i class="far fa-file"></i>';
1191
+ const meta = isDir ? '' : `<span class="t-stat">${formatBytes(node.size)}</span>`;
1192
+ const url = `${HF_BASE}/${state.type}/${state.repoId}/resolve/${state.branch}/${node.path}`;
1193
+
1194
+ row.innerHTML = `
1195
+ <span class="t-prefix">${escapeHtml(linePrefix + connector)}</span>
1196
+ <span class="t-name">${icon}<a href="${url}" target="_blank" rel="noopener" title="${escapeHtml(node.path)}">${nameHtml}</a></span>
1197
+ <span class="t-meta">${meta}</span>`;
1198
+ fragT.appendChild(row);
1199
+ }
1200
+
1201
+ let childPrefix = '';
1202
+ if (state.style === 'classic') childPrefix = prefix + (isLast ? ' ' : 'β”‚ ');
1203
+ else if (state.style === 'plus') childPrefix = prefix + (isLast ? ' ' : '| ');
1204
+ else if (state.style === 'minimal') childPrefix = ' '.repeat(depth);
1205
+
1206
+ const children = Object.values(node.children);
1207
+ children.forEach((child, idx) => {
1208
+ traverse(child, childPrefix, idx === children.length - 1, depth + 1);
1209
+ });
1210
+ }
1211
+
1212
+ const rootChildren = Object.values(treeRoot.children);
1213
+ rootChildren.forEach((child, idx) => {
1214
+ traverse(child, '', idx === rootChildren.length - 1, 1);
1215
+ });
1216
+
1217
+ els.lineNumbers.appendChild(fragL);
1218
+ els.treeContent.appendChild(fragT);
1219
+ }
1220
+
1221
+ function getTreePlainText() {
1222
+ const treeRoot = buildTreeStructure(state.treeData);
1223
+ filterTree(treeRoot);
1224
+ sortTree(treeRoot);
1225
+ const lines = [];
1226
+
1227
+ function traverse(node, prefix, isLast, depth) {
1228
+ if (depth > 0) {
1229
+ let connector = '';
1230
+ let linePrefix = '';
1231
+ if (state.style === 'classic') { connector = isLast ? '└── ' : 'β”œβ”€β”€ '; linePrefix = prefix; }
1232
+ else if (state.style === 'plus') { connector = isLast ? '`-- ' : '+-- '; linePrefix = prefix; }
1233
+ else if (state.style === 'slashed') { connector = '/' + node.path; linePrefix = ''; }
1234
+ else if (state.style === 'minimal') { connector = ''; linePrefix = ' '.repeat(depth - 1); }
1235
+ lines.push(linePrefix + connector + node.name);
1236
+ }
1237
+ let childPrefix = '';
1238
+ if (state.style === 'classic') childPrefix = prefix + (isLast ? ' ' : 'β”‚ ');
1239
+ else if (state.style === 'plus') childPrefix = prefix + (isLast ? ' ' : '| ');
1240
+ else if (state.style === 'minimal') childPrefix = ' '.repeat(depth);
1241
+
1242
+ const children = Object.values(node.children);
1243
+ children.forEach((child, idx) => traverse(child, childPrefix, idx === children.length - 1, depth + 1));
1244
+ }
1245
+
1246
+ const rootChildren = Object.values(treeRoot.children);
1247
+ rootChildren.forEach((child, idx) => traverse(child, '', idx === rootChildren.length - 1, 1));
1248
+ return lines.join('\n');
1249
+ }
1250
+
1251
+ function buildHomepage() {
1252
+ const createChip = (id, arr, type) => {
1253
+ const c = $(id);
1254
+ arr.forEach(name => {
1255
+ const btn = document.createElement('button');
1256
+ btn.className = 'org-chip';
1257
+ btn.textContent = name;
1258
+ btn.addEventListener('click', () => {
1259
+ els.repoInput.value = name;
1260
+ els.branchInput.value = 'main';
1261
+ state.type = type;
1262
+ document.querySelectorAll('.type-btn').forEach(b => b.classList.toggle('active', b.dataset.type === type));
1263
+ fetchTree();
1264
+ });
1265
+ c.appendChild(btn);
1266
+ });
1267
+ };
1268
+ createChip('featuredCloud', FEATURED, 'models');
1269
+ createChip('modelCloud', POPULAR_MODELS, 'models');
1270
+ createChip('datasetCloud', POPULAR_DATASETS, 'datasets');
1271
+ }
1272
+
1273
+ function parseHash() {
1274
+ const h = window.location.hash.substring(1);
1275
+ if (!h) return;
1276
+ const params = new URLSearchParams(h);
1277
+ if (params.has('type')) {
1278
+ state.type = params.get('type');
1279
+ document.querySelectorAll('.type-btn').forEach(b => b.classList.toggle('active', b.dataset.type === state.type));
1280
+ }
1281
+ if (params.has('repo')) {
1282
+ els.repoInput.value = params.get('repo');
1283
+ } else {
1284
+ return;
1285
+ }
1286
+ if (params.has('branch')) {
1287
+ els.branchInput.value = params.get('branch');
1288
+ } else {
1289
+ els.branchInput.value = 'main';
1290
+ }
1291
+ fetchTree();
1292
+ }
1293
+
1294
+ document.addEventListener('DOMContentLoaded', () => {
1295
+ checkToken();
1296
+ buildHomepage();
1297
+
1298
+ els.privateRepoBtn.addEventListener('click', () => els.tokenSection.classList.toggle('hidden'));
1299
+
1300
+ els.saveTokenBtn.addEventListener('click', () => {
1301
+ const t = els.hfToken.value.trim();
1302
+ if (!t) return;
1303
+ localStorage.setItem('hf_token', t);
1304
+ checkToken();
1305
+ showMsg('token saved.', 'loading');
1306
+ setTimeout(() => showMsg('', ''), 1500);
1307
+ });
1308
+
1309
+ els.clearTokenBtn.addEventListener('click', () => {
1310
+ localStorage.removeItem('hf_token');
1311
+ els.hfToken.value = '';
1312
+ checkToken();
1313
+ showMsg('token cleared.', 'loading');
1314
+ setTimeout(() => showMsg('', ''), 1500);
1315
+ });
1316
+
1317
+ document.querySelectorAll('.type-btn').forEach(btn => {
1318
+ btn.addEventListener('click', () => {
1319
+ document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
1320
+ btn.classList.add('active');
1321
+ state.type = btn.dataset.type;
1322
+ });
1323
+ });
1324
+
1325
+ els.fetchBtn.addEventListener('click', fetchTree);
1326
+ els.repoInput.addEventListener('keypress', e => { if (e.key === 'Enter') fetchTree(); });
1327
+ els.branchInput.addEventListener('keypress', e => { if (e.key === 'Enter') fetchTree(); });
1328
+
1329
+ els.treeFilter.addEventListener('input', e => {
1330
+ state.filterText = e.target.value.trim().toLowerCase();
1331
+ renderTree();
1332
+ });
1333
+
1334
+ document.querySelectorAll('#sortDropdown a').forEach(a => {
1335
+ a.addEventListener('click', (e) => {
1336
+ e.preventDefault();
1337
+ state.sort = a.dataset.sort;
1338
+ els.sortLabel.textContent = a.textContent.toLowerCase();
1339
+ renderTree();
1340
+ });
1341
+ });
1342
+
1343
+ document.querySelectorAll('#styleDropdown a').forEach(a => {
1344
+ a.addEventListener('click', (e) => {
1345
+ e.preventDefault();
1346
+ state.style = a.dataset.style;
1347
+ els.styleLabel.textContent = a.dataset.style;
1348
+ renderTree();
1349
+ });
1350
+ });
1351
+
1352
+ els.copyAllBtn.addEventListener('click', () => {
1353
+ const text = getTreePlainText();
1354
+ navigator.clipboard.writeText(text).then(() => {
1355
+ const orig = els.copyAllBtn.innerHTML;
1356
+ els.copyAllBtn.innerHTML = '<i class="fas fa-check"></i> copied!';
1357
+ setTimeout(() => { els.copyAllBtn.innerHTML = orig; }, 1800);
1358
+ });
1359
+ });
1360
+
1361
+ els.shareBtn.addEventListener('click', () => {
1362
+ const shareUrl = `${window.location.origin}${window.location.pathname}#type=${state.type}&repo=${state.repoId}&branch=${state.branch}`;
1363
+ els.shareInput.value = shareUrl;
1364
+ els.shareOverlay.classList.remove('hidden');
1365
+ });
1366
+
1367
+ els.copyShareBtn.addEventListener('click', () => {
1368
+ els.shareInput.select();
1369
+ navigator.clipboard.writeText(els.shareInput.value).then(() => {
1370
+ const orig = els.copyShareBtn.innerHTML;
1371
+ els.copyShareBtn.innerHTML = '<i class="fas fa-check"></i>';
1372
+ setTimeout(() => { els.copyShareBtn.innerHTML = orig; }, 1500);
1373
+ });
1374
+ });
1375
+
1376
+ els.closeOverlay.addEventListener('click', () => els.shareOverlay.classList.add('hidden'));
1377
+ els.shareOverlay.addEventListener('click', (e) => {
1378
+ if (e.target === els.shareOverlay) els.shareOverlay.classList.add('hidden');
1379
+ });
1380
+
1381
+ parseHash();
1382
+ });
1383
+ </script>
1384
  </body>
1385
  </html>