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 +6 -0
- package/package.json +1 -1
- package/public/index.html +487 -24
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
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);
|
|
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));
|
|
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':
|
|
701
|
-
'cache-whisperer':
|
|
702
|
-
'clean-slate':
|
|
703
|
-
'model-sniper':
|
|
704
|
-
'efficiency-diamond':
|
|
705
|
-
'opus-addict':
|
|
706
|
-
'token-furnace':
|
|
707
|
-
'context-hoarder':
|
|
708
|
-
'vague-commander':
|
|
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()">×</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 & 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
|
-
|
|
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
|
-
|
|
1147
|
-
<
|
|
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)">×</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
|
-
|
|
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
|
-
|
|
1538
|
-
|
|
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();
|
|
2006
|
+
loadCachedAIInsights();
|
|
1544
2007
|
if (autoRefreshActive) startAutoRefresh();
|
|
1545
2008
|
});
|
|
1546
2009
|
|