claude-code-templates 1.28.3 → 1.28.5

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,2106 @@
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 (grouped by type)`);
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, // Now just "Plan" or "Explore", not "Plan-a68a"
1648
+ date: eventDate,
1649
+ dayOfYear: eventDayOfYear,
1650
+ count: event.count || 1 // How many times used that day
1651
+ });
1652
+ if (index === 0) {
1653
+ console.log(` - First subagent "${event.name}": date=${eventDate.toLocaleDateString()}, dayOfYear=${eventDayOfYear.toFixed(3)}, count=${event.count || 1}`);
1654
+ }
1655
+ });
1656
+ }
1657
+
1658
+ timeline.sort((a, b) => a.dayOfYear - b.dayOfYear);
1659
+
1660
+ console.log('🔍 DEBUG: First 10 events in sorted timeline:');
1661
+ timeline.slice(0, 10).forEach((event, i) => {
1662
+ console.log(` ${i}: type=${event.type}, name=${event.name || event.toolName}, dayOfYear=${event.dayOfYear?.toFixed(3)}, date=${event.date?.toLocaleDateString()}`);
1663
+ });
1664
+
1665
+ // Calculate actual activity range
1666
+ const firstActivityDay = timeline.length > 0 ? timeline[0].dayOfYear : 0;
1667
+ const lastActivityDay = timeline.length > 0 ? timeline[timeline.length - 1].dayOfYear : 365;
1668
+ const totalDays = lastActivityDay - firstActivityDay + 1;
1669
+
1670
+ const layer2Events = timeline.filter(e => e.type === 'component-layer2').length;
1671
+ console.log(`🔷 Total component-layer2 events in timeline: ${layer2Events}`);
1672
+ console.log(`📊 Activity range: Day ${firstActivityDay} to ${lastActivityDay} (${totalDays} days)`);
1673
+ console.log(`📅 First activity: ${timeline[0]?.date.toLocaleDateString()}`);
1674
+ console.log(`📅 Last activity: ${timeline[timeline.length - 1]?.date.toLocaleDateString()}`);
1675
+
1676
+ return {
1677
+ timeline,
1678
+ totalDays,
1679
+ firstActivityDay,
1680
+ lastActivityDay,
1681
+ startDate: timeline[0]?.date || new Date('2025-01-01')
1682
+ };
1683
+ }
1684
+
1685
+ function getDayOfYear(date) {
1686
+ const start = new Date(date.getFullYear(), 0, 0);
1687
+ const diff = date - start;
1688
+ const oneDay = 1000 * 60 * 60 * 24;
1689
+ return Math.floor(diff / oneDay);
1690
+ }
1691
+
1692
+ // Start animation
1693
+ function startAnimation() {
1694
+ console.log('🎬 STARTING ANIMATION');
1695
+ console.log('Timeline events:', animationData.timeline.length);
1696
+ console.log('Activity range: Day', animationData.firstActivityDay, 'to', animationData.lastActivityDay);
1697
+ console.log('Total days:', animationData.totalDays);
1698
+ console.log('First 5 events:', animationData.timeline.slice(0, 5));
1699
+
1700
+ document.getElementById('introScreen').style.display = 'none';
1701
+ isPlaying = true;
1702
+ startTime = Date.now();
1703
+
1704
+ // Update timeline labels based on actual data range
1705
+ if (animationData && animationData.startDate) {
1706
+ const startDate = new Date(animationData.startDate);
1707
+ const endDate = new Date(animationData.timeline[animationData.timeline.length - 1]?.date || startDate);
1708
+
1709
+ const labels = document.querySelector('.timeline-labels');
1710
+ const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1711
+
1712
+ // Show range in timeline
1713
+ const rangeText = `${monthNames[startDate.getMonth()]} ${startDate.getDate()} - ${monthNames[endDate.getMonth()]} ${endDate.getDate()}, 2025`;
1714
+ labels.innerHTML = `<span>${rangeText}</span>`;
1715
+ labels.style.justifyContent = 'center';
1716
+ }
1717
+
1718
+ animate();
1719
+ }
1720
+
1721
+ // Animation loop
1722
+ function animate() {
1723
+ requestAnimationFrame(animate);
1724
+
1725
+ // Clear canvas (before transform)
1726
+ ctx.fillStyle = '#0a0a0f';
1727
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
1728
+
1729
+ if (isPlaying) {
1730
+ updateAnimationState();
1731
+ }
1732
+
1733
+ // Apply zoom and pan transformations
1734
+ ctx.save();
1735
+ ctx.translate(panX, panY);
1736
+ ctx.scale(zoom, zoom);
1737
+
1738
+ // Draw grid lines (subtle)
1739
+ ctx.save();
1740
+ ctx.strokeStyle = 'rgba(255,255,255,0.02)';
1741
+ ctx.lineWidth = 1 / zoom; // Keep grid lines same thickness regardless of zoom
1742
+ for (let i = -1000; i < canvas.width / zoom + 1000; i += 50) {
1743
+ ctx.beginPath();
1744
+ ctx.moveTo(i, -1000);
1745
+ ctx.lineTo(i, canvas.height / zoom + 1000);
1746
+ ctx.stroke();
1747
+ }
1748
+ for (let i = 0; i < canvas.height; i += 50) {
1749
+ ctx.beginPath();
1750
+ ctx.moveTo(0, i);
1751
+ ctx.lineTo(canvas.width, i);
1752
+ ctx.stroke();
1753
+ }
1754
+ ctx.restore();
1755
+
1756
+ // Update and draw permanent connections first (behind everything)
1757
+ permanentConnections.forEach(beam => {
1758
+ beam.update();
1759
+ beam.draw(ctx);
1760
+ });
1761
+
1762
+ // Update and draw animating beams
1763
+ for (let i = beams.length - 1; i >= 0; i--) {
1764
+ beams[i].update();
1765
+ beams[i].draw(ctx);
1766
+ if (beams[i].isDead()) {
1767
+ beams.splice(i, 1);
1768
+ }
1769
+ }
1770
+
1771
+ // Update and draw tool nodes
1772
+ toolNodes.forEach(node => {
1773
+ node.update();
1774
+ node.draw(ctx);
1775
+ });
1776
+
1777
+ // Update and draw model nodes (pie slices in center)
1778
+ modelNodes.forEach(node => {
1779
+ node.update();
1780
+ node.draw(ctx);
1781
+ });
1782
+
1783
+ // Update and draw component nodes (second layer)
1784
+ componentNodes.forEach(node => {
1785
+ node.update();
1786
+ node.draw(ctx);
1787
+ });
1788
+
1789
+ // Update and draw all nodes
1790
+ Object.values(branches).forEach(branch => {
1791
+ branch.nodes.forEach(node => {
1792
+ node.update();
1793
+ node.draw(ctx);
1794
+ });
1795
+ });
1796
+
1797
+ // Draw center node only if no models detected yet
1798
+ if (modelNodes.size === 0) {
1799
+ ctx.save();
1800
+ ctx.fillStyle = centerNode.color;
1801
+ ctx.shadowBlur = 30;
1802
+ ctx.shadowColor = centerNode.color;
1803
+ ctx.beginPath();
1804
+ ctx.arc(centerNode.x, centerNode.y, 35, 0, Math.PI * 2);
1805
+ ctx.fill();
1806
+ ctx.restore();
1807
+
1808
+ // Center label
1809
+ ctx.save();
1810
+ ctx.font = 'bold 16px Monaco';
1811
+ ctx.fillStyle = '#e0e0e0';
1812
+ ctx.textAlign = 'center';
1813
+ ctx.textBaseline = 'middle';
1814
+ ctx.fillText('Claude', centerNode.x, centerNode.y);
1815
+ ctx.restore();
1816
+ }
1817
+
1818
+ // Restore zoom/pan transformation
1819
+ ctx.restore();
1820
+
1821
+ // Draw zoom indicator (outside transform so it stays in corner)
1822
+ if (zoom !== 1 || panX !== 0 || panY !== 0) {
1823
+ ctx.save();
1824
+ ctx.fillStyle = 'rgba(255,255,255,0.5)';
1825
+ ctx.font = '11px Monaco';
1826
+ ctx.textAlign = 'left';
1827
+ ctx.fillText(`Zoom: ${Math.round(zoom * 100)}% | Double-click to reset`, 20, canvas.height - 20);
1828
+ ctx.restore();
1829
+ }
1830
+ }
1831
+
1832
+ // Update animation state
1833
+ function updateAnimationState() {
1834
+ if (!animationData) return;
1835
+
1836
+ const elapsed = (Date.now() - startTime) * speedMultiplier;
1837
+ const duration = 40000; // 40 seconds
1838
+ const progress = Math.min(elapsed / duration, 1);
1839
+
1840
+ // Map progress to actual activity range
1841
+ const rangeStart = animationData.firstActivityDay || 0;
1842
+ const rangeEnd = animationData.lastActivityDay || 365;
1843
+ const rangeDuration = rangeEnd - rangeStart;
1844
+
1845
+ currentDayIndex = rangeStart + Math.floor(progress * rangeDuration);
1846
+
1847
+ document.getElementById('timelineProgress').style.width = `${progress * 100}%`;
1848
+
1849
+ // Use startDate from data
1850
+ const date = new Date(animationData.startDate);
1851
+ const dayOffset = currentDayIndex - rangeStart;
1852
+ date.setDate(date.getDate() + dayOffset);
1853
+
1854
+ const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1855
+ document.getElementById('currentDate').textContent = `${monthNames[date.getMonth()]} ${date.getDate()}`;
1856
+
1857
+ // Process events
1858
+ let eventsThisFrame = 0;
1859
+ animationData.timeline.forEach((event, index) => {
1860
+ if (event.dayOfYear <= currentDayIndex && !processedEvents.has(index)) {
1861
+ processedEvents.add(index);
1862
+ eventsThisFrame++;
1863
+
1864
+ if (event.type === 'conversation') {
1865
+ // Calculate actual tool count from toolCounts
1866
+ const actualToolCount = event.toolCounts ?
1867
+ Object.values(event.toolCounts).reduce((sum, count) => sum + count, 0) :
1868
+ event.tools.length * event.count;
1869
+
1870
+ console.log(`🎬 Processing conversation on day ${event.dayOfYear}: ${event.count} conversations, ${actualToolCount} tool calls`);
1871
+ stats.conversations += event.count;
1872
+ stats.tools += actualToolCount;
1873
+
1874
+ // Add tool beams using actual counts
1875
+ if (event.toolCounts) {
1876
+ Object.entries(event.toolCounts).forEach(([tool, count]) => {
1877
+ console.log(` 🔧 Adding ${count} beams for tool: ${tool}`);
1878
+ // Create multiple beams based on actual usage count
1879
+ for (let i = 0; i < count; i++) {
1880
+ setTimeout(() => addToolBeam(tool), Math.random() * 1000 + i * 50);
1881
+ }
1882
+ });
1883
+ } else {
1884
+ // Fallback to old method if toolCounts not available
1885
+ event.tools.forEach(tool => {
1886
+ console.log(` 🔧 Adding tool beam: ${tool}`);
1887
+ setTimeout(() => addToolBeam(tool), Math.random() * 500);
1888
+ });
1889
+ }
1890
+
1891
+ // Process models using actual counts
1892
+ if (event.models && event.models.length > 0) {
1893
+ event.models.forEach(modelName => {
1894
+ // Skip Unknown models
1895
+ if (modelName === 'Unknown') return;
1896
+
1897
+ const modelNode = getOrCreateModelNode(modelName);
1898
+
1899
+ // Add actual usage count from modelCounts
1900
+ const modelCount = event.modelCounts ? (event.modelCounts[modelName] || 1) : 1;
1901
+ for (let i = 0; i < modelCount; i++) {
1902
+ modelNode.addUse();
1903
+ }
1904
+
1905
+ console.log(` 🎨 Model used: ${modelNode.displayName} - added ${modelCount} uses (total: ${modelNode.count})`);
1906
+ });
1907
+
1908
+ // Update models list after processing all models
1909
+ updateModelsList();
1910
+ }
1911
+
1912
+ const uniqueDays = new Set();
1913
+ animationData.timeline.forEach((e, i) => {
1914
+ if (processedEvents.has(i)) {
1915
+ uniqueDays.add(Math.floor(e.dayOfYear));
1916
+ }
1917
+ });
1918
+ stats.days = uniqueDays.size;
1919
+
1920
+ } else if (event.type === 'component') {
1921
+ console.log(`📦 Processing component: ${event.name} (${event.componentType})`);
1922
+ addComponent(event.name, event.componentType);
1923
+ // showEvent(`Installed: ${event.name}`); // Disabled - notifications removed
1924
+ } else if (event.type === 'component-layer2') {
1925
+ const node = getOrCreateComponentNode(event.name, event.componentType);
1926
+
1927
+ // Add uses based on event count (for grouped subagents)
1928
+ const useCount = event.count || 1;
1929
+ for (let j = 0; j < useCount; j++) {
1930
+ node.addUse();
1931
+ }
1932
+
1933
+ // Create beam to this component node
1934
+ const beam = new Beam(node, event.name);
1935
+ beams.push(beam);
1936
+
1937
+ console.log(`🔷 ${event.componentType}: ${event.name} at ${event.date?.toLocaleDateString()} (added: ${useCount}, total: ${node.count}, size: ${node.targetSize.toFixed(1)})`);
1938
+ }
1939
+ }
1940
+ });
1941
+
1942
+ if (eventsThisFrame > 0) {
1943
+ console.log(`✅ Processed ${eventsThisFrame} events this frame. Current day: ${currentDayIndex}`);
1944
+
1945
+ // Recalculate percentages and positions after processing events
1946
+ recalculateToolPercentages();
1947
+ recalculateComponentPercentages();
1948
+ recalculateModelPercentages();
1949
+ }
1950
+
1951
+ // Update stats
1952
+ document.getElementById('statConversations').textContent = stats.conversations;
1953
+ document.getElementById('statComponents').textContent = stats.components;
1954
+ document.getElementById('statTools').textContent = stats.tools;
1955
+ document.getElementById('statDays').textContent = stats.days;
1956
+
1957
+ // Check milestones
1958
+ const milestones = [
1959
+ { threshold: 10, msg: 'First 10 conversations! 🎯' },
1960
+ { threshold: 50, msg: 'Half century! 🔥' },
1961
+ { threshold: 100, msg: '100 conversations! 💯' },
1962
+ { threshold: 500, msg: 'Power user! ⚡' }
1963
+ ];
1964
+
1965
+ milestones.forEach(m => {
1966
+ if (stats.conversations >= m.threshold && !shownMilestones.has(m.threshold)) {
1967
+ shownMilestones.add(m.threshold);
1968
+ // showEvent(m.msg); // Disabled - notifications removed
1969
+ }
1970
+ });
1971
+
1972
+ if (progress >= 1) {
1973
+ isPlaying = false;
1974
+ // showEvent('🎉 2025 Complete!'); // Disabled - notifications removed
1975
+ }
1976
+ }
1977
+
1978
+ function showEvent(message) {
1979
+ const toast = document.getElementById('eventToast');
1980
+ toast.innerHTML = `<h3>${message}</h3>`;
1981
+ toast.className = 'event-toast show';
1982
+ setTimeout(() => toast.className = 'event-toast', 2500);
1983
+ }
1984
+
1985
+ // Update models list in the legend
1986
+ function updateModelsList() {
1987
+ const modelsList = document.getElementById('modelsList');
1988
+ modelsList.innerHTML = '';
1989
+
1990
+ // Get models with counts from modelNodes
1991
+ const modelsWithCounts = [];
1992
+ modelNodes.forEach((node, modelName) => {
1993
+ modelsWithCounts.push({
1994
+ name: node.displayName,
1995
+ rawName: modelName,
1996
+ color: node.color,
1997
+ count: node.count
1998
+ });
1999
+ });
2000
+
2001
+ // Sort models by count (most used first)
2002
+ modelsWithCounts.sort((a, b) => b.count - a.count);
2003
+
2004
+ modelsWithCounts.forEach(model => {
2005
+ const item = document.createElement('div');
2006
+ item.className = 'legend-item';
2007
+ item.innerHTML = `
2008
+ <div class="legend-dot" style="background: ${model.color};"></div>
2009
+ <span>${model.name}</span>
2010
+ <span style="margin-left: auto; color: rgba(255,255,255,0.5); font-size: 11px;">${model.count}</span>
2011
+ `;
2012
+ modelsList.appendChild(item);
2013
+ });
2014
+ }
2015
+
2016
+ // Update tools list in the legend
2017
+ function updateToolsList() {
2018
+ const toolsList = document.getElementById('toolsList');
2019
+ toolsList.innerHTML = '';
2020
+
2021
+ // Get tools with counts from toolNodes
2022
+ const toolsWithCounts = [];
2023
+ toolNodes.forEach((node, toolName) => {
2024
+ toolsWithCounts.push({
2025
+ name: toolName,
2026
+ color: uniqueTools.get(toolName) || '#888',
2027
+ count: node.count
2028
+ });
2029
+ });
2030
+
2031
+ // Sort tools alphabetically
2032
+ toolsWithCounts.sort((a, b) => a.name.localeCompare(b.name));
2033
+
2034
+ toolsWithCounts.forEach(tool => {
2035
+ const item = document.createElement('div');
2036
+ item.className = 'legend-item';
2037
+ item.innerHTML = `
2038
+ <div class="legend-dot" style="background: ${tool.color};"></div>
2039
+ <span>${tool.name}</span>
2040
+ <span style="margin-left: auto; color: rgba(255,255,255,0.5); font-size: 11px;">${tool.count}</span>
2041
+ `;
2042
+ toolsList.appendChild(item);
2043
+ });
2044
+ }
2045
+
2046
+ // Update components list in the legend
2047
+ function updateComponentsList() {
2048
+ const componentsList = document.getElementById('componentsList');
2049
+ componentsList.innerHTML = '';
2050
+
2051
+ // Get all components from componentNodes map with counts
2052
+ const components = [];
2053
+ componentNodes.forEach((node, key) => {
2054
+ components.push({
2055
+ name: node.name,
2056
+ color: node.color,
2057
+ type: node.type,
2058
+ count: node.count
2059
+ });
2060
+ });
2061
+
2062
+ // Sort by type, then by name
2063
+ components.sort((a, b) => {
2064
+ const typeOrder = { 'command': 1, 'skill': 2, 'mcp': 3, 'subagent': 4 };
2065
+ const typeA = typeOrder[a.type] || 5;
2066
+ const typeB = typeOrder[b.type] || 5;
2067
+ if (typeA !== typeB) return typeA - typeB;
2068
+ return a.name.localeCompare(b.name);
2069
+ });
2070
+
2071
+ components.forEach(component => {
2072
+ const item = document.createElement('div');
2073
+ item.className = 'legend-item';
2074
+ item.innerHTML = `
2075
+ <div class="legend-dot" style="background: ${component.color};"></div>
2076
+ <span>${component.name}</span>
2077
+ <span style="margin-left: auto; color: rgba(255,255,255,0.5); font-size: 11px;">${component.count}</span>
2078
+ `;
2079
+ componentsList.appendChild(item);
2080
+ });
2081
+ }
2082
+
2083
+ // Controls
2084
+ function restartAnimation() {
2085
+ stats = { conversations: 0, components: 0, tools: 0, days: 0 };
2086
+ processedEvents.clear();
2087
+ shownMilestones.clear();
2088
+ currentDayIndex = 0;
2089
+ beams = [];
2090
+ toolNodes.clear(); // Clear tool nodes
2091
+ uniqueTools.clear(); // Clear unique tools
2092
+ modelNodes.clear(); // Clear model nodes
2093
+ componentNodes.clear(); // Clear component nodes (second layer)
2094
+ updateModelsList(); // Clear the models list display
2095
+ updateToolsList(); // Clear the tools list display
2096
+ updateComponentsList(); // Clear the components list display
2097
+ Object.values(branches).forEach(b => b.nodes = []);
2098
+ startTime = Date.now();
2099
+ isPlaying = true;
2100
+ }
2101
+
2102
+ // Initialize
2103
+ loadData();
2104
+ </script>
2105
+ </body>
2106
+ </html>