ai-gains 1.3.5 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/dashboard.html +458 -38
- package/template/.claude/skills/ai-gains/SKILL.md +14 -2
package/package.json
CHANGED
package/src/dashboard.html
CHANGED
|
@@ -182,6 +182,7 @@
|
|
|
182
182
|
.chart-wrap {
|
|
183
183
|
position: relative;
|
|
184
184
|
height: 240px;
|
|
185
|
+
cursor: pointer;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
/* ── Session cards ── */
|
|
@@ -407,6 +408,93 @@
|
|
|
407
408
|
}
|
|
408
409
|
.placeholder-icon { font-size: 44px; margin-bottom: 14px; }
|
|
409
410
|
|
|
411
|
+
/* ── Category breakdown ── */
|
|
412
|
+
.cat-layout {
|
|
413
|
+
display: flex;
|
|
414
|
+
align-items: center;
|
|
415
|
+
gap: 32px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.cat-donut-wrap {
|
|
419
|
+
position: relative;
|
|
420
|
+
width: 180px;
|
|
421
|
+
height: 180px;
|
|
422
|
+
flex-shrink: 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.cat-legend {
|
|
426
|
+
flex: 1;
|
|
427
|
+
min-width: 0;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.cat-legend-row {
|
|
431
|
+
display: grid;
|
|
432
|
+
grid-template-columns: 10px 1fr auto auto;
|
|
433
|
+
align-items: center;
|
|
434
|
+
gap: 8px 12px;
|
|
435
|
+
padding: 6px 0;
|
|
436
|
+
border-bottom: 1px solid var(--border);
|
|
437
|
+
cursor: default;
|
|
438
|
+
transition: background 0.1s;
|
|
439
|
+
border-radius: 4px;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.cat-legend-row:last-child { border-bottom: none; }
|
|
443
|
+
.cat-legend-row:hover { background: var(--surface-2); }
|
|
444
|
+
|
|
445
|
+
.cat-dot {
|
|
446
|
+
width: 8px;
|
|
447
|
+
height: 8px;
|
|
448
|
+
border-radius: 50%;
|
|
449
|
+
flex-shrink: 0;
|
|
450
|
+
justify-self: center;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.cat-legend-name {
|
|
454
|
+
font-size: 13px;
|
|
455
|
+
font-weight: 500;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.cat-legend-time {
|
|
459
|
+
font-size: 13px;
|
|
460
|
+
font-weight: 700;
|
|
461
|
+
text-align: right;
|
|
462
|
+
white-space: nowrap;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.cat-legend-meta {
|
|
466
|
+
font-size: 11px;
|
|
467
|
+
color: var(--muted);
|
|
468
|
+
text-align: right;
|
|
469
|
+
white-space: nowrap;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
@media (max-width: 600px) {
|
|
473
|
+
.cat-layout { flex-direction: column; align-items: flex-start; }
|
|
474
|
+
.cat-donut-wrap { width: 150px; height: 150px; }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* ── Category badge ── */
|
|
478
|
+
.cat-badge {
|
|
479
|
+
display: inline-flex;
|
|
480
|
+
align-items: center;
|
|
481
|
+
gap: 4px;
|
|
482
|
+
border-radius: 4px;
|
|
483
|
+
padding: 2px 7px;
|
|
484
|
+
font-size: 10px;
|
|
485
|
+
font-weight: 600;
|
|
486
|
+
letter-spacing: 0.05em;
|
|
487
|
+
text-transform: uppercase;
|
|
488
|
+
white-space: nowrap;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.cat-summary {
|
|
492
|
+
display: flex;
|
|
493
|
+
flex-wrap: wrap;
|
|
494
|
+
gap: 6px;
|
|
495
|
+
margin-bottom: 18px;
|
|
496
|
+
}
|
|
497
|
+
|
|
410
498
|
/* ── Scrollbar ── */
|
|
411
499
|
::-webkit-scrollbar { width: 7px; }
|
|
412
500
|
::-webkit-scrollbar-track { background: var(--bg); }
|
|
@@ -431,7 +519,6 @@
|
|
|
431
519
|
</select>
|
|
432
520
|
<div class="filter-divider"></div>
|
|
433
521
|
<div class="period-pills" id="period-pills">
|
|
434
|
-
<button class="period-pill active" data-period="all" onclick="setPeriod(this)">All time</button>
|
|
435
522
|
<button class="period-pill" data-period="today" onclick="setPeriod(this)">Today</button>
|
|
436
523
|
<button class="period-pill" data-period="3d" onclick="setPeriod(this)">3 days</button>
|
|
437
524
|
<button class="period-pill" data-period="week" onclick="setPeriod(this)">Week</button>
|
|
@@ -439,8 +526,10 @@
|
|
|
439
526
|
<button class="period-pill" data-period="3m" onclick="setPeriod(this)">3 months</button>
|
|
440
527
|
<button class="period-pill" data-period="6m" onclick="setPeriod(this)">6 months</button>
|
|
441
528
|
<button class="period-pill" data-period="year" onclick="setPeriod(this)">Year</button>
|
|
529
|
+
<button class="period-pill" data-period="all" onclick="setPeriod(this)">All time</button>
|
|
442
530
|
</div>
|
|
443
531
|
</div>
|
|
532
|
+
<div id="last-session-note" style="font-size:12px;color:var(--muted);margin-bottom:20px;min-height:16px"></div>
|
|
444
533
|
|
|
445
534
|
<!-- Stats -->
|
|
446
535
|
<div class="stats-grid">
|
|
@@ -458,18 +547,34 @@
|
|
|
458
547
|
</div>
|
|
459
548
|
<div class="stat-card">
|
|
460
549
|
<div class="stat-label">Avg Speedup</div>
|
|
461
|
-
<div class="stat-value c-orange"
|
|
550
|
+
<div class="stat-value c-orange" style="display:flex;align-items:baseline;gap:8px">
|
|
551
|
+
<span id="stat-speedup">—</span>
|
|
552
|
+
<span id="stat-speedup-trend" style="font-size:14px;font-weight:600;letter-spacing:0"></span>
|
|
553
|
+
</div>
|
|
462
554
|
</div>
|
|
463
555
|
</div>
|
|
464
556
|
|
|
465
557
|
<!-- Chart -->
|
|
466
558
|
<div class="section">
|
|
467
|
-
<div class="section-label">
|
|
559
|
+
<div class="section-label">Cumulative Time Saved</div>
|
|
468
560
|
<div class="chart-wrap">
|
|
469
561
|
<canvas id="main-chart"></canvas>
|
|
470
562
|
</div>
|
|
471
563
|
</div>
|
|
472
564
|
|
|
565
|
+
<!-- Category breakdown -->
|
|
566
|
+
<div id="cat-section" style="display:none">
|
|
567
|
+
<div class="section">
|
|
568
|
+
<div class="section-label">By Category</div>
|
|
569
|
+
<div class="cat-layout">
|
|
570
|
+
<div class="cat-donut-wrap">
|
|
571
|
+
<canvas id="cat-chart"></canvas>
|
|
572
|
+
</div>
|
|
573
|
+
<div class="cat-legend" id="cat-legend"></div>
|
|
574
|
+
</div>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
473
578
|
<!-- Sessions list -->
|
|
474
579
|
<div class="sessions-label">Sessions</div>
|
|
475
580
|
<div id="sessions-list">
|
|
@@ -496,9 +601,94 @@
|
|
|
496
601
|
'use strict';
|
|
497
602
|
|
|
498
603
|
let sessions = [];
|
|
499
|
-
let chart
|
|
604
|
+
let chart = null;
|
|
605
|
+
let catChart = null;
|
|
500
606
|
let filterAuthor = '';
|
|
501
|
-
let filterPeriod = 'all';
|
|
607
|
+
let filterPeriod = 'all'; // overwritten by pickDefaultPeriod() after load
|
|
608
|
+
|
|
609
|
+
// ── Categories ───────────────────────────────────────────────────────────────
|
|
610
|
+
|
|
611
|
+
const CATEGORIES = [
|
|
612
|
+
{
|
|
613
|
+
id: 'bug-fix',
|
|
614
|
+
label: 'Bug Fix',
|
|
615
|
+
color: '#ef4444',
|
|
616
|
+
dim: 'rgba(239,68,68,0.15)',
|
|
617
|
+
keywords: ['fixed', 'fix ', 'bug', 'broken', 'regression', 'incorrect', 'wrong', 'invalid',
|
|
618
|
+
'failure', 'failed', 'http 4', 'http 5', '410', '400', '500',
|
|
619
|
+
'error', 'always showing', 'not working', 'deprecated', 'corrupt'],
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
id: 'debugging',
|
|
623
|
+
label: 'Debugging',
|
|
624
|
+
color: '#f59e0b',
|
|
625
|
+
dim: 'rgba(245,158,11,0.15)',
|
|
626
|
+
keywords: ['debug', 'diagnos', 'root cause', 'traced', 'reproduc', 'pinpoint',
|
|
627
|
+
'found the issue', 'found issue', 'identified the bug'],
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
id: 'enhancement',
|
|
631
|
+
label: 'Enhancement',
|
|
632
|
+
color: '#3b82f6',
|
|
633
|
+
dim: 'rgba(59,130,246,0.15)',
|
|
634
|
+
keywords: ['added', 'implemented', 'built', 'created', 'feature', 'improved', 'enhanced',
|
|
635
|
+
'redesigned', 'extended', 'upgraded', 'integrated', 'new '],
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: 'research',
|
|
639
|
+
label: 'Research',
|
|
640
|
+
color: '#a855f7',
|
|
641
|
+
dim: 'rgba(168,85,247,0.15)',
|
|
642
|
+
keywords: ['research', 'analyz', 'review', 'explor', 'compar', 'evaluat', 'audit',
|
|
643
|
+
'investigat', 'survey', 'visual review', 'assess', 'benchmark'],
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
id: 'documentation',
|
|
647
|
+
label: 'Documentation',
|
|
648
|
+
color: '#14b8a6',
|
|
649
|
+
dim: 'rgba(20,184,166,0.15)',
|
|
650
|
+
keywords: ['doc', 'readme', 'comment', 'wrote', 'written', 'document', 'spec', 'guide', 'changelog'],
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
id: 'testing',
|
|
654
|
+
label: 'Testing',
|
|
655
|
+
color: '#06b6d4',
|
|
656
|
+
dim: 'rgba(6,182,212,0.15)',
|
|
657
|
+
keywords: ['test', 'spec', 'unit test', 'integration test', 'e2e', 'assert', 'coverage', 'validat'],
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
id: 'refactoring',
|
|
661
|
+
label: 'Refactoring',
|
|
662
|
+
color: '#8b5cf6',
|
|
663
|
+
dim: 'rgba(139,92,246,0.15)',
|
|
664
|
+
keywords: ['refactor', 'restructur', 'clean up', 'cleanup', 'simplif', 'consolidat', 'reorganiz', 'dedup'],
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
id: 'ui-ux',
|
|
668
|
+
label: 'UI/UX',
|
|
669
|
+
color: '#ec4899',
|
|
670
|
+
dim: 'rgba(236,72,153,0.15)',
|
|
671
|
+
keywords: ['ui', 'ux', 'design', 'layout', 'style', 'css', 'component', 'visual', 'interface', 'responsive'],
|
|
672
|
+
},
|
|
673
|
+
];
|
|
674
|
+
|
|
675
|
+
const CAT_OTHER = { id: 'other', label: 'Other', color: '#94a3b8', dim: 'rgba(148,163,184,0.15)' };
|
|
676
|
+
const CAT_MAP = Object.fromEntries([...CATEGORIES, CAT_OTHER].map(c => [c.id, c]));
|
|
677
|
+
|
|
678
|
+
function classifyByKeywords(description) {
|
|
679
|
+
const lower = description.toLowerCase();
|
|
680
|
+
let best = CAT_OTHER, bestScore = 0;
|
|
681
|
+
for (const cat of CATEGORIES) {
|
|
682
|
+
const score = cat.keywords.filter(k => lower.includes(k)).length;
|
|
683
|
+
if (score > bestScore) { bestScore = score; best = cat; }
|
|
684
|
+
}
|
|
685
|
+
return best;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function getCategory(achievement) {
|
|
689
|
+
if (achievement.category) return CAT_MAP[achievement.category] || CAT_OTHER;
|
|
690
|
+
return classifyByKeywords(achievement.description);
|
|
691
|
+
}
|
|
502
692
|
|
|
503
693
|
// ── Filters ──────────────────────────────────────────────────────────────────
|
|
504
694
|
|
|
@@ -539,7 +729,9 @@
|
|
|
539
729
|
|
|
540
730
|
function applyFilters() {
|
|
541
731
|
filterAuthor = document.getElementById('filter-author').value;
|
|
732
|
+
renderLastSessionNote();
|
|
542
733
|
renderStats();
|
|
734
|
+
renderCategories();
|
|
543
735
|
renderChart();
|
|
544
736
|
renderSessions();
|
|
545
737
|
}
|
|
@@ -601,6 +793,33 @@
|
|
|
601
793
|
.replace(/"/g, '"');
|
|
602
794
|
}
|
|
603
795
|
|
|
796
|
+
// ── Last session note ────────────────────────────────────────────────────────
|
|
797
|
+
|
|
798
|
+
function renderLastSessionNote() {
|
|
799
|
+
const el = document.getElementById('last-session-note');
|
|
800
|
+
if (!sessions.length) { el.textContent = ''; return; }
|
|
801
|
+
|
|
802
|
+
const latest = sessions.reduce((a, b) =>
|
|
803
|
+
new Date(a.start_time) > new Date(b.start_time) ? a : b);
|
|
804
|
+
const diff = Date.now() - new Date(latest.start_time);
|
|
805
|
+
const mins = Math.floor(diff / 60000);
|
|
806
|
+
const hours = Math.floor(diff / 3600000);
|
|
807
|
+
const days = Math.floor(diff / 86400000);
|
|
808
|
+
|
|
809
|
+
let ago;
|
|
810
|
+
if (mins < 1) ago = 'just now';
|
|
811
|
+
else if (mins < 60) ago = `${mins} minute${mins !== 1 ? 's' : ''} ago`;
|
|
812
|
+
else if (hours < 24) ago = `${hours} hour${hours !== 1 ? 's' : ''} ago`;
|
|
813
|
+
else if (days === 1) ago = 'yesterday';
|
|
814
|
+
else ago = `${days} days ago`;
|
|
815
|
+
|
|
816
|
+
if (filterPeriod === 'today' && days === 0) {
|
|
817
|
+
el.textContent = '';
|
|
818
|
+
} else {
|
|
819
|
+
el.textContent = `Most recent session ${ago}`;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
604
823
|
// ── Render stats ────────────────────────────────────────────────────────────
|
|
605
824
|
|
|
606
825
|
function renderStats() {
|
|
@@ -615,58 +834,205 @@
|
|
|
615
834
|
document.getElementById('stat-achievements').textContent = ach;
|
|
616
835
|
document.getElementById('stat-saved').textContent = fmtDur(saved);
|
|
617
836
|
document.getElementById('stat-speedup').textContent = sp.toFixed(1) + '×';
|
|
837
|
+
|
|
838
|
+
// Trend: compare avg speedup of most recent 3 sessions vs prior 3
|
|
839
|
+
const trendEl = document.getElementById('stat-speedup-trend');
|
|
840
|
+
const sorted = [...data].sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
|
|
841
|
+
const avgSp = arr => arr.reduce((s, x) => s + speedup(x), 0) / arr.length;
|
|
842
|
+
if (sorted.length >= 4) {
|
|
843
|
+
const recent = sorted.slice(-3);
|
|
844
|
+
const prior = sorted.slice(-6, -3);
|
|
845
|
+
const diff = avgSp(recent) - avgSp(prior);
|
|
846
|
+
if (diff > 0.1) {
|
|
847
|
+
trendEl.textContent = '↑';
|
|
848
|
+
trendEl.style.color = 'var(--green)';
|
|
849
|
+
trendEl.title = `Recent avg ${avgSp(recent).toFixed(1)}× vs prior ${avgSp(prior).toFixed(1)}×`;
|
|
850
|
+
} else if (diff < -0.1) {
|
|
851
|
+
trendEl.textContent = '↓';
|
|
852
|
+
trendEl.style.color = '#f87171';
|
|
853
|
+
trendEl.title = `Recent avg ${avgSp(recent).toFixed(1)}× vs prior ${avgSp(prior).toFixed(1)}×`;
|
|
854
|
+
} else {
|
|
855
|
+
trendEl.textContent = '→';
|
|
856
|
+
trendEl.style.color = 'var(--muted)';
|
|
857
|
+
trendEl.title = `Recent avg ${avgSp(recent).toFixed(1)}× vs prior ${avgSp(prior).toFixed(1)}×`;
|
|
858
|
+
}
|
|
859
|
+
} else {
|
|
860
|
+
trendEl.textContent = '';
|
|
861
|
+
trendEl.title = '';
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ── Render category breakdown ────────────────────────────────────────────────
|
|
866
|
+
|
|
867
|
+
function renderCategories() {
|
|
868
|
+
const data = getFiltered();
|
|
869
|
+
const section = document.getElementById('cat-section');
|
|
870
|
+
if (catChart) { catChart.destroy(); catChart = null; }
|
|
871
|
+
if (!data.length) { section.style.display = 'none'; return; }
|
|
872
|
+
|
|
873
|
+
const tally = {};
|
|
874
|
+
let totalSaved = 0;
|
|
875
|
+
for (const s of data) {
|
|
876
|
+
for (const a of s.achievements) {
|
|
877
|
+
const cat = getCategory(a);
|
|
878
|
+
const mins = a.estimated_human_time_minutes || 0;
|
|
879
|
+
if (!tally[cat.id]) tally[cat.id] = { cat, saved: 0, count: 0 };
|
|
880
|
+
tally[cat.id].saved += mins;
|
|
881
|
+
tally[cat.id].count += 1;
|
|
882
|
+
totalSaved += mins;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const sorted = Object.values(tally).sort((a, b) => b.saved - a.saved);
|
|
887
|
+
if (!sorted.length) { section.style.display = 'none'; return; }
|
|
888
|
+
|
|
889
|
+
section.style.display = '';
|
|
890
|
+
|
|
891
|
+
// Centre-text plugin — draws total time saved + label inside the donut hole
|
|
892
|
+
const centerTextPlugin = {
|
|
893
|
+
id: 'centerText',
|
|
894
|
+
afterDatasetsDraw(ch) {
|
|
895
|
+
const { ctx, chartArea: { top, bottom, left, right } } = ch;
|
|
896
|
+
const cx = (left + right) / 2, cy = (top + bottom) / 2;
|
|
897
|
+
ctx.save();
|
|
898
|
+
ctx.textAlign = 'center';
|
|
899
|
+
ctx.fillStyle = '#f1f5f9';
|
|
900
|
+
ctx.font = 'bold 16px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
|
901
|
+
ctx.fillText(fmtDur(totalSaved), cx, cy - 9);
|
|
902
|
+
ctx.fillStyle = '#64748b';
|
|
903
|
+
ctx.font = '10px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif';
|
|
904
|
+
ctx.fillText('TIME SAVED', cx, cy + 10);
|
|
905
|
+
ctx.restore();
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
catChart = new Chart(document.getElementById('cat-chart').getContext('2d'), {
|
|
910
|
+
type: 'doughnut',
|
|
911
|
+
plugins: [centerTextPlugin],
|
|
912
|
+
data: {
|
|
913
|
+
labels: sorted.map(r => r.cat.label),
|
|
914
|
+
datasets: [{
|
|
915
|
+
data: sorted.map(r => r.saved),
|
|
916
|
+
backgroundColor: sorted.map(r => r.cat.color),
|
|
917
|
+
borderColor: '#1e293b',
|
|
918
|
+
borderWidth: 3,
|
|
919
|
+
hoverOffset: 6,
|
|
920
|
+
}]
|
|
921
|
+
},
|
|
922
|
+
options: {
|
|
923
|
+
responsive: true,
|
|
924
|
+
maintainAspectRatio: false,
|
|
925
|
+
cutout: '68%',
|
|
926
|
+
plugins: {
|
|
927
|
+
legend: { display: false },
|
|
928
|
+
tooltip: {
|
|
929
|
+
backgroundColor: '#1e293b',
|
|
930
|
+
borderColor: '#334155',
|
|
931
|
+
borderWidth: 1,
|
|
932
|
+
titleColor: '#f1f5f9',
|
|
933
|
+
bodyColor: '#94a3b8',
|
|
934
|
+
padding: 10,
|
|
935
|
+
callbacks: {
|
|
936
|
+
label(ctx) {
|
|
937
|
+
const { saved, count } = sorted[ctx.dataIndex];
|
|
938
|
+
const pct = totalSaved > 0 ? ((saved / totalSaved) * 100).toFixed(1) : 0;
|
|
939
|
+
return [
|
|
940
|
+
` ${fmtDur(saved)} saved (${pct}%)`,
|
|
941
|
+
` ${count} achievement${count !== 1 ? 's' : ''}`,
|
|
942
|
+
];
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
// Legend table
|
|
951
|
+
document.getElementById('cat-legend').innerHTML = sorted.map(({ cat, saved, count }) => {
|
|
952
|
+
const pct = totalSaved > 0 ? Math.round((saved / totalSaved) * 100) : 0;
|
|
953
|
+
return `
|
|
954
|
+
<div class="cat-legend-row">
|
|
955
|
+
<span class="cat-dot" style="background:${cat.color}"></span>
|
|
956
|
+
<span class="cat-legend-name">${esc(cat.label)}</span>
|
|
957
|
+
<span class="cat-legend-time" style="color:${cat.color}">${esc(fmtDur(saved))}</span>
|
|
958
|
+
<span class="cat-legend-meta">${count} achievement${count !== 1 ? 's' : ''} · ${pct}%</span>
|
|
959
|
+
</div>`;
|
|
960
|
+
}).join('');
|
|
618
961
|
}
|
|
619
962
|
|
|
620
963
|
// ── Render chart ────────────────────────────────────────────────────────────
|
|
621
964
|
|
|
622
965
|
function renderChart() {
|
|
623
|
-
const sorted
|
|
624
|
-
const labels = sorted.map(s => fmtDateShort(s.start_time));
|
|
625
|
-
const aiTimes = sorted.map(s => s.duration_minutes);
|
|
626
|
-
const humanTimes = sorted.map(s => humanTime(s));
|
|
966
|
+
const sorted = [...getFiltered()].sort((a, b) => new Date(a.start_time) - new Date(b.start_time));
|
|
627
967
|
|
|
628
968
|
if (chart) chart.destroy();
|
|
629
969
|
|
|
630
|
-
|
|
631
|
-
|
|
970
|
+
if (!sorted.length) return;
|
|
971
|
+
|
|
972
|
+
// Build cumulative time-saved values, keeping session reference per point
|
|
973
|
+
let cumulative = 0;
|
|
974
|
+
const points = sorted.map(s => {
|
|
975
|
+
cumulative += (humanTime(s) - s.duration_minutes);
|
|
976
|
+
return { x: fmtDateShort(s.start_time), y: cumulative, session: s };
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const canvas = document.getElementById('main-chart');
|
|
980
|
+
|
|
981
|
+
chart = new Chart(canvas.getContext('2d'), {
|
|
982
|
+
type: 'line',
|
|
632
983
|
data: {
|
|
633
|
-
labels,
|
|
634
|
-
datasets: [
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
borderColor: 'rgb(34,197,94)',
|
|
649
|
-
borderWidth: 1,
|
|
650
|
-
borderRadius: 5,
|
|
651
|
-
borderSkipped: false,
|
|
652
|
-
}
|
|
653
|
-
]
|
|
984
|
+
labels: points.map(p => p.x),
|
|
985
|
+
datasets: [{
|
|
986
|
+
label: 'Time Saved (min)',
|
|
987
|
+
data: points.map(p => p.y),
|
|
988
|
+
borderColor: 'rgb(34,197,94)',
|
|
989
|
+
backgroundColor: 'rgba(34,197,94,0.08)',
|
|
990
|
+
borderWidth: 2.5,
|
|
991
|
+
pointBackgroundColor: 'rgb(34,197,94)',
|
|
992
|
+
pointBorderColor: '#0f172a',
|
|
993
|
+
pointBorderWidth: 2,
|
|
994
|
+
pointRadius: 5,
|
|
995
|
+
pointHoverRadius: 7,
|
|
996
|
+
fill: true,
|
|
997
|
+
tension: 0.35,
|
|
998
|
+
}]
|
|
654
999
|
},
|
|
655
1000
|
options: {
|
|
656
1001
|
responsive: true,
|
|
657
1002
|
maintainAspectRatio: false,
|
|
1003
|
+
onClick(_evt, elements) {
|
|
1004
|
+
if (elements.length) {
|
|
1005
|
+
const s = points[elements[0].index].session;
|
|
1006
|
+
showSession(s.uuid || s.session_id);
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
658
1009
|
plugins: {
|
|
659
|
-
legend: {
|
|
660
|
-
labels: { color: '#94a3b8', font: { size: 12 }, boxWidth: 13, padding: 18 }
|
|
661
|
-
},
|
|
1010
|
+
legend: { display: false },
|
|
662
1011
|
tooltip: {
|
|
663
1012
|
backgroundColor: '#1e293b',
|
|
664
1013
|
borderColor: '#334155',
|
|
665
1014
|
borderWidth: 1,
|
|
666
1015
|
titleColor: '#f1f5f9',
|
|
667
1016
|
bodyColor: '#94a3b8',
|
|
1017
|
+
padding: 12,
|
|
668
1018
|
callbacks: {
|
|
669
|
-
|
|
1019
|
+
title(items) {
|
|
1020
|
+
const s = points[items[0].dataIndex].session;
|
|
1021
|
+
return fmtDate(s.start_time);
|
|
1022
|
+
},
|
|
1023
|
+
label(ctx) {
|
|
1024
|
+
const s = points[ctx.dataIndex].session;
|
|
1025
|
+
const saved = humanTime(s) - s.duration_minutes;
|
|
1026
|
+
const sp = speedup(s);
|
|
1027
|
+
return [
|
|
1028
|
+
` Cumulative saved: ${fmtDur(ctx.parsed.y)}`,
|
|
1029
|
+
` This session: +${fmtDur(saved)}`,
|
|
1030
|
+
` AI time: ${fmtDur(s.duration_minutes)} · Human est: ${fmtDur(humanTime(s))}`,
|
|
1031
|
+
` Speedup: ${sp.toFixed(1)}× · ${s.achievements.length} achievement${s.achievements.length !== 1 ? 's' : ''}`,
|
|
1032
|
+
...(s.author ? [` Author: ${s.author}`] : []),
|
|
1033
|
+
];
|
|
1034
|
+
},
|
|
1035
|
+
afterBody() { return ['', ' Click to open session ↗']; }
|
|
670
1036
|
}
|
|
671
1037
|
}
|
|
672
1038
|
},
|
|
@@ -677,7 +1043,7 @@
|
|
|
677
1043
|
},
|
|
678
1044
|
y: {
|
|
679
1045
|
grid: { color: 'rgba(51,65,85,0.5)' },
|
|
680
|
-
ticks: { color: '#94a3b8', font: { size: 11 }, callback: v => v
|
|
1046
|
+
ticks: { color: '#94a3b8', font: { size: 11 }, callback: v => fmtDur(v) },
|
|
681
1047
|
beginAtZero: true
|
|
682
1048
|
}
|
|
683
1049
|
}
|
|
@@ -731,14 +1097,34 @@
|
|
|
731
1097
|
const ht = humanTime(s);
|
|
732
1098
|
const sv = ht - s.duration_minutes;
|
|
733
1099
|
|
|
1100
|
+
// Build category summary for this session
|
|
1101
|
+
const catTally = {};
|
|
1102
|
+
for (const a of s.achievements) {
|
|
1103
|
+
const cat = getCategory(a);
|
|
1104
|
+
if (!catTally[cat.id]) catTally[cat.id] = { cat, saved: 0, count: 0 };
|
|
1105
|
+
catTally[cat.id].saved += a.estimated_human_time_minutes || 0;
|
|
1106
|
+
catTally[cat.id].count += 1;
|
|
1107
|
+
}
|
|
1108
|
+
const catSummaryHTML = Object.values(catTally)
|
|
1109
|
+
.sort((a, b) => b.saved - a.saved)
|
|
1110
|
+
.map(({ cat, saved, count }) =>
|
|
1111
|
+
`<span class="cat-badge" style="background:${cat.dim};color:${cat.color}">
|
|
1112
|
+
● ${esc(cat.label)} · ${esc(fmtDur(saved))}
|
|
1113
|
+
</span>`)
|
|
1114
|
+
.join('');
|
|
1115
|
+
|
|
734
1116
|
const achievementsHTML = s.achievements.map((a, i) => {
|
|
1117
|
+
const cat = getCategory(a);
|
|
735
1118
|
const created = (a.files_created || []).map(f => `<span class="file-chip created">+ ${esc(f)}</span>`).join('');
|
|
736
1119
|
const modified = (a.files_modified || []).map(f => `<span class="file-chip modified">~ ${esc(f)}</span>`).join('');
|
|
737
1120
|
const hasFiles = created || modified;
|
|
738
1121
|
|
|
739
1122
|
return `
|
|
740
1123
|
<div class="achievement-card">
|
|
741
|
-
<div
|
|
1124
|
+
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px">
|
|
1125
|
+
<div class="achievement-num" style="margin-bottom:0">Achievement ${i + 1} of ${s.achievements.length}</div>
|
|
1126
|
+
<span class="cat-badge" style="background:${cat.dim};color:${cat.color}">● ${esc(cat.label)}</span>
|
|
1127
|
+
</div>
|
|
742
1128
|
<div class="achievement-desc">${esc(a.description)}</div>
|
|
743
1129
|
<div class="achievement-footer">
|
|
744
1130
|
${hasFiles ? `<div class="files-row">${created}${modified}</div>` : ''}
|
|
@@ -789,6 +1175,9 @@
|
|
|
789
1175
|
|
|
790
1176
|
<div class="speedup-banner">⚡ ${esc(s.ai_speedup)}</div>
|
|
791
1177
|
|
|
1178
|
+
<div class="section-label" style="margin-bottom:10px;">By Category</div>
|
|
1179
|
+
<div class="cat-summary" style="margin-bottom:24px">${catSummaryHTML}</div>
|
|
1180
|
+
|
|
792
1181
|
<div class="section-label" style="margin-bottom:14px;">Achievements</div>
|
|
793
1182
|
${achievementsHTML}
|
|
794
1183
|
`;
|
|
@@ -806,13 +1195,44 @@
|
|
|
806
1195
|
|
|
807
1196
|
// ── Init ────────────────────────────────────────────────────────────────────
|
|
808
1197
|
|
|
1198
|
+
function pickDefaultPeriod() {
|
|
1199
|
+
const ORDER = ['today', '3d', 'week', 'month', '3m', '6m', 'year', 'all'];
|
|
1200
|
+
const DAY = 86400000;
|
|
1201
|
+
const cutoffs = {
|
|
1202
|
+
today: () => { const d = new Date(); d.setHours(0,0,0,0); return d; },
|
|
1203
|
+
'3d': () => new Date(Date.now() - 3 * DAY),
|
|
1204
|
+
week: () => new Date(Date.now() - 7 * DAY),
|
|
1205
|
+
month: () => new Date(Date.now() - 30 * DAY),
|
|
1206
|
+
'3m': () => new Date(Date.now() - 90 * DAY),
|
|
1207
|
+
'6m': () => new Date(Date.now() - 180 * DAY),
|
|
1208
|
+
year: () => new Date(Date.now() - 365 * DAY),
|
|
1209
|
+
all: () => null,
|
|
1210
|
+
};
|
|
1211
|
+
for (const period of ORDER) {
|
|
1212
|
+
const cutoff = cutoffs[period]();
|
|
1213
|
+
const hasData = cutoff
|
|
1214
|
+
? sessions.some(s => new Date(s.start_time) >= cutoff)
|
|
1215
|
+
: sessions.length > 0;
|
|
1216
|
+
if (hasData) {
|
|
1217
|
+
filterPeriod = period;
|
|
1218
|
+
const btn = document.querySelector(`.period-pill[data-period="${period}"]`);
|
|
1219
|
+
document.querySelectorAll('.period-pill').forEach(b => b.classList.remove('active'));
|
|
1220
|
+
if (btn) btn.classList.add('active');
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
809
1226
|
async function init() {
|
|
810
1227
|
try {
|
|
811
1228
|
const res = await fetch('/api/sessions');
|
|
812
1229
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
813
1230
|
sessions = await res.json();
|
|
814
1231
|
populateAuthors();
|
|
1232
|
+
pickDefaultPeriod();
|
|
1233
|
+
renderLastSessionNote();
|
|
815
1234
|
renderStats();
|
|
1235
|
+
renderCategories();
|
|
816
1236
|
renderChart();
|
|
817
1237
|
renderSessions();
|
|
818
1238
|
} catch (err) {
|
|
@@ -49,7 +49,18 @@ When the user invokes `/ai-gains` or confirms they want to update the log:
|
|
|
49
49
|
|
|
50
50
|
6. For each achievement, estimate how long a human developer would take to do the same work without AI assistance.
|
|
51
51
|
|
|
52
|
-
7.
|
|
52
|
+
7. For each achievement, assign a `category` from this list — pick the one that best describes the primary nature of the work:
|
|
53
|
+
- `bug-fix` — fixing a broken or incorrect behaviour
|
|
54
|
+
- `debugging` — diagnosing, tracing or reproducing a problem (without necessarily fixing it yet)
|
|
55
|
+
- `enhancement` — adding or improving a feature, refactoring, or extending functionality
|
|
56
|
+
- `research` — investigating options, reviewing code/docs, evaluating approaches, visual review
|
|
57
|
+
- `documentation` — writing or updating docs, comments, specs, READMEs
|
|
58
|
+
- `testing` — writing or executing tests, validating functionality
|
|
59
|
+
- `refactoring` — restructuring existing code without changing its external behavior
|
|
60
|
+
- `ui-ux` — designing or improving user interfaces and user experiences
|
|
61
|
+
- `other` — anything that doesn't fit the above
|
|
62
|
+
|
|
63
|
+
8. Write the updated JSON back to `.ai-gains/<start_utc_timestamp>_<uuid>.json`, preserving existing fields. The JSON structure should look like this:
|
|
53
64
|
|
|
54
65
|
```json
|
|
55
66
|
{
|
|
@@ -61,7 +72,8 @@ When the user invokes `/ai-gains` or confirms they want to update the log:
|
|
|
61
72
|
"achievements": [
|
|
62
73
|
{
|
|
63
74
|
"description": "<what was done>",
|
|
64
|
-
"estimated_human_time_minutes": "<number>"
|
|
75
|
+
"estimated_human_time_minutes": "<number>",
|
|
76
|
+
"category": "<bug-fix | debugging | enhancement | research | documentation | testing | refactoring | ui-ux | other>"
|
|
65
77
|
}
|
|
66
78
|
],
|
|
67
79
|
"ai_speedup": "<summary of time saved vs human>"
|