claude-code-templates 1.28.2 → 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.
@@ -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>