claude-code-templates 1.28.3 → 1.28.4
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 +1 -0
- package/package.json +1 -1
- package/src/analytics/core/YearInReview2025.js +928 -0
- package/src/analytics-web/2025.html +2100 -0
- package/src/analytics.js +39 -3
- package/src/index.js +11 -2
- package/src/plugin-dashboard.js +11 -3
- package/src/validation/README.md +1 -1
|
@@ -0,0 +1,2100 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Claude Code 2025 - Activity Graph</title>
|
|
7
|
+
|
|
8
|
+
<style>
|
|
9
|
+
:root {
|
|
10
|
+
--bg-primary: #0a0a0f;
|
|
11
|
+
--text-primary: #e0e0e0;
|
|
12
|
+
--text-accent: #d97706;
|
|
13
|
+
--agent-color: #f59e0b;
|
|
14
|
+
--mcp-color: #8b5cf6;
|
|
15
|
+
--command-color: #10b981;
|
|
16
|
+
--skill-color: #ec4899;
|
|
17
|
+
--tool-color: #3b82f6;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
* {
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 0;
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
body {
|
|
27
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
28
|
+
background: var(--bg-primary);
|
|
29
|
+
color: var(--text-primary);
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
height: 100vh;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#canvas {
|
|
35
|
+
display: block;
|
|
36
|
+
width: 100%;
|
|
37
|
+
height: 100%;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.overlay {
|
|
41
|
+
position: absolute;
|
|
42
|
+
top: 0;
|
|
43
|
+
left: 0;
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: 100%;
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
z-index: 10;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.hud {
|
|
51
|
+
position: absolute;
|
|
52
|
+
top: 20px;
|
|
53
|
+
left: 20px;
|
|
54
|
+
background: rgba(10, 10, 15, 0.8);
|
|
55
|
+
padding: 20px;
|
|
56
|
+
border-radius: 8px;
|
|
57
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
58
|
+
backdrop-filter: blur(10px);
|
|
59
|
+
pointer-events: auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.current-date {
|
|
63
|
+
font-size: 32px;
|
|
64
|
+
font-weight: 700;
|
|
65
|
+
color: var(--text-accent);
|
|
66
|
+
margin-bottom: 15px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.stat-line {
|
|
70
|
+
font-size: 13px;
|
|
71
|
+
margin: 5px 0;
|
|
72
|
+
opacity: 0.9;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.stat-value {
|
|
76
|
+
color: var(--text-accent);
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.legend {
|
|
81
|
+
position: absolute;
|
|
82
|
+
top: 20px;
|
|
83
|
+
right: 20px;
|
|
84
|
+
background: rgba(10, 10, 15, 0.8);
|
|
85
|
+
padding: 15px;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
88
|
+
backdrop-filter: blur(10px);
|
|
89
|
+
font-size: 12px;
|
|
90
|
+
max-height: calc(100vh - 200px);
|
|
91
|
+
overflow-y: auto;
|
|
92
|
+
pointer-events: auto;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Custom scrollbar */
|
|
96
|
+
.legend::-webkit-scrollbar,
|
|
97
|
+
#toolsList::-webkit-scrollbar,
|
|
98
|
+
#componentsList::-webkit-scrollbar {
|
|
99
|
+
width: 6px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.legend::-webkit-scrollbar-track,
|
|
103
|
+
#toolsList::-webkit-scrollbar-track,
|
|
104
|
+
#componentsList::-webkit-scrollbar-track {
|
|
105
|
+
background: rgba(255,255,255,0.05);
|
|
106
|
+
border-radius: 3px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.legend::-webkit-scrollbar-thumb,
|
|
110
|
+
#toolsList::-webkit-scrollbar-thumb,
|
|
111
|
+
#componentsList::-webkit-scrollbar-thumb {
|
|
112
|
+
background: rgba(255,255,255,0.2);
|
|
113
|
+
border-radius: 3px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.legend::-webkit-scrollbar-thumb:hover,
|
|
117
|
+
#toolsList::-webkit-scrollbar-thumb:hover,
|
|
118
|
+
#componentsList::-webkit-scrollbar-thumb:hover {
|
|
119
|
+
background: rgba(255,255,255,0.3);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.legend-item {
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
gap: 8px;
|
|
126
|
+
margin: 6px 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.legend-dot {
|
|
130
|
+
width: 12px;
|
|
131
|
+
height: 12px;
|
|
132
|
+
border-radius: 50%;
|
|
133
|
+
box-shadow: 0 0 8px currentColor;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.timeline {
|
|
137
|
+
position: absolute;
|
|
138
|
+
bottom: 30px;
|
|
139
|
+
left: 50%;
|
|
140
|
+
transform: translateX(-50%);
|
|
141
|
+
width: 80%;
|
|
142
|
+
max-width: 1000px;
|
|
143
|
+
pointer-events: auto;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.timeline-bar {
|
|
147
|
+
height: 3px;
|
|
148
|
+
background: rgba(255,255,255,0.1);
|
|
149
|
+
border-radius: 2px;
|
|
150
|
+
position: relative;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.timeline-progress {
|
|
154
|
+
height: 100%;
|
|
155
|
+
background: linear-gradient(90deg, var(--text-accent), #fbbf24);
|
|
156
|
+
width: 0%;
|
|
157
|
+
box-shadow: 0 0 10px var(--text-accent);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.timeline-labels {
|
|
161
|
+
display: flex;
|
|
162
|
+
justify-content: space-between;
|
|
163
|
+
margin-top: 8px;
|
|
164
|
+
font-size: 10px;
|
|
165
|
+
color: rgba(255,255,255,0.4);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.controls {
|
|
169
|
+
position: absolute;
|
|
170
|
+
bottom: 90px;
|
|
171
|
+
right: 20px;
|
|
172
|
+
display: flex;
|
|
173
|
+
gap: 8px;
|
|
174
|
+
pointer-events: all;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.control-btn {
|
|
178
|
+
background: rgba(26, 26, 46, 0.8);
|
|
179
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
180
|
+
color: var(--text-primary);
|
|
181
|
+
padding: 8px 16px;
|
|
182
|
+
border-radius: 5px;
|
|
183
|
+
cursor: pointer;
|
|
184
|
+
font-family: inherit;
|
|
185
|
+
font-size: 11px;
|
|
186
|
+
transition: all 0.2s;
|
|
187
|
+
backdrop-filter: blur(10px);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.control-btn:hover {
|
|
191
|
+
background: var(--text-accent);
|
|
192
|
+
border-color: var(--text-accent);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.control-btn.active {
|
|
196
|
+
background: var(--text-accent);
|
|
197
|
+
border-color: var(--text-accent);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.intro-screen {
|
|
201
|
+
position: absolute;
|
|
202
|
+
top: 0;
|
|
203
|
+
left: 0;
|
|
204
|
+
width: 100%;
|
|
205
|
+
height: 100%;
|
|
206
|
+
background: rgba(10, 10, 15, 0.95);
|
|
207
|
+
display: flex;
|
|
208
|
+
flex-direction: column;
|
|
209
|
+
align-items: center;
|
|
210
|
+
justify-content: center;
|
|
211
|
+
z-index: 100;
|
|
212
|
+
backdrop-filter: blur(20px);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.intro-title {
|
|
216
|
+
font-size: 64px;
|
|
217
|
+
font-weight: 700;
|
|
218
|
+
color: var(--text-accent);
|
|
219
|
+
margin-bottom: 15px;
|
|
220
|
+
text-shadow: 0 0 30px var(--text-accent);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.intro-subtitle {
|
|
224
|
+
font-size: 20px;
|
|
225
|
+
color: var(--text-primary);
|
|
226
|
+
margin-bottom: 40px;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.start-btn {
|
|
230
|
+
background: var(--text-accent);
|
|
231
|
+
border: none;
|
|
232
|
+
color: white;
|
|
233
|
+
padding: 18px 36px;
|
|
234
|
+
border-radius: 6px;
|
|
235
|
+
font-size: 16px;
|
|
236
|
+
font-family: inherit;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
box-shadow: 0 0 20px var(--text-accent);
|
|
239
|
+
transition: all 0.3s;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.start-btn:hover {
|
|
243
|
+
transform: scale(1.05);
|
|
244
|
+
box-shadow: 0 0 30px var(--text-accent);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.loading {
|
|
248
|
+
position: absolute;
|
|
249
|
+
top: 50%;
|
|
250
|
+
left: 50%;
|
|
251
|
+
transform: translate(-50%, -50%);
|
|
252
|
+
text-align: center;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.spinner {
|
|
256
|
+
width: 40px;
|
|
257
|
+
height: 40px;
|
|
258
|
+
border: 3px solid rgba(255,255,255,0.1);
|
|
259
|
+
border-top-color: var(--text-accent);
|
|
260
|
+
border-radius: 50%;
|
|
261
|
+
animation: spin 1s linear infinite;
|
|
262
|
+
margin: 0 auto 15px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@keyframes spin {
|
|
266
|
+
to { transform: rotate(360deg); }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.event-toast {
|
|
270
|
+
position: absolute;
|
|
271
|
+
top: 80px;
|
|
272
|
+
left: 50%;
|
|
273
|
+
transform: translate(-50%, -100%);
|
|
274
|
+
background: rgba(26, 26, 46, 0.95);
|
|
275
|
+
border: 2px solid var(--text-accent);
|
|
276
|
+
border-radius: 8px;
|
|
277
|
+
padding: 15px 35px;
|
|
278
|
+
text-align: center;
|
|
279
|
+
box-shadow: 0 0 30px var(--text-accent);
|
|
280
|
+
z-index: 50;
|
|
281
|
+
backdrop-filter: blur(20px);
|
|
282
|
+
opacity: 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.event-toast.show {
|
|
286
|
+
animation: toastSlide 2.5s ease forwards;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
@keyframes toastSlide {
|
|
290
|
+
0% {
|
|
291
|
+
transform: translate(-50%, -100%);
|
|
292
|
+
opacity: 0;
|
|
293
|
+
}
|
|
294
|
+
10% {
|
|
295
|
+
transform: translate(-50%, 0);
|
|
296
|
+
opacity: 1;
|
|
297
|
+
}
|
|
298
|
+
90% {
|
|
299
|
+
transform: translate(-50%, 0);
|
|
300
|
+
opacity: 1;
|
|
301
|
+
}
|
|
302
|
+
100% {
|
|
303
|
+
transform: translate(-50%, -100%);
|
|
304
|
+
opacity: 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.event-toast h3 {
|
|
309
|
+
font-size: 18px;
|
|
310
|
+
color: var(--text-accent);
|
|
311
|
+
margin: 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.tooltip {
|
|
315
|
+
position: absolute;
|
|
316
|
+
background: rgba(26, 26, 46, 0.95);
|
|
317
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
318
|
+
border-radius: 8px;
|
|
319
|
+
padding: 10px 15px;
|
|
320
|
+
color: var(--text-primary);
|
|
321
|
+
font-size: 13px;
|
|
322
|
+
pointer-events: none;
|
|
323
|
+
z-index: 100;
|
|
324
|
+
opacity: 0;
|
|
325
|
+
transition: opacity 0.2s;
|
|
326
|
+
backdrop-filter: blur(10px);
|
|
327
|
+
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.tooltip.show {
|
|
332
|
+
opacity: 1;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.tooltip-title {
|
|
336
|
+
font-weight: 600;
|
|
337
|
+
color: var(--text-accent);
|
|
338
|
+
margin-bottom: 5px;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.tooltip-info {
|
|
342
|
+
color: var(--text-secondary);
|
|
343
|
+
font-size: 11px;
|
|
344
|
+
}
|
|
345
|
+
</style>
|
|
346
|
+
</head>
|
|
347
|
+
<body>
|
|
348
|
+
<!-- Intro Screen -->
|
|
349
|
+
<div id="introScreen" class="intro-screen">
|
|
350
|
+
<div class="loading" id="loading">
|
|
351
|
+
<div class="spinner"></div>
|
|
352
|
+
<p>Loading your journey...</p>
|
|
353
|
+
</div>
|
|
354
|
+
<div id="introContent" style="display: none;">
|
|
355
|
+
<h1 class="intro-title">2025</h1>
|
|
356
|
+
<p class="intro-subtitle">Your Year with Claude Code</p>
|
|
357
|
+
<button class="start-btn" onclick="startAnimation()">▶ Start</button>
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<!-- Canvas -->
|
|
362
|
+
<canvas id="canvas"></canvas>
|
|
363
|
+
|
|
364
|
+
<!-- Overlay -->
|
|
365
|
+
<div class="overlay">
|
|
366
|
+
<div class="hud">
|
|
367
|
+
<div class="current-date" id="currentDate">Jan 1, 2025</div>
|
|
368
|
+
<div class="stat-line">Conversations: <span class="stat-value" id="statConversations">0</span></div>
|
|
369
|
+
<div class="stat-line">Components: <span class="stat-value" id="statComponents">0</span></div>
|
|
370
|
+
<div class="stat-line">Tool Calls: <span class="stat-value" id="statTools">0</span></div>
|
|
371
|
+
<div class="stat-line">Active Days: <span class="stat-value" id="statDays">0</span></div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div class="legend">
|
|
375
|
+
<div style="font-weight: 600; margin-bottom: 8px;">Models Used</div>
|
|
376
|
+
<div id="modelsList">
|
|
377
|
+
<!-- Models will be added dynamically -->
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
<div style="font-weight: 600; margin-bottom: 8px; margin-top: 16px;">Tools Used</div>
|
|
381
|
+
<div id="toolsList">
|
|
382
|
+
<!-- Tools will be added dynamically -->
|
|
383
|
+
</div>
|
|
384
|
+
|
|
385
|
+
<div style="font-weight: 600; margin-bottom: 8px; margin-top: 16px;">Components Used</div>
|
|
386
|
+
<div id="componentsList">
|
|
387
|
+
<!-- Components will be added dynamically -->
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<div class="timeline">
|
|
392
|
+
<div class="timeline-bar">
|
|
393
|
+
<div class="timeline-progress" id="timelineProgress"></div>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="timeline-labels">
|
|
396
|
+
<span>Jan</span><span>Feb</span><span>Mar</span><span>Apr</span>
|
|
397
|
+
<span>May</span><span>Jun</span><span>Jul</span><span>Aug</span>
|
|
398
|
+
<span>Sep</span><span>Oct</span><span>Nov</span><span>Dec</span>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<div class="controls">
|
|
403
|
+
<button class="control-btn" onclick="restartAnimation()">🔄 Restart</button>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<!-- Event Toast -->
|
|
408
|
+
<div id="eventToast" class="event-toast"></div>
|
|
409
|
+
|
|
410
|
+
<!-- Tooltip -->
|
|
411
|
+
<div id="tooltip" class="tooltip"></div>
|
|
412
|
+
|
|
413
|
+
<script>
|
|
414
|
+
// Canvas setup
|
|
415
|
+
const canvas = document.getElementById('canvas');
|
|
416
|
+
const ctx = canvas.getContext('2d');
|
|
417
|
+
|
|
418
|
+
// State
|
|
419
|
+
let animationData = null;
|
|
420
|
+
let isPlaying = false;
|
|
421
|
+
let speedMultiplier = 5; // Fixed at 5x speed
|
|
422
|
+
let startTime = null;
|
|
423
|
+
let currentDayIndex = 0;
|
|
424
|
+
let stats = { conversations: 0, components: 0, tools: 0, days: 0 };
|
|
425
|
+
let processedEvents = new Set();
|
|
426
|
+
let shownMilestones = new Set();
|
|
427
|
+
let toolNodes = new Map(); // Map of tool name -> tool node
|
|
428
|
+
let uniqueTools = new Map(); // Track unique tools with their colors
|
|
429
|
+
let modelNodes = new Map(); // Map of model name -> model node
|
|
430
|
+
|
|
431
|
+
// Mouse tracking
|
|
432
|
+
let mouseX = 0;
|
|
433
|
+
let mouseY = 0;
|
|
434
|
+
let hoveredNode = null;
|
|
435
|
+
let componentNodes = new Map(); // Map of component name -> component node (second layer)
|
|
436
|
+
|
|
437
|
+
// Zoom and pan state
|
|
438
|
+
let zoom = 1;
|
|
439
|
+
let panX = 0;
|
|
440
|
+
let panY = 0;
|
|
441
|
+
let isDragging = false;
|
|
442
|
+
let draggedNode = null;
|
|
443
|
+
let isPanning = false;
|
|
444
|
+
let lastMouseX = 0;
|
|
445
|
+
let lastMouseY = 0;
|
|
446
|
+
|
|
447
|
+
// Graph structure
|
|
448
|
+
const centerNode = { x: 0, y: 0, name: 'Claude', color: '#d97706' };
|
|
449
|
+
const branches = {
|
|
450
|
+
agents: { nodes: [], angle: 0, color: '#f59e0b' },
|
|
451
|
+
mcps: { nodes: [], angle: Math.PI / 2, color: '#8b5cf6' },
|
|
452
|
+
commands: { nodes: [], angle: Math.PI, color: '#10b981' },
|
|
453
|
+
skills: { nodes: [], angle: 3 * Math.PI / 2, color: '#ec4899' }
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// Active beams (tool calls) - animating beams
|
|
457
|
+
let beams = [];
|
|
458
|
+
// Permanent connections (one per node) - Map of nodeId -> beam
|
|
459
|
+
let permanentConnections = new Map();
|
|
460
|
+
|
|
461
|
+
// Resize canvas
|
|
462
|
+
function resizeCanvas() {
|
|
463
|
+
canvas.width = window.innerWidth;
|
|
464
|
+
canvas.height = window.innerHeight;
|
|
465
|
+
centerNode.x = canvas.width / 2;
|
|
466
|
+
centerNode.y = canvas.height / 2;
|
|
467
|
+
}
|
|
468
|
+
resizeCanvas();
|
|
469
|
+
window.addEventListener('resize', resizeCanvas);
|
|
470
|
+
|
|
471
|
+
// Mouse tracking for tooltips
|
|
472
|
+
canvas.addEventListener('mousemove', (e) => {
|
|
473
|
+
const rect = canvas.getBoundingClientRect();
|
|
474
|
+
mouseX = e.clientX - rect.left;
|
|
475
|
+
mouseY = e.clientY - rect.top;
|
|
476
|
+
|
|
477
|
+
// Transform mouse coordinates to account for zoom and pan
|
|
478
|
+
const transformedMouseX = (mouseX - panX) / zoom;
|
|
479
|
+
const transformedMouseY = (mouseY - panY) / zoom;
|
|
480
|
+
|
|
481
|
+
// Find hovered node
|
|
482
|
+
hoveredNode = null;
|
|
483
|
+
|
|
484
|
+
// Check model nodes (pie slices in center)
|
|
485
|
+
const dx = transformedMouseX - centerNode.x;
|
|
486
|
+
const dy = transformedMouseY - centerNode.y;
|
|
487
|
+
const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
|
|
488
|
+
const mouseAngle = Math.atan2(dy, dx) + Math.PI / 2; // Offset by -90 degrees to match pie drawing
|
|
489
|
+
const normalizedAngle = (mouseAngle + Math.PI * 2) % (Math.PI * 2); // Normalize to 0-2π
|
|
490
|
+
|
|
491
|
+
modelNodes.forEach((node) => {
|
|
492
|
+
if (distanceFromCenter < node.size + 10) {
|
|
493
|
+
const startAngle = (node.index / node.total) * Math.PI * 2;
|
|
494
|
+
const endAngle = ((node.index + 1) / node.total) * Math.PI * 2;
|
|
495
|
+
|
|
496
|
+
// Check if mouse angle is within this slice's angle range
|
|
497
|
+
if (normalizedAngle >= startAngle && normalizedAngle <= endAngle) {
|
|
498
|
+
hoveredNode = { type: 'model', node };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Check tool nodes
|
|
504
|
+
if (!hoveredNode) {
|
|
505
|
+
toolNodes.forEach((node) => {
|
|
506
|
+
const dx = transformedMouseX - node.x;
|
|
507
|
+
const dy = transformedMouseY - node.y;
|
|
508
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
509
|
+
if (distance < node.size + 5) {
|
|
510
|
+
hoveredNode = { type: 'tool', node };
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Check component nodes (second layer)
|
|
516
|
+
if (!hoveredNode) {
|
|
517
|
+
componentNodes.forEach((node) => {
|
|
518
|
+
const dx = transformedMouseX - node.x;
|
|
519
|
+
const dy = transformedMouseY - node.y;
|
|
520
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
521
|
+
if (distance < node.size + 5) {
|
|
522
|
+
hoveredNode = { type: 'component', node };
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Update tooltip
|
|
528
|
+
updateTooltip();
|
|
529
|
+
|
|
530
|
+
// Update cursor based on hover state
|
|
531
|
+
if (!isDragging && !isPanning) {
|
|
532
|
+
if (hoveredNode && (hoveredNode.type === 'tool' || hoveredNode.type === 'component')) {
|
|
533
|
+
canvas.style.cursor = 'grab';
|
|
534
|
+
} else {
|
|
535
|
+
canvas.style.cursor = 'default';
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
canvas.addEventListener('mouseleave', () => {
|
|
541
|
+
hoveredNode = null;
|
|
542
|
+
isDragging = false;
|
|
543
|
+
draggedNode = null;
|
|
544
|
+
isPanning = false;
|
|
545
|
+
updateTooltip();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Zoom with mouse wheel
|
|
549
|
+
canvas.addEventListener('wheel', (e) => {
|
|
550
|
+
e.preventDefault();
|
|
551
|
+
const rect = canvas.getBoundingClientRect();
|
|
552
|
+
const mouseXCanvas = e.clientX - rect.left;
|
|
553
|
+
const mouseYCanvas = e.clientY - rect.top;
|
|
554
|
+
|
|
555
|
+
// Calculate zoom factor
|
|
556
|
+
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
|
557
|
+
const newZoom = Math.max(0.3, Math.min(3, zoom * zoomFactor));
|
|
558
|
+
|
|
559
|
+
// Adjust pan to zoom towards mouse position
|
|
560
|
+
const zoomRatio = newZoom / zoom;
|
|
561
|
+
panX = mouseXCanvas - (mouseXCanvas - panX) * zoomRatio;
|
|
562
|
+
panY = mouseYCanvas - (mouseYCanvas - panY) * zoomRatio;
|
|
563
|
+
|
|
564
|
+
zoom = newZoom;
|
|
565
|
+
}, { passive: false });
|
|
566
|
+
|
|
567
|
+
// Mouse down - start drag or pan
|
|
568
|
+
canvas.addEventListener('mousedown', (e) => {
|
|
569
|
+
const rect = canvas.getBoundingClientRect();
|
|
570
|
+
lastMouseX = e.clientX - rect.left;
|
|
571
|
+
lastMouseY = e.clientY - rect.top;
|
|
572
|
+
|
|
573
|
+
// Check if clicking on a node
|
|
574
|
+
if (hoveredNode && (hoveredNode.type === 'tool' || hoveredNode.type === 'component')) {
|
|
575
|
+
isDragging = true;
|
|
576
|
+
draggedNode = hoveredNode.node;
|
|
577
|
+
canvas.style.cursor = 'grabbing';
|
|
578
|
+
} else {
|
|
579
|
+
// Start panning
|
|
580
|
+
isPanning = true;
|
|
581
|
+
canvas.style.cursor = 'move';
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// Mouse move - drag node or pan canvas
|
|
586
|
+
canvas.addEventListener('mousemove', (e) => {
|
|
587
|
+
if (!isDragging && !isPanning) return;
|
|
588
|
+
|
|
589
|
+
const rect = canvas.getBoundingClientRect();
|
|
590
|
+
const currentX = e.clientX - rect.left;
|
|
591
|
+
const currentY = e.clientY - rect.top;
|
|
592
|
+
const deltaX = (currentX - lastMouseX) / zoom;
|
|
593
|
+
const deltaY = (currentY - lastMouseY) / zoom;
|
|
594
|
+
|
|
595
|
+
if (isDragging && draggedNode) {
|
|
596
|
+
// Move the dragged node
|
|
597
|
+
draggedNode.x += deltaX;
|
|
598
|
+
draggedNode.y += deltaY;
|
|
599
|
+
draggedNode.targetX = draggedNode.x;
|
|
600
|
+
draggedNode.targetY = draggedNode.y;
|
|
601
|
+
// Update random angle to match new position (for future percentage updates)
|
|
602
|
+
draggedNode.randomAngle = Math.atan2(
|
|
603
|
+
draggedNode.y - centerNode.y,
|
|
604
|
+
draggedNode.x - centerNode.x
|
|
605
|
+
);
|
|
606
|
+
} else if (isPanning) {
|
|
607
|
+
// Pan the canvas
|
|
608
|
+
panX += (currentX - lastMouseX);
|
|
609
|
+
panY += (currentY - lastMouseY);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
lastMouseX = currentX;
|
|
613
|
+
lastMouseY = currentY;
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Mouse up - stop drag or pan
|
|
617
|
+
canvas.addEventListener('mouseup', () => {
|
|
618
|
+
isDragging = false;
|
|
619
|
+
draggedNode = null;
|
|
620
|
+
isPanning = false;
|
|
621
|
+
canvas.style.cursor = 'default';
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// Double click to reset zoom and pan
|
|
625
|
+
canvas.addEventListener('dblclick', () => {
|
|
626
|
+
zoom = 1;
|
|
627
|
+
panX = 0;
|
|
628
|
+
panY = 0;
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// Update tooltip display
|
|
632
|
+
function updateTooltip() {
|
|
633
|
+
const tooltip = document.getElementById('tooltip');
|
|
634
|
+
|
|
635
|
+
if (!hoveredNode) {
|
|
636
|
+
tooltip.classList.remove('show');
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const node = hoveredNode.node;
|
|
641
|
+
let content = '';
|
|
642
|
+
|
|
643
|
+
if (hoveredNode.type === 'model') {
|
|
644
|
+
const percentage = node.count > 0 ? node.percentage || 0 : 0;
|
|
645
|
+
const percentDisplay = percentage >= 1 ? Math.round(percentage) + '%' : percentage.toFixed(1) + '%';
|
|
646
|
+
content = `
|
|
647
|
+
<div class="tooltip-title">${node.displayName}</div>
|
|
648
|
+
<div class="tooltip-info">Model • ${percentDisplay} of activity (${node.count.toLocaleString()} calls)</div>
|
|
649
|
+
`;
|
|
650
|
+
} else if (hoveredNode.type === 'tool') {
|
|
651
|
+
const percentDisplay = node.percentage >= 1 ? Math.round(node.percentage) + '%' : node.percentage.toFixed(1) + '%';
|
|
652
|
+
content = `
|
|
653
|
+
<div class="tooltip-title">${node.toolName}</div>
|
|
654
|
+
<div class="tooltip-info">Tool • ${percentDisplay} of tool calls (${node.count.toLocaleString()})</div>
|
|
655
|
+
`;
|
|
656
|
+
} else if (hoveredNode.type === 'component') {
|
|
657
|
+
const typeLabels = {
|
|
658
|
+
'command': 'Command',
|
|
659
|
+
'skill': 'Skill',
|
|
660
|
+
'mcp': 'MCP',
|
|
661
|
+
'subagent': 'Subagent'
|
|
662
|
+
};
|
|
663
|
+
const typeLabel = typeLabels[node.type] || node.type;
|
|
664
|
+
const percentDisplay = node.percentage >= 1 ? Math.round(node.percentage) + '%' : node.percentage.toFixed(1) + '%';
|
|
665
|
+
content = `
|
|
666
|
+
<div class="tooltip-title">${node.name}</div>
|
|
667
|
+
<div class="tooltip-info">${typeLabel} • ${percentDisplay} of components (${node.count})</div>
|
|
668
|
+
`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
tooltip.innerHTML = content;
|
|
672
|
+
tooltip.style.left = (mouseX + 15) + 'px';
|
|
673
|
+
tooltip.style.top = (mouseY - 10) + 'px';
|
|
674
|
+
tooltip.classList.add('show');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Node class
|
|
678
|
+
class Node {
|
|
679
|
+
constructor(name, type, branch, index) {
|
|
680
|
+
this.name = name;
|
|
681
|
+
this.type = type;
|
|
682
|
+
this.branch = branch;
|
|
683
|
+
this.index = index;
|
|
684
|
+
this.scale = 0;
|
|
685
|
+
this.targetScale = 1;
|
|
686
|
+
this.pulse = 0;
|
|
687
|
+
this.usageCount = 0;
|
|
688
|
+
|
|
689
|
+
// Position based on branch
|
|
690
|
+
const branchData = branches[branch];
|
|
691
|
+
const radius = 150 + (index * 30);
|
|
692
|
+
const angleOffset = (index * 0.3) - (branchData.nodes.length * 0.15);
|
|
693
|
+
|
|
694
|
+
this.x = centerNode.x + Math.cos(branchData.angle + angleOffset) * radius;
|
|
695
|
+
this.y = centerNode.y + Math.sin(branchData.angle + angleOffset) * radius;
|
|
696
|
+
|
|
697
|
+
this.color = branchData.color;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
update() {
|
|
701
|
+
// Smooth scale growth
|
|
702
|
+
this.scale += (this.targetScale - this.scale) * 0.1;
|
|
703
|
+
|
|
704
|
+
// Pulse decay
|
|
705
|
+
if (this.pulse > 0) {
|
|
706
|
+
this.pulse -= 0.05;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Gentle floating
|
|
710
|
+
const time = Date.now() * 0.001;
|
|
711
|
+
this.floatY = Math.sin(time + this.index) * 3;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
draw(ctx) {
|
|
715
|
+
const x = this.x;
|
|
716
|
+
const y = this.y + (this.floatY || 0);
|
|
717
|
+
const size = 8 * this.scale;
|
|
718
|
+
|
|
719
|
+
// Glow
|
|
720
|
+
const glowSize = size + this.pulse * 10;
|
|
721
|
+
ctx.save();
|
|
722
|
+
ctx.globalAlpha = 0.3 + this.pulse * 0.4;
|
|
723
|
+
ctx.fillStyle = this.color;
|
|
724
|
+
ctx.shadowBlur = 15 + this.pulse * 15;
|
|
725
|
+
ctx.shadowColor = this.color;
|
|
726
|
+
ctx.beginPath();
|
|
727
|
+
ctx.arc(x, y, glowSize, 0, Math.PI * 2);
|
|
728
|
+
ctx.fill();
|
|
729
|
+
ctx.restore();
|
|
730
|
+
|
|
731
|
+
// Node
|
|
732
|
+
ctx.fillStyle = this.color;
|
|
733
|
+
ctx.beginPath();
|
|
734
|
+
ctx.arc(x, y, size, 0, Math.PI * 2);
|
|
735
|
+
ctx.fill();
|
|
736
|
+
|
|
737
|
+
// Label
|
|
738
|
+
if (this.scale > 0.5) {
|
|
739
|
+
ctx.save();
|
|
740
|
+
ctx.font = '11px Monaco';
|
|
741
|
+
ctx.fillStyle = '#e0e0e0';
|
|
742
|
+
ctx.textAlign = 'center';
|
|
743
|
+
ctx.textBaseline = 'top';
|
|
744
|
+
ctx.fillText(this.name, x, y + size + 5);
|
|
745
|
+
ctx.restore();
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Connection line to center
|
|
749
|
+
if (this.scale > 0.3) {
|
|
750
|
+
ctx.save();
|
|
751
|
+
ctx.strokeStyle = this.color;
|
|
752
|
+
ctx.globalAlpha = 0.2 * this.scale;
|
|
753
|
+
ctx.lineWidth = 1;
|
|
754
|
+
ctx.beginPath();
|
|
755
|
+
ctx.moveTo(centerNode.x, centerNode.y);
|
|
756
|
+
ctx.lineTo(x, y);
|
|
757
|
+
ctx.stroke();
|
|
758
|
+
ctx.restore();
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
activate() {
|
|
763
|
+
this.pulse = 1;
|
|
764
|
+
this.usageCount++;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ModelNode class (pie slice in center for each model)
|
|
769
|
+
class ModelNode {
|
|
770
|
+
constructor(modelName, color, index, total) {
|
|
771
|
+
this.modelName = modelName;
|
|
772
|
+
this.displayName = this.formatModelName(modelName);
|
|
773
|
+
this.color = color;
|
|
774
|
+
this.count = 0; // Number of conversations using this model
|
|
775
|
+
this.percentage = 0; // Percentage of total model activity
|
|
776
|
+
this.size = 0;
|
|
777
|
+
this.targetSize = 50; // Base size, will grow with usage
|
|
778
|
+
this.alpha = 0;
|
|
779
|
+
this.targetAlpha = 1;
|
|
780
|
+
this.pulsePhase = Math.random() * Math.PI * 2;
|
|
781
|
+
|
|
782
|
+
// Position in pie (angle range)
|
|
783
|
+
this.index = index;
|
|
784
|
+
this.total = total;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
formatModelName(modelName) {
|
|
788
|
+
const nameMap = {
|
|
789
|
+
'claude-sonnet-4-5-20250929': 'Sonnet 4.5',
|
|
790
|
+
'claude-haiku-4-5-20251001': 'Haiku 4.5',
|
|
791
|
+
'claude-3-5-sonnet': 'Sonnet 3.5',
|
|
792
|
+
'claude-3-opus': 'Opus 3',
|
|
793
|
+
'claude-3-haiku': 'Haiku 3',
|
|
794
|
+
'Unknown': 'Unknown'
|
|
795
|
+
};
|
|
796
|
+
return nameMap[modelName] || modelName;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
updatePercentage(totalModelActivity) {
|
|
800
|
+
this.percentage = totalModelActivity > 0 ? (this.count / totalModelActivity) * 100 : 0;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
addUse() {
|
|
804
|
+
this.count++;
|
|
805
|
+
// Grow size with each use (cap at 100)
|
|
806
|
+
this.targetSize = Math.min(50 + this.count * 3, 100);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
updatePosition(index, total) {
|
|
810
|
+
this.index = index;
|
|
811
|
+
this.total = total;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
update() {
|
|
815
|
+
// Smooth size transition
|
|
816
|
+
if (Math.abs(this.size - this.targetSize) > 0.5) {
|
|
817
|
+
this.size += (this.targetSize - this.size) * 0.1;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Fade in
|
|
821
|
+
if (this.alpha < this.targetAlpha) {
|
|
822
|
+
this.alpha += 0.05;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
this.pulsePhase += 0.05;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
draw(ctx) {
|
|
829
|
+
if (this.size < 1) return;
|
|
830
|
+
|
|
831
|
+
const pulse = Math.sin(this.pulsePhase) * 0.15 + 0.85;
|
|
832
|
+
const startAngle = (this.index / this.total) * Math.PI * 2 - Math.PI / 2;
|
|
833
|
+
const endAngle = ((this.index + 1) / this.total) * Math.PI * 2 - Math.PI / 2;
|
|
834
|
+
|
|
835
|
+
// Draw pie slice with glow
|
|
836
|
+
ctx.save();
|
|
837
|
+
ctx.globalAlpha = this.alpha * 0.4 * pulse;
|
|
838
|
+
ctx.fillStyle = this.color;
|
|
839
|
+
ctx.shadowBlur = 30;
|
|
840
|
+
ctx.shadowColor = this.color;
|
|
841
|
+
ctx.beginPath();
|
|
842
|
+
ctx.moveTo(centerNode.x, centerNode.y);
|
|
843
|
+
ctx.arc(centerNode.x, centerNode.y, this.size + 10, startAngle, endAngle);
|
|
844
|
+
ctx.closePath();
|
|
845
|
+
ctx.fill();
|
|
846
|
+
ctx.restore();
|
|
847
|
+
|
|
848
|
+
// Draw main pie slice
|
|
849
|
+
ctx.save();
|
|
850
|
+
ctx.globalAlpha = this.alpha;
|
|
851
|
+
ctx.fillStyle = this.color;
|
|
852
|
+
ctx.shadowBlur = 15;
|
|
853
|
+
ctx.shadowColor = this.color;
|
|
854
|
+
ctx.beginPath();
|
|
855
|
+
ctx.moveTo(centerNode.x, centerNode.y);
|
|
856
|
+
ctx.arc(centerNode.x, centerNode.y, this.size, startAngle, endAngle);
|
|
857
|
+
ctx.closePath();
|
|
858
|
+
ctx.fill();
|
|
859
|
+
ctx.restore();
|
|
860
|
+
|
|
861
|
+
// Draw label (if slice is large enough)
|
|
862
|
+
if (this.size > 40 && this.total <= 3) {
|
|
863
|
+
const midAngle = (startAngle + endAngle) / 2;
|
|
864
|
+
const labelRadius = this.size * 0.6;
|
|
865
|
+
const labelX = centerNode.x + Math.cos(midAngle) * labelRadius;
|
|
866
|
+
const labelY = centerNode.y + Math.sin(midAngle) * labelRadius;
|
|
867
|
+
|
|
868
|
+
ctx.save();
|
|
869
|
+
ctx.globalAlpha = this.alpha;
|
|
870
|
+
ctx.font = 'bold 12px Monaco';
|
|
871
|
+
ctx.fillStyle = '#ffffff';
|
|
872
|
+
ctx.textAlign = 'center';
|
|
873
|
+
ctx.textBaseline = 'middle';
|
|
874
|
+
ctx.fillText(this.displayName, labelX, labelY);
|
|
875
|
+
ctx.restore();
|
|
876
|
+
|
|
877
|
+
// Count badge
|
|
878
|
+
if (this.count > 1) {
|
|
879
|
+
ctx.save();
|
|
880
|
+
ctx.globalAlpha = this.alpha * 0.8;
|
|
881
|
+
ctx.font = 'bold 10px Monaco';
|
|
882
|
+
ctx.fillStyle = '#ffffff';
|
|
883
|
+
ctx.textAlign = 'center';
|
|
884
|
+
ctx.textBaseline = 'middle';
|
|
885
|
+
ctx.fillText(this.count, labelX, labelY + 15);
|
|
886
|
+
ctx.restore();
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Draw outer label (if too many models)
|
|
891
|
+
if (this.total > 3 || this.size <= 40) {
|
|
892
|
+
const midAngle = (startAngle + endAngle) / 2;
|
|
893
|
+
const labelRadius = this.size + 20;
|
|
894
|
+
const labelX = centerNode.x + Math.cos(midAngle) * labelRadius;
|
|
895
|
+
const labelY = centerNode.y + Math.sin(midAngle) * labelRadius;
|
|
896
|
+
|
|
897
|
+
ctx.save();
|
|
898
|
+
ctx.globalAlpha = this.alpha;
|
|
899
|
+
ctx.font = 'bold 11px Monaco';
|
|
900
|
+
ctx.fillStyle = this.color;
|
|
901
|
+
ctx.textAlign = 'center';
|
|
902
|
+
ctx.textBaseline = 'middle';
|
|
903
|
+
ctx.fillText(this.displayName, labelX, labelY);
|
|
904
|
+
|
|
905
|
+
if (this.count > 1) {
|
|
906
|
+
ctx.font = 'bold 9px Monaco';
|
|
907
|
+
ctx.fillText(`(${this.count})`, labelX, labelY + 12);
|
|
908
|
+
}
|
|
909
|
+
ctx.restore();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// ToolNode class (persistent growing node for each tool type)
|
|
915
|
+
class ToolNode {
|
|
916
|
+
constructor(toolName, color, index, total) {
|
|
917
|
+
this.toolName = toolName;
|
|
918
|
+
this.color = color;
|
|
919
|
+
this.count = 0; // Number of times this tool was used
|
|
920
|
+
this.percentage = 0; // Percentage of total tool calls
|
|
921
|
+
this.size = 0;
|
|
922
|
+
this.targetSize = 10; // Will grow with each use
|
|
923
|
+
this.alpha = 0;
|
|
924
|
+
this.targetAlpha = 1;
|
|
925
|
+
this.pulsePhase = Math.random() * Math.PI * 2;
|
|
926
|
+
|
|
927
|
+
// Random angle for this node (stays fixed)
|
|
928
|
+
this.randomAngle = Math.random() * Math.PI * 2;
|
|
929
|
+
// Small random offset for organic feel
|
|
930
|
+
this.randomOffset = {
|
|
931
|
+
x: (Math.random() - 0.5) * 40,
|
|
932
|
+
y: (Math.random() - 0.5) * 40
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
// Initial position (will be updated based on percentage)
|
|
936
|
+
const radius = 350; // Start far, will move closer based on %
|
|
937
|
+
this.x = centerNode.x + Math.cos(this.randomAngle) * radius + this.randomOffset.x;
|
|
938
|
+
this.y = centerNode.y + Math.sin(this.randomAngle) * radius + this.randomOffset.y;
|
|
939
|
+
this.targetX = this.x;
|
|
940
|
+
this.targetY = this.y;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
addUse() {
|
|
944
|
+
this.count++;
|
|
945
|
+
// Grow size with each use (cap at 40)
|
|
946
|
+
this.targetSize = Math.min(10 + this.count * 2, 40);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
updatePercentage(totalToolCalls) {
|
|
950
|
+
this.percentage = totalToolCalls > 0 ? (this.count / totalToolCalls) * 100 : 0;
|
|
951
|
+
// Recalculate position based on percentage
|
|
952
|
+
// Higher percentage = closer to center (min 150px, max 350px)
|
|
953
|
+
const minRadius = 150;
|
|
954
|
+
const maxRadius = 350;
|
|
955
|
+
// Invert: high % -> low radius (closer to center)
|
|
956
|
+
const radius = maxRadius - (this.percentage / 100) * (maxRadius - minRadius) * 2;
|
|
957
|
+
const clampedRadius = Math.max(minRadius, Math.min(maxRadius, radius));
|
|
958
|
+
|
|
959
|
+
this.targetX = centerNode.x + Math.cos(this.randomAngle) * clampedRadius + this.randomOffset.x;
|
|
960
|
+
this.targetY = centerNode.y + Math.sin(this.randomAngle) * clampedRadius + this.randomOffset.y;
|
|
961
|
+
|
|
962
|
+
// Also scale size based on percentage
|
|
963
|
+
this.targetSize = Math.min(15 + this.percentage * 1.5, 50);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
update() {
|
|
967
|
+
// Smooth size growth
|
|
968
|
+
if (this.size < this.targetSize) {
|
|
969
|
+
this.size += (this.targetSize - this.size) * 0.1;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Fade in
|
|
973
|
+
if (this.alpha < this.targetAlpha) {
|
|
974
|
+
this.alpha += 0.05;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
this.pulsePhase += 0.05;
|
|
978
|
+
|
|
979
|
+
// Smooth position transition
|
|
980
|
+
this.x += (this.targetX - this.x) * 0.05;
|
|
981
|
+
this.y += (this.targetY - this.y) * 0.05;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
updatePosition(index, total) {
|
|
985
|
+
// Keep for compatibility, but position is now based on percentage
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
draw(ctx) {
|
|
989
|
+
if (this.size < 1) return;
|
|
990
|
+
|
|
991
|
+
const pulse = Math.sin(this.pulsePhase) * 0.2 + 0.8;
|
|
992
|
+
|
|
993
|
+
// Glow
|
|
994
|
+
ctx.save();
|
|
995
|
+
ctx.globalAlpha = this.alpha * 0.3 * pulse;
|
|
996
|
+
ctx.fillStyle = this.color;
|
|
997
|
+
ctx.shadowBlur = 20;
|
|
998
|
+
ctx.shadowColor = this.color;
|
|
999
|
+
ctx.beginPath();
|
|
1000
|
+
ctx.arc(this.x, this.y, this.size + 5, 0, Math.PI * 2);
|
|
1001
|
+
ctx.fill();
|
|
1002
|
+
ctx.restore();
|
|
1003
|
+
|
|
1004
|
+
// Node
|
|
1005
|
+
ctx.save();
|
|
1006
|
+
ctx.globalAlpha = this.alpha;
|
|
1007
|
+
ctx.fillStyle = this.color;
|
|
1008
|
+
ctx.shadowBlur = 10;
|
|
1009
|
+
ctx.shadowColor = this.color;
|
|
1010
|
+
ctx.beginPath();
|
|
1011
|
+
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
1012
|
+
ctx.fill();
|
|
1013
|
+
ctx.restore();
|
|
1014
|
+
|
|
1015
|
+
// Label (always visible once node appears)
|
|
1016
|
+
if (this.size > 5) {
|
|
1017
|
+
ctx.save();
|
|
1018
|
+
ctx.globalAlpha = this.alpha;
|
|
1019
|
+
ctx.font = 'bold 11px Monaco';
|
|
1020
|
+
ctx.fillStyle = '#e0e0e0';
|
|
1021
|
+
ctx.textAlign = 'center';
|
|
1022
|
+
ctx.textBaseline = 'top';
|
|
1023
|
+
ctx.fillText(this.toolName, this.x, this.y + this.size + 5);
|
|
1024
|
+
ctx.restore();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Percentage badge (show when percentage > 0)
|
|
1028
|
+
if (this.percentage > 0 && this.size > 5) {
|
|
1029
|
+
ctx.save();
|
|
1030
|
+
ctx.globalAlpha = this.alpha;
|
|
1031
|
+
ctx.font = 'bold 10px Monaco';
|
|
1032
|
+
ctx.fillStyle = '#ffffff';
|
|
1033
|
+
ctx.textAlign = 'center';
|
|
1034
|
+
ctx.textBaseline = 'middle';
|
|
1035
|
+
const displayPercent = this.percentage >= 1 ?
|
|
1036
|
+
Math.round(this.percentage) + '%' :
|
|
1037
|
+
this.percentage.toFixed(1) + '%';
|
|
1038
|
+
ctx.fillText(displayPercent, this.x, this.y);
|
|
1039
|
+
ctx.restore();
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ComponentNode class (second layer: commands, skills, MCPs, subagents)
|
|
1045
|
+
class ComponentNode {
|
|
1046
|
+
constructor(name, type, color, index, total) {
|
|
1047
|
+
this.name = name;
|
|
1048
|
+
this.type = type; // 'command', 'skill', 'mcp', 'subagent'
|
|
1049
|
+
this.color = color;
|
|
1050
|
+
this.count = 0;
|
|
1051
|
+
this.percentage = 0; // Percentage within its type category
|
|
1052
|
+
this.size = 0;
|
|
1053
|
+
this.targetSize = 8; // Smaller base size for second layer
|
|
1054
|
+
this.alpha = 0;
|
|
1055
|
+
this.targetAlpha = 1;
|
|
1056
|
+
this.pulsePhase = Math.random() * Math.PI * 2;
|
|
1057
|
+
|
|
1058
|
+
// Random angle for this node (stays fixed)
|
|
1059
|
+
this.randomAngle = Math.random() * Math.PI * 2;
|
|
1060
|
+
// Small random offset for organic feel
|
|
1061
|
+
this.randomOffset = {
|
|
1062
|
+
x: (Math.random() - 0.5) * 30,
|
|
1063
|
+
y: (Math.random() - 0.5) * 30
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
// Position in circle around center (farther than tools)
|
|
1067
|
+
this.index = index;
|
|
1068
|
+
this.total = total;
|
|
1069
|
+
const radius = 450; // Start far, will move closer based on %
|
|
1070
|
+
this.x = centerNode.x + Math.cos(this.randomAngle) * radius + this.randomOffset.x;
|
|
1071
|
+
this.y = centerNode.y + Math.sin(this.randomAngle) * radius + this.randomOffset.y;
|
|
1072
|
+
this.targetX = this.x;
|
|
1073
|
+
this.targetY = this.y;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
addUse() {
|
|
1077
|
+
this.count++;
|
|
1078
|
+
// Grow size with each use (smaller max than tools)
|
|
1079
|
+
this.targetSize = Math.min(8 + this.count * 1.5, 30);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
updatePercentage(totalInCategory) {
|
|
1083
|
+
this.percentage = totalInCategory > 0 ? (this.count / totalInCategory) * 100 : 0;
|
|
1084
|
+
// Recalculate position based on percentage
|
|
1085
|
+
// Higher percentage = closer to center (min 250px, max 450px)
|
|
1086
|
+
const minRadius = 250;
|
|
1087
|
+
const maxRadius = 450;
|
|
1088
|
+
// Invert: high % -> low radius (closer to center)
|
|
1089
|
+
const radius = maxRadius - (this.percentage / 100) * (maxRadius - minRadius) * 2;
|
|
1090
|
+
const clampedRadius = Math.max(minRadius, Math.min(maxRadius, radius));
|
|
1091
|
+
|
|
1092
|
+
this.targetX = centerNode.x + Math.cos(this.randomAngle) * clampedRadius + this.randomOffset.x;
|
|
1093
|
+
this.targetY = centerNode.y + Math.sin(this.randomAngle) * clampedRadius + this.randomOffset.y;
|
|
1094
|
+
|
|
1095
|
+
// Also scale size based on percentage
|
|
1096
|
+
this.targetSize = Math.min(10 + this.percentage * 1.2, 40);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
updatePosition(index, total) {
|
|
1100
|
+
// Keep for compatibility, but position is now based on percentage
|
|
1101
|
+
this.index = index;
|
|
1102
|
+
this.total = total;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
update() {
|
|
1106
|
+
// Smooth transitions
|
|
1107
|
+
if (Math.abs(this.size - this.targetSize) > 0.5) {
|
|
1108
|
+
this.size += (this.targetSize - this.size) * 0.1;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (this.alpha < this.targetAlpha) {
|
|
1112
|
+
this.alpha += 0.05;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
this.pulsePhase += 0.05;
|
|
1116
|
+
|
|
1117
|
+
// Smooth position transition
|
|
1118
|
+
this.x += (this.targetX - this.x) * 0.05;
|
|
1119
|
+
this.y += (this.targetY - this.y) * 0.05;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
draw(ctx) {
|
|
1123
|
+
if (this.size < 1) return;
|
|
1124
|
+
|
|
1125
|
+
const pulse = Math.sin(this.pulsePhase) * 0.2 + 0.8;
|
|
1126
|
+
|
|
1127
|
+
// Glow
|
|
1128
|
+
ctx.save();
|
|
1129
|
+
ctx.globalAlpha = this.alpha * 0.3 * pulse;
|
|
1130
|
+
ctx.fillStyle = this.color;
|
|
1131
|
+
ctx.shadowBlur = 15;
|
|
1132
|
+
ctx.shadowColor = this.color;
|
|
1133
|
+
ctx.beginPath();
|
|
1134
|
+
ctx.arc(this.x, this.y, this.size + 3, 0, Math.PI * 2);
|
|
1135
|
+
ctx.fill();
|
|
1136
|
+
ctx.restore();
|
|
1137
|
+
|
|
1138
|
+
// Node
|
|
1139
|
+
ctx.save();
|
|
1140
|
+
ctx.globalAlpha = this.alpha;
|
|
1141
|
+
ctx.fillStyle = this.color;
|
|
1142
|
+
ctx.shadowBlur = 8;
|
|
1143
|
+
ctx.shadowColor = this.color;
|
|
1144
|
+
ctx.beginPath();
|
|
1145
|
+
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
|
1146
|
+
ctx.fill();
|
|
1147
|
+
ctx.restore();
|
|
1148
|
+
|
|
1149
|
+
// Label (show for larger nodes)
|
|
1150
|
+
if (this.size > 5) {
|
|
1151
|
+
ctx.save();
|
|
1152
|
+
ctx.globalAlpha = this.alpha;
|
|
1153
|
+
ctx.font = 'bold 9px Monaco';
|
|
1154
|
+
ctx.fillStyle = '#e0e0e0';
|
|
1155
|
+
ctx.textAlign = 'center';
|
|
1156
|
+
ctx.textBaseline = 'top';
|
|
1157
|
+
// Clean up name (remove slashes and long paths)
|
|
1158
|
+
const displayName = this.name.startsWith('/') ? this.name : this.name.split('@')[0];
|
|
1159
|
+
ctx.fillText(displayName, this.x, this.y + this.size + 3);
|
|
1160
|
+
ctx.restore();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Percentage badge (show when percentage > 0)
|
|
1164
|
+
if (this.percentage > 0 && this.size > 5) {
|
|
1165
|
+
ctx.save();
|
|
1166
|
+
ctx.globalAlpha = this.alpha;
|
|
1167
|
+
ctx.font = 'bold 8px Monaco';
|
|
1168
|
+
ctx.fillStyle = '#ffffff';
|
|
1169
|
+
ctx.textAlign = 'center';
|
|
1170
|
+
ctx.textBaseline = 'middle';
|
|
1171
|
+
const displayPercent = this.percentage >= 1 ?
|
|
1172
|
+
Math.round(this.percentage) + '%' :
|
|
1173
|
+
this.percentage.toFixed(1) + '%';
|
|
1174
|
+
ctx.fillText(displayPercent, this.x, this.y);
|
|
1175
|
+
ctx.restore();
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Beam class (tool call visualization)
|
|
1181
|
+
class Beam {
|
|
1182
|
+
constructor(targetNode, toolName, isPermanent = false) {
|
|
1183
|
+
this.targetNode = targetNode;
|
|
1184
|
+
this.toolName = toolName;
|
|
1185
|
+
this.progress = isPermanent ? 1 : 0; // Permanent starts complete
|
|
1186
|
+
this.speed = 0.02; // Slower for better visibility
|
|
1187
|
+
this.color = '#3b82f6';
|
|
1188
|
+
this.pointCreated = isPermanent; // Permanent already counted
|
|
1189
|
+
this.isComplete = isPermanent;
|
|
1190
|
+
this.shouldDie = false; // Flag to remove animating beam
|
|
1191
|
+
this.pulsePhase = Math.random() * Math.PI * 2; // For subtle animation
|
|
1192
|
+
|
|
1193
|
+
// Random color based on tool type
|
|
1194
|
+
const toolColors = {
|
|
1195
|
+
'Read': '#60a5fa',
|
|
1196
|
+
'Write': '#34d399',
|
|
1197
|
+
'Edit': '#fbbf24',
|
|
1198
|
+
'Bash': '#f87171',
|
|
1199
|
+
'TodoWrite': '#a78bfa',
|
|
1200
|
+
'Task': '#fb923c',
|
|
1201
|
+
'Glob': '#2dd4bf',
|
|
1202
|
+
'Grep': '#c084fc'
|
|
1203
|
+
};
|
|
1204
|
+
this.color = toolColors[toolName] || '#3b82f6';
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
update() {
|
|
1208
|
+
if (!this.isComplete) {
|
|
1209
|
+
this.progress += this.speed;
|
|
1210
|
+
|
|
1211
|
+
// When beam reaches destination
|
|
1212
|
+
if (this.progress >= 1 && !this.pointCreated) {
|
|
1213
|
+
this.pointCreated = true;
|
|
1214
|
+
this.isComplete = true;
|
|
1215
|
+
|
|
1216
|
+
// Increment the tool node's usage count
|
|
1217
|
+
if (this.targetNode.addUse) {
|
|
1218
|
+
this.targetNode.addUse();
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
stats.components++;
|
|
1222
|
+
|
|
1223
|
+
// Create or update permanent connection for this node
|
|
1224
|
+
const nodeId = this.targetNode.toolName || this.targetNode.name || 'unknown';
|
|
1225
|
+
permanentConnections.set(nodeId, new Beam(this.targetNode, this.toolName, true));
|
|
1226
|
+
|
|
1227
|
+
// Mark this animating beam to be removed
|
|
1228
|
+
this.shouldDie = true;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Subtle pulse animation for permanent connections
|
|
1233
|
+
this.pulsePhase += 0.02;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
draw(ctx) {
|
|
1237
|
+
const startX = centerNode.x;
|
|
1238
|
+
const startY = centerNode.y;
|
|
1239
|
+
const endX = this.targetNode.x;
|
|
1240
|
+
const endY = this.targetNode.y + (this.targetNode.floatY || 0);
|
|
1241
|
+
|
|
1242
|
+
const currentX = startX + (endX - startX) * Math.min(this.progress, 1);
|
|
1243
|
+
const currentY = startY + (endY - startY) * Math.min(this.progress, 1);
|
|
1244
|
+
|
|
1245
|
+
// Calculate opacity based on state
|
|
1246
|
+
let opacity;
|
|
1247
|
+
if (this.isComplete) {
|
|
1248
|
+
// Permanent connection: subtle pulse effect
|
|
1249
|
+
const pulse = Math.sin(this.pulsePhase) * 0.1 + 0.25;
|
|
1250
|
+
opacity = pulse;
|
|
1251
|
+
} else {
|
|
1252
|
+
// Animating beam: full opacity
|
|
1253
|
+
opacity = 0.8;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// Beam line
|
|
1257
|
+
ctx.save();
|
|
1258
|
+
ctx.strokeStyle = this.color;
|
|
1259
|
+
ctx.globalAlpha = opacity;
|
|
1260
|
+
ctx.lineWidth = this.isComplete ? 1 : 2;
|
|
1261
|
+
ctx.shadowBlur = this.isComplete ? 5 : 10;
|
|
1262
|
+
ctx.shadowColor = this.color;
|
|
1263
|
+
ctx.beginPath();
|
|
1264
|
+
ctx.moveTo(startX, startY);
|
|
1265
|
+
ctx.lineTo(currentX, currentY);
|
|
1266
|
+
ctx.stroke();
|
|
1267
|
+
ctx.restore();
|
|
1268
|
+
|
|
1269
|
+
// Tool label (only during animation)
|
|
1270
|
+
if (!this.isComplete && this.progress > 0.3 && this.progress < 0.9) {
|
|
1271
|
+
ctx.save();
|
|
1272
|
+
ctx.font = '10px Monaco';
|
|
1273
|
+
ctx.fillStyle = this.color;
|
|
1274
|
+
ctx.globalAlpha = 0.8;
|
|
1275
|
+
ctx.textAlign = 'center';
|
|
1276
|
+
ctx.fillText(this.toolName, currentX, currentY - 10);
|
|
1277
|
+
ctx.restore();
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Particle at tip (only during animation)
|
|
1281
|
+
if (!this.isComplete) {
|
|
1282
|
+
ctx.save();
|
|
1283
|
+
ctx.fillStyle = this.color;
|
|
1284
|
+
ctx.globalAlpha = 0.8;
|
|
1285
|
+
ctx.shadowBlur = 15;
|
|
1286
|
+
ctx.shadowColor = this.color;
|
|
1287
|
+
ctx.beginPath();
|
|
1288
|
+
ctx.arc(currentX, currentY, 3, 0, Math.PI * 2);
|
|
1289
|
+
ctx.fill();
|
|
1290
|
+
ctx.restore();
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
isDead() {
|
|
1295
|
+
return this.shouldDie;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Add component node
|
|
1300
|
+
function addComponent(name, type) {
|
|
1301
|
+
const branchName = type === 'agent' ? 'agents' :
|
|
1302
|
+
type === 'mcp' ? 'mcps' :
|
|
1303
|
+
type === 'command' ? 'commands' : 'skills';
|
|
1304
|
+
|
|
1305
|
+
const branch = branches[branchName];
|
|
1306
|
+
const index = branch.nodes.length;
|
|
1307
|
+
const node = new Node(name, type, branchName, index);
|
|
1308
|
+
branch.nodes.push(node);
|
|
1309
|
+
|
|
1310
|
+
stats.components++;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// Get or create tool node
|
|
1314
|
+
function getOrCreateToolNode(toolName, color) {
|
|
1315
|
+
if (!toolNodes.has(toolName)) {
|
|
1316
|
+
// Create new tool node
|
|
1317
|
+
const index = toolNodes.size;
|
|
1318
|
+
const total = toolNodes.size + 1;
|
|
1319
|
+
const node = new ToolNode(toolName, color, index, total);
|
|
1320
|
+
toolNodes.set(toolName, node);
|
|
1321
|
+
|
|
1322
|
+
// Reposition all nodes to distribute evenly
|
|
1323
|
+
let i = 0;
|
|
1324
|
+
toolNodes.forEach((node, name) => {
|
|
1325
|
+
node.updatePosition(i, toolNodes.size);
|
|
1326
|
+
i++;
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
// Add to unique tools for legend
|
|
1330
|
+
if (!uniqueTools.has(toolName)) {
|
|
1331
|
+
uniqueTools.set(toolName, color);
|
|
1332
|
+
updateToolsList();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return toolNodes.get(toolName);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Get or create model node
|
|
1340
|
+
function getOrCreateModelNode(modelName) {
|
|
1341
|
+
if (!modelNodes.has(modelName)) {
|
|
1342
|
+
// Assign colors to models
|
|
1343
|
+
const modelColors = {
|
|
1344
|
+
'claude-sonnet-4-5-20250929': '#3b82f6', // Blue for Sonnet 4.5
|
|
1345
|
+
'claude-haiku-4-5-20251001': '#10b981', // Green for Haiku 4.5
|
|
1346
|
+
'claude-3-5-sonnet': '#8b5cf6', // Purple for Sonnet 3.5
|
|
1347
|
+
'claude-3-opus': '#f59e0b', // Orange for Opus 3
|
|
1348
|
+
'claude-3-haiku': '#14b8a6', // Teal for Haiku 3
|
|
1349
|
+
'Unknown': '#6b7280' // Gray for Unknown
|
|
1350
|
+
};
|
|
1351
|
+
const color = modelColors[modelName] || '#6b7280';
|
|
1352
|
+
|
|
1353
|
+
// Create new model node
|
|
1354
|
+
const index = modelNodes.size;
|
|
1355
|
+
const total = modelNodes.size + 1;
|
|
1356
|
+
const node = new ModelNode(modelName, color, index, total);
|
|
1357
|
+
modelNodes.set(modelName, node);
|
|
1358
|
+
|
|
1359
|
+
// Reposition all nodes to distribute evenly in pie
|
|
1360
|
+
let i = 0;
|
|
1361
|
+
modelNodes.forEach((node, name) => {
|
|
1362
|
+
node.updatePosition(i, modelNodes.size);
|
|
1363
|
+
i++;
|
|
1364
|
+
});
|
|
1365
|
+
|
|
1366
|
+
console.log(`🎨 New model detected: ${node.displayName} (${modelName})`);
|
|
1367
|
+
|
|
1368
|
+
// Update models list in legend
|
|
1369
|
+
updateModelsList();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
return modelNodes.get(modelName);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// Get or create component node (second layer)
|
|
1376
|
+
function getOrCreateComponentNode(name, type) {
|
|
1377
|
+
const key = `${type}:${name}`;
|
|
1378
|
+
|
|
1379
|
+
if (!componentNodes.has(key)) {
|
|
1380
|
+
// Assign colors by type
|
|
1381
|
+
const typeColors = {
|
|
1382
|
+
'command': '#ec4899', // Pink for commands
|
|
1383
|
+
'skill': '#8b5cf6', // Purple for skills
|
|
1384
|
+
'mcp': '#f59e0b', // Orange for MCPs
|
|
1385
|
+
'subagent': '#06b6d4' // Cyan for subagents
|
|
1386
|
+
};
|
|
1387
|
+
const color = typeColors[type] || '#6b7280';
|
|
1388
|
+
|
|
1389
|
+
// Create new component node
|
|
1390
|
+
const index = componentNodes.size;
|
|
1391
|
+
const total = componentNodes.size + 1;
|
|
1392
|
+
const node = new ComponentNode(name, type, color, index, total);
|
|
1393
|
+
componentNodes.set(key, node);
|
|
1394
|
+
|
|
1395
|
+
// Reposition all nodes to distribute evenly
|
|
1396
|
+
let i = 0;
|
|
1397
|
+
componentNodes.forEach((node, key) => {
|
|
1398
|
+
node.updatePosition(i, componentNodes.size);
|
|
1399
|
+
i++;
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
console.log(`🔷 New ${type} detected: ${name}`);
|
|
1403
|
+
|
|
1404
|
+
// Update components list
|
|
1405
|
+
updateComponentsList();
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return componentNodes.get(key);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Recalculate all tool percentages and update positions
|
|
1412
|
+
function recalculateToolPercentages() {
|
|
1413
|
+
// Calculate total tool calls
|
|
1414
|
+
let totalToolCalls = 0;
|
|
1415
|
+
toolNodes.forEach(node => {
|
|
1416
|
+
totalToolCalls += node.count;
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
// Update each tool node's percentage and position
|
|
1420
|
+
toolNodes.forEach(node => {
|
|
1421
|
+
node.updatePercentage(totalToolCalls);
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Recalculate all model percentages
|
|
1426
|
+
function recalculateModelPercentages() {
|
|
1427
|
+
// Calculate total model activity
|
|
1428
|
+
let totalModelActivity = 0;
|
|
1429
|
+
modelNodes.forEach(node => {
|
|
1430
|
+
totalModelActivity += node.count;
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
// Update each model node's percentage
|
|
1434
|
+
modelNodes.forEach(node => {
|
|
1435
|
+
node.updatePercentage(totalModelActivity);
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// Recalculate all component percentages and update positions
|
|
1440
|
+
function recalculateComponentPercentages() {
|
|
1441
|
+
// Calculate totals by type
|
|
1442
|
+
const typeTotals = {
|
|
1443
|
+
'command': 0,
|
|
1444
|
+
'skill': 0,
|
|
1445
|
+
'mcp': 0,
|
|
1446
|
+
'subagent': 0
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
componentNodes.forEach(node => {
|
|
1450
|
+
if (typeTotals[node.type] !== undefined) {
|
|
1451
|
+
typeTotals[node.type] += node.count;
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
// Calculate grand total for all components
|
|
1456
|
+
const grandTotal = Object.values(typeTotals).reduce((a, b) => a + b, 0);
|
|
1457
|
+
|
|
1458
|
+
// Update each component node's percentage and position
|
|
1459
|
+
// Using grand total so all components compete for center position
|
|
1460
|
+
componentNodes.forEach(node => {
|
|
1461
|
+
node.updatePercentage(grandTotal);
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// Add tool call beam
|
|
1466
|
+
function addToolBeam(toolName) {
|
|
1467
|
+
// Get color for this tool
|
|
1468
|
+
const toolColors = {
|
|
1469
|
+
'Read': '#60a5fa',
|
|
1470
|
+
'Write': '#34d399',
|
|
1471
|
+
'Edit': '#fbbf24',
|
|
1472
|
+
'Bash': '#f87171',
|
|
1473
|
+
'TodoWrite': '#a78bfa',
|
|
1474
|
+
'Task': '#fb923c',
|
|
1475
|
+
'Glob': '#2dd4bf',
|
|
1476
|
+
'Grep': '#c084fc',
|
|
1477
|
+
'WebFetch': '#f472b6',
|
|
1478
|
+
'WebSearch': '#818cf8',
|
|
1479
|
+
'KillShell': '#ef4444',
|
|
1480
|
+
'TaskOutput': '#06b6d4',
|
|
1481
|
+
'AskUserQuestion': '#fbbf24',
|
|
1482
|
+
'EnterPlanMode': '#a78bfa',
|
|
1483
|
+
'ExitPlanMode': '#8b5cf6'
|
|
1484
|
+
};
|
|
1485
|
+
const color = toolColors[toolName] || '#3b82f6';
|
|
1486
|
+
|
|
1487
|
+
// Get or create the tool node
|
|
1488
|
+
const targetNode = getOrCreateToolNode(toolName, color);
|
|
1489
|
+
|
|
1490
|
+
// Create beam to this tool node
|
|
1491
|
+
const beam = new Beam(targetNode, toolName);
|
|
1492
|
+
beams.push(beam);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
// Load data from API ONLY (no mock data)
|
|
1496
|
+
async function loadData() {
|
|
1497
|
+
try {
|
|
1498
|
+
console.log('🔄 Fetching real data from /api/2025...');
|
|
1499
|
+
const response = await fetch('/api/2025');
|
|
1500
|
+
|
|
1501
|
+
if (!response.ok) {
|
|
1502
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
const data = await response.json();
|
|
1506
|
+
console.log('✅ Data received:', {
|
|
1507
|
+
conversations: data.totalConversations,
|
|
1508
|
+
tools: data.toolsCount,
|
|
1509
|
+
components: data.componentInstalls?.length || 0,
|
|
1510
|
+
heatmapWeeks: data.activityHeatmap?.length || 0
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
// Verify we have real data
|
|
1514
|
+
const activeDays = [];
|
|
1515
|
+
if (data.activityHeatmap) {
|
|
1516
|
+
data.activityHeatmap.forEach(week => {
|
|
1517
|
+
week.forEach(day => {
|
|
1518
|
+
if (day.count > 0) {
|
|
1519
|
+
activeDays.push(day);
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
console.log(`📊 Active days found: ${activeDays.length}`);
|
|
1526
|
+
if (activeDays.length > 0) {
|
|
1527
|
+
console.log(`📅 First: ${activeDays[0].date}, Last: ${activeDays[activeDays.length - 1].date}`);
|
|
1528
|
+
console.log(`🔧 Sample tools:`, activeDays[0].tools);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
animationData = processData(data);
|
|
1532
|
+
|
|
1533
|
+
} catch (error) {
|
|
1534
|
+
console.error('❌ Error loading data:', error);
|
|
1535
|
+
document.getElementById('loading').innerHTML = `
|
|
1536
|
+
<p style="color: #ef4444;">Failed to load data</p>
|
|
1537
|
+
<p style="margin-top: 10px; font-size: 12px;">${error.message}</p>
|
|
1538
|
+
<p style="margin-top: 10px; font-size: 12px;">Make sure analytics server is running on port 3333</p>
|
|
1539
|
+
`;
|
|
1540
|
+
return; // Don't proceed without data
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
document.getElementById('loading').style.display = 'none';
|
|
1544
|
+
document.getElementById('introContent').style.display = 'block';
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// Process data into timeline
|
|
1548
|
+
function processData(data) {
|
|
1549
|
+
const timeline = [];
|
|
1550
|
+
|
|
1551
|
+
if (data.componentInstalls) {
|
|
1552
|
+
data.componentInstalls.forEach(install => {
|
|
1553
|
+
const date = new Date(install.date);
|
|
1554
|
+
timeline.push({
|
|
1555
|
+
type: 'component',
|
|
1556
|
+
componentType: install.type,
|
|
1557
|
+
name: install.name,
|
|
1558
|
+
date,
|
|
1559
|
+
dayOfYear: getDayOfYear(date)
|
|
1560
|
+
});
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (data.activityHeatmap) {
|
|
1565
|
+
data.activityHeatmap.forEach(week => {
|
|
1566
|
+
week.forEach(day => {
|
|
1567
|
+
if (day.count > 0) {
|
|
1568
|
+
const date = new Date(day.date);
|
|
1569
|
+
timeline.push({
|
|
1570
|
+
type: 'conversation',
|
|
1571
|
+
date,
|
|
1572
|
+
count: day.count,
|
|
1573
|
+
tools: day.tools || [],
|
|
1574
|
+
models: day.models || [],
|
|
1575
|
+
dayOfYear: getDayOfYear(date)
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
});
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Add commands, skills, MCPs, and subagents based on their actual timestamps
|
|
1583
|
+
console.log('🔍 DEBUG: Component layer events:');
|
|
1584
|
+
|
|
1585
|
+
if (data.commands && data.commands.events) {
|
|
1586
|
+
console.log(`📋 Adding ${data.commands.events.length} command events to timeline`);
|
|
1587
|
+
data.commands.events.forEach((event, index) => {
|
|
1588
|
+
const eventDate = new Date(event.timestamp);
|
|
1589
|
+
const eventDayOfYear = getDayOfYear(eventDate);
|
|
1590
|
+
timeline.push({
|
|
1591
|
+
type: 'component-layer2',
|
|
1592
|
+
componentType: 'command',
|
|
1593
|
+
name: event.name,
|
|
1594
|
+
date: eventDate,
|
|
1595
|
+
dayOfYear: eventDayOfYear
|
|
1596
|
+
});
|
|
1597
|
+
if (index === 0) {
|
|
1598
|
+
console.log(` - First command "${event.name}": date=${eventDate.toLocaleDateString()}, dayOfYear=${eventDayOfYear.toFixed(3)}`);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (data.skills && data.skills.events) {
|
|
1604
|
+
console.log(`📚 Adding ${data.skills.events.length} skill events to timeline`);
|
|
1605
|
+
data.skills.events.forEach((event, index) => {
|
|
1606
|
+
const eventDate = new Date(event.timestamp);
|
|
1607
|
+
const eventDayOfYear = getDayOfYear(eventDate);
|
|
1608
|
+
timeline.push({
|
|
1609
|
+
type: 'component-layer2',
|
|
1610
|
+
componentType: 'skill',
|
|
1611
|
+
name: event.name,
|
|
1612
|
+
date: eventDate,
|
|
1613
|
+
dayOfYear: eventDayOfYear
|
|
1614
|
+
});
|
|
1615
|
+
if (index === 0) {
|
|
1616
|
+
console.log(` - First skill "${event.name}": date=${eventDate.toLocaleDateString()}, dayOfYear=${eventDayOfYear.toFixed(3)}`);
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (data.mcps && data.mcps.events) {
|
|
1622
|
+
console.log(`🔌 Adding ${data.mcps.events.length} MCP events to timeline`);
|
|
1623
|
+
data.mcps.events.forEach((event, index) => {
|
|
1624
|
+
const eventDate = new Date(event.timestamp);
|
|
1625
|
+
const eventDayOfYear = getDayOfYear(eventDate);
|
|
1626
|
+
timeline.push({
|
|
1627
|
+
type: 'component-layer2',
|
|
1628
|
+
componentType: 'mcp',
|
|
1629
|
+
name: event.name,
|
|
1630
|
+
date: eventDate,
|
|
1631
|
+
dayOfYear: eventDayOfYear
|
|
1632
|
+
});
|
|
1633
|
+
if (index === 0) {
|
|
1634
|
+
console.log(` - First MCP "${event.name}": date=${eventDate.toLocaleDateString()}, dayOfYear=${eventDayOfYear.toFixed(3)}`);
|
|
1635
|
+
}
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
if (data.subagents && data.subagents.events) {
|
|
1640
|
+
console.log(`🤖 Adding ${data.subagents.events.length} subagent events to timeline`);
|
|
1641
|
+
data.subagents.events.forEach((event, index) => {
|
|
1642
|
+
const eventDate = new Date(event.timestamp);
|
|
1643
|
+
const eventDayOfYear = getDayOfYear(eventDate);
|
|
1644
|
+
timeline.push({
|
|
1645
|
+
type: 'component-layer2',
|
|
1646
|
+
componentType: 'subagent',
|
|
1647
|
+
name: event.name,
|
|
1648
|
+
date: eventDate,
|
|
1649
|
+
dayOfYear: eventDayOfYear
|
|
1650
|
+
});
|
|
1651
|
+
if (index === 0) {
|
|
1652
|
+
console.log(` - First subagent "${event.name}": date=${eventDate.toLocaleDateString()}, dayOfYear=${eventDayOfYear.toFixed(3)}`);
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
timeline.sort((a, b) => a.dayOfYear - b.dayOfYear);
|
|
1658
|
+
|
|
1659
|
+
console.log('🔍 DEBUG: First 10 events in sorted timeline:');
|
|
1660
|
+
timeline.slice(0, 10).forEach((event, i) => {
|
|
1661
|
+
console.log(` ${i}: type=${event.type}, name=${event.name || event.toolName}, dayOfYear=${event.dayOfYear?.toFixed(3)}, date=${event.date?.toLocaleDateString()}`);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
// Calculate actual activity range
|
|
1665
|
+
const firstActivityDay = timeline.length > 0 ? timeline[0].dayOfYear : 0;
|
|
1666
|
+
const lastActivityDay = timeline.length > 0 ? timeline[timeline.length - 1].dayOfYear : 365;
|
|
1667
|
+
const totalDays = lastActivityDay - firstActivityDay + 1;
|
|
1668
|
+
|
|
1669
|
+
const layer2Events = timeline.filter(e => e.type === 'component-layer2').length;
|
|
1670
|
+
console.log(`🔷 Total component-layer2 events in timeline: ${layer2Events}`);
|
|
1671
|
+
console.log(`📊 Activity range: Day ${firstActivityDay} to ${lastActivityDay} (${totalDays} days)`);
|
|
1672
|
+
console.log(`📅 First activity: ${timeline[0]?.date.toLocaleDateString()}`);
|
|
1673
|
+
console.log(`📅 Last activity: ${timeline[timeline.length - 1]?.date.toLocaleDateString()}`);
|
|
1674
|
+
|
|
1675
|
+
return {
|
|
1676
|
+
timeline,
|
|
1677
|
+
totalDays,
|
|
1678
|
+
firstActivityDay,
|
|
1679
|
+
lastActivityDay,
|
|
1680
|
+
startDate: timeline[0]?.date || new Date('2025-01-01')
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
function getDayOfYear(date) {
|
|
1685
|
+
const start = new Date(date.getFullYear(), 0, 0);
|
|
1686
|
+
const diff = date - start;
|
|
1687
|
+
const oneDay = 1000 * 60 * 60 * 24;
|
|
1688
|
+
return Math.floor(diff / oneDay);
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// Start animation
|
|
1692
|
+
function startAnimation() {
|
|
1693
|
+
console.log('🎬 STARTING ANIMATION');
|
|
1694
|
+
console.log('Timeline events:', animationData.timeline.length);
|
|
1695
|
+
console.log('Activity range: Day', animationData.firstActivityDay, 'to', animationData.lastActivityDay);
|
|
1696
|
+
console.log('Total days:', animationData.totalDays);
|
|
1697
|
+
console.log('First 5 events:', animationData.timeline.slice(0, 5));
|
|
1698
|
+
|
|
1699
|
+
document.getElementById('introScreen').style.display = 'none';
|
|
1700
|
+
isPlaying = true;
|
|
1701
|
+
startTime = Date.now();
|
|
1702
|
+
|
|
1703
|
+
// Update timeline labels based on actual data range
|
|
1704
|
+
if (animationData && animationData.startDate) {
|
|
1705
|
+
const startDate = new Date(animationData.startDate);
|
|
1706
|
+
const endDate = new Date(animationData.timeline[animationData.timeline.length - 1]?.date || startDate);
|
|
1707
|
+
|
|
1708
|
+
const labels = document.querySelector('.timeline-labels');
|
|
1709
|
+
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
1710
|
+
|
|
1711
|
+
// Show range in timeline
|
|
1712
|
+
const rangeText = `${monthNames[startDate.getMonth()]} ${startDate.getDate()} - ${monthNames[endDate.getMonth()]} ${endDate.getDate()}, 2025`;
|
|
1713
|
+
labels.innerHTML = `<span>${rangeText}</span>`;
|
|
1714
|
+
labels.style.justifyContent = 'center';
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
animate();
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// Animation loop
|
|
1721
|
+
function animate() {
|
|
1722
|
+
requestAnimationFrame(animate);
|
|
1723
|
+
|
|
1724
|
+
// Clear canvas (before transform)
|
|
1725
|
+
ctx.fillStyle = '#0a0a0f';
|
|
1726
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
1727
|
+
|
|
1728
|
+
if (isPlaying) {
|
|
1729
|
+
updateAnimationState();
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// Apply zoom and pan transformations
|
|
1733
|
+
ctx.save();
|
|
1734
|
+
ctx.translate(panX, panY);
|
|
1735
|
+
ctx.scale(zoom, zoom);
|
|
1736
|
+
|
|
1737
|
+
// Draw grid lines (subtle)
|
|
1738
|
+
ctx.save();
|
|
1739
|
+
ctx.strokeStyle = 'rgba(255,255,255,0.02)';
|
|
1740
|
+
ctx.lineWidth = 1 / zoom; // Keep grid lines same thickness regardless of zoom
|
|
1741
|
+
for (let i = -1000; i < canvas.width / zoom + 1000; i += 50) {
|
|
1742
|
+
ctx.beginPath();
|
|
1743
|
+
ctx.moveTo(i, -1000);
|
|
1744
|
+
ctx.lineTo(i, canvas.height / zoom + 1000);
|
|
1745
|
+
ctx.stroke();
|
|
1746
|
+
}
|
|
1747
|
+
for (let i = 0; i < canvas.height; i += 50) {
|
|
1748
|
+
ctx.beginPath();
|
|
1749
|
+
ctx.moveTo(0, i);
|
|
1750
|
+
ctx.lineTo(canvas.width, i);
|
|
1751
|
+
ctx.stroke();
|
|
1752
|
+
}
|
|
1753
|
+
ctx.restore();
|
|
1754
|
+
|
|
1755
|
+
// Update and draw permanent connections first (behind everything)
|
|
1756
|
+
permanentConnections.forEach(beam => {
|
|
1757
|
+
beam.update();
|
|
1758
|
+
beam.draw(ctx);
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
// Update and draw animating beams
|
|
1762
|
+
for (let i = beams.length - 1; i >= 0; i--) {
|
|
1763
|
+
beams[i].update();
|
|
1764
|
+
beams[i].draw(ctx);
|
|
1765
|
+
if (beams[i].isDead()) {
|
|
1766
|
+
beams.splice(i, 1);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// Update and draw tool nodes
|
|
1771
|
+
toolNodes.forEach(node => {
|
|
1772
|
+
node.update();
|
|
1773
|
+
node.draw(ctx);
|
|
1774
|
+
});
|
|
1775
|
+
|
|
1776
|
+
// Update and draw model nodes (pie slices in center)
|
|
1777
|
+
modelNodes.forEach(node => {
|
|
1778
|
+
node.update();
|
|
1779
|
+
node.draw(ctx);
|
|
1780
|
+
});
|
|
1781
|
+
|
|
1782
|
+
// Update and draw component nodes (second layer)
|
|
1783
|
+
componentNodes.forEach(node => {
|
|
1784
|
+
node.update();
|
|
1785
|
+
node.draw(ctx);
|
|
1786
|
+
});
|
|
1787
|
+
|
|
1788
|
+
// Update and draw all nodes
|
|
1789
|
+
Object.values(branches).forEach(branch => {
|
|
1790
|
+
branch.nodes.forEach(node => {
|
|
1791
|
+
node.update();
|
|
1792
|
+
node.draw(ctx);
|
|
1793
|
+
});
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
// Draw center node only if no models detected yet
|
|
1797
|
+
if (modelNodes.size === 0) {
|
|
1798
|
+
ctx.save();
|
|
1799
|
+
ctx.fillStyle = centerNode.color;
|
|
1800
|
+
ctx.shadowBlur = 30;
|
|
1801
|
+
ctx.shadowColor = centerNode.color;
|
|
1802
|
+
ctx.beginPath();
|
|
1803
|
+
ctx.arc(centerNode.x, centerNode.y, 35, 0, Math.PI * 2);
|
|
1804
|
+
ctx.fill();
|
|
1805
|
+
ctx.restore();
|
|
1806
|
+
|
|
1807
|
+
// Center label
|
|
1808
|
+
ctx.save();
|
|
1809
|
+
ctx.font = 'bold 16px Monaco';
|
|
1810
|
+
ctx.fillStyle = '#e0e0e0';
|
|
1811
|
+
ctx.textAlign = 'center';
|
|
1812
|
+
ctx.textBaseline = 'middle';
|
|
1813
|
+
ctx.fillText('Claude', centerNode.x, centerNode.y);
|
|
1814
|
+
ctx.restore();
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Restore zoom/pan transformation
|
|
1818
|
+
ctx.restore();
|
|
1819
|
+
|
|
1820
|
+
// Draw zoom indicator (outside transform so it stays in corner)
|
|
1821
|
+
if (zoom !== 1 || panX !== 0 || panY !== 0) {
|
|
1822
|
+
ctx.save();
|
|
1823
|
+
ctx.fillStyle = 'rgba(255,255,255,0.5)';
|
|
1824
|
+
ctx.font = '11px Monaco';
|
|
1825
|
+
ctx.textAlign = 'left';
|
|
1826
|
+
ctx.fillText(`Zoom: ${Math.round(zoom * 100)}% | Double-click to reset`, 20, canvas.height - 20);
|
|
1827
|
+
ctx.restore();
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// Update animation state
|
|
1832
|
+
function updateAnimationState() {
|
|
1833
|
+
if (!animationData) return;
|
|
1834
|
+
|
|
1835
|
+
const elapsed = (Date.now() - startTime) * speedMultiplier;
|
|
1836
|
+
const duration = 40000; // 40 seconds
|
|
1837
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
1838
|
+
|
|
1839
|
+
// Map progress to actual activity range
|
|
1840
|
+
const rangeStart = animationData.firstActivityDay || 0;
|
|
1841
|
+
const rangeEnd = animationData.lastActivityDay || 365;
|
|
1842
|
+
const rangeDuration = rangeEnd - rangeStart;
|
|
1843
|
+
|
|
1844
|
+
currentDayIndex = rangeStart + Math.floor(progress * rangeDuration);
|
|
1845
|
+
|
|
1846
|
+
document.getElementById('timelineProgress').style.width = `${progress * 100}%`;
|
|
1847
|
+
|
|
1848
|
+
// Use startDate from data
|
|
1849
|
+
const date = new Date(animationData.startDate);
|
|
1850
|
+
const dayOffset = currentDayIndex - rangeStart;
|
|
1851
|
+
date.setDate(date.getDate() + dayOffset);
|
|
1852
|
+
|
|
1853
|
+
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
1854
|
+
document.getElementById('currentDate').textContent = `${monthNames[date.getMonth()]} ${date.getDate()}`;
|
|
1855
|
+
|
|
1856
|
+
// Process events
|
|
1857
|
+
let eventsThisFrame = 0;
|
|
1858
|
+
animationData.timeline.forEach((event, index) => {
|
|
1859
|
+
if (event.dayOfYear <= currentDayIndex && !processedEvents.has(index)) {
|
|
1860
|
+
processedEvents.add(index);
|
|
1861
|
+
eventsThisFrame++;
|
|
1862
|
+
|
|
1863
|
+
if (event.type === 'conversation') {
|
|
1864
|
+
// Calculate actual tool count from toolCounts
|
|
1865
|
+
const actualToolCount = event.toolCounts ?
|
|
1866
|
+
Object.values(event.toolCounts).reduce((sum, count) => sum + count, 0) :
|
|
1867
|
+
event.tools.length * event.count;
|
|
1868
|
+
|
|
1869
|
+
console.log(`🎬 Processing conversation on day ${event.dayOfYear}: ${event.count} conversations, ${actualToolCount} tool calls`);
|
|
1870
|
+
stats.conversations += event.count;
|
|
1871
|
+
stats.tools += actualToolCount;
|
|
1872
|
+
|
|
1873
|
+
// Add tool beams using actual counts
|
|
1874
|
+
if (event.toolCounts) {
|
|
1875
|
+
Object.entries(event.toolCounts).forEach(([tool, count]) => {
|
|
1876
|
+
console.log(` 🔧 Adding ${count} beams for tool: ${tool}`);
|
|
1877
|
+
// Create multiple beams based on actual usage count
|
|
1878
|
+
for (let i = 0; i < count; i++) {
|
|
1879
|
+
setTimeout(() => addToolBeam(tool), Math.random() * 1000 + i * 50);
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
} else {
|
|
1883
|
+
// Fallback to old method if toolCounts not available
|
|
1884
|
+
event.tools.forEach(tool => {
|
|
1885
|
+
console.log(` 🔧 Adding tool beam: ${tool}`);
|
|
1886
|
+
setTimeout(() => addToolBeam(tool), Math.random() * 500);
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Process models using actual counts
|
|
1891
|
+
if (event.models && event.models.length > 0) {
|
|
1892
|
+
event.models.forEach(modelName => {
|
|
1893
|
+
// Skip Unknown models
|
|
1894
|
+
if (modelName === 'Unknown') return;
|
|
1895
|
+
|
|
1896
|
+
const modelNode = getOrCreateModelNode(modelName);
|
|
1897
|
+
|
|
1898
|
+
// Add actual usage count from modelCounts
|
|
1899
|
+
const modelCount = event.modelCounts ? (event.modelCounts[modelName] || 1) : 1;
|
|
1900
|
+
for (let i = 0; i < modelCount; i++) {
|
|
1901
|
+
modelNode.addUse();
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
console.log(` 🎨 Model used: ${modelNode.displayName} - added ${modelCount} uses (total: ${modelNode.count})`);
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
// Update models list after processing all models
|
|
1908
|
+
updateModelsList();
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const uniqueDays = new Set();
|
|
1912
|
+
animationData.timeline.forEach((e, i) => {
|
|
1913
|
+
if (processedEvents.has(i)) {
|
|
1914
|
+
uniqueDays.add(Math.floor(e.dayOfYear));
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
stats.days = uniqueDays.size;
|
|
1918
|
+
|
|
1919
|
+
} else if (event.type === 'component') {
|
|
1920
|
+
console.log(`📦 Processing component: ${event.name} (${event.componentType})`);
|
|
1921
|
+
addComponent(event.name, event.componentType);
|
|
1922
|
+
// showEvent(`Installed: ${event.name}`); // Disabled - notifications removed
|
|
1923
|
+
} else if (event.type === 'component-layer2') {
|
|
1924
|
+
const node = getOrCreateComponentNode(event.name, event.componentType);
|
|
1925
|
+
node.addUse();
|
|
1926
|
+
|
|
1927
|
+
// Create beam to this component node
|
|
1928
|
+
const beam = new Beam(node, event.name);
|
|
1929
|
+
beams.push(beam);
|
|
1930
|
+
|
|
1931
|
+
console.log(`🔷 ${event.componentType}: ${event.name} at ${event.date?.toLocaleDateString()} (count: ${node.count}, size: ${node.targetSize.toFixed(1)})`);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
if (eventsThisFrame > 0) {
|
|
1937
|
+
console.log(`✅ Processed ${eventsThisFrame} events this frame. Current day: ${currentDayIndex}`);
|
|
1938
|
+
|
|
1939
|
+
// Recalculate percentages and positions after processing events
|
|
1940
|
+
recalculateToolPercentages();
|
|
1941
|
+
recalculateComponentPercentages();
|
|
1942
|
+
recalculateModelPercentages();
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
// Update stats
|
|
1946
|
+
document.getElementById('statConversations').textContent = stats.conversations;
|
|
1947
|
+
document.getElementById('statComponents').textContent = stats.components;
|
|
1948
|
+
document.getElementById('statTools').textContent = stats.tools;
|
|
1949
|
+
document.getElementById('statDays').textContent = stats.days;
|
|
1950
|
+
|
|
1951
|
+
// Check milestones
|
|
1952
|
+
const milestones = [
|
|
1953
|
+
{ threshold: 10, msg: 'First 10 conversations! 🎯' },
|
|
1954
|
+
{ threshold: 50, msg: 'Half century! 🔥' },
|
|
1955
|
+
{ threshold: 100, msg: '100 conversations! 💯' },
|
|
1956
|
+
{ threshold: 500, msg: 'Power user! ⚡' }
|
|
1957
|
+
];
|
|
1958
|
+
|
|
1959
|
+
milestones.forEach(m => {
|
|
1960
|
+
if (stats.conversations >= m.threshold && !shownMilestones.has(m.threshold)) {
|
|
1961
|
+
shownMilestones.add(m.threshold);
|
|
1962
|
+
// showEvent(m.msg); // Disabled - notifications removed
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
if (progress >= 1) {
|
|
1967
|
+
isPlaying = false;
|
|
1968
|
+
// showEvent('🎉 2025 Complete!'); // Disabled - notifications removed
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
function showEvent(message) {
|
|
1973
|
+
const toast = document.getElementById('eventToast');
|
|
1974
|
+
toast.innerHTML = `<h3>${message}</h3>`;
|
|
1975
|
+
toast.className = 'event-toast show';
|
|
1976
|
+
setTimeout(() => toast.className = 'event-toast', 2500);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
// Update models list in the legend
|
|
1980
|
+
function updateModelsList() {
|
|
1981
|
+
const modelsList = document.getElementById('modelsList');
|
|
1982
|
+
modelsList.innerHTML = '';
|
|
1983
|
+
|
|
1984
|
+
// Get models with counts from modelNodes
|
|
1985
|
+
const modelsWithCounts = [];
|
|
1986
|
+
modelNodes.forEach((node, modelName) => {
|
|
1987
|
+
modelsWithCounts.push({
|
|
1988
|
+
name: node.displayName,
|
|
1989
|
+
rawName: modelName,
|
|
1990
|
+
color: node.color,
|
|
1991
|
+
count: node.count
|
|
1992
|
+
});
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1995
|
+
// Sort models by count (most used first)
|
|
1996
|
+
modelsWithCounts.sort((a, b) => b.count - a.count);
|
|
1997
|
+
|
|
1998
|
+
modelsWithCounts.forEach(model => {
|
|
1999
|
+
const item = document.createElement('div');
|
|
2000
|
+
item.className = 'legend-item';
|
|
2001
|
+
item.innerHTML = `
|
|
2002
|
+
<div class="legend-dot" style="background: ${model.color};"></div>
|
|
2003
|
+
<span>${model.name}</span>
|
|
2004
|
+
<span style="margin-left: auto; color: rgba(255,255,255,0.5); font-size: 11px;">${model.count}</span>
|
|
2005
|
+
`;
|
|
2006
|
+
modelsList.appendChild(item);
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// Update tools list in the legend
|
|
2011
|
+
function updateToolsList() {
|
|
2012
|
+
const toolsList = document.getElementById('toolsList');
|
|
2013
|
+
toolsList.innerHTML = '';
|
|
2014
|
+
|
|
2015
|
+
// Get tools with counts from toolNodes
|
|
2016
|
+
const toolsWithCounts = [];
|
|
2017
|
+
toolNodes.forEach((node, toolName) => {
|
|
2018
|
+
toolsWithCounts.push({
|
|
2019
|
+
name: toolName,
|
|
2020
|
+
color: uniqueTools.get(toolName) || '#888',
|
|
2021
|
+
count: node.count
|
|
2022
|
+
});
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
// Sort tools alphabetically
|
|
2026
|
+
toolsWithCounts.sort((a, b) => a.name.localeCompare(b.name));
|
|
2027
|
+
|
|
2028
|
+
toolsWithCounts.forEach(tool => {
|
|
2029
|
+
const item = document.createElement('div');
|
|
2030
|
+
item.className = 'legend-item';
|
|
2031
|
+
item.innerHTML = `
|
|
2032
|
+
<div class="legend-dot" style="background: ${tool.color};"></div>
|
|
2033
|
+
<span>${tool.name}</span>
|
|
2034
|
+
<span style="margin-left: auto; color: rgba(255,255,255,0.5); font-size: 11px;">${tool.count}</span>
|
|
2035
|
+
`;
|
|
2036
|
+
toolsList.appendChild(item);
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Update components list in the legend
|
|
2041
|
+
function updateComponentsList() {
|
|
2042
|
+
const componentsList = document.getElementById('componentsList');
|
|
2043
|
+
componentsList.innerHTML = '';
|
|
2044
|
+
|
|
2045
|
+
// Get all components from componentNodes map with counts
|
|
2046
|
+
const components = [];
|
|
2047
|
+
componentNodes.forEach((node, key) => {
|
|
2048
|
+
components.push({
|
|
2049
|
+
name: node.name,
|
|
2050
|
+
color: node.color,
|
|
2051
|
+
type: node.type,
|
|
2052
|
+
count: node.count
|
|
2053
|
+
});
|
|
2054
|
+
});
|
|
2055
|
+
|
|
2056
|
+
// Sort by type, then by name
|
|
2057
|
+
components.sort((a, b) => {
|
|
2058
|
+
const typeOrder = { 'command': 1, 'skill': 2, 'mcp': 3, 'subagent': 4 };
|
|
2059
|
+
const typeA = typeOrder[a.type] || 5;
|
|
2060
|
+
const typeB = typeOrder[b.type] || 5;
|
|
2061
|
+
if (typeA !== typeB) return typeA - typeB;
|
|
2062
|
+
return a.name.localeCompare(b.name);
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
components.forEach(component => {
|
|
2066
|
+
const item = document.createElement('div');
|
|
2067
|
+
item.className = 'legend-item';
|
|
2068
|
+
item.innerHTML = `
|
|
2069
|
+
<div class="legend-dot" style="background: ${component.color};"></div>
|
|
2070
|
+
<span>${component.name}</span>
|
|
2071
|
+
<span style="margin-left: auto; color: rgba(255,255,255,0.5); font-size: 11px;">${component.count}</span>
|
|
2072
|
+
`;
|
|
2073
|
+
componentsList.appendChild(item);
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// Controls
|
|
2078
|
+
function restartAnimation() {
|
|
2079
|
+
stats = { conversations: 0, components: 0, tools: 0, days: 0 };
|
|
2080
|
+
processedEvents.clear();
|
|
2081
|
+
shownMilestones.clear();
|
|
2082
|
+
currentDayIndex = 0;
|
|
2083
|
+
beams = [];
|
|
2084
|
+
toolNodes.clear(); // Clear tool nodes
|
|
2085
|
+
uniqueTools.clear(); // Clear unique tools
|
|
2086
|
+
modelNodes.clear(); // Clear model nodes
|
|
2087
|
+
componentNodes.clear(); // Clear component nodes (second layer)
|
|
2088
|
+
updateModelsList(); // Clear the models list display
|
|
2089
|
+
updateToolsList(); // Clear the tools list display
|
|
2090
|
+
updateComponentsList(); // Clear the components list display
|
|
2091
|
+
Object.values(branches).forEach(b => b.nodes = []);
|
|
2092
|
+
startTime = Date.now();
|
|
2093
|
+
isPlaying = true;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// Initialize
|
|
2097
|
+
loadData();
|
|
2098
|
+
</script>
|
|
2099
|
+
</body>
|
|
2100
|
+
</html>
|