claude-roi 0.5.0 → 0.6.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Codelens AI
2
2
 
3
+ **[codelensai-dev.vercel.app](https://codelensai-dev.vercel.app/)**
4
+
3
5
  **Agent Productivity-to-Cost Correlator** — Is your AI coding agent actually shipping code?
4
6
 
5
7
  Codelens AI ties Claude Code token usage to actual git output. It reads your local Claude Code session files, correlates them with git commits by timestamp, and serves a dashboard answering: *"Am I getting ROI from my AI coding agent?"*
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-roi",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Correlate Claude Code token usage with git output to measure AI coding agent ROI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "bugs": {
32
32
  "url": "https://github.com/Akshat2634/Codelens-AI/issues"
33
33
  },
34
- "homepage": "https://github.com/Akshat2634/Codelens-AI#readme",
34
+ "homepage": "https://codelensai-dev.vercel.app/",
35
35
  "engines": {
36
36
  "node": ">=18.0.0"
37
37
  },
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Codelens AI — Agent Productivity Dashboard</title>
7
7
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet">
8
+ <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect x='8' y='14' width='4' height='12' rx='1.5' fill='%23e67e22'/%3E%3Crect x='14' y='6' width='4' height='20' rx='1.5' fill='%232ecc71'/%3E%3Crect x='20' y='10' width='4' height='16' rx='1.5' fill='%233498db'/%3E%3C/svg%3E">
8
9
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
9
10
  <script>
