claude-session-insights 0.3.0 → 0.3.2

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
@@ -18,6 +18,12 @@ Think "Spotify Wrapped" for your Claude Code usage — scores, summaries, badges
18
18
  - **Auto-Refresh** — optional 15-second polling to keep the dashboard current while you work
19
19
  - **Account Info** — displays your subscription type, org, and email from `claude auth status`
20
20
 
21
+ ## Screenshots
22
+ <img width="1080" height="880" alt="image" src="https://github.com/user-attachments/assets/d9914527-4ea5-49ed-aa8a-09a2896b1c67" />
23
+ <img width="1080" height="845" alt="image" src="https://github.com/user-attachments/assets/20799313-617c-4bc6-a999-76a0d95e2d8e" />
24
+ <img width="1080" height="823" alt="image" src="https://github.com/user-attachments/assets/249b4c78-8849-4db6-91bf-5ac87b4defbc" />
25
+
26
+
21
27
  ## Quick Start
22
28
 
23
29
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-insights",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Analyze Claude Code sessions for efficiency insights, scores, and team metrics",
5
5
  "type": "module",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -207,14 +207,14 @@
207
207
  .badge-icon {
208
208
  width: 24px; height: 24px; border-radius: 5px;
209
209
  display: flex; align-items: center; justify-content: center;
210
- flex-shrink: 0;
211
- }
212
- .badge-icon.surgical-prompter { background: var(--accent-dim); color: var(--accent); }
213
- .badge-icon.cache-whisperer { background: var(--green-dim); color: var(--green); }
214
- .badge-icon.clean-slate { background: var(--yellow-dim); color: var(--yellow); }
215
- .badge-icon.model-sniper { background: var(--purple-dim); color: var(--purple); }
216
- .badge-icon.efficiency-diamond { background: var(--green-dim); color: var(--green); }
217
- .badge.negative .badge-icon { background: var(--red-dim, rgba(239,68,68,0.15)); color: var(--red, #ef4444); }
210
+ flex-shrink: 0; font-size: 14px; line-height: 1;
211
+ }
212
+ .badge-icon.surgical-prompter { background: var(--accent-dim); }
213
+ .badge-icon.cache-whisperer { background: var(--green-dim); }
214
+ .badge-icon.clean-slate { background: var(--yellow-dim); }
215
+ .badge-icon.model-sniper { background: var(--purple-dim); }
216
+ .badge-icon.efficiency-diamond { background: var(--green-dim); }
217
+ .badge.negative .badge-icon { background: var(--red-dim, rgba(239,68,68,0.15)); }
218
218
  .badge.negative .badge-name { color: var(--red, #ef4444); }
219
219
  .badge-name { font-size: 12px; font-weight: 600; color: var(--text); white-space: nowrap; }
220
220
  .badge-tip {
@@ -644,10 +644,97 @@
644
644
  color: var(--text3); cursor: pointer; font-size: 18px; line-height: 1; padding: 4px;
645
645
  }
646
646
  .ai-prompt-close:hover { color: var(--text); }
647
+
648
+ /* Share modal */
649
+ .share-btn {
650
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text2);
651
+ height: 34px; border-radius: 7px; cursor: pointer; padding: 0 12px;
652
+ display: flex; align-items: center; gap: 6px;
653
+ transition: all 0.2s; font-size: 12px; font-weight: 500; white-space: nowrap;
654
+ }
655
+ .share-btn:hover { background: var(--surface3); color: var(--text); border-color: var(--border-hover); }
656
+ .share-overlay {
657
+ display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4);
658
+ z-index: 1000; align-items: center; justify-content: center;
659
+ backdrop-filter: blur(2px);
660
+ }
661
+ .share-overlay.open { display: flex; }
662
+ .share-modal {
663
+ background: var(--surface); border: 1px solid var(--border);
664
+ border-radius: var(--radius); padding: 20px 20px; max-width: 520px; width: 95%;
665
+ box-shadow: 0 8px 32px rgba(0,0,0,0.18); position: relative;
666
+ }
667
+ .share-modal-title { font-size: 14px; font-weight: 600; color: var(--text); margin-bottom: 14px; }
668
+ .share-tabs { display: flex; gap: 6px; margin-bottom: 12px; }
669
+ .share-tab {
670
+ background: var(--surface2); border: 1px solid var(--border);
671
+ border-radius: 6px; padding: 6px 14px; font-size: 12px; font-weight: 500;
672
+ color: var(--text2); cursor: pointer; transition: all 0.15s;
673
+ }
674
+ .share-tab:hover { border-color: var(--border-hover); color: var(--text); }
675
+ .share-tab.active { background: var(--accent-dim); border-color: var(--accent); color: var(--accent); }
676
+ .share-preview {
677
+ border-radius: 8px; overflow: hidden; border: 1px solid var(--border);
678
+ background: var(--surface2); margin-bottom: 14px; min-height: 60px;
679
+ display: flex; align-items: center; justify-content: center;
680
+ }
681
+ .share-preview img { width: 100%; display: block; border-radius: 7px; }
682
+ .share-preview-loading { font-size: 12px; color: var(--text3); padding: 20px; }
683
+ .share-actions { display: flex; gap: 8px; justify-content: flex-end; }
684
+ .share-cancel-btn {
685
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text2);
686
+ padding: 8px 18px; border-radius: 7px; cursor: pointer; font-size: 13px; font-weight: 500;
687
+ transition: all 0.15s;
688
+ }
689
+ .share-cancel-btn:hover { border-color: var(--border-hover); color: var(--text); }
690
+ .share-download-btn {
691
+ background: var(--accent); border: none; color: #fff;
692
+ padding: 8px 18px; border-radius: 7px; cursor: pointer; font-size: 13px; font-weight: 600;
693
+ transition: opacity 0.15s;
694
+ }
695
+ .share-download-btn:hover { opacity: 0.88; }
696
+ .share-close {
697
+ position: absolute; top: 12px; right: 14px; background: none; border: none;
698
+ color: var(--text3); cursor: pointer; font-size: 18px; line-height: 1; padding: 4px;
699
+ }
700
+ .share-close:hover { color: var(--text); }
701
+
702
+ /* Session drawer */
703
+ .session-drawer-overlay {
704
+ position: fixed; inset: 0; z-index: 500;
705
+ background: rgba(0,0,0,0.4); backdrop-filter: blur(2px);
706
+ opacity: 0; pointer-events: none; transition: opacity 0.25s;
707
+ }
708
+ .session-drawer-overlay.open { opacity: 1; pointer-events: auto; }
709
+ .session-drawer {
710
+ position: absolute; top: 0; right: 0; bottom: 0;
711
+ width: min(840px, 92vw);
712
+ background: var(--bg); overflow-y: auto;
713
+ padding: 24px 32px;
714
+ transform: translateX(100%);
715
+ transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1);
716
+ box-shadow: -4px 0 40px rgba(0,0,0,0.18);
717
+ }
718
+ .session-drawer-overlay.open .session-drawer { transform: translateX(0); }
719
+ .drawer-topbar {
720
+ display: flex; justify-content: space-between; align-items: center;
721
+ margin-bottom: 20px; padding-bottom: 14px; border-bottom: 1px solid var(--border);
722
+ }
723
+ .drawer-topbar-label { font-size: 11px; color: var(--text3); font-weight: 500; }
724
+ .drawer-close-btn {
725
+ background: var(--surface2); border: 1px solid var(--border); color: var(--text2);
726
+ width: 30px; height: 30px; border-radius: 7px; cursor: pointer;
727
+ display: flex; align-items: center; justify-content: center;
728
+ font-size: 17px; line-height: 1; padding: 0; transition: all 0.15s; flex-shrink: 0;
729
+ }
730
+ .drawer-close-btn:hover { background: var(--surface3); color: var(--text); border-color: var(--border-hover); }
647
731
  </style>
