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

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +559 -385
index.html CHANGED
@@ -4,7 +4,7 @@
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>
@@ -69,6 +69,87 @@
69
  letter-spacing: 0.02em;
70
  }
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  /* ── Main Content ── */
73
  .term-body {
74
  padding: 20px;
@@ -93,6 +174,8 @@
93
  border-radius: 3px;
94
  padding: 0 14px;
95
  height: 40px;
 
 
96
  transition: border-color 0.15s;
97
  }
98
  .prompt-group:focus-within {
@@ -107,7 +190,7 @@
107
  font-weight: 500;
108
  }
109
 
110
- .prompt-group input, .prompt-group select {
111
  background: transparent;
112
  border: none;
113
  color: #ffffff;
@@ -116,10 +199,21 @@
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 {
124
  background: #ffffff;
125
  color: #000000;
@@ -150,20 +244,19 @@
150
  }
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;
@@ -171,7 +264,7 @@
171
  line-height: 1.6;
172
  }
173
 
174
- /* ── Profile Card ── */
175
  .profile-block {
176
  border: 1px solid #222222;
177
  border-radius: 3px;
@@ -180,9 +273,29 @@
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; }
@@ -199,79 +312,66 @@
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;
219
  font-size: 0.75rem;
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 ── */
@@ -300,17 +400,16 @@
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>
@@ -320,24 +419,38 @@
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>
@@ -348,367 +461,428 @@
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>
401
 
402
  <script>
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) {
434
- const d = document.createElement('div');
435
- d.textContent = s;
436
- return d.innerHTML;
437
- }
438
-
439
- function showMsg(text, type) {
440
- els.statusMsg.style.display = text ? 'block' : 'none';
441
- els.statusMsg.innerHTML = text;
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);
511
- showMsg(`error: ${err.message}`, 'error');
512
- els.emptyState.classList.remove('hidden');
513
- } finally {
514
- els.fetchBtn.disabled = false;
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',
656
- titleColor: '#ffffff',
657
- bodyColor: '#ffffff',
658
- borderColor: '#333333',
659
- borderWidth: 1,
660
- titleFont: { family: "'JetBrains Mono', monospace" },
661
- bodyFont: { family: "'JetBrains Mono', monospace" }
662
- }
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>
714
  </html>
 
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 growth 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>
 
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
+ .token-section {
98
+ border-bottom: 1px solid #222222;
99
+ padding: 14px 20px;
100
+ animation: termSlide 0.2s ease-out;
101
+ }
102
+
103
+ @keyframes termSlide {
104
+ from { opacity: 0; max-height: 0; }
105
+ to { opacity: 1; max-height: 200px; }
106
+ }
107
+
108
+ .token-row {
109
+ display: flex;
110
+ gap: 10px;
111
+ align-items: center;
112
+ }
113
+
114
+ .token-prompt {
115
+ color: #888888;
116
+ font-size: 0.78rem;
117
+ flex-shrink: 0;
118
+ }
119
+
120
+ .token-row input {
121
+ background: #111111;
122
+ border: 1px solid #333333;
123
+ padding: 7px 12px;
124
+ border-radius: 3px;
125
+ flex: 1;
126
+ color: #ffffff;
127
+ font-size: 0.78rem;
128
+ }
129
+ .token-row input:focus {
130
+ outline: none;
131
+ border-color: #ffffff;
132
+ }
133
+
134
+ .token-row button {
135
+ background: #ffffff;
136
+ color: #000000;
137
+ border: none;
138
+ padding: 7px 16px;
139
+ border-radius: 3px;
140
+ cursor: pointer;
141
+ font-weight: 600;
142
+ font-size: 0.78rem;
143
+ transition: opacity 0.15s;
144
+ }
145
+ .token-row button:hover { opacity: 0.85; }
146
+
147
+ .token-note {
148
+ font-size: 0.68rem;
149
+ color: #555555;
150
+ margin-top: 8px;
151
+ }
152
+
153
  /* ── Main Content ── */
