prithivMLmods commited on
Commit
c8b3cd5
·
verified ·
1 Parent(s): d39899e

Update index.html

Browse files
Files changed (1) hide show
  1. 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 &quot; 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 (&quot;) — raw profile page */
1052
- let m1 = text.match(/&quot;numFollowingUsers&quot;:(\d+)/);
1053
- let m2 = text.match(/&quot;numFollowingOrgs&quot;:(\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>