From 7ccfa13f6edb1d00291f6bc238b03d0ba4af5f42 Mon Sep 17 00:00:00 2001 From: Aleksei Fedorov Date: Wed, 10 Sep 2025 19:50:18 +0400 Subject: [PATCH] feat: Add memory-based line length and tooltips - Add memory usage data to processes API endpoint - Implement dynamic line length based on process memory consumption - Add gray circles at line endpoints (1px radius) - Add 'in development' tooltips for right semicircle menu items - Maintain original line styling (color, thickness, opacity) - Base line length: 250px, additional: up to 100px based on memory usage --- app.py | 22 ++++ index.html | 2 + static/js/main.js | 65 +++++++++++ static/js/right-semicircle-menu.js | 54 +++++++++ static/js/semicircle-menu.js | 179 +++++++++++++++++++++++++++++ 5 files changed, 322 insertions(+) create mode 100755 static/js/semicircle-menu.js diff --git a/app.py b/app.py index b70b62e..8c18134 100644 --- a/app.py +++ b/app.py @@ -208,6 +208,28 @@ def process_kernel_map(): except Exception as e: return jsonify({'error': str(e)}), 500 +@app.route('/api/processes') +def get_processes(): + """API for getting all Linux processes""" + try: + processes = [] + for proc in psutil.process_iter(['pid', 'name', 'status', 'memory_info']): + try: + memory_info = proc.info['memory_info'] + memory_mb = memory_info.rss / 1024 / 1024 # Convert to MB + processes.append({ + 'pid': proc.info['pid'], + 'name': proc.info['name'], + 'status': proc.info['status'], + 'memory_mb': round(memory_mb, 1) + }) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + return jsonify({'processes': processes}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + @app.route('/health') def health_check(): """Application health check""" diff --git a/index.html b/index.html index ba0479d..a2f3503 100755 --- a/index.html +++ b/index.html @@ -6,6 +6,8 @@ Kernel AI + + diff --git a/static/js/main.js b/static/js/main.js index b492354..b45d08d 100755 --- a/static/js/main.js +++ b/static/js/main.js @@ -268,6 +268,9 @@ function draw() { // Load processes and kernel subsystems loadProcessKernelMap(centerX, centerY); + // Draw additional process lines + drawProcessKernelMap2(centerX, centerY); + // Draw curves at bottom drawLowerBezierGrid(); @@ -428,6 +431,68 @@ function drawProcessKernelMap(data, centerX, centerY) { }); } +// Draw additional process lines (without circles and names) +function drawProcessKernelMap2(centerX, centerY) { + // Fetch all Linux processes + fetch('/api/processes') + .then(res => res.json()) + .then(data => { + const processes = data.processes || []; + const numProcesses = processes.length; + + // Find min and max memory usage for scaling + const memoryValues = processes.map(p => p.memory_mb || 0); + const minMemory = Math.min(...memoryValues); + const maxMemory = Math.max(...memoryValues); + const memoryRange = maxMemory - minMemory; + + processes.forEach((process, i) => { + const angle = i * 2 * Math.PI / numProcesses; + + // Calculate line length based on memory usage + const memoryMb = process.memory_mb || 0; + const memoryRatio = memoryRange > 0 ? (memoryMb - minMemory) / memoryRange : 0; + + // Base distance: 250px (original), max additional: 100px based on memory + const baseDistance = 250; + const maxAdditionalDistance = 100; + const distance = baseDistance + (memoryRatio * maxAdditionalDistance); + + const px = centerX + distance * Math.cos(angle); + const py = centerY + distance * Math.sin(angle); + + // Curve to process (same style as original) + const cx1 = centerX + (px - centerX) * 0.3 + (Math.random() - 0.5) * 40; + const cy1 = centerY + (py - centerY) * 0.3 + (Math.random() - 0.5) * 40; + const cx2 = centerX + (px - centerX) * 0.7 + (Math.random() - 0.5) * 40; + const cy2 = centerY + (py - centerY) * 0.7 + (Math.random() - 0.5) * 40; + + const path = `M${centerX},${centerY} C${cx1},${cy1} ${cx2},${cy2} ${px},${py}`; + + // Draw the line + svg.append("path") + .attr("d", path) + .attr("class", "process-line") + .attr("stroke", "#222") // Same color as Bezier curves + .attr("stroke-width", 0.4) // Same thickness as Bezier curves + .attr("opacity", 0.05 + Math.random() * 0.03) // Same opacity as Bezier curves + .attr("fill", "none"); + + // Add gray circle at the end of the line (like in drawProcessKernelMap) + svg.append("circle") + .attr("cx", px) + .attr("cy", py) + .attr("r", 1) + .attr("fill", "#888") + .attr("stroke", "#555") + .attr("stroke-width", 0.5); + }); + }) + .catch(error => { + console.error('Error fetching processes:', error); + }); +} + // Draw curves at bottom function drawLowerBezierGrid(num = 90) { const width = window.innerWidth; diff --git a/static/js/right-semicircle-menu.js b/static/js/right-semicircle-menu.js index 4972a10..bd73e82 100644 --- a/static/js/right-semicircle-menu.js +++ b/static/js/right-semicircle-menu.js @@ -74,9 +74,13 @@ class RightSemicircleMenuManager { .on('click', () => this.handleMenuClick(item.id)) .on('mouseenter', function() { d3.select(this).style('fill', '#444'); + // Show tooltip + showTooltip('in development', itemX, itemY); }) .on('mouseleave', function() { d3.select(this).style('fill', '#333'); + // Hide tooltip + hideTooltip(); }); // SVG icon inside the circle @@ -205,3 +209,53 @@ class RightSemicircleMenuManager { this.isVisible = false; } } +<<<<<<< HEAD +======= + +/** + * Show tooltip with text + */ +function showTooltip(text, x, y) { + // Remove existing tooltip + d3.selectAll('.tooltip').remove(); + + // Create tooltip + const tooltip = d3.select('body') + .append('div') + .attr('class', 'tooltip') + .style('position', 'absolute') + .style('background', 'rgba(0, 0, 0, 0.8)') + .style('color', 'white') + .style('padding', '6px 10px') + .style('border-radius', '4px') + .style('font-size', '12px') + .style('font-family', "'Share Tech Mono', monospace") + .style('pointer-events', 'none') + .style('z-index', '1000') + .style('opacity', 0) + .text(text); + + // Position tooltip + const tooltipWidth = tooltip.node().offsetWidth; + const tooltipHeight = tooltip.node().offsetHeight; + const offsetX = 10; + const offsetY = -tooltipHeight - 10; + + tooltip + .style('left', (x + offsetX) + 'px') + .style('top', (y + offsetY) + 'px') + .transition() + .duration(200) + .style('opacity', 1); +} + +/** + * Hide tooltip + */ +function hideTooltip() { + d3.selectAll('.tooltip') + .transition() + .duration(200) + .style('opacity', 0) + .remove(); +} diff --git a/static/js/semicircle-menu.js b/static/js/semicircle-menu.js new file mode 100755 index 0000000..260a6fd --- /dev/null +++ b/static/js/semicircle-menu.js @@ -0,0 +1,179 @@ +// Semicircle Menu Manager - Right Side (as per example) +class SemicircleMenuManager { + constructor() { + console.log("SemicircleMenuManager: constructor called"); + this.menuItems = [ + { id: 'processes', icon: '⚙️', label: 'Processes' }, + { id: 'network', icon: '🌐', label: 'Network' }, + { id: 'files', icon: '📁', label: 'Files' }, + { id: 'system', icon: '🔧', label: 'System' }, + { id: 'logs', icon: '📊', label: 'Logs' } + ]; + this.isVisible = false; + } + + renderSemicircleMenu() { + const svg = d3.select('svg'); + + // Remove existing menu elements + svg.selectAll('.semicircle-menu, .menu-item, .menu-bar, .menu-icon, .menu-label, .individual-menu-bar, .individual-menu-label').remove(); + + const width = window.innerWidth; + const height = window.innerHeight; + + // Semicircle menu position (LEFT side, larger radius like example) + const menuRadius = height * 0.4; // Larger radius - 40% of screen height + const menuX = -menuRadius * 0.3; // Shifted left (negative X) + const menuY = height / 2; // Centered vertically + + console.log(`Menu dimensions: radius=${menuRadius}, x=${menuX}, y=${menuY}`); + + // Create semicircle background - trimmed on right edge + const trimRight = menuRadius * 0.2; // Trim 20% from right edge + const semicirclePath = `M ${menuX} ${menuY - menuRadius} + A ${menuRadius} ${menuRadius} 0 0 1 ${menuX + menuRadius - trimRight} ${menuY + menuRadius} + L ${menuX + menuRadius - trimRight} ${menuY + menuRadius} + L ${menuX} ${menuY - menuRadius}`; + + svg.append('path') + .attr('d', semicirclePath) + .attr('class', 'semicircle-menu') + .style('fill', 'rgba(255, 255, 255, 0.04)') // Force CSS style + .style('stroke', '#aaa') + .style('stroke-width', '0.5px') + .attr('opacity', 1); + + // Removed horizontal bar with "Menu" text + + // Create 5 menu items (circles) arranged in semicircle like original + this.menuItems.forEach((item, index) => { + // Spread across semicircle (180 degrees, left side) - like original positioning + const angle = Math.PI + (index * Math.PI / 4); // Start from left, go to right + const itemRadius = menuRadius * 0.12; // Restored to previous size + const itemDistance = menuRadius * 0.75; // 75% from center + + const itemX = menuX + Math.cos(angle) * itemDistance; + const itemY = menuY + Math.sin(angle) * itemDistance; + + console.log(`Menu item ${index}: angle=${angle}, x=${itemX}, y=${itemY}, radius=${itemRadius}`); + + // Menu item circle + svg.append('circle') + .attr('cx', itemX) + .attr('cy', itemY) + .attr('r', itemRadius) + .attr('class', 'menu-item') + .style('fill', 'rgba(255, 255, 255, 0.04)') // Force CSS style + .style('stroke', '#aaa') + .style('stroke-width', '0.5px') + .attr('cursor', 'pointer') + .on('mouseover', function() { + d3.select(this) + .attr('fill', '#888') + .attr('stroke', '#aaa') + .attr('r', itemRadius * 1.1); // Slight scale on hover + }) + .on('mouseout', function() { + d3.select(this) + .style('fill', 'rgba(255, 255, 255, 0.04)') // Reset to original color + .style('stroke', '#aaa') + .attr('r', itemRadius); + }) + .on('click', () => this.handleMenuClick(item.id)); + + // Menu item icon + svg.append('text') + .attr('x', itemX) + .attr('y', itemY + itemRadius * 0.3) + .attr('class', 'menu-icon') + .style('fill', '#444') // Force CSS style + .style('font-family', "'Share Tech Mono', monospace") + .attr('text-anchor', 'middle') + .attr('font-size', `${itemRadius * 0.8}px`) + .text(item.icon); + + // Individual menu bar for each circle (pointing to the right) + const individualBarWidth = menuRadius * 0.3; // 30% of radius + const individualBarHeight = menuRadius * 0.08; // 8% of radius + + svg.append('rect') + .attr('x', itemX + itemRadius) // Start from right edge of circle + .attr('y', itemY - individualBarHeight/2) + .attr('width', individualBarWidth) + .attr('height', individualBarHeight) + .attr('class', 'individual-menu-bar') + .style('fill', 'rgba(255, 255, 255, 0.04)') // Force CSS style + .style('stroke', '#aaa') + .style('stroke-width', '0.5px') + .attr('opacity', 1) + .attr('rx', individualBarHeight/4); + + // Individual menu label for each circle + svg.append('text') + .attr('x', itemX + itemRadius + individualBarWidth/2) + .attr('y', itemY + individualBarHeight * 0.3) + .attr('class', 'individual-menu-label') + .style('fill', '#444') // Force CSS style + .style('font-family', "'Share Tech Mono', monospace") + .attr('text-anchor', 'middle') + .attr('font-size', `${individualBarHeight * 0.6}px`) + .text(item.label); + }); + + // Removed "Menu" label + + this.isVisible = true; + console.log("SemicircleMenuManager: left-side large menu rendered"); + } + + handleMenuClick(itemId) { + console.log(`SemicircleMenuManager: clicked ${itemId}`); + + // Add visual feedback + const svg = d3.select('svg'); + svg.selectAll('.menu-item') + .filter(function() { + return d3.select(this).datum() === itemId; + }) + .transition() + .duration(200) + .attr('fill', '#aaa') + .transition() + .duration(200) + .attr('fill', '#666'); + + // Handle different menu actions + switch(itemId) { + case 'processes': + console.log('Switching to processes view'); + break; + case 'network': + console.log('Switching to network view'); + break; + case 'files': + console.log('Switching to files view'); + break; + case 'system': + console.log('Switching to system view'); + break; + case 'logs': + console.log('Switching to logs view'); + break; + } + } + + toggleMenu() { + if (this.isVisible) { + this.hideMenu(); + } else { + this.renderSemicircleMenu(); + } + } + + hideMenu() { + const svg = d3.select('svg'); + svg.selectAll('.semicircle-menu, .menu-item, .menu-bar, .menu-icon, .menu-label, .individual-menu-bar, .individual-menu-label').remove(); + this.isVisible = false; + console.log("SemicircleMenuManager: menu hidden"); + } +}