154
  .term-body {
155
  padding: 20px;
 
174
  border-radius: 3px;
175
  padding: 0 14px;
176
  height: 40px;
177
+ flex: 1;
178
+ min-width: 200px;
179
  transition: border-color 0.15s;
180
  }
181
  .prompt-group:focus-within {
 
190
  font-weight: 500;
191
  }
192
 
193
+ .prompt-group input {
194
  background: transparent;
195
  border: none;
196
  color: #ffffff;
 
199
  font-size: 0.82rem;
200
  font-weight: 400;
201
  }
 
 
202
  .prompt-group input::placeholder { color: #444444; }
203
 
204
+ .type-select {
205
+ background: #0a0a0a;
206
+ border: 1px solid #222222;
207
+ color: #ffffff;
208
+ padding: 0 14px;
209
+ height: 40px;
210
+ border-radius: 3px;
211
+ font-size: 0.82rem;
212
+ font-weight: 500;
213
+ cursor: pointer;
214
+ }
215
+ .type-select:focus { outline: none; border-color: #ffffff; }
216
+
217
  .exec-btn {
218
  background: #ffffff;
219
  color: #000000;
 
244
  }
245
  .error { color: #ffffff; background: #1a0000; border-color: #440000 !important; }
246
  .loading { color: #ffffff; background: #0a0a0a; border-color: #333333 !important; }
247
+ .success { color: #ffffff; background: #001a00; border-color: #004400 !important; }
248
 
249
+ /* ── Empty State ── */
250
  .empty-state {
251
  text-align: center;
252
+ padding-top: 60px;
253
  }
 
254
  .empty-title {
255
  font-size: 1.1rem;
256
  font-weight: 600;
257
  color: #ffffff;
258
  margin-bottom: 8px;
259
  }
 
260
  .empty-desc {
261
  color: #666666;
262
  font-size: 0.8rem;
 
264
  line-height: 1.6;
265
  }
266
 
267
+ /* ── Profile / Repo Card ── */
268
  .profile-block {
269
  border: 1px solid #222222;
270
  border-radius: 3px;
 
273
  animation: termSlide 0.25s ease-out;
274
  }
275
 
276
+ .profile-top {
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 16px;
280
+ }
281
+
282
+ .profile-avatar {
283
+ width: 56px;
284
+ height: 56px;
285
+ border-radius: 50%;
286
+ border: 2px solid #333333;
287
+ display: flex;
288
+ align-items: center;
289
+ justify-content: center;
290
+ overflow: hidden;
291
+ flex-shrink: 0;
292
+ background: #111111;
293
+ }
294
+ .profile-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
295
+ .profile-avatar .avatar-letter {
296
+ font-size: 1.4rem;
297
+ font-weight: 700;
298
+ color: #666666;
299
  }
300
 
301
  .profile-info { flex: 1; }
 
312
  .profile-name a { color: #ffffff; }
313
  .profile-name a:hover { text-decoration: underline; }
314
 
315
+ .type-badge {
316
+ background: #222222;
317
+ color: #aaaaaa;
318
+ padding: 2px 8px;
319
+ border-radius: 3px;
320
+ font-size: 0.65rem;
321
+ text-transform: uppercase;
322
+ letter-spacing: 1px;
323
+ font-weight: 700;
324
  }
325
 
326
+ .profile-meta {
327
  display: flex;
328
  gap: 16px;
329
  margin-top: 8px;
330
  flex-wrap: wrap;
 
 
 
 
 
 
331
  font-size: 0.75rem;
332
  color: #666666;
 
 
 
 
 
 
 
 
 
 
 
 
333
  }
334
+ .profile-meta span { display: flex; align-items: center; gap: 6px; }
335
 
336
+ /* ── Graph Section ── */
337
+ .graph-wrapper {
338
+ border: 1px solid #222222;
339
  border-radius: 3px;
340
+ background: #000000;
341
+ padding: 20px;
342
+ position: relative;
343
+ height: 450px;
344
+ margin-bottom: 16px;
345
  }
346
 
347
+ .graph-header {
348
+ display: flex;
349
+ justify-content: space-between;
350
+ align-items: center;
351
+ margin-bottom: 14px;
352
+ flex-wrap: wrap;
353
+ gap: 10px;
354
+ position: absolute;
355
+ top: 20px;
356
+ left: 20px;
357
+ right: 20px;
358
+ z-index: 1;
359
+ pointer-events: none;
360
  }
361
+
362
+ .graph-title {
363
+ font-size: 0.78rem;
 
364
  font-weight: 600;
365
+ color: #888888;
366
  text-transform: uppercase;
367
  letter-spacing: 1px;
 
 
 
 
 
 
 
 
 
 
 
 
368
  }
369
 
370
+ .graph-note {
371
+ font-size: 0.65rem;
372
+ color: #555555;
373
+ margin-top: 8px;
374
  text-align: center;
 
 
 
 
375
  }
376
 
377
  /* ── Footer ── */
 
400
  }
401
 
402
  @keyframes spin { to { transform: rotate(360deg); } }
 
403
  .hidden { display: none !important; }
404
 
405
  /* ── Responsive ── */
406
  @media (max-width: 640px) {
407
  .input-line { flex-direction: column; }
408
  .exec-btn { width: 100%; justify-content: center; }
409
+ .profile-top { flex-direction: column; text-align: center; }
410
+ .profile-name { justify-content: center; }
411
+ .profile-meta { justify-content: center; }
412
+ .graph-wrapper { height: 350px; padding: 10px; }
413
  }
414
  </style>
415
  </head>
 
419
  <div class="title-bar">
420
  <div class="title-dot"></div>
421
  <span class="title-text">[like-graph]</span>
422
+ <div class="title-actions">
423
+ <button id="privateRepoBtn" class="title-btn" title="Access Private Repos">
424
+ <i class="fas fa-lock"></i> token
425
+ </button>
426
+ </div>
427
+ </div>
428
+
429
+ <!-- Token Panel -->
430
+ <div id="tokenSection" class="token-section hidden">
431
+ <div class="token-row">
432
+ <span class="token-prompt">token ~$</span>
433
+ <input type="password" id="hfToken" placeholder="paste hf access token (hf_...)">
434
+ <button id="saveTokenBtn">save</button>
435
+ <button id="clearTokenBtn" class="hidden">clear</button>
436
+ </div>
437
+ <p class="token-note">token stored in localStorage. required for private repos.</p>
438
  </div>
439
 
440
  <!-- Main Body -->
441
  <div class="term-body">
442
  <!-- Input -->
443
  <div class="input-line">
444
+ <div class="prompt-group">
445
+ <span class="prompt-symbol">~$ fetch&nbsp;</span>
446
+ <input type="text" id="repoInput" placeholder="repo path (e.g. prithivMLmods/README or org/dataset)" autocomplete="off">
 
 
 
 
 
 
 
 
447
  </div>
448
+ <select id="typeSelect" class="type-select">
449
+ <option value="auto">auto-detect</option>
450
+ <option value="models">model</option>
451
+ <option value="datasets">dataset</option>
452
+ <option value="spaces">space</option>
453
+ </select>
454
  <button id="fetchBtn" class="exec-btn">
455
  <i class="fas fa-chart-line"></i> generate
456
  </button>
 
461
  <!-- Empty State -->
462
  <div id="emptyState" class="empty-state">
463
  <div style="margin-bottom:32px;">
464
+ <div class="empty-title">hugging-face like tracker</div>
465
+ <div class="empty-desc">paste a model, dataset, or space path to generate a daily like growth graph.</div>
466
  </div>
467
  </div>
468
 
469
  <!-- Results -->
470
  <div id="resultsSection" class="hidden">
471
+ <!-- Repo Info -->
472
+ <div class="profile-block">
473
  <div class="profile-top">
474
+ <div class="profile-avatar" id="profileAvatar">
475
+ <span class="avatar-letter" id="avatarLetter"></span>
476
+ </div>
477
  <div class="profile-info">
478
  <div class="profile-name">
479
+ <a id="repoLink" href="#" target="_blank" rel="noopener">
480
+ <span id="repoId"></span>
481
  </a>
482
+ <span class="type-badge" id="repoType"></span>
483
  </div>
484
+ <div class="profile-meta" id="repoMeta"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  </div>
486
  </div>
487
  </div>
488
 
489
+ <!-- Graph -->
490
+ <div class="graph-wrapper">
491
+ <div class="graph-header">
492
+ <span class="graph-title">cumulative likes over time</span>
493
+ </div>
494
+ <canvas id="likesChart"></canvas>
495
  </div>
496
+ <div class="graph-note">
497
+ Note: HuggingFace API provides current like totals but not historical daily snapshots. The graph projects an estimated organic growth curve from the publication date to the current like count.
498
  </div>
499
  </div>
500
 
501
  <div class="footer">
502
+ built by <a href="https://hf.co/prithivMLmods" target="_blank" rel="noopener">prithivMLmods</a>
503
  </div>
504
  </div>
505
  </div>
506
 
507
  <script>
508
+ const HF_API = 'https://huggingface.co/api';
509
+ const HF_BASE = 'https://huggingface.co';
510
+
511
+ let state = {
512
+ chart: null
513
+ };
514
+
515
+ const $ = id => document.getElementById(id);
516
+
517
+ const els = {
518
+ repoInput: $('repoInput'),
519
+ typeSelect: $('typeSelect'),
520
+ fetchBtn: $('fetchBtn'),
521
+ statusMsg: $('statusMsg'),
522
+ emptyState: $('emptyState'),
523
+ resultsSection: $('resultsSection'),
524
+ profileAvatar: $('profileAvatar'),
525
+ avatarLetter: $('avatarLetter'),
526
+ repoLink: $('repoLink'),
527
+ repoId: $('repoId'),
528
+ repoType: $('repoType'),
529
+ repoMeta: $('repoMeta'),
530
+ tokenSection: $('tokenSection'),
531
+ hfToken: $('hfToken'),
532
+ saveTokenBtn: $('saveTokenBtn'),
533
+ clearTokenBtn: $('clearTokenBtn'),
534
+ privateRepoBtn: $('privateRepoBtn'),
535
+ chartCanvas: $('likesChart').getContext('2d')
536
+ };
537
+
538
+ function getHeaders() {
539
+ const t = localStorage.getItem('hf_token');
540
+ const h = { Accept: 'application/json' };
541
+ if (t) h['Authorization'] = `Bearer ${t}`;
542
+ return h;
543
+ }
544
+
545
+ function formatNum(n) {
546
+ if (n == null) return '—';
547
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
548
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
549
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
550
+ return n.toLocaleString();
551
+ }
552
+
553
+ function relativeTime(d) {
554
+ if (!d) return '';
555
+ const diff = Date.now() - new Date(d).getTime();
556
+ const m = Math.floor(diff / 60000);
557
+ if (m < 1) return 'just now';
558
+ if (m < 60) return m + 'm ago';
559
+ const h = Math.floor(m / 60);
560
+ if (h < 24) return h + 'h ago';
561
+ const dy = Math.floor(h / 24);
562
+ if (dy < 7) return dy + 'd ago';
563
+ const w = Math.floor(dy / 7);
564
+ if (w < 5) return w + 'w ago';
565
+ const mo = Math.floor(dy / 30);
566
+ if (mo < 12) return mo + 'mo ago';
567
+ return Math.floor(dy / 365) + 'y ago';
568
+ }
569
+
570
+ function escapeHtml(s) {
571
+ const d = document.createElement('div');
572
+ d.textContent = s;
573
+ return d.innerHTML;
574
+ }
575
+
576
+ function showMsg(text, type) {
577
+ els.statusMsg.style.display = text ? 'block' : 'none';
578
+ els.statusMsg.innerHTML = text;
579
+ els.statusMsg.className = type || '';
580
+ }
581
+
582
+ function checkToken() {
583
+ const t = localStorage.getItem('hf_token');
584
+ if (t) {
585
+ els.hfToken.value = t;
586
+ els.privateRepoBtn.innerHTML = '<i class="fas fa-lock-open"></i> active';
587
+ els.privateRepoBtn.classList.add('active-token');
588
+ els.saveTokenBtn.classList.add('hidden');
589
+ els.clearTokenBtn.classList.remove('hidden');
590
+ } else {
591
+ els.privateRepoBtn.innerHTML = '<i class="fas fa-lock"></i> token';
592
+ els.privateRepoBtn.classList.remove('active-token');
593
+ els.saveTokenBtn.classList.remove('hidden');
594
+ els.clearTokenBtn.classList.add('hidden');
 
 
 
 
 
 
 
595
  }
596
+ }
597
 
598
+ async function fetchRepoData(repoPath, type) {
599
+ const headers = getHeaders();
600
+ const cleanPath = repoPath.replace(/^\/+|\/+$/g, '');
601
+
602
+ if (type !== 'auto') {
603
+ const resp = await fetch(`${HF_API}/${type}/${encodeURIComponent(cleanPath)}`, { headers });
604
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
605
+ const data = await resp.json();
606
+ return { data, type };
607
  }
608
 
609
+ // Auto-detect
610
+ const types = ['models', 'datasets', 'spaces'];
611
+ let foundData = null;
612
+ let foundType = null;
613
+
614
+ // Try all 3 simultaneously and take the first success
615
+ const promises = types.map(t =>
616
+ fetch(`${HF_API}/${t}/${encodeURIComponent(cleanPath)}`, { headers })
617
+ .then(resp => resp.ok ? resp.json() : Promise.reject(new Error(`HTTP ${resp.status}`)))
618
+ .then(data => ({ data, type: t }))
619
+ );
620
+
621
+ try {
622
+ const result = await Promise.any(promises);
623
+ foundData = result.data;
624
+ foundType = result.type;
625
+ } catch (e) {
626
+ throw new Error('Repo not found in models, datasets, or spaces.');
627
+ }
628
 
629
+ return { data: foundData, type: foundType };
 
 
 
 
 
630
  }
 
631
 
632
+ async function fetchAuthorAvatar(author) {
633
+ const headers = getHeaders();
634
+ try {
635
+ const resp = await fetch(`${HF_API}/users/${encodeURIComponent(author)}/overview`, { headers });
636
+ if (resp.ok) {
637
+ const data = await resp.json();
638
+ return data.avatarUrl || null;
639
+ }
640
+ } catch (e) {}
641
+ return null;
642
+ }
643
 
644
+ async function generateGraph() {
645
+ const repoPath = els.repoInput.value.trim();
646
+ if (!repoPath) return showMsg("error: please enter a repo path.", "error");
647
 
648
+ const type = els.typeSelect.value;
649
+
650
+ els.emptyState.classList.add('hidden');
651
+ els.resultsSection.classList.add('hidden');
652
+ els.fetchBtn.disabled = true;
653
+ showMsg(`<span class="spinner"></span> fetching data for ${escapeHtml(repoPath)}...`, 'loading');
654
+
655
+ try {
656
+ const { data, type: resolvedType } = await fetchRepoData(repoPath, type);
657
+
658
+ if (!data.likes || !data.createdAt) {
659
+ if (!data.createdAt) throw new Error('Creation date not found for this repo.');
660
+ }
661
 
662
+ const likes = data.likes || 0;
663
+ const createdAt = data.createdAt;
664
+ const author = data.author || repoPath.split('/')[0];
665
+ const id = data.id || repoPath;
666
+
667
+ showMsg(`<span class="spinner"></span> generating graph...`, 'loading');
668
+
669
+ // Fetch Avatar
670
+ const avatarUrl = await fetchAuthorAvatar(author);
671
+
672
+ // Render Repo Info
673
+ els.repoId.textContent = id;
674
+ els.repoLink.href = resolvedType === 'datasets' ? `${HF_BASE}/datasets/${id}` :
675
+ resolvedType === 'spaces' ? `${HF_BASE}/spaces/${id}` :
676
+ `${HF_BASE}/${id}`;
677
+ els.repoType.textContent = resolvedType.replace('s', '').replace('model', 'model').replace('dataset', 'dataset').replace('space', 'space');
678
+
679
+ if (avatarUrl) {
680
+ const full = avatarUrl.startsWith('http') ? avatarUrl : `${HF_BASE}${avatarUrl}`;
681
+ els.profileAvatar.innerHTML = `<img src="${full}" alt="${escapeHtml(author)}" onerror="this.parentElement.innerHTML='<span class=\\'avatar-letter\\'>${author[0].toUpperCase()}</span>'">`;
682
+ } else {
683
+ els.profileAvatar.innerHTML = `<span class="avatar-letter">${author[0].toUpperCase()}</span>`;
684
+ }
685
 
686
+ els.repoMeta.innerHTML = `
687
+ <span><i class="fas fa-user"></i> ${escapeHtml(author)}</span>
688
+ <span><i class="fas fa-calendar-alt"></i> created ${new Date(createdAt).toLocaleDateString()}</span>
689
+ <span><i class="fas fa-heart"></i> ${formatNum(likes)} total likes</span>
690
+ <span><i class="far fa-clock"></i> updated ${relativeTime(data.lastModified)}</span>
691
+ `;
692
+
693
+ els.resultsSection.classList.remove('hidden');
694
+ showMsg('', '');
695
+
696
+ // Generate Daily Data
697
+ const dailyData = generateDailyLikeData(new Date(createdAt), likes);
698
+ renderChart(dailyData, id);
699
+
700
+ } catch (err) {
701
+ console.error(err);
702
+ showMsg(`error: ${err.message}`, 'error');
703
+ els.emptyState.classList.remove('hidden');
704
+ } finally {
705
+ els.fetchBtn.disabled = false;
706
+ }
707
  }
708
 
709
+ function generateDailyLikeData(startDate, totalLikes) {
710
+ const endDate = new Date();
711
+ const days = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
 
712
 
713
+ if (days <= 0 || totalLikes <= 0) {
714
+ return { labels: [startDate.toLocaleDateString()], data: [totalLikes] };
 
 
715
  }
716
 
717
+ const labels = [];
718
+ const data = [];
719
+ let currentLikes = 0;
720
+
721
+ for (let i = 0; i <= days; i++) {
722
+ const currentDate = new Date(startDate);
723
+ currentDate.setDate(startDate.getDate() + i);
724
+
725
+ // To avoid massive arrays for very old repos, sample data points
726
+ // If days > 365, we take weekly snapshots (every 7 days)
727
+ // If days > 1095 (3 years), we take monthly snapshots (every 30 days)
728
+ const shouldRecord = days <= 365 ? true :
729
+ days <= 1095 ? (i % 7 === 0 || i === days) :
730
+ (i % 30 === 0 || i === days);
731
+
732
+ if (shouldRecord || i === days) {
733
+ labels.push(currentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' }));
734
+
735
+ // Organic growth simulation using an ease-out cubic curve
736
+ const t = i / days;
737
+ const factor = 1 - Math.pow(1 - t, 3);
738
+ let estimatedLikes = Math.round(factor * totalLikes);
739
+
740
+ // Ensure strictly non-decreasing and lands exactly on totalLikes
741
+ estimatedLikes = Math.max(currentLikes, estimatedLikes);
742
+ if (i === days) estimatedLikes = totalLikes;
743
+
744
+ currentLikes = estimatedLikes;
745
+ data.push(currentLikes);
746
+ }
747
  }
748
+
749
+ return { labels, data };
 
 
 
 
 
 
 
 
 
 
750
  }
751
 
752
+ function renderChart(dailyData, repoId) {
753
+ if (state.chart) {
754
+ state.chart.destroy();
755
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
 
757
+ // Chart.js Global Dark Theme Settings
758
+ Chart.defaults.color = '#666666';
759
+ Chart.defaults.borderColor = '#1a1a1a';
760
+ Chart.defaults.font.family = "'JetBrains Mono', monospace";
761
+
762
+ // Create gradient
763
+ const ctx = els.chartCanvas;
764
+ const gradient = ctx.createLinearGradient(0, 0, 0, 400);
765
+ gradient.addColorStop(0, 'rgba(255, 255, 255, 0.2)');
766
+ gradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
767
+
768
+ state.chart = new Chart(ctx, {
769
+ type: 'line',
770
+ data: {
771
+ labels: dailyData.labels,
772
+ datasets: [{
773
  label: 'Cumulative Likes',
774
+ data: dailyData.data,
775
  borderColor: '#ffffff',
776
+ backgroundColor: gradient,
777
+ borderWidth: 2,
 
 
778
  pointRadius: 0,
779
  pointHoverRadius: 5,
780
+ pointHoverBackgroundColor: '#ffffff',
781
+ pointHoverBorderColor: '#000000',
782
+ pointHoverBorderWidth: 2,
783
+ fill: true,
784
+ tension: 0.3
785
+ }]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
786
  },
787
+ options: {
788
+ responsive: true,
789
+ maintainAspectRatio: false,
790
+ interaction: {
791
+ mode: 'index',
792
+ intersect: false,
 
 
 
 
793
  },
794
+ plugins: {
795
+ legend: {
796
+ display: false
 
 
 
 
 
 
797
  },
798
+ tooltip: {
799
+ backgroundColor: '#111111',
800
+ borderColor: '#333333',
801
+ borderWidth: 1,
802
+ titleColor: '#ffffff',
803
+ bodyColor: '#aaaaaa',
804
+ padding: 10,
805
+ cornerRadius: 3,
806
+ titleFont: { weight: 'bold' },
807
+ callbacks: {
808
+ title: function(context) {
809
+ return context[0].label;
810
+ },
811
+ label: function(context) {
812
+ return ` ${context.parsed.y.toLocaleString()} likes`;
813
+ }
814
+ }
815
+ }
816
  },
817
+ scales: {
818
+ x: {
819
+ grid: {
820
+ color: '#111111',
821
+ drawTicks: false
822
+ },
823
+ ticks: {
824
+ maxRotation: 0,
825
+ autoSkip: true,
826
+ maxTicksLimit: 8,
827
+ color: '#555555'
828
+ },
829
+ border: {
830
+ display: false
831
+ }
832
  },
833
+ y: {
834
+ beginAtZero: true,
835
+ grid: {
836
+ color: '#111111'
837
+ },
838
+ ticks: {
839
+ color: '#555555',
840
+ callback: function(value) {
841
+ return formatNum(value);
842
+ }
843
+ },
844
+ border: {
845
+ display: false
846
+ }
847
+ }
848
  }
849
  }
850
+ });
851
+ }
852
+
853
+ document.addEventListener('DOMContentLoaded', () => {
854
+ checkToken();
855
+
856
+ els.privateRepoBtn.addEventListener('click', () => els.tokenSection.classList.toggle('hidden'));
857
+
858
+ els.saveTokenBtn.addEventListener('click', () => {
859
+ const t = els.hfToken.value.trim();
860
+ if (!t) return;
861
+ localStorage.setItem('hf_token', t);
862
+ checkToken();
863
+ showMsg('token saved.', 'success');
864
+ setTimeout(() => showMsg('', ''), 1500);
865
+ });
866
+
867
+ els.clearTokenBtn.addEventListener('click', () => {
868
+ localStorage.removeItem('hf_token');
869
+ els.hfToken.value = '';
870
+ checkToken();
871
+ showMsg('token cleared.', 'success');
872
+ setTimeout(() => showMsg('', ''), 1500);
873
+ });
874
+
875
+ els.hfToken.addEventListener('keypress', e => { if (e.key === 'Enter') els.saveTokenBtn.click(); });
876
+ els.fetchBtn.addEventListener('click', generateGraph);
877
+ els.repoInput.addEventListener('keypress', e => { if (e.key === 'Enter') generateGraph(); });
878
+
879
+ // Parse hash if present
880
+ const h = window.location.hash;
881
+ if (h && h.length > 1) {
882
+ els.repoInput.value = decodeURIComponent(h.substring(1));
883
+ generateGraph();
884
  }
885
  });
 
 
 
 
 
 
886
  </script>
887
  </body>
888
  </html>