10
11
  (function() {
@@ -206,8 +207,37 @@
206
207
  background-size: 200% 200%;
207
208
  -webkit-background-clip: text;
208
209
  -webkit-text-fill-color: transparent;
210
+ background-clip: text;
209
211
  animation: gradientMove 8s ease-in-out infinite;
210
212
  margin-bottom: 12px;
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ gap: 12px;
217
+ }
218
+ .logo-bars {
219
+ display: flex;
220
+ align-items: flex-end;
221
+ gap: 3px;
222
+ -webkit-text-fill-color: initial;
223
+ }
224
+ .logo-bars .bar-orange {
225
+ width: 5px;
226
+ height: 18px;
227
+ border-radius: 2px;
228
+ background: #e67e22;
229
+ }
230
+ .logo-bars .bar-teal {
231
+ width: 5px;
232
+ height: 28px;
233
+ border-radius: 2px;
234
+ background: #2ecc71;
235
+ }
236
+ .logo-bars .bar-blue {
237
+ width: 5px;
238
+ height: 22px;
239
+ border-radius: 2px;
240
+ background: #3498db;
211
241
  }
212
242
  @keyframes gradientMove {
213
243
  0%, 100% { background-position: 0% 50%; }
@@ -1317,6 +1347,175 @@
1317
1347
  .container { padding: 16px; }
1318
1348
  header h1 { font-size: 1.8rem; }
1319
1349
  .hero-legend, .cost-legend, .token-legend { flex-wrap: wrap; }
1350
+ .share-modal { padding: 20px; }
1351
+ .share-actions { flex-direction: column; }
1352
+ }
1353
+
1354
+ /* ── Share Button Glow ─────────────────────── */
1355
+ .share-btn:hover {
1356
+ color: var(--accent-purple);
1357
+ border-color: rgba(168, 85, 247, 0.3);
1358
+ box-shadow: 0 0 12px rgba(168, 85, 247, 0.15);
1359
+ }
1360
+
1361
+ /* ── Share Modal ───────────────────────────── */
1362
+ .share-modal-backdrop {
1363
+ position: fixed;
1364
+ inset: 0;
1365
+ z-index: 1000;
1366
+ background: rgba(0, 0, 0, 0.6);
1367
+ backdrop-filter: blur(8px);
1368
+ display: flex;
1369
+ align-items: center;
1370
+ justify-content: center;
1371
+ animation: modalFadeIn 0.25s ease;
1372
+ }
1373
+ @keyframes modalFadeIn {
1374
+ from { opacity: 0; }
1375
+ to { opacity: 1; }
1376
+ }
1377
+ .share-modal {
1378
+ background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
1379
+ backdrop-filter: blur(20px);
1380
+ border: 1px solid var(--glass-border);
1381
+ border-radius: var(--radius);
1382
+ padding: 32px;
1383
+ max-width: 680px;
1384
+ width: 90vw;
1385
+ max-height: 90vh;
1386
+ overflow-y: auto;
1387
+ position: relative;
1388
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.4);
1389
+ animation: modalSlideUp 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
1390
+ }
1391
+ @keyframes modalSlideUp {
1392
+ from { opacity: 0; transform: translateY(20px); }
1393
+ to { opacity: 1; transform: translateY(0); }
1394
+ }
1395
+ .share-modal-title {
1396
+ font-family: var(--font-display);
1397
+ font-size: 1.2rem;
1398
+ font-weight: 700;
1399
+ margin-bottom: 4px;
1400
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
1401
+ -webkit-background-clip: text;
1402
+ -webkit-text-fill-color: transparent;
1403
+ background-clip: text;
1404
+ }
1405
+ .share-modal-subtitle {
1406
+ font-size: 0.85rem;
1407
+ color: var(--text-muted);
1408
+ margin-bottom: 24px;
1409
+ }
1410
+ .share-modal-close {
1411
+ position: absolute;
1412
+ top: 16px;
1413
+ right: 16px;
1414
+ width: 32px;
1415
+ height: 32px;
1416
+ border-radius: 50%;
1417
+ border: 1px solid var(--glass-border);
1418
+ background: var(--overlay-medium);
1419
+ color: var(--text-secondary);
1420
+ font-size: 1.2rem;
1421
+ cursor: pointer;
1422
+ display: flex;
1423
+ align-items: center;
1424
+ justify-content: center;
1425
+ transition: background 0.2s, color 0.2s;
1426
+ }
1427
+ .share-modal-close:hover {
1428
+ background: var(--overlay-intense);
1429
+ color: var(--text-primary);
1430
+ }
1431
+ .share-theme-toggle {
1432
+ display: flex;
1433
+ gap: 4px;
1434
+ margin-bottom: 16px;
1435
+ background: var(--bg-hover);
1436
+ border: 1px solid var(--border);
1437
+ border-radius: 20px;
1438
+ padding: 3px;
1439
+ width: fit-content;
1440
+ }
1441
+ .share-theme-toggle button {
1442
+ display: flex;
1443
+ align-items: center;
1444
+ gap: 6px;
1445
+ padding: 6px 16px;
1446
+ border-radius: 16px;
1447
+ border: none;
1448
+ background: transparent;
1449
+ color: var(--text-muted);
1450
+ font-family: var(--font-body);
1451
+ font-size: 0.8rem;
1452
+ font-weight: 500;
1453
+ cursor: pointer;
1454
+ transition: background 0.2s, color 0.2s;
1455
+ }
1456
+ .share-theme-toggle button:hover {
1457
+ color: var(--text-primary);
1458
+ }
1459
+ .share-theme-toggle button.active {
1460
+ background: var(--bg-card);
1461
+ color: var(--text-primary);
1462
+ box-shadow: 0 1px 6px var(--shadow-card);
1463
+ }
1464
+ .share-preview-wrap {
1465
+ display: flex;
1466
+ justify-content: center;
1467
+ margin-bottom: 24px;
1468
+ }
1469
+ .share-preview-wrap canvas {
1470
+ width: 100%;
1471
+ max-width: 420px;
1472
+ aspect-ratio: 3 / 4;
1473
+ border-radius: var(--radius-sm);
1474
+ box-shadow: 0 8px 32px var(--shadow-card);
1475
+ }
1476
+ .share-actions {
1477
+ display: flex;
1478
+ gap: 12px;
1479
+ justify-content: center;
1480
+ }
1481
+ .share-action-btn {
1482
+ display: flex;
1483
+ align-items: center;
1484
+ gap: 8px;
1485
+ padding: 10px 20px;
1486
+ border-radius: var(--radius-sm);
1487
+ border: 1px solid var(--glass-border);
1488
+ background: var(--overlay-medium);
1489
+ color: var(--text-secondary);
1490
+ font-family: var(--font-body);
1491
+ font-size: 0.85rem;
1492
+ cursor: pointer;
1493
+ transition: background 0.2s, border-color 0.2s, color 0.2s, transform 0.15s;
1494
+ }
1495
+ .share-action-btn:hover {
1496
+ background: var(--overlay-intense);
1497
+ border-color: var(--glass-border-hover);
1498
+ color: var(--text-primary);
1499
+ transform: translateY(-1px);
1500
+ }
1501
+ .share-action-btn.primary {
1502
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(168, 85, 247, 0.2));
1503
+ border-color: rgba(59, 130, 246, 0.3);
1504
+ color: var(--text-primary);
1505
+ }
1506
+ .share-action-btn.primary:hover {
1507
+ background: linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(168, 85, 247, 0.3));
1508
+ }
1509
+ .share-toast {
1510
+ text-align: center;
1511
+ margin-top: 16px;
1512
+ font-size: 0.8rem;
1513
+ color: var(--accent-green);
1514
+ opacity: 0;
1515
+ transition: opacity 0.3s;
1516
+ }
1517
+ .share-toast.visible {
1518
+ opacity: 1;
1320
1519
  }