648
732
  </head>
649
733
  <body>
650
734
  <div id="app" class="loading">Loading sessions...</div>
735
+ <div class="session-drawer-overlay" id="session-drawer-overlay" onclick="if(event.target===this)closeSessionDrawer()">
736
+ <div class="session-drawer" id="session-drawer"></div>
737
+ </div>
651
738
 
652
739
  <script>
653
740
  const $ = (s) => document.querySelector(s);
@@ -697,15 +784,15 @@ async function fetchSession(id) {
697
784
  }
698
785
 
699
786
  const BADGE_ICONS = {
700
- 'surgical-prompter': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>`,
701
- 'cache-whisperer': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" 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="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`,
702
- 'clean-slate': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>`,
703
- 'model-sniper': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>`,
704
- 'efficiency-diamond': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 22 12 12 22 2 12"/></svg>`,
705
- 'opus-addict': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 1 0 10 10"/><path d="M12 8v4l3 3"/></svg>`,
706
- 'token-furnace': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 12c2-2.96 0-7-1-8 0 3.038-1.773 4.741-3 6-1.226 1.26-2 3.24-2 5a6 6 0 1 0 12 0c0-1.532-1.056-3.94-2-5-1.786 3-2.791 3-4 2z"/></svg>`,
707
- 'context-hoarder': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`,
708
- 'vague-commander': `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
787
+ 'surgical-prompter': '🔬',
788
+ 'cache-whisperer': '⚡',
789
+ 'clean-slate': '🧹',
790
+ 'model-sniper': '🎯',
791
+ 'efficiency-diamond': '💎',
792
+ 'opus-addict': '💸',
793
+ 'token-furnace': '🔥',
794
+ 'context-hoarder': '📚',
795
+ 'vague-commander': '❓',
709
796
  };
