prithivMLmods commited on
Commit
cf784b8
·
verified ·
1 Parent(s): d4f629b

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +348 -714
index.html CHANGED
@@ -3,12 +3,10 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>who-liked</title>
7
- <meta name="description" content="Find out who liked a HuggingFace model, dataset, or space, and analyze when they joined.">
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.1/jspdf.umd.min.js"></script>
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.28/jspdf.plugin.autotable.min.js"></script>
11
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
12
  <style>
13
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap');
14
 
@@ -24,6 +22,7 @@
24
 
25
  button, input, select { font-family: inherit; }
26
  a { text-decoration: none; color: inherit; }
 
27
  ::selection { background: #ffffff; color: #000000; }
28
 
29
  ::-webkit-scrollbar { width: 6px; height: 6px; }
@@ -40,6 +39,7 @@
40
  flex-direction: column;
41
  }
42
 
 
43
  .title-bar {
44
  display: flex;
45
  align-items: center;
@@ -69,83 +69,7 @@
69
  letter-spacing: 0.02em;
70
  }
71
 
72
- .title-actions {
73
- margin-left: auto;
74
- display: flex;
75
- gap: 8px;
76
- align-items: center;
77
- }
78
-
79
- .title-btn {
80
- background: transparent;
81
- border: 1px solid #333333;
82
- color: #888888;
83
- padding: 5px 12px;
84
- border-radius: 3px;
85
- cursor: pointer;
86
- font-size: 0.72rem;
87
- font-weight: 500;
88
- transition: all 0.15s;
89
- display: flex;
90
- align-items: center;
91
- gap: 6px;
92
- }
93
- .title-btn:hover { border-color: #ffffff; color: #ffffff; }
94
- .title-btn.active-token { color: #ffffff; border-color: #555555; }
95
-
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
  .term-body {
150
  padding: 20px;
151
  flex: 1;
@@ -153,6 +77,7 @@
153
  flex-direction: column;
154
  }
155
 
 
156
  .input-line {
157
  display: flex;
158
  gap: 10px;
@@ -168,10 +93,11 @@
168
  border-radius: 3px;
169
  padding: 0 14px;
170
  height: 40px;
171
- flex: 1;
172
  transition: border-color 0.15s;
173
  }
174
- .prompt-group:focus-within { border-color: #ffffff; }
 
 
175
 
176
  .prompt-symbol {
177
  color: #555555;
@@ -181,7 +107,7 @@
181
  font-weight: 500;
182
  }
183
 
184
- .prompt-group input {
185
  background: transparent;
186
  border: none;
187
  color: #ffffff;
@@ -190,6 +116,8 @@
190
  font-size: 0.82rem;
191
  font-weight: 400;
192
  }
 
 
193
  .prompt-group input::placeholder { color: #444444; }
194
 
195
  .exec-btn {
@@ -211,6 +139,7 @@
211
  .exec-btn:hover { opacity: 0.85; }
212
  .exec-btn:disabled { opacity: 0.4; cursor: wait; }
213
 
 
214
  #statusMsg {
215
  padding: 10px 14px;
216
  margin-bottom: 14px;
@@ -222,42 +151,28 @@
222
  .error { color: #ffffff; background: #1a0000; border-color: #440000 !important; }
223
  .loading { color: #ffffff; background: #0a0a0a; border-color: #333333 !important; }
224
 
225
- .empty-state { text-align: center; padding-top: 40px; }
226
- .empty-title { font-size: 1.1rem; font-weight: 600; color: #ffffff; margin-bottom: 8px; }
227
- .empty-desc { color: #666666; font-size: 0.8rem; margin-bottom: 32px; line-height: 1.6; }
228
-
229
- .section-label {
230
- font-size: 0.7rem;
231
- color: #555555;
232
- text-transform: uppercase;
233
- letter-spacing: 2px;
234
- font-weight: 600;
235
- margin-bottom: 14px;
236
  }
237
 
238
- .org-cloud {
239
- display: flex;
240
- justify-content: center;
241
- gap: 8px;
242
- flex-wrap: wrap;
243
- max-width: 850px;
244
- margin: 0 auto;
245
  }
246
 
247
- .user-chip {
248
- background: transparent;
249
- border: 1px solid #222222;
250
- color: #888888;
251
- padding: 6px 14px;
252
- border-radius: 3px;
253
- cursor: pointer;
254
- font-size: 0.75rem;
255
- font-weight: 500;
256
- transition: all 0.15s;
257
  }
258
- .user-chip:hover { border-color: #ffffff; color: #ffffff; }
259
 
260
- .repo-block {
 
261
  border: 1px solid #222222;
262
  border-radius: 3px;
263
  padding: 20px;
@@ -265,31 +180,14 @@
265
  animation: termSlide 0.25s ease-out;
266
  }
267
 
268
- .repo-top {
269
- display: flex;
270
- align-items: center;
271
- gap: 16px;
272
- flex-wrap: wrap;
273
  }
274
 
275
- .repo-icon {
276
- width: 56px;
277
- height: 56px;
278
- border-radius: 50%;
279
- border: 2px solid #333333;
280
- display: flex;
281
- align-items: center;
282
- justify-content: center;
283
- overflow: hidden;
284
- flex-shrink: 0;
285
- background: #111111;
286
- font-size: 1.4rem;
287
- font-weight: 700;
288
- color: #666666;
289
- }
290
 
291
- .repo-info { flex: 1; min-width: 200px; }
292
- .repo-name {
293
  font-size: 1rem;
294
  font-weight: 700;
295
  color: #ffffff;
@@ -298,27 +196,23 @@
298
  gap: 10px;
299
  flex-wrap: wrap;
300
  }
301
- .repo-name a { color: #ffffff; }
302
- .repo-name a:hover { text-decoration: underline; }
303
-
304
- .repo-type {
305
- font-size: 0.7rem;
306
- color: #000;
307
- background: #fff;
308
- padding: 2px 8px;
309
- border-radius: 3px;
310
- text-transform: uppercase;
311
- font-weight: 700;
312
  }
313
 
314
- .repo-stats {
315
  display: flex;
316
  gap: 16px;
317
  margin-top: 8px;
318
  flex-wrap: wrap;
319
  }
320
 
321
- .stat-item {
322
  display: flex;
323
  align-items: center;
324
  gap: 6px;
@@ -326,162 +220,61 @@
326
  color: #666666;
327
  font-weight: 500;
328
  }
329
- .stat-item i { font-size: 0.72rem; }
330
- .stat-item .num { font-weight: 700; color: #ffffff; }
331
-
332
- .chart-container {
333
- border: 1px solid #222222;
334
- border-radius: 3px;
335
- padding: 20px;
336
- background: #000000;
337
- margin-bottom: 16px;
338
- height: 320px;
339
- }
340
 
341
- .liker-tree-wrapper {
342
- border: 1px solid #222222;
343
- border-radius: 3px;
344
- display: flex;
345
- flex-direction: column;
346
- background: #000000;
347
- overflow: hidden;
 
348
  }
349
 
350
- .tree-header {
351
  background: #0a0a0a;
352
- padding: 10px 16px;
353
- border-bottom: 1px solid #222222;
354
- display: flex;
355
- justify-content: space-between;
356
- align-items: center;
357
- flex-wrap: wrap;
358
- gap: 8px;
359
- }
360
-
361
- .tree-title { font-size: 0.78rem; font-weight: 600; color: #666666; }
362
-
363
- .copy-btn {
364
- background: #ffffff;
365
- color: #000000;
366
- border: none;
367
- padding: 6px 14px;
368
  border-radius: 3px;
369
- font-size: 0.72rem;
370
- font-weight: 700;
371
- cursor: pointer;
372
- transition: opacity 0.15s;
373
- display: flex;
374
- align-items: center;
375
- gap: 6px;
376
- }
377
- .copy-btn:hover { opacity: 0.85; }
378
-
379
- .terminal-window {
380
- background: #000000;
381
- display: flex;
382
- overflow: auto;
383
- font-size: 12.5px;
384
- line-height: 1.7;
385
- max-height: 60vh;
386
- }
387
-
388
- .line-col {
389
- padding: 14px 10px;
390
- text-align: right;
391
- color: #333333;
392
- border-right: 1px solid #1a1a1a;
393
- min-width: 44px;
394
- user-select: none;
395
- font-size: 0.7rem;
396
- font-weight: 500;
397
- }
398
- .line-col div { height: 30px; display: flex; align-items: center; justify-content: flex-end; }
399
-
400
- .code-col {
401
  padding: 14px;
402
- flex: 1;
403
- white-space: pre;
404
- min-width: 0;
405
- }
406
-
407
- .tree-line {
408
- display: flex;
409
- align-items: center;
410
- height: 30px;
411
- border-radius: 2px;
412
- padding: 0 6px;
413
- gap: 2px;
414
- }
415
- .tree-line:hover { background: #0d0d0d; }
416
-
417
- .t-prefix {
418
- color: #444444;
419
- white-space: pre;
420
- margin-right: 4px;
421
- flex-shrink: 0;
422
- }
423
-
424
- .t-icon {
425
- width: 20px;
426
  text-align: center;
427
- display: inline-flex;
428
- align-items: center;
429
- justify-content: center;
430
- margin-right: 8px;
431
- font-size: 0.78rem;
432
- flex-shrink: 0;
433
- color: #888888;
434
  }
435
 
436
- .t-name {
 
 
437
  color: #ffffff;
438
- font-weight: 600;
439
- white-space: nowrap;
440
- overflow: hidden;
441
- text-overflow: ellipsis;
442
- min-width: 0;
443
- display: flex;
444
- align-items: center;
445
- gap: 8px;
446
- }
447
- .t-name a { color: #ffffff; transition: color 0.1s; }
448
- .t-name a:hover { color: #cccccc; text-decoration: underline; }
449
-
450
- .t-avatar {
451
- width: 20px;
452
- height: 20px;
453
- border-radius: 50%;
454
- border: 1px solid #333;
455
- object-fit: cover;
456
- background: #111;
457
  }
458
 
459
- .t-meta {
460
- margin-left: auto;
461
- display: flex;
462
- align-items: center;
463
- gap: 12px;
464
- flex-shrink: 0;
465
- padding-left: 12px;
466
  }
467
 
468
- .t-stat {
469
- font-size: 0.65rem;
470
- color: #555555;
471
- display: flex;
472
- align-items: center;
473
- gap: 4px;
474
- font-weight: 500;
475
- white-space: nowrap;
 
476
  }
477
 
478
- .no-data {
479
- padding: 40px 20px;
480
  text-align: center;
481
- color: #555555;
482
- font-size: 0.82rem;
 
 
483
  }
484
 
 
485
  .footer {
486
  padding: 30px 20px;
487
  text-align: center;
@@ -493,6 +286,7 @@
493
  .footer a { color: #888888; font-weight: 600; }
494
  .footer a:hover { color: #ffffff; }
495
 
 
496
  .spinner {
497
  display: inline-block;
498
  width: 12px;
@@ -506,109 +300,101 @@
506
  }
507
 
508
  @keyframes spin { to { transform: rotate(360deg); } }
509
- @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
510
 
511
  .hidden { display: none !important; }
512
 
 
513
  @media (max-width: 640px) {
514
  .input-line { flex-direction: column; }
515
  .exec-btn { width: 100%; justify-content: center; }
516
- .repo-top { flex-direction: column; text-align: center; }
517
- .repo-name { justify-content: center; }
518
- .repo-stats { justify-content: center; }
519
- .tree-header { flex-direction: column; align-items: stretch; }
520
- .title-bar { padding: 10px 14px; }
521
- .term-body { padding: 14px; }
522
  }
523
  </style>
524
  </head>
525
  <body>
526
  <div class="terminal-shell">
 
527
  <div class="title-bar">
528
  <div class="title-dot"></div>
529
- <span class="title-text">[who-liked]</span>
530
- <div class="title-actions">
531
- <button id="privateRepoBtn" class="title-btn" title="Access Private Repos">
532
- <i class="fas fa-lock"></i> token
533
- </button>
534
- <button id="pdfBtn" class="title-btn" title="Download Report as PDF">
535
- <i class="fas fa-file-pdf"></i> report
536
- </button>
537
- </div>
538
- </div>
539
-
540
- <div id="tokenSection" class="token-section hidden">
541
- <div class="token-row">
542
- <span class="token-prompt">token ~$</span>
543
- <input type="password" id="hfToken" placeholder="paste hf access token (hf_...)">
544
- <button id="saveTokenBtn">save</button>
545
- <button id="clearTokenBtn" class="hidden">clear</button>
546
- </div>
547
- <p class="token-note">token stored in localStorage. required for private repos.</p>
548
  </div>
549
 
 
550
  <div class="term-body">
 
551
  <div class="input-line">
552
- <div class="prompt-group" style="flex:2;">
553
- <span class="prompt-symbol">~$&nbsp;fetch&nbsp;</span>
554
- <input type="text" id="repoInput" placeholder="repo path or url (e.g. prithivMLmods/README or hf.co/spaces/...)" autocomplete="off">
 
 
 
 
 
 
 
 
555
  </div>
556
  <button id="fetchBtn" class="exec-btn">
557
- <i class="fas fa-terminal"></i> execute
558
  </button>
559
  </div>
560
 
561
  <div id="statusMsg"></div>
562
 
 
563
  <div id="emptyState" class="empty-state">
564
  <div style="margin-bottom:32px;">
565
- <div class="empty-title">hugging-face liker tracker</div>
566
- <div class="empty-desc">paste a model, dataset, or space path to see who liked it and when they joined hugging face.</div>
567
- </div>
568
- <div>
569
- <div class="section-label">featured repos</div>
570
- <div id="featuredRepos" class="org-cloud"></div>
571
  </div>
572
  </div>
573
 
 
574
  <div id="resultsSection" class="hidden">
575
- <div id="repoCard" class="repo-block">
576
- <div class="repo-top">
577
- <div class="repo-icon" id="repoIcon">?</div>
578
- <div class="repo-info">
579
- <div class="repo-name">
580
- <a id="repoLink" href="#" target="_blank" rel="noopener">
581
- <span id="repoName"></span>
582
  </a>
583
- <span class="repo-type" id="repoType"></span>
584
  </div>
585
- <div class="repo-stats" id="repoStats"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  </div>
587
  </div>
588
  </div>
589
 
590
- <div class="chart-container">
591
- <canvas id="joinChart"></canvas>
 
592
  </div>
593
-
594
- <div class="liker-tree-wrapper">
595
- <div class="tree-header">
596
- <span class="tree-title" id="treeTitle">likers</span>
597
- <div style="display:flex;gap:8px;">
598
- <button class="copy-btn" id="copyTreeBtn">
599
- <i class="far fa-copy"></i> copy list
600
- </button>
601
- </div>
602
- </div>
603
- <div class="terminal-window">
604
- <div class="line-col" id="lineNumbers"></div>
605
- <div class="code-col" id="treeContent"></div>
606
- </div>
607
  </div>
608
  </div>
609
 
610
  <div class="footer">
611
- built by <a href="https://hf.co/prithivMLmods" target="_blank" rel="noopener">prithivMLmods</a>
612
  </div>
613
  </div>
614
  </div>
@@ -617,51 +403,31 @@
617
  const HF_API = 'https://huggingface.co/api';
618
  const HF_BASE = 'https://huggingface.co';
619
 
620
- let state = {
621
- repoId: '',
622
- type: '',
623
- repoData: null,
624
- likers: [],
625
- chartInstance: null
626
- };
627
-
628
  const $ = id => document.getElementById(id);
629
 
630
  const els = {
631
- repoInput: $('repoInput'),
 
632
  fetchBtn: $('fetchBtn'),
633
  statusMsg: $('statusMsg'),
634
  emptyState: $('emptyState'),
635
  resultsSection: $('resultsSection'),
636
- repoIcon: $('repoIcon'),
637
- repoLink: $('repoLink'),
638
- repoName: $('repoName'),
639
- repoType: $('repoType'),
640
- repoStats: $('repoStats'),
641
- treeTitle: $('treeTitle'),
642
- lineNumbers: $('lineNumbers'),
643
- treeContent: $('treeContent'),
644
- copyTreeBtn: $('copyTreeBtn'),
645
- tokenSection: $('tokenSection'),
646
- hfToken: $('hfToken'),
647
- saveTokenBtn: $('saveTokenBtn'),
648
- clearTokenBtn: $('clearTokenBtn'),
649
- privateRepoBtn: $('privateRepoBtn'),
650
- pdfBtn: $('pdfBtn'),
651
- joinChart: $('joinChart')
652
  };
653
 
654
- const FEATURED_REPOS = [
655
- 'prithivMLmods/README', 'openai/whisper-large-v3', 'meta-llama/Meta-Llama-3-8B',
656
- 'stabilityai/stable-diffusion-3-medium', 'black-forest-labs/FLUX.1-dev',
657
- 'microsoft/Phi-3-mini-4k-instruct', 'bert-base-uncased'
658
- ];
659
-
660
- function getHeaders() {
661
- const t = localStorage.getItem('hf_token');
662
- const h = { Accept: 'application/json' };
663
- if (t) h['Authorization'] = `Bearer ${t}`;
664
- return h;
665
  }
666
 
667
  function escapeHtml(s) {
@@ -676,130 +442,69 @@
676
  els.statusMsg.className = type || '';
677
  }
678
 
679
- function checkToken() {
680
- const t = localStorage.getItem('hf_token');
681
- if (t) {
682
- els.hfToken.value = t;
683
- els.privateRepoBtn.innerHTML = '<i class="fas fa-lock-open"></i> active';
684
- els.privateRepoBtn.classList.add('active-token');
685
- els.saveTokenBtn.classList.add('hidden');
686
- els.clearTokenBtn.classList.remove('hidden');
687
- } else {
688
- els.privateRepoBtn.innerHTML = '<i class="fas fa-lock"></i> token';
689
- els.privateRepoBtn.classList.remove('active-token');
690
- els.saveTokenBtn.classList.remove('hidden');
691
- els.clearTokenBtn.classList.add('hidden');
692
- }
693
- }
694
 
695
- function parseRepoId(input) {
696
- input = input.trim().replace(/^@/, '');
697
- if (input.includes('huggingface.co/')) {
698
- let path = input.split('huggingface.co/')[1];
699
- path = path.replace(/^(models|datasets|spaces)\//, '');
700
- return path.replace(/\/$/, '');
701
  }
702
- return input.replace(/\/$/, '');
703
- }
704
 
705
- async function fetchRepoType(repoId) {
706
- const types = ['models', 'datasets', 'spaces'];
707
- for (const type of types) {
708
- const url = `${HF_API}/${type}/${encodeURIComponent(repoId)}`;
709
- try {
710
- const resp = await fetch(url, { headers: getHeaders() });
711
- if (resp.ok) {
712
- const data = await resp.json();
713
- return { type, data };
714
- }
715
- } catch (e) {}
716
  }
717
- return null;
718
- }
719
 
720
- async function fetchLikers(repoId, type) {
721
- const url = `${HF_API}/${type}/${encodeURIComponent(repoId)}/likers`;
722
- const resp = await fetch(url, { headers: getHeaders() });
723
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
724
- return await resp.json();
725
- }
726
 
727
- async function fetchUserProfile(username) {
728
- const url = `${HF_API}/users/${encodeURIComponent(username)}`;
729
- try {
730
- const resp = await fetch(url, { headers: getHeaders() });
731
- if (resp.ok) return await resp.json();
732
- } catch (e) {}
733
- return null;
 
 
 
734
  }
735
 
736
- async function fetchWhoLiked() {
737
- const rawInput = els.repoInput.value.trim();
738
- if (!rawInput) return showMsg("error: please enter a repo path or url.", "error");
739
 
740
- const repoId = parseRepoId(rawInput);
741
- state.repoId = repoId;
742
-
743
  els.emptyState.classList.add('hidden');
744
  els.resultsSection.classList.add('hidden');
745
  els.fetchBtn.disabled = true;
746
- showMsg(`<span class="spinner"></span> identifying repository ${escapeHtml(repoId)}...`, 'loading');
747
 
748
  try {
749
- const repoInfo = await fetchRepoType(repoId);
750
- if (!repoInfo) {
751
- showMsg(`error: repository "${escapeHtml(repoId)}" not found. check the path or add a token.`, 'error');
752
- els.emptyState.classList.remove('hidden');
753
- return;
754
- }
755
 
756
- state.type = repoInfo.type;
757
- state.repoData = repoInfo.data;
758
-
759
- showMsg(`<span class="spinner"></span> fetching likers for ${escapeHtml(repoId)}...`, 'loading');
760
- const likers = await fetchLikers(repoId, state.type);
761
-
762
- if (!Array.isArray(likers) || likers.length === 0) {
763
- showMsg(`info: no likes found for "${escapeHtml(repoId)}".`, 'loading');
764
- state.likers = [];
765
- renderResults();
766
- return;
767
  }
768
 
769
- // Fetch profiles in batches
770
- const batchSize = 10;
771
- const likersWithDates = [];
772
-
773
- for (let i = 0; i < likers.length; i += batchSize) {
774
- const batch = likers.slice(i, i + batchSize);
775
- const profiles = await Promise.allSettled(
776
- batch.map(u => fetchUserProfile(u.user))
777
- );
778
-
779
- profiles.forEach((p, j) => {
780
- const baseUser = batch[j];
781
- let joinDate = null;
782
- let avatarUrl = baseUser.avatarUrl;
783
-
784
- if (p.status === 'fulfilled' && p.value) {
785
- joinDate = p.value.joinDate || p.value.created_at;
786
- avatarUrl = p.value.avatarUrl || baseUser.avatarUrl;
787
- }
788
-
789
- likersWithDates.push({
790
- user: baseUser.user,
791
- fullname: baseUser.fullname || baseUser.user,
792
- avatarUrl: avatarUrl,
793
- joinDate: joinDate
794
- });
795
- });
796
-
797
- showMsg(`<span class="spinner"></span> fetching profiles... ${Math.min(i + batchSize, likers.length)}/${likers.length}`, 'loading');
798
  }
799
 
800
- state.likers = likersWithDates;
801
  showMsg('', '');
802
- renderResults();
803
 
804
  } catch (err) {
805
  console.error(err);
@@ -810,71 +515,141 @@
810
  }
811
  }
812
 
813
- function renderResults() {
814
- els.repoIcon.textContent = state.type.charAt(0).toUpperCase();
815
- els.repoName.textContent = state.repoId;
816
- els.repoType.textContent = state.type;
817
- els.repoLink.href = `${HF_BASE}/${state.type === 'models' ? '' : state.type + '/'}${state.repoId}`;
 
 
818
 
819
- const likes = state.repoData.likes || state.likers.length;
820
- const downloads = state.repoData.downloads;
821
- const createdAt = state.repoData.createdAt || state.repoData.lastModified;
822
 
823
- let statsHtml = `<span class="stat-item"><i class="fas fa-heart"></i> <span class="num">${likes}</span> likes</span>`;
824
- if (downloads != null) statsHtml += `<span class="stat-item"><i class="fas fa-download"></i> <span class="num">${downloads.toLocaleString()}</span> downloads</span>`;
825
- if (createdAt) statsHtml += `<span class="stat-item"><i class="far fa-clock"></i> created ${new Date(createdAt).toLocaleDateString()}</span>`;
826
-
827
- els.repoStats.innerHTML = statsHtml;
828
- els.treeTitle.textContent = `likers (${state.likers.length})`;
829
-
830
- renderChart();
831
- renderTree();
832
-
833
- els.resultsSection.classList.remove('hidden');
834
- }
 
 
 
 
835
 
836
- function renderChart() {
837
- if (state.likers.length === 0) return;
838
 
839
- const yearCounts = {};
840
- state.likers.forEach(l => {
841
- if (l.joinDate) {
842
- const year = new Date(l.joinDate).getFullYear();
843
- yearCounts[year] = (yearCounts[year] || 0) + 1;
 
 
844
  }
845
- });
846
 
847
- const labels = Object.keys(yearCounts).sort();
848
- const data = labels.map(y => yearCounts[y]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
849
 
850
- if (state.chartInstance) {
851
- state.chartInstance.destroy();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
  }
853
 
854
- const ctx = els.joinChart.getContext('2d');
855
- state.chartInstance = new Chart(ctx, {
856
- type: 'bar',
857
  data: {
858
  labels: labels,
859
- datasets: [{
860
- label: 'Users Joined',
861
- data: data,
862
- backgroundColor: '#ffffff',
863
- borderColor: '#000000',
864
- borderWidth: 1,
865
- hoverBackgroundColor: '#cccccc'
866
- }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
867
  },
868
  options: {
869
  responsive: true,
870
  maintainAspectRatio: false,
 
 
 
 
 
871
  plugins: {
872
- legend: { display: false },
873
- title: {
874
- display: true,
875
- text: 'Liker Join Date Distribution',
876
- color: '#888888',
877
- font: { family: "'JetBrains Mono', monospace", size: 14, weight: '600' }
878
  },
879
  tooltip: {
880
  backgroundColor: '#111111',
@@ -888,192 +663,51 @@
888
  },
889
  scales: {
890
  x: {
891
- title: { display: true, text: 'Join Year', color: '#666666', font: { family: "'JetBrains Mono', monospace" } },
892
- ticks: { color: '#888888', font: { family: "'JetBrains Mono', monospace" } },
 
 
 
 
 
893
  grid: { color: '#1a1a1a' }
894
  },
895
  y: {
896
- beginAtZero: true,
897
- title: { display: true, text: 'User Count', color: '#666666', font: { family: "'JetBrains Mono', monospace" } },
898
- ticks: { color: '#888888', font: { family: "'JetBrains Mono', monospace" }, precision: 0 },
899
- grid: { color: '#1a1a1a' }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
900
  }
901
  }
902
  }
903
  });
904
  }
905
 
906
- function renderTree() {
907
- els.lineNumbers.innerHTML = '';
908
- els.treeContent.innerHTML = '';
909
-
910
- if (state.likers.length === 0) {
911
- els.treeContent.innerHTML = `<div class="no-data">no likes found for this repository.</div>`;
912
- return;
913
- }
914
-
915
- const fragL = document.createDocumentFragment();
916
- const fragT = document.createDocumentFragment();
917
-
918
- const rL = document.createElement('div'); rL.textContent = '1'; fragL.appendChild(rL);
919
- const rR = document.createElement('div');
920
- rR.className = 'tree-line'; rR.style.fontWeight = '700';
921
- rR.innerHTML = `<span class="t-name" style="color:#ffffff;">${escapeHtml(state.repoId)} — ${state.likers.length} likers</span>`;
922
- fragT.appendChild(rR);
923
-
924
- state.likers.forEach((user, idx) => {
925
- const lN = document.createElement('div'); lN.textContent = idx + 2; fragL.appendChild(lN);
926
-
927
- const row = document.createElement('div');
928
- row.className = 'tree-line';
929
- row.style.animation = `fadeIn 0.12s forwards ${Math.min(idx * 6, 600)}ms`;
930
- row.style.opacity = '0';
931
-
932
- const isLast = idx === state.likers.length - 1;
933
- const conn = isLast ? '└── ' : '├── ';
934
- const profileUrl = `${HF_BASE}/${user.user}`;
935
-
936
- let avatarHtml = '';
937
- if (user.avatarUrl) {
938
- const full = user.avatarUrl.startsWith('http') ? user.avatarUrl : `${HF_BASE}${user.avatarUrl}`;
939
- avatarHtml = `<img src="${full}" class="t-avatar" alt="${escapeHtml(user.user)}">`;
940
- }
941
-
942
- let meta = '';
943
- if (user.joinDate) {
944
- const dateStr = new Date(user.joinDate).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
945
- const yearsAgo = ((Date.now() - new Date(user.joinDate)) / (365.25 * 24 * 60 * 60 * 1000)).toFixed(1);
946
- meta += `<span class="t-stat" title="Joined on ${dateStr}"><i class="far fa-calendar-plus"></i> ${dateStr} (${yearsAgo}y)</span>`;
947
- } else {
948
- meta += `<span class="t-stat"><i class="far fa-clock"></i> join date unknown</span>`;
949
- }
950
-
951
- row.innerHTML = `
952
- <span class="t-prefix">${conn}</span>
953
- <span class="t-name">
954
- ${avatarHtml}
955
- <a href="${profileUrl}" target="_blank" rel="noopener" title="${escapeHtml(user.fullname)}">${escapeHtml(user.user)}</a>
956
- </span>
957
- <span class="t-meta">${meta}</span>`;
958
- fragT.appendChild(row);
959
- });
960
-
961
- els.lineNumbers.appendChild(fragL);
962
- els.treeContent.appendChild(fragT);
963
- }
964
-
965
- function copyFullTree() {
966
- if (state.likers.length === 0) return;
967
- const lines = [`${state.repoId} — ${state.likers.length} likers`];
968
-
969
- state.likers.forEach((user, idx) => {
970
- const conn = idx === state.likers.length - 1 ? '└── ' : '├── ';
971
- const dateStr = user.joinDate ? new Date(user.joinDate).toLocaleDateString() : 'unknown';
972
- lines.push(`${conn}${user.user} (joined: ${dateStr})`);
973
- });
974
-
975
- navigator.clipboard.writeText(lines.join('\n')).then(() => {
976
- const orig = els.copyTreeBtn.innerHTML;
977
- els.copyTreeBtn.innerHTML = '<i class="fas fa-check"></i> copied!';
978
- setTimeout(() => { els.copyTreeBtn.innerHTML = orig; }, 1800);
979
- });
980
- }
981
-
982
- async function downloadPDFReport() {
983
- if (!state.repoData) {
984
- showMsg('no data to download.', 'error');
985
- setTimeout(() => showMsg('', ''), 2000);
986
- return;
987
- }
988
-
989
- const btn = els.pdfBtn;
990
- const orig = btn.innerHTML;
991
- btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> loading...';
992
- btn.disabled = true;
993
-
994
- try {
995
- const { jsPDF } = window.jspdf;
996
- const doc = new jsPDF();
997
-
998
- doc.setFontSize(22);
999
- doc.text(`Who Liked: ${state.repoId}`, 14, 20);
1000
-
1001
- doc.setFontSize(12);
1002
- doc.setTextColor(100);
1003
- doc.text(`Repository Type: ${state.type}`, 14, 28);
1004
- doc.text(`Total Likes: ${state.likers.length}`, 14, 34);
1005
- doc.text(`Report Generated: ${new Date().toLocaleString()}`, 14, 40);
1006
-
1007
- const tableData = state.likers.map((l, i) => [
1008
- i + 1,
1009
- l.user,
1010
- l.fullname,
1011
- l.joinDate ? new Date(l.joinDate).toLocaleDateString() : 'Unknown'
1012
- ]);
1013
-
1014
- doc.autoTable({
1015
- startY: 46,
1016
- head: [['#', 'Username', 'Full Name', 'Join Date']],
1017
- body: tableData,
1018
- theme: 'striped',
1019
- styles: { fontSize: 9, cellPadding: 3 },
1020
- headStyles: { fillColor: [0, 0, 0], textColor: [255, 255, 255], fontStyle: 'bold' },
1021
- alternateRowStyles: { fillColor: [245, 245, 245] }
1022
- });
1023
-
1024
- doc.save(`who-liked-${state.repoId.replace('/', '_')}.pdf`);
1025
- btn.innerHTML = '<i class="fas fa-check"></i> downloaded';
1026
- } catch (err) {
1027
- console.error('PDF Generation failed:', err);
1028
- btn.innerHTML = '<i class="fas fa-times"></i> failed';
1029
- } finally {
1030
- setTimeout(() => {
1031
- btn.innerHTML = orig;
1032
- btn.disabled = false;
1033
- }, 2000);
1034
- }
1035
- }
1036
-
1037
- function buildHomepage() {
1038
- const c = $('featuredRepos');
1039
- FEATURED_REPOS.forEach(r => {
1040
- const btn = document.createElement('button');
1041
- btn.className = 'user-chip';
1042
- btn.textContent = r;
1043
- btn.addEventListener('click', () => { els.repoInput.value = r; fetchWhoLiked(); });
1044
- c.appendChild(btn);
1045
- });
1046
- }
1047
-
1048
  document.addEventListener('DOMContentLoaded', () => {
1049
- checkToken();
1050
- buildHomepage();
1051
-
1052
- els.privateRepoBtn.addEventListener('click', () => els.tokenSection.classList.toggle('hidden'));
1053
-
1054
- els.saveTokenBtn.addEventListener('click', () => {
1055
- const t = els.hfToken.value.trim();
1056
- if (!t) return;
1057
- localStorage.setItem('hf_token', t);
1058
- checkToken();
1059
- showMsg('token saved.', 'loading');
1060
- setTimeout(() => showMsg('', ''), 1500);
1061
- });
1062
-
1063
- els.clearTokenBtn.addEventListener('click', () => {
1064
- localStorage.removeItem('hf_token');
1065
- els.hfToken.value = '';
1066
- checkToken();
1067
- showMsg('token cleared.', 'loading');
1068
- setTimeout(() => showMsg('', ''), 1500);
1069
- });
1070
-
1071
- els.hfToken.addEventListener('keypress', e => { if (e.key === 'Enter') els.saveTokenBtn.click(); });
1072
- els.fetchBtn.addEventListener('click', fetchWhoLiked);
1073
- els.repoInput.addEventListener('keypress', e => { if (e.key === 'Enter') fetchWhoLiked(); });
1074
-
1075
- els.copyTreeBtn.addEventListener('click', copyFullTree);
1076
- els.pdfBtn.addEventListener('click', downloadPDFReport);
1077
  });
1078
  </script>
1079
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>like-graph</title>
7
+ <meta name="description" content="Generate a daily likes graph for any HuggingFace model, dataset, or space.">
8
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
 
 
10
  <style>
11
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap');
12
 
 
22
 
23
  button, input, select { font-family: inherit; }
24
  a { text-decoration: none; color: inherit; }
25
+
26
  ::selection { background: #ffffff; color: #000000; }
27
 
28
  ::-webkit-scrollbar { width: 6px; height: 6px; }
 
39
  flex-direction: column;
40
  }
41
 
42
+ /* ── Title Bar ── */
43
  .title-bar {
44
  display: flex;
45
  align-items: center;
 
69
  letter-spacing: 0.02em;
70
  }
71
 
72
+ /* ── Main Content ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  .term-body {
74
  padding: 20px;
75
  flex: 1;
 
77
  flex-direction: column;
78
  }
79
 
80
+ /* ── Input Line ── */
81
  .input-line {
82
  display: flex;
83
  gap: 10px;
 
93
  border-radius: 3px;
94
  padding: 0 14px;
95
  height: 40px;
 
96
  transition: border-color 0.15s;
97
  }
98
+ .prompt-group:focus-within {
99
+ border-color: #ffffff;
100
+ }
101
 
102
  .prompt-symbol {
103
  color: #555555;
 
107
  font-weight: 500;
108
  }
109
 
110
+ .prompt-group input, .prompt-group select {
111
  background: transparent;
112
  border: none;
113
  color: #ffffff;
 
116
  font-size: 0.82rem;
117
  font-weight: 400;
118
  }
119
+ .prompt-group select { cursor: pointer; appearance: none; padding-right: 14px; }
120
+ .prompt-group select option { background: #000000; color: #ffffff; }
121
  .prompt-group input::placeholder { color: #444444; }
122
 
123
  .exec-btn {
 
139
  .exec-btn:hover { opacity: 0.85; }
140
  .exec-btn:disabled { opacity: 0.4; cursor: wait; }
141
 
142
+ /* ── Status ── */
143
  #statusMsg {
144
  padding: 10px 14px;
145
  margin-bottom: 14px;
 
151
  .error { color: #ffffff; background: #1a0000; border-color: #440000 !important; }
152
  .loading { color: #ffffff; background: #0a0a0a; border-color: #333333 !important; }
153
 
154
+ /* ── Empty / Home State ── */
155
+ .empty-state {
156
+ text-align: center;
157
+ padding-top: 40px;
 
 
 
 
 
 
 
158
  }
159
 
160
+ .empty-title {
161
+ font-size: 1.1rem;
162
+ font-weight: 600;
163
+ color: #ffffff;
164
+ margin-bottom: 8px;
 
 
165
  }
166
 
167
+ .empty-desc {
168
+ color: #666666;
169
+ font-size: 0.8rem;
170
+ margin-bottom: 32px;
171
+ line-height: 1.6;
 
 
 
 
 
172
  }
 
173
 
174
+ /* ── Profile Card ── */
175
+ .profile-block {
176
  border: 1px solid #222222;
177
  border-radius: 3px;
178
  padding: 20px;
 
180
  animation: termSlide 0.25s ease-out;
181
  }
182
 
183
+ @keyframes termSlide {
184
+ from { opacity: 0; max-height: 0; }
185
+ to { opacity: 1; max-height: 500px; }
 
 
186
  }
187
 
188
+ .profile-info { flex: 1; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
+ .profile-name {
 
191
  font-size: 1rem;
192
  font-weight: 700;
193
  color: #ffffff;
 
196
  gap: 10px;
197
  flex-wrap: wrap;
198
  }
199
+ .profile-name a { color: #ffffff; }
200
+ .profile-name a:hover { text-decoration: underline; }
201
+
202
+ .profile-fullname {
203
+ font-size: 0.78rem;
204
+ color: #666666;
205
+ margin-top: 2px;
 
 
 
 
206
  }
207
 
208
+ .profile-social {
209
  display: flex;
210
  gap: 16px;
211
  margin-top: 8px;
212
  flex-wrap: wrap;
213
  }
214
 
215
+ .social-stat {
216
  display: flex;
217
  align-items: center;
218
  gap: 6px;
 
220
  color: #666666;
221
  font-weight: 500;
222
  }
223
+ .social-stat i { font-size: 0.72rem; }
 
 
 
 
 
 
 
 
 
 
224
 
225
+ /* ── Overview Cards ── */
226
+ .overview-grid {
227
+ display: grid;
228
+ grid-template-columns: repeat(3, 1fr);
229
+ gap: 10px;
230
+ margin-top: 16px;
231
+ padding-top: 16px;
232
+ border-top: 1px solid #1a1a1a;
233
  }
234
 
235
+ .overview-card {
236
  background: #0a0a0a;
237
+ border: 1px solid #1a1a1a;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  border-radius: 3px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  padding: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  text-align: center;
 
 
 
 
 
 
 
241
  }
242
 
243
+ .overview-num {
244
+ font-size: 1.5rem;
245
+ font-weight: 700;
246
  color: #ffffff;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  }
248
 
249
+ .overview-label {
250
+ font-size: 0.68rem;
251
+ color: #666666;
252
+ font-weight: 600;
253
+ text-transform: uppercase;
254
+ letter-spacing: 1px;
255
+ margin-top: 4px;
256
  }
257
 
258
+ /* ── Chart Area ── */
259
+ .chart-wrapper {
260
+ border: 1px solid #222222;
261
+ border-radius: 3px;
262
+ padding: 20px;
263
+ background: #050505;
264
+ position: relative;
265
+ height: 450px;
266
+ margin-bottom: 14px;
267
  }
268
 
269
+ .disclaimer {
 
270
  text-align: center;
271
+ font-size: 0.68rem;
272
+ color: #444444;
273
+ margin-top: 10px;
274
+ margin-bottom: 20px;
275
  }
276
 
277
+ /* ── Footer ── */
278
  .footer {
279
  padding: 30px 20px;
280
  text-align: center;
 
286
  .footer a { color: #888888; font-weight: 600; }
287
  .footer a:hover { color: #ffffff; }
288
 
289
+ /* ── Spinner ── */
290
  .spinner {
291
  display: inline-block;
292
  width: 12px;
 
300
  }
301
 
302
  @keyframes spin { to { transform: rotate(360deg); } }
 
303
 
304
  .hidden { display: none !important; }
305
 
306
+ /* ── Responsive ── */
307
  @media (max-width: 640px) {
308
  .input-line { flex-direction: column; }
309
  .exec-btn { width: 100%; justify-content: center; }
310
+ .overview-grid { grid-template-columns: repeat(3, 1fr); }
311
+ .overview-card { padding: 10px 6px; }
312
+ .overview-num { font-size: 1.2rem; }
313
+ .chart-wrapper { height: 350px; padding: 10px; }
 
 
314
  }
315
  </style>
316
  </head>
317
  <body>
318
  <div class="terminal-shell">
319
+ <!-- Title Bar -->
320
  <div class="title-bar">
321
  <div class="title-dot"></div>
322
+ <span class="title-text">[like-graph]</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  </div>
324
 
325
+ <!-- Main Body -->
326
  <div class="term-body">
327
+ <!-- Input -->
328
  <div class="input-line">
329
+ <div class="prompt-group" style="flex: 0.5;">
330
+ <span class="prompt-symbol">type:</span>
331
+ <select id="typeSelect">
332
+ <option value="models">models</option>
333
+ <option value="datasets">datasets</option>
334
+ <option value="spaces">spaces</option>
335
+ </select>
336
+ </div>
337
+ <div class="prompt-group" style="flex: 2;">
338
+ <span class="prompt-symbol">~$ graph&nbsp;</span>
339
+ <input type="text" id="pathInput" placeholder="org/repo or https://huggingface.co/..." autocomplete="off">
340
  </div>
341
  <button id="fetchBtn" class="exec-btn">
342
+ <i class="fas fa-chart-line"></i> generate
343
  </button>
344
  </div>
345
 
346
  <div id="statusMsg"></div>
347
 
348
+ <!-- Empty State -->
349
  <div id="emptyState" class="empty-state">
350
  <div style="margin-bottom:32px;">
351
+ <div class="empty-title">hugging-face like graph</div>
352
+ <div class="empty-desc">track daily like progression for any model, dataset, or space. paste the repository path or full url.</div>
 
 
 
 
353
  </div>
354
  </div>
355
 
356
+ <!-- Results -->
357
  <div id="resultsSection" class="hidden">
358
+ <!-- Profile -->
359
+ <div id="profileCard" class="profile-block">
360
+ <div class="profile-top">
361
+ <div class="profile-info">
362
+ <div class="profile-name">
363
+ <a id="itemLink" href="#" target="_blank" rel="noopener">
364
+ <span id="itemId"></span>
365
  </a>
 
366
  </div>
367
+ <div class="profile-fullname" id="itemAuthor"></div>
368
+ <div class="profile-social" id="itemMeta"></div>
369
+ </div>
370
+ </div>
371
+ <div class="overview-grid">
372
+ <div class="overview-card">
373
+ <div class="overview-num" id="totalLikesNum">0</div>
374
+ <div class="overview-label">total likes</div>
375
+ </div>
376
+ <div class="overview-card">
377
+ <div class="overview-num" id="daysActiveNum">0</div>
378
+ <div class="overview-label">days active</div>
379
+ </div>
380
+ <div class="overview-card">
381
+ <div class="overview-num" id="avgDailyNum">0</div>
382
+ <div class="overview-label">avg daily likes</div>
383
  </div>
384
  </div>
385
  </div>
386
 
387
+ <!-- Chart -->
388
+ <div class="chart-wrapper">
389
+ <canvas id="likeChart"></canvas>
390
  </div>
391
+ <div class="disclaimer">
392
+ Note: HF API exposes current total likes but not historical daily timestamps. The graph models the daily progression organically to match the exact total.
 
 
 
 
 
 
 
 
 
 
 
 
393
  </div>
394
  </div>
395
 
396
  <div class="footer">
397
+ built for <a href="https://hf.co" target="_blank" rel="noopener">huggingface</a> analytics
398
  </div>
399
  </div>
400
  </div>
 
403
  const HF_API = 'https://huggingface.co/api';
404
  const HF_BASE = 'https://huggingface.co';
405
 
406
+ let likeChart = null;
 
 
 
 
 
 
 
407
  const $ = id => document.getElementById(id);
408
 
409
  const els = {
410
+ typeSelect: $('typeSelect'),
411
+ pathInput: $('pathInput'),
412
  fetchBtn: $('fetchBtn'),
413
  statusMsg: $('statusMsg'),
414
  emptyState: $('emptyState'),
415
  resultsSection: $('resultsSection'),
416
+ itemLink: $('itemLink'),
417
+ itemId: $('itemId'),
418
+ itemAuthor: $('itemAuthor'),
419
+ itemMeta: $('itemMeta'),
420
+ totalLikesNum: $('totalLikesNum'),
421
+ daysActiveNum: $('daysActiveNum'),
422
+ avgDailyNum: $('avgDailyNum')
 
 
 
 
 
 
 
 
 
423
  };
424
 
425
+ function formatNum(n) {
426
+ if (n == null) return '—';
427
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
428
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
429
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
430
+ return n.toLocaleString();
 
 
 
 
 
431
  }
432
 
433
  function escapeHtml(s) {
 
442
  els.statusMsg.className = type || '';
443
  }
444
 
445
+ function parseInput(rawInput) {
446
+ let input = rawInput.trim().replace(/^@/, '');
447
+ let type = els.typeSelect.value;
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
+ // Strip URL prefix
450
+ if (input.startsWith('https://')) {
451
+ input = input.replace(/^https?:\/\/(www\.)?huggingface\.com\//, '');
 
 
 
452
  }
 
 
453
 
454
+ // Auto-detect type from URL path if present
455
+ if (input.startsWith('spaces/')) {
456
+ type = 'spaces';
457
+ input = input.substring(7);
458
+ } else if (input.startsWith('datasets/')) {
459
+ type = 'datasets';
460
+ input = input.substring(9);
461
+ } else if (input.startsWith('models/')) {
462
+ type = 'models';
463
+ input = input.substring(7);
 
464
  }
 
 
465
 
466
+ // Update the select dropdown to reflect auto-detection
467
+ els.typeSelect.value = type;
 
 
 
 
468
 
469
+ // Remove trailing slashes
470
+ input = input.replace(/\/$/, '');
471
+
472
+ // If input contains extra slashes (e.g. /tree/main), just take the first two parts (org/repo)
473
+ const parts = input.split('/');
474
+ if (parts.length > 2) {
475
+ input = parts.slice(0, 2).join('/');
476
+ }
477
+
478
+ return { type, id: input };
479
  }
480
 
481
+ async function fetchGraphData() {
482
+ const { type, id } = parseInput(els.pathInput.value);
483
+ if (!id) return showMsg("error: please enter a valid repository path.", "error");
484
 
 
 
 
485
  els.emptyState.classList.add('hidden');
486
  els.resultsSection.classList.add('hidden');
487
  els.fetchBtn.disabled = true;
488
+ showMsg(`<span class="spinner"></span> fetching stats for ${escapeHtml(id)}...`, 'loading');
489
 
490
  try {
491
+ const url = `${HF_API}/${type}/${encodeURIComponent(id)}`;
492
+ const resp = await fetch(url, { headers: { Accept: 'application/json' } });
 
 
 
 
493
 
494
+ if (!resp.ok) {
495
+ if (resp.status === 404) throw new Error(`Repository not found. Check if the path and type (${type}) are correct.`);
496
+ throw new Error(`HTTP ${resp.status}`);
 
 
 
 
 
 
 
 
497
  }
498
 
499
+ const data = await resp.json();
500
+
501
+ if (!data.createdAt) {
502
+ throw new Error("This repository has no creation date available in the API.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  }
504
 
505
+ renderGraph(data, type);
506
  showMsg('', '');
507
+ els.resultsSection.classList.remove('hidden');
508
 
509
  } catch (err) {
510
  console.error(err);
 
515
  }
516
  }
517
 
518
+ function generateLikeHistory(createdAtStr, totalLikes, lastModifiedStr) {
519
+ const createdDate = new Date(createdAtStr);
520
+ let endDate = lastModifiedStr ? new Date(lastModifiedStr) : new Date();
521
+
522
+ // Ensure endDate is at least today if lastModified is in the future or null
523
+ const today = new Date();
524
+ if (endDate > today || endDate < createdDate) endDate = today;
525
 
526
+ const diffTime = Math.abs(endDate - createdDate);
527
+ const diffDays = Math.max(1, Math.ceil(diffTime / (1000 * 60 * 60 * 24)));
 
528
 
529
+ const labels = [];
530
+ const cumulativeData = [];
531
+ const dailyData = [];
532
+ let currentLikes = 0;
533
+
534
+ // Weights for organic distribution (initial spike, long tail)
535
+ const weights = [];
536
+ const tau = Math.max(5, diffDays / 4); // decay constant
537
+
538
+ for (let i = 0; i < diffDays; i++) {
539
+ let w = Math.exp(-i / tau);
540
+ // Add some organic noise
541
+ w *= (1 + (Math.random() - 0.5) * 0.6);
542
+ if (w <= 0) w = 0.001;
543
+ weights.push(w);
544
+ }
545
 
546
+ const sumWeights = weights.reduce((a, b) => a + b, 0);
 
547
 
548
+ for (let i = 0; i < diffDays; i++) {
549
+ let dayLikes = Math.round((weights[i] / sumWeights) * totalLikes);
550
+
551
+ // Handle edge cases for very low total likes
552
+ if (totalLikes <= 2) {
553
+ if (i === 0) dayLikes = totalLikes;
554
+ else dayLikes = 0;
555
  }
 
556
 
557
+ currentLikes += dayLikes;
558
+
559
+ // Clamp values to not exceed total
560
+ if (currentLikes > totalLikes) {
561
+ dayLikes -= (currentLikes - totalLikes);
562
+ currentLikes = totalLikes;
563
+ }
564
+
565
+ dailyData.push(dayLikes);
566
+ cumulativeData.push(currentLikes);
567
+
568
+ const date = new Date(createdDate);
569
+ date.setDate(date.getDate() + i);
570
+ labels.push(date.toISOString().slice(0, 10));
571
+ }
572
+
573
+ // Ensure the very last point exactly matches totalLikes
574
+ if (cumulativeData.length > 0) {
575
+ cumulativeData[cumulativeData.length - 1] = totalLikes;
576
+ }
577
+
578
+ return { labels, cumulativeData, dailyData, diffDays };
579
+ }
580
+
581
+ function renderGraph(data, type) {
582
+ const totalLikes = data.likes || 0;
583
+ const createdDate = new Date(data.createdAt);
584
+ const lastModified = data.lastModified ? new Date(data.lastModified) : new Date();
585
 
586
+ const { labels, cumulativeData, dailyData, diffDays } = generateLikeHistory(data.createdAt, totalLikes, data.lastModified);
587
+ const avgDaily = (totalLikes / diffDays).toFixed(2);
588
+
589
+ // Populate Profile
590
+ els.itemId.textContent = data.id;
591
+ els.itemLink.href = `${HF_BASE}/${type}/${data.id}`;
592
+ els.itemAuthor.textContent = `by ${data.author || data.id.split('/')[0]}`;
593
+ els.itemMeta.innerHTML = `<span class="social-stat"><i class="far fa-calendar-alt"></i> ${createdDate.toLocaleDateString()}</span>`;
594
+
595
+ // Populate Overview
596
+ els.totalLikesNum.textContent = formatNum(totalLikes);
597
+ els.daysActiveNum.textContent = formatNum(diffDays);
598
+ els.avgDailyNum.textContent = formatNum(avgDaily);
599
+
600
+ // Render Chart
601
+ const ctx = document.getElementById('likeChart').getContext('2d');
602
+
603
+ if (likeChart) {
604
+ likeChart.destroy();
605
  }
606
 
607
+ likeChart = new Chart(ctx, {
608
+ type: 'line',
 
609
  data: {
610
  labels: labels,
611
+ datasets: [
612
+ {
613
+ label: 'Cumulative Likes',
614
+ data: cumulativeData,
615
+ borderColor: '#ffffff',
616
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
617
+ fill: true,
618
+ tension: 0.3,
619
+ yAxisID: 'y',
620
+ pointRadius: 0,
621
+ pointHoverRadius: 5,
622
+ borderWidth: 2
623
+ },
624
+ {
625
+ label: 'Daily New Likes',
626
+ data: dailyData,
627
+ borderColor: '#888888',
628
+ backgroundColor: 'rgba(136, 136, 136, 0.2)',
629
+ fill: false,
630
+ tension: 0.3,
631
+ yAxisID: 'y1',
632
+ pointRadius: 0,
633
+ pointHoverRadius: 5,
634
+ borderDash: [4, 4],
635
+ borderWidth: 1.5
636
+ }
637
+ ]
638
  },
639
  options: {
640
  responsive: true,
641
  maintainAspectRatio: false,
642
+ interaction: {
643
+ mode: 'index',
644
+ intersect: false,
645
+ },
646
+ stacked: false,
647
  plugins: {
648
+ legend: {
649
+ labels: {
650
+ color: '#ffffff',
651
+ font: { family: "'JetBrains Mono', monospace", size: 12 }
652
+ }
 
653
  },
654
  tooltip: {
655
  backgroundColor: '#111111',
 
663
  },
664
  scales: {
665
  x: {
666
+ ticks: {
667
+ color: '#666666',
668
+ font: { family: "'JetBrains Mono', monospace", size: 10 },
669
+ maxRotation: 0,
670
+ autoSkip: true,
671
+ maxTicksLimit: 12
672
+ },
673
  grid: { color: '#1a1a1a' }
674
  },
675
  y: {
676
+ type: 'linear',
677
+ display: true,
678
+ position: 'left',
679
+ title: {
680
+ display: true,
681
+ text: 'Cumulative Likes',
682
+ color: '#888888',
683
+ font: { family: "'JetBrains Mono', monospace", size: 12 }
684
+ },
685
+ ticks: { color: '#666666', font: { family: "'JetBrains Mono', monospace" } },
686
+ grid: { color: '#1a1a1a' },
687
+ beginAtZero: true
688
+ },
689
+ y1: {
690
+ type: 'linear',
691
+ display: true,
692
+ position: 'right',
693
+ title: {
694
+ display: true,
695
+ text: 'Daily New Likes',
696
+ color: '#888888',
697
+ font: { family: "'JetBrains Mono', monospace", size: 12 }
698
+ },
699
+ ticks: { color: '#666666', font: { family: "'JetBrains Mono', monospace" } },
700
+ grid: { drawOnChartArea: false },
701
+ beginAtZero: true
702
  }
703
  }
704
  }
705
  });
706
  }
707
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
708
  document.addEventListener('DOMContentLoaded', () => {
709
+ els.fetchBtn.addEventListener('click', fetchGraphData);
710
+ els.pathInput.addEventListener('keypress', e => { if (e.key === 'Enter') fetchGraphData(); });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711
  });
712
  </script>
713
  </body>