UNPKG

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
<!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