claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
1,367 lines (1,185 loc) • 83.6 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude Code 2025 - Activity Graph</title>
<style>
:root {
--bg-primary: #0a0a0f;
--text-primary: #e0e0e0;
--text-accent: #d97706;
--agent-color: #f59e0b;
--mcp-color: #8b5cf6;
--command-color: #10b981;
--skill-color: #ec4899;
--tool-color: #3b82f6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
background: var(--bg-primary);
color: var(--text-primary);
overflow: hidden;
height: 100vh;
}
#canvas {
display: block;
width: 100%;
height: 100%;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
.hud {
position: absolute;
top: 20px;
left: 20px;
background: rgba(10, 10, 15, 0.8);
padding: 20px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
pointer-events: auto;
}
.current-date {
display: none;
}
.stat-line {
font-size: 13px;
margin: 5px 0;
opacity: 0.9;
}
.stat-value {
color: var(--text-accent);
font-weight: 600;
}
.legend {
position: absolute;
top: 20px;
right: 20px;
background: rgba(10, 10, 15, 0.8);
padding: 15px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
font-size: 12px;
max-height: calc(100vh - 200px);
overflow-y: auto;
pointer-events: auto;
}
/* Custom scrollbar */
.legend::-webkit-scrollbar,
#toolsList::-webkit-scrollbar,
#componentsList::-webkit-scrollbar {
width: 6px;
}
.legend::-webkit-scrollbar-track,
#toolsList::-webkit-scrollbar-track,
#componentsList::-webkit-scrollbar-track {
background: rgba(255,255,255,0.05);
border-radius: 3px;
}
.legend::-webkit-scrollbar-thumb,
#toolsList::-webkit-scrollbar-thumb,
#componentsList::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.2);
border-radius: 3px;
}
.legend::-webkit-scrollbar-thumb:hover,
#toolsList::-webkit-scrollbar-thumb:hover,
#componentsList::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.3);
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
margin: 6px 0;
}
.legend-dot {
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: 0 0 8px currentColor;
}
.timeline {
display: none;
}
.timeline-bar {
height: 3px;
background: rgba(255,255,255,0.1);
border-radius: 2px;
position: relative;
}
.timeline-progress {
height: 100%;
background: linear-gradient(90deg, var(--text-accent), #fbbf24);
width: 0%;
box-shadow: 0 0 10px var(--text-accent);
}
.timeline-labels {
display: flex;
justify-content: space-between;
margin-top: 8px;
font-size: 10px;
color: rgba(255,255,255,0.4);
}
.controls {
position: absolute;
bottom: 90px;
right: 20px;
display: flex;
gap: 8px;
pointer-events: all;
}
.control-btn {
background: rgba(26, 26, 46, 0.8);
border: 1px solid rgba(255,255,255,0.2);
color: var(--text-primary);
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
font-family: inherit;
font-size: 11px;
transition: all 0.2s;
backdrop-filter: blur(10px);
}
.control-btn:hover {
background: var(--text-accent);
border-color: var(--text-accent);
}
.control-btn.active {
background: var(--text-accent);
border-color: var(--text-accent);
}
.intro-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(10, 10, 15, 0.95);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 100;
backdrop-filter: blur(20px);
}
.intro-title {
font-size: 64px;
font-weight: 700;
color: var(--text-accent);
margin-bottom: 15px;
text-shadow: 0 0 30px var(--text-accent);
}
.intro-subtitle {
font-size: 20px;
color: var(--text-primary);
margin-bottom: 40px;
}
.start-btn {
background: var(--text-accent);
border: none;
color: white;
padding: 18px 36px;
border-radius: 6px;
font-size: 16px;
font-family: inherit;
cursor: pointer;
box-shadow: 0 0 20px var(--text-accent);
transition: all 0.3s;
}
.start-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 30px var(--text-accent);
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255,255,255,0.1);
border-top-color: var(--text-accent);
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.event-toast {
position: absolute;
top: 80px;
left: 50%;
transform: translate(-50%, -100%);
background: rgba(26, 26, 46, 0.95);
border: 2px solid var(--text-accent);
border-radius: 8px;
padding: 15px 35px;
text-align: center;
box-shadow: 0 0 30px var(--text-accent);
z-index: 50;
backdrop-filter: blur(20px);
opacity: 0;
}
.event-toast.show {
animation: toastSlide 2.5s ease forwards;
}
@keyframes toastSlide {
0% {
transform: translate(-50%, -100%);
opacity: 0;
}
10% {
transform: translate(-50%, 0);
opacity: 1;
}
90% {
transform: translate(-50%, 0);
opacity: 1;
}
100% {
transform: translate(-50%, -100%);
opacity: 0;
}
}
.event-toast h3 {
font-size: 18px;
color: var(--text-accent);
margin: 0;
}
.tooltip {
position: absolute;
background: rgba(26, 26, 46, 0.95);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 8px;
padding: 10px 15px;
color: var(--text-primary);
font-size: 13px;
pointer-events: none;
z-index: 100;
opacity: 0;
transition: opacity 0.2s;
backdrop-filter: blur(10px);
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
white-space: nowrap;
}
.tooltip.show {
opacity: 1;
}
.tooltip-title {
font-weight: 600;
color: var(--text-accent);
margin-bottom: 5px;
}
.tooltip-info {
color: var(--text-secondary);
font-size: 11px;
}
</style>
</head>
<body>
<!-- Intro Screen -->
<div id="introScreen" class="intro-screen">
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Loading your journey...</p>
</div>
<div id="introContent" style="display: none;">
<h1 class="intro-title">2025</h1>
<p class="intro-subtitle">Your Year with Claude Code</p>
<button class="start-btn" onclick="startAnimation()">▶ Start</button>
</div>
</div>
<!-- Canvas -->
<canvas id="canvas"></canvas>
<!-- Overlay -->
<div class="overlay">
<div class="hud">
<div class="current-date" id="currentDate">Jan 1, 2025</div>
<div class="stat-line">Conversations: <span class="stat-value" id="statConversations">0</span></div>
<div class="stat-line">Components: <span class="stat-value" id="statComponents">0</span></div>
<div class="stat-line">Tool Calls: <span class="stat-value" id="statTools">0</span></div>
<div class="stat-line">Active Days: <span class="stat-value" id="statDays">0</span></div>
</div>
<div class="legend">
<div style="font-weight: 600; margin-bottom: 8px;">Models Used</div>
<div id="modelsList">
<!-- Models will be added dynamically -->
</div>
<div style="font-weight: 600; margin-bottom: 8px; margin-top: 16px;">Tools Used</div>
<div id="toolsList">
<!-- Tools will be added dynamically -->
</div>
<div style="font-weight: 600; margin-bottom: 8px; margin-top: 16px;">Components Used</div>
<div id="componentsList">
<!-- Components will be added dynamically -->
</div>
</div>
<div class="timeline">
<div class="timeline-bar">
<div class="timeline-progress" id="timelineProgress"></div>
</div>
<div class="timeline-labels">
<span>Jan</span><span>Feb</span><span>Mar</span><span>Apr</span>
<span>May</span><span>Jun</span><span>Jul</span><span>Aug</span>
<span>Sep</span><span>Oct</span><span>Nov</span><span>Dec</span>
</div>
</div>
<div class="controls">
<button class="control-btn" onclick="restartAnimation()">🔄 Restart</button>
</div>
</div>
<!-- Event Toast -->
<div id="eventToast" class="event-toast"></div>
<!-- Tooltip -->
<div id="tooltip" class="tooltip"></div>
<script>
// Canvas setup
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// State
let animationData = null;
let isPlaying = false;
let speedMultiplier = 5; // Fixed at 5x speed
let startTime = null;
let currentDayIndex = 0;
let stats = { conversations: 0, components: 0, tools: 0, days: 0 };
let processedEvents = new Set();
let shownMilestones = new Set();
let toolNodes = new Map(); // Map of tool name -> tool node
let uniqueTools = new Map(); // Track unique tools with their colors
let modelNodes = new Map(); // Map of model name -> model node
let toolQueue = []; // Queue for gradual tool processing
// Mouse tracking
let mouseX = 0;
let mouseY = 0;
let hoveredNode = null;
let componentNodes = new Map(); // Map of component name -> component node (second layer)
// Zoom and pan state
let zoom = 1;
let panX = 0;
let panY = 0;
let isDragging = false;
let draggedNode = null;
let isPanning = false;
let lastMouseX = 0;
let lastMouseY = 0;
// Graph structure
const centerNode = { x: 0, y: 0, name: 'Claude', color: '#d97706' };
const branches = {
agents: { nodes: [], angle: 0, color: '#f59e0b' },
mcps: { nodes: [], angle: Math.PI / 2, color: '#8b5cf6' },
commands: { nodes: [], angle: Math.PI, color: '#10b981' },
skills: { nodes: [], angle: 3 * Math.PI / 2, color: '#ec4899' }
};
// Active beams (tool calls) - animating beams
let beams = [];
// Permanent connections (one per node) - Map of nodeId -> beam
let permanentConnections = new Map();
// Resize canvas
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
centerNode.x = canvas.width / 2;
centerNode.y = canvas.height / 2;
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Mouse tracking for tooltips
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
// Transform mouse coordinates to account for zoom and pan
const transformedMouseX = (mouseX - panX) / zoom;
const transformedMouseY = (mouseY - panY) / zoom;
// Find hovered node
hoveredNode = null;
// Check model nodes (pie slices in center)
const dx = transformedMouseX - centerNode.x;
const dy = transformedMouseY - centerNode.y;
const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);
const mouseAngle = Math.atan2(dy, dx) + Math.PI / 2; // Offset by -90 degrees to match pie drawing
const normalizedAngle = (mouseAngle + Math.PI * 2) % (Math.PI * 2); // Normalize to 0-2π
modelNodes.forEach((node) => {
if (distanceFromCenter < node.size + 10) {
const startAngle = (node.index / node.total) * Math.PI * 2;
const endAngle = ((node.index + 1) / node.total) * Math.PI * 2;
// Check if mouse angle is within this slice's angle range
if (normalizedAngle >= startAngle && normalizedAngle <= endAngle) {
hoveredNode = { type: 'model', node };
}
}
});
// Check tool nodes
if (!hoveredNode) {
toolNodes.forEach((node) => {
const dx = transformedMouseX - node.x;
const dy = transformedMouseY - node.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < node.size + 5) {
hoveredNode = { type: 'tool', node };
}
});
}
// Check component nodes (second layer)
if (!hoveredNode) {
componentNodes.forEach((node) => {
const dx = transformedMouseX - node.x;
const dy = transformedMouseY - node.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < node.size + 5) {
hoveredNode = { type: 'component', node };
}
});
}
// Update tooltip
updateTooltip();
// Update cursor based on hover state
if (!isDragging && !isPanning) {
if (hoveredNode && (hoveredNode.type === 'tool' || hoveredNode.type === 'component')) {
canvas.style.cursor = 'grab';
} else {
canvas.style.cursor = 'default';
}
}
});
canvas.addEventListener('mouseleave', () => {
hoveredNode = null;
isDragging = false;
draggedNode = null;
isPanning = false;
updateTooltip();
});
// Zoom with mouse wheel
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const mouseXCanvas = e.clientX - rect.left;
const mouseYCanvas = e.clientY - rect.top;
// Calculate zoom factor
const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
const newZoom = Math.max(0.3, Math.min(3, zoom * zoomFactor));
// Adjust pan to zoom towards mouse position
const zoomRatio = newZoom / zoom;
panX = mouseXCanvas - (mouseXCanvas - panX) * zoomRatio;
panY = mouseYCanvas - (mouseYCanvas - panY) * zoomRatio;
zoom = newZoom;
}, { passive: false });
// Mouse down - start drag or pan
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
lastMouseX = e.clientX - rect.left;
lastMouseY = e.clientY - rect.top;
// Check if clicking on a node
if (hoveredNode && (hoveredNode.type === 'tool' || hoveredNode.type === 'component')) {
isDragging = true;
draggedNode = hoveredNode.node;
canvas.style.cursor = 'grabbing';
} else {
// Start panning
isPanning = true;
canvas.style.cursor = 'move';
}
});
// Mouse move - drag node or pan canvas
canvas.addEventListener('mousemove', (e) => {
if (!isDragging && !isPanning) return;
const rect = canvas.getBoundingClientRect();
const currentX = e.clientX - rect.left;
const currentY = e.clientY - rect.top;
const deltaX = (currentX - lastMouseX) / zoom;
const deltaY = (currentY - lastMouseY) / zoom;
if (isDragging && draggedNode) {
// Move the dragged node
draggedNode.x += deltaX;
draggedNode.y += deltaY;
draggedNode.targetX = draggedNode.x;
draggedNode.targetY = draggedNode.y;
// Update random angle to match new position (for future percentage updates)
draggedNode.randomAngle = Math.atan2(
draggedNode.y - centerNode.y,
draggedNode.x - centerNode.x
);
} else if (isPanning) {
// Pan the canvas
panX += (currentX - lastMouseX);
panY += (currentY - lastMouseY);
}
lastMouseX = currentX;
lastMouseY = currentY;
});
// Mouse up - stop drag or pan
canvas.addEventListener('mouseup', () => {
isDragging = false;
draggedNode = null;
isPanning = false;
canvas.style.cursor = 'default';
});
// Double click to reset zoom and pan
canvas.addEventListener('dblclick', () => {
zoom = 1;
panX = 0;
panY = 0;
});
// Update tooltip display
function updateTooltip() {
const tooltip = document.getElementById('tooltip');
if (!hoveredNode) {
tooltip.classList.remove('show');
return;
}
const node = hoveredNode.node;
let content = '';
if (hoveredNode.type === 'model') {
const percentage = node.count > 0 ? node.percentage || 0 : 0;
const percentDisplay = percentage >= 1 ? Math.round(percentage) + '%' : percentage.toFixed(1) + '%';
content = `
<div class="tooltip-title">${node.displayName}</div>
<div class="tooltip-info">Model • ${percentDisplay} of activity (${node.count.toLocaleString()} calls)</div>
`;
} else if (hoveredNode.type === 'tool') {
const percentDisplay = node.percentage >= 1 ? Math.round(node.percentage) + '%' : node.percentage.toFixed(1) + '%';
content = `
<div class="tooltip-title">${node.toolName}</div>
<div class="tooltip-info">Tool • ${percentDisplay} of tool calls (${node.count.toLocaleString()})</div>
`;
} else if (hoveredNode.type === 'component') {
const typeLabels = {
'command': 'Command',
'skill': 'Skill',
'mcp': 'MCP',
'subagent': 'Subagent'
};
const typeLabel = typeLabels[node.type] || node.type;
const percentDisplay = node.percentage >= 1 ? Math.round(node.percentage) + '%' : node.percentage.toFixed(1) + '%';
content = `
<div class="tooltip-title">${node.name}</div>
<div class="tooltip-info">${typeLabel} • ${percentDisplay} of components (${node.count})</div>
`;
}
tooltip.innerHTML = content;
tooltip.style.left = (mouseX + 15) + 'px';
tooltip.style.top = (mouseY - 10) + 'px';
tooltip.classList.add('show');
}
// Node class
class Node {
constructor(name, type, branch, index) {
this.name = name;
this.type = type;
this.branch = branch;
this.index = index;
this.scale = 0;
this.targetScale = 1;
this.pulse = 0;
this.usageCount = 0;
// Position based on branch
const branchData = branches[branch];
const radius = 150 + (index * 30);
const angleOffset = (index * 0.3) - (branchData.nodes.length * 0.15);
this.x = centerNode.x + Math.cos(branchData.angle + angleOffset) * radius;
this.y = centerNode.y + Math.sin(branchData.angle + angleOffset) * radius;
this.color = branchData.color;
}
update() {
// Smooth scale growth
this.scale += (this.targetScale - this.scale) * 0.1;
// Pulse decay
if (this.pulse > 0) {
this.pulse -= 0.05;
}
// Gentle floating
const time = Date.now() * 0.001;
this.floatY = Math.sin(time + this.index) * 3;
}
draw(ctx) {
const x = this.x;
const y = this.y + (this.floatY || 0);
const size = 8 * this.scale;
// Glow
const glowSize = size + this.pulse * 10;
ctx.save();
ctx.globalAlpha = 0.3 + this.pulse * 0.4;
ctx.fillStyle = this.color;
ctx.shadowBlur = 15 + this.pulse * 15;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(x, y, glowSize, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Node
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(x, y, size, 0, Math.PI * 2);
ctx.fill();
// Label
if (this.scale > 0.5) {
ctx.save();
ctx.font = '11px Monaco';
ctx.fillStyle = '#e0e0e0';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(this.name, x, y + size + 5);
ctx.restore();
}
// Connection line to center
if (this.scale > 0.3) {
ctx.save();
ctx.strokeStyle = this.color;
ctx.globalAlpha = 0.2 * this.scale;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(centerNode.x, centerNode.y);
ctx.lineTo(x, y);
ctx.stroke();
ctx.restore();
}
}
activate() {
this.pulse = 1;
this.usageCount++;
}
}
// ModelNode class (pie slice in center for each model)
class ModelNode {
constructor(modelName, color, index, total) {
this.modelName = modelName;
this.displayName = this.formatModelName(modelName);
this.color = color;
this.count = 0; // Number of conversations using this model
this.percentage = 0; // Percentage of total model activity
this.size = 0;
this.targetSize = 50; // Base size, will grow with usage
this.alpha = 0;
this.targetAlpha = 1;
this.pulsePhase = Math.random() * Math.PI * 2;
// Position in pie (angle range)
this.index = index;
this.total = total;
}
formatModelName(modelName) {
const nameMap = {
'claude-sonnet-4-5-20250929': 'Sonnet 4.5',
'claude-haiku-4-5-20251001': 'Haiku 4.5',
'claude-3-5-sonnet': 'Sonnet 3.5',
'claude-3-opus': 'Opus 3',
'claude-3-haiku': 'Haiku 3',
'Unknown': 'Unknown'
};
return nameMap[modelName] || modelName;
}
updatePercentage(totalModelActivity) {
this.percentage = totalModelActivity > 0 ? (this.count / totalModelActivity) * 100 : 0;
}
addUse() {
this.count++;
// Grow size with each use (cap at 100)
this.targetSize = Math.min(50 + this.count * 3, 100);
}
updatePosition(index, total) {
this.index = index;
this.total = total;
}
update() {
// Smooth size transition
if (Math.abs(this.size - this.targetSize) > 0.5) {
this.size += (this.targetSize - this.size) * 0.1;
}
// Fade in
if (this.alpha < this.targetAlpha) {
this.alpha += 0.05;
}
this.pulsePhase += 0.05;
}
draw(ctx) {
if (this.size < 1) return;
const pulse = Math.sin(this.pulsePhase) * 0.15 + 0.85;
const startAngle = (this.index / this.total) * Math.PI * 2 - Math.PI / 2;
const endAngle = ((this.index + 1) / this.total) * Math.PI * 2 - Math.PI / 2;
// Draw pie slice with glow
ctx.save();
ctx.globalAlpha = this.alpha * 0.4 * pulse;
ctx.fillStyle = this.color;
ctx.shadowBlur = 30;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.moveTo(centerNode.x, centerNode.y);
ctx.arc(centerNode.x, centerNode.y, this.size + 10, startAngle, endAngle);
ctx.closePath();
ctx.fill();
ctx.restore();
// Draw main pie slice
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.shadowBlur = 15;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.moveTo(centerNode.x, centerNode.y);
ctx.arc(centerNode.x, centerNode.y, this.size, startAngle, endAngle);
ctx.closePath();
ctx.fill();
ctx.restore();
// Draw label (if slice is large enough)
if (this.size > 40 && this.total <= 3) {
const midAngle = (startAngle + endAngle) / 2;
const labelRadius = this.size * 0.6;
const labelX = centerNode.x + Math.cos(midAngle) * labelRadius;
const labelY = centerNode.y + Math.sin(midAngle) * labelRadius;
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.font = 'bold 12px Monaco';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.displayName, labelX, labelY);
ctx.restore();
// Count badge
if (this.count > 1) {
ctx.save();
ctx.globalAlpha = this.alpha * 0.8;
ctx.font = 'bold 10px Monaco';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.count, labelX, labelY + 15);
ctx.restore();
}
}
// Draw outer label (if too many models)
if (this.total > 3 || this.size <= 40) {
const midAngle = (startAngle + endAngle) / 2;
const labelRadius = this.size + 20;
const labelX = centerNode.x + Math.cos(midAngle) * labelRadius;
const labelY = centerNode.y + Math.sin(midAngle) * labelRadius;
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.font = 'bold 11px Monaco';
ctx.fillStyle = this.color;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(this.displayName, labelX, labelY);
if (this.count > 1) {
ctx.font = 'bold 9px Monaco';
ctx.fillText(`(${this.count})`, labelX, labelY + 12);
}
ctx.restore();
}
}
}
// ToolNode class (persistent growing node for each tool type)
class ToolNode {
constructor(toolName, color, index, total) {
this.toolName = toolName;
this.color = color;
this.count = 0; // Number of times this tool was used
this.percentage = 0; // Percentage of total tool calls
this.size = 0;
this.targetSize = 10; // Will grow with each use
this.alpha = 0;
this.targetAlpha = 1;
this.pulsePhase = Math.random() * Math.PI * 2;
// Random angle for this node (stays fixed)
this.randomAngle = Math.random() * Math.PI * 2;
// Small random offset for organic feel
this.randomOffset = {
x: (Math.random() - 0.5) * 40,
y: (Math.random() - 0.5) * 40
};
// Initial position (will be updated based on percentage)
const radius = 350; // Start far, will move closer based on %
this.x = centerNode.x + Math.cos(this.randomAngle) * radius + this.randomOffset.x;
this.y = centerNode.y + Math.sin(this.randomAngle) * radius + this.randomOffset.y;
this.targetX = this.x;
this.targetY = this.y;
}
addUse() {
this.count++;
// Grow size with each use (cap at 40)
this.targetSize = Math.min(10 + this.count * 2, 40);
}
updatePercentage(totalToolCalls) {
this.percentage = totalToolCalls > 0 ? (this.count / totalToolCalls) * 100 : 0;
// Recalculate position based on percentage
// Higher percentage = closer to center (min 150px, max 350px)
const minRadius = 150;
const maxRadius = 350;
// Invert: high % -> low radius (closer to center)
const radius = maxRadius - (this.percentage / 100) * (maxRadius - minRadius) * 2;
const clampedRadius = Math.max(minRadius, Math.min(maxRadius, radius));
this.targetX = centerNode.x + Math.cos(this.randomAngle) * clampedRadius + this.randomOffset.x;
this.targetY = centerNode.y + Math.sin(this.randomAngle) * clampedRadius + this.randomOffset.y;
// Also scale size based on percentage
this.targetSize = Math.min(15 + this.percentage * 1.5, 50);
}
update() {
// Smooth size growth
if (this.size < this.targetSize) {
this.size += (this.targetSize - this.size) * 0.1;
}
// Fade in
if (this.alpha < this.targetAlpha) {
this.alpha += 0.05;
}
this.pulsePhase += 0.05;
// Smooth position transition
this.x += (this.targetX - this.x) * 0.05;
this.y += (this.targetY - this.y) * 0.05;
}
updatePosition(index, total) {
// Keep for compatibility, but position is now based on percentage
}
draw(ctx) {
if (this.size < 1) return;
const pulse = Math.sin(this.pulsePhase) * 0.2 + 0.8;
// Glow
ctx.save();
ctx.globalAlpha = this.alpha * 0.3 * pulse;
ctx.fillStyle = this.color;
ctx.shadowBlur = 20;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size + 5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Node
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.shadowBlur = 10;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Label (always visible once node appears)
if (this.size > 5) {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.font = 'bold 11px Monaco';
ctx.fillStyle = '#e0e0e0';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(this.toolName, this.x, this.y + this.size + 5);
ctx.restore();
}
// Percentage badge (show when percentage > 0)
if (this.percentage > 0 && this.size > 5) {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.font = 'bold 10px Monaco';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const displayPercent = this.percentage >= 1 ?
Math.round(this.percentage) + '%' :
this.percentage.toFixed(1) + '%';
ctx.fillText(displayPercent, this.x, this.y);
ctx.restore();
}
}
}
// ComponentNode class (second layer: commands, skills, MCPs, subagents)
class ComponentNode {
constructor(name, type, color, index, total) {
this.name = name;
this.type = type; // 'command', 'skill', 'mcp', 'subagent'
this.color = color;
this.count = 0;
this.percentage = 0; // Percentage within its type category
this.size = 0;
this.targetSize = 8; // Smaller base size for second layer
this.alpha = 0;
this.targetAlpha = 1;
this.pulsePhase = Math.random() * Math.PI * 2;
// Random angle for this node (stays fixed)
this.randomAngle = Math.random() * Math.PI * 2;
// Small random offset for organic feel
this.randomOffset = {
x: (Math.random() - 0.5) * 30,
y: (Math.random() - 0.5) * 30
};
// Position in circle around center (farther than tools)
this.index = index;
this.total = total;
const radius = 450; // Start far, will move closer based on %
this.x = centerNode.x + Math.cos(this.randomAngle) * radius + this.randomOffset.x;
this.y = centerNode.y + Math.sin(this.randomAngle) * radius + this.randomOffset.y;
this.targetX = this.x;
this.targetY = this.y;
}
addUse() {
this.count++;
// Grow size with each use (smaller max than tools)
this.targetSize = Math.min(8 + this.count * 1.5, 30);
}
updatePercentage(totalInCategory) {
this.percentage = totalInCategory > 0 ? (this.count / totalInCategory) * 100 : 0;
// Recalculate position based on percentage
// Higher percentage = closer to center (min 250px, max 450px)
const minRadius = 250;
const maxRadius = 450;
// Invert: high % -> low radius (closer to center)
const radius = maxRadius - (this.percentage / 100) * (maxRadius - minRadius) * 2;
const clampedRadius = Math.max(minRadius, Math.min(maxRadius, radius));
this.targetX = centerNode.x + Math.cos(this.randomAngle) * clampedRadius + this.randomOffset.x;
this.targetY = centerNode.y + Math.sin(this.randomAngle) * clampedRadius + this.randomOffset.y;
// Also scale size based on percentage
this.targetSize = Math.min(10 + this.percentage * 1.2, 40);
}
updatePosition(index, total) {
// Keep for compatibility, but position is now based on percentage
this.index = index;
this.total = total;
}
update() {
// Smooth transitions
if (Math.abs(this.size - this.targetSize) > 0.5) {
this.size += (this.targetSize - this.size) * 0.1;
}
if (this.alpha < this.targetAlpha) {
this.alpha += 0.05;
}
this.pulsePhase += 0.05;
// Smooth position transition
this.x += (this.targetX - this.x) * 0.05;
this.y += (this.targetY - this.y) * 0.05;
}
draw(ctx) {
if (this.size < 1) return;
const pulse = Math.sin(this.pulsePhase) * 0.2 + 0.8;
// Glow
ctx.save();
ctx.globalAlpha = this.alpha * 0.3 * pulse;
ctx.fillStyle = this.color;
ctx.shadowBlur = 15;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size + 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Node
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.fillStyle = this.color;
ctx.shadowBlur = 8;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// Label (show for larger nodes)
if (this.size > 5) {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.font = 'bold 9px Monaco';
ctx.fillStyle = '#e0e0e0';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
// Clean up name (remove slashes and long paths)
const displayName = this.name.startsWith('/') ? this.name : this.name.split('@')[0];
ctx.fillText(displayName, this.x, this.y + this.size + 3);
ctx.restore();
}
// Percentage badge (show when percentage > 0)
if (this.percentage > 0 && this.size > 5) {
ctx.save();
ctx.globalAlpha = this.alpha;
ctx.font = 'bold 8px Monaco';
ctx.fillStyle = '#ffffff';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const displayPercent = this.percentage >= 1 ?
Math.round(this.percentage) + '%' :
this.percentage.toFixed(1) + '%';
ctx.fillText(displayPercent, this.x, this.y);
ctx.restore();
}
}
}
// Beam class (tool call visualization)
class Beam {
constructor(targetNode, toolName, isPermanent = false) {
this.targetNode = targetNode;
this.toolName = toolName;
this.progress = isPermanent ? 1 : 0; // Permanent starts complete
this.speed = 0.02; // Slower for better visibility
this.color = '#3b82f6';
this.pointCreated = isPermanent; // Permanent already counted
this.isComplete = isPermanent;
this.shouldDie = false; // Flag to remove animating beam
this.pulsePhase = Math.random() * Math.PI * 2; // For subtle animation
// Random color based on tool type
const toolColors = {
'Read': '#60a5fa',
'Write': '#34d399',
'Edit': '#fbbf24',
'Bash': '#f87171',
'TodoWrite': '#a78bfa',
'Task': '#fb923c',
'Glob': '#2dd4bf',
'Grep': '#c084fc'
};
this.color = toolColors[toolName] || '#3b82f6';
}
update() {
if (!this.isComplete) {
this.progress += this.speed;
// When beam reaches destination
if (this.progress >= 1 && !this.pointCreated) {
this.pointCreated = true;
this.isComplete = true;
// Increment the tool node's usage count
if (this.targetNode.addUse) {
this.targetNode.addUse();
}
stats.components++;
// Create or update permanent connection for this node
const nodeId = this.targetNode.toolName || this.targetNode.name || 'unknown';
permanentConnections.set(nodeId, new Beam(this.targetNode, this.toolName, true));
// Mark this animating beam to be removed
this.shouldDie = true;
}
}
// Subtle pulse animation for permanent connections
this.pulsePhase += 0.02;
}
draw(ctx) {
const startX = centerNode.x;
const startY = centerNode.y;
const endX = this.targetNode.x;
const endY = this.targetNode.y + (this.targetNode.floatY || 0);
const currentX = startX + (endX - startX) * Math.min(this.progress, 1);
const currentY = startY + (endY - startY) * Math.min(this.progress, 1);
// Calculate opacity based on state
let opacity;
if (this.isComplete) {
// Permanent connection: subtle pulse effect
const pulse = Math.sin(this.pulsePhase) * 0.1 + 0.25;
opacity = pulse;
} else {
// Animating beam: full opacity
opacity = 0.8;
}
// Beam line
ctx.save();
ctx.strokeStyle = this.color;
ctx.globalAlpha = opacity;
ctx.lineWidth = this.isComplete ? 1 : 2;
ctx.shadowBlur = this.isComplete ? 5 : 10;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.moveTo(startX, startY);
ctx.lineTo(currentX, currentY);
ctx.stroke();
ctx.restore();
// Tool label (only during animation)
if (!this.isComplete && this.progress > 0.3 && this.progress < 0.9) {
ctx.save();
ctx.font = '10px Monaco';
ctx.fillStyle = this.color;
ctx.globalAlpha = 0.8;
ctx.textAlign = 'center';
ctx.fillText(this.toolName, currentX, currentY - 10);
ctx.restore();
}
// Particle at tip (only during animation)
if (!this.isComplete) {
ctx.save();
ctx.fillStyle = this.color;
ctx.globalAlpha = 0.8;
ctx.shadowBlur = 15;
ctx.shadowColor = this.color;
ctx.beginPath();
ctx.arc(currentX, currentY, 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
isDead() {
return this.shouldDie;
}
}
// Add component node
function addComponent(name, type) {
const branchName = type === 'agent' ? 'agents' :
type === 'mcp' ? 'mcps' :
type === 'command' ? 'commands' : 'skills';
const branch = branches[branchName];
const index = branch.nodes.length;
const node = new Node(name, type, branchName, index);
branch.nodes.push(node);
stats.components++;
}
// Get or create tool node
function getOrCreateToolNode(toolName, color) {
if (!toolNodes.has(toolName)) {
// Create new tool node
const index = toolNodes.size;
const total = toolNodes.size + 1;
const node = new ToolNode(toolName, color, index, total);
toolNodes.set(toolName, node);
// Reposition all nodes to distribute evenly
let i = 0;
toolNodes.forEach((node, name) => {
node.updatePosition(i, toolNodes.size);
i++;
});
// Add to unique tools for legend
if (!uniqueTools.has(toolName)) {
uniqueTools.set(toolName, color);
updateToolsList();
}
}
return toolNodes.get(toolName);
}
// Get or create model node
function getOrCreateModelNode(modelName) {
if (!modelNodes.has(modelName)) {
// Assign colors to models
const modelColors = {
'claude-sonnet-4-5-20250929': '#3b82f6', // Blue for Sonnet 4.5
'claude-haiku-4-5-20251001': '#10b981', // Green for Haiku 4.5
'claude-3-5-sonnet': '#8b5cf6', // Purple for Sonnet 3.5
'claude-3-opus': '#f59e0b', // Orange for Opus 3
'claude-3-haiku': '#14b8a6', // Teal for Haiku 3
'Unknown': '#6b7280' // Gray for Unknown
};
const color = modelColors[modelName] || '#6b7280';
// Create new model node
const index = modelNodes.size;
const total = modelNodes.size + 1;
const node = new ModelNode(modelName, color, index, total);
modelNodes.set(modelName, node);
// Reposition all nodes to distribute evenly in pie
let i = 0;
modelNodes.forEach((node, name) => {
node.updatePosition(i, modelNodes.size);
i++;
});
console.log(`🎨 New model detected: ${node.displayName} (${modelName})`);
// Update models list in legend
updateModelsList();
}
return modelNodes.get(modelName);
}
// G