update [maps, profile, timelines] ✅
#2
by prithivMLmods - opened
- index.html +372 -3
index.html
CHANGED
|
@@ -6,6 +6,7 @@
|
|
| 6 |
<title>HF User Stats — HuggingFace User Statistics</title>
|
| 7 |
<meta name="description" content="View detailed statistics for any HuggingFace user — models, datasets, spaces, lifetime downloads, and likes.">
|
| 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@400;500;600&family=Inter:wght@400;500;600;700&display=swap');
|
| 11 |
|
|
@@ -289,6 +290,105 @@
|
|
| 289 |
font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 4px;
|
| 290 |
}
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
.profile-overview {
|
| 293 |
display: grid; grid-template-columns: repeat(3, 1fr);
|
| 294 |
gap: 12px; margin-top: 20px; padding-top: 20px;
|
|
@@ -571,6 +671,9 @@
|
|
| 571 |
<button id="privateRepoBtn" class="text-btn" title="Access Private Repos">
|
| 572 |
<i class="fas fa-lock"></i> Token
|
| 573 |
</button>
|
|
|
|
|
|
|
|
|
|
| 574 |
<button id="themeToggle" class="icon-btn" aria-label="Toggle Theme">
|
| 575 |
<i class="fas fa-sun sun-icon"></i>
|
| 576 |
<i class="fas fa-moon moon-icon"></i>
|
|
@@ -628,6 +731,25 @@
|
|
| 628 |
</a>
|
| 629 |
</div>
|
| 630 |
<div class="profile-fullname" id="profileFullname"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 631 |
</div>
|
| 632 |
</div>
|
| 633 |
<div class="profile-overview">
|
|
@@ -680,6 +802,7 @@
|
|
| 680 |
<div class="footer">
|
| 681 |
Built by <a href="https://hf.co/prithivMLmods" target="_blank" rel="noopener">prithivMLmods</a>
|
| 682 |
</div>
|
|
|
|
| 683 |
</div>
|
| 684 |
|
| 685 |
<script>
|
|
@@ -694,6 +817,8 @@
|
|
| 694 |
activeTab: 'models',
|
| 695 |
activeFilter: 'downloads-alltime',
|
| 696 |
filterText: '',
|
|
|
|
|
|
|
| 697 |
};
|
| 698 |
|
| 699 |
const $ = id => document.getElementById(id);
|
|
@@ -730,6 +855,15 @@
|
|
| 730 |
overviewModels: $('overviewModels'),
|
| 731 |
overviewDatasets: $('overviewDatasets'),
|
| 732 |
overviewSpaces: $('overviewSpaces'),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
};
|
| 734 |
|
| 735 |
const FEATURED_USERS = [
|
|
@@ -878,6 +1012,49 @@
|
|
| 878 |
return null;
|
| 879 |
}
|
| 880 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
async function fetchUserStats() {
|
| 882 |
const username = els.usernameInput.value.trim().replace(/^@/, '').replace(/\/$/, '');
|
| 883 |
if (!username) return showMsg("Please enter a username.", "error");
|
|
@@ -898,7 +1075,7 @@
|
|
| 898 |
fetchProfile(username),
|
| 899 |
fetchAllPages(`${HF_API}/models?author=${uEnc}&limit=1000&full=true&${expandParams}`),
|
| 900 |
fetchAllPages(`${HF_API}/datasets?author=${uEnc}&limit=1000&full=true&${expandParams}`),
|
| 901 |
-
fetchAllPages(`${HF_API}/spaces?author=${uEnc}&limit=1000&full=true&expand[]=likes`),
|
| 902 |
]);
|
| 903 |
|
| 904 |
state.profile = profileData.status === 'fulfilled' ? profileData.value : null;
|
|
@@ -983,6 +1160,9 @@
|
|
| 983 |
window.location.hash = username;
|
| 984 |
document.title = `${username} — HF User Stats`;
|
| 985 |
|
|
|
|
|
|
|
|
|
|
| 986 |
renderProfile();
|
| 987 |
renderOverview();
|
| 988 |
setActiveTab('models');
|
|
@@ -1015,6 +1195,27 @@
|
|
| 1015 |
const fullname = p?.fullname || p?.name || '';
|
| 1016 |
els.profileFullname.textContent = fullname;
|
| 1017 |
els.profileFullname.style.display = fullname ? 'block' : 'none';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1018 |
}
|
| 1019 |
|
| 1020 |
function renderOverview() {
|
|
@@ -1026,6 +1227,170 @@
|
|
| 1026 |
els.tabSpaceCount.textContent = state.spaces.length;
|
| 1027 |
}
|
| 1028 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1029 |
function setActiveTab(tab) {
|
| 1030 |
state.activeTab = tab;
|
| 1031 |
state.activeFilter = (tab === 'spaces') ? 'likes' : 'downloads-alltime';
|
|
@@ -1253,8 +1618,11 @@
|
|
| 1253 |
if (tab !== 'spaces') {
|
| 1254 |
const dlAll = getLifetimeDownloads(item);
|
| 1255 |
const dlMonth = getMonthlyDownloads(item);
|
| 1256 |
-
|
| 1257 |
-
|
|
|
|
|
|
|
|
|
|
| 1258 |
}
|
| 1259 |
const likes = getItemLikes(item);
|
| 1260 |
meta += `<span class="t-stat like-stat" title="${formatNumFull(likes)} likes"><i class="fas fa-heart"></i> ${formatNum(likes)}</span>`;
|
|
@@ -1379,6 +1747,7 @@
|
|
| 1379 |
);
|
| 1380 |
|
| 1381 |
els.copyTreeBtn.addEventListener('click', copyFullTree);
|
|
|
|
| 1382 |
parseHash();
|
| 1383 |
});
|
| 1384 |
</script>
|
|
|
|
| 6 |
<title>HF User Stats — HuggingFace User Statistics</title>
|
| 7 |
<meta name="description" content="View detailed statistics for any HuggingFace user — models, datasets, spaces, lifetime downloads, and likes.">
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
| 10 |
<style>
|
| 11 |
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap');
|
| 12 |
|
|
|
|
| 290 |
font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 4px;
|
| 291 |
}
|
| 292 |
|
| 293 |
+
.profile-social {
|
| 294 |
+
display: flex; gap: 16px; margin-top: 8px; flex-wrap: wrap;
|
| 295 |
+
}
|
| 296 |
+
.social-stat {
|
| 297 |
+
display: flex; align-items: center; gap: 6px;
|
| 298 |
+
font-size: 0.82rem; color: var(--text-secondary);
|
| 299 |
+
font-weight: 500; cursor: pointer; transition: color 0.2s;
|
| 300 |
+
}
|
| 301 |
+
.social-stat:hover { color: var(--accent); }
|
| 302 |
+
.social-stat i { font-size: 0.78rem; }
|
| 303 |
+
.social-stat .social-num {
|
| 304 |
+
font-weight: 700; color: var(--text);
|
| 305 |
+
font-family: var(--mono);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.contribution-section {
|
| 309 |
+
margin-top: 20px; padding-top: 20px;
|
| 310 |
+
border-top: 1px solid var(--border);
|
| 311 |
+
}
|
| 312 |
+
.contrib-header {
|
| 313 |
+
display: flex; justify-content: space-between;
|
| 314 |
+
align-items: center; margin-bottom: 12px;
|
| 315 |
+
flex-wrap: wrap; gap: 10px;
|
| 316 |
+
}
|
| 317 |
+
.contrib-title {
|
| 318 |
+
font-size: 0.78rem; font-weight: 600;
|
| 319 |
+
color: var(--text-secondary); text-transform: uppercase;
|
| 320 |
+
letter-spacing: 1px;
|
| 321 |
+
}
|
| 322 |
+
.contrib-total {
|
| 323 |
+
font-size: 0.75rem; color: var(--text-secondary);
|
| 324 |
+
font-family: var(--mono); font-weight: 500;
|
| 325 |
+
}
|
| 326 |
+
.contrib-total span { color: var(--accent); font-weight: 700; }
|
| 327 |
+
.contrib-map-wrapper {
|
| 328 |
+
overflow-x: auto; padding-bottom: 4px;
|
| 329 |
+
}
|
| 330 |
+
.contrib-map {
|
| 331 |
+
display: grid;
|
| 332 |
+
grid-template-rows: repeat(7, 14px);
|
| 333 |
+
grid-auto-flow: column;
|
| 334 |
+
grid-auto-columns: 14px;
|
| 335 |
+
gap: 3px;
|
| 336 |
+
min-width: fit-content;
|
| 337 |
+
}
|
| 338 |
+
.contrib-cell {
|
| 339 |
+
width: 14px; height: 14px;
|
| 340 |
+
border-radius: 3px;
|
| 341 |
+
background: var(--border);
|
| 342 |
+
transition: transform 0.15s;
|
| 343 |
+
}
|
| 344 |
+
.contrib-cell:hover { transform: scale(1.4); z-index: 2; position: relative; }
|
| 345 |
+
.contrib-cell[data-level="0"] { background: var(--border); }
|
| 346 |
+
.contrib-cell[data-level="1"] { background: rgba(255, 157, 0, 0.25); }
|
| 347 |
+
.contrib-cell[data-level="2"] { background: rgba(255, 157, 0, 0.5); }
|
| 348 |
+
.contrib-cell[data-level="3"] { background: rgba(255, 157, 0, 0.75); }
|
| 349 |
+
.contrib-cell[data-level="4"] { background: var(--accent); }
|
| 350 |
+
[data-theme="dark"] .contrib-cell[data-level="1"] { background: rgba(251, 191, 36, 0.2); }
|
| 351 |
+
[data-theme="dark"] .contrib-cell[data-level="2"] { background: rgba(251, 191, 36, 0.4); }
|
| 352 |
+
[data-theme="dark"] .contrib-cell[data-level="3"] { background: rgba(251, 191, 36, 0.65); }
|
| 353 |
+
[data-theme="dark"] .contrib-cell[data-level="4"] { background: var(--accent); }
|
| 354 |
+
.contrib-legend {
|
| 355 |
+
display: flex; align-items: center; gap: 6px;
|
| 356 |
+
justify-content: flex-end; margin-top: 8px;
|
| 357 |
+
font-size: 0.68rem; color: var(--text-secondary);
|
| 358 |
+
}
|
| 359 |
+
.contrib-legend .contrib-cell { width: 12px; height: 12px; cursor: default; }
|
| 360 |
+
.contrib-legend .contrib-cell:hover { transform: none; }
|
| 361 |
+
.contrib-months {
|
| 362 |
+
display: flex; gap: 3px; margin-bottom: 6px;
|
| 363 |
+
font-size: 0.65rem; color: var(--text-secondary);
|
| 364 |
+
font-weight: 500; padding-left: 0;
|
| 365 |
+
}
|
| 366 |
+
.contrib-months span {
|
| 367 |
+
min-width: 14px; text-align: center;
|
| 368 |
+
}
|
| 369 |
+
.contrib-tooltip {
|
| 370 |
+
position: fixed; background: var(--header-bg);
|
| 371 |
+
border: 1px solid var(--border); border-radius: 6px;
|
| 372 |
+
padding: 6px 10px; font-size: 0.72rem;
|
| 373 |
+
font-family: var(--mono); pointer-events: none;
|
| 374 |
+
z-index: 100; box-shadow: 0 4px 12px var(--shadow-lg);
|
| 375 |
+
display: none;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
.contrib-years {
|
| 379 |
+
display: flex; gap: 6px; flex-wrap: wrap;
|
| 380 |
+
}
|
| 381 |
+
.contrib-year-btn {
|
| 382 |
+
background: var(--btn-bg); border: 1px solid var(--btn-border);
|
| 383 |
+
color: var(--text-secondary); padding: 4px 12px;
|
| 384 |
+
border-radius: 14px; cursor: pointer; font-size: 0.72rem;
|
| 385 |
+
font-weight: 600; font-family: var(--mono);
|
| 386 |
+
transition: all 0.2s; white-space: nowrap;
|
| 387 |
+
}
|
| 388 |
+
.contrib-year-btn:hover { border-color: var(--accent); color: var(--text); }
|
| 389 |
+
.contrib-year-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
|
| 390 |
+
|
| 391 |
+
|
| 392 |
.profile-overview {
|
| 393 |
display: grid; grid-template-columns: repeat(3, 1fr);
|
| 394 |
gap: 12px; margin-top: 20px; padding-top: 20px;
|
|
|
|
| 671 |
<button id="privateRepoBtn" class="text-btn" title="Access Private Repos">
|
| 672 |
<i class="fas fa-lock"></i> Token
|
| 673 |
</button>
|
| 674 |
+
<button id="screenshotBtn" class="text-btn" title="Screenshot & Share">
|
| 675 |
+
<i class="fas fa-camera"></i> Share
|
| 676 |
+
</button>
|
| 677 |
<button id="themeToggle" class="icon-btn" aria-label="Toggle Theme">
|
| 678 |
<i class="fas fa-sun sun-icon"></i>
|
| 679 |
<i class="fas fa-moon moon-icon"></i>
|
|
|
|
| 731 |
</a>
|
| 732 |
</div>
|
| 733 |
<div class="profile-fullname" id="profileFullname"></div>
|
| 734 |
+
<div class="profile-social" id="profileSocial"></div>
|
| 735 |
+
</div>
|
| 736 |
+
</div>
|
| 737 |
+
<div class="contribution-section" id="contribSection" style="display:none;">
|
| 738 |
+
<div class="contrib-header">
|
| 739 |
+
<span class="contrib-title"><i class="fas fa-fire" style="color:var(--accent);margin-right:6px;"></i>Contributions</span>
|
| 740 |
+
<div class="contrib-years" id="contribYears"></div>
|
| 741 |
+
<span class="contrib-total" id="contribTotal"></span>
|
| 742 |
+
</div>
|
| 743 |
+
<div class="contrib-months" id="contribMonths"></div>
|
| 744 |
+
<div class="contrib-map-wrapper"><div class="contrib-map" id="contribMap"></div></div>
|
| 745 |
+
<div class="contrib-legend">
|
| 746 |
+
Less
|
| 747 |
+
<div class="contrib-cell" data-level="0"></div>
|
| 748 |
+
<div class="contrib-cell" data-level="1"></div>
|
| 749 |
+
<div class="contrib-cell" data-level="2"></div>
|
| 750 |
+
<div class="contrib-cell" data-level="3"></div>
|
| 751 |
+
<div class="contrib-cell" data-level="4"></div>
|
| 752 |
+
More
|
| 753 |
</div>
|
| 754 |
</div>
|
| 755 |
<div class="profile-overview">
|
|
|
|
| 802 |
<div class="footer">
|
| 803 |
Built by <a href="https://hf.co/prithivMLmods" target="_blank" rel="noopener">prithivMLmods</a>
|
| 804 |
</div>
|
| 805 |
+
<div class="contrib-tooltip" id="contribTooltip"></div>
|
| 806 |
</div>
|
| 807 |
|
| 808 |
<script>
|
|
|
|
| 817 |
activeTab: 'models',
|
| 818 |
activeFilter: 'downloads-alltime',
|
| 819 |
filterText: '',
|
| 820 |
+
contribYear: null, /* null = last 12 months, else specific year */
|
| 821 |
+
followingData: null, /* { users, orgs, total } from profile page scrape */
|
| 822 |
};
|
| 823 |
|
| 824 |
const $ = id => document.getElementById(id);
|
|
|
|
| 855 |
overviewModels: $('overviewModels'),
|
| 856 |
overviewDatasets: $('overviewDatasets'),
|
| 857 |
overviewSpaces: $('overviewSpaces'),
|
| 858 |
+
profileSocial: $('profileSocial'),
|
| 859 |
+
contribSection: $('contribSection'),
|
| 860 |
+
contribMap: $('contribMap'),
|
| 861 |
+
contribMonths: $('contribMonths'),
|
| 862 |
+
contribTotal: $('contribTotal'),
|
| 863 |
+
contribTooltip: $('contribTooltip'),
|
| 864 |
+
contribYears: $('contribYears'),
|
| 865 |
+
|
| 866 |
+
screenshotBtn: $('screenshotBtn'),
|
| 867 |
};
|
| 868 |
|
| 869 |
const FEATURED_USERS = [
|
|
|
|
| 1012 |
return null;
|
| 1013 |
}
|
| 1014 |
|
| 1015 |
+
async function getHuggingFaceFollowing(username) {
|
| 1016 |
+
const url = `https://huggingface.co/${username}`;
|
| 1017 |
+
try {
|
| 1018 |
+
// Use corsproxy to avoid client-side fetch blocking
|
| 1019 |
+
const response = await fetch(`https://corsproxy.io/?${encodeURIComponent(url)}`);
|
| 1020 |
+
if (!response.ok) {
|
| 1021 |
+
// fallback to direct if proxy fails
|
| 1022 |
+
const fbResponse = await fetch(url);
|
| 1023 |
+
if (!fbResponse.ok) throw new Error(`Failed to fetch profile. Status: ${fbResponse.status}`);
|
| 1024 |
+
const text = await fbResponse.text();
|
| 1025 |
+
return extractStats(text);
|
| 1026 |
+
}
|
| 1027 |
+
const text = await response.text();
|
| 1028 |
+
return extractStats(text);
|
| 1029 |
+
} catch (error) {
|
| 1030 |
+
console.error("Error:", error.message);
|
| 1031 |
+
return null;
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
function extractStats(text) {
|
| 1035 |
+
// Extract the individual metrics from the page's embedded JSON data
|
| 1036 |
+
const usersMatch = text.match(/"numFollowingUsers":(\d+)/);
|
| 1037 |
+
const orgsMatch = text.match(/"numFollowingOrgs":(\d+)/);
|
| 1038 |
+
|
| 1039 |
+
if (usersMatch && orgsMatch) {
|
| 1040 |
+
const numUsers = parseInt(usersMatch[1], 10);
|
| 1041 |
+
const numOrgs = parseInt(orgsMatch[1], 10);
|
| 1042 |
+
|
| 1043 |
+
// Hugging Face logic: sum the followed users and followed orgs
|
| 1044 |
+
const totalFollowing = numUsers + numOrgs;
|
| 1045 |
+
|
| 1046 |
+
return {
|
| 1047 |
+
totalFollowing: totalFollowing,
|
| 1048 |
+
followingUsers: numUsers,
|
| 1049 |
+
followingOrgs: numOrgs
|
| 1050 |
+
};
|
| 1051 |
+
} else {
|
| 1052 |
+
console.error("Could not find following stats in the page source.");
|
| 1053 |
+
return null;
|
| 1054 |
+
}
|
| 1055 |
+
}
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
async function fetchUserStats() {
|
| 1059 |
const username = els.usernameInput.value.trim().replace(/^@/, '').replace(/\/$/, '');
|
| 1060 |
if (!username) return showMsg("Please enter a username.", "error");
|
|
|
|
| 1075 |
fetchProfile(username),
|
| 1076 |
fetchAllPages(`${HF_API}/models?author=${uEnc}&limit=1000&full=true&${expandParams}`),
|
| 1077 |
fetchAllPages(`${HF_API}/datasets?author=${uEnc}&limit=1000&full=true&${expandParams}`),
|
| 1078 |
+
fetchAllPages(`${HF_API}/spaces?author=${uEnc}&limit=1000&full=true&expand[]=likes&expand[]=createdAt&expand[]=lastModified`),
|
| 1079 |
]);
|
| 1080 |
|
| 1081 |
state.profile = profileData.status === 'fulfilled' ? profileData.value : null;
|
|
|
|
| 1160 |
window.location.hash = username;
|
| 1161 |
document.title = `${username} — HF User Stats`;
|
| 1162 |
|
| 1163 |
+
/* Fetch real following counts (users + orgs) from profile page */
|
| 1164 |
+
state.followingData = await getHuggingFaceFollowing(username);
|
| 1165 |
+
|
| 1166 |
renderProfile();
|
| 1167 |
renderOverview();
|
| 1168 |
setActiveTab('models');
|
|
|
|
| 1195 |
const fullname = p?.fullname || p?.name || '';
|
| 1196 |
els.profileFullname.textContent = fullname;
|
| 1197 |
els.profileFullname.style.display = fullname ? 'block' : 'none';
|
| 1198 |
+
|
| 1199 |
+
/* Followers / Following / Orgs */
|
| 1200 |
+
const followers = p?.numFollowers ?? 0;
|
| 1201 |
+
const fd = state.followingData;
|
| 1202 |
+
const followingUsers = fd ? fd.followingUsers : (p?.numFollowing ?? 0);
|
| 1203 |
+
const followingOrgs = fd ? fd.followingOrgs : 0;
|
| 1204 |
+
const followingTotal = fd ? fd.totalFollowing : (p?.numFollowing ?? 0);
|
| 1205 |
+
const memberOrgsCount = Array.isArray(p?.orgs) ? p.orgs.length : 0;
|
| 1206 |
+
let socialHtml = '';
|
| 1207 |
+
socialHtml += `<a class="social-stat" href="https://huggingface.co/${encodeURIComponent(u)}?followers=true" target="_blank" rel="noopener" title="${formatNumFull(followers)} followers">
|
| 1208 |
+
<i class="fas fa-users"></i><span class="social-num">${formatNum(followers)}</span> Followers</a>`;
|
| 1209 |
+
socialHtml += `<a class="social-stat" href="https://huggingface.co/${encodeURIComponent(u)}?following=true" target="_blank" rel="noopener" title="${followingUsers} users + ${followingOrgs} orgs">
|
| 1210 |
+
<i class="fas fa-user-plus"></i><span class="social-num">${formatNum(followingTotal)}</span> Following</a>`;
|
| 1211 |
+
if (memberOrgsCount > 0) {
|
| 1212 |
+
socialHtml += `<span class="social-stat" title="Member of ${memberOrgsCount} organizations">
|
| 1213 |
+
<i class="fas fa-building"></i><span class="social-num">${memberOrgsCount}</span> Orgs</span>`;
|
| 1214 |
+
}
|
| 1215 |
+
els.profileSocial.innerHTML = socialHtml;
|
| 1216 |
+
|
| 1217 |
+
/* Contribution map */
|
| 1218 |
+
renderContribMap();
|
| 1219 |
}
|
| 1220 |
|
| 1221 |
function renderOverview() {
|
|
|
|
| 1227 |
els.tabSpaceCount.textContent = state.spaces.length;
|
| 1228 |
}
|
| 1229 |
|
| 1230 |
+
function renderContribMap(yearOverride) {
|
| 1231 |
+
const allItems = [...state.models, ...state.datasets, ...state.spaces];
|
| 1232 |
+
if (allItems.length === 0) { els.contribSection.style.display = 'none'; return; }
|
| 1233 |
+
|
| 1234 |
+
/* Determine available years from all items */
|
| 1235 |
+
const allDates = allItems
|
| 1236 |
+
.map(i => i.createdAt || i.lastModified)
|
| 1237 |
+
.filter(Boolean)
|
| 1238 |
+
.map(d => new Date(d));
|
| 1239 |
+
if (allDates.length === 0) { els.contribSection.style.display = 'none'; return; }
|
| 1240 |
+
|
| 1241 |
+
const years = [...new Set(allDates.map(d => d.getFullYear()))].sort((a, b) => b - a);
|
| 1242 |
+
const currentYear = new Date().getFullYear();
|
| 1243 |
+
|
| 1244 |
+
/* Render year filter pills */
|
| 1245 |
+
const selectedYear = yearOverride !== undefined ? yearOverride : state.contribYear;
|
| 1246 |
+
state.contribYear = selectedYear;
|
| 1247 |
+
let yearsHtml = `<button class="contrib-year-btn ${selectedYear === null ? 'active' : ''}" data-year="">Last 12 months</button>`;
|
| 1248 |
+
years.forEach(y => {
|
| 1249 |
+
yearsHtml += `<button class="contrib-year-btn ${selectedYear === y ? 'active' : ''}" data-year="${y}">${y}</button>`;
|
| 1250 |
+
});
|
| 1251 |
+
els.contribYears.innerHTML = yearsHtml;
|
| 1252 |
+
els.contribYears.querySelectorAll('.contrib-year-btn').forEach(btn => {
|
| 1253 |
+
btn.addEventListener('click', () => {
|
| 1254 |
+
const y = btn.dataset.year;
|
| 1255 |
+
renderContribMap(y === '' ? null : parseInt(y));
|
| 1256 |
+
});
|
| 1257 |
+
});
|
| 1258 |
+
|
| 1259 |
+
/* Determine date range */
|
| 1260 |
+
let rangeStart, rangeEnd, rangeLabel;
|
| 1261 |
+
if (selectedYear !== null) {
|
| 1262 |
+
rangeStart = new Date(selectedYear, 0, 1);
|
| 1263 |
+
rangeEnd = new Date(selectedYear, 11, 31, 23, 59, 59);
|
| 1264 |
+
rangeLabel = `in ${selectedYear}`;
|
| 1265 |
+
} else {
|
| 1266 |
+
rangeEnd = new Date();
|
| 1267 |
+
rangeStart = new Date(rangeEnd);
|
| 1268 |
+
rangeStart.setFullYear(rangeStart.getFullYear() - 1);
|
| 1269 |
+
rangeStart.setHours(0,0,0,0);
|
| 1270 |
+
rangeLabel = 'in the last year';
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
/* Count contributions per day */
|
| 1274 |
+
const dayCounts = {};
|
| 1275 |
+
let totalContribs = 0;
|
| 1276 |
+
allItems.forEach(item => {
|
| 1277 |
+
const d = item.createdAt || item.lastModified;
|
| 1278 |
+
if (!d) return;
|
| 1279 |
+
const dt = new Date(d);
|
| 1280 |
+
if (dt < rangeStart || dt > rangeEnd) return;
|
| 1281 |
+
const key = dt.toISOString().slice(0, 10);
|
| 1282 |
+
dayCounts[key] = (dayCounts[key] || 0) + 1;
|
| 1283 |
+
totalContribs++;
|
| 1284 |
+
});
|
| 1285 |
+
|
| 1286 |
+
els.contribSection.style.display = 'block';
|
| 1287 |
+
els.contribTotal.innerHTML = `<span>${totalContribs}</span> contributions ${rangeLabel}`;
|
| 1288 |
+
|
| 1289 |
+
/* Build grid starting from Sunday */
|
| 1290 |
+
const startDate = new Date(rangeStart);
|
| 1291 |
+
startDate.setDate(startDate.getDate() - startDate.getDay());
|
| 1292 |
+
const endDate = new Date(rangeEnd);
|
| 1293 |
+
const totalDays = Math.ceil((endDate - startDate) / 86400000) + 1;
|
| 1294 |
+
const maxCount = Math.max(1, ...Object.values(dayCounts));
|
| 1295 |
+
|
| 1296 |
+
const mapEl = els.contribMap;
|
| 1297 |
+
const tooltip = els.contribTooltip;
|
| 1298 |
+
mapEl.innerHTML = '';
|
| 1299 |
+
|
| 1300 |
+
const fragment = document.createDocumentFragment();
|
| 1301 |
+
const monthLabels = [];
|
| 1302 |
+
let lastMonth = -1;
|
| 1303 |
+
|
| 1304 |
+
for (let i = 0; i < totalDays; i++) {
|
| 1305 |
+
const d = new Date(startDate);
|
| 1306 |
+
d.setDate(d.getDate() + i);
|
| 1307 |
+
const key = d.toISOString().slice(0, 10);
|
| 1308 |
+
const count = dayCounts[key] || 0;
|
| 1309 |
+
let level = 0;
|
| 1310 |
+
if (count > 0) {
|
| 1311 |
+
const ratio = count / maxCount;
|
| 1312 |
+
if (ratio <= 0.25) level = 1;
|
| 1313 |
+
else if (ratio <= 0.5) level = 2;
|
| 1314 |
+
else if (ratio <= 0.75) level = 3;
|
| 1315 |
+
else level = 4;
|
| 1316 |
+
}
|
| 1317 |
+
const cell = document.createElement('div');
|
| 1318 |
+
cell.className = 'contrib-cell';
|
| 1319 |
+
cell.setAttribute('data-level', level);
|
| 1320 |
+
cell.setAttribute('data-date', key);
|
| 1321 |
+
cell.setAttribute('data-count', count);
|
| 1322 |
+
fragment.appendChild(cell);
|
| 1323 |
+
|
| 1324 |
+
if (i % 7 === 0) {
|
| 1325 |
+
const m = d.getMonth();
|
| 1326 |
+
if (m !== lastMonth) {
|
| 1327 |
+
monthLabels.push({ month: m, col: Math.floor(i / 7) });
|
| 1328 |
+
lastMonth = m;
|
| 1329 |
+
}
|
| 1330 |
+
}
|
| 1331 |
+
}
|
| 1332 |
+
mapEl.appendChild(fragment);
|
| 1333 |
+
|
| 1334 |
+
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
| 1335 |
+
let monthHtml = '', labelCol = 0;
|
| 1336 |
+
for (const ml of monthLabels) {
|
| 1337 |
+
const gap = ml.col - labelCol;
|
| 1338 |
+
for (let g = 0; g < gap; g++) monthHtml += '<span></span>';
|
| 1339 |
+
monthHtml += `<span>${MONTHS[ml.month]}</span>`;
|
| 1340 |
+
labelCol = ml.col + 1;
|
| 1341 |
+
}
|
| 1342 |
+
els.contribMonths.innerHTML = monthHtml;
|
| 1343 |
+
|
| 1344 |
+
/* Tooltip */
|
| 1345 |
+
mapEl.onmouseover = e => {
|
| 1346 |
+
const cell = e.target.closest('.contrib-cell');
|
| 1347 |
+
if (!cell) { tooltip.style.display = 'none'; return; }
|
| 1348 |
+
const date = cell.dataset.date, count = cell.dataset.count;
|
| 1349 |
+
const dateStr = new Date(date + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
| 1350 |
+
tooltip.innerHTML = `<strong>${count}</strong> contribution${count !== '1' ? 's' : ''} on ${dateStr}`;
|
| 1351 |
+
tooltip.style.display = 'block';
|
| 1352 |
+
const rect = cell.getBoundingClientRect();
|
| 1353 |
+
tooltip.style.left = (rect.left + rect.width / 2 - tooltip.offsetWidth / 2) + 'px';
|
| 1354 |
+
tooltip.style.top = (rect.top - tooltip.offsetHeight - 8) + 'px';
|
| 1355 |
+
};
|
| 1356 |
+
mapEl.onmouseleave = () => { tooltip.style.display = 'none'; };
|
| 1357 |
+
|
| 1358 |
+
}
|
| 1359 |
+
|
| 1360 |
+
async function takeScreenshot() {
|
| 1361 |
+
const btn = els.screenshotBtn;
|
| 1362 |
+
const orig = btn.innerHTML;
|
| 1363 |
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Capturing…';
|
| 1364 |
+
btn.disabled = true;
|
| 1365 |
+
try {
|
| 1366 |
+
const target = document.querySelector('.app-container');
|
| 1367 |
+
const canvas = await html2canvas(target, {
|
| 1368 |
+
backgroundColor: getComputedStyle(document.body).getPropertyValue('background-color'),
|
| 1369 |
+
scale: 2,
|
| 1370 |
+
useCORS: true,
|
| 1371 |
+
logging: false,
|
| 1372 |
+
});
|
| 1373 |
+
canvas.toBlob(blob => {
|
| 1374 |
+
const url = URL.createObjectURL(blob);
|
| 1375 |
+
const a = document.createElement('a');
|
| 1376 |
+
a.href = url;
|
| 1377 |
+
a.download = `hf-stats-${state.username || 'page'}-${Date.now()}.png`;
|
| 1378 |
+
document.body.appendChild(a);
|
| 1379 |
+
a.click();
|
| 1380 |
+
document.body.removeChild(a);
|
| 1381 |
+
URL.revokeObjectURL(url);
|
| 1382 |
+
btn.innerHTML = '<i class="fas fa-check"></i> Saved!';
|
| 1383 |
+
setTimeout(() => { btn.innerHTML = orig; }, 2000);
|
| 1384 |
+
}, 'image/png');
|
| 1385 |
+
} catch (err) {
|
| 1386 |
+
console.error('Screenshot failed:', err);
|
| 1387 |
+
btn.innerHTML = '<i class="fas fa-times"></i> Failed';
|
| 1388 |
+
setTimeout(() => { btn.innerHTML = orig; }, 2000);
|
| 1389 |
+
} finally {
|
| 1390 |
+
btn.disabled = false;
|
| 1391 |
+
}
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
function setActiveTab(tab) {
|
| 1395 |
state.activeTab = tab;
|
| 1396 |
state.activeFilter = (tab === 'spaces') ? 'likes' : 'downloads-alltime';
|
|
|
|
| 1618 |
if (tab !== 'spaces') {
|
| 1619 |
const dlAll = getLifetimeDownloads(item);
|
| 1620 |
const dlMonth = getMonthlyDownloads(item);
|
| 1621 |
+
if (state.activeFilter === 'downloads-monthly') {
|
| 1622 |
+
meta += `<span class="t-stat monthly-stat" title="${formatNumFull(dlMonth)} downloads (last month)"><i class="fas fa-calendar-alt"></i> ${formatNum(dlMonth)}</span>`;
|
| 1623 |
+
} else {
|
| 1624 |
+
meta += `<span class="t-stat dl-stat" title="${formatNumFull(dlAll)} downloads (all time)"><i class="fas fa-download"></i> ${formatNum(dlAll)}</span>`;
|
| 1625 |
+
}
|
| 1626 |
}
|
| 1627 |
const likes = getItemLikes(item);
|
| 1628 |
meta += `<span class="t-stat like-stat" title="${formatNumFull(likes)} likes"><i class="fas fa-heart"></i> ${formatNum(likes)}</span>`;
|
|
|
|
| 1747 |
);
|
| 1748 |
|
| 1749 |
els.copyTreeBtn.addEventListener('click', copyFullTree);
|
| 1750 |
+
els.screenshotBtn.addEventListener('click', takeScreenshot);
|
| 1751 |
parseHash();
|
| 1752 |
});
|
| 1753 |
</script>
|