Update index.html
Browse files- index.html +143 -92
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 |
|
| 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');
|
|
@@ -787,6 +788,9 @@
|
|
| 787 |
<button class="tool-btn action-btn" id="copyTreeBtn">
|
| 788 |
<i class="far fa-copy"></i> Copy Tree
|
| 789 |
</button>
|
|
|
|
|
|
|
|
|
|
| 790 |
</div>
|
| 791 |
</div>
|
| 792 |
<div class="terminal-window">
|
|
@@ -815,7 +819,6 @@
|
|
| 815 |
activeFilter: 'downloads-alltime',
|
| 816 |
filterText: '',
|
| 817 |
contribYear: null, /* null = last 12 months, else specific year */
|
| 818 |
-
followingData: null, /* { users, orgs, total } from profile page scrape */
|
| 819 |
};
|
| 820 |
|
| 821 |
const $ = id => document.getElementById(id);
|
|
@@ -844,6 +847,7 @@
|
|
| 844 |
lineNumbers: $('lineNumbers'),
|
| 845 |
treeContent: $('treeContent'),
|
| 846 |
copyTreeBtn: $('copyTreeBtn'),
|
|
|
|
| 847 |
tokenSection: $('tokenSection'),
|
| 848 |
hfToken: $('hfToken'),
|
| 849 |
saveTokenBtn: $('saveTokenBtn'),
|
|
@@ -1007,84 +1011,6 @@
|
|
| 1007 |
return null;
|
| 1008 |
}
|
| 1009 |
|
| 1010 |
-
async function getHuggingFaceFollowing(username) {
|
| 1011 |
-
const profileUrl = `https://huggingface.co/${username}`;
|
| 1012 |
-
|
| 1013 |
-
/* Try fetching the profile HTML from multiple sources */
|
| 1014 |
-
const attempts = [
|
| 1015 |
-
/* 1. Direct fetch — works when hosted on HF Spaces (same origin) */
|
| 1016 |
-
() => fetch(profileUrl),
|
| 1017 |
-
/* 2. allorigins proxy */
|
| 1018 |
-
() => fetch(`https://api.allorigins.win/raw?url=${encodeURIComponent(profileUrl)}`),
|
| 1019 |
-
/* 3. corsproxy.io */
|
| 1020 |
-
() => fetch(`https://corsproxy.io/?${encodeURIComponent(profileUrl)}`),
|
| 1021 |
-
/* 4. cors-anywhere style */
|
| 1022 |
-
() => fetch(`https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(profileUrl)}`),
|
| 1023 |
-
];
|
| 1024 |
-
|
| 1025 |
-
for (const attempt of attempts) {
|
| 1026 |
-
try {
|
| 1027 |
-
const response = await attempt();
|
| 1028 |
-
if (!response.ok) continue;
|
| 1029 |
-
const text = await response.text();
|
| 1030 |
-
const stats = extractFollowingStats(text);
|
| 1031 |
-
if (stats) {
|
| 1032 |
-
console.log('[HF-Stats] Following counts fetched:', stats);
|
| 1033 |
-
return stats;
|
| 1034 |
-
}
|
| 1035 |
-
} catch (e) {
|
| 1036 |
-
/* try next proxy */
|
| 1037 |
-
}
|
| 1038 |
-
}
|
| 1039 |
-
|
| 1040 |
-
console.warn('[HF-Stats] All fetch methods failed for following counts');
|
| 1041 |
-
return null;
|
| 1042 |
-
}
|
| 1043 |
-
|
| 1044 |
-
function extractFollowingStats(text) {
|
| 1045 |
-
/*
|
| 1046 |
-
* The HF profile page embeds JSON in HTML attributes using " encoding.
|
| 1047 |
-
* Some proxies may decode these, so we check both patterns.
|
| 1048 |
-
*/
|
| 1049 |
-
let numUsers = null, numOrgs = null;
|
| 1050 |
-
|
| 1051 |
-
/* Pattern 1: HTML-encoded quotes (") — raw profile page */
|
| 1052 |
-
let m1 = text.match(/"numFollowingUsers":(\d+)/);
|
| 1053 |
-
let m2 = text.match(/"numFollowingOrgs":(\d+)/);
|
| 1054 |
-
if (m1 && m2) {
|
| 1055 |
-
numUsers = parseInt(m1[1], 10);
|
| 1056 |
-
numOrgs = parseInt(m2[1], 10);
|
| 1057 |
-
}
|
| 1058 |
-
|
| 1059 |
-
/* Pattern 2: raw JSON quotes — some proxies decode entities */
|
| 1060 |
-
if (numUsers === null) {
|
| 1061 |
-
m1 = text.match(/"numFollowingUsers":(\d+)/);
|
| 1062 |
-
m2 = text.match(/"numFollowingOrgs":(\d+)/);
|
| 1063 |
-
if (m1 && m2) {
|
| 1064 |
-
numUsers = parseInt(m1[1], 10);
|
| 1065 |
-
numOrgs = parseInt(m2[1], 10);
|
| 1066 |
-
}
|
| 1067 |
-
}
|
| 1068 |
-
|
| 1069 |
-
/* Pattern 3: single-quoted or escaped */
|
| 1070 |
-
if (numUsers === null) {
|
| 1071 |
-
m1 = text.match(/numFollowingUsers['":\s]+(\d+)/);
|
| 1072 |
-
m2 = text.match(/numFollowingOrgs['":\s]+(\d+)/);
|
| 1073 |
-
if (m1 && m2) {
|
| 1074 |
-
numUsers = parseInt(m1[1], 10);
|
| 1075 |
-
numOrgs = parseInt(m2[1], 10);
|
| 1076 |
-
}
|
| 1077 |
-
}
|
| 1078 |
-
|
| 1079 |
-
if (numUsers !== null && numOrgs !== null) {
|
| 1080 |
-
return {
|
| 1081 |
-
totalFollowing: numUsers + numOrgs,
|
| 1082 |
-
followingUsers: numUsers,
|
| 1083 |
-
followingOrgs: numOrgs
|
| 1084 |
-
};
|
| 1085 |
-
}
|
| 1086 |
-
return null;
|
| 1087 |
-
}
|
| 1088 |
|
| 1089 |
async function fetchUserStats() {
|
| 1090 |
const username = els.usernameInput.value.trim().replace(/^@/, '').replace(/\/$/, '');
|
|
@@ -1191,13 +1117,6 @@
|
|
| 1191 |
window.location.hash = username;
|
| 1192 |
document.title = `${username} — HF User Stats`;
|
| 1193 |
|
| 1194 |
-
/* Fetch following counts in background — don't block rendering */
|
| 1195 |
-
getHuggingFaceFollowing(username).then(fd => {
|
| 1196 |
-
if (fd) {
|
| 1197 |
-
state.followingData = fd;
|
| 1198 |
-
renderProfileSocial();
|
| 1199 |
-
}
|
| 1200 |
-
});
|
| 1201 |
|
| 1202 |
renderProfile();
|
| 1203 |
renderOverview();
|
|
@@ -1381,16 +1300,10 @@
|
|
| 1381 |
const u = state.username;
|
| 1382 |
const p = state.profile;
|
| 1383 |
const followers = p?.numFollowers ?? 0;
|
| 1384 |
-
const fd = state.followingData;
|
| 1385 |
-
const followingUsers = fd ? fd.followingUsers : (p?.numFollowing ?? 0);
|
| 1386 |
-
const followingOrgs = fd ? fd.followingOrgs : 0;
|
| 1387 |
-
const followingTotal = fd ? fd.totalFollowing : (p?.numFollowing ?? 0);
|
| 1388 |
const memberOrgsCount = Array.isArray(p?.orgs) ? p.orgs.length : 0;
|
| 1389 |
let socialHtml = '';
|
| 1390 |
socialHtml += `<a class="social-stat" href="https://huggingface.co/${encodeURIComponent(u)}?followers=true" target="_blank" rel="noopener" title="${formatNumFull(followers)} followers">
|
| 1391 |
<i class="fas fa-users"></i><span class="social-num">${formatNum(followers)}</span> Followers</a>`;
|
| 1392 |
-
socialHtml += `<a class="social-stat" href="https://huggingface.co/${encodeURIComponent(u)}?following=true" target="_blank" rel="noopener" title="${followingUsers} users + ${followingOrgs} orgs">
|
| 1393 |
-
<i class="fas fa-user-plus"></i><span class="social-num">${formatNum(followingTotal)}</span> Following</a>`;
|
| 1394 |
if (memberOrgsCount > 0) {
|
| 1395 |
socialHtml += `<span class="social-stat" title="Member of ${memberOrgsCount} organizations">
|
| 1396 |
<i class="fas fa-building"></i><span class="social-num">${memberOrgsCount}</span> Orgs</span>`;
|
|
@@ -1398,6 +1311,143 @@
|
|
| 1398 |
els.profileSocial.innerHTML = socialHtml;
|
| 1399 |
}
|
| 1400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1401 |
function setActiveTab(tab) {
|
| 1402 |
state.activeTab = tab;
|
| 1403 |
state.activeFilter = (tab === 'spaces') ? 'likes' : 'downloads-alltime';
|
|
@@ -1754,6 +1804,7 @@
|
|
| 1754 |
);
|
| 1755 |
|
| 1756 |
els.copyTreeBtn.addEventListener('click', copyFullTree);
|
|
|
|
| 1757 |
parseHash();
|
| 1758 |
});
|
| 1759 |
</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/jspdf/2.5.2/jspdf.umd.min.js"></script>
|
| 10 |
|
| 11 |
<style>
|
| 12 |
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600;700&display=swap');
|
|
|
|
| 788 |
<button class="tool-btn action-btn" id="copyTreeBtn">
|
| 789 |
<i class="far fa-copy"></i> Copy Tree
|
| 790 |
</button>
|
| 791 |
+
<button class="tool-btn action-btn" id="downloadReportBtn" style="background:var(--accent);color:#fff;border-color:var(--accent);">
|
| 792 |
+
<i class="fas fa-file-pdf"></i> Report
|
| 793 |
+
</button>
|
| 794 |
</div>
|
| 795 |
</div>
|
| 796 |
<div class="terminal-window">
|
|
|
|
| 819 |
activeFilter: 'downloads-alltime',
|
| 820 |
filterText: '',
|
| 821 |
contribYear: null, /* null = last 12 months, else specific year */
|
|
|
|
| 822 |
};
|
| 823 |
|
| 824 |
const $ = id => document.getElementById(id);
|
|
|
|
| 847 |
lineNumbers: $('lineNumbers'),
|
| 848 |
treeContent: $('treeContent'),
|
| 849 |
copyTreeBtn: $('copyTreeBtn'),
|
| 850 |
+
downloadReportBtn: $('downloadReportBtn'),
|
| 851 |
tokenSection: $('tokenSection'),
|
| 852 |
hfToken: $('hfToken'),
|
| 853 |
saveTokenBtn: $('saveTokenBtn'),
|
|
|
|
| 1011 |
return null;
|
| 1012 |
}
|
| 1013 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1014 |
|
| 1015 |
async function fetchUserStats() {
|
| 1016 |
const username = els.usernameInput.value.trim().replace(/^@/, '').replace(/\/$/, '');
|
|
|
|
| 1117 |
window.location.hash = username;
|
| 1118 |
document.title = `${username} — HF User Stats`;
|
| 1119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1120 |
|
| 1121 |
renderProfile();
|
| 1122 |
renderOverview();
|
|
|
|
| 1300 |
const u = state.username;
|
| 1301 |
const p = state.profile;
|
| 1302 |
const followers = p?.numFollowers ?? 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1303 |
const memberOrgsCount = Array.isArray(p?.orgs) ? p.orgs.length : 0;
|
| 1304 |
let socialHtml = '';
|
| 1305 |
socialHtml += `<a class="social-stat" href="https://huggingface.co/${encodeURIComponent(u)}?followers=true" target="_blank" rel="noopener" title="${formatNumFull(followers)} followers">
|
| 1306 |
<i class="fas fa-users"></i><span class="social-num">${formatNum(followers)}</span> Followers</a>`;
|
|
|
|
|
|
|
| 1307 |
if (memberOrgsCount > 0) {
|
| 1308 |
socialHtml += `<span class="social-stat" title="Member of ${memberOrgsCount} organizations">
|
| 1309 |
<i class="fas fa-building"></i><span class="social-num">${memberOrgsCount}</span> Orgs</span>`;
|
|
|
|
| 1311 |
els.profileSocial.innerHTML = socialHtml;
|
| 1312 |
}
|
| 1313 |
|
| 1314 |
+
function downloadReport() {
|
| 1315 |
+
const { jsPDF } = window.jspdf;
|
| 1316 |
+
const doc = new jsPDF({ unit: 'mm', format: 'a4' });
|
| 1317 |
+
const u = state.username;
|
| 1318 |
+
const p = state.profile;
|
| 1319 |
+
const pw = 210; /* A4 width */
|
| 1320 |
+
let y = 18;
|
| 1321 |
+
|
| 1322 |
+
const colors = {
|
| 1323 |
+
accent: [255, 157, 0],
|
| 1324 |
+
dark: [30, 30, 30],
|
| 1325 |
+
mid: [100, 100, 100],
|
| 1326 |
+
light: [160, 160, 160],
|
| 1327 |
+
line: [220, 220, 220],
|
| 1328 |
+
};
|
| 1329 |
+
|
| 1330 |
+
/* Header bar */
|
| 1331 |
+
doc.setFillColor(...colors.accent);
|
| 1332 |
+
doc.rect(0, 0, pw, 12, 'F');
|
| 1333 |
+
doc.setFontSize(11); doc.setTextColor(255, 255, 255);
|
| 1334 |
+
doc.setFont('helvetica', 'bold');
|
| 1335 |
+
doc.text(`HuggingFace Stats Report — ${u}`, 10, 8);
|
| 1336 |
+
doc.setFontSize(7); doc.setFont('helvetica', 'normal');
|
| 1337 |
+
doc.text(new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }), pw - 10, 8, { align: 'right' });
|
| 1338 |
+
|
| 1339 |
+
/* Profile section */
|
| 1340 |
+
y = 22;
|
| 1341 |
+
doc.setFontSize(14); doc.setTextColor(...colors.dark); doc.setFont('helvetica', 'bold');
|
| 1342 |
+
doc.text(p?.fullname || u, 10, y);
|
| 1343 |
+
y += 5;
|
| 1344 |
+
doc.setFontSize(9); doc.setTextColor(...colors.mid); doc.setFont('helvetica', 'normal');
|
| 1345 |
+
doc.text(`@${u} • https://huggingface.co/${u}`, 10, y);
|
| 1346 |
+
y += 5;
|
| 1347 |
+
const followers = p?.numFollowers ?? 0;
|
| 1348 |
+
const orgsCount = Array.isArray(p?.orgs) ? p.orgs.length : 0;
|
| 1349 |
+
doc.text(`Followers: ${followers.toLocaleString()} | Organizations: ${orgsCount}`, 10, y);
|
| 1350 |
+
y += 8;
|
| 1351 |
+
|
| 1352 |
+
/* Divider */
|
| 1353 |
+
doc.setDrawColor(...colors.line); doc.setLineWidth(0.3); doc.line(10, y, pw - 10, y);
|
| 1354 |
+
y += 6;
|
| 1355 |
+
|
| 1356 |
+
/* Summary boxes */
|
| 1357 |
+
doc.setFontSize(10); doc.setTextColor(...colors.dark); doc.setFont('helvetica', 'bold');
|
| 1358 |
+
doc.text('Overview', 10, y); y += 6;
|
| 1359 |
+
|
| 1360 |
+
const totalDownloads = state.models.reduce((s, m) => s + getLifetimeDownloads(m), 0)
|
| 1361 |
+
+ state.datasets.reduce((s, d) => s + getLifetimeDownloads(d), 0);
|
| 1362 |
+
const totalLikes = [...state.models, ...state.datasets, ...state.spaces].reduce((s, i) => s + getItemLikes(i), 0);
|
| 1363 |
+
const monthlyDl = state.models.reduce((s, m) => s + getMonthlyDownloads(m), 0)
|
| 1364 |
+
+ state.datasets.reduce((s, d) => s + getMonthlyDownloads(d), 0);
|
| 1365 |
+
|
| 1366 |
+
const summaryItems = [
|
| 1367 |
+
['Models', state.models.length],
|
| 1368 |
+
['Datasets', state.datasets.length],
|
| 1369 |
+
['Spaces', state.spaces.length],
|
| 1370 |
+
['Total Downloads (All Time)', totalDownloads.toLocaleString()],
|
| 1371 |
+
['Total Downloads (Last Month)', monthlyDl.toLocaleString()],
|
| 1372 |
+
['Total Likes', totalLikes.toLocaleString()],
|
| 1373 |
+
];
|
| 1374 |
+
|
| 1375 |
+
doc.setFont('helvetica', 'normal'); doc.setFontSize(9);
|
| 1376 |
+
summaryItems.forEach(([label, val]) => {
|
| 1377 |
+
doc.setTextColor(...colors.mid); doc.text(label + ':', 14, y);
|
| 1378 |
+
doc.setTextColor(...colors.dark); doc.text(String(val), 80, y);
|
| 1379 |
+
y += 5;
|
| 1380 |
+
});
|
| 1381 |
+
y += 4;
|
| 1382 |
+
|
| 1383 |
+
/* Section renderer helper */
|
| 1384 |
+
function addSection(title, items, type) {
|
| 1385 |
+
if (items.length === 0) return;
|
| 1386 |
+
|
| 1387 |
+
/* Page break check */
|
| 1388 |
+
if (y > 260) { doc.addPage(); y = 15; }
|
| 1389 |
+
|
| 1390 |
+
doc.setDrawColor(...colors.line); doc.line(10, y, pw - 10, y); y += 6;
|
| 1391 |
+
doc.setFontSize(11); doc.setTextColor(...colors.accent); doc.setFont('helvetica', 'bold');
|
| 1392 |
+
doc.text(`${title} (${items.length})`, 10, y); y += 6;
|
| 1393 |
+
|
| 1394 |
+
/* Table header */
|
| 1395 |
+
doc.setFillColor(245, 245, 245);
|
| 1396 |
+
doc.rect(10, y - 3.5, pw - 20, 5.5, 'F');
|
| 1397 |
+
doc.setFontSize(7); doc.setTextColor(...colors.mid); doc.setFont('helvetica', 'bold');
|
| 1398 |
+
doc.text('Name', 12, y);
|
| 1399 |
+
if (type !== 'spaces') {
|
| 1400 |
+
doc.text('Downloads (All)', 100, y);
|
| 1401 |
+
doc.text('Downloads (Month)', 130, y);
|
| 1402 |
+
}
|
| 1403 |
+
doc.text('Likes', 170, y);
|
| 1404 |
+
doc.text('Last Modified', 182, y);
|
| 1405 |
+
y += 5;
|
| 1406 |
+
|
| 1407 |
+
doc.setFont('helvetica', 'normal'); doc.setFontSize(7.5);
|
| 1408 |
+
|
| 1409 |
+
items.forEach(item => {
|
| 1410 |
+
if (y > 280) { doc.addPage(); y = 15; }
|
| 1411 |
+
|
| 1412 |
+
const name = (item.id || item.modelId || '').split('/').pop();
|
| 1413 |
+
doc.setTextColor(...colors.dark);
|
| 1414 |
+
doc.text(name.length > 45 ? name.slice(0, 42) + '...' : name, 12, y);
|
| 1415 |
+
|
| 1416 |
+
if (type !== 'spaces') {
|
| 1417 |
+
doc.setTextColor(...colors.mid);
|
| 1418 |
+
doc.text(getLifetimeDownloads(item).toLocaleString(), 100, y);
|
| 1419 |
+
doc.text(getMonthlyDownloads(item).toLocaleString(), 130, y);
|
| 1420 |
+
}
|
| 1421 |
+
doc.setTextColor(...colors.mid);
|
| 1422 |
+
doc.text(String(getItemLikes(item)), 170, y);
|
| 1423 |
+
|
| 1424 |
+
const modified = item.lastModified ? new Date(item.lastModified).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' }) : '-';
|
| 1425 |
+
doc.text(modified, 182, y);
|
| 1426 |
+
y += 4.2;
|
| 1427 |
+
});
|
| 1428 |
+
y += 4;
|
| 1429 |
+
}
|
| 1430 |
+
|
| 1431 |
+
/* Sort each section by downloads/likes */
|
| 1432 |
+
const sortedModels = [...state.models].sort((a, b) => getLifetimeDownloads(b) - getLifetimeDownloads(a));
|
| 1433 |
+
const sortedDatasets = [...state.datasets].sort((a, b) => getLifetimeDownloads(b) - getLifetimeDownloads(a));
|
| 1434 |
+
const sortedSpaces = [...state.spaces].sort((a, b) => getItemLikes(b) - getItemLikes(a));
|
| 1435 |
+
|
| 1436 |
+
addSection('Models', sortedModels, 'models');
|
| 1437 |
+
addSection('Datasets', sortedDatasets, 'datasets');
|
| 1438 |
+
addSection('Spaces', sortedSpaces, 'spaces');
|
| 1439 |
+
|
| 1440 |
+
/* Footer on every page */
|
| 1441 |
+
const totalPages = doc.internal.getNumberOfPages();
|
| 1442 |
+
for (let i = 1; i <= totalPages; i++) {
|
| 1443 |
+
doc.setPage(i);
|
| 1444 |
+
doc.setFontSize(7); doc.setTextColor(...colors.light); doc.setFont('helvetica', 'normal');
|
| 1445 |
+
doc.text(`Generated by hf-user-stats • Page ${i}/${totalPages}`, pw / 2, 290, { align: 'center' });
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
doc.save(`hf-stats-${u}-${Date.now()}.pdf`);
|
| 1449 |
+
}
|
| 1450 |
+
|
| 1451 |
function setActiveTab(tab) {
|
| 1452 |
state.activeTab = tab;
|
| 1453 |
state.activeFilter = (tab === 'spaces') ? 'likes' : 'downloads-alltime';
|
|
|
|
| 1804 |
);
|
| 1805 |
|
| 1806 |
els.copyTreeBtn.addEventListener('click', copyFullTree);
|
| 1807 |
+
els.downloadReportBtn.addEventListener('click', downloadReport);
|
| 1808 |
parseHash();
|
| 1809 |
});
|
| 1810 |
</script>
|