1321
1520
  </style>
1322
1521
  </head>
@@ -1324,6 +1523,11 @@
1324
1523
  <div class="container">
1325
1524
  <header>
1326
1525
  <div class="header-actions">
1526
+ <button class="header-action-btn share-btn" id="share-btn" aria-label="Share report card" title="Share AI Report Card">
1527
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1528
+ <circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
1529
+ </svg>
1530
+ </button>
1327
1531
  <button class="header-action-btn refresh-btn" id="refresh-btn" aria-label="Refresh data" title="Refresh dashboard data">
1328
1532
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1329
1533
  <polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
@@ -1347,7 +1551,7 @@
1347
1551
  </button>
1348
1552
  </div>
1349
1553
  </div>
1350
- <h1>Codelens AI</h1>
1554
+ <h1><span class="logo-bars"><span class="bar-orange"></span><span class="bar-teal"></span><span class="bar-blue"></span></span>Codelens AI</h1>
1351
1555
  <p class="tagline">Correlates your AI coding agent's token spend with actual git output — see what shipped, what churned, and what it cost.</p>
1352
1556
  <div class="meta-info">
1353
1557
  <span class="badge" id="date-range"></span>
@@ -1364,10 +1568,43 @@
1364
1568
  </div>
1365
1569
 
1366
1570
  <footer>
1367
- Made by <a href="https://www.linkedin.com/in/akshat2634/">Akshat</a> &middot; Powered by <a href="https://code.claude.com/docs/en/overview">Claude Code</a> &middot; <a href="https://github.com/Akshat2634/Codelens-AI">GitHub</a>
1571
+ Made by <a href="https://www.linkedin.com/in/akshat2634/">Akshat</a> &middot; Powered by <a href="https://code.claude.com/docs/en/overview">Claude Code</a> &middot; <a href="https://github.com/Akshat2634/Codelens-AI">GitHub</a> &middot; <a href="https://codelensai-dev.vercel.app/">Website</a>
1368
1572
  <div style="margin-top:6px;font-size:0.75rem;opacity:0.7;">Open source — contributions, ideas, and feedback welcome! <a href="https://github.com/Akshat2634/Codelens-AI" style="color:var(--accent-blue);">Star the repo</a> if you find it useful.</div>
1369
1573
  <div style="margin-top:6px;font-size:0.7rem;opacity:0.4;">Cost estimates are approximate — based on Anthropic's published per-token pricing and may vary from actual billing. Currently supports Claude Code only. Support for Cursor, Codex, Gemini CLI, and more coming soon.</div>
1370
1574
  </footer>
1575
+
1576
+ <!-- Share Report Card Modal -->
1577
+ <div class="share-modal-backdrop" id="share-modal" style="display:none;">
1578
+ <div class="share-modal">
1579
+ <button class="share-modal-close" id="share-modal-close" aria-label="Close">&times;</button>
1580
+ <h2 class="share-modal-title">Your AI Report Card</h2>
1581
+ <p class="share-modal-subtitle">Share your AI coding insights</p>
1582
+ <div class="share-theme-toggle" id="share-theme-toggle">
1583
+ <button data-share-theme="dark" class="active" title="Dark card">
1584
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
1585
+ Dark
1586
+ </button>
1587
+ <button data-share-theme="light" title="Light card">
1588
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
1589
+ Light
1590
+ </button>
1591
+ </div>
1592
+ <div class="share-preview-wrap">
1593
+ <canvas id="share-canvas"></canvas>
1594
+ </div>
1595
+ <div class="share-actions">
1596
+ <button class="share-action-btn primary" id="share-copy-btn">
1597
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
1598
+ Copy to Clipboard
1599
+ </button>
1600
+ <button class="share-action-btn" id="share-download-btn">
1601
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1602
+ Download PNG
1603
+ </button>
1604
+ </div>
1605
+ <div class="share-toast" id="share-toast"></div>
1606
+ </div>
1607
+ </div>
1371
1608
  </div>
1372
1609
 
1373
1610
  <script>
@@ -2379,6 +2616,439 @@ window.toggleTimelineScale = function() {
2379
2616
  if (btn) btn.textContent = timelineLogScale ? 'Linear scale' : 'Log scale';
2380
2617
  };
2381
2618
 