710
797
 
711
798
  function renderBadge(b) {
@@ -938,6 +1025,10 @@ function renderDashboard(data) {
938
1025
  <button class="refresh-btn" onclick="doRefresh()" title="Refresh" id="refresh-btn">
939
1026
  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6"/><path d="M21.34 13a10 10 0 1 1-2.84-8.84L21.5 8"/></svg>
940
1027
  </button>
1028
+ <button class="share-btn" onclick="openShareModal()" title="Share stats as PNG">
1029
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
1030
+ Share
1031
+ </button>
941
1032
  <button class="theme-toggle" onclick="toggleTheme()" title="Toggle light/dark mode" id="theme-toggle-btn"></button>
942
1033
  </div>
943
1034
  </header>
@@ -1029,6 +1120,23 @@ function renderDashboard(data) {
1029
1120
  <pre id="ai-prompt-content">Loading...</pre>
1030
1121
  </div>
1031
1122
  </div>
1123
+ <div class="share-overlay" id="share-overlay" onclick="if(event.target===this)closeShareModal()">
1124
+ <div class="share-modal">
1125
+ <button class="share-close" onclick="closeShareModal()">&times;</button>
1126
+ <div class="share-modal-title">Share your stats</div>
1127
+ <div class="share-tabs">
1128
+ <button class="share-tab active" data-type="score" onclick="switchShareType('score')">Score &amp; Badges</button>
1129
+ <button class="share-tab" data-type="full" onclick="switchShareType('full')">Full Summary</button>
1130
+ </div>
1131
+ <div class="share-preview" id="share-preview">
1132
+ <span class="share-preview-loading">Generating preview…</span>
1133
+ </div>
1134
+ <div class="share-actions">
1135
+ <button class="share-cancel-btn" onclick="closeShareModal()">Cancel</button>
1136
+ <button class="share-download-btn" onclick="downloadCurrentShare()">Download PNG</button>
1137
+ </div>
1138
+ </div>
1139
+ </div>
1032
1140
  <div class="stats">
1033
1141
  <div class="stat"><div class="value">${data.sessions.length}</div><div class="label">Sessions</div></div>
1034
1142
  <div class="stat"><div class="value">${formatTokens(totalTokens)}</div><div class="label">Total Tokens</div><div class="label" style="color:var(--text3); font-family:var(--mono); font-size:10px">${formatTokens(totalInput)} in / ${formatTokens(totalOutput)} out</div></div>
@@ -1127,7 +1235,12 @@ function renderDashboard(data) {
1127
1235
 
1128
1236
  async function openSession(id) {
1129
1237
  currentView = 'session';
1130
- $('#app').innerHTML = '<div class="loading">Loading session...</div>';
1238
+ const overlay = document.getElementById('session-drawer-overlay');
1239
+ const drawer = document.getElementById('session-drawer');
1240
+ drawer.innerHTML = '<div class="loading">Loading session...</div>';
1241
+ overlay.classList.add('open');
1242
+ document.body.style.overflow = 'hidden';
1243
+ if (autoRefreshActive) pauseAutoRefresh();
1131
1244
  const s = await fetchSession(id);
1132
1245
  renderSession(s);
1133
1246
  }
@@ -1143,8 +1256,11 @@ function renderSession(s) {
1143
1256
 
1144
1257
  let cumCost = 0;
1145
1258
 
1146
- $('#app').innerHTML = `
1147
- <button class="back-btn" onclick="goBack()">&larr; Back to dashboard</button>
1259
+ document.getElementById('session-drawer').innerHTML = `
1260
+ <div class="drawer-topbar">
1261
+ <span class="drawer-topbar-label">Session Detail</span>
1262
+ <button class="drawer-close-btn" onclick="closeSessionDrawer()" title="Close (Esc)">&times;</button>
1263
+ </div>
1148
1264
 
1149
1265
  <div class="session-header">
1150
1266
  <h2>${escHtml(s.title)}</h2>
@@ -1225,13 +1341,20 @@ function renderSession(s) {
1225
1341
  if (costCanvas) drawCostPerTurn(costCanvas, s.turns);
1226
1342
  }
1227
1343
 
1344
+ function closeSessionDrawer() {
1345
+ document.getElementById('session-drawer-overlay').classList.remove('open');
1346
+ document.body.style.overflow = '';
1347
+ currentView = 'dashboard';
1348
+ resumeAutoRefreshIfPaused();
1349
+ }
1350
+
1228
1351
  function toggleScoreBreakdown() {
1229
1352
  const el = document.getElementById('score-breakdown-overlay');
1230
1353
  if (el) el.classList.toggle('open');
1231
1354
  }
1232
1355
 
1233
1356
  function goBack() {
1234
- if (currentData) { renderDashboard(currentData); loadCachedAIInsights(); }
1357
+ closeSessionDrawer();
1235
1358
  }
1236
1359
 
1237
1360
  // --- AI Insights ---
@@ -1239,6 +1362,7 @@ function goBack() {
1239
1362
  let aiModels = [];
1240
1363
  let aiDefaultModel = null;
1241
1364
  let aiDefaultModelLabel = null;
1365
+ let cachedAIRender = null; // persists AI insights across renderDashboard calls
1242
1366
 
1243
1367
  function renderMarkdown(text) {
1244
1368
  return text
@@ -1299,7 +1423,16 @@ document.addEventListener('click', () => {
1299
1423
  document.querySelectorAll('.ai-model-picker.open').forEach(p => p.classList.remove('open'));
1300
1424
  });
1301
1425
 
1426
+ function applyAIState() {
1427
+ if (cachedAIRender) {
1428
+ renderAIComplete(cachedAIRender.content, cachedAIRender.generatedAt, cachedAIRender.model);
1429
+ } else {
1430
+ renderModelPicker();
1431
+ }
1432
+ }
1433
+
1302
1434
  function renderAIComplete(content, generatedAt, model) {
1435
+ cachedAIRender = { content, generatedAt, model };
1303
1436
  const body = document.getElementById('ai-insights-body');
1304
1437
  if (!body) return;
1305
1438
  body.innerHTML = `
@@ -1318,6 +1451,7 @@ function renderAIComplete(content, generatedAt, model) {
1318
1451
  }
1319
1452
 
1320
1453
  function clearAIInsights() {
1454
+ cachedAIRender = null;
1321
1455
  fetch('/api/ai-analyze', { method: 'DELETE' }).catch(() => {});
1322
1456
  const body = document.getElementById('ai-insights-body');
1323
1457
  if (!body) return;
@@ -1456,6 +1590,332 @@ async function loadCachedAIInsights() {
1456
1590
  } catch {}
1457
1591
  }
1458
1592
 
1593
+ // --- Share as PNG ---
1594
+
1595
+ const BADGE_EMOJI = BADGE_ICONS;
1596
+
1597
+ let currentShareType = 'score';
1598
+
1599
+ function getShareColors() {
1600
+ const isDark = (document.documentElement.getAttribute('data-theme') || getPreferredTheme()) === 'dark';
1601
+ return {
1602
+ bg: isDark ? '#0c0f14' : '#f5f6f8',
1603
+ surface: isDark ? '#14181f' : '#ffffff',
1604
+ text: isDark ? '#d8dce4' : '#1a1d23',
1605
+ text2: isDark ? '#8b919a' : '#5c6370',
1606
+ text3: isDark ? '#5c6370' : '#8b919a',
1607
+ accent: isDark ? '#6ea4f7' : '#2563eb',
1608
+ green: isDark ? '#56d364' : '#16a34a',
1609
+ yellow: isDark ? '#e3b341' : '#ca8a04',
1610
+ red: isDark ? '#f47067' : '#dc2626',
1611
+ purple: isDark ? '#d2a8ff' : '#7c3aed',
1612
+ border: isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
1613
+ redDim: isDark ? 'rgba(244,112,103,0.14)' : 'rgba(220,38,38,0.10)',
1614
+ redBorder: isDark ? 'rgba(244,112,103,0.35)' : 'rgba(220,38,38,0.25)',
1615
+ };
1616
+ }
1617
+
1618
+ function buildShareCanvas(type, dpr) {
1619
+ if (!currentData) return null;
1620
+ const colors = getShareColors();
1621
+ const W = 600;
1622
+
1623
+ const measureCanvas = document.createElement('canvas');
1624
+ measureCanvas.width = W * dpr;
1625
+ measureCanvas.height = 3000 * dpr;
1626
+ const measureCtx = measureCanvas.getContext('2d');
1627
+ measureCtx.scale(dpr, dpr);
1628
+ const H = drawShareContent(measureCtx, currentData, type, W, colors, false);
1629
+
1630
+ const canvas = document.createElement('canvas');
1631
+ canvas.width = W * dpr;
1632
+ canvas.height = H * dpr;
1633
+ const ctx = canvas.getContext('2d');
1634
+ ctx.scale(dpr, dpr);
1635
+ drawShareContent(ctx, currentData, type, W, colors, true);
1636
+ return canvas;
1637
+ }
1638
+
1639
+ function openShareModal() {
1640
+ document.getElementById('share-overlay').classList.add('open');
1641
+ pauseAutoRefresh();
1642
+ currentShareType = 'score';
1643
+ renderSharePreview();
1644
+ }
1645
+
1646
+ function closeShareModal() {
1647
+ document.getElementById('share-overlay').classList.remove('open');
1648
+ resumeAutoRefreshIfPaused();
1649
+ }
1650
+
1651
+ function switchShareType(type) {
1652
+ currentShareType = type;
1653
+ document.querySelectorAll('.share-tab').forEach(t => t.classList.toggle('active', t.dataset.type === type));
1654
+ renderSharePreview();
1655
+ }
1656
+
1657
+ function renderSharePreview() {
1658
+ const preview = document.getElementById('share-preview');
1659
+ if (!preview) return;
1660
+ preview.innerHTML = '<span class="share-preview-loading">Generating preview\u2026</span>';
1661
+ // Yield to browser to show loading message before heavy canvas work
1662
+ requestAnimationFrame(() => {
1663
+ const canvas = buildShareCanvas(currentShareType, 1);
1664
+ if (!canvas) return;
1665
+ const img = document.createElement('img');
1666
+ img.src = canvas.toDataURL('image/png');
1667
+ preview.innerHTML = '';
1668
+ preview.appendChild(img);
1669
+ });
1670
+ }
1671
+
1672
+ function downloadCurrentShare() {
1673
+ closeShareModal();
1674
+ const dpr = Math.max(window.devicePixelRatio || 1, 2);
1675
+ const canvas = buildShareCanvas(currentShareType, dpr);
1676
+ if (!canvas) return;
1677
+ canvas.toBlob(blob => {
1678
+ const url = URL.createObjectURL(blob);
1679
+ const a = document.createElement('a');
1680
+ a.href = url;
1681
+ a.download = `claude-insights-${currentShareType}-${new Date().toISOString().slice(0, 10)}.png`;
1682
+ a.click();
1683
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
1684
+ });
1685
+ }
1686
+
1687
+ function shareRoundRect(ctx, x, y, w, h, r) {
1688
+ ctx.beginPath();
1689
+ ctx.moveTo(x + r, y);
1690
+ ctx.lineTo(x + w - r, y);
1691
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
1692
+ ctx.lineTo(x + w, y + h - r);
1693
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
1694
+ ctx.lineTo(x + r, y + h);
1695
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
1696
+ ctx.lineTo(x, y + r);
1697
+ ctx.quadraticCurveTo(x, y, x + r, y);
1698
+ ctx.closePath();
1699
+ }
1700
+
1701
+ function shareWrapText(ctx, text, maxWidth) {
1702
+ const words = text.split(' ');
1703
+ const lines = [];
1704
+ let line = '';
1705
+ for (const word of words) {
1706
+ const test = line ? line + ' ' + word : word;
1707
+ if (ctx.measureText(test).width > maxWidth && line) {
1708
+ lines.push(line);
1709
+ line = word;
1710
+ } else {
1711
+ line = test;
1712
+ }
1713
+ }
1714
+ if (line) lines.push(line);
1715
+ return lines;
1716
+ }
1717
+
1718
+ function drawShareContent(ctx, data, type, W, colors, draw) {
1719
+ const pad = 28;
1720
+ let y = pad + 16;
1721
+
1722
+ if (draw) {
1723
+ ctx.fillStyle = colors.bg;
1724
+ ctx.fillRect(0, 0, W, 9999);
1725
+
1726
+ // Title row
1727
+ ctx.font = 'bold 14px -apple-system, BlinkMacSystemFont, sans-serif';
1728
+ ctx.fillStyle = colors.text;
1729
+ ctx.textAlign = 'left';
1730
+ ctx.fillText('claude-session-insights', pad, y + 14);
1731
+
1732
+ const now = new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
1733
+ ctx.font = '11px -apple-system, BlinkMacSystemFont, monospace';
1734
+ ctx.fillStyle = colors.text3;
1735
+ ctx.textAlign = 'right';
1736
+ ctx.fillText(now, W - pad, y + 14);
1737
+ }
1738
+ y += 24 + 14;
1739
+
1740
+ if (draw) {
1741
+ ctx.strokeStyle = colors.border;
1742
+ ctx.lineWidth = 1;
1743
+ ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W - pad, y); ctx.stroke();
1744
+ // Gradient accent line
1745
+ const grad = ctx.createLinearGradient(pad, 0, W - pad, 0);
1746
+ grad.addColorStop(0, colors.green);
1747
+ grad.addColorStop(0.5, colors.accent);
1748
+ grad.addColorStop(1, colors.purple);
1749
+ ctx.strokeStyle = grad;
1750
+ ctx.lineWidth = 2;
1751
+ ctx.globalAlpha = 0.6;
1752
+ ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W - pad, y); ctx.stroke();
1753
+ ctx.globalAlpha = 1;
1754
+ ctx.lineWidth = 1;
1755
+ }
1756
+ y += 1 + 24;
1757
+
1758
+ // Score number
1759
+ const score = data.overallScore || 0;
1760
+ const scoreColor = score >= 80 ? colors.green : score >= 50 ? colors.yellow : colors.red;
1761
+ if (draw) {
1762
+ ctx.font = 'bold 64px -apple-system, BlinkMacSystemFont, monospace';
1763
+ ctx.fillStyle = scoreColor;
1764
+ ctx.textAlign = 'center';
1765
+ ctx.fillText(score, W / 2, y + 56);
1766
+ }
1767
+ y += 64;
1768
+
1769
+ if (draw) {
1770
+ ctx.font = '500 12px -apple-system, BlinkMacSystemFont, sans-serif';
1771
+ ctx.fillStyle = colors.text2;
1772
+ ctx.textAlign = 'center';
1773
+ ctx.fillText('Efficiency Score (7-day)', W / 2, y + 16);
1774
+ }
1775
+ y += 20 + 24;
1776
+
1777
+ // Badges
1778
+ const badges = data.badges || [];
1779
+ if (badges.length > 0) {
1780
+ if (draw) {
1781
+ ctx.font = '600 10px -apple-system, BlinkMacSystemFont, sans-serif';
1782
+ ctx.fillStyle = colors.text2;
1783
+ ctx.textAlign = 'left';
1784
+ ctx.fillText('BADGES EARNED', pad, y + 11);
1785
+ }
1786
+ y += 14 + 10;
1787
+
1788
+ const badgesPerRow = 3;
1789
+ const gapX = 8;
1790
+ const badgeW = Math.floor((W - pad * 2 - (badgesPerRow - 1) * gapX) / badgesPerRow);
1791
+ const badgeH = 36;
1792
+ const gapY = 8;
1793
+
1794
+ if (draw) {
1795
+ badges.forEach((b, i) => {
1796
+ const col = i % badgesPerRow;
1797
+ const row = Math.floor(i / badgesPerRow);
1798
+ const bx = pad + col * (badgeW + gapX);
1799
+ const by = y + row * (badgeH + gapY);
1800
+ const isNeg = b.negative;
1801
+
1802
+ ctx.fillStyle = isNeg ? colors.redDim : colors.surface;
1803
+ shareRoundRect(ctx, bx, by, badgeW, badgeH, 7);
1804
+ ctx.fill();
1805
+
1806
+ ctx.strokeStyle = isNeg ? colors.redBorder : colors.border;
1807
+ ctx.lineWidth = 1;
1808
+ shareRoundRect(ctx, bx, by, badgeW, badgeH, 7);
1809
+ ctx.stroke();
1810
+
1811
+ ctx.font = '15px serif';
1812
+ ctx.textAlign = 'left';
1813
+ ctx.fillStyle = colors.text;
1814
+ ctx.fillText(BADGE_EMOJI[b.id] || '•', bx + 8, by + 23);
1815
+
1816
+ ctx.font = '600 11px -apple-system, BlinkMacSystemFont, sans-serif';
1817
+ ctx.fillStyle = isNeg ? colors.red : colors.text;
1818
+ let name = b.name;
1819
+ const maxNameW = badgeW - 38;
1820
+ while (name.length > 3 && ctx.measureText(name + '\u2026').width > maxNameW) {
1821
+ name = name.slice(0, -1);
1822
+ }
1823
+ if (name !== b.name) name += '\u2026';
1824
+ ctx.fillText(name, bx + 30, by + 23);
1825
+ });
1826
+ }
1827
+
1828
+ const badgeRows = Math.ceil(badges.length / badgesPerRow);
1829
+ y += badgeRows * (badgeH + gapY) - gapY + 20;
1830
+ }
1831
+
1832
+ if (type === 'full' && data.overallSummary) {
1833
+ if (draw) {
1834
+ ctx.strokeStyle = colors.border;
1835
+ ctx.lineWidth = 1;
1836
+ ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W - pad, y); ctx.stroke();
1837
+ }
1838
+ y += 1 + 20;
1839
+
1840
+ if (draw) {
1841
+ ctx.font = '600 10px -apple-system, BlinkMacSystemFont, sans-serif';
1842
+ ctx.fillStyle = colors.text2;
1843
+ ctx.textAlign = 'left';
1844
+ ctx.fillText("HOW YOU'RE USING CLAUDE CODE", pad, y + 11);
1845
+ }
1846
+ y += 14 + 10;
1847
+
1848
+ const textW = W - pad * 2;
1849
+ ctx.font = '13px -apple-system, BlinkMacSystemFont, sans-serif';
1850
+ for (const p of data.overallSummary.paragraphs || []) {
1851
+ const lines = shareWrapText(ctx, p, textW);
1852
+ if (draw) {
1853
+ ctx.fillStyle = colors.text;
1854
+ ctx.textAlign = 'left';
1855
+ for (const line of lines) {
1856
+ ctx.fillText(line, pad, y + 14);
1857
+ y += 22;
1858
+ }
1859
+ } else {
1860
+ y += lines.length * 22;
1861
+ }
1862
+ y += 8;
1863
+ }
1864
+ y += 8;
1865
+
1866
+ if ((data.overallSummary.recommendations || []).length > 0) {
1867
+ if (draw) {
1868
+ ctx.strokeStyle = colors.border;
1869
+ ctx.lineWidth = 1;
1870
+ ctx.beginPath(); ctx.moveTo(pad, y); ctx.lineTo(W - pad, y); ctx.stroke();
1871
+ }
1872
+ y += 1 + 16;
1873
+
1874
+ if (draw) {
1875
+ ctx.font = '600 10px -apple-system, BlinkMacSystemFont, sans-serif';
1876
+ ctx.fillStyle = colors.accent;
1877
+ ctx.textAlign = 'left';
1878
+ ctx.fillText('RECOMMENDATIONS', pad, y + 11);
1879
+ }
1880
+ y += 14 + 10;
1881
+
1882
+ ctx.font = '13px -apple-system, BlinkMacSystemFont, sans-serif';
1883
+ for (const r of data.overallSummary.recommendations) {
1884
+ if (draw) {
1885
+ ctx.font = '600 11px monospace';
1886
+ ctx.fillStyle = colors.accent;
1887
+ ctx.fillText('->', pad, y + 14);
1888
+ ctx.font = '13px -apple-system, BlinkMacSystemFont, sans-serif';
1889
+ ctx.fillStyle = colors.text;
1890
+ ctx.textAlign = 'left';
1891
+ }
1892
+ const lines = shareWrapText(ctx, r, textW - 22);
1893
+ if (draw) {
1894
+ for (const line of lines) {
1895
+ ctx.fillText(line, pad + 22, y + 14);
1896
+ y += 20;
1897
+ }
1898
+ } else {
1899
+ y += lines.length * 20;
1900
+ }
1901
+ y += 8;
1902
+ }
1903
+ y += 8;
1904
+ }
1905
+ }
1906
+
1907
+ // Footer
1908
+ y += 12;
1909
+ if (draw) {
1910
+ ctx.font = '10px -apple-system, BlinkMacSystemFont, monospace';
1911
+ ctx.fillStyle = colors.text3;
1912
+ ctx.textAlign = 'center';
1913
+ ctx.fillText('claude-session-insights', W / 2, y);
1914
+ }
1915
+ y += pad;
1916
+ return y;
1917
+ }
1918
+
1459
1919
  // --- Init ---
1460
1920
 
1461
1921
  let currentData;
@@ -1470,6 +1930,7 @@ async function doRefresh() {
1470
1930
  btn.classList.add('spinning');
1471
1931
  currentData = await fetchData(true);
1472
1932
  renderDashboard(currentData);
1933
+ applyAIState();
1473
1934
  btn.classList.remove('spinning');
1474
1935
  }
1475
1936
 
@@ -1496,6 +1957,7 @@ function startAutoRefresh() {
1496
1957
  if (btn) btn.classList.add('spinning');
1497
1958
  currentData = await fetchData(true);
1498
1959
  renderDashboard(currentData);
1960
+ applyAIState();
1499
1961
  if (btn) btn.classList.remove('spinning');
1500
1962
  updateCountdownLabel();
1501
1963
  })();
@@ -1534,13 +1996,14 @@ function resumeAutoRefreshIfPaused() {
1534
1996
  }
1535
1997
  }
1536
1998
 
1537
- // Clear AI insights on fresh page load (not on in-app refresh)
1538
- fetch('/api/ai-analyze', { method: 'DELETE' }).catch(() => {});
1999
+ document.addEventListener('keydown', e => {
2000
+ if (e.key === 'Escape' && currentView === 'session') closeSessionDrawer();
2001
+ });
1539
2002
 
1540
2003
  fetchData().then(data => {
1541
2004
  currentData = data;
1542
2005
  renderDashboard(data);
1543
- loadCachedAIInsights(); // loads model list (cache already cleared above)
2006
+ loadCachedAIInsights();
1544
2007
  if (autoRefreshActive) startAutoRefresh();
1545
2008
  });
1546
2009