claude-code-templates 1.20.1 → 1.20.3
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/bin/create-claude-config.js +25 -14
- package/package.json +1 -1
- package/src/analytics/core/AgentAnalyzer.js +3 -3
- package/src/analytics/core/ConversationAnalyzer.js +2 -1
- package/src/analytics-web/components/ActivityHeatmap.js +738 -0
- package/src/analytics-web/components/DashboardPage.js +54 -0
- package/src/analytics-web/index.html +471 -1
- package/src/analytics.js +187 -0
- package/src/index.js +63 -1
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivityHeatmap - GitHub-style contribution calendar for Claude Code activity
|
|
3
|
+
* Shows daily Claude Code usage over the last year with orange theme
|
|
4
|
+
*/
|
|
5
|
+
class ActivityHeatmap {
|
|
6
|
+
constructor(container, dataService) {
|
|
7
|
+
this.container = container;
|
|
8
|
+
this.dataService = dataService;
|
|
9
|
+
this.activityData = null;
|
|
10
|
+
this.tooltip = null;
|
|
11
|
+
this.currentYear = new Date().getFullYear();
|
|
12
|
+
this.currentMetric = 'messages'; // Default metric
|
|
13
|
+
|
|
14
|
+
console.log('🔥 ActivityHeatmap initialized');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Initialize the heatmap component
|
|
19
|
+
*/
|
|
20
|
+
async initialize() {
|
|
21
|
+
try {
|
|
22
|
+
console.log('🔥 Initializing ActivityHeatmap...');
|
|
23
|
+
await this.render();
|
|
24
|
+
await this.loadActivityData();
|
|
25
|
+
console.log('✅ ActivityHeatmap initialized successfully');
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error('❌ Failed to initialize ActivityHeatmap:', error);
|
|
28
|
+
this.showErrorState();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Render the heatmap structure
|
|
34
|
+
*/
|
|
35
|
+
async render() {
|
|
36
|
+
this.container.innerHTML = `
|
|
37
|
+
<div class="activity-heatmap-container">
|
|
38
|
+
<div class="heatmap-loading">
|
|
39
|
+
<div class="heatmap-loading-spinner"></div>
|
|
40
|
+
<span>Loading activity...</span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="heatmap-tooltip" id="heatmap-tooltip">
|
|
44
|
+
<div class="heatmap-tooltip-date"></div>
|
|
45
|
+
<div class="heatmap-tooltip-activity"></div>
|
|
46
|
+
</div>
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
this.tooltip = document.getElementById('heatmap-tooltip');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load activity data from the API
|
|
54
|
+
*/
|
|
55
|
+
async loadActivityData() {
|
|
56
|
+
try {
|
|
57
|
+
console.log('🔥 Loading activity data...');
|
|
58
|
+
|
|
59
|
+
// Get complete activity data from backend (pre-processed with tools)
|
|
60
|
+
const response = await this.dataService.cachedFetch('/api/activity');
|
|
61
|
+
if (response && response.dailyActivity) {
|
|
62
|
+
console.log(`🔥 Loaded pre-processed activity data: ${response.dailyActivity.length} active days`);
|
|
63
|
+
|
|
64
|
+
// Use pre-processed data from backend instead of processing raw conversations
|
|
65
|
+
const dailyActivityMap = new Map();
|
|
66
|
+
response.dailyActivity.forEach(day => {
|
|
67
|
+
dailyActivityMap.set(day.date, day);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
this.activityData = this.processPrecomputedActivityData(dailyActivityMap);
|
|
71
|
+
await this.renderHeatmap();
|
|
72
|
+
this.updateTitle();
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error('No activity data available');
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('❌ Error loading activity data:', error);
|
|
78
|
+
this.showErrorState();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Process pre-computed activity data from backend
|
|
84
|
+
*/
|
|
85
|
+
processPrecomputedActivityData(dailyActivityMap) {
|
|
86
|
+
console.log(`🔥 Processing ${dailyActivityMap.size} days of pre-computed data...`);
|
|
87
|
+
|
|
88
|
+
// Calculate thresholds based on current metric
|
|
89
|
+
const metricCounts = Array.from(dailyActivityMap.values())
|
|
90
|
+
.map(activity => activity[this.currentMetric] || 0)
|
|
91
|
+
.filter(count => count > 0)
|
|
92
|
+
.sort((a, b) => a - b);
|
|
93
|
+
|
|
94
|
+
const thresholds = this.calculateDynamicThresholds(metricCounts);
|
|
95
|
+
|
|
96
|
+
// Calculate total activity for current metric
|
|
97
|
+
let totalActivity = 0;
|
|
98
|
+
let totalTools = 0;
|
|
99
|
+
let totalMessages = 0;
|
|
100
|
+
dailyActivityMap.forEach(activity => {
|
|
101
|
+
totalActivity += activity[this.currentMetric] || 0;
|
|
102
|
+
totalTools += activity.tools || 0;
|
|
103
|
+
totalMessages += activity.messages || 0;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
console.log(`🔥 Pre-computed data stats: ${totalMessages} messages, ${totalTools} tools`);
|
|
107
|
+
console.log(`🔥 Current metric (${this.currentMetric}): ${totalActivity} total`);
|
|
108
|
+
console.log(`🔥 Dynamic thresholds:`, thresholds);
|
|
109
|
+
console.log(`🔥 Sample ${this.currentMetric} counts:`, metricCounts.slice(0, 10), '...', metricCounts.slice(-10));
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
dailyActivity: dailyActivityMap,
|
|
113
|
+
totalActivity,
|
|
114
|
+
activeDays: dailyActivityMap.size,
|
|
115
|
+
thresholds
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Process conversation data into daily activity counts (legacy method)
|
|
121
|
+
*/
|
|
122
|
+
processActivityData(conversations) {
|
|
123
|
+
const dailyActivity = new Map();
|
|
124
|
+
const oneYearAgo = new Date();
|
|
125
|
+
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
|
126
|
+
const today = new Date();
|
|
127
|
+
|
|
128
|
+
console.log(`🔥 Processing ${conversations.length} conversations for activity data...`);
|
|
129
|
+
console.log(`🔥 Date range: ${oneYearAgo.toLocaleDateString()} to ${today.toLocaleDateString()}`);
|
|
130
|
+
console.log(`🔥 Cutoff timestamp: ${oneYearAgo.getTime()}`);
|
|
131
|
+
|
|
132
|
+
let validConversations = 0;
|
|
133
|
+
let latestDate = null;
|
|
134
|
+
let oldestDate = null;
|
|
135
|
+
let beforeOneYearCount = 0;
|
|
136
|
+
|
|
137
|
+
// Sample some conversations to see their date formats and data structure
|
|
138
|
+
console.log(`🔥 Sampling first 5 conversations:`);
|
|
139
|
+
conversations.slice(0, 5).forEach((conv, i) => {
|
|
140
|
+
console.log(` ${i+1}: ${conv.filename} - lastModified: ${conv.lastModified} (${new Date(conv.lastModified).toLocaleDateString()})`);
|
|
141
|
+
console.log(` messages: ${conv.messageCount || 0}, tokens: ${conv.tokens || 0}, toolUsage: ${conv.toolUsage?.totalToolCalls || 0}`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
conversations.forEach((conversation, index) => {
|
|
145
|
+
if (!conversation.lastModified) {
|
|
146
|
+
if (index < 5) console.log(`⚠️ Conversation ${index} has no lastModified:`, conversation);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const date = new Date(conversation.lastModified);
|
|
151
|
+
|
|
152
|
+
// Track date range
|
|
153
|
+
if (!latestDate || date > latestDate) latestDate = date;
|
|
154
|
+
if (!oldestDate || date < oldestDate) oldestDate = date;
|
|
155
|
+
|
|
156
|
+
if (date < oneYearAgo) {
|
|
157
|
+
beforeOneYearCount++;
|
|
158
|
+
if (beforeOneYearCount <= 5) {
|
|
159
|
+
console.log(`⚠️ Excluding old conversation: ${date.toISOString()} (${conversation.filename})`);
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
validConversations++;
|
|
165
|
+
const dateKey = date.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
166
|
+
|
|
167
|
+
const current = dailyActivity.get(dateKey) || {
|
|
168
|
+
conversations: 0,
|
|
169
|
+
tokens: 0,
|
|
170
|
+
messages: 0,
|
|
171
|
+
tools: 0
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
current.conversations += 1;
|
|
175
|
+
current.tokens += conversation.tokens || 0;
|
|
176
|
+
current.messages += conversation.messageCount || 0;
|
|
177
|
+
current.tools += (conversation.toolUsage?.totalToolCalls || 0);
|
|
178
|
+
|
|
179
|
+
dailyActivity.set(dateKey, current);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
console.log(`🔥 Valid conversations in last year: ${validConversations}`);
|
|
183
|
+
console.log(`🔥 Excluded conversations (older than 1 year): ${beforeOneYearCount}`);
|
|
184
|
+
console.log(`🔥 Complete date range in data: ${oldestDate?.toLocaleDateString()} to ${latestDate?.toLocaleDateString()}`);
|
|
185
|
+
console.log(`🔥 One year ago cutoff: ${oneYearAgo.toLocaleDateString()}`);
|
|
186
|
+
|
|
187
|
+
// Show first and last few conversations for debugging
|
|
188
|
+
const sortedDates = Array.from(dailyActivity.keys()).sort();
|
|
189
|
+
console.log(`🔥 First 5 activity dates:`, sortedDates.slice(0, 5));
|
|
190
|
+
console.log(`🔥 Last 5 activity dates:`, sortedDates.slice(-5));
|
|
191
|
+
console.log(`🔥 Total activity days: ${sortedDates.length}`);
|
|
192
|
+
|
|
193
|
+
// Calculate total activity for the year (based on current metric)
|
|
194
|
+
let totalActivity = 0;
|
|
195
|
+
dailyActivity.forEach(activity => {
|
|
196
|
+
totalActivity += activity[this.currentMetric] || 0;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Calculate dynamic thresholds based on data distribution
|
|
200
|
+
const messageCounts = Array.from(dailyActivity.values())
|
|
201
|
+
.map(activity => activity[this.currentMetric] || 0)
|
|
202
|
+
.filter(count => count > 0)
|
|
203
|
+
.sort((a, b) => a - b);
|
|
204
|
+
|
|
205
|
+
const thresholds = this.calculateDynamicThresholds(messageCounts);
|
|
206
|
+
|
|
207
|
+
// Calculate total tools for debugging
|
|
208
|
+
let totalTools = 0;
|
|
209
|
+
let totalMessages = 0;
|
|
210
|
+
dailyActivity.forEach(activity => {
|
|
211
|
+
totalTools += activity.tools || 0;
|
|
212
|
+
totalMessages += activity.messages || 0;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
console.log(`🔥 Processed activity data: ${dailyActivity.size} active days, ${totalActivity} total ${this.currentMetric}`);
|
|
216
|
+
console.log(`🔥 Debug totals: ${totalMessages} messages, ${totalTools} tools`);
|
|
217
|
+
console.log(`🔥 ${this.currentMetric} range: ${Math.min(...messageCounts)} to ${Math.max(...messageCounts)} ${this.currentMetric} per day`);
|
|
218
|
+
console.log(`🔥 Dynamic thresholds:`, thresholds);
|
|
219
|
+
console.log(`🔥 Sample ${this.currentMetric} counts:`, messageCounts.slice(0, 10), '...', messageCounts.slice(-10));
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
dailyActivity,
|
|
223
|
+
totalActivity,
|
|
224
|
+
activeDays: dailyActivity.size,
|
|
225
|
+
thresholds
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Calculate dynamic thresholds based on data distribution
|
|
231
|
+
* Ensures that even 1 message shows visible color (like GitHub)
|
|
232
|
+
*/
|
|
233
|
+
calculateDynamicThresholds(messageCounts) {
|
|
234
|
+
if (messageCounts.length === 0) {
|
|
235
|
+
return { level1: 1, level2: 5, level3: 15, level4: 30 };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const len = messageCounts.length;
|
|
239
|
+
const max = messageCounts[len - 1];
|
|
240
|
+
const min = messageCounts[0];
|
|
241
|
+
|
|
242
|
+
// ALWAYS start level1 at 1 so any activity shows color
|
|
243
|
+
let level1 = 1;
|
|
244
|
+
let level2, level3, level4;
|
|
245
|
+
|
|
246
|
+
if (len <= 4) {
|
|
247
|
+
// Very few data points - simple distribution
|
|
248
|
+
level2 = Math.max(2, Math.ceil(max * 0.3));
|
|
249
|
+
level3 = Math.max(level2 + 1, Math.ceil(max * 0.6));
|
|
250
|
+
level4 = Math.max(level3 + 1, Math.ceil(max * 0.8));
|
|
251
|
+
} else {
|
|
252
|
+
// Use percentiles but ensure good visual distribution
|
|
253
|
+
const p33 = messageCounts[Math.floor(len * 0.33)] || 2;
|
|
254
|
+
const p66 = messageCounts[Math.floor(len * 0.66)] || 3;
|
|
255
|
+
const p85 = messageCounts[Math.floor(len * 0.85)] || 4;
|
|
256
|
+
|
|
257
|
+
// Ensure reasonable spacing between levels
|
|
258
|
+
level2 = Math.max(2, Math.min(p33, max * 0.2));
|
|
259
|
+
level3 = Math.max(level2 + 1, Math.min(p66, max * 0.5));
|
|
260
|
+
level4 = Math.max(level3 + 1, Math.min(p85, max * 0.75));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { level1, level2, level3, level4 };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Render the heatmap calendar
|
|
268
|
+
*/
|
|
269
|
+
async renderHeatmap() {
|
|
270
|
+
if (!this.activityData) return;
|
|
271
|
+
|
|
272
|
+
const { dailyActivity, totalActivity } = this.activityData;
|
|
273
|
+
|
|
274
|
+
// Generate calendar structure
|
|
275
|
+
const calendarData = this.generateCalendarData(dailyActivity);
|
|
276
|
+
|
|
277
|
+
const container = this.container.querySelector('.activity-heatmap-container');
|
|
278
|
+
const modeClass = this.currentMetric === 'tools' ? 'tools-mode' : '';
|
|
279
|
+
container.className = `activity-heatmap-container ${modeClass}`;
|
|
280
|
+
container.innerHTML = `
|
|
281
|
+
<div class="heatmap-header">
|
|
282
|
+
<div class="heatmap-legend">
|
|
283
|
+
<span class="heatmap-legend-text">Less</span>
|
|
284
|
+
<div class="heatmap-legend-scale">
|
|
285
|
+
<div class="heatmap-legend-square level-0"></div>
|
|
286
|
+
<div class="heatmap-legend-square level-1"></div>
|
|
287
|
+
<div class="heatmap-legend-square level-2"></div>
|
|
288
|
+
<div class="heatmap-legend-square level-3"></div>
|
|
289
|
+
<div class="heatmap-legend-square level-4"></div>
|
|
290
|
+
</div>
|
|
291
|
+
<span class="heatmap-legend-more">More</span>
|
|
292
|
+
</div>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="heatmap-grid">
|
|
295
|
+
<div class="heatmap-months" id="heatmap-months-container">
|
|
296
|
+
${calendarData.months.map((month, index) =>
|
|
297
|
+
`<div class="heatmap-month" data-week-index="${index}">${month}</div>`
|
|
298
|
+
).join('')}
|
|
299
|
+
</div>
|
|
300
|
+
<div class="heatmap-weekdays">
|
|
301
|
+
<div class="heatmap-weekday">Mon</div>
|
|
302
|
+
<div class="heatmap-weekday"></div>
|
|
303
|
+
<div class="heatmap-weekday">Wed</div>
|
|
304
|
+
<div class="heatmap-weekday"></div>
|
|
305
|
+
<div class="heatmap-weekday">Fri</div>
|
|
306
|
+
<div class="heatmap-weekday"></div>
|
|
307
|
+
<div class="heatmap-weekday"></div>
|
|
308
|
+
</div>
|
|
309
|
+
<div class="heatmap-weeks">
|
|
310
|
+
${calendarData.weeks.map(week => this.renderWeek(week, dailyActivity)).join('')}
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
`;
|
|
314
|
+
|
|
315
|
+
this.attachEventListeners();
|
|
316
|
+
this.attachSettingsListeners();
|
|
317
|
+
this.positionMonthLabels();
|
|
318
|
+
|
|
319
|
+
// Re-position months on window resize
|
|
320
|
+
this.resizeHandler = () => this.positionMonthLabels();
|
|
321
|
+
window.addEventListener('resize', this.resizeHandler);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Position month labels based on actual week positions
|
|
326
|
+
*/
|
|
327
|
+
positionMonthLabels() {
|
|
328
|
+
setTimeout(() => {
|
|
329
|
+
const weeksContainer = this.container.querySelector('.heatmap-weeks');
|
|
330
|
+
const monthsContainer = this.container.querySelector('#heatmap-months-container');
|
|
331
|
+
const monthElements = monthsContainer?.querySelectorAll('.heatmap-month');
|
|
332
|
+
const weekElements = weeksContainer?.children;
|
|
333
|
+
|
|
334
|
+
if (!weeksContainer || !monthsContainer || !monthElements || !weekElements) return;
|
|
335
|
+
|
|
336
|
+
// Calculate the actual width and position of each week column
|
|
337
|
+
Array.from(monthElements).forEach((monthEl, index) => {
|
|
338
|
+
if (index < weekElements.length && monthEl.textContent.trim()) {
|
|
339
|
+
const weekEl = weekElements[index];
|
|
340
|
+
const weekRect = weekEl.getBoundingClientRect();
|
|
341
|
+
const containerRect = monthsContainer.getBoundingClientRect();
|
|
342
|
+
|
|
343
|
+
// Position month label at the start of its corresponding week
|
|
344
|
+
const leftPosition = weekRect.left - containerRect.left;
|
|
345
|
+
monthEl.style.left = `${leftPosition}px`;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
}, 50); // Small delay to ensure DOM is fully rendered
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Generate calendar structure for the last year
|
|
353
|
+
*/
|
|
354
|
+
generateCalendarData(dailyActivity) {
|
|
355
|
+
const today = new Date();
|
|
356
|
+
const oneYearAgo = new Date(today);
|
|
357
|
+
oneYearAgo.setFullYear(today.getFullYear() - 1);
|
|
358
|
+
oneYearAgo.setDate(today.getDate() + 1);
|
|
359
|
+
|
|
360
|
+
// Find the start of the week (Sunday) for the start date
|
|
361
|
+
const startDate = new Date(oneYearAgo);
|
|
362
|
+
startDate.setDate(startDate.getDate() - startDate.getDay());
|
|
363
|
+
|
|
364
|
+
const weeks = [];
|
|
365
|
+
const months = [];
|
|
366
|
+
const current = new Date(startDate);
|
|
367
|
+
|
|
368
|
+
// Generate weeks first
|
|
369
|
+
while (current <= today) {
|
|
370
|
+
const week = [];
|
|
371
|
+
|
|
372
|
+
for (let day = 0; day < 7; day++) {
|
|
373
|
+
if (current <= today) {
|
|
374
|
+
week.push(new Date(current));
|
|
375
|
+
} else {
|
|
376
|
+
week.push(null);
|
|
377
|
+
}
|
|
378
|
+
current.setDate(current.getDate() + 1);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
weeks.push(week);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Generate month labels - show when month changes
|
|
385
|
+
let lastDisplayedMonth = -1;
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < weeks.length; i++) {
|
|
388
|
+
const week = weeks[i];
|
|
389
|
+
let monthName = '';
|
|
390
|
+
|
|
391
|
+
if (week && week.length > 0) {
|
|
392
|
+
// Get the most representative day of the week (middle of week)
|
|
393
|
+
const middleDay = week[3] || week[2] || week[1] || week[0];
|
|
394
|
+
|
|
395
|
+
if (middleDay) {
|
|
396
|
+
const currentMonth = middleDay.getMonth();
|
|
397
|
+
|
|
398
|
+
// Show month name if it's the first occurrence or if month changed
|
|
399
|
+
if (currentMonth !== lastDisplayedMonth) {
|
|
400
|
+
monthName = middleDay.toLocaleDateString('en-US', { month: 'short' });
|
|
401
|
+
lastDisplayedMonth = currentMonth;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
months.push(monthName);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return { weeks, months };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Render a week column
|
|
414
|
+
*/
|
|
415
|
+
renderWeek(week, dailyActivity) {
|
|
416
|
+
const weekHtml = week.map(date => {
|
|
417
|
+
if (!date) return '<div class="heatmap-day empty"></div>';
|
|
418
|
+
|
|
419
|
+
const dateKey = date.toISOString().split('T')[0];
|
|
420
|
+
const activity = dailyActivity.get(dateKey);
|
|
421
|
+
const level = this.getActivityLevel(activity);
|
|
422
|
+
const modeClass = this.currentMetric === 'tools' ? 'tools-mode' : '';
|
|
423
|
+
|
|
424
|
+
return `
|
|
425
|
+
<div class="heatmap-day level-${level} ${modeClass}"
|
|
426
|
+
data-date="${dateKey}"
|
|
427
|
+
data-activity='${JSON.stringify(activity || { conversations: 0, tokens: 0, messages: 0, tools: 0 })}'>
|
|
428
|
+
</div>
|
|
429
|
+
`;
|
|
430
|
+
}).join('');
|
|
431
|
+
|
|
432
|
+
return `<div class="heatmap-week">${weekHtml}</div>`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Calculate activity level based on current metric using dynamic thresholds
|
|
437
|
+
*/
|
|
438
|
+
getActivityLevel(activity) {
|
|
439
|
+
if (!activity) return 0;
|
|
440
|
+
|
|
441
|
+
const metricValue = activity[this.currentMetric] || 0;
|
|
442
|
+
if (metricValue === 0) return 0;
|
|
443
|
+
|
|
444
|
+
const thresholds = this.activityData?.thresholds;
|
|
445
|
+
|
|
446
|
+
if (!thresholds) {
|
|
447
|
+
// Fallback to static levels if thresholds not available
|
|
448
|
+
if (metricValue >= 50) return 4;
|
|
449
|
+
if (metricValue >= 30) return 3;
|
|
450
|
+
if (metricValue >= 15) return 2;
|
|
451
|
+
if (metricValue >= 1) return 1;
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Use dynamic thresholds for better distribution
|
|
456
|
+
if (metricValue >= thresholds.level4) return 4;
|
|
457
|
+
if (metricValue >= thresholds.level3) return 3;
|
|
458
|
+
if (metricValue >= thresholds.level2) return 2;
|
|
459
|
+
if (metricValue >= thresholds.level1) return 1;
|
|
460
|
+
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Attach event listeners for tooltips and interactions
|
|
466
|
+
*/
|
|
467
|
+
attachEventListeners() {
|
|
468
|
+
const days = this.container.querySelectorAll('.heatmap-day');
|
|
469
|
+
|
|
470
|
+
days.forEach(day => {
|
|
471
|
+
day.addEventListener('mouseenter', (e) => this.showTooltip(e));
|
|
472
|
+
day.addEventListener('mouseleave', () => this.hideTooltip());
|
|
473
|
+
day.addEventListener('mousemove', (e) => this.updateTooltipPosition(e));
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Attach event listeners for settings dropdown
|
|
479
|
+
*/
|
|
480
|
+
attachSettingsListeners() {
|
|
481
|
+
// Remove existing listeners first to prevent duplicates
|
|
482
|
+
this.removeSettingsListeners();
|
|
483
|
+
|
|
484
|
+
const settingsButton = document.querySelector('.heatmap-settings');
|
|
485
|
+
const dropdown = document.getElementById('heatmap-settings-dropdown');
|
|
486
|
+
const metricOptions = dropdown?.querySelectorAll('.heatmap-metric-option');
|
|
487
|
+
|
|
488
|
+
console.log('🔥 Attaching settings listeners:', {
|
|
489
|
+
settingsButton: !!settingsButton,
|
|
490
|
+
dropdown: !!dropdown,
|
|
491
|
+
metricOptions: metricOptions?.length || 0
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
if (settingsButton && dropdown) {
|
|
495
|
+
// Store references to handlers for cleanup
|
|
496
|
+
this.settingsClickHandler = (e) => {
|
|
497
|
+
e.stopPropagation();
|
|
498
|
+
dropdown.classList.toggle('show');
|
|
499
|
+
console.log('🔥 Settings dropdown toggled:', dropdown.classList.contains('show'));
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
this.documentClickHandler = (e) => {
|
|
503
|
+
if (!settingsButton.contains(e.target) && !dropdown.contains(e.target)) {
|
|
504
|
+
dropdown.classList.remove('show');
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Add event listeners
|
|
509
|
+
settingsButton.addEventListener('click', this.settingsClickHandler);
|
|
510
|
+
document.addEventListener('click', this.documentClickHandler);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Handle metric selection
|
|
514
|
+
if (metricOptions) {
|
|
515
|
+
this.metricHandlers = [];
|
|
516
|
+
metricOptions.forEach(option => {
|
|
517
|
+
const handler = (e) => {
|
|
518
|
+
const metric = e.target.dataset.metric;
|
|
519
|
+
this.changeMetric(metric);
|
|
520
|
+
|
|
521
|
+
// Update active state
|
|
522
|
+
metricOptions.forEach(opt => opt.classList.remove('active'));
|
|
523
|
+
e.target.classList.add('active');
|
|
524
|
+
|
|
525
|
+
// Close dropdown
|
|
526
|
+
dropdown.classList.remove('show');
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
option.addEventListener('click', handler);
|
|
530
|
+
this.metricHandlers.push({ element: option, handler });
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Remove existing settings event listeners to prevent duplicates
|
|
537
|
+
*/
|
|
538
|
+
removeSettingsListeners() {
|
|
539
|
+
const settingsButton = document.querySelector('.heatmap-settings');
|
|
540
|
+
|
|
541
|
+
if (this.settingsClickHandler && settingsButton) {
|
|
542
|
+
settingsButton.removeEventListener('click', this.settingsClickHandler);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (this.documentClickHandler) {
|
|
546
|
+
document.removeEventListener('click', this.documentClickHandler);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (this.metricHandlers) {
|
|
550
|
+
this.metricHandlers.forEach(({ element, handler }) => {
|
|
551
|
+
element.removeEventListener('click', handler);
|
|
552
|
+
});
|
|
553
|
+
this.metricHandlers = [];
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Change the activity metric (messages or tools)
|
|
559
|
+
*/
|
|
560
|
+
async changeMetric(metric) {
|
|
561
|
+
console.log(`🔥 Changing metric to: ${metric}`);
|
|
562
|
+
this.currentMetric = metric;
|
|
563
|
+
|
|
564
|
+
// Recalculate thresholds based on new metric
|
|
565
|
+
if (this.activityData) {
|
|
566
|
+
await this.recalculateForMetric(metric);
|
|
567
|
+
await this.renderHeatmap();
|
|
568
|
+
this.updateTitle();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Recalculate activity data for the new metric
|
|
574
|
+
*/
|
|
575
|
+
async recalculateForMetric(metric) {
|
|
576
|
+
const { dailyActivity } = this.activityData;
|
|
577
|
+
|
|
578
|
+
// Recalculate thresholds based on the new metric
|
|
579
|
+
const metricCounts = Array.from(dailyActivity.values())
|
|
580
|
+
.map(activity => activity[metric] || 0)
|
|
581
|
+
.filter(count => count > 0)
|
|
582
|
+
.sort((a, b) => a - b);
|
|
583
|
+
|
|
584
|
+
const thresholds = this.calculateDynamicThresholds(metricCounts);
|
|
585
|
+
|
|
586
|
+
// Calculate new total activity
|
|
587
|
+
let totalActivity = 0;
|
|
588
|
+
dailyActivity.forEach(activity => {
|
|
589
|
+
totalActivity += activity[metric] || 0;
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
this.activityData.thresholds = thresholds;
|
|
593
|
+
this.activityData.totalActivity = totalActivity;
|
|
594
|
+
|
|
595
|
+
console.log(`🔥 Recalculated for ${metric}:`, thresholds);
|
|
596
|
+
console.log(`🔥 New total: ${totalActivity}`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Show tooltip on day hover
|
|
601
|
+
*/
|
|
602
|
+
showTooltip(event) {
|
|
603
|
+
const day = event.target;
|
|
604
|
+
const date = day.dataset.date;
|
|
605
|
+
const activity = JSON.parse(day.dataset.activity || '{}');
|
|
606
|
+
|
|
607
|
+
if (!date) return;
|
|
608
|
+
|
|
609
|
+
const dateObj = new Date(date);
|
|
610
|
+
const formattedDate = dateObj.toLocaleDateString('en-US', {
|
|
611
|
+
weekday: 'short',
|
|
612
|
+
month: 'short',
|
|
613
|
+
day: 'numeric',
|
|
614
|
+
year: 'numeric'
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const currentValue = activity[this.currentMetric] || 0;
|
|
618
|
+
const otherMetric = this.currentMetric === 'messages' ? 'conversations' : 'messages';
|
|
619
|
+
const otherValue = activity[otherMetric] || 0;
|
|
620
|
+
|
|
621
|
+
let activityText = `No ${this.currentMetric}`;
|
|
622
|
+
if (currentValue > 0) {
|
|
623
|
+
const suffix = currentValue === 1 ? '' : 's';
|
|
624
|
+
activityText = `${currentValue} ${this.currentMetric.slice(0, -1)}${suffix}`;
|
|
625
|
+
|
|
626
|
+
if (otherValue > 0) {
|
|
627
|
+
const otherSuffix = otherValue === 1 ? '' : 's';
|
|
628
|
+
const otherLabel = otherMetric === 'conversations' ? 'conversation' : 'message';
|
|
629
|
+
activityText += ` • ${otherValue} ${otherLabel}${otherSuffix}`;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
this.tooltip.querySelector('.heatmap-tooltip-date').textContent = formattedDate;
|
|
634
|
+
this.tooltip.querySelector('.heatmap-tooltip-activity').textContent = activityText;
|
|
635
|
+
|
|
636
|
+
this.tooltip.classList.add('show');
|
|
637
|
+
this.updateTooltipPosition(event);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Hide tooltip
|
|
642
|
+
*/
|
|
643
|
+
hideTooltip() {
|
|
644
|
+
this.tooltip.classList.remove('show');
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Update tooltip position
|
|
649
|
+
*/
|
|
650
|
+
updateTooltipPosition(event) {
|
|
651
|
+
const tooltip = this.tooltip;
|
|
652
|
+
const rect = tooltip.getBoundingClientRect();
|
|
653
|
+
|
|
654
|
+
let x = event.pageX + 10;
|
|
655
|
+
let y = event.pageY - rect.height - 10;
|
|
656
|
+
|
|
657
|
+
// Adjust if tooltip would go off screen
|
|
658
|
+
if (x + rect.width > window.innerWidth) {
|
|
659
|
+
x = event.pageX - rect.width - 10;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (y < window.scrollY) {
|
|
663
|
+
y = event.pageY + 10;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
tooltip.style.left = `${x}px`;
|
|
667
|
+
tooltip.style.top = `${y}px`;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Update the title with total activity count
|
|
672
|
+
*/
|
|
673
|
+
updateTitle() {
|
|
674
|
+
if (!this.activityData) return;
|
|
675
|
+
|
|
676
|
+
const { totalActivity } = this.activityData;
|
|
677
|
+
const titleElement = document.getElementById('activity-total');
|
|
678
|
+
|
|
679
|
+
if (titleElement) {
|
|
680
|
+
titleElement.textContent = `${this.formatNumber(totalActivity)} ${this.currentMetric} in the last year`;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Format large numbers with commas
|
|
686
|
+
*/
|
|
687
|
+
formatNumber(num) {
|
|
688
|
+
if (num >= 1000) {
|
|
689
|
+
return (num / 1000).toFixed(1) + 'k';
|
|
690
|
+
}
|
|
691
|
+
return num.toLocaleString();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Show error state
|
|
696
|
+
*/
|
|
697
|
+
showErrorState() {
|
|
698
|
+
const container = this.container.querySelector('.activity-heatmap-container');
|
|
699
|
+
container.innerHTML = `
|
|
700
|
+
<div class="heatmap-empty-state">
|
|
701
|
+
<div class="heatmap-empty-icon">📊</div>
|
|
702
|
+
<div class="heatmap-empty-text">Unable to load activity data</div>
|
|
703
|
+
<div class="heatmap-empty-subtext">Please try refreshing the page</div>
|
|
704
|
+
</div>
|
|
705
|
+
`;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Refresh the heatmap data
|
|
710
|
+
*/
|
|
711
|
+
async refresh() {
|
|
712
|
+
console.log('🔥 Refreshing heatmap data...');
|
|
713
|
+
await this.loadActivityData();
|
|
714
|
+
this.positionMonthLabels();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Cleanup resources
|
|
719
|
+
*/
|
|
720
|
+
destroy() {
|
|
721
|
+
if (this.tooltip && this.tooltip.parentNode) {
|
|
722
|
+
this.tooltip.parentNode.removeChild(this.tooltip);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Remove resize listener
|
|
726
|
+
if (this.resizeHandler) {
|
|
727
|
+
window.removeEventListener('resize', this.resizeHandler);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Remove settings listeners
|
|
731
|
+
this.removeSettingsListeners();
|
|
732
|
+
|
|
733
|
+
console.log('🔥 ActivityHeatmap destroyed');
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Make it globally available
|
|
738
|
+
window.ActivityHeatmap = ActivityHeatmap;
|