2619
+ /* ── Share Report Card ─────────────────────────── */
2620
+
2621
+ const GRADE_HEX = { A: '#22d3a8', B: '#3b82f6', C: '#f59e0b', D: '#f0883e', F: '#ef4444' };
2622
+ const INSIGHT_COLORS = { warning: '#f59e0b', success: '#22d3a8', info: '#3b82f6', tip: '#a855f7' };
2623
+
2624
+ const SHARE_THEME = {
2625
+ dark: {
2626
+ bg1: '#0a0e17', bg2: '#111827',
2627
+ orb1: 'rgba(59, 130, 246, 0.06)', orb2: 'rgba(168, 85, 247, 0.05)', orb3: 'rgba(34, 211, 168, 0.04)',
2628
+ border: 'rgba(255,255,255,0.06)',
2629
+ glassBg: 'rgba(255,255,255,0.03)', glassBorder: 'rgba(255,255,255,0.06)',
2630
+ textPrimary: '#f0f4f8', textSecondary: '#94a3b8', textMuted: '#64748b', textDim: '#475569',
2631
+ barTrack: 'rgba(255,255,255,0.06)', heatmapEmpty: 'rgba(255,255,255,0.04)',
2632
+ separator: 'rgba(255,255,255,0.06)', separatorLight: 'rgba(255,255,255,0.04)',
2633
+ insightText: '#cbd5e1',
2634
+ },
2635
+ light: {
2636
+ bg1: '#f8fafc', bg2: '#e2e8f0',
2637
+ orb1: 'rgba(59, 130, 246, 0.08)', orb2: 'rgba(168, 85, 247, 0.06)', orb3: 'rgba(34, 211, 168, 0.05)',
2638
+ border: 'rgba(0,0,0,0.08)',
2639
+ glassBg: 'rgba(0,0,0,0.03)', glassBorder: 'rgba(0,0,0,0.08)',
2640
+ textPrimary: '#0f172a', textSecondary: '#475569', textMuted: '#64748b', textDim: '#94a3b8',
2641
+ barTrack: 'rgba(0,0,0,0.06)', heatmapEmpty: 'rgba(0,0,0,0.05)',
2642
+ separator: 'rgba(0,0,0,0.08)', separatorLight: 'rgba(0,0,0,0.05)',
2643
+ insightText: '#334155',
2644
+ }
2645
+ };
2646
+
2647
+ function drawRoundRect(ctx, x, y, w, h, r) {
2648
+ ctx.beginPath();
2649
+ ctx.moveTo(x + r, y);
2650
+ ctx.lineTo(x + w - r, y);
2651
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
2652
+ ctx.lineTo(x + w, y + h - r);
2653
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
2654
+ ctx.lineTo(x + r, y + h);
2655
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
2656
+ ctx.lineTo(x, y + r);
2657
+ ctx.quadraticCurveTo(x, y, x + r, y);
2658
+ ctx.closePath();
2659
+ }
2660
+
2661
+ function drawOrb(ctx, x, y, radius, color) {
2662
+ const grad = ctx.createRadialGradient(x, y, 0, x, y, radius);
2663
+ grad.addColorStop(0, color);
2664
+ grad.addColorStop(1, 'transparent');
2665
+ ctx.fillStyle = grad;
2666
+ ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
2667
+ }
2668
+
2669
+ function drawGradeCircle(ctx, cx, cy, r, grade, th) {
2670
+ const color = GRADE_HEX[grade] || GRADE_HEX.F;
2671
+ // Background circle
2672
+ ctx.beginPath();
2673
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
2674
+ ctx.strokeStyle = th.barTrack;
2675
+ ctx.lineWidth = 6;
2676
+ ctx.stroke();
2677
+ // Grade arc
2678
+ const pct = { A: 0.95, B: 0.8, C: 0.6, D: 0.4, F: 0.2 }[grade] || 0.2;
2679
+ ctx.beginPath();
2680
+ ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * pct);
2681
+ ctx.strokeStyle = color;
2682
+ ctx.lineWidth = 6;
2683
+ ctx.lineCap = 'round';
2684
+ ctx.stroke();
2685
+ // Grade letter
2686
+ ctx.font = 'bold 36px "JetBrains Mono", monospace';
2687
+ ctx.fillStyle = color;
2688
+ ctx.textAlign = 'center';
2689
+ ctx.textBaseline = 'middle';
2690
+ ctx.fillText(grade, cx, cy);
2691
+ // Label below
2692
+ ctx.font = '600 10px "JetBrains Mono", monospace';
2693
+ ctx.fillStyle = th.textMuted;
2694
+ ctx.fillText('ROI GRADE', cx, cy + r + 18);
2695
+ ctx.textAlign = 'left';
2696
+ ctx.textBaseline = 'alphabetic';
2697
+ }
2698
+
2699
+ function drawStatBox(ctx, x, y, w, h, value, label, accentColor, th) {
2700
+ // Glass background
2701
+ drawRoundRect(ctx, x, y, w, h, 8);
2702
+ ctx.fillStyle = th.glassBg;
2703
+ ctx.fill();
2704
+ ctx.strokeStyle = th.glassBorder;
2705
+ ctx.lineWidth = 1;
2706
+ ctx.stroke();
2707
+ // Value
2708
+ ctx.font = 'bold 20px "JetBrains Mono", monospace';
2709
+ ctx.fillStyle = accentColor;
2710
+ ctx.fillText(value, x + 14, y + 28);
2711
+ // Label
2712
+ ctx.font = '400 11px "DM Sans", sans-serif';
2713
+ ctx.fillStyle = th.textMuted;
2714
+ ctx.fillText(label, x + 14, y + h - 10);
2715
+ }
2716
+
2717
+ function drawProgressBar(ctx, x, y, w, label, value, suffix, color, th) {
2718
+ const barH = 8;
2719
+ const labelW = 140;
2720
+ const valueW = 50;
2721
+ const barW = w - labelW - valueW - 16;
2722
+ // Label
2723
+ ctx.font = '400 12px "DM Sans", sans-serif';
2724
+ ctx.fillStyle = th.textSecondary;
2725
+ ctx.fillText(label, x, y + 10);
2726
+ // Bar track
2727
+ const bx = x + labelW;
2728
+ drawRoundRect(ctx, bx, y + 3, barW, barH, 4);
2729
+ ctx.fillStyle = th.barTrack;
2730
+ ctx.fill();
2731
+ // Bar fill
2732
+ const fillW = Math.max(0, (value / 100) * barW);
2733
+ if (fillW > 0) {
2734
+ drawRoundRect(ctx, bx, y + 3, fillW, barH, 4);
2735
+ ctx.fillStyle = color;
2736
+ ctx.fill();
2737
+ }
2738
+ // Value text
2739
+ ctx.font = '600 12px "JetBrains Mono", monospace';
2740
+ ctx.fillStyle = color;
2741
+ ctx.textAlign = 'right';
2742
+ ctx.fillText(Math.round(value) + suffix, x + w, y + 11);
2743
+ ctx.textAlign = 'left';
2744
+ }
2745
+
2746
+ function drawMiniHeatmap(ctx, x, y, w, heatmapData, th, isLight) {
2747
+ if (!heatmapData || !heatmapData.length) return;
2748
+ const cols = 24;
2749
+ const rows = 7;
2750
+ const gap = 2;
2751
+ const cellW = (w - (cols - 1) * gap) / cols;
2752
+ const cellH = Math.min(cellW, 10);
2753
+ const maxVal = Math.max(1, ...heatmapData.flat());
2754
+ const dayLabels = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
2755
+ const labelOffset = 14;
2756
+
2757
+ for (let day = 0; day < rows; day++) {
2758
+ // Day label
2759
+ ctx.font = '500 8px "JetBrains Mono", monospace';
2760
+ ctx.fillStyle = th.textDim;
2761
+ ctx.fillText(dayLabels[day], x, y + day * (cellH + gap) + cellH - 1);
2762
+ for (let hour = 0; hour < cols; hour++) {
2763
+ const val = heatmapData[day]?.[hour] || 0;
2764
+ const cx = x + labelOffset + hour * (cellW + gap);
2765
+ const cy = y + day * (cellH + gap);
2766
+ drawRoundRect(ctx, cx, cy, cellW, cellH, 2);
2767
+ if (val === 0) {
2768
+ ctx.fillStyle = th.heatmapEmpty;
2769
+ } else {
2770
+ const intensity = val / maxVal;
2771
+ if (isLight) {
2772
+ ctx.fillStyle = `rgba(5,150,105,${0.25 + intensity * 0.75})`;
2773
+ } else {
2774
+ ctx.fillStyle = `rgba(34,211,168,${0.2 + intensity * 0.8})`;
2775
+ }
2776
+ }
2777
+ ctx.fill();
2778
+ }
2779
+ }
2780
+ }
2781
+
2782
+ function drawInsightRow(ctx, x, y, w, insight, th) {
2783
+ const color = INSIGHT_COLORS[insight.type] || INSIGHT_COLORS.info;
2784
+ // Dot indicator
2785
+ ctx.beginPath();
2786
+ ctx.arc(x + 6, y + 8, 4, 0, Math.PI * 2);
2787
+ ctx.fillStyle = color;
2788
+ ctx.fill();
2789
+ // Text (truncate if needed)
2790
+ ctx.font = '400 12px "DM Sans", sans-serif';
2791
+ ctx.fillStyle = th.insightText;
2792
+ let text = insight.text;
2793
+ // Truncate to fit width
2794
+ const maxW = w - 24;
2795
+ while (ctx.measureText(text).width > maxW && text.length > 3) {
2796
+ text = text.slice(0, -4) + '...';
2797
+ }
2798
+ ctx.fillText(text, x + 18, y + 12);
2799
+ }
2800
+
2801
+ function renderShareCard(canvas) {
2802
+ const W = 600, H = 800;
2803
+ canvas.width = W * 2;
2804
+ canvas.height = H * 2;
2805
+ const ctx = canvas.getContext('2d');
2806
+ ctx.scale(2, 2);
2807
+
2808
+ const isLight = shareCardTheme === 'light';
2809
+ const th = isLight ? SHARE_THEME.light : SHARE_THEME.dark;
2810
+
2811
+ const d = DATA;
2812
+ const s = d.summary;
2813
+ const t = d.tokenAnalytics;
2814
+ const ls = d.lineSurvival;
2815
+
2816
+ // ── Background ──
2817
+ const bgGrad = ctx.createLinearGradient(0, 0, W, H);
2818
+ bgGrad.addColorStop(0, th.bg1);
2819
+ bgGrad.addColorStop(0.5, th.bg2);
2820
+ bgGrad.addColorStop(1, th.bg1);
2821
+ ctx.fillStyle = bgGrad;
2822
+ ctx.fillRect(0, 0, W, H);
2823
+
2824
+ // Subtle orbs
2825
+ drawOrb(ctx, W * 0.2, H * 0.12, 200, th.orb1);
2826
+ drawOrb(ctx, W * 0.85, H * 0.35, 180, th.orb2);
2827
+ drawOrb(ctx, W * 0.3, H * 0.85, 160, th.orb3);
2828
+
2829
+ // Border
2830
+ drawRoundRect(ctx, 0, 0, W, H, 16);
2831
+ ctx.strokeStyle = th.border;
2832
+ ctx.lineWidth = 1;
2833
+ ctx.stroke();
2834
+
2835
+ // ── Header with logo bars ──
2836
+ let y = 36;
2837
+ // Draw 3-bar logo
2838
+ const barX = 32;
2839
+ const barW = 5;
2840
+ const barGap = 4;
2841
+ const barBaseY = y + 26;
2842
+ drawRoundRect(ctx, barX, barBaseY - 16, barW, 16, 2);
2843
+ ctx.fillStyle = '#e67e22';
2844
+ ctx.fill();
2845
+ drawRoundRect(ctx, barX + barW + barGap, barBaseY - 26, barW, 26, 2);
2846
+ ctx.fillStyle = '#2ecc71';
2847
+ ctx.fill();
2848
+ drawRoundRect(ctx, barX + (barW + barGap) * 2, barBaseY - 20, barW, 20, 2);
2849
+ ctx.fillStyle = '#3498db';
2850
+ ctx.fill();
2851
+ // Title text after logo
2852
+ const titleX = barX + (barW + barGap) * 3 + 8;
2853
+ ctx.font = 'bold 28px "JetBrains Mono", monospace';
2854
+ const titleGrad = ctx.createLinearGradient(titleX, y, titleX + 240, y + 28);
2855
+ if (isLight) {
2856
+ titleGrad.addColorStop(0, '#2563eb');
2857
+ titleGrad.addColorStop(0.5, '#7c3aed');
2858
+ titleGrad.addColorStop(1, '#0891b2');
2859
+ } else {
2860
+ titleGrad.addColorStop(0, '#3b82f6');
2861
+ titleGrad.addColorStop(0.5, '#a855f7');
2862
+ titleGrad.addColorStop(1, '#06b6d4');
2863
+ }
2864
+ ctx.fillStyle = titleGrad;
2865
+ ctx.fillText('Codelens AI', titleX, y + 26);
2866
+
2867
+ y += 44;
2868
+ ctx.font = '300 13px "DM Sans", sans-serif';
2869
+ ctx.fillStyle = th.textMuted;
2870
+ ctx.fillText('AI Coding Report Card', 32, y);
2871
+
2872
+ y += 20;
2873
+ const fmtDate = iso => {
2874
+ try { return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); }
2875
+ catch { return iso; }
2876
+ };
2877
+ const dateLabel = (d.meta.startDate && d.meta.endDate)
2878
+ ? fmtDate(d.meta.startDate) + ' – ' + fmtDate(d.meta.endDate)
2879
+ : 'Last ' + d.meta.daysAnalyzed + ' days';
2880
+ ctx.font = '500 11px "JetBrains Mono", monospace';
2881
+ ctx.fillStyle = th.textDim;
2882
+ ctx.fillText(dateLabel, 32, y);
2883
+
2884
+ // Thin separator
2885
+ y += 16;
2886
+ ctx.strokeStyle = th.separator;
2887
+ ctx.lineWidth = 1;
2888
+ ctx.beginPath();
2889
+ ctx.moveTo(32, y);
2890
+ ctx.lineTo(W - 32, y);
2891
+ ctx.stroke();
2892
+
2893
+ // ── Grade + Stats Row ──
2894
+ y += 20;
2895
+ drawGradeCircle(ctx, 80, y + 52, 42, s.overallGrade, th);
2896
+
2897
+ const boxW = 152;
2898
+ const boxH = 52;
2899
+ const boxGap = 12;
2900
+ const boxStartX = 160;
2901
+ // Deeper accent colors for light theme so they pop against white bg
2902
+ const cOrange = isLight ? '#d97706' : '#f59e0b';
2903
+ const cGreen = isLight ? '#059669' : '#22d3a8';
2904
+ const cBlue = isLight ? '#2563eb' : '#3b82f6';
2905
+ const cPurple = isLight ? '#7c3aed' : '#a855f7';
2906
+ drawStatBox(ctx, boxStartX, y, boxW, boxH, '$' + s.totalCost.toFixed(2), 'Total Spend', cOrange, th);
2907
+ drawStatBox(ctx, boxStartX + boxW + boxGap, y, boxW, boxH, String(s.totalCommits), 'Commits Shipped', cGreen, th);
2908
+ drawStatBox(ctx, boxStartX, y + boxH + boxGap, boxW, boxH,
2909
+ s.avgCostPerCommit != null ? '$' + s.avgCostPerCommit.toFixed(2) : 'N/A', 'Avg $/Commit', cBlue, th);
2910
+ drawStatBox(ctx, boxStartX + boxW + boxGap, y + boxH + boxGap, boxW, boxH,
2911
+ String(s.totalSessions), 'Sessions', cPurple, th);
2912
+
2913
+ // ── Efficiency Bars ──
2914
+ y += (boxH + boxGap) * 2 + 24;
2915
+ ctx.font = '600 10px "JetBrains Mono", monospace';
2916
+ ctx.fillStyle = th.textDim;
2917
+ ctx.letterSpacing = '0.08em';
2918
+ ctx.fillText('EFFICIENCY', 32, y);
2919
+ ctx.letterSpacing = '0';
2920
+ y += 18;
2921
+
2922
+ // Each bar gets its own accent color for visual variety
2923
+ const effColor = isLight ? '#0d9488' : '#06b6d4'; // cyan/teal
2924
+ const cacheColor = isLight ? '#7c3aed' : '#a855f7'; // purple
2925
+ const survColor = isLight ? '#059669' : '#22d3a8'; // green
2926
+ drawProgressBar(ctx, 32, y, W - 64, 'Token Efficiency', t.tokenEfficiencyRate, '%', effColor, th);
2927
+ y += 34;
2928
+ drawProgressBar(ctx, 32, y, W - 64, 'Cache Hit Rate', t.cacheHitRate, '%', cacheColor, th);
2929
+ y += 34;
2930
+ drawProgressBar(ctx, 32, y, W - 64, 'Line Survival', ls.survivalRate, '%', survColor, th);
2931
+
2932
+ // ── Heatmap ──
2933
+ y += 40;
2934
+ ctx.font = '600 10px "JetBrains Mono", monospace';
2935
+ ctx.fillStyle = th.textDim;
2936
+ ctx.fillText('PRODUCTIVITY HEATMAP', 32, y);
2937
+ y += 14;
2938
+ drawMiniHeatmap(ctx, 32, y, W - 64, d.heatmap.commits, th, isLight);
2939
+
2940
+ // ── Top Insights ──
2941
+ y += 100;
2942
+ const topInsights = (d.insights || []).slice(0, 3);
2943
+ if (topInsights.length > 0) {
2944
+ ctx.font = '600 10px "JetBrains Mono", monospace';
2945
+ ctx.fillStyle = th.textDim;
2946
+ ctx.fillText('KEY INSIGHTS', 32, y);
2947
+ y += 14;
2948
+ for (const insight of topInsights) {
2949
+ drawInsightRow(ctx, 32, y, W - 64, insight, th);
2950
+ y += 28;
2951
+ }
2952
+ }
2953
+
2954
+ // ── Footer ──
2955
+ ctx.font = '400 10px "DM Sans", sans-serif';
2956
+ ctx.fillStyle = th.textDim;
2957
+ ctx.fillText('Generated by Codelens AI · codelensai-dev.vercel.app', 32, H - 24);
2958
+
2959
+ // Subtle footer separator
2960
+ ctx.strokeStyle = th.separatorLight;
2961
+ ctx.lineWidth = 1;
2962
+ ctx.beginPath();
2963
+ ctx.moveTo(32, H - 40);
2964
+ ctx.lineTo(W - 32, H - 40);
2965
+ ctx.stroke();
2966
+ }
2967
+
2968
+ let shareCardTheme = 'dark';
2969
+
2970
+ function updateShareThemeToggle() {
2971
+ document.querySelectorAll('#share-theme-toggle button').forEach(btn => {
2972
+ btn.classList.toggle('active', btn.dataset.shareTheme === shareCardTheme);
2973
+ });
2974
+ }
2975
+
2976
+ function openShareModal() {
2977
+ if (!DATA) return;
2978
+ shareCardTheme = getTheme();
2979
+ updateShareThemeToggle();
2980
+ const modal = document.getElementById('share-modal');
2981
+ modal.style.display = '';
2982
+ document.body.style.overflow = 'hidden';
2983
+ const canvas = document.getElementById('share-canvas');
2984
+ document.fonts.ready.then(() => renderShareCard(canvas));
2985
+ }
2986
+
2987
+ function closeShareModal() {
2988
+ const modal = document.getElementById('share-modal');
2989
+ modal.style.display = 'none';
2990
+ document.body.style.overflow = '';
2991
+ }
2992
+
2993
+ async function copyShareCard() {
2994
+ const canvas = document.getElementById('share-canvas');
2995
+ try {
2996
+ const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
2997
+ await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
2998
+ showShareToast('Copied to clipboard!');
2999
+ } catch {
3000
+ showShareToast('Clipboard not supported — downloading instead');
3001
+ downloadShareCard();
3002
+ }
3003
+ }
3004
+
3005
+ function downloadShareCard() {
3006
+ const canvas = document.getElementById('share-canvas');
3007
+ const link = document.createElement('a');
3008
+ link.download = 'codelens-ai-report-card.png';
3009
+ link.href = canvas.toDataURL('image/png');
3010
+ link.click();
3011
+ showShareToast('Downloaded!');
3012
+ }
3013
+
3014
+ function showShareToast(msg) {
3015
+ const toast = document.getElementById('share-toast');
3016
+ toast.textContent = msg;
3017
+ toast.classList.add('visible');
3018
+ setTimeout(() => toast.classList.remove('visible'), 2500);
3019
+ }
3020
+
3021
+ // Wire up share modal events
3022
+ (function initShareModal() {
3023
+ const shareBtn = document.getElementById('share-btn');
3024
+ const shareModal = document.getElementById('share-modal');
3025
+ const closeBtn = document.getElementById('share-modal-close');
3026
+ const copyBtn = document.getElementById('share-copy-btn');
3027
+ const downloadBtn = document.getElementById('share-download-btn');
3028
+
3029
+ if (shareBtn) shareBtn.addEventListener('click', openShareModal);
3030
+ if (closeBtn) closeBtn.addEventListener('click', closeShareModal);
3031
+ if (shareModal) shareModal.addEventListener('click', function(e) { if (e.target === shareModal) closeShareModal(); });
3032
+ if (copyBtn) copyBtn.addEventListener('click', copyShareCard);
3033
+ if (downloadBtn) downloadBtn.addEventListener('click', downloadShareCard);
3034
+ document.addEventListener('keydown', function(e) {
3035
+ if (e.key === 'Escape' && shareModal && shareModal.style.display !== 'none') closeShareModal();
3036
+ });
3037
+
3038
+ // Share card theme toggle
3039
+ const themeToggle = document.getElementById('share-theme-toggle');
3040
+ if (themeToggle) {
3041
+ themeToggle.addEventListener('click', function(e) {
3042
+ const btn = e.target.closest('[data-share-theme]');
3043
+ if (!btn) return;
3044
+ shareCardTheme = btn.dataset.shareTheme;
3045
+ updateShareThemeToggle();
3046
+ const canvas = document.getElementById('share-canvas');
3047
+ renderShareCard(canvas);
3048
+ });
3049
+ }
3050
+ })();
3051
+
2382
3052
  </script>
2383
3053
  </body>
2384
3054
